diff --git a/.dockerignore b/.dockerignore index 42e8a818a418..d51b5556178f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -46,11 +46,11 @@ packages/beacon-node/mainnet_pubkeys.csv # Autogenerated docs packages/**/docs packages/**/typedocs -docs/packages -docs/contributing.md -docs/assets -docs/reference/cli.md -/site +docs/pages/**/*-cli.md +docs/pages/assets +docs/pages/api/api-reference.md +docs/pages/contribution/getting-started.md +docs/site # Lodestar artifacts .lodestar diff --git a/.github/actions/core-dump/action.yml b/.github/actions/core-dump/action.yml new file mode 100644 index 000000000000..eae37c2101b8 --- /dev/null +++ b/.github/actions/core-dump/action.yml @@ -0,0 +1,16 @@ +name: 'Take core dump files' +description: 'List down and upload core dumps as artifacts' +runs: + using: "composite" + steps: + - name: List down core dump files + run: | + ls -lah /cores/ + sudo chmod -R +rwx /cores/* + shell: sh + + - name: Backup core dump + uses: actions/upload-artifact@v3 + with: + name: core-dump + path: /cores/* diff --git a/.github/actions/setup-debug-node/action.yml b/.github/actions/setup-debug-node/action.yml new file mode 100644 index 000000000000..91890a5f4258 --- /dev/null +++ b/.github/actions/setup-debug-node/action.yml @@ -0,0 +1,23 @@ +name: "Setup node with debug support" +description: "Setup the nodejs version with debug support" +runs: + using: "composite" + steps: + # For now we only have the Node 20 debug build + - run: | + sudo apt-get install unzip && curl -L "https://drive.google.com/uc?export=download&id=1hlhbbQi-NJi8_WjULvOdo-K_tfZFzN3Z&confirm=t" > nodejs.zip && unzip nodejs.zip + sudo cp -f node /usr/bin/node-with-debug + sudo chmod +x /usr/bin/node-with-debug + shell: sh + + # List of naming patterns + # https://man7.org/linux/man-pages/man5/core.5.html + - run: | + sudo mkdir -p /cores + sudo sh -c "echo /cores/core-%e-%s-%u-%g-%p-%t > /proc/sys/kernel/core_pattern" + shell: sh + + - run: | + echo $(/usr/bin/node-with-debug --print "process.version") + echo $(/usr/bin/node-with-debug --print "process.features.debug") + shell: sh diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index bd802e83bc69..b5fa586c7a8b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,6 +17,7 @@ on: branches: - stable - unstable + workflow_dispatch: jobs: run: diff --git a/.github/workflows/build-debug-node.yml b/.github/workflows/build-debug-node.yml new file mode 100644 index 000000000000..9e7c1ac66fab --- /dev/null +++ b/.github/workflows/build-debug-node.yml @@ -0,0 +1,50 @@ +name: Build debug node + +on: + workflow_dispatch: + inputs: + version: + required: true + description: 'Node.js version' + +jobs: + build: + name: Build Debug version of Node.js + runs-on: buildjet-4vcpu-ubuntu-2204 + strategy: + fail-fast: false + steps: + - name: Install dependencies + run: apt-get install python3 g++ make python3-pip + + - name: Download Node.js source + uses: actions/checkout@v4 + with: + repository: 'nodejs/node' + ref: 'v${{ github.event.inputs.version }}' + path: 'nodejs' + + - name: Configure nodejs with debug flag + run: ./configure --debug + working-directory: 'nodejs' + + - name: Compile the nodejs + run: make -j$(nproc --all) + working-directory: 'nodejs' + + - name: Verify the build + run: make test-only + working-directory: 'nodejs' + + - name: Create destination folder + run: mkdir -p ${{ github.workspace }}/nodejs-debug-build-${{ github.event.inputs.version }} + + - name: Copy nodejs debug build + run: cp out/Debug/node ${{ github.workspace }}/nodejs-debug-build-${{ github.event.inputs.version }} + working-directory: 'nodejs' + + - name: Upload build to artifacts + uses: actions/upload-artifact@v3 + with: + name: nodejs-debug-build-${{ github.event.inputs.version }} + path: nodejs-debug-build-${{ github.event.inputs.version }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a19def8e72de..3ff9018372c9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -45,15 +45,17 @@ jobs: uses: actions/setup-python@v1 - name: Install dependencies + working-directory: docs run: | python -m pip install --upgrade pip - pip install -r docs/requirements.txt + pip install -r requirements.txt - name: Build docs - run: mkdocs build --site-dir site -v --clean + working-directory: docs + run: mkdocs build --verbose --clean --site-dir site - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./site + publish_dir: ./docs/site diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3355d36fb86..76d62ae576be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -153,12 +153,16 @@ jobs: matrix: node: [20] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: ${{matrix.node}} + node-version: ${{ matrix.node }} check-latest: true - cache: yarn + cache: yarn + + # Remove when finished debugging core dumps + - uses: './.github/actions/setup-debug-node' + - name: Restore build cache id: cache-primes-restore uses: actions/cache/restore@v3 @@ -180,7 +184,14 @@ jobs: key: spec-test-data-${{ hashFiles('packages/validator/test/spec/params.ts') }} - name: Unit tests - run: yarn test:unit + id: unit_tests + # Rever to "yarn test:unit" when finished debugging core dumps + run: sudo sh -c "ulimit -c unlimited && /usr/bin/node-with-debug $(which yarn) test:unit" + + # Remove when finished debugging core dumps + - uses: './.github/actions/core-dump' + if: ${{ failure() && steps.unit_tests.conclusion == 'failure' }} + - name: Upload coverage data run: yarn coverage @@ -259,7 +270,10 @@ jobs: packages/*/.git-data.json key: ${{ runner.os }}-node-${{ matrix.node }}-${{ github.sha }} fail-on-cache-miss: true - + - name: Install Chrome browser + run: npx @puppeteer/browsers install chrome + - name: Install Firefox browser + run: npx @puppeteer/browsers install firefox - name: Browser tests run: | export DISPLAY=':99.0' diff --git a/.gitignore b/.gitignore index ce1ec6074979..a85d4af7794e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,11 +40,14 @@ packages/api/oapi-schemas # Autogenerated docs packages/**/docs packages/**/typedocs -docs/assets -docs/packages -docs/reference -docs/contributing.md -/site +docs/pages/**/*-cli.md +docs/pages/assets +docs/pages/images +docs/pages/lightclient-prover/lightclient.md +docs/pages/lightclient-prover/prover.md +docs/pages/api/api-reference.md +docs/pages/contribution/getting-started.md +docs/site # Testnet artifacts .lodestar diff --git a/.wordlist.txt b/.wordlist.txt index 83d2bd51aa73..42510b175a07 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -1,14 +1,19 @@ APIs +Andreas +Antonopoulos AssemblyScript BLS BeaconNode Besu +Buterin CLA CLI CTRL +Casper Chai ChainSafe Customizations +DPoS Discv DockerHub Dockerized @@ -19,22 +24,33 @@ ENR ENRs ESLint ETH +Edgington Erigon EthStaker +EtherScan Ethereum +EthereumJS +FINDNODE FX Flamegraph Flamegraphs +Geth Github Gossipsub Grafana HackMD +Homebrew +IPFS IPv Infura JSON +JSObjects JWT +KDE LGPL LGPLv +LMD +LPoS LTS Lerna MEV @@ -45,10 +61,12 @@ NVM Nethermind NodeJS NodeSource +OSI PR PRs Plaintext PoS +Prysm Quickstart RPC SHA @@ -57,63 +75,102 @@ SSZ Stakehouse TOC TTD +Teku TypeScript UI UID +UPnP UTF VM Vitalik Wagyu api async +backfill beaconcha +blockchain bootnode bootnodes chainConfig chainsafe +chiado cli cmd +codebase config configs const constantish +coreutils cors +cryptocurrency cryptographic dApp dApps +ddos decrypt deserialization +dev devnet devnets +devtools +eg +enodes enum +env envs +ephemery flamegraph flamegraphs +gnosis goerli +heapdump +heaptrack +holesky interop +js keypair keystore keystores +libp lightclient linter +lldb +llnode lockfile mainnet +malloc mdns merkle merkleization monorepo +multiaddr +multifork namespace namespaced namespaces nodemodule +orchestrator +osx overriden params +pid plaintext +pre +premined produceBlockV +protolambda prover +repo +repos req reqresp +responder +ropsten runtime +scalability +secp +sepolia sharding ssz stakers @@ -130,4 +187,6 @@ utils validator validators wip +xcode yaml +yamux diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eaa110a60467..b5990d2eabbf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,13 @@ Thanks for your interest in contributing to Lodestar. It's people like you that - :gear: [NodeJS](https://nodejs.org/) (LTS) - :toolbox: [Yarn](https://yarnpkg.com/) +### MacOS Specifics + +When using MacOS, there are a couple of extra prerequisites that are required. + +- python +- coreutils (e.g. via `brew install coreutils`) + ## Getting Started - :gear: Run `yarn` to install dependencies. diff --git a/dashboards/lodestar_block_production.json b/dashboards/lodestar_block_production.json index 7bb0b4a2db7e..b999e47a33d4 100644 --- a/dashboards/lodestar_block_production.json +++ b/dashboards/lodestar_block_production.json @@ -53,6 +53,358 @@ ], "liveNow": false, "panels": [ + { + "type": "timeseries", + "title": "Full block production avg time with steps", + "gridPos": { + "x": 0, + "y": 1, + "w": 12, + "h": 8 + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "id": 546, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "refId": "proposerSlashing", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])", + "range": true, + "instant": false, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}", + "exemplar": false + }, + { + "refId": "attesterSlashings", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "voluntaryExits", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "blsToExecutionChanges", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "attestations", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"attestations\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"attestations\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "eth1DataAndDeposits", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "syncAggregate", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"syncAggregate\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"syncAggregate\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "executionPayload", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"executionPayload\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"executionPayload\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + } + ], + "options": { + "tooltip": { + "mode": "multi", + "sort": "none" + }, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom", + "calcs": [] + } + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 30, + "gradientMode": "opacity", + "spanNulls": false, + "insertNulls": false, + "showPoints": "auto", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "unit": "s" + }, + "overrides": [] + }, + "transformations": [] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 30, + "gradientMode": "opacity", + "spanNulls": false, + "insertNulls": false, + "showPoints": "auto", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "x": 12, + "y": 1, + "w": 12, + "h": 8 + }, + "id": 547, + "options": { + "tooltip": { + "mode": "multi", + "sort": "none" + }, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom", + "calcs": [] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "refId": "proposerSlashing", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])", + "range": true, + "instant": false, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}", + "exemplar": false + }, + { + "refId": "attesterSlashings", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "voluntaryExits", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "blsToExecutionChanges", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "attestations", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"attestations\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"attestations\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "eth1DataAndDeposits", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "syncAggregate", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"syncAggregate\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"syncAggregate\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "executionPayload", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"executionPayload\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"executionPayload\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + } + ], + "title": "Blinded block production avg time with steps", + "type": "timeseries", + "transformations": [] + }, { "collapsed": false, "datasource": { @@ -309,7 +661,7 @@ "expr": "rate(beacon_block_production_seconds_sum[$rate_interval])\n/\nrate(beacon_block_production_seconds_count[$rate_interval])", "format": "heatmap", "interval": "", - "legendFormat": "time", + "legendFormat": "{{instance}} - {{source}}", "refId": "A" } ], diff --git a/dashboards/lodestar_networking.json b/dashboards/lodestar_networking.json index 8633faeb7668..77e4be04048f 100644 --- a/dashboards/lodestar_networking.json +++ b/dashboards/lodestar_networking.json @@ -1,12 +1,12 @@ { "__inputs": [ { - "name": "DS_PROMETHEUS", - "type": "datasource", - "label": "Prometheus", "description": "", + "label": "Prometheus", + "name": "DS_PROMETHEUS", "pluginId": "prometheus", - "pluginName": "Prometheus" + "pluginName": "Prometheus", + "type": "datasource" } ], "annotations": { diff --git a/dashboards/lodestar_validator_monitor.json b/dashboards/lodestar_validator_monitor.json index 5bc844a639fc..7579a595b550 100644 --- a/dashboards/lodestar_validator_monitor.json +++ b/dashboards/lodestar_validator_monitor.json @@ -112,6 +112,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "https://hackmd.io/@dapplion/lodestar_attestation_summary", "fieldConfig": { "defaults": { "color": { @@ -166,6 +167,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "https://hackmd.io/@dapplion/lodestar_attestation_summary", "fieldConfig": { "defaults": { "color": { diff --git a/docs/images/heap-dumps/devtools.png b/docs/images/heap-dumps/devtools.png new file mode 100644 index 000000000000..9bdef24f7e20 Binary files /dev/null and b/docs/images/heap-dumps/devtools.png differ diff --git a/docs/images/heap-dumps/load-profile.png b/docs/images/heap-dumps/load-profile.png new file mode 100644 index 000000000000..c6e04d0922f4 Binary files /dev/null and b/docs/images/heap-dumps/load-profile.png differ diff --git a/docs/images/heap-dumps/memory-tab.png b/docs/images/heap-dumps/memory-tab.png new file mode 100644 index 000000000000..857309571971 Binary files /dev/null and b/docs/images/heap-dumps/memory-tab.png differ diff --git a/docs/install/docker.md b/docs/install/docker.md deleted file mode 100644 index 40468e7ad7aa..000000000000 --- a/docs/install/docker.md +++ /dev/null @@ -1,29 +0,0 @@ -# Install with Docker - -The [`chainsafe/lodestar`](https://hub.docker.com/r/chainsafe/lodestar) Docker Hub repository is maintained actively. It contains the `lodestar` CLI preinstalled. - - -!!! info - The Docker Hub image tagged as `chainsafe/lodestar:next` is run on CI every commit on our `unstable` branch. - For `stable` releases, the image is tagged as `chainsafe/lodestar:latest`. - - -Ensure you have Docker installed by issuing the command: - -```bash -docker -v -``` - -It should return a non error message such as `Docker version xxxx, build xxxx`. - -Pull, run the image and Lodestar should now be ready to use - -```bash -docker pull chainsafe/lodestar -docker run chainsafe/lodestar --help -``` - - -!!! info - Docker is the recommended setup for Lodestar. Use our [Lodestar Quickstart scripts](https://github.com/ChainSafe/lodestar-quickstart) with Docker for detailed instructions. - diff --git a/docs/install/npm.md b/docs/install/npm.md deleted file mode 100644 index 805141d01523..000000000000 --- a/docs/install/npm.md +++ /dev/null @@ -1,6 +0,0 @@ -# Install from NPM [not recommended] - - -!!! danger - For mainnet (production) usage, we only recommend installing with docker due to [NPM supply chain attacks](https://hackaday.com/2021/10/22/supply-chain-attack-npm-library-used-by-facebook-and-others-was-compromised/). Until a [safer installation method has been found](https://github.com/ChainSafe/lodestar/issues/3596), do not use this install method except for experimental purposes only. - diff --git a/docs/install/source.md b/docs/install/source.md deleted file mode 100644 index 4fba0a625111..000000000000 --- a/docs/install/source.md +++ /dev/null @@ -1,54 +0,0 @@ -# Install from source - -## Prerequisites - -Make sure to have [Yarn installed](https://classic.yarnpkg.com/en/docs/install). It is also recommended to [install NVM (Node Version Manager)](https://github.com/nvm-sh/nvm) and use the LTS version (currently v20) of [NodeJS](https://nodejs.org/en/). - - -!!! info - NodeJS versions older than the current LTS are not supported by Lodestar. We recommend running the latest Node LTS. - It is important to make sure the NodeJS version is not changed after reboot by setting a default `nvm alias default && nvm use default`. - -!!! note - Node Version Manager (NVM) will only install NodeJS for use with the active user. If you intend on setting up Lodestar to run under another user, we recommend using [NodeSource's source for NodeJS](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions) so you can install NodeJS globally. - - -## Clone repository - -Clone the repository locally and build from the stable release branch. - -```bash -git clone -b stable https://github.com/chainsafe/lodestar.git -``` - -Switch to created directory. - -```bash -cd lodestar -``` - -## Install packages - -Install across all packages. Lodestar follows a [monorepo](https://github.com/lerna/lerna) structure, so all commands below must be run in the project root. - -```bash -yarn install -``` - -## Build source code - -Build across all packages. - -```bash -yarn run build -``` - -## Lodestar CLI - -Lodestar should now be ready for use. - -```bash -./lodestar --help -``` - -See [Command Line Reference](./../reference/cli.md) for further information. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000000..056325e19104 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,143 @@ +site_name: Lodestar Documentation +site_description: Lodestar Documentation - Typescript Ethereum Consensus client +site_url: https://chainsafe.github.io/lodestar + +repo_name: chainsafe/lodestar +repo_url: https://github.com/chainsafe/lodestar + +docs_dir: pages + +# Configuration +theme: + name: material + logo: assets/lodestar_icon_300.png + favicon: assets/round-icon.ico + nav_style: dark + palette: + - scheme: preference + media: "(prefers-color-scheme: light)" + primary: black + accent: deep purple + toggle: + icon: material/weather-night + name: Switch to dark mode + - scheme: slate + media: "(prefers-color-scheme: dark)" + primary: black + accent: deep purple + toggle: + icon: material/weather-sunny + name: Switch to light mode + +plugins: + - search + - mermaid2: + version: 8.6.4 + arguments: + theme: | + ^(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light' + +markdown_extensions: + - meta + - codehilite: + guess_lang: false + - admonition + - toc: + permalink: true + - pymdownx.superfences: + # make exceptions to highlighting of code (for mermaid): + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:mermaid2.fence_mermaid + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +extra_css: + - stylesheets/extras.css + +# Socials +extra: + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/ChainSafe/lodestar + - icon: fontawesome/brands/twitter + link: https://twitter.com/lodestar_eth + - icon: fontawesome/brands/discord + link: https://discord.gg/yjyvFRP + - icon: fontawesome/brands/medium + link: https://blog.chainsafe.io + +# Customize left navigation menu +nav: + - Home: index.md + - Introduction: introduction.md + - Getting Started: + - Quick Start: getting-started/quick-start.md + - Installation: getting-started/installation.md + # - Creating a JWT: getting-started/creating-a-jwt.md + - Starting a Node: getting-started/starting-a-node.md + - Data Retention: data-retention.md + - Beacon Node: + - Configuration: beacon-management/beacon-cli.md + - Networking: beacon-management/networking.md + - MEV and Builder Integration: beacon-management/mev-and-builder-integration.md + - Syncing: beacon-management/syncing.md + - Validator: + - Configuration: validator-management/validator-cli.md + # - Key Management: validator-management/key-management.md + # - Withdrawals: validator-management/withdrawals.md + # - Multiple and Fall-Back Validation: validator-management/multiple-and-fallback-validation.md + - Bootnode: + - Configuration: bootnode/bootnode-cli.md + - Light Client and Prover: + - Light Client: lightclient-prover/lightclient.md + - Light Client Configuration: lightclient-prover/lightclient-cli.md + - Prover: lightclient-prover/prover.md + # - Prover Configuration: lightclient-prover/prover-cli.md + - Logging and Metrics: + - Prometheus and Grafana: logging-and-metrics/prometheus-grafana.md + - Client Monitoring: logging-and-metrics/client-monitoring.md + # - Log Management: logging-and-metrics/log-management.md + # - Metrics Management: logging-and-metrics/metrics-management.md + # - Dashboards: logging-and-metrics/dashboards.md + # - Api: + # - Using the API: api/using-the-api.md + # - API Reference: api/api-reference.md // Auto-generate from API endpoint + # - Troubleshooting: + # - Installation Issues: troubleshooting/installation-issues.md + # - Syncing Issues: troubleshooting/syncing-issues.md + # - Validation Issues: troubleshooting/validation-issues.md + # - Execution Layer Issues: troubleshooting/execution-layer-issues.md + - Supporting Libraries: supporting-libraries/index.md + # - libp2p: supporting-libraries/libp2p.md + # - "@chainsafe/ssz": supporting-libraries/ssz.md + # - "@chainsafe/blst": supporting-libraries/blst.md + # - "@chainsafe/libp2p-gossipsub": supporting-libraries/gossipsub.md + - Contributing: + - Getting Started: contribution/getting-started.md + # - Bug Reports: contribution/bug-reports.md + - Dependency Graph: contribution/depgraph.md + # - Repo: contribution/repo.md + - Testing: + - Overview: contribution/testing/overview.md + # - Unit Tests: contribution/testing/unit-tests.md + # - Integration Tests: contribution/testing/integration-tests.md + # - E2E Tests: contribution/testing/e2e-tests.md + - Simulation Tests: contribution/testing/simulation-tests.md + # - Spec Tests: contribution/testing/spec-tests.md + # - Performance Tests: contribution/testing/performance-tests.md + # - PR Submission: contribution/pr-submission.md + - Tools: + # - Debugging: tools/debugging.md + # - perf: tools/perf.md + - Flame Graphs: tools/flamegraphs.md + - Heap Dumps: tools/heap-dumps.md + - Core Dumps: tools/core-dumps.md + - Advanced Topics: + # - Migrating from Other Clients: advanced-topics/migrating-from-other-clients.md + # - Block Exploration: advanced-topics/block-exploration.md + # - Slashing Protection: advanced-topics/slashing-protection.md + - Setting Up a Testnet: advanced-topics/setting-up-a-testnet.md + # - Doppelganger Detection: advanced-topics/doppelganger-detection.md \ No newline at end of file diff --git a/docs/pages/advanced-topics/block-exploration.md b/docs/pages/advanced-topics/block-exploration.md new file mode 100644 index 000000000000..05ee657bb607 --- /dev/null +++ b/docs/pages/advanced-topics/block-exploration.md @@ -0,0 +1 @@ +# Block Exploration diff --git a/docs/pages/advanced-topics/doppelganger-detection.md b/docs/pages/advanced-topics/doppelganger-detection.md new file mode 100644 index 000000000000..165590bda55a --- /dev/null +++ b/docs/pages/advanced-topics/doppelganger-detection.md @@ -0,0 +1 @@ +# Doppelganger Detection diff --git a/docs/pages/advanced-topics/migrating-from-other-clients.md b/docs/pages/advanced-topics/migrating-from-other-clients.md new file mode 100644 index 000000000000..302314a27b23 --- /dev/null +++ b/docs/pages/advanced-topics/migrating-from-other-clients.md @@ -0,0 +1 @@ +# Migration From Other Clients diff --git a/docs/usage/local.md b/docs/pages/advanced-topics/setting-up-a-testnet.md similarity index 99% rename from docs/usage/local.md rename to docs/pages/advanced-topics/setting-up-a-testnet.md index 51465d68c92b..a6350b3a03de 100644 --- a/docs/usage/local.md +++ b/docs/pages/advanced-topics/setting-up-a-testnet.md @@ -1,4 +1,4 @@ -# Local testnet +# Setting-Up a Testnet To quickly test and run Lodestar we recommend starting a local testnet. We recommend a simple configuration of two beacon nodes with multiple validators diff --git a/docs/pages/advanced-topics/slashing-protection.md b/docs/pages/advanced-topics/slashing-protection.md new file mode 100644 index 000000000000..527cbb06040a --- /dev/null +++ b/docs/pages/advanced-topics/slashing-protection.md @@ -0,0 +1 @@ +# Slashing Protection diff --git a/docs/pages/api/using-the-api.md b/docs/pages/api/using-the-api.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/usage/mev-integration.md b/docs/pages/beacon-management/mev-and-builder-integration.md similarity index 97% rename from docs/usage/mev-integration.md rename to docs/pages/beacon-management/mev-and-builder-integration.md index c2f2529edbe6..c2f9db9b6846 100644 --- a/docs/usage/mev-integration.md +++ b/docs/pages/beacon-management/mev-and-builder-integration.md @@ -20,7 +20,7 @@ All you have to do is: 1. Provide lodestar beacon node with a Builder endpoint (which corresponds to the network you are running) via these additional flags: ```shell - --builder --builder.urls + --builder --builder.url ``` 2. Run lodestar validator client with these additional flags ```shell diff --git a/docs/pages/beacon-management/networking.md b/docs/pages/beacon-management/networking.md new file mode 100644 index 000000000000..9305b683ae47 --- /dev/null +++ b/docs/pages/beacon-management/networking.md @@ -0,0 +1,91 @@ +# Networking + +Starting up Lodestar will automatically connect it to peers on the network. Peers are found through the discv5 protocol and one peers are established communications happen via gossipsub over libp2p. While not necessary, having a basic understanding of how the various protocols and transport work will help with debugging and troubleshooting as some of the more common challenges come up with [firewalls](#firewall-management) and [NAT traversal](#nat-traversal). + +## Networking Flags + +Some of the important Lodestar flags related to networking are: + +- [`--discv5`](./configuration.md#--discv5) +- [`--listenAddress`](./configuration.md#--listenAddress) +- [`--port`](./configuration.md#--port) +- [`--discoveryPort`](./configuration.md#--discoveryPort) +- [`--listenAddress6`](./configuration.md#--listenAddress6) +- [`--port6`](./configuration.md#--port6) +- [`--discoveryPort6`](./configuration.md#--discoveryPort6) +- [`--bootnodes`](./configuration.md#--bootnodes) +- [`--deterministicLongLivedAttnets`](./configuration.md#--deterministicLongLivedAttnets) +- [`--subscribeAllSubnets`](./configuration.md#--subscribeAllSubnets) +- [`--disablePeerScoring`](./configuration.md#--disablePeerScoring) +- [`--enr.ip`](./configuration.md#--enr.ip) +- [`--enr.tcp`](./configuration.md#--enr.tcp) +- [`--enr.udp`](./configuration.md#--enr.udp) +- [`--enr.ip6`](./configuration.md#--enr.ip6) +- [`--enr.tcp6`](./configuration.md#--enr.tcp6) +- [`--enr.udp6`](./configuration.md#--enr.udp6) +- [`--nat`](./configuration.md#--nat) +- [`--private`](./configuration.md#`--private`) + +## Peer Discovery (Discv5) + +In Ethereum, discv5 plays a pivotal role in the peer discovery process, facilitating nodes to find and locate each other in order to form the peer-to-peer network​. The process begins with an interaction between new nodes and bootnodes at start-up. Bootnodes are nodes with hard-coded addresses, or can be overridden via the cli flag `--bootnodes`, to bootstrap the discovery process​. Through a method called FINDNODE-NODES, a new node establishes a bond with each bootnode, and it returns a list of peers for the new node to connect to. Following this trail, the new node engages through FINDNODE-NODES with the provided peers to further establish a web of connections​. + +Discv5 operates as a peer advertisement medium in this network, where nodes can act as both providers and consumers of data. Every participating node in the Discv5 protocol discovers peer data from other nodes and later relays it, making the discovery process dynamic and efficient​. + +Discv5 is designed to be a standalone protocol running via UDP on a dedicated port solely for peer discovery. Peer data is exchanged via self-certified, flexible peer records (ENRs). These key features cater to the Ethereum network​ and being a good peer often means running a discv5 worker​. Lodestar offers simple configuration to setup and run a bootnode independently of a beacon node. See [bootnode](./bootnode.md) for more information and configuration options. + +## ENR + +Ethereum Node Records (ENRs) are a standardized format utilized for peer discovery - see [EIP-778](https://eips.ethereum.org/EIPS/eip-778) for the specification. An ENR consists of a set of key-value pairs. These pairs include crucial information such as the node's ID, IP address, the port on which it's listening, and the protocols it supports. This information helps other nodes in the network locate and connect to the node. + +The primary purpose of ENRs is to facilitate node discovery and connectivity in the Ethereum network. Nodes use ENRs to announce their presence and capabilities to other nodes, making it easier to establish and maintain a robust, interconnected network. + +Note that bootnodes are announced via ENR. + +## Peer Communication (gossipsub and ReqResp) + +Gossipsub and ReqResp are the two mechanisms that beacon nodes use to exchange chain data. Gossipsub is used disseminate the most recent relevant data proactively throughout the network. ReqResp is used to directly ask specific peers for specific information (eg: during syncing). + +### Gossipsub + +GossipSub is a foundational protocol in peer-to-peer (P2P) communication, particularly decentralized networks like Ethereum and IPFS. At its core, GossipSub efficiently propagates data, filtered by topic, through a P2P network. It organizes peers into a collection of overlay networks, each associated with a distinct topic. By routing data through relevant overlay networks based on topics of interest, large amounts of data can be efficiently disseminated without excessive bandwidth, latency, etc. + +In GossipSub, nodes can subscribe to topics, effectively joining the corresponding overlay to receive messages published to a specific topic. This topic-based structure enables nodes to congregate around shared interests, ensuring that relevant messages are delivered to all interested parties. Each message published to a topic gets disseminated and relayed to all subscribed peers, similar to a chat room. + +Messages are propagated through a blend of eager-push and lazy-pull models. Specifically, the protocol employs "mesh links" to carry full messages actively and "gossip links" to carry only message identifiers (lazy-pull propagation model). This hybrid approach allows for both active message propagation and reactive message retrieval​ which is an extension of the traditional hub-and-spoke pub/sub model. + +### ReqResp + +ReqResp is the domain of protocols that establish a flexible, on-demand mechanism to retrieve historical data and data missed by gossip. This family of methods, implemented as separate libp2p protocols, operate between a single requester and responder. A method is initiated via a libp2p protocol ID, with the initiator sending a request message and the responder sending a response message. Every method defines a specific request and response message type, and a specific protocol ID. This framework also facilitates streaming responses and robust error handling. + +## Data Transport (libp2p) + +Libp2p is a modular and extensible network stack that serves as the data transport layer below both gossipsub and ReqResp and facilitates the lower-level peer-to-peer communications. It provides a suite of protocols for various networking functionalities including network transports, connection encryption and protocol multiplexing. Its modular design allows for the easy addition, replacement, or upgrading of protocols, ensuring an adaptable and evolving networking stack. + +Libp2p operates at the lower levels of the OSI model, particularly at the Transport and Network layers. Libp2p supports both TCP and UDP protocols for establishing connections and data transmission. Combined with libp2p's modular design it can integrate with various networking technologies to facilitating both routing and addressing. + +## Firewall Management + +If your setup is behind a firewall there are a few ports that will need to be opened to allow for P2P discovery and communication. There are also some ports that need to be protected to prevent unwanted access or DDOS attacks on your node. + +Ports that should be opened: + +- 30303/TCP+UDP - Execution layer p2p communication port +- 9000/TCP+UDP - Beacon Node P2P communication port +- 9090/TCP - Lodestar IPv6 P2P communication port +- 13000/TCP - Prysm P2P communication port +- 12000/UDP - Prysm P2P communication port + +Ports that should be inbound protected: + +- 9596/TCP - Lodestar Beacon-Node JSON RPC api calls +- 5062/TCP - Lodestar validator key manager api calls +- 18550/TCP - Lodestar MEV Boost/Builder port +- 8008/TCP - Lodestar Metrics +- 5064/TCP - Validator Metrics +- 8545/TCP - Execution client JSON RPC port api calls +- 8551/TCP - Execution engine port for Lodestar to communicate with the execution client + +## NAT Traversal + +Lodestar does not support UPnP. If you are behind a NAT you will need to manually forward the ports listed above. diff --git a/docs/pages/beacon-management/syncing.md b/docs/pages/beacon-management/syncing.md new file mode 100644 index 000000000000..21cd05d8a8a2 --- /dev/null +++ b/docs/pages/beacon-management/syncing.md @@ -0,0 +1,42 @@ +# Syncing + +Syncing an Ethereum node involves obtaining a copy of the blockchain data from other peers in the network to reach a consistent state. This process is crucial for new nodes or nodes that have been offline and need to catch up with the network's current state. Syncing can be performed for both the execution layer and the beacon chain, although the focus here will be primarily on the beacon chain. + +Lodestar allows for several methods of syncing however the recommended method is `checkpoint sync` as it is the fastest and least resource intensive. It is generally a good idea to sync via a [`--checkpointSyncUrl`](./configuration.md#--checkpointSyncUrl). If starting at a specific point is necessary specify the [`--checkpointState`](./configuration.md#--checkpointState) that should be where the sync begins. + +## Weak Subjectivity + +Weak subjectivity is a concept specific to Proof of Stake (PoS) systems, addressing how new nodes can safely join the network and synchronize with the correct blockchain history. Unlike in Proof of Work (PoW) systems, where a node can trust the longest chain due to the significant computational effort required to forge it, PoS systems present different challenges. In PoS, the cost of creating or altering blockchain history is lower, as it is not based on computational work but on the stake held by validators. This difference raises the possibility that an attacker, if possessing sufficient stake, could feasibly create a misleading version of the blockchain history. + +The concept of weak subjectivity becomes particularly crucial in two scenarios: when new nodes join the network and when existing nodes reconnect after a significant period of being offline. During these times, the 'weak subjectivity period' defines a time frame within which a client, upon rejoining, can reliably process blocks to reach the consensus chain head. This approach is essential for mitigating the risks associated with long-range attacks, which could occur if nodes relied solely on the longest chain principle without any initial trust in a specific network state. + +To counter these risks, weak subjectivity requires new nodes to obtain a recent, trusted state of the blockchain from a reliable source upon joining the network. This state includes vital information about the current set of validators and their stakes. Starting from this trusted state helps new nodes avoid being misled by false histories, as any attempt to rewrite history beyond this point would require an unrealistically large portion of the total stake. + +## Syncing Methods + +### Checkpoint Sync + +Checkpoint sync, also known as state sync, allows a node to sync to a specific state checkpoint without having to process all historical data leading up to that point. In the context of a beacon node, this involves syncing to a recent finalized checkpoint, allowing the node to quickly join the network and participate in consensus activities. This is especially beneficial for new nodes or nodes that have been offline for a considerable duration. + +### Historical Sync + +Historical sync involves processing all blocks from the genesis block or from a specified starting point to the current block. This is the most comprehensive sync method but also the most resource and time-intensive. For beacon nodes, historical sync is crucial for nodes that aim to maintain a complete history of the beacon chain, facilitating a deeper understanding and analysis of the network's history. In the execution layer, it ensures a complete historical record of the execution layer data. + +### Range Sync + +Range sync involves syncing blocks within a specified range, beneficial when a node is only temporarily offline and needs to catch up over a short range. In the beacon node context, this entails requesting and processing blocks within a defined range, ensuring the node quickly gets updated to the current network state. + +### Backfill Sync + +This is another version of checkpoint sync that allows a node that has not been historically synchronized to verify data prior to the checkpoint. It is done via downloading a checkpoint and then fetch blocks backwards from that point until the desired data can be verified. It is a relatively inexpensive sync from a cpu perspective because it only checks the block hashes and verifies the proposer signatures along the way. + +## Syncing Lodestar + +The implementation of the different syncing styles in Lodestar are actually one of two types under the hood, range sync and unknown-parent sync. Range sync is used when the start point of syncing is known. In the case of historical and checkpoint sync the starting points are well defined, genesis and the last finalized epoch boundary. Snapshot sync is not supported by Lodestar. If the starting point for sync is not known Lodestar must first determine where the starting point is. While the discussion about how that happens is out of scope for this document, the gist is that the beacon node will listen to gossipsub for blocks being broadcast on the network. It will also request [`MetaData`](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/p2p-interface.md#getmetadata) from its peers and use that to start requesting the correct blocks from the network. + +There are several flags that can be used to configure the sync process. + +- [`--checkpointSyncUrl`](./configuration.md#--checkpointSyncUrl) +- [`--checkpointState`](./configuration.md#--checkpointState) +- [`--wssCheckpoint`](./configuration.md#--wssCheckpoint) +- [`--forceCheckpointSync`](./configuration.md#--forceCheckpointSync) diff --git a/docs/pages/contribution/bug-reports.md b/docs/pages/contribution/bug-reports.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/design/depgraph.md b/docs/pages/contribution/depgraph.md similarity index 95% rename from docs/design/depgraph.md rename to docs/pages/contribution/depgraph.md index 9838dfaccac7..0135f86b672a 100644 --- a/docs/design/depgraph.md +++ b/docs/pages/contribution/depgraph.md @@ -99,7 +99,7 @@ Let's talk about how each package fits together in finer detail, from top to bot ## `@lodestar/fork-choice` [@lodestar/fork-choice](https://github.com/ChainSafe/lodestar/tree/unstable/packages/fork-choice) holds the methods for reading/writing the fork choice DAG. The `@lodestar/beacon-node` package is the sole consumer of this package because the beacon node itself is what controls when the fork choice DAG is updated. -For a good explanation on how the fork choice itself works, see the [annotated fork choice spec](https://github.com/ethereum/annotated-spec/blob/v1.1.10/phase0/fork-choice.md). This is an annotated version of the [Ethereum Consensus fork choice spec](https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md) which `lodestar-fork-choice` is based on. +For a good explanation on how the fork choice itself works, see the [annotated fork choice spec](https://github.com/ethereum/annotated-spec/blob/master/phase0/fork-choice.md). This is an annotated version of the [Ethereum Consensus fork choice spec](https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md) which `lodestar-fork-choice` is based on. ## `@lodestar/validator` diff --git a/docs/pages/contribution/pr-submission.md b/docs/pages/contribution/pr-submission.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/contribution/repo.md b/docs/pages/contribution/repo.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/contribution/testing/index.md b/docs/pages/contribution/testing/index.md new file mode 100644 index 000000000000..9de62895323c --- /dev/null +++ b/docs/pages/contribution/testing/index.md @@ -0,0 +1,27 @@ +# Testing + +Testing is critical to the Lodestar project and there are many types of tests that are run to build a product that is both effective AND efficient. This page will help to break down the different types of tests you will find in the Lodestar repo. + +### Unit Tests + +This is the most fundamental type of test in most code bases. In all instances mocks, stubs and other forms of isolation are used to test code on a functional, unit level. See the [Unit Tests](./unit-tests.md) page for more information. + +### Spec Tests + +The Ethereum Consensus Specifications are what ensure that the various consensus clients do not diverge on critical computations and will work harmoniously on the network. See the [Spec Tests](./spec-tests.md) page for more information. + +### Performance Tests + +Node.js is an unforgiving virtual machine when it comes to high performance, multi-threaded applications. In order to ensure that Lodestar can not only keep up with the chain, but to push the boundary of what is possible, there are lots of performance tests that benchmark programming paradigms and prevent regression. See the [Performance Testing](./performance-tests.md) page for more information. + +### End-To-End Tests + +E2E test are where Lodestar is run in its full form, often from the CLI as a user would to check that the system as a whole works as expected. These tests are meant to exercise the entire system in isolation and there is no network interaction, nor interaction with any other code outside of Lodestar. See the [End-To-End Testing](./end-to-end-tests.md) page for more information. + +### Integration Tests + +Integration tests are meant to test how Lodestar interacts with other clients, but are not considered full simulations. This is where Lodestar may make API calls or otherwise work across the process boundary, but there is required mocking, stubbing, or class isolation. An example of this is using the `ExecutionEngine` class to make API calls to a Geth instance to check that the http requests are properly formatted. + +### Simulation Tests + +These are the most comprehensive types of tests. They aim to test Lodestar in a fully functioning ephemeral devnet environment. See the [Simulation Testing](./simulation-tests.md) page for more information. diff --git a/docs/pages/contribution/testing/integration-tests.md b/docs/pages/contribution/testing/integration-tests.md new file mode 100644 index 000000000000..b45110033460 --- /dev/null +++ b/docs/pages/contribution/testing/integration-tests.md @@ -0,0 +1,27 @@ +# Integration Tests + +The following tests are found in `packages/beacon-node` + +#### `test:sim:withdrawals` + +This test simulates capella blocks with withdrawals. It tests lodestar against Geth and EthereumJS. + +There are two ENV variables that are required to run this test: + +- `EL_BINARY_DIR`: the docker image setup to handle the test case +- `EL_SCRIPT_DIR`: the script that will be used to start the EL client. All of the scripts can be found in `packages/beacon-node/test/scripts/el-interop` and the `EL_SCRIPT_DIR` is the sub-directory name in that root that should be used to run the test. + +The command to run this test is: + +`EL_BINARY_DIR=g11tech/geth:withdrawals EL_SCRIPT_DIR=gethdocker yarn mocha test/sim/withdrawal-interop.test.ts` + +The images used by this test during CI are: + +- `GETH_WITHDRAWALS_IMAGE: g11tech/geth:withdrawalsfeb8` +- `ETHEREUMJS_WITHDRAWALS_IMAGE: g11tech/ethereumjs:blobs-b6b63` + +#### `test:sim:merge-interop` + +#### `test:sim:mergemock` + +#### `yarn test:sim:blobs` diff --git a/docs/pages/contribution/testing/performance-tests.md b/docs/pages/contribution/testing/performance-tests.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/contribution/testing/simulation-tests.md b/docs/pages/contribution/testing/simulation-tests.md new file mode 100644 index 000000000000..ed36d1351307 --- /dev/null +++ b/docs/pages/contribution/testing/simulation-tests.md @@ -0,0 +1,141 @@ +# Simulation Testing + +"Sim" testing for Lodestar is the most comprehensive, and complex, testing that is run. The goal is to fully simulate a testnet and to actuate the code in a way that closely mimics what will happen when turning on Lodestar in the wild. This is a very complex task and requires a lot of moving parts to work together. The following sections will describe the various components and how they work together. + +At a very high level, simulation testing will setup a testnet from genesis and let proceed through "normal" execution exactly as the nodes would under production circumstances. To get feedback there are regular checks along the way to asses how the testnet nodes are working. These "assertions" can be added and removed at will to allow developers to check for specific conditions in a tightly controlled, reproducible, environment to get high quality and actionable feedback on how Lodestar performs. The end goal of these tests is to to run a full Lodestar client in an environment that is as close to what an end user would experience. + +These tests usually setup full testnets with multiple consensus clients and their paired execution node. In many instance we are looking to just exercise the Lodestar code but there are some places where there is also testing to see how Lodestar works in relation to the other consensus clients, like Lighthouse. As you can imagine, there is quite a bit of machinery that is responsible for setting up and managing the simulations and assertions. This section will help to go over those bits and pieces. Many, but not all, of these classes can be found in `packages/cli/test/utils/simulation`. + +## Running Sim Tests + +There are a number of sim tests that are available and each has a slightly different purpose. All are run by CI and must pass for a PR to be valid for merging. Most tests require a couple of environment variables to be set. + +### Environment Variables + +To see what typical values for these are check out the `test-sim.yaml` workflow file in the `.github/workflows` directory. + +- `GETH_DOCKER_IMAGE`: The geth docker image that will be used +- `NETHERMIND_IMAGE`: The nethermind docker image that will be used +- `LIGHTHOUSE_IMAGE`: The lighthouse docker image that will be used + +### `test:sim:multifork` + +The multi-fork sim test checks most of the functionality Lodestar provides. Is verifies that Lodestar is capable of peering, moving through all of the forks and using various sync methods in a testnet environment. Lodestar is tested with both Geth and Nethermind as the execution client. It also checks a Lighthouse/Geth node for cross client compatibility. + +```sh +GETH_DOCKER_IMAGE=ethereum/client-go:v1.11.6 \ + LIGHTHOUSE_DOCKER_IMAGE=sigp/lighthouse:latest-amd64-modern-dev \ + NETHERMIND_DOCKER_IMAGE=nethermind/nethermind:1.18.0 \ + yarn workspace @chainsafe/lodestar test:sim:multifork +``` + +### `test:sim:endpoints` + +This tests that various endpoints of the beacon node and validator client are working as expected. + +```sh +GETH_DOCKER_IMAGE=ethereum/client-go:v1.11.6 \ + yarn workspace @chainsafe/lodestar test:sim:endpoints +``` + +### `test:sim:deneb` + +This test is still included in our CI but is no longer as important as it once was. Lodestar is often the first client to implement new features and this test was created before geth was upgraded with the features required to support the Deneb fork. To test that Lodestar was ready this test uses mocked geth instances. It is left as a placeholder for when the next fork comes along that requires a similar approach. + +### `test:sim:mixedcleint` + +Checks that Lodestar is compatible with other consensus validators and vice-versa. All tests use Geth as the EL. + +```sh +GETH_DOCKER_IMAGE=ethereum/client-go:v1.11.6 \ + LIGHTHOUSE_DOCKER_IMAGE=sigp/lighthouse:latest-amd64-modern-dev \ + yarn workspace @chainsafe/lodestar test:sim:mixedclient +``` + +## Sim Test Infrastructure + +When setting up and running the simulations, interactions with the nodes is through the published node API's. All functionality is actuated via http request and by "plugging in" this way it is possible to run the nodes in a stand-alone fashion, as they would be run in production, but to still achieve a tightly monitored and controlled environment. If code needs to be executed on a "class by class" basis or with mocking involved then the test is not a simulation test and would fall into one of the other testing categories. See the [Testing](../testing.md) page for more information on the other types of tests available for Lodestar. + +### Simulation Environment + +The simulation environment has many pieces and those are orchestrated by the `SimulationEnvironment` class. The testnet nodes will be run as a mixture of Docker containers and bare metal code execution via Node.js. In order to monitor the various clients there is a `SimulationTracker` that's primary function is to `register` assertions that will track and gauge how the nodes are doing during the simulation. See the section on [Simulation Assertions](#simulation-assertions) below for more information on them. There is an `EpochClock` that has helper functions related to timing of slots and epochs and there is also a `Runner` that will help to start/stop the various Docker container and spawn the Node.js child processes as necessary. + +The `SimulationEnvironment` is the orchestrator for all the various functions to great the test net and start it from genesis. It is also how the various forks are configured to exercise code through various fork transitions. + +### Simulation Assertions + +These are the secret sauce for making the simulation tests meaningful. There are several predefined assertions that can be added to a simulation tracker and one can also create custom assertions and add them to the environment. Assertions can be added per slot, per epoch, per fork or per node. They can even be added to check conditions across nodes. + +Assertions are added to the `SimulationTracker` with the `register` method and the tracker follows the environment to make sure that assertions are run at the appropriate times, and on the correct targets. + +Assertions are implemented via API calls to the various targets and meta from the API calls is stored and used to assert that the desired conditions were met. Any information that can be retrieved via API call can be added to the assertion `stores` for validation, and validations can be asserted at a specific time or on an interval. + +There are a number of assertions that are added to simulations by default. They are: + +- `inclusionDelayAssertion` +- `attestationsCountAssertion` +- `attestationParticipationAssertion` +- `connectedPeerCountAssertion` +- `finalizedAssertion` +- `headAssertion` +- `missedBlocksAssertion` +- `syncCommitteeParticipationAssertion` + +Because of the flexibility, and complexity, there is a section specifically for how to create custom assertions below. See [custom assertions](#custom-assertions) for more info. + +### Custom Assertions + +Check back soon for more information on how to create custom assertions. + +### Simulation Reports + +Sim tests that are run using the simulation framework output a table of information to the console. The table summarizes the state of all of the nodes and the network at each slot. + +Here is an example of the table and how to interpret it: + +```sh +┼─────────────────────────────────────────────────────────────────────────────────────────────────┼ +│ fork │ eph │ slot │ head │ finzed │ peers │ attCount │ incDelay │ errors │ +┼─────────────────────────────────────────────────────────────────────────────────────────────────┼ +│ capella │ 9/0 │ 72 │ 0x95c4.. │ 56 │ 3 │ 16 │ 1.00 │ 0 │ +│ capella │ 9/1 │ 73 │ 0x9dfc.. │ 56 │ 3 │ 16 │ 1.00 │ 0 │ +│ capella │ 9/2 │ 74 │ 0xdf3f.. │ 56 │ 3 │ 16 │ 1.00 │ 0 │ +│ capella │ 9/3 │ 75 │ 0xbeae.. │ 56 │ 3 │ 16 │ 1.00 │ 0 │ +│ capella │ 9/4 │ 76 │ 0x15fa.. │ 56 │ 3 │ 16 │ 1.00 │ 0 │ +│ capella │ 9/5 │ 77 │ 0xf8ff.. │ 56 │ 2,3,3,2 │ 16 │ 1.00 │ 0 │ +│ capella │ 9/6 │ 78 │ 0x8199.. │ 56 │ 2,3,3,2 │ 16 │ 1.20 │ 0 │ +│ capella │ 9/7 │ 79 │ different │ 56 │ 2,3,3,2 │ 16 │ 1.50 │ 2 │ +┼─────────────────────────────────────────────────────────────────────────────────────────────────┼ +│ Att Participation: H: 0.75, S: 1.00, T: 0.75 - SC Participation: 1.00 │ +┼─────────────────────────────────────────────────────────────────────────────────────────────────┼ +``` + +#### Slot Information + +- `fork`: shows what fork is currently being tested +- `eph`: During simulation tests the Lodestar repo is setup to use 8 slot per epoch so what is shown is the epoch number and the slot number within that epoch as `epoch/slot` +- `slot`: The slot number that is currently being processed +- `head`: If all clients have the the same head the first couple of bytes of the hash are shown. If all clients do not have the same head `different` is reported. +- `finzed`: Shows the number of the last finalized slot +- `peers`: The number of peers that each node is connected to. If all have the same number then only a single value is shown. If they do not have the same number of peers count for each node is reported in a comma-separated list +- `attCount`: The number of attestations that the node has seen. +- `incDelay`: The average number of slots inclusion delay was experienced for the attestations. Often attestations for the current head arrive more than one slot behind and this value tracks that +- `errors`: The number of errors that were encountered during the slot + +#### Epoch Information + +- `H`: The percentage of nodes, at epoch transition, that voted for the head block +- `S`: The percentage of nodes, at epoch transition, that voted for the source block +- `T`: The percentage of nodes, at epoch transition, that voted for the target block +- `SC Participation`: The sync committee participation rate + +### Simulation Logging + +The simulation environment will capture all of the logs from all nodes that are running. The logs can be found in the `packages/cli/test-logs` directory. The logs are named with the following convention: + +`-_.log` + +Some examples are: + +- `node-1-beacon_lodestar.log`: The is the first node in the simulation. It is the consensus layer. It is running the lodestar validator client. +- `range-sync-execution_geth.log`: This is the node that was added to test pulling history in range sync mode. It was the execution layer and was running the geth execution client. diff --git a/docs/pages/contribution/testing/spec-tests.md b/docs/pages/contribution/testing/spec-tests.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/contribution/testing/unit-tests.md b/docs/pages/contribution/testing/unit-tests.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/data-retention.md b/docs/pages/data-retention.md new file mode 100644 index 000000000000..c8512858441f --- /dev/null +++ b/docs/pages/data-retention.md @@ -0,0 +1,54 @@ +# Data Retention + +There are two components for an ethereum node database, the execution client and the beacon node. Both need to hold data for a full node to work correctly. In particular the execution node holds state such as wallet information and smart contract code. It also holds the execution blocks with the transaction record. The beacon node is responsible for holding beacon node blocks and state. The beacon state is responsible primarily for the validator information. + +There are several processes that need to store data for Lodestar. These data sets can grow quite large over time so it is important to understand how to manage them so the host machine can support operations effectively. + +```bash +$executionDir # this changes depending on the execution client + └── execution-db + +$dataDir # specified by --dataDir on the beacon command +├── .log_rotate_audit.json +├── beacon.log # there can be many of these +├── enr +├── peer-id.json +├── chain-db # default if --dbDir not specified +│ └── (db files) +└── peerstore # default if --peerStoreDir not specified + └── (peerstore files) + +$dataDir # specified by --dataDir on the validator command +├── .log_rotate_audit.json +├── validator.log # there can be many of these +├── validator-db # default if --validatorsDbDir not specified +│ └── (db files) +├── proposerConfigs # default if --proposerDir not specified +│ └── (config files) +├── cache # default if --cacheDir not specified +│ └── (cache files) +├── secrets # default if --secretsDir not specified +│ ├── 0x8e41b969493454318c27ec6fac90645769331c07ebc8db5037... +│ └── 0xa329f988c16993768299643d918a2694892c012765d896a16f... +├── keystores # default if --keystoresDir not specified +│ ├── 0x8e41b969493454318c27ec6fac90645769331c07ebc8db5037... +│ │ └── voting-keystore.json +│ └── 0xa329f988c16993768299643d918a2694892c012765d896a16f... +│ └── voting-keystore.json +└── remoteKeys # default if --remoteKeysDir not specified + └── 0xa329f988c16993768299643d918a2694892c012765d896a16f.json +``` + +## Data Management + +Configuring your node to store and prune data is key to success. On average you can expect for the database to grow by the follow amounts: + +- `execution-db` grows at 2-30GB per week +- `chain-db` grows at 1GB per month +- `validator-db` grows at less than 2MB per year, per key (2000 keys = 4GB per year) + +`keystores`, `keystore-cache` and `peerstore` are not usually very large and are not expected to grow much during normal operation. + +Logs can also become quite large so please check out the section on [log management](../logging-and-metrics/log-management.md) for more information. + +There is really only one flag that is needed to manage the data for Lodestar, [`--dataDir`](./configuration.md#--dataDir). Other than that handling log management is really the heart of the data management story. Beacon node data is what it is. Depending on the execution client that is chosen, there may be flags to help with data storage growth but that is outside the scope of this document. diff --git a/docs/pages/getting-started/installation.md b/docs/pages/getting-started/installation.md new file mode 100644 index 000000000000..61ecb5b128ef --- /dev/null +++ b/docs/pages/getting-started/installation.md @@ -0,0 +1,93 @@ +# Installation + +## Docker Installation + +The [`chainsafe/lodestar`](https://hub.docker.com/r/chainsafe/lodestar) Docker Hub repository is maintained actively. It contains the `lodestar` CLI preinstalled. + + +!!! info + The Docker Hub image tagged as `chainsafe/lodestar:next` is run on CI every commit on our `unstable` branch. + For `stable` releases, the image is tagged as `chainsafe/lodestar:latest`. + + +Ensure you have Docker installed by issuing the command: + +```bash +docker -v +``` + +It should return a non error message such as `Docker version xxxx, build xxxx`. + +Pull, run the image and Lodestar should now be ready to use + +```bash +docker pull chainsafe/lodestar +docker run chainsafe/lodestar --help +``` + + +!!! info + Docker is the recommended setup for Lodestar. Use our [Lodestar Quickstart scripts](https://github.com/ChainSafe/lodestar-quickstart) with Docker for detailed instructions. + + +## Build from Source + +### Prerequisites + +Make sure to have [Yarn installed](https://classic.yarnpkg.com/en/docs/install). It is also recommended to [install NVM (Node Version Manager)](https://github.com/nvm-sh/nvm) and use the LTS version (currently v20) of [NodeJS](https://nodejs.org/en/). + + +!!! info + NodeJS versions older than the current LTS are not supported by Lodestar. We recommend running the latest Node LTS. + It is important to make sure the NodeJS version is not changed after reboot by setting a default `nvm alias default && nvm use default`. + +!!! note + Node Version Manager (NVM) will only install NodeJS for use with the active user. If you intend on setting up Lodestar to run under another user, we recommend using [NodeSource's source for NodeJS](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions) so you can install NodeJS globally. + + +### Clone repository + +Clone the repository locally and build from the stable release branch. + +```bash +git clone -b stable https://github.com/chainsafe/lodestar.git +``` + +Switch to created directory. + +```bash +cd lodestar +``` + +### Install packages + +Install across all packages. Lodestar follows a [monorepo](https://github.com/lerna/lerna) structure, so all commands below must be run in the project root. + +```bash +yarn install +``` + +### Build source code + +Build across all packages. + +```bash +yarn run build +``` + +### Lodestar CLI + +Lodestar should now be ready for use. + +```bash +./lodestar --help +``` + +See [Command Line Reference](./../reference/cli.md) for further information. + +## Install from NPM [not recommended] + + +!!! danger + For mainnet (production) usage, we only recommend installing with docker due to [NPM supply chain attacks](https://hackaday.com/2021/10/22/supply-chain-attack-npm-library-used-by-facebook-and-others-was-compromised/). Until a [safer installation method has been found](https://github.com/ChainSafe/lodestar/issues/3596), do not use this install method except for experimental purposes only. + \ No newline at end of file diff --git a/docs/quickstart.md b/docs/pages/getting-started/quick-start.md similarity index 100% rename from docs/quickstart.md rename to docs/pages/getting-started/quick-start.md diff --git a/docs/usage/beacon-management.md b/docs/pages/getting-started/starting-a-node.md similarity index 100% rename from docs/usage/beacon-management.md rename to docs/pages/getting-started/starting-a-node.md diff --git a/docs/pages/getting-started/starting-a-node.new.md b/docs/pages/getting-started/starting-a-node.new.md new file mode 100644 index 000000000000..b66e797b29ed --- /dev/null +++ b/docs/pages/getting-started/starting-a-node.new.md @@ -0,0 +1,21 @@ +# Starting a Node + +## Prerequisites + +### Creating a Client Communication JWT + +### Creating a Validator Keystore + +## Base Considerations + +### Execution Client + +### Beacon Node + +### Validator Client + +## Production Considerations + +### Ingress/Egress + +### Fail-Over diff --git a/docs/index.md b/docs/pages/index.md similarity index 100% rename from docs/index.md rename to docs/pages/index.md diff --git a/docs/pages/introduction.md b/docs/pages/introduction.md new file mode 100644 index 000000000000..f8fe03386c0a --- /dev/null +++ b/docs/pages/introduction.md @@ -0,0 +1,34 @@ +# Introduction + +Ethereum is one of the most profoundly important inventions in recent history. It is a decentralized, open-source blockchain featuring smart contract functionality. It is the second-largest cryptocurrency by market capitalization, after Bitcoin, and is the most actively used blockchain. Ethereum was proposed in 2013 by programmer Vitalik Buterin. Development was crowdfunded in 2014, and the network went live on 30 July 2015, with 72 million coins premined. ChainSafe was founded not too long afterwards and has been actively working in the Ethereum space ever since. We are proud to develop Lodestar and to present this documentation as a resource for the Ethereum community. + +## Proof of Stake + +In Ethereum's Proof of Stake (PoS) model, validators replace miners from the Proof of Work (PoW) system. Validators are Ethereum stakeholders who lock up a portion of their Ether as a stake. The protocol randomly selects these validators to propose new blocks. The chance of being chosen is tied to the size of their stake: the more Ether staked, the higher the probability of being selected to propose the block. Proposers receive transaction fees and block rewards as incentives. Validators are also responsible for voting on the validity of blocks proposed by other validators. However, they face penalties, known as slashing, for actions like double-signing, votes on a block that is not in the majority or going offline, ensuring network integrity and reliability. The PoS mechanism significantly reduces energy consumption compared to PoW, because it does not require extensive computational power. Moreover, PoS tends to facilitate faster transaction validations and block creations, enhancing the overall performance and scalability of the network. + +## Consensus Clients + +In an effort to promote client diversity there are several beacon-nodes being developed. Each is programmed in a different language and by a different team. The following is a list of the current beacon-node clients: + +[Lodestar](https://chainsafe.io/lodestar.html) +[Prysm](https://prysmaticlabs.com/) +[Lighthouse](https://lighthouse.sigmaprime.io/) +[Teku](https://consensys.net/knowledge-base/ethereum-2/teku/) +[Nimbus](https://nimbus.team/) + +## Why Client Diversity? + +The Ethereum network's robustness is significantly enhanced by its client diversity, whereby multiple, independently-developed clients conforming to a common specification facilitate seamless interaction and function equivalently across nodes. This client variety not only fosters a rich ecosystem but also provides a buffer against network-wide issues stemming from bugs or malicious attacks targeted at particular clients. For instance, during the Shanghai denial-of-service attack in 2016, the diversified client structure enabled the network to withstand the assault, underscoring the resilience afforded by multiple client configurations. + +On the consensus layer, client distribution is crucial for maintaining network integrity and finality, ensuring transactions are irreversible once validated. A balanced spread of nodes across various clients helps mitigate risks associated with potential bugs or attacks that could, in extreme cases, derail the consensus process or lead to incorrect chain splits, thereby jeopardizing the network's stability and trust. While the data suggests a dominance of Prysm client on the consensus layer, efforts are ongoing to promote a more even distribution among others like Lighthouse, Teku, and Nimbus. Encouraging the adoption of minority clients, bolstering their documentation, and leveraging real-time client diversity dashboards are among the strategies being employed to enhance client diversity, which in turn fortifies the Ethereum consensus layer against adversities and fosters a healthier decentralized network ecosystem. + +The non-finality event in May 2023 on the Ethereum network posed a significant challenge. The issue arose from attestations for a fork, which necessitated state replays to validate the attestations, causing a notable strain on system resources. As a result, nodes fell out of sync, which deterred the accurate tracking of the actual head of the chain. This situation was exacerbated by a decline in attestations during specific epochs, further hampering the consensus mechanism. The Lodestar team noticed late attestations several weeks prior to the event and implemented a feature that attempted to address such challenges by not processing untimely attestations, and thus not requiring expensive state replays​. While it was done for slightly different reasons, the result was the same. Lodestar was able to follow the chain correctly and helped to stabilize the network. This example underscored the importance of client diversity and network resilience against potential forks and replay attacks. These are considered realistic threats, especially in the context of system complexity like in Ethereum's consensus mechanism. + +## Ethereum Reading List + +- [Ethereum Docs](https://ethereum.org/en/developers/docs/) +- [Upgrading Ethereum](https://eth2book.info/capella/) by Ben Edgington +- [Ethereum Book](https://github.com/ethereumbook/ethereumbook) by Andreas M. Antonopoulos and Gavin Wood +- [Ethereum Consensus Specification](https://github.com/ethereum/consensus-specs) +- [Casper the Friendly Finality Gadget](https://browse.arxiv.org/pdf/1710.09437.pdf) by Vitalik Buterin and Virgil Griffith +- [LMD Ghost](https://github.com/protolambda/lmd-ghost) by protolambda diff --git a/docs/pages/lightclient-prover/.gitkeep b/docs/pages/lightclient-prover/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/usage/client-monitoring.md b/docs/pages/logging-and-metrics/client-monitoring.md similarity index 100% rename from docs/usage/client-monitoring.md rename to docs/pages/logging-and-metrics/client-monitoring.md diff --git a/docs/pages/logging-and-metrics/dashboards.md b/docs/pages/logging-and-metrics/dashboards.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/logging-and-metrics/log-management.md b/docs/pages/logging-and-metrics/log-management.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/logging-and-metrics/metrics-management.md b/docs/pages/logging-and-metrics/metrics-management.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/usage/prometheus-grafana.md b/docs/pages/logging-and-metrics/prometheus-grafana.md similarity index 100% rename from docs/usage/prometheus-grafana.md rename to docs/pages/logging-and-metrics/prometheus-grafana.md diff --git a/docs/pages/supporting-libraries/index.md b/docs/pages/supporting-libraries/index.md new file mode 100644 index 000000000000..eb1e7821db18 --- /dev/null +++ b/docs/pages/supporting-libraries/index.md @@ -0,0 +1,27 @@ +# Supporting Libraries + +## Networking + +### LibP2P + +- [`@chainsafe/js-libp2p-noise`](https://github.com/NodeFactoryIo/js-libp2p-noise) - [Noise](https://noiseprotocol.org/noise.html) handshake for `js-libp2p` +- [`@chainsafe/js-libp2p-gossipsub`](https://github.com/ChainSafe/js-libp2p-gossipsub) - [Gossipsub](https://github.com/libp2p/specs/tree/master/pubsub/gossipsub) protocol for `js-libp2p` +- [@chainsafe/libp2p-yamux](https://github.com/ChainSafe/libp2p-yamux) + +### Discv5 + +- [`discv5`](https://github.com/ChainSafe/discv5) - [Discv5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) protocol + +## Serialization and Hashing + +- [`ssz`](https://github.com/ChainSafe/ssz) - Simple Serialize (SSZ) +- [`persistent-merkle-tree`](https://github.com/ChainSafe/persistent-merkle-tree) - binary merkle tree implemented as a [persistent data structure](https://en.wikipedia.org/wiki/Persistent_data_structure) +- [`as-sha256`](https://github.com/ChainSafe/as-sha256) - Small AssemblyScript implementation of SHA256 + +## BLS + +- [`bls`](https://github.com/ChainSafe/bls) - Isomorphic Ethereum Consensus BLS sign / verify / aggregate +- [`blst-ts`](https://github.com/ChainSafe/blst) - Node specific Ethereum Consensus BLS sign / verify / aggregate +- [`bls-keystore`](https://github.com/ChainSafe/bls-keystore) - store / retrieve a BLS secret key from an [EIP-2335](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2335.md) JSON keystore +- [`bls-keygen`](https://github.com/ChainSafe/bls-keygen) - utility functions to generate BLS secret keys, following [EIP-2333](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md) and [EIP-2334](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2334.md) +- [`bls-hd-key`](https://github.com/ChainSafe/bls-hd-key) - low level [EIP-2333](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md) and [EIP-2334](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2334.md) functionality diff --git a/docs/libraries/index.md b/docs/pages/supporting-libraries/libraries.md similarity index 100% rename from docs/libraries/index.md rename to docs/pages/supporting-libraries/libraries.md diff --git a/docs/pages/tools/core-dumps.md b/docs/pages/tools/core-dumps.md new file mode 100644 index 000000000000..98d564eb9308 --- /dev/null +++ b/docs/pages/tools/core-dumps.md @@ -0,0 +1,66 @@ +# Core Dump Analysis + +Core dump analysis is some ninja level stuff. Once you get the hang of it you will feel like you have super powers. It will up your game to a whole new level because you will be able to debug issues that seemed impossible before. Post-crash analysis is a very powerful tool to have in your tool belt. A core dump has all of the objects in memory as well as all of the stack frame information at the exact moment the dump was taken, usually when a hard crash occurs. + +It is important to note that debug symbols will greatly aid you in your debugging for issues related to native code like `C/C++`. When compiled languages are optimized the compiler will often strip out identifiers and all that will be remaining are mangled symbols and addresses. Compiling with debug symbols will leave all of the identifiers, file names and line numbers in-tact. + +While it is not always practical to be running code in a Debug version of node, if you run across a persistent issue it will be helpful to recreate it on a debug build and to use that for analysis. + +It is important to note that the EXACT binary that was running when the dump was created MUST be loaded when doing analysis. There is a lot of information in the dump that is specific to the binary that was running (like function offsets, etc). If you load a different binary you will get a lot of errors and the analysis will not be useful (if it loads at all). + +It is also a nice-to-know that you can create the dump on linux, using a linux compiled version of node, and then read it on a mac. All that is needed is to download the node binary and dump file to the mac. It is possible to load them into a mac compiled version of llnode and all will work as expected. Its just the meta in the linux binary that is needed for analysis, it doesn't actually run the code. + +## Installing `llnode` + +`llnode` is a Node.js plugin for the [LLDB](https://lldb.llvm.org/) debugger. It is the officially sanctioned tool from Node and powerful way to do postmortem analysis of Node.js processes. The process for install is pretty straight-forward unless you have an M1 mac. XCode ships with an instance of `lldb` and installing `llnode` is as simple as running `npm install -g llnode`. + +On an M1 mac the install will work fine but the plugin will crash at load time. See [this issue](https://github.com/nodejs/llnode/issues/430#issuecomment-1844628224) for updates. The workaround is to install `lldb` via homebrew. + +```sh +# should only be necessary on M1 macs at time of writing +$ brew install llvm +$ echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc +$ # note that its before recopying PATH to make sure it resolves +$ zsh ~/.zshrc +$ which llvm-config +/opt/homebrew/opt/llvm/bin/llvm-config # if this is not what comes up restart the shell +$ npm install -g llnode +$ llnode +(lldb) plugin load '/Users/ninja_user/.nvm/versions/node/v20.5.1/lib/node_modules/llnode/llnode.dylib' +(lldb) settings set prompt '(llnode) ' +(llnode) +``` + +## Collecting a core dump + +Before a core dump can be created the system must be enabled. + +```sh +ulimit -c unlimited +``` + +This is a critical step. If that command is not run the core will not be dumped to disk. + +Core dumps are normally created by the kernel when certain process signals are encountered. `SIGSEGV` is the most common signal that will cause a dump and its sent by the kernel to the process when a segfault occurs. `SIGSEGV` is not the only signal that works and you can see the full list [here](https://man7.org/linux/man-pages/man7/signal.7.html) under the "Standard Signals" section (all the ones that say "Core" in the "Action" column). + +If you want to create a dump on demand you can use the `gcore` command on linux. This will create a dump of the process without killing it. If you don't mind termination you can also use `kill -SIGSEGV ` to send the a dump signal to the process. + +## Analyzing a core dump + +Once you collect the core dump you can load it into `llnode` for debugging. + +```sh +# remember that the node binary must be the exact same one that was running when the core was created +$ llnode -f /path/to/node_debug -c /Users/ninja_user/coredumps/node.coredump +(lldb) target create "node_debug" --core "node.coredump" +Core file '/Users/ninja_user/coredumps/node.coredump' (x86_64) was loaded. +(lldb) plugin load '/Users/ninja_user/.nvm/versions/node/v20.5.1/lib/node_modules/llnode/llnode.dylib' +(lldb) settings set prompt '(llnode) ' +(llnode) +``` + +Once the dump is loaded the first few steps will be to figure out what types of objects were in memory and what was the processor working on when the crash occurred. Lets start with the stack trace. + +There are two distinct commands for pulling the stack because node is both a native runtime and a virtual machine. The `bt`, back trace, command will pull the native stack frames and the `v8 bt` command will use the `llnode` plugin to pull the JavaScript stack frames. Newer versions of `llnode` will automatically pull the JavaScript stack frames when the `bt` command is run but it is still good to know the difference. It is also possible to add the `all` verb to the `bt` command and it will pull the back trace for all threads. + +To start looking through memory there are two commands that are helpful. The `v8 findjsobjects` command will list all of the JavaScript objects in memory. The `v8 findjsinstances` command will list all of the instances of a particular JavaScript object. diff --git a/docs/pages/tools/debugging.md b/docs/pages/tools/debugging.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/tools/flamegraphs.md b/docs/pages/tools/flamegraphs.md similarity index 100% rename from docs/tools/flamegraphs.md rename to docs/pages/tools/flamegraphs.md diff --git a/docs/pages/tools/heap-dumps.md b/docs/pages/tools/heap-dumps.md new file mode 100644 index 000000000000..379f7e4de2f2 --- /dev/null +++ b/docs/pages/tools/heap-dumps.md @@ -0,0 +1,279 @@ +# Heap Dump Analysis + +There are a number of reason why one would want to do a heap dump but in particular, they are helpful for find memory intensive operations and leaks. There are two major types of heap dumps that are available to node developers. The first is a JavaScript heap dump, and the second is a native heap dump. The JS heap dump is much more common and is the default heap dump that is generated by `node`. It is useful when analyzing JS generated objects that are managed by the runtime. However there is one major limitation to the JS heap dump, and that is that it does not include native objects. This is where the native heap dump comes in handy. The native heap dump is a snapshot of the entire process memory, and includes objects that are allocated by `C/C++` code, including native modules in use by the application. The limitation to the native heap dump is that it will not include any JS objects that are allocated by the `V8` runtime. Those are generally created within `mmap`'ed pages and the native heap dump tools are specific to `C` objects that are created with `malloc` and destroyed via `free`. `C++` is also covered as `new` and `delete` are wrappers around `malloc` and `free`. This is why it is important to understand how to analyze both types of memory usage. + +## JavaScript Heap Dump + +Node has built in `V8` heap dump access and its a very powerful tool for analyzing memory usage. Understanding how the dump is created will both help to understand how it is displayed and how to use the analysis more effectively. + +The `V8` heap dump is a stop the world process because walking the entire heap graph is necessary to create one. This is similar to a full, major garbage collection event. The VM starts at the heap entrance node and walks the entire graph and makes note of every edge that connects each node along the way. Nodes are JSObjects and edges are references between those objects. + +By time the whole heap is walked the full size and values of all nodes are known and all of the connections between those nodes is well understood. The object that is returned is a set of three arrays, the nodes, the edges and the string values that are encountered (because strings are themselves arrays of characters in `C` so they are treated a bit differently by `V8`). + +### Creating a `V8` heap dump + +There are two functions for creating a heap dump but both call the same functionality under the hood. One streams the result, `require("v8").getHeapSnapshot([options])`, and is primarily intended for use by the Chrome devtools button to "take a snapshot". The second writes the heap dump to a file, `require("v8").writeHeapSnapshot(filename[,options])`. + +The optional `options` argument, in both cases, is the same and contains two props.`exposeInternals` and `exposeNumericValues` to enrich the dump. In many cases its the application layer that one wants to debug so `exposeInternals` is not usually necessary. In `V8` numbers are stored as 32bit integers and the size of pointers is also 32bits. So as an optimization, the pointer to the numeric value can be eliminated and the value itself can be stored in the `Address` of the `Value` instead. `exposeNumericValues` transcribes those "pointers" to the actual numeric value and appends them to the dump. + +Because heap analysis happens frequently during Lodestar development there is a helper api endpoint to capture a heap dump. **It is IMPORTANT** that this endpoint is not public facing as it will open the threat of DDOS attack. + +The endpoint accepts a `POST` request and you may include an optional `dirpath` query parameter to specify the directory where the heap dump will be written. If the `dirpath` is not specified then the heap dump will be written to the current working directory. + +To create a Lodestar heap dump you can use the following command: + +```sh +curl -X POST http://localhost:9596/eth/v1/lodestar/write_heapdump?dirpath=/some/directory/path +``` + +### Viewing a `V8` heap dump + +It is best to analyze on a local development machine so if Lodestar is running on a cloud instance download the dump to the local environment. Open Chrome, or any Chromium based browser (the example photos were taken using Brave). In the url bar type `chrome:://inspect` to bring up the DevTools menu (in brave the url will be rewritten to `brave://inspect`). + +![DevTools](../images/heap-dumps/devtools.png) + +Click on the `Open dedicated DevTools for Node` link to open the node specific window and click on the `Memory` tab as shown below. + +![Memory Tab](../images/heap-dumps/memory-tab.png) + +Load the profile by either right clicking on the left pane or by clicking the `Load` button at the bottom. + +![Load Profile](../images/heap-dumps/load-profile.png) + +### Analyzing a `V8` heap dump + +Analysis is as much an art as it is a science and the best way to learn is to do it a few times. Generally the goal is looking for memory leaks but reducing memory overhead is also something that happens. This guide will focus on leaks. With memory leaks one is looking for why objects have references that prevent them from being garbage collected. + +To spot sources of leaks, focus on objects that have large quantities or very large `retained size`. Retained size is the amount of memory that would be freed if the object was garbage collected. As an example if there is an object that has lots and lots of instances, like 100,000, and they are all pushed into an array then the array will have a very large retained size. This is because the array is holding references to all of the objects that it contains. + + + + +If it is not immediately apparent what objects are being leaked then another tool in your arsenal will be to take a second snapshot and compare it to the first. This will show what objects have been created/changed since the first snapshot. + +If there is an object that has a large retained size but is roughly the same, but not exactly the same, changes are that is NOT the leak. Some objects can get quite large during runtime but if its roughly the same size over time, but not exactly the same, it means that the application is modifying the object (why its not exactly identical in size) but if it hasn't grown significantly over time it can be assumed it is probably the working size of the instances. + +Try to focus on objects that are growing in size or in number over time. Growing in size means the object is holding references to other objects and growing in number means a function closure somewhere is retaining the small instances. + + + + +That is the science part, but these clues are just breadcrumbs to follow. In order to actually resolve the leak, one needs to go into the code to figure out where those objects are being created, or more often, why the references to them are being retained. This is where the art comes in. + +Having a good understanding of the codebase will help to narrow down where to look. It is also common that the leak is not coming directly from Lodestar code, but rather one of the dependencies so be careful not to rule those out. + +## Native Heap Dump + +_**note: collecting a native heap dump is only supported on linux, analysis can be done from linux or Mac**_ + +There are several tools that can be used to do native heap dump analysis. The most common are [`massif`](https://valgrind.org/docs/manual/ms-manual.html) from the [`Valgrind`](https://valgrind.org/) suite, google's [`gperftools`](https://github.com/gperftools/gperftools) and `heaptrack` from [KDE](https://community.kde.org/Main_Page). Of the three, `heaptrack` is the most user friendly tool, and it is specifically designed for the task. It is much faster than `Valgrind`, easier to integrate than `gperftools` and also includes a gui for result analysis. Often times there are also memory allocations that are not related to memory leaks, and tools like `Valgrind` and `gperftools` become less useful. This is why `heaptrack` is the recommended tool for heap dump analysis on Lodestar. + +There are a few things that will make the results with `heaptrack` far better. The most important is using debug builds of all libraries included in a binary, including the application itself. This will make the results usable. Not to say that they will be useless without debug symbols but it will be kinda tough to optimize functions without knowing the function names nor the file and line numbers. + +This is the heart of what `heaptrack` will do for us. It hooks into the memory allocation and adds in stack traces for each `malloc` call site. That way every time memory is reserved there is a way to track back where it happened in the code. `heaptrack` also hooks into the `free` function and checks that versus the allocations to check for memory leaks and for temporary variables that can be optimized. This also allows for optimization of how many of each object is created by identifying high frequency allocations. + +Generally the .heapdump file will be created on a cloud server and then copied to a local machine for analysis, mostly because the gui is not available through ssh. The gui is not required for analysis but it is much easier to use than the command line tools. The first step will be to install `heaptrack` on the target server and to capture a profile. + +### Build collection tools + +Assume the following directory structure: + +```sh +├── beacon-node +│   ├── db +│   ├── logs +│   ├── start-lodestar.sh +│   └── rc-config.yml +├── lodestar +└── node # step below will clone this repo +``` + +We will start from the directory that contains `lodestar` and the `beacon-node` files. + +```sh +# Install heaptrack +$ sudo apt-get update +$ sudo apt-get -y install heaptrack + +# Using a debug build of node is recommended and it can be build +# from source. Clone the node repo to get started. +$ git clone https://github.com/nodejs/node.git +$ cd node + +# Use whichever version of node you prefer +$ git checkout v20.10.0 +$ ./configure --debug + +# This command only builds the debug version of node and assumes +# that a release version of node is already installed on the system +$ make -C out BUILDTYPE=Debug -j$(nproc --all) + +# Move the debug version of node the the same folder that the release +# version is installed in and name it `node_debug`. This will put the +# debug binary on the path and allow you to run it with the +# `node_debug` command +$ cp out/Debug/node "$(which node)_debug" +$ which node_debug +/your/home/directory/.nvm/versions/node/v20.10.0/bin/node_debug + +# Return to the lodestar repo +$ cd ../lodestar + +# Clean the build artifacts and node_modules +$ yarn clean && yarn clean:nm + +# Install the dependencies +$ yarn install + +# Ensure that all native modules are rebuilt with debug symbols. Some +# modules are prebuilt, like classic-level, and the debug symbols may +# not be included. If the the debugging exercise is focussed around +# one of these dependencies, then you will need to manually clone those +# repos and manually build them with debug symbols. +$ npm rebuild --debug +``` + +### Collect a heap dump + +```sh +# Move to th `beacon-node` directory +$ cd ../beacon-node + +# Start lodestar with profiling enabled +$ heaptrack \ +$ --output ./lodestar.heapdump \ +$ node_debug \ +$ --max-old-space-size=8192 \ +$ ../lodestar/packages/cli/bin/lodestar.js \ +$ beacon \ +$ --rcConfig ./rc-config.yml \ +$ > /dev/null 2>&1 & +# Wait some period of time for the heap dump data to be collected + +# The data will not be persisted until the process is stopped. You can gracefully +# stop the process with the following command and if you want to hard kill it +# add `-9` to the end of the `kill` command although that should not be necessary +$ ps aux | grep lodestar | grep -v grep | awk '{print $2}' | head -n 1 | xargs kill +``` + +### Collecting a heap dump on a running process + +Collecting a heap dump can also be done on a running process. There are both advantages and disadvantages to this approach. The main advantage is that you can collect a heap dump without having to restart. The down side is that the dump will only include allocations/de-allocations while the tracker is running. This means that all the non-paired calls to malloc/free will register as leaks. It will also not give a true representation of how the heap is being used. On the upside, however the dump will be much smaller in size. + +It is important to note a warning that is in the `heaptrack` source code: + +_WARNING: Runtime-attaching heaptrack is UNSTABLE and can lead to CRASHES in your application, especially after you detach heaptrack again. You are hereby warned, use it at your own risk!_ + +```sh +# Move to th `beacon-node` directory +$ cd ../beacon-node + +# Start lodestar +$ node_debug \ +$ --max-old-space-size=8192 \ +$ ../lodestar/packages/cli/bin/lodestar.js \ +$ beacon \ +$ --rcConfig ./rc-config.yml \ +$ > /dev/null 2>&1 & +# Wait some period of time to start collecting the dump + +# GDB is required to inject heaptrack into a running process +# so you may need to install it +$ sudo apt-get update +$ sudo apt-get install -y gdb + +# Elevated `perf` permissions are also required depending on your +# system configuration. Change until the next reboot +$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope + +# Get the pid of the lodestar process +$ export LODESTAR_PID=$(ps aux | grep lodestar | grep -v grep | awk '{print $2}' | head -n 1) + +# Inject heaptrack into the running process +$ heaptrack --pid $LODESTAR_PID + +heaptrack output will be written to "/home/user/beacon-node/heaptrack.node_debug.111868.zst" +/usr/lib/heaptrack/libheaptrack_preload.so +injecting heaptrack into application via GDB, this might take some time... +injection finished +# Wait some period of time to collect the heap dump. See below +# for the termination command that can be run from a separate +# terminal when ready to stop collecting data +Terminated +removing heaptrack injection via GDB, this might take some time... +Heaptrack finished! Now run the following to investigate the data: + + heaptrack --analyze "/home/user/beacon-node/heaptrack.node_debug.111868.zst" +``` + +There is a trap in `heaptrack` but the process uses a nested shell to do the actual injection so it is not possible to just Ctrl+C out of the injected process without corrupting the output file. To properly kill the collection one needs to target the nested shell pid. Here is a helper command to target that process: + +```sh +ps -ef | grep '[h]eaptrack --pid' | awk '$3 == '$(ps -ef | grep '[h]eaptrack --pid' | awk '$3 != 1 {print $2}' | head -n 1)' {print $2}' | xargs -r kill +``` + +After working with the injected process for a while, I cannot honestly recommend it. It can work in a pinch, and is best suited for when the profiled process can be exited gracefully without repercussions (not on mainnet for instance). The benefit, though, is that the heapdump will be much smaller and targeted to runtime (will not have the transient, startup allocations) which can make it easier to see what is happening. + +### Installing `heaptrack-gui` on Linux + +```sh +# You can you apt, apt-get or aptitude to install the gui +$ sudo apt-get update +$ sudo apt-get install -y heaptrack-gui +``` + +### Installing `heaptrack-gui` on OSX + +At the time of writing this there is no official pre-built binary for OSX. This was a bit of a challenge but it was WELL worth the effort as the tool works very well. There were a number of bugs along the way while "using the docs" so your mileage may vary, but this is what worked for me. + +Most of the dependencies can be installed via Homebrew and the tool itself needs to be built from source. There was one dependency that needed to be built from source. This process assumes a working folder that the repos can be cloned into. + +```sh +# Start in the root folder where the repos will be cloned +$ brew install qt@5 + +# prepare tap of kde-mac/kde +$ brew tap kde-mac/kde https://invent.kde.org/packaging/homebrew-kde.git +$ "$(brew --repo kde-mac/kde)/tools/do-caveats.sh" + +# install the kde-mac and other required dependencies +$ brew install kde-mac/kde/kf5-kcoreaddons \ +$ kde-mac/kde/kf5-kitemmodels \ +$ kde-mac/kde/kf5-kconfigwidgets \ +$ kde-mac/kde/kdiagram \ +$ extra-cmake-modules \ +$ ki18n \ +$ threadweaver \ +$ boost \ +$ zstd \ +$ gettext + +# There is a bug in the current version of kde-mac/kde and one dependency needs +# to be built manually. This is the workaround to get it built. +$ git clone https://invent.kde.org/frameworks/kio.git +$ mkdir kio/build +$ cd kio/build +$ export CMAKE_PREFIX_PATH=$(brew --prefix qt@5) +$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release .. +$ ninja +$ sudo ninja install +$ cd ../.. + +# Now make sure that the dependencies are available to the system during runtime +$ ln -sfv "$(brew --prefix)/share/kf5" "$HOME/Library/Application Support" +$ ln -sfv "$(brew --prefix)/share/knotifications5" "$HOME/Library/Application Support" +$ ln -sfv "$(brew --prefix)/share/kservices5" "$HOME/Library/Application Support" +$ ln -sfv "$(brew --prefix)/share/kservicetypes5" "$HOME/Library/Application Support" + +# We are now ready to build the heaptrack_gui binaries for analysis on OSX +$ git clone https://invent.kde.org/sdk/heaptrack.git +$ cd heaptrack +$ mkdir build +$ cd build +$ CMAKE_PREFIX_PATH=$(brew --prefix qt@5) PATH=$PATH:/opt/homebrew/opt/gettext/bin cmake .. +$ cmake -DCMAKE_BUILD_TYPE=Release .. +$ make heaptrack_gui +$ sudo make install +# You can now find heaptrack_gui with your gui Applications. It is default +# placed as /Applications/KDE/heaptrack_gui.app +``` diff --git a/docs/pages/tools/perf.md b/docs/pages/tools/perf.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/trouble-shooting.md b/docs/pages/trouble-shooting.md new file mode 100644 index 000000000000..144aeb90ce20 --- /dev/null +++ b/docs/pages/trouble-shooting.md @@ -0,0 +1 @@ +# Trouble Shooting diff --git a/docs/pages/validator-management/key-management.md b/docs/pages/validator-management/key-management.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/pages/validator-management/multiple-and-fallback-validation.md b/docs/pages/validator-management/multiple-and-fallback-validation.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/usage/validator-management.md b/docs/pages/validator-management/validator-management.md similarity index 100% rename from docs/usage/validator-management.md rename to docs/pages/validator-management/validator-management.md diff --git a/docs/pages/validator-management/withdrawals.md b/docs/pages/validator-management/withdrawals.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lerna.json b/lerna.json index ebae5dcafabe..bb0c43fed5fe 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ ], "npmClient": "yarn", "useNx": true, - "version": "1.12.1", + "version": "1.13.0", "stream": true, "command": { "version": { diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 759a8dfd7151..000000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,85 +0,0 @@ -site_name: Lodestar Documentation -site_description: Lodestar Documentation - Typescript Ethereum Consensus client -site_url: https://chainsafe.github.io/lodestar - -repo_name: chainsafe/lodestar -repo_url: https://github.com/chainsafe/lodestar - -# Configuration -theme: - name: material - logo: assets/lodestar_icon_300.png - favicon: assets/round-icon.ico - palette: - - scheme: preference - media: "(prefers-color-scheme: light)" - primary: black - accent: deep purple - toggle: - icon: material/weather-night - name: Switch to dark mode - - scheme: slate - media: "(prefers-color-scheme: dark)" - primary: black - accent: deep purple - toggle: - icon: material/weather-sunny - name: Switch to light mode - nav_style: dark - -plugins: - - search - - mermaid2: - version: 8.6.4 - arguments: - theme: | - ^(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light' - -markdown_extensions: - - meta - - codehilite: - guess_lang: false - - admonition - - toc: - permalink: true - - pymdownx.superfences: - # make exceptions to highlighting of code (for mermaid): - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:mermaid2.fence_mermaid -extra_css: - - stylesheets/extras.css - -# Socials -extra: - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/ChainSafe/lodestar - - icon: fontawesome/brands/twitter - link: https://twitter.com/ChainSafeth - - icon: fontawesome/brands/discord - link: https://discord.gg/yjyvFRP - - icon: fontawesome/brands/medium - link: https://blog.chainsafe.io - -# Customize left navigation menu -nav: - - Getting Started: index.md - - Installation: - - Install from source: install/source.md - - Install from NPM: install/npm.md - - Install with Docker: install/docker.md - - Using Lodestar: - - Beacon management: usage/beacon-management.md - - Local testnet: usage/local.md - - Validator management: usage/validator-management.md - - Prometheus & Grafana Setup: usage/prometheus-grafana.md - - MEV Builder Integration: usage/mev-integration.md - - Client monitoring: usage/client-monitoring.md - - Reference: - - Command line: reference/cli.md - - Libraries: libraries/index.md - - Design: - - Lodestar package structure: design/depgraph.md - - Contributing: contributing.md diff --git a/package.json b/package.json index c8910209a83b..158ac2affe68 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "clean": "rm -rf ./packages/*/lib ./packages/*/*.tsbuildinfo", "clean:nm": "rm -rf ./packages/*/node_modules ./node_modules", "build": "lerna run build", - "build:docs": "lerna run build:refdocs && ./scripts/prepare-docs.sh", + "build:docs": "lerna run check-readme && lerna run build:docs && ./scripts/prepare-docs.sh", "build:watch": "lerna exec --parallel -- 'yarn run build:watch'", "build:ifchanged": "lerna exec -- ../../scripts/build_if_changed.sh", "lint": "eslint --color --ext .ts packages/*/src packages/*/test", @@ -22,6 +22,7 @@ "check-build": "lerna run check-build", "check-readme": "lerna run check-readme", "check-types": "lerna run check-types", + "check-spelling": "pyspelling -c .pyspelling.yml -v", "coverage": "lerna run coverage", "test": "lerna run test --concurrency 1", "test:unit": "lerna run test:unit --concurrency 1", @@ -52,6 +53,8 @@ "@types/sinon-chai": "^3.2.9", "@typescript-eslint/eslint-plugin": "6.7.2", "@typescript-eslint/parser": "6.7.2", + "@vitest/coverage-v8": "^1.0.1", + "@vitest/browser": "^1.0.1", "c8": "^8.0.1", "chai": "^4.3.8", "chai-as-promised": "^7.1.1", @@ -59,12 +62,13 @@ "crypto-browserify": "^3.12.0", "electron": "^26.2.2", "eslint": "^8.50.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-prettier": "^5.0.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-chai-expect": "^3.0.0", + "eslint-plugin-import": "^2.28.1", "eslint-plugin-mocha": "^10.2.0", - "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-prettier": "^5.0.0", "https-browserify": "^1.0.0", + "jsdom": "^23.0.1", "karma": "^6.4.2", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^3.2.0", @@ -93,13 +97,17 @@ "ts-node": "^10.9.1", "typescript": "^5.2.2", "typescript-docs-verifier": "^2.5.0", - "webpack": "^5.88.2", + "vite-plugin-node-polyfills": "^0.17.0", + "vite-plugin-top-level-await": "^1.3.1", + "vitest": "^1.0.2", + "vitest-when": "^0.3.0", "wait-port": "^1.1.0", - "vitest": "^0.34.6", - "vitest-when": "^0.2.0", - "@vitest/coverage-v8": "^0.34.6" + "webdriverio": "^8.24.12", + "webpack": "^5.88.2" }, "resolutions": { - "dns-over-http-resolver": "^2.1.1" + "dns-over-http-resolver": "^2.1.1", + "chai": "^4.3.10", + "loupe": "^2.3.6" } } diff --git a/packages/api/.mocharc.yaml b/packages/api/.mocharc.yaml deleted file mode 100644 index f9375365e517..000000000000 --- a/packages/api/.mocharc.yaml +++ /dev/null @@ -1,8 +0,0 @@ -colors: true -timeout: 2000 -exit: true -extension: ["ts"] -require: - - ./test/setup.ts -node-option: - - "loader=ts-node/esm" diff --git a/packages/api/.nycrc.json b/packages/api/.nycrc.json deleted file mode 100644 index 69aa626339a0..000000000000 --- a/packages/api/.nycrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../.nycrc.json" -} diff --git a/packages/api/package.json b/packages/api/package.json index 99f5094b4d24..2dfcbc73b65c 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -65,16 +65,16 @@ "lint:fix": "yarn run lint --fix", "pretest": "yarn run check-types", "test": "yarn test:unit && yarn test:e2e", - "test:unit": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'", + "test:unit": "vitest --run --dir test/unit/ --coverage", "check-readme": "typescript-docs-verifier" }, "dependencies": { "@chainsafe/persistent-merkle-tree": "^0.6.1", "@chainsafe/ssz": "^0.14.0", - "@lodestar/config": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/config": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", "eventsource": "^2.0.2", "qs": "^6.11.1" }, diff --git a/packages/api/src/beacon/client/events.ts b/packages/api/src/beacon/client/events.ts index 19252c8a7c9e..4be5078735e4 100644 --- a/packages/api/src/beacon/client/events.ts +++ b/packages/api/src/beacon/client/events.ts @@ -1,6 +1,6 @@ import {ChainForkConfig} from "@lodestar/config"; import {Api, BeaconEvent, routesData, getEventSerdes} from "../routes/events.js"; -import {stringifyQuery} from "../../utils/client/format.js"; +import {stringifyQuery, urlJoin} from "../../utils/client/format.js"; import {getEventSource} from "../../utils/client/eventSource.js"; import {HttpStatusCode} from "../../utils/client/httpStatusCode.js"; @@ -13,8 +13,7 @@ export function getClient(config: ChainForkConfig, baseUrl: string): Api { return { eventstream: async (topics, signal, onEvent) => { const query = stringifyQuery({topics}); - // TODO: Use a proper URL formatter - const url = `${baseUrl}${routesData.eventstream.url}?${query}`; + const url = `${urlJoin(baseUrl, routesData.eventstream.url)}?${query}`; // eslint-disable-next-line @typescript-eslint/naming-convention const EventSource = await getEventSource(); const eventSource = new EventSource(url); @@ -58,4 +57,4 @@ export function getClient(config: ChainForkConfig, baseUrl: string): Api { } // https://github.com/EventSource/eventsource/blob/82e034389bd2c08d532c63172b8e858c5b185338/lib/eventsource.js#L143 -type EventSourceError = {status: number; message: string}; +type EventSourceError = {status?: number; message: string}; diff --git a/packages/api/src/beacon/routes/lodestar.ts b/packages/api/src/beacon/routes/lodestar.ts index 8979f31a14c1..3c12fa1d4b80 100644 --- a/packages/api/src/beacon/routes/lodestar.ts +++ b/packages/api/src/beacon/routes/lodestar.ts @@ -77,9 +77,12 @@ export type LodestarNodePeer = NodePeer & { export type LodestarThreadType = "main" | "network" | "discv5"; export type Api = { - /** Trigger to write a heapdump to disk at `dirpath`. May take > 1min */ - writeHeapdump(dirpath?: string): Promise>; - /** Trigger to write 10m network thread profile to disk */ + /** Trigger to write a heapdump of either main/network/discv5 thread to disk at `dirpath`. May take > 1min */ + writeHeapdump( + thread?: LodestarThreadType, + dirpath?: string + ): Promise>; + /** Trigger to write profile of either main/network/discv5 thread to disk */ writeProfile( thread?: LodestarThreadType, duration?: number, @@ -153,7 +156,7 @@ export const routesData: RoutesData = { }; export type ReqTypes = { - writeHeapdump: {query: {dirpath?: string}}; + writeHeapdump: {query: {thread?: LodestarThreadType; dirpath?: string}}; writeProfile: {query: {thread?: LodestarThreadType; duration?: number; dirpath?: string}}; getLatestWeakSubjectivityCheckpointEpoch: ReqEmpty; getSyncChainsDebugState: ReqEmpty; @@ -176,8 +179,8 @@ export type ReqTypes = { export function getReqSerializers(): ReqSerializers { return { writeHeapdump: { - writeReq: (dirpath) => ({query: {dirpath}}), - parseReq: ({query}) => [query.dirpath], + writeReq: (thread, dirpath) => ({query: {thread, dirpath}}), + parseReq: ({query}) => [query.thread, query.dirpath], schema: {query: {dirpath: Schema.String}}, }, writeProfile: { diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index d7c063d38ede..f5ae20937a0e 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -18,6 +18,7 @@ import { StringType, SubcommitteeIndex, Wei, + Gwei, } from "@lodestar/types"; import {ApiClientResponse} from "../../interfaces.js"; import {HttpStatusCode} from "../../utils/client/httpStatusCode.js"; @@ -27,7 +28,7 @@ import { ArrayOf, Schema, WithVersion, - WithExecutionPayloadValue, + WithBlockValues, reqOnlyBody, ReqSerializers, jsonType, @@ -54,11 +55,11 @@ export type ExtraProduceBlockOps = { strictFeeRecipientCheck?: boolean; }; -export type ProduceBlockOrContentsRes = {executionPayloadValue: Wei} & ( +export type ProduceBlockOrContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Gwei} & ( | {data: allForks.BeaconBlock; version: ForkPreBlobs} | {data: allForks.BlockContents; version: ForkBlobs} ); -export type ProduceBlindedBlockOrContentsRes = {executionPayloadValue: Wei} & ( +export type ProduceBlindedBlockOrContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Gwei} & ( | {data: allForks.BlindedBeaconBlock; version: ForkPreBlobs} | {data: allForks.BlindedBlockContents; version: ForkBlobs} ); @@ -715,12 +716,12 @@ export function getReturnTypes(): ReturnTypes { {jsonCase: "eth2"} ); - const produceBlockOrContents = WithExecutionPayloadValue( + const produceBlockOrContents = WithBlockValues( WithVersion((fork: ForkName) => isForkBlobs(fork) ? allForksBlockContentsResSerializer(fork) : ssz[fork].BeaconBlock ) ) as TypeJson; - const produceBlindedBlockOrContents = WithExecutionPayloadValue( + const produceBlindedBlockOrContents = WithBlockValues( WithVersion((fork: ForkName) => isForkBlobs(fork) ? allForksBlindedBlockContentsResSerializer(fork) diff --git a/packages/api/src/beacon/server/beacon.ts b/packages/api/src/beacon/server/beacon.ts index da5a0997a0d8..c71decd1ac8e 100644 --- a/packages/api/src/beacon/server/beacon.ts +++ b/packages/api/src/beacon/server/beacon.ts @@ -1,4 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; +import {ssz} from "@lodestar/types"; import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/beacon/index.js"; import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; @@ -30,15 +31,35 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR }, getBlockV2: { ...serverRoutes.getBlockV2, - handler: async (req) => { + handler: async (req, res) => { const response = await api.getBlockV2(...reqSerializers.getBlockV2.parseReq(req)); if (response instanceof Uint8Array) { + const slot = extractSlotFromBlockBytes(response); + const version = config.getForkName(slot); + void res.header("Eth-Consensus-Version", version); // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer return Buffer.from(response); } else { + void res.header("Eth-Consensus-Version", response.version); return returnTypes.getBlockV2.toJson(response); } }, }, }; } + +function extractSlotFromBlockBytes(block: Uint8Array): number { + const {signature} = ssz.phase0.SignedBeaconBlock.fields; + /** + * class SignedBeaconBlock(Container): + * message: BeaconBlock [offset - 4 bytes] + * signature: BLSSignature [fixed - 96 bytes] + * + * class BeaconBlock(Container): + * slot: Slot [fixed - 8 bytes] + * ... + */ + const offset = 4 + signature.lengthBytes; + const bytes = block.subarray(offset, offset + ssz.Slot.byteLength); + return ssz.Slot.deserialize(bytes); +} diff --git a/packages/api/src/beacon/server/debug.ts b/packages/api/src/beacon/server/debug.ts index 7dd767d4a2fd..fd9de661edd2 100644 --- a/packages/api/src/beacon/server/debug.ts +++ b/packages/api/src/beacon/server/debug.ts @@ -1,4 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; +import {ssz} from "@lodestar/types"; import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/debug.js"; import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; @@ -31,15 +32,33 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR }, getStateV2: { ...serverRoutes.getStateV2, - handler: async (req) => { + handler: async (req, res) => { const response = await api.getStateV2(...reqSerializers.getStateV2.parseReq(req)); if (response instanceof Uint8Array) { + const slot = extractSlotFromStateBytes(response); + const version = config.getForkName(slot); + void res.header("Eth-Consensus-Version", version); // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer return Buffer.from(response); } else { + void res.header("Eth-Consensus-Version", response.version); return returnTypes.getStateV2.toJson(response); } }, }, }; } + +function extractSlotFromStateBytes(state: Uint8Array): number { + const {genesisTime, genesisValidatorsRoot} = ssz.phase0.BeaconState.fields; + /** + * class BeaconState(Container): + * genesisTime: BeaconBlock [offset - 4 bytes] + * genesisValidatorsRoot: BLSSignature [fixed - 96 bytes] + * slot: Slot [fixed - 8 bytes] + * ... + */ + const offset = genesisTime.byteLength + genesisValidatorsRoot.lengthBytes; + const bytes = state.subarray(offset, offset + ssz.Slot.byteLength); + return ssz.Slot.deserialize(bytes); +} diff --git a/packages/api/src/utils/client/httpClient.ts b/packages/api/src/utils/client/httpClient.ts index 2070d8c582f1..674c6ddadbe8 100644 --- a/packages/api/src/utils/client/httpClient.ts +++ b/packages/api/src/utils/client/httpClient.ts @@ -292,7 +292,7 @@ export class HttpClient implements IHttpClient { } if (url.username || url.password) { if (headers["Authorization"] === undefined) { - headers["Authorization"] = `Basic ${toBase64(`${url.username}:${url.password}`)}`; + headers["Authorization"] = `Basic ${toBase64(decodeURIComponent(`${url.username}:${url.password}`))}`; } // Remove the username and password from the URL url.username = ""; diff --git a/packages/api/src/utils/types.ts b/packages/api/src/utils/types.ts index fc7e86a8dbc0..b01649a0ef38 100644 --- a/packages/api/src/utils/types.ts +++ b/packages/api/src/utils/types.ts @@ -179,20 +179,26 @@ export function WithExecutionOptimistic( } /** - * SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei}` + * SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei, consensusBlockValue: GWei}` */ -export function WithExecutionPayloadValue( +export function WithBlockValues( type: TypeJson -): TypeJson { +): TypeJson { return { - toJson: ({executionPayloadValue, ...data}) => ({ + toJson: ({executionPayloadValue, consensusBlockValue, ...data}) => ({ ...(type.toJson(data as unknown as T) as Record), execution_payload_value: executionPayloadValue.toString(), + consensus_block_value: consensusBlockValue.toString(), }), - fromJson: ({execution_payload_value, ...data}: T & {execution_payload_value: string}) => ({ + fromJson: ({ + execution_payload_value, + consensus_block_value, + ...data + }: T & {execution_payload_value: string; consensus_block_value: string}) => ({ ...type.fromJson(data), // For cross client usage where beacon or validator are of separate clients, executionPayloadValue could be missing executionPayloadValue: BigInt(execution_payload_value ?? "0"), + consensusBlockValue: BigInt(consensus_block_value ?? "0"), }), }; } diff --git a/packages/api/test/globalSetup.ts b/packages/api/test/globalSetup.ts new file mode 100644 index 000000000000..0ab57c057472 --- /dev/null +++ b/packages/api/test/globalSetup.ts @@ -0,0 +1,2 @@ +export async function setup(): Promise {} +export async function teardown(): Promise {} diff --git a/packages/api/test/setup.ts b/packages/api/test/setup.ts deleted file mode 100644 index b83e6cb78511..000000000000 --- a/packages/api/test/setup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import sinonChai from "sinon-chai"; - -chai.use(chaiAsPromised); -chai.use(sinonChai); diff --git a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts index ae4428b9fc8e..7972e4bfca65 100644 --- a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {Api, ReqTypes} from "../../../../src/beacon/routes/beacon/index.js"; import {getClient} from "../../../../src/beacon/client/beacon.js"; diff --git a/packages/api/test/unit/beacon/genericServerTest/config.test.ts b/packages/api/test/unit/beacon/genericServerTest/config.test.ts index da791aa2c334..e11e4cbff6cb 100644 --- a/packages/api/test/unit/beacon/genericServerTest/config.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/config.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {config} from "@lodestar/config/default"; import {Api, ReqTypes, getReturnTypes} from "../../../../src/beacon/routes/config.js"; import {getClient} from "../../../../src/beacon/client/config.js"; @@ -27,6 +27,6 @@ describe("beacon / config", () => { const jsonRes = returnTypes.getSpec.toJson({data: partialJsonSpec}); const specRes = returnTypes.getSpec.fromJson(jsonRes); - expect(specRes).to.deep.equal({data: partialJsonSpec}, "Wrong toJson -> fromJson"); + expect(specRes).toEqual({data: partialJsonSpec}); }); }); diff --git a/packages/api/test/unit/beacon/genericServerTest/debug.test.ts b/packages/api/test/unit/beacon/genericServerTest/debug.test.ts index 44b080e29bf4..6f7889677ec6 100644 --- a/packages/api/test/unit/beacon/genericServerTest/debug.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/debug.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect, MockInstance} from "vitest"; import {toHexString} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; import {config} from "@lodestar/config/default"; @@ -11,40 +11,42 @@ import {registerRoute} from "../../../../src/utils/server/registerRoute.js"; import {HttpClient} from "../../../../src/utils/client/httpClient.js"; import {testData} from "../testData/debug.js"; -describe("beacon / debug", function () { +describe( + "beacon / debug", + function () { + describe("Run generic server test", () => { + runGenericServerTest(config, getClient, getRoutes, testData); + }); + + // Get state by SSZ + + describe("getState() in SSZ format", () => { + const {baseUrl, server} = getTestServer(); + const mockApi = getMockApi(routesData); + for (const route of Object.values(getRoutes(config, mockApi))) { + registerRoute(server, route); + } + + for (const method of ["getState" as const, "getStateV2" as const]) { + it(method, async () => { + const state = ssz.phase0.BeaconState.defaultValue(); + const stateSerialized = ssz.phase0.BeaconState.serialize(state); + (mockApi[method] as MockInstance).mockResolvedValue(stateSerialized); + + const httpClient = new HttpClient({baseUrl}); + const client = getClient(config, httpClient); + + const res = await client[method]("head", "ssz"); + + expect(res.ok).toBe(true); + + if (res.ok) { + expect(toHexString(res.response)).toBe(toHexString(stateSerialized)); + } + }); + } + }); + }, // Extend timeout since states are very big - this.timeout(30 * 1000); - - describe("Run generic server test", () => { - runGenericServerTest(config, getClient, getRoutes, testData); - }); - - // Get state by SSZ - - describe("getState() in SSZ format", () => { - const {baseUrl, server} = getTestServer(); - const mockApi = getMockApi(routesData); - for (const route of Object.values(getRoutes(config, mockApi))) { - registerRoute(server, route); - } - - for (const method of ["getState" as const, "getStateV2" as const]) { - it(method, async () => { - const state = ssz.phase0.BeaconState.defaultValue(); - const stateSerialized = ssz.phase0.BeaconState.serialize(state); - mockApi[method].resolves(stateSerialized); - - const httpClient = new HttpClient({baseUrl}); - const client = getClient(config, httpClient); - - const res = await client[method]("head", "ssz"); - - expect(res.ok).to.be.true; - - if (res.ok) { - expect(toHexString(res.response)).to.equal(toHexString(stateSerialized), "returned state value is not equal"); - } - }); - } - }); -}); + {timeout: 30 * 1000} +); diff --git a/packages/api/test/unit/beacon/genericServerTest/events.test.ts b/packages/api/test/unit/beacon/genericServerTest/events.test.ts index deaf0da9c1b9..48ff8ad3d157 100644 --- a/packages/api/test/unit/beacon/genericServerTest/events.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/events.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect, beforeEach, afterEach} from "vitest"; import {sleep} from "@lodestar/utils"; import {config} from "@lodestar/config/default"; import {Api, routesData, EventType, BeaconEvent} from "../../../../src/beacon/routes/events.js"; @@ -16,7 +16,9 @@ describe("beacon / events", () => { } let controller: AbortController; - beforeEach(() => (controller = new AbortController())); + beforeEach(() => { + controller = new AbortController(); + }); afterEach(() => controller.abort()); it("Receive events", async () => { @@ -38,9 +40,9 @@ describe("beacon / events", () => { const eventsReceived: BeaconEvent[] = []; await new Promise((resolve, reject) => { - mockApi.eventstream.callsFake(async (topics, signal, onEvent) => { + mockApi.eventstream.mockImplementation(async (topics, signal, onEvent) => { try { - expect(topics).to.deep.equal(topicsToRequest, "Wrong received topics"); + expect(topics).toEqual(topicsToRequest); for (const event of eventsToSend) { onEvent(event); await sleep(5); @@ -58,6 +60,6 @@ describe("beacon / events", () => { }); }); - expect(eventsReceived).to.deep.equal(eventsToSend, "Wrong received events"); + expect(eventsReceived).toEqual(eventsToSend); }); }); diff --git a/packages/api/test/unit/beacon/genericServerTest/lightclient.test.ts b/packages/api/test/unit/beacon/genericServerTest/lightclient.test.ts index 888236dd32f7..10031a150490 100644 --- a/packages/api/test/unit/beacon/genericServerTest/lightclient.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/lightclient.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {config} from "@lodestar/config/default"; import {Api, ReqTypes} from "../../../../src/beacon/routes/lightclient.js"; import {getClient} from "../../../../src/beacon/client/lightclient.js"; diff --git a/packages/api/test/unit/beacon/genericServerTest/node.test.ts b/packages/api/test/unit/beacon/genericServerTest/node.test.ts index cf87e78da4c1..059bd4ca2c88 100644 --- a/packages/api/test/unit/beacon/genericServerTest/node.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/node.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {config} from "@lodestar/config/default"; import {Api, ReqTypes} from "../../../../src/beacon/routes/node.js"; import {getClient} from "../../../../src/beacon/client/node.js"; diff --git a/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts b/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts index 5b3a8dea5b91..4619d20d989f 100644 --- a/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {config} from "@lodestar/config/default"; import {Api, ReqTypes} from "../../../../src/beacon/routes/proof.js"; import {getClient} from "../../../../src/beacon/client/proof.js"; diff --git a/packages/api/test/unit/beacon/genericServerTest/validator.test.ts b/packages/api/test/unit/beacon/genericServerTest/validator.test.ts index 399747a82d54..5a87ea9eee5f 100644 --- a/packages/api/test/unit/beacon/genericServerTest/validator.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/validator.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {config} from "@lodestar/config/default"; import {Api, ReqTypes} from "../../../../src/beacon/routes/validator.js"; import {getClient} from "../../../../src/beacon/client/validator.js"; diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index 5bfacce6a683..c1abd32cb591 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import {fileURLToPath} from "node:url"; -import {expect} from "chai"; +import {describe, it, beforeAll, expect} from "vitest"; import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {OpenApiFile} from "../../utils/parseOpenApiSpec.js"; import {routes} from "../../../src/beacon/index.js"; @@ -104,7 +104,7 @@ describe("eventstream event data", () => { const eventstreamExamples = openApiJson.paths["/eth/v1/events"]["get"].responses["200"].content?.["text/event-stream"].examples; - before("Check eventstreamExamples exists", () => { + beforeAll(() => { if (!eventstreamExamples) { throw Error(`eventstreamExamples not defined: ${eventstreamExamples}`); } @@ -136,7 +136,7 @@ describe("eventstream event data", () => { message: testEvent, } as routes.events.BeaconEvent); - expect(testEventJson).deep.equals(exampleDataJson, `eventTestData[${topic}] does not match spec's example`); + expect(testEventJson).toEqual(exampleDataJson); }); } }); diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 54b2537648cb..7fa8368c590b 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -9,8 +9,8 @@ import { } from "../../../../src/beacon/routes/beacon/index.js"; import {GenericServerTestCases} from "../../../utils/genericServerTest.js"; -const root = Buffer.alloc(32, 1); -const randao = Buffer.alloc(32, 1); +const root = new Uint8Array(32).fill(1); +const randao = new Uint8Array(32).fill(1); const balance = 32e9; const pubkeyHex = toHexString(Buffer.alloc(48, 1)); diff --git a/packages/api/test/unit/beacon/testData/config.ts b/packages/api/test/unit/beacon/testData/config.ts index 463d584880a3..642ed5e7e224 100644 --- a/packages/api/test/unit/beacon/testData/config.ts +++ b/packages/api/test/unit/beacon/testData/config.ts @@ -15,7 +15,7 @@ export const testData: GenericServerTestCases = { res: { data: { chainId: 1, - address: Buffer.alloc(20, 1), + address: new Uint8Array(20).fill(1), }, }, }, diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 92e413037bcf..af33f4a2b011 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -4,7 +4,7 @@ import {Api, EventData, EventType, blobSidecarSSE} from "../../../../src/beacon/ import {GenericServerTestCases} from "../../../utils/genericServerTest.js"; const abortController = new AbortController(); -const root = Buffer.alloc(32, 0); +const root = new Uint8Array(32); /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/naming-convention */ diff --git a/packages/api/test/unit/beacon/testData/lightclient.ts b/packages/api/test/unit/beacon/testData/lightclient.ts index 553f11d685d1..13e08e365987 100644 --- a/packages/api/test/unit/beacon/testData/lightclient.ts +++ b/packages/api/test/unit/beacon/testData/lightclient.ts @@ -46,6 +46,6 @@ export const testData: GenericServerTestCases = { }, getCommitteeRoot: { args: [1, 2], - res: {data: [Buffer.alloc(32, 0), Buffer.alloc(32, 1)]}, + res: {data: [Uint8Array.from(Buffer.alloc(32, 0)), Uint8Array.from(Buffer.alloc(32, 1))]}, }, }; diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index da245646f8d5..b827bad0be90 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -3,10 +3,10 @@ import {ssz} from "@lodestar/types"; import {Api} from "../../../../src/beacon/routes/validator.js"; import {GenericServerTestCases} from "../../../utils/genericServerTest.js"; -const ZERO_HASH = Buffer.alloc(32, 0); -const ZERO_HASH_HEX = "0x" + ZERO_HASH.toString("hex"); -const randaoReveal = Buffer.alloc(96, 1); -const selectionProof = Buffer.alloc(96, 1); +const ZERO_HASH = new Uint8Array(32); +const ZERO_HASH_HEX = "0x" + Buffer.from(ZERO_HASH).toString("hex"); +const randaoReveal = new Uint8Array(96).fill(1); +const selectionProof = new Uint8Array(96).fill(1); const graffiti = "a".repeat(32); const feeRecipient = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; @@ -17,7 +17,7 @@ export const testData: GenericServerTestCases = { executionOptimistic: true, data: [ { - pubkey: Buffer.alloc(48, 1), + pubkey: new Uint8Array(48).fill(1), validatorIndex: 2, committeeIndex: 3, committeeLength: 4, @@ -33,7 +33,7 @@ export const testData: GenericServerTestCases = { args: [1000], res: { executionOptimistic: true, - data: [{slot: 1, validatorIndex: 2, pubkey: Buffer.alloc(48, 3)}], + data: [{slot: 1, validatorIndex: 2, pubkey: new Uint8Array(48).fill(3)}], dependentRoot: ZERO_HASH_HEX, }, }, @@ -41,19 +41,32 @@ export const testData: GenericServerTestCases = { args: [1000, [1, 2, 3]], res: { executionOptimistic: true, - data: [{pubkey: Buffer.alloc(48, 1), validatorIndex: 2, validatorSyncCommitteeIndices: [3]}], + data: [{pubkey: Uint8Array.from(Buffer.alloc(48, 1)), validatorIndex: 2, validatorSyncCommitteeIndices: [3]}], }, }, produceBlock: { - args: [32000, randaoReveal, graffiti], + args: [ + 32000, + randaoReveal, + graffiti, + undefined, + {feeRecipient: undefined, builderSelection: undefined, strictFeeRecipientCheck: undefined}, + ] as unknown as GenericServerTestCases["produceBlock"]["args"], res: {data: ssz.phase0.BeaconBlock.defaultValue()}, }, produceBlockV2: { - args: [32000, randaoReveal, graffiti], + args: [ + 32000, + randaoReveal, + graffiti, + undefined, + {feeRecipient: undefined, builderSelection: undefined, strictFeeRecipientCheck: undefined}, + ] as unknown as GenericServerTestCases["produceBlockV2"]["args"], res: { data: ssz.altair.BeaconBlock.defaultValue(), version: ForkName.altair, executionPayloadValue: ssz.Wei.defaultValue(), + consensusBlockValue: ssz.Gwei.defaultValue(), }, }, produceBlockV3: { @@ -68,15 +81,23 @@ export const testData: GenericServerTestCases = { data: ssz.altair.BeaconBlock.defaultValue(), version: ForkName.altair, executionPayloadValue: ssz.Wei.defaultValue(), + consensusBlockValue: ssz.Gwei.defaultValue(), executionPayloadBlinded: false, }, }, produceBlindedBlock: { - args: [32000, randaoReveal, graffiti], + args: [ + 32000, + randaoReveal, + graffiti, + undefined, + {feeRecipient: undefined, builderSelection: undefined, strictFeeRecipientCheck: undefined}, + ] as unknown as GenericServerTestCases["produceBlindedBlock"]["args"], res: { data: ssz.bellatrix.BlindedBeaconBlock.defaultValue(), version: ForkName.bellatrix, executionPayloadValue: ssz.Wei.defaultValue(), + consensusBlockValue: ssz.Gwei.defaultValue(), }, }, produceAttestationData: { diff --git a/packages/api/test/unit/builder/builder.test.ts b/packages/api/test/unit/builder/builder.test.ts index 8a4766e64f00..56b8eee45ea5 100644 --- a/packages/api/test/unit/builder/builder.test.ts +++ b/packages/api/test/unit/builder/builder.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {Api, ReqTypes} from "../../../src/builder/routes.js"; import {getClient} from "../../../src/builder/client.js"; diff --git a/packages/api/test/unit/builder/testData.ts b/packages/api/test/unit/builder/testData.ts index 94ef3c393b20..e198e6971905 100644 --- a/packages/api/test/unit/builder/testData.ts +++ b/packages/api/test/unit/builder/testData.ts @@ -7,7 +7,7 @@ import {GenericServerTestCases} from "../../utils/genericServerTest.js"; // randomly pregenerated pubkey const pubkeyRand = "0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576"; -const root = Buffer.alloc(32, 1); +const root = new Uint8Array(32).fill(1); export const testData: GenericServerTestCases = { status: { diff --git a/packages/api/test/unit/client/fetch.test.ts b/packages/api/test/unit/client/fetch.test.ts index e0f87e1c57e2..80e5f58b164a 100644 --- a/packages/api/test/unit/client/fetch.test.ts +++ b/packages/api/test/unit/client/fetch.test.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import http from "node:http"; -import {expect} from "chai"; +import {describe, it, expect, afterEach} from "vitest"; import {FetchError, FetchErrorType, fetch} from "../../../src/utils/client/fetch.js"; describe("FetchError", function () { @@ -116,12 +116,16 @@ describe("FetchError", function () { ); } - await expect(fetch(url, {signal: signalHandler?.()})).to.be.rejected.then((error: FetchError) => { - expect(error.type).to.be.equal(testCase.errorType); - expect(error.code).to.be.equal(testCase.errorCode); + await expect(fetch(url, {signal: signalHandler?.()})).rejects.toSatisfy((err) => { + expect(err).toBeInstanceOf(FetchError); + expect((err as FetchError).code).toBe(testCase.errorCode); + expect((err as FetchError).type).toBe(testCase.errorType); + if (testCase.expectCause) { - expect(error.cause).to.be.instanceof(Error); + expect((err as FetchError).cause).toBeInstanceOf(Error); } + + return true; }); }); } diff --git a/packages/api/test/unit/client/format.test.ts b/packages/api/test/unit/client/format.test.ts index 0e388c3cb825..2ab73c9295c5 100644 --- a/packages/api/test/unit/client/format.test.ts +++ b/packages/api/test/unit/client/format.test.ts @@ -1,9 +1,9 @@ -import {expect} from "chai"; +import {describe, expect, it} from "vitest"; import {EventType} from "../../../src/beacon/routes/events.js"; import {stringifyQuery} from "../../../src/utils/client/format.js"; describe("client / utils / format", () => { it("Should repeat topic query", () => { - expect(stringifyQuery({topics: [EventType.finalizedCheckpoint]})).to.equal("topics=finalized_checkpoint"); + expect(stringifyQuery({topics: [EventType.finalizedCheckpoint]})).toBe("topics=finalized_checkpoint"); }); }); diff --git a/packages/api/test/unit/client/httpClient.test.ts b/packages/api/test/unit/client/httpClient.test.ts index be6006d9e375..b22727d6a22b 100644 --- a/packages/api/test/unit/client/httpClient.test.ts +++ b/packages/api/test/unit/client/httpClient.test.ts @@ -1,7 +1,7 @@ import {IncomingMessage} from "node:http"; -import {expect} from "chai"; +import {describe, it, afterEach, expect} from "vitest"; import fastify, {RouteOptions} from "fastify"; -import {ErrorAborted, TimeoutError} from "@lodestar/utils"; +import {ErrorAborted, TimeoutError, toBase64} from "@lodestar/utils"; import {HttpClient, HttpError} from "../../../src/utils/client/index.js"; import {HttpStatusCode} from "../../../src/utils/client/httpStatusCode.js"; @@ -52,8 +52,8 @@ describe("httpClient json client", () => { const {body: resBody, status} = await httpClient.json({url, method: "GET"}); - expect(status).to.equal(HttpStatusCode.OK); - expect(resBody).to.deep.equal({test: 1}, "Wrong res body"); + expect(status).toBe(HttpStatusCode.OK); + expect(resBody).toEqual({test: 1}); }); it("should handle successful POST request correctly", async () => { @@ -76,10 +76,10 @@ describe("httpClient json client", () => { const {body: resBodyReceived, status} = await httpClient.json({url, method: "POST", query, body}); - expect(status).to.equal(HttpStatusCode.OK); - expect(resBodyReceived).to.deep.equal(resBody, "Wrong resBody"); - expect(queryReceived).to.deep.equal(query, "Wrong query"); - expect(bodyReceived).to.deep.equal(body, "Wrong body"); + expect(status).toBe(HttpStatusCode.OK); + expect(resBodyReceived).toEqual(resBody); + expect(queryReceived).toEqual(query); + expect(bodyReceived).toEqual(body); }); it("should handle http status code 404 correctly", async () => { @@ -94,8 +94,8 @@ describe("httpClient json client", () => { return Promise.reject(Error("did not throw")); // So it doesn't gets catch {} } catch (e) { if (!(e instanceof HttpError)) throw Error(`Not an HttpError: ${(e as Error).message}`); - expect(e.message).to.equal("Not Found: Route GET:/test-route not found", "Wrong error message"); - expect(e.status).to.equal(404, "Wrong error status code"); + expect(e.message).toBe("Not Found: Route GET:/test-route not found"); + expect(e.status).toBe(404); } }); @@ -112,8 +112,8 @@ describe("httpClient json client", () => { return Promise.reject(Error("did not throw")); } catch (e) { if (!(e instanceof HttpError)) throw Error(`Not an HttpError: ${(e as Error).message}`); - expect(e.message).to.equal("Internal Server Error: Test error"); - expect(e.status).to.equal(500, "Wrong error status code"); + expect(e.message).toBe("Internal Server Error: Test error"); + expect(e.status).toBe(500); } }); @@ -130,8 +130,8 @@ describe("httpClient json client", () => { return Promise.reject(Error("did not throw")); } catch (e) { if (!(e instanceof HttpError)) throw Error(`Not an HttpError: ${(e as Error).message}`); - expect(e.message).to.equal("Service Unavailable: Node is syncing"); - expect(e.status).to.equal(503, "Wrong error status code"); + expect(e.message).toBe("Service Unavailable: Node is syncing"); + expect(e.status).toBe(503); } }); @@ -139,7 +139,7 @@ describe("httpClient json client", () => { const {baseUrl} = await getServer({ ...testRoute, handler: async (req) => { - expect(req.headers.authorization).to.equal("Basic dXNlcjpwYXNzd29yZA=="); + expect(req.headers.authorization).toBe("Basic dXNlcjpwYXNzd29yZA=="); return {}; }, }); @@ -151,6 +151,29 @@ describe("httpClient json client", () => { await httpClient.json(testRoute); }); + it("should not URI-encode user credentials in Authorization header", async () => { + // Semi exhaustive set of characters that RFC-3986 allows in the userinfo portion of a URI + // Notably absent is `%`. See comment on isValidHttpUrl(). + const username = "A1-._~!$'&\"()*+,;="; + const password = "b2-._~!$'&\"()*+,;="; + let {baseUrl} = await getServer({ + ...testRoute, + handler: async (req) => { + expect(req.headers.authorization).toBe(`Basic ${toBase64(`${username}:${password}`)}`); + return {}; + }, + }); + // Since `new URL()` is what URI-encodes, we have to do string manipulation to set the username/password + // First validate the assumption that the URL starts with http:// + expect(baseUrl.indexOf("http://")).toBe(0); + // We avoid using baseUrl.replace() because it treats $ as a special character + baseUrl = `http://${username}:${password}@${baseUrl.substring("http://".length)}`; + + const httpClient = new HttpClient({baseUrl: baseUrl}); + + await httpClient.json(testRoute); + }); + it("should handle aborting request with timeout", async () => { const {baseUrl} = await getServer({ ...testRoute, diff --git a/packages/api/test/unit/client/httpClientFallback.test.ts b/packages/api/test/unit/client/httpClientFallback.test.ts index 2c0846d00148..ff02095b1cc6 100644 --- a/packages/api/test/unit/client/httpClientFallback.test.ts +++ b/packages/api/test/unit/client/httpClientFallback.test.ts @@ -1,5 +1,4 @@ -import Sinon from "sinon"; -import {expect} from "chai"; +import {describe, it, beforeEach, afterEach, expect, vi} from "vitest"; import {HttpClient} from "../../../src/utils/client/index.js"; describe("httpClient fallback", () => { @@ -8,7 +7,7 @@ describe("httpClient fallback", () => { // Using fetchSub instead of actually setting up servers because there are some strange // race conditions, where the server stub doesn't count the call in time before the test is over. - const fetchStub = Sinon.stub<[URL], ReturnType>(); + const fetchStub = vi.fn(); let httpClient: HttpClient; @@ -37,7 +36,7 @@ describe("httpClient fallback", () => { fetch: fetchStub as typeof fetch, }); - fetchStub.callsFake(async (url) => { + fetchStub.mockImplementation(async (url) => { // Simulate network delay await new Promise((r) => setTimeout(r, 10)); const i = getServerIndex(url); @@ -50,7 +49,6 @@ describe("httpClient fallback", () => { }); afterEach(() => { - fetchStub.reset(); serverErrors.clear(); }); @@ -58,13 +56,13 @@ describe("httpClient fallback", () => { function assertServerCallCount(step: number, expectedCallCounts: number[]): void { const callCounts: number[] = []; for (let i = 0; i < serverCount; i++) callCounts[i] = 0; - for (const call of fetchStub.getCalls()) { - callCounts[getServerIndex(call.args[0])]++; + for (const call of fetchStub.mock.calls) { + callCounts[getServerIndex(call)]++; } - expect(callCounts.join(",")).equals(expectedCallCounts.join(","), `step ${step} - callCounts`); + expect(callCounts.join(",")).toBe(expectedCallCounts.join(",")); - fetchStub.resetHistory(); + fetchStub.mockClear(); // eslint-disable-next-line no-console if (DEBUG_LOGS) console.log("completed assertions step", step); @@ -114,7 +112,7 @@ describe("httpClient fallback", () => { serverErrors.set(0, true); serverErrors.set(1, true); serverErrors.set(2, true); - await expect(requestTestRoute()).rejectedWith("test_error_server_2"); + await expect(requestTestRoute()).rejects.toThrow("test_error_server_2"); assertServerCallCount(0, [1, 1, 1]); }); }); diff --git a/packages/api/test/unit/client/httpClientOptions.test.ts b/packages/api/test/unit/client/httpClientOptions.test.ts index 1bad7c69a406..af0968777219 100644 --- a/packages/api/test/unit/client/httpClientOptions.test.ts +++ b/packages/api/test/unit/client/httpClientOptions.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {HttpClient} from "../../../src/index.js"; describe("HTTPClient options", () => { @@ -10,7 +10,7 @@ describe("HTTPClient options", () => { it("Single root baseUrl option", () => { const httpClient = new HttpClient({baseUrl: baseUrl1, bearerToken: bearerToken1}); - expect(httpClient["urlsOpts"]).deep.equals([{baseUrl: baseUrl1, bearerToken: bearerToken1}]); + expect(httpClient["urlsOpts"]).toEqual([{baseUrl: baseUrl1, bearerToken: bearerToken1}]); }); it("Multiple urls option with common bearerToken", () => { @@ -19,7 +19,7 @@ describe("HTTPClient options", () => { bearerToken: bearerToken1, }); - expect(httpClient["urlsOpts"]).deep.equals([ + expect(httpClient["urlsOpts"]).toEqual([ {baseUrl: baseUrl1, bearerToken: bearerToken1}, {baseUrl: baseUrl2, bearerToken: bearerToken1}, ]); @@ -33,7 +33,7 @@ describe("HTTPClient options", () => { ], }); - expect(httpClient["urlsOpts"]).deep.equals([ + expect(httpClient["urlsOpts"]).toEqual([ {baseUrl: baseUrl1, bearerToken: bearerToken1}, {baseUrl: baseUrl2, bearerToken: bearerToken2}, ]); @@ -46,7 +46,7 @@ describe("HTTPClient options", () => { urls: [{baseUrl: baseUrl2, bearerToken: bearerToken2}], }); - expect(httpClient["urlsOpts"]).deep.equals([ + expect(httpClient["urlsOpts"]).toEqual([ {baseUrl: baseUrl1, bearerToken: bearerToken1}, {baseUrl: baseUrl2, bearerToken: bearerToken2}, ]); @@ -62,25 +62,29 @@ describe("HTTPClient options", () => { {baseUrl: baseUrl2, bearerToken: bearerToken2}, ], }); - expect(httpClient["urlsOpts"]).deep.equals([ + expect(httpClient["urlsOpts"]).toEqual([ {baseUrl: baseUrl1, bearerToken: bearerToken1}, {baseUrl: baseUrl2, bearerToken: bearerToken2}, ]); }); it("Throw if empty baseUrl", () => { - expect(() => new HttpClient({baseUrl: ""})).to.throw(Error); + expect(() => new HttpClient({baseUrl: ""})).toThrow(Error); }); it("Throw if invalid baseUrl", () => { - expect(() => new HttpClient({baseUrl: "invalid"})).to.throw(Error); + expect(() => new HttpClient({baseUrl: "invalid"})).toThrow(Error); }); it("Throw if empty value in urls option", () => { - expect(() => new HttpClient({urls: [""]})).to.throw(Error); + expect(() => new HttpClient({urls: [""]})).toThrow(Error); }); it("Throw if invalid value in urls option", () => { - expect(() => new HttpClient({urls: ["invalid"]})).to.throw(Error); + expect(() => new HttpClient({urls: ["invalid"]})).toThrow(Error); + }); + + it("Throw if invalid username/password", () => { + expect(() => new HttpClient({baseUrl: "http://hasa%:%can'tbedecoded@localhost"})).toThrow(Error); }); }); diff --git a/packages/api/test/unit/client/urlFormat.test.ts b/packages/api/test/unit/client/urlFormat.test.ts index 851742ac1ed5..5b8e1f294976 100644 --- a/packages/api/test/unit/client/urlFormat.test.ts +++ b/packages/api/test/unit/client/urlFormat.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import { compileRouteUrlFormater, toColonNotationPath, @@ -55,14 +55,14 @@ describe("utils / urlFormat", () => { for (const {urlTemplate, colonNotation, tokens, cases} of testCases) { it(urlTemplate, () => { - expect(urlToTokens(urlTemplate)).deep.equal(tokens, "Wrong tokens"); + expect(urlToTokens(urlTemplate)).toEqual(tokens); - expect(toColonNotationPath(urlTemplate)).equal(colonNotation, "Wrong colonNotation"); + expect(toColonNotationPath(urlTemplate)).toBe(colonNotation); const utlFormater = compileRouteUrlFormater(urlTemplate); - for (const [i, {args, url}] of cases.entries()) { - expect(utlFormater(args)).to.equal(url, `wrong case ${i}`); + for (const [_, {args, url}] of cases.entries()) { + expect(utlFormater(args)).toBe(url); } }); } diff --git a/packages/api/test/unit/keymanager/keymanager.test.ts b/packages/api/test/unit/keymanager/keymanager.test.ts index f00e6e754a51..1adf5b1e44da 100644 --- a/packages/api/test/unit/keymanager/keymanager.test.ts +++ b/packages/api/test/unit/keymanager/keymanager.test.ts @@ -1,3 +1,4 @@ +import {describe} from "vitest"; import {config} from "@lodestar/config/default"; import {Api, ReqTypes} from "../../../src/keymanager/routes.js"; import {getClient} from "../../../src/keymanager/client.js"; diff --git a/packages/api/test/unit/utils/acceptHeader.test.ts b/packages/api/test/unit/utils/acceptHeader.test.ts index b1ce3cf48d81..b93f07ba286d 100644 --- a/packages/api/test/unit/utils/acceptHeader.test.ts +++ b/packages/api/test/unit/utils/acceptHeader.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {parseAcceptHeader} from "../../../src/utils/acceptHeader.js"; import {ResponseFormat} from "../../../src/interfaces.js"; @@ -28,10 +28,8 @@ describe("utils / acceptHeader", () => { {header: "application/json;q=1,application/octet-stream;q=1", expected: "ssz"}, ]; - for (const testCase of testCases) { - it(`should correctly parse the header ${testCase.header}`, () => { - expect(parseAcceptHeader(testCase.header)).to.equal(testCase.expected); - }); - } + it.each(testCases)("should correctly parse the header $header", ({header, expected}) => { + expect(parseAcceptHeader(header)).toBe(expected); + }); }); }); diff --git a/packages/api/test/unit/utils/serdes.test.ts b/packages/api/test/unit/utils/serdes.test.ts index c390e3e6b6da..5b55ef66805e 100644 --- a/packages/api/test/unit/utils/serdes.test.ts +++ b/packages/api/test/unit/utils/serdes.test.ts @@ -1,70 +1,68 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {fromGraffitiHex, toGraffitiHex} from "../../../src/utils/serdes.js"; describe("utils / serdes", () => { describe("toGraffitiHex", () => { it("should convert a UTF-8 graffiti to hex", () => { - expect(toGraffitiHex("a".repeat(32))).to.equal( - "0x6161616161616161616161616161616161616161616161616161616161616161" - ); + expect(toGraffitiHex("a".repeat(32))).toBe("0x6161616161616161616161616161616161616161616161616161616161616161"); }); it("should convert a graffiti with Unicode symbols to hex", () => { - expect(toGraffitiHex("🦇🔊".repeat(4))).to.equal( + expect(toGraffitiHex("🦇🔊".repeat(4))).toBe( "0xf09fa687f09f948af09fa687f09f948af09fa687f09f948af09fa687f09f948a" ); }); it("should trim the hex graffiti if it is too long", () => { - expect(toGraffitiHex("a".repeat(50))).to.equal(toGraffitiHex("a".repeat(32))); + expect(toGraffitiHex("a".repeat(50))).toBe(toGraffitiHex("a".repeat(32))); }); it("should trim the hex graffiti if the last character is a Unicode symbol", () => { - expect(toGraffitiHex("a".repeat(31) + "🐼")).to.equal( + expect(toGraffitiHex("a".repeat(31) + "🐼")).toBe( "0x61616161616161616161616161616161616161616161616161616161616161f0" ); }); it("should right-pad the hex graffiti with zeros if it is too short", () => { - expect(toGraffitiHex("a")).to.equal("0x6100000000000000000000000000000000000000000000000000000000000000"); - expect(toGraffitiHex("ab")).to.equal("0x6162000000000000000000000000000000000000000000000000000000000000"); - expect(toGraffitiHex("abc")).to.equal("0x6162630000000000000000000000000000000000000000000000000000000000"); + expect(toGraffitiHex("a")).toBe("0x6100000000000000000000000000000000000000000000000000000000000000"); + expect(toGraffitiHex("ab")).toBe("0x6162000000000000000000000000000000000000000000000000000000000000"); + expect(toGraffitiHex("abc")).toBe("0x6162630000000000000000000000000000000000000000000000000000000000"); }); }); describe("fromGraffitiHex", () => { it("should convert a hex graffiti to UTF-8", () => { - expect(fromGraffitiHex("0x6161616161616161616161616161616161616161616161616161616161616161")).to.equal( + expect(fromGraffitiHex("0x6161616161616161616161616161616161616161616161616161616161616161")).toBe( "a".repeat(32) ); }); it("should convert a hex graffiti with Unicode symbols to UTF-8", () => { - expect(fromGraffitiHex("0xf09fa687f09f948af09fa687f09f948af09fa687f09f948af09fa687f09f948a")).to.equal( + expect(fromGraffitiHex("0xf09fa687f09f948af09fa687f09f948af09fa687f09f948af09fa687f09f948a")).toBe( "🦇🔊".repeat(4) ); }); it("should convert a padded hex graffiti to UTF-8", () => { - expect(fromGraffitiHex("0x6100000000000000000000000000000000000000000000000000000000000000")).to.equal( + expect(fromGraffitiHex("0x6100000000000000000000000000000000000000000000000000000000000000")).toBe( // null bytes will not be displayed/ignored later on "a" + "\u0000".repeat(31) ); }); it("should decode a hex graffiti with a cut off Unicode character at the end", () => { - expect(fromGraffitiHex("0x61616161616161616161616161616161616161616161616161616161616161f0")).to.equal( + expect(fromGraffitiHex("0x61616161616161616161616161616161616161616161616161616161616161f0")).toBe( // last character will be displayed as � "a".repeat(31) + "\ufffd" ); }); it("should not throw an error if an invalid hex graffiti is provided", () => { - expect(() => fromGraffitiHex("a")).to.not.throw(); + expect(() => fromGraffitiHex("a")).not.toThrow(); }); it("should return the provided graffiti string if decoding fails", () => { - expect(fromGraffitiHex("a")).to.equal("a"); + expect(fromGraffitiHex("a")).toBe("a"); }); }); }); diff --git a/packages/api/test/utils/checkAgainstSpec.ts b/packages/api/test/utils/checkAgainstSpec.ts index eba274e16ef6..01e7df255db2 100644 --- a/packages/api/test/utils/checkAgainstSpec.ts +++ b/packages/api/test/utils/checkAgainstSpec.ts @@ -1,5 +1,5 @@ import Ajv, {ErrorObject} from "ajv"; -import {expect} from "chai"; +import {expect, describe, beforeAll, it} from "vitest"; import {ReqGeneric, ReqSerializer, ReturnTypes, RouteDef} from "../../src/utils/types.js"; import {applyRecursively, OpenApiJson, parseOpenApiSpec, ParseOpenApiSpecOpts} from "./parseOpenApiSpec.js"; import {GenericServerTestCases} from "./genericServerTest.js"; @@ -36,7 +36,7 @@ export function runTestCheckAgainstSpec( const testData = testDatas[routeId]; const routeData = routesData[routeId]; - before("route is defined", () => { + beforeAll(() => { if (routeData == null) { throw Error(`No routeData for ${routeId}`); } diff --git a/packages/api/test/utils/genericServerTest.ts b/packages/api/test/utils/genericServerTest.ts index d5e091bc25af..f0f805b7469a 100644 --- a/packages/api/test/utils/genericServerTest.ts +++ b/packages/api/test/utils/genericServerTest.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {it, expect, MockInstance} from "vitest"; import {ChainForkConfig} from "@lodestar/config"; import {ReqGeneric, Resolves} from "../../src/utils/index.js"; import {FetchOpts, HttpClient, IHttpClient} from "../../src/utils/client/index.js"; @@ -44,30 +44,25 @@ export function runGenericServerTest< it(routeId as string, async () => { // Register mock data for this route // TODO: Look for the type error - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - mockApi[routeId].resolves(testCases[routeId].res); + (mockApi[routeId] as MockInstance).mockResolvedValue(testCases[routeId].res); // Do the call const res = await (client[routeId] as APIClientHandler)(...(testCase.args as any[])); // Use spy to assert argument serialization if (testCase.query) { - expect(httpClient.opts?.query).to.deep.equal(testCase.query, "Wrong fetch opts.query"); + expect(httpClient.opts?.query).toEqual(testCase.query); } // Assert server handler called with correct args - expect(mockApi[routeId].callCount).to.equal(1, `mockApi[${routeId as string}] must be called once`); + expect(mockApi[routeId] as MockInstance).toHaveBeenCalledTimes(1); // if mock api args are > testcase args, there may be some undefined extra args parsed towards the end // to obtain a match, ignore the extra args - expect(mockApi[routeId].getCall(0).args.slice(0, testCase.args.length)).to.deep.equal( - testCase.args, - `mockApi[${routeId as string}] wrong args` - ); + expect(mockApi[routeId] as MockInstance).toHaveBeenNthCalledWith(1, ...(testCase.args as any[])); // Assert returned value is correct - expect(res.response).to.deep.equal(testCase.res, "Wrong returned value"); + expect(res.response).toEqual(testCase.res); }); } } diff --git a/packages/api/test/utils/utils.ts b/packages/api/test/utils/utils.ts index 793f8b2c61ef..8faa2c90d187 100644 --- a/packages/api/test/utils/utils.ts +++ b/packages/api/test/utils/utils.ts @@ -1,6 +1,6 @@ +import {beforeAll, afterAll, MockedObject, vi} from "vitest"; import qs from "qs"; import fastify, {FastifyInstance} from "fastify"; -import Sinon from "sinon"; import {mapValues} from "@lodestar/utils"; import {ServerApi} from "../../src/interfaces.js"; @@ -19,7 +19,7 @@ export function getTestServer(): {baseUrl: string; server: FastifyInstance} { done(); }); - before("start server", async () => { + beforeAll(async () => { await new Promise((resolve, reject) => { server.listen({port}, function (err, address) { if (err !== null && err != undefined) { @@ -31,7 +31,7 @@ export function getTestServer(): {baseUrl: string; server: FastifyInstance} { }); }); - after("stop server", async () => { + afterAll(async () => { await server.close(); }); @@ -41,6 +41,6 @@ export function getTestServer(): {baseUrl: string; server: FastifyInstance} { /** Type helper to get a Sinon mock object type with Api */ export function getMockApi>( routeIds: Record -): Sinon.SinonStubbedInstance> & ServerApi { - return mapValues(routeIds, () => Sinon.stub()) as Sinon.SinonStubbedInstance> & ServerApi; +): MockedObject> & ServerApi { + return mapValues(routeIds, () => vi.fn()) as MockedObject> & ServerApi; } diff --git a/packages/api/vitest.config.ts b/packages/api/vitest.config.ts new file mode 100644 index 000000000000..9f325a6477e2 --- /dev/null +++ b/packages/api/vitest.config.ts @@ -0,0 +1,12 @@ +import {defineConfig, mergeConfig} from "vitest/config"; +import vitestConfig from "../../vitest.base.config"; + +export default mergeConfig( + vitestConfig, + defineConfig({ + test: { + globalSetup: ["./test/globalSetup.ts"], + restoreMocks: true, + }, + }) +); diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index 1b8c9f607208..b29169777549 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -77,10 +77,10 @@ "lint:fix": "yarn run lint --fix", "pretest": "yarn run check-types", "test": "yarn test:unit && yarn test:e2e", - "test:unit:minimal": "vitest --run --dir test/unit/ --coverage", + "test:unit:minimal": "vitest --run --segfaultRetry 3 --dir test/unit/ --coverage", "test:unit:mainnet": "LODESTAR_PRESET=mainnet nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit-mainnet/**/*.test.ts'", "test:unit": "yarn test:unit:minimal && yarn test:unit:mainnet", - "test:e2e": "LODESTAR_PRESET=minimal vitest --run --single-thread --dir test/e2e", + "test:e2e": "LODESTAR_PRESET=minimal vitest --run --segfaultRetry 3 --poolOptions.threads.singleThread --dir test/e2e", "test:sim": "mocha 'test/sim/**/*.test.ts'", "test:sim:merge-interop": "mocha 'test/sim/merge-interop.test.ts'", "test:sim:mergemock": "mocha 'test/sim/mergemock.test.ts'", @@ -119,18 +119,18 @@ "@libp2p/peer-id-factory": "^3.0.4", "@libp2p/prometheus-metrics": "^2.0.7", "@libp2p/tcp": "8.0.8", - "@lodestar/api": "^1.12.1", - "@lodestar/config": "^1.12.1", - "@lodestar/db": "^1.12.1", - "@lodestar/fork-choice": "^1.12.1", - "@lodestar/light-client": "^1.12.1", - "@lodestar/logger": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/reqresp": "^1.12.1", - "@lodestar/state-transition": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", - "@lodestar/validator": "^1.12.1", + "@lodestar/api": "^1.13.0", + "@lodestar/config": "^1.13.0", + "@lodestar/db": "^1.13.0", + "@lodestar/fork-choice": "^1.13.0", + "@lodestar/light-client": "^1.13.0", + "@lodestar/logger": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/reqresp": "^1.13.0", + "@lodestar/state-transition": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", + "@lodestar/validator": "^1.13.0", "@multiformats/multiaddr": "^12.1.3", "@types/datastore-level": "^3.0.0", "buffer-xor": "^2.0.2", diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 1b2d0f10a85b..c54e040ceb06 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -131,6 +131,7 @@ export function getBeaconBlockApi({ verifyOnly: true, skipVerifyBlockSignatures: true, skipVerifyExecutionPayload: true, + seenTimestampSec, } ); } catch (error) { diff --git a/packages/beacon-node/src/api/impl/lodestar/index.ts b/packages/beacon-node/src/api/impl/lodestar/index.ts index edbb154c7feb..8048e668662b 100644 --- a/packages/beacon-node/src/api/impl/lodestar/index.ts +++ b/packages/beacon-node/src/api/impl/lodestar/index.ts @@ -14,7 +14,7 @@ import {QueuedStateRegenerator, RegenRequest} from "../../../chain/regen/index.j import {GossipType} from "../../../network/index.js"; import {IBeaconDb} from "../../../db/interface.js"; import {ApiModules} from "../types.js"; -import {profileNodeJS} from "../../../util/profile.js"; +import {profileNodeJS, writeHeapSnapshot} from "../../../util/profile.js"; export function getLodestarApi({ chain, @@ -28,26 +28,26 @@ export function getLodestarApi({ const defaultProfileMs = SLOTS_PER_EPOCH * config.SECONDS_PER_SLOT * 1000; return { - async writeHeapdump(dirpath = ".") { + async writeHeapdump(thread = "main", dirpath = ".") { if (writingHeapdump) { throw Error("Already writing heapdump"); } - // Lazily import NodeJS only modules - const fs = await import("node:fs"); - const v8 = await import("v8"); - const snapshotStream = v8.getHeapSnapshot(); - // It's important that the filename end with `.heapsnapshot`, - // otherwise Chrome DevTools won't open it. - const filepath = `${dirpath}/${new Date().toISOString()}.heapsnapshot`; - const fileStream = fs.createWriteStream(filepath); + try { writingHeapdump = true; - await new Promise((resolve) => { - snapshotStream.pipe(fileStream); - snapshotStream.on("end", () => { - resolve(); - }); - }); + let filepath: string; + switch (thread) { + case "network": + filepath = await network.writeNetworkHeapSnapshot("network_thread", dirpath); + break; + case "discv5": + filepath = await network.writeDiscv5HeapSnapshot("discv5_thread", dirpath); + break; + default: + // main thread + filepath = await writeHeapSnapshot("main_thread", dirpath); + break; + } return {data: {filepath}}; } finally { writingHeapdump = false; diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index e41c4c97bc63..8f92fa483908 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -32,6 +32,7 @@ import { BLSSignature, isBlindedBeaconBlock, isBlindedBlockContents, + phase0, } from "@lodestar/types"; import {ExecutionStatus} from "@lodestar/fork-choice"; import {toHex, racePromisesWithCutoff, RaceEvent} from "@lodestar/utils"; @@ -71,7 +72,7 @@ const SYNC_TOLERANCE_EPOCHS = 1; * Cutoff time to wait for execution and builder block production apis to resolve * Post this time, race execution and builder to pick whatever resolves first * - * Emprically the builder block resolves in ~1.5+ seconds, and executon should resolve <1 sec. + * Empirically the builder block resolves in ~1.5+ seconds, and execution should resolve <1 sec. * So lowering the cutoff to 2 sec from 3 seconds to publish faster for successful proposal * as proposals post 4 seconds into the slot seems to be not being included */ @@ -172,12 +173,17 @@ export function getValidatorApi({ /** * This function is called 1s before next epoch, usually at that time PrepareNextSlotScheduler finishes - * so we should have checkpoint state, otherwise wait for up to `timeoutMs`. + * so we should have checkpoint state, otherwise wait for up to the slot 1 of epoch. + * slot epoch 0 1 + * |------------|------------| + * ^ ^ + * | | + * | | + * | waitForCheckpointState (1s before slot 0 of epoch, wait until slot 1 of epoch) + * | + * prepareNextSlot (4s before next slot) */ - async function waitForCheckpointState( - cpHex: CheckpointHex, - timeoutMs: number - ): Promise { + async function waitForCheckpointState(cpHex: CheckpointHex): Promise { const cpState = chain.regen.getCheckpointStateSync(cpHex); if (cpState) { return cpState; @@ -186,16 +192,30 @@ export function getValidatorApi({ epoch: cpHex.epoch, root: fromHexString(cpHex.rootHex), }; - // if not, wait for ChainEvent.checkpoint event until timeoutMs - return new Promise((resolve) => { - const timer = setTimeout(() => resolve(null), timeoutMs); - chain.emitter.on(ChainEvent.checkpoint, (eventCp, cpState) => { - if (ssz.phase0.Checkpoint.equals(eventCp, cp)) { - clearTimeout(timer); - resolve(cpState); - } - }); - }); + const slot0 = computeStartSlotAtEpoch(cp.epoch); + // if not, wait for ChainEvent.checkpoint event until slot 1 of epoch + let listener: ((eventCp: phase0.Checkpoint) => void) | null = null; + const foundCPState = await Promise.race([ + new Promise((resolve) => { + listener = (eventCp) => { + resolve(ssz.phase0.Checkpoint.equals(eventCp, cp)); + }; + chain.emitter.once(ChainEvent.checkpoint, listener); + }), + // in rare case, checkpoint state cache may happen up to 6s of slot 0 of epoch + // so we wait for it until the slot 1 of epoch + chain.clock.waitForSlot(slot0 + 1), + ]); + + if (listener != null) { + chain.emitter.off(ChainEvent.checkpoint, listener); + } + + if (foundCPState === true) { + return chain.regen.getCheckpointStateSync(cpHex); + } + + return null; } /** @@ -294,7 +314,7 @@ export function getValidatorApi({ let timer; try { timer = metrics?.blockProductionTime.startTimer(); - const {block, executionPayloadValue} = await chain.produceBlindedBlock({ + const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlindedBlock({ slot, randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), @@ -305,6 +325,7 @@ export function getValidatorApi({ logger.verbose("Produced blinded block", { slot, executionPayloadValue, + consensusBlockValue, root: toHexString(config.getBlindedForkTypes(slot).BeaconBlock.hashTreeRoot(block)), }); @@ -322,9 +343,10 @@ export function getValidatorApi({ data: {blindedBlock: block, blindedBlobSidecars} as allForks.BlindedBlockContents, version, executionPayloadValue, + consensusBlockValue, }; } else { - return {data: block, version, executionPayloadValue}; + return {data: block, version, executionPayloadValue, consensusBlockValue}; } } finally { if (timer) timer({source}); @@ -358,13 +380,12 @@ export function getValidatorApi({ let timer; try { timer = metrics?.blockProductionTime.startTimer(); - const {block, executionPayloadValue} = await chain.produceBlock({ + const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlock({ slot, randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), feeRecipient, }); - const version = config.getForkName(block.slot); if (strictFeeRecipientCheck && feeRecipient && isForkExecution(version)) { const blockFeeRecipient = toHexString((block as bellatrix.BeaconBlock).body.executionPayload.feeRecipient); @@ -378,6 +399,7 @@ export function getValidatorApi({ logger.verbose("Produced execution block", { slot, executionPayloadValue, + consensusBlockValue, root: toHexString(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block)), }); if (chain.opts.persistProducedBlocks) { @@ -389,9 +411,14 @@ export function getValidatorApi({ if (blobSidecars === undefined) { throw Error("blobSidecars missing in cache"); } - return {data: {block, blobSidecars} as allForks.BlockContents, version, executionPayloadValue}; + return { + data: {block, blobSidecars} as allForks.BlockContents, + version, + executionPayloadValue, + consensusBlockValue, + }; } else { - return {data: block, version, executionPayloadValue}; + return {data: block, version, executionPayloadValue, consensusBlockValue}; } } finally { if (timer) timer({source}); @@ -423,7 +450,7 @@ export function getValidatorApi({ chain.executionBuilder !== undefined && builderSelection !== routes.validator.BuilderSelection.ExecutionOnly; - logger.verbose("produceBlockV3 assembling block", { + logger.verbose("Assembling block with produceBlockV3 ", { fork, builderSelection, slot, @@ -511,15 +538,18 @@ export function getValidatorApi({ const builderPayloadValue = blindedBlock?.executionPayloadValue ?? BigInt(0); const enginePayloadValue = fullBlock?.executionPayloadValue ?? BigInt(0); + const consensusBlockValueBuilder = blindedBlock?.consensusBlockValue ?? BigInt(0); + const consensusBlockValueEngine = fullBlock?.consensusBlockValue ?? BigInt(0); + + const blockValueBuilder = builderPayloadValue + consensusBlockValueBuilder; + const blockValueEngine = enginePayloadValue + consensusBlockValueEngine; let selectedSource: ProducedBlockSource | null = null; if (fullBlock && blindedBlock) { switch (builderSelection) { case routes.validator.BuilderSelection.MaxProfit: { - // If executionPayloadValues are zero, than choose builder as most likely beacon didn't provide executionPayloadValue - // and builder blocks are most likely thresholded by a min bid - if (enginePayloadValue >= builderPayloadValue && enginePayloadValue !== BigInt(0)) { + if (blockValueEngine >= blockValueBuilder) { selectedSource = ProducedBlockSource.engine; } else { selectedSource = ProducedBlockSource.builder; @@ -542,6 +572,10 @@ export function getValidatorApi({ // winston logger doesn't like bigint enginePayloadValue: `${enginePayloadValue}`, builderPayloadValue: `${builderPayloadValue}`, + consensusBlockValueEngine: `${consensusBlockValueEngine}`, + consensusBlockValueBuilder: `${consensusBlockValueBuilder}`, + blockValueEngine: `${blockValueEngine}`, + blockValueBuilder: `${blockValueBuilder}`, slot, }); } else if (fullBlock && !blindedBlock) { @@ -549,6 +583,8 @@ export function getValidatorApi({ logger.verbose("Selected engine block: no builder block produced", { // winston logger doesn't like bigint enginePayloadValue: `${enginePayloadValue}`, + consensusBlockValueEngine: `${consensusBlockValueEngine}`, + blockValueEngine: `${blockValueEngine}`, slot, }); } else if (blindedBlock && !fullBlock) { @@ -556,6 +592,8 @@ export function getValidatorApi({ logger.verbose("Selected builder block: no engine block produced", { // winston logger doesn't like bigint builderPayloadValue: `${builderPayloadValue}`, + consensusBlockValueBuilder: `${consensusBlockValueBuilder}`, + blockValueBuilder: `${blockValueBuilder}`, slot, }); } @@ -731,7 +769,7 @@ export function getValidatorApi({ // this is to avoid missed block proposal due to 0 epoch look ahead if (epoch === nextEpoch && toNextEpochMs < prepareNextSlotLookAheadMs) { // wait for maximum 1 slot for cp state which is the timeout of validator api - const cpState = await waitForCheckpointState({rootHex: head.blockRoot, epoch}, slotMs); + const cpState = await waitForCheckpointState({rootHex: head.blockRoot, epoch}); if (cpState) { state = cpState; metrics?.duties.requestNextEpochProposalDutiesHit.inc(); diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index cd258e42c146..feaddfbad39d 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -63,6 +63,7 @@ export async function importBlock( const blockRootHex = toHexString(blockRoot); const currentEpoch = computeEpochAtSlot(this.forkChoice.getTime()); const blockEpoch = computeEpochAtSlot(block.message.slot); + const parentEpoch = computeEpochAtSlot(parentBlockSlot); const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; const blockDelaySec = (fullyVerifiedBlock.seenTimestampSec - postState.genesisTime) % this.config.SECONDS_PER_SLOT; @@ -347,6 +348,12 @@ export async function importBlock( this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); } + if (parentEpoch < blockEpoch) { + // current epoch and previous epoch are likely cached in previous states + this.shufflingCache.processState(postState, postState.epochCtx.nextShuffling.epoch); + this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: block.message.slot}); + } + if (block.message.slot % SLOTS_PER_EPOCH === 0) { // Cache state to preserve epoch transition work const checkpointState = postState; diff --git a/packages/beacon-node/src/chain/blocks/verifyBlock.ts b/packages/beacon-node/src/chain/blocks/verifyBlock.ts index a273b2ba7d55..72db1d801b48 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlock.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlock.ts @@ -71,6 +71,7 @@ export async function verifyBlocksInEpoch( if (!isStateValidatorsNodesPopulated(preState0)) { this.logger.verbose("verifyBlocksInEpoch preState0 SSZ cache stats", { + slot: preState0.slot, cache: isStateValidatorsNodesPopulated(preState0), clonedCount: preState0.clonedCount, clonedCountWithTransferCache: preState0.clonedCountWithTransferCache, diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index 2afc9543f847..709ad0c02b27 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -57,7 +57,7 @@ export async function verifyBlocksStateTransitionOnly( metrics ); - const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer(); + const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({source: "block_transition"}); const stateRoot = postState.hashTreeRoot(); hashTreeRootTimer?.(); diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 56409dc89d21..45cda3d94bc9 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import {CompositeTypeAny, fromHexString, toHexString, TreeView, Type} from "@chainsafe/ssz"; +import {CompositeTypeAny, fromHexString, TreeView, Type, toHexString} from "@chainsafe/ssz"; import { BeaconStateAllForks, CachedBeaconStateAllForks, @@ -11,6 +11,7 @@ import { isCachedBeaconState, Index2PubkeyCache, PubkeyIndexMap, + EpochShuffling, } from "@lodestar/state-transition"; import {BeaconConfig} from "@lodestar/config"; import { @@ -26,6 +27,7 @@ import { Wei, bellatrix, isBlindedBeaconBlock, + Gwei, } from "@lodestar/types"; import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {ProcessShutdownCallback} from "@lodestar/validator"; @@ -40,7 +42,6 @@ import {IExecutionEngine, IExecutionBuilder} from "../execution/index.js"; import {Clock, ClockEvent, IClock} from "../util/clock.js"; import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; -import {CheckpointStateCache, StateContextCache} from "./stateCache/index.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {ChainEventEmitter, ChainEvent} from "./emitter.js"; import {IBeaconChain, ProposerPreparationData, BlockHash, StateGetOpts} from "./interface.js"; @@ -76,6 +77,9 @@ import {BlockAttributes, produceBlockBody} from "./produceBlock/produceBlockBody import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; import {BlockInput} from "./blocks/types.js"; import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; +import {ShufflingCache} from "./shufflingCache.js"; +import {StateContextCache} from "./stateCache/stateContextCache.js"; +import {CheckpointStateCache} from "./stateCache/stateContextCheckpointsCache.js"; /** * Arbitrary constants, blobs and payloads should be consumed immediately in the same slot @@ -130,6 +134,7 @@ export class BeaconChain implements IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; + readonly shufflingCache: ShufflingCache; /** Map keyed by executionPayload.blockHash of the block for those blobs */ readonly producedBlobSidecarsCache = new Map(); readonly producedBlindedBlobSidecarsCache = new Map(); @@ -210,6 +215,7 @@ export class BeaconChain implements IBeaconChain { this.beaconProposerCache = new BeaconProposerCache(opts); this.checkpointBalancesCache = new CheckpointBalancesCache(); + this.shufflingCache = new ShufflingCache(metrics, this.opts); // Restore state caches // anchorState may already by a CachedBeaconState. If so, don't create the cache again, since deserializing all @@ -224,6 +230,9 @@ export class BeaconChain implements IBeaconChain { pubkey2index: new PubkeyIndexMap(), index2pubkey: [], }); + this.shufflingCache.processState(cachedState, cachedState.epochCtx.previousShuffling.epoch); + this.shufflingCache.processState(cachedState, cachedState.epochCtx.currentShuffling.epoch); + this.shufflingCache.processState(cachedState, cachedState.epochCtx.nextShuffling.epoch); // Persist single global instance of state caches this.pubkey2index = cachedState.epochCtx.pubkey2index; @@ -461,20 +470,22 @@ export class BeaconChain implements IBeaconChain { return data && {block: data, executionOptimistic: false}; } - produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei}> { + produceBlock( + blockAttributes: BlockAttributes + ): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}> { return this.produceBlockWrapper(BlockType.Full, blockAttributes); } produceBlindedBlock( blockAttributes: BlockAttributes - ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei}> { + ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}> { return this.produceBlockWrapper(BlockType.Blinded, blockAttributes); } async produceBlockWrapper( blockType: T, {randaoReveal, graffiti, slot, feeRecipient}: BlockAttributes - ): Promise<{block: AssembledBlockType; executionPayloadValue: Wei}> { + ): Promise<{block: AssembledBlockType; executionPayloadValue: Wei; consensusBlockValue: Gwei}> { const head = this.forkChoice.getHead(); const state = await this.regen.getBlockSlotState( head.blockRoot, @@ -515,7 +526,9 @@ export class BeaconChain implements IBeaconChain { stateRoot: ZERO_HASH, body, } as AssembledBlockType; - block.stateRoot = computeNewStateRoot(this.metrics, state, block); + + const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block); + block.stateRoot = newStateRoot; const blockRoot = blockType === BlockType.Full ? this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block) @@ -567,7 +580,7 @@ export class BeaconChain implements IBeaconChain { ); } - return {block, executionPayloadValue}; + return {block, executionPayloadValue, consensusBlockValue: proposerReward}; } /** @@ -667,6 +680,49 @@ export class BeaconChain implements IBeaconChain { } } + /** + * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . + * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification + * at the same time, bounded inside "ShufflingCache.insertPromise()" function. + * Leave this function in chain instead of attestatation verification code to make sure we're aware of its performance impact. + */ + async regenStateForAttestationVerification( + attEpoch: Epoch, + shufflingDependentRoot: RootHex, + attHeadBlock: ProtoBlock, + regenCaller: RegenCaller + ): Promise { + // this is to prevent multiple calls to get shuffling for the same epoch and dependent root + // any subsequent calls of the same epoch and dependent root will wait for this promise to resolve + this.shufflingCache.insertPromise(attEpoch, shufflingDependentRoot); + const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); + + let state: CachedBeaconStateAllForks; + if (blockEpoch < attEpoch - 1) { + // thanks to one epoch look ahead, we don't need to dial up to attEpoch + const targetSlot = computeStartSlotAtEpoch(attEpoch - 1); + this.metrics?.gossipAttestation.useHeadBlockStateDialedToTargetEpoch.inc({caller: regenCaller}); + state = await this.regen.getBlockSlotState( + attHeadBlock.blockRoot, + targetSlot, + {dontTransferCache: true}, + regenCaller + ); + } else if (blockEpoch > attEpoch) { + // should not happen, handled inside attestation verification code + throw Error(`Block epoch ${blockEpoch} is after attestation epoch ${attEpoch}`); + } else { + // should use either current or next shuffling of head state + // it's not likely to hit this since these shufflings are cached already + // so handle just in case + this.metrics?.gossipAttestation.useHeadBlockState.inc({caller: regenCaller}); + state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); + } + + // resolve the promise to unblock other calls of the same epoch and dependent root + return this.shufflingCache.processState(state, attEpoch); + } + /** * `ForkChoice.onBlock` must never throw for a block that is valid with respect to the network * `justifiedBalancesGetter()` must never throw and it should always return a state. @@ -823,7 +879,7 @@ export class BeaconChain implements IBeaconChain { this.metrics?.blockProductionCaches.producedBlockRoot.set(this.producedBlockRoot.size); pruneSetToMax(this.producedBlindedBlockRoot, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS); - this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlockRoot.size); + this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlindedBlockRoot.size); if (this.config.getForkSeq(slot) >= ForkSeq.deneb) { pruneSetToMax( diff --git a/packages/beacon-node/src/chain/errors/attestationError.ts b/packages/beacon-node/src/chain/errors/attestationError.ts index a93f5b42e439..8e0dc925f32e 100644 --- a/packages/beacon-node/src/chain/errors/attestationError.ts +++ b/packages/beacon-node/src/chain/errors/attestationError.ts @@ -1,5 +1,5 @@ import {toHexString} from "@chainsafe/ssz"; -import {CommitteeIndex, Epoch, Slot, ValidatorIndex, RootHex} from "@lodestar/types"; +import {Epoch, Slot, ValidatorIndex, RootHex} from "@lodestar/types"; import {GossipActionError} from "./gossipValidation.js"; export enum AttestationErrorCode { @@ -65,11 +65,6 @@ export enum AttestationErrorCode { * A signature on the attestation is invalid. */ INVALID_SIGNATURE = "ATTESTATION_ERROR_INVALID_SIGNATURE", - /** - * There is no committee for the slot and committee index of this attestation - * and the attestation should not have been produced. - */ - NO_COMMITTEE_FOR_SLOT_AND_INDEX = "ATTESTATION_ERROR_NO_COMMITTEE_FOR_SLOT_AND_INDEX", /** * The unaggregated attestation doesn't have only one aggregation bit set. */ @@ -150,7 +145,6 @@ export type AttestationErrorType = | {code: AttestationErrorCode.HEAD_NOT_TARGET_DESCENDANT} | {code: AttestationErrorCode.UNKNOWN_TARGET_ROOT; root: Uint8Array} | {code: AttestationErrorCode.INVALID_SIGNATURE} - | {code: AttestationErrorCode.NO_COMMITTEE_FOR_SLOT_AND_INDEX; slot: Slot; index: CommitteeIndex} | {code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET} | {code: AttestationErrorCode.PRIOR_ATTESTATION_KNOWN; validatorIndex: ValidatorIndex; epoch: Epoch} | {code: AttestationErrorCode.FUTURE_EPOCH; attestationEpoch: Epoch; currentEpoch: Epoch} diff --git a/packages/beacon-node/src/chain/errors/blockError.ts b/packages/beacon-node/src/chain/errors/blockError.ts index 59775f5694fb..ee06927a4fc1 100644 --- a/packages/beacon-node/src/chain/errors/blockError.ts +++ b/packages/beacon-node/src/chain/errors/blockError.ts @@ -134,14 +134,20 @@ export function renderBlockErrorType(type: BlockErrorType): Record; readonly producedBlockRoot: Map; readonly producedBlindedBlobSidecarsCache: Map; + readonly shufflingCache: ShufflingCache; readonly producedBlindedBlockRoot: Set; readonly opts: IChainOptions; @@ -138,10 +153,12 @@ export interface IBeaconChain { getBlobSidecars(beaconBlock: deneb.BeaconBlock): deneb.BlobSidecars; - produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei}>; + produceBlock( + blockAttributes: BlockAttributes + ): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}>; produceBlindedBlock( blockAttributes: BlockAttributes - ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei}>; + ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}>; /** Process a block until complete */ processBlock(block: BlockInput, opts?: ImportBlockOpts): Promise; @@ -161,6 +178,12 @@ export interface IBeaconChain { persistInvalidSszBytes(type: string, sszBytes: Uint8Array, suffix?: string): void; /** Persist bad items to persistInvalidSszObjectsDir dir, for example invalid state, attestations etc. */ persistInvalidSszView(view: TreeView, suffix?: string): void; + regenStateForAttestationVerification( + attEpoch: Epoch, + shufflingDependentRoot: RootHex, + attHeadBlock: ProtoBlock, + regenCaller: RegenCaller + ): Promise; updateBuilderStatus(clockSlot: Slot): void; regenCanAcceptWork(): boolean; diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index b2a49ae4c07c..cee8d0614c30 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -12,10 +12,13 @@ import { MAX_VOLUNTARY_EXITS, MAX_BLS_TO_EXECUTION_CHANGES, BLS_WITHDRAWAL_PREFIX, + MAX_ATTESTER_SLASHINGS, } from "@lodestar/params"; import {Epoch, phase0, capella, ssz, ValidatorIndex} from "@lodestar/types"; import {IBeaconDb} from "../../db/index.js"; import {SignedBLSToExecutionChangeVersioned} from "../../util/types.js"; +import {BlockType} from "../interface.js"; +import {Metrics} from "../../metrics/metrics.js"; import {isValidBlsToExecutionChangeForBlockInclusion} from "./utils.js"; type HexRoot = string; @@ -164,7 +167,9 @@ export class OpPool { * slashings included earlier in the block. */ getSlashingsAndExits( - state: CachedBeaconStateAllForks + state: CachedBeaconStateAllForks, + blockType: BlockType, + metrics: Metrics | null ): [ phase0.AttesterSlashing[], phase0.ProposerSlashing[], @@ -177,6 +182,12 @@ export class OpPool { const toBeSlashedIndices = new Set(); const proposerSlashings: phase0.ProposerSlashing[] = []; + const stepsMetrics = + blockType === BlockType.Full + ? metrics?.executionBlockProductionTimeSteps + : metrics?.builderBlockProductionTimeSteps; + + const endProposerSlashing = stepsMetrics?.startTimer(); for (const proposerSlashing of this.proposerSlashings.values()) { const index = proposerSlashing.signedHeader1.message.proposerIndex; const validator = state.validators.getReadonly(index); @@ -189,22 +200,29 @@ export class OpPool { } } } + endProposerSlashing?.({ + step: "proposerSlashing", + }); + const endAttesterSlashings = stepsMetrics?.startTimer(); const attesterSlashings: phase0.AttesterSlashing[] = []; attesterSlashing: for (const attesterSlashing of this.attesterSlashings.values()) { /** Indices slashable in this attester slashing */ const slashableIndices = new Set(); for (let i = 0; i < attesterSlashing.intersectingIndices.length; i++) { const index = attesterSlashing.intersectingIndices[i]; - const validator = state.validators.getReadonly(index); - // If we already have a slashing for this index, we can continue on to the next slashing if (toBeSlashedIndices.has(index)) { continue attesterSlashing; } + + const validator = state.validators.getReadonly(index); if (isSlashableAtEpoch(validator, stateEpoch)) { slashableIndices.add(index); } + if (attesterSlashings.length >= MAX_ATTESTER_SLASHINGS) { + break attesterSlashing; + } } // If there were slashable indices in this slashing @@ -216,7 +234,11 @@ export class OpPool { } } } + endAttesterSlashings?.({ + step: "attesterSlashings", + }); + const endVoluntaryExits = stepsMetrics?.startTimer(); const voluntaryExits: phase0.SignedVoluntaryExit[] = []; for (const voluntaryExit of this.voluntaryExits.values()) { if ( @@ -233,7 +255,11 @@ export class OpPool { } } } + endVoluntaryExits?.({ + step: "voluntaryExits", + }); + const endBlsToExecutionChanges = stepsMetrics?.startTimer(); const blsToExecutionChanges: capella.SignedBLSToExecutionChange[] = []; for (const blsToExecutionChange of this.blsToExecutionChanges.values()) { if (isValidBlsToExecutionChangeForBlockInclusion(state, blsToExecutionChange.data)) { @@ -243,6 +269,9 @@ export class OpPool { } } } + endBlsToExecutionChanges?.({ + step: "blsToExecutionChanges", + }); return [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges]; } diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index d71cd55df673..cc7795ade0a1 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -3,12 +3,14 @@ import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; import {ArchiverOpts} from "./archiver/index.js"; import {ForkChoiceOpts} from "./forkChoice/index.js"; import {LightClientServerOpts} from "./lightClient/index.js"; +import {ShufflingCacheOpts} from "./shufflingCache.js"; export type IChainOptions = BlockProcessOpts & PoolOpts & SeenCacheOpts & ForkChoiceOpts & ArchiverOpts & + ShufflingCacheOpts & LightClientServerOpts & { blsVerifyAllMainThread?: boolean; blsVerifyAllMultiThread?: boolean; diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 8babd82756f8..ce8e720cd766 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -98,10 +98,18 @@ export class PrepareNextSlotScheduler { const prepareState = await this.chain.regen.getBlockSlotState( headRoot, prepareSlot, - {dontTransferCache: true}, + // the slot 0 of next epoch will likely use this Previous Root Checkpoint state for state transition so we transfer cache here + // for other slots dontTransferCached=true because we don't run state transition on this state + {dontTransferCache: !isEpochTransition}, RegenCaller.precomputeEpoch ); + // cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to produce a block at slot 0 of epoch + // see https://github.com/ChainSafe/lodestar/issues/6194 + const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({source: "prepare_next_slot"}); + prepareState.hashTreeRoot(); + hashTreeRootTimer?.(); + // assuming there is no reorg, it caches the checkpoint state & helps avoid doing a full state transition in the next slot // + when gossip block comes, we need to validate and run state transition // + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations @@ -116,6 +124,7 @@ export class PrepareNextSlotScheduler { nextEpoch, headSlot, prepareSlot, + previousHits, }); } diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts index bac501ed725c..f5d02dbf9b6f 100644 --- a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts +++ b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts @@ -4,7 +4,7 @@ import { ExecutionPayloadStatus, stateTransition, } from "@lodestar/state-transition"; -import {allForks, Root} from "@lodestar/types"; +import {allForks, Gwei, Root} from "@lodestar/types"; import {ZERO_HASH} from "../../constants/index.js"; import {Metrics} from "../../metrics/index.js"; @@ -17,7 +17,7 @@ export function computeNewStateRoot( metrics: Metrics | null, state: CachedBeaconStateAllForks, block: allForks.FullOrBlindedBeaconBlock -): Root { +): {newStateRoot: Root; proposerReward: Gwei} { // Set signature to zero to re-use stateTransition() function which requires the SignedBeaconBlock type const blockEmptySig = {message: block, signature: ZERO_HASH} as allForks.FullOrBlindedSignedBeaconBlock; @@ -41,5 +41,12 @@ export function computeNewStateRoot( metrics ); - return postState.hashTreeRoot(); + const {attestations, syncAggregate, slashing} = postState.proposerRewards; + const proposerReward = BigInt(attestations + syncAggregate + slashing); + + const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({source: "compute_new_state_root"}); + const newStateRoot = postState.hashTreeRoot(); + hashTreeRootTimer?.(); + + return {newStateRoot, proposerReward}; } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index acefbbf765a1..1c522c54a93d 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -123,10 +123,25 @@ export async function produceBlockBody( // } // } + const stepsMetrics = + blockType === BlockType.Full + ? this.metrics?.executionBlockProductionTimeSteps + : this.metrics?.builderBlockProductionTimeSteps; + const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = - this.opPool.getSlashingsAndExits(currentState); + this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); + + const endAttestations = stepsMetrics?.startTimer(); const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(this.forkChoice, currentState); + endAttestations?.({ + step: "attestations", + }); + + const endEth1DataAndDeposits = stepsMetrics?.startTimer(); const {eth1Data, deposits} = await this.eth1.getEth1DataAndDeposits(currentState); + endEth1DataAndDeposits?.({ + step: "eth1DataAndDeposits", + }); const blockBody: phase0.BeaconBlockBody = { randaoReveal, @@ -141,6 +156,7 @@ export async function produceBlockBody( const blockEpoch = computeEpochAtSlot(blockSlot); + const endSyncAggregate = stepsMetrics?.startTimer(); if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot); this.metrics?.production.producedSyncAggregateParticipants.observe( @@ -148,6 +164,9 @@ export async function produceBlockBody( ); (blockBody as altair.BeaconBlockBody).syncAggregate = syncAggregate; } + endSyncAggregate?.({ + step: "syncAggregate", + }); Object.assign(logMeta, { attestations: attestations.length, @@ -157,6 +176,7 @@ export async function produceBlockBody( proposerSlashings: proposerSlashings.length, }); + const endExecutionPayload = stepsMetrics?.startTimer(); if (isForkExecution(fork)) { const safeBlockHash = this.forkChoice.getJustifiedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; @@ -359,6 +379,9 @@ export async function produceBlockBody( blobsResult = {type: BlobsResultType.preDeneb}; executionPayloadValue = BigInt(0); } + endExecutionPayload?.({ + step: "executionPayload", + }); if (ForkSeq[fork] >= ForkSeq.capella) { // TODO: blsToExecutionChanges should be passed in the produceBlock call diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index dd111f14b4d1..5305502c8c05 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -149,14 +149,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { if (parentEpoch < blockEpoch) { const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch); if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) { - // TODO: Miss-use of checkpointStateCache here return checkpointState; - // console.error({ - // "checkpointState.slot": checkpointState.slot, - // "block.slot": block.slot, - // blockEpoch, - // blockEpochStartSlot: computeStartSlotAtEpoch(blockEpoch), - // }); } } diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts new file mode 100644 index 000000000000..c8468f3b6db5 --- /dev/null +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -0,0 +1,193 @@ +import {toHexString} from "@chainsafe/ssz"; +import {CachedBeaconStateAllForks, EpochShuffling, getShufflingDecisionBlock} from "@lodestar/state-transition"; +import {Epoch, RootHex, ssz} from "@lodestar/types"; +import {MapDef, pruneSetToMax} from "@lodestar/utils"; +import {GENESIS_SLOT} from "@lodestar/params"; +import {Metrics} from "../metrics/metrics.js"; +import {computeAnchorCheckpoint} from "./initState.js"; + +/** + * Same value to CheckpointBalancesCache, with the assumption that we don't have to use it for old epochs. In the worse case: + * - when loading state bytes from disk, we need to compute shuffling for all epochs (~1s as of Sep 2023) + * - don't have shuffling to verify attestations, need to do 1 epoch transition to add shuffling to this cache. This never happens + * with default chain option of maxSkipSlots = 32 + **/ +const MAX_EPOCHS = 4; + +/** + * With default chain option of maxSkipSlots = 32, there should be no shuffling promise. If that happens a lot, it could blow up Lodestar, + * with MAX_EPOCHS = 4, only allow 2 promise at a time. Note that regen already bounds number of concurrent requests at 1 already. + */ +const MAX_PROMISES = 2; + +enum CacheItemType { + shuffling, + promise, +} + +type ShufflingCacheItem = { + type: CacheItemType.shuffling; + shuffling: EpochShuffling; +}; + +type PromiseCacheItem = { + type: CacheItemType.promise; + promise: Promise; + resolveFn: (shuffling: EpochShuffling) => void; +}; + +type CacheItem = ShufflingCacheItem | PromiseCacheItem; + +export type ShufflingCacheOpts = { + maxShufflingCacheEpochs?: number; +}; + +/** + * A shuffling cache to help: + * - get committee quickly for attestation verification + * - if a shuffling is not available (which does not happen with default chain option of maxSkipSlots = 32), track a promise to make sure we don't compute the same shuffling twice + * - skip computing shuffling when loading state bytes from disk + */ +export class ShufflingCache { + /** LRU cache implemented as a map, pruned every time we add an item */ + private readonly itemsByDecisionRootByEpoch: MapDef> = new MapDef( + () => new Map() + ); + + private readonly maxEpochs: number; + + constructor( + private readonly metrics: Metrics | null = null, + opts: ShufflingCacheOpts = {} + ) { + if (metrics) { + metrics.shufflingCache.size.addCollect(() => + metrics.shufflingCache.size.set( + Array.from(this.itemsByDecisionRootByEpoch.values()).reduce((total, innerMap) => total + innerMap.size, 0) + ) + ); + } + + this.maxEpochs = opts.maxShufflingCacheEpochs ?? MAX_EPOCHS; + } + + /** + * Extract shuffling from state and add to cache + */ + processState(state: CachedBeaconStateAllForks, shufflingEpoch: Epoch): EpochShuffling { + const decisionBlockHex = getDecisionBlock(state, shufflingEpoch); + let shuffling: EpochShuffling; + switch (shufflingEpoch) { + case state.epochCtx.nextShuffling.epoch: + shuffling = state.epochCtx.nextShuffling; + break; + case state.epochCtx.currentShuffling.epoch: + shuffling = state.epochCtx.currentShuffling; + break; + case state.epochCtx.previousShuffling.epoch: + shuffling = state.epochCtx.previousShuffling; + break; + default: + throw new Error(`Shuffling not found from state ${state.slot} for epoch ${shufflingEpoch}`); + } + + let cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(shufflingEpoch).get(decisionBlockHex); + if (cacheItem !== undefined) { + // update existing promise + if (isPromiseCacheItem(cacheItem)) { + // unblock consumers of this promise + cacheItem.resolveFn(shuffling); + // then update item type to shuffling + cacheItem = { + type: CacheItemType.shuffling, + shuffling, + }; + this.add(shufflingEpoch, decisionBlockHex, cacheItem); + // we updated type to CacheItemType.shuffling so the above fields are not used anyway + this.metrics?.shufflingCache.processStateUpdatePromise.inc(); + } else { + // ShufflingCacheItem, do nothing + this.metrics?.shufflingCache.processStateNoOp.inc(); + } + } else { + // not found, new shuffling + this.add(shufflingEpoch, decisionBlockHex, {type: CacheItemType.shuffling, shuffling}); + this.metrics?.shufflingCache.processStateInsertNew.inc(); + } + + return shuffling; + } + + /** + * Insert a promise to make sure we don't regen state for the same shuffling. + * Bound by MAX_SHUFFLING_PROMISE to make sure our node does not blow up. + */ + insertPromise(shufflingEpoch: Epoch, decisionRootHex: RootHex): void { + const promiseCount = Array.from(this.itemsByDecisionRootByEpoch.values()) + .flatMap((innerMap) => Array.from(innerMap.values())) + .filter((item) => isPromiseCacheItem(item)).length; + if (promiseCount >= MAX_PROMISES) { + throw new Error( + `Too many shuffling promises: ${promiseCount}, shufflingEpoch: ${shufflingEpoch}, decisionRootHex: ${decisionRootHex}` + ); + } + let resolveFn: ((shuffling: EpochShuffling) => void) | null = null; + const promise = new Promise((resolve) => { + resolveFn = resolve; + }); + if (resolveFn === null) { + throw new Error("Promise Constructor was not executed immediately"); + } + + const cacheItem: PromiseCacheItem = { + type: CacheItemType.promise, + promise, + resolveFn, + }; + this.add(shufflingEpoch, decisionRootHex, cacheItem); + this.metrics?.shufflingCache.insertPromiseCount.inc(); + } + + /** + * Most of the time, this should return a shuffling immediately. + * If there's a promise, it means we are computing the same shuffling, so we wait for the promise to resolve. + * Return null if we don't have a shuffling for this epoch and dependentRootHex. + */ + async get(shufflingEpoch: Epoch, decisionRootHex: RootHex): Promise { + const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(shufflingEpoch).get(decisionRootHex); + if (cacheItem === undefined) { + return null; + } + + if (isShufflingCacheItem(cacheItem)) { + return cacheItem.shuffling; + } else { + // promise + return cacheItem.promise; + } + } + + private add(shufflingEpoch: Epoch, decisionBlock: RootHex, cacheItem: CacheItem): void { + this.itemsByDecisionRootByEpoch.getOrDefault(shufflingEpoch).set(decisionBlock, cacheItem); + pruneSetToMax(this.itemsByDecisionRootByEpoch, this.maxEpochs); + } +} + +function isShufflingCacheItem(item: CacheItem): item is ShufflingCacheItem { + return item.type === CacheItemType.shuffling; +} + +function isPromiseCacheItem(item: CacheItem): item is PromiseCacheItem { + return item.type === CacheItemType.promise; +} + +/** + * Get the shuffling decision block root for the given epoch of given state + * - Special case close to genesis block, return the genesis block root + * - This is similar to forkchoice.getDependentRoot() function, otherwise we cannot get cached shuffing in attestation verification when syncing from genesis. + */ +function getDecisionBlock(state: CachedBeaconStateAllForks, epoch: Epoch): RootHex { + return state.slot > GENESIS_SLOT + ? getShufflingDecisionBlock(state, epoch) + : toHexString(ssz.phase0.BeaconBlockHeader.hashTreeRoot(computeAnchorCheckpoint(state.config, state).blockHeader)); +} diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 0cd96a8278ec..5c6a308808ce 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -4,8 +4,6 @@ import {phase0, RootHex, ssz, ValidatorIndex} from "@lodestar/types"; import { computeEpochAtSlot, isAggregatorFromCommitteeLength, - getIndexedAttestationSignatureSet, - ISignatureSet, createAggregateSignatureSetFromComponents, } from "@lodestar/state-transition"; import {IBeaconChain} from ".."; @@ -14,8 +12,9 @@ import {RegenCaller} from "../regen/index.js"; import {getAttDataBase64FromSignedAggregateAndProofSerialized} from "../../util/sszBytes.js"; import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from "./signatureSets/index.js"; import { + getAttestationDataSigningRoot, getCommitteeIndices, - getStateForAttestationVerification, + getShufflingForAttestationVerification, verifyHeadBlockAndTargetRoot, verifyPropagationSlotRange, } from "./attestation.js"; @@ -142,17 +141,16 @@ async function validateAggregateAndProof( // -- i.e. get_ancestor(store, aggregate.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root // > Altready check in `chain.forkChoice.hasBlock(attestation.data.beaconBlockRoot)` - const attHeadState = await getStateForAttestationVerification( + const shuffling = await getShufflingForAttestationVerification( chain, - attSlot, attEpoch, attHeadBlock, - RegenCaller.validateGossipAggregateAndProof + RegenCaller.validateGossipAttestation ); const committeeIndices: number[] = cachedAttData ? cachedAttData.committeeIndices - : getCommitteeIndices(attHeadState, attSlot, attIndex); + : getCommitteeIndices(shuffling, attSlot, attIndex); const attestingIndices = aggregate.aggregationBits.intersectValues(committeeIndices); const indexedAttestation: phase0.IndexedAttestation = { @@ -185,21 +183,16 @@ async function validateAggregateAndProof( // by the validator with index aggregate_and_proof.aggregator_index. // [REJECT] The aggregator signature, signed_aggregate_and_proof.signature, is valid. // [REJECT] The signature of aggregate is valid. - const aggregator = attHeadState.epochCtx.index2pubkey[aggregateAndProof.aggregatorIndex]; - let indexedAttestationSignatureSet: ISignatureSet; - if (cachedAttData) { - const {signingRoot} = cachedAttData; - indexedAttestationSignatureSet = createAggregateSignatureSetFromComponents( - indexedAttestation.attestingIndices.map((i) => chain.index2pubkey[i]), - signingRoot, - indexedAttestation.signature - ); - } else { - indexedAttestationSignatureSet = getIndexedAttestationSignatureSet(attHeadState, indexedAttestation); - } + const aggregator = chain.index2pubkey[aggregateAndProof.aggregatorIndex]; + const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(chain.config, attData); + const indexedAttestationSignatureSet = createAggregateSignatureSetFromComponents( + indexedAttestation.attestingIndices.map((i) => chain.index2pubkey[i]), + signingRoot, + indexedAttestation.signature + ); const signatureSets = [ - getSelectionProofSignatureSet(attHeadState, attSlot, aggregator, signedAggregateAndProof), - getAggregateAndProofSignatureSet(attHeadState, attEpoch, aggregator, signedAggregateAndProof), + getSelectionProofSignatureSet(chain.config, attSlot, aggregator, signedAggregateAndProof), + getAggregateAndProofSignatureSet(chain.config, attEpoch, aggregator, signedAggregateAndProof), indexedAttestationSignatureSet, ]; // no need to write to SeenAttestationDatas diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 0b642101f010..31e105911ab4 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -1,16 +1,18 @@ import {toHexString} from "@chainsafe/ssz"; import {phase0, Epoch, Root, Slot, RootHex, ssz} from "@lodestar/types"; import {ProtoBlock} from "@lodestar/fork-choice"; -import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq} from "@lodestar/params"; +import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq, DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import { computeEpochAtSlot, - CachedBeaconStateAllForks, - getAttestationDataSigningRoot, createSingleSignatureSetFromComponents, SingleSignatureSet, EpochCacheError, EpochCacheErrorCode, + EpochShuffling, + computeStartSlotAtEpoch, + computeSigningRoot, } from "@lodestar/state-transition"; +import {BeaconConfig} from "@lodestar/config"; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; import {MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC} from "../../constants/index.js"; import {RegenCaller} from "../regen/index.js"; @@ -24,6 +26,7 @@ import {AttestationDataCacheEntry} from "../seenCache/seenAttestationData.js"; import {sszDeserializeAttestation} from "../../network/gossip/topic.js"; import {Result, wrapError} from "../../util/wrapError.js"; import {IBeaconChain} from "../interface.js"; +import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; export type BatchResult = { results: Result[]; @@ -56,12 +59,6 @@ export type Step0Result = AttestationValidationResult & { validatorIndex: number; }; -/** - * The beacon chain shufflings are designed to provide 1 epoch lookahead - * At each state, we have previous shuffling, current shuffling and next shuffling - */ -const SHUFFLING_LOOK_AHEAD_EPOCHS = 1; - /** * Validate a single gossip attestation, do not prioritize bls signature set */ @@ -359,9 +356,8 @@ async function validateGossipAttestationNoSignatureCheck( // --i.e. get_ancestor(store, attestation.data.beacon_block_root, compute_start_slot_at_epoch(attestation.data.target.epoch)) == attestation.data.target.root // > Altready check in `verifyHeadBlockAndTargetRoot()` - const attHeadState = await getStateForAttestationVerification( + const shuffling = await getShufflingForAttestationVerification( chain, - attSlot, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation @@ -369,9 +365,9 @@ async function validateGossipAttestationNoSignatureCheck( // [REJECT] The committee index is within the expected range // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) - committeeIndices = getCommitteeIndices(attHeadState, attSlot, attIndex); - getSigningRoot = () => getAttestationDataSigningRoot(attHeadState, attData); - expectedSubnet = attHeadState.epochCtx.computeSubnetForSlot(attSlot, attIndex); + committeeIndices = getCommitteeIndices(shuffling, attSlot, attIndex); + getSigningRoot = () => getAttestationDataSigningRoot(chain.config, attData); + expectedSubnet = computeSubnetForSlot(shuffling, attSlot, attIndex); } const validatorIndex = committeeIndices[bitIndex]; @@ -568,36 +564,46 @@ export function verifyHeadBlockAndTargetRoot( } /** - * Get a state for attestation verification. - * Use head state if: - * - attestation slot is in the same fork as head block - * - head state includes committees of target epoch + * Get a shuffling for attestation verification from the ShufflingCache. + * - if blockEpoch is attEpoch, use current shuffling of head state + * - if blockEpoch is attEpoch - 1, use next shuffling of head state + * - if blockEpoch is less than attEpoch - 1, dial head state to attEpoch - 1, and add to ShufflingCache + * + * This implementation does not require to dial head state to attSlot at fork boundary because we always get domain of attSlot + * in consumer context. * - * Otherwise, regenerate state from head state dialing to target epoch + * This is similar to the old getStateForAttestationVerification + * see https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L566 */ -export async function getStateForAttestationVerification( +export async function getShufflingForAttestationVerification( chain: IBeaconChain, - attSlot: Slot, attEpoch: Epoch, attHeadBlock: ProtoBlock, regenCaller: RegenCaller -): Promise { - const isSameFork = chain.config.getForkSeq(attSlot) === chain.config.getForkSeq(attHeadBlock.slot); - // thanks for 1 epoch look ahead of shuffling, a state at epoch n can get committee for epoch n+1 - const headStateHasTargetEpochCommmittee = - attEpoch - computeEpochAtSlot(attHeadBlock.slot) <= SHUFFLING_LOOK_AHEAD_EPOCHS; - try { - if (isSameFork && headStateHasTargetEpochCommmittee) { - // most of the time it should just use head state - chain.metrics?.gossipAttestation.useHeadBlockState.inc({caller: regenCaller}); - return await chain.regen.getState(attHeadBlock.stateRoot, regenCaller); - } +): Promise { + const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); + const shufflingDependentRoot = getShufflingDependentRoot(chain.forkChoice, attEpoch, blockEpoch, attHeadBlock); + + const shuffling = await chain.shufflingCache.get(attEpoch, shufflingDependentRoot); + if (shuffling) { + // most of the time, we should get the shuffling from cache + chain.metrics?.gossipAttestation.shufflingCacheHit.inc({caller: regenCaller}); + return shuffling; + } - // at fork boundary we should dial head state to target epoch - // see https://github.com/ChainSafe/lodestar/pull/4849 - chain.metrics?.gossipAttestation.useHeadBlockStateDialedToTargetEpoch.inc({caller: regenCaller}); - return await chain.regen.getBlockSlotState(attHeadBlock.blockRoot, attSlot, {dontTransferCache: true}, regenCaller); + chain.metrics?.gossipAttestation.shufflingCacheMiss.inc({caller: regenCaller}); + try { + // for the 1st time of the same epoch and dependent root, it awaits for the regen state + // from the 2nd time, it should use the same cached promise and it should reach the above code + chain.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({caller: regenCaller}); + return await chain.regenStateForAttestationVerification( + attEpoch, + shufflingDependentRoot, + attHeadBlock, + regenCaller + ); } catch (e) { + chain.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({caller: regenCaller}); throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.MISSING_STATE_TO_VERIFY_ATTESTATION, error: e as Error, @@ -605,6 +611,19 @@ export async function getStateForAttestationVerification( } } +/** + * Different version of getAttestationDataSigningRoot in state-transition which doesn't require a state. + */ +export function getAttestationDataSigningRoot(config: BeaconConfig, data: phase0.AttestationData): Uint8Array { + const slot = computeStartSlotAtEpoch(data.target.epoch); + // previously, we call `domain = config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot)` + // at fork boundary, it's required to dial to target epoch https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L573 + // instead of that, just use the fork at slot in the attestation data + const fork = config.getForkName(slot); + const domain = config.getDomainAtFork(fork, DOMAIN_BEACON_ATTESTER); + return computeSigningRoot(ssz.phase0.AttestationData, data, domain); +} + /** * Checks if the `attestation.data.beaconBlockRoot` is known to this chain. * @@ -680,21 +699,10 @@ function verifyAttestationTargetRoot(headBlock: ProtoBlock, targetRoot: Root, at } export function getCommitteeIndices( - attestationTargetState: CachedBeaconStateAllForks, + shuffling: EpochShuffling, attestationSlot: Slot, attestationIndex: number ): number[] { - const shuffling = attestationTargetState.epochCtx.getShufflingAtSlotOrNull(attestationSlot); - if (shuffling === null) { - // this may come from an out-of-synced node, the spec did not define it so should not REJECT - // see https://github.com/ChainSafe/lodestar/issues/4396 - throw new AttestationError(GossipAction.IGNORE, { - code: AttestationErrorCode.NO_COMMITTEE_FOR_SLOT_AND_INDEX, - index: attestationIndex, - slot: attestationSlot, - }); - } - const {committees} = shuffling; const slotCommittees = committees[attestationSlot % SLOTS_PER_EPOCH]; @@ -710,9 +718,8 @@ export function getCommitteeIndices( /** * Compute the correct subnet for a slot/committee index */ -export function computeSubnetForSlot(state: CachedBeaconStateAllForks, slot: number, committeeIndex: number): number { +export function computeSubnetForSlot(shuffling: EpochShuffling, slot: number, committeeIndex: number): number { const slotsSinceEpochStart = slot % SLOTS_PER_EPOCH; - const committeesPerSlot = state.epochCtx.getCommitteeCountPerSlot(computeEpochAtSlot(slot)); - const committeesSinceEpochStart = committeesPerSlot * slotsSinceEpochStart; + const committeesSinceEpochStart = shuffling.committeesPerSlot * slotsSinceEpochStart; return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT; } diff --git a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts index 099590ee019e..2bc2e62c861f 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts @@ -3,32 +3,36 @@ import {DOMAIN_AGGREGATE_AND_PROOF} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {Epoch, phase0} from "@lodestar/types"; import { - CachedBeaconStateAllForks, computeSigningRoot, computeStartSlotAtEpoch, createSingleSignatureSetFromComponents, ISignatureSet, } from "@lodestar/state-transition"; +import {BeaconConfig} from "@lodestar/config"; export function getAggregateAndProofSigningRoot( - state: CachedBeaconStateAllForks, + config: BeaconConfig, epoch: Epoch, aggregateAndProof: phase0.SignedAggregateAndProof ): Uint8Array { + // previously, we call `const aggregatorDomain = state.config.getDomain(state.slot, DOMAIN_AGGREGATE_AND_PROOF, slot);` + // at fork boundary, it's required to dial to target epoch https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L573 + // instead of that, just use the fork of slot in the attestation data const slot = computeStartSlotAtEpoch(epoch); - const aggregatorDomain = state.config.getDomain(state.slot, DOMAIN_AGGREGATE_AND_PROOF, slot); + const fork = config.getForkName(slot); + const aggregatorDomain = config.getDomainAtFork(fork, DOMAIN_AGGREGATE_AND_PROOF); return computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof.message, aggregatorDomain); } export function getAggregateAndProofSignatureSet( - state: CachedBeaconStateAllForks, + config: BeaconConfig, epoch: Epoch, aggregator: PublicKey, aggregateAndProof: phase0.SignedAggregateAndProof ): ISignatureSet { return createSingleSignatureSetFromComponents( aggregator, - getAggregateAndProofSigningRoot(state, epoch, aggregateAndProof), + getAggregateAndProofSigningRoot(config, epoch, aggregateAndProof), aggregateAndProof.signature ); } diff --git a/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts index dbb8e3380606..09e0a5ef12be 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts @@ -1,27 +1,27 @@ import type {PublicKey} from "@chainsafe/bls/types"; import {DOMAIN_SELECTION_PROOF} from "@lodestar/params"; import {phase0, Slot, ssz} from "@lodestar/types"; -import { - CachedBeaconStateAllForks, - computeSigningRoot, - createSingleSignatureSetFromComponents, - ISignatureSet, -} from "@lodestar/state-transition"; +import {computeSigningRoot, createSingleSignatureSetFromComponents, ISignatureSet} from "@lodestar/state-transition"; +import {BeaconConfig} from "@lodestar/config"; -export function getSelectionProofSigningRoot(state: CachedBeaconStateAllForks, slot: Slot): Uint8Array { - const selectionProofDomain = state.config.getDomain(state.slot, DOMAIN_SELECTION_PROOF, slot); +export function getSelectionProofSigningRoot(config: BeaconConfig, slot: Slot): Uint8Array { + // previously, we call `const selectionProofDomain = config.getDomain(state.slot, DOMAIN_SELECTION_PROOF, slot)` + // at fork boundary, it's required to dial to target epoch https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L573 + // instead of that, just use the fork of slot in the attestation data + const fork = config.getForkName(slot); + const selectionProofDomain = config.getDomainAtFork(fork, DOMAIN_SELECTION_PROOF); return computeSigningRoot(ssz.Slot, slot, selectionProofDomain); } export function getSelectionProofSignatureSet( - state: CachedBeaconStateAllForks, + config: BeaconConfig, slot: Slot, aggregator: PublicKey, aggregateAndProof: phase0.SignedAggregateAndProof ): ISignatureSet { return createSingleSignatureSetFromComponents( aggregator, - getSelectionProofSigningRoot(state, slot), + getSelectionProofSigningRoot(config, slot), aggregateAndProof.message.selectionProof ); } diff --git a/packages/beacon-node/src/eth1/provider/eth1Provider.ts b/packages/beacon-node/src/eth1/provider/eth1Provider.ts index 151d729e66e3..d594c74a3abc 100644 --- a/packages/beacon-node/src/eth1/provider/eth1Provider.ts +++ b/packages/beacon-node/src/eth1/provider/eth1Provider.ts @@ -1,7 +1,7 @@ import {toHexString} from "@chainsafe/ssz"; import {phase0} from "@lodestar/types"; import {ChainConfig} from "@lodestar/config"; -import {fromHex, isErrorAborted, createElapsedTimeTracker} from "@lodestar/utils"; +import {fromHex, isErrorAborted, createElapsedTimeTracker, toSafePrintableUrl} from "@lodestar/utils"; import {Logger} from "@lodestar/logger"; import {FetchError, isFetchError} from "@lodestar/api"; @@ -73,7 +73,9 @@ export class Eth1Provider implements IEth1Provider { this.logger = opts.logger; this.deployBlock = opts.depositContractDeployBlock ?? 0; this.depositContractAddress = toHexString(config.DEPOSIT_CONTRACT_ADDRESS); - this.rpc = new JsonRpcHttpClient(opts.providerUrls ?? DEFAULT_PROVIDER_URLS, { + + const providerUrls = opts.providerUrls ?? DEFAULT_PROVIDER_URLS; + this.rpc = new JsonRpcHttpClient(providerUrls, { signal, // Don't fallback with is truncated error. Throw early and let the retry on this class handle it shouldNotFallback: isJsonRpcTruncatedError, @@ -82,6 +84,7 @@ export class Eth1Provider implements IEth1Provider { jwtVersion: opts.jwtVersion, metrics: metrics, }); + this.logger?.info("Eth1 provider", {urls: providerUrls.map(toSafePrintableUrl).toString()}); this.rpc.emitter.on(JsonRpcHttpClientEvent.RESPONSE, () => { const oldState = this.state; diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index bfe003372ced..20b7d4751c81 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -6,16 +6,17 @@ import { reconstructFullBlockOrContents, } from "@lodestar/state-transition"; import {ChainForkConfig} from "@lodestar/config"; +import {Logger} from "@lodestar/logger"; import {getClient, Api as BuilderApi} from "@lodestar/api/builder"; import {SLOTS_PER_EPOCH, ForkExecution} from "@lodestar/params"; - +import {toSafePrintableUrl} from "@lodestar/utils"; import {ApiError} from "@lodestar/api"; import {Metrics} from "../../metrics/metrics.js"; import {IExecutionBuilder} from "./interface.js"; export type ExecutionBuilderHttpOpts = { enabled: boolean; - urls: string[]; + url: string; timeout?: number; faultInspectionWindow?: number; allowedFaults?: number; @@ -28,7 +29,7 @@ export type ExecutionBuilderHttpOpts = { export const defaultExecutionBuilderHttpOpts: ExecutionBuilderHttpOpts = { enabled: false, - urls: ["http://localhost:8661"], + url: "http://localhost:8661", timeout: 12000, }; @@ -41,8 +42,13 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { faultInspectionWindow: number; allowedFaults: number; - constructor(opts: ExecutionBuilderHttpOpts, config: ChainForkConfig, metrics: Metrics | null = null) { - const baseUrl = opts.urls[0]; + constructor( + opts: ExecutionBuilderHttpOpts, + config: ChainForkConfig, + metrics: Metrics | null = null, + logger?: Logger + ) { + const baseUrl = opts.url; if (!baseUrl) throw Error("No Url provided for executionBuilder"); this.api = getClient( { @@ -52,6 +58,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { }, {config, metrics: metrics?.builderHttpClient} ); + logger?.info("External builder", {url: toSafePrintableUrl(baseUrl)}); this.config = config; this.issueLocalFcUWithFeeRecipient = opts.issueLocalFcUWithFeeRecipient; diff --git a/packages/beacon-node/src/execution/builder/index.ts b/packages/beacon-node/src/execution/builder/index.ts index 530d541f8450..2f584ad7dadd 100644 --- a/packages/beacon-node/src/execution/builder/index.ts +++ b/packages/beacon-node/src/execution/builder/index.ts @@ -1,4 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; +import {Logger} from "@lodestar/logger"; import {Metrics} from "../../metrics/metrics.js"; import {IExecutionBuilder} from "./interface.js"; @@ -12,11 +13,12 @@ export const defaultExecutionBuilderOpts: ExecutionBuilderOpts = defaultExecutio export function initializeExecutionBuilder( opts: ExecutionBuilderOpts, config: ChainForkConfig, - metrics: Metrics | null = null + metrics: Metrics | null = null, + logger?: Logger ): IExecutionBuilder { switch (opts.mode) { case "http": default: - return new ExecutionBuilderHttp(opts, config, metrics); + return new ExecutionBuilderHttp(opts, config, metrics, logger); } } diff --git a/packages/beacon-node/src/execution/engine/index.ts b/packages/beacon-node/src/execution/engine/index.ts index 743abf203de9..2d92a439c86d 100644 --- a/packages/beacon-node/src/execution/engine/index.ts +++ b/packages/beacon-node/src/execution/engine/index.ts @@ -1,4 +1,4 @@ -import {fromHex} from "@lodestar/utils"; +import {fromHex, toSafePrintableUrl} from "@lodestar/utils"; import {JsonRpcHttpClient} from "../../eth1/provider/jsonRpcHttpClient.js"; import {IExecutionEngine} from "./interface.js"; import {ExecutionEngineDisabled} from "./disabled.js"; @@ -39,6 +39,7 @@ export function getExecutionEngineHttp( jwtId: opts.jwtId, jwtVersion: opts.jwtVersion, }); + modules.logger.info("Execution client", {urls: opts.urls.map(toSafePrintableUrl).toString()}); return new ExecutionEngineHttp(rpc, modules); } diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 2b763599f6e1..8d9094f19a25 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -121,6 +121,38 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { buckets: [0.1, 1, 2, 4, 10], labelNames: ["source"], }), + executionBlockProductionTimeSteps: register.histogram<"step">({ + name: "beacon_block_production_execution_steps_seconds", + help: "Detailed steps runtime of execution block production", + buckets: [0.01, 0.1, 0.2, 0.5, 1], + /** + * - proposerSlashing + * - attesterSlashings + * - voluntaryExits + * - blsToExecutionChanges + * - attestations + * - eth1DataAndDeposits + * - syncAggregate + * - executionPayload + */ + labelNames: ["step"], + }), + builderBlockProductionTimeSteps: register.histogram<"step">({ + name: "beacon_block_production_builder_steps_seconds", + help: "Detailed steps runtime of builder block production", + buckets: [0.01, 0.1, 0.2, 0.5, 1], + /** + * - proposerSlashing + * - attesterSlashings + * - voluntaryExits + * - blsToExecutionChanges + * - attestations + * - eth1DataAndDeposits + * - syncAggregate + * - executionPayload + */ + labelNames: ["step"], + }), blockProductionRequests: register.gauge<"source">({ name: "beacon_block_production_requests_total", help: "Count of all block production requests", diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index b6f0d78f3b06..8a22fe8f0a9b 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -286,6 +286,12 @@ export function createLodestarMetrics( help: "Time to call commit after process a single epoch transition in seconds", buckets: [0.01, 0.05, 0.1, 0.2, 0.5, 0.75, 1], }), + epochTransitionStepTime: register.histogram<"step">({ + name: "lodestar_stfn_epoch_transition_step_seconds", + help: "Time to call each step of epoch transition in seconds", + labelNames: ["step"], + buckets: [0.01, 0.05, 0.1, 0.2, 0.5, 0.75, 1], + }), processBlockTime: register.histogram({ name: "lodestar_stfn_process_block_seconds", help: "Time to process a single block in seconds", @@ -298,10 +304,11 @@ export function createLodestarMetrics( help: "Time to call commit after process a single block in seconds", buckets: [0.005, 0.01, 0.02, 0.05, 0.1, 1], }), - stateHashTreeRootTime: register.histogram({ + stateHashTreeRootTime: register.histogram<"source">({ name: "lodestar_stfn_hash_tree_root_seconds", help: "Time to compute the hash tree root of a post state in seconds", - buckets: [0.005, 0.01, 0.02, 0.05, 0.1, 1], + buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5], + labelNames: ["source"], }), preStateBalancesNodesPopulatedMiss: register.gauge<"source">({ name: "lodestar_stfn_balances_nodes_populated_miss_total", @@ -590,6 +597,26 @@ export function createLodestarMetrics( labelNames: ["caller"], buckets: [0, 1, 2, 4, 8, 16, 32, 64], }), + shufflingCacheHit: register.gauge<"caller">({ + name: "lodestar_gossip_attestation_shuffling_cache_hit_count", + help: "Count of gossip attestation verification shuffling cache hit", + labelNames: ["caller"], + }), + shufflingCacheMiss: register.gauge<"caller">({ + name: "lodestar_gossip_attestation_shuffling_cache_miss_count", + help: "Count of gossip attestation verification shuffling cache miss", + labelNames: ["caller"], + }), + shufflingCacheRegenHit: register.gauge<"caller">({ + name: "lodestar_gossip_attestation_shuffling_cache_regen_hit_count", + help: "Count of gossip attestation verification shuffling cache regen hit", + labelNames: ["caller"], + }), + shufflingCacheRegenMiss: register.gauge<"caller">({ + name: "lodestar_gossip_attestation_shuffling_cache_regen_miss_count", + help: "Count of gossip attestation verification shuffling cache regen miss", + labelNames: ["caller"], + }), attestationSlotToClockSlot: register.histogram<"caller">({ name: "lodestar_gossip_attestation_attestation_slot_to_clock_slot", help: "Slot distance between clock slot and attestation slot", @@ -1072,6 +1099,29 @@ export function createLodestarMetrics( }), }, + shufflingCache: { + size: register.gauge({ + name: "lodestar_shuffling_cache_size", + help: "Shuffling cache size", + }), + processStateInsertNew: register.gauge({ + name: "lodestar_shuffling_cache_process_state_insert_new_total", + help: "Total number of times processState is called resulting a new shuffling", + }), + processStateUpdatePromise: register.gauge({ + name: "lodestar_shuffling_cache_process_state_update_promise_total", + help: "Total number of times processState is called resulting a promise being updated with shuffling", + }), + processStateNoOp: register.gauge({ + name: "lodestar_shuffling_cache_process_state_no_op_total", + help: "Total number of times processState is called resulting no changes", + }), + insertPromiseCount: register.gauge({ + name: "lodestar_shuffling_cache_insert_promise_count", + help: "Total number of times insertPromise is called", + }), + }, + seenCache: { aggregatedAttestations: { superSetCheckTotal: register.histogram({ diff --git a/packages/beacon-node/src/network/core/metrics.ts b/packages/beacon-node/src/network/core/metrics.ts index e5ce0bede447..4f416ad4fba2 100644 --- a/packages/beacon-node/src/network/core/metrics.ts +++ b/packages/beacon-node/src/network/core/metrics.ts @@ -291,45 +291,6 @@ export function createNetworkCoreMetrics(register: RegistryMetricCreator) { labelNames: ["subnet"], }), }, - - // Gossip block - gossipBlock: { - elapsedTimeTillReceived: register.histogram({ - name: "lodestar_gossip_block_elapsed_time_till_received", - help: "Time elapsed between block slot time and the time block received via gossip", - buckets: [0.5, 1, 2, 4, 6, 12], - }), - elapsedTimeTillProcessed: register.histogram({ - name: "lodestar_gossip_block_elapsed_time_till_processed", - help: "Time elapsed between block slot time and the time block processed", - buckets: [0.5, 1, 2, 4, 6, 12], - }), - receivedToGossipValidate: register.histogram({ - name: "lodestar_gossip_block_received_to_gossip_validate", - help: "Time elapsed between block received and block validated", - buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5, 2, 4], - }), - receivedToStateTransition: register.histogram({ - name: "lodestar_gossip_block_received_to_state_transition", - help: "Time elapsed between block received and block state transition", - buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5, 2, 4], - }), - receivedToSignaturesVerification: register.histogram({ - name: "lodestar_gossip_block_received_to_signatures_verification", - help: "Time elapsed between block received and block signatures verification", - buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5, 2, 4], - }), - receivedToExecutionPayloadVerification: register.histogram({ - name: "lodestar_gossip_block_received_to_execution_payload_verification", - help: "Time elapsed between block received and execution payload verification", - buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5, 2, 4], - }), - receivedToBlockImport: register.histogram({ - name: "lodestar_gossip_block_received_to_block_import", - help: "Time elapsed between block received and block import", - buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5, 2, 4], - }), - }, }; } diff --git a/packages/beacon-node/src/network/core/networkCore.ts b/packages/beacon-node/src/network/core/networkCore.ts index 498e556b040e..3037b69b4263 100644 --- a/packages/beacon-node/src/network/core/networkCore.ts +++ b/packages/beacon-node/src/network/core/networkCore.ts @@ -429,6 +429,14 @@ export class NetworkCore implements INetworkCore { return this.peerManager["discovery"]?.discv5.writeProfile(durationMs, dirpath) ?? "no discv5"; } + writeNetworkHeapSnapshot(): Promise { + throw new Error("Method not implemented, please configure network thread"); + } + + writeDiscv5HeapSnapshot(prefix: string, dirpath: string): Promise { + return this.peerManager["discovery"]?.discv5.writeHeapSnapshot(prefix, dirpath) ?? Promise.resolve("no discv5"); + } + /** * Handle subscriptions through fork transitions, @see FORK_EPOCH_LOOKAHEAD */ diff --git a/packages/beacon-node/src/network/core/networkCoreWorker.ts b/packages/beacon-node/src/network/core/networkCoreWorker.ts index 35303190a8f8..2cbef57a16a2 100644 --- a/packages/beacon-node/src/network/core/networkCoreWorker.ts +++ b/packages/beacon-node/src/network/core/networkCoreWorker.ts @@ -10,7 +10,7 @@ import {RegistryMetricCreator, collectNodeJSMetrics} from "../../metrics/index.j import {AsyncIterableBridgeCaller, AsyncIterableBridgeHandler} from "../../util/asyncIterableToEvents.js"; import {Clock} from "../../util/clock.js"; import {peerIdToString} from "../../util/peerId.js"; -import {profileNodeJS} from "../../util/profile.js"; +import {profileNodeJS, writeHeapSnapshot} from "../../util/profile.js"; import {NetworkEventBus, NetworkEventData, networkEventDirection} from "../events.js"; import {wireEventsOnWorkerThread} from "../../util/workerEvents.js"; import {getNetworkCoreWorkerMetrics} from "./metrics.js"; @@ -162,6 +162,12 @@ const libp2pWorkerApi: NetworkWorkerApi = { writeDiscv5Profile: async (durationMs: number, dirpath: string) => { return core.writeDiscv5Profile(durationMs, dirpath); }, + writeHeapSnapshot: async (prefix: string, dirpath: string) => { + return writeHeapSnapshot(prefix, dirpath); + }, + writeDiscv5HeapSnapshot: async (prefix: string, dirpath: string) => { + return core.writeDiscv5HeapSnapshot(prefix, dirpath); + }, }; expose(libp2pWorkerApi as ModuleThread); diff --git a/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts b/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts index ddffb5fc1460..ae39e6759099 100644 --- a/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts +++ b/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts @@ -249,6 +249,12 @@ export class WorkerNetworkCore implements INetworkCore { writeDiscv5Profile(durationMs: number, dirpath: string): Promise { return this.getApi().writeDiscv5Profile(durationMs, dirpath); } + writeNetworkHeapSnapshot(prefix: string, dirpath: string): Promise { + return this.getApi().writeHeapSnapshot(prefix, dirpath); + } + writeDiscv5HeapSnapshot(prefix: string, dirpath: string): Promise { + return this.getApi().writeDiscv5HeapSnapshot(prefix, dirpath); + } private getApi(): ModuleThread { return this.modules.networkThreadApi; diff --git a/packages/beacon-node/src/network/core/types.ts b/packages/beacon-node/src/network/core/types.ts index 790c532aa2a4..41b7f669eaac 100644 --- a/packages/beacon-node/src/network/core/types.ts +++ b/packages/beacon-node/src/network/core/types.ts @@ -63,6 +63,8 @@ export interface INetworkCore extends INetworkCorePublic { scrapeMetrics(): Promise; writeNetworkThreadProfile(durationMs: number, dirpath: string): Promise; writeDiscv5Profile(durationMs: number, dirpath: string): Promise; + writeNetworkHeapSnapshot(prefix: string, dirpath: string): Promise; + writeDiscv5HeapSnapshot(prefix: string, dirpath: string): Promise; } /** diff --git a/packages/beacon-node/src/network/discv5/index.ts b/packages/beacon-node/src/network/discv5/index.ts index bf0108ebe571..babe279afe86 100644 --- a/packages/beacon-node/src/network/discv5/index.ts +++ b/packages/beacon-node/src/network/discv5/index.ts @@ -107,6 +107,10 @@ export class Discv5Worker extends (EventEmitter as {new (): StrictEventEmitter { + return this.workerApi.writeHeapSnapshot(prefix, dirpath); + } + private decodeEnrs(objs: ENRData[]): ENR[] { const enrs: ENR[] = []; for (const obj of objs) { diff --git a/packages/beacon-node/src/network/discv5/types.ts b/packages/beacon-node/src/network/discv5/types.ts index cea262cc9e89..3c7677b4575c 100644 --- a/packages/beacon-node/src/network/discv5/types.ts +++ b/packages/beacon-node/src/network/discv5/types.ts @@ -65,6 +65,8 @@ export type Discv5WorkerApi = { /** write profile to disc */ writeProfile(durationMs: number, dirpath: string): Promise; + /** write heap snapshot to disc */ + writeHeapSnapshot(prefix: string, dirpath: string): Promise; /** tear down discv5 resources */ close(): Promise; }; diff --git a/packages/beacon-node/src/network/discv5/worker.ts b/packages/beacon-node/src/network/discv5/worker.ts index 31f434f94476..1b50ee86aa29 100644 --- a/packages/beacon-node/src/network/discv5/worker.ts +++ b/packages/beacon-node/src/network/discv5/worker.ts @@ -11,7 +11,7 @@ import {createBeaconConfig} from "@lodestar/config"; import {getNodeLogger} from "@lodestar/logger/node"; import {RegistryMetricCreator} from "../../metrics/index.js"; import {collectNodeJSMetrics} from "../../metrics/nodeJsMetrics.js"; -import {profileNodeJS} from "../../util/profile.js"; +import {profileNodeJS, writeHeapSnapshot} from "../../util/profile.js"; import {Discv5WorkerApi, Discv5WorkerData} from "./types.js"; import {enrRelevance, ENRRelevance} from "./utils.js"; @@ -107,6 +107,9 @@ const module: Discv5WorkerApi = { fs.writeFileSync(filePath, profile); return filePath; }, + writeHeapSnapshot: async (prefix: string, dirpath: string) => { + return writeHeapSnapshot(prefix, dirpath); + }, async close() { closeMetrics?.(); discv5.removeListener("discovered", onDiscovered); diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index e0718368c672..047263d15022 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -60,6 +60,8 @@ export interface INetwork extends INetworkCorePublic { dumpGossipQueue(gossipType: GossipType): Promise; writeNetworkThreadProfile(durationMs: number, dirpath: string): Promise; writeDiscv5Profile(durationMs: number, dirpath: string): Promise; + writeNetworkHeapSnapshot(prefix: string, dirpath: string): Promise; + writeDiscv5HeapSnapshot(prefix: string, dirpath: string): Promise; } export type LodestarComponents = Pick< diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 834ebacaa7a8..d2571a2a92e0 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -557,6 +557,14 @@ export class Network implements INetwork { return this.core.writeDiscv5Profile(durationMs, dirpath); } + async writeNetworkHeapSnapshot(prefix: string, dirpath: string): Promise { + return this.core.writeNetworkHeapSnapshot(prefix, dirpath); + } + + async writeDiscv5HeapSnapshot(prefix: string, dirpath: string): Promise { + return this.core.writeDiscv5HeapSnapshot(prefix, dirpath); + } + private onLightClientFinalityUpdate = async (finalityUpdate: allForks.LightClientFinalityUpdate): Promise => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) diff --git a/packages/beacon-node/src/node/nodejs.ts b/packages/beacon-node/src/node/nodejs.ts index 3c9f2ec0b54b..da4f802a521c 100644 --- a/packages/beacon-node/src/node/nodejs.ts +++ b/packages/beacon-node/src/node/nodejs.ts @@ -214,7 +214,7 @@ export class BeaconNode { logger: logger.child({module: LoggerModule.execution}), }), executionBuilder: opts.executionBuilder.enabled - ? initializeExecutionBuilder(opts.executionBuilder, config, metrics) + ? initializeExecutionBuilder(opts.executionBuilder, config, metrics, logger) : undefined, }); diff --git a/packages/beacon-node/src/node/notifier.ts b/packages/beacon-node/src/node/notifier.ts index 33ff1d185bb1..8c393a4fcb05 100644 --- a/packages/beacon-node/src/node/notifier.ts +++ b/packages/beacon-node/src/node/notifier.ts @@ -33,7 +33,7 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise(); - - /** - * The number of slots ahead of us that is allowed before starting a RangeSync - * If a peer is within this tolerance (forwards or backwards), it is treated as a fully sync'd peer. - * - * This means that we consider ourselves synced (and hence subscribe to all subnets and block - * gossip if no peers are further than this range ahead of us that we have not already downloaded - * blocks for. - */ private readonly slotImportTolerance: Slot; constructor(opts: SyncOptions, modules: SyncModules) { @@ -48,7 +39,7 @@ export class BeaconSync implements IBeaconSync { this.logger = logger; this.rangeSync = new RangeSync(modules, opts); this.unknownBlockSync = new UnknownBlockSync(config, network, chain, logger, metrics, opts); - this.slotImportTolerance = SLOTS_PER_EPOCH; + this.slotImportTolerance = opts.slotImportTolerance ?? SLOTS_PER_EPOCH; // Subscribe to RangeSync completing a SyncChain and recompute sync state if (!opts.disableRangeSync) { @@ -241,7 +232,7 @@ export class BeaconSync implements IBeaconSync { } } - // If we stopped being synced and falled significantly behind, stop gossip + // If we stopped being synced and fallen significantly behind, stop gossip else if (state !== SyncState.Synced) { const syncDiff = this.chain.clock.currentSlot - this.chain.forkChoice.getHead().slot; if (syncDiff > this.slotImportTolerance * 2) { diff --git a/packages/beacon-node/src/util/dependentRoot.ts b/packages/beacon-node/src/util/dependentRoot.ts new file mode 100644 index 000000000000..58b26c30b872 --- /dev/null +++ b/packages/beacon-node/src/util/dependentRoot.ts @@ -0,0 +1,47 @@ +import {EpochDifference, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {Epoch, RootHex} from "@lodestar/types"; + +/** + * Get dependent root of a shuffling given attestation epoch and head block. + */ +export function getShufflingDependentRoot( + forkChoice: IForkChoice, + attEpoch: Epoch, + blockEpoch: Epoch, + attHeadBlock: ProtoBlock +): RootHex { + let shufflingDependentRoot: RootHex; + if (blockEpoch === attEpoch) { + // current shuffling, this is equivalent to `headState.currentShuffling` + // given blockEpoch = attEpoch = n + // epoch: (n-2) (n-1) n (n+1) + // |-------|-------|-------|-------| + // attHeadBlock ------------------------^ + // shufflingDependentRoot ------^ + shufflingDependentRoot = forkChoice.getDependentRoot(attHeadBlock, EpochDifference.previous); + } else if (blockEpoch === attEpoch - 1) { + // next shuffling, this is equivalent to `headState.nextShuffling` + // given blockEpoch = n-1, attEpoch = n + // epoch: (n-2) (n-1) n (n+1) + // |-------|-------|-------|-------| + // attHeadBlock -------------------^ + // shufflingDependentRoot ------^ + shufflingDependentRoot = forkChoice.getDependentRoot(attHeadBlock, EpochDifference.current); + } else if (blockEpoch < attEpoch - 1) { + // this never happens with default chain option of maxSkipSlots = 32, however we still need to handle it + // check the verifyHeadBlockAndTargetRoot() function above + // given blockEpoch = n-2, attEpoch = n + // epoch: (n-2) (n-1) n (n+1) + // |-------|-------|-------|-------| + // attHeadBlock -----------^ + // shufflingDependentRoot -----^ + shufflingDependentRoot = attHeadBlock.blockRoot; + // use lodestar_gossip_attestation_head_slot_to_attestation_slot metric to track this case + } else { + // blockEpoch > attEpoch + // should not happen, handled in verifyAttestationTargetRoot + throw Error(`attestation epoch ${attEpoch} is before head block epoch ${blockEpoch}`); + } + + return shufflingDependentRoot; +} diff --git a/packages/beacon-node/src/util/profile.ts b/packages/beacon-node/src/util/profile.ts index 09be059e3eba..9c130d7a1eb7 100644 --- a/packages/beacon-node/src/util/profile.ts +++ b/packages/beacon-node/src/util/profile.ts @@ -30,3 +30,21 @@ export async function profileNodeJS(durationMs: number): Promise { }); }); } + +/** + * Write heap snapshot of the current thread to the specified file. + */ +export async function writeHeapSnapshot(prefix: string, dirpath: string): Promise { + // Lazily import NodeJS only modules + const fs = await import("node:fs"); + const v8 = await import("v8"); + const snapshotStream = v8.getHeapSnapshot(); + const filepath = `${dirpath}/${prefix}_${new Date().toISOString()}.heapsnapshot`; + const fileStream = fs.createWriteStream(filepath); + return new Promise((resolve) => { + snapshotStream.pipe(fileStream); + snapshotStream.on("end", () => { + resolve(filepath); + }); + }); +} diff --git a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts index 6e8056ea7458..3c5dacc9c971 100644 --- a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts +++ b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts @@ -13,6 +13,7 @@ import {BeaconProposerCache} from "../../src/chain/beaconProposerCache.js"; import {QueuedStateRegenerator} from "../../src/chain/regen/index.js"; import {LightClientServer} from "../../src/chain/lightClient/index.js"; import {Clock} from "../../src/util/clock.js"; +import {ShufflingCache} from "../../src/chain/shufflingCache.js"; import {getMockedLogger} from "./loggerMock.js"; export type MockedBeaconChain = MockedObject & { @@ -24,6 +25,7 @@ export type MockedBeaconChain = MockedObject & { opPool: MockedObject; aggregatedAttestationPool: MockedObject; beaconProposerCache: MockedObject; + shufflingCache: MockedObject; regen: MockedObject; bls: { verifySignatureSets: Mock<[boolean]>; @@ -40,6 +42,7 @@ vi.mock("../../src/eth1/index.js"); vi.mock("../../src/chain/opPools/opPool.js"); vi.mock("../../src/chain/opPools/aggregatedAttestationPool.js"); vi.mock("../../src/chain/beaconProposerCache.js"); +vi.mock("../../src/chain/shufflingCache.js"); vi.mock("../../src/chain/regen/index.js"); vi.mock("../../src/chain/lightClient/index.js"); vi.mock("../../src/chain/index.js", async (requireActual) => { @@ -75,6 +78,7 @@ vi.mock("../../src/chain/index.js", async (requireActual) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error beaconProposerCache: new BeaconProposerCache(), + shufflingCache: new ShufflingCache(), produceBlock: vi.fn(), produceBlindedBlock: vi.fn(), getCanonicalBlockAtSlot: vi.fn(), @@ -83,6 +87,7 @@ vi.mock("../../src/chain/index.js", async (requireActual) => { getHeadState: vi.fn(), updateBuilderStatus: vi.fn(), processBlock: vi.fn(), + regenStateForAttestationVerification: vi.fn(), close: vi.fn(), logger: getMockedLogger(), regen: new QueuedStateRegenerator({} as any), diff --git a/packages/beacon-node/test/fixtures/capella.ts b/packages/beacon-node/test/fixtures/capella.ts new file mode 100644 index 000000000000..fe9b0206efb1 --- /dev/null +++ b/packages/beacon-node/test/fixtures/capella.ts @@ -0,0 +1,24 @@ +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +import {capella} from "@lodestar/types"; + +export function generateBlsToExecutionChanges( + state: CachedBeaconStateAltair, + count: number +): capella.SignedBLSToExecutionChange[] { + const result: capella.SignedBLSToExecutionChange[] = []; + + for (const validatorIndex of state.epochCtx.proposers) { + result.push({ + message: { + fromBlsPubkey: state.epochCtx.index2pubkey[validatorIndex].toBytes(), + toExecutionAddress: Buffer.alloc(20), + validatorIndex, + }, + signature: Buffer.alloc(96), + }); + + if (result.length >= count) return result; + } + + return result; +} diff --git a/packages/beacon-node/test/fixtures/phase0.ts b/packages/beacon-node/test/fixtures/phase0.ts new file mode 100644 index 000000000000..a273f55e967d --- /dev/null +++ b/packages/beacon-node/test/fixtures/phase0.ts @@ -0,0 +1,98 @@ +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import { + CachedBeaconStateAltair, + computeEpochAtSlot, + computeStartSlotAtEpoch, + getBlockRootAtSlot, +} from "@lodestar/state-transition"; +import {phase0} from "@lodestar/types"; + +export function generateIndexedAttestations( + state: CachedBeaconStateAltair, + count: number +): phase0.IndexedAttestation[] { + const result: phase0.IndexedAttestation[] = []; + + for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { + const slot = state.slot - 1 - epochSlot; + const epoch = computeEpochAtSlot(slot); + const committeeCount = state.epochCtx.getCommitteeCountPerSlot(epoch); + + for (let committeeIndex = 0; committeeIndex < committeeCount; committeeIndex++) { + result.push({ + attestingIndices: state.epochCtx.getBeaconCommittee(slot, committeeIndex), + data: { + slot: slot, + index: committeeIndex, + beaconBlockRoot: getBlockRootAtSlot(state, slot), + source: { + epoch: state.currentJustifiedCheckpoint.epoch, + root: state.currentJustifiedCheckpoint.root, + }, + target: { + epoch: epoch, + root: getBlockRootAtSlot(state, computeStartSlotAtEpoch(epoch)), + }, + }, + signature: Buffer.alloc(96), + }); + + if (result.length >= count) return result; + } + } + + return result; +} + +export function generateBeaconBlockHeader(state: CachedBeaconStateAltair, count: number): phase0.BeaconBlockHeader[] { + const headers: phase0.BeaconBlockHeader[] = []; + + for (let i = 1; i <= count; i++) { + const slot = state.slot - i; + const epoch = computeEpochAtSlot(slot); + const epochStartSlot = computeStartSlotAtEpoch(epoch); + const parentRoot = getBlockRootAtSlot(state, slot - 1); + const stateRoot = getBlockRootAtSlot(state, epochStartSlot); + const bodyRoot = getBlockRootAtSlot(state, epochStartSlot + 1); + const header: phase0.BeaconBlockHeader = { + slot, + proposerIndex: state.epochCtx.proposers[slot % SLOTS_PER_EPOCH], + parentRoot, + stateRoot, + bodyRoot, + }; + + headers.push(header); + } + return headers; +} + +export function generateSignedBeaconBlockHeader( + state: CachedBeaconStateAltair, + count: number +): phase0.SignedBeaconBlockHeader[] { + const headers = generateBeaconBlockHeader(state, count); + + return headers.map((header) => ({ + message: header, + signature: Buffer.alloc(96), + })); +} + +export function generateVoluntaryExits(state: CachedBeaconStateAltair, count: number): phase0.SignedVoluntaryExit[] { + const result: phase0.SignedVoluntaryExit[] = []; + + for (const validatorIndex of state.epochCtx.proposers) { + result.push({ + message: { + epoch: state.currentJustifiedCheckpoint.epoch, + validatorIndex, + }, + signature: Buffer.alloc(96), + }); + + if (result.length >= count) return result; + } + + return result; +} diff --git a/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts new file mode 100644 index 000000000000..6e420f0e1011 --- /dev/null +++ b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts @@ -0,0 +1,107 @@ +import {itBench} from "@dapplion/benchmark"; +import { + MAX_ATTESTER_SLASHINGS, + MAX_BLS_TO_EXECUTION_CHANGES, + MAX_PROPOSER_SLASHINGS, + MAX_VOLUNTARY_EXITS, +} from "@lodestar/params"; +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +import {ssz} from "@lodestar/types"; +// eslint-disable-next-line import/no-relative-packages +import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js"; +import {OpPool} from "../../../../src/chain/opPools/opPool.js"; +import {generateBlsToExecutionChanges} from "../../../fixtures/capella.js"; +import { + generateIndexedAttestations, + generateSignedBeaconBlockHeader, + generateVoluntaryExits, +} from "../../../fixtures/phase0.js"; +import {BlockType} from "../../../../src/chain/interface.js"; + +describe("opPool", () => { + let originalState: CachedBeaconStateAltair; + + before(function () { + this.timeout(2 * 60 * 1000); // Generating the states for the first time is very slow + + originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true}); + }); + + itBench({ + id: "getSlashingsAndExits - default max", + beforeEach: () => { + const pool = new OpPool(); + fillAttesterSlashing(pool, originalState, MAX_ATTESTER_SLASHINGS); + fillProposerSlashing(pool, originalState, MAX_PROPOSER_SLASHINGS); + fillVoluntaryExits(pool, originalState, MAX_VOLUNTARY_EXITS); + fillBlsToExecutionChanges(pool, originalState, MAX_BLS_TO_EXECUTION_CHANGES); + + return pool; + }, + fn: (pool) => { + pool.getSlashingsAndExits(originalState, BlockType.Full, null); + }, + }); + + itBench({ + id: "getSlashingsAndExits - 2k", + beforeEach: () => { + const pool = new OpPool(); + const maxItemsInPool = 2_000; + + fillAttesterSlashing(pool, originalState, maxItemsInPool); + fillProposerSlashing(pool, originalState, maxItemsInPool); + fillVoluntaryExits(pool, originalState, maxItemsInPool); + fillBlsToExecutionChanges(pool, originalState, maxItemsInPool); + + return pool; + }, + fn: (pool) => { + pool.getSlashingsAndExits(originalState, BlockType.Full, null); + }, + }); +}); + +function fillAttesterSlashing(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const attestation of generateIndexedAttestations(state, count)) { + pool.insertAttesterSlashing({ + attestation1: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)), + attestation2: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)), + }); + } + + return pool; +} + +function fillProposerSlashing(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const blockHeader of generateSignedBeaconBlockHeader(state, count)) { + pool.insertProposerSlashing({ + signedHeader1: ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( + ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) + ), + signedHeader2: ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( + ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) + ), + }); + } + + return pool; +} + +function fillVoluntaryExits(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const exit of generateVoluntaryExits(state, count)) { + pool.insertVoluntaryExit(exit); + } + + return pool; +} + +// This does not set the `withdrawalCredentials` for the validator +// So it will be in the pool but not returned from `getSlashingsAndExits` +function fillBlsToExecutionChanges(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const blsToExecution of generateBlsToExecutionChanges(state, count)) { + pool.insertBlsToExecutionChange(blsToExecution); + } + + return pool; +} diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts new file mode 100644 index 000000000000..dbe86c2c5868 --- /dev/null +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -0,0 +1,86 @@ +import {fromHexString} from "@chainsafe/ssz"; +import {itBench} from "@dapplion/benchmark"; +import {config} from "@lodestar/config/default"; +import {LevelDbController} from "@lodestar/db"; +import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@lodestar/params"; +import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +// eslint-disable-next-line import/no-relative-packages +import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js"; +import {BeaconChain} from "../../../../src/chain/index.js"; +import {BlockType, produceBlockBody} from "../../../../src/chain/produceBlock/produceBlockBody.js"; +import {Eth1ForBlockProductionDisabled} from "../../../../src/eth1/index.js"; +import {ExecutionEngineDisabled} from "../../../../src/execution/engine/index.js"; +import {BeaconDb} from "../../../../src/index.js"; +import {testLogger} from "../../../utils/logger.js"; + +const logger = testLogger(); + +describe("produceBlockBody", () => { + const stateOg = generatePerfTestCachedStateAltair({goBackOneSlot: false}); + + let db: BeaconDb; + let chain: BeaconChain; + let state: CachedBeaconStateAltair; + + before(async () => { + db = new BeaconDb(config, await LevelDbController.create({name: ".tmpdb"}, {logger})); + state = stateOg.clone(); + chain = new BeaconChain( + { + proposerBoostEnabled: true, + computeUnrealized: false, + safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, + disableArchiveOnCheckpoint: true, + suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient, + skipCreateStateCacheIfAvailable: true, + archiveStateEpochFrequency: 1024, + minSameMessageSignatureSetsToBatch: 32, + }, + { + config: state.config, + db, + logger, + // eslint-disable-next-line @typescript-eslint/no-empty-function + processShutdownCallback: () => {}, + metrics: null, + anchorState: state, + eth1: new Eth1ForBlockProductionDisabled(), + executionEngine: new ExecutionEngineDisabled(), + } + ); + }); + + after(async () => { + // If before blocks fail, db won't be declared + if (db !== undefined) await db.close(); + if (chain !== undefined) await chain.close(); + }); + + itBench({ + id: "proposeBlockBody type=full, size=empty", + minRuns: 5, + maxMs: Infinity, + timeoutBench: 60 * 1000, + beforeEach: async () => { + const head = chain.forkChoice.getHead(); + const proposerIndex = state.epochCtx.getBeaconProposer(state.slot); + const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].toBytes(); + + return {chain, state, head, proposerIndex, proposerPubKey}; + }, + fn: async ({chain, state, head, proposerIndex, proposerPubKey}) => { + const slot = state.slot; + + await produceBlockBody.call(chain, BlockType.Full, state, { + parentSlot: slot, + slot: slot + 1, + graffiti: Buffer.alloc(32), + randaoReveal: Buffer.alloc(96), + parentBlockRoot: fromHexString(head.blockRoot), + proposerIndex, + proposerPubKey, + }); + }, + }); +}); diff --git a/packages/beacon-node/test/perf/chain/verifyImportBlocks.test.ts b/packages/beacon-node/test/perf/chain/verifyImportBlocks.test.ts index 21b70c69a425..923436d6f96d 100644 --- a/packages/beacon-node/test/perf/chain/verifyImportBlocks.test.ts +++ b/packages/beacon-node/test/perf/chain/verifyImportBlocks.test.ts @@ -7,11 +7,14 @@ import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; // eslint-disable-next-line import/no-relative-packages import {rangeSyncTest} from "../../../../state-transition/test/perf/params.js"; import { - beforeValue, getNetworkCachedState, getNetworkCachedBlock, // eslint-disable-next-line import/no-relative-packages -} from "../../../../state-transition/test/utils/index.js"; +} from "../../../../state-transition/test/utils/testFileCache.js"; +import { + beforeValue, + // eslint-disable-next-line import/no-relative-packages +} from "../../../../state-transition/test/utils/beforeValueMocha.js"; import {BeaconChain} from "../../../src/chain/index.js"; import {ExecutionEngineDisabled} from "../../../src/execution/engine/index.js"; import {Eth1ForBlockProductionDisabled} from "../../../src/eth1/index.js"; diff --git a/packages/beacon-node/test/sim/mergemock.test.ts b/packages/beacon-node/test/sim/mergemock.test.ts index 0761005714bd..d9492723599d 100644 --- a/packages/beacon-node/test/sim/mergemock.test.ts +++ b/packages/beacon-node/test/sim/mergemock.test.ts @@ -157,7 +157,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { eth1: {enabled: false, providerUrls: [engineRpcUrl], jwtSecretHex}, executionEngine: {urls: [engineRpcUrl], jwtSecretHex}, executionBuilder: { - urls: [ethRpcUrl], + url: ethRpcUrl, enabled: true, issueLocalFcUWithFeeRecipient: feeRecipientMevBoost, allowedFaults: 16, diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts index febb027303b7..3e96f3b932c8 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts @@ -72,6 +72,7 @@ describe("api/validator - produceBlockV2", function () { const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); const executionPayloadValue = ssz.Wei.defaultValue(); + const consensusBlockValue = ssz.Gwei.defaultValue(); const currentSlot = 100000; vi.spyOn(server.chainStub.clock, "currentSlot", "get").mockReturnValue(currentSlot); @@ -84,7 +85,7 @@ describe("api/validator - produceBlockV2", function () { const feeRecipient = "0xcccccccccccccccccccccccccccccccccccccccc"; const api = getValidatorApi(modules); - server.chainStub.produceBlock.mockResolvedValue({block: fullBlock, executionPayloadValue}); + server.chainStub.produceBlock.mockResolvedValue({block: fullBlock, executionPayloadValue, consensusBlockValue}); // check if expectedFeeRecipient is passed to produceBlock await api.produceBlockV2(slot, randaoReveal, graffiti, {feeRecipient}); diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts index 0835777dd7ec..83e1e7887510 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -41,93 +41,96 @@ describe("api/validator - produceBlockV3", function () { vi.clearAllMocks(); }); - const testCases: [routes.validator.BuilderSelection, number | null, number | null, string][] = [ - [routes.validator.BuilderSelection.MaxProfit, 1, 0, "builder"], - [routes.validator.BuilderSelection.MaxProfit, 1, 2, "engine"], - [routes.validator.BuilderSelection.MaxProfit, null, 0, "engine"], - [routes.validator.BuilderSelection.MaxProfit, 0, null, "builder"], - - [routes.validator.BuilderSelection.BuilderAlways, 1, 2, "builder"], - [routes.validator.BuilderSelection.BuilderAlways, 1, 0, "builder"], - [routes.validator.BuilderSelection.BuilderAlways, null, 0, "engine"], - [routes.validator.BuilderSelection.BuilderAlways, 0, null, "builder"], - - [routes.validator.BuilderSelection.BuilderOnly, 0, 2, "builder"], - [routes.validator.BuilderSelection.ExecutionOnly, 2, 0, "execution"], + const testCases: [routes.validator.BuilderSelection, number | null, number | null, number, string][] = [ + [routes.validator.BuilderSelection.MaxProfit, 1, 0, 0, "builder"], + [routes.validator.BuilderSelection.MaxProfit, 1, 2, 1, "engine"], + [routes.validator.BuilderSelection.MaxProfit, null, 0, 0, "engine"], + [routes.validator.BuilderSelection.MaxProfit, 0, null, 1, "builder"], + + [routes.validator.BuilderSelection.BuilderAlways, 1, 2, 0, "builder"], + [routes.validator.BuilderSelection.BuilderAlways, 1, 0, 1, "builder"], + [routes.validator.BuilderSelection.BuilderAlways, null, 0, 0, "engine"], + [routes.validator.BuilderSelection.BuilderAlways, 0, null, 1, "builder"], + + [routes.validator.BuilderSelection.BuilderOnly, 0, 2, 0, "builder"], + [routes.validator.BuilderSelection.ExecutionOnly, 2, 0, 1, "execution"], ]; - testCases.forEach(([builderSelection, builderPayloadValue, enginePayloadValue, finalSelection]) => { - it(`produceBlockV3 - ${finalSelection} produces block`, async () => { - syncStub = server.syncStub; - modules = { - chain: server.chainStub, - config, - db: server.dbStub, - logger, - network: server.networkStub, - sync: syncStub, - metrics: null, - }; - - const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); - const blindedBlock = ssz.bellatrix.BlindedBeaconBlock.defaultValue(); - - const slot = 1 * SLOTS_PER_EPOCH; - const randaoReveal = fullBlock.body.randaoReveal; - const graffiti = "a".repeat(32); - const feeRecipient = "0xccccccccccccccccccccccccccccccccccccccaa"; - const currentSlot = 1 * SLOTS_PER_EPOCH; - - vi.spyOn(server.chainStub.clock, "currentSlot", "get").mockReturnValue(currentSlot); - vi.spyOn(syncStub, "state", "get").mockReturnValue(SyncState.Synced); - - const api = getValidatorApi(modules); - - if (enginePayloadValue !== null) { - chainStub.produceBlock.mockResolvedValue({ - block: fullBlock, - executionPayloadValue: BigInt(enginePayloadValue), - }); - } else { - chainStub.produceBlock.mockRejectedValue(Error("not produced")); - } - - if (builderPayloadValue !== null) { - chainStub.produceBlindedBlock.mockResolvedValue({ - block: blindedBlock, - executionPayloadValue: BigInt(builderPayloadValue), - }); - } else { - chainStub.produceBlindedBlock.mockRejectedValue(Error("not produced")); - } - - const _skipRandaoVerification = false; - const produceBlockOpts = { - strictFeeRecipientCheck: false, - builderSelection, - feeRecipient, - }; - - const block = await api.produceBlockV3(slot, randaoReveal, graffiti, _skipRandaoVerification, produceBlockOpts); - - const expectedBlock = finalSelection === "builder" ? blindedBlock : fullBlock; - const expectedExecution = finalSelection === "builder" ? true : false; - - expect(block.data).toEqual(expectedBlock); - expect(block.executionPayloadBlinded).toEqual(expectedExecution); - - // check call counts - if (builderSelection === routes.validator.BuilderSelection.ExecutionOnly) { - expect(chainStub.produceBlindedBlock).toBeCalledTimes(0); - } else { - expect(chainStub.produceBlindedBlock).toBeCalledTimes(1); - } - - if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) { - expect(chainStub.produceBlock).toBeCalledTimes(0); - } else { - expect(chainStub.produceBlock).toBeCalledTimes(1); - } - }); - }); + testCases.forEach( + ([builderSelection, builderPayloadValue, enginePayloadValue, consensusBlockValue, finalSelection]) => { + it(`produceBlockV3 - ${finalSelection} produces block`, async () => { + syncStub = server.syncStub; + modules = { + chain: server.chainStub, + config, + db: server.dbStub, + logger, + network: server.networkStub, + sync: syncStub, + metrics: null, + }; + + const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); + const blindedBlock = ssz.bellatrix.BlindedBeaconBlock.defaultValue(); + + const slot = 1 * SLOTS_PER_EPOCH; + const randaoReveal = fullBlock.body.randaoReveal; + const graffiti = "a".repeat(32); + const feeRecipient = "0xccccccccccccccccccccccccccccccccccccccaa"; + const currentSlot = 1 * SLOTS_PER_EPOCH; + + vi.spyOn(server.chainStub.clock, "currentSlot", "get").mockReturnValue(currentSlot); + vi.spyOn(syncStub, "state", "get").mockReturnValue(SyncState.Synced); + + const api = getValidatorApi(modules); + + if (enginePayloadValue !== null) { + chainStub.produceBlock.mockResolvedValue({ + block: fullBlock, + executionPayloadValue: BigInt(enginePayloadValue), + consensusBlockValue: BigInt(consensusBlockValue), + }); + } else { + chainStub.produceBlock.mockRejectedValue(Error("not produced")); + } + + if (builderPayloadValue !== null) { + chainStub.produceBlindedBlock.mockResolvedValue({ + block: blindedBlock, + executionPayloadValue: BigInt(builderPayloadValue), + consensusBlockValue: BigInt(consensusBlockValue), + }); + } else { + chainStub.produceBlindedBlock.mockRejectedValue(Error("not produced")); + } + const _skipRandaoVerification = false; + const produceBlockOpts = { + strictFeeRecipientCheck: false, + builderSelection, + feeRecipient, + }; + + const block = await api.produceBlockV3(slot, randaoReveal, graffiti, _skipRandaoVerification, produceBlockOpts); + + const expectedBlock = finalSelection === "builder" ? blindedBlock : fullBlock; + const expectedExecution = finalSelection === "builder" ? true : false; + + expect(block.data).toEqual(expectedBlock); + expect(block.executionPayloadBlinded).toEqual(expectedExecution); + + // check call counts + if (builderSelection === routes.validator.BuilderSelection.ExecutionOnly) { + expect(chainStub.produceBlindedBlock).toBeCalledTimes(0); + } else { + expect(chainStub.produceBlindedBlock).toBeCalledTimes(1); + } + + if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) { + expect(chainStub.produceBlock).toBeCalledTimes(0); + } else { + expect(chainStub.produceBlock).toBeCalledTimes(1); + } + }); + } + ); }); diff --git a/packages/beacon-node/test/unit/chain/opPools/syncCommittee.test.ts b/packages/beacon-node/test/unit/chain/opPools/syncCommittee.test.ts index 54a2e5102d78..3cb5d496bf9b 100644 --- a/packages/beacon-node/test/unit/chain/opPools/syncCommittee.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/syncCommittee.test.ts @@ -38,7 +38,7 @@ describe("chain / opPools / SyncCommitteeMessagePool", function () { vi.clearAllMocks(); }); - it("should preaggregate SyncCommitteeContribution", () => { + it("should propagate SyncCommitteeContribution", () => { clockStub.secFromSlot.mockReturnValue(0); let contribution = cache.getContribution(subcommitteeIndex, syncCommittee.slot, syncCommittee.beaconBlockRoot); expect(contribution).not.toBeNull(); diff --git a/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts b/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts index 4decbc1b749c..f4fa68609015 100644 --- a/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts +++ b/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, beforeEach, afterEach, vi, SpyInstance, Mock} from "vitest"; +import {describe, it, expect, beforeEach, afterEach, vi, Mock, MockInstance} from "vitest"; import {config} from "@lodestar/config/default"; import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {routes} from "@lodestar/api"; @@ -20,7 +20,7 @@ describe("PrepareNextSlot scheduler", () => { let regenStub: MockedBeaconChain["regen"]; let loggerStub: MockedLogger; let beaconProposerCacheStub: MockedBeaconChain["beaconProposerCache"]; - let getForkStub: SpyInstance<[number], ForkName>; + let getForkStub: MockInstance<[number], ForkName>; let updateBuilderStatus: MockedBeaconChain["updateBuilderStatus"]; let executionEngineStub: MockedBeaconChain["executionEngine"]; const emitPayloadAttributes = true; diff --git a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts new file mode 100644 index 000000000000..186739ff2475 --- /dev/null +++ b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts @@ -0,0 +1,54 @@ +import {describe, it, expect, beforeEach} from "vitest"; + +import {getShufflingDecisionBlock} from "@lodestar/state-transition"; +// eslint-disable-next-line import/no-relative-packages +import {generateTestCachedBeaconStateOnlyValidators} from "../../../../state-transition/test/perf/util.js"; +import {ShufflingCache} from "../../../src/chain/shufflingCache.js"; + +describe("ShufflingCache", function () { + const vc = 64; + const stateSlot = 100; + const state = generateTestCachedBeaconStateOnlyValidators({vc, slot: stateSlot}); + const currentEpoch = state.epochCtx.currentShuffling.epoch; + let shufflingCache: ShufflingCache; + + beforeEach(() => { + shufflingCache = new ShufflingCache(null, {maxShufflingCacheEpochs: 1}); + shufflingCache.processState(state, currentEpoch); + }); + + it("should get shuffling from cache", async function () { + const decisionRoot = getShufflingDecisionBlock(state, currentEpoch); + expect(await shufflingCache.get(currentEpoch, decisionRoot)).to.deep.equal(state.epochCtx.currentShuffling); + }); + + it("should bound by maxSize(=1)", async function () { + const decisionRoot = getShufflingDecisionBlock(state, currentEpoch); + expect(await shufflingCache.get(currentEpoch, decisionRoot)).to.deep.equal(state.epochCtx.currentShuffling); + // insert promises at the same epoch does not prune the cache + shufflingCache.insertPromise(currentEpoch, "0x00"); + expect(await shufflingCache.get(currentEpoch, decisionRoot)).to.deep.equal(state.epochCtx.currentShuffling); + // insert shufflings at other epochs does prune the cache + shufflingCache.processState(state, currentEpoch + 1); + // the current shuffling is not available anymore + expect(await shufflingCache.get(currentEpoch, decisionRoot)).to.be.null; + }); + + it("should return shuffling from promise", async function () { + const nextDecisionRoot = getShufflingDecisionBlock(state, currentEpoch + 1); + shufflingCache.insertPromise(currentEpoch + 1, nextDecisionRoot); + const shufflingRequest0 = shufflingCache.get(currentEpoch + 1, nextDecisionRoot); + const shufflingRequest1 = shufflingCache.get(currentEpoch + 1, nextDecisionRoot); + shufflingCache.processState(state, currentEpoch + 1); + expect(await shufflingRequest0).to.deep.equal(state.epochCtx.nextShuffling); + expect(await shufflingRequest1).to.deep.equal(state.epochCtx.nextShuffling); + }); + + it("should support up to 2 promises at a time", async function () { + // insert 2 promises at the same epoch + shufflingCache.insertPromise(currentEpoch, "0x00"); + shufflingCache.insertPromise(currentEpoch, "0x01"); + // inserting other promise should throw error + expect(() => shufflingCache.insertPromise(currentEpoch, "0x02")).to.throw(); + }); +}); diff --git a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts index 99b3783574e9..f614ec10551d 100644 --- a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts @@ -2,7 +2,6 @@ import {toHexString} from "@chainsafe/ssz"; import {describe, it} from "vitest"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; -import {processSlots} from "@lodestar/state-transition"; // eslint-disable-next-line import/no-relative-packages import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js"; import {IBeaconChain} from "../../../../src/chain/index.js"; @@ -14,7 +13,6 @@ import { getAggregateAndProofValidData, AggregateAndProofValidDataOpts, } from "../../../utils/validationData/aggregateAndProof.js"; -import {IStateRegenerator} from "../../../../src/chain/regen/interface.js"; describe("chain / validation / aggregateAndProof", () => { const vc = 64; @@ -112,22 +110,6 @@ describe("chain / validation / aggregateAndProof", () => { await expectError(chain, signedAggregateAndProof, AttestationErrorCode.INVALID_TARGET_ROOT); }); - it("NO_COMMITTEE_FOR_SLOT_AND_INDEX", async () => { - const {chain, signedAggregateAndProof} = getValidData(); - // slot is out of the commitee range - // simulate https://github.com/ChainSafe/lodestar/issues/4396 - // this way we cannot get committeeIndices - const committeeState = processSlots( - getState(), - signedAggregateAndProof.message.aggregate.data.slot + 2 * SLOTS_PER_EPOCH - ); - (chain as {regen: IStateRegenerator}).regen = { - getState: async () => committeeState, - } as Partial as IStateRegenerator; - - await expectError(chain, signedAggregateAndProof, AttestationErrorCode.NO_COMMITTEE_FOR_SLOT_AND_INDEX); - }); - it("EMPTY_AGGREGATION_BITFIELD", async () => { const {chain, signedAggregateAndProof} = getValidData(); // Unset all aggregationBits diff --git a/packages/beacon-node/test/unit/chain/validation/attestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation.test.ts index 8d7394ecc4ed..efd7d3c00cbb 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation.test.ts @@ -3,11 +3,9 @@ import type {PublicKey, SecretKey} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; import {describe, it, expect, beforeEach, afterEach, vi} from "vitest"; import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {defaultChainConfig, createChainForkConfig} from "@lodestar/config"; -import {ProtoBlock} from "@lodestar/fork-choice"; -// eslint-disable-next-line import/no-relative-packages -import {SignatureSetType, computeEpochAtSlot, computeStartSlotAtEpoch, processSlots} from "@lodestar/state-transition"; -import {Slot, ssz} from "@lodestar/types"; +import {EpochDifference, ProtoBlock} from "@lodestar/fork-choice"; +import {EpochShuffling, SignatureSetType, computeStartSlotAtEpoch} from "@lodestar/state-transition"; +import {ssz} from "@lodestar/types"; // eslint-disable-next-line import/no-relative-packages import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js"; import {IBeaconChain} from "../../../../src/chain/index.js"; @@ -20,17 +18,16 @@ import { import { ApiAttestation, GossipAttestation, - getStateForAttestationVerification, validateApiAttestation, Step0Result, validateAttestation, validateGossipAttestationsSameAttData, + getShufflingForAttestationVerification, } from "../../../../src/chain/validation/index.js"; import {expectRejectedWithLodestarError} from "../../../utils/errors.js"; import {memoOnce} from "../../../utils/cache.js"; import {getAttestationValidData, AttestationValidDataOpts} from "../../../utils/validationData/attestation.js"; -import {IStateRegenerator, RegenCaller} from "../../../../src/chain/regen/interface.js"; -import {StateRegenerator} from "../../../../src/chain/regen/regen.js"; +import {RegenCaller} from "../../../../src/chain/regen/interface.js"; import {ZERO_HASH_HEX} from "../../../../src/constants/constants.js"; import {BlsSingleThreadVerifier} from "../../../../src/chain/bls/singleThread.js"; @@ -343,35 +340,6 @@ describe("validateAttestation", () => { ); }); - it("NO_COMMITTEE_FOR_SLOT_AND_INDEX", async () => { - const {chain, attestation, subnet} = getValidData(); - // slot is out of the commitee range - // simulate https://github.com/ChainSafe/lodestar/issues/4396 - // this way we cannot get committeeIndices - const committeeState = processSlots(getState(), attestation.data.slot + 2 * SLOTS_PER_EPOCH); - (chain as {regen: IStateRegenerator}).regen = { - getState: async () => committeeState, - } as Partial as IStateRegenerator; - const serializedData = ssz.phase0.Attestation.serialize(attestation); - - await expectApiError( - chain, - {attestation, serializedData: null}, - AttestationErrorCode.NO_COMMITTEE_FOR_SLOT_AND_INDEX - ); - await expectGossipError( - chain, - { - attestation: null, - serializedData, - attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), - }, - subnet, - AttestationErrorCode.NO_COMMITTEE_FOR_SLOT_AND_INDEX - ); - }); - it("WRONG_NUMBER_OF_AGGREGATION_BITS", async () => { const {chain, attestation, subnet} = getValidData(); // Increase the length of aggregationBits beyond the committee size @@ -481,15 +449,17 @@ describe("validateAttestation", () => { } }); -describe("getStateForAttestationVerification", () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const config = createChainForkConfig({...defaultChainConfig, CAPELLA_FORK_EPOCH: 2}); +describe("getShufflingForAttestationVerification", () => { let regenStub: MockedBeaconChain["regen"]; + let forkchoiceStub: MockedBeaconChain["forkChoice"]; + let shufflingCacheStub: MockedBeaconChain["shufflingCache"]; let chain: MockedBeaconChain; beforeEach(() => { chain = getMockedBeaconChain(); regenStub = chain.regen; + forkchoiceStub = chain.forkChoice; + shufflingCacheStub = chain.shufflingCache; vi.spyOn(regenStub, "getBlockSlotState"); vi.spyOn(regenStub, "getState"); }); @@ -498,52 +468,124 @@ describe("getStateForAttestationVerification", () => { vi.clearAllMocks(); }); - const forkSlot = computeStartSlotAtEpoch(config.CAPELLA_FORK_EPOCH); - const getBlockSlotStateTestCases: {id: string; attSlot: Slot; headSlot: Slot; regenCall: keyof StateRegenerator}[] = [ - // TODO: This case is not passing inspect later - // { - // id: "should call regen.getBlockSlotState at fork boundary", - // attSlot: forkSlot + 1, - // headSlot: forkSlot - 1, - // regenCall: "getBlockSlotState", - // }, - { - id: "should call regen.getBlockSlotState if > 1 epoch difference", - attSlot: forkSlot + 2 * SLOTS_PER_EPOCH, - headSlot: forkSlot + 1, - regenCall: "getBlockSlotState", - }, - { - id: "should call getState if 1 epoch difference", - attSlot: forkSlot + 2 * SLOTS_PER_EPOCH, - headSlot: forkSlot + SLOTS_PER_EPOCH, - regenCall: "getState", - }, - { - id: "should call getState if 0 epoch difference", - attSlot: forkSlot + 2 * SLOTS_PER_EPOCH, - headSlot: forkSlot + 2 * SLOTS_PER_EPOCH, - regenCall: "getState", - }, - ]; + const attEpoch = 1000; + const blockRoot = "0xd76aed834b4feef32efb53f9076e407c0d344cfdb70f0a770fa88416f70d304d"; + + it("block epoch is the same to attestation epoch", async () => { + const headSlot = computeStartSlotAtEpoch(attEpoch); + const attHeadBlock = { + slot: headSlot, + stateRoot: ZERO_HASH_HEX, + blockRoot, + } as Partial as ProtoBlock; + const previousDependentRoot = "0xa916b57729dbfb89a082820e0eb2b669d9d511a675d3d8c888b2f300f10b0bdf"; + forkchoiceStub.getDependentRoot.mockImplementationOnce((block, epochDiff) => { + if (block === attHeadBlock && epochDiff === EpochDifference.previous) { + return previousDependentRoot; + } else { + throw new Error("Unexpected input"); + } + }); + const expectedShuffling = {epoch: attEpoch} as EpochShuffling; + shufflingCacheStub.get.mockImplementationOnce((epoch, root) => { + if (epoch === attEpoch && root === previousDependentRoot) { + return Promise.resolve(expectedShuffling); + } else { + return Promise.resolve(null); + } + }); + const resultShuffling = await getShufflingForAttestationVerification( + chain, + attEpoch, + attHeadBlock, + RegenCaller.validateGossipAttestation + ); + expect(resultShuffling).to.be.deep.equal(expectedShuffling); + }); + + it("block epoch is previous attestation epoch", async () => { + const headSlot = computeStartSlotAtEpoch(attEpoch - 1); + const attHeadBlock = { + slot: headSlot, + stateRoot: ZERO_HASH_HEX, + blockRoot, + } as Partial as ProtoBlock; + const currentDependentRoot = "0xa916b57729dbfb89a082820e0eb2b669d9d511a675d3d8c888b2f300f10b0bdf"; + forkchoiceStub.getDependentRoot.mockImplementationOnce((block, epochDiff) => { + if (block === attHeadBlock && epochDiff === EpochDifference.current) { + return currentDependentRoot; + } else { + throw new Error("Unexpected input"); + } + }); + const expectedShuffling = {epoch: attEpoch} as EpochShuffling; + shufflingCacheStub.get.mockImplementationOnce((epoch, root) => { + if (epoch === attEpoch && root === currentDependentRoot) { + return Promise.resolve(expectedShuffling); + } else { + return Promise.resolve(null); + } + }); + const resultShuffling = await getShufflingForAttestationVerification( + chain, + attEpoch, + attHeadBlock, + RegenCaller.validateGossipAttestation + ); + expect(resultShuffling).to.be.deep.equal(expectedShuffling); + }); - for (const {id, attSlot, headSlot, regenCall} of getBlockSlotStateTestCases) { - it(id, async () => { - const attEpoch = computeEpochAtSlot(attSlot); - const attHeadBlock = { - slot: headSlot, - stateRoot: ZERO_HASH_HEX, - blockRoot: ZERO_HASH_HEX, - } as Partial as ProtoBlock; - expect(regenStub[regenCall]).toBeCalledTimes(0); - await getStateForAttestationVerification( + it("block epoch is attestation epoch - 2", async () => { + const headSlot = computeStartSlotAtEpoch(attEpoch - 2); + const attHeadBlock = { + slot: headSlot, + stateRoot: ZERO_HASH_HEX, + blockRoot, + } as Partial as ProtoBlock; + const expectedShuffling = {epoch: attEpoch} as EpochShuffling; + let callCount = 0; + shufflingCacheStub.get.mockImplementationOnce((epoch, root) => { + if (epoch === attEpoch && root === blockRoot) { + if (callCount === 0) { + callCount++; + return Promise.resolve(null); + } else { + return Promise.resolve(expectedShuffling); + } + } else { + return Promise.resolve(null); + } + }); + chain.regenStateForAttestationVerification.mockImplementationOnce(() => Promise.resolve(expectedShuffling)); + + const resultShuffling = await getShufflingForAttestationVerification( + chain, + attEpoch, + attHeadBlock, + RegenCaller.validateGossipAttestation + ); + // sandbox.assert.notCalled(forkchoiceStub.getDependentRoot); + expect(forkchoiceStub.getDependentRoot).not.toHaveBeenCalledTimes(1); + expect(resultShuffling).to.be.deep.equal(expectedShuffling); + }); + + it("block epoch is attestation epoch + 1", async () => { + const headSlot = computeStartSlotAtEpoch(attEpoch + 1); + const attHeadBlock = { + slot: headSlot, + stateRoot: ZERO_HASH_HEX, + blockRoot, + } as Partial as ProtoBlock; + try { + await getShufflingForAttestationVerification( chain, - attSlot, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation ); - expect(regenStub[regenCall]).toBeCalledTimes(1); - }); - } + expect.fail("Expect error because attestation epoch is greater than block epoch"); + } catch (e) { + expect(e instanceof Error).to.be.true; + } + }); }); diff --git a/packages/beacon-node/test/unit/eth1/eth1DepositDataTracker.test.ts b/packages/beacon-node/test/unit/eth1/eth1DepositDataTracker.test.ts index b195e16d5bd0..37e4b84a3d68 100644 --- a/packages/beacon-node/test/unit/eth1/eth1DepositDataTracker.test.ts +++ b/packages/beacon-node/test/unit/eth1/eth1DepositDataTracker.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, beforeEach, afterEach, vi, SpyInstance} from "vitest"; +import {describe, it, expect, beforeEach, afterEach, vi, MockInstance} from "vitest"; import {config} from "@lodestar/config/default"; import {TimeoutError} from "@lodestar/utils"; import {Eth1DepositDataTracker} from "../../../src/eth1/eth1DepositDataTracker.js"; @@ -17,8 +17,8 @@ describe("Eth1DepositDataTracker", function () { const eth1Provider = new Eth1Provider(config, opts, signal, null); let db: BeaconDb; let eth1DepositDataTracker: Eth1DepositDataTracker; - let getBlocksByNumberStub: SpyInstance; - let getDepositEventsStub: SpyInstance; + let getBlocksByNumberStub: MockInstance; + let getDepositEventsStub: MockInstance; beforeEach(() => { db = getMockedBeaconDb(); diff --git a/packages/beacon-node/test/unit/monitoring/service.test.ts b/packages/beacon-node/test/unit/monitoring/service.test.ts index 9c1f8b89bae4..27b8ca35c307 100644 --- a/packages/beacon-node/test/unit/monitoring/service.test.ts +++ b/packages/beacon-node/test/unit/monitoring/service.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, beforeEach, beforeAll, afterAll, vi, afterEach, SpyInstance} from "vitest"; +import {describe, it, expect, beforeEach, beforeAll, afterAll, vi, afterEach, MockInstance} from "vitest"; import {ErrorAborted, TimeoutError} from "@lodestar/utils"; import {RegistryMetricCreator} from "../../../src/index.js"; import {HistogramExtra} from "../../../src/metrics/utils/histogram.js"; @@ -115,7 +115,7 @@ describe("monitoring / service", () => { }); describe("MonitoringService - close", () => { - let clearTimeout: SpyInstance; + let clearTimeout: MockInstance; beforeAll(() => { clearTimeout = vi.spyOn(global, "clearTimeout"); diff --git a/packages/beacon-node/test/unit/util/dependentRoot.test.ts b/packages/beacon-node/test/unit/util/dependentRoot.test.ts new file mode 100644 index 000000000000..e2f11acc3eba --- /dev/null +++ b/packages/beacon-node/test/unit/util/dependentRoot.test.ts @@ -0,0 +1,67 @@ +import {describe, it, expect, beforeEach, afterEach, vi} from "vitest"; +import {EpochDifference, ProtoBlock} from "@lodestar/fork-choice"; +import {computeEpochAtSlot} from "@lodestar/state-transition"; +import {getShufflingDependentRoot} from "../../../src/util/dependentRoot.js"; +import {MockedBeaconChain, getMockedBeaconChain} from "../../__mocks__/mockedBeaconChain.js"; + +describe("util / getShufflingDependentRoot", () => { + let forkchoiceStub: MockedBeaconChain["forkChoice"]; + + const headBattHeadBlock = { + slot: 100, + } as ProtoBlock; + const blockEpoch = computeEpochAtSlot(headBattHeadBlock.slot); + + beforeEach(() => { + forkchoiceStub = getMockedBeaconChain().forkChoice; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should return current dependent root", () => { + const attEpoch = blockEpoch; + forkchoiceStub.getDependentRoot.mockImplementation((block, epochDiff) => { + if (block === headBattHeadBlock && epochDiff === EpochDifference.previous) { + return "current"; + } else { + throw new Error("should not be called"); + } + }); + expect(getShufflingDependentRoot(forkchoiceStub, attEpoch, blockEpoch, headBattHeadBlock)).to.be.equal("current"); + }); + + it("should return next dependent root", () => { + const attEpoch = blockEpoch + 1; + // forkchoiceStub.getDependentRoot.withArgs(headBattHeadBlock, EpochDifference.current).returns("previous"); + forkchoiceStub.getDependentRoot.mockImplementation((block, epochDiff) => { + if (block === headBattHeadBlock && epochDiff === EpochDifference.current) { + return "0x000"; + } else { + throw new Error("should not be called"); + } + }); + expect(getShufflingDependentRoot(forkchoiceStub, attEpoch, blockEpoch, headBattHeadBlock)).to.be.equal("0x000"); + }); + + it("should return head block root as dependent root", () => { + const attEpoch = blockEpoch + 2; + // forkchoiceStub.getDependentRoot.throws("should not be called"); + forkchoiceStub.getDependentRoot.mockImplementation(() => { + throw Error("should not be called"); + }); + expect(getShufflingDependentRoot(forkchoiceStub, attEpoch, blockEpoch, headBattHeadBlock)).to.be.equal( + headBattHeadBlock.blockRoot + ); + }); + + it("should throw error if attestation epoch is before head block epoch", () => { + const attEpoch = blockEpoch - 1; + // forkchoiceStub.getDependentRoot.throws("should not be called"); + forkchoiceStub.getDependentRoot.mockImplementation(() => { + throw Error("should not be called"); + }); + expect(() => getShufflingDependentRoot(forkchoiceStub, attEpoch, blockEpoch, headBattHeadBlock)).to.throw(); + }); +}); diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index 6f768227e5cd..fa3c4d479ade 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -1,10 +1,13 @@ import {BitArray, toHexString} from "@chainsafe/ssz"; -import {computeEpochAtSlot, computeSigningRoot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; +import { + computeEpochAtSlot, + computeSigningRoot, + computeStartSlotAtEpoch, + getShufflingDecisionBlock, +} from "@lodestar/state-transition"; import {ProtoBlock, IForkChoice, ExecutionStatus} from "@lodestar/fork-choice"; import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import {phase0, Slot, ssz} from "@lodestar/types"; -import {config} from "@lodestar/config/default"; -import {BeaconConfig} from "@lodestar/config"; import { generateTestCachedBeaconStateOnlyValidators, getSecretKeyFromIndexCached, @@ -21,6 +24,7 @@ import {SeenAggregatedAttestations} from "../../../src/chain/seenCache/seenAggre import {SeenAttestationDatas} from "../../../src/chain/seenCache/seenAttestationData.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; import {testLogger} from "../logger.js"; +import {ShufflingCache} from "../../../src/chain/shufflingCache.js"; export type AttestationValidDataOpts = { currentSlot?: Slot; @@ -73,6 +77,12 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }; + + const shufflingCache = new ShufflingCache(); + shufflingCache.processState(state, state.epochCtx.currentShuffling.epoch); + shufflingCache.processState(state, state.epochCtx.nextShuffling.epoch); + const dependentRoot = getShufflingDecisionBlock(state, state.epochCtx.currentShuffling.epoch); + const forkChoice = { getBlock: (root) => { if (!ssz.Root.equals(root, beaconBlockRoot)) return null; @@ -82,6 +92,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { if (rootHex !== toHexString(beaconBlockRoot)) return null; return headBlock; }, + getDependentRoot: () => dependentRoot, } as Partial as IForkChoice; const committeeIndices = state.epochCtx.getBeaconCommittee(attSlot, attIndex); @@ -117,11 +128,13 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { // Add state to regen const regen = { getState: async () => state, + // TODO: remove this once we have a better way to get state + getStateSync: () => state, } as Partial as IStateRegenerator; const chain = { clock, - config: config as BeaconConfig, + config: state.config, forkChoice, regen, seenAttesters: new SeenAttesters(), @@ -132,6 +145,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { : new BlsMultiThreadWorkerPool({}, {logger: testLogger(), metrics: null}), waitForBlock: () => Promise.resolve(false), index2pubkey: state.epochCtx.index2pubkey, + shufflingCache, opts: defaultChainOptions, } as Partial as IBeaconChain; diff --git a/packages/cli/docsgen/changeCase.ts b/packages/cli/docsgen/changeCase.ts new file mode 100644 index 000000000000..096d26e61833 --- /dev/null +++ b/packages/cli/docsgen/changeCase.ts @@ -0,0 +1,29 @@ +const wordPattern = new RegExp(["[A-Z][a-z]+", "[A-Z]+(?=[A-Z][a-z])", "[A-Z]+", "[a-z]+", "[0-9]+"].join("|"), "g"); +function splitString(str: string): string[] { + const normalized = str + // sanitize characters that cannot be included + .replace(/[!@#$%^&*]/g, "-") + // normalize separators to '-' + .replace(/[._/\s\\]/g, "-") + .split("-"); + return normalized.map((seg) => seg.match(wordPattern) || []).flat(); +} +function capitalizeFirstLetter(segment: string): string { + return segment[0].toUpperCase() + segment.slice(1); +} +function lowercaseFirstLetter(segment: string): string { + return segment[0].toLowerCase() + segment.slice(1); +} +function toKebab(str: string): string { + return splitString(str).join("-").toLowerCase(); +} +function toPascal(str: string): string { + return splitString(str).map(capitalizeFirstLetter).join(""); +} +function toCamel(str: string): string { + return lowercaseFirstLetter(toPascal(str)); +} +function toEnv(str: string): string { + return splitString(str).join("_").toUpperCase(); +} +export {capitalizeFirstLetter, toKebab, toCamel, toPascal, toEnv}; diff --git a/packages/cli/docsgen/index.ts b/packages/cli/docsgen/index.ts index 5e0a3364f73d..524f70a51c5b 100644 --- a/packages/cli/docsgen/index.ts +++ b/packages/cli/docsgen/index.ts @@ -1,103 +1,25 @@ import fs from "node:fs"; import path from "node:path"; -import {Options} from "yargs"; -import omit from "lodash/omit.js"; import {cmds} from "../src/cmds/index.js"; -import {CliCommand} from "../src/util/index.js"; import {globalOptions} from "../src/options/index.js"; -import {beaconOptions} from "../src/cmds/beacon/options.js"; -import {renderMarkdownSections, toMarkdownTable, MarkdownSection} from "./markdown.js"; +import {renderCommandPage} from "./markdown.js"; // Script to generate a reference of all CLI commands and options // Outputs a markdown format ready to be consumed by mkdocs // // Usage: -// ts-node docsgen docs/cli.md +// ts-node packages/cli/docsgen // -// After generation the resulting .md should be mv to the path expected +// After generation the resulting .md files, they are written to the path expected // by the mkdocs index and other existing paths in the documentation -const docsMarkdownPath = process.argv[2]; -if (!docsMarkdownPath) throw Error("Run script with output path: 'ts-node docsgen docs/cli.md'"); +const dirname = path.dirname(new URL(import.meta.url).pathname); +const LODESTAR_COMMAND = "./lodestar"; +const DOCS_PAGES_FOLDER = path.join(dirname, "..", "..", "..", "docs", "pages"); -const docsString = renderMarkdownSections([ - { - title: "Command Line Reference", - body: "This reference describes the syntax of the Lodestar CLI commands and their options.", - subsections: [ - { - title: "Global Options", - body: getOptionsTable(globalOptions), - }, - ...cmds.map((cmd) => cmdToMarkdownSection(cmd)), - ], - }, -]); - -fs.mkdirSync(path.parse(docsMarkdownPath).dir, {recursive: true}); -fs.writeFileSync(docsMarkdownPath, docsString); - -/** - * Parse an CliCommand type recursively and output a MarkdownSection - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function cmdToMarkdownSection(cmd: CliCommand, parentCommand?: string): MarkdownSection { - const commandJson = [parentCommand, cmd.command.replace("", "")].filter(Boolean).join(" "); - const body = [cmd.describe]; - - if (cmd.examples) { - body.push("**Examples**"); - for (const example of cmd.examples) { - if (example.command.startsWith("lodestar")) example.command = `lodestar ${example.command}`; - body.push(example.description); - body.push(`\`\`\` \n${example.command}\n\`\`\``); - } - } - - if (cmd.options) { - body.push("**Options**"); - - if (cmd.subcommands) { - body.push("The options below apply to all sub-commands."); - } - - // De-duplicate beaconOptions. If all beaconOptions exists in this command, skip them - if ( - cmds.some((c) => c.command === "beacon") && - commandJson !== "beacon" && - Object.keys(beaconOptions).every((key) => cmd.options?.[key]) - ) { - cmd.options = omit(cmd.options, Object.keys(beaconOptions)); - body.push(`Cmd \`${commandJson}\` has all the options from the [\`beacon\` cmd](#beacon).`); - } - - body.push(getOptionsTable(cmd.options)); - } - return { - title: `\`${commandJson}\``, - body, - subsections: (cmd.subcommands || []).map((subcmd) => cmdToMarkdownSection(subcmd, commandJson)), - }; -} - -/** - * Render a Yargs options dictionary to a markdown table - */ -function getOptionsTable(options: Record, {showHidden}: {showHidden?: boolean} = {}): string { - const visibleOptions = Object.entries(options).filter(([, opt]) => showHidden || !opt.hidden); - - if (visibleOptions.length === 0) { - return ""; - } - - /* eslint-disable @typescript-eslint/naming-convention */ - return toMarkdownTable( - visibleOptions.map(([key, opt]) => ({ - Option: `\`--${key}\``, - Type: opt.type ?? "", - Description: opt.description ?? "", - Default: String(opt.defaultDescription || opt.default || ""), - })), - ["Option", "Type", "Description", "Default"] - ); +for (const cmd of cmds) { + const docstring = renderCommandPage(cmd, globalOptions, LODESTAR_COMMAND); + const folder = path.join(DOCS_PAGES_FOLDER, cmd.docsFolder ?? ""); + if (!fs.existsSync(folder)) fs.mkdirSync(folder, {recursive: true}); + fs.writeFileSync(path.join(folder, `${cmd.command}-cli.md`), docstring); } diff --git a/packages/cli/docsgen/markdown.ts b/packages/cli/docsgen/markdown.ts index 80952f367c73..c05c7ad8c90f 100644 --- a/packages/cli/docsgen/markdown.ts +++ b/packages/cli/docsgen/markdown.ts @@ -1,41 +1,273 @@ -export type MarkdownSection = { - title: string; - body: string | string[]; - subsections?: MarkdownSection[]; -}; +import {CliOptionDefinition, CliCommand, CliExample, CliCommandOptions} from "../src/util/index.js"; +import {toKebab} from "./changeCase.js"; + +const DEFAULT_SEPARATOR = "\n\n"; +const LINE_BREAK = "\n\n
"; + +function renderExampleBody(example: CliExample, lodestarCommand?: string): string { + const cliExample = [ + `\`\`\` +${lodestarCommand ? `${lodestarCommand} ` : ""}${example.command} +\`\`\``, + ]; + + if (example.description) { + cliExample.unshift(example.description); + } + + return cliExample.join(DEFAULT_SEPARATOR); +} /** - * Render MarkdownSection recursively tracking its level depth + * Renders a single example like shown below. Title and description are optional. + * ------------------- + * #### Basic `validator` command example + * + * Run one validator client with all the keystores available in the directory .goerli/keystores + * + * ``` + * validator --network goerli + * ``` + * ------------------- */ -export function renderMarkdownSections(sections: MarkdownSection[], level = 1): string { - return sections - .map((section) => { - const parts = section.title ? [`${"\n" + "#".repeat(level)} ${section.title}`] : [""]; - if (section.body) { - parts.push(Array.isArray(section.body) ? section.body.join("\n\n") : section.body); - } - if (section.subsections) { - parts.push(renderMarkdownSections(section.subsections, level + 1)); - } - return parts.join(section.title ? "\n" : ""); - }) - .join("\n"); +function renderCommandExample(example: CliExample, lodestarCommand?: string): string { + const title = example.title ? `#### ${example.title}${DEFAULT_SEPARATOR}` : ""; + return title.concat(renderExampleBody(example, lodestarCommand)); } /** - * Render an array of objects as a markdown table + * Renders a example section like shown below + * ------------------- + * ## Examples + * + * #### Basic `validator` command example + * + * Run one validator client with all the keystores available in the directory .goerli/keystores + * + * ``` + * validator --network goerli + * ``` + * + * #### Advanced `validator` command example + * + * Run one validator client with all the keystores available in the directory .goerli/keystores + * using an rcConfig file for configuration + * + * ``` + * validator --rcConfig validator-dir/validator.rcconfig.yaml + * ``` + * ------------------- */ -export function toMarkdownTable(rows: T[], headers: (keyof T)[]): string { - return [ - toMarkdownTableRow(headers as string[]), - toMarkdownTableRow(headers.map(() => "---")), - ...rows.map((row) => toMarkdownTableRow(headers.map((key) => row[key]))), - ].join("\n"); +function renderExamplesSection(examples: CliExample[], sectionTitle?: string, lodestarCommand?: string): string { + const exampleSection = [sectionTitle]; + for (const example of examples) { + exampleSection.push(renderCommandExample(example, lodestarCommand)); + } + return exampleSection.filter(Boolean).join(DEFAULT_SEPARATOR); } /** - * Render an array of items as a markdown table row + * Renders a single cli option like shown below + * ------------------- + * #### `--logLevel` + * + * Logging verbosity level for emitting logs to terminal + * + * type: string + * default: info + * choices: "error", "warn", "info", "verbose", "debug" + * example: Set log level to debug + * + * ``` + * validator --logLevel debug + * ``` + * ------------------- */ -export function toMarkdownTableRow(row: string[]): string { - return `| ${row.join(" | ")} |`; +function renderOption(optionName: string, option: CliOptionDefinition): string | undefined { + if (option.hidden) return; + + const commandOption = [`#### \`--${optionName}\``]; + if (option.description) commandOption.push(`description: ${option.description}`); + + if (option.demandOption === true) { + commandOption.push("required: true"); + } + + if (option.type === "array") { + commandOption.push("type: `string[]`"); + } else if (option.type) { + commandOption.push(`type: \`${option.type}\``); + } + + if (option.choices) { + commandOption.push(`choices: ${option.choices.map((c) => `"${c}"`).join(", ")}`); + } + + let defaultValue = String(option.defaultDescription || option.default || ""); + if (defaultValue) { + if (option.type === "string" || option.string) { + defaultValue = `"${defaultValue}"`; + } + if (option.type === "array") { + // eslint-disable-next-line quotes + if (!defaultValue.includes(`"`)) { + defaultValue = `"${defaultValue}"`; + } + defaultValue = `[ ${defaultValue} ]`; + } + commandOption.push(`default: \`${defaultValue}\``); + } + + if (option.example) { + commandOption.push(`example: ${renderExampleBody(option.example)}`); + } + + return commandOption.join(DEFAULT_SEPARATOR).concat(LINE_BREAK); +} + +function renderOptions(options: CliCommandOptions>, title: string, description?: string): string { + const optionsSection = [title, description]; + for (const [name, option] of Object.entries(options)) { + const optionString = renderOption(name, option as CliOptionDefinition); + // Skip hidden options + if (optionString) { + optionsSection.push(optionString); + } + } + return optionsSection.filter(Boolean).join(DEFAULT_SEPARATOR); +} + +interface SubCommandDefinition { + command: string; + description?: string; + options?: CliCommandOptions>; + examples?: CliExample[]; +} + +function renderSubCommandsList(command: string, subCommands: SubCommandDefinition[]): string { + const list = [ + `## Available Sub-Commands + +The following sub-commands are available with the \`${command}\` command:`, + ]; + + for (const sub of subCommands) { + list.push(`- [${sub.command}](#${toKebab(sub.command)})`); + } + + return list.join(DEFAULT_SEPARATOR); +} + +/** + * ## `validator slashing-protection import` + * + * Import an interchange file from another client + * + * #### `validator slashing-protection import` Options + * + * `--file` + * + * The slashing protection interchange file to import (.json). + * + * type: string + * required: true + * + * #### Sub-Command Examples + * + * Import an interchange file to the slashing protection DB + * + * ``` + * ./lodestar validator slashing-protection import --network goerli --file interchange.json + * ``` + */ +function renderSubCommand(sub: SubCommandDefinition, lodestarCommand?: string): string { + const subCommand = [`## \`${sub.command}\``]; + + if (sub.description) { + subCommand.push(sub.description); + } + + if (sub.examples) { + subCommand.push(renderExamplesSection(sub.examples, `### \`${sub.command}\` Examples`, lodestarCommand)); + } + + if (sub.options) { + subCommand.push( + renderOptions( + sub.options, + `### \`${sub.command}\` Options`, + "_Supports all parent command options plus the following:_\n\n
" + ) + ); + } + + return subCommand.join(DEFAULT_SEPARATOR); +} + +function getSubCommands(rootCommand: string, sub: CliCommand): SubCommandDefinition[] { + const subCommands = [] as SubCommandDefinition[]; + + if (sub.command.includes("")) { + // If subcommand is a nested subcommand recursively render each of its subcommands by + // merging its props with its nested children but do not render the subcommand itself + for (const subSub of sub.subcommands ?? []) { + subCommands.push( + ...getSubCommands(rootCommand, { + ...subSub, + command: sub.command.replace("", subSub.command), + options: { + ...(sub.options ?? {}), + ...(subSub.options ?? {}), + }, + examples: sub.examples?.concat(subSub.examples ?? []), + }) + ); + } + } else { + // If subcommand is not nested build actual markdown + subCommands.push({ + command: `${rootCommand} ${sub.command}`, + description: sub.describe, + options: sub.options, + examples: sub.examples, + }); + + // render any sub-subcommands + if (sub.subcommands) { + for (const subSub of sub.subcommands) { + subCommands.push(...getSubCommands(`${rootCommand} ${sub.command}`, subSub)); + } + } + } + + return subCommands; +} + +export function renderCommandPage( + cmd: CliCommand, + globalOptions: CliCommandOptions>, + lodestarCommand?: string +): string { + const page = [`# \`${cmd.command}\` CLI Command`, cmd.describe]; + + const subCommands = (cmd.subcommands ?? []).map((sub) => getSubCommands(cmd.command, sub)).flat(); + if (subCommands.length > 0) { + page.push(renderSubCommandsList(cmd.command, subCommands)); + } + + if (cmd.examples) { + page.push(renderExamplesSection(cmd.examples, "## Examples", lodestarCommand)); + } + + if (cmd.options) { + page.push(renderOptions({...globalOptions, ...cmd.options}, `## \`${cmd.command}\` Options`)); + } + + if (subCommands.length > 0) { + for (const sub of subCommands) { + page.push(renderSubCommand(sub, lodestarCommand)); + } + } + + return page.join(LINE_BREAK.concat(DEFAULT_SEPARATOR)); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 2d58e12a987a..a318eb6193e9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@chainsafe/lodestar", - "version": "1.12.1", + "version": "1.13.0", "description": "Command line interface for lodestar", "author": "ChainSafe Systems", "license": "LGPL-3.0", @@ -24,7 +24,7 @@ "build": "tsc -p tsconfig.build.json && yarn write-git-data", "build:release": "yarn clean && yarn run build", "build:watch": "tsc -p tsconfig.build.json --watch", - "build:refdocs": "node --loader ts-node/esm ./docsgen/index.ts docs/cli.md", + "build:docs": "node --loader ts-node/esm ./docsgen/index.ts", "write-git-data": "node lib/util/gitData/writeGitData.js", "check-build": "node -e \"(async function() { await import('./lib/index.js') })()\" lodestar --help", "check-types": "tsc", @@ -65,17 +65,17 @@ "@libp2p/crypto": "^2.0.4", "@libp2p/peer-id": "^3.0.2", "@libp2p/peer-id-factory": "^3.0.4", - "@lodestar/api": "^1.12.1", - "@lodestar/beacon-node": "^1.12.1", - "@lodestar/config": "^1.12.1", - "@lodestar/db": "^1.12.1", - "@lodestar/light-client": "^1.12.1", - "@lodestar/logger": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/state-transition": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", - "@lodestar/validator": "^1.12.1", + "@lodestar/api": "^1.13.0", + "@lodestar/beacon-node": "^1.13.0", + "@lodestar/config": "^1.13.0", + "@lodestar/db": "^1.13.0", + "@lodestar/light-client": "^1.13.0", + "@lodestar/logger": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/state-transition": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", + "@lodestar/validator": "^1.13.0", "@multiformats/multiaddr": "^12.1.3", "@types/lockfile": "^1.0.2", "bip39": "^3.1.0", @@ -96,7 +96,7 @@ "yargs": "^17.7.1" }, "devDependencies": { - "@lodestar/test-utils": "^1.12.1", + "@lodestar/test-utils": "^1.13.0", "@types/debug": "^4.1.7", "@types/expand-tilde": "^2.0.0", "@types/got": "^9.6.12", diff --git a/packages/cli/src/cmds/beacon/index.ts b/packages/cli/src/cmds/beacon/index.ts index 0b2e431aec04..38d1d4cad221 100644 --- a/packages/cli/src/cmds/beacon/index.ts +++ b/packages/cli/src/cmds/beacon/index.ts @@ -6,6 +6,7 @@ import {beaconHandler} from "./handler.js"; export const beacon: CliCommand = { command: "beacon", describe: "Run a beacon chain node", + docsFolder: "beacon-management", examples: [ { command: "beacon --network goerli", diff --git a/packages/cli/src/cmds/beacon/options.ts b/packages/cli/src/cmds/beacon/options.ts index 3947e2ba17d0..c9918b5d2e41 100644 --- a/packages/cli/src/cmds/beacon/options.ts +++ b/packages/cli/src/cmds/beacon/options.ts @@ -1,7 +1,6 @@ -import {Options} from "yargs"; import {beaconNodeOptions, paramsOptions, BeaconNodeArgs} from "../../options/index.js"; import {LogArgs, logOptions} from "../../options/logOptions.js"; -import {CliCommandOptions} from "../../util/index.js"; +import {CliCommandOptions, CliOptionDefinition} from "../../util/index.js"; import {defaultBeaconPaths, BeaconPaths} from "./paths.js"; type BeaconExtraArgs = { @@ -144,7 +143,7 @@ type ENRArgs = { nat?: boolean; }; -const enrOptions: Record = { +const enrOptions: Record = { "enr.ip": { description: "Override ENR IP entry", type: "string", @@ -184,7 +183,7 @@ const enrOptions: Record = { export type BeaconArgs = BeaconExtraArgs & LogArgs & BeaconPaths & BeaconNodeArgs & ENRArgs; -export const beaconOptions: {[k: string]: Options} = { +export const beaconOptions: {[k: string]: CliOptionDefinition} = { ...beaconExtraOptions, ...logOptions, ...beaconNodeOptions, diff --git a/packages/cli/src/cmds/bootnode/index.ts b/packages/cli/src/cmds/bootnode/index.ts index c9a7db71eadc..4030c4a73b0f 100644 --- a/packages/cli/src/cmds/bootnode/index.ts +++ b/packages/cli/src/cmds/bootnode/index.ts @@ -7,6 +7,7 @@ export const bootnode: CliCommand = { command: "bootnode", describe: "Run a discv5 bootnode. This will NOT perform any beacon node functions, rather, it will run a discv5 service that allows nodes on the network to discover one another.", + docsFolder: "bootnode", options: bootnodeOptions as CliCommandOptions, handler: bootnodeHandler, }; diff --git a/packages/cli/src/cmds/bootnode/options.ts b/packages/cli/src/cmds/bootnode/options.ts index 622d7b2d506a..ab92ec00e155 100644 --- a/packages/cli/src/cmds/bootnode/options.ts +++ b/packages/cli/src/cmds/bootnode/options.ts @@ -1,6 +1,5 @@ -import {Options} from "yargs"; import {LogArgs, logOptions} from "../../options/logOptions.js"; -import {CliCommandOptions} from "../../util/index.js"; +import {CliOptionDefinition, CliCommandOptions} from "../../util/index.js"; import {MetricsArgs, options as metricsOptions} from "../../options/beaconNodeOptions/metrics.js"; import {defaultListenAddress, defaultP2pPort, defaultP2pPort6} from "../../options/beaconNodeOptions/network.js"; @@ -102,7 +101,7 @@ export const bootnodeExtraOptions: CliCommandOptions = { export type BootnodeArgs = BootnodeExtraArgs & LogArgs & MetricsArgs; -export const bootnodeOptions: {[k: string]: Options} = { +export const bootnodeOptions: {[k: string]: CliOptionDefinition} = { ...bootnodeExtraOptions, ...logOptions, ...metricsOptions, diff --git a/packages/cli/src/cmds/dev/index.ts b/packages/cli/src/cmds/dev/index.ts index 728e80b6ce28..d213c8b3218d 100644 --- a/packages/cli/src/cmds/dev/index.ts +++ b/packages/cli/src/cmds/dev/index.ts @@ -6,6 +6,7 @@ import {devHandler} from "./handler.js"; export const dev: CliCommand = { command: "dev", describe: "Quickly bootstrap a beacon node and multiple validators. Use for development and testing", + docsFolder: "contribution", examples: [ { command: "dev --genesisValidators 8 --reset", diff --git a/packages/cli/src/cmds/dev/options.ts b/packages/cli/src/cmds/dev/options.ts index ae3737646e4f..4665fe529776 100644 --- a/packages/cli/src/cmds/dev/options.ts +++ b/packages/cli/src/cmds/dev/options.ts @@ -1,5 +1,4 @@ -import {Options} from "yargs"; -import {CliCommandOptions} from "../../util/index.js"; +import {CliCommandOptions, CliOptionDefinition} from "../../util/index.js"; import {beaconOptions, BeaconArgs} from "../beacon/options.js"; import {NetworkName} from "../../networks/index.js"; import {beaconNodeOptions, globalOptions} from "../../options/index.js"; @@ -63,7 +62,7 @@ const devOwnOptions: CliCommandOptions = { * - and have api enabled by default (as it's used by validator) * Note: use beaconNodeOptions and globalOptions to make sure option key is correct */ -const externalOptionsOverrides: Partial> = { +const externalOptionsOverrides: Partial> = { // Custom paths different than regular beacon, validator paths // network="dev" will store all data in separate dir than other networks network: { diff --git a/packages/cli/src/cmds/lightclient/index.ts b/packages/cli/src/cmds/lightclient/index.ts index 6d2a8f1ecb4f..1fceb3823154 100644 --- a/packages/cli/src/cmds/lightclient/index.ts +++ b/packages/cli/src/cmds/lightclient/index.ts @@ -6,6 +6,7 @@ import {lightclientHandler} from "./handler.js"; export const lightclient: CliCommand = { command: "lightclient", describe: "Run lightclient", + docsFolder: "lightclient-prover", examples: [ { command: "lightclient --network goerli", diff --git a/packages/cli/src/cmds/validator/index.ts b/packages/cli/src/cmds/validator/index.ts index 46d7f2327452..49c7211c740d 100644 --- a/packages/cli/src/cmds/validator/index.ts +++ b/packages/cli/src/cmds/validator/index.ts @@ -12,9 +12,11 @@ import {validatorHandler} from "./handler.js"; export const validator: CliCommand = { command: "validator", describe: "Run one or multiple validator clients", + docsFolder: "validator-management", examples: [ { command: "validator --network goerli", + title: "Base `validator` command", description: "Run one validator client with all the keystores available in the directory" + ` ${getAccountPaths({dataDir: ".goerli"}, "goerli").keystoresDir}`, diff --git a/packages/cli/src/cmds/validator/keymanager/impl.ts b/packages/cli/src/cmds/validator/keymanager/impl.ts index c6b0ab200c01..2abda3c9642e 100644 --- a/packages/cli/src/cmds/validator/keymanager/impl.ts +++ b/packages/cli/src/cmds/validator/keymanager/impl.ts @@ -240,11 +240,11 @@ export class KeymanagerApi implements Api { // Skip unknown keys or remote signers const signer = this.validator.validatorStore.getSigner(pubkeyHex); - if (signer && signer?.type === SignerType.Local) { + if (signer && signer.type === SignerType.Local) { // Remove key from live local signer deletedKey[i] = this.validator.validatorStore.removeSigner(pubkeyHex); - // Remove key from blockduties + // Remove key from block duties // Remove from attestation duties // Remove from Sync committee duties // Remove from indices @@ -367,9 +367,12 @@ export class KeymanagerApi implements Api { // Remove key from live local signer const deletedFromMemory = - signer && signer?.type === SignerType.Remote ? this.validator.validatorStore.removeSigner(pubkeyHex) : false; + signer && signer.type === SignerType.Remote ? this.validator.validatorStore.removeSigner(pubkeyHex) : false; - // TODO: Remove duties + if (deletedFromMemory) { + // Remove duties if key was deleted from in-memory store + this.validator.removeDutiesForKey(pubkeyHex); + } const deletedFromDisk = this.persistedKeysBackend.deleteRemoteKey(pubkeyHex); diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index a14e5530844d..4f0ec476f01c 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -241,7 +241,7 @@ export const validatorOptions: CliCommandOptions = { "builder.selection": { type: "string", description: "Builder block selection strategy `maxprofit`, `builderalways`, `builderonly` or `executiononly`", - defaultDescription: `\`${defaultOptions.builderSelection}\``, + defaultDescription: `${defaultOptions.builderSelection}`, group: "builder", }, @@ -267,7 +267,7 @@ export const validatorOptions: CliCommandOptions = { importKeystoresPassword: { alias: ["passphraseFile"], // Backwards compatibility with old `validator import` cmd description: "Path to a file with password to decrypt all keystores from `importKeystores` option", - defaultDescription: "`./password.txt`", + defaultDescription: "./password.txt", type: "string", }, @@ -278,8 +278,6 @@ export const validatorOptions: CliCommandOptions = { type: "boolean", }, - // HIDDEN INTEROP OPTIONS - // Remote signer "externalSigner.url": { @@ -290,7 +288,7 @@ export const validatorOptions: CliCommandOptions = { "externalSigner.pubkeys": { description: - "List of validator public keys used by an external signer. May also provide a single string a comma separated public keys", + "List of validator public keys used by an external signer. May also provide a single string of comma-separated public keys", type: "array", string: true, // Ensures the pubkey string is not automatically converted to numbers coerce: (pubkeys: string[]): string[] => @@ -304,7 +302,8 @@ export const validatorOptions: CliCommandOptions = { "externalSigner.fetch": { conflicts: ["externalSigner.pubkeys"], - description: "Fetch then list of public keys to validate from an external signer", + description: + "Fetch the list of public keys to validate from an external signer. Cannot be used in combination with `--externalSigner.pubkeys`", type: "boolean", group: "externalSignerUrl", }, diff --git a/packages/cli/src/cmds/validator/signers/index.ts b/packages/cli/src/cmds/validator/signers/index.ts index 3fdf2460d1c6..be028461c0ee 100644 --- a/packages/cli/src/cmds/validator/signers/index.ts +++ b/packages/cli/src/cmds/validator/signers/index.ts @@ -21,8 +21,8 @@ const KEYSTORE_IMPORT_PROGRESS_MS = 10000; * --interopIndexes * --fromMnemonic, then requires --mnemonicIndexes * --importKeystores, then requires --importKeystoresPassword - * --externalSignerFetchPubkeys, then requires --externalSignerUrl - * --externalSignerPublicKeys, then requires --externalSignerUrl + * --externalSigner.fetch, then requires --externalSigner.url + * --externalSigner.pubkeys, then requires --externalSigner.url * else load from persisted * - both remote keys and local keystores * @@ -104,7 +104,7 @@ export async function getSignersFromArgs( }); } - // Remote keys declared manually with --externalSignerPublicKeys + // Remote keys are declared manually or will be fetched from external signer else if (args["externalSigner.pubkeys"] || args["externalSigner.fetch"]) { return getRemoteSigners(args); } @@ -158,13 +158,17 @@ export function getSignerPubkeyHex(signer: Signer): string { async function getRemoteSigners(args: IValidatorCliArgs & GlobalArgs): Promise { const externalSignerUrl = args["externalSigner.url"]; if (!externalSignerUrl) { - throw new YargsError("Must set externalSignerUrl with externalSignerPublicKeys"); + throw new YargsError( + `Must set externalSigner.url with ${ + args["externalSigner.pubkeys"] ? "externalSigner.pubkeys" : "externalSigner.fetch" + }` + ); } if (!isValidHttpUrl(externalSignerUrl)) { - throw new YargsError(`Invalid external signer URL ${externalSignerUrl}`); + throw new YargsError(`Invalid external signer URL: ${externalSignerUrl}`); } if (args["externalSigner.pubkeys"] && args["externalSigner.pubkeys"].length === 0) { - throw new YargsError("externalSignerPublicKeys is set to an empty list"); + throw new YargsError("externalSigner.pubkeys is set to an empty list"); } const pubkeys = args["externalSigner.pubkeys"] ?? (await externalSignerGetKeys(externalSignerUrl)); diff --git a/packages/cli/src/cmds/validator/signers/logSigners.ts b/packages/cli/src/cmds/validator/signers/logSigners.ts index 85d17ad323ae..85b7922cca15 100644 --- a/packages/cli/src/cmds/validator/signers/logSigners.ts +++ b/packages/cli/src/cmds/validator/signers/logSigners.ts @@ -1,5 +1,5 @@ import {Signer, SignerLocal, SignerRemote, SignerType} from "@lodestar/validator"; -import {LogLevel, Logger} from "@lodestar/utils"; +import {LogLevel, Logger, toSafePrintableUrl} from "@lodestar/utils"; /** * Log each pubkeys for auditing out keys are loaded from the logs @@ -27,7 +27,7 @@ export function logSigners(logger: Pick, signers: Signer[ } for (const {url, pubkeys} of groupExternalSignersByUrl(remoteSigners)) { - logger.info(`External signers on URL: ${url}`); + logger.info(`External signers on URL: ${toSafePrintableUrl(url)}`); for (const pubkey of pubkeys) { logger.info(pubkey); } diff --git a/packages/cli/src/cmds/validator/voluntaryExit.ts b/packages/cli/src/cmds/validator/voluntaryExit.ts index 2b56751edf41..770843c6445b 100644 --- a/packages/cli/src/cmds/validator/voluntaryExit.ts +++ b/packages/cli/src/cmds/validator/voluntaryExit.ts @@ -1,16 +1,17 @@ import inquirer from "inquirer"; +import bls from "@chainsafe/bls"; import { - computeSigningRoot, computeEpochAtSlot, + computeSigningRoot, computeStartSlotAtEpoch, getCurrentSlot, } from "@lodestar/state-transition"; import {createBeaconConfig} from "@lodestar/config"; -import {ssz, phase0} from "@lodestar/types"; +import {phase0, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; -import {Signer, SignerLocal, SignerType} from "@lodestar/validator"; +import {externalSignerPostSignature, SignableMessageType, Signer, SignerType} from "@lodestar/validator"; import {Api, ApiError, getClient} from "@lodestar/api"; -import {ensure0xPrefix, CliCommand, YargsError} from "../../util/index.js"; +import {CliCommand, ensure0xPrefix, YargsError} from "../../util/index.js"; import {GlobalArgs} from "../../options/index.js"; import {getBeaconConfigFromArgs} from "../../config/index.js"; import {IValidatorCliArgs} from "./options.js"; @@ -36,6 +37,12 @@ If no `pubkeys` are provided, it will exit all validators that have been importe command: "validator voluntary-exit --network goerli --pubkeys 0xF00", description: "Perform a voluntary exit for the validator who has a public key 0xF00", }, + { + command: + "validator voluntary-exit --network goerli --externalSigner.url http://signer:9000 --externalSigner.fetch --pubkeys 0xF00", + description: + "Perform a voluntary exit for the validator who has a public key 0xF00 and its secret key is on a remote signer", + }, ], options: { @@ -46,7 +53,7 @@ If no `pubkeys` are provided, it will exit all validators that have been importe }, pubkeys: { - description: "Public keys to exit, must be available as local signers", + description: "Public keys to exit", type: "array", string: true, // Ensures the pubkey string is not automatically converted to numbers coerce: (pubkeys: string[]): string[] => @@ -82,9 +89,12 @@ If no `pubkeys` are provided, it will exit all validators that have been importe // Select signers to exit const signers = await getSignersFromArgs(args, network, {logger: console, signal: new AbortController().signal}); if (signers.length === 0) { - throw new YargsError(`No local keystores found with current args. + throw new YargsError(`No validators to exit found with current args. Ensure --dataDir and --network match values used when importing keys via validator import - or alternatively, import keys by providing --importKeystores arg to voluntary-exit command.`); + or alternatively, import keys by providing --importKeystores arg to voluntary-exit command. + If attempting to exit validators on a remote signer, make sure values are provided for + the necessary --externalSigner options. + `); } const signersToExit = selectSignersToExit(args, signers); const validatorsToExit = await resolveValidatorIndexes(client, signersToExit); @@ -106,25 +116,41 @@ ${validatorsToExit.map((v) => `${v.pubkey} ${v.index} ${v.status}`).join("\n")}` } for (const [i, {index, signer, pubkey}] of validatorsToExit.entries()) { - const domain = config.getDomainForVoluntaryExit(computeStartSlotAtEpoch(exitEpoch)); + const slot = computeStartSlotAtEpoch(exitEpoch); + const domain = config.getDomainForVoluntaryExit(slot); const voluntaryExit: phase0.VoluntaryExit = {epoch: exitEpoch, validatorIndex: index}; const signingRoot = computeSigningRoot(ssz.phase0.VoluntaryExit, voluntaryExit, domain); + let signature; + switch (signer.type) { + case SignerType.Local: + signature = signer.secretKey.sign(signingRoot); + break; + case SignerType.Remote: { + const signatureHex = await externalSignerPostSignature(config, signer.url, pubkey, signingRoot, slot, { + data: voluntaryExit, + type: SignableMessageType.VOLUNTARY_EXIT, + }); + signature = bls.Signature.fromHex(signatureHex); + break; + } + default: + throw new YargsError(`Unexpected signer type for ${pubkey}`); + } ApiError.assert( await client.beacon.submitPoolVoluntaryExit({ message: voluntaryExit, - signature: signer.secretKey.sign(signingRoot).toBytes(), + signature: signature.toBytes(), }) ); - console.log(`Submitted voluntary exit for ${pubkey} ${i + 1}/${signersToExit.length}`); } }, }; -type SignerLocalPubkey = {signer: SignerLocal; pubkey: string}; +type SignerPubkey = {signer: Signer; pubkey: string}; -function selectSignersToExit(args: VoluntaryExitArgs, signers: Signer[]): SignerLocalPubkey[] { +function selectSignersToExit(args: VoluntaryExitArgs, signers: Signer[]): SignerPubkey[] { const signersWithPubkey = signers.map((signer) => ({ signer, pubkey: getSignerPubkeyHex(signer), @@ -132,14 +158,12 @@ function selectSignersToExit(args: VoluntaryExitArgs, signers: Signer[]): Signer if (args.pubkeys) { const signersByPubkey = new Map(signersWithPubkey.map(({pubkey, signer}) => [pubkey, signer])); - const selectedSigners: SignerLocalPubkey[] = []; + const selectedSigners: SignerPubkey[] = []; for (const pubkey of args.pubkeys) { const signer = signersByPubkey.get(pubkey); if (!signer) { throw new YargsError(`Unknown pubkey ${pubkey}`); - } else if (signer.type !== SignerType.Local) { - throw new YargsError(`pubkey ${pubkey} is not a local signer`); } else { selectedSigners.push({pubkey, signer}); } @@ -147,12 +171,12 @@ function selectSignersToExit(args: VoluntaryExitArgs, signers: Signer[]): Signer return selectedSigners; } else { - return signersWithPubkey.filter((signer): signer is SignerLocalPubkey => signer.signer.type === SignerType.Local); + return signersWithPubkey; } } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -async function resolveValidatorIndexes(client: Api, signersToExit: SignerLocalPubkey[]) { +async function resolveValidatorIndexes(client: Api, signersToExit: SignerPubkey[]) { const pubkeys = signersToExit.map(({pubkey}) => pubkey); const res = await client.beacon.getStateValidators("head", {id: pubkeys}); diff --git a/packages/cli/src/networks/gnosis.ts b/packages/cli/src/networks/gnosis.ts index b9d23bb274c1..4abe2a17b53d 100644 --- a/packages/cli/src/networks/gnosis.ts +++ b/packages/cli/src/networks/gnosis.ts @@ -19,4 +19,5 @@ export const bootEnrs = [ "enr:-Ly4QLKgv5M2D4DYJgo6s4NG_K4zu4sk5HOLCfGCdtgoezsbfRbfGpQ4iSd31M88ec3DHA5FWVbkgIas9EaJeXia0nwBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYRaJc2VjcDI1NmsxoQLpK_A47iNBkVjka9Mde1F-Kie-R0sq97MCNKCxt2HwOIhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA", "enr:-Ly4QF_0qvji6xqXrhQEhwJR1W9h5dXV7ZjVCN_NlosKxcgZW6emAfB_KXxEiPgKr_-CZG8CWvTiojEohG1ewF7P368Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYUqJc2VjcDI1NmsxoQIpNRUT6llrXqEbjkAodsZOyWv8fxQkyQtSvH4sg2D7n4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA", "enr:-Ly4QCD5D99p36WafgTSxB6kY7D2V1ca71C49J4VWI2c8UZCCPYBvNRWiv0-HxOcbpuUdwPVhyWQCYm1yq2ZH0ukCbQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYVSJc2VjcDI1NmsxoQJJMSV8iSZ8zvkgbi8cjIGEUVJeekLqT0LQha_co-siT4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA", + "enr:-KK4QKXJq1QOVWuJAGige4uaT8LRPQGCVRf3lH3pxjaVScMRUfFW1eiiaz8RwOAYvw33D4EX-uASGJ5QVqVCqwccxa-Bi4RldGgykCGm-DYDAABk__________-CaWSCdjSCaXCEM0QnzolzZWNwMjU2azGhAhNvrRkpuK4MWTf3WqiOXSOePL8Zc-wKVpZ9FQx_BDadg3RjcIIjKIN1ZHCCIyg", ]; diff --git a/packages/cli/src/options/beaconNodeOptions/builder.ts b/packages/cli/src/options/beaconNodeOptions/builder.ts index 7313d836a92c..96388ddfe2dd 100644 --- a/packages/cli/src/options/beaconNodeOptions/builder.ts +++ b/packages/cli/src/options/beaconNodeOptions/builder.ts @@ -1,18 +1,24 @@ import {defaultExecutionBuilderHttpOpts, IBeaconNodeOptions} from "@lodestar/beacon-node"; -import {CliCommandOptions} from "../../util/index.js"; +import {CliCommandOptions, YargsError} from "../../util/index.js"; export type ExecutionBuilderArgs = { builder: boolean; - "builder.urls"?: string[]; + "builder.url"?: string; "builder.timeout"?: number; "builder.faultInspectionWindow"?: number; "builder.allowedFaults"?: number; }; export function parseArgs(args: ExecutionBuilderArgs): IBeaconNodeOptions["executionBuilder"] { + if (Array.isArray(args["builder.url"]) || args["builder.url"]?.includes(",http")) { + throw new YargsError( + "Lodestar only supports a single builder URL. External tooling like mev-boost can be used to connect to multiple builder/relays" + ); + } + return { enabled: args["builder"], - urls: args["builder.urls"] ?? defaultExecutionBuilderHttpOpts.urls, + url: args["builder.url"] ?? defaultExecutionBuilderHttpOpts.url, timeout: args["builder.timeout"], faultInspectionWindow: args["builder.faultInspectionWindow"], allowedFaults: args["builder.allowedFaults"], @@ -27,14 +33,11 @@ export const options: CliCommandOptions = { group: "builder", }, - "builder.urls": { - description: "Urls hosting the builder API", - defaultDescription: defaultExecutionBuilderHttpOpts.urls.join(","), - type: "array", - string: true, - coerce: (urls: string[]): string[] => - // Parse ["url1,url2"] to ["url1", "url2"] - urls.map((item) => item.split(",")).flat(1), + "builder.url": { + alias: ["builder.urls"], + description: "Url hosting the builder API", + defaultDescription: defaultExecutionBuilderHttpOpts.url, + type: "string", group: "builder", }, diff --git a/packages/cli/src/options/beaconNodeOptions/chain.ts b/packages/cli/src/options/beaconNodeOptions/chain.ts index 5aef805f61f2..0c9280f5330c 100644 --- a/packages/cli/src/options/beaconNodeOptions/chain.ts +++ b/packages/cli/src/options/beaconNodeOptions/chain.ts @@ -25,6 +25,7 @@ export type ChainArgs = { emitPayloadAttributes?: boolean; broadcastValidationStrictness?: string; "chain.minSameMessageSignatureSetsToBatch"?: number; + "chain.maxShufflingCacheEpochs"?: number; }; export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] { @@ -51,6 +52,7 @@ export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] { broadcastValidationStrictness: args["broadcastValidationStrictness"], minSameMessageSignatureSetsToBatch: args["chain.minSameMessageSignatureSetsToBatch"] ?? defaultOptions.chain.minSameMessageSignatureSetsToBatch, + maxShufflingCacheEpochs: args["chain.maxShufflingCacheEpochs"] ?? defaultOptions.chain.maxShufflingCacheEpochs, }; } @@ -202,4 +204,12 @@ Will double processing times. Use only for debugging purposes.", default: defaultOptions.chain.minSameMessageSignatureSetsToBatch, group: "chain", }, + + "chain.maxShufflingCacheEpochs": { + hidden: true, + description: "Maximum ShufflingCache epochs to keep in memory", + type: "number", + default: defaultOptions.chain.maxShufflingCacheEpochs, + group: "chain", + }, }; diff --git a/packages/cli/src/options/beaconNodeOptions/sync.ts b/packages/cli/src/options/beaconNodeOptions/sync.ts index 6a28338adad3..7130b835b987 100644 --- a/packages/cli/src/options/beaconNodeOptions/sync.ts +++ b/packages/cli/src/options/beaconNodeOptions/sync.ts @@ -6,6 +6,7 @@ export type SyncArgs = { "sync.disableProcessAsChainSegment"?: boolean; "sync.disableRangeSync"?: boolean; "sync.backfillBatchSize"?: number; + "sync.slotImportTolerance"?: number; }; export function parseArgs(args: SyncArgs): IBeaconNodeOptions["sync"] { @@ -14,6 +15,7 @@ export function parseArgs(args: SyncArgs): IBeaconNodeOptions["sync"] { disableProcessAsChainSegment: args["sync.disableProcessAsChainSegment"], backfillBatchSize: args["sync.backfillBatchSize"] ?? defaultOptions.sync.backfillBatchSize, disableRangeSync: args["sync.disableRangeSync"], + slotImportTolerance: args["sync.slotImportTolerance"] ?? defaultOptions.sync.slotImportTolerance, }; } @@ -36,6 +38,14 @@ Use only for local networks with a single node, can be dangerous in regular netw group: "sync", }, + "sync.slotImportTolerance": { + hidden: true, + type: "number", + description: "Number of slot tolerance to trigger range sync and to measure if node is synced.", + defaultDescription: String(defaultOptions.sync.slotImportTolerance), + group: "sync", + }, + "sync.disableProcessAsChainSegment": { hidden: true, type: "boolean", diff --git a/packages/cli/src/options/paramsOptions.ts b/packages/cli/src/options/paramsOptions.ts index 49ddc1b563f5..643fb991bc61 100644 --- a/packages/cli/src/options/paramsOptions.ts +++ b/packages/cli/src/options/paramsOptions.ts @@ -1,7 +1,6 @@ -import {Options} from "yargs"; import {ChainConfig, chainConfigTypes} from "@lodestar/config"; import {IBeaconParamsUnparsed} from "../config/types.js"; -import {ObjectKeys, CliCommandOptions} from "../util/index.js"; +import {ObjectKeys, CliCommandOptions, CliOptionDefinition} from "../util/index.js"; // No options are statically declared // If an arbitrary key notation is used, it removes type safety on most of this CLI arg parsing code. @@ -25,7 +24,7 @@ export function parseBeaconParamsArgs(args: Record): IB } const paramsOptionsByName = ObjectKeys(chainConfigTypes).reduce( - (options: Record, key): Record => ({ + (options: Record, key): Record => ({ ...options, [getArgKey(key)]: { hidden: true, diff --git a/packages/cli/src/util/command.ts b/packages/cli/src/util/command.ts index 32d7b24e02bf..0dd2fd82bc9f 100644 --- a/packages/cli/src/util/command.ts +++ b/packages/cli/src/util/command.ts @@ -1,17 +1,32 @@ import {Options, Argv} from "yargs"; +export interface CliExample { + command: string; + title?: string; + description?: string; +} + +export interface CliOptionDefinition extends Options { + example?: Omit; +} + export type CliCommandOptions = Required<{ [K in keyof OwnArgs]: undefined extends OwnArgs[K] - ? Options + ? CliOptionDefinition : // If arg cannot be undefined it must specify a default value - Options & Required>; + CliOptionDefinition & Required>; }>; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface CliCommand, ParentArgs = Record, R = any> { command: string; describe: string; - examples?: {command: string; description: string}[]; + /** + * The folder in docs/pages that the cli.md should be placed in. If not provided no + * cli flags page will be generated for the command + */ + docsFolder?: string; + examples?: CliExample[]; options?: CliCommandOptions; // 1st arg: any = free own sub command options // 2nd arg: subcommand parent options is = to this command options + parent options @@ -37,7 +52,7 @@ export function registerCommandToYargs(yargs: Argv, cliCommand: CliCommand { + const password = "password"; + externalSigner = await startExternalSigner({ + keystoreStrings: await getKeystoresStr( + password, + interopSecretKeys(2).map((k) => k.toHex()) + ), + password: password, + }); + }); + + after("stop external signer container", async () => { + await externalSigner.container.stop(); + }); + + it("Perform a voluntary exit", async () => { + const testContext = getMochaContext(this); + + const restPort = 9596; + const devBnProc = await spawnCliCommand( + "packages/cli/bin/lodestar.js", + [ + // ⏎ + "dev", + `--dataDir=${path.join(testFilesDir, "dev-voluntary-exit")}`, + "--genesisValidators=8", + "--startValidators=0..7", + "--rest", + `--rest.port=${restPort}`, + // Speed up test to make genesis happen faster + "--params.SECONDS_PER_SLOT=2", + // Allow voluntary exists to be valid immediately + "--params.SHARD_COMMITTEE_PERIOD=0", + ], + {pipeStdioToParent: false, logPrefix: "dev", testContext} + ); + + // Exit early if process exits + devBnProc.on("exit", (code) => { + if (code !== null && code > 0) { + throw new Error(`devBnProc process exited with code ${code}`); + } + }); + + const baseUrl = `http://127.0.0.1:${restPort}`; + const client = getClient({baseUrl}, {config}); + + // Wait for beacon node API to be available + genesis + await retry( + async () => { + const head = await client.beacon.getBlockHeader("head"); + ApiError.assert(head); + if (head.response.data.header.message.slot < 1) throw Error("pre-genesis"); + }, + {retryDelay: 1000, retries: 20} + ); + + const indexesToExit = [0, 1]; + const pubkeysToExit = indexesToExit.map((i) => interopSecretKey(i).toPublicKey().toHex()); + + await execCliCommand( + "packages/cli/bin/lodestar.js", + [ + "validator", + "voluntary-exit", + "--network=dev", + "--yes", + `--externalSigner.url=${externalSigner.url}`, + "--externalSigner.fetch=true", + `--server=${baseUrl}`, + `--pubkeys=${pubkeysToExit.join(",")}`, + ], + {pipeStdioToParent: false, logPrefix: "voluntary-exit"} + ); + + for (const pubkey of pubkeysToExit) { + await retry( + async () => { + const res = await client.beacon.getStateValidator("head", pubkey); + ApiError.assert(res); + if (res.response.data.status !== "active_exiting") { + throw Error("Validator not exiting"); + } else { + // eslint-disable-next-line no-console + console.log(`Confirmed validator ${pubkey} = ${res.response.data.status}`); + } + }, + {retryDelay: 1000, retries: 20} + ); + } + }); +}); diff --git a/packages/cli/test/sim/multi_fork.test.ts b/packages/cli/test/sim/multi_fork.test.ts index 0ac8d18ed055..71d6eb4a42ea 100644 --- a/packages/cli/test/sim/multi_fork.test.ts +++ b/packages/cli/test/sim/multi_fork.test.ts @@ -2,6 +2,7 @@ import path from "node:path"; import {sleep, toHex, toHexString} from "@lodestar/utils"; import {ApiError} from "@lodestar/api"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {CLIQUE_SEALING_PERIOD, SIM_TESTS_SECONDS_PER_SLOT} from "../utils/simulation/constants.js"; import {AssertionMatch, BeaconClient, ExecutionClient, ValidatorClient} from "../utils/simulation/interfaces.js"; import {SimulationEnvironment} from "../utils/simulation/SimulationEnvironment.js"; @@ -199,19 +200,30 @@ await checkpointSync.execution.job.stop(); // Unknown block sync // ======================================================== +const headForUnknownBlockSync = await env.nodes[0].beacon.api.beacon.getBlockV2("head"); +ApiError.assert(headForUnknownBlockSync); const unknownBlockSync = await env.createNodePair({ id: "unknown-block-sync-node", beacon: { type: BeaconClient.Lodestar, - options: {clientOptions: {"network.allowPublishToZeroPeers": true, "sync.disableRangeSync": true}}, + options: { + clientOptions: { + "network.allowPublishToZeroPeers": true, + "sync.disableRangeSync": true, + // unknownBlockSync node start when other nodes are multiple epoch ahead and + // unknown block sync can work only if the gap is maximum `slotImportTolerance * 2` + // default value for slotImportTolerance is one epoch, so if gap is more than 2 epoch + // unknown block sync will not work. So why we have to increase it for tests. + // Adding SLOTS_PER_EPOCH will cover the case if the node starts on the last slot of epoch + "sync.slotImportTolerance": headForUnknownBlockSync.response.data.message.slot / 2 + SLOTS_PER_EPOCH, + }, + }, }, execution: ExecutionClient.Geth, keysCount: 0, }); await unknownBlockSync.execution.job.start(); await unknownBlockSync.beacon.job.start(); -const headForUnknownBlockSync = await env.nodes[0].beacon.api.beacon.getBlockV2("head"); -ApiError.assert(headForUnknownBlockSync); await connectNewNode(unknownBlockSync, env.nodes); // Wait for EL node to start and sync diff --git a/packages/cli/test/unit/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index fe2433bb5030..bc302bbc17d6 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -35,6 +35,7 @@ describe("options / beaconNodeOptions", () => { "chain.archiveStateEpochFrequency": 1024, "chain.trustedSetup": "", "chain.minSameMessageSignatureSetsToBatch": 32, + "chain.maxShufflingCacheEpochs": 100, emitPayloadAttributes: false, eth1: true, @@ -52,7 +53,7 @@ describe("options / beaconNodeOptions", () => { "execution.retryAttempts": 1, builder: false, - "builder.urls": ["http://localhost:8661"], + "builder.url": "http://localhost:8661", "builder.timeout": 12000, "builder.faultInspectionWindow": 32, "builder.allowedFaults": 16, @@ -137,6 +138,7 @@ describe("options / beaconNodeOptions", () => { emitPayloadAttributes: false, trustedSetup: "", minSameMessageSignatureSetsToBatch: 32, + maxShufflingCacheEpochs: 100, }, eth1: { enabled: true, @@ -155,7 +157,7 @@ describe("options / beaconNodeOptions", () => { }, executionBuilder: { enabled: false, - urls: ["http://localhost:8661"], + url: "http://localhost:8661", timeout: 12000, faultInspectionWindow: 32, allowedFaults: 16, @@ -206,6 +208,7 @@ describe("options / beaconNodeOptions", () => { }, sync: { isSingleNode: true, + slotImportTolerance: 32, disableProcessAsChainSegment: true, backfillBatchSize: 64, disableRangeSync: false, diff --git a/packages/cli/test/unit/validator/decryptKeystoreDefinitions.test.ts b/packages/cli/test/unit/validator/decryptKeystoreDefinitions.test.ts index 296105012b69..dc3ce5dff5ad 100644 --- a/packages/cli/test/unit/validator/decryptKeystoreDefinitions.test.ts +++ b/packages/cli/test/unit/validator/decryptKeystoreDefinitions.test.ts @@ -2,8 +2,8 @@ import fs from "node:fs"; import path from "node:path"; import {rimraf} from "rimraf"; import {expect} from "chai"; +import {getKeystoresStr} from "@lodestar/test-utils"; import {cachedSeckeysHex} from "../../utils/cachedKeys.js"; -import {getKeystoresStr} from "../../utils/keystores.js"; import {testFilesDir} from "../../utils.js"; import {decryptKeystoreDefinitions} from "../../../src/cmds/validator/keymanager/decryptKeystoreDefinitions.js"; import {LocalKeystoreDefinition} from "../../../src/cmds/validator/keymanager/interface.js"; diff --git a/packages/config/package.json b/packages/config/package.json index 93f60ebeef5e..c63a666f4fd6 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/config", - "version": "1.12.1", + "version": "1.13.0", "description": "Chain configuration required for lodestar", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -65,7 +65,7 @@ ], "dependencies": { "@chainsafe/ssz": "^0.14.0", - "@lodestar/params": "^1.12.1", - "@lodestar/types": "^1.12.1" + "@lodestar/params": "^1.13.0", + "@lodestar/types": "^1.13.0" } } diff --git a/packages/db/package.json b/packages/db/package.json index 153960931e00..1f83c4e25f3c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/db", - "version": "1.12.1", + "version": "1.13.0", "description": "DB modules of Lodestar", "author": "ChainSafe Systems", "homepage": "https://github.com/ChainSafe/lodestar#readme", @@ -38,13 +38,13 @@ }, "dependencies": { "@chainsafe/ssz": "^0.14.0", - "@lodestar/config": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/config": "^1.13.0", + "@lodestar/utils": "^1.13.0", "@types/levelup": "^4.3.3", "it-all": "^3.0.2", "level": "^8.0.0" }, "devDependencies": { - "@lodestar/logger": "^1.12.1" + "@lodestar/logger": "^1.13.0" } } diff --git a/packages/db/test/unit/controller/level.test.ts b/packages/db/test/unit/controller/level.test.ts index 38d8274ebb22..768ef3a39006 100644 --- a/packages/db/test/unit/controller/level.test.ts +++ b/packages/db/test/unit/controller/level.test.ts @@ -144,7 +144,8 @@ describe("LevelDB controller", () => { return "gdu"; } } catch { - /* no-op */ + /* eslint-disable no-console */ + console.error("Cannot find gdu command, falling back to du"); } } return "du"; diff --git a/packages/flare/package.json b/packages/flare/package.json index deef9bb34d38..159ff7d3c69f 100644 --- a/packages/flare/package.json +++ b/packages/flare/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/flare", - "version": "1.12.1", + "version": "1.13.0", "description": "Beacon chain debugging tool", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -60,12 +60,12 @@ "dependencies": { "@chainsafe/bls": "7.1.1", "@chainsafe/bls-keygen": "^0.3.0", - "@lodestar/api": "^1.12.1", - "@lodestar/config": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/state-transition": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/api": "^1.13.0", + "@lodestar/config": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/state-transition": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", "source-map-support": "^0.5.21", "yargs": "^17.7.1" }, diff --git a/packages/flare/src/util/command.ts b/packages/flare/src/util/command.ts index f22aca319af0..81a3993f3c43 100644 --- a/packages/flare/src/util/command.ts +++ b/packages/flare/src/util/command.ts @@ -1,6 +1,16 @@ import {Options, Argv} from "yargs"; -export type CliCommandOptions = Required<{[key in keyof OwnArgs]: Options}>; +export interface CliExample { + command: string; + title?: string; + description?: string; +} + +export interface CliOptionDefinition extends Options { + example?: CliExample; +} + +export type CliCommandOptions = Required<{[key in keyof OwnArgs]: CliOptionDefinition}>; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface CliCommand, ParentArgs = Record, R = any> { diff --git a/packages/fork-choice/package.json b/packages/fork-choice/package.json index c1b71d818b52..dbbbcb6d3dbd 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.12.1", + "version": "1.13.0", "type": "module", "exports": "./lib/index.js", "types": "./lib/index.d.ts", @@ -39,11 +39,11 @@ }, "dependencies": { "@chainsafe/ssz": "^0.14.0", - "@lodestar/config": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/state-transition": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1" + "@lodestar/config": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/state-transition": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0" }, "keywords": [ "ethereum", diff --git a/packages/light-client/README.md b/packages/light-client/README.md index 85ebd86d3c19..7afd871b3f2e 100644 --- a/packages/light-client/README.md +++ b/packages/light-client/README.md @@ -1,26 +1,129 @@ -# Lodestar Light-client +# Lodestar Light Client + +Ethereum light clients provide a pathway for users to interact with the Ethereum blockchain in a trust-minimized manner, comparable to the level of trust required when engaging with a third-party provider like Infura or EtherScan. Not that those platforms are bad, but trust in any centralized provider goes against the ethos of blockchain. Light clients are a way that low-power devices, like cell phones, can do self validation of transactions and dApp state. + +Unlike full nodes, light clients do not download and store the entire blockchain. Instead, they download only the headers of each block and employ Merkle proofs to verify transactions. This enables a quick synchronization with the network and access the latest information without using significant system resources​. This streamlined approach to accessing Ethereum is crucial, especially in scenarios where full-scale network participation is infeasible or undesired. + +The evolution of light clients is emblematic of the broader trajectory of Ethereum towards becoming more accessible and resource-efficient, making blockchain technology more inclusive and adaptable to a wide array of use cases and environments. The Altair hard fork introduced sync committees to allow light-clients to synchronize to the network. + +## Prerequisites [![Discord](https://img.shields.io/discord/593655374469660673.svg?label=Discord&logo=discord)](https://discord.gg/aMxzVcr) [![Eth Consensus Spec v1.1.10](https://img.shields.io/badge/ETH%20consensus--spec-1.1.10-blue)](https://github.com/ethereum/consensus-specs/releases/tag/v1.1.10) ![ES Version](https://img.shields.io/badge/ES-2021-yellow) ![Node Version](https://img.shields.io/badge/node-16.x-green) +[Yarn](https://yarnpkg.com/) > This package is part of [ChainSafe's Lodestar](https://lodestar.chainsafe.io) project -## Prerequisites +## Requirements for Running a Light-Client + +Access to an beacon node that supports the light client specification is necessary. The client must support the following routes from the [consensus API spec](https://github.com/ethereum/consensus-specs/tree/dev): -- [NodeJS](https://nodejs.org/) (LTS) -- [Yarn](https://yarnpkg.com/) +- `/eth/v1/beacon/light_client/updates` +- `/eth/v1/beacon/light_client/optimistic_update` +- `/eth/v1/beacon/light_client/finality_update` +- `/eth/v1/beacon/light_client/bootstrap/{block_root}` +- `/eth/v0/beacon/light_client/committee_root` -## What you need +System requirements are quite low so its possible to run a light client in the browser as part of a website. There are a few examples of this on github that you can use as reference, our [prover](https://chainsafe.github.io/lodestar/lightclient-prover/prover.md) being one of them. -You will need to go over the [specification](https://github.com/ethereum/consensus-specs). +You can find more information about the light-client protocol in the [specification](https://github.com/ethereum/consensus-specs). ## Getting started - Follow the [installation guide](https://chainsafe.github.io/lodestar/) to install Lodestar. - Quickly try out the whole stack by [starting a local testnet](https://chainsafe.github.io/lodestar/usage/local). +## Light-Client CLI Example + +It is possible to start up the light-client as a standalone process. + +```bash +lodestar lightclient \ + --network mainnet \ + --beacon-api-url https://beacon-node.your-domain.com \ + --checkpoint-root "0xccaff4b99986a7b05e06738f1828a32e40799b277fd9f9ff069be55341fe0229" +``` + +## Light-Client Programmatic Example + +For this example we will assume there is a running beacon node at `https://beacon-node.your-domain.com` + +```ts +import {Api} from "@lodestar/api/beacon"; +import {ApiError} from "@lodestar/api"; +import {Bytes32} from "@lodestar/types"; +import {createChainForkConfig} from "@lodestar/config"; +import {networksChainConfig} from "@lodestar/config/networks"; +import { + GenesisData, + Lightclient, + LightclientEvent, + RunStatusCode, + getLcLoggerConsole +} from `@lodestar/lightclient`; + +async function getGenesisData(api: Pick): Promise { + const res = await api.beacon.getGenesis(); + ApiError.assert(res); + + return { + genesisTime: Number(res.response.data.genesisTime), + genesisValidatorsRoot: res.response.data.genesisValidatorsRoot, + }; +} + +async function getSyncCheckpoint(api: Pick): Promise { + const res = await api.beacon.getStateFinalityCheckpoints("head"); + ApiError.assert(res); + return res.response.data.finalized.root; +} + +const config = createChainForkConfig(networksChainConfig.mainnet); + +const logger = getLcLoggerConsole({logDebug: Boolean(process.env.DEBUG)}); + +const api = getClient({urls: ["https://beacon-node.your-domain.com"]}, {config}); + +const transport = new LightClientRestTransport(api); + +const lightclient = await Lightclient.initializeFromCheckpointRoot({ + config, + logger, + transport, + genesisData: await getGenesisData(api), + checkpointRoot: await getSyncCheckpoint(api), + opts: { + allowForcedUpdates: true, + updateHeadersOnForcedUpdate: true, + } +}); + +// Wait for the lightclient to start +await new Promise((resolve) => { + const lightclientStarted = (status: RunStatusCode): void => { + if (status === RunStatusCode.started) { + this.lightclient?.emitter.off(LightclientEvent.statusChange, lightclientStarted); + resolve(); + } + }; + lightclient?.emitter.on(LightclientEvent.statusChange, lightclientStarted); + logger.info("Initiating lightclient"); + lightclient?.start(); +}); + +logger.info("Lightclient synced"); + +lightclient.emitter.on(LightclientEvent.lightClientFinalityHeader, async (finalityUpdate) => { + console.log(finalityUpdate); +}); + +lightclient.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (optimisticUpdate) => { + console.log(optimisticUpdate); +}); +``` + ## Contributors Read our [contributors document](/CONTRIBUTING.md), [submit an issue](https://github.com/ChainSafe/lodestar/issues/new/choose) or talk to us on our [discord](https://discord.gg/yjyvFRP)! diff --git a/packages/light-client/package.json b/packages/light-client/package.json index d145cdb40708..468e82e0ed0f 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -67,12 +67,12 @@ "@chainsafe/bls": "7.1.1", "@chainsafe/persistent-merkle-tree": "^0.6.1", "@chainsafe/ssz": "^0.14.0", - "@lodestar/api": "^1.12.1", - "@lodestar/config": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/state-transition": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/api": "^1.13.0", + "@lodestar/config": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/state-transition": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", "mitt": "^3.0.0", "strict-event-emitter-types": "^2.0.0" }, diff --git a/packages/logger/package.json b/packages/logger/package.json index 82a503652006..8a2f40a176a7 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -63,13 +63,13 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@lodestar/utils": "^1.12.1", + "@lodestar/utils": "^1.13.0", "winston": "^3.8.2", "winston-daily-rotate-file": "^4.7.1", "winston-transport": "^4.5.0" }, "devDependencies": { - "@lodestar/test-utils": "^1.12.1", + "@lodestar/test-utils": "^1.13.0", "@types/triple-beam": "^1.3.2", "rimraf": "^4.4.1", "triple-beam": "^1.3.0" diff --git a/packages/params/package.json b/packages/params/package.json index 805b5bdb0fc1..11606f1d3b07 100644 --- a/packages/params/package.json +++ b/packages/params/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/params", - "version": "1.12.1", + "version": "1.13.0", "description": "Chain parameters required for lodestar", "author": "ChainSafe Systems", "license": "Apache-2.0", diff --git a/packages/prover/.mocharc.yaml b/packages/prover/.mocharc.yaml deleted file mode 100644 index f9375365e517..000000000000 --- a/packages/prover/.mocharc.yaml +++ /dev/null @@ -1,8 +0,0 @@ -colors: true -timeout: 2000 -exit: true -extension: ["ts"] -require: - - ./test/setup.ts -node-option: - - "loader=ts-node/esm" diff --git a/packages/prover/.nycrc.json b/packages/prover/.nycrc.json deleted file mode 100644 index 69aa626339a0..000000000000 --- a/packages/prover/.nycrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../.nycrc.json" -} diff --git a/packages/prover/karma.config.cjs b/packages/prover/karma.config.cjs deleted file mode 100644 index a3ebb967e2ce..000000000000 --- a/packages/prover/karma.config.cjs +++ /dev/null @@ -1,9 +0,0 @@ -const karmaConfig = require("../../karma.base.config.js"); -const webpackConfig = require("./webpack.test.config.cjs"); - -module.exports = function karmaConfigurator(config) { - config.set({ - ...karmaConfig, - webpack: webpackConfig, - }); -}; diff --git a/packages/prover/package.json b/packages/prover/package.json index cf117f46a346..8ce95d5e79d7 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -53,9 +53,12 @@ "lint:fix": "yarn run lint --fix", "pretest": "yarn run check-types", "test": "yarn test:unit && yarn test:e2e", - "test:unit": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'", - "test:browsers": "yarn karma start karma.config.cjs", - "test:e2e": "LODESTAR_PRESET=minimal mocha 'test/e2e/**/*.test.ts'", + "test:unit": "vitest --run --dir test/unit/ --coverage", + "test:browsers": "yarn test:browsers:chrome && yarn test:browsers:firefox && yarn test:browsers:electron", + "test:browsers:chrome": "vitest --run --browser chrome --config ./vitest.browser.config.ts --dir test/unit", + "test:browsers:firefox": "vitest --run --browser firefox --config ./vitest.browser.config.ts --dir test/unit", + "test:browsers:electron": "echo 'Electron tests will be introduced back in the future as soon vitest supports electron.'", + "test:e2e": "LODESTAR_PRESET=minimal vitest --run --poolOptions.threads.singleThread --dir test/e2e", "check-readme": "typescript-docs-verifier", "generate-fixtures": "node --loader ts-node/esm scripts/generate_fixtures.ts" }, @@ -69,13 +72,13 @@ "@ethereumjs/tx": "^4.1.2", "@ethereumjs/util": "^8.0.6", "@ethereumjs/vm": "^6.4.2", - "@lodestar/api": "^1.12.1", - "@lodestar/config": "^1.12.1", - "@lodestar/light-client": "^1.12.1", - "@lodestar/logger": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/api": "^1.13.0", + "@lodestar/config": "^1.13.0", + "@lodestar/light-client": "^1.13.0", + "@lodestar/logger": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", "ethereum-cryptography": "^1.2.0", "find-up": "^6.3.0", "http-proxy": "^1.18.1", @@ -84,7 +87,7 @@ "yargs": "^17.7.1" }, "devDependencies": { - "@lodestar/test-utils": "^1.12.1", + "@lodestar/test-utils": "^1.13.0", "@types/http-proxy": "^1.17.10", "@types/yargs": "^17.0.24", "axios": "^1.3.4", diff --git a/packages/prover/src/utils/command.ts b/packages/prover/src/utils/command.ts index f22aca319af0..81a3993f3c43 100644 --- a/packages/prover/src/utils/command.ts +++ b/packages/prover/src/utils/command.ts @@ -1,6 +1,16 @@ import {Options, Argv} from "yargs"; -export type CliCommandOptions = Required<{[key in keyof OwnArgs]: Options}>; +export interface CliExample { + command: string; + title?: string; + description?: string; +} + +export interface CliOptionDefinition extends Options { + example?: CliExample; +} + +export type CliCommandOptions = Required<{[key in keyof OwnArgs]: CliOptionDefinition}>; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface CliCommand, ParentArgs = Record, R = any> { diff --git a/packages/prover/src/utils/conversion.ts b/packages/prover/src/utils/conversion.ts index 1f143baf1bda..c809de5c4555 100644 --- a/packages/prover/src/utils/conversion.ts +++ b/packages/prover/src/utils/conversion.ts @@ -23,7 +23,8 @@ export function bufferToHex(buffer: Buffer | Uint8Array): string { } export function hexToBuffer(val: string): Buffer { - return Buffer.from(val.replace("0x", ""), "hex"); + const hexWithEvenLength = val.length % 2 ? `0${val}` : val; + return Buffer.from(hexWithEvenLength.replace("0x", ""), "hex"); } export function padLeft(v: T, length: number): T { diff --git a/packages/prover/src/utils/evm.ts b/packages/prover/src/utils/evm.ts index ecebda78b8ad..63cfd3f14026 100644 --- a/packages/prover/src/utils/evm.ts +++ b/packages/prover/src/utils/evm.ts @@ -56,7 +56,7 @@ export async function getVMWithState({ const accessListTx = cleanObject({ to, from, - data: tx.data, + data: tx.input ? tx.input : tx.data, value: tx.value, gas: tx.gas ? tx.gas : numberToHex(gasLimit), gasPrice: "0x0", diff --git a/packages/prover/test/e2e/cli/cmds/start.test.ts b/packages/prover/test/e2e/cli/cmds/start.test.ts index 576cc0fd5e2a..da0c9dceb405 100644 --- a/packages/prover/test/e2e/cli/cmds/start.test.ts +++ b/packages/prover/test/e2e/cli/cmds/start.test.ts @@ -1,7 +1,7 @@ import childProcess from "node:child_process"; import {writeFile, mkdir} from "node:fs/promises"; import path from "node:path"; -import {expect} from "chai"; +import {describe, it, expect, beforeAll, afterAll} from "vitest"; import Web3 from "web3"; import {runCliCommand, spawnCliCommand, stopChildProcess} from "@lodestar/test-utils"; import {sleep} from "@lodestar/utils"; @@ -15,11 +15,11 @@ describe("prover/start", () => { it("should show help", async () => { const output = await runCliCommand(cli, ["start", "--help"]); - expect(output).contains("Show help"); + expect(output).toEqual(expect.stringContaining("Show help")); }); it("should fail when --executionRpcUrl is missing", async () => { - await expect(runCliCommand(cli, ["start", "--port", "8088"])).eventually.rejectedWith( + await expect(runCliCommand(cli, ["start", "--port", "8088"])).rejects.toThrow( "Missing required argument: executionRpcUrl" ); }); @@ -33,13 +33,13 @@ describe("prover/start", () => { "--beaconBootnodes", "http://localhost:0000", ]) - ).eventually.rejectedWith("Arguments beaconBootnodes and beaconUrls are mutually exclusive"); + ).rejects.toThrow("Arguments beaconBootnodes and beaconUrls are mutually exclusive"); }); it("should fail when both of --beaconUrls and --beaconBootnodes are not provided", async () => { await expect( runCliCommand(cli, ["start", "--port", "8088", "--executionRpcUrl", "http://localhost:3000"]) - ).eventually.rejectedWith("Either --beaconUrls or --beaconBootnodes must be provided"); + ).rejects.toThrow("Either --beaconUrls or --beaconBootnodes must be provided"); }); describe("when started", () => { @@ -47,8 +47,7 @@ describe("prover/start", () => { const paramsFilePath = path.join("/tmp", "e2e-test-env", "params.json"); const web3: Web3 = new Web3(proxyUrl); - before(async function () { - this.timeout(50000); + beforeAll(async function () { await waitForCapellaFork(); await mkdir(path.dirname(paramsFilePath), {recursive: true}); await writeFile(paramsFilePath, JSON.stringify(chainConfigToJson(config as ChainConfig))); @@ -72,22 +71,22 @@ describe("prover/start", () => { ); // Give sometime to the prover to start proxy server await sleep(3000); - }); + }, 50000); - after(async () => { + afterAll(async () => { await stopChildProcess(proc); }); it("should respond to verified calls", async () => { const accounts = await web3.eth.getAccounts(); - expect(accounts.length).to.be.gt(0); - await expect(web3.eth.getBalance(accounts[0])).eventually.not.null; + expect(accounts.length).toBeGreaterThan(0); + await expect(web3.eth.getBalance(accounts[0])).resolves.not.toBeNull(); }); it("should respond to unverified calls", async () => { // Because web3 latest version return numbers as bigint by default - await expect(web3.eth.getChainId()).eventually.eql(BigInt(chainId)); + await expect(web3.eth.getChainId()).resolves.toEqual(BigInt(chainId)); }); }); }); diff --git a/packages/prover/test/e2e/web3_batch_request.test.ts b/packages/prover/test/e2e/web3_batch_request.test.ts index cadd7af52203..afb995d088a0 100644 --- a/packages/prover/test/e2e/web3_batch_request.test.ts +++ b/packages/prover/test/e2e/web3_batch_request.test.ts @@ -1,18 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {expect} from "chai"; +import {describe, it, expect, beforeAll} from "vitest"; import Web3 from "web3"; import {LCTransport} from "../../src/interfaces.js"; import {createVerifiedExecutionProvider} from "../../src/web3_provider.js"; import {rpcUrl, beaconUrl, config} from "../utils/e2e_env.js"; import {getVerificationFailedMessage} from "../../src/utils/json_rpc.js"; +/* prettier-ignore */ describe("web3_batch_requests", function () { - // Give some margin to sync light client - this.timeout("10s"); - let web3: Web3; - before(() => { + beforeAll(() => { const {provider} = createVerifiedExecutionProvider(new Web3.providers.HttpProvider(rpcUrl), { transport: LCTransport.Rest, urls: [beaconUrl], @@ -45,8 +43,8 @@ describe("web3_batch_requests", function () { await batch.execute(); - expect(results.length).to.be.gt(1); - await expect(Promise.all(results)).to.be.fulfilled; + expect(results.length).toBeGreaterThan(1); + await expect(Promise.all(results)).resolves.toBeDefined(); }); it("should be able to process batch request containing error", async () => { @@ -66,8 +64,8 @@ describe("web3_batch_requests", function () { await batch.execute(); - await expect(successRequest).to.be.fulfilled; - await expect(errorRequest).to.be.rejectedWith(getVerificationFailedMessage("eth_getBlockByHash")); + await expect(successRequest).resolves.toBeDefined(); + await expect(errorRequest).rejects.toThrow(getVerificationFailedMessage("eth_getBlockByHash")); }); }); -}); +}, {timeout: 10_000}); diff --git a/packages/prover/test/e2e/web3_provider.test.ts b/packages/prover/test/e2e/web3_provider.test.ts index b2b4b94277d8..4de51dc94051 100644 --- a/packages/prover/test/e2e/web3_provider.test.ts +++ b/packages/prover/test/e2e/web3_provider.test.ts @@ -1,15 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {expect} from "chai"; +import {describe, it, expect, beforeAll} from "vitest"; import Web3 from "web3"; import {ethers} from "ethers"; import {LCTransport} from "../../src/interfaces.js"; import {createVerifiedExecutionProvider} from "../../src/web3_provider.js"; import {waitForCapellaFork, testTimeout, rpcUrl, beaconUrl, config} from "../utils/e2e_env.js"; +/* prettier-ignore */ describe("web3_provider", function () { - this.timeout(testTimeout); - - before("wait for the capella fork", async () => { + beforeAll(async () => { await waitForCapellaFork(); }); @@ -26,8 +25,8 @@ describe("web3_provider", function () { const accounts = await web3.eth.getAccounts(); // `getProof` will always remain the non-verified method // as we use it to create proof and verify - expect(accounts).not.to.be.empty; - await expect(web3.eth.getProof(accounts[0], [], "latest")).fulfilled; + expect(Object.keys(accounts)).not.toHaveLength(0); + await expect(web3.eth.getProof(accounts[0], [], "latest")).resolves.toBeDefined(); }); }); @@ -40,9 +39,9 @@ describe("web3_provider", function () { }); const accounts = await provider.listAccounts(); - expect(accounts).not.to.be.empty; - await expect(provider.send("eth_getProof", [accounts[0].address, [], "latest"])).fulfilled; + expect(Object.keys(accounts)).not.toHaveLength(0); + await expect(provider.send("eth_getProof", [accounts[0].address, [], "latest"])).resolves.toBeDefined(); }); }); }); -}); +}, {timeout: testTimeout}); diff --git a/packages/prover/test/globalSetup.ts b/packages/prover/test/globalSetup.ts new file mode 100644 index 000000000000..0ab57c057472 --- /dev/null +++ b/packages/prover/test/globalSetup.ts @@ -0,0 +1,2 @@ +export async function setup(): Promise {} +export async function teardown(): Promise {} diff --git a/packages/prover/test/mocks/request_handler.ts b/packages/prover/test/mocks/request_handler.ts index 0a61214c52e2..bbe4f24057de 100644 --- a/packages/prover/test/mocks/request_handler.ts +++ b/packages/prover/test/mocks/request_handler.ts @@ -1,4 +1,5 @@ -import sinon from "sinon"; +import {vi, expect} from "vitest"; +import {when} from "vitest-when"; import deepmerge from "deepmerge"; import {NetworkName} from "@lodestar/config/networks"; import {ForkConfig} from "@lodestar/config"; @@ -51,28 +52,29 @@ export interface TestFixture { dependentRequests: {payload: JsonRpcRequestOrBatch; response: Writeable}[]; } -function matchTransaction(value: ELTransaction, expected: ELTransaction): boolean { - if ( - value.to?.toLowerCase() !== expected.to?.toLowerCase() || - value.from.toLocaleLowerCase() !== expected.from.toLowerCase() - ) { +function matchTransaction(received: ELTransaction, expected: ELTransaction): boolean { + if (received.to?.toLowerCase() !== expected.to?.toLowerCase()) { return false; } - if ("value" in value && value.value.toLowerCase() !== expected.value.toLowerCase()) { + if ("from" in expected && "from" in received && received.from.toLowerCase() !== expected.from.toLowerCase()) { return false; } - if ("data" in value && value.data?.toLowerCase() !== expected.data?.toLowerCase()) { + if ("value" in received && received.value.toLowerCase() !== expected.value.toLowerCase()) { + return false; + } + + if ("data" in received && received.data?.toLowerCase() !== expected.data?.toLowerCase()) { return false; } return true; } -function matchParams(params: unknown[], expected: unknown[]): boolean { - for (let i = 0; i < params.length; i++) { - const item = params[i]; +function matchParams(received: unknown[], expected: unknown[]): boolean { + for (let i = 0; i < received.length; i++) { + const item = received[i]; const expectedItem = expected[i]; if (typeof item === "string" && typeof expectedItem === "string") { @@ -92,20 +94,12 @@ function matchParams(params: unknown[], expected: unknown[]): boolean { return true; } -function getPayloadParamsMatcher(expected: unknown[]): sinon.SinonMatcher { - return sinon.match(function (params: unknown[]): boolean { - return matchParams(params, expected); - }, "payload match params"); -} - -function getBatchPayloadMatcher(expected: JsonRpcBatchRequest): sinon.SinonMatcher { - return sinon.match(function (value: JsonRpcBatchRequest): boolean { - for (const [index, item] of value.entries()) { - if (item.method !== expected[index].method) return false; - if (!matchParams(item.params, expected[index].params)) return false; - } - return true; - }, "batch payload match"); +function matchBatchPayload(received: JsonRpcBatchRequest, expected: JsonRpcBatchRequest): boolean { + for (const [index, item] of received.entries()) { + if (item.method !== expected[index].method) return false; + if (!matchParams(item.params, expected[index].params)) return false; + } + return true; } export function generateReqHandlerOptionsMock( @@ -119,7 +113,7 @@ export function generateReqHandlerOptionsMock( const options = { logger: getEmptyLogger(), proofProvider: { - getExecutionPayload: sinon.stub().resolves(executionPayload), + getExecutionPayload: vi.fn().mockResolvedValue(executionPayload), config: { ...config, // eslint-disable-next-line @typescript-eslint/naming-convention @@ -129,35 +123,46 @@ export function generateReqHandlerOptionsMock( } as unknown as ProofProvider, network: data.network as NetworkName, rpc: { - request: sinon.stub(), - batchRequest: sinon.stub(), + request: vi.fn(), + batchRequest: vi.fn(), getRequestId: () => (Math.random() * 10000).toFixed(0), }, }; - options.rpc.request - .withArgs(data.request.method, getPayloadParamsMatcher(data.request.params), sinon.match.any) - .resolves(data.response); + when(options.rpc.request) + .calledWith( + data.request.method, + expect.toSatisfy((received) => matchParams(received as unknown[], data.request.params)), + expect.anything() + ) + .thenResolve(data.response); for (const {payload, response} of data.dependentRequests) { if (isBatchRequest(payload)) { - options.rpc.batchRequest - .withArgs(getBatchPayloadMatcher(payload), sinon.match.any) - .resolves(mergeBatchReqResp(payload, response as JsonRpcBatchResponse)); + when(options.rpc.batchRequest) + .calledWith( + expect.toSatisfy((received) => matchBatchPayload(received as JsonRpcBatchRequest, payload)), + expect.anything() + ) + .thenResolve(mergeBatchReqResp(payload, response as JsonRpcBatchResponse)); } else { - options.rpc.request - .withArgs(payload.method, getPayloadParamsMatcher(payload.params), sinon.match.any) - .resolves(response); + when(options.rpc.request) + .calledWith( + payload.method, + expect.toSatisfy((received) => matchParams(received as unknown[], data.request.params)), + expect.anything() + ) + .thenResolve(response); } } - options.rpc.request - .withArgs("eth_getBlockByNumber", [data.execution.block.number, true], sinon.match.any) - .resolves({id: 1233, jsonrpc: "2.0", result: data.execution.block}); + when(options.rpc.request) + .calledWith("eth_getBlockByNumber", [data.execution.block.number, true], expect.anything()) + .thenResolve({id: 1233, jsonrpc: "2.0", result: data.execution.block}); - options.rpc.request - .withArgs("eth_getBlockByHash", [data.execution.block.hash, true], sinon.match.any) - .resolves({id: 1233, jsonrpc: "2.0", result: data.execution.block}); + when(options.rpc.request) + .calledWith("eth_getBlockByHash", [data.execution.block.hash, true], expect.anything()) + .thenResolve({id: 1233, jsonrpc: "2.0", result: data.execution.block}); return options as unknown as Omit, "payload">; } diff --git a/packages/prover/test/setup.ts b/packages/prover/test/setup.ts deleted file mode 100644 index b83e6cb78511..000000000000 --- a/packages/prover/test/setup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import sinonChai from "sinon-chai"; - -chai.use(chaiAsPromised); -chai.use(sinonChai); diff --git a/packages/prover/test/tsconfig.json b/packages/prover/test/tsconfig.json new file mode 100644 index 000000000000..7e6bad81b22f --- /dev/null +++ b/packages/prover/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "noEmit": false + } +} \ No newline at end of file diff --git a/packages/prover/test/unit/proof_provider/orderd_map.test.ts b/packages/prover/test/unit/proof_provider/orderd_map.test.ts index 098f4f9127d5..309c4de71568 100644 --- a/packages/prover/test/unit/proof_provider/orderd_map.test.ts +++ b/packages/prover/test/unit/proof_provider/orderd_map.test.ts @@ -1,25 +1,25 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {OrderedMap} from "../../../src/proof_provider/ordered_map.js"; describe("proof_provider/ordered_map", () => { it("should initialize the min with undefined", () => { const omap = new OrderedMap(); - expect(omap.min).to.undefined; + expect(omap.min).toBeUndefined(); }); it("should initialize the max with undefined", () => { const omap = new OrderedMap(); - expect(omap.max).to.undefined; + expect(omap.max).toBeUndefined(); }); it("should set the min and max to the first value ", () => { const omap = new OrderedMap(); omap.set(11, "value"); - expect(omap.min).eql(11); - expect(omap.max).eql(11); + expect(omap.min).toEqual(11); + expect(omap.max).toEqual(11); }); it("should set the max value", () => { @@ -27,7 +27,7 @@ describe("proof_provider/ordered_map", () => { omap.set(10, "value"); omap.set(11, "value"); - expect(omap.max).eql(11); + expect(omap.max).toEqual(11); }); it("should set the min value", () => { @@ -35,6 +35,6 @@ describe("proof_provider/ordered_map", () => { omap.set(10, "value"); omap.set(11, "value"); - expect(omap.min).eql(10); + expect(omap.min).toEqual(10); }); }); diff --git a/packages/prover/test/unit/proof_provider/payload_store.test.ts b/packages/prover/test/unit/proof_provider/payload_store.test.ts index dc99ab1baa22..02c9cc39f87f 100644 --- a/packages/prover/test/unit/proof_provider/payload_store.test.ts +++ b/packages/prover/test/unit/proof_provider/payload_store.test.ts @@ -1,17 +1,14 @@ -import {expect} from "chai"; -import chai from "chai"; -import sinon from "sinon"; -import sinonChai from "sinon-chai"; -import {Api} from "@lodestar/api"; +import {describe, it, expect, beforeEach, vi, MockedObject} from "vitest"; +import {when} from "vitest-when"; +import {Api, HttpStatusCode, routes} from "@lodestar/api"; import {hash} from "@lodestar/utils"; import {Logger} from "@lodestar/logger"; import {allForks, capella} from "@lodestar/types"; import {toHexString} from "@lodestar/utils"; +import {ForkName} from "@lodestar/params"; import {PayloadStore} from "../../../src/proof_provider/payload_store.js"; import {MAX_PAYLOAD_HISTORY} from "../../../src/constants.js"; -chai.use(sinonChai); - const createHash = (input: string): Uint8Array => hash(Buffer.from(input, "utf8")); const buildPayload = ({blockNumber}: {blockNumber: number}): allForks.ExecutionPayload => @@ -47,22 +44,23 @@ const buildBlockResponse = ({ }: { slot: number; blockNumber: number; -}): {ok: boolean; response: {version: number; executionOptimistic: boolean; data: allForks.SignedBeaconBlock}} => ({ +}): routes.beacon.block.BlockV2Response<"json"> => ({ ok: true, + status: HttpStatusCode.OK, response: { - version: 12, + version: ForkName.altair, executionOptimistic: true, data: buildBlock({slot, blockNumber}), }, }); describe("proof_provider/payload_store", function () { - let api: Api; + let api: Api & {beacon: MockedObject}; let logger: Logger; let store: PayloadStore; beforeEach(() => { - api = {beacon: {getBlockV2: sinon.stub()}} as unknown as Api; + api = {beacon: {getBlockV2: vi.fn()}} as unknown as Api & {beacon: MockedObject}; logger = console as unknown as Logger; store = new PayloadStore({api, logger}); }); @@ -82,7 +80,7 @@ describe("proof_provider/payload_store", function () { const payload = buildPayload({blockNumber: 10}); store.set(payload, true); - expect(store.finalized).to.eql(payload); + expect(store.finalized).toEqual(payload); }); it("should return highest finalized payload", () => { @@ -91,7 +89,7 @@ describe("proof_provider/payload_store", function () { store.set(payload1, true); store.set(payload2, true); - expect(store.finalized).to.eql(payload2); + expect(store.finalized).toEqual(payload2); }); }); @@ -106,7 +104,7 @@ describe("proof_provider/payload_store", function () { store.set(payload1, true); store.set(payload2, true); - expect(store.latest).to.eql(payload2); + expect(store.latest).toEqual(payload2); }); it("should return latest payload if not finalized", () => { @@ -115,20 +113,20 @@ describe("proof_provider/payload_store", function () { store.set(payload1, false); store.set(payload2, false); - expect(store.latest).to.eql(payload2); + expect(store.latest).toEqual(payload2); }); }); describe("get", () => { it("should return undefined for an empty store", async () => { - await expect(store.get(10)).to.eventually.undefined; + await expect(store.get(10)).resolves.toBeUndefined(); }); it("should return undefined for non existing block id", async () => { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, false); - await expect(store.get(11)).to.eventually.undefined; + await expect(store.get(11)).resolves.toBeUndefined(); }); it("should return undefined for non existing block hash", async () => { @@ -136,7 +134,7 @@ describe("proof_provider/payload_store", function () { store.set(payload1, false); const nonExistingBlockHash = createHash("non-existing-block-hash"); - await expect(store.get(toHexString(nonExistingBlockHash))).to.eventually.undefined; + await expect(store.get(toHexString(nonExistingBlockHash))).resolves.toBeUndefined(); }); describe("block hash as blockId", () => { @@ -144,7 +142,7 @@ describe("proof_provider/payload_store", function () { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, false); - await expect(store.get(toHexString(payload1.blockHash))).to.eventually.eql(payload1); + await expect(store.get(toHexString(payload1.blockHash))).resolves.toEqual(payload1); }); }); @@ -153,7 +151,7 @@ describe("proof_provider/payload_store", function () { const finalizedPayload = buildPayload({blockNumber: 10}); store.set(finalizedPayload, true); - await expect(store.get(11)).to.rejectedWith( + await expect(store.get(11)).rejects.toThrow( "Block number 11 is higher than the latest finalized block number. We recommend to use block hash for unfinalized blocks." ); }); @@ -162,28 +160,28 @@ describe("proof_provider/payload_store", function () { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, false); - await expect(store.get(10)).to.eventually.undefined; + await expect(store.get(10)).resolves.toBeUndefined(); }); it("should return payload for a block number in hex", async () => { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, true); - await expect(store.get(`0x${payload1.blockNumber.toString(16)}`)).to.eventually.eql(payload1); + await expect(store.get(`0x${payload1.blockNumber.toString(16)}`)).resolves.toEqual(payload1); }); it("should return payload for a block number as string", async () => { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, true); - await expect(store.get(payload1.blockNumber.toString())).to.eventually.eql(payload1); + await expect(store.get(payload1.blockNumber.toString())).resolves.toEqual(payload1); }); it("should return payload for a block number as integer", async () => { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, true); - await expect(store.get(10)).to.eventually.eql(payload1); + await expect(store.get(10)).resolves.toEqual(payload1); }); it("should fetch the finalized payload from API if payload root not exists", async () => { @@ -193,22 +191,22 @@ describe("proof_provider/payload_store", function () { const availablePayload = buildPayload({blockNumber}); const unavailablePayload = buildPayload({blockNumber: unavailableBlockNumber}); - (api.beacon.getBlockV2 as sinon.SinonStub) - .withArgs(blockNumber) - .resolves(buildBlockResponse({blockNumber, slot: blockNumber})); + when(api.beacon.getBlockV2) + .calledWith(blockNumber) + .thenResolve(buildBlockResponse({blockNumber, slot: blockNumber})); - (api.beacon.getBlockV2 as sinon.SinonStub) - .withArgs(unavailableBlockNumber) - .resolves(buildBlockResponse({blockNumber: unavailableBlockNumber, slot: unavailableBlockNumber})); + when(api.beacon.getBlockV2) + .calledWith(unavailableBlockNumber) + .thenResolve(buildBlockResponse({blockNumber: unavailableBlockNumber, slot: unavailableBlockNumber})); store.set(availablePayload, true); const result = await store.get(unavailablePayload.blockNumber); - expect(api.beacon.getBlockV2 as sinon.SinonStub).calledTwice; - expect(api.beacon.getBlockV2 as sinon.SinonStub).calledWith(blockNumber); - expect(api.beacon.getBlockV2 as sinon.SinonStub).calledWith(unavailableBlockNumber); - expect(result).to.eql(unavailablePayload); + expect(api.beacon.getBlockV2).toHaveBeenCalledTimes(2); + expect(api.beacon.getBlockV2).toHaveBeenCalledWith(blockNumber); + expect(api.beacon.getBlockV2).toHaveBeenCalledWith(unavailableBlockNumber); + expect(result).toEqual(unavailablePayload); }); }); }); @@ -219,16 +217,16 @@ describe("proof_provider/payload_store", function () { store.set(payload1, false); // Unfinalized blocks are not indexed by block hash - await expect(store.get(toHexString(payload1.blockHash))).to.eventually.eql(payload1); - expect(store.finalized).to.eql(undefined); + await expect(store.get(toHexString(payload1.blockHash))).resolves.toEqual(payload1); + expect(store.finalized).toEqual(undefined); }); it("should set the payload for finalized blocks", async () => { const payload1 = buildPayload({blockNumber: 10}); store.set(payload1, true); - await expect(store.get(payload1.blockNumber.toString())).to.eventually.eql(payload1); - expect(store.finalized).to.eql(payload1); + await expect(store.get(payload1.blockNumber.toString())).resolves.toEqual(payload1); + expect(store.finalized).toEqual(payload1); }); }); @@ -243,15 +241,15 @@ describe("proof_provider/payload_store", function () { const slot = 20; const header = buildLCHeader({slot, blockNumber}); const blockResponse = buildBlockResponse({blockNumber, slot}); - const executionPayload = (blockResponse.response.data as capella.SignedBeaconBlock).message.body + const executionPayload = (blockResponse.response?.data as capella.SignedBeaconBlock).message.body .executionPayload; - (api.beacon.getBlockV2 as sinon.SinonStub).resolves(blockResponse); + api.beacon.getBlockV2.mockResolvedValue(blockResponse); await store.processLCHeader(header, true); - expect(api.beacon.getBlockV2).calledOnce; - expect(api.beacon.getBlockV2).calledWith(20); - expect(store.finalized).to.eql(executionPayload); + expect(api.beacon.getBlockV2).toHaveBeenCalledOnce(); + expect(api.beacon.getBlockV2).toHaveBeenCalledWith(20); + expect(store.finalized).toEqual(executionPayload); }); it("should process lightclient header for finalized block which exists as un-finalized in store", async () => { @@ -259,9 +257,9 @@ describe("proof_provider/payload_store", function () { const slot = 20; const header = buildLCHeader({slot, blockNumber}); const blockResponse = buildBlockResponse({blockNumber, slot}); - const executionPayload = (blockResponse.response.data as capella.SignedBeaconBlock).message.body + const executionPayload = (blockResponse.response?.data as capella.SignedBeaconBlock).message.body .executionPayload; - (api.beacon.getBlockV2 as sinon.SinonStub).resolves(blockResponse); + api.beacon.getBlockV2.mockResolvedValue(blockResponse); expect(store.finalized).to.undefined; // First process as unfinalized @@ -271,8 +269,8 @@ describe("proof_provider/payload_store", function () { await store.processLCHeader(header, true); // Called only once when we process unfinalized - expect(api.beacon.getBlockV2).to.be.calledOnce; - expect(store.finalized).to.eql(executionPayload); + expect(api.beacon.getBlockV2).to.be.toHaveBeenCalledOnce(); + expect(store.finalized).toEqual(executionPayload); }); }); @@ -280,19 +278,19 @@ describe("proof_provider/payload_store", function () { const blockNumber = 10; const slot = 20; const header = buildLCHeader({slot, blockNumber}); - (api.beacon.getBlockV2 as sinon.SinonStub).resolves(buildBlockResponse({blockNumber, slot})); + api.beacon.getBlockV2.mockResolvedValue(buildBlockResponse({blockNumber, slot})); await store.processLCHeader(header); - expect(api.beacon.getBlockV2).calledOnce; - expect(api.beacon.getBlockV2).calledWith(20); + expect(api.beacon.getBlockV2).toHaveBeenCalledOnce(); + expect(api.beacon.getBlockV2).toHaveBeenCalledWith(20); }); it("should not fetch existing payload for lightclient header", async () => { const blockNumber = 10; const slot = 20; const header = buildLCHeader({slot, blockNumber}); - (api.beacon.getBlockV2 as sinon.SinonStub).resolves(buildBlockResponse({blockNumber, slot})); + api.beacon.getBlockV2.mockResolvedValue(buildBlockResponse({blockNumber, slot})); await store.processLCHeader(header); @@ -300,21 +298,21 @@ describe("proof_provider/payload_store", function () { await store.processLCHeader(header); // The network fetch should be done once - expect(api.beacon.getBlockV2).calledOnce; - expect(api.beacon.getBlockV2).calledWith(20); + expect(api.beacon.getBlockV2).toHaveBeenCalledOnce(); + expect(api.beacon.getBlockV2).toHaveBeenCalledWith(20); }); it("should prune the existing payloads", async () => { const blockNumber = 10; const slot = 20; const header = buildLCHeader({slot, blockNumber}); - (api.beacon.getBlockV2 as sinon.SinonStub).resolves(buildBlockResponse({blockNumber, slot})); + api.beacon.getBlockV2.mockResolvedValue(buildBlockResponse({blockNumber, slot})); - sinon.spy(store, "prune"); + vi.spyOn(store, "prune"); await store.processLCHeader(header); - expect(store.prune).to.be.calledOnce; + expect(store.prune).toHaveBeenCalledOnce(); }); }); @@ -330,11 +328,11 @@ describe("proof_provider/payload_store", function () { store.set(buildPayload({blockNumber: i}), true); } - expect(store["payloads"].size).to.equal(numberOfPayloads); + expect(store["payloads"].size).toEqual(numberOfPayloads); store.prune(); - expect(store["payloads"].size).to.equal(MAX_PAYLOAD_HISTORY); + expect(store["payloads"].size).toEqual(MAX_PAYLOAD_HISTORY); }); it("should not prune the existing payloads if equal to MAX_PAYLOAD_HISTORY", () => { @@ -344,11 +342,11 @@ describe("proof_provider/payload_store", function () { store.set(buildPayload({blockNumber: i}), true); } - expect(store["payloads"].size).to.equal(MAX_PAYLOAD_HISTORY); + expect(store["payloads"].size).toEqual(MAX_PAYLOAD_HISTORY); store.prune(); - expect(store["payloads"].size).to.equal(MAX_PAYLOAD_HISTORY); + expect(store["payloads"].size).toEqual(MAX_PAYLOAD_HISTORY); }); it("should not prune the existing payloads if less than MAX_PAYLOAD_HISTORY", () => { @@ -358,11 +356,11 @@ describe("proof_provider/payload_store", function () { store.set(buildPayload({blockNumber: i}), true); } - expect(store["payloads"].size).to.equal(numberOfPayloads); + expect(store["payloads"].size).toEqual(numberOfPayloads); store.prune(); - expect(store["payloads"].size).to.equal(numberOfPayloads); + expect(store["payloads"].size).toEqual(numberOfPayloads); }); it("should prune finalized roots", () => { @@ -372,33 +370,33 @@ describe("proof_provider/payload_store", function () { store.set(buildPayload({blockNumber: i}), true); } - expect(store["finalizedRoots"].size).to.equal(numberOfPayloads); + expect(store["finalizedRoots"].size).toEqual(numberOfPayloads); store.prune(); - expect(store["finalizedRoots"].size).to.equal(MAX_PAYLOAD_HISTORY); + expect(store["finalizedRoots"].size).toEqual(MAX_PAYLOAD_HISTORY); }); it("should prune unfinalized roots", async () => { const numberOfPayloads = MAX_PAYLOAD_HISTORY + 2; for (let i = 1; i <= numberOfPayloads; i++) { - (api.beacon.getBlockV2 as sinon.SinonStub) - .withArgs(i) - .resolves(buildBlockResponse({blockNumber: 500 + i, slot: i})); + when(api.beacon.getBlockV2) + .calledWith(i) + .thenResolve(buildBlockResponse({blockNumber: 500 + i, slot: i})); await store.processLCHeader(buildLCHeader({blockNumber: 500 + i, slot: i}), false); } // Because all payloads are unfinalized, they are not pruned - expect(store["unfinalizedRoots"].size).to.equal(numberOfPayloads); + expect(store["unfinalizedRoots"].size).toEqual(numberOfPayloads); // Let make some payloads finalized await store.processLCHeader(buildLCHeader({blockNumber: 500 + 1, slot: 1}), true); await store.processLCHeader(buildLCHeader({blockNumber: 500 + 2, slot: 2}), true); // store.processLCHeader will call the prune method internally and clean the unfinalized roots - expect(store["unfinalizedRoots"].size).to.equal(numberOfPayloads - 2); + expect(store["unfinalizedRoots"].size).toEqual(numberOfPayloads - 2); }); }); }); diff --git a/packages/prover/test/unit/utils/assertion.test.ts b/packages/prover/test/unit/utils/assertion.test.ts index 9bbe07a19b9f..5cf481bacdf1 100644 --- a/packages/prover/test/unit/utils/assertion.test.ts +++ b/packages/prover/test/unit/utils/assertion.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {ethers} from "ethers"; import Web3 from "web3"; import {isSendProvider, isWeb3jsProvider, isEthersProvider} from "../../../src/utils/assertion.js"; @@ -11,41 +11,41 @@ describe("utils/assertion", () => { // Do nothing; }, }; - expect(isSendProvider(provider)).to.be.true; + expect(isSendProvider(provider)).toBe(true); }); it("should return false for ethers provider", () => { const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isSendProvider(provider)).to.be.false; + expect(isSendProvider(provider)).toBe(false); }); it("should return false for web3 provider", () => { const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isSendProvider(provider)).to.be.false; + expect(isSendProvider(provider)).toBe(false); }); }); describe("isWeb3jsProvider", () => { it("should return true if provider is web3.js provider", () => { const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isWeb3jsProvider(provider)).to.be.true; + expect(isWeb3jsProvider(provider)).toBe(true); }); it("should return false if provider is not web3.js provider", () => { const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isWeb3jsProvider(provider)).to.be.false; + expect(isWeb3jsProvider(provider)).toBe(false); }); }); describe("isEthersProvider", () => { it("should return false if provider is not ethers provider", () => { const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isEthersProvider(provider)).to.be.false; + expect(isEthersProvider(provider)).toBe(false); }); it("should return true if provider is ethers provider", () => { const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isEthersProvider(provider)).to.be.true; + expect(isEthersProvider(provider)).toBe(true); }); }); }); diff --git a/packages/prover/test/unit/utils/conversion.test.ts b/packages/prover/test/unit/utils/conversion.test.ts index 50ed03a89450..ee9c16b9cb94 100644 --- a/packages/prover/test/unit/utils/conversion.test.ts +++ b/packages/prover/test/unit/utils/conversion.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {chunkIntoN} from "../../../src/utils/conversion.js"; describe("utils/conversion", () => { @@ -71,12 +71,12 @@ describe("utils/conversion", () => { for (const {title, input, output} of testCases) { it(`should chunkify data when ${title}`, async () => { - expect(chunkIntoN(input.data, input.n)).to.be.deep.eq(output); + expect(chunkIntoN(input.data, input.n)).toEqual(output); }); } it("should not change the order of elements", () => { - expect(chunkIntoN([6, 5, 4, 3, 2, 1], 2)).to.be.deep.eq([ + expect(chunkIntoN([6, 5, 4, 3, 2, 1], 2)).toEqual([ [6, 5], [4, 3], [2, 1], diff --git a/packages/prover/test/unit/utils/execution.test.ts b/packages/prover/test/unit/utils/execution.test.ts index dc2702e5f9d0..9219b5ec0c03 100644 --- a/packages/prover/test/unit/utils/execution.test.ts +++ b/packages/prover/test/unit/utils/execution.test.ts @@ -1,6 +1,4 @@ -import {expect} from "chai"; -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; +import {describe, it, expect} from "vitest"; import deepmerge from "deepmerge"; import {getEnvLogger} from "@lodestar/logger/env"; import {ELProof, ELStorageProof} from "../../../src/types.js"; @@ -16,8 +14,6 @@ const validStateRoot = hexToBuffer(eoaProof.beacon.executionPayload.state_root); const invalidAccountProof = deepmerge(validAccountProof, {}); delete invalidAccountProof.accountProof[0]; -chai.use(chaiAsPromised); - describe("uitls/execution", () => { const logger = getEnvLogger(); @@ -30,7 +26,7 @@ describe("uitls/execution", () => { stateRoot: validStateRoot, logger, }) - ).eventually.to.be.true; + ).resolves.toBe(true); }); it("should fail with error if proof is valid but address is wrong", async () => { @@ -48,7 +44,7 @@ describe("uitls/execution", () => { stateRoot, logger, }) - ).eventually.to.be.false; + ).resolves.toBe(false); }); it("should fail with error if account is not valid", async () => { @@ -62,7 +58,7 @@ describe("uitls/execution", () => { stateRoot, logger, }) - ).eventually.to.be.false; + ).resolves.toBe(false); }); }); @@ -76,7 +72,7 @@ describe("uitls/execution", () => { storageKeys, logger, }) - ).eventually.to.be.true; + ).resolves.toBe(true); }); it("should fail with error for a wrong proof", async () => { @@ -88,7 +84,7 @@ describe("uitls/execution", () => { proof: invalidStorageProof, storageKeys, }) - ).eventually.to.be.false; + ).resolves.toBe(false); }); it("should fail with error for a non existance key", async () => { @@ -110,7 +106,7 @@ describe("uitls/execution", () => { storageKeys, logger, }) - ).eventually.to.be.false; + ).resolves.toBe(false); }); it("should return true empty keys", async () => { @@ -126,7 +122,7 @@ describe("uitls/execution", () => { storageKeys, logger, }) - ).eventually.to.be.true; + ).resolves.toBe(true); }); }); }); diff --git a/packages/prover/test/unit/verified_requests/eth_call.test.ts b/packages/prover/test/unit/verified_requests/eth_call.test.ts index 89f45ee28fa7..e76b3fe4ed83 100644 --- a/packages/prover/test/unit/verified_requests/eth_call.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_call.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {ELTransaction} from "../../../lib/types.js"; @@ -28,7 +28,7 @@ describe("verified_requests / eth_call", () => { }, }); - expect(response).to.eql(testCase.response); + expect(response).toEqual(testCase.response); }); it("should return the json-rpc response with error for an invalid call", async () => { @@ -57,7 +57,7 @@ describe("verified_requests / eth_call", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_call")}, diff --git a/packages/prover/test/unit/verified_requests/eth_estimateGas.test.ts b/packages/prover/test/unit/verified_requests/eth_estimateGas.test.ts index 2c827bcb9445..27fbfb98a1b3 100644 --- a/packages/prover/test/unit/verified_requests/eth_estimateGas.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_estimateGas.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {ELTransaction} from "../../../lib/types.js"; @@ -29,7 +29,7 @@ describe("verified_requests / eth_estimateGas", () => { }, }); - expect(response).to.eql(testCase.response); + expect(response).toEqual(testCase.response); }); it("should return the json-rpc response with error for an invalid call", async () => { @@ -59,7 +59,7 @@ describe("verified_requests / eth_estimateGas", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_estimateGas")}, diff --git a/packages/prover/test/unit/verified_requests/eth_getBalance.test.ts b/packages/prover/test/unit/verified_requests/eth_getBalance.test.ts index 79f73a632fdc..ea0c902750c8 100644 --- a/packages/prover/test/unit/verified_requests/eth_getBalance.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_getBalance.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {VERIFICATION_FAILED_RESPONSE_CODE} from "../../../src/constants.js"; @@ -25,7 +25,7 @@ describe("verified_requests / eth_getBalance", () => { params: [data.request.params[0], data.request.params[1]], }, }); - expect(response).to.eql(data.response); + expect(response).toEqual(data.response); }); it("should return the json-rpc response with error for an invalid account", async () => { @@ -43,7 +43,7 @@ describe("verified_requests / eth_getBalance", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: data.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_getBalance")}, diff --git a/packages/prover/test/unit/verified_requests/eth_getBlockByHash.test.ts b/packages/prover/test/unit/verified_requests/eth_getBlockByHash.test.ts index 423dfc9b3d6c..8052b290ff0d 100644 --- a/packages/prover/test/unit/verified_requests/eth_getBlockByHash.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_getBlockByHash.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {VERIFICATION_FAILED_RESPONSE_CODE} from "../../../src/constants.js"; @@ -29,7 +29,7 @@ describe("verified_requests / eth_getBlockByHash", () => { params: testCase.request.params as [string, boolean], }, }); - expect(response).to.eql(testCase.response); + expect(response).toEqual(testCase.response); }); it("should return the json-rpc response with error for an invalid block header with valid execution payload", async () => { @@ -48,7 +48,7 @@ describe("verified_requests / eth_getBlockByHash", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_getBlockByHash")}, @@ -71,7 +71,7 @@ describe("verified_requests / eth_getBlockByHash", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_getBlockByHash")}, @@ -94,7 +94,7 @@ describe("verified_requests / eth_getBlockByHash", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_getBlockByHash")}, diff --git a/packages/prover/test/unit/verified_requests/eth_getBlockByNumber.test.ts b/packages/prover/test/unit/verified_requests/eth_getBlockByNumber.test.ts index 06d8d60af27c..fa60be225d20 100644 --- a/packages/prover/test/unit/verified_requests/eth_getBlockByNumber.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_getBlockByNumber.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {VERIFICATION_FAILED_RESPONSE_CODE} from "../../../src/constants.js"; @@ -29,7 +29,7 @@ describe("verified_requests / eth_getBlockByNumber", () => { params: testCase.request.params as [string | number, boolean], }, }); - expect(response).to.eql(testCase.response); + expect(response).toEqual(testCase.response); }); it("should return the json-rpc response with error for an invalid block header with valid execution payload", async () => { @@ -48,7 +48,7 @@ describe("verified_requests / eth_getBlockByNumber", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: { @@ -74,7 +74,7 @@ describe("verified_requests / eth_getBlockByNumber", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: { @@ -103,7 +103,7 @@ describe("verified_requests / eth_getBlockByNumber", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: { diff --git a/packages/prover/test/unit/verified_requests/eth_getCode.test.ts b/packages/prover/test/unit/verified_requests/eth_getCode.test.ts index 5fe6944904c4..51cf0c153857 100644 --- a/packages/prover/test/unit/verified_requests/eth_getCode.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_getCode.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {VERIFICATION_FAILED_RESPONSE_CODE} from "../../../src/constants.js"; @@ -25,7 +25,7 @@ describe("verified_requests / eth_getCode", () => { }, }); - expect(response).to.eql(testCase.response); + expect(response).toEqual(testCase.response); }); it("should return the json-rpc response with error for an invalid account", async () => { @@ -41,7 +41,7 @@ describe("verified_requests / eth_getCode", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: {code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_getCode")}, diff --git a/packages/prover/test/unit/verified_requests/eth_getTransactionCount.test.ts b/packages/prover/test/unit/verified_requests/eth_getTransactionCount.test.ts index 49477aac5747..8baf8fd7976b 100644 --- a/packages/prover/test/unit/verified_requests/eth_getTransactionCount.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_getTransactionCount.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {createForkConfig} from "@lodestar/config"; import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; import {VERIFICATION_FAILED_RESPONSE_CODE} from "../../../src/constants.js"; @@ -25,7 +25,7 @@ describe("verified_requests / eth_getTransactionCount", () => { }, }); - expect(response).to.eql(testCase.response); + expect(response).toEqual(testCase.response); }); it("should return the json-rpc response with error for an invalid account", async () => { @@ -43,7 +43,7 @@ describe("verified_requests / eth_getTransactionCount", () => { }, }); - expect(response).to.eql({ + expect(response).toEqual({ jsonrpc: "2.0", id: testCase.request.id, error: { diff --git a/packages/prover/test/unit/web3_provider.test.ts b/packages/prover/test/unit/web3_provider.node.test.ts similarity index 72% rename from packages/prover/test/unit/web3_provider.test.ts rename to packages/prover/test/unit/web3_provider.node.test.ts index e29188503b96..eed6c336483f 100644 --- a/packages/prover/test/unit/web3_provider.test.ts +++ b/packages/prover/test/unit/web3_provider.node.test.ts @@ -1,22 +1,19 @@ -import {expect} from "chai"; +import {describe, it, expect, afterEach, vi} from "vitest"; import Web3 from "web3"; import {ethers} from "ethers"; -import sinon from "sinon"; import {createVerifiedExecutionProvider, ProofProvider, LCTransport} from "@lodestar/prover/browser"; import {ELRpc} from "../../src/utils/rpc.js"; describe("web3_provider", () => { - const sandbox = sinon.createSandbox(); - afterEach(() => { - sandbox.restore(); + vi.clearAllMocks(); }); describe("createVerifiedExecutionProvider", () => { describe("web3", () => { it("should create a verified execution provider for the web3 provider", () => { // Don't invoke network in unit tests - sandbox.stub(ELRpc.prototype, "verifyCompatibility").resolves(); + vi.spyOn(ELRpc.prototype, "verifyCompatibility").mockResolvedValue(); const {provider, proofProvider} = createVerifiedExecutionProvider( new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"), @@ -27,15 +24,15 @@ describe("web3_provider", () => { } ); - expect(provider).be.instanceof(Web3.providers.HttpProvider); - expect(proofProvider).be.instanceOf(ProofProvider); + expect(provider).toBeInstanceOf(Web3.providers.HttpProvider); + expect(proofProvider).toBeInstanceOf(ProofProvider); }); }); describe("ethers", () => { it("should create a verified execution provider for the ethers provider", () => { // Don't invoke network in unit tests - sandbox.stub(ELRpc.prototype, "verifyCompatibility").resolves(); + vi.spyOn(ELRpc.prototype, "verifyCompatibility").mockResolvedValue(); const {provider, proofProvider} = createVerifiedExecutionProvider( new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"), @@ -46,8 +43,8 @@ describe("web3_provider", () => { } ); - expect(provider).be.instanceof(ethers.JsonRpcProvider); - expect(proofProvider).be.instanceOf(ProofProvider); + expect(provider).toBeInstanceOf(ethers.JsonRpcProvider); + expect(proofProvider).toBeInstanceOf(ProofProvider); }); }); }); diff --git a/packages/prover/vitest.browser.config.ts b/packages/prover/vitest.browser.config.ts new file mode 100644 index 000000000000..3c4b48885a33 --- /dev/null +++ b/packages/prover/vitest.browser.config.ts @@ -0,0 +1,14 @@ +import {defineConfig, mergeConfig} from "vitest/config"; +import vitestConfig from "../../vitest.base.browser.config"; + +export default mergeConfig( + vitestConfig, + defineConfig({ + test: { + globalSetup: ["./test/globalSetup.ts"], + }, + optimizeDeps: { + exclude: ["@chainsafe/blst"], + }, + }) +); diff --git a/packages/prover/vitest.config.ts b/packages/prover/vitest.config.ts new file mode 100644 index 000000000000..1df0de848936 --- /dev/null +++ b/packages/prover/vitest.config.ts @@ -0,0 +1,11 @@ +import {defineConfig, mergeConfig} from "vitest/config"; +import vitestConfig from "../../vitest.base.config"; + +export default mergeConfig( + vitestConfig, + defineConfig({ + test: { + globalSetup: ["./test/globalSetup.ts"], + }, + }) +); diff --git a/packages/prover/webpack.test.config.cjs b/packages/prover/webpack.test.config.cjs deleted file mode 100644 index 711c6ac891a7..000000000000 --- a/packages/prover/webpack.test.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -const webpackConfig = require("../../webpack.test.config.js"); - -module.exports = { - ...webpackConfig, -}; diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index 641bdb1a060e..793134dbe84f 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -56,9 +56,9 @@ "dependencies": { "@chainsafe/fast-crc32c": "^4.1.1", "@libp2p/interface": "^0.1.2", - "@lodestar/config": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/config": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/utils": "^1.13.0", "it-all": "^3.0.2", "it-pipe": "^3.0.1", "snappy": "^7.2.2", @@ -67,8 +67,8 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "@lodestar/logger": "^1.12.1", - "@lodestar/types": "^1.12.1", + "@lodestar/logger": "^1.13.0", + "@lodestar/types": "^1.13.0", "libp2p": "0.46.12" }, "peerDependencies": { diff --git a/packages/spec-test-util/package.json b/packages/spec-test-util/package.json index 6bd90a959a3f..5fc59cd76e12 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.12.1", + "version": "1.13.0", "description": "Spec test suite generator from yaml test files", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "blockchain" ], "dependencies": { - "@lodestar/utils": "^1.12.1", + "@lodestar/utils": "^1.13.0", "async-retry": "^1.3.3", "axios": "^1.3.4", "chai": "^4.3.7", diff --git a/packages/state-transition/.mocharc.yaml b/packages/state-transition/.mocharc.yaml deleted file mode 100644 index f28ebdf663a0..000000000000 --- a/packages/state-transition/.mocharc.yaml +++ /dev/null @@ -1,6 +0,0 @@ -colors: true -timeout: 5000 -exit: true -extension: ["ts"] -node-option: - - "loader=ts-node/esm" diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 4d6a0f9cfffe..f0f2f150f673 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -52,7 +52,7 @@ "check-types": "tsc", "lint": "eslint --color --ext .ts src/ test/", "lint:fix": "yarn run lint --fix", - "test:unit": "mocha 'test/unit/**/*.test.ts'", + "test:unit": "vitest --run --dir test/unit/ --coverage", "check-readme": "typescript-docs-verifier" }, "types": "lib/index.d.ts", @@ -63,10 +63,10 @@ "@chainsafe/persistent-merkle-tree": "^0.6.1", "@chainsafe/persistent-ts": "^0.19.1", "@chainsafe/ssz": "^0.14.0", - "@lodestar/config": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/config": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", "bigint-buffer": "^1.1.5", "buffer-xor": "^2.0.2" }, diff --git a/packages/state-transition/src/block/processAttestationsAltair.ts b/packages/state-transition/src/block/processAttestationsAltair.ts index cabe28b88b27..e37629712194 100644 --- a/packages/state-transition/src/block/processAttestationsAltair.ts +++ b/packages/state-transition/src/block/processAttestationsAltair.ts @@ -123,6 +123,7 @@ export function processAttestationsAltair( } increaseBalance(state, epochCtx.getBeaconProposer(state.slot), proposerReward); + state.proposerRewards.attestations = proposerReward; } /** diff --git a/packages/state-transition/src/block/processSyncCommittee.ts b/packages/state-transition/src/block/processSyncCommittee.ts index e0bfc318e052..70c918ed60bf 100644 --- a/packages/state-transition/src/block/processSyncCommittee.ts +++ b/packages/state-transition/src/block/processSyncCommittee.ts @@ -41,6 +41,7 @@ export function processSyncAggregate( } // Proposer reward proposerBalance += syncProposerReward; + state.proposerRewards.syncAggregate += syncProposerReward; } else { // Negative rewards for non participants if (index === proposerIndex) { diff --git a/packages/state-transition/src/block/slashValidator.ts b/packages/state-transition/src/block/slashValidator.ts index 7f4811a8f163..133041d36869 100644 --- a/packages/state-transition/src/block/slashValidator.ts +++ b/packages/state-transition/src/block/slashValidator.ts @@ -27,7 +27,7 @@ export function slashValidator( whistleblowerIndex?: ValidatorIndex ): void { const {epochCtx} = state; - const epoch = epochCtx.epoch; + const {epoch, effectiveBalanceIncrements} = epochCtx; const validator = state.validators.get(slashedIndex); // TODO: Bellatrix initiateValidatorExit validators.update() with the one below @@ -37,9 +37,15 @@ export function slashValidator( validator.withdrawableEpoch = Math.max(validator.withdrawableEpoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR); const {effectiveBalance} = validator; - // TODO: could state.slashings be number? + + // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: + // - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() + // - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet + // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + // - we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache const slashingIndex = epoch % EPOCHS_PER_SLASHINGS_VECTOR; - state.slashings.set(slashingIndex, state.slashings.get(slashingIndex) + BigInt(effectiveBalance)); + state.slashings.set(slashingIndex, (state.slashings.get(slashingIndex) ?? 0) + effectiveBalance); + epochCtx.totalSlashingsByIncrement += effectiveBalanceIncrements[slashedIndex]; const minSlashingPenaltyQuotient = fork === ForkSeq.phase0 @@ -60,9 +66,11 @@ export function slashValidator( if (whistleblowerIndex === undefined || !Number.isSafeInteger(whistleblowerIndex)) { // Call increaseBalance() once with `(whistleblowerReward - proposerReward) + proposerReward` increaseBalance(state, proposerIndex, whistleblowerReward); + state.proposerRewards.slashing += whistleblowerReward; } else { increaseBalance(state, proposerIndex, proposerReward); increaseBalance(state, whistleblowerIndex, whistleblowerReward - proposerReward); + state.proposerRewards.slashing += proposerReward; } // TODO: describe issue. Compute progressive target balances diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 0ca09526e7ec..8b63b0285098 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -29,6 +29,7 @@ import { import {computeEpochShuffling, EpochShuffling, getShufflingDecisionBlock} from "../util/epochShuffling.js"; import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; +import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js"; import {Index2PubkeyCache, PubkeyIndexMap, syncPubkeys} from "./pubkeyCache.js"; import {BeaconStateAllForks, BeaconStateAltair, ShufflingGetter} from "./types.js"; @@ -131,6 +132,10 @@ export class EpochCache { * Effective balances, for altair processAttestations() */ effectiveBalanceIncrements: EffectiveBalanceIncrements; + /** + * Total state.slashings by increment, for processSlashing() + */ + totalSlashingsByIncrement: number; syncParticipantReward: number; syncProposerReward: number; /** @@ -206,6 +211,7 @@ export class EpochCache { currentShuffling: EpochShuffling; nextShuffling: EpochShuffling; effectiveBalanceIncrements: EffectiveBalanceIncrements; + totalSlashingsByIncrement: number; syncParticipantReward: number; syncProposerReward: number; baseRewardPerIncrement: number; @@ -231,6 +237,7 @@ export class EpochCache { this.currentShuffling = data.currentShuffling; this.nextShuffling = data.nextShuffling; this.effectiveBalanceIncrements = data.effectiveBalanceIncrements; + this.totalSlashingsByIncrement = data.totalSlashingsByIncrement; this.syncParticipantReward = data.syncParticipantReward; this.syncProposerReward = data.syncProposerReward; this.baseRewardPerIncrement = data.baseRewardPerIncrement; @@ -277,6 +284,7 @@ export class EpochCache { const validatorCount = validators.length; const effectiveBalanceIncrements = getEffectiveBalanceIncrementsWithLen(validatorCount); + const totalSlashingsByIncrement = getTotalSlashingsByIncrement(state); const previousActiveIndices: ValidatorIndex[] = []; const currentActiveIndices: ValidatorIndex[] = []; const nextActiveIndices: ValidatorIndex[] = []; @@ -425,6 +433,7 @@ export class EpochCache { currentShuffling, nextShuffling, effectiveBalanceIncrements, + totalSlashingsByIncrement, syncParticipantReward, syncProposerReward, baseRewardPerIncrement, @@ -464,6 +473,7 @@ export class EpochCache { // Uint8Array, requires cloning, but it is cloned only when necessary before an epoch transition // See EpochCache.beforeEpochTransition() effectiveBalanceIncrements: this.effectiveBalanceIncrements, + totalSlashingsByIncrement: this.totalSlashingsByIncrement, // Basic types (numbers) cloned implicitly syncParticipantReward: this.syncParticipantReward, syncProposerReward: this.syncProposerReward, diff --git a/packages/state-transition/src/cache/rewardCache.ts b/packages/state-transition/src/cache/rewardCache.ts new file mode 100644 index 000000000000..669060d143cd --- /dev/null +++ b/packages/state-transition/src/cache/rewardCache.ts @@ -0,0 +1,18 @@ +/** + * A simple data structure to store rewards payable to block proposer in the memory. + * Rewards are updated throughout the state transition + * Should only hold info for one state transition + */ +export type RewardCache = { + attestations: number; + syncAggregate: number; + slashing: number; // Sum of attester and proposer slashing reward +}; + +export function createEmptyRewardCache(): RewardCache { + return { + attestations: 0, + syncAggregate: 0, + slashing: 0, + }; +} diff --git a/packages/state-transition/src/cache/stateCache.ts b/packages/state-transition/src/cache/stateCache.ts index 14a29b5f09c0..140e3d04c155 100644 --- a/packages/state-transition/src/cache/stateCache.ts +++ b/packages/state-transition/src/cache/stateCache.ts @@ -1,5 +1,5 @@ import bls from "@chainsafe/bls"; -import {CoordType} from "@chainsafe/blst"; +import {CoordType} from "@chainsafe/bls/types"; import {BeaconConfig} from "@lodestar/config"; import {loadState} from "../util/loadState/loadState.js"; import {EpochCache, EpochCacheImmutableData, EpochCacheOpts} from "./epochCache.js"; @@ -12,6 +12,7 @@ import { BeaconStateCapella, BeaconStateDeneb, } from "./types.js"; +import {RewardCache, createEmptyRewardCache} from "./rewardCache.js"; export type BeaconStateCache = { config: BeaconConfig; @@ -20,6 +21,7 @@ export type BeaconStateCache = { readonly clonedCount: number; readonly clonedCountWithTransferCache: number; readonly createdWithTransferCache: boolean; + proposerRewards: RewardCache; }; type Mutable = { @@ -147,6 +149,7 @@ export function createCachedBeaconState( clonedCount: 0, clonedCountWithTransferCache: 0, createdWithTransferCache: false, + proposerRewards: createEmptyRewardCache(), }); return cachedState; @@ -198,6 +201,7 @@ export function getCachedBeaconState( (cachedState as BeaconStateCacheMutable).clonedCount = cache.clonedCount; (cachedState as BeaconStateCacheMutable).clonedCountWithTransferCache = cache.clonedCountWithTransferCache; (cachedState as BeaconStateCacheMutable).createdWithTransferCache = cache.createdWithTransferCache; + cachedState.proposerRewards = cache.proposerRewards; // Overwrite .clone function to preserve cache // TreeViewDU.clone() creates a new object that does not have the attached cache @@ -219,6 +223,7 @@ export function getCachedBeaconState( clonedCount: 0, clonedCountWithTransferCache: 0, createdWithTransferCache: !dontTransferCache, + proposerRewards: createEmptyRewardCache(), // this sets the rewards to 0 while cloning new state }) as T & BeaconStateCache; } diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index 80b6f83f4b8b..05c8b55d0435 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -1,4 +1,10 @@ -import {ForkSeq} from "@lodestar/params"; +import { + ForkSeq, + MAX_ATTESTER_SLASHINGS, + MAX_EFFECTIVE_BALANCE, + MAX_VALIDATORS_PER_COMMITTEE, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; import { CachedBeaconStateAllForks, CachedBeaconStateCapella, @@ -6,6 +12,7 @@ import { CachedBeaconStatePhase0, EpochTransitionCache, } from "../types.js"; +import {BeaconStateTransitionMetrics} from "../metrics.js"; import {processEffectiveBalanceUpdates} from "./processEffectiveBalanceUpdates.js"; import {processEth1DataReset} from "./processEth1DataReset.js"; import {processHistoricalRootsUpdate} from "./processHistoricalRootsUpdate.js"; @@ -41,17 +48,70 @@ export { }; export {computeUnrealizedCheckpoints} from "./computeUnrealizedCheckpoints.js"; +const maxValidatorsPerStateSlashing = SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE; +const maxSafeValidators = Math.floor(Number.MAX_SAFE_INTEGER / MAX_EFFECTIVE_BALANCE); + +export function processEpoch( + fork: ForkSeq, + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache, + metrics?: BeaconStateTransitionMetrics | null +): void { + // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: + // - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() + // - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet + // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + if (maxValidatorsPerStateSlashing > maxSafeValidators) { + throw new Error("Lodestar does not support this network, parameters don't fit number value inside state.slashings"); + } + + { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: "processJustificationAndFinalization", + }); + processJustificationAndFinalization(state, cache); + timer?.(); + } -export function processEpoch(fork: ForkSeq, state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { - processJustificationAndFinalization(state, cache); if (fork >= ForkSeq.altair) { + const timer = metrics?.epochTransitionStepTime.startTimer({step: "processInactivityUpdates"}); processInactivityUpdates(state as CachedBeaconStateAltair, cache); + timer?.(); + } + + // processRewardsAndPenalties() is 2nd step in the specs, we optimize to do it + // after processSlashings() to update balances only once + // processRewardsAndPenalties(state, cache); + { + const timer = metrics?.epochTransitionStepTime.startTimer({step: "processRegistryUpdates"}); + processRegistryUpdates(state, cache); + timer?.(); + } + + // accumulate slashing penalties and only update balances once in processRewardsAndPenalties() + let slashingPenalties: number[]; + { + const timer = metrics?.epochTransitionStepTime.startTimer({step: "processSlashings"}); + slashingPenalties = processSlashings(state, cache, false); + timer?.(); + } + + { + const timer = metrics?.epochTransitionStepTime.startTimer({step: "processRewardsAndPenalties"}); + processRewardsAndPenalties(state, cache, slashingPenalties); + timer?.(); } - processRewardsAndPenalties(state, cache); - processRegistryUpdates(state, cache); - processSlashings(state, cache); + processEth1DataReset(state, cache); - processEffectiveBalanceUpdates(state, cache); + + { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: "processEffectiveBalanceUpdates", + }); + processEffectiveBalanceUpdates(state, cache); + timer?.(); + } + processSlashingsReset(state, cache); processRandaoMixesReset(state, cache); @@ -64,7 +124,20 @@ export function processEpoch(fork: ForkSeq, state: CachedBeaconStateAllForks, ca if (fork === ForkSeq.phase0) { processParticipationRecordUpdates(state as CachedBeaconStatePhase0); } else { - processParticipationFlagUpdates(state as CachedBeaconStateAltair); - processSyncCommitteeUpdates(state as CachedBeaconStateAltair); + { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: "processParticipationFlagUpdates", + }); + processParticipationFlagUpdates(state as CachedBeaconStateAltair); + timer?.(); + } + + { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: "processSyncCommitteeUpdates", + }); + processSyncCommitteeUpdates(state as CachedBeaconStateAltair); + timer?.(); + } } } diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index 20cf8597298b..61680b81002a 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -14,7 +14,11 @@ import {getRewardsAndPenaltiesAltair} from "./getRewardsAndPenalties.js"; * * PERF: Cost = 'proportional' to $VALIDATOR_COUNT. Extra work is done per validator the more status flags are set */ -export function processRewardsAndPenalties(state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { +export function processRewardsAndPenalties( + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache, + slashingPenalties: number[] = [] +): void { // No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch if (cache.currentEpoch === GENESIS_EPOCH) { return; @@ -24,7 +28,7 @@ export function processRewardsAndPenalties(state: CachedBeaconStateAllForks, cac const balances = state.balances.getAll(); for (let i = 0, len = rewards.length; i < len; i++) { - balances[i] += rewards[i] - penalties[i]; + balances[i] += rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0); } // important: do not change state one balance at a time. Set them all at once, constructing the tree in one go diff --git a/packages/state-transition/src/epoch/processSlashings.ts b/packages/state-transition/src/epoch/processSlashings.ts index 456f1ebc31fa..7f4403dc027a 100644 --- a/packages/state-transition/src/epoch/processSlashings.ts +++ b/packages/state-transition/src/epoch/processSlashings.ts @@ -1,4 +1,3 @@ -import {bigIntMin} from "@lodestar/utils"; import { EFFECTIVE_BALANCE_INCREMENT, ForkSeq, @@ -8,33 +7,35 @@ import { } from "@lodestar/params"; import {decreaseBalance} from "../util/index.js"; -import {CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; +import {BeaconStateAllForks, CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; /** * Update validator registry for validators that activate + exit + * updateBalance is an optimization: + * - For spec test, it's true + * - For processEpoch flow, it's false, i.e to only update balances once in processRewardsAndPenalties() * - * PERF: Cost 'proportional' to only validators that are slashed. For mainnet conditions: + * PERF: almost no (constant) cost. + * - Total slashings by increment is computed once and stored in state.epochCtx.totalSlashingsByIncrement so no need to compute here + * - Penalties for validators with the same effective balance are the same and computed once + * - No need to apply penalties to validators here, do it once in processRewardsAndPenalties() * - indicesToSlash: max len is 8704. But it's very unlikely since it would require all validators on the same * committees to sign slashable attestations. - * * - On normal mainnet conditions indicesToSlash = 0 + * + * @returns slashing penalties to be applied in processRewardsAndPenalties() */ -export function processSlashings(state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { - // No need to compute totalSlashings if there no index to slash +export function processSlashings( + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache, + updateBalance = true +): number[] { + // Return early if there no index to slash if (cache.indicesToSlash.length === 0) { - return; - } - // TODO: have the regular totalBalance in EpochTransitionCache too? - const totalBalance = BigInt(cache.totalActiveStakeByIncrement) * BigInt(EFFECTIVE_BALANCE_INCREMENT); - - // TODO: Could totalSlashings be number? - // TODO: Could totalSlashing be cached? - let totalSlashings = BigInt(0); - const slashings = state.slashings.getAll(); - for (let i = 0; i < slashings.length; i++) { - totalSlashings += slashings[i]; + return []; } + const totalBalanceByIncrement = cache.totalActiveStakeByIncrement; const fork = state.config.getForkSeq(state.slot); const proportionalSlashingMultiplier = fork === ForkSeq.phase0 @@ -44,12 +45,46 @@ export function processSlashings(state: CachedBeaconStateAllForks, cache: EpochT : PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX; const {effectiveBalanceIncrements} = state.epochCtx; - const adjustedTotalSlashingBalance = bigIntMin(totalSlashings * BigInt(proportionalSlashingMultiplier), totalBalance); + const adjustedTotalSlashingBalanceByIncrement = Math.min( + state.epochCtx.totalSlashingsByIncrement * proportionalSlashingMultiplier, + totalBalanceByIncrement + ); const increment = EFFECTIVE_BALANCE_INCREMENT; + const penalties: number[] = []; + + const penaltiesByEffectiveBalanceIncrement = new Map(); for (const index of cache.indicesToSlash) { const effectiveBalanceIncrement = effectiveBalanceIncrements[index]; - const penaltyNumerator = BigInt(effectiveBalanceIncrement) * adjustedTotalSlashingBalance; - const penalty = Number(penaltyNumerator / totalBalance) * increment; - decreaseBalance(state, index, penalty); + let penalty = penaltiesByEffectiveBalanceIncrement.get(effectiveBalanceIncrement); + if (penalty === undefined) { + const penaltyNumeratorByIncrement = effectiveBalanceIncrement * adjustedTotalSlashingBalanceByIncrement; + penalty = Math.floor(penaltyNumeratorByIncrement / totalBalanceByIncrement) * increment; + penaltiesByEffectiveBalanceIncrement.set(effectiveBalanceIncrement, penalty); + } + + if (updateBalance) { + // for spec test only + decreaseBalance(state, index, penalty); + } else { + // do it later in processRewardsAndPenalties() + penalties[index] = penalty; + } + } + + return penalties; +} + +/** + * Get total slashings by increment. + * By default, total slashings are computed every time we run processSlashings() function above. + * We improve it by computing it once and store it in state.epochCtx.totalSlashingsByIncrement + * Every change to state.slashings should update totalSlashingsByIncrement. + */ +export function getTotalSlashingsByIncrement(state: BeaconStateAllForks): number { + let totalSlashingsByIncrement = 0; + const slashings = state.slashings.getAll(); + for (let i = 0; i < slashings.length; i++) { + totalSlashingsByIncrement += Math.floor(slashings[i] / EFFECTIVE_BALANCE_INCREMENT); } + return totalSlashingsByIncrement; } diff --git a/packages/state-transition/src/epoch/processSlashingsReset.ts b/packages/state-transition/src/epoch/processSlashingsReset.ts index 38fd43ffa6a8..6ab22d47526c 100644 --- a/packages/state-transition/src/epoch/processSlashingsReset.ts +++ b/packages/state-transition/src/epoch/processSlashingsReset.ts @@ -1,4 +1,4 @@ -import {EPOCHS_PER_SLASHINGS_VECTOR} from "@lodestar/params"; +import {EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_SLASHINGS_VECTOR} from "@lodestar/params"; import {EpochTransitionCache, CachedBeaconStateAllForks} from "../types.js"; /** @@ -10,5 +10,11 @@ export function processSlashingsReset(state: CachedBeaconStateAllForks, cache: E const nextEpoch = cache.currentEpoch + 1; // reset slashings - state.slashings.set(nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR, BigInt(0)); + const slashIndex = nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR; + const oldSlashingValueByIncrement = Math.floor(state.slashings.get(slashIndex) / EFFECTIVE_BALANCE_INCREMENT); + state.slashings.set(slashIndex, 0); + state.epochCtx.totalSlashingsByIncrement = Math.max( + 0, + state.epochCtx.totalSlashingsByIncrement - oldSlashingValueByIncrement + ); } diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index c5179a1df5b3..681bb2b910cf 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -5,6 +5,7 @@ import {AttesterStatus} from "./util/attesterStatus.js"; export type BeaconStateTransitionMetrics = { epochTransitionTime: Histogram; epochTransitionCommitTime: Histogram; + epochTransitionStepTime: Histogram<"step">; processBlockTime: Histogram; processBlockCommitTime: Histogram; stateHashTreeRootTime: Histogram; @@ -23,7 +24,7 @@ export type BeaconStateTransitionMetrics = { type LabelValues = Partial>; interface Histogram { - startTimer(): () => void; + startTimer(labels?: LabelValues): (labels?: LabelValues) => number; observe(value: number): void; observe(labels: LabelValues, values: number): void; diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 8fd98f4df03e..cdb8878c87fa 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -2,7 +2,7 @@ import {toHexString} from "@chainsafe/ssz"; import {allForks, Slot, ssz} from "@lodestar/types"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconStateTransitionMetrics, onPostStateMetrics, onStateCloneMetrics} from "./metrics.js"; -import {beforeProcessEpoch, EpochTransitionCacheOpts} from "./cache/epochTransitionCache.js"; +import {beforeProcessEpoch, EpochTransitionCache, EpochTransitionCacheOpts} from "./cache/epochTransitionCache.js"; import { CachedBeaconStateAllForks, CachedBeaconStatePhase0, @@ -165,19 +165,33 @@ function processSlotsWithTransientCache( const epochTransitionTimer = metrics?.epochTransitionTime.startTimer(); - const epochTransitionCache = beforeProcessEpoch(postState, epochTransitionCacheOpts); - processEpoch(fork, postState, epochTransitionCache); + let epochTransitionCache: EpochTransitionCache; + { + const timer = metrics?.epochTransitionStepTime.startTimer({step: "beforeProcessEpoch"}); + epochTransitionCache = beforeProcessEpoch(postState, epochTransitionCacheOpts); + timer?.(); + } + + processEpoch(fork, postState, epochTransitionCache, metrics); + const {currentEpoch, statuses, balances} = epochTransitionCache; metrics?.registerValidatorStatuses(currentEpoch, statuses, balances); postState.slot++; - postState.epochCtx.afterProcessEpoch(postState, epochTransitionCache); + + { + const timer = metrics?.epochTransitionStepTime.startTimer({step: "afterProcessEpoch"}); + postState.epochCtx.afterProcessEpoch(postState, epochTransitionCache); + timer?.(); + } // Running commit here is not strictly necessary. The cost of running commit twice (here + after process block) // Should be negligible but gives better metrics to differentiate the cost of it for block and epoch proc. - const epochTransitionCommitTimer = metrics?.epochTransitionCommitTime.startTimer(); - postState.commit(); - epochTransitionCommitTimer?.(); + { + const timer = metrics?.epochTransitionCommitTime.startTimer(); + postState.commit(); + timer?.(); + } // Note: time only on success. Include beforeProcessEpoch, processEpoch, afterProcessEpoch, commit epochTransitionTimer?.(); diff --git a/packages/state-transition/test/globalSetup.ts b/packages/state-transition/test/globalSetup.ts new file mode 100644 index 000000000000..0ab57c057472 --- /dev/null +++ b/packages/state-transition/test/globalSetup.ts @@ -0,0 +1,2 @@ +export async function setup(): Promise {} +export async function teardown(): Promise {} diff --git a/packages/state-transition/test/perf/epoch/epochAltair.test.ts b/packages/state-transition/test/perf/epoch/epochAltair.test.ts index 414cd05164c2..273353d8632b 100644 --- a/packages/state-transition/test/perf/epoch/epochAltair.test.ts +++ b/packages/state-transition/test/perf/epoch/epochAltair.test.ts @@ -6,7 +6,8 @@ import { CachedBeaconStateAltair, beforeProcessEpoch, } from "../../../src/index.js"; -import {getNetworkCachedState, beforeValue, LazyValue} from "../../utils/index.js"; +import {beforeValue, LazyValue} from "../../utils/beforeValueMocha.js"; +import {getNetworkCachedState} from "../../utils/testFileCache.js"; import {StateEpoch} from "../types.js"; import {altairState} from "../params.js"; import {processJustificationAndFinalization} from "../../../src/epoch/processJustificationAndFinalization.js"; @@ -126,7 +127,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - altair processSlashings`, beforeEach: () => stateOg.value.clone() as CachedBeaconStateAltair, - fn: (state) => processSlashings(state, cache.value), + fn: (state) => { + processSlashings(state, cache.value, false); + }, }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochCapella.test.ts b/packages/state-transition/test/perf/epoch/epochCapella.test.ts index 86620fa2dfcf..eeaf8bfc5400 100644 --- a/packages/state-transition/test/perf/epoch/epochCapella.test.ts +++ b/packages/state-transition/test/perf/epoch/epochCapella.test.ts @@ -7,7 +7,8 @@ import { CachedBeaconStateAltair, beforeProcessEpoch, } from "../../../src/index.js"; -import {getNetworkCachedState, beforeValue, LazyValue} from "../../utils/index.js"; +import {beforeValue, LazyValue} from "../../utils/beforeValueMocha.js"; +import {getNetworkCachedState} from "../../utils/testFileCache.js"; import {StateEpoch} from "../types.js"; import {capellaState} from "../params.js"; import {processJustificationAndFinalization} from "../../../src/epoch/processJustificationAndFinalization.js"; @@ -105,7 +106,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - capella processSlashings`, beforeEach: () => stateOg.value.clone() as CachedBeaconStateCapella, - fn: (state) => processSlashings(state, cache.value), + fn: (state) => { + processSlashings(state, cache.value, false); + }, }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts index fa5538a7a66a..4e43634b1669 100644 --- a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts +++ b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts @@ -6,7 +6,8 @@ import { CachedBeaconStatePhase0, beforeProcessEpoch, } from "../../../src/index.js"; -import {getNetworkCachedState, beforeValue, LazyValue} from "../../utils/index.js"; +import {beforeValue, LazyValue} from "../../utils/beforeValueMocha.js"; +import {getNetworkCachedState} from "../../utils/testFileCache.js"; import {StateEpoch} from "../types.js"; import {phase0State} from "../params.js"; import {processEpoch} from "../../../src/epoch/index.js"; @@ -108,7 +109,9 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - phase0 processSlashings`, beforeEach: () => stateOg.value.clone() as CachedBeaconStatePhase0, - fn: (state) => processSlashings(state, cache.value), + fn: (state) => { + processSlashings(state, cache.value, false); + }, }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts b/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts index e3701985dac8..3b0bfa623fb2 100644 --- a/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts +++ b/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts @@ -1,4 +1,5 @@ import {itBench} from "@dapplion/benchmark"; +import {MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; import { beforeProcessEpoch, CachedBeaconStatePhase0, @@ -34,7 +35,9 @@ describe("phase0 processSlashings", () => { minRuns: 5, // Worst case is very slow before: () => getProcessSlashingsTestData(indicesToSlashLen), beforeEach: ({state, cache}) => ({state: state.clone(), cache}), - fn: ({state, cache}) => processSlashings(state as CachedBeaconStatePhase0, cache), + fn: ({state, cache}) => { + processSlashings(state as CachedBeaconStatePhase0, cache, false); + }, }); } }); @@ -48,6 +51,11 @@ function getProcessSlashingsTestData(indicesToSlashLen: number): { } { const state = generatePerfTestCachedStatePhase0({goBackOneSlot: true}); const cache = beforeProcessEpoch(state); + state.slashings.set(0, indicesToSlashLen * MAX_EFFECTIVE_BALANCE); + for (let i = 1; i < state.slashings.length; i++) { + state.slashings.set(i, MAX_EFFECTIVE_BALANCE); + } + state.commit(); cache.indicesToSlash = linspace(indicesToSlashLen); diff --git a/packages/state-transition/test/perf/util.ts b/packages/state-transition/test/perf/util.ts index 46faf11c50f1..4df6746ea938 100644 --- a/packages/state-transition/test/perf/util.ts +++ b/packages/state-transition/test/perf/util.ts @@ -211,6 +211,10 @@ export function cachedStateAltairPopulateCaches(state: CachedBeaconStateAltair): state.inactivityScores.getAll(); } +/** + * Warning: This function has side effects on the cached state + * The order in which the caches are populated is important and can cause stable tests to fail. + */ export function generatePerfTestCachedStateAltair(opts?: { goBackOneSlot: boolean; vc?: number; diff --git a/packages/state-transition/test/perf/util/loadState/loadState.test.ts b/packages/state-transition/test/perf/util/loadState/loadState.test.ts index c0df6cf1af47..5d40c64f6ab4 100644 --- a/packages/state-transition/test/perf/util/loadState/loadState.test.ts +++ b/packages/state-transition/test/perf/util/loadState/loadState.test.ts @@ -1,5 +1,5 @@ import bls from "@chainsafe/bls"; -import {CoordType} from "@chainsafe/blst"; +import {CoordType} from "@chainsafe/bls/types"; import {itBench, setBenchOpts} from "@dapplion/benchmark"; import {loadState} from "../../../../src/util/loadState/loadState.js"; import {createCachedBeaconState} from "../../../../src/cache/stateCache.js"; diff --git a/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts b/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts index c526ab13c17c..c219943b940f 100644 --- a/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts +++ b/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {config} from "@lodestar/config/default"; import {FAR_FUTURE_EPOCH, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; @@ -35,18 +35,16 @@ describe("validate indexed attestation", () => { }, ]; - for (const testValue of testValues) { - it(testValue.name, function () { - const attestationData = ssz.phase0.AttestationData.defaultValue(); - attestationData.source.epoch = 0; - attestationData.target.epoch = 1; + it.each(testValues)("$name", ({indices, expectedValue}) => { + const attestationData = ssz.phase0.AttestationData.defaultValue(); + attestationData.source.epoch = 0; + attestationData.target.epoch = 1; - const indexedAttestation: phase0.IndexedAttestation = { - attestingIndices: testValue.indices, - data: attestationData, - signature: EMPTY_SIGNATURE, - }; - expect(isValidIndexedAttestation(state, indexedAttestation, false)).to.be.equal(testValue.expectedValue); - }); - } + const indexedAttestation: phase0.IndexedAttestation = { + attestingIndices: indices, + data: attestationData, + signature: EMPTY_SIGNATURE, + }; + expect(isValidIndexedAttestation(state, indexedAttestation, false)).toBe(expectedValue); + }); }); diff --git a/packages/state-transition/test/unit/block/processWithdrawals.test.ts b/packages/state-transition/test/unit/block/processWithdrawals.test.ts index 628aae9496da..2841da635472 100644 --- a/packages/state-transition/test/unit/block/processWithdrawals.test.ts +++ b/packages/state-transition/test/unit/block/processWithdrawals.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {getExpectedWithdrawals} from "../../../src/block/processWithdrawals.js"; import {numValidators} from "../../perf/util.js"; import {getExpectedWithdrawalsTestData, WithdrawalOpts} from "../../utils/capella.js"; @@ -38,8 +38,8 @@ describe("getExpectedWithdrawals", () => { it(`getExpectedWithdrawals ${vc} ${caseID}`, () => { const {sampledValidators, withdrawals} = getExpectedWithdrawals(state.value); - expect(sampledValidators).equals(opts.sampled, "Wrong sampledValidators"); - expect(withdrawals.length).equals(opts.withdrawals, "Wrong withdrawals"); + expect(sampledValidators).toBe(opts.sampled); + expect(withdrawals.length).toBe(opts.withdrawals); }); } }); diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index 072261c1000e..f3089f39d913 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {ssz} from "@lodestar/types"; import {toHexString} from "@lodestar/utils"; import {config} from "@lodestar/config/default"; @@ -16,18 +16,15 @@ describe("CachedBeaconState", () => { const state2 = state1.clone(); state1.slot = 1; - expect(state2.slot).to.equal(0, "state2.slot was mutated"); + expect(state2.slot).toBe(0); const prevRoot = state2.currentJustifiedCheckpoint.root; const newRoot = Buffer.alloc(32, 1); state1.currentJustifiedCheckpoint.root = newRoot; - expect(toHexString(state2.currentJustifiedCheckpoint.root)).to.equal( - toHexString(prevRoot), - "state2.currentJustifiedCheckpoint.root was mutated" - ); + expect(toHexString(state2.currentJustifiedCheckpoint.root)).toBe(toHexString(prevRoot)); state1.epochCtx.epoch = 1; - expect(state2.epochCtx.epoch).to.equal(0, "state2.epochCtx.epoch was mutated"); + expect(state2.epochCtx.epoch).toBe(0); }); it("Auto-commit on hashTreeRoot", () => { @@ -40,10 +37,7 @@ describe("CachedBeaconState", () => { // Only commit state1 beforehand cp1.commit(); - expect(toHexString(cp1.hashTreeRoot())).to.equal( - toHexString(cp2.hashTreeRoot()), - ".hashTreeRoot() does not automatically commit" - ); + expect(toHexString(cp1.hashTreeRoot())).toBe(toHexString(cp2.hashTreeRoot())); }); it("Auto-commit on serialize", () => { @@ -55,10 +49,7 @@ describe("CachedBeaconState", () => { // Only commit state1 beforehand cp1.commit(); - expect(toHexString(cp1.serialize())).to.equal( - toHexString(cp2.serialize()), - ".serialize() does not automatically commit" - ); + expect(toHexString(cp1.serialize())).toBe(toHexString(cp2.serialize())); }); describe("loadCachedBeaconState", () => { @@ -138,16 +129,13 @@ describe("CachedBeaconState", () => { const stateBytes = state.serialize(); const newCachedState = loadUnfinalizedCachedBeaconState(seedState, stateBytes, {skipSyncCommitteeCache: true}); const newStateBytes = newCachedState.serialize(); - expect(newStateBytes).to.be.deep.equal(stateBytes, "loadState: state bytes are not equal"); - expect(newCachedState.hashTreeRoot()).to.be.deep.equal( - state.hashTreeRoot(), - "loadState: state root is not equal" - ); + expect(newStateBytes).toEqual(stateBytes); + expect(newCachedState.hashTreeRoot()).toEqual(state.hashTreeRoot()); // confirm loadUnfinalizedCachedBeaconState() result for (let i = 0; i < newCachedState.validators.length; i++) { - expect(newCachedState.epochCtx.pubkey2index.get(newCachedState.validators.get(i).pubkey)).to.be.equal(i); - expect(newCachedState.epochCtx.index2pubkey[i].toBytes()).to.be.deep.equal(pubkeys[i]); + expect(newCachedState.epochCtx.pubkey2index.get(newCachedState.validators.get(i).pubkey)).toBe(i); + expect(newCachedState.epochCtx.index2pubkey[i].toBytes()).toEqual(pubkeys[i]); } }); } diff --git a/packages/state-transition/test/unit/constants.test.ts b/packages/state-transition/test/unit/constants.test.ts index d4975a8a86f5..5b8cc66da73a 100644 --- a/packages/state-transition/test/unit/constants.test.ts +++ b/packages/state-transition/test/unit/constants.test.ts @@ -1,10 +1,10 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import * as blst from "@chainsafe/blst"; import {G2_POINT_AT_INFINITY} from "../../src/index.js"; describe("constants", () => { it("G2_POINT_AT_INFINITY", () => { const p2 = blst.Signature.fromBytes(G2_POINT_AT_INFINITY); - expect(p2.value.is_inf()).to.equal(true, "is not infinity"); + expect(p2.value.is_inf()).toBe(true); }); }); diff --git a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts index 9e084dc783a3..1a5b15e1b041 100644 --- a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts +++ b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import bls from "@chainsafe/bls"; import {BitArray} from "@chainsafe/ssz"; import {config} from "@lodestar/config/default"; @@ -67,7 +67,7 @@ describe("signatureSets", () => { const state = generateCachedState(config, {validators}); const signatureSets = getBlockSignatureSets(state, signedBlock); - expect(signatureSets.length).to.equal( + expect(signatureSets.length).toBe( // block signature 1 + // randao reveal diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index ba9ff187a26c..2ea8eef182ac 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {expect, describe, it} from "vitest"; import {ssz} from "@lodestar/types"; import {ForkName} from "@lodestar/params"; import {createBeaconConfig, ChainForkConfig, createChainForkConfig} from "@lodestar/config"; @@ -22,7 +22,7 @@ describe("upgradeState", () => { {skipSyncCommitteeCache: true} ); const newState = upgradeStateToDeneb(stateView); - expect(() => newState.toValue()).to.not.throw(); + expect(() => newState.toValue()).not.toThrow(); }); }); diff --git a/packages/state-transition/test/unit/util/aggregator.test.ts b/packages/state-transition/test/unit/util/aggregator.test.ts index f6e7bbd4a6ed..07fd3172926c 100644 --- a/packages/state-transition/test/unit/util/aggregator.test.ts +++ b/packages/state-transition/test/unit/util/aggregator.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect, beforeAll} from "vitest"; import {fromHexString} from "@chainsafe/ssz"; import { SYNC_COMMITTEE_SIZE, @@ -13,10 +13,10 @@ import {isAggregatorFromCommitteeLength, isSyncCommitteeAggregator} from "../../ describe("isAttestationAggregator", function () { const committeeLength = 130; - before("Ensure constants don't change", () => { + beforeAll(() => { expect({ TARGET_AGGREGATORS_PER_COMMITTEE, - }).to.deep.equal({ + }).toEqual({ TARGET_AGGREGATORS_PER_COMMITTEE: 16, }); }); @@ -28,8 +28,9 @@ describe("isAttestationAggregator", function () { "0x8191d16330837620f0ed85d0d3d52af5b56f7cec12658fa391814251d4b32977eb2e6ca055367354fd63175f8d1d2d7b0678c3c482b738f96a0df40bd06450d99c301a659b8396c227ed781abb37a1604297922219374772ab36b46b84817036" ) ); - expect(result).to.be.equal(false); + expect(result).toBe(false); }); + it("should be true", function () { const result = isAggregatorFromCommitteeLength( committeeLength, @@ -37,17 +38,17 @@ describe("isAttestationAggregator", function () { "0xa8f8bb92931234ca6d8a34530526bcd6a4cfa3bf33bd0470200dc8fa3ebdc3ba24bc8c6e994d58a0f884eb24336d746c01a29693ed0354c0862c2d5de5859e3f58747045182844d267ba232058f7df1867a406f63a1eb8afec0cf3f00a115125" ) ); - expect(result).to.be.equal(true); + expect(result).toBe(true); }); }); describe("isSyncCommitteeAggregator", function () { - before("Ensure constants don't change", () => { + beforeAll(() => { expect({ SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_COUNT, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, - }).to.deep.equal({ + }).toEqual({ SYNC_COMMITTEE_SIZE: 512, SYNC_COMMITTEE_SUBNET_COUNT: 4, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16, @@ -60,7 +61,7 @@ describe("isSyncCommitteeAggregator", function () { "0x8191d16330837620f0ed85d0d3d52af5b56f7cec12658fa391814251d4b32977eb2e6ca055367354fd63175f8d1d2d7b0678c3c482b738f96a0df40bd06450d99c301a659b8396c227ed781abb37a1604297922219374772ab36b46b84817036" ) ); - expect(result).to.be.equal(false); + expect(result).toBe(false); }); // NOTE: Invalid sig, bruteforced last characters to get a true result @@ -70,6 +71,6 @@ describe("isSyncCommitteeAggregator", function () { "0xa8f8bb92931234ca6d8a34530526bcd6a4cfa3bf33bd0470200dc8fa3ebdc3ba24bc8c6e994d58a0f884eb24336d746c01a29693ed0354c0862c2d5de5859e3f58747045182844d267ba232058f7df1867a406f63a1eb8afec0cf3f00a115142" ) ); - expect(result).to.be.equal(true); + expect(result).toBe(true); }); }); diff --git a/packages/state-transition/test/unit/util/balance.test.ts b/packages/state-transition/test/unit/util/balance.test.ts index dedb4023c5ca..5b666cb0524e 100644 --- a/packages/state-transition/test/unit/util/balance.test.ts +++ b/packages/state-transition/test/unit/util/balance.test.ts @@ -1,4 +1,4 @@ -import {assert, expect} from "chai"; +import {describe, it, expect} from "vitest"; import {config as minimalConfig} from "@lodestar/config/default"; import {EFFECTIVE_BALANCE_INCREMENT} from "@lodestar/params"; @@ -23,7 +23,7 @@ describe("getTotalBalance", () => { const result = getTotalBalance(state, validatorIndices); const expected = BigInt(num * validatorBalance); - assert(result === expected, `Expected: ${expected} :: Result: ${result}`); + expect(result).toEqual(expected); }); it("should return correct balances - 5 validators", () => { @@ -34,8 +34,8 @@ describe("getTotalBalance", () => { const validatorIndices: ValidatorIndex[] = Array.from({length: num}, (_, i) => i); const result = getTotalBalance(state, validatorIndices); - const expected = EFFECTIVE_BALANCE_INCREMENT; - assert(result === BigInt(expected), `Expected: ${expected} :: Result: ${result}`); + const expected = BigInt(EFFECTIVE_BALANCE_INCREMENT); + expect(result).toEqual(expected); }); }); @@ -43,12 +43,12 @@ describe("increaseBalance", () => { it("should add to a validators balance", () => { const state = generateCachedState(); state.balances.push(0); - expect(state.balances.get(0)).to.be.equal(0); + expect(state.balances.get(0)).toBe(0); const delta = 5; for (let i = 1; i < 10; i++) { increaseBalance(state, 0, delta); - expect(state.balances.get(0)).to.be.equal(delta * i); + expect(state.balances.get(0)).toBe(delta * i); } }); }); @@ -62,7 +62,7 @@ describe("decreaseBalance", () => { const delta = 5; for (let i = 1; i < 10; i++) { decreaseBalance(state, 0, delta); - expect(state.balances.get(0)).to.be.equal(initial - delta * i); + expect(state.balances.get(0)).toBe(initial - delta * i); } }); @@ -72,7 +72,7 @@ describe("decreaseBalance", () => { state.balances.push(initial); const delta = 11; decreaseBalance(state, 0, delta); - expect(state.balances.get(0)).to.be.equal(0); + expect(state.balances.get(0)).toBe(0); }); }); @@ -99,9 +99,6 @@ describe("getEffectiveBalanceIncrementsZeroInactive", () => { : 0; } - expect(getEffectiveBalanceIncrementsZeroInactive(justifiedState)).to.be.deep.equal( - effectiveBalances, - "wrong effectiveBalances" - ); + expect(getEffectiveBalanceIncrementsZeroInactive(justifiedState)).toEqual(effectiveBalances); }); }); diff --git a/packages/state-transition/test/unit/util/cachedBeaconState.test.ts b/packages/state-transition/test/unit/util/cachedBeaconState.test.ts index 1a34e2472b50..654e0752adb8 100644 --- a/packages/state-transition/test/unit/util/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/util/cachedBeaconState.test.ts @@ -1,3 +1,4 @@ +import {describe, it} from "vitest"; import {createBeaconConfig} from "@lodestar/config"; import {config} from "@lodestar/config/default"; import {ssz} from "@lodestar/types"; diff --git a/packages/state-transition/test/unit/util/epoch.test.ts b/packages/state-transition/test/unit/util/epoch.test.ts index fe404f93f852..e86a41875e1d 100644 --- a/packages/state-transition/test/unit/util/epoch.test.ts +++ b/packages/state-transition/test/unit/util/epoch.test.ts @@ -1,4 +1,4 @@ -import {assert} from "chai"; +import {describe, it, expect} from "vitest"; import {GENESIS_SLOT, MAX_SEED_LOOKAHEAD} from "@lodestar/params"; import {Epoch, Slot} from "@lodestar/types"; @@ -12,7 +12,7 @@ import { import {generateState} from "../../utils/state.js"; describe("computeEpochAtSlot", () => { - const pairs = [ + it.each([ {test: 0, expected: 0}, {test: 1, expected: 0}, {test: 10, expected: 0}, @@ -21,17 +21,14 @@ describe("computeEpochAtSlot", () => { {test: 10000, expected: 312}, {test: 100000, expected: 3125}, {test: 1000000, expected: 31250}, - ]; - for (const pair of pairs) { - it(`Slot ${pair.test} should map to epoch ${pair.expected}`, () => { - const result: Epoch = computeEpochAtSlot(pair.test); - assert.equal(result, pair.expected); - }); - } + ])("Slot $test should map to epoch $expected", ({test, expected}) => { + const result: Epoch = computeEpochAtSlot(test); + expect(result).toEqual(expected); + }); }); describe("computeStartSlotAtEpoch", () => { - const pairs = [ + it.each([ {test: 0, expected: 0}, {test: 1, expected: 32}, {test: 10, expected: 320}, @@ -40,38 +37,31 @@ describe("computeStartSlotAtEpoch", () => { {test: 10000, expected: 320000}, {test: 100000, expected: 3200000}, {test: 1000000, expected: 32000000}, - ]; - for (const pair of pairs) { - it(`Epoch ${pair.test} should map to slot ${pair.expected}`, () => { - const result: Slot = computeStartSlotAtEpoch(pair.test); - assert.equal(result, pair.expected); - }); - } + ])("Epoch $test should map to slot $expected", ({test, expected}) => { + const result: Slot = computeStartSlotAtEpoch(test); + expect(result).toEqual(expected); + }); }); describe("getPreviousEpoch", () => { - const testValues = [ + it.each([ {slot: 512, expectedEpoch: 15}, {slot: 256, expectedEpoch: 7}, { slot: GENESIS_SLOT, expectedEpoch: computeEpochAtSlot(GENESIS_SLOT), }, - ]; - - for (const testValue of testValues) { - it("epoch should return previous epoch", () => { - const state = generateState({slot: testValue.slot}); - const result = getPreviousEpoch(state); - assert.equal(result, testValue.expectedEpoch); - }); - } + ])("epoch should return previous epoch", ({slot, expectedEpoch}) => { + const state = generateState({slot}); + const result = getPreviousEpoch(state); + expect(result).toEqual(expectedEpoch); + }); }); describe("computeActivationExitEpoch", () => { it("epoch is always equal to the epoch after the exit delay", () => { for (let e: Epoch = 0; e < 1000; e++) { - assert.equal(computeActivationExitEpoch(e), e + 1 + MAX_SEED_LOOKAHEAD); + expect(computeActivationExitEpoch(e)).toEqual(e + 1 + MAX_SEED_LOOKAHEAD); } }); }); diff --git a/packages/state-transition/test/unit/util/flags.test.ts b/packages/state-transition/test/unit/util/flags.test.ts index 6566ef8205be..07a8ce3fe097 100644 --- a/packages/state-transition/test/unit/util/flags.test.ts +++ b/packages/state-transition/test/unit/util/flags.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; describe("Altair status flags", () => { for (let prev = 0b000; prev <= 0b111; prev++) { @@ -7,7 +7,7 @@ describe("Altair status flags", () => { expect( // Actual function toStr(getResFlags(prev, att)) - ).to.equal( + ).toBe( // Naive but correct implementation toStr(getResFlagsNaive(prev, att)) ); diff --git a/packages/state-transition/test/unit/util/loadState/findModifiedInactivityScores.test.ts b/packages/state-transition/test/unit/util/loadState/findModifiedInactivityScores.test.ts index e1ad0cf972da..85697af2b7c1 100644 --- a/packages/state-transition/test/unit/util/loadState/findModifiedInactivityScores.test.ts +++ b/packages/state-transition/test/unit/util/loadState/findModifiedInactivityScores.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import { INACTIVITY_SCORE_SIZE, findModifiedInactivityScores, @@ -27,7 +27,7 @@ describe("findModifiedInactivityScores", () => { } const modifiedValidators: number[] = []; findModifiedInactivityScores(inactivityScoresBytes, inactivityScoresBytes2, modifiedValidators); - expect(modifiedValidators.sort((a, b) => a - b)).to.be.deep.equal(expectedModifiedValidators); + expect(modifiedValidators.sort((a, b) => a - b)).toEqual(expectedModifiedValidators); }); } }); diff --git a/packages/state-transition/test/unit/util/loadState/findModifiedValidators.test.ts b/packages/state-transition/test/unit/util/loadState/findModifiedValidators.test.ts index aa2378276d22..25c6233d2738 100644 --- a/packages/state-transition/test/unit/util/loadState/findModifiedValidators.test.ts +++ b/packages/state-transition/test/unit/util/loadState/findModifiedValidators.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {fromHexString} from "@chainsafe/ssz"; import {findModifiedValidators} from "../../../../src/util/loadState/findModifiedValidators.js"; import {generateState} from "../../../utils/state.js"; @@ -35,7 +35,7 @@ describe("findModifiedValidators", () => { const validatorsBytes2 = clonedState.validators.serialize(); const modifiedValidators: number[] = []; findModifiedValidators(validatorsBytes, validatorsBytes2, modifiedValidators); - expect(modifiedValidators.sort((a, b) => a - b)).to.be.deep.equal(expectedModifiedValidators); + expect(modifiedValidators.sort((a, b) => a - b)).toEqual(expectedModifiedValidators); }); } }); diff --git a/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts b/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts index 7c3112537490..9a2094531813 100644 --- a/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts +++ b/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {CompositeViewDU} from "@chainsafe/ssz"; import {phase0, ssz} from "@lodestar/types"; import {loadValidator} from "../../../../src/util/loadState/loadValidator.js"; @@ -105,19 +105,11 @@ describe("loadValidator", () => { }, ]; - for (const {name, getValidator} of testCases) { - it(name, () => { - const newValidator = getValidator(); - const newValidatorBytes = newValidator.serialize(); - const loadedValidator = loadValidator(validator, newValidatorBytes); - expect(Buffer.compare(loadedValidator.hashTreeRoot(), newValidator.hashTreeRoot())).to.be.equal( - 0, - "root is not correct" - ); - expect(Buffer.compare(loadedValidator.serialize(), newValidator.serialize())).to.be.equal( - 0, - "serialized value is not correct" - ); - }); - } + it.each(testCases)("$name", ({getValidator}) => { + const newValidator = getValidator(); + const newValidatorBytes = newValidator.serialize(); + const loadedValidator = loadValidator(validator, newValidatorBytes); + expect(Buffer.compare(loadedValidator.hashTreeRoot(), newValidator.hashTreeRoot())).toBe(0); + expect(Buffer.compare(loadedValidator.serialize(), newValidator.serialize())).toBe(0); + }); }); diff --git a/packages/state-transition/test/unit/util/misc.test.ts b/packages/state-transition/test/unit/util/misc.test.ts index f487ad41aca2..5651da5ac5d1 100644 --- a/packages/state-transition/test/unit/util/misc.test.ts +++ b/packages/state-transition/test/unit/util/misc.test.ts @@ -1,4 +1,4 @@ -import {assert} from "chai"; +import {describe, it, expect} from "vitest"; import {toBigIntLE} from "bigint-buffer"; import {GENESIS_SLOT, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; @@ -13,14 +13,14 @@ describe("getBlockRoot", () => { }); const res = Buffer.from(getBlockRoot(state, GENESIS_SLOT)); const expectedRes = BigInt("0xab"); - assert(toBigIntLE(res) === expectedRes, `got: ${toBigIntLE(res)}, expected: ${expectedRes.toString(16)}`); + expect(toBigIntLE(res)).toEqual(expectedRes); }); it("should fail if slot is current slot", () => { const state = generateState({slot: GENESIS_SLOT}); - assert.throws(() => getBlockRoot(state, GENESIS_SLOT), ""); + expect(() => getBlockRoot(state, GENESIS_SLOT)).toThrow(""); }); it("should fail if slot is not within SLOTS_PER_HISTORICAL_ROOT of current slot", () => { const state = generateState({slot: GENESIS_SLOT + SLOTS_PER_HISTORICAL_ROOT + 1}); - assert.throws(() => getBlockRoot(state, GENESIS_SLOT), ""); + expect(() => getBlockRoot(state, GENESIS_SLOT)).toThrow(""); }); }); diff --git a/packages/state-transition/test/unit/util/seed.test.ts b/packages/state-transition/test/unit/util/seed.test.ts index ccdc3332cdba..7f7c0e1f8bc7 100644 --- a/packages/state-transition/test/unit/util/seed.test.ts +++ b/packages/state-transition/test/unit/util/seed.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {toHexString} from "@chainsafe/ssz"; import {GENESIS_EPOCH, GENESIS_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params"; @@ -16,7 +16,7 @@ describe("getRandaoMix", () => { state.randaoMixes.set(0, randaoMix1); const res = getRandaoMix(state, GENESIS_EPOCH); - expect(toHexString(res)).to.equal(toHexString(randaoMix1)); + expect(toHexString(res)).toBe(toHexString(randaoMix1)); }); it("should return second randao mix for GENESIS_EPOCH + 1", () => { // Empty state in 2nd epoch @@ -25,6 +25,6 @@ describe("getRandaoMix", () => { state.randaoMixes.set(1, randaoMix2); const res = getRandaoMix(state, GENESIS_EPOCH + 1); - expect(toHexString(res)).to.equal(toHexString(randaoMix2)); + expect(toHexString(res)).toBe(toHexString(randaoMix2)); }); }); diff --git a/packages/state-transition/test/unit/util/shuffle.test.ts b/packages/state-transition/test/unit/util/shuffle.test.ts index 7cb1e54e2619..9186968674ae 100644 --- a/packages/state-transition/test/unit/util/shuffle.test.ts +++ b/packages/state-transition/test/unit/util/shuffle.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {expect, describe, it} from "vitest"; import {unshuffleList} from "../../../src/index.js"; describe("util / shuffle", () => { @@ -22,10 +22,8 @@ describe("util / shuffle", () => { const seed = new Uint8Array([42, 32]); - for (const {id, input, res} of testCases) { - it(id, () => { - unshuffleList(input, seed); - expect(input).to.deep.equal(res); - }); - } + it.each(testCases)("$id", ({input, res}) => { + unshuffleList(input, seed); + expect(input).toEqual(res); + }); }); diff --git a/packages/state-transition/test/unit/util/slashing.test.ts b/packages/state-transition/test/unit/util/slashing.test.ts index 411933080792..49a7b6454c25 100644 --- a/packages/state-transition/test/unit/util/slashing.test.ts +++ b/packages/state-transition/test/unit/util/slashing.test.ts @@ -1,4 +1,4 @@ -import {assert} from "chai"; +import {expect, it, describe} from "vitest"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {Epoch, phase0, ssz} from "@lodestar/types"; @@ -11,7 +11,7 @@ describe("isSlashableAttestationData", () => { const epoch2 = epoch1 + 1; const a1 = getAttestationDataAt(epoch1, epoch2); const a2 = getAttestationDataAt(epoch1 - 1, epoch2); - assert.isTrue(isSlashableAttestationData(a1, a2)); + expect(isSlashableAttestationData(a1, a2)).toBe(true); }); it("Attestation data with disjoint source/target epochs should return false", () => { @@ -21,7 +21,7 @@ describe("isSlashableAttestationData", () => { const epoch4 = epoch1 + 3; const a1 = getAttestationDataAt(epoch1, epoch2); const a2 = getAttestationDataAt(epoch3, epoch4); - assert.isFalse(isSlashableAttestationData(a1, a2)); + expect(isSlashableAttestationData(a1, a2)).toBe(false); }); it("Should return false if the second attestation does not have a greater source epoch", () => { @@ -35,12 +35,12 @@ describe("isSlashableAttestationData", () => { const a1 = getAttestationDataAt(sourceEpoch1, targetEpoch1); const a2Hi = getAttestationDataAt(sourceEpoch2Hi, targetEpoch2); - assert.isFalse(isSlashableAttestationData(a1, a2Hi)); + expect(isSlashableAttestationData(a1, a2Hi)).toBe(false); // Second attestation has a smaller source epoch. const sourceEpoch2Lo = sourceEpoch1 - 1; const a2Lo = getAttestationDataAt(sourceEpoch2Lo, targetEpoch2); - assert.isFalse(isSlashableAttestationData(a1, a2Lo)); + expect(isSlashableAttestationData(a1, a2Lo)).toBe(false); }); it("Should return false if the second attestation does not have a smaller target epoch", () => { @@ -58,14 +58,14 @@ describe("isSlashableAttestationData", () => { let a1 = getAttestationDataAt(targetSlot1, sourceEpoch1); let a2 = getAttestationDataAt(targetSlot2, sourceEpoch2); - assert.isFalse(isSlashableAttestationData(a1, a2)); + expect(isSlashableAttestationData(a1, a2)).toBe(false); // Second attestation has a greater target epoch. targetSlot1 = targetEpoch * SLOTS_PER_EPOCH; targetSlot2 = (targetEpoch + 1) * SLOTS_PER_EPOCH; a1 = getAttestationDataAt(targetSlot1, sourceEpoch1); a2 = getAttestationDataAt(targetSlot2, sourceEpoch2); - assert.isFalse(isSlashableAttestationData(a1, a2)); + expect(isSlashableAttestationData(a1, a2)).toBe(false); }); }); diff --git a/packages/state-transition/test/unit/util/slot.test.ts b/packages/state-transition/test/unit/util/slot.test.ts index f25a1f7b9fe9..c9546ad60043 100644 --- a/packages/state-transition/test/unit/util/slot.test.ts +++ b/packages/state-transition/test/unit/util/slot.test.ts @@ -1,4 +1,4 @@ -import {assert} from "chai"; +import {describe, it, expect} from "vitest"; import {Slot} from "@lodestar/types"; import {computeSlotsSinceEpochStart} from "../../../src/util/index.js"; @@ -14,7 +14,7 @@ describe("computeSlotsSinceEpochStart", () => { for (const pair of pairs) { it(`Slot ${pair.test} is ${pair.expected} from current Epoch start`, () => { const result: Slot = computeSlotsSinceEpochStart(pair.test); - assert.equal(result, pair.expected); + expect(result).toEqual(pair.expected); }); } @@ -23,6 +23,6 @@ describe("computeSlotsSinceEpochStart", () => { const slot = 70; const result = computeSlotsSinceEpochStart(slot, epoch); // 70 - NUM_SLOT_PER_EPOCH - assert.equal(result, 38); + expect(result).toEqual(38); }); }); diff --git a/packages/state-transition/test/unit/util/validator.test.ts b/packages/state-transition/test/unit/util/validator.test.ts index 7a79ce8474f7..65727126742d 100644 --- a/packages/state-transition/test/unit/util/validator.test.ts +++ b/packages/state-transition/test/unit/util/validator.test.ts @@ -1,4 +1,4 @@ -import {assert, expect} from "chai"; +import {describe, it, expect, beforeEach} from "vitest"; import {phase0, ssz} from "@lodestar/types"; @@ -10,7 +10,7 @@ import {generateState} from "../../utils/state.js"; describe("getActiveValidatorIndices", () => { it("empty list of validators should return no indices (empty list)", () => { - assert.deepEqual(getActiveValidatorIndices(generateState(), randBetween(0, 4)), []); + expect(getActiveValidatorIndices(generateState(), randBetween(0, 4))).toStrictEqual([]); }); it("list of cloned validators should return all or none", () => { const state = generateState(); @@ -22,8 +22,8 @@ describe("getActiveValidatorIndices", () => { const allActiveIndices = state.validators.getAllReadonlyValues().map((_, i) => i); const allInactiveIndices: any = []; - assert.deepEqual(getActiveValidatorIndices(state, activationEpoch), allActiveIndices); - assert.deepEqual(getActiveValidatorIndices(state, exitEpoch), allInactiveIndices); + expect(getActiveValidatorIndices(state, activationEpoch)).toStrictEqual(allActiveIndices); + expect(getActiveValidatorIndices(state, exitEpoch)).toStrictEqual(allInactiveIndices); }); }); @@ -41,7 +41,7 @@ describe("isActiveValidator", () => { it(`should be ${testValue.expected ? "" : "not "}active`, () => { const v: phase0.Validator = generateValidator(testValue.validatorOpts); const result: boolean = isActiveValidator(v, testValue.epoch); - expect(result).to.be.equal(testValue.expected); + expect(result).toBe(testValue.expected); }); } }); @@ -57,28 +57,31 @@ describe("isSlashableValidator", () => { validator.activationEpoch = 0; validator.withdrawableEpoch = Infinity; validator.slashed = false; - assert(isSlashableValidator(validator, 0), "unslashed validator should be slashable"); + expect(isSlashableValidator(validator, 0)).toBeWithMessage(true, "unslashed validator should be slashable"); validator.slashed = true; - assert(!isSlashableValidator(validator, 0), "slashed validator should not be slashable"); + expect(!isSlashableValidator(validator, 0)).toBeWithMessage(true, "slashed validator should not be slashable"); }); it("should check validator.activationEpoch", () => { validator.activationEpoch = 10; validator.withdrawableEpoch = Infinity; - assert( - !isSlashableValidator(validator, validator.activationEpoch - 1), + expect(!isSlashableValidator(validator, validator.activationEpoch - 1)).toBeWithMessage( + true, "unactivated validator should not be slashable" ); - assert(isSlashableValidator(validator, validator.activationEpoch), "activated validator should be slashable"); + expect(isSlashableValidator(validator, validator.activationEpoch)).toBeWithMessage( + true, + "activated validator should be slashable" + ); }); it("should check validator.withdrawableEpoch", () => { validator.activationEpoch = 0; validator.withdrawableEpoch = 10; - assert( - isSlashableValidator(validator, validator.withdrawableEpoch - 1), + expect(isSlashableValidator(validator, validator.withdrawableEpoch - 1)).toBeWithMessage( + true, "nonwithdrawable validator should be slashable" ); - assert( - !isSlashableValidator(validator, validator.withdrawableEpoch), + expect(!isSlashableValidator(validator, validator.withdrawableEpoch)).toBeWithMessage( + true, "withdrawable validator should not be slashable" ); }); diff --git a/packages/state-transition/test/unit/util/weakSubjectivity.test.ts b/packages/state-transition/test/unit/util/weakSubjectivity.test.ts index a421caed64ed..5f5c784e975a 100644 --- a/packages/state-transition/test/unit/util/weakSubjectivity.test.ts +++ b/packages/state-transition/test/unit/util/weakSubjectivity.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {config} from "@lodestar/config/default"; import {computeWeakSubjectivityPeriodFromConstituents} from "../../../src/util/weakSubjectivity.js"; import {getChurnLimit} from "../../../src/util/validator.js"; @@ -23,16 +23,17 @@ describe("weak subjectivity tests", () => { {avgValBalance: balance32, valCount: 1048576, wsPeriod: 3532}, ]; - for (const {avgValBalance, valCount, wsPeriod} of testValues) { - it(`should have wsPeriod: ${wsPeriod} with avgValBalance: ${avgValBalance} and valCount: ${valCount}`, () => { + it.each(testValues)( + "should have wsPeriod: $wsPeriod with avgValBalance: $avgValBalance and valCount: $valCount", + ({valCount, avgValBalance}) => { const wsPeriod = computeWeakSubjectivityPeriodFromConstituents( valCount, avgValBalance * valCount, getChurnLimit(config, valCount), config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY ); - expect(wsPeriod).to.equal(wsPeriod); - }); - } + expect(wsPeriod).toBe(wsPeriod); + } + ); }); }); diff --git a/packages/state-transition/test/utils/beforeValue.ts b/packages/state-transition/test/utils/beforeValue.ts index 0d5f8f77d203..61ae3daa32a1 100644 --- a/packages/state-transition/test/utils/beforeValue.ts +++ b/packages/state-transition/test/utils/beforeValue.ts @@ -1,3 +1,5 @@ +import {beforeAll} from "vitest"; + export type LazyValue = {value: T}; /** @@ -12,10 +14,9 @@ export type LazyValue = {value: T}; export function beforeValue(fn: () => T | Promise, timeout?: number): LazyValue { let value: T = null as unknown as T; - before(async function () { - this.timeout(timeout ?? 300_000); + beforeAll(async function () { value = await fn(); - }); + }, timeout ?? 300_000); return new Proxy<{value: T}>( {value}, diff --git a/packages/state-transition/test/utils/beforeValueMocha.ts b/packages/state-transition/test/utils/beforeValueMocha.ts new file mode 100644 index 000000000000..0d5f8f77d203 --- /dev/null +++ b/packages/state-transition/test/utils/beforeValueMocha.ts @@ -0,0 +1,36 @@ +export type LazyValue = {value: T}; + +/** + * Register a callback to compute a value in the before() block of mocha tests + * ```ts + * const state = beforeValue(() => getState()) + * it("test", () => { + * doTest(state.value) + * }) + * ``` + */ +export function beforeValue(fn: () => T | Promise, timeout?: number): LazyValue { + let value: T = null as unknown as T; + + before(async function () { + this.timeout(timeout ?? 300_000); + value = await fn(); + }); + + return new Proxy<{value: T}>( + {value}, + { + get: function (target, prop) { + if (prop === "value") { + if (value === null) { + throw Error("beforeValue has not yet run the before() block"); + } else { + return value; + } + } else { + return undefined; + } + }, + } + ); +} diff --git a/packages/state-transition/test/utils/capella.ts b/packages/state-transition/test/utils/capella.ts index 5789c260f67c..e2cdc47b7e1d 100644 --- a/packages/state-transition/test/utils/capella.ts +++ b/packages/state-transition/test/utils/capella.ts @@ -102,7 +102,7 @@ export function modifyStateSameValidator(seedState: BeaconStateCapella): BeaconS state.eth1DepositIndex = 1000; state.balances.set(0, 30); state.randaoMixes.set(0, crypto.randomBytes(32)); - state.slashings.set(0, 1n); + state.slashings.set(0, 1); state.previousEpochParticipation.set(0, 0b11111110); state.currentEpochParticipation.set(0, 0b11111110); state.justificationBits.set(0, true); diff --git a/packages/state-transition/test/utils/index.ts b/packages/state-transition/test/utils/index.ts deleted file mode 100644 index 4a79d4371787..000000000000 --- a/packages/state-transition/test/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./beforeValue.js"; -export * from "./testFileCache.js"; diff --git a/packages/state-transition/test/utils/state.ts b/packages/state-transition/test/utils/state.ts index 614210e44afa..29a1f98b5562 100644 --- a/packages/state-transition/test/utils/state.ts +++ b/packages/state-transition/test/utils/state.ts @@ -11,7 +11,7 @@ import {config} from "@lodestar/config/default"; import {createBeaconConfig, ChainForkConfig} from "@lodestar/config"; import {ZERO_HASH} from "../../src/constants/index.js"; -import {newZeroedBigIntArray} from "../../src/util/index.js"; +import {newZeroedArray} from "../../src/util/index.js"; import { BeaconStatePhase0, @@ -64,7 +64,7 @@ export function generateState(opts?: TestBeaconState): BeaconStatePhase0 { validators: [], balances: [], randaoMixes: Array.from({length: EPOCHS_PER_HISTORICAL_VECTOR}, () => ZERO_HASH), - slashings: newZeroedBigIntArray(EPOCHS_PER_SLASHINGS_VECTOR), + slashings: newZeroedArray(EPOCHS_PER_SLASHINGS_VECTOR), previousEpochAttestations: [], currentEpochAttestations: [], justificationBits: ssz.phase0.JustificationBits.defaultValue(), diff --git a/packages/state-transition/vitest.config.ts b/packages/state-transition/vitest.config.ts new file mode 100644 index 000000000000..1df0de848936 --- /dev/null +++ b/packages/state-transition/vitest.config.ts @@ -0,0 +1,11 @@ +import {defineConfig, mergeConfig} from "vitest/config"; +import vitestConfig from "../../vitest.base.config"; + +export default mergeConfig( + vitestConfig, + defineConfig({ + test: { + globalSetup: ["./test/globalSetup.ts"], + }, + }) +); diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index de0996d028f6..586be26fccab 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.12.1", + "version": "1.13.0", "description": "Test utilities reused across other packages", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -61,13 +61,19 @@ "blockchain" ], "dependencies": { - "@lodestar/utils": "^1.12.1", + "@chainsafe/bls": "7.1.1", + "@chainsafe/bls-keystore": "^2.0.0", + "@lodestar/params": "^1.13.0", + "@lodestar/utils": "^1.13.0", "axios": "^1.3.4", "chai": "^4.3.7", "mocha": "^10.2.0", - "sinon": "^15.0.3" + "sinon": "^15.0.3", + "testcontainers": "^10.2.1", + "tmp": "^0.2.1" }, "devDependencies": { + "@types/dockerode": "^3.3.19", "@types/mocha": "^10.0.1", "@types/yargs": "^17.0.24", "yargs": "^17.7.1" diff --git a/packages/test-utils/src/cli.ts b/packages/test-utils/src/cli.ts index 13689da5bea9..8b4a84ec467a 100644 --- a/packages/test-utils/src/cli.ts +++ b/packages/test-utils/src/cli.ts @@ -30,11 +30,19 @@ export async function runCliCommand( return wrapTimeout( // eslint-disable-next-line no-async-promise-executor new Promise(async (resolve, reject) => { - await cli.parseAsync(parseArgs(args), {}, (err, _argv, output) => { - if (err) return reject(err); + try { + await cli + .parseAsync(parseArgs(args), {}, (err, _argv, output) => { + if (err) return reject(err); - resolve(output); - }); + resolve(output); + }) + .catch(() => { + // We are suppressing error here as we are throwing from inside the callback + }); + } catch (err) { + reject(err); + } }), opts.timeoutMs ); diff --git a/packages/test-utils/src/externalSigner.ts b/packages/test-utils/src/externalSigner.ts new file mode 100644 index 000000000000..20cf59cc26d0 --- /dev/null +++ b/packages/test-utils/src/externalSigner.ts @@ -0,0 +1,80 @@ +import fs from "node:fs"; +import path from "node:path"; +import tmp from "tmp"; +import {GenericContainer, Wait, StartedTestContainer} from "testcontainers"; +import {ForkSeq} from "@lodestar/params"; + +const web3signerVersion = "23.11.0"; + +/** Till what version is the web3signer image updated for signature verification */ +const supportedForkSeq = ForkSeq.capella; + +export type StartedExternalSigner = { + container: StartedTestContainer; + url: string; + supportedForkSeq: ForkSeq; +}; + +export async function startExternalSigner({ + keystoreStrings, + password, +}: { + keystoreStrings: string[]; + password: string; +}): Promise { + // path to store configuration + const tmpDir = tmp.dirSync({ + unsafeCleanup: true, + // In Github runner NodeJS process probably runs as root, so web3signer doesn't have permissions to read config dir + mode: 755, + }); + // Apply permissions again to hopefully make Github runner happy >.< + fs.chmodSync(tmpDir.name, 0o755); + + const configDirPathHost = tmpDir.name; + const configDirPathContainer = "/var/web3signer/config"; + + // keystore content and file paths + const passwordFilename = "password.txt"; + + for (const [idx, keystoreString] of keystoreStrings.entries()) { + fs.writeFileSync(path.join(configDirPathHost, `keystore-${idx}.json`), keystoreString); + } + fs.writeFileSync(path.join(configDirPathHost, passwordFilename), password); + const port = 9000; + + const startedContainer = await new GenericContainer(`consensys/web3signer:${web3signerVersion}`) + .withHealthCheck({ + test: ["CMD-SHELL", `curl -f http://localhost:${port}/healthcheck || exit 1`], + interval: 1000, + timeout: 3000, + retries: 5, + startPeriod: 1000, + }) + .withWaitStrategy(Wait.forHealthCheck()) + .withExposedPorts(port) + .withBindMounts([{source: configDirPathHost, target: configDirPathContainer, mode: "ro"}]) + .withCommand([ + "eth2", + `--keystores-path=${configDirPathContainer}`, + // Don't use path.join here, the container is running on unix filesystem + `--keystores-password-file=${configDirPathContainer}/${passwordFilename}`, + "--slashing-protection-enabled=false", + ]) + .start(); + + const url = `http://localhost:${startedContainer.getMappedPort(port)}`; + + const stream = await startedContainer.logs(); + stream + .on("data", (line) => process.stdout.write(line)) + .on("err", (line) => process.stderr.write(line)) + // eslint-disable-next-line no-console + .on("end", () => console.log("Stream closed")); + + return { + container: startedContainer, + url: url, + supportedForkSeq, + }; +} diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 84f0efe0f587..1b90c5419f60 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -1,5 +1,7 @@ export * from "./cli.js"; export * from "./childProcess.js"; +export * from "./externalSigner.js"; +export * from "./keystores.js"; export * from "./path.js"; export * from "./timeout.js"; export * from "./http.js"; diff --git a/packages/cli/test/utils/keystores.ts b/packages/test-utils/src/keystores.ts similarity index 100% rename from packages/cli/test/utils/keystores.ts rename to packages/test-utils/src/keystores.ts diff --git a/packages/types/package.json b/packages/types/package.json index 0bc556078a73..5c7a6599d890 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.12.1", + "version": "1.13.0", "type": "module", "exports": { ".": { @@ -68,7 +68,7 @@ "types": "lib/index.d.ts", "dependencies": { "@chainsafe/ssz": "^0.14.0", - "@lodestar/params": "^1.12.1" + "@lodestar/params": "^1.13.0" }, "keywords": [ "ethereum", diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index ea06be588849..78988465d274 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -39,7 +39,6 @@ const { EpochInf, CommitteeIndex, ValidatorIndex, - Gwei, Root, Version, ForkDigest, @@ -248,7 +247,14 @@ export const Validator = ValidatorNodeStruct; export const Validators = new ListCompositeType(ValidatorNodeStruct, VALIDATOR_REGISTRY_LIMIT); export const Balances = new ListBasicType(UintNum64, VALIDATOR_REGISTRY_LIMIT); export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICAL_VECTOR); -export const Slashings = new VectorBasicType(Gwei, EPOCHS_PER_SLASHINGS_VECTOR); +/** + * This is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: + * - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() + * - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet + * - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + * - we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache + */ +export const Slashings = new VectorBasicType(UintNum64, EPOCHS_PER_SLASHINGS_VECTOR); export const JustificationBits = new BitVectorType(JUSTIFICATION_BITS_LENGTH); // Misc dependants diff --git a/packages/utils/package.json b/packages/utils/package.json index b2238b651b83..8490ba2fa89d 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.12.1", + "version": "1.13.0", "type": "module", "exports": "./lib/index.js", "files": [ diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index be939673845c..9ecb78e62533 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -15,7 +15,7 @@ export * from "./sleep.js"; export * from "./sort.js"; export * from "./timeout.js"; export {type RecursivePartial, bnToNum} from "./types.js"; -export * from "./validation.js"; +export * from "./url.js"; export * from "./verifyMerkleBranch.js"; export * from "./promise.js"; export * from "./waitFor.js"; diff --git a/packages/utils/src/url.ts b/packages/utils/src/url.ts new file mode 100644 index 000000000000..7d0b23347617 --- /dev/null +++ b/packages/utils/src/url.ts @@ -0,0 +1,29 @@ +export function isValidHttpUrl(urlStr: string): boolean { + let url; + try { + url = new URL(urlStr); + + // `new URL` encodes the username/password with the userinfo percent-encode set. + // This means the `%` character is not encoded, but others are (such as `=`). + // If a username/password contain a `%`, they will not be able to be decoded. + // + // Make sure that we can successfully decode the username and password here. + // + // Unfortunately this means we don't accept every character supported by RFC-3986. + decodeURIComponent(url.username); + decodeURIComponent(url.password); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + +/** + * Sanitize URL to prevent leaking user credentials in logs + * + * Note: `urlStr` must be a valid URL + */ +export function toSafePrintableUrl(urlStr: string): string { + return new URL(urlStr).origin; +} diff --git a/packages/utils/src/validation.ts b/packages/utils/src/validation.ts deleted file mode 100644 index 0b47a70e2340..000000000000 --- a/packages/utils/src/validation.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function isValidHttpUrl(urlStr: string): boolean { - let url; - try { - url = new URL(urlStr); - } catch (_) { - return false; - } - - return url.protocol === "http:" || url.protocol === "https:"; -} diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index b09625d7f135..f47e4c7ac3ed 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -1,6 +1,6 @@ import "../setup.js"; import {assert, expect} from "chai"; -import {intToBytes, bytesToInt} from "../../src/index.js"; +import {intToBytes, bytesToInt, toHex, fromHex, toHexString} from "../../src/index.js"; describe("intToBytes", () => { const zeroedArray = (length: number): number[] => Array.from({length}, () => 0); @@ -47,3 +47,54 @@ describe("bytesToInt", () => { }); } }); + +describe("toHex", () => { + const testCases: {input: Buffer | Uint8Array | string; output: string}[] = [ + {input: Buffer.from("Hello, World!", "utf-8"), output: "0x48656c6c6f2c20576f726c6421"}, + {input: new Uint8Array([72, 101, 108, 108, 111]), output: "0x48656c6c6f"}, + {input: Buffer.from([72, 101, 108, 108, 111]), output: "0x48656c6c6f"}, + {input: Buffer.from([]), output: "0x"}, + ]; + for (const {input, output} of testCases) { + it(`should convert Uint8Array to hex string ${output}`, () => { + expect(toHex(input)).to.be.equal(output); + }); + } +}); + +describe("fromHex", () => { + const testCases: {input: string; output: Buffer | Uint8Array}[] = [ + { + input: "0x48656c6c6f2c20576f726c6421", + output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]), + }, + { + input: "48656c6c6f2c20576f726c6421", + output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]), + }, + {input: "0x", output: new Uint8Array([])}, + ]; + + for (const {input, output} of testCases) { + it(`should convert hex string ${input} to Uint8Array`, () => { + expect(fromHex(input)).to.deep.equal(output); + }); + } +}); + +describe("toHexString", () => { + const testCases: {input: Uint8Array; output: string}[] = [ + {input: new Uint8Array([1, 2, 3]), output: "0x010203"}, + {input: new Uint8Array([72, 101, 108, 108, 111]), output: "0x48656c6c6f"}, + {input: new Uint8Array([]), output: "0x"}, + {input: new Uint8Array([0, 0, 0, 0]), output: "0x00000000"}, + {input: new Uint8Array([15, 255, 16, 0, 127]), output: "0x0fff10007f"}, + {input: new Uint8Array(5).fill(255), output: "0x" + "ff".repeat(5)}, + ]; + + for (const {input, output} of testCases) { + it(`should convert Uint8Array to hex string ${output}`, () => { + expect(toHexString(input)).to.be.equal(output); + }); + } +}); diff --git a/packages/validator/package.json b/packages/validator/package.json index 788a7a797a36..ecd15116169a 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/validator", - "version": "1.12.1", + "version": "1.13.0", "description": "A Typescript implementation of the validator client", "author": "ChainSafe Systems", "license": "LGPL-3.0", @@ -50,20 +50,19 @@ "dependencies": { "@chainsafe/bls": "7.1.1", "@chainsafe/ssz": "^0.14.0", - "@lodestar/api": "^1.12.1", - "@lodestar/config": "^1.12.1", - "@lodestar/db": "^1.12.1", - "@lodestar/params": "^1.12.1", - "@lodestar/state-transition": "^1.12.1", - "@lodestar/types": "^1.12.1", - "@lodestar/utils": "^1.12.1", + "@lodestar/api": "^1.13.0", + "@lodestar/config": "^1.13.0", + "@lodestar/db": "^1.13.0", + "@lodestar/params": "^1.13.0", + "@lodestar/state-transition": "^1.13.0", + "@lodestar/types": "^1.13.0", + "@lodestar/utils": "^1.13.0", "bigint-buffer": "^1.1.5", "strict-event-emitter-types": "^2.0.0" }, "devDependencies": { - "@types/dockerode": "^3.3.19", + "@lodestar/test-utils": "^1.13.0", "bigint-buffer": "^1.1.5", - "rimraf": "^4.4.1", - "testcontainers": "^10.2.1" + "rimraf": "^4.4.1" } } diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 6582b660d011..39a331af6657 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -15,6 +15,7 @@ export { externalSignerGetKeys, externalSignerPostSignature, externalSignerUpCheck, + SignableMessageType, } from "./util/externalSignerClient.js"; // Types diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 55b7c4cd74ce..d65ce2616fb5 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -220,6 +220,12 @@ export class BlockProposingService { source: response.executionPayloadBlinded ? ProducedBlockSource.builder : ProducedBlockSource.engine, // winston logger doesn't like bigint executionPayloadValue: `${formatBigDecimal(response.executionPayloadValue, ETH_TO_WEI, MAX_DECIMAL_FACTOR)} ETH`, + consensusBlockValue: `${formatBigDecimal(response.consensusBlockValue, ETH_TO_WEI, MAX_DECIMAL_FACTOR)} ETH`, + totalBlockValue: `${formatBigDecimal( + response.executionPayloadValue + response.consensusBlockValue, + ETH_TO_WEI, + MAX_DECIMAL_FACTOR + )} ETH`, // TODO PR: should be used in api call instead of adding in log strictFeeRecipientCheck, builderSelection, diff --git a/packages/validator/src/services/chainHeaderTracker.ts b/packages/validator/src/services/chainHeaderTracker.ts index ed8471721e32..ebb20670bb24 100644 --- a/packages/validator/src/services/chainHeaderTracker.ts +++ b/packages/validator/src/services/chainHeaderTracker.ts @@ -31,8 +31,10 @@ export class ChainHeaderTracker { ) {} start(signal: AbortSignal): void { - void this.api.events.eventstream([EventType.head], signal, this.onHeadUpdate); - this.logger.verbose("Subscribed to head event"); + this.logger.verbose("Subscribing to head event"); + this.api.events + .eventstream([EventType.head], signal, this.onHeadUpdate) + .catch((e) => this.logger.error("Failed to subscribe to head event", {}, e)); } getCurrentChainHead(slot: Slot): Root | null { diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 307aefc2abbf..571aec71e019 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -2,7 +2,7 @@ import {toHexString} from "@chainsafe/ssz"; import {BLSPubkey, phase0, ssz} from "@lodestar/types"; import {createBeaconConfig, BeaconConfig, ChainForkConfig} from "@lodestar/config"; import {Genesis} from "@lodestar/types/phase0"; -import {Logger} from "@lodestar/utils"; +import {Logger, toSafePrintableUrl} from "@lodestar/utils"; import {getClient, Api, routes, ApiError} from "@lodestar/api"; import {computeEpochAtSlot, getCurrentSlot} from "@lodestar/state-transition"; import {Clock, IClock} from "./util/clock.js"; @@ -271,6 +271,7 @@ export class Validator { // This new api instance can make do with default timeout as a faster timeout is // not necessary since this instance won't be used for validator duties api = getClient({urls, getAbortSignal: () => opts.abortController.signal}, {config, logger}); + logger.info("Beacon node", {urls: urls.map(toSafePrintableUrl).toString()}); } else { api = opts.api; } diff --git a/packages/validator/test/e2e/web3signer.test.ts b/packages/validator/test/e2e/web3signer.test.ts index 3cd6db833750..933e59d800ad 100644 --- a/packages/validator/test/e2e/web3signer.test.ts +++ b/packages/validator/test/e2e/web3signer.test.ts @@ -1,43 +1,34 @@ -import fs from "node:fs"; -import path from "node:path"; -import tmp from "tmp"; import {expect} from "chai"; -import {GenericContainer, Wait, StartedTestContainer} from "testcontainers"; -import {Keystore} from "@chainsafe/bls-keystore"; -import bls from "@chainsafe/bls"; import {fromHex, toHex} from "@lodestar/utils"; import {config} from "@lodestar/config/default"; -import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; +import {computeStartSlotAtEpoch, interopSecretKey, interopSecretKeys} from "@lodestar/state-transition"; import {createBeaconConfig} from "@lodestar/config"; import {genesisData} from "@lodestar/config/networks"; import {getClient, routes} from "@lodestar/api"; import {ssz} from "@lodestar/types"; import {ForkSeq} from "@lodestar/params"; +import {getKeystoresStr, StartedExternalSigner, startExternalSigner} from "@lodestar/test-utils"; import {Interchange, ISlashingProtection, Signer, SignerType, ValidatorStore} from "../../src/index.js"; import {IndicesService} from "../../src/services/indices.js"; import {testLogger} from "../utils/logger.js"; -const web3signerVersion = "22.8.1"; -/** Till what version is the image updated for signature verification */ -const validTillSignatureForkSeq = ForkSeq.bellatrix; - -/* eslint-disable no-console */ - describe("web3signer signature test", function () { this.timeout("60s"); - let validatorStoreRemote: ValidatorStore; - let validatorStoreLocal: ValidatorStore; - let startedContainer: StartedTestContainer; - - const pubkey = "0x8837af2a7452aff5a8b6906c3e5adefce5690e1bba6d73d870b9e679fece096b97a255bae0978e3a344aa832f68c6b47"; - const pubkeyBytes = fromHex(pubkey); const altairSlot = 2375711; const epoch = 0; // Sample validator const validatorIndex = 4; const subcommitteeIndex = 0; + const secretKey = interopSecretKey(0); + const pubkeyBytes = secretKey.toPublicKey().toBytes(); + + let validatorStoreRemote: ValidatorStore; + let validatorStoreLocal: ValidatorStore; + + let externalSigner: StartedExternalSigner; + const duty: routes.validator.AttesterDuty = { slot: altairSlot, committeeIndex: 0, @@ -48,77 +39,32 @@ describe("web3signer signature test", function () { pubkey: pubkeyBytes, }; - after("stop container", async function () { - await startedContainer.stop(); - }); - - before("start container", async function () { - this.timeout("300s"); - // path to store configuration - const tmpDir = tmp.dirSync({ - unsafeCleanup: true, - // In Github runner NodeJS process probably runs as root, so web3signer doesn't have permissions to read config dir - mode: 755, - }); - // Apply permissions again to hopefully make Github runner happy >.< - fs.chmodSync(tmpDir.name, 0o755); - - const configDirPathHost = tmpDir.name; - const configDirPathContainer = "/var/web3signer/config"; - - // keystore content and file paths - // const keystoreStr = getKeystore(); - // const password = "password"; - const passwordFilename = "password.txt"; + before("set up validator stores", async () => { + validatorStoreLocal = await getValidatorStore({type: SignerType.Local, secretKey: secretKey}); - const keystoreStr = getKeystore(); const password = "password"; + externalSigner = await startExternalSigner({ + keystoreStrings: await getKeystoresStr( + password, + interopSecretKeys(2).map((k) => k.toHex()) + ), + password: password, + }); + validatorStoreRemote = await getValidatorStore({ + type: SignerType.Remote, + url: externalSigner.url, + pubkey: secretKey.toPublicKey().toHex(), + }); + }); - fs.writeFileSync(path.join(configDirPathHost, "keystore.json"), keystoreStr); - fs.writeFileSync(path.join(configDirPathHost, passwordFilename), password); - - const secretKey = bls.SecretKey.fromBytes(await Keystore.parse(keystoreStr).decrypt(password)); - - const port = 9000; - - // using the latest image to be alerted in case there is a breaking change - startedContainer = await new GenericContainer(`consensys/web3signer:${web3signerVersion}`) - .withHealthCheck({ - test: ["CMD-SHELL", `curl -f http://localhost:${port}/healthcheck || exit 1`], - interval: 1000, - timeout: 3000, - retries: 5, - startPeriod: 1000, - }) - .withWaitStrategy(Wait.forHealthCheck()) - .withExposedPorts(port) - .withBindMounts([{source: configDirPathHost, target: configDirPathContainer, mode: "ro"}]) - .withCommand([ - "eth2", - `--keystores-path=${configDirPathContainer}`, - // Don't use path.join here, the container is running on unix filesystem - `--keystores-password-file=${configDirPathContainer}/${passwordFilename}`, - "--slashing-protection-enabled=false", - ]) - .start(); - - const web3signerUrl = `http://localhost:${startedContainer.getMappedPort(port)}`; - - // http://localhost:9000/api/v1/eth2/sign/0x8837af2a7452aff5a8b6906c3e5adefce5690e1bba6d73d870b9e679fece096b97a255bae0978e3a344aa832f68c6b47 - validatorStoreRemote = await getValidatorStore({type: SignerType.Remote, url: web3signerUrl, pubkey}); - validatorStoreLocal = await getValidatorStore({type: SignerType.Local, secretKey}); - - const stream = await startedContainer.logs(); - stream - .on("data", (line) => process.stdout.write(line)) - .on("err", (line) => process.stderr.write(line)) - .on("end", () => console.log("Stream closed")); + after("stop external signer container", async () => { + await externalSigner.container.stop(); }); for (const fork of config.forksAscendingEpochOrder) { it(`signBlock ${fork.name}`, async function () { // Only test till the fork the signer version supports - if (ForkSeq[fork.name] > validTillSignatureForkSeq) { + if (ForkSeq[fork.name] > externalSigner.supportedForkSeq) { this.skip(); } @@ -261,36 +207,3 @@ class SlashingProtectionDisabled implements ISlashingProtection { throw Error("not implemented"); } } - -function getKeystore(): string { - return `{ - "version": 4, - "uuid": "f31f3377-694d-4943-8686-5b20356b2597", - "path": "m/12381/3600/0/0/0", - "pubkey": "8837af2a7452aff5a8b6906c3e5adefce5690e1bba6d73d870b9e679fece096b97a255bae0978e3a344aa832f68c6b47", - "crypto": { - "kdf": { - "function": "pbkdf2", - "params": { - "dklen": 32, - "c": 262144, - "prf": "hmac-sha256", - "salt": "ab2c11fe1a288a8344972e5e03a746f42867f5a9e749bf286f8e26cf16702c93" - }, - "message": "" - }, - "checksum": { - "function": "sha256", - "params": {}, - "message": "1f0eda362360b51b85591e99fee6c5d030cc48f36af28eb055b19a2bf55b38a6" - }, - "cipher": { - "function": "aes-128-ctr", - "params": { - "iv": "acf3173c5d0b074e1646bb6058dc0f2a" - }, - "message": "402d1cecaa378e4f079c96437bd1d4771e09a85df2073d014b43980b623b9978" - } - } - }`; -} diff --git a/packages/validator/test/unit/services/block.test.ts b/packages/validator/test/unit/services/block.test.ts index 0c78fdc82eec..745ada3de9ca 100644 --- a/packages/validator/test/unit/services/block.test.ts +++ b/packages/validator/test/unit/services/block.test.ts @@ -63,6 +63,7 @@ describe("BlockDutiesService", function () { data: signedBlock.message, version: ForkName.bellatrix, executionPayloadValue: BigInt(1), + consensusBlockValue: BigInt(1), executionPayloadBlinded: false, }, ok: true, diff --git a/packages/validator/test/utils/createExternalSignerServer.ts b/packages/validator/test/utils/createExternalSignerServer.ts deleted file mode 100644 index 9c09f7e33beb..000000000000 --- a/packages/validator/test/utils/createExternalSignerServer.ts +++ /dev/null @@ -1,50 +0,0 @@ -import fastify from "fastify"; -import {fromHexString, toHexString} from "@chainsafe/ssz"; -import type {SecretKey} from "@chainsafe/bls/types"; -import {PubkeyHex} from "../../src/types.js"; - -/** - * Creates a fastify server with registered remote signer routes. - * Must call .listen() on the server to start - */ -export function createExternalSignerServer(secretKeys: SecretKey[]): ReturnType { - const secretKeyMap = new Map(); - for (const secretKey of secretKeys) { - const pubkeyHex = toHexString(secretKey.toPublicKey().toBytes()); - secretKeyMap.set(pubkeyHex, secretKey); - } - - const server = fastify(); - - server.get("/upcheck", async () => { - return {status: "OK"}; - }); - - server.get("/api/v1/eth2/publicKeys", async () => { - return Array.from(secretKeyMap.keys()); - }); - - /* eslint-disable @typescript-eslint/naming-convention */ - server.post<{ - Params: { - /** BLS public key as a hex string. */ - identifier: string; - }; - Body: { - /** Data to sign as a hex string */ - signingRoot: string; - }; - }>("/api/v1/eth2/sign/:identifier", async (req) => { - const pubkeyHex: string = req.params.identifier; - const signingRootHex: string = req.body.signingRoot; - - const secretKey = secretKeyMap.get(pubkeyHex); - if (!secretKey) { - throw Error(`pubkey not known ${pubkeyHex}`); - } - - return {signature: secretKey.sign(fromHexString(signingRootHex)).toHex()}; - }); - - return server; -} diff --git a/scripts/assert_no_yarn_warnings.sh b/scripts/assert_no_yarn_warnings.sh index 60dace730f92..9b7ff1d76779 100755 --- a/scripts/assert_no_yarn_warnings.sh +++ b/scripts/assert_no_yarn_warnings.sh @@ -5,8 +5,21 @@ OUTPUT=$(yarn install --check-files 2>&1) echo $OUTPUT +MATCH=("warning") + +# There are few yarn warnings we can't find a fix for. Excluding those. +# TODO: Keep checking occasionally if the warnings are fixed upstream. +EXCLUDE=("Pattern \[\".*\"\] is trying to unpack in the same destination") +ARGS=() + +for m in "${MATCH[@]}"; do ARGS+=(-e "$m"); done +for e in "${EXCLUDE[@]}"; do ARGS+=(--exclude "$e"); done +COMMAND="grep -qi ${ARGS[@]}" + +echo "Running $COMMAND" + # grep the output for 'warning' -if echo "$OUTPUT" | grep -qi 'warning'; then +if echo "$OUTPUT" | ${COMMAND}; then echo "There were warnings in yarn install --check-files" exit 1 else diff --git a/scripts/prepare-docs.sh b/scripts/prepare-docs.sh index 5475f22c398e..c46e2596440d 100755 --- a/scripts/prepare-docs.sh +++ b/scripts/prepare-docs.sh @@ -1,17 +1,19 @@ #!/bin/bash DOCS_DIR=docs +ASSETS_DIR=assets # exit when any command fails set -e -# Move autogenerated reference -mkdir -p $DOCS_DIR/reference -mv packages/cli/docs/cli.md $DOCS_DIR/reference/cli.md +# Copy contributing docs +cp CONTRIBUTING.md $DOCS_DIR/pages/contribution/getting-started.md -# Copy contributing doc -cp CONTRIBUTING.md $DOCS_DIR/contributing.md +# Copy package README.md to docs +cp -r packages/light-client/README.md $DOCS_DIR/pages/lightclient-prover/lightclient.md +cp -r packages/prover/README.md $DOCS_DIR/pages/lightclient-prover/prover.md # Copy visual assets -rm -rf $DOCS_DIR/assets -cp -r assets $DOCS_DIR/assets +rm -rf $DOCS_DIR/pages/assets $DOCS_DIR/pages/images +cp -r $ASSETS_DIR $DOCS_DIR/pages/assets +cp -r $DOCS_DIR/images $DOCS_DIR/pages/images diff --git a/scripts/vitest/customMatchers.ts b/scripts/vitest/customMatchers.ts index 04b665bf3242..72735869b1c3 100644 --- a/scripts/vitest/customMatchers.ts +++ b/scripts/vitest/customMatchers.ts @@ -52,4 +52,17 @@ expect.extend({ message: () => message, }; }, + toSatisfy: (received: unknown, func: (received: unknown) => boolean) => { + if (func(received)) { + return { + message: () => "Expected value satisfied the condition", + pass: true, + }; + } + + return { + pass: false, + message: () => "Expected value did not satisfy the condition", + }; + }, }); diff --git a/scripts/vitest/polyfills/perf_hooks.js b/scripts/vitest/polyfills/perf_hooks.js new file mode 100644 index 000000000000..a96781ba23df --- /dev/null +++ b/scripts/vitest/polyfills/perf_hooks.js @@ -0,0 +1,2 @@ +export default null; +export const performance = {}; diff --git a/tsconfig.build.json b/tsconfig.build.json index d4868d14e13a..d767c8eaec8a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -23,6 +23,12 @@ "declaration": true, "declarationMap": true, "incremental": true, - "preserveWatchOutput": true + "preserveWatchOutput": true, + + + // There are two duplicate type definitions included from `chai` and `vitest` packages. + // There is one invalid type declaration introduced from `webdriverio -> got` package. + // TODO: Once we completely remove `chai` and upgrade `webdriverio` we can enable this check again. + "skipLibCheck": true, } } diff --git a/types/vitest/index.d.ts b/types/vitest/index.d.ts index 38ccf5252d52..effc9507161c 100644 --- a/types/vitest/index.d.ts +++ b/types/vitest/index.d.ts @@ -28,8 +28,15 @@ interface CustomMatchers { toBeWithMessage(expected: unknown, message: string): R; } +interface CustomAsymmetricMatchers extends CustomMatchers { + /** + * Non-asymmetric matcher already exists, we just need to add asymmetric version + */ + toSatisfy(func: (received: unknown) => boolean): R; +} + declare module "vitest" { // eslint-disable-next-line @typescript-eslint/no-explicit-any interface Assertion extends CustomMatchers {} - interface AsymmetricMatchersContaining extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomAsymmetricMatchers {} } diff --git a/vitest.base.browser.config.ts b/vitest.base.browser.config.ts new file mode 100644 index 000000000000..5559e0f77b9c --- /dev/null +++ b/vitest.base.browser.config.ts @@ -0,0 +1,49 @@ +import path from "node:path"; +import {defineConfig} from "vitest/config"; +const __dirname = new URL(".", import.meta.url).pathname; +import {nodePolyfills} from "vite-plugin-node-polyfills"; +import topLevelAwait from "vite-plugin-top-level-await"; + +export default defineConfig({ + plugins: [ + topLevelAwait(), + nodePolyfills({ + include: ["buffer", "process", "util", "string_decoder", "url", "querystring", "events"], + globals: {Buffer: true, process: true}, + protocolImports: true, + }), + ], + test: { + include: ["**/*.test.ts"], + exclude: [ + "**/*.node.test.ts", + "**/node_modules/**", + "**/dist/**", + "**/lib/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", + ], + setupFiles: [path.join(__dirname, "./scripts/vitest/customMatchers.ts")], + reporters: ["default", "hanging-process"], + coverage: { + enabled: false, + }, + browser: { + name: "chrome", + headless: true, + provider: "webdriverio", + slowHijackESM: false, + providerOptions: { + capabilities: { + browserVersion: "latest", + }, + }, + }, + }, + resolve: { + alias: { + "node:perf_hooks": path.join(__dirname, "scripts/vitest/polyfills/perf_hooks.js"), + }, + }, +}); diff --git a/vitest.base.config.ts b/vitest.base.config.ts index 34c0d56e40d5..2c12cbf41b9b 100644 --- a/vitest.base.config.ts +++ b/vitest.base.config.ts @@ -4,6 +4,16 @@ const __dirname = new URL(".", import.meta.url).pathname; export default defineConfig({ test: { + pool: "threads", + include: ["**/*.test.ts"], + exclude: [ + "**/*.browser.test.ts", + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", + ], setupFiles: [path.join(__dirname, "./scripts/vitest/customMatchers.ts")], reporters: ["default", "hanging-process"], coverage: { diff --git a/yarn.lock b/yarn.lock index 0e6a8f3757b6..4663c7aaf0ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -346,6 +346,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-validator-identifier@^7.10.4": version "7.14.9" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz" @@ -356,6 +361,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -393,6 +403,11 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz" integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA== +"@babel/parser@^7.23.3": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563" + integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ== + "@babel/template@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz" @@ -426,6 +441,15 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@babel/types@^7.23.3": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.5.tgz#48d730a00c95109fa4393352705954d74fb5b602" + integrity sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@balena/dockerignore@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" @@ -711,115 +735,115 @@ optionalDependencies: global-agent "^3.0.0" -"@esbuild/android-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" - integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== - -"@esbuild/android-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" - integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== - -"@esbuild/android-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" - integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== - -"@esbuild/darwin-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" - integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== - -"@esbuild/darwin-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" - integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== - -"@esbuild/freebsd-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" - integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== - -"@esbuild/freebsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" - integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== - -"@esbuild/linux-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" - integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== - -"@esbuild/linux-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" - integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== - -"@esbuild/linux-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" - integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== - -"@esbuild/linux-loong64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" - integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== - -"@esbuild/linux-mips64el@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" - integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== - -"@esbuild/linux-ppc64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" - integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== - -"@esbuild/linux-riscv64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" - integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== - -"@esbuild/linux-s390x@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" - integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== - -"@esbuild/linux-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" - integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== - -"@esbuild/netbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" - integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== - -"@esbuild/openbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" - integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== - -"@esbuild/sunos-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" - integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== - -"@esbuild/win32-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" - integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== - -"@esbuild/win32-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" - integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== - -"@esbuild/win32-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" - integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== +"@esbuild/android-arm64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz#fb7130103835b6d43ea499c3f30cfb2b2ed58456" + integrity sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA== + +"@esbuild/android-arm@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.8.tgz#b46e4d9e984e6d6db6c4224d72c86b7757e35bcb" + integrity sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA== + +"@esbuild/android-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.8.tgz#a13db9441b5a4f4e4fec4a6f8ffacfea07888db7" + integrity sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A== + +"@esbuild/darwin-arm64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz#49f5718d36541f40dd62bfdf84da9c65168a0fc2" + integrity sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw== + +"@esbuild/darwin-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz#75c5c88371eea4bfc1f9ecfd0e75104c74a481ac" + integrity sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q== + +"@esbuild/freebsd-arm64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz#9d7259fea4fd2b5f7437b52b542816e89d7c8575" + integrity sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw== + +"@esbuild/freebsd-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz#abac03e1c4c7c75ee8add6d76ec592f46dbb39e3" + integrity sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg== + +"@esbuild/linux-arm64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz#c577932cf4feeaa43cb9cec27b89cbe0df7d9098" + integrity sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ== + +"@esbuild/linux-arm@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz#d6014d8b98b5cbc96b95dad3d14d75bb364fdc0f" + integrity sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ== + +"@esbuild/linux-ia32@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz#2379a0554307d19ac4a6cdc15b08f0ea28e7a40d" + integrity sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ== + +"@esbuild/linux-loong64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz#e2a5bbffe15748b49356a6cd7b2d5bf60c5a7123" + integrity sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ== + +"@esbuild/linux-mips64el@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz#1359331e6f6214f26f4b08db9b9df661c57cfa24" + integrity sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q== + +"@esbuild/linux-ppc64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz#9ba436addc1646dc89dae48c62d3e951ffe70951" + integrity sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg== + +"@esbuild/linux-riscv64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz#fbcf0c3a0b20f40b5fc31c3b7695f0769f9de66b" + integrity sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg== + +"@esbuild/linux-s390x@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz#989e8a05f7792d139d5564ffa7ff898ac6f20a4a" + integrity sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg== + +"@esbuild/linux-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz#b187295393a59323397fe5ff51e769ec4e72212b" + integrity sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg== + +"@esbuild/netbsd-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz#c1ec0e24ea82313cb1c7bae176bd5acd5bde7137" + integrity sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw== + +"@esbuild/openbsd-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz#0c5b696ac66c6d70cf9ee17073a581a28af9e18d" + integrity sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ== + +"@esbuild/sunos-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz#2a697e1f77926ff09fcc457d8f29916d6cd48fb1" + integrity sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w== + +"@esbuild/win32-arm64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz#ec029e62a2fca8c071842ecb1bc5c2dd20b066f1" + integrity sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg== + +"@esbuild/win32-ia32@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz#cbb9a3146bde64dc15543e48afe418c7a3214851" + integrity sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw== + +"@esbuild/win32-x64@0.19.8": + version "0.19.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz#c8285183dbdb17008578dbacb6e22748709b4822" + integrity sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -2599,6 +2623,11 @@ picocolors "^1.0.0" tslib "^2.6.0" +"@polka/url@^1.0.0-next.20": + version "1.0.0-next.24" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" + integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -2652,6 +2681,115 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@puppeteer/browsers@1.4.6": + version "1.4.6" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-1.4.6.tgz#1f70fd23d5d2ccce9d29b038e5039d7a1049ca77" + integrity sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ== + dependencies: + debug "4.3.4" + extract-zip "2.0.1" + progress "2.0.3" + proxy-agent "6.3.0" + tar-fs "3.0.4" + unbzip2-stream "1.4.3" + yargs "17.7.1" + +"@puppeteer/browsers@^1.6.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-1.8.0.tgz#fb6ee61de15e7f0e67737aea9f9bab1512dbd7d8" + integrity sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg== + dependencies: + debug "4.3.4" + extract-zip "2.0.1" + progress "2.0.3" + proxy-agent "6.3.1" + tar-fs "3.0.4" + unbzip2-stream "1.4.3" + yargs "17.7.2" + +"@rollup/plugin-inject@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz#616f3a73fe075765f91c5bec90176608bed277a3" + integrity sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + estree-walker "^2.0.2" + magic-string "^0.30.3" + +"@rollup/plugin-virtual@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz#17e17eeecb4c9fa1c0a6e72c9e5f66382fddbb82" + integrity sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A== + +"@rollup/pluginutils@^5.0.1": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c" + integrity sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + +"@rollup/rollup-android-arm-eabi@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz#0ea289f68ff248b50fea5716ca9f65f7d4dba3ae" + integrity sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA== + +"@rollup/rollup-android-arm64@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz#27c8c67fc5de574874085a1b480ac65b3e18378e" + integrity sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA== + +"@rollup/rollup-darwin-arm64@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz#c5735c042980c85495411af7183dd20294763bd8" + integrity sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw== + +"@rollup/rollup-darwin-x64@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz#af844bd54abb73ca3c9cf89a31eec17861d1375d" + integrity sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg== + +"@rollup/rollup-linux-arm-gnueabihf@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz#5e972f63c441eaf859551039b3f18db9b035977d" + integrity sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ== + +"@rollup/rollup-linux-arm64-gnu@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz#f4cfbc71e3b6fdb395b28b1472414e181515c72d" + integrity sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw== + +"@rollup/rollup-linux-arm64-musl@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz#6a94c691830dc29bf708de7c640f494996130893" + integrity sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw== + +"@rollup/rollup-linux-x64-gnu@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz#f07bae3f7dc532d9ea5ab36c9071db329f9a1efb" + integrity sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA== + +"@rollup/rollup-linux-x64-musl@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz#357a34fdbf410af88ce48bd802bea6462bb9a8bc" + integrity sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ== + +"@rollup/rollup-win32-arm64-msvc@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz#b6e97fd38281667e35297033393cd1101f4a31be" + integrity sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ== + +"@rollup/rollup-win32-ia32-msvc@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz#a95db026c640c8128bfd38546d85342f2329beaf" + integrity sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw== + +"@rollup/rollup-win32-x64-msvc@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz#45785b5caf83200a34a9867ba50d69560880c120" + integrity sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A== + "@scure/base@~1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@scure/base/-/base-1.0.0.tgz" @@ -2743,6 +2881,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + "@sinonjs/commons@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" @@ -2785,6 +2928,85 @@ resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@swc/core-darwin-arm64@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.93.tgz#aefd94625451988286bebccb1c072bae0a36bcdb" + integrity sha512-gEKgk7FVIgltnIfDO6GntyuQBBlAYg5imHpRgLxB1zSI27ijVVkksc6QwISzFZAhKYaBWIsFSVeL9AYSziAF7A== + +"@swc/core-darwin-x64@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.93.tgz#18409c6effdf508ddf1ebccfa77d35aaa6cd72f0" + integrity sha512-ZQPxm/fXdDQtn3yrYSL/gFfA8OfZ5jTi33yFQq6vcg/Y8talpZ+MgdSlYM0FkLrZdMTYYTNFiuBQuuvkA+av+Q== + +"@swc/core-linux-arm-gnueabihf@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.93.tgz#23a97bc94a8b2f23fb6cc4bc9d8936899e5eeff5" + integrity sha512-OYFMMI2yV+aNe3wMgYhODxHdqUB/jrK0SEMHHS44GZpk8MuBXEF+Mcz4qjkY5Q1EH7KVQqXb/gVWwdgTHpjM2A== + +"@swc/core-linux-arm64-gnu@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.93.tgz#7a17406a7cf76a959a617626d5ee2634ae9afa26" + integrity sha512-BT4dT78odKnJMNiq5HdjBsv29CiIdcCcImAPxeFqAeFw1LL6gh9nzI8E96oWc+0lVT5lfhoesCk4Qm7J6bty8w== + +"@swc/core-linux-arm64-musl@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.93.tgz#a30be7780090afefd3b8706398418cbe1d23db49" + integrity sha512-yH5fWEl1bktouC0mhh0Chuxp7HEO4uCtS/ly1Vmf18gs6wZ8DOOkgAEVv2dNKIryy+Na++ljx4Ym7C8tSJTrLw== + +"@swc/core-linux-x64-gnu@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.93.tgz#41e903fd82e059952d16051b442cbe65ee5b8cb3" + integrity sha512-OFUdx64qvrGJhXKEyxosHxgoUVgba2ztYh7BnMiU5hP8lbI8G13W40J0SN3CmFQwPP30+3oEbW7LWzhKEaYjlg== + +"@swc/core-linux-x64-musl@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.93.tgz#0866807545c44eac9b3254b374310ad5e1c573f9" + integrity sha512-4B8lSRwEq1XYm6xhxHhvHmKAS7pUp1Q7E33NQ2TlmFhfKvCOh86qvThcjAOo57x8DRwmpvEVrqvpXtYagMN6Ig== + +"@swc/core-win32-arm64-msvc@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.93.tgz#c72411dea2fd4f62a832f71a6e15424d849e7610" + integrity sha512-BHShlxtkven8ZjjvZ5QR6sC5fZCJ9bMujEkiha6W4cBUTY7ce7qGFyHmQd+iPC85d9kD/0cCiX/Xez8u0BhO7w== + +"@swc/core-win32-ia32-msvc@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.93.tgz#05c2b031b976af4ef81f5073ee114254678a5d5d" + integrity sha512-nEwNWnz4JzYAK6asVvb92yeylfxMYih7eMQOnT7ZVlZN5ba9WF29xJ6kcQKs9HRH6MvWhz9+wRgv3FcjlU6HYA== + +"@swc/core-win32-x64-msvc@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.93.tgz#f8748b3fd1879f13084b1b0814edf328c662935c" + integrity sha512-jibQ0zUr4kwJaQVwgmH+svS04bYTPnPw/ZkNInzxS+wFAtzINBYcU8s2PMWbDb2NGYiRSEeoSGyAvS9H+24JFA== + +"@swc/core@^1.3.10": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.93.tgz#be4282aa44deffb0e5081a2613bac00335600630" + integrity sha512-690GRr1wUGmGYZHk7fUduX/JUwViMF2o74mnZYIWEcJaCcd9MQfkhsxPBtjeg6tF+h266/Cf3RPYhsFBzzxXcA== + dependencies: + "@swc/counter" "^0.1.1" + "@swc/types" "^0.1.5" + optionalDependencies: + "@swc/core-darwin-arm64" "1.3.93" + "@swc/core-darwin-x64" "1.3.93" + "@swc/core-linux-arm-gnueabihf" "1.3.93" + "@swc/core-linux-arm64-gnu" "1.3.93" + "@swc/core-linux-arm64-musl" "1.3.93" + "@swc/core-linux-x64-gnu" "1.3.93" + "@swc/core-linux-x64-musl" "1.3.93" + "@swc/core-win32-arm64-msvc" "1.3.93" + "@swc/core-win32-ia32-msvc" "1.3.93" + "@swc/core-win32-x64-msvc" "1.3.93" + +"@swc/counter@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.2.tgz#bf06d0770e47c6f1102270b744e17b934586985e" + integrity sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw== + +"@swc/types@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a" + integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw== + "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -2792,6 +3014,13 @@ dependencies: defer-to-connect "^2.0.0" +"@szmarczak/http-timer@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" + integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + dependencies: + defer-to-connect "^2.0.1" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2802,6 +3031,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz" @@ -2871,19 +3105,12 @@ dependencies: "@types/chai" "*" -"@types/chai-subset@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" - integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw== - dependencies: - "@types/chai" "*" - "@types/chai@*": version "4.2.17" resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.17.tgz" integrity sha512-LaiwWNnYuL8xJlQcE91QB2JoswWZckq9A4b+nMPq8dt8AP96727Nb3X4e74u+E3tm4NLTILNI9MYFsyVc30wSA== -"@types/chai@^4.3.5", "@types/chai@^4.3.6": +"@types/chai@^4.3.6": version "4.3.6" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.6.tgz#7b489e8baf393d5dd1266fb203ddd4ea941259e6" integrity sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw== @@ -2991,9 +3218,14 @@ form-data "^2.5.0" "@types/http-cache-semantics@*": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" - integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#a3ff232bf7d5c55f38e4e45693eda2ebb545794d" + integrity sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA== + +"@types/http-cache-semantics@^4.0.2": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== "@types/http-proxy@^1.17.10": version "1.17.10" @@ -3140,6 +3372,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.14.tgz#a621ad26e7eb076d6846dd3d39557ddf9d89f04b" integrity sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw== +"@types/node@^20.1.0": + version "20.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.1.tgz#d2c96f356c3125fedc983d74c424910c3767141c" + integrity sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -3166,9 +3405,9 @@ safe-buffer "~5.1.1" "@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.2.tgz#8de1b0477fd7c12df77e50832fa51701a8414bd6" + integrity sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA== dependencies: "@types/node" "*" @@ -3310,6 +3549,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== +"@types/which@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae" + integrity sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw== + "@types/ws@^8.5.3": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -3421,65 +3665,147 @@ "@typescript-eslint/types" "6.7.2" eslint-visitor-keys "^3.4.1" -"@vitest/coverage-v8@^0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz#931d9223fa738474e00c08f52b84e0f39cedb6d1" - integrity sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw== +"@vitest/browser@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-1.0.1.tgz#d908e8015c5a449e0db28636c6afb969a8be9fcf" + integrity sha512-zKJvgfZMzahaFrIS5fbYnP2We+KRPJQUfog4mjOCOOVpLbk5DWtDD15XPYKaIY2IydD0ir0aCPrlcKlWGrcNww== + dependencies: + estree-walker "^3.0.3" + magic-string "^0.30.5" + sirv "^2.0.3" + +"@vitest/coverage-v8@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-1.0.1.tgz#b1249ca6e8f2617e56c7a15caa546e8b1abae4c7" + integrity sha512-Z4a7ig4VjUCT/P+LRB3IZrBRXb9xWRUM8rSBH9cKgfrU1Oe01/K2WJKtGshOnQwXZoSfQtwCGpbnHmB/qJwjcw== dependencies: "@ampproject/remapping" "^2.2.1" "@bcoe/v8-coverage" "^0.2.3" - istanbul-lib-coverage "^3.2.0" + debug "^4.3.4" + istanbul-lib-coverage "^3.2.2" istanbul-lib-report "^3.0.1" istanbul-lib-source-maps "^4.0.1" - istanbul-reports "^3.1.5" - magic-string "^0.30.1" + istanbul-reports "^3.1.6" + magic-string "^0.30.5" + magicast "^0.3.2" picocolors "^1.0.0" - std-env "^3.3.3" + std-env "^3.5.0" test-exclude "^6.0.0" - v8-to-istanbul "^9.1.0" + v8-to-istanbul "^9.2.0" -"@vitest/expect@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.6.tgz#608a7b7a9aa3de0919db99b4cc087340a03ea77e" - integrity sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw== +"@vitest/expect@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.0.2.tgz#7fc5ee3fe0e649f5a5e3df1a9744efe0163d1237" + integrity sha512-mAIo/8uddSWkjQMLFcjqZP3WmkwvvN0OtlyZIu33jFnwme3vZds8m8EDMxtj+Uzni2DwtPfHNjJcTM8zTV1f4A== dependencies: - "@vitest/spy" "0.34.6" - "@vitest/utils" "0.34.6" + "@vitest/spy" "1.0.2" + "@vitest/utils" "1.0.2" chai "^4.3.10" -"@vitest/runner@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.6.tgz#6f43ca241fc96b2edf230db58bcde5b974b8dcaf" - integrity sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ== +"@vitest/runner@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.0.2.tgz#aad21c03fdcd1f380564fad37be7d5a2feb2f733" + integrity sha512-ZcHJXPT2kg/9Hc4fNkCbItlsgZSs3m4vQbxB8LCSdzpbG85bExCmSvu6K9lWpMNdoKfAr1Jn0BwS9SWUcGnbTQ== dependencies: - "@vitest/utils" "0.34.6" - p-limit "^4.0.0" + "@vitest/utils" "1.0.2" + p-limit "^5.0.0" pathe "^1.1.1" -"@vitest/snapshot@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.6.tgz#b4528cf683b60a3e8071cacbcb97d18b9d5e1d8b" - integrity sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w== +"@vitest/snapshot@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.0.2.tgz#df11b066c9593e3539640a41f38452a6b5889da1" + integrity sha512-9ClDz2/aV5TfWA4reV7XR9p+hE0e7bifhwxlURugj3Fw0YXeTFzHmKCNEHd6wOIFMfthbGGwhlq7TOJ2jDO4/g== dependencies: - magic-string "^0.30.1" + magic-string "^0.30.5" pathe "^1.1.1" - pretty-format "^29.5.0" + pretty-format "^29.7.0" -"@vitest/spy@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.6.tgz#b5e8642a84aad12896c915bce9b3cc8cdaf821df" - integrity sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ== +"@vitest/spy@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.0.2.tgz#c28205427e77e589e3f0e6017f55d1c5b9defee3" + integrity sha512-YlnHmDntp+zNV3QoTVFI5EVHV0AXpiThd7+xnDEbWnD6fw0TH/J4/+3GFPClLimR39h6nA5m0W4Bjm5Edg4A/A== dependencies: - tinyspy "^2.1.1" + tinyspy "^2.2.0" -"@vitest/utils@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.6.tgz#38a0a7eedddb8e7291af09a2409cb8a189516968" - integrity sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A== +"@vitest/utils@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.0.2.tgz#fbc483a62d13a02fa4e2b470fbf565fdd616a242" + integrity sha512-GPQkGHAnFAP/+seSbB9pCsj339yRrMgILoI5H2sPevTLCYgBq0VRjF8QSllmnQyvf0EontF6KUIt2t5s2SmqoQ== dependencies: - diff-sequences "^29.4.3" - loupe "^2.3.6" - pretty-format "^29.5.0" + diff-sequences "^29.6.3" + loupe "^2.3.7" + pretty-format "^29.7.0" + +"@wdio/config@8.24.12": + version "8.24.12" + resolved "https://registry.yarnpkg.com/@wdio/config/-/config-8.24.12.tgz#07d30aafcf0ef476e9930623b9c8e0f986943d00" + integrity sha512-3HW7qG1rIHzOIybV6oHR1CqLghsN0G3Xzs90ZciGL8dYhtcLtYCHwuWmBw4mkaB5xViU4AmZDuj7ChiG8Cr6Qw== + dependencies: + "@wdio/logger" "8.24.12" + "@wdio/types" "8.24.12" + "@wdio/utils" "8.24.12" + decamelize "^6.0.0" + deepmerge-ts "^5.0.0" + glob "^10.2.2" + import-meta-resolve "^4.0.0" + +"@wdio/logger@8.24.12": + version "8.24.12" + resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-8.24.12.tgz#03cb8bb7ce7ee443e1dcd200a3b44270ae16a1f9" + integrity sha512-QisOiVIWKTUCf1H7S+DOtC+gruhlpimQrUXfWMTeeh672PvAJYnTpOJDWA+BtXfsikkUYFAzAaq8SeMJk8rqKg== + dependencies: + chalk "^5.1.2" + loglevel "^1.6.0" + loglevel-plugin-prefix "^0.8.4" + strip-ansi "^7.1.0" + +"@wdio/logger@^8.11.0", "@wdio/logger@^8.16.17": + version "8.16.17" + resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-8.16.17.tgz#c2055857ed3e3cf12cfad843140fa79264c6a632" + integrity sha512-zeQ41z3T+b4IsrriZZipayXxLNDuGsm7TdExaviNGojPVrIsQUCSd/FvlLHM32b7ZrMyInHenu/zx1cjAZO71g== + dependencies: + chalk "^5.1.2" + loglevel "^1.6.0" + loglevel-plugin-prefix "^0.8.4" + strip-ansi "^7.1.0" + +"@wdio/protocols@8.24.12": + version "8.24.12" + resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-8.24.12.tgz#16c2e3dff4cfc0ed694f3f8142fb68b74609fbf5" + integrity sha512-QnVj3FkapmVD3h2zoZk+ZQ8gevSj9D9MiIQIy8eOnY4FAneYZ9R9GvoW+mgNcCZO8S8++S/jZHetR8n+8Q808g== + +"@wdio/repl@8.24.12": + version "8.24.12" + resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-8.24.12.tgz#b09746ae4f51f7da684312db617e598f2d064d9a" + integrity sha512-321F3sWafnlw93uRTSjEBVuvWCxTkWNDs7ektQS15drrroL3TMeFOynu4rDrIz0jXD9Vas0HCD2Tq/P0uxFLdw== + dependencies: + "@types/node" "^20.1.0" + +"@wdio/types@8.24.12": + version "8.24.12" + resolved "https://registry.yarnpkg.com/@wdio/types/-/types-8.24.12.tgz#c7a182ecc7effdd8ed7ea1967567a84da2c89100" + integrity sha512-SaD3OacDiW06DvSgAQ7sDBbpiI9qZRg7eoVYeBg3uSGVtUq84vTETRhhV7D6xTC00IqZu+mmN2TY5/q+7Gqy7w== + dependencies: + "@types/node" "^20.1.0" + +"@wdio/utils@8.24.12": + version "8.24.12" + resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-8.24.12.tgz#4d4e03d62728b181f44c05584f3988659c6c7a38" + integrity sha512-uzwZyBVgqz0Wz1KL3aOUaQsxT8TNkzxti4NNTSMrU256qAPqc/n75rB7V73QASapCMpy70mZZTsuPgQYYj4ytQ== + dependencies: + "@puppeteer/browsers" "^1.6.0" + "@wdio/logger" "8.24.12" + "@wdio/types" "8.24.12" + decamelize "^6.0.0" + deepmerge-ts "^5.1.0" + edgedriver "^5.3.5" + geckodriver "^4.2.0" + get-port "^7.0.0" + import-meta-resolve "^4.0.0" + locate-app "^2.1.0" + safaridriver "^0.1.0" + split2 "^4.2.0" + wait-port "^1.0.4" "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" @@ -3708,11 +4034,16 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.1.1, acorn-walk@^8.2.0: +acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn-walk@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== + acorn@^8.10.0, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" @@ -3750,6 +4081,13 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" @@ -3923,6 +4261,18 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" +archiver-utils@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-4.0.1.tgz#66ad15256e69589a77f706c90c6dbcc1b2775d2a" + integrity sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg== + dependencies: + glob "^8.0.0" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + archiver@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" @@ -3936,6 +4286,19 @@ archiver@^5.3.1: tar-stream "^2.2.0" zip-stream "^4.1.0" +archiver@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-6.0.1.tgz#d56968d4c09df309435adb5a1bbfc370dae48133" + integrity sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ== + dependencies: + archiver-utils "^4.0.1" + async "^3.2.4" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^5.0.1" + archy@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz" @@ -3971,6 +4334,13 @@ argv@0.0.2: resolved "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz" integrity sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas= +aria-query@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + array-buffer-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" @@ -4109,6 +4479,13 @@ assertion-error@^1.1.0: resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + async-lock@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.0.tgz#c8b6630eff68fbbdd8a5b6eb763dac3bfbb8bf02" @@ -4126,6 +4503,11 @@ async@^3.2.3, async@~3.2.2: resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== +async@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -4230,10 +4612,15 @@ base64url@^3.0.1: resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== +basic-ftp@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.3.tgz#b14c0fe8111ce001ec913686434fe0c2fb461228" + integrity sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g== + bcrypt-pbkdf@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== dependencies: tweetnacl "^0.14.3" @@ -4263,6 +4650,11 @@ benchmark@^2.1.4: lodash "^4.17.4" platform "^1.3.3" +big-integer@^1.6.17: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + big-integer@^1.6.44: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -4285,6 +4677,14 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + bindings@^1.3.0, bindings@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" @@ -4342,6 +4742,11 @@ bls-eth-wasm@^0.4.8: resolved "https://registry.npmjs.org/bls-eth-wasm/-/bls-eth-wasm-0.4.8.tgz" integrity sha512-ye7+G6KFLb3i9xSrLASAoYqOUK5WLB6XA5DD8Sh0UQpZ3T999ylsYbFdoOJpmvTDuBuMi23Vy8Jm0pn/GF01CA== +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.1, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" @@ -4424,6 +4829,13 @@ browser-level@^1.0.1: module-error "^1.0.2" run-parallel-limit "^1.1.0" +browser-resolve@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b" + integrity sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ== + dependencies: + resolve "^1.17.0" + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" @@ -4526,6 +4938,20 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +"buffer-polyfill@npm:buffer@^6.0.3", buffer@^6.0.3: + name buffer-polyfill + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" @@ -4547,7 +4973,7 @@ buffer@4.9.2, buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.4.3, buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -4555,13 +4981,10 @@ buffer@^5.4.3, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== bufio@~1.0.7: version "1.0.7" @@ -4722,6 +5145,24 @@ cacheable-lookup@^5.0.3: resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + dependencies: + "@types/http-cache-semantics" "^4.0.2" + get-stream "^6.0.1" + http-cache-semantics "^4.1.1" + keyv "^4.5.3" + mimic-response "^4.0.0" + normalize-url "^8.0.0" + responselike "^3.0.0" + cacheable-request@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" @@ -4799,7 +5240,7 @@ chai-as-promised@^7.1.1: dependencies: check-error "^1.0.2" -chai@^4.3.10: +chai@^4.3.10, chai@^4.3.7, chai@^4.3.8: version "4.3.10" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g== @@ -4812,31 +5253,12 @@ chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" -chai@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" - integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^4.1.2" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" - -chai@^4.3.8: - version "4.3.8" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.8.tgz#40c59718ad6928da6629c70496fe990b2bb5b17c" - integrity sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ== +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^4.1.2" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" + traverse ">=0.3.0 <0.4" chalk@4.1.0: version "4.1.0" @@ -4876,6 +5298,11 @@ chalk@^5.0.0, chalk@^5.2.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== +chalk@^5.1.2: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -4923,6 +5350,13 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== +chromium-bidi@0.4.16: + version "0.4.16" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.16.tgz#8a67bfdf6bb8804efc22765a82859d20724b46ab" + integrity sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA== + dependencies: + mitt "3.0.0" + ci-info@^3.2.0, ci-info@^3.6.1: version "3.8.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" @@ -5028,9 +5462,9 @@ clone-deep@4.0.1: shallow-clone "^3.0.0" clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== dependencies: mimic-response "^1.0.0" @@ -5195,6 +5629,16 @@ compress-commons@^4.1.0: normalize-path "^3.0.0" readable-stream "^3.6.0" +compress-commons@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-5.0.1.tgz#e46723ebbab41b50309b27a0e0f6f3baed2d6590" + integrity sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^5.0.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -5411,6 +5855,14 @@ crc32-stream@^4.0.2: crc-32 "^1.2.0" readable-stream "^3.4.0" +crc32-stream@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-5.0.0.tgz#a97d3a802c8687f101c27cc17ca5253327354720" + integrity sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz" @@ -5442,11 +5894,18 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-require@^1.1.0: +create-require@^1.1.0, create-require@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -5491,6 +5950,23 @@ crypto-browserify@^3.11.0, crypto-browserify@^3.12.0: randombytes "^2.0.0" randomfill "^1.0.3" +css-shorthand-properties@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz#1c808e63553c283f289f2dd56fcee8f3337bd935" + integrity sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A== + +css-value@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea" + integrity sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q== + +cssstyle@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-3.0.0.tgz#17ca9c87d26eac764bb8cfd00583cff21ce0277a" + integrity sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg== + dependencies: + rrweb-cssom "^0.6.0" + csv-parse@^4.16.0: version "4.16.0" resolved "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.0.tgz" @@ -5511,6 +5987,24 @@ dargs@^7.0.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +data-uri-to-buffer@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz#540bd4c8753a25ee129035aebdedf63b078703c7" + integrity sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg== + +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + datastore-core@^9.0.0, datastore-core@^9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/datastore-core/-/datastore-core-9.1.1.tgz#613db89a9bb2624943811dd39b831125319fab79" @@ -5618,6 +6112,16 @@ decamelize@^4.0.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +decamelize@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-6.0.0.tgz#8cad4d916fde5c41a264a43d0ecc56fe3d31749e" + integrity sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA== + +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" @@ -5630,7 +6134,7 @@ dedent@0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== -deep-eql@^4.1.2, deep-eql@^4.1.3: +deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== @@ -5642,6 +6146,11 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge-ts@^5.0.0, deepmerge-ts@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a" + integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw== + deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -5686,7 +6195,7 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -defer-to-connect@^2.0.0: +defer-to-connect@^2.0.0, defer-to-connect@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== @@ -5727,6 +6236,15 @@ define-properties@^1.2.0: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + delay@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-6.0.0.tgz#43749aefdf6cabd9e17b0d00bd3904525137e607" @@ -5762,6 +6280,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + des.js@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz" @@ -5785,6 +6308,16 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +devtools-protocol@0.0.1147663: + version "0.0.1147663" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz#4ec5610b39a6250d1f87e6b9c7e16688ed0ac78e" + integrity sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ== + +devtools-protocol@^0.0.1233178: + version "0.0.1233178" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1233178.tgz#dfc83cdc487c0cae8f059047293be9d6267a19f9" + integrity sha512-jmMfyaqlzddwmDaSR1AQ+5ek+f7rupZdxKuPdkRcoxrZoF70Idg/4dTgXA08TLPmwAwB54gh49Wm2l/gRM0eUg== + dezalgo@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -5803,7 +6336,7 @@ di@^0.0.1: resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== -diff-sequences@^29.4.3, diff-sequences@^29.6.3: +diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== @@ -5911,6 +6444,11 @@ domain-browser@^1.1.1: resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== +domain-browser@^4.22.0: + version "4.22.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.22.0.tgz#6ddd34220ec281f9a65d3386d267ddd35c491f9f" + integrity sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw== + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -5928,6 +6466,13 @@ dotenv@~16.3.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + duplexer@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -5938,6 +6483,26 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +edge-paths@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/edge-paths/-/edge-paths-3.0.5.tgz#9a35361d701d9b5dc07f641cebe8da01ede80937" + integrity sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg== + dependencies: + "@types/which" "^2.0.1" + which "^2.0.2" + +edgedriver@^5.3.5: + version "5.3.8" + resolved "https://registry.yarnpkg.com/edgedriver/-/edgedriver-5.3.8.tgz#ba304bd05a6696e0121df14b49c428af5eebf4e3" + integrity sha512-FWLPDuwJDeGGgtmlqTXb4lQi/HV9yylLo1F9O1g9TLqSemA5T6xH28seUIfyleVirLFtDQyKNUxKsMhMT4IfnA== + dependencies: + "@wdio/logger" "^8.16.17" + decamelize "^6.0.0" + edge-paths "^3.0.5" + node-fetch "^3.3.2" + unzipper "^0.10.14" + which "^4.0.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -6060,6 +6625,11 @@ ent@~2.2.0: resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -6293,33 +6863,33 @@ es6-object-assign@^1.1.0: resolved "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= -esbuild@^0.18.10: - version "0.18.20" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" - integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== +esbuild@^0.19.3: + version "0.19.8" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.8.tgz#ad05b72281d84483fa6b5345bd246c27a207b8f1" + integrity sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w== optionalDependencies: - "@esbuild/android-arm" "0.18.20" - "@esbuild/android-arm64" "0.18.20" - "@esbuild/android-x64" "0.18.20" - "@esbuild/darwin-arm64" "0.18.20" - "@esbuild/darwin-x64" "0.18.20" - "@esbuild/freebsd-arm64" "0.18.20" - "@esbuild/freebsd-x64" "0.18.20" - "@esbuild/linux-arm" "0.18.20" - "@esbuild/linux-arm64" "0.18.20" - "@esbuild/linux-ia32" "0.18.20" - "@esbuild/linux-loong64" "0.18.20" - "@esbuild/linux-mips64el" "0.18.20" - "@esbuild/linux-ppc64" "0.18.20" - "@esbuild/linux-riscv64" "0.18.20" - "@esbuild/linux-s390x" "0.18.20" - "@esbuild/linux-x64" "0.18.20" - "@esbuild/netbsd-x64" "0.18.20" - "@esbuild/openbsd-x64" "0.18.20" - "@esbuild/sunos-x64" "0.18.20" - "@esbuild/win32-arm64" "0.18.20" - "@esbuild/win32-ia32" "0.18.20" - "@esbuild/win32-x64" "0.18.20" + "@esbuild/android-arm" "0.19.8" + "@esbuild/android-arm64" "0.19.8" + "@esbuild/android-x64" "0.19.8" + "@esbuild/darwin-arm64" "0.19.8" + "@esbuild/darwin-x64" "0.19.8" + "@esbuild/freebsd-arm64" "0.19.8" + "@esbuild/freebsd-x64" "0.19.8" + "@esbuild/linux-arm" "0.19.8" + "@esbuild/linux-arm64" "0.19.8" + "@esbuild/linux-ia32" "0.19.8" + "@esbuild/linux-loong64" "0.19.8" + "@esbuild/linux-mips64el" "0.19.8" + "@esbuild/linux-ppc64" "0.19.8" + "@esbuild/linux-riscv64" "0.19.8" + "@esbuild/linux-s390x" "0.19.8" + "@esbuild/linux-x64" "0.19.8" + "@esbuild/netbsd-x64" "0.19.8" + "@esbuild/openbsd-x64" "0.19.8" + "@esbuild/sunos-x64" "0.19.8" + "@esbuild/win32-arm64" "0.19.8" + "@esbuild/win32-ia32" "0.19.8" + "@esbuild/win32-x64" "0.19.8" escalade@^3.1.1: version "3.1.1" @@ -6346,6 +6916,17 @@ escape-string-regexp@^5.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + eslint-import-resolver-node@^0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" @@ -6555,7 +7136,7 @@ espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -6584,6 +7165,18 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -6696,17 +7289,12 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.4, eventemitter3@^4.0.7: events@1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - -events@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz" - integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== -events@^3.2.0, events@^3.3.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@^2.0.2: @@ -6767,6 +7355,21 @@ execa@^7.1.1: signal-exit "^3.0.7" strip-final-newline "^3.0.0" +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz" @@ -6793,7 +7396,7 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extract-zip@^2.0.1: +extract-zip@2.0.1, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -6814,6 +7417,11 @@ fast-decode-uri-component@^1.0.1: resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -6966,6 +7574,14 @@ fecha@^4.2.0: resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz" integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figures@3.2.0, figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -7142,6 +7758,11 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== + form-data@^2.5.0: version "2.5.1" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz" @@ -7169,6 +7790,13 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + formidable@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" @@ -7250,6 +7878,21 @@ fsevents@~2.3.2: resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -7314,6 +7957,20 @@ gauge@^4.0.3: strip-ansi "^6.0.1" wide-align "^1.1.5" +geckodriver@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-4.2.1.tgz#03ad628241417737b962966aa8f8b13fa0f8bf75" + integrity sha512-4m/CRk0OI8MaANRuFIahvOxYTSjlNAO2p9JmE14zxueknq6cdtB5M9UGRQ8R9aMV0bLGNVHHDnDXmoXdOwJfWg== + dependencies: + "@wdio/logger" "^8.11.0" + decamelize "^6.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + node-fetch "^3.3.1" + tar-fs "^3.0.4" + unzipper "^0.10.14" + which "^4.0.0" + generate-function@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" @@ -7415,6 +8072,11 @@ get-port@5.1.1, get-port@^5.1.1: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== +get-port@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-7.0.0.tgz#ffcd83da826146529e307a341d7801cae351daff" + integrity sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw== + get-stream@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718" @@ -7432,6 +8094,11 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -7447,6 +8114,16 @@ get-tsconfig@^4.5.0: dependencies: resolve-pkg-maps "^1.0.0" +get-uri@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.2.tgz#e019521646f4a8ff6d291fbaea2c46da204bb75b" + integrity sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.0" + debug "^4.3.4" + fs-extra "^8.1.0" + git-raw-commits@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-3.0.0.tgz#5432f053a9744f67e8db03dbc48add81252cfdeb" @@ -7560,6 +8237,17 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + glob@^8.0.1: version "8.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" @@ -7658,7 +8346,24 @@ got@^11.8.5, got@^11.8.6: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@4.2.11: +got@^12.6.1: + version "12.6.1" + resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" + integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + cacheable-lookup "^7.0.0" + cacheable-request "^10.2.8" + decompress-response "^6.0.0" + form-data-encoder "^2.1.2" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^3.0.0" + +graceful-fs@4.2.11, graceful-fs@^4.2.2: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7678,7 +8383,12 @@ graceful-fs@^4.2.6: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== -graphemer@^1.4.0: +grapheme-splitter@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== @@ -7828,6 +8538,13 @@ hosted-git-info@^6.0.0: dependencies: lru-cache "^7.5.1" +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" @@ -7867,6 +8584,14 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673" + integrity sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" @@ -7884,6 +8609,14 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" +http2-wrapper@^2.1.10: + version "2.2.1" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" + integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -7897,6 +8630,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" + integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -7907,6 +8648,11 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -7921,7 +8667,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@0.6.3, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -7990,6 +8736,11 @@ import-local@3.1.0: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" +import-meta-resolve@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz#0b1195915689f60ab00f830af0f15cc841e8919e" + integrity sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -8013,7 +8764,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -8137,11 +8888,16 @@ ip-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-5.0.0.tgz#cd313b2ae9c80c07bd3851e12bf4fa4dc5480632" integrity sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== -ip@^1.1.5: +ip@^1.1.5, ip@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -8406,6 +9162,11 @@ is-plain-obj@^2.1.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -8418,6 +9179,11 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-property@^1.0.0, is-property@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -8569,11 +9335,21 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic-timers-promises@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz#e4137c24dbc54892de8abae3a4b5c1ffff381598" + integrity sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ== + isomorphic-ws@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" @@ -8589,6 +9365,11 @@ istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== +istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + istanbul-lib-hook@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz" @@ -8663,7 +9444,7 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -istanbul-reports@^3.1.5, istanbul-reports@^3.1.6: +istanbul-reports@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== @@ -8929,6 +9710,33 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsdom@^23.0.1: + version "23.0.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-23.0.1.tgz#ede7ff76e89ca035b11178d200710d8982ebfee0" + integrity sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ== + dependencies: + cssstyle "^3.0.0" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.7" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.3" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.14.2" + xml-name-validator "^5.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -9149,10 +9957,10 @@ keypress@0.1.x: resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.1.0.tgz#4a3188d4291b66b4f65edb99f806aa9ae293592a" integrity sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA== -keyv@^4.0.0: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== +keyv@^4.0.0, keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" @@ -9166,6 +9974,11 @@ kuler@^2.0.0: resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +ky@^0.33.0: + version "0.33.3" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" + integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== + lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -9396,6 +10209,11 @@ lines-and-columns@~2.0.3: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== + load-json-file@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" @@ -9426,10 +10244,22 @@ loady@~0.0.5: resolved "https://registry.npmjs.org/loady/-/loady-0.0.5.tgz" integrity sha512-uxKD2HIj042/HBx77NBcmEPsD+hxCgAtjEWlYNScuUjIsh/62Uyu39GOR68TBR68v+jqDL9zfftCWoUo4y03sQ== -local-pkg@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963" - integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g== +local-pkg@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" + integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + dependencies: + mlly "^1.4.2" + pkg-types "^1.0.3" + +locate-app@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/locate-app/-/locate-app-2.1.0.tgz#97bbbeb3be59eec55368d20f69c77ebaaddacac1" + integrity sha512-rcVo/iLUxrd9d0lrmregK/Z5Y5NCpSwf9KlMbPpOHmKmdxdQY1Fj8NDQ5QymJTryCsBLqwmniFv2f3JKbk9Bvg== + dependencies: + n12 "0.4.0" + type-fest "2.13.0" + userhome "1.0.0" locate-path@^2.0.0: version "2.0.0" @@ -9472,6 +10302,11 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -9542,6 +10377,11 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== +lodash.zip@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" + integrity sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg== + lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -9586,19 +10426,22 @@ logform@^2.3.2, logform@^2.4.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" +loglevel-plugin-prefix@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644" + integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g== + +loglevel@^1.6.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + long@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/long/-/long-5.2.0.tgz#2696dadf4b4da2ce3f6f6b89186085d94d52fd61" integrity sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w== -loupe@^2.3.1: - version "2.3.4" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" - integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== - dependencies: - get-func-name "^2.0.0" - -loupe@^2.3.6: +loupe@^2.3.6, loupe@^2.3.7: version "2.3.6" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== @@ -9607,9 +10450,14 @@ loupe@^2.3.6: lowercase-keys@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lowercase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" + integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -9624,6 +10472,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru-cache@^7.4.4, lru-cache@^7.5.1: version "7.14.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.0.tgz#21be64954a4680e303a09e9468f880b98a0b3c7f" @@ -9639,13 +10492,22 @@ lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== -magic-string@^0.30.1: - version "0.30.4" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.4.tgz#c2c683265fc18dda49b56fc7318d33ca0332c98c" - integrity sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg== +magic-string@^0.30.3, magic-string@^0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" +magicast@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.2.tgz#42dcade5573ed8f10f5540f9d04964e21dba9130" + integrity sha512-Fjwkl6a0syt9TFN0JSYpOybxiMCkYNEeOTnOTNRbjphirLakznZXAqrXgj/7GG3D1dvETONNwrBfinvAbpunDg== + dependencies: + "@babel/parser" "^7.23.3" + "@babel/types" "^7.23.3" + source-map-js "^1.0.2" + make-dir@4.0.0, make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -9903,7 +10765,7 @@ mimic-fn@^4.0.0: mimic-response@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== mimic-response@^3.1.0: @@ -9911,6 +10773,11 @@ mimic-response@^3.1.0: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -10102,9 +10969,9 @@ minstache@~1.2.0: dependencies: commander "1.0.4" -mitt@^3.0.0: +mitt@3.0.0, mitt@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== mkdirp-classic@^0.5.2: @@ -10112,7 +10979,7 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.5: +"mkdirp@>=0.5 0", mkdirp@^0.5.5: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -10124,7 +10991,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mlly@^1.2.0, mlly@^1.4.0: +mlly@^1.2.0, mlly@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== @@ -10198,6 +11065,11 @@ mortice@^3.0.1: p-queue "^7.2.0" p-timeout "^6.0.0" +mrmime@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -10262,6 +11134,11 @@ mute-stream@1.0.0, mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== +n12@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/n12/-/n12-0.4.0.tgz#363058560b435e6857b5e039ed5eab08c5122e5e" + integrity sha512-p/hj4zQ8d3pbbFLQuN1K9honUxiDDhueOWyFLw/XgBv+wZCE44bcLH4CIcsolOceJQduh4Jf7m/LfaTxyGmGtQ== + nan@^2.16.0, nan@^2.17.0: version "2.17.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" @@ -10272,10 +11149,10 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== nanoid@^4.0.0: version "4.0.2" @@ -10343,6 +11220,11 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz" @@ -10355,6 +11237,15 @@ node-fetch@^2.6.12: dependencies: whatwg-url "^5.0.0" +node-fetch@^3.3.1, node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-forge@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -10465,6 +11356,39 @@ node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== +node-stdlib-browser@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/node-stdlib-browser/-/node-stdlib-browser-1.2.0.tgz#5ddcfdf4063b88fb282979a1aa6ddab9728d5e4c" + integrity sha512-VSjFxUhRhkyed8AtLwSCkMrJRfQ3e2lGtG3sP6FEgaLKBBbxM/dLfjRe1+iLhjvyLFW3tBQ8+c0pcOtXGbAZJg== + dependencies: + assert "^2.0.0" + browser-resolve "^2.0.0" + browserify-zlib "^0.2.0" + buffer "^5.7.1" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + create-require "^1.1.1" + crypto-browserify "^3.11.0" + domain-browser "^4.22.0" + events "^3.0.0" + https-browserify "^1.0.0" + isomorphic-timers-promises "^1.0.1" + os-browserify "^0.3.0" + path-browserify "^1.0.1" + pkg-dir "^5.0.0" + process "^0.11.10" + punycode "^1.4.1" + querystring-es3 "^0.2.1" + readable-stream "^3.6.0" + stream-browserify "^3.0.0" + stream-http "^3.2.0" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.1" + url "^0.11.0" + util "^0.12.4" + vm-browserify "^1.0.1" + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -10519,6 +11443,11 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +normalize-url@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.0.tgz#593dbd284f743e8dcf6a5ddf8fadff149c82701a" + integrity sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw== + npm-bundled@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz" @@ -10671,6 +11600,11 @@ npmlog@^6.0.2: gauge "^4.0.3" set-blocking "^2.0.0" +nwsapi@^2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" + integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== + nx@16.9.0, "nx@>=16.5.1 < 17": version "16.9.0" resolved "https://registry.yarnpkg.com/nx/-/nx-16.9.0.tgz#fad51967bb80c12b311f3699292566cf445232f0" @@ -10976,6 +11910,11 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + p-defer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-4.0.0.tgz#8082770aeeb10eb6b408abe91866738741ddd5d2" @@ -11014,6 +11953,13 @@ p-limit@^4.0.0: dependencies: yocto-queue "^1.0.0" +p-limit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" + integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -11143,6 +12089,29 @@ p-waterfall@2.1.1: dependencies: p-reduce "^2.0.0" +pac-proxy-agent@^7.0.0, pac-proxy-agent@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz#6b9ddc002ec3ff0ba5fdf4a8a21d363bcc612d75" + integrity sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.0.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + pac-resolver "^7.0.0" + socks-proxy-agent "^8.0.2" + +pac-resolver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.0.tgz#79376f1ca26baf245b96b34c339d79bff25e900c" + integrity sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg== + dependencies: + degenerator "^5.0.0" + ip "^1.1.8" + netmask "^2.0.2" + package-hash@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz" @@ -11237,6 +12206,13 @@ parse-url@^8.1.0: dependencies: parse-path "^7.0.0" +parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -11422,6 +12398,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + pkg-types@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" @@ -11436,12 +12419,12 @@ platform@^1.3.3: resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -postcss@^8.4.27: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +postcss@^8.4.32: + version "8.4.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9" + integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw== dependencies: - nanoid "^3.3.6" + nanoid "^3.3.7" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -11462,7 +12445,7 @@ prettier@^3.0.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== -pretty-format@^29.5.0, pretty-format@^29.7.0: +pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== @@ -11508,7 +12491,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -progress@^2.0.3: +progress@2.0.3, progress@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -11614,6 +12597,34 @@ proxy-addr@^2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-agent@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.3.0.tgz#72f7bb20eb06049db79f7f86c49342c34f9ba08d" + integrity sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.0" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.1" + +proxy-agent@6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.3.1.tgz#40e7b230552cf44fd23ffaf7c59024b692612687" + integrity sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.1" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.2" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -11654,7 +12665,7 @@ punycode@1.3.2: resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4, punycode@^1.3.2: +punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -11664,6 +12675,23 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +puppeteer-core@^20.9.0: + version "20.9.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-20.9.0.tgz#6f4b420001b64419deab38d398a4d9cd071040e6" + integrity sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg== + dependencies: + "@puppeteer/browsers" "1.4.6" + chromium-bidi "0.4.16" + cross-fetch "4.0.0" + debug "4.3.4" + devtools-protocol "0.0.1147663" + ws "8.13.0" + qjobs@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" @@ -11683,7 +12711,12 @@ qs@^6.11.0, qs@^6.11.1: dependencies: side-channel "^1.0.4" -querystring-es3@^0.2.0: +query-selector-shadow-dom@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" + integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== + +querystring-es3@^0.2.0, querystring-es3@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz" integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= @@ -11693,6 +12726,11 @@ querystring@0.2.0: resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -11883,6 +12921,13 @@ readdir-glob@^1.0.0: dependencies: minimatch "^5.1.0" +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -11965,7 +13010,7 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -resolve-alpn@^1.0.0: +resolve-alpn@^1.0.0, resolve-alpn@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== @@ -12015,6 +13060,15 @@ resolve@^1.10.1: dependencies: path-parse "^1.0.6" +resolve@^1.17.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.3.2: version "1.20.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz" @@ -12030,6 +13084,20 @@ responselike@^2.0.0: dependencies: lowercase-keys "^2.0.0" +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + dependencies: + lowercase-keys "^3.0.0" + +resq@^1.9.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/resq/-/resq-1.11.0.tgz#edec8c58be9af800fd628118c0ca8815283de196" + integrity sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw== + dependencies: + fast-deep-equal "^2.0.1" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -12085,6 +13153,18 @@ rfdc@^1.1.4, rfdc@^1.2.0, rfdc@^1.3.0: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rgb2hex@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.5.tgz#f82230cd3ab1364fa73c99be3a691ed688f8dbdc" + integrity sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw== + +rimraf@2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -12126,13 +13206,30 @@ roarr@^2.15.3: semver-compare "^1.0.0" sprintf-js "^1.1.2" -rollup@^3.27.1: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== +rollup@^4.2.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.6.1.tgz#351501c86b5b4f976dde8c5837516452b59921f8" + integrity sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ== optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.6.1" + "@rollup/rollup-android-arm64" "4.6.1" + "@rollup/rollup-darwin-arm64" "4.6.1" + "@rollup/rollup-darwin-x64" "4.6.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.6.1" + "@rollup/rollup-linux-arm64-gnu" "4.6.1" + "@rollup/rollup-linux-arm64-musl" "4.6.1" + "@rollup/rollup-linux-x64-gnu" "4.6.1" + "@rollup/rollup-linux-x64-musl" "4.6.1" + "@rollup/rollup-win32-arm64-msvc" "4.6.1" + "@rollup/rollup-win32-ia32-msvc" "4.6.1" + "@rollup/rollup-win32-x64-msvc" "4.6.1" fsevents "~2.3.2" +rrweb-cssom@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" + integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== + run-applescript@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" @@ -12185,6 +13282,11 @@ rxjs@^7.8.0: dependencies: tslib "^2.1.0" +safaridriver@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/safaridriver/-/safaridriver-0.1.0.tgz#8ff901e847b003c6a52b534028f57cddc82d6b14" + integrity sha512-azzzIP3gR1TB9bVPv7QO4Zjw0rR1BWEU/s2aFdUMN48gxDjxEB13grAEuXDmkKPgE74cObymDxmAmZnL3clj4w== + safe-array-concat@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" @@ -12248,6 +13350,13 @@ sax@>=0.6.0: resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" @@ -12314,6 +13423,13 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semve dependencies: lru-cache "^6.0.0" +serialize-error@^11.0.1: + version "11.0.3" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-11.0.3.tgz#b54f439e15da5b4961340fbbd376b6b04aa52e92" + integrity sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g== + dependencies: + type-fest "^2.12.2" + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -12354,7 +13470,7 @@ set-function-name@^2.0.0: functions-have-names "^1.2.3" has-property-descriptors "^1.0.0" -setimmediate@^1.0.4, setimmediate@^1.0.5: +setimmediate@^1.0.4, setimmediate@^1.0.5, setimmediate@~1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -12427,7 +13543,7 @@ signal-exit@3.0.7, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -12479,6 +13595,15 @@ sinon@^16.0.0: nise "^5.1.4" supports-color "^7.2.0" +sirv@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446" + integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA== + dependencies: + "@polka/url" "^1.0.0-next.20" + mrmime "^1.0.0" + totalist "^3.0.0" + slash@3.0.0, slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -12557,6 +13682,15 @@ socks-proxy-agent@^7.0.0: debug "^4.3.3" socks "^2.6.2" +socks-proxy-agent@^8.0.1, socks-proxy-agent@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz#5acbd7be7baf18c46a3f293a840109a430a640ad" + integrity sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + socks "^2.7.1" + socks@^2.6.1, socks@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz" @@ -12565,6 +13699,14 @@ socks@^2.6.1, socks@^2.6.2: ip "^1.1.5" smart-buffer "^4.2.0" +socks@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + sonic-boom@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c" @@ -12597,7 +13739,7 @@ source-map@^0.5.0, source-map@~0.5.3: resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -12652,7 +13794,7 @@ split2@^3.2.2: dependencies: readable-stream "^3.0.0" -split2@^4.0.0: +split2@^4.0.0, split2@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== @@ -12734,10 +13876,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.3.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" - integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== +std-env@^3.5.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e" + integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg== stdin-discarder@^0.1.0: version "0.1.0" @@ -12958,6 +14100,13 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -12995,7 +14144,7 @@ strip-json-comments@^2.0.0: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strip-literal@^1.0.1: +strip-literal@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== @@ -13073,6 +14222,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + synckit@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" @@ -13091,7 +14245,7 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@^3.0.4: +tar-fs@3.0.4, tar-fs@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.4.tgz#a21dc60a2d5d9f55e0089ccd78124f1d3771dbbf" integrity sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w== @@ -13121,7 +14275,7 @@ tar-stream@^2.0.0, tar-stream@^2.2.0, tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar-stream@^3.1.5: +tar-stream@^3.0.0, tar-stream@^3.1.5: version "3.1.6" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.6.tgz#6520607b55a06f4a2e2e04db360ba7d338cc5bab" integrity sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg== @@ -13257,7 +14411,7 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -13286,17 +14440,17 @@ tiny-lru@^11.0.1: dependencies: esm "^3.2.25" -tinybench@^2.5.0: +tinybench@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.1.tgz#3408f6552125e53a5a48adee31261686fd71587e" integrity sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg== -tinypool@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021" - integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww== +tinypool@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.1.tgz#b6c4e4972ede3e3e5cda74a3da1679303d386b03" + integrity sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg== -tinyspy@^2.1.1: +tinyspy@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce" integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg== @@ -13342,6 +14496,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + tough-cookie@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz" @@ -13360,11 +14519,33 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" +tough-cookie@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -13465,6 +14646,11 @@ tslib@^2.0.0, tslib@^2.2.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tslib@^2.0.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^2.1.0: version "2.2.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz" @@ -13485,6 +14671,11 @@ tty-browserify@0.0.0: resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= +tty-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" + integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== + tuf-js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-1.1.7.tgz#21b7ae92a9373015be77dfe0cb282a80ec3bbe43" @@ -13511,11 +14702,16 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb" + integrity sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw== + type-fest@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" @@ -13551,6 +14747,11 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.12.2: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-fest@^3.0.0: version "3.7.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.7.2.tgz#08f83ee3229b63077e95c9035034d32905969457" @@ -13694,6 +14895,19 @@ unbox-primitive@^1.0.0, unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici@^5.12.0: version "5.22.1" resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" @@ -13739,6 +14953,11 @@ universalify@^0.1.0, universalify@^0.1.2: resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -13754,6 +14973,22 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== +unzipper@^0.10.14: + version "0.10.14" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1" + integrity sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + upath@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -13774,6 +15009,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url@0.10.3: version "0.10.3" resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz" @@ -13797,6 +15040,11 @@ urlgrey@1.0.0: dependencies: fast-url-parser "^1.1.3" +userhome@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/userhome/-/userhome-1.0.0.tgz#b6491ff12d21a5e72671df9ccc8717e1c6688c0b" + integrity sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig== + utf8-byte-length@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz" @@ -13833,7 +15081,7 @@ util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" -util@^0.12.5: +util@^0.12.4, util@^0.12.5: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== @@ -13896,10 +15144,10 @@ v8-to-istanbul@^9.0.0: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" -v8-to-istanbul@^9.1.0: - version "9.1.3" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz#ea456604101cd18005ac2cae3cdd1aa058a6306b" - integrity sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg== +v8-to-istanbul@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" + integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" @@ -13932,62 +15180,77 @@ vary@^1: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -vite-node@0.34.6: - version "0.34.6" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.6.tgz#34d19795de1498562bf21541a58edcd106328a17" - integrity sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA== +vite-node@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.0.2.tgz#5e6096e31b851f245ccbd353bf3939130dfd0224" + integrity sha512-h7BbMJf46fLvFW/9Ygo3snkIBEHFh6fHpB4lge98H5quYrDhPFeI3S0LREz328uqPWSnii2yeJXktQ+Pmqk5BQ== dependencies: cac "^6.7.14" debug "^4.3.4" - mlly "^1.4.0" pathe "^1.1.1" picocolors "^1.0.0" - vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" + vite "^5.0.0" -"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0": - version "4.4.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.11.tgz#babdb055b08c69cfc4c468072a2e6c9ca62102b0" - integrity sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A== +vite-plugin-node-polyfills@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.17.0.tgz#1044a4955174ddaeedbc3679b179e1efac9da006" + integrity sha512-iPmPn7376e5u6QvoTSJa16hf5Q0DFwHFXJk2uYpsNlmI3JdPms7hWyh55o+OysJ5jo9J5XPhLC9sMOYifwFd1w== dependencies: - esbuild "^0.18.10" - postcss "^8.4.27" - rollup "^3.27.1" - optionalDependencies: - fsevents "~2.3.2" + "@rollup/plugin-inject" "^5.0.5" + buffer-polyfill "npm:buffer@^6.0.3" + node-stdlib-browser "^1.2.0" + process "^0.11.10" -vitest-when@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/vitest-when/-/vitest-when-0.2.0.tgz#3b3234efa6be0f976616f54e35357b56ed5e5f5f" - integrity sha512-BS1+L6HPwV3cMQB+pGa1Zr7gFkKX1TG8GbdgzpTlyW19nvWBmqDZW5GucS79K/lEu0ULWOUceHM56dnr8P/ajg== +vite-plugin-top-level-await@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.3.1.tgz#7e7293be01489b508692627529c0a3b3218a23a3" + integrity sha512-55M1h4NAwkrpxPNOJIBzKZFihqLUzIgnElLSmPNPMR2Fn9+JHKaNg3sVX1Fq+VgvuBksQYxiD3OnwQAUu7kaPQ== + dependencies: + "@rollup/plugin-virtual" "^3.0.1" + "@swc/core" "^1.3.10" + uuid "^9.0.0" -vitest@^0.34.6: - version "0.34.6" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.6.tgz#44880feeeef493c04b7f795ed268f24a543250d7" - integrity sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q== +vite@^5.0.0: + version "5.0.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c" + integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ== dependencies: - "@types/chai" "^4.3.5" - "@types/chai-subset" "^1.3.3" - "@types/node" "*" - "@vitest/expect" "0.34.6" - "@vitest/runner" "0.34.6" - "@vitest/snapshot" "0.34.6" - "@vitest/spy" "0.34.6" - "@vitest/utils" "0.34.6" - acorn "^8.9.0" - acorn-walk "^8.2.0" + esbuild "^0.19.3" + postcss "^8.4.32" + rollup "^4.2.0" + optionalDependencies: + fsevents "~2.3.3" + +vitest-when@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/vitest-when/-/vitest-when-0.3.0.tgz#663d4274f1e7302bd24ec00dda8269d20b2eff04" + integrity sha512-wYfmzd+GkvdNNhbeb/40PnKpetUP5I7qxvdbu1OAXRXaLrnLfSrJTa/dMIbqqrc8SA0vhonpw5p0RHDXwhDM1Q== + +vitest@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.0.2.tgz#a7c3bf41bd5ef8c1c781c98c84a749d26b31f944" + integrity sha512-F3NVwwpXfRSDnJmyv+ALPwSRVt0zDkRRE18pwUHSUPXAlWQ47rY1dc99ziMW5bBHyqwK2ERjMisLNoef64qk9w== + dependencies: + "@vitest/expect" "1.0.2" + "@vitest/runner" "1.0.2" + "@vitest/snapshot" "1.0.2" + "@vitest/spy" "1.0.2" + "@vitest/utils" "1.0.2" + acorn-walk "^8.3.0" cac "^6.7.14" chai "^4.3.10" debug "^4.3.4" - local-pkg "^0.4.3" - magic-string "^0.30.1" + execa "^8.0.1" + local-pkg "^0.5.0" + magic-string "^0.30.5" pathe "^1.1.1" picocolors "^1.0.0" - std-env "^3.3.3" - strip-literal "^1.0.1" - tinybench "^2.5.0" - tinypool "^0.7.0" - vite "^3.1.0 || ^4.0.0 || ^5.0.0-0" - vite-node "0.34.6" + std-env "^3.5.0" + strip-literal "^1.3.0" + tinybench "^2.5.1" + tinypool "^0.8.1" + vite "^5.0.0" + vite-node "1.0.2" why-is-node-running "^2.2.2" vm-browserify@^1.0.1: @@ -14000,7 +15263,14 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== -wait-port@^1.1.0: +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + +wait-port@^1.0.4, wait-port@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-1.1.0.tgz#e5d64ee071118d985e2b658ae7ad32b2ce29b6b5" integrity sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q== @@ -14024,6 +15294,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + web3-core@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-4.0.3.tgz#eab6cc23a43ff202d8f38bbd9801a7a2ec750cc2" @@ -14235,11 +15510,63 @@ web3@^4.0.3: web3-utils "^4.0.3" web3-validator "^1.0.2" +webdriver@8.24.12: + version "8.24.12" + resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-8.24.12.tgz#fd443550f2fa25498af8d6a7a1261dc3d6c4f462" + integrity sha512-03DQIClHoaAqTsmDkxGwo4HwHfkn9LzJ1wfNyUerzKg8DnyXeiT6ILqj6EXLfsvh5zddU2vhYGLFXSerPgkuOQ== + dependencies: + "@types/node" "^20.1.0" + "@types/ws" "^8.5.3" + "@wdio/config" "8.24.12" + "@wdio/logger" "8.24.12" + "@wdio/protocols" "8.24.12" + "@wdio/types" "8.24.12" + "@wdio/utils" "8.24.12" + deepmerge-ts "^5.1.0" + got "^12.6.1" + ky "^0.33.0" + ws "^8.8.0" + +webdriverio@^8.24.12: + version "8.24.12" + resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-8.24.12.tgz#05a2107ae8a3927e1a01503a05fc2050fa4e06bd" + integrity sha512-Ddu0NNRMVkTzRzqvm3m0wt2eLUn+Plz2Cj+1QXDnVpddYJvk9J3elZC2hqNyscEtecQ+h2y3r36OcJqkl9jPag== + dependencies: + "@types/node" "^20.1.0" + "@wdio/config" "8.24.12" + "@wdio/logger" "8.24.12" + "@wdio/protocols" "8.24.12" + "@wdio/repl" "8.24.12" + "@wdio/types" "8.24.12" + "@wdio/utils" "8.24.12" + archiver "^6.0.0" + aria-query "^5.0.0" + css-shorthand-properties "^1.1.1" + css-value "^0.0.1" + devtools-protocol "^0.0.1233178" + grapheme-splitter "^1.0.2" + import-meta-resolve "^4.0.0" + is-plain-obj "^4.1.0" + lodash.clonedeep "^4.5.0" + lodash.zip "^4.2.0" + minimatch "^9.0.0" + puppeteer-core "^20.9.0" + query-selector-shadow-dom "^1.0.0" + resq "^1.9.1" + rgb2hex "0.2.5" + serialize-error "^11.0.1" + webdriver "8.24.12" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-merge@^4.1.5: version "4.2.2" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" @@ -14282,6 +15609,26 @@ webpack@^5.88.2: watchpack "^2.4.0" webpack-sources "^3.2.3" +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" + integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -14370,6 +15717,13 @@ which@^3.0.0: dependencies: isexe "^2.0.0" +which@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== + dependencies: + isexe "^3.1.1" + why-is-node-running@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" @@ -14536,21 +15890,31 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.13.0, ws@^8.8.1: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== -ws@^8.8.1: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@^8.14.2, ws@^8.8.0: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== ws@~8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + xml2js@0.4.19: version "0.4.19" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz" @@ -14585,6 +15949,11 @@ xmlbuilder@~9.0.1: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + xsalsa20@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/xsalsa20/-/xsalsa20-1.1.0.tgz" @@ -14671,6 +16040,32 @@ yargs@16.2.0, yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@17.7.1, yargs@^17.5.1, yargs@^17.6.2, yargs@^17.7.1: + version "17.7.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" + integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yargs@17.7.2, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yargs@^15.0.2: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -14701,32 +16096,6 @@ yargs@^17.1.1: y18n "^5.0.5" yargs-parser "^21.1.1" -yargs@^17.5.1, yargs@^17.6.2, yargs@^17.7.1: - version "17.7.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" - integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yargs@^17.7.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" @@ -14758,3 +16127,12 @@ zip-stream@^4.1.0: archiver-utils "^2.1.0" compress-commons "^4.1.0" readable-stream "^3.6.0" + +zip-stream@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-5.0.1.tgz#cf3293bba121cad98be2ec7f05991d81d9f18134" + integrity sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA== + dependencies: + archiver-utils "^4.0.1" + compress-commons "^5.0.1" + readable-stream "^3.6.0"