diff --git a/.env.test b/.env.test index 9d3b289a5151..6eb7ab17a4df 100644 --- a/.env.test +++ b/.env.test @@ -4,7 +4,7 @@ GETH_DOCKER_IMAGE=ethereum/client-go:v1.13.14 # Use either image or local binary for the testing GETH_BINARY_DIR= -LIGHTHOUSE_DOCKER_IMAGE=sigp/lighthouse:v5.1.1-amd64-modern-dev +LIGHTHOUSE_DOCKER_IMAGE=sigp/lighthouse:latest-amd64-unstable # We can't upgrade nethermind further due to genesis hash mismatch with the geth # https://github.com/NethermindEth/nethermind/issues/6683 diff --git a/dashboards/lodestar_block_processor.json b/dashboards/lodestar_block_processor.json index a945916308ab..0f2d63a0d1f2 100644 --- a/dashboards/lodestar_block_processor.json +++ b/dashboards/lodestar_block_processor.json @@ -7027,6 +7027,1011 @@ ], "title": "Avg block verified to blob availability delay", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 311 + }, + "id": 569, + "panels": [], + "title": "GetBlobs Metrics", + "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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unit": "percentunit", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 312 + }, + "id": 575, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_blockinputs_already_available_total[$rate_interval])/((rate(beacon_blockinputs_already_available_total[$rate_interval]) +\nrate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])) or vector(1))", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "Already available blocks when arrived", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_blockinput_blobs_already_available_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "Blobs of already available blocks", + "range": true, + "refId": "B" + } + ], + "title": "Availability at block arrival", + "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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unit": "percentunit", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 312 + }, + "id": 584, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_found_nonnull_in_getblobs_cache_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "old unavailable blobs already present in getblobs cache", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_found_null_in_getblobs_cache_total[$rate_interval])/((rate(beacon_blockinput_blobs_already_available_total[$rate_interval])+\nrate(beacon_datapromise_blockinput_blobs_responded_nonnull_in_getblobs_api_total[$rate_interval])+\nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "old unavailable blobs but null in getblobs cache", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_notfound_in_getblobs_cache_total[$rate_interval])/((rate(beacon_blockinput_blobs_already_available_total[$rate_interval])+\nrate(beacon_datapromise_blockinput_blobs_responded_nonnull_in_getblobs_api_total[$rate_interval])+\nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "new unavailable blobs for getblobs", + "range": true, + "refId": "C" + } + ], + "title": "Blobs Availability in GetBlobs Cache", + "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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unit": "percentunit", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 320 + }, + "id": 577, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_queried_in_getblobs_api_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "total blobs queried in api", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_responded_nonnull_in_getblobs_api_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blobs available", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_responded_null_in_getblobs_api_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blobs not available", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_errored_as_null_in_getblobs_api_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blobs errored", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "actual useful blobs (not delayed goissip available)", + "range": true, + "refId": "E" + } + ], + "title": "GetBlobs API 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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unit": "percentunit", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 320 + }, + "id": 578, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_finally_queried_from_network_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "blobs finally req/resp queried", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blobs finally req/resp fetched", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_retried_from_network_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "total blobs retried", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_retried_and_resolved_from_network_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "total blobs retried and resolved", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "delayed gossip blobs available", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_blobs_delayed_gossip_saved_computation_total[$rate_interval])/\n((\nrate(beacon_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_already_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_delayed_gossip_available_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_getblobs_api_nonnull_responses_used_total[$rate_interval]) + \nrate(beacon_datapromise_blockinput_blobs_finally_resolved_from_network_total[$rate_interval])\n) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "delayed gossip blobs used", + "range": true, + "refId": "F" + } + ], + "title": "Network fetch 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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unit": "percentunit", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 328 + }, + "id": 580, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinputs_available_using_getblobs_total[$rate_interval])/((rate(beacon_blockinputs_already_available_total[$rate_interval]) +\nrate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])) or vector(1))", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "blocks used getBlobs blobs", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinputs_tried_for_blobs_pull_total[$rate_interval])/((rate(beacon_blockinputs_already_available_total[$rate_interval]) +\nrate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blocks queried getblobs", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])/((rate(beacon_blockinputs_already_available_total[$rate_interval]) +\nrate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blocks available from getblobs", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinputs_retried_and_resolved_from_network_total[$rate_interval])/((rate(beacon_blockinputs_already_available_total[$rate_interval]) +\nrate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])) or vector(1))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blocks available after network blobs retries", + "range": true, + "refId": "D" + } + ], + "title": "Block availability resolution 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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unit": "percentunit", + "unitScale": true + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "beacon_datapromise_blockinputs_retried_for_blobs_pull_total" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 328 + }, + "id": 581, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinputs_retried_for_blobs_pull_total[$rate_interval])/((rate(beacon_blockinputs_already_available_total[$rate_interval]) +\nrate(beacon_datapromise_blockinputs_available_post_blobs_pull_total[$rate_interval])) or vector(1))", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "beacon_datapromise_blockinputs_retried_for_blobs_pull_total", + "range": true, + "refId": "A" + } + ], + "title": "Retry", + "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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 336 + }, + "id": 582, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "getblob_cache_size", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "getblob cache size", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "beacon_datapromise_blockinput_retry_tracker_cache_size", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blockinput retry cache size", + "range": true, + "refId": "B" + } + ], + "title": "Cache size", + "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": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 336 + }, + "id": 585, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(getblob_cache_pruned_total[$rate_interval])", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "getblob cache pruned", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(beacon_datapromise_blockinput_retry_tracker_cache_pruned[$rate_interval])", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "blockinput retry cache pruned", + "range": true, + "refId": "B" + } + ], + "title": "Cache prune rate", + "type": "timeseries" } ], "refresh": "10s", diff --git a/lerna.json b/lerna.json index 99654f9e81de..97c908dc50ee 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ ], "npmClient": "yarn", "useNx": true, - "version": "1.24.0", + "version": "1.25.0", "stream": true, "command": { "version": { diff --git a/lodestar b/lodestar index 7cf5301e4de4..dc512c65b35b 100755 --- a/lodestar +++ b/lodestar @@ -4,4 +4,4 @@ # # ./lodestar.sh beacon --network mainnet -node --trace-deprecation --max-old-space-size=8192 ./packages/cli/bin/lodestar.js "$@" \ No newline at end of file +exec node --trace-deprecation --max-old-space-size=8192 ./packages/cli/bin/lodestar.js "$@" diff --git a/packages/api/package.json b/packages/api/package.json index f650a377f6a5..061828a0e470 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -70,12 +70,12 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/persistent-merkle-tree": "^0.8.0", - "@chainsafe/ssz": "^0.18.0", - "@lodestar/config": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@chainsafe/persistent-merkle-tree": "^1.0.1", + "@chainsafe/ssz": "^1.0.1", + "@lodestar/config": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", "eventsource": "^2.0.2", "qs": "^6.11.1" }, diff --git a/packages/api/src/beacon/routes/beacon/pool.ts b/packages/api/src/beacon/routes/beacon/pool.ts index 4d909c2aac7b..9a65bd489a81 100644 --- a/packages/api/src/beacon/routes/beacon/pool.ts +++ b/packages/api/src/beacon/routes/beacon/pool.ts @@ -1,7 +1,16 @@ import {ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; -import {isForkPostElectra} from "@lodestar/params"; -import {AttesterSlashing, CommitteeIndex, Slot, capella, electra, phase0, ssz} from "@lodestar/types"; +import {ForkPostElectra, ForkPreElectra, isForkPostElectra} from "@lodestar/params"; +import { + AttesterSlashing, + CommitteeIndex, + SingleAttestation, + Slot, + capella, + electra, + phase0, + ssz, +} from "@lodestar/types"; import { ArrayOf, EmptyArgs, @@ -20,6 +29,8 @@ import {MetaHeader, VersionCodec, VersionMeta} from "../../../utils/metadata.js" // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes +const SingleAttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation); +const SingleAttestationListTypeElectra = ArrayOf(ssz.electra.SingleAttestation); const AttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation); const AttestationListTypeElectra = ArrayOf(ssz.electra.Attestation); const AttesterSlashingListTypePhase0 = ArrayOf(ssz.phase0.AttesterSlashing); @@ -142,7 +153,7 @@ export type Endpoints = { */ submitPoolAttestations: Endpoint< "POST", - {signedAttestations: AttestationListPhase0}, + {signedAttestations: SingleAttestation[]}, {body: unknown}, EmptyResponseData, EmptyMeta @@ -158,7 +169,7 @@ export type Endpoints = { */ submitPoolAttestationsV2: Endpoint< "POST", - {signedAttestations: AttestationList}, + {signedAttestations: SingleAttestation[]}, {body: unknown; headers: {[MetaHeader.Version]: string}}, EmptyResponseData, EmptyMeta @@ -316,10 +327,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({body: AttestationListTypePhase0.toJson(signedAttestations)}), - parseReqJson: ({body}) => ({signedAttestations: AttestationListTypePhase0.fromJson(body)}), - writeReqSsz: ({signedAttestations}) => ({body: AttestationListTypePhase0.serialize(signedAttestations)}), - parseReqSsz: ({body}) => ({signedAttestations: AttestationListTypePhase0.deserialize(body)}), + writeReqJson: ({signedAttestations}) => ({body: SingleAttestationListTypePhase0.toJson(signedAttestations)}), + parseReqJson: ({body}) => ({signedAttestations: SingleAttestationListTypePhase0.fromJson(body)}), + writeReqSsz: ({signedAttestations}) => ({body: SingleAttestationListTypePhase0.serialize(signedAttestations)}), + parseReqSsz: ({body}) => ({signedAttestations: SingleAttestationListTypePhase0.deserialize(body)}), schema: { body: Schema.ObjectArray, }, @@ -334,8 +345,8 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions[]) + : SingleAttestationListTypePhase0.toJson(signedAttestations as SingleAttestation[]), headers: {[MetaHeader.Version]: fork}, }; }, @@ -343,16 +354,16 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0); return { body: isForkPostElectra(fork) - ? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra) - : AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0), + ? SingleAttestationListTypeElectra.serialize(signedAttestations as SingleAttestation[]) + : SingleAttestationListTypePhase0.serialize(signedAttestations as SingleAttestation[]), headers: {[MetaHeader.Version]: fork}, }; }, @@ -360,8 +371,8 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions + ); + } else { + chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation); + chain.emitter.emit( + routes.events.EventType.singleAttestation, + toElectraSingleAttestation( + attestation as SingleAttestation, + indexedAttestation.attestingIndices[0] + ) + ); + } const sentPeers = await network.publishBeaconAttestation(attestation, subnet); metrics?.onPoolSubmitUnaggregatedAttestation(seenTimestampSec, indexedAttestation, subnet, sentPeers); diff --git a/packages/beacon-node/src/api/impl/config/constants.ts b/packages/beacon-node/src/api/impl/config/constants.ts index 6b390727cec3..48ca02bf1174 100644 --- a/packages/beacon-node/src/api/impl/config/constants.ts +++ b/packages/beacon-node/src/api/impl/config/constants.ts @@ -4,7 +4,9 @@ import { BLOB_TX_TYPE, BLS_WITHDRAWAL_PREFIX, COMPOUNDING_WITHDRAWAL_PREFIX, + CONSOLIDATION_REQUEST_TYPE, DEPOSIT_CONTRACT_TREE_DEPTH, + DEPOSIT_REQUEST_TYPE, DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_APPLICATION_BUILDER, DOMAIN_APPLICATION_MASK, @@ -40,6 +42,7 @@ import { UNSET_DEPOSIT_REQUESTS_START_INDEX, VERSIONED_HASH_VERSION_KZG, WEIGHT_DENOMINATOR, + WITHDRAWAL_REQUEST_TYPE, } from "@lodestar/params"; /** @@ -108,4 +111,7 @@ export const specConstants = { // electra UNSET_DEPOSIT_REQUESTS_START_INDEX, FULL_EXIT_REQUEST_AMOUNT, + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + CONSOLIDATION_REQUEST_TYPE, }; diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 0a84136b556f..fc8934ef5617 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1524,7 +1524,7 @@ export function getValidatorApi( ); }); - await chain.executionBuilder.registerValidator(filteredRegistrations); + await chain.executionBuilder.registerValidator(currentEpoch, filteredRegistrations); logger.debug("Forwarded validator registrations to connected builder", { epoch: currentEpoch, diff --git a/packages/beacon-node/src/chain/errors/attestationError.ts b/packages/beacon-node/src/chain/errors/attestationError.ts index 618a334928ae..1f907be96e7c 100644 --- a/packages/beacon-node/src/chain/errors/attestationError.ts +++ b/packages/beacon-node/src/chain/errors/attestationError.ts @@ -135,6 +135,10 @@ export enum AttestationErrorCode { * Electra: Invalid attestationData index: is non-zero */ NON_ZERO_ATTESTATION_DATA_INDEX = "ATTESTATION_ERROR_NON_ZERO_ATTESTATION_DATA_INDEX", + /** + * Electra: Attester not in committee + */ + ATTESTER_NOT_IN_COMMITTEE = "ATTESTATION_ERROR_ATTESTER_NOT_IN_COMMITTEE", } export type AttestationErrorType = @@ -170,7 +174,8 @@ export type AttestationErrorType = | {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES} | {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot} | {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET} - | {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}; + | {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX} + | {code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE}; export class AttestationError extends GossipActionError { getMetadata(): Record { diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index 9809c9c6304a..e7f302e42a50 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -1,8 +1,8 @@ import {Signature, aggregateSignatures} from "@chainsafe/blst"; import {BitArray} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; -import {isForkPostElectra} from "@lodestar/params"; -import {Attestation, RootHex, Slot, isElectraAttestation} from "@lodestar/types"; +import {MAX_COMMITTEES_PER_SLOT, isForkPostElectra} from "@lodestar/params"; +import {Attestation, RootHex, SingleAttestation, Slot, isElectraSingleAttestation} from "@lodestar/types"; import {assert, MapDef} from "@lodestar/utils"; import {IClock} from "../../util/clock.js"; import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; @@ -105,7 +105,13 @@ export class AttestationPool { * - Valid committeeIndex * - Valid data */ - add(committeeIndex: CommitteeIndex, attestation: Attestation, attDataRootHex: RootHex): InsertOutcome { + add( + committeeIndex: CommitteeIndex, + attestation: SingleAttestation, + attDataRootHex: RootHex, + committeeValidatorIndex: number, + committeeSize: number + ): InsertOutcome { const slot = attestation.data.slot; const fork = this.config.getForkName(slot); const lowestPermissibleSlot = this.lowestPermissibleSlot; @@ -129,9 +135,9 @@ export class AttestationPool { if (isForkPostElectra(fork)) { // Electra only: this should not happen because attestation should be validated before reaching this assert.notNull(committeeIndex, "Committee index should not be null in attestation pool post-electra"); - assert.true(isElectraAttestation(attestation), "Attestation should be type electra.Attestation"); + assert.true(isElectraSingleAttestation(attestation), "Attestation should be type electra.SingleAttestation"); } else { - assert.true(!isElectraAttestation(attestation), "Attestation should be type phase0.Attestation"); + assert.true(!isElectraSingleAttestation(attestation), "Attestation should be type phase0.Attestation"); committeeIndex = null; // For pre-electra, committee index info is encoded in attDataRootIndex } @@ -144,10 +150,10 @@ export class AttestationPool { const aggregate = aggregateByIndex.get(committeeIndex); if (aggregate) { // Aggregate mutating - return aggregateAttestationInto(aggregate, attestation); + return aggregateAttestationInto(aggregate, attestation, committeeValidatorIndex); } // Create new aggregate - aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation)); + aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, committeeValidatorIndex, committeeSize)); return InsertOutcome.NewData; } @@ -216,8 +222,18 @@ export class AttestationPool { /** * Aggregate a new attestation into `aggregate` mutating it */ -function aggregateAttestationInto(aggregate: AggregateFast, attestation: Attestation): InsertOutcome { - const bitIndex = attestation.aggregationBits.getSingleTrueBit(); +function aggregateAttestationInto( + aggregate: AggregateFast, + attestation: SingleAttestation, + committeeValidatorIndex: number +): InsertOutcome { + let bitIndex: number | null; + + if (isElectraSingleAttestation(attestation)) { + bitIndex = committeeValidatorIndex; + } else { + bitIndex = attestation.aggregationBits.getSingleTrueBit(); + } // Should never happen, attestations are verified against this exact condition before assert.notNull(bitIndex, "Invalid attestation in pool, not exactly one bit set"); @@ -234,13 +250,16 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: Attesta /** * Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto() */ -function attestationToAggregate(attestation: Attestation): AggregateFast { - if (isElectraAttestation(attestation)) { +function attestationToAggregate( + attestation: SingleAttestation, + committeeValidatorIndex: number, + committeeSize: number +): AggregateFast { + if (isElectraSingleAttestation(attestation)) { return { data: attestation.data, - // clone because it will be mutated - aggregationBits: attestation.aggregationBits.clone(), - committeeBits: attestation.committeeBits, + aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex), + committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, attestation.committeeIndex), signature: signatureFromBytesNoCheck(attestation.signature), }; } diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index 5eec1597a24e..819873d2e835 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -247,7 +247,7 @@ export class OpPool { for (const voluntaryExit of this.voluntaryExits.values()) { if ( !toBeSlashedIndices.has(voluntaryExit.message.validatorIndex) && - isValidVoluntaryExit(state, voluntaryExit, false) && + isValidVoluntaryExit(stateFork, state, voluntaryExit, false) && // Signature validation is skipped in `isValidVoluntaryExit(,,false)` since it was already validated in gossip // However we must make sure that the signature fork is the same, or it will become invalid if included through // a future fork. diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index d7b36e06abc4..cbd46b1655aa 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -32,11 +32,17 @@ import { ssz, sszTypesFor, } from "@lodestar/types"; -import {Logger, sleep, toHex, toRootHex} from "@lodestar/utils"; +import {Logger, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IEth1ForBlockProduction} from "../../eth1/index.js"; import {numToQuantity} from "../../eth1/provider/utils.js"; -import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js"; +import { + IExecutionBuilder, + IExecutionEngine, + PayloadAttributes, + PayloadId, + getExpectedGasLimit, +} from "../../execution/index.js"; import {fromGraffitiBuffer} from "../../util/graffiti.js"; import type {BeaconChain} from "../chain.js"; import {CommonBlockBody} from "../interface.js"; @@ -223,6 +229,39 @@ export async function produceBlockBody( fetchedTime, }); + const targetGasLimit = this.executionBuilder.getValidatorRegistration(proposerPubKey)?.gasLimit; + if (!targetGasLimit) { + // This should only happen if cache was cleared due to restart of beacon node + this.logger.warn("Failed to get validator registration, could not check header gas limit", { + slot: blockSlot, + proposerIndex, + proposerPubKey: toPubkeyHex(proposerPubKey), + }); + } else { + const headerGasLimit = builderRes.header.gasLimit; + const parentGasLimit = (currentState as CachedBeaconStateBellatrix).latestExecutionPayloadHeader.gasLimit; + const expectedGasLimit = getExpectedGasLimit(parentGasLimit, targetGasLimit); + + const lowerBound = Math.min(parentGasLimit, expectedGasLimit); + const upperBound = Math.max(parentGasLimit, expectedGasLimit); + + if (headerGasLimit < lowerBound || headerGasLimit > upperBound) { + throw Error( + `Header gas limit ${headerGasLimit} is outside of acceptable range [${lowerBound}, ${upperBound}]` + ); + } + + if (headerGasLimit !== expectedGasLimit) { + this.logger.warn("Header gas limit does not match expected value", { + slot: blockSlot, + headerGasLimit, + expectedGasLimit, + parentGasLimit, + targetGasLimit, + }); + } + } + if (ForkSeq[fork] >= ForkSeq.deneb) { const {blobKzgCommitments} = builderRes; if (blobKzgCommitments === undefined) { diff --git a/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts b/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts index 8c5df69e783b..d0f615b2bc26 100644 --- a/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts +++ b/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts @@ -4,17 +4,13 @@ import {MapDef} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {InsertOutcome} from "../opPools/types.js"; -export type SeenAttDataKey = AttDataBase64 | AttDataCommitteeBitsBase64; -// pre-electra, AttestationData is used to cache attestations +export type SeenAttDataKey = AttDataBase64; +// AttestationData is used to cache attestations type AttDataBase64 = string; -// electra, AttestationData + CommitteeBits are used to cache attestations -type AttDataCommitteeBitsBase64 = string; export type AttestationDataCacheEntry = { // part of shuffling data, so this does not take memory committeeValidatorIndices: Uint32Array; - // undefined for phase0 Attestation - committeeBits?: BitArray; committeeIndex: CommitteeIndex; // IndexedAttestationData signing root, 32 bytes signingRoot: Uint8Array; @@ -35,6 +31,10 @@ export enum RejectReason { already_known = "already_known", } +// For pre-electra, there is no committeeIndex in SingleAttestation, so we hard code it to 0 +// AttDataBase64 has committeeIndex instead +export const PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX = 0; + /** * There are maximum 64 committees per slot, assuming 1 committee may have up to 3 different data due to some nodes * are not up to date, we can have up to 192 different attestation data per slot. @@ -53,8 +53,14 @@ const DEFAULT_CACHE_SLOT_DISTANCE = 2; * Having this cache help saves a lot of cpu time since most of the gossip attestations are on the same slot. */ export class SeenAttestationDatas { - private cacheEntryByAttDataBase64BySlot = new MapDef>( - () => new Map() + private cacheEntryByAttDataByIndexBySlot = new MapDef< + Slot, + MapDef> + >( + () => + new MapDef>( + () => new Map() + ) ); private lowestPermissibleSlot = 0; @@ -67,31 +73,47 @@ export class SeenAttestationDatas { metrics?.seenCache.attestationData.totalSlot.addCollect(() => this.onScrapeLodestarMetrics(metrics)); } - // TODO: Move InsertOutcome type definition to a common place - add(slot: Slot, attDataKey: SeenAttDataKey, cacheEntry: AttestationDataCacheEntry): InsertOutcome { + /** + * Add an AttestationDataCacheEntry to the cache. + * - preElectra: add(slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, attDataBase64, cacheEntry) + * - electra: add(slot, committeeIndex, attDataBase64, cacheEntry) + */ + add( + slot: Slot, + committeeIndex: CommitteeIndex, + attDataBase64: AttDataBase64, + cacheEntry: AttestationDataCacheEntry + ): InsertOutcome { if (slot < this.lowestPermissibleSlot) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.too_old}); return InsertOutcome.Old; } - const cacheEntryByAttDataBase64 = this.cacheEntryByAttDataBase64BySlot.getOrDefault(slot); - if (cacheEntryByAttDataBase64.has(attDataKey)) { + const cacheEntryByAttDataByIndex = this.cacheEntryByAttDataByIndexBySlot.getOrDefault(slot); + const cacheEntryByAttData = cacheEntryByAttDataByIndex.getOrDefault(committeeIndex); + if (cacheEntryByAttData.has(attDataBase64)) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.already_known}); return InsertOutcome.AlreadyKnown; } - if (cacheEntryByAttDataBase64.size >= this.maxCacheSizePerSlot) { + if (cacheEntryByAttData.size >= this.maxCacheSizePerSlot) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.reached_limit}); return InsertOutcome.ReachLimit; } - cacheEntryByAttDataBase64.set(attDataKey, cacheEntry); + cacheEntryByAttData.set(attDataBase64, cacheEntry); return InsertOutcome.NewData; } - get(slot: Slot, attDataBase64: SeenAttDataKey): AttestationDataCacheEntry | null { - const cacheEntryByAttDataBase64 = this.cacheEntryByAttDataBase64BySlot.get(slot); - const cacheEntry = cacheEntryByAttDataBase64?.get(attDataBase64); + /** + * Get an AttestationDataCacheEntry from the cache. + * - preElectra: get(slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, attDataBase64) + * - electra: get(slot, committeeIndex, attDataBase64) + */ + get(slot: Slot, committeeIndex: CommitteeIndex, attDataBase64: SeenAttDataKey): AttestationDataCacheEntry | null { + const cacheEntryByAttDataByIndex = this.cacheEntryByAttDataByIndexBySlot.get(slot); + const cacheEntryByAttData = cacheEntryByAttDataByIndex?.get(committeeIndex); + const cacheEntry = cacheEntryByAttData?.get(attDataBase64); if (cacheEntry) { this.metrics?.seenCache.attestationData.hit.inc(); } else { @@ -102,20 +124,23 @@ export class SeenAttestationDatas { onSlot(clockSlot: Slot): void { this.lowestPermissibleSlot = Math.max(clockSlot - this.cacheSlotDistance, 0); - for (const slot of this.cacheEntryByAttDataBase64BySlot.keys()) { + for (const slot of this.cacheEntryByAttDataByIndexBySlot.keys()) { if (slot < this.lowestPermissibleSlot) { - this.cacheEntryByAttDataBase64BySlot.delete(slot); + this.cacheEntryByAttDataByIndexBySlot.delete(slot); } } } private onScrapeLodestarMetrics(metrics: Metrics): void { - metrics?.seenCache.attestationData.totalSlot.set(this.cacheEntryByAttDataBase64BySlot.size); + metrics?.seenCache.attestationData.totalSlot.set(this.cacheEntryByAttDataByIndexBySlot.size); // tracking number of attestation data at current slot may not be correct if scrape time is not at the end of slot // so we track it at the previous slot const previousSlot = this.lowestPermissibleSlot + this.cacheSlotDistance - 1; - metrics?.seenCache.attestationData.countPerSlot.set( - this.cacheEntryByAttDataBase64BySlot.get(previousSlot)?.size ?? 0 - ); + const cacheEntryByAttDataByIndex = this.cacheEntryByAttDataByIndexBySlot.get(previousSlot); + let count = 0; + for (const cacheEntryByAttDataBase64 of cacheEntryByAttDataByIndex?.values() ?? []) { + count += cacheEntryByAttDataBase64.size; + } + metrics?.seenCache.attestationData.countPerSlot.set(count); } } diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 781e63cf623a..8f4a3dd53993 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -71,9 +71,6 @@ async function validateAggregateAndProof( const attData = aggregate.data; const attSlot = attData.slot; - const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; - const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, seenAttDataKey) : null; - let attIndex: number | null; if (ForkSeq[fork] >= ForkSeq.electra) { attIndex = (aggregate as electra.Attestation).committeeBits.getSingleTrueBit(); @@ -89,6 +86,9 @@ async function validateAggregateAndProof( attIndex = attData.index; } + const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; + const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, attIndex, seenAttDataKey) : null; + const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 05f3faeaebd6..95471534ef53 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -5,6 +5,8 @@ import { ATTESTATION_SUBNET_COUNT, DOMAIN_BEACON_ATTESTER, ForkName, + ForkPostElectra, + ForkPreElectra, ForkSeq, SLOTS_PER_EPOCH, isForkPostElectra, @@ -20,36 +22,41 @@ import { createSingleSignatureSetFromComponents, } from "@lodestar/state-transition"; import { - Attestation, CommitteeIndex, Epoch, IndexedAttestation, Root, RootHex, + SingleAttestation, Slot, SubnetID, - electra, - isElectraAttestation, + ValidatorIndex, + isElectraSingleAttestation, phase0, ssz, } from "@lodestar/types"; -import {toRootHex} from "@lodestar/utils"; +import {assert, toRootHex} from "@lodestar/utils"; import {MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC} from "../../constants/index.js"; -import {sszDeserializeAttestation} from "../../network/gossip/topic.js"; +import {sszDeserializeSingleAttestation} from "../../network/gossip/topic.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import { getAggregationBitsFromAttestationSerialized, getAttDataFromSignedAggregateAndProofElectra, getAttDataFromSignedAggregateAndProofPhase0, - getCommitteeBitsFromAttestationSerialized, - getCommitteeBitsFromSignedAggregateAndProofElectra, + getAttesterIndexFromSingleAttestationSerialized, + getCommitteeIndexFromSingleAttestationSerialized, getSignatureFromAttestationSerialized, + getSignatureFromSingleAttestationSerialized, } from "../../util/sszBytes.js"; import {Result, wrapError} from "../../util/wrapError.js"; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; import {IBeaconChain} from "../interface.js"; import {RegenCaller} from "../regen/index.js"; -import {AttestationDataCacheEntry, SeenAttDataKey} from "../seenCache/seenAttestationData.js"; +import { + AttestationDataCacheEntry, + PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, + SeenAttDataKey, +} from "../seenCache/seenAttestationData.js"; export type BatchResult = { results: Result[]; @@ -57,17 +64,19 @@ export type BatchResult = { }; export type AttestationValidationResult = { - attestation: Attestation; + attestation: SingleAttestation; indexedAttestation: IndexedAttestation; subnet: SubnetID; attDataRootHex: RootHex; committeeIndex: CommitteeIndex; + committeeValidatorIndex: number; + committeeSize: number; }; export type AttestationOrBytes = ApiAttestation | GossipAttestation; /** attestation from api */ -export type ApiAttestation = {attestation: Attestation; serializedData: null}; +export type ApiAttestation = {attestation: SingleAttestation; serializedData: null}; /** attestation from gossip */ export type GossipAttestation = { @@ -225,7 +234,7 @@ export async function validateApiAttestation( } /** - * Only deserialize the attestation if needed, use the cached AttestationData instead + * Only deserialize the single attestation if needed, use the cached AttestationData instead * This is to avoid deserializing similar attestation multiple times which could help the gc */ async function validateAttestationNoSignatureCheck( @@ -246,16 +255,20 @@ async function validateAttestationNoSignatureCheck( // Run the checks that happen before an indexed attestation is constructed. let attestationOrCache: - | {attestation: Attestation; cache: null} + | {attestation: SingleAttestation; cache: null} | {attestation: null; cache: AttestationDataCacheEntry; serializedData: Uint8Array}; let attDataKey: SeenAttDataKey | null = null; if (attestationOrBytes.serializedData) { // gossip const attSlot = attestationOrBytes.attSlot; - attDataKey = getSeenAttDataKeyFromGossipAttestation(fork, attestationOrBytes); - const cachedAttData = attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, attDataKey) : null; + attDataKey = getSeenAttDataKeyFromGossipAttestation(attestationOrBytes); + const committeeIndexForLookup = isForkPostElectra(fork) + ? (getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) ?? 0) + : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX; + const cachedAttData = + attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null; if (cachedAttData === null) { - const attestation = sszDeserializeAttestation(fork, attestationOrBytes.serializedData); + const attestation = sszDeserializeSingleAttestation(fork, attestationOrBytes.serializedData); // only deserialize on the first AttestationData that's not cached attestationOrCache = {attestation, cache: null}; } else { @@ -276,21 +289,11 @@ async function validateAttestationNoSignatureCheck( const targetEpoch = attTarget.epoch; let committeeIndex: number | null; if (attestationOrCache.attestation) { - if (isElectraAttestation(attestationOrCache.attestation)) { + if (isElectraSingleAttestation(attestationOrCache.attestation)) { // api or first time validation of a gossip attestation - const {committeeBits} = attestationOrCache.attestation; - // throw in both in case of undefined and null - if (committeeBits == null) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}); - } - - committeeIndex = committeeBits.getSingleTrueBit(); - // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) - if (committeeIndex === null) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); - } + committeeIndex = attestationOrCache.attestation.committeeIndex; - // [REJECT] aggregate.data.index == 0 + // [REJECT] attestation.data.index == 0 if (attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); } @@ -322,23 +325,28 @@ async function validateAttestationNoSignatureCheck( verifyPropagationSlotRange(fork, chain, attestationOrCache.attestation.data.slot); } - // [REJECT] The attestation is unaggregated -- that is, it has exactly one participating validator - // (len([bit for bit in attestation.aggregation_bits if bit]) == 1, i.e. exactly 1 bit is set). - // > TODO: Do this check **before** getting the target state but don't recompute zipIndexes - const aggregationBits = attestationOrCache.attestation - ? attestationOrCache.attestation.aggregationBits - : getAggregationBitsFromAttestationSerialized(fork, attestationOrCache.serializedData); - if (aggregationBits === null) { - throw new AttestationError(GossipAction.REJECT, { - code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, - }); - } + let aggregationBits: BitArray | null = null; + let committeeValidatorIndex: number | null = null; + if (!isForkPostElectra(fork)) { + // [REJECT] The attestation is unaggregated -- that is, it has exactly one participating validator + // (len([bit for bit in attestation.aggregation_bits if bit]) == 1, i.e. exactly 1 bit is set). + // > TODO: Do this check **before** getting the target state but don't recompute zipIndexes + aggregationBits = attestationOrCache.attestation + ? (attestationOrCache.attestation as SingleAttestation).aggregationBits + : getAggregationBitsFromAttestationSerialized(attestationOrCache.serializedData); + if (aggregationBits === null) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, + }); + } - const bitIndex = aggregationBits.getSingleTrueBit(); - if (bitIndex === null) { - throw new AttestationError(GossipAction.REJECT, { - code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET, - }); + const bitIndex = aggregationBits.getSingleTrueBit(); + if (bitIndex === null) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET, + }); + } + committeeValidatorIndex = bitIndex; } let committeeValidatorIndices: Uint32Array; @@ -392,15 +400,44 @@ async function validateAttestationNoSignatureCheck( expectedSubnet = computeSubnetForSlot(shuffling, attSlot, committeeIndex); } - const validatorIndex = committeeValidatorIndices[bitIndex]; + let validatorIndex: number; - // [REJECT] The number of aggregation bits matches the committee size - // -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)). - // > TODO: Is this necessary? Lighthouse does not do this check. - if (aggregationBits.bitLen !== committeeValidatorIndices.length) { - throw new AttestationError(GossipAction.REJECT, { - code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS, - }); + if (!isForkPostElectra(fork)) { + // The validity of aggregation bits are already checked above + assert.notNull(aggregationBits); + assert.notNull(committeeValidatorIndex); + + validatorIndex = committeeValidatorIndices[committeeValidatorIndex]; + // [REJECT] The number of aggregation bits matches the committee size + // -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)). + // > TODO: Is this necessary? Lighthouse does not do this check. + if (aggregationBits.bitLen !== committeeValidatorIndices.length) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS, + }); + } + } else { + if (attestationOrCache.attestation) { + validatorIndex = (attestationOrCache.attestation as SingleAttestation).attesterIndex; + } else { + const attesterIndex = getAttesterIndexFromSingleAttestationSerialized(attestationOrCache.serializedData); + if (attesterIndex === null) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, + }); + } + validatorIndex = attesterIndex; + } + + // [REJECT] The attester is a member of the committee -- i.e. + // `attestation.attester_index in get_beacon_committee(state, attestation.data.slot, index)`. + // Position of the validator in its committee + committeeValidatorIndex = committeeValidatorIndices.indexOf(validatorIndex); + if (committeeValidatorIndex === -1) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE, + }); + } } // LH > verify_middle_checks @@ -436,14 +473,15 @@ async function validateAttestationNoSignatureCheck( let attDataRootHex: RootHex; const signature = attestationOrCache.attestation ? attestationOrCache.attestation.signature - : getSignatureFromAttestationSerialized(attestationOrCache.serializedData); + : !isForkPostElectra(fork) + ? getSignatureFromAttestationSerialized(attestationOrCache.serializedData) + : getSignatureFromSingleAttestationSerialized(attestationOrCache.serializedData); if (signature === null) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, }); } - let committeeBits: BitArray | undefined = undefined; if (attestationOrCache.cache) { // there could be up to 6% of cpu time to compute signing root if we don't clone the signature set signatureSet = createSingleSignatureSetFromComponents( @@ -452,7 +490,6 @@ async function validateAttestationNoSignatureCheck( signature ); attDataRootHex = attestationOrCache.cache.attDataRootHex; - committeeBits = attestationOrCache.cache.committeeBits; } else { signatureSet = createSingleSignatureSetFromComponents( chain.index2pubkey[validatorIndex], @@ -462,14 +499,9 @@ async function validateAttestationNoSignatureCheck( // add cached attestation data before verifying signature attDataRootHex = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData)); - // if attestation is phase0 the committeeBits is undefined anyway - committeeBits = isElectraAttestation(attestationOrCache.attestation) - ? attestationOrCache.attestation.committeeBits.clone() - : undefined; if (attDataKey) { - chain.seenAttestationDatas.add(attSlot, attDataKey, { + chain.seenAttestationDatas.add(attSlot, committeeIndex, attDataKey, { committeeValidatorIndices, - committeeBits, committeeIndex, signingRoot: signatureSet.signingRoot, subnet: expectedSubnet, @@ -482,22 +514,28 @@ async function validateAttestationNoSignatureCheck( } // no signature check, leave that for step1 - const indexedAttestationContent = { + const indexedAttestation: IndexedAttestation = { attestingIndices, data: attData, signature, }; - const indexedAttestation = - ForkSeq[fork] >= ForkSeq.electra - ? (indexedAttestationContent as electra.IndexedAttestation) - : (indexedAttestationContent as phase0.IndexedAttestation); - const attestation: Attestation = attestationOrCache.attestation ?? { - aggregationBits, - data: attData, - committeeBits, - signature, - }; + const attestation: SingleAttestation = attestationOrCache.attestation + ? attestationOrCache.attestation + : !isForkPostElectra(fork) + ? { + // Aggregation bits are already asserted above to not be null + aggregationBits: aggregationBits as BitArray, + data: attData, + signature, + } + : { + committeeIndex, + attesterIndex: validatorIndex, + data: attData, + signature, + }; + return { attestation, indexedAttestation, @@ -506,6 +544,8 @@ async function validateAttestationNoSignatureCheck( signatureSet, validatorIndex, committeeIndex, + committeeValidatorIndex, + committeeSize: committeeValidatorIndices.length, }; } @@ -771,38 +811,58 @@ export function computeSubnetForSlot(shuffling: EpochShuffling, slot: number, co /** * Return fork-dependent seen attestation key - * - for pre-electra, it's the AttestationData base64 - * - for electra and later, it's the AttestationData base64 + committeeBits base64 + * - for pre-electra, it's the AttestationData base64 from Attestation + * - for electra and later, it's the AttestationData base64 from SingleAttestation + * - consumers need to also pass slot + committeeIndex to get the correct SeenAttestationData */ -export function getSeenAttDataKeyFromGossipAttestation( - fork: ForkName, - attestation: GossipAttestation -): SeenAttDataKey | null { - const {attDataBase64, serializedData} = attestation; - if (isForkPostElectra(fork)) { - const committeeBits = getCommitteeBitsFromAttestationSerialized(serializedData); - return attDataBase64 && committeeBits ? attDataBase64 + committeeBits : null; - } - - // pre-electra - return attDataBase64; +export function getSeenAttDataKeyFromGossipAttestation(attestation: GossipAttestation): SeenAttDataKey | null { + // SeenAttDataKey is the same as gossip index + return attestation.attDataBase64; } /** * Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas - * - for pre-electra, it's the AttestationData base64 - * - for electra and later, it's the AttestationData base64 + committeeBits base64 + * - for both electra + pre-electra, it's the AttestationData base64 + * - consumers need to also pass slot + committeeIndex to get the correct SeenAttestationData */ export function getSeenAttDataKeyFromSignedAggregateAndProof( fork: ForkName, aggregateAndProof: Uint8Array ): SeenAttDataKey | null { + return isForkPostElectra(fork) + ? getAttDataFromSignedAggregateAndProofElectra(aggregateAndProof) + : getAttDataFromSignedAggregateAndProofPhase0(aggregateAndProof); +} + +export function getCommitteeIndexFromAttestationOrBytes( + fork: ForkName, + attestationOrBytes: AttestationOrBytes +): CommitteeIndex | null { + const isGossipAttestation = attestationOrBytes.serializedData !== null; + if (isForkPostElectra(fork)) { - const attData = getAttDataFromSignedAggregateAndProofElectra(aggregateAndProof); - const committeeBits = getCommitteeBitsFromSignedAggregateAndProofElectra(aggregateAndProof); - return attData && committeeBits ? attData + committeeBits : null; + if (isGossipAttestation) { + return getCommitteeIndexFromSingleAttestationSerialized(ForkName.electra, attestationOrBytes.serializedData); + } + return (attestationOrBytes.attestation as SingleAttestation).committeeIndex; } + if (isGossipAttestation) { + return getCommitteeIndexFromSingleAttestationSerialized(ForkName.phase0, attestationOrBytes.serializedData); + } + return (attestationOrBytes.attestation as SingleAttestation).data.index; +} - // pre-electra - return getAttDataFromSignedAggregateAndProofPhase0(aggregateAndProof); +/** + * Convert pre-electra single attestation (`phase0.Attestation`) to post-electra `SingleAttestation` + */ +export function toElectraSingleAttestation( + attestation: SingleAttestation, + attesterIndex: ValidatorIndex +): SingleAttestation { + return { + committeeIndex: attestation.data.index, + attesterIndex, + data: attestation.data, + signature: attestation.signature, + }; } diff --git a/packages/beacon-node/src/chain/validation/blobSidecar.ts b/packages/beacon-node/src/chain/validation/blobSidecar.ts index 228bf9f788f2..37fa3067b80b 100644 --- a/packages/beacon-node/src/chain/validation/blobSidecar.ts +++ b/packages/beacon-node/src/chain/validation/blobSidecar.ts @@ -1,5 +1,10 @@ import {ChainConfig} from "@lodestar/config"; -import {KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, KZG_COMMITMENT_SUBTREE_INDEX0} from "@lodestar/params"; +import { + ForkName, + KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, + KZG_COMMITMENT_SUBTREE_INDEX0, + isForkPostElectra, +} from "@lodestar/params"; import {computeStartSlotAtEpoch, getBlockHeaderProposerSignatureSet} from "@lodestar/state-transition"; import {BlobIndex, Root, Slot, SubnetID, deneb, ssz} from "@lodestar/types"; import {toRootHex, verifyMerkleBranch} from "@lodestar/utils"; @@ -12,6 +17,7 @@ import {IBeaconChain} from "../interface.js"; import {RegenCaller} from "../regen/index.js"; export async function validateGossipBlobSidecar( + fork: ForkName, chain: IBeaconChain, blobSidecar: deneb.BlobSidecar, subnet: SubnetID @@ -19,16 +25,17 @@ export async function validateGossipBlobSidecar( const blobSlot = blobSidecar.signedBlockHeader.message.slot; // [REJECT] The sidecar's index is consistent with `MAX_BLOBS_PER_BLOCK` -- i.e. `blob_sidecar.index < MAX_BLOBS_PER_BLOCK`. - if (blobSidecar.index >= chain.config.MAX_BLOBS_PER_BLOCK) { + const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(fork); + if (blobSidecar.index >= maxBlobsPerBlock) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.INDEX_TOO_LARGE, blobIdx: blobSidecar.index, - maxBlobsPerBlock: chain.config.MAX_BLOBS_PER_BLOCK, + maxBlobsPerBlock, }); } // [REJECT] The sidecar is for the correct subnet -- i.e. `compute_subnet_for_blob_sidecar(sidecar.index) == subnet_id`. - if (computeSubnetForBlobSidecar(blobSidecar.index, chain.config) !== subnet) { + if (computeSubnetForBlobSidecar(fork, chain.config, blobSidecar.index) !== subnet) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.INVALID_INDEX, blobIdx: blobSidecar.index, @@ -236,6 +243,8 @@ function validateInclusionProof(blobSidecar: deneb.BlobSidecar): boolean { ); } -function computeSubnetForBlobSidecar(blobIndex: BlobIndex, config: ChainConfig): SubnetID { - return blobIndex % config.BLOB_SIDECAR_SUBNET_COUNT; +function computeSubnetForBlobSidecar(fork: ForkName, config: ChainConfig, blobIndex: BlobIndex): SubnetID { + return ( + blobIndex % (isForkPostElectra(fork) ? config.BLOB_SIDECAR_SUBNET_COUNT_ELECTRA : config.BLOB_SIDECAR_SUBNET_COUNT) + ); } diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index b2623aa4f79d..2b18999db402 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -113,11 +113,12 @@ export async function validateGossipBlock( // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer -- i.e. validate that len(body.signed_beacon_block.message.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK if (isForkBlobs(fork)) { const blobKzgCommitmentsLen = (block as deneb.BeaconBlock).body.blobKzgCommitments.length; - if (blobKzgCommitmentsLen > chain.config.MAX_BLOBS_PER_BLOCK) { + const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(fork); + if (blobKzgCommitmentsLen > maxBlobsPerBlock) { throw new BlockGossipError(GossipAction.REJECT, { code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS, blobKzgCommitmentsLen, - commitmentLimit: chain.config.MAX_BLOBS_PER_BLOCK, + commitmentLimit: maxBlobsPerBlock, }); } } diff --git a/packages/beacon-node/src/chain/validation/voluntaryExit.ts b/packages/beacon-node/src/chain/validation/voluntaryExit.ts index 79da31084a36..60ee133e4b69 100644 --- a/packages/beacon-node/src/chain/validation/voluntaryExit.ts +++ b/packages/beacon-node/src/chain/validation/voluntaryExit.ts @@ -43,7 +43,7 @@ async function validateVoluntaryExit( // [REJECT] All of the conditions within process_voluntary_exit pass validation. // verifySignature = false, verified in batch below - if (!isValidVoluntaryExit(state, voluntaryExit, false)) { + if (!isValidVoluntaryExit(chain.config.getForkSeq(state.slot), state, voluntaryExit, false)) { throw new VoluntaryExitError(GossipAction.REJECT, { code: VoluntaryExitErrorCode.INVALID, }); diff --git a/packages/beacon-node/src/execution/builder/cache.ts b/packages/beacon-node/src/execution/builder/cache.ts new file mode 100644 index 000000000000..3fe192460a0b --- /dev/null +++ b/packages/beacon-node/src/execution/builder/cache.ts @@ -0,0 +1,39 @@ +import {BLSPubkey, Epoch, bellatrix} from "@lodestar/types"; +import {toPubkeyHex} from "@lodestar/utils"; + +const REGISTRATION_PRESERVE_EPOCHS = 2; + +export type ValidatorRegistration = { + epoch: Epoch; + /** Preferred gas limit of validator */ + gasLimit: number; +}; + +export class ValidatorRegistrationCache { + /** + * Map to track registrations by validator pubkey which is used here instead of + * validator index as `bellatrix.ValidatorRegistrationV1` does not contain the index + * and builder flow in general prefers to use pubkey over index. + */ + private readonly registrationByValidatorPubkey: Map; + constructor() { + this.registrationByValidatorPubkey = new Map(); + } + + add(epoch: Epoch, {pubkey, gasLimit}: bellatrix.ValidatorRegistrationV1): void { + this.registrationByValidatorPubkey.set(toPubkeyHex(pubkey), {epoch, gasLimit}); + } + + prune(epoch: Epoch): void { + for (const [pubkeyHex, registration] of this.registrationByValidatorPubkey.entries()) { + // We only retain an registrations for REGISTRATION_PRESERVE_EPOCHS epochs + if (registration.epoch + REGISTRATION_PRESERVE_EPOCHS < epoch) { + this.registrationByValidatorPubkey.delete(pubkeyHex); + } + } + } + + get(pubkey: BLSPubkey): ValidatorRegistration | undefined { + return this.registrationByValidatorPubkey.get(toPubkeyHex(pubkey)); + } +} diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 12e45412bafc..d743cedea545 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -6,6 +6,7 @@ import {ForkExecution, SLOTS_PER_EPOCH} from "@lodestar/params"; import {parseExecutionPayloadAndBlobsBundle, reconstructFullBlockOrContents} from "@lodestar/state-transition"; import { BLSPubkey, + Epoch, ExecutionPayloadHeader, Root, SignedBeaconBlockOrContents, @@ -19,6 +20,7 @@ import { } from "@lodestar/types"; import {toPrintableUrl} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; +import {ValidatorRegistration, ValidatorRegistrationCache} from "./cache.js"; import {IExecutionBuilder} from "./interface.js"; export type ExecutionBuilderHttpOpts = { @@ -60,6 +62,7 @@ const BUILDER_PROPOSAL_DELAY_TOLERANCE = 1000; export class ExecutionBuilderHttp implements IExecutionBuilder { readonly api: BuilderApi; readonly config: ChainForkConfig; + readonly registrations: ValidatorRegistrationCache; readonly issueLocalFcUWithFeeRecipient?: string; // Builder needs to be explicity enabled using updateStatus status = false; @@ -93,6 +96,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { ); logger?.info("External builder", {url: toPrintableUrl(baseUrl)}); this.config = config; + this.registrations = new ValidatorRegistrationCache(); this.issueLocalFcUWithFeeRecipient = opts.issueLocalFcUWithFeeRecipient; /** @@ -128,8 +132,17 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { } } - async registerValidator(registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise { + async registerValidator(epoch: Epoch, registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise { (await this.api.registerValidator({registrations})).assertOk(); + + for (const registration of registrations) { + this.registrations.add(epoch, registration.message); + } + this.registrations.prune(epoch); + } + + getValidatorRegistration(pubkey: BLSPubkey): ValidatorRegistration | undefined { + return this.registrations.get(pubkey); } async getHeader( diff --git a/packages/beacon-node/src/execution/builder/index.ts b/packages/beacon-node/src/execution/builder/index.ts index fff66a3e8bc5..929e88461c8a 100644 --- a/packages/beacon-node/src/execution/builder/index.ts +++ b/packages/beacon-node/src/execution/builder/index.ts @@ -2,6 +2,7 @@ import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/logger"; import {Metrics} from "../../metrics/metrics.js"; import {IExecutionBuilder} from "./interface.js"; +export {getExpectedGasLimit} from "./utils.js"; import {ExecutionBuilderHttp, ExecutionBuilderHttpOpts, defaultExecutionBuilderHttpOpts} from "./http.js"; diff --git a/packages/beacon-node/src/execution/builder/interface.ts b/packages/beacon-node/src/execution/builder/interface.ts index 06cdc1da4ed0..f326ec4bc451 100644 --- a/packages/beacon-node/src/execution/builder/interface.ts +++ b/packages/beacon-node/src/execution/builder/interface.ts @@ -1,6 +1,7 @@ import {ForkExecution} from "@lodestar/params"; import { BLSPubkey, + Epoch, ExecutionPayloadHeader, Root, SignedBeaconBlockOrContents, @@ -12,6 +13,7 @@ import { deneb, electra, } from "@lodestar/types"; +import {ValidatorRegistration} from "./cache.js"; export interface IExecutionBuilder { /** @@ -28,7 +30,8 @@ export interface IExecutionBuilder { updateStatus(shouldEnable: boolean): void; checkStatus(): Promise; - registerValidator(registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise; + registerValidator(epoch: Epoch, registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise; + getValidatorRegistration(pubkey: BLSPubkey): ValidatorRegistration | undefined; getHeader( fork: ForkExecution, slot: Slot, diff --git a/packages/beacon-node/src/execution/builder/utils.ts b/packages/beacon-node/src/execution/builder/utils.ts new file mode 100644 index 000000000000..76d75fda2b35 --- /dev/null +++ b/packages/beacon-node/src/execution/builder/utils.ts @@ -0,0 +1,19 @@ +/** + * From https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md + */ +const gasLimitAdjustmentFactor = 1024; + +/** + * Calculates expected gas limit based on parent gas limit and target gas limit + */ +export function getExpectedGasLimit(parentGasLimit: number, targetGasLimit: number): number { + const maxGasLimitDifference = Math.max(Math.floor(parentGasLimit / gasLimitAdjustmentFactor) - 1, 0); + + if (targetGasLimit > parentGasLimit) { + const gasDiff = targetGasLimit - parentGasLimit; + return parentGasLimit + Math.min(gasDiff, maxGasLimitDifference); + } + + const gasDiff = parentGasLimit - targetGasLimit; + return parentGasLimit - Math.min(gasDiff, maxGasLimitDifference); +} diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index c32cc1bc7215..40ca06c4d2c2 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -1,4 +1,4 @@ -import {ForkName} from "@lodestar/params"; +import {CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, ForkName, WITHDRAWAL_REQUEST_TYPE} from "@lodestar/params"; import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei, capella} from "@lodestar/types"; import {Blob, BlobAndProof, KZGCommitment, KZGProof} from "@lodestar/types/deneb"; @@ -58,6 +58,15 @@ export enum ClientCode { XX = "XX", // unknown } +export type ExecutionRequestType = + | typeof DEPOSIT_REQUEST_TYPE + | typeof WITHDRAWAL_REQUEST_TYPE + | typeof CONSOLIDATION_REQUEST_TYPE; + +export function isExecutionRequestType(type: number): type is ExecutionRequestType { + return type === DEPOSIT_REQUEST_TYPE || type === WITHDRAWAL_REQUEST_TYPE || type === CONSOLIDATION_REQUEST_TYPE; +} + export type ExecutePayloadResponse = | { status: ExecutionPayloadStatus.SYNCING | ExecutionPayloadStatus.ACCEPTED; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index f35a63aa3d96..24de1b9e6576 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -1,9 +1,12 @@ import { BYTES_PER_FIELD_ELEMENT, BYTES_PER_LOGS_BLOOM, + CONSOLIDATION_REQUEST_TYPE, + DEPOSIT_REQUEST_TYPE, FIELD_ELEMENTS_PER_BLOB, ForkName, ForkSeq, + WITHDRAWAL_REQUEST_TYPE, } from "@lodestar/params"; import {ExecutionPayload, ExecutionRequests, Root, Wei, bellatrix, capella, deneb, electra, ssz} from "@lodestar/types"; import {BlobAndProof} from "@lodestar/types/deneb"; @@ -17,7 +20,14 @@ import { quantityToBigint, quantityToNum, } from "../../eth1/provider/utils.js"; -import {BlobsBundle, ExecutionPayloadStatus, PayloadAttributes, VersionedHashes} from "./interface.js"; +import { + BlobsBundle, + ExecutionPayloadStatus, + ExecutionRequestType, + PayloadAttributes, + VersionedHashes, + isExecutionRequestType, +} from "./interface.js"; import {WithdrawalV1} from "./payloadIdCache.js"; export type EngineApiRpcParamTypes = { @@ -165,12 +175,12 @@ export type WithdrawalRpc = { }; /** - * ExecutionRequestsRpc only holds 3 elements in the following order: + * ExecutionRequestsRpc only holds at most 3 elements and no repeated type: * - ssz'ed DepositRequests * - ssz'ed WithdrawalRequests * - ssz'ed ConsolidationRequests */ -export type ExecutionRequestsRpc = [DepositRequestsRpc, WithdrawalRequestsRpc, ConsolidationRequestsRpc]; +export type ExecutionRequestsRpc = (DepositRequestsRpc | WithdrawalRequestsRpc | ConsolidationRequestsRpc)[]; export type DepositRequestsRpc = DATA; export type WithdrawalRequestsRpc = DATA; @@ -404,8 +414,20 @@ export function deserializeWithdrawal(serialized: WithdrawalRpc): capella.Withdr } as capella.Withdrawal; } +/** + * Prepend a single-byte requestType to requestsBytes + */ +function prefixRequests(requestsBytes: Uint8Array, requestType: ExecutionRequestType): Uint8Array { + const prefixedRequests = new Uint8Array(1 + requestsBytes.length); + prefixedRequests[0] = requestType; + prefixedRequests.set(requestsBytes, 1); + + return prefixedRequests; +} + function serializeDepositRequests(depositRequests: electra.DepositRequests): DepositRequestsRpc { - return bytesToData(ssz.electra.DepositRequests.serialize(depositRequests)); + const requestsBytes = ssz.electra.DepositRequests.serialize(depositRequests); + return bytesToData(prefixRequests(requestsBytes, DEPOSIT_REQUEST_TYPE)); } function deserializeDepositRequests(serialized: DepositRequestsRpc): electra.DepositRequests { @@ -413,17 +435,19 @@ function deserializeDepositRequests(serialized: DepositRequestsRpc): electra.Dep } function serializeWithdrawalRequests(withdrawalRequests: electra.WithdrawalRequests): WithdrawalRequestsRpc { - return bytesToData(ssz.electra.WithdrawalRequests.serialize(withdrawalRequests)); + const requestsBytes = ssz.electra.WithdrawalRequests.serialize(withdrawalRequests); + return bytesToData(prefixRequests(requestsBytes, WITHDRAWAL_REQUEST_TYPE)); } -function deserializeWithdrawalRequest(serialized: WithdrawalRequestsRpc): electra.WithdrawalRequests { +function deserializeWithdrawalRequests(serialized: WithdrawalRequestsRpc): electra.WithdrawalRequests { return ssz.electra.WithdrawalRequests.deserialize(dataToBytes(serialized, null)); } function serializeConsolidationRequests( consolidationRequests: electra.ConsolidationRequests ): ConsolidationRequestsRpc { - return bytesToData(ssz.electra.ConsolidationRequests.serialize(consolidationRequests)); + const requestsBytes = ssz.electra.ConsolidationRequests.serialize(consolidationRequests); + return bytesToData(prefixRequests(requestsBytes, CONSOLIDATION_REQUEST_TYPE)); } function deserializeConsolidationRequests(serialized: ConsolidationRequestsRpc): electra.ConsolidationRequests { @@ -436,22 +460,74 @@ function deserializeConsolidationRequests(serialized: ConsolidationRequestsRpc): */ export function serializeExecutionRequests(executionRequests: ExecutionRequests): ExecutionRequestsRpc { const {deposits, withdrawals, consolidations} = executionRequests; + const result = []; - return [ - serializeDepositRequests(deposits), - serializeWithdrawalRequests(withdrawals), - serializeConsolidationRequests(consolidations), - ]; + if (deposits.length !== 0) { + result.push(serializeDepositRequests(deposits)); + } + + if (withdrawals.length !== 0) { + result.push(serializeWithdrawalRequests(withdrawals)); + } + + if (consolidations.length !== 0) { + result.push(serializeConsolidationRequests(consolidations)); + } + + return result; } export function deserializeExecutionRequests(serialized: ExecutionRequestsRpc): ExecutionRequests { - const [deposits, withdrawals, consolidations] = serialized; - - return { - deposits: deserializeDepositRequests(deposits), - withdrawals: deserializeWithdrawalRequest(withdrawals), - consolidations: deserializeConsolidationRequests(consolidations), + const result: ExecutionRequests = { + deposits: [], + withdrawals: [], + consolidations: [], }; + + if (serialized.length === 0) { + return result; + } + + let prevRequestType: ExecutionRequestType | undefined; + + for (let prefixedRequests of serialized) { + // Slice out 0x so it is easier to extract request type + if (prefixedRequests.startsWith("0x")) { + prefixedRequests = prefixedRequests.slice(2); + } + + const currentRequestType = parseInt(prefixedRequests.substring(0, 2), 16); + + if (!isExecutionRequestType(currentRequestType)) { + throw Error(`Invalid request type currentRequestType=${prefixedRequests.substring(0, 2)}`); + } + + const requests = prefixedRequests.slice(2); + + if (prevRequestType !== undefined && prevRequestType >= currentRequestType) { + throw Error( + `Current request type must be larger than previous request type prevRequestType=${prevRequestType} currentRequestType=${currentRequestType}` + ); + } + + switch (currentRequestType) { + case DEPOSIT_REQUEST_TYPE: { + result.deposits = deserializeDepositRequests(requests); + break; + } + case WITHDRAWAL_REQUEST_TYPE: { + result.withdrawals = deserializeWithdrawalRequests(requests); + break; + } + case CONSOLIDATION_REQUEST_TYPE: { + result.consolidations = deserializeConsolidationRequests(requests); + break; + } + } + prevRequestType = currentRequestType; + } + + return result; } export function deserializeExecutionPayloadBody(data: ExecutionPayloadBodyRpc | null): ExecutionPayloadBody | null { diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 60a49b0b673d..e153294da630 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -227,13 +227,13 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { // of those which need to be fetched dataPromiseBlobsAlreadyAvailable: register.gauge({ name: "beacon_datapromise_blockinput_blobs_already_available_total", - help: "Count of blocks that were already available in blockinput cache via gossip", + help: "Count of data promise blocks' blobs that were already available in blockinput cache via gossip", }), dataPromiseBlobsDelayedGossipAvailable: register.gauge({ name: "beacon_datapromise_blockinput_blobs_delayed_gossip_available_total", - help: "Count of blobs that became available delayed via gossip post block arrival", + help: "Count of data promise blocks' blobs that became available delayed via gossip post block arrival", }), - dataPromiseBlobsDeplayedGossipAvailableSavedGetBlobsCompute: register.gauge({ + dataPromiseBlobsDelayedGossipAvailableSavedGetBlobsCompute: register.gauge({ name: "beacon_datapromise_blockinput_blobs_delayed_gossip_saved_computation_total", help: "Count of late available blobs that saved blob sidecar computation from getblobs", }), diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index 1c9a093656ce..86ab9edaa54d 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -306,6 +306,11 @@ export function createValidatorMonitor( lastRegisteredStatusEpoch = currentEpoch; const previousEpoch = currentEpoch - 1; + // There won't be any validator activity in epoch -1 + if (previousEpoch === -1) { + return; + } + for (const [index, monitoredValidator] of validators.entries()) { // We subtract two from the state of the epoch that generated these summaries. // diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index 1a91a5bc6598..93e9e3983d38 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -3,11 +3,11 @@ import {Message, TopicValidatorResult} from "@libp2p/interface"; import {BeaconConfig} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; import { - Attestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, SignedAggregateAndProof, SignedBeaconBlock, + SingleAttestation, Slot, SubnetID, altair, @@ -87,7 +87,7 @@ export type GossipTypeMap = { [GossipType.beacon_block]: SignedBeaconBlock; [GossipType.blob_sidecar]: deneb.BlobSidecar; [GossipType.beacon_aggregate_and_proof]: SignedAggregateAndProof; - [GossipType.beacon_attestation]: Attestation; + [GossipType.beacon_attestation]: SingleAttestation; [GossipType.voluntary_exit]: phase0.SignedVoluntaryExit; [GossipType.proposer_slashing]: phase0.ProposerSlashing; [GossipType.attester_slashing]: phase0.AttesterSlashing; @@ -102,7 +102,7 @@ export type GossipFnByType = { [GossipType.beacon_block]: (signedBlock: SignedBeaconBlock) => Promise | void; [GossipType.blob_sidecar]: (blobSidecar: deneb.BlobSidecar) => Promise | void; [GossipType.beacon_aggregate_and_proof]: (aggregateAndProof: SignedAggregateAndProof) => Promise | void; - [GossipType.beacon_attestation]: (attestation: Attestation) => Promise | void; + [GossipType.beacon_attestation]: (attestation: SingleAttestation) => Promise | void; [GossipType.voluntary_exit]: (voluntaryExit: phase0.SignedVoluntaryExit) => Promise | void; [GossipType.proposer_slashing]: (proposerSlashing: phase0.ProposerSlashing) => Promise | void; [GossipType.attester_slashing]: (attesterSlashing: phase0.AttesterSlashing) => Promise | void; diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index 88ef4143f8ff..bf44dd90b8af 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -5,8 +5,9 @@ import { ForkSeq, SYNC_COMMITTEE_SUBNET_COUNT, isForkLightClient, + isForkPostElectra, } from "@lodestar/params"; -import {Attestation, ssz, sszTypesFor} from "@lodestar/types"; +import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types"; import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js"; import {DEFAULT_ENCODING} from "./constants.js"; @@ -87,7 +88,7 @@ export function getGossipSSZType(topic: GossipTopic) { case GossipType.beacon_aggregate_and_proof: return sszTypesFor(topic.fork).SignedAggregateAndProof; case GossipType.beacon_attestation: - return sszTypesFor(topic.fork).Attestation; + return sszTypesFor(topic.fork).SingleAttestation; case GossipType.proposer_slashing: return ssz.phase0.ProposerSlashing; case GossipType.attester_slashing: @@ -124,7 +125,9 @@ export function sszDeserialize(topic: T, serializedData: } /** + * @deprecated * Deserialize a gossip serialized data into an Attestation object. + * No longer used post-electra. Use `sszDeserializeSingleAttestation` instead */ export function sszDeserializeAttestation(fork: ForkName, serializedData: Uint8Array): Attestation { try { @@ -134,6 +137,20 @@ export function sszDeserializeAttestation(fork: ForkName, serializedData: Uint8A } } +/** + * Deserialize a gossip seralized data into an SingleAttestation object. + */ +export function sszDeserializeSingleAttestation(fork: ForkName, serializedData: Uint8Array): SingleAttestation { + try { + if (isForkPostElectra(fork)) { + return sszTypesFor(fork).SingleAttestation.deserialize(serializedData); + } + return sszTypesFor(fork).Attestation.deserialize(serializedData) as SingleAttestation; + } catch (_e) { + throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE}); + } +} + // Parsing const gossipTopicRegex = /^\/eth2\/(\w+)\/(\w+)\/(\w+)/; @@ -213,7 +230,11 @@ export function getCoreTopicsAtFork( // After Deneb also track blob_sidecar_{subnet_id} if (ForkSeq[fork] >= ForkSeq.deneb) { - for (let subnet = 0; subnet < config.BLOB_SIDECAR_SUBNET_COUNT; subnet++) { + const subnetCount = isForkPostElectra(fork) + ? config.BLOB_SIDECAR_SUBNET_COUNT_ELECTRA + : config.BLOB_SIDECAR_SUBNET_COUNT; + + for (let subnet = 0; subnet < subnetCount; subnet++) { topics.push({type: GossipType.blob_sidecar, subnet}); } } diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 7655c4d9d214..0403519856d1 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -20,6 +20,7 @@ import { LightClientOptimisticUpdate, SignedAggregateAndProof, SignedBeaconBlock, + SingleAttestation, Slot, SlotRootHex, SubnetID, @@ -75,7 +76,7 @@ export interface INetwork extends INetworkCorePublic { publishBeaconBlock(signedBlock: SignedBeaconBlock): Promise; publishBlobSidecar(blobSidecar: deneb.BlobSidecar): Promise; publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise; - publishBeaconAttestation(attestation: phase0.Attestation, subnet: SubnetID): Promise; + publishBeaconAttestation(attestation: SingleAttestation, subnet: SubnetID): Promise; publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise; publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise; publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 6f28394da90e..c205a252b712 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -16,6 +16,7 @@ import { Root, SignedAggregateAndProof, SignedBeaconBlock, + SingleAttestation, SlotRootHex, SubnetID, WithBytes, @@ -331,7 +332,7 @@ export class Network implements INetwork { ); } - async publishBeaconAttestation(attestation: phase0.Attestation, subnet: SubnetID): Promise { + async publishBeaconAttestation(attestation: SingleAttestation, subnet: SubnetID): Promise { const fork = this.config.getForkName(attestation.data.slot); return this.publishGossip( {type: GossipType.beacon_attestation, fork, subnet}, @@ -503,10 +504,11 @@ export class Network implements INetwork { peerId: PeerIdStr, request: deneb.BlobSidecarsByRangeRequest ): Promise { + const fork = this.config.getForkName(request.startSlot); return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.BlobSidecarsByRange, [Version.V1], request), // request's count represent the slots, so the actual max count received could be slots * blobs per slot - request.count * this.config.MAX_BLOBS_PER_BLOCK, + request.count * this.config.getMaxBlobsPerBlock(fork), responseSszTypeByMethod[ReqRespMethod.BlobSidecarsByRange] ); } @@ -525,7 +527,8 @@ export class Network implements INetwork { versions: number[], request: Req ): AsyncIterable { - const requestType = requestSszTypeByMethod(this.config)[method]; + const fork = this.config.getForkName(this.clock.currentSlot); + const requestType = requestSszTypeByMethod(this.config, fork)[method]; const requestData = requestType ? requestType.serialize(request as never) : new Uint8Array(); // ReqResp outgoing request, emit from main thread to worker diff --git a/packages/beacon-node/src/network/processor/extractSlotRootFns.ts b/packages/beacon-node/src/network/processor/extractSlotRootFns.ts index 57a4861b4cb8..d478078d5df5 100644 --- a/packages/beacon-node/src/network/processor/extractSlotRootFns.ts +++ b/packages/beacon-node/src/network/processor/extractSlotRootFns.ts @@ -1,8 +1,9 @@ +import {ForkName} from "@lodestar/params"; import {SlotOptionalRoot, SlotRootHex} from "@lodestar/types"; import { - getBlockRootFromAttestationSerialized, + getBlockRootFromBeaconAttestationSerialized, getBlockRootFromSignedAggregateAndProofSerialized, - getSlotFromAttestationSerialized, + getSlotFromBeaconAttestationSerialized, getSlotFromBlobSidecarSerialized, getSlotFromSignedAggregateAndProofSerialized, getSlotFromSignedBeaconBlockSerialized, @@ -16,9 +17,9 @@ import {ExtractSlotRootFns} from "./types.js"; */ export function createExtractBlockSlotRootFns(): ExtractSlotRootFns { return { - [GossipType.beacon_attestation]: (data: Uint8Array): SlotRootHex | null => { - const slot = getSlotFromAttestationSerialized(data); - const root = getBlockRootFromAttestationSerialized(data); + [GossipType.beacon_attestation]: (data: Uint8Array, fork: ForkName): SlotRootHex | null => { + const slot = getSlotFromBeaconAttestationSerialized(fork, data); + const root = getBlockRootFromBeaconAttestationSerialized(fork, data); if (slot === null || root === null) { return null; diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 7ef32ffc32b1..ec61aa277d03 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -1,8 +1,18 @@ import {routes} from "@lodestar/api"; import {BeaconConfig, ChainForkConfig} from "@lodestar/config"; -import {ForkName, ForkSeq} from "@lodestar/params"; +import {ForkName, ForkPostElectra, ForkPreElectra, ForkSeq, isForkPostElectra} from "@lodestar/params"; import {computeTimeAtSlot} from "@lodestar/state-transition"; -import {Root, SignedBeaconBlock, Slot, SubnetID, UintNum64, deneb, ssz, sszTypesFor} from "@lodestar/types"; +import { + Root, + SignedBeaconBlock, + SingleAttestation, + Slot, + SubnetID, + UintNum64, + deneb, + ssz, + sszTypesFor, +} from "@lodestar/types"; import {LogLevel, Logger, prettyBytes, toRootHex} from "@lodestar/utils"; import { BlobSidecarValidation, @@ -28,6 +38,7 @@ import {validateGossipBlobSidecar} from "../../chain/validation/blobSidecar.js"; import { AggregateAndProofValidationResult, GossipAttestation, + toElectraSingleAttestation, validateGossipAggregateAndProof, validateGossipAttestationsSameAttData, validateGossipAttesterSlashing, @@ -185,6 +196,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand ): Promise { const blobBlockHeader = blobSidecar.signedBlockHeader.message; const slot = blobBlockHeader.slot; + const fork = config.getForkName(slot); const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blobBlockHeader); const blockHex = prettyBytes(blockRoot); @@ -202,7 +214,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand ); try { - await validateGossipBlobSidecar(chain, blobSidecar, subnet); + await validateGossipBlobSidecar(fork, chain, blobSidecar, subnet); const recvToValidation = Date.now() / 1000 - seenTimestampSec; const validationTime = recvToValidation - recvToValLatency; @@ -636,14 +648,27 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp results.push(null); // Handler - const {indexedAttestation, attDataRootHex, attestation, committeeIndex} = validationResult.result; + const { + indexedAttestation, + attDataRootHex, + attestation, + committeeIndex, + committeeValidatorIndex, + committeeSize, + } = validationResult.result; metrics?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation); try { // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages // but don't add to attestation pool, to save CPU and RAM if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { - const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex); + const insertOutcome = chain.attestationPool.add( + committeeIndex, + attestation, + attDataRootHex, + committeeValidatorIndex, + committeeSize + ); metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); } } catch (e) { @@ -658,7 +683,21 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp } } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + if (isForkPostElectra(fork)) { + chain.emitter.emit( + routes.events.EventType.singleAttestation, + attestation as SingleAttestation + ); + } else { + chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation); + chain.emitter.emit( + routes.events.EventType.singleAttestation, + toElectraSingleAttestation( + attestation as SingleAttestation, + indexedAttestation.attestingIndices[0] + ) + ); + } } if (batchableBls) { diff --git a/packages/beacon-node/src/network/processor/gossipQueues/index.ts b/packages/beacon-node/src/network/processor/gossipQueues/index.ts index b76ebe2d875d..4958fd8a50e6 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/index.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/index.ts @@ -1,5 +1,5 @@ import {mapValues} from "@lodestar/utils"; -import {getGossipAttestationIndex} from "../../../util/sszBytes.js"; +import {getBeaconAttestationGossipIndex} from "../../../util/sszBytes.js"; import {BatchGossipType, GossipType, SequentialGossipType} from "../../gossip/interface.js"; import {PendingGossipsubMessage} from "../types.js"; import {IndexedGossipQueueMinSize} from "./indexed.js"; @@ -72,8 +72,7 @@ const indexedGossipQueueOpts: { // this topic may cause node to be overload and drop 100% of lower priority queues maxLength: 24576, indexFn: (item: PendingGossipsubMessage) => { - // Note indexFn is fork agnostic despite changes introduced in Electra - return getGossipAttestationIndex(item.msg.data); + return getBeaconAttestationGossipIndex(item.topic.fork, item.msg.data); }, minChunkSize: MIN_SIGNATURE_SETS_TO_BATCH_VERIFY, maxChunkSize: MAX_GOSSIP_ATTESTATION_BATCH_SIZE, diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 4bfc4263adc8..2b471034aee5 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -247,7 +247,7 @@ export class NetworkProcessor { const extractBlockSlotRootFn = this.extractBlockSlotRootFns[topicType]; // check block root of Attestation and SignedAggregateAndProof messages if (extractBlockSlotRootFn) { - const slotRoot = extractBlockSlotRootFn(message.msg.data); + const slotRoot = extractBlockSlotRootFn(message.msg.data, message.topic.fork); // if slotRoot is null, it means the msg.data is invalid // in that case message will be rejected when deserializing data in later phase (gossipValidatorFn) if (slotRoot) { diff --git a/packages/beacon-node/src/network/processor/types.ts b/packages/beacon-node/src/network/processor/types.ts index ec78116bc766..c059f77eb727 100644 --- a/packages/beacon-node/src/network/processor/types.ts +++ b/packages/beacon-node/src/network/processor/types.ts @@ -1,4 +1,5 @@ import {Message} from "@libp2p/interface"; +import {ForkName} from "@lodestar/params"; import {Slot, SlotOptionalRoot} from "@lodestar/types"; import {PeerIdStr} from "../../util/peerId.js"; import {GossipTopic, GossipType} from "../gossip/index.js"; @@ -22,5 +23,5 @@ export type PendingGossipsubMessage = { }; export type ExtractSlotRootFns = { - [K in GossipType]?: (data: Uint8Array) => SlotOptionalRoot | null; + [K in GossipType]?: (data: Uint8Array, forkName: ForkName) => SlotOptionalRoot | null; }; diff --git a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts index 96b5c6c0a776..b84886bd2974 100644 --- a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts +++ b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts @@ -207,7 +207,8 @@ export class ReqRespBeaconNode extends ReqResp { versions: number[], request: Req ): AsyncIterable { - const requestType = requestSszTypeByMethod(this.config)[method]; + const fork = ForkName[ForkSeq[this.currentRegisteredFork] as ForkName]; + const requestType = requestSszTypeByMethod(this.config, fork)[method]; const requestData = requestType ? requestType.serialize(request as never) : new Uint8Array(); return this.sendRequestWithoutEncoding(peerId, method, versions, requestData); } diff --git a/packages/beacon-node/src/network/reqresp/beaconBlocksMaybeBlobsByRoot.ts b/packages/beacon-node/src/network/reqresp/beaconBlocksMaybeBlobsByRoot.ts index 3bbe00bfc56b..7680634246cd 100644 --- a/packages/beacon-node/src/network/reqresp/beaconBlocksMaybeBlobsByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/beaconBlocksMaybeBlobsByRoot.ts @@ -181,7 +181,7 @@ export async function unavailableBeaconBlobsByRoot( blobsCache.set(blobSidecar.index, {blobSidecar, blobBytes: null}); } else { metrics?.blockInputFetchStats.dataPromiseBlobsDelayedGossipAvailable.inc(); - metrics?.blockInputFetchStats.dataPromiseBlobsDeplayedGossipAvailableSavedGetBlobsCompute.inc(); + metrics?.blockInputFetchStats.dataPromiseBlobsDelayedGossipAvailableSavedGetBlobsCompute.inc(); } } // may be blobsidecar arrived in the timespan of making the request @@ -255,7 +255,7 @@ export async function unavailableBeaconBlobsByRoot( resolveAvailability(blockData); metrics?.syncUnknownBlock.resolveAvailabilitySource.inc({source: BlockInputAvailabilitySource.UNKNOWN_SYNC}); - metrics?.blockInputFetchStats.totalDataAvailableBlockInputs.inc(); + metrics?.blockInputFetchStats.totalDataPromiseBlockInputsResolvedAvailable.inc(); if (getBlobsUseful) { metrics?.blockInputFetchStats.totalDataPromiseBlockInputsAvailableUsingGetBlobs.inc(); } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts index e8c19fb49628..ce563c8f3d82 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -1,4 +1,5 @@ -import {GENESIS_SLOT, MAX_REQUEST_BLOCKS} from "@lodestar/params"; +import {BeaconConfig} from "@lodestar/config"; +import {GENESIS_SLOT, MAX_REQUEST_BLOCKS, MAX_REQUEST_BLOCKS_DENEB, isForkBlobs} from "@lodestar/params"; import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {deneb, phase0} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; @@ -12,7 +13,7 @@ export async function* onBeaconBlocksByRange( chain: IBeaconChain, db: IBeaconDb ): AsyncIterable { - const {startSlot, count} = validateBeaconBlocksByRangeRequest(request); + const {startSlot, count} = validateBeaconBlocksByRangeRequest(chain.config, request); const endSlot = startSlot + count; const finalized = db.blockArchive; @@ -69,6 +70,7 @@ export async function* onBeaconBlocksByRange( } export function validateBeaconBlocksByRangeRequest( + config: BeaconConfig, request: deneb.BlobSidecarsByRangeRequest ): deneb.BlobSidecarsByRangeRequest { const {startSlot} = request; @@ -84,8 +86,10 @@ export function validateBeaconBlocksByRangeRequest( // step > 1 is deprecated, see https://github.com/ethereum/consensus-specs/pull/2856 - if (count > MAX_REQUEST_BLOCKS) { - count = MAX_REQUEST_BLOCKS; + const maxRequestBlocks = isForkBlobs(config.getForkName(startSlot)) ? MAX_REQUEST_BLOCKS_DENEB : MAX_REQUEST_BLOCKS; + + if (count > maxRequestBlocks) { + count = maxRequestBlocks; } return {startSlot, count}; diff --git a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts index 9cac846fdb61..557d01a22840 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts @@ -1,4 +1,5 @@ -import {BLOBSIDECAR_FIXED_SIZE, GENESIS_SLOT, MAX_REQUEST_BLOCKS_DENEB} from "@lodestar/params"; +import {BeaconConfig} from "@lodestar/config"; +import {BLOBSIDECAR_FIXED_SIZE, GENESIS_SLOT} from "@lodestar/params"; import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {Slot, deneb} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; @@ -12,7 +13,7 @@ export async function* onBlobSidecarsByRange( db: IBeaconDb ): AsyncIterable { // Non-finalized range of blobs - const {startSlot, count} = validateBlobSidecarsByRangeRequest(request); + const {startSlot, count} = validateBlobSidecarsByRangeRequest(chain.config, request); const endSlot = startSlot + count; const finalized = db.blobSidecarsArchive; @@ -90,6 +91,7 @@ export function* iterateBlobBytesFromWrapper( } export function validateBlobSidecarsByRangeRequest( + config: BeaconConfig, request: deneb.BlobSidecarsByRangeRequest ): deneb.BlobSidecarsByRangeRequest { const {startSlot} = request; @@ -103,8 +105,10 @@ export function validateBlobSidecarsByRangeRequest( throw new ResponseError(RespStatus.INVALID_REQUEST, "startSlot < genesis"); } - if (count > MAX_REQUEST_BLOCKS_DENEB) { - count = MAX_REQUEST_BLOCKS_DENEB; + const maxRequestBlobSidecars = config.getMaxRequestBlobSidecars(config.getForkName(startSlot)); + + if (count > maxRequestBlobSidecars) { + count = maxRequestBlobSidecars; } return {startSlot, count}; diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 83f6620dbbd4..85eb4a0136b2 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -38,7 +38,8 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh return onBeaconBlocksByRoot(body, chain, db); }, [ReqRespMethod.BlobSidecarsByRoot]: (req) => { - const body = BlobSidecarsByRootRequestType(chain.config).deserialize(req.data); + const fork = chain.config.getForkName(chain.clock.currentSlot); + const body = BlobSidecarsByRootRequestType(fork, chain.config).deserialize(req.data); return onBlobSidecarsByRoot(body, chain, db); }, [ReqRespMethod.BlobSidecarsByRange]: (req) => { diff --git a/packages/beacon-node/src/network/reqresp/rateLimit.ts b/packages/beacon-node/src/network/reqresp/rateLimit.ts index 771d01f6c339..7553310b96d7 100644 --- a/packages/beacon-node/src/network/reqresp/rateLimit.ts +++ b/packages/beacon-node/src/network/reqresp/rateLimit.ts @@ -1,10 +1,10 @@ -import {ChainConfig} from "@lodestar/config"; +import {BeaconConfig} from "@lodestar/config"; import {MAX_REQUEST_BLOCKS, MAX_REQUEST_LIGHT_CLIENT_UPDATES} from "@lodestar/params"; import {InboundRateLimitQuota} from "@lodestar/reqresp"; import {ReqRespMethod, RequestBodyByMethod} from "./types.js"; import {requestSszTypeByMethod} from "./types.js"; -export const rateLimitQuotas: (config: ChainConfig) => Record = (config) => ({ +export const rateLimitQuotas: (config: BeaconConfig) => Record = (config) => ({ [ReqRespMethod.Status]: { // Rationale: https://github.com/sigp/lighthouse/blob/bf533c8e42cc73c35730e285c21df8add0195369/beacon_node/lighthouse_network/src/rpc/mod.rs#L118-L130 byPeer: {quota: 5, quotaTimeMs: 15_000}, @@ -34,11 +34,13 @@ export const rateLimitQuotas: (config: ChainConfig) => Record req.count), }, [ReqRespMethod.BlobSidecarsByRoot]: { // Rationale: quota of BeaconBlocksByRoot * MAX_BLOBS_PER_BLOCK + // TODO Electra: Stays as `MAX_REQUEST_BLOB_SIDECARS` until we have fork-aware `byPeer` and set it to `MAX_REQUEST_BLOB_SIDECARS_ELECTRA` byPeer: {quota: config.MAX_REQUEST_BLOB_SIDECARS, quotaTimeMs: 10_000}, getRequestCount: getRequestCountFn(config, ReqRespMethod.BlobSidecarsByRoot, (req) => req.length), }, @@ -65,7 +67,7 @@ export const rateLimitQuotas: (config: ChainConfig) => Record( - config: ChainConfig, + config: BeaconConfig, method: T, fn: (req: RequestBodyByMethod[T]) => number ): (reqData: Uint8Array) => number { diff --git a/packages/beacon-node/src/network/reqresp/types.ts b/packages/beacon-node/src/network/reqresp/types.ts index b7c18ebdfeb5..476262bc51a2 100644 --- a/packages/beacon-node/src/network/reqresp/types.ts +++ b/packages/beacon-node/src/network/reqresp/types.ts @@ -1,5 +1,5 @@ import {Type} from "@chainsafe/ssz"; -import {ChainConfig} from "@lodestar/config"; +import {BeaconConfig} from "@lodestar/config"; import {ForkLightClient, ForkName, isForkLightClient} from "@lodestar/params"; import {Protocol, ProtocolHandler, ReqRespRequest} from "@lodestar/reqresp"; import { @@ -70,9 +70,13 @@ type ResponseBodyByMethod = { }; /** Request SSZ type for each method and ForkName */ -export const requestSszTypeByMethod: (config: ChainConfig) => { +// TODO Electra: Currently setting default fork to deneb because not every caller of requestSszTypeByMethod can provide fork info +export const requestSszTypeByMethod: ( + config: BeaconConfig, + fork?: ForkName +) => { [K in ReqRespMethod]: RequestBodyByMethod[K] extends null ? null : Type; -} = (config) => ({ +} = (config, fork = ForkName.deneb) => ({ [ReqRespMethod.Status]: ssz.phase0.Status, [ReqRespMethod.Goodbye]: ssz.phase0.Goodbye, [ReqRespMethod.Ping]: ssz.phase0.Ping, @@ -80,7 +84,7 @@ export const requestSszTypeByMethod: (config: ChainConfig) => { [ReqRespMethod.BeaconBlocksByRange]: ssz.phase0.BeaconBlocksByRangeRequest, [ReqRespMethod.BeaconBlocksByRoot]: ssz.phase0.BeaconBlocksByRootRequest, [ReqRespMethod.BlobSidecarsByRange]: ssz.deneb.BlobSidecarsByRangeRequest, - [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequestType(config), + [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequestType(fork, config), [ReqRespMethod.LightClientBootstrap]: ssz.Root, [ReqRespMethod.LightClientUpdatesByRange]: ssz.altair.LightClientUpdatesByRange, [ReqRespMethod.LightClientFinalityUpdate]: null, diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index cb80d5d1bb4b..13ce4c417ee9 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -5,8 +5,9 @@ import { ForkName, ForkSeq, MAX_COMMITTEES_PER_SLOT, + isForkPostElectra, } from "@lodestar/params"; -import {BLSSignature, RootHex, Slot} from "@lodestar/types"; +import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex} from "@lodestar/types"; export type BlockRootHex = RootHex; // pre-electra, AttestationData is used to cache attestations @@ -26,6 +27,12 @@ export type CommitteeBitsBase64 = string; // data: AttestationData - target data - 128 // signature: BLSSignature - 96 // committee_bits: BitVector[MAX_COMMITTEES_PER_SLOT] +// electra +// class SingleAttestation(Container): +// committeeIndex: CommitteeIndex - data 8 +// attesterIndex: ValidatorIndex - data 8 +// data: AttestationData - data 128 +// signature: BLSSignature - data 96 // // for all forks // class AttestationData(Container): 128 bytes fixed size @@ -39,10 +46,18 @@ const VARIABLE_FIELD_OFFSET = 4; const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8; const ROOT_SIZE = 32; const SLOT_SIZE = 8; +const COMMITTEE_INDEX_SIZE = 8; const ATTESTATION_DATA_SIZE = 128; // MAX_COMMITTEES_PER_SLOT is in bit, need to convert to byte const COMMITTEE_BITS_SIZE = Math.max(Math.ceil(MAX_COMMITTEES_PER_SLOT / 8), 1); const SIGNATURE_SIZE = 96; +const SINGLE_ATTESTATION_ATTDATA_OFFSET = 8 + 8; +const SINGLE_ATTESTATION_SLOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET; +const SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET = 0; +const SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET = 8; +const SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + 8 + 8; +const SINGLE_ATTESTATION_SIGNATURE_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE; +const SINGLE_ATTESTATION_SIZE = SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE; // shared Buffers to convert bytes to hex/base64 const blockRootBuf = Buffer.alloc(ROOT_SIZE); @@ -91,21 +106,40 @@ export function getAttDataFromAttestationSerialized(data: Uint8Array): AttDataBa } /** - * Alias of `getAttDataFromAttestationSerialized` specifically for batch handling indexing in gossip queue + * Extract AttDataBase64 from `beacon_attestation` gossip message serialized bytes. + * This is used for GossipQueue. + */ +export function getBeaconAttestationGossipIndex(fork: ForkName, data: Uint8Array): AttDataBase64 | null { + return ForkSeq[fork] >= ForkSeq.electra + ? getAttDataFromSingleAttestationSerialized(data) + : getAttDataFromAttestationSerialized(data); +} + +/** + * Extract slot from `beacon_attestation` gossip message serialized bytes. + */ +export function getSlotFromBeaconAttestationSerialized(fork: ForkName, data: Uint8Array): Slot | null { + return ForkSeq[fork] >= ForkSeq.electra + ? getSlotFromSingleAttestationSerialized(data) + : getSlotFromAttestationSerialized(data); +} + +/** + * Extract block root from `beacon_attestation` gossip message serialized bytes. */ -export function getGossipAttestationIndex(data: Uint8Array): AttDataBase64 | null { - return getAttDataFromAttestationSerialized(data); +export function getBlockRootFromBeaconAttestationSerialized(fork: ForkName, data: Uint8Array): BlockRootHex | null { + return ForkSeq[fork] >= ForkSeq.electra + ? getBlockRootFromSingleAttestationSerialized(data) + : getBlockRootFromAttestationSerialized(data); } /** * Extract aggregation bits from attestation serialized bytes. * Return null if data is not long enough to extract aggregation bits. + * Pre-electra attestation only */ -export function getAggregationBitsFromAttestationSerialized(fork: ForkName, data: Uint8Array): BitArray | null { - const aggregationBitsStartIndex = - ForkSeq[fork] >= ForkSeq.electra - ? VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE - : VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; +export function getAggregationBitsFromAttestationSerialized(data: Uint8Array): BitArray | null { + const aggregationBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; if (data.length < aggregationBitsStartIndex) { return null; @@ -130,18 +164,93 @@ export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSign } /** - * Extract committee bits from Electra attestation serialized bytes. - * Return null if data is not long enough to extract committee bits. + * Extract slot from SingleAttestation serialized bytes. + * Return null if data is not long enough to extract slot. */ -export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): CommitteeBitsBase64 | null { - const committeeBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; +export function getSlotFromSingleAttestationSerialized(data: Uint8Array): Slot | null { + if (data.length !== SINGLE_ATTESTATION_SIZE) { + return null; + } - if (data.length < committeeBitsStartIndex + COMMITTEE_BITS_SIZE) { + return getSlotFromOffset(data, SINGLE_ATTESTATION_SLOT_OFFSET); +} + +/** + * Extract committee index from SingleAttestation serialized bytes. + * Return null if data is not long enough to extract slot. + */ +export function getCommitteeIndexFromSingleAttestationSerialized( + fork: ForkName, + data: Uint8Array +): CommitteeIndex | null { + if (isForkPostElectra(fork)) { + if (data.length !== SINGLE_ATTESTATION_SIZE) { + return null; + } + + return getIndexFromOffset(data, SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET); + } + + if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE + COMMITTEE_INDEX_SIZE) { return null; } - committeeBitsDataBuf.set(data.subarray(committeeBitsStartIndex, committeeBitsStartIndex + COMMITTEE_BITS_SIZE)); - return committeeBitsDataBuf.toString("base64"); + return getIndexFromOffset(data, VARIABLE_FIELD_OFFSET + SLOT_SIZE); +} + +/** + * Extract attester index from SingleAttestation serialized bytes. + * Return null if data is not long enough to extract index. + */ +export function getAttesterIndexFromSingleAttestationSerialized(data: Uint8Array): ValidatorIndex | null { + if (data.length !== SINGLE_ATTESTATION_SIZE) { + return null; + } + + return getIndexFromOffset(data, SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET); +} + +/** + * Extract block root from SingleAttestation serialized bytes. + * Return null if data is not long enough to extract block root. + */ +export function getBlockRootFromSingleAttestationSerialized(data: Uint8Array): BlockRootHex | null { + if (data.length !== SINGLE_ATTESTATION_SIZE) { + return null; + } + + blockRootBuf.set( + data.subarray(SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET, SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) + ); + return `0x${blockRootBuf.toString("hex")}`; +} + +/** + * Extract attestation data base64 from SingleAttestation serialized bytes. + * Return null if data is not long enough to extract attestation data. + */ +export function getAttDataFromSingleAttestationSerialized(data: Uint8Array): AttDataBase64 | null { + if (data.length !== SINGLE_ATTESTATION_SIZE) { + return null; + } + + // base64 is a bit efficient than hex + attDataBuf.set( + data.subarray(SINGLE_ATTESTATION_ATTDATA_OFFSET, SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE) + ); + return attDataBuf.toString("base64"); +} + +/** + * Extract signature from SingleAttestation serialized bytes. + * Return null if data is not long enough to extract signature. + */ +export function getSignatureFromSingleAttestationSerialized(data: Uint8Array): BLSSignature | null { + if (data.length !== SINGLE_ATTESTATION_SIZE) { + return null; + } + + return data.subarray(SINGLE_ATTESTATION_SIGNATURE_OFFSET, SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE); } // @@ -295,6 +404,13 @@ function getSlotFromOffset(data: Uint8Array, offset: number): Slot | null { return checkSlotHighBytes(data, offset) ? getSlotFromOffsetTrusted(data, offset) : null; } +/** + * Alias of `getSlotFromOffset` for readability + */ +function getIndexFromOffset(data: Uint8Array, offset: number): (ValidatorIndex | CommitteeIndex) | null { + return getSlotFromOffset(data, offset); +} + /** * Read only the first 4 bytes of Slot, max value is 4,294,967,295 will be reached 1634 years after genesis */ diff --git a/packages/beacon-node/src/util/types.ts b/packages/beacon-node/src/util/types.ts index 5b9c7a784277..4133d6db102f 100644 --- a/packages/beacon-node/src/util/types.ts +++ b/packages/beacon-node/src/util/types.ts @@ -1,5 +1,6 @@ import {ContainerType, ListCompositeType, ValueOf} from "@chainsafe/ssz"; -import {ChainConfig} from "@lodestar/config"; +import {BeaconConfig} from "@lodestar/config"; +import {ForkName} from "@lodestar/params"; import {ssz} from "@lodestar/types"; // Misc SSZ types used only in the beacon-node package, no need to upstream to types @@ -14,6 +15,6 @@ export const signedBLSToExecutionChangeVersionedType = new ContainerType( ); export type SignedBLSToExecutionChangeVersioned = ValueOf; -export const BlobSidecarsByRootRequestType = (config: ChainConfig) => - new ListCompositeType(ssz.deneb.BlobIdentifier, config.MAX_REQUEST_BLOB_SIDECARS); +export const BlobSidecarsByRootRequestType = (fork: ForkName, config: BeaconConfig) => + new ListCompositeType(ssz.deneb.BlobIdentifier, config.getMaxRequestBlobSidecars(fork)); export type BlobSidecarsByRootRequest = ValueOf>; diff --git a/packages/beacon-node/test/memory/seenAttestationData.ts b/packages/beacon-node/test/memory/seenAttestationData.ts index 44a82dcd5841..53aa519f40f7 100644 --- a/packages/beacon-node/test/memory/seenAttestationData.ts +++ b/packages/beacon-node/test/memory/seenAttestationData.ts @@ -35,7 +35,7 @@ function getRandomSeenAttestationDatas(n: number): SeenAttestationDatas { attDataRootHex: toHexString(crypto.randomBytes(32)), subnet: i, } as unknown as AttestationDataCacheEntry; - seenAttestationDatas.add(slot, key, attDataCacheEntry); + seenAttestationDatas.add(slot, i, key, attDataCacheEntry); } return seenAttestationDatas; } diff --git a/packages/beacon-node/test/spec/specTestVersioning.ts b/packages/beacon-node/test/spec/specTestVersioning.ts index 89c701a83514..7925a9af80f1 100644 --- a/packages/beacon-node/test/spec/specTestVersioning.ts +++ b/packages/beacon-node/test/spec/specTestVersioning.ts @@ -14,7 +14,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util/downloadTests"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const ethereumConsensusSpecsTests: DownloadTestsOptions = { - specVersion: "v1.5.0-alpha.8", + specVersion: "v1.5.0-beta.0", // Target directory is the host package root: 'packages/*/spec-tests' outputDir: path.join(__dirname, "../../spec-tests"), specTestsRepoUrl: "https://github.com/ethereum/consensus-spec-tests", diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 0868183295cf..deb2988cfdfe 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -58,7 +58,7 @@ const coveredTestRunners = [ // ], // ``` export const defaultSkipOpts: SkipOpts = { - skippedForks: ["eip7594"], + skippedForks: ["eip7594", "fulu"], // TODO: capella // BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix // Skip them for now to enable subsequently @@ -66,6 +66,7 @@ export const defaultSkipOpts: SkipOpts = { /^capella\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^deneb\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^electra\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, + /^.+\/light_client\/data_collection\/.*/, ], skippedTests: [], skippedRunners: ["merkle_proof", "networking"], diff --git a/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts index fd22f9a7c6a5..5e897bb6380a 100644 --- a/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts @@ -1,7 +1,7 @@ -import {fromHexString, toHexString} from "@chainsafe/ssz"; +import {BitArray, fromHexString, toHexString} from "@chainsafe/ssz"; import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; -import {GENESIS_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {ssz} from "@lodestar/types"; +import {GENESIS_SLOT, MAX_COMMITTEES_PER_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {electra, phase0, ssz} from "@lodestar/types"; import {beforeEach, describe, expect, it, vi} from "vitest"; import {AttestationPool} from "../../../../src/chain/opPools/attestationPool.js"; import {InsertOutcome} from "../../../../src/chain/opPools/types.js"; @@ -24,20 +24,32 @@ describe("AttestationPool", () => { const clockStub = getMockedClock(); vi.spyOn(clockStub, "secFromSlot").mockReturnValue(0); + const committeeValidatorIndex = 0; + const committeeSize = 128; + const cutOffSecFromSlot = (2 / 3) * config.SECONDS_PER_SLOT; // Mock attestations - const electraAttestationData = { + const electraAttestationData: phase0.AttestationData = { ...ssz.phase0.AttestationData.defaultValue(), slot: config.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH, }; - const electraAttestation = { - ...ssz.electra.Attestation.defaultValue(), + const electraSingleAttestation: electra.SingleAttestation = { + ...ssz.electra.SingleAttestation.defaultValue(), + data: electraAttestationData, + signature: validSignature, + }; + const electraAttestation: electra.Attestation = { + aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex), data: electraAttestationData, signature: validSignature, + committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, electraSingleAttestation.committeeIndex), }; - const phase0AttestationData = {...ssz.phase0.AttestationData.defaultValue(), slot: GENESIS_SLOT}; - const phase0Attestation = { + const phase0AttestationData: phase0.AttestationData = { + ...ssz.phase0.AttestationData.defaultValue(), + slot: GENESIS_SLOT, + }; + const phase0Attestation: phase0.Attestation = { ...ssz.phase0.Attestation.defaultValue(), data: phase0AttestationData, signature: validSignature, @@ -51,8 +63,14 @@ describe("AttestationPool", () => { it("add correct electra attestation", () => { const committeeIndex = 0; - const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestation.data)); - const outcome = pool.add(committeeIndex, electraAttestation, attDataRootHex); + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraSingleAttestation.data)); + const outcome = pool.add( + committeeIndex, + electraSingleAttestation, + attDataRootHex, + committeeValidatorIndex, + committeeSize + ); expect(outcome).equal(InsertOutcome.NewData); expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toEqual(electraAttestation); @@ -61,7 +79,7 @@ describe("AttestationPool", () => { it("add correct phase0 attestation", () => { const committeeIndex = null; const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data)); - const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex); + const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, committeeValidatorIndex, committeeSize); expect(outcome).equal(InsertOutcome.NewData); expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation); @@ -72,16 +90,18 @@ describe("AttestationPool", () => { it("add electra attestation without committee index", () => { const committeeIndex = null; - const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestation.data)); + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraSingleAttestation.data)); - expect(() => pool.add(committeeIndex, electraAttestation, attDataRootHex)).toThrow(); + expect(() => + pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, committeeValidatorIndex, committeeSize) + ).toThrow(); expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toBeNull(); }); it("add phase0 attestation with committee index", () => { const committeeIndex = 0; const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data)); - const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex); + const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, committeeValidatorIndex, committeeSize); expect(outcome).equal(InsertOutcome.NewData); expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation); @@ -92,14 +112,14 @@ describe("AttestationPool", () => { it("add electra attestation with phase0 slot", () => { const electraAttestationDataWithPhase0Slot = {...ssz.phase0.AttestationData.defaultValue(), slot: GENESIS_SLOT}; - const attestation = { - ...ssz.electra.Attestation.defaultValue(), + const singleAttestation = { + ...ssz.electra.SingleAttestation.defaultValue(), data: electraAttestationDataWithPhase0Slot, signature: validSignature, }; const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestationDataWithPhase0Slot)); - expect(() => pool.add(0, attestation, attDataRootHex)).toThrow(); + expect(() => pool.add(0, singleAttestation, attDataRootHex, committeeValidatorIndex, committeeSize)).toThrow(); }); it("add phase0 attestation with electra slot", () => { @@ -114,6 +134,6 @@ describe("AttestationPool", () => { }; const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0AttestationDataWithElectraSlot)); - expect(() => pool.add(0, attestation, attDataRootHex)).toThrow(); + expect(() => pool.add(0, attestation, attDataRootHex, committeeValidatorIndex, committeeSize)).toThrow(); }); }); diff --git a/packages/beacon-node/test/unit/chain/seenCache/seenAttestationData.test.ts b/packages/beacon-node/test/unit/chain/seenCache/seenAttestationData.test.ts index ee5cb94ae959..aaac18b6f33a 100644 --- a/packages/beacon-node/test/unit/chain/seenCache/seenAttestationData.test.ts +++ b/packages/beacon-node/test/unit/chain/seenCache/seenAttestationData.test.ts @@ -1,6 +1,10 @@ import {beforeEach, describe, expect, it} from "vitest"; import {InsertOutcome} from "../../../../src/chain/opPools/types.js"; -import {AttestationDataCacheEntry, SeenAttestationDatas} from "../../../../src/chain/seenCache/seenAttestationData.js"; +import { + AttestationDataCacheEntry, + PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, + SeenAttestationDatas, +} from "../../../../src/chain/seenCache/seenAttestationData.js"; // Compare this snippet from packages/beacon-node/src/chain/seenCache/seenAttestationData.ts: describe("SeenAttestationDatas", () => { @@ -11,9 +15,15 @@ describe("SeenAttestationDatas", () => { beforeEach(() => { cache = new SeenAttestationDatas(null, 1, 2); cache.onSlot(100); - cache.add(99, "99a", {attDataRootHex: "99a"} as AttestationDataCacheEntry); - cache.add(99, "99b", {attDataRootHex: "99b"} as AttestationDataCacheEntry); - cache.add(100, "100a", {attDataRootHex: "100a"} as AttestationDataCacheEntry); + cache.add(99, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, "99a", { + attDataRootHex: "99a", + } as AttestationDataCacheEntry); + cache.add(99, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, "99b", { + attDataRootHex: "99b", + } as AttestationDataCacheEntry); + cache.add(100, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, "100a", { + attDataRootHex: "100a", + } as AttestationDataCacheEntry); }); const addTestCases: {slot: number; attDataBase64: string; expected: InsertOutcome}[] = [ @@ -26,7 +36,7 @@ describe("SeenAttestationDatas", () => { for (const testCase of addTestCases) { it(`add slot ${testCase.slot} data ${testCase.attDataBase64} should return ${testCase.expected}`, () => { expect( - cache.add(testCase.slot, testCase.attDataBase64, { + cache.add(testCase.slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, testCase.attDataBase64, { attDataRootHex: testCase.attDataBase64, } as AttestationDataCacheEntry) ).toBe(testCase.expected); @@ -44,9 +54,13 @@ describe("SeenAttestationDatas", () => { testCase.expectedNull ? "null" : "not null" }`, () => { if (testCase.expectedNull) { - expect(cache.get(testCase.slot, testCase.attDataBase64)).toBeNull(); + expect( + cache.get(testCase.slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, testCase.attDataBase64) + ).toBeNull(); } else { - expect(cache.get(testCase.slot, testCase.attDataBase64)).not.toBeNull(); + expect( + cache.get(testCase.slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, testCase.attDataBase64) + ).not.toBeNull(); } }); } diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts index 228f705355ad..5aecb5f7ccf1 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts @@ -341,7 +341,7 @@ describe("getSeenAttDataKey", () => { signedAggregateAndProof.message.aggregate.data.beaconBlockRoot = blockRoot; const aggregateAndProofBytes = ssz.phase0.SignedAggregateAndProof.serialize(signedAggregateAndProof); - expect(getSeenAttDataKeyFromGossipAttestation(ForkName.phase0, gossipAttestation)).toEqual( + expect(getSeenAttDataKeyFromGossipAttestation(gossipAttestation)).toEqual( getSeenAttDataKeyFromSignedAggregateAndProof(ForkName.phase0, aggregateAndProofBytes) ); }); @@ -363,7 +363,7 @@ describe("getSeenAttDataKey", () => { signedAggregateAndProof.message.aggregate.data.beaconBlockRoot = blockRoot; const aggregateAndProofBytes = ssz.electra.SignedAggregateAndProof.serialize(signedAggregateAndProof); - expect(getSeenAttDataKeyFromGossipAttestation(ForkName.electra, gossipAttestation)).toEqual( + expect(getSeenAttDataKeyFromGossipAttestation(gossipAttestation)).toEqual( getSeenAttDataKeyFromSignedAggregateAndProof(ForkName.electra, aggregateAndProofBytes) ); }); diff --git a/packages/beacon-node/test/unit/execution/builder/cache.test.ts b/packages/beacon-node/test/unit/execution/builder/cache.test.ts new file mode 100644 index 000000000000..60667caf0a50 --- /dev/null +++ b/packages/beacon-node/test/unit/execution/builder/cache.test.ts @@ -0,0 +1,58 @@ +import {ssz} from "@lodestar/types"; +import {beforeEach, describe, expect, it} from "vitest"; +import {ValidatorRegistrationCache} from "../../../../src/execution/builder/cache.js"; + +describe("ValidatorRegistrationCache", () => { + const gasLimit1 = 30000000; + const gasLimit2 = 36000000; + + const validatorPubkey1 = new Uint8Array(48).fill(1); + const validatorPubkey2 = new Uint8Array(48).fill(2); + + const validatorRegistration1 = ssz.bellatrix.ValidatorRegistrationV1.defaultValue(); + validatorRegistration1.pubkey = validatorPubkey1; + validatorRegistration1.gasLimit = gasLimit1; + + const validatorRegistration2 = ssz.bellatrix.ValidatorRegistrationV1.defaultValue(); + validatorRegistration2.pubkey = validatorPubkey2; + validatorRegistration2.gasLimit = gasLimit2; + + let cache: ValidatorRegistrationCache; + + beforeEach(() => { + // max 2 items + cache = new ValidatorRegistrationCache(); + cache.add(1, validatorRegistration1); + cache.add(3, validatorRegistration2); + }); + + it("get for registered validator", () => { + expect(cache.get(validatorPubkey1)?.gasLimit).toBe(gasLimit1); + }); + + it("get for unknown validator", () => { + const unknownValidatorPubkey = new Uint8Array(48).fill(3); + expect(cache.get(unknownValidatorPubkey)).toBe(undefined); + }); + + it("override and get latest", () => { + const newGasLimit = 60000000; + const registration = ssz.bellatrix.ValidatorRegistrationV1.defaultValue(); + registration.pubkey = validatorPubkey1; + registration.gasLimit = newGasLimit; + + cache.add(5, registration); + + expect(cache.get(validatorPubkey1)?.gasLimit).toBe(newGasLimit); + }); + + it("prune", () => { + cache.prune(4); + + // No registration as it has been pruned + expect(cache.get(validatorPubkey1)).toBe(undefined); + + // Registration hasn't been pruned + expect(cache.get(validatorPubkey2)?.gasLimit).toBe(gasLimit2); + }); +}); diff --git a/packages/beacon-node/test/unit/execution/builder/utils.test.ts b/packages/beacon-node/test/unit/execution/builder/utils.test.ts new file mode 100644 index 000000000000..90520fb00e1f --- /dev/null +++ b/packages/beacon-node/test/unit/execution/builder/utils.test.ts @@ -0,0 +1,66 @@ +import {describe, expect, it} from "vitest"; +import {getExpectedGasLimit} from "../../../../src/execution/builder/utils.js"; + +describe("execution / builder / utils", () => { + describe("getExpectedGasLimit", () => { + const testCases: { + name: string; + parentGasLimit: number; + targetGasLimit: number; + expected: number; + }[] = [ + { + name: "Increase within limit", + parentGasLimit: 30000000, + targetGasLimit: 30000100, + expected: 30000100, + }, + { + name: "Increase exceeding limit", + parentGasLimit: 30000000, + targetGasLimit: 36000000, + expected: 30029295, // maxGasLimitDifference = (30000000 / 1024) - 1 = 29295 + }, + { + name: "Decrease within limit", + parentGasLimit: 30000000, + targetGasLimit: 29999990, + expected: 29999990, + }, + { + name: "Decrease exceeding limit", + parentGasLimit: 36000000, + targetGasLimit: 30000000, + expected: 35964845, // maxGasLimitDifference = (36000000 / 1024) - 1 = 35155 + }, + { + name: "Target equals parent", + parentGasLimit: 30000000, + targetGasLimit: 30000000, + expected: 30000000, // No change + }, + { + name: "Very small parent gas limit", + parentGasLimit: 1025, + targetGasLimit: 2000, + expected: 1025, + }, + { + name: "Target far below parent but limited", + parentGasLimit: 30000000, + targetGasLimit: 10000000, + expected: 29970705, // maxGasLimitDifference = (30000000 / 1024) - 1 = 29295 + }, + { + name: "Parent gas limit underflows", + parentGasLimit: 1023, + targetGasLimit: 30000000, + expected: 1023, + }, + ]; + + it.each(testCases)("$name", ({parentGasLimit, targetGasLimit, expected}) => { + expect(getExpectedGasLimit(parentGasLimit, targetGasLimit)).toBe(expected); + }); + }); +}); diff --git a/packages/beacon-node/test/unit/execution/engine/types.test.ts b/packages/beacon-node/test/unit/execution/engine/types.test.ts new file mode 100644 index 000000000000..6943c7c21a34 --- /dev/null +++ b/packages/beacon-node/test/unit/execution/engine/types.test.ts @@ -0,0 +1,131 @@ +import {CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, WITHDRAWAL_REQUEST_TYPE} from "@lodestar/params"; +import {ExecutionRequests, ssz} from "@lodestar/types"; +import {fromHex, strip0xPrefix} from "@lodestar/utils"; +import {describe, expect, it} from "vitest"; +import {deserializeExecutionRequests, serializeExecutionRequests} from "../../../../src/execution/engine/types.js"; + +describe("execution / engine / types", () => { + describe("serializeExecutionRequests", () => { + it("should serialize execution requests according to EIP-7685", () => { + const executionRequests: ExecutionRequests = { + deposits: [ssz.electra.DepositRequest.defaultValue()], + withdrawals: [ssz.electra.WithdrawalRequest.defaultValue()], + consolidations: [ssz.electra.ConsolidationRequest.defaultValue()], + }; + + const serialized = serializeExecutionRequests(executionRequests).map(strip0xPrefix); + + // Assert 1-byte request_type prefix is set correctly + expect(serialized.length).toBe(3); + expect(Number(serialized[0].substring(0, 2))).toBe(DEPOSIT_REQUEST_TYPE); + expect(Number(serialized[1].substring(0, 2))).toBe(WITHDRAWAL_REQUEST_TYPE); + expect(Number(serialized[2].substring(0, 2))).toBe(CONSOLIDATION_REQUEST_TYPE); + + // Assert execution requests can be deserialized + expect(ssz.electra.DepositRequests.deserialize(fromHex(serialized[0].slice(2)))).toEqual( + executionRequests.deposits + ); + expect(ssz.electra.WithdrawalRequests.deserialize(fromHex(serialized[1].slice(2)))).toEqual( + executionRequests.withdrawals + ); + expect(ssz.electra.ConsolidationRequests.deserialize(fromHex(serialized[2].slice(2)))).toEqual( + executionRequests.consolidations + ); + }); + + it("should omit empty requests when serializing data", () => { + const executionRequests: ExecutionRequests = { + deposits: [ssz.electra.DepositRequest.defaultValue()], + withdrawals: [], + consolidations: [ssz.electra.ConsolidationRequest.defaultValue()], + }; + + const serialized = serializeExecutionRequests(executionRequests).map(strip0xPrefix); + + // Assert withdrawals are omitted + expect(serialized.length).toBe(2); + expect(Number(serialized[0].substring(0, 2))).toBe(DEPOSIT_REQUEST_TYPE); + expect(Number(serialized[1].substring(0, 2))).toBe(CONSOLIDATION_REQUEST_TYPE); + + // Assert execution requests can be deserialized + expect(ssz.electra.DepositRequests.deserialize(fromHex(serialized[0].slice(2)))).toEqual( + executionRequests.deposits + ); + expect(ssz.electra.ConsolidationRequests.deserialize(fromHex(serialized[1].slice(2)))).toEqual( + executionRequests.consolidations + ); + }); + + it("should return an empty array if all requests are empty", () => { + const executionRequests: ExecutionRequests = { + deposits: [], + withdrawals: [], + consolidations: [], + }; + + const serialized = serializeExecutionRequests(executionRequests); + + expect(serialized.length).toBe(0); + }); + }); + + describe("deserializeExecutionRequests", () => { + // From https://github.com/ethereum/execution-apis/blob/f6a6f52bccdb05f8b2f894a56fe1232432069d65/src/engine/openrpc/methods/payload.yaml#L553-L556 + const serializedRequests: string[] = [ + "0x0096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9003f" + + "5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20100000000000000b1acdb2c4d3df3f1b8d3bfd334" + + "21660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d" + + "857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9f000000000000000a5c85a60ba2905c215f6a1" + + "2872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b001db70c485b6264692f26b8aeaa" + + "b5b0c384180df8e2184a21a808a3ec8e86ca01000000000000009561731785b48cf1886412234531e4940064584463e96ac63a" + + "1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4" + + "f9f9f294a8a7c32db895de3b01bee0132c9209e1f100000000000000", + "0x01a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb73" + + "2eabaa47ffa288f0d54de28209a62a7d29d0000000000000000000000000000000000000000000000000000010f698daeed734" + + "da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a75530100000000000000", + "0x02a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb73" + + "2eabaa47ffa288f0d54de28209a62a7d29d098daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc" + + "6005f3672fada2a3645edb297a7553", + ]; + + it("should deserialize execution requests according to EIP-7685", () => { + const executionRequests = deserializeExecutionRequests(serializedRequests); + + expect(executionRequests.deposits.length).toBe(2); + expect(executionRequests.withdrawals.length).toBe(2); + expect(executionRequests.consolidations.length).toBe(1); + + expect(serializeExecutionRequests(executionRequests)).toEqual(serializedRequests); + }); + + it("should correctly deserialize if execution request is omitted", () => { + const serializedOmitted = [serializedRequests[0], serializedRequests[2]]; + + const executionRequests = deserializeExecutionRequests(serializedOmitted); + + expect(executionRequests.deposits.length).toBe(2); + expect(executionRequests.withdrawals.length).toBe(0); + expect(executionRequests.consolidations.length).toBe(1); + + expect(serializeExecutionRequests(executionRequests)).toEqual(serializedOmitted); + }); + + it("should throw an error if execution requests order is incorrect", () => { + const serializedUnordered = [serializedRequests[0], serializedRequests[2], serializedRequests[1]]; + + expect(() => deserializeExecutionRequests(serializedUnordered)).toThrow(); + }); + + it("should throw an error if execution request is missing type prefix", () => { + const serializedNoPrefix = [serializedRequests[0], `0x${serializedRequests[1].slice(4)}`]; + + expect(() => deserializeExecutionRequests(serializedNoPrefix)).toThrow(); + }); + + it("should throw an error if execution request has incorrect prefix", () => { + const serializedWrongPrefix = [serializedRequests[0], `0x05${serializedRequests[1].slice(4)}`]; + + expect(() => deserializeExecutionRequests(serializedWrongPrefix)).toThrow(); + }); + }); +}); diff --git a/packages/beacon-node/test/unit/util/kzg.test.ts b/packages/beacon-node/test/unit/util/kzg.test.ts index 616505268ae1..589e8d28d834 100644 --- a/packages/beacon-node/test/unit/util/kzg.test.ts +++ b/packages/beacon-node/test/unit/util/kzg.test.ts @@ -45,6 +45,7 @@ describe("C-KZG", () => { afterEachCallbacks.push(() => chain.close()); const slot = 0; + const fork = config.getForkName(slot); const blobs = [generateRandomBlob(), generateRandomBlob()]; const kzgCommitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob)); @@ -65,7 +66,7 @@ describe("C-KZG", () => { for (const blobSidecar of blobSidecars) { try { - await validateGossipBlobSidecar(chain, blobSidecar, blobSidecar.index); + await validateGossipBlobSidecar(fork, chain, blobSidecar, blobSidecar.index); } catch (_e) { // We expect some error from here // console.log(error); diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index 8fd6011e6255..6496c9e80619 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -1,28 +1,46 @@ import {BitArray} from "@chainsafe/ssz"; import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; -import {Epoch, RootHex, Slot, deneb, electra, isElectraAttestation, phase0, ssz} from "@lodestar/types"; -import {fromHex, toHex} from "@lodestar/utils"; +import { + CommitteeIndex, + Epoch, + RootHex, + SingleAttestation, + Slot, + ValidatorIndex, + deneb, + electra, + isElectraSingleAttestation, + phase0, + ssz, + sszTypesFor, +} from "@lodestar/types"; +import {fromHex, toHex, toRootHex} from "@lodestar/utils"; import {describe, expect, it} from "vitest"; import { getAggregationBitsFromAttestationSerialized, getAttDataFromAttestationSerialized, getAttDataFromSignedAggregateAndProofElectra, getAttDataFromSignedAggregateAndProofPhase0, + getAttDataFromSingleAttestationSerialized, + getAttesterIndexFromSingleAttestationSerialized, getBlockRootFromAttestationSerialized, getBlockRootFromSignedAggregateAndProofSerialized, - getCommitteeBitsFromAttestationSerialized, + getBlockRootFromSingleAttestationSerialized, getCommitteeBitsFromSignedAggregateAndProofElectra, + getCommitteeIndexFromSingleAttestationSerialized, getSignatureFromAttestationSerialized, + getSignatureFromSingleAttestationSerialized, getSlotFromAttestationSerialized, getSlotFromBlobSidecarSerialized, getSlotFromSignedAggregateAndProofSerialized, getSlotFromSignedBeaconBlockSerialized, + getSlotFromSingleAttestationSerialized, } from "../../../src/util/sszBytes.js"; -describe("attestation SSZ serialized picking", () => { - const testCases: (phase0.Attestation | electra.Attestation)[] = [ +describe("SinlgeAttestation SSZ serialized picking", () => { + const testCases: SingleAttestation[] = [ ssz.phase0.Attestation.defaultValue(), - attestationFromValues( + phase0SingleAttestationFromValues( 4_000_000, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 200_00, @@ -30,46 +48,53 @@ describe("attestation SSZ serialized picking", () => { ), ssz.electra.Attestation.defaultValue(), { - ...attestationFromValues( + ...electraSingleAttestationFromValues( 4_000_000, + 127, + 1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 200_00, "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff" ), - committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, 3), }, ]; for (const [i, attestation] of testCases.entries()) { it(`attestation ${i}`, () => { - const isElectra = isElectraAttestation(attestation); + const isElectra = isElectraSingleAttestation(attestation); const bytes = isElectra - ? ssz.electra.Attestation.serialize(attestation) + ? sszTypesFor(ForkName.electra, "SingleAttestation").serialize(attestation) : ssz.phase0.Attestation.serialize(attestation); - expect(getSlotFromAttestationSerialized(bytes)).toBe(attestation.data.slot); - expect(getBlockRootFromAttestationSerialized(bytes)).toBe(toHex(attestation.data.beaconBlockRoot)); - if (isElectra) { - expect(getAggregationBitsFromAttestationSerialized(ForkName.electra, bytes)?.toBoolArray()).toEqual( - attestation.aggregationBits.toBoolArray() + expect(getSlotFromSingleAttestationSerialized(bytes)).toEqual(attestation.data.slot); + expect(getCommitteeIndexFromSingleAttestationSerialized(ForkName.electra, bytes)).toEqual( + attestation.committeeIndex ); - expect(getCommitteeBitsFromAttestationSerialized(bytes)).toEqual( - Buffer.from(attestation.committeeBits.uint8Array).toString("base64") + expect(getAttesterIndexFromSingleAttestationSerialized(bytes)).toEqual(attestation.attesterIndex); + expect(getBlockRootFromSingleAttestationSerialized(bytes)).toEqual(toRootHex(attestation.data.beaconBlockRoot)); + // base64, not hex + expect(getAttDataFromSingleAttestationSerialized(bytes)).toEqual( + Buffer.from(ssz.phase0.AttestationData.serialize(attestation.data)).toString("base64") ); - expect(getSignatureFromAttestationSerialized(bytes)).toEqual(attestation.signature); + expect(getSignatureFromSingleAttestationSerialized(bytes)).toEqual(attestation.signature); } else { - expect(getAggregationBitsFromAttestationSerialized(ForkName.phase0, bytes)?.toBoolArray()).toEqual( + expect(getSlotFromAttestationSerialized(bytes)).toBe(attestation.data.slot); + expect(getCommitteeIndexFromSingleAttestationSerialized(ForkName.phase0, bytes)).toEqual( + attestation.data.index + ); + expect(getBlockRootFromAttestationSerialized(bytes)).toBe(toRootHex(attestation.data.beaconBlockRoot)); + expect(getAggregationBitsFromAttestationSerialized(bytes)?.toBoolArray()).toEqual( attestation.aggregationBits.toBoolArray() ); + const attDataBase64 = ssz.phase0.AttestationData.serialize(attestation.data); + expect(getAttDataFromAttestationSerialized(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); expect(getSignatureFromAttestationSerialized(bytes)).toEqual(attestation.signature); } - - const attDataBase64 = ssz.phase0.AttestationData.serialize(attestation.data); - expect(getAttDataFromAttestationSerialized(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); }); } + // negative tests for phase0 it("getSlotFromAttestationSerialized - invalid data", () => { const invalidSlotDataSizes = [0, 4, 11]; for (const size of invalidSlotDataSizes) { @@ -94,8 +119,8 @@ describe("attestation SSZ serialized picking", () => { it("getAggregationBitsFromAttestationSerialized - invalid data", () => { const invalidAggregationBitsDataSizes = [0, 4, 100, 128, 227]; for (const size of invalidAggregationBitsDataSizes) { - expect(getAggregationBitsFromAttestationSerialized(ForkName.phase0, Buffer.alloc(size))).toBeNull(); - expect(getAggregationBitsFromAttestationSerialized(ForkName.electra, Buffer.alloc(size))).toBeNull(); + expect(getAggregationBitsFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); + expect(getAggregationBitsFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); } }); @@ -106,6 +131,42 @@ describe("attestation SSZ serialized picking", () => { expect(getSignatureFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); } }); + + // negative tests for electra + it("getSlotFromSingleAttestationSerialized - invalid data", () => { + const invalidSlotDataSizes = [0, 4, 11]; + for (const size of invalidSlotDataSizes) { + expect(getSlotFromSingleAttestationSerialized(Buffer.alloc(size))).toBeNull(); + } + }); + + it("getCommitteeIndexFromSingleAttestationSerialized - invalid data", () => { + const invalidCommitteeIndexDataSizes = [0, 4, 11]; + for (const size of invalidCommitteeIndexDataSizes) { + expect(getCommitteeIndexFromSingleAttestationSerialized(ForkName.electra, Buffer.alloc(size))).toBeNull(); + } + }); + + it("getBlockRootFromSingleAttestationSerialized - invalid data", () => { + const invalidBlockRootDataSizes = [0, 4, 20, 49]; + for (const size of invalidBlockRootDataSizes) { + expect(getBlockRootFromSingleAttestationSerialized(Buffer.alloc(size))).toBeNull(); + } + }); + + it("getAttDataFromSingleAttestationSerialized - invalid data", () => { + const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 131]; + for (const size of invalidAttDataBase64DataSizes) { + expect(getAttDataFromSingleAttestationSerialized(Buffer.alloc(size))).toBeNull(); + } + }); + + it("getSignatureFromSingleAttestationSerialized - invalid data", () => { + const invalidSignatureDataSizes = [0, 4, 100, 128, 227]; + for (const size of invalidSignatureDataSizes) { + expect(getSignatureFromSingleAttestationSerialized(Buffer.alloc(size))).toBeNull(); + } + }); }); describe("phase0 SignedAggregateAndProof SSZ serialized picking", () => { @@ -258,7 +319,7 @@ describe("BlobSidecar SSZ serialized picking", () => { }); }); -function attestationFromValues( +function phase0SingleAttestationFromValues( slot: Slot, blockRoot: RootHex, targetEpoch: Epoch, @@ -272,6 +333,24 @@ function attestationFromValues( return attestation; } +function electraSingleAttestationFromValues( + slot: Slot, + committeeIndex: CommitteeIndex, + attesterIndex: ValidatorIndex, + blockRoot: RootHex, + targetEpoch: Epoch, + targetRoot: RootHex +): electra.SingleAttestation { + const attestation = ssz.electra.SingleAttestation.defaultValue(); + attestation.data.slot = slot; + attestation.data.beaconBlockRoot = fromHex(blockRoot); + attestation.data.target.epoch = targetEpoch; + attestation.data.target.root = fromHex(targetRoot); + attestation.committeeIndex = committeeIndex; + attestation.attesterIndex = attesterIndex; + return attestation; +} + function phase0SignedAggregateAndProofFromValues( slot: Slot, blockRoot: RootHex, diff --git a/packages/cli/package.json b/packages/cli/package.json index aa80e651a5f7..6af183d27e79 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@chainsafe/lodestar", - "version": "1.24.0", + "version": "1.25.0", "description": "Command line interface for lodestar", "author": "ChainSafe Systems", "license": "LGPL-3.0", @@ -56,23 +56,23 @@ "@chainsafe/blst": "^2.1.0", "@chainsafe/discv5": "^10.0.1", "@chainsafe/enr": "^4.0.1", - "@chainsafe/persistent-merkle-tree": "^0.8.0", - "@chainsafe/ssz": "^0.18.0", + "@chainsafe/persistent-merkle-tree": "^1.0.1", + "@chainsafe/ssz": "^1.0.1", "@chainsafe/threads": "^1.11.1", "@libp2p/crypto": "^5.0.4", "@libp2p/interface": "^2.1.2", "@libp2p/peer-id": "^5.0.4", - "@lodestar/api": "^1.24.0", - "@lodestar/beacon-node": "^1.24.0", - "@lodestar/config": "^1.24.0", - "@lodestar/db": "^1.24.0", - "@lodestar/light-client": "^1.24.0", - "@lodestar/logger": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/state-transition": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", - "@lodestar/validator": "^1.24.0", + "@lodestar/api": "^1.25.0", + "@lodestar/beacon-node": "^1.25.0", + "@lodestar/config": "^1.25.0", + "@lodestar/db": "^1.25.0", + "@lodestar/light-client": "^1.25.0", + "@lodestar/logger": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/state-transition": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", + "@lodestar/validator": "^1.25.0", "@multiformats/multiaddr": "^12.1.3", "deepmerge": "^4.3.1", "ethers": "^6.7.0", @@ -88,7 +88,7 @@ "yargs": "^17.7.1" }, "devDependencies": { - "@lodestar/test-utils": "^1.24.0", + "@lodestar/test-utils": "^1.25.0", "@types/debug": "^4.1.7", "@types/got": "^9.6.12", "@types/inquirer": "^9.0.3", diff --git a/packages/cli/src/applyPreset.ts b/packages/cli/src/applyPreset.ts index 06d29d353af8..4b666ecfcfc8 100644 --- a/packages/cli/src/applyPreset.ts +++ b/packages/cli/src/applyPreset.ts @@ -1,7 +1,7 @@ // MUST import this file first before anything and not import any Lodestar code. -import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/as-sha256.js"; -import {setHasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/index.js"; +import {setHasher} from "@chainsafe/persistent-merkle-tree"; +import {hasher} from "@chainsafe/persistent-merkle-tree/hasher/as-sha256"; // without setting this first, persistent-merkle-tree will use noble instead setHasher(hasher); diff --git a/packages/cli/test/utils/crucible/clients/beacon/lighthouse.ts b/packages/cli/test/utils/crucible/clients/beacon/lighthouse.ts index 725c08389105..38329d09a40c 100644 --- a/packages/cli/test/utils/crucible/clients/beacon/lighthouse.ts +++ b/packages/cli/test/utils/crucible/clients/beacon/lighthouse.ts @@ -28,11 +28,8 @@ export const generateLighthouseBeaconNode: BeaconNodeGenerator = { "testnet-dir": rootDirMounted, datadir: rootDirMounted, + // Enable the RESTful HTTP API server. Disabled by default. http: null, - // Enable the RESTful HTTP API server. Disabled by default. - // Forces the HTTP to indicate that the node is synced when sync is actually - // stalled. This is useful for very small testnets. TESTING ONLY. DO NOT USE ON MAINNET. - "http-allow-sync-stalled": null, "http-address": "0.0.0.0", "http-port": ports.beacon.httpPort, "http-allow-origin": "*", @@ -74,7 +71,7 @@ export const generateLighthouseBeaconNode: BeaconNodeGenerator { await writeFile(path.join(rootDir, "config.yaml"), yaml.dump(chainConfigToJson(forkConfig))); - await writeFile(path.join(rootDir, "deploy_block.txt"), "0"); + await writeFile(path.join(rootDir, "deposit_contract_block.txt"), "0"); }, cli: { command: isDocker ? "lighthouse" : (process.env.LIGHTHOUSE_BINARY_PATH as string), diff --git a/packages/cli/test/utils/crucible/clients/validator/lighthouse.ts b/packages/cli/test/utils/crucible/clients/validator/lighthouse.ts index a5f137a861d5..8f5ab64caffb 100644 --- a/packages/cli/test/utils/crucible/clients/validator/lighthouse.ts +++ b/packages/cli/test/utils/crucible/clients/validator/lighthouse.ts @@ -65,7 +65,7 @@ export const generateLighthouseValidatorNode: ValidatorNodeGenerator = { @@ -142,6 +145,9 @@ export const chainConfigTypes: SpecTypes = { BLOB_SIDECAR_SUBNET_COUNT: "number", MAX_BLOBS_PER_BLOCK: "number", MAX_REQUEST_BLOB_SIDECARS: "number", + BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: "number", + MAX_BLOBS_PER_BLOCK_ELECTRA: "number", + MAX_REQUEST_BLOB_SIDECARS_ELECTRA: "number", }; /** Allows values in a Spec file */ diff --git a/packages/config/src/forkConfig/index.ts b/packages/config/src/forkConfig/index.ts index 725e0a3af572..9551627188d5 100644 --- a/packages/config/src/forkConfig/index.ts +++ b/packages/config/src/forkConfig/index.ts @@ -10,6 +10,7 @@ import { isForkBlobs, isForkExecution, isForkLightClient, + isForkPostElectra, } from "@lodestar/params"; import {Epoch, SSZTypesFor, Slot, Version, sszTypesFor} from "@lodestar/types"; import {ChainConfig} from "../chainConfig/index.js"; @@ -129,5 +130,11 @@ export function createForkConfig(config: ChainConfig): ForkConfig { } return sszTypesFor(forkName); }, + getMaxBlobsPerBlock(fork: ForkName): number { + return isForkPostElectra(fork) ? config.MAX_BLOBS_PER_BLOCK_ELECTRA : config.MAX_BLOBS_PER_BLOCK; + }, + getMaxRequestBlobSidecars(fork: ForkName): number { + return isForkPostElectra(fork) ? config.MAX_REQUEST_BLOB_SIDECARS_ELECTRA : config.MAX_REQUEST_BLOB_SIDECARS; + }, }; } diff --git a/packages/config/src/forkConfig/types.ts b/packages/config/src/forkConfig/types.ts index ebb2899a2a21..c7874482770a 100644 --- a/packages/config/src/forkConfig/types.ts +++ b/packages/config/src/forkConfig/types.ts @@ -39,4 +39,8 @@ export type ForkConfig = { getExecutionForkTypes(slot: Slot): SSZTypesFor; /** Get blobs SSZ types by hard-fork*/ getBlobsForkTypes(slot: Slot): SSZTypesFor; + /** Get max blobs per block by hard-fork */ + getMaxBlobsPerBlock(fork: ForkName): number; + /** Get max request blob sidecars by hard-fork */ + getMaxRequestBlobSidecars(fork: ForkName): number; }; diff --git a/packages/db/package.json b/packages/db/package.json index c71fae529350..9f91f7bb936b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/db", - "version": "1.24.0", + "version": "1.25.0", "description": "DB modules of Lodestar", "author": "ChainSafe Systems", "homepage": "https://github.com/ChainSafe/lodestar#readme", @@ -35,13 +35,13 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/ssz": "^0.18.0", - "@lodestar/config": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@chainsafe/ssz": "^1.0.1", + "@lodestar/config": "^1.25.0", + "@lodestar/utils": "^1.25.0", "classic-level": "^1.4.1", "it-all": "^3.0.4" }, "devDependencies": { - "@lodestar/logger": "^1.24.0" + "@lodestar/logger": "^1.25.0" } } diff --git a/packages/flare/package.json b/packages/flare/package.json index a4fa6ca96399..d57a9de7c4b7 100644 --- a/packages/flare/package.json +++ b/packages/flare/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/flare", - "version": "1.24.0", + "version": "1.25.0", "description": "Beacon chain debugging tool", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -60,12 +60,12 @@ "dependencies": { "@chainsafe/bls-keygen": "^0.4.0", "@chainsafe/blst": "^2.1.0", - "@lodestar/api": "^1.24.0", - "@lodestar/config": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/state-transition": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@lodestar/api": "^1.25.0", + "@lodestar/config": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/state-transition": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", "source-map-support": "^0.5.21", "yargs": "^17.7.1" }, diff --git a/packages/fork-choice/package.json b/packages/fork-choice/package.json index a3d0470b21d2..8264926c6fc8 100644 --- a/packages/fork-choice/package.json +++ b/packages/fork-choice/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": "./lib/index.js", "types": "./lib/index.d.ts", @@ -36,12 +36,12 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/ssz": "^0.18.0", - "@lodestar/config": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/state-transition": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0" + "@chainsafe/ssz": "^1.0.1", + "@lodestar/config": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/state-transition": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0" }, "keywords": [ "ethereum", diff --git a/packages/light-client/package.json b/packages/light-client/package.json index 2ceac4f703bb..410a5855360f 100644 --- a/packages/light-client/package.json +++ b/packages/light-client/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -75,17 +75,17 @@ "dependencies": { "@chainsafe/bls": "7.1.3", "@chainsafe/blst": "^0.2.0", - "@chainsafe/persistent-merkle-tree": "^0.8.0", - "@chainsafe/ssz": "^0.18.0", - "@lodestar/api": "^1.24.0", - "@lodestar/config": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@chainsafe/persistent-merkle-tree": "^1.0.1", + "@chainsafe/ssz": "^1.0.1", + "@lodestar/api": "^1.25.0", + "@lodestar/config": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", "mitt": "^3.0.0" }, "devDependencies": { - "@chainsafe/as-sha256": "^0.5.0", + "@chainsafe/as-sha256": "^1.0.0", "@types/qs": "^6.9.7", "fastify": "^5.0.0", "qs": "^6.11.1", diff --git a/packages/logger/package.json b/packages/logger/package.json index badf74add4a6..a0c4ed936d3e 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -66,14 +66,14 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@lodestar/utils": "^1.24.0", + "@lodestar/utils": "^1.25.0", "winston": "^3.8.2", "winston-daily-rotate-file": "^4.7.1", "winston-transport": "^4.5.0" }, "devDependencies": { "@chainsafe/threads": "^1.11.1", - "@lodestar/test-utils": "^1.24.0", + "@lodestar/test-utils": "^1.25.0", "@types/triple-beam": "^1.3.2", "triple-beam": "^1.3.0" }, diff --git a/packages/params/package.json b/packages/params/package.json index 7089fef3a13c..8ec18b62efc9 100644 --- a/packages/params/package.json +++ b/packages/params/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/params", - "version": "1.24.0", + "version": "1.25.0", "description": "Chain parameters required for lodestar", "author": "ChainSafe Systems", "license": "Apache-2.0", diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 64d3b64dbd60..838624e29f23 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -255,11 +255,11 @@ export const BLOB_TX_TYPE = 0x03; export const VERSIONED_HASH_VERSION_KZG = 0x01; // ssz.deneb.BeaconBlockBody.getPathInfo(['blobKzgCommitments',0]).gindex -export const KZG_COMMITMENT_GINDEX0 = ACTIVE_PRESET === PresetName.minimal ? 864 : 221184; +export const KZG_COMMITMENT_GINDEX0 = ACTIVE_PRESET === PresetName.minimal ? 1728 : 221184; export const KZG_COMMITMENT_SUBTREE_INDEX0 = KZG_COMMITMENT_GINDEX0 - 2 ** KZG_COMMITMENT_INCLUSION_PROOF_DEPTH; // ssz.deneb.BlobSidecars.elementType.fixedSize -export const BLOBSIDECAR_FIXED_SIZE = ACTIVE_PRESET === PresetName.minimal ? 131672 : 131928; +export const BLOBSIDECAR_FIXED_SIZE = ACTIVE_PRESET === PresetName.minimal ? 131704 : 131928; // Electra Misc export const UNSET_DEPOSIT_REQUESTS_START_INDEX = 2n ** 64n - 1n; @@ -270,3 +270,6 @@ export const FINALIZED_ROOT_INDEX_ELECTRA = 41; export const NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA = 87; export const NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6; export const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA = 23; +export const DEPOSIT_REQUEST_TYPE = 0x00; +export const WITHDRAWAL_REQUEST_TYPE = 0x01; +export const CONSOLIDATION_REQUEST_TYPE = 0x02; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index afbfd78eba95..b4136f83da29 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -131,6 +131,6 @@ export const mainnetPreset: BeaconPreset = { PENDING_DEPOSITS_LIMIT: 134217728, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728, PENDING_CONSOLIDATIONS_LIMIT: 262144, - MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2, WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index d9be1b1468ab..4595ee9ee879 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -115,8 +115,8 @@ export const minimalPreset: BeaconPreset = { // DENEB /////////// FIELD_ELEMENTS_PER_BLOB: 4096, - MAX_BLOB_COMMITMENTS_PER_BLOCK: 16, - KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 9, + MAX_BLOB_COMMITMENTS_PER_BLOCK: 32, + KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 10, // ELECTRA MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 4, @@ -132,6 +132,6 @@ export const minimalPreset: BeaconPreset = { PENDING_DEPOSITS_LIMIT: 134217728, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64, PENDING_CONSOLIDATIONS_LIMIT: 64, - MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2, WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096, }; diff --git a/packages/params/test/e2e/ensure-config-is-synced.test.ts b/packages/params/test/e2e/ensure-config-is-synced.test.ts index b1d9c05a54b1..22224e6421c4 100644 --- a/packages/params/test/e2e/ensure-config-is-synced.test.ts +++ b/packages/params/test/e2e/ensure-config-is-synced.test.ts @@ -8,7 +8,7 @@ import {loadConfigYaml} from "../yaml.js"; // Not e2e, but slow. Run with e2e tests /** https://github.com/ethereum/consensus-specs/releases */ -const specConfigCommit = "v1.5.0-alpha.8"; +const specConfigCommit = "v1.5.0-beta.0"; /** * Fields that we filter from local config when doing comparison. * Ideally this should be empty as it is not spec compliant diff --git a/packages/prover/package.json b/packages/prover/package.json index 7df21de669d8..58563612ea9c 100644 --- a/packages/prover/package.json +++ b/packages/prover/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -69,13 +69,13 @@ "@ethereumjs/tx": "^4.1.2", "@ethereumjs/util": "^8.0.6", "@ethereumjs/vm": "^6.4.2", - "@lodestar/api": "^1.24.0", - "@lodestar/config": "^1.24.0", - "@lodestar/light-client": "^1.24.0", - "@lodestar/logger": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@lodestar/api": "^1.25.0", + "@lodestar/config": "^1.25.0", + "@lodestar/light-client": "^1.25.0", + "@lodestar/logger": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", "ethereum-cryptography": "^2.0.0", "find-up": "^6.3.0", "http-proxy": "^1.18.1", @@ -84,7 +84,7 @@ "yargs": "^17.7.1" }, "devDependencies": { - "@lodestar/test-utils": "^1.24.0", + "@lodestar/test-utils": "^1.25.0", "@types/http-proxy": "^1.17.10", "@types/yargs": "^17.0.24", "axios": "^1.3.4", diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index 75ab3608ac7f..de400108d4e8 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -54,9 +54,9 @@ "dependencies": { "@chainsafe/fast-crc32c": "^4.1.1", "@libp2p/interface": "^2.1.2", - "@lodestar/config": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@lodestar/config": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/utils": "^1.25.0", "it-all": "^3.0.4", "it-pipe": "^3.0.1", "snappy": "^7.2.2", @@ -65,8 +65,8 @@ "uint8arraylist": "^2.4.7" }, "devDependencies": { - "@lodestar/logger": "^1.24.0", - "@lodestar/types": "^1.24.0", + "@lodestar/logger": "^1.25.0", + "@lodestar/types": "^1.25.0", "libp2p": "2.1.7" }, "peerDependencies": { diff --git a/packages/spec-test-util/package.json b/packages/spec-test-util/package.json index 5ed1a6cdf833..d556e09707ce 100644 --- a/packages/spec-test-util/package.json +++ b/packages/spec-test-util/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/spec-test-util", - "version": "1.24.0", + "version": "1.25.0", "description": "Spec test suite generator from yaml test files", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "blockchain" ], "dependencies": { - "@lodestar/utils": "^1.24.0", + "@lodestar/utils": "^1.25.0", "axios": "^1.3.4", "rimraf": "^4.4.1", "snappyjs": "^0.7.0", diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 5caf4d4ff3db..ab5782d29217 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -58,17 +58,17 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/as-sha256": "^0.5.0", + "@chainsafe/as-sha256": "^1.0.0", "@chainsafe/blst": "^2.1.0", - "@chainsafe/persistent-merkle-tree": "^0.8.0", - "@chainsafe/persistent-ts": "^0.19.1", + "@chainsafe/persistent-merkle-tree": "^1.0.1", + "@chainsafe/persistent-ts": "^1.0.0", "@chainsafe/pubkey-index-map": "2.0.0", - "@chainsafe/ssz": "^0.18.0", + "@chainsafe/ssz": "^1.0.1", "@chainsafe/swap-or-not-shuffle": "^0.0.2", - "@lodestar/config": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@lodestar/config": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", "bigint-buffer": "^1.1.5" }, "keywords": [ diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index ab57bd27c80d..3a44d11e7afc 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -100,15 +100,30 @@ export function validateAttestation(fork: ForkSeq, state: CachedBeaconStateAllFo ); } - // Get total number of attestation participant of every committee specified - const participantCount = committeeIndices - .map((committeeIndex) => epochCtx.getBeaconCommittee(data.slot, committeeIndex).length) - .reduce((acc, committeeSize) => acc + committeeSize, 0); + const validatorsByCommittee = epochCtx.getBeaconCommittees(data.slot, committeeIndices); + const aggregationBitsArray = attestationElectra.aggregationBits.toBoolArray(); + + // Total number of attestation participants of every committee specified + let committeeOffset = 0; + for (const committeeValidators of validatorsByCommittee) { + const committeeAggregationBits = aggregationBitsArray.slice( + committeeOffset, + committeeOffset + committeeValidators.length + ); + + // Assert aggregation bits in this committee have at least one true bit + if (committeeAggregationBits.every((bit) => !bit)) { + throw new Error("Every committee in aggregation bits must have at least one attester"); + } + + committeeOffset += committeeValidators.length; + } + // Bitfield length matches total number of participants assert.equal( attestationElectra.aggregationBits.bitLen, - participantCount, - `Attestation aggregation bits length does not match total number of committee participant aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${participantCount}` + committeeOffset, + `Attestation aggregation bits length does not match total number of committee participants aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${committeeOffset}` ); } else { if (!(data.index < committeeCount)) { diff --git a/packages/state-transition/src/block/processConsolidationRequest.ts b/packages/state-transition/src/block/processConsolidationRequest.ts index 1a1d83eee0be..8d3cfdc062c9 100644 --- a/packages/state-transition/src/block/processConsolidationRequest.ts +++ b/packages/state-transition/src/block/processConsolidationRequest.ts @@ -3,9 +3,9 @@ import {electra, ssz} from "@lodestar/types"; import {CachedBeaconStateElectra} from "../types.js"; import {hasEth1WithdrawalCredential} from "../util/capella.js"; -import {hasExecutionWithdrawalCredential, isPubkeyKnown, switchToCompoundingValidator} from "../util/electra.js"; +import {hasCompoundingWithdrawalCredential, isPubkeyKnown, switchToCompoundingValidator} from "../util/electra.js"; import {computeConsolidationEpochAndUpdateChurn} from "../util/epoch.js"; -import {getConsolidationChurnLimit, isActiveValidator} from "../util/validator.js"; +import {getConsolidationChurnLimit, getPendingBalanceToWithdraw, isActiveValidator} from "../util/validator.js"; // TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest export function processConsolidationRequest( @@ -49,11 +49,8 @@ export function processConsolidationRequest( const sourceWithdrawalAddress = sourceValidator.withdrawalCredentials.subarray(12); const currentEpoch = state.epochCtx.epoch; - // Verify withdrawal credentials - if ( - !hasExecutionWithdrawalCredential(sourceValidator.withdrawalCredentials) || - !hasExecutionWithdrawalCredential(targetValidator.withdrawalCredentials) - ) { + // Verify that target has compounding withdrawal credentials + if (!hasCompoundingWithdrawalCredential(targetValidator.withdrawalCredentials)) { return; } @@ -71,6 +68,16 @@ export function processConsolidationRequest( return; } + // Verify the source has been active long enough + if (currentEpoch < sourceValidator.activationEpoch + state.config.SHARD_COMMITTEE_PERIOD) { + return; + } + + // Verify the source has no pending withdrawals in the queue + if (getPendingBalanceToWithdraw(state, sourceIndex) > 0) { + return; + } + // TODO Electra: See if we can get rid of big int const exitEpoch = computeConsolidationEpochAndUpdateChurn(state, BigInt(sourceValidator.effectiveBalance)); sourceValidator.exitEpoch = exitEpoch; @@ -81,11 +88,6 @@ export function processConsolidationRequest( targetIndex, }); state.pendingConsolidations.push(pendingConsolidation); - - // Churn any target excess active balance of target and raise its max - if (hasEth1WithdrawalCredential(targetValidator.withdrawalCredentials)) { - switchToCompoundingValidator(state, targetIndex); - } } /** diff --git a/packages/state-transition/src/block/processExecutionPayload.ts b/packages/state-transition/src/block/processExecutionPayload.ts index 0ea2fc7a16f7..ddc24e884d98 100644 --- a/packages/state-transition/src/block/processExecutionPayload.ts +++ b/packages/state-transition/src/block/processExecutionPayload.ts @@ -1,5 +1,5 @@ import {byteArrayEquals} from "@chainsafe/ssz"; -import {ForkSeq} from "@lodestar/params"; +import {ForkName, ForkSeq, isForkBlobs} from "@lodestar/params"; import {BeaconBlockBody, BlindedBeaconBlockBody, deneb, isExecutionPayload} from "@lodestar/types"; import {toHex, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js"; @@ -18,6 +18,7 @@ export function processExecutionPayload( externalData: Omit ): void { const payload = getFullOrBlindedPayloadFromBody(body); + const forkName = ForkName[ForkSeq[fork] as ForkName]; // Verify consistency of the parent hash, block number, base fee per gas and gas limit // with respect to the previous execution payload header if (isMergeTransitionComplete(state)) { @@ -47,10 +48,11 @@ export function processExecutionPayload( throw Error(`Invalid timestamp ${payload.timestamp} genesisTime=${state.genesisTime} slot=${state.slot}`); } - if (fork >= ForkSeq.deneb) { + if (isForkBlobs(forkName)) { + const maxBlobsPerBlock = state.config.getMaxBlobsPerBlock(forkName); const blobKzgCommitmentsLen = (body as deneb.BeaconBlockBody).blobKzgCommitments?.length ?? 0; - if (blobKzgCommitmentsLen > state.config.MAX_BLOBS_PER_BLOCK) { - throw Error(`blobKzgCommitmentsLen exceeds limit=${state.config.MAX_BLOBS_PER_BLOCK}`); + if (blobKzgCommitmentsLen > maxBlobsPerBlock) { + throw Error(`blobKzgCommitmentsLen exceeds limit=${maxBlobsPerBlock}`); } } diff --git a/packages/state-transition/src/block/processVoluntaryExit.ts b/packages/state-transition/src/block/processVoluntaryExit.ts index a0a0271a0d1e..3cc6afb958f8 100644 --- a/packages/state-transition/src/block/processVoluntaryExit.ts +++ b/packages/state-transition/src/block/processVoluntaryExit.ts @@ -1,5 +1,5 @@ import {FAR_FUTURE_EPOCH, ForkSeq} from "@lodestar/params"; -import {phase0} from "@lodestar/types"; +import {ValidatorIndex, phase0} from "@lodestar/types"; import {verifyVoluntaryExitSignature} from "../signatureSets/index.js"; import {CachedBeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/index.js"; @@ -16,11 +16,7 @@ export function processVoluntaryExit( signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature = true ): void { - const isValidExit = - fork >= ForkSeq.electra - ? isValidVoluntaryExitElectra(state as CachedBeaconStateElectra, signedVoluntaryExit, verifySignature) - : isValidVoluntaryExit(state, signedVoluntaryExit, verifySignature); - if (!isValidExit) { + if (!isValidVoluntaryExit(fork, state, signedVoluntaryExit, verifySignature)) { throw Error(`Invalid voluntary exit at forkSeq=${fork}`); } @@ -29,6 +25,7 @@ export function processVoluntaryExit( } export function isValidVoluntaryExit( + fork: ForkSeq, state: CachedBeaconStateAllForks, signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature = true @@ -47,20 +44,12 @@ export function isValidVoluntaryExit( currentEpoch >= voluntaryExit.epoch && // verify the validator had been active long enough currentEpoch >= validator.activationEpoch + config.SHARD_COMMITTEE_PERIOD && + (fork >= ForkSeq.electra + ? // only exit validator if it has no pending withdrawals in the queue + getPendingBalanceToWithdraw(state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) === 0 + : // there are no pending withdrawals in previous forks + true) && // verify signature (!verifySignature || verifyVoluntaryExitSignature(state, signedVoluntaryExit)) ); } - -function isValidVoluntaryExitElectra( - state: CachedBeaconStateElectra, - signedVoluntaryExit: phase0.SignedVoluntaryExit, - verifySignature = true -): boolean { - // only exit validator if it has no pending withdrawals in the queue (post-Electra only) - if (getPendingBalanceToWithdraw(state, signedVoluntaryExit.message.validatorIndex) === 0) { - return isValidVoluntaryExit(state, signedVoluntaryExit, verifySignature); - } - - return false; -} diff --git a/packages/state-transition/src/block/processWithdrawalRequest.ts b/packages/state-transition/src/block/processWithdrawalRequest.ts index 573c0a49dfc8..7dab10dbb32b 100644 --- a/packages/state-transition/src/block/processWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processWithdrawalRequest.ts @@ -71,7 +71,7 @@ export function processWithdrawalRequest( const withdrawableEpoch = exitQueueEpoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; const pendingPartialWithdrawal = ssz.electra.PendingPartialWithdrawal.toViewDU({ - index: validatorIndex, + validatorIndex, amount: amountToWithdraw, withdrawableEpoch, }); diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index ab1df570eb30..7149bc3c2697 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -8,7 +8,7 @@ import { MAX_WITHDRAWALS_PER_PAYLOAD, MIN_ACTIVATION_BALANCE, } from "@lodestar/params"; -import {capella, ssz} from "@lodestar/types"; +import {ValidatorIndex, capella, ssz} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js"; @@ -25,9 +25,8 @@ export function processWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra, payload: capella.FullOrBlindedExecutionPayload ): void { - // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - // TODO - electra: may switch to executionWithdrawalsCount - const {withdrawals: expectedWithdrawals, partialWithdrawalsCount} = getExpectedWithdrawals(fork, state); + // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) + const {withdrawals: expectedWithdrawals, processedPartialWithdrawalsCount} = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; if (isCapellaPayloadHeader(payload)) { @@ -59,7 +58,9 @@ export function processWithdrawals( if (fork >= ForkSeq.electra) { const stateElectra = state as CachedBeaconStateElectra; - stateElectra.pendingPartialWithdrawals = stateElectra.pendingPartialWithdrawals.sliceFrom(partialWithdrawalsCount); + stateElectra.pendingPartialWithdrawals = stateElectra.pendingPartialWithdrawals.sliceFrom( + processedPartialWithdrawalsCount + ); } // Update the nextWithdrawalIndex @@ -87,7 +88,7 @@ export function getExpectedWithdrawals( ): { withdrawals: capella.Withdrawal[]; sampledValidators: number; - partialWithdrawalsCount: number; + processedPartialWithdrawalsCount: number; } { if (fork < ForkSeq.capella) { throw new Error(`getExpectedWithdrawals not supported at forkSeq=${fork} < ForkSeq.capella`); @@ -100,7 +101,7 @@ export function getExpectedWithdrawals( const withdrawals: capella.Withdrawal[] = []; const isPostElectra = fork >= ForkSeq.electra; // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - let partialWithdrawalsCount = 0; + let processedPartialWithdrawalsCount = 0; if (isPostElectra) { const stateElectra = state as CachedBeaconStateElectra; @@ -122,25 +123,27 @@ export function getExpectedWithdrawals( break; } - const validator = validators.getReadonly(withdrawal.index); + const validator = validators.getReadonly(withdrawal.validatorIndex); if ( validator.exitEpoch === FAR_FUTURE_EPOCH && validator.effectiveBalance >= MIN_ACTIVATION_BALANCE && - balances.get(withdrawal.index) > MIN_ACTIVATION_BALANCE + balances.get(withdrawal.validatorIndex) > MIN_ACTIVATION_BALANCE ) { - const balanceOverMinActivationBalance = BigInt(balances.get(withdrawal.index) - MIN_ACTIVATION_BALANCE); + const balanceOverMinActivationBalance = BigInt( + balances.get(withdrawal.validatorIndex) - MIN_ACTIVATION_BALANCE + ); const withdrawableBalance = balanceOverMinActivationBalance < withdrawal.amount ? balanceOverMinActivationBalance : withdrawal.amount; withdrawals.push({ index: withdrawalIndex, - validatorIndex: withdrawal.index, + validatorIndex: withdrawal.validatorIndex, address: validator.withdrawalCredentials.subarray(12), amount: withdrawableBalance, }); withdrawalIndex++; } - partialWithdrawalsCount++; + processedPartialWithdrawalsCount++; } } @@ -153,7 +156,9 @@ export function getExpectedWithdrawals( const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length; const validator = validators.getReadonly(validatorIndex); - const balance = balances.get(validatorIndex); + const balance = isPostElectra + ? balances.get(validatorIndex) - getPartiallyWithdrawnBalance(withdrawals, validatorIndex) + : balances.get(validatorIndex); const {withdrawableEpoch, withdrawalCredentials, effectiveBalance} = validator; const hasWithdrawableCredentials = isPostElectra ? hasExecutionWithdrawalCredential(withdrawalCredentials) @@ -193,5 +198,15 @@ export function getExpectedWithdrawals( } } - return {withdrawals, sampledValidators: n, partialWithdrawalsCount}; + return {withdrawals, sampledValidators: n, processedPartialWithdrawalsCount}; +} + +function getPartiallyWithdrawnBalance(withdrawals: capella.Withdrawal[], validatorIndex: ValidatorIndex): number { + let total = BigInt(0); + for (const withdrawal of withdrawals) { + if (withdrawal.validatorIndex === validatorIndex) { + total += withdrawal.amount; + } + } + return Number(total); } diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 4714d6169204..2c1f0c41da6d 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -749,13 +749,13 @@ export class EpochCache { * Return the beacon committee at slot for index. */ getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array { - return this.getBeaconCommittees(slot, [index]); + return this.getBeaconCommittees(slot, [index])[0]; } /** - * Return a single Uint32Array representing concatted committees of indices + * Return a Uint32Array[] representing committees of indices */ - getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array { + getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array[] { if (indices.length === 0) { throw new Error("Attempt to get committees without providing CommitteeIndex"); } @@ -774,22 +774,7 @@ export class EpochCache { committees.push(slotCommittees[index]); } - // Early return if only one index - if (committees.length === 1) { - return committees[0]; - } - - // Create a new Uint32Array to flatten `committees` - const totalLength = committees.reduce((acc, curr) => acc + curr.length, 0); - const result = new Uint32Array(totalLength); - - let offset = 0; - for (const committee of committees) { - result.set(committee, offset); - offset += committee.length; - } - - return result; + return committees; } getCommitteeCountPerSlot(epoch: Epoch): number { @@ -912,9 +897,19 @@ export class EpochCache { // TODO Electra: resolve the naming conflicts const committeeIndices = committeeBits.getTrueBitIndexes(); - const validatorIndices = this.getBeaconCommittees(data.slot, committeeIndices); + const validatorsByCommittee = this.getBeaconCommittees(data.slot, committeeIndices); + + // Create a new Uint32Array to flatten `validatorsByCommittee` + const totalLength = validatorsByCommittee.reduce((acc, curr) => acc + curr.length, 0); + const committeeValidators = new Uint32Array(totalLength); + + let offset = 0; + for (const committee of validatorsByCommittee) { + committeeValidators.set(committee, offset); + offset += committee.length; + } - return aggregationBits.intersectValues(validatorIndices); + return aggregationBits.intersectValues(committeeValidators); } getCommitteeAssignments( diff --git a/packages/state-transition/src/epoch/processPendingConsolidations.ts b/packages/state-transition/src/epoch/processPendingConsolidations.ts index 0ec39409f8a7..f9afc7fb122b 100644 --- a/packages/state-transition/src/epoch/processPendingConsolidations.ts +++ b/packages/state-transition/src/epoch/processPendingConsolidations.ts @@ -34,8 +34,10 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca break; } // Move active balance to target. Excess balance is withdrawable. - const maxEffectiveBalance = getMaxEffectiveBalance(state.validators.getReadonly(sourceIndex).withdrawalCredentials); - const sourceEffectiveBalance = Math.min(state.balances.get(sourceIndex), maxEffectiveBalance); + const sourceEffectiveBalance = Math.min( + state.balances.get(sourceIndex), + state.validators.getReadonly(sourceIndex).effectiveBalance + ); decreaseBalance(state, sourceIndex, sourceEffectiveBalance); increaseBalance(state, targetIndex, sourceEffectiveBalance); if (cachedBalances) { diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index f030f9d572fe..3aae0f5b487e 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -56,7 +56,8 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache stateElectraView.exitBalanceToConsume = BigInt(0); const validatorsArr = stateElectraView.validators.getAllReadonly(); - const exitEpochs: Epoch[] = []; + const currentEpochPre = stateDeneb.epochCtx.epoch; + let earliestExitEpoch = computeActivationExitEpoch(currentEpochPre); // [EIP-7251]: add validators that are not yet active to pending balance deposits const preActivation: ValidatorIndex[] = []; @@ -65,17 +66,12 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache if (activationEpoch === FAR_FUTURE_EPOCH) { preActivation.push(validatorIndex); } - if (exitEpoch !== FAR_FUTURE_EPOCH) { - exitEpochs.push(exitEpoch); + if (exitEpoch !== FAR_FUTURE_EPOCH && exitEpoch > earliestExitEpoch) { + earliestExitEpoch = exitEpoch; } } - const currentEpochPre = stateDeneb.epochCtx.epoch; - - if (exitEpochs.length === 0) { - exitEpochs.push(currentEpochPre); - } - stateElectraView.earliestExitEpoch = Math.max(...exitEpochs) + 1; + stateElectraView.earliestExitEpoch = earliestExitEpoch + 1; stateElectraView.consolidationBalanceToConsume = BigInt(0); stateElectraView.earliestConsolidationEpoch = computeActivationExitEpoch(currentEpochPre); // TODO-electra: can we improve this? diff --git a/packages/state-transition/src/util/seed.ts b/packages/state-transition/src/util/seed.ts index 129cf6bfaf72..70604ac21fcc 100644 --- a/packages/state-transition/src/util/seed.ts +++ b/packages/state-transition/src/util/seed.ts @@ -12,7 +12,7 @@ import { SYNC_COMMITTEE_SIZE, } from "@lodestar/params"; import {Bytes32, DomainType, Epoch, ValidatorIndex} from "@lodestar/types"; -import {assert, bytesToBigInt, intToBytes} from "@lodestar/utils"; +import {assert, bytesToBigInt, bytesToInt, intToBytes} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks} from "../types.js"; import {computeStartSlotAtEpoch} from "./epoch.js"; @@ -57,30 +57,40 @@ export function computeProposerIndex( throw Error("Validator indices must not be empty"); } - // TODO: Inline outside this function - const MAX_RANDOM_BYTE = 2 ** 8 - 1; - const MAX_EFFECTIVE_BALANCE_INCREMENT = - fork >= ForkSeq.electra - ? MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT - : MAX_EFFECTIVE_BALANCE / EFFECTIVE_BALANCE_INCREMENT; - - let i = 0; - /* eslint-disable-next-line no-constant-condition */ - while (true) { - const candidateIndex = indices[computeShuffledIndex(i % indices.length, indices.length, seed)]; - const randByte = digest( - Buffer.concat([ - seed, - // - intToBytes(Math.floor(i / 32), 8, "le"), - ]) - )[i % 32]; - - const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; - if (effectiveBalanceIncrement * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randByte) { - return candidateIndex; + if (fork >= ForkSeq.electra) { + const MAX_RANDOM_VALUE = 2 ** 16 - 1; + const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT; + + let i = 0; + while (true) { + const candidateIndex = indices[computeShuffledIndex(i % indices.length, indices.length, seed)]; + const randomBytes = digest(Buffer.concat([seed, intToBytes(Math.floor(i / 16), 8, "le")])); + const offset = (i % 16) * 2; + const randomValue = bytesToInt(randomBytes.subarray(offset, offset + 2)); + + const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; + if (effectiveBalanceIncrement * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomValue) { + return candidateIndex; + } + + i += 1; + } + } else { + const MAX_RANDOM_BYTE = 2 ** 8 - 1; + const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE / EFFECTIVE_BALANCE_INCREMENT; + + let i = 0; + while (true) { + const candidateIndex = indices[computeShuffledIndex(i % indices.length, indices.length, seed)]; + const randomByte = digest(Buffer.concat([seed, intToBytes(Math.floor(i / 32), 8, "le")]))[i % 32]; + + const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; + if (effectiveBalanceIncrement * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomByte) { + return candidateIndex; + } + + i += 1; } - i += 1; } } @@ -100,37 +110,54 @@ export function getNextSyncCommitteeIndices( activeValidatorIndices: ArrayLike, effectiveBalanceIncrements: EffectiveBalanceIncrements ): ValidatorIndex[] { - // TODO: Bechmark if it's necessary to inline outside of this function - const MAX_RANDOM_BYTE = 2 ** 8 - 1; - const MAX_EFFECTIVE_BALANCE_INCREMENT = - fork >= ForkSeq.electra - ? MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT - : MAX_EFFECTIVE_BALANCE / EFFECTIVE_BALANCE_INCREMENT; - - const epoch = computeEpochAtSlot(state.slot) + 1; - - const activeValidatorCount = activeValidatorIndices.length; - const seed = getSeed(state, epoch, DOMAIN_SYNC_COMMITTEE); - let i = 0; const syncCommitteeIndices = []; - while (syncCommitteeIndices.length < SYNC_COMMITTEE_SIZE) { - const shuffledIndex = computeShuffledIndex(i % activeValidatorCount, activeValidatorCount, seed); - const candidateIndex = activeValidatorIndices[shuffledIndex]; - const randByte = digest( - Buffer.concat([ - seed, - // - intToBytes(Math.floor(i / 32), 8, "le"), - ]) - )[i % 32]; - - const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; - if (effectiveBalanceIncrement * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randByte) { - syncCommitteeIndices.push(candidateIndex); + + if (fork >= ForkSeq.electra) { + const MAX_RANDOM_VALUE = 2 ** 16 - 1; + const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT; + + const epoch = computeEpochAtSlot(state.slot) + 1; + const activeValidatorCount = activeValidatorIndices.length; + const seed = getSeed(state, epoch, DOMAIN_SYNC_COMMITTEE); + + let i = 0; + while (syncCommitteeIndices.length < SYNC_COMMITTEE_SIZE) { + const shuffledIndex = computeShuffledIndex(i % activeValidatorCount, activeValidatorCount, seed); + const candidateIndex = activeValidatorIndices[shuffledIndex]; + const randomBytes = digest(Buffer.concat([seed, intToBytes(Math.floor(i / 16), 8, "le")])); + const offset = (i % 16) * 2; + const randomValue = bytesToInt(randomBytes.subarray(offset, offset + 2)); + + const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; + if (effectiveBalanceIncrement * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomValue) { + syncCommitteeIndices.push(candidateIndex); + } + + i += 1; } + } else { + const MAX_RANDOM_BYTE = 2 ** 8 - 1; + const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE / EFFECTIVE_BALANCE_INCREMENT; + + const epoch = computeEpochAtSlot(state.slot) + 1; + const activeValidatorCount = activeValidatorIndices.length; + const seed = getSeed(state, epoch, DOMAIN_SYNC_COMMITTEE); - i++; + let i = 0; + while (syncCommitteeIndices.length < SYNC_COMMITTEE_SIZE) { + const shuffledIndex = computeShuffledIndex(i % activeValidatorCount, activeValidatorCount, seed); + const candidateIndex = activeValidatorIndices[shuffledIndex]; + const randomByte = digest(Buffer.concat([seed, intToBytes(Math.floor(i / 32), 8, "le")]))[i % 32]; + + const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; + if (effectiveBalanceIncrement * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomByte) { + syncCommitteeIndices.push(candidateIndex); + } + + i += 1; + } } + return syncCommitteeIndices; } diff --git a/packages/state-transition/src/util/validator.ts b/packages/state-transition/src/util/validator.ts index 555b8a09b614..732a98802c70 100644 --- a/packages/state-transition/src/util/validator.ts +++ b/packages/state-transition/src/util/validator.ts @@ -83,8 +83,12 @@ export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): numbe } export function getPendingBalanceToWithdraw(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number { - return state.pendingPartialWithdrawals - .getAllReadonly() - .filter((item) => item.index === validatorIndex) - .reduce((total, item) => total + Number(item.amount), 0); + let total = 0; + for (let i = 0; i < state.pendingPartialWithdrawals.length; i++) { + const item = state.pendingPartialWithdrawals.get(i); + if (item.validatorIndex === validatorIndex) { + total += Number(item.amount); + } + } + return total; } diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index a5588dddfd93..bc8a8d1c282d 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@lodestar/test-utils", "private": true, - "version": "1.24.0", + "version": "1.25.0", "description": "Test utilities reused across other packages", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -59,8 +59,8 @@ "dependencies": { "@chainsafe/bls-keystore": "^3.1.0", "@chainsafe/blst": "^2.1.0", - "@lodestar/params": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@lodestar/params": "^1.25.0", + "@lodestar/utils": "^1.25.0", "axios": "^1.3.4", "testcontainers": "^10.2.1", "tmp": "^0.2.1", diff --git a/packages/types/package.json b/packages/types/package.json index 949755d62a97..b97b7d87a61b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": { ".": { @@ -73,8 +73,8 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/ssz": "^0.18.0", - "@lodestar/params": "^1.24.0", + "@chainsafe/ssz": "^1.0.1", + "@lodestar/params": "^1.25.0", "ethereum-cryptography": "^2.0.0" }, "keywords": [ diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index f6b6c745803e..081853b26ade 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -44,6 +44,7 @@ const { UintBn64, ExecutionAddress, ValidatorIndex, + CommitteeIndex, } = primitiveSsz; export const AggregationBits = new BitListType(MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT); @@ -67,6 +68,17 @@ export const Attestation = new ContainerType( {typeName: "Attestation", jsonCase: "eth2"} ); +// New type in ELECTRA +export const SingleAttestation = new ContainerType( + { + committeeIndex: CommitteeIndex, + attesterIndex: ValidatorIndex, + data: phase0Ssz.AttestationData, + signature: BLSSignature, + }, + {typeName: "SingleAttestation", jsonCase: "eth2"} +); + export const IndexedAttestation = new ContainerType( { attestingIndices: AttestingIndices, // Modified in ELECTRA @@ -268,7 +280,7 @@ export const PendingDeposits = new ListCompositeType(PendingDeposit, PENDING_DEP export const PendingPartialWithdrawal = new ContainerType( { - index: ValidatorIndex, + validatorIndex: ValidatorIndex, amount: Gwei, withdrawableEpoch: Epoch, }, diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index 691de409ed91..ee7585692d47 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -2,6 +2,7 @@ import {ValueOf} from "@chainsafe/ssz"; import * as ssz from "./sszTypes.js"; export type Attestation = ValueOf; +export type SingleAttestation = ValueOf; export type IndexedAttestation = ValueOf; export type IndexedAttestationBigint = ValueOf; export type AttesterSlashing = ValueOf; diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 2f9eead77608..f64415439b3e 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -316,6 +316,8 @@ export const Attestation = new ContainerType( {typeName: "Attestation", jsonCase: "eth2"} ); +export const SingleAttestation = Attestation; + export const AttesterSlashing = new ContainerType( { // In state transition, AttesterSlashing attestations are only partially validated. Their slot and epoch could diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 40806aa40f4a..d7c74857077c 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -51,7 +51,7 @@ export const SubcommitteeIndex = UintNum64; */ export const ValidatorIndex = UintNum64; export const WithdrawalIndex = UintNum64; -export const DepositIndex = UintNum64; +export const DepositIndex = UintBn64; export const Gwei = UintBn64; export const Wei = UintBn256; export const Root = new ByteVectorType(32); diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index badb1f1f2333..ce54c2f7ae63 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -56,6 +56,7 @@ type TypesByFork = { BeaconState: phase0.BeaconState; SignedBeaconBlock: phase0.SignedBeaconBlock; Metadata: phase0.Metadata; + SingleAttestation: phase0.Attestation; Attestation: phase0.Attestation; IndexedAttestation: phase0.IndexedAttestation; IndexedAttestationBigint: phase0.IndexedAttestationBigint; @@ -79,6 +80,7 @@ type TypesByFork = { LightClientStore: altair.LightClientStore; SyncCommittee: altair.SyncCommittee; SyncAggregate: altair.SyncAggregate; + SingleAttestation: phase0.Attestation; Attestation: phase0.Attestation; IndexedAttestation: phase0.IndexedAttestation; IndexedAttestationBigint: phase0.IndexedAttestationBigint; @@ -110,6 +112,7 @@ type TypesByFork = { SSEPayloadAttributes: bellatrix.SSEPayloadAttributes; SyncCommittee: altair.SyncCommittee; SyncAggregate: altair.SyncAggregate; + SingleAttestation: phase0.Attestation; Attestation: phase0.Attestation; IndexedAttestation: phase0.IndexedAttestation; IndexedAttestationBigint: phase0.IndexedAttestationBigint; @@ -141,6 +144,7 @@ type TypesByFork = { SSEPayloadAttributes: capella.SSEPayloadAttributes; SyncCommittee: altair.SyncCommittee; SyncAggregate: altair.SyncAggregate; + SingleAttestation: phase0.Attestation; Attestation: phase0.Attestation; IndexedAttestation: phase0.IndexedAttestation; IndexedAttestationBigint: phase0.IndexedAttestationBigint; @@ -177,6 +181,7 @@ type TypesByFork = { Contents: deneb.Contents; SyncCommittee: altair.SyncCommittee; SyncAggregate: altair.SyncAggregate; + SingleAttestation: phase0.Attestation; Attestation: phase0.Attestation; IndexedAttestation: phase0.IndexedAttestation; IndexedAttestationBigint: phase0.IndexedAttestationBigint; @@ -213,6 +218,7 @@ type TypesByFork = { Contents: deneb.Contents; SyncCommittee: altair.SyncCommittee; SyncAggregate: altair.SyncAggregate; + SingleAttestation: electra.SingleAttestation; Attestation: electra.Attestation; IndexedAttestation: electra.IndexedAttestation; IndexedAttestationBigint: electra.IndexedAttestationBigint; @@ -281,6 +287,7 @@ export type SignedBuilderBid = TypesByF export type SSEPayloadAttributes = TypesByFork[F]["SSEPayloadAttributes"]; export type Attestation = TypesByFork[F]["Attestation"]; +export type SingleAttestation = TypesByFork[F]["SingleAttestation"]; export type IndexedAttestation = TypesByFork[F]["IndexedAttestation"]; export type IndexedAttestationBigint = TypesByFork[F]["IndexedAttestationBigint"]; export type AttesterSlashing = TypesByFork[F]["AttesterSlashing"]; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index c212e4726e78..8622bc805982 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -16,6 +16,7 @@ import { SignedBeaconBlockOrContents, SignedBlindedBeaconBlock, SignedBlockContents, + SingleAttestation, } from "../types.js"; export function isExecutionPayload( @@ -74,6 +75,12 @@ export function isElectraAttestation(attestation: Attestation): attestation is A return (attestation as Attestation).committeeBits !== undefined; } +export function isElectraSingleAttestation( + singleAttestation: SingleAttestation +): singleAttestation is SingleAttestation { + return (singleAttestation as SingleAttestation).committeeIndex !== undefined; +} + export function isElectraLightClientUpdate(update: LightClientUpdate): update is LightClientUpdate { const updatePostElectra = update as LightClientUpdate; return ( diff --git a/packages/utils/package.json b/packages/utils/package.json index 3347b4815ee3..ae7216b822de 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.24.0", + "version": "1.25.0", "type": "module", "exports": "./lib/index.js", "files": [ @@ -39,7 +39,7 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/as-sha256": "^0.5.0", + "@chainsafe/as-sha256": "^1.0.0", "any-signal": "3.0.1", "bigint-buffer": "^1.1.5", "case": "^1.6.3", diff --git a/packages/validator/package.json b/packages/validator/package.json index 255782e1b531..e92455d81fd1 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/validator", - "version": "1.24.0", + "version": "1.25.0", "description": "A Typescript implementation of the validator client", "author": "ChainSafe Systems", "license": "LGPL-3.0", @@ -46,18 +46,18 @@ ], "dependencies": { "@chainsafe/blst": "^2.1.0", - "@chainsafe/ssz": "^0.18.0", - "@lodestar/api": "^1.24.0", - "@lodestar/config": "^1.24.0", - "@lodestar/db": "^1.24.0", - "@lodestar/params": "^1.24.0", - "@lodestar/state-transition": "^1.24.0", - "@lodestar/types": "^1.24.0", - "@lodestar/utils": "^1.24.0", + "@chainsafe/ssz": "^1.0.1", + "@lodestar/api": "^1.25.0", + "@lodestar/config": "^1.25.0", + "@lodestar/db": "^1.25.0", + "@lodestar/params": "^1.25.0", + "@lodestar/state-transition": "^1.25.0", + "@lodestar/types": "^1.25.0", + "@lodestar/utils": "^1.25.0", "strict-event-emitter-types": "^2.0.0" }, "devDependencies": { - "@lodestar/test-utils": "^1.24.0", + "@lodestar/test-utils": "^1.25.0", "bigint-buffer": "^1.1.5", "rimraf": "^4.4.1" } diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 514ecbbd613d..56be02131af6 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -1,8 +1,8 @@ import {ApiClient, routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {ForkSeq} from "@lodestar/params"; +import {ForkPreElectra, ForkSeq} from "@lodestar/params"; import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition"; -import {Attestation, BLSSignature, SignedAggregateAndProof, Slot, phase0, ssz} from "@lodestar/types"; +import {BLSSignature, SignedAggregateAndProof, SingleAttestation, Slot, phase0, ssz} from "@lodestar/types"; import {prettyBytes, sleep, toRootHex} from "@lodestar/utils"; import {Metrics} from "../metrics.js"; import {PubkeyHex} from "../types.js"; @@ -193,7 +193,7 @@ export class AttestationService { attestationNoCommittee: phase0.AttestationData, duties: AttDutyAndProof[] ): Promise { - const signedAttestations: Attestation[] = []; + const signedAttestations: SingleAttestation[] = []; const headRootHex = toRootHex(attestationNoCommittee.beaconBlockRoot); const currentEpoch = computeEpochAtSlot(slot); const isPostElectra = currentEpoch >= this.config.ELECTRA_FORK_EPOCH; @@ -239,7 +239,11 @@ export class AttestationService { if (isPostElectra) { (await this.api.beacon.submitPoolAttestationsV2({signedAttestations})).assertOk(); } else { - (await this.api.beacon.submitPoolAttestations({signedAttestations})).assertOk(); + ( + await this.api.beacon.submitPoolAttestations({ + signedAttestations: signedAttestations as SingleAttestation[], + }) + ).assertOk(); } this.logger.info("Published attestations", { ...logCtx, diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 7e052fbd73f1..565574c758cc 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -13,7 +13,6 @@ import { DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, ForkSeq, - MAX_COMMITTEES_PER_SLOT, } from "@lodestar/params"; import { ZERO_HASH, @@ -35,6 +34,7 @@ import { SignedAggregateAndProof, SignedBeaconBlock, SignedBlindedBeaconBlock, + SingleAttestation, Slot, ValidatorIndex, altair, @@ -505,7 +505,7 @@ export class ValidatorStore { duty: routes.validator.AttesterDuty, attestationData: phase0.AttestationData, currentEpoch: Epoch - ): Promise { + ): Promise { // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. if (attestationData.target.epoch > currentEpoch) { throw Error( @@ -539,10 +539,10 @@ export class ValidatorStore { if (this.config.getForkSeq(signingSlot) >= ForkSeq.electra) { return { - aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), + committeeIndex: duty.committeeIndex, + attesterIndex: duty.validatorIndex, data: attestationData, signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), - committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, duty.committeeIndex), }; } diff --git a/packages/validator/src/util/clock.ts b/packages/validator/src/util/clock.ts index 6b7e0868233a..d98a84dfe91c 100644 --- a/packages/validator/src/util/clock.ts +++ b/packages/validator/src/util/clock.ts @@ -120,13 +120,13 @@ export class Clock implements IClock { if (msFromGenesis >= 0) { return milliSecondsPerSlot - (msFromGenesis % milliSecondsPerSlot); } - return Math.abs(msFromGenesis % milliSecondsPerSlot); + return Math.abs(msFromGenesis) % milliSecondsPerSlot; } const milliSecondsPerEpoch = SLOTS_PER_EPOCH * milliSecondsPerSlot; if (msFromGenesis >= 0) { return milliSecondsPerEpoch - (msFromGenesis % milliSecondsPerEpoch); } - return Math.abs(msFromGenesis % milliSecondsPerEpoch); + return Math.abs(msFromGenesis) % milliSecondsPerEpoch; } } diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 825f60e8c7fa..383275bfb47b 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -137,7 +137,9 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record { opts ); - const attestation = isPostElectra + const singleAttestation = isPostElectra + ? ssz.electra.SingleAttestation.defaultValue() + : ssz.phase0.Attestation.defaultValue(); + const aggregatedAttestation = isPostElectra ? ssz.electra.Attestation.defaultValue() : ssz.phase0.Attestation.defaultValue(); - const aggregate = isPostElectra + const aggregateAndProof = isPostElectra ? ssz.electra.SignedAggregateAndProof.defaultValue() : ssz.phase0.SignedAggregateAndProof.defaultValue(); const duties: AttDutyAndProof[] = [ { duty: { slot: 0, - committeeIndex: attestation.data.index, + committeeIndex: singleAttestation.data.index, committeeLength: 120, committeesAtSlot: 120, validatorCommitteeIndex: 1, @@ -115,15 +118,15 @@ describe("AttestationService", () => { vi.spyOn(attestationService["dutiesService"], "getDutiesAtSlot").mockImplementation(() => duties); // Mock beacon's attestation and aggregates endpoints - api.validator.produceAttestationData.mockResolvedValue(mockApiResponse({data: attestation.data})); + api.validator.produceAttestationData.mockResolvedValue(mockApiResponse({data: singleAttestation.data})); if (isPostElectra) { api.validator.getAggregatedAttestationV2.mockResolvedValue( - mockApiResponse({data: attestation, meta: {version: ForkName.electra}}) + mockApiResponse({data: aggregatedAttestation, meta: {version: ForkName.electra}}) ); api.beacon.submitPoolAttestationsV2.mockResolvedValue(mockApiResponse({})); api.validator.publishAggregateAndProofsV2.mockResolvedValue(mockApiResponse({})); } else { - api.validator.getAggregatedAttestation.mockResolvedValue(mockApiResponse({data: attestation})); + api.validator.getAggregatedAttestation.mockResolvedValue(mockApiResponse({data: aggregatedAttestation})); api.beacon.submitPoolAttestations.mockResolvedValue(mockApiResponse({})); api.validator.publishAggregateAndProofs.mockResolvedValue(mockApiResponse({})); } @@ -139,8 +142,8 @@ describe("AttestationService", () => { } // Mock signing service - validatorStore.signAttestation.mockResolvedValue(attestation); - validatorStore.signAggregateAndProof.mockResolvedValue(aggregate); + validatorStore.signAttestation.mockResolvedValue(singleAttestation); + validatorStore.signAggregateAndProof.mockResolvedValue(aggregateAndProof); // Trigger clock onSlot for slot 0 await clock.tickSlotFns(0, controller.signal); @@ -170,21 +173,23 @@ describe("AttestationService", () => { if (isPostElectra) { // Must submit the attestation received through produceAttestationData() expect(api.beacon.submitPoolAttestationsV2).toHaveBeenCalledOnce(); - expect(api.beacon.submitPoolAttestationsV2).toHaveBeenCalledWith({signedAttestations: [attestation]}); + expect(api.beacon.submitPoolAttestationsV2).toHaveBeenCalledWith({signedAttestations: [singleAttestation]}); // Must submit the aggregate received through getAggregatedAttestationV2() then createAndSignAggregateAndProof() expect(api.validator.publishAggregateAndProofsV2).toHaveBeenCalledOnce(); expect(api.validator.publishAggregateAndProofsV2).toHaveBeenCalledWith({ - signedAggregateAndProofs: [aggregate], + signedAggregateAndProofs: [aggregateAndProof], }); } else { // Must submit the attestation received through produceAttestationData() expect(api.beacon.submitPoolAttestations).toHaveBeenCalledOnce(); - expect(api.beacon.submitPoolAttestations).toHaveBeenCalledWith({signedAttestations: [attestation]}); + expect(api.beacon.submitPoolAttestations).toHaveBeenCalledWith({signedAttestations: [singleAttestation]}); // Must submit the aggregate received through getAggregatedAttestation() then createAndSignAggregateAndProof() expect(api.validator.publishAggregateAndProofs).toHaveBeenCalledOnce(); - expect(api.validator.publishAggregateAndProofs).toHaveBeenCalledWith({signedAggregateAndProofs: [aggregate]}); + expect(api.validator.publishAggregateAndProofs).toHaveBeenCalledWith({ + signedAggregateAndProofs: [aggregateAndProof], + }); } }); }); diff --git a/packages/validator/test/unit/utils/interopConfigs.ts b/packages/validator/test/unit/utils/interopConfigs.ts index 4805154b4b64..a575b796e095 100644 --- a/packages/validator/test/unit/utils/interopConfigs.ts +++ b/packages/validator/test/unit/utils/interopConfigs.ts @@ -126,7 +126,7 @@ export const lighthouseHoleskyConfig = { PENDING_DEPOSITS_LIMIT: "134217728", PENDING_PARTIAL_WITHDRAWALS_LIMIT: "134217728", PENDING_CONSOLIDATIONS_LIMIT: "262144", - MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "1", + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "2", }; export const prysmHoleskyConfig = { @@ -266,7 +266,7 @@ export const prysmHoleskyConfig = { PENDING_DEPOSITS_LIMIT: "134217728", PENDING_PARTIAL_WITHDRAWALS_LIMIT: "134217728", PENDING_CONSOLIDATIONS_LIMIT: "262144", - MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "1", + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "2", }; export const tekuHoleskyConfig = { @@ -406,7 +406,7 @@ export const tekuHoleskyConfig = { PENDING_DEPOSITS_LIMIT: "134217728", PENDING_PARTIAL_WITHDRAWALS_LIMIT: "134217728", PENDING_CONSOLIDATIONS_LIMIT: "262144", - MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "1", + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "2", }; export const nimbusHoleskyConfig = { @@ -549,5 +549,5 @@ export const nimbusHoleskyConfig = { PENDING_DEPOSITS_LIMIT: "134217728", PENDING_PARTIAL_WITHDRAWALS_LIMIT: "134217728", PENDING_CONSOLIDATIONS_LIMIT: "262144", - MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "1", + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "2", }; diff --git a/yarn.lock b/yarn.lock index 1743171779bd..4fb1e5980e7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -357,10 +357,10 @@ resolved "https://registry.yarnpkg.com/@chainsafe/as-chacha20poly1305/-/as-chacha20poly1305-0.1.0.tgz#7da6f8796f9b42dac6e830a086d964f1f9189e09" integrity sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew== -"@chainsafe/as-sha256@0.5.0", "@chainsafe/as-sha256@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.5.0.tgz#2523fbef2b80b5000f9aa71f4a76e5c2c5c076bb" - integrity sha512-dTIY6oUZNdC5yDTVP5Qc9hAlKAsn0QTQ2DnQvvsbTnKSTbYs3p5RPN0aIUqN0liXei/9h24c7V0dkV44cnWIQA== +"@chainsafe/as-sha256@1.0.0", "@chainsafe/as-sha256@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-1.0.0.tgz#9095ad42dce13887b5877fce70592e573940ecd7" + integrity sha512-EYw5IZ99Mhn7K8d1eDDH66AFhPy9GcD7bfiqm9mwFjsg8MViEEicGl62b5YPzufBTFh7X7qWAe6yWpr/gbaVEw== "@chainsafe/as-sha256@^0.4.1": version "0.4.1" @@ -578,12 +578,12 @@ dependencies: "@chainsafe/is-ip" "^2.0.1" -"@chainsafe/persistent-merkle-tree@0.8.0", "@chainsafe/persistent-merkle-tree@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.8.0.tgz#18e2f0a5de3a0b59c6e5be8797a78e0d209dd7dc" - integrity sha512-hh6C1JO6SKlr0QGNTNtTLqgGVMA/Bc20wD6CeMHp+wqbFKCULRJuBUxhF4WDx/7mX8QlqF3nFriF/Eo8oYJ4/A== +"@chainsafe/persistent-merkle-tree@1.0.1", "@chainsafe/persistent-merkle-tree@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-1.0.1.tgz#4eb5a8e3367bc3957c88c8b9bad9610209e00fed" + integrity sha512-aQtYdXHmWRowcQK0h91HfHMO3bezQLk9wjQXv2CCcTbTim31BnCbPVpNbvAUWvEbifLQYvM18moygvEtdUNhXg== dependencies: - "@chainsafe/as-sha256" "0.5.0" + "@chainsafe/as-sha256" "1.0.0" "@chainsafe/hashtree" "1.0.1" "@noble/hashes" "^1.3.0" @@ -595,10 +595,10 @@ "@chainsafe/as-sha256" "^0.4.1" "@noble/hashes" "^1.3.0" -"@chainsafe/persistent-ts@^0.19.1": - version "0.19.1" - resolved "https://registry.npmjs.org/@chainsafe/persistent-ts/-/persistent-ts-0.19.1.tgz" - integrity sha512-fUFFFFxdcpYkMAHnjm83EYL/R/smtVmEkJr3FGSI6dwPk4ue9rXjEHf7FTd3V8AbVOcTJGriN4cYf2V+HOYkjQ== +"@chainsafe/persistent-ts@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-ts/-/persistent-ts-1.0.0.tgz#09ed7ab163a72d8ee9a154be589901bbc570a359" + integrity sha512-Xwu59vDQwJWcF4QbIdi9gvRVnkLBOc7Y5JUpINS4TVRtp4omhjEsqO4rFSCUhC8opyg1HcNSQEjL4IgYLGouuw== "@chainsafe/prometheus-gc-stats@^1.0.0": version "1.0.2" @@ -649,13 +649,13 @@ "@chainsafe/as-sha256" "^0.4.1" "@chainsafe/persistent-merkle-tree" "^0.6.1" -"@chainsafe/ssz@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.18.0.tgz#773d40df9dff3b6a2a4c6685d9797abceb9d36f7" - integrity sha512-1ikTjk3JK6+fsGWiT5IvQU0AP6gF3fDzGmPfkKthbcbgTUR8fjB83Ywp9ko/ZoiDGfrSFkATgT4hvRzclu0IAA== +"@chainsafe/ssz@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-1.0.1.tgz#dd1373cb4387fdd869d377f0fc5460edf422bd78" + integrity sha512-+QugG2Wbw3zWmCSIYsjAGoJXmT899ecdfI9OJVG6e3A6pPMJHH4EgENzXYy02ZUDhHXNhJ5c9pA4dElGfT7b4Q== dependencies: - "@chainsafe/as-sha256" "0.5.0" - "@chainsafe/persistent-merkle-tree" "0.8.0" + "@chainsafe/as-sha256" "1.0.0" + "@chainsafe/persistent-merkle-tree" "1.0.1" "@chainsafe/swap-or-not-shuffle-darwin-arm64@0.0.2": version "0.0.2"