├── .dockerignore ├── .env.example ├── .github ├── CODEOWNERS └── workflows │ ├── build-image.yml │ ├── checks.yml │ ├── ci-dev-holesky.yml │ ├── ci-dev.yml │ ├── ci-prod.yml │ ├── ci-staging.yml │ ├── create-tag-and-trigger-deploy.yml │ ├── docker_build_reproducibility.yml │ ├── mainnet_fork_tests.yml │ ├── prepare-release.yml │ └── tests.yml ├── .gitignore ├── .hadolint.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── AccountingOracle.json ├── Burner.json ├── CSAccounting.json ├── CSFeeDistributor.json ├── CSFeeOracle.json ├── CSModule.json ├── DepositContract.json ├── HashConsensus.json ├── Lido.json ├── LidoLocator.json ├── OracleDaemonConfig.json ├── OracleReportSanityChecker.json ├── StakingRouter.json ├── ValidatorsExitBusOracle.json └── WithdrawalQueueERC721.json ├── docs ├── reproducible-builds.md └── testing.md ├── fixtures ├── common │ ├── chainId.json │ └── contracts.json └── tests │ ├── modules │ ├── accounting │ │ ├── test_safe_border_unit.py │ │ │ ├── test_calc_validator_slashed_epoch_from_state.json │ │ │ ├── test_calc_validator_slashed_epoch_from_state_undetectable.json │ │ │ ├── test_filter_validators_with_earliest_exit_epoch.json │ │ │ ├── test_get_associated_slashings_border_epoch.json │ │ │ ├── test_get_bunker_start_or_last_successful_report_epoch.json │ │ │ ├── test_get_bunker_start_or_last_successful_report_epoch_no_bunker_start.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_all_withdrawable.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_at_least_one_unpredictable_epoch.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_no_slashed_validators.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_no_validators.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_predicted.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_predicted_different_exit_epoch.cl.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_unable_to_predict.json │ │ │ ├── test_get_earliest_slashed_epoch_among_incomplete_slashings_withdrawable_validators.json │ │ │ ├── test_get_last_finalized_withdrawal_request_slot.json │ │ │ ├── test_get_last_finalized_withdrawal_request_slot_no_requests.json │ │ │ ├── test_get_negative_rebase_border_epoch.json │ │ │ ├── test_get_negative_rebase_border_epoch_bunker_not_started_yet.json │ │ │ ├── test_get_negative_rebase_border_epoch_max.json │ │ │ └── test_get_new_requests_border_epoch.json │ │ ├── test_withdrawal_integration.py │ │ │ ├── test_happy_path.cl.json │ │ │ └── test_happy_path.json │ │ └── test_withdrawal_unit.py │ │ │ ├── test_calculate_finalization_batches.cl.json │ │ │ ├── test_calculate_finalization_batches.json │ │ │ ├── test_get_available_eth.cl.json │ │ │ ├── test_get_available_eth.json │ │ │ ├── test_has_unfinalized_requests[1-1].cl.json │ │ │ ├── test_has_unfinalized_requests[1-1].json │ │ │ ├── test_has_unfinalized_requests[2-1].cl.json │ │ │ ├── test_has_unfinalized_requests[2-1].json │ │ │ ├── test_no_available_eth_to_cover_wc.cl.json │ │ │ ├── test_returns_batch_if_there_are_finalizable_requests.cl.json │ │ │ ├── test_returns_batch_if_there_are_finalizable_requests.json │ │ │ ├── test_returns_empty_batch_if_paused.cl.json │ │ │ ├── test_returns_empty_batch_if_paused.json │ │ │ ├── test_returns_empty_batch_if_there_is_no_requests.cl.json │ │ │ ├── test_returns_empty_batch_if_there_is_no_requests.json │ │ │ ├── test_returns_last_finalizable_id.cl.json │ │ │ ├── test_returns_last_finalizable_id.json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests.cl.json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests.json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests[False-False-0].cl.json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests[False-False-0].json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests[False-True-100].cl.json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests[False-True-100].json │ │ │ ├── test_returns_zero_if_no_unfinalized_requests[True-True-0].cl.json │ │ │ └── test_returns_zero_if_no_unfinalized_requests[True-True-0].json │ ├── ejector │ │ ├── test_ejector.py │ │ │ ├── test_ejector_build_report.json │ │ │ ├── test_get_processing_state.json │ │ │ ├── test_get_reserved_buffer.json │ │ │ └── test_get_unfinalized_steth.json │ │ └── test_prediction.py │ │ │ └── test_get_rewards_no_matching_events.json │ └── submodules │ │ ├── consensus │ │ └── test_consensus.py │ │ │ ├── test_get_blockstamp_for_report_slot_deadline_missed.cl.json │ │ │ ├── test_get_blockstamp_for_report_slot_deadline_missed.json │ │ │ ├── test_get_blockstamp_for_report_slot_member_is_not_in_fast_line_ready.cl.json │ │ │ ├── test_get_blockstamp_for_report_slot_member_is_not_in_fast_line_ready.json │ │ │ ├── test_get_blockstamp_for_report_slot_member_ready_to_report.cl.json │ │ │ ├── test_get_blockstamp_for_report_slot_member_ready_to_report.json │ │ │ ├── test_get_blockstamp_for_report_slot_not_finalized.cl.json │ │ │ ├── test_get_blockstamp_for_report_slot_not_finalized.json │ │ │ ├── test_get_latest_blockstamp.cl.json │ │ │ ├── test_get_member_info_no_member_account.json │ │ │ ├── test_get_member_info_submit_only_account.json │ │ │ ├── test_get_member_info_with_account.json │ │ │ └── test_get_member_info_without_account.json │ │ └── test_oracle_module.py │ │ └── test_receive_last_finalized_slot.cl.json │ └── utils │ └── test_slot.py │ ├── test_all_slots_are_missed.cl.json │ ├── test_get_first_non_missed_slot.cl.json │ ├── test_get_third_non_missed_slot_backward.cl.json │ └── test_get_third_non_missed_slot_forward.cl.json ├── poetry.lock ├── pyproject.toml ├── src ├── __init__.py ├── constants.py ├── main.py ├── metrics │ ├── __init__.py │ ├── healthcheck_server.py │ ├── logging.py │ └── prometheus │ │ ├── __init__.py │ │ ├── accounting.py │ │ ├── basic.py │ │ ├── business.py │ │ ├── csm.py │ │ ├── duration_meter.py │ │ ├── ejector.py │ │ └── validators.py ├── modules │ ├── __init__.py │ ├── accounting │ │ ├── __init__.py │ │ ├── accounting.py │ │ ├── third_phase │ │ │ ├── __init__.py │ │ │ ├── extra_data.py │ │ │ └── types.py │ │ └── types.py │ ├── checks │ │ ├── __init__.py │ │ ├── checks_module.py │ │ ├── pytest.ini │ │ └── suites │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── conftest.py │ │ │ ├── consensus_node.py │ │ │ ├── execution_node.py │ │ │ ├── ipfs.py │ │ │ └── keys_api.py │ ├── csm │ │ ├── __init__.py │ │ ├── checkpoint.py │ │ ├── csm.py │ │ ├── log.py │ │ ├── state.py │ │ ├── tree.py │ │ └── types.py │ ├── ejector │ │ ├── __init__.py │ │ ├── data_encode.py │ │ ├── ejector.py │ │ ├── sweep.py │ │ └── types.py │ └── submodules │ │ ├── __init__.py │ │ ├── consensus.py │ │ ├── exceptions.py │ │ ├── oracle_module.py │ │ └── types.py ├── providers │ ├── __init__.py │ ├── consensus │ │ ├── __init__.py │ │ ├── client.py │ │ └── types.py │ ├── consistency.py │ ├── execution │ │ ├── __init__.py │ │ ├── base_interface.py │ │ ├── contracts │ │ │ ├── accounting_oracle.py │ │ │ ├── base_oracle.py │ │ │ ├── burner.py │ │ │ ├── cs_accounting.py │ │ │ ├── cs_fee_distributor.py │ │ │ ├── cs_fee_oracle.py │ │ │ ├── cs_module.py │ │ │ ├── deposit_contract.py │ │ │ ├── exit_bus_oracle.py │ │ │ ├── hash_consensus.py │ │ │ ├── lido.py │ │ │ ├── lido_locator.py │ │ │ ├── oracle_daemon_config.py │ │ │ ├── oracle_report_sanity_checker.py │ │ │ ├── staking_router.py │ │ │ └── withdrawal_queue_nft.py │ │ └── exceptions.py │ ├── http_provider.py │ ├── ipfs │ │ ├── __init__.py │ │ ├── cid.py │ │ ├── dummy.py │ │ ├── gw3.py │ │ ├── multi.py │ │ ├── pinata.py │ │ ├── public.py │ │ └── types.py │ └── keys │ │ ├── __init__.py │ │ ├── client.py │ │ └── types.py ├── services │ ├── __init__.py │ ├── bunker.py │ ├── bunker_cases │ │ ├── __init__.py │ │ ├── abnormal_cl_rebase.py │ │ ├── midterm_slashing_penalty.py │ │ └── types.py │ ├── exit_order_iterator.py │ ├── prediction.py │ ├── safe_border.py │ ├── validator_state.py │ └── withdrawal.py ├── types.py ├── utils │ ├── __init__.py │ ├── abi.py │ ├── blockstamp.py │ ├── build.py │ ├── cache.py │ ├── dataclass.py │ ├── env.py │ ├── events.py │ ├── exception.py │ ├── input.py │ ├── range.py │ ├── slot.py │ ├── timeit.py │ ├── types.py │ ├── units.py │ ├── validator_state.py │ └── web3converter.py ├── variables.py └── web3py │ ├── __init__.py │ ├── contract_tweak.py │ ├── extensions │ ├── __init__.py │ ├── consensus.py │ ├── contracts.py │ ├── csm.py │ ├── fallback.py │ ├── keys_api.py │ ├── lido_validators.py │ └── tx_utils.py │ ├── middleware.py │ └── types.py ├── stubs ├── lazy_object_proxy │ ├── __init__.pyi │ └── lazy_object_proxy.pyi ├── timeout_decorator │ ├── __init__.pyi │ └── timeout_decorator.pyi └── web3_multi_provider │ ├── __init__.pyi │ └── multi_http_provider.pyi └── tests ├── __init__.py ├── conftest.py ├── e2e ├── conftest.py └── test_accounting.py ├── execution ├── __init__.py └── base_interface_test.py ├── factory ├── base_oracle.py ├── bitarrays.py ├── blockstamp.py ├── configs.py ├── consensus.py ├── contract_responses.py ├── member_info.py ├── no_registry.py └── web3_factory.py ├── fork ├── __init__.py ├── conftest.py ├── contracts │ ├── csm │ │ └── HashConsensus_bin │ └── lido │ │ └── HashConsensus_bin ├── test_csm_oracle_cycle.py └── test_lido_oracle_cycle.py ├── integration ├── __init__.py ├── conftest.py └── contracts │ ├── __init__.py │ ├── contract_utils.py │ ├── test_accounting_oracle.py │ ├── test_bunker.py │ ├── test_lido.py │ ├── test_lido_locator.py │ ├── test_oracle_daemon_config.py │ ├── test_oracle_report_sanity_checker.py │ ├── test_staking_router.py │ ├── test_validator_exit_bus_oracle.py │ └── test_withdrawal_queue_nft_contract.py ├── metrics ├── __init__.py ├── test_healthcheck_server.py └── test_logging.py ├── modules ├── accounting │ ├── bunker │ │ ├── conftest.py │ │ ├── test_bunker.py │ │ ├── test_bunker_abnormal_cl_rebase.py │ │ └── test_bunker_midterm_penalty.py │ ├── test_accounting_module.py │ ├── test_extra_data.py │ ├── test_safe_border_integration.py │ ├── test_safe_border_unit.py │ ├── test_validator_state.py │ ├── test_withdrawal_integration.py │ └── test_withdrawal_unit.py ├── csm │ ├── test_checkpoint.py │ ├── test_csm_module.py │ ├── test_log.py │ ├── test_processing_attestation.py │ ├── test_state.py │ └── test_tree.py ├── ejector │ ├── test_data_encode.py │ ├── test_ejector.py │ ├── test_prediction.py │ ├── test_sweep.py │ └── test_validator_exit_order_iterator.py └── submodules │ ├── consensus │ ├── conftest.py │ ├── test_consensus.py │ └── test_reports.py │ └── test_oracle_module.py ├── providers ├── __init__.py ├── consensus │ ├── __init__.py │ └── test_consensus_client.py ├── test_consistency.py └── test_ipfs.py ├── providers_clients ├── test_http_provider.py └── test_keys_api_client.py ├── providers_utils.py ├── services └── test_safe_border.py ├── utils ├── test_abi.py ├── test_build.py ├── test_cache.py ├── test_dataclass.py ├── test_env.py ├── test_events.py ├── test_range.py ├── test_slot.py ├── test_timeit.py ├── test_types.py ├── test_validator_state_utils.py └── test_web3_converter.py └── web3py ├── test_lido_validators.py ├── test_middleware.py └── test_tx_utils.py /.env.example: -------------------------------------------------------------------------------- 1 | CONSENSUS_CLIENT_URI=http://... 2 | EXECUTION_CLIENT_URI=http://... 3 | KEYS_API_URI=https://... 4 | LIDO_LOCATOR_ADDRESS=0x1... 5 | CSM_MODULE_ADDRESS=0x... 6 | MEMBER_PRIV_KEY=aaa... 7 | GW3_ACCESS_KEY=1234.... 8 | GW3_SECRET_KEY=abcd1234... 9 | PINATA_JWT=... 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lidofinance/lido-valset-oracles 2 | .github @lidofinance/review-gh-workflows 3 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build Docker image 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: 8 | - develop 9 | - master 10 | paths-ignore: 11 | - ".github/**" 12 | - "README.md" 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | with: 25 | persist-credentials: false 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | 30 | - name: Build image 31 | uses: docker/build-push-action@v3 32 | with: 33 | tags: app:ci 34 | push: false 35 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Tests and Checks 2 | 3 | on: push 4 | 5 | jobs: 6 | security: 7 | uses: lidofinance/linters/.github/workflows/security.yml@master 8 | permissions: 9 | security-events: write 10 | contents: read 11 | docker: 12 | uses: lidofinance/linters/.github/workflows/docker.yml@master 13 | actions: 14 | uses: lidofinance/linters/.github/workflows/actions.yml@master 15 | -------------------------------------------------------------------------------- /.github/workflows/ci-dev-holesky.yml: -------------------------------------------------------------------------------- 1 | name: CI Dev Holesky 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - holesky 8 | paths-ignore: 9 | - ".github/**" 10 | - "README.md" 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | # test: 16 | # ... 17 | 18 | deploy: 19 | runs-on: ubuntu-latest 20 | # needs: test 21 | name: Build and deploy 22 | steps: 23 | - name: Testnet deploy 24 | uses: lidofinance/dispatch-workflow@v1 25 | env: 26 | APP_ID: ${{ secrets.APP_ID }} 27 | APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 28 | TARGET_REPO: "lidofinance/infra-mainnet" 29 | TARGET_WORKFLOW: "deploy_holesky_testnet_lido_oracle.yaml" 30 | TARGET: "holesky" 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-dev.yml: -------------------------------------------------------------------------------- 1 | name: CI Dev Hoodi 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - develop 8 | paths-ignore: 9 | - ".github/**" 10 | - "README.md" 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | # test: 16 | # ... 17 | 18 | deploy: 19 | runs-on: ubuntu-latest 20 | # needs: test 21 | name: Build and deploy 22 | steps: 23 | - name: Testnet deploy 24 | uses: lidofinance/dispatch-workflow@v1 25 | env: 26 | APP_ID: ${{ secrets.APP_ID }} 27 | APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 28 | TARGET_REPO: "lidofinance/infra-mainnet" 29 | TARGET_WORKFLOW: "deploy_hoodi_testnet_lido_oracle.yaml" 30 | TARGET: "develop" 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-prod.yml: -------------------------------------------------------------------------------- 1 | name: CI Build prod image 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | tag: 7 | description: "tag to deploy from" 8 | default: "" 9 | required: false 10 | type: string 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | # test: 17 | # ... 18 | 19 | deploy: 20 | runs-on: ubuntu-latest 21 | # needs: test 22 | name: Build and deploy 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Tag name 30 | id: tag_name 31 | run: | 32 | if [ '${{ inputs.tag }}' = '' ]; then 33 | echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 34 | else 35 | echo "TAG=$TAG" >> $GITHUB_OUTPUT 36 | fi 37 | env: 38 | TAG: ${{ inputs.tag }} 39 | 40 | - name: Build prod image 41 | uses: lidofinance/dispatch-workflow@v1 42 | env: 43 | APP_ID: ${{ secrets.APP_ID }} 44 | APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 45 | TARGET_REPO: "lidofinance/infra-mainnet" 46 | TAG: "${{ steps.tag_name.outputs.TAG }}" 47 | TARGET_WORKFLOW: "build_mainnet_lido_oracle.yaml" 48 | -------------------------------------------------------------------------------- /.github/workflows/ci-staging.yml: -------------------------------------------------------------------------------- 1 | name: CI Staging 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - ".github/**" 10 | - "README.md" 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | # test: 16 | # ... 17 | 18 | deploy: 19 | runs-on: ubuntu-latest 20 | # needs: test 21 | name: Build and deploy 22 | steps: 23 | - name: Staging deploy 24 | uses: lidofinance/dispatch-workflow@v1 25 | env: 26 | APP_ID: ${{ secrets.APP_ID }} 27 | APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 28 | TARGET_REPO: "lidofinance/infra-mainnet" 29 | TARGET_WORKFLOW: "deploy_staging_mainnet_lido_oracle.yaml" 30 | TARGET: "master" 31 | -------------------------------------------------------------------------------- /.github/workflows/create-tag-and-trigger-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Create tag and trigger deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | bump: 12 | name: Create tag and release 13 | runs-on: ubuntu-latest 14 | if: "contains(github.event.head_commit.message, 'chore(release)')" 15 | outputs: 16 | tag: ${{ steps.tag.outputs.tag }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | persist-credentials: false 22 | - name: Get tag value 23 | id: tag 24 | run: | 25 | TAG="$(grep -oP '^chore\(release\).*\K(\d+\.\d+\.\d+)' <<< "$MESSAGE")" 26 | echo "$TAG" 27 | echo "tag=$TAG" >> $GITHUB_OUTPUT 28 | env: 29 | MESSAGE: ${{ github.event.head_commit.message }} 30 | - name: Create and push tag 31 | run: | 32 | git tag ${{ steps.tag.outputs.tag }} 33 | git push https://x-access-token:${{ github.token }}@github.com/$GITHUB_REPOSITORY --tags 34 | - name: Create release 35 | uses: lidofinance/action-gh-release@v1 36 | with: 37 | tag_name: ${{ steps.tag.outputs.tag }} 38 | 39 | deploy-trigger: 40 | needs: bump 41 | name: Trigger build and PR creation in the infra-mainnet 42 | if: "contains(github.event.head_commit.message, 'chore(release)')" 43 | uses: ./.github/workflows/ci-prod.yml 44 | secrets: inherit 45 | with: 46 | tag: ${{ needs.bump.outputs.tag }} 47 | -------------------------------------------------------------------------------- /.github/workflows/mainnet_fork_tests.yml: -------------------------------------------------------------------------------- 1 | name: Mainnet Fork Tests 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - edited 10 | - closed 11 | branches: 12 | - main 13 | - develop 14 | paths: 15 | - "src/**" 16 | 17 | permissions: 18 | contents: read 19 | security-events: write 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: Set up Python 3.12 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: "3.12" 32 | 33 | - name: Setup poetry 34 | run: > 35 | curl -sSL https://install.python-poetry.org | python - && 36 | echo "$POETRY_HOME/bin" >> "$GITHUB_PATH" 37 | env: 38 | POETRY_HOME: "/opt/poetry" 39 | POETRY_VERSION: 1.3.2 40 | 41 | - name: Install Python dependencies 42 | run: | 43 | poetry install --no-interaction --with=dev 44 | 45 | - name: Install Foundry 46 | uses: foundry-rs/foundry-toolchain@v1 47 | 48 | - name: Mainnet Fork Tests 49 | run: poetry run pytest -m 'fork' -n auto tests 50 | env: 51 | EXECUTION_CLIENT_URI: ${{ secrets.EXECUTION_CLIENT_URI }} 52 | CONSENSUS_CLIENT_URI: ${{ secrets.CONSENSUS_CLIENT_URI }} 53 | KEYS_API_URI: ${{ secrets.KEYS_API_URI }} 54 | LIDO_LOCATOR_ADDRESS: "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb" 55 | CSM_MODULE_ADDRESS: "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F" 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | jobs: 11 | prepare-release: 12 | name: Prepare release and create pre-release PR 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | persist-credentials: false 19 | 20 | - name: Bump version 21 | id: changelog 22 | uses: lidofinance/conventional-changelog-action@v3 23 | with: 24 | git-message: "chore(release): {version}" 25 | tag-prefix: "" 26 | # Changelog generated manually 27 | output-file: false 28 | version-file: pyproject.toml 29 | version-path: tool.poetry.version 30 | git-push: false 31 | skip-on-empty: false 32 | skip-ci: false 33 | create-summary: true 34 | 35 | - name: Fail on missing args 36 | if: ${{ !steps.changelog.outputs.version }} 37 | run: > 38 | echo "::error::No version output found for the prev step! Try restarting action" && exit 1 39 | 40 | - name: Create Pull Request 41 | uses: lidofinance/create-pull-request@v4 42 | if: ${{ steps.changelog.outputs.version }} 43 | with: 44 | branch: pre-release-${{ steps.changelog.outputs.version }} 45 | title: "chore(release): ${{ steps.changelog.outputs.version }}" 46 | body: "This PR is generated automatically.\nMerge it with **Rebase and merge** option or with the **Squash and merge** keeping default commit message (CRUCIAL) for the automatic tag creation.\nIf you don't need this PR than close it and **delete source branch**! " 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | permissions: 6 | contents: read 7 | security-events: write 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Python 3.12 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.12" 20 | 21 | - name: Setup poetry 22 | run: > 23 | curl -sSL https://install.python-poetry.org | python - && 24 | echo "$POETRY_HOME/bin" >> "$GITHUB_PATH" 25 | env: 26 | POETRY_HOME: "/opt/poetry" 27 | POETRY_VERSION: 1.3.2 28 | 29 | - name: Install dependencies 30 | run: | 31 | poetry install --no-interaction --with=dev 32 | 33 | - name: Test with pytest 34 | run: poetry run pytest --cov=src tests 35 | env: 36 | EXECUTION_CLIENT_URI: ${{ secrets.EXECUTION_CLIENT_URI }} 37 | CONSENSUS_CLIENT_URI: ${{ secrets.CONSENSUS_CLIENT_URI }} 38 | KEYS_API_URI: ${{ secrets.KEYS_API_URI }} 39 | 40 | linters: 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: Set up Python 3.12 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: "3.12" 50 | 51 | - name: Setup poetry 52 | run: > 53 | curl -sSL https://install.python-poetry.org | python - && 54 | echo "$POETRY_HOME/bin" >> "$GITHUB_PATH" 55 | env: 56 | POETRY_HOME: "/opt/poetry" 57 | POETRY_VERSION: 1.3.2 58 | 59 | - name: Install dependencies 60 | run: | 61 | poetry install --no-interaction --with=dev 62 | 63 | - name: Lint with black 64 | run: poetry run black --check tests 65 | 66 | - name: Lint with pylint 67 | run: poetry run pylint src tests 68 | 69 | - name: Lint mypy 70 | run: poetry run mypy src 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | .direnv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | .ruff_cache/ 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # IDE 135 | .idea/ 136 | 137 | # vim 138 | *.swp 139 | 140 | # Cache 141 | *.pkl 142 | *.buf 143 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/.hadolint.yaml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.4-slim AS base 2 | 3 | ARG SOURCE_DATE_EPOCH 4 | 5 | RUN apt-get update && apt-get install -y --no-install-recommends -qq \ 6 | libffi-dev=3.4.4-1 \ 7 | g++=4:12.2.0-3 \ 8 | curl=7.88.1-10+deb12u12 \ 9 | && apt-get clean \ 10 | && rm -rf /var/lib/apt/lists/* \ 11 | && rm -rf /var/cache/* \ 12 | && rm -rf /var/log/* 13 | 14 | ENV PYTHONUNBUFFERED=1 \ 15 | PYTHONDONTWRITEBYTECODE=1 \ 16 | PIP_NO_CACHE_DIR=off \ 17 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 18 | PIP_DEFAULT_TIMEOUT=100 \ 19 | POETRY_VIRTUALENVS_IN_PROJECT=true \ 20 | POETRY_NO_INTERACTION=1 \ 21 | VENV_PATH="/.venv" \ 22 | CFLAGS="-g0 -O2 -ffile-prefix-map=/src=." 23 | 24 | ENV PATH="$VENV_PATH/bin:$PATH" 25 | 26 | FROM base AS builder 27 | 28 | ENV POETRY_VERSION=1.3.2 29 | RUN pip install --no-cache-dir poetry==$POETRY_VERSION 30 | 31 | WORKDIR / 32 | COPY pyproject.toml poetry.lock ./ 33 | 34 | # Building lru-dict from source for reproducible .so files by enforcing consistent CFLAGS across builds 35 | RUN poetry config --local installer.no-binary lru-dict && \ 36 | poetry install --only main --no-root --no-cache && \ 37 | find "$VENV_PATH" -type d -name '.git' -exec rm -rf {} + && \ 38 | find "$VENV_PATH" -name '*.dist-info' -exec rm -rf {}/RECORD \; && \ 39 | find "$VENV_PATH" -name '*.dist-info' -exec rm -rf {}/WHEEL \; && \ 40 | find "$VENV_PATH" -name '__pycache__' -exec rm -rf {} + 41 | 42 | 43 | FROM base AS production 44 | 45 | COPY --from=builder $VENV_PATH $VENV_PATH 46 | WORKDIR /app 47 | COPY . . 48 | 49 | RUN apt-get clean && find /var/lib/apt/lists/ -type f -delete && chown -R www-data /app/ 50 | 51 | ENV PROMETHEUS_PORT=9000 52 | ENV HEALTHCHECK_SERVER_PORT=9010 53 | 54 | EXPOSE $PROMETHEUS_PORT 55 | USER www-data 56 | 57 | HEALTHCHECK --interval=10s --timeout=3s \ 58 | CMD curl -f http://localhost:$HEALTHCHECK_SERVER_PORT/healthcheck || exit 1 59 | 60 | WORKDIR /app/ 61 | 62 | ENTRYPOINT ["python3", "-m", "src.main"] 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | reproducible-build-oracle: 2 | docker buildx rm oracle-buildkit || true 3 | docker volume rm $(shell docker volume ls -q --filter name=buildx_buildkit_oracle-buildkit) || true 4 | docker buildx create --name oracle-buildkit --use || true 5 | docker buildx use oracle-buildkit 6 | docker buildx build \ 7 | --platform linux/amd64 \ 8 | --build-arg SOURCE_DATE_EPOCH=0 \ 9 | --no-cache \ 10 | --output type=docker,name=oracle-reproducible-container:local,rewrite-timestamp=true \ 11 | -t oracle-reproducible-container:local \ 12 | . 13 | docker image inspect oracle-reproducible-container:local --format 'Image hash: {{.Id}}' 14 | -------------------------------------------------------------------------------- /assets/DepositContract.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"pubkey","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"withdrawal_credentials","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"amount","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"signature","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"index","type":"bytes"}],"name":"DepositEvent","type":"event"},{"inputs":[{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"bytes","name":"withdrawal_credentials","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"bytes32","name":"deposit_data_root","type":"bytes32"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"get_deposit_count","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"get_deposit_root","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}] 2 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Tests in this directory are run using the [pytest](https://docs.pytest.org/en/latest/) framework. 4 | You can run it by running `pytest tests` in the root directory of the repository. 5 | 6 | Most tests are unit tests and marked with the `@pytest.mark.unit` decorator. 7 | They use predefined rpc responses and do not require either consensus or beacon nodes. 8 | Rpc and Http responses are stored in the `tests/responses` directory and can be overridden using `--update-responses` 9 | flag while running tests (make sure that you set rpc node environment variables in this case). 10 | They are useful when you do not need to change response data for testing. 11 | In case if you need to test something with using specific responses, you can mock it directly using `add_mock` function from `MockProvider`. 12 | 13 | To run tests with a coverage report, run `pytest --cov=src tests` in the root directory of the repository. 14 | 15 | ## TODOS 16 | - [ ] run tests marked with possible_integration as a part of integration tests with a real providers 17 | -------------------------------------------------------------------------------- /fixtures/common/chainId.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_chainId", 4 | "params": [], 5 | "response": { 6 | "jsonrpc": "2.0", 7 | "id": 0, 8 | "result": "0x1469cb" 9 | } 10 | } 11 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_calc_validator_slashed_epoch_from_state.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_calc_validator_slashed_epoch_from_state_undetectable.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_filter_validators_with_earliest_exit_epoch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_associated_slashings_border_epoch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_bunker_start_or_last_successful_report_epoch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_bunker_start_or_last_successful_report_epoch_no_bunker_start.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_all_withdrawable.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_at_least_one_unpredictable_epoch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_no_slashed_validators.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_no_validators.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_predicted.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_unable_to_predict.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_earliest_slashed_epoch_among_incomplete_slashings_withdrawable_validators.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_last_finalized_withdrawal_request_slot.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_last_finalized_withdrawal_request_slot_no_requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_negative_rebase_border_epoch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_negative_rebase_border_epoch_bunker_not_started_yet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_negative_rebase_border_epoch_max.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_safe_border_unit.py/test_get_new_requests_border_epoch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_withdrawal_unit.py/test_calculate_finalization_batches.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0xc9e044efd31473cff256a6bbb434d30c479b37db2a32c2cd1335a80887067aef" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0xc9e044efd31473cff256a6bbb434d30c479b37db2a32c2cd1335a80887067aef" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_withdrawal_unit.py/test_returns_batch_if_there_are_finalizable_requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | }, 32 | { 33 | "method": "eth_call", 34 | "params": [ 35 | { 36 | "to": "0x4c1F6cA213abdbc19b27f2562d7b1A645A019bD9", 37 | "data": "0xb187bd26" 38 | }, 39 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 40 | ], 41 | "response": { 42 | "jsonrpc": "2.0", 43 | "id": 2, 44 | "result": "0x0000000000000000000000000000000000000000000000000000000000000000" 45 | } 46 | } 47 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_withdrawal_unit.py/test_returns_empty_batch_if_paused.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | }, 32 | { 33 | "method": "eth_call", 34 | "params": [ 35 | { 36 | "to": "0x4c1F6cA213abdbc19b27f2562d7b1A645A019bD9", 37 | "data": "0xb187bd26" 38 | }, 39 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 40 | ], 41 | "response": { 42 | "jsonrpc": "2.0", 43 | "id": 2, 44 | "result": "0x0000000000000000000000000000000000000000000000000000000000000000" 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_withdrawal_unit.py/test_returns_empty_batch_if_there_is_no_requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x8D49f1b4AF30598679D4D37Be4B094da1b459b82", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b40" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c46494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f45504f43485f53484946540000000000000000000000000000000000000000" 23 | }, 24 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | }, 32 | { 33 | "method": "eth_call", 34 | "params": [ 35 | { 36 | "to": "0x4c1F6cA213abdbc19b27f2562d7b1A645A019bD9", 37 | "data": "0xb187bd26" 38 | }, 39 | "0x90659c38ec3b064b0196dee00903424a75185ca21c37bdeca79254042a372777" 40 | ], 41 | "response": { 42 | "jsonrpc": "2.0", 43 | "id": 2, 44 | "result": "0x0000000000000000000000000000000000000000000000000000000000000000" 45 | } 46 | } 47 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_withdrawal_unit.py/test_returns_last_finalizable_id.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x5dE66f1E2fC2BEAFA802D6b8fB00805745266BCb", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0xb4921bfeee5a63c3b7d9fb1d35f1e3a506d64fa9c87e469b4e47909d56d308d7" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000000fa000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b4000000000000000000000000000000000000000000000000000000000000007d00000000000000000000000000000000000000000000000000000000000000064" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0xce59E362b6a91bC090775B230e4EFe791d5005FB", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002646494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f53484946540000000000000000000000000000000000000000000000000000" 23 | }, 24 | "0xb4921bfeee5a63c3b7d9fb1d35f1e3a506d64fa9c87e469b4e47909d56d308d7" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 2, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/accounting/test_withdrawal_unit.py/test_returns_zero_if_no_unfinalized_requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x5dE66f1E2fC2BEAFA802D6b8fB00805745266BCb", 7 | "data": "0xa3a3fd5d" 8 | }, 9 | "0xb4921bfeee5a63c3b7d9fb1d35f1e3a506d64fa9c87e469b4e47909d56d308d7" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000000fa000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000004c4b4000000000000000000000000000000000000000000000000000000000000007d00000000000000000000000000000000000000000000000000000000000000064" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0xce59E362b6a91bC090775B230e4EFe791d5005FB", 22 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002646494e414c495a4154494f4e5f4d41585f4e454741544956455f5245424153455f53484946540000000000000000000000000000000000000000000000000000" 23 | }, 24 | "0xb4921bfeee5a63c3b7d9fb1d35f1e3a506d64fa9c87e469b4e47909d56d308d7" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 2, 29 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/ejector/test_ejector.py/test_ejector_build_report.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x64E79C2E3A112e9CDc055e64afba9f1a8f0aB1Ef", 7 | "data": "0x3584d59c" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x0000000000000000000000000000000000000000000000000000000000000000" 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/ejector/test_ejector.py/test_get_processing_state.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x64E79C2E3A112e9CDc055e64afba9f1a8f0aB1Ef", 7 | "data": "0x8f7797c2" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x0000000000000000000000000000000000000000000000000000000000047d7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/ejector/test_ejector.py/test_get_reserved_buffer.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0xDe82ADEd58dA35add75Ea4676239Ca169c8dCD15", 7 | "data": "0x47b714e0" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x0000000000000000000000000000000000000000000000a2249a23f3291d000a" 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/ejector/test_ejector.py/test_get_unfinalized_steth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x4c1F6cA213abdbc19b27f2562d7b1A645A019bD9", 7 | "data": "0xd0fb84e8" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x000000000000000000000000000000000000000000000000740c74c59063b000" 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/ejector/test_prediction.py/test_get_rewards_no_matching_events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x53Fdb8445af417103E2f9e04bD935D7af0692Fc3", 7 | "data": "0x693ec85e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001c50524544494354494f4e5f4455524154494f4e5f494e5f534c4f545300000000" 8 | }, 9 | "latest" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001c20" 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/submodules/consensus/test_consensus.py/test_get_blockstamp_for_report_slot_deadline_missed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x3FD30E8360e2E637be3428fB78A3d8D0ad157197", 7 | "data": "0x8f55b571" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x0000000000000000000000009eccba8125b9145e6e5a8e2c757907ba75946a0f" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x9ECCba8125B9145E6e5a8E2C757907BA75946A0F", 22 | "data": "0x72f79b13" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x0000000000000000000000000000000000000000000000000000000000047d7f0000000000000000000000000000000000000000000000000000000000047eff" 30 | } 31 | }, 32 | { 33 | "method": "eth_call", 34 | "params": [ 35 | { 36 | "to": "0x9ECCba8125B9145E6e5a8E2C757907BA75946A0F", 37 | "data": "0x6fb1bf66" 38 | }, 39 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 40 | ], 41 | "response": { 42 | "jsonrpc": "2.0", 43 | "id": 2, 44 | "result": "0x0000000000000000000000000000000000000000000000000000000000002170000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000a" 45 | } 46 | }, 47 | { 48 | "method": "eth_call", 49 | "params": [ 50 | { 51 | "to": "0x3FD30E8360e2E637be3428fB78A3d8D0ad157197", 52 | "data": "0x8aa10435" 53 | }, 54 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 55 | ], 56 | "response": { 57 | "jsonrpc": "2.0", 58 | "id": 6, 59 | "result": "0x0000000000000000000000000000000000000000000000000000000000000001" 60 | } 61 | }, 62 | { 63 | "method": "eth_call", 64 | "params": [ 65 | { 66 | "to": "0x3FD30E8360e2E637be3428fB78A3d8D0ad157197", 67 | "data": "0x5be20425" 68 | }, 69 | "0xe8f747cf6290c329c0ea73a1ffcf3dff24423810b4e5513e8acf95a294647dfa" 70 | ], 71 | "response": { 72 | "jsonrpc": "2.0", 73 | "id": 7, 74 | "result": "0x0000000000000000000000000000000000000000000000000000000000000001" 75 | } 76 | } 77 | ] -------------------------------------------------------------------------------- /fixtures/tests/modules/submodules/consensus/test_consensus.py/test_get_member_info_without_account.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "eth_call", 4 | "params": [ 5 | { 6 | "to": "0x3FD30E8360e2E637be3428fB78A3d8D0ad157197", 7 | "data": "0x8f55b571" 8 | }, 9 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 10 | ], 11 | "response": { 12 | "jsonrpc": "2.0", 13 | "id": 0, 14 | "result": "0x0000000000000000000000009eccba8125b9145e6e5a8e2c757907ba75946a0f" 15 | } 16 | }, 17 | { 18 | "method": "eth_call", 19 | "params": [ 20 | { 21 | "to": "0x9ECCba8125B9145E6e5a8E2C757907BA75946A0F", 22 | "data": "0x72f79b13" 23 | }, 24 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 25 | ], 26 | "response": { 27 | "jsonrpc": "2.0", 28 | "id": 1, 29 | "result": "0x0000000000000000000000000000000000000000000000000000000000047d7f0000000000000000000000000000000000000000000000000000000000047eff" 30 | } 31 | }, 32 | { 33 | "method": "eth_call", 34 | "params": [ 35 | { 36 | "to": "0x9ECCba8125B9145E6e5a8E2C757907BA75946A0F", 37 | "data": "0x6fb1bf66" 38 | }, 39 | "0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b" 40 | ], 41 | "response": { 42 | "jsonrpc": "2.0", 43 | "id": 2, 44 | "result": "0x0000000000000000000000000000000000000000000000000000000000002170000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000a" 45 | } 46 | } 47 | ] -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/__init__.py -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | from src.types import Gwei, SlotNumber 2 | 3 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#misc 4 | GENESIS_SLOT = SlotNumber(0) 5 | FAR_FUTURE_EPOCH = 2**64 - 1 6 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 7 | MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 2**8 8 | SHARD_COMMITTEE_PERIOD = 2**8 9 | MAX_SEED_LOOKAHEAD = 4 10 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#state-list-lengths 11 | EPOCHS_PER_SLASHINGS_VECTOR = 2**13 12 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#rewards-and-penalties 13 | PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX = 3 14 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#gwei-values 15 | EFFECTIVE_BALANCE_INCREMENT = Gwei(2**0 * 10**9) 16 | MAX_EFFECTIVE_BALANCE = Gwei(32 * 10**9) 17 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values 18 | MAX_EFFECTIVE_BALANCE_ELECTRA = Gwei(2**11 * 10**9) 19 | MIN_ACTIVATION_BALANCE = Gwei(2**5 * 10**9) 20 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#execution 21 | MAX_WITHDRAWALS_PER_PAYLOAD = 2**4 22 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#withdrawal-prefixes 23 | ETH1_ADDRESS_WITHDRAWAL_PREFIX = '0x01' 24 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#withdrawal-prefixes 25 | COMPOUNDING_WITHDRAWAL_PREFIX = '0x02' 26 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator-cycle 27 | MIN_PER_EPOCH_CHURN_LIMIT = 2**2 28 | CHURN_LIMIT_QUOTIENT = 2**16 29 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#validator-cycle 30 | MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA = Gwei(2**7 * 10**9) 31 | MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT = Gwei(2**8 * 10**9) 32 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters 33 | SLOTS_PER_HISTORICAL_ROOT = 2**13 # 8192 34 | 35 | # https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#withdrawals-processing 36 | MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 2**3 37 | 38 | # Lido contracts constants 39 | # We assume that the Lido deposit amount is currently 32 ETH (MIN_ACTIVATION_BALANCE). 40 | # If Lido decides to support 0x2 withdrawal credentials in the future, this variable 41 | # should be revisited to accommodate potential changes in deposit requirements. 42 | LIDO_DEPOSIT_AMOUNT = MIN_ACTIVATION_BALANCE 43 | SHARE_RATE_PRECISION_E27 = 10**27 44 | TOTAL_BASIS_POINTS = 10000 45 | 46 | # Local constants 47 | GWEI_TO_WEI = 10**9 48 | MAX_BLOCK_GAS_LIMIT = 30_000_000 49 | UINT64_MAX = 2**64 - 1 50 | -------------------------------------------------------------------------------- /src/metrics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/metrics/__init__.py -------------------------------------------------------------------------------- /src/metrics/healthcheck_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from datetime import datetime, timedelta 4 | from http.server import SimpleHTTPRequestHandler, HTTPServer 5 | 6 | import requests 7 | from requests.exceptions import ConnectionError as RequestsConnectionError 8 | 9 | from src import variables 10 | from src.variables import MAX_CYCLE_LIFETIME_IN_SECONDS 11 | 12 | 13 | _last_pulse = datetime.now() 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def pulse(): 18 | """Ping to healthcheck server that application is ok""" 19 | try: 20 | requests.get(f'http://localhost:{variables.HEALTHCHECK_SERVER_PORT}/pulse/', timeout=10) 21 | except RequestsConnectionError: 22 | logger.warning({'Healthcheck server is not responding.'}) 23 | 24 | 25 | class PulseRequestHandler(SimpleHTTPRequestHandler): 26 | """Request handler for Docker HEALTHCHECK""" 27 | def do_GET(self): 28 | global _last_pulse 29 | 30 | if self.path == '/pulse/': 31 | _last_pulse = datetime.now() 32 | 33 | if datetime.now() - _last_pulse > timedelta(seconds=MAX_CYCLE_LIFETIME_IN_SECONDS): 34 | self.send_response(503) 35 | self.end_headers() 36 | self.wfile.write(b'{"metrics": "fail", "reason": "timeout exceeded"}\n') 37 | else: 38 | self.send_response(200) 39 | self.end_headers() 40 | self.wfile.write(b'{"metrics": "ok", "reason": "ok"}\n') 41 | 42 | def log_request(self, *args, **kwargs): 43 | # Disable non-error logs 44 | pass 45 | 46 | 47 | def start_pulse_server(): 48 | """ 49 | This is simple server for bots without any API. 50 | If bot didn't call pulse for a while (5 minutes but should be changed individually) 51 | Docker healthcheck fails to do request 52 | """ 53 | server = HTTPServer(('localhost', variables.HEALTHCHECK_SERVER_PORT), RequestHandlerClass=PulseRequestHandler) 54 | thread = threading.Thread(target=server.serve_forever, daemon=True) 55 | thread.start() 56 | -------------------------------------------------------------------------------- /src/metrics/logging.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | 5 | class JsonFormatter(logging.Formatter): 6 | def format(self, record: logging.LogRecord) -> str: 7 | message = record.msg if isinstance(record.msg, dict) else {'msg': record.getMessage()} 8 | 9 | if 'value' in message: 10 | message['value'] = str(message['value']) 11 | 12 | to_json_msg = json.dumps({ 13 | 'timestamp': int(record.created), 14 | 'name': record.name, 15 | 'levelname': record.levelname, 16 | 'funcName': record.funcName, 17 | 'lineno': record.lineno, 18 | 'module': record.module, 19 | 'pathname': record.pathname, 20 | **message, 21 | }) 22 | return to_json_msg 23 | 24 | 25 | handler = logging.StreamHandler() 26 | handler.setFormatter(JsonFormatter()) 27 | 28 | logging.basicConfig( 29 | level=logging.INFO, 30 | handlers=[handler], 31 | ) 32 | -------------------------------------------------------------------------------- /src/metrics/prometheus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/metrics/prometheus/__init__.py -------------------------------------------------------------------------------- /src/metrics/prometheus/accounting.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Gauge 2 | 3 | from src.variables import PROMETHEUS_PREFIX 4 | 5 | 6 | ACCOUNTING_IS_BUNKER = Gauge( 7 | "accounting_is_bunker", 8 | "Is bunker mode enabled", 9 | namespace=PROMETHEUS_PREFIX, 10 | ) 11 | 12 | ACCOUNTING_CL_BALANCE_GWEI = Gauge( 13 | "accounting_cl_balance_gwei", 14 | "Reported CL balance in gwei", 15 | namespace=PROMETHEUS_PREFIX, 16 | ) 17 | 18 | ACCOUNTING_EL_REWARDS_VAULT_BALANCE_WEI = Gauge( 19 | "accounting_el_rewards_vault_wei", 20 | "Reported EL rewards", 21 | namespace=PROMETHEUS_PREFIX, 22 | ) 23 | 24 | ACCOUNTING_WITHDRAWAL_VAULT_BALANCE_WEI = Gauge( 25 | "accounting_withdrawal_vault_balance_wei", 26 | "Reported withdrawal vault balance", 27 | namespace=PROMETHEUS_PREFIX, 28 | ) 29 | 30 | ACCOUNTING_EXITED_VALIDATORS = Gauge( 31 | "accounting_exited_validators", 32 | "Reported exited validators count", 33 | ["module_id", "no_id"], 34 | namespace=PROMETHEUS_PREFIX, 35 | ) 36 | 37 | ACCOUNTING_STUCK_VALIDATORS = Gauge( 38 | "accounting_stuck_validators", 39 | "Reported stuck validators count", 40 | ["module_id", "no_id"], 41 | namespace=PROMETHEUS_PREFIX, 42 | ) 43 | 44 | ACCOUNTING_DELAYED_VALIDATORS = Gauge( 45 | "accounting_delayed_validators", 46 | "Reported delayed validators count", 47 | ["module_id", "no_id"], 48 | namespace=PROMETHEUS_PREFIX, 49 | ) 50 | -------------------------------------------------------------------------------- /src/metrics/prometheus/basic.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from prometheus_client import Gauge, Histogram, Counter, Info 4 | from prometheus_client.utils import INF 5 | 6 | from src.variables import PROMETHEUS_PREFIX 7 | 8 | 9 | class Status(Enum): 10 | SUCCESS = 'success' 11 | FAILURE = 'failure' 12 | 13 | 14 | BUILD_INFO = Info( 15 | 'build', 16 | 'Build info', 17 | namespace=PROMETHEUS_PREFIX, 18 | ) 19 | 20 | ENV_VARIABLES_INFO = Info( 21 | 'env_variables', 22 | 'Env variables for the app', 23 | namespace=PROMETHEUS_PREFIX, 24 | ) 25 | 26 | GENESIS_TIME = Gauge( 27 | 'genesis_time', 28 | 'Genesis time', 29 | namespace=PROMETHEUS_PREFIX, 30 | ) 31 | 32 | ACCOUNT_BALANCE = Gauge( 33 | 'account_balance', 34 | 'Account balance', 35 | ['address'], 36 | namespace=PROMETHEUS_PREFIX, 37 | ) 38 | 39 | ORACLE_SLOT_NUMBER = Gauge( 40 | "slot_number", 41 | "Oracle head or finalized slot number", 42 | ["state"], # "head" or "finalized" 43 | namespace=PROMETHEUS_PREFIX, 44 | ) 45 | 46 | ORACLE_BLOCK_NUMBER = Gauge( 47 | "block_number", 48 | "Oracle head or finalized block number", 49 | ["state"], # "head" or "finalized" 50 | namespace=PROMETHEUS_PREFIX, 51 | ) 52 | 53 | FUNCTIONS_DURATION = Histogram( 54 | 'functions_duration', 55 | 'Duration of oracle daemon tasks', 56 | ['name', 'status'], 57 | namespace=PROMETHEUS_PREFIX, 58 | buckets=(.1, .5, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0, 30.0, 60.0, 120.0, 180.0, 240.0, 300.0, 600.0, INF), 59 | ) 60 | 61 | requests_buckets = (.01, .05, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 120.0, INF) 62 | 63 | EL_REQUESTS_DURATION = Histogram( 64 | 'el_requests_duration', 65 | 'Duration of requests to EL RPC', 66 | ['endpoint', 'call_method', 'call_to', 'code', 'domain'], 67 | namespace=PROMETHEUS_PREFIX, 68 | buckets=requests_buckets, 69 | ) 70 | 71 | CL_REQUESTS_DURATION = Histogram( 72 | 'cl_requests_duration', 73 | 'Duration of requests to CL API', 74 | ['endpoint', 'code', 'domain'], 75 | namespace=PROMETHEUS_PREFIX, 76 | buckets=requests_buckets, 77 | ) 78 | 79 | KEYS_API_REQUESTS_DURATION = Histogram( 80 | 'keys_api_requests_duration', 81 | 'Duration of requests to Keys API', 82 | ['endpoint', 'code', 'domain'], 83 | namespace=PROMETHEUS_PREFIX, 84 | buckets=requests_buckets, 85 | ) 86 | 87 | KEYS_API_LATEST_BLOCKNUMBER = Gauge( 88 | 'keys_api_latest_blocknumber', 89 | 'Latest blocknumber from Keys API metadata', 90 | namespace=PROMETHEUS_PREFIX, 91 | ) 92 | 93 | TRANSACTIONS_COUNT = Counter( 94 | 'transactions_count', 95 | 'Total count of transactions. Success or failure', 96 | ['status'], 97 | namespace=PROMETHEUS_PREFIX, 98 | ) 99 | -------------------------------------------------------------------------------- /src/metrics/prometheus/business.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Gauge, Info 2 | 3 | from src.variables import PROMETHEUS_PREFIX 4 | 5 | 6 | ORACLE_MEMBER_INFO = Info( 7 | "member", 8 | "Oracle member info", 9 | namespace=PROMETHEUS_PREFIX, 10 | ) 11 | 12 | ORACLE_MEMBER_LAST_REPORT_REF_SLOT = Gauge( 13 | "member_last_report_ref_slot", 14 | "Member last report ref slot", 15 | namespace=PROMETHEUS_PREFIX, 16 | ) 17 | 18 | FRAME_CURRENT_REF_SLOT = Gauge( 19 | "frame_current_ref_slot", 20 | "Oracle frame current ref slot", 21 | namespace=PROMETHEUS_PREFIX, 22 | ) 23 | 24 | FRAME_DEADLINE_SLOT = Gauge( 25 | "frame_deadline_slot", 26 | "Oracle frame deadline slot", 27 | namespace=PROMETHEUS_PREFIX, 28 | ) 29 | 30 | FRAME_PREV_REPORT_REF_SLOT = Gauge( 31 | "frame_prev_report_ref_slot", 32 | "Oracle frame previous report ref slot", 33 | ['type'], 34 | namespace=PROMETHEUS_PREFIX, 35 | ) 36 | 37 | CONTRACT_ON_PAUSE = Gauge( 38 | "contract_on_pause", 39 | "Contract on pause", 40 | ['type'], 41 | namespace=PROMETHEUS_PREFIX, 42 | ) 43 | -------------------------------------------------------------------------------- /src/metrics/prometheus/csm.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Gauge 2 | 3 | from src.variables import PROMETHEUS_PREFIX 4 | 5 | 6 | CSM_CURRENT_FRAME_RANGE_L_EPOCH = Gauge( 7 | "csm_current_frame_range_l_epoch", 8 | "Left epoch of the current frame range", 9 | namespace=PROMETHEUS_PREFIX, 10 | ) 11 | 12 | CSM_CURRENT_FRAME_RANGE_R_EPOCH = Gauge( 13 | "csm_current_frame_range_r_epoch", 14 | "Right epoch of the current frame range", 15 | namespace=PROMETHEUS_PREFIX, 16 | ) 17 | 18 | CSM_UNPROCESSED_EPOCHS_COUNT = Gauge( 19 | "csm_unprocessed_epochs_count", 20 | "Unprocessed epochs count", 21 | namespace=PROMETHEUS_PREFIX, 22 | ) 23 | 24 | 25 | CSM_MIN_UNPROCESSED_EPOCH = Gauge( 26 | "csm_min_unprocessed_epoch", 27 | "Minimum unprocessed epoch", 28 | namespace=PROMETHEUS_PREFIX, 29 | ) 30 | -------------------------------------------------------------------------------- /src/metrics/prometheus/duration_meter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import wraps 3 | from time import perf_counter 4 | from typing import Callable 5 | 6 | from src.metrics.prometheus.basic import FUNCTIONS_DURATION, Status 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def duration_meter[T](): 12 | def decorator(func: Callable[..., T]) -> Callable[..., T]: 13 | @wraps(func) 14 | def wrapper(*args, **kwargs) -> T: 15 | full_name = f"{func.__module__}.{func.__name__}" 16 | with FUNCTIONS_DURATION.time() as t: 17 | try: 18 | logger.debug({"msg": f"Function '{full_name}' started"}) 19 | result = func(*args, **kwargs) 20 | t.labels(name=full_name, status=Status.SUCCESS) 21 | return result 22 | except Exception as e: 23 | t.labels(name=full_name, status=Status.FAILURE) 24 | raise e 25 | finally: 26 | stop = perf_counter() 27 | logger.debug( 28 | { 29 | "msg": f"Task '{full_name}' finished", 30 | "duration (sec)": stop 31 | - t._start, # pylint: disable=protected-access 32 | } 33 | ) 34 | 35 | return wrapper 36 | 37 | return decorator 38 | -------------------------------------------------------------------------------- /src/metrics/prometheus/ejector.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Gauge 2 | 3 | from src.variables import PROMETHEUS_PREFIX 4 | 5 | 6 | EJECTOR_TO_WITHDRAW_WEI_AMOUNT = Gauge( 7 | "ejector_withdrawal_wei_amount", 8 | "Withdrawal wei amount", 9 | namespace=PROMETHEUS_PREFIX, 10 | ) 11 | 12 | EJECTOR_MAX_WITHDRAWAL_EPOCH = Gauge( 13 | "ejector_max_withdrawal_epoch", 14 | "The max exit epoch", 15 | namespace=PROMETHEUS_PREFIX, 16 | ) 17 | 18 | EJECTOR_VALIDATORS_COUNT_TO_EJECT = Gauge( 19 | "ejector_validators_count_to_eject", 20 | "Reported validators count to eject", 21 | namespace=PROMETHEUS_PREFIX, 22 | ) 23 | -------------------------------------------------------------------------------- /src/metrics/prometheus/validators.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Gauge 2 | 3 | from src.variables import PROMETHEUS_PREFIX 4 | 5 | 6 | ALL_VALIDATORS = Gauge( 7 | "all_validators", 8 | "All validators", 9 | namespace=PROMETHEUS_PREFIX, 10 | ) 11 | 12 | LIDO_VALIDATORS = Gauge( 13 | "lido_validators", 14 | "Lido validators", 15 | namespace=PROMETHEUS_PREFIX, 16 | ) 17 | 18 | ALL_SLASHED_VALIDATORS = Gauge( 19 | "all_slashed_validators", 20 | "All slashed validators", 21 | namespace=PROMETHEUS_PREFIX, 22 | ) 23 | 24 | LIDO_SLASHED_VALIDATORS = Gauge( 25 | "lido_slashed_validators", 26 | "Lido slashed validators", 27 | namespace=PROMETHEUS_PREFIX, 28 | ) 29 | -------------------------------------------------------------------------------- /src/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/__init__.py -------------------------------------------------------------------------------- /src/modules/accounting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/accounting/__init__.py -------------------------------------------------------------------------------- /src/modules/accounting/third_phase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/accounting/third_phase/__init__.py -------------------------------------------------------------------------------- /src/modules/accounting/third_phase/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | class ItemType(Enum): 6 | EXTRA_DATA_TYPE_STUCK_VALIDATORS = 1 7 | EXTRA_DATA_TYPE_EXITED_VALIDATORS = 2 8 | 9 | 10 | class FormatList(Enum): 11 | EXTRA_DATA_FORMAT_LIST_EMPTY = 0 12 | EXTRA_DATA_FORMAT_LIST_NON_EMPTY = 1 13 | 14 | 15 | @dataclass 16 | class ExtraData: 17 | extra_data_list: list[bytes] 18 | data_hash: bytes 19 | format: int 20 | items_count: int 21 | 22 | 23 | class ExtraDataLengths: 24 | NEXT_HASH = 32 25 | ITEM_INDEX = 3 26 | ITEM_TYPE = 2 27 | MODULE_ID = 3 28 | NODE_OPS_COUNT = 8 29 | NODE_OPERATOR_ID = 8 30 | STUCK_OR_EXITED_VALS_COUNT = 16 31 | -------------------------------------------------------------------------------- /src/modules/checks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/checks/__init__.py -------------------------------------------------------------------------------- /src/modules/checks/checks_module.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class ChecksModule: 5 | """ 6 | Module that executes all tests to figure out that environment is ready for Oracle. 7 | 8 | Checks: 9 | - Consensus Layer node 10 | - Execution Layer node 11 | - Keys API service 12 | if LIDO_LOCATOR address provided 13 | - Checks configs in Accounting module and Ejector module 14 | if CSM_MODULE_ADDRESS provided 15 | - Checks configs in CSM oracle module 16 | - Checks with special blockstamp value (6300 slots in the past) 17 | """ 18 | def execute_module(self): 19 | return pytest.main([ 20 | 'src/modules/checks/suites', 21 | '-c', 'src/modules/checks/pytest.ini', 22 | ]) 23 | -------------------------------------------------------------------------------- /src/modules/checks/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbosity=0 --no-header --tb=short --show-capture=no --assert=plain --color=yes -n auto 3 | python_files = *.py 4 | python_classes = Check 5 | python_functions = check_* 6 | filterwarnings = 7 | ignore::pytest.PytestAssertRewriteWarning 8 | -------------------------------------------------------------------------------- /src/modules/checks/suites/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/checks/suites/__init__.py -------------------------------------------------------------------------------- /src/modules/checks/suites/common.py: -------------------------------------------------------------------------------- 1 | """Common checks""" 2 | import pytest 3 | 4 | from src.main import check_providers_chain_ids as chain_ids_check # rename to not conflict with test 5 | from src.modules.accounting.accounting import Accounting 6 | from src.modules.ejector.ejector import Ejector 7 | from src.modules.csm.csm import CSOracle 8 | 9 | 10 | @pytest.fixture() 11 | def skip_locator(web3): 12 | if not hasattr(web3, 'lido_contracts'): 13 | pytest.skip('LIDO_LOCATOR_ADDRESS is not set') 14 | 15 | 16 | @pytest.fixture() 17 | def skip_csm(web3): 18 | if not hasattr(web3, 'csm'): 19 | pytest.skip('CSM_MODULE_ADDRESS is not set') 20 | 21 | 22 | @pytest.fixture() 23 | def accounting(web3, skip_locator): 24 | return Accounting(web3) 25 | 26 | 27 | @pytest.fixture() 28 | def ejector(web3, skip_locator): 29 | return Ejector(web3) 30 | 31 | 32 | @pytest.fixture() 33 | def csm(web3, skip_locator, skip_csm): 34 | return CSOracle(web3) 35 | 36 | 37 | def check_providers_chain_ids(web3): 38 | """Make sure all providers are on the same chain""" 39 | chain_ids_check(web3, web3.cc, web3.kac) 40 | 41 | 42 | def check_accounting_contract_configs(accounting): 43 | """Make sure accounting contract configs are valid""" 44 | accounting.check_contract_configs() 45 | 46 | 47 | def check_ejector_contract_configs(ejector): 48 | """Make sure ejector contract configs are valid""" 49 | ejector.check_contract_configs() 50 | 51 | 52 | def check_csm_contract_configs(csm): 53 | """Make sure csm contract configs are valid""" 54 | csm.check_contract_configs() 55 | -------------------------------------------------------------------------------- /src/modules/checks/suites/consensus_node.py: -------------------------------------------------------------------------------- 1 | """Consensus node""" 2 | 3 | from src.web3py.types import Web3 4 | 5 | 6 | def check_validators_provided(web3: Web3, blockstamp): 7 | """Check that consensus-client able to provide validators""" 8 | assert web3.cc.get_validators_no_cache(blockstamp), "consensus-client provide no validators" 9 | 10 | 11 | def check_block_details_provided(web3: Web3, blockstamp): 12 | """Check that consensus-client able to provide block details""" 13 | assert web3.cc.get_block_details(blockstamp.slot_number), "consensus-client provide no block details" 14 | 15 | 16 | def check_block_root_provided(web3: Web3, blockstamp): 17 | """Check that consensus-client able to provide block root""" 18 | assert web3.cc.get_block_root(blockstamp.slot_number), "consensus-client provide no block root" 19 | 20 | 21 | def check_block_header_provided(web3: Web3, blockstamp): 22 | """Check that consensus-client able to provide block header""" 23 | assert web3.cc.get_block_header(blockstamp.slot_number), "consensus-client provide no block header" 24 | 25 | 26 | def check_block_roots_from_state_provided(web3: Web3, blockstamp): 27 | """Check that consensus-client able to provide block roots from state""" 28 | assert web3.cc.get_state_block_roots(blockstamp.slot_number), "consensus-client provide no block roots from state" 29 | 30 | 31 | def check_attestation_committees(web3: Web3, blockstamp): 32 | """Check that consensus-client able to provide attestation committees""" 33 | cc_config = web3.cc.get_config_spec() 34 | slots_per_epoch = cc_config.SLOTS_PER_EPOCH 35 | epoch = blockstamp.slot_number // slots_per_epoch - cc_config.SLOTS_PER_HISTORICAL_ROOT // slots_per_epoch 36 | assert web3.cc.get_attestation_committees(blockstamp, epoch), "consensus-client provide no attestation committees" 37 | 38 | 39 | def check_block_attestations(web3: Web3, blockstamp): 40 | """Check that consensus-client able to provide block attestations""" 41 | assert web3.cc.get_block_attestations(blockstamp.slot_number), "consensus-client provide no block attestations" 42 | -------------------------------------------------------------------------------- /src/modules/checks/suites/execution_node.py: -------------------------------------------------------------------------------- 1 | """Execution node""" 2 | import pytest 3 | 4 | from src.providers.execution.contracts.deposit_contract import DepositContract 5 | from src.utils.events import get_events_in_range 6 | 7 | get_deposit_count_abi = { 8 | "inputs": [], 9 | "name": "get_deposit_count", 10 | "outputs": [ 11 | {'internalType': "bytes", 'name': "", 'type': "bytes"} 12 | ], 13 | "stateMutability": "view", 14 | "type": "function" 15 | } 16 | 17 | deposit_event_abi = {'anonymous': False, 'inputs': [ 18 | {'indexed': False, 'internalType': "bytes", 'name': "pubkey", 'type': "bytes"}, 19 | {'indexed': False, 'internalType': "bytes", 'name': "withdrawal_credentials", 'type': "bytes"}, 20 | {'indexed': False, 'internalType': "bytes", 'name': "amount", 'type': "bytes"}, 21 | {'indexed': False, 'internalType': "bytes", 'name': "signature", 'type': "bytes"}, 22 | {'indexed': False, 'internalType': "bytes", 'name': "index", 'type': "bytes"} 23 | ], 'name': "DepositEvent", 'type': "event"} 24 | 25 | 26 | @pytest.fixture 27 | def deposit_contract(web3): 28 | cc_config = web3.cc.get_config_spec() 29 | return web3.eth.contract( 30 | address=web3.to_checksum_address(cc_config.DEPOSIT_CONTRACT_ADDRESS), 31 | ContractFactoryClass=DepositContract, 32 | decode_tuples=True, 33 | ) 34 | 35 | 36 | def check_eth_call_availability(blockstamp, deposit_contract): 37 | """Check that execution-client able to make eth_call on the provided blockstamp""" 38 | deposit_contract.get_deposit_count(block_identifier=blockstamp.block_hash) 39 | 40 | 41 | def check_balance_availability(web3, blockstamp, deposit_contract): 42 | """Check that execution-client able to get balance on the provided blockstamp""" 43 | web3.eth.get_balance(deposit_contract.address, block_identifier=blockstamp.block_hash) 44 | 45 | 46 | def check_events_range_availability(deposit_contract, blockstamp, finalized_blockstamp): 47 | """Check that execution-client able to get event logs in a range""" 48 | events = list( 49 | get_events_in_range( 50 | deposit_contract.events.DepositEvent, 51 | l_block=blockstamp.block_number, 52 | r_block=finalized_blockstamp.block_number, 53 | ) 54 | ) 55 | deposits_count_before = deposit_contract.get_deposit_count(blockstamp.block_number - 1) 56 | deposits_count_now = deposit_contract.get_deposit_count(finalized_blockstamp.block_hash) 57 | assert deposits_count_now >= deposits_count_before, "Deposits count decreased" 58 | assert len(events) == (deposits_count_now - deposits_count_before), "Events count doesn't match deposits count" 59 | -------------------------------------------------------------------------------- /src/modules/checks/suites/ipfs.py: -------------------------------------------------------------------------------- 1 | """IPFS provider""" 2 | 3 | 4 | import random 5 | import string 6 | 7 | import pytest 8 | 9 | from src import variables 10 | from src.main import ipfs_providers 11 | from src.providers.ipfs import GW3, IPFSError, IPFSProvider, Pinata, PublicIPFS 12 | 13 | 14 | @pytest.fixture() 15 | def content(): 16 | letters = string.ascii_letters 17 | return "".join(random.choice(letters) for _ in range(255)) 18 | 19 | 20 | def providers(): 21 | configured_providers = tuple(ipfs_providers()) 22 | 23 | for typ in (GW3, Pinata): 24 | try: 25 | provider = [p for p in configured_providers if isinstance(p, typ)].pop() 26 | except IndexError: 27 | yield pytest.param( 28 | None, 29 | marks=pytest.mark.skip(f"{typ.__name__} provider is not configured"), 30 | id=typ.__name__, 31 | ) 32 | else: 33 | yield pytest.param(provider, id=typ.__name__) 34 | 35 | 36 | @pytest.mark.parametrize("provider", providers()) 37 | def check_ipfs_provider(provider: IPFSProvider, content: str): 38 | """Checks that configured IPFS provider can be used by CSM""" 39 | 40 | try: 41 | cid = provider.publish(content.encode()) 42 | ret = provider.fetch(cid).decode() 43 | if ret != content: 44 | raise IPFSError(f"Content mismatch, got={ret}, expected={content}") 45 | except IPFSError as e: 46 | raise AssertionError(f"Provider {provider.__class__.__name__} is not working") from e 47 | 48 | 49 | def check_csm_requires_ipfs_provider(): 50 | if not variables.CSM_MODULE_ADDRESS: 51 | pytest.skip("IPFS provider is not requirement for non-CSM oracle") 52 | 53 | providers = [p for p in ipfs_providers() if not isinstance(p, PublicIPFS)] 54 | if not providers: 55 | pytest.fail("CSM oracle requires IPFS provider with pinnig support") 56 | -------------------------------------------------------------------------------- /src/modules/checks/suites/keys_api.py: -------------------------------------------------------------------------------- 1 | """Keys api""" 2 | 3 | 4 | def check_keys_api_provide_keys(web3, blockstamp): 5 | """Check that keys-api able to provide keys""" 6 | result = web3.kac.get_used_lido_keys(blockstamp) 7 | assert len(result) > 0, "keys-api service provide no keys" 8 | -------------------------------------------------------------------------------- /src/modules/csm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/csm/__init__.py -------------------------------------------------------------------------------- /src/modules/csm/log.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import defaultdict 3 | from dataclasses import asdict, dataclass, field 4 | 5 | from src.modules.csm.state import AttestationsAccumulator 6 | from src.modules.csm.types import Shares 7 | from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, ValidatorIndex 8 | 9 | 10 | class LogJSONEncoder(json.JSONEncoder): ... 11 | 12 | 13 | @dataclass 14 | class ValidatorFrameSummary: 15 | perf: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) 16 | slashed: bool = False 17 | 18 | 19 | @dataclass 20 | class OperatorFrameSummary: 21 | distributed: int = 0 22 | validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) 23 | stuck: bool = False 24 | 25 | 26 | @dataclass 27 | class FramePerfLog: 28 | """A log of performance assessed per operator in the given frame""" 29 | 30 | blockstamp: ReferenceBlockStamp 31 | frame: tuple[EpochNumber, EpochNumber] 32 | threshold: float = 0.0 33 | distributable: Shares = 0 34 | operators: dict[NodeOperatorId, OperatorFrameSummary] = field( 35 | default_factory=lambda: defaultdict(OperatorFrameSummary) 36 | ) 37 | 38 | def encode(self) -> bytes: 39 | return ( 40 | LogJSONEncoder( 41 | indent=None, 42 | separators=(',', ':'), 43 | sort_keys=True, 44 | ) 45 | .encode(asdict(self)) 46 | .encode() 47 | ) 48 | -------------------------------------------------------------------------------- /src/modules/csm/tree.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from typing import Self, Sequence 4 | 5 | from hexbytes import HexBytes 6 | from oz_merkle_tree import Dump, StandardMerkleTree 7 | 8 | from src.modules.csm.types import RewardTreeLeaf 9 | from src.providers.ipfs.cid import CID 10 | 11 | 12 | class TreeJSONEncoder(json.JSONEncoder): 13 | def default(self, o): 14 | if isinstance(o, bytes): 15 | return f"0x{o.hex()}" 16 | if isinstance(o, CID): 17 | return str(o) 18 | return super().default(o) 19 | 20 | 21 | @dataclass 22 | class Tree: 23 | """A wrapper around StandardMerkleTree to cover use cases of the CSM oracle""" 24 | 25 | tree: StandardMerkleTree[RewardTreeLeaf] 26 | 27 | @property 28 | def root(self) -> HexBytes: 29 | return HexBytes(self.tree.root) 30 | 31 | @classmethod 32 | def decode(cls, content: bytes) -> Self: 33 | """Restore a tree from a supported binary representation""" 34 | 35 | try: 36 | return cls(StandardMerkleTree.load(json.loads(content))) 37 | except json.JSONDecodeError as e: 38 | raise ValueError("Unsupported tree format") from e 39 | 40 | def encode(self) -> bytes: 41 | """Convert the underlying StandardMerkleTree to a binary representation""" 42 | 43 | return ( 44 | TreeJSONEncoder( 45 | indent=None, 46 | separators=(',', ':'), 47 | sort_keys=True, 48 | ) 49 | .encode(self.dump()) 50 | .encode() 51 | ) 52 | 53 | def dump(self) -> Dump[RewardTreeLeaf]: 54 | return self.tree.dump() 55 | 56 | @classmethod 57 | def new(cls, values: Sequence[RewardTreeLeaf]) -> Self: 58 | """Create new instance around the wrapped tree out of the given values""" 59 | return cls(StandardMerkleTree(values, ("uint256", "uint256"))) 60 | -------------------------------------------------------------------------------- /src/modules/csm/types.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dataclasses import dataclass 3 | from typing import TypeAlias, Literal 4 | 5 | from hexbytes import HexBytes 6 | 7 | from src.providers.ipfs import CID 8 | from src.types import NodeOperatorId, SlotNumber 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | Shares: TypeAlias = int 14 | RewardTreeLeaf: TypeAlias = tuple[NodeOperatorId, Shares] 15 | 16 | 17 | @dataclass 18 | class ReportData: 19 | consensusVersion: int 20 | ref_slot: SlotNumber 21 | tree_root: HexBytes 22 | tree_cid: CID | Literal[""] 23 | log_cid: CID 24 | distributed: int 25 | 26 | def as_tuple(self): 27 | # Tuple with report in correct order 28 | return ( 29 | self.consensusVersion, 30 | self.ref_slot, 31 | self.tree_root, 32 | str(self.tree_cid), 33 | str(self.log_cid), 34 | self.distributed, 35 | ) 36 | -------------------------------------------------------------------------------- /src/modules/ejector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/ejector/__init__.py -------------------------------------------------------------------------------- /src/modules/ejector/data_encode.py: -------------------------------------------------------------------------------- 1 | from eth_typing import HexStr 2 | 3 | from src.types import ValidatorIndex 4 | from src.utils.types import hex_str_to_bytes 5 | from src.web3py.extensions.lido_validators import LidoValidator, NodeOperatorGlobalIndex 6 | 7 | 8 | DATA_FORMAT_LIST = 1 9 | 10 | MODULE_ID_LENGTH = 3 11 | NODE_OPERATOR_ID_LENGTH = 5 12 | VALIDATOR_INDEX_LENGTH = 8 13 | VALIDATOR_PUB_KEY_LENGTH = 48 14 | 15 | 16 | def encode_data(validators_to_eject: list[tuple[NodeOperatorGlobalIndex, LidoValidator]]): 17 | """ 18 | Encodes report data for Exit Bus Contract into bytes. 19 | 20 | MSB <------------------------------------------------------- LSB 21 | | 3 bytes | 5 bytes | 8 bytes | 48 bytes | 22 | | moduleId | nodeOpId | validatorIndex | validatorPubkey | 23 | """ 24 | validators = sort_validators_to_eject(validators_to_eject) 25 | 26 | result = b'' 27 | 28 | for (module_id, op_id), validator in validators: 29 | result += module_id.to_bytes(MODULE_ID_LENGTH) 30 | result += op_id.to_bytes(NODE_OPERATOR_ID_LENGTH) 31 | result += validator.index.to_bytes(VALIDATOR_INDEX_LENGTH) 32 | 33 | pubkey_bytes = hex_str_to_bytes(HexStr(validator.validator.pubkey)) 34 | 35 | if len(pubkey_bytes) != VALIDATOR_PUB_KEY_LENGTH: 36 | raise ValueError(f'Unexpected size of validator pub key. Pub key size: {len(validator.validator.pubkey)}') 37 | 38 | result += pubkey_bytes 39 | 40 | return result, DATA_FORMAT_LIST 41 | 42 | 43 | def sort_validators_to_eject( 44 | validators_to_eject: list[tuple[NodeOperatorGlobalIndex, LidoValidator]], 45 | ) -> list[tuple[NodeOperatorGlobalIndex, LidoValidator]]: 46 | def _nog_validator_key(no_validator: tuple[NodeOperatorGlobalIndex, LidoValidator]) -> tuple[int, int, ValidatorIndex]: 47 | (module_id, no_id), validator = no_validator 48 | return module_id, no_id, validator.index 49 | 50 | validators = sorted(validators_to_eject, key=_nog_validator_key) 51 | 52 | return validators 53 | -------------------------------------------------------------------------------- /src/modules/ejector/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from src.types import SlotNumber 4 | 5 | 6 | @dataclass 7 | class EjectorProcessingState: 8 | current_frame_ref_slot: SlotNumber 9 | processing_deadline_time: int 10 | data_hash: bytes 11 | data_submitted: bool 12 | data_format: int 13 | requests_count: int 14 | requests_submitted: int 15 | 16 | 17 | @dataclass 18 | class ReportData: 19 | consensusVersion: int 20 | ref_slot: SlotNumber 21 | requests_count: int 22 | data_format: int 23 | data: bytes 24 | 25 | def as_tuple(self): 26 | # Tuple with report in correct order 27 | return ( 28 | self.consensusVersion, 29 | self.ref_slot, 30 | self.requests_count, 31 | self.data_format, 32 | self.data, 33 | ) 34 | -------------------------------------------------------------------------------- /src/modules/submodules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/modules/submodules/__init__.py -------------------------------------------------------------------------------- /src/modules/submodules/exceptions.py: -------------------------------------------------------------------------------- 1 | class IsNotMemberException(Exception): 2 | pass 3 | 4 | 5 | class IncompatibleOracleVersion(Exception): 6 | pass 7 | 8 | 9 | class ContractVersionMismatch(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /src/modules/submodules/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from src.types import SlotNumber 4 | 5 | 6 | ZERO_HASH = bytes([0]*32) 7 | 8 | 9 | @dataclass 10 | class MemberInfo: 11 | is_report_member: bool 12 | is_submit_member: bool 13 | is_fast_lane: bool 14 | last_report_ref_slot: SlotNumber 15 | fast_lane_length_slot: int 16 | current_frame_ref_slot: SlotNumber 17 | deadline_slot: SlotNumber 18 | current_frame_member_report: bytes 19 | current_frame_consensus_report: bytes 20 | 21 | 22 | @dataclass(frozen=True) 23 | class ChainConfig: 24 | slots_per_epoch: int 25 | seconds_per_slot: int 26 | genesis_time: int 27 | 28 | 29 | @dataclass(frozen=True) 30 | class CurrentFrame: 31 | # Order is important! 32 | ref_slot: SlotNumber 33 | report_processing_deadline_slot: SlotNumber 34 | 35 | 36 | @dataclass(frozen=True) 37 | class FrameConfig: 38 | # Order is important! 39 | initial_epoch: int 40 | epochs_per_frame: int 41 | fast_lane_length_slots: int 42 | -------------------------------------------------------------------------------- /src/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/providers/__init__.py -------------------------------------------------------------------------------- /src/providers/consensus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/providers/consensus/__init__.py -------------------------------------------------------------------------------- /src/providers/consistency.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from abc import abstractmethod, ABC 3 | 4 | 5 | class InconsistentProviders(Exception): 6 | pass 7 | 8 | 9 | class NotHealthyProvider(Exception): 10 | pass 11 | 12 | 13 | class ProviderConsistencyModule(ABC): 14 | """ 15 | A class that provides HTTP provider ability to check that 16 | provided hosts are alive and chain ids are same. 17 | 18 | Methods must be implemented: 19 | def get_all_providers(self) -> [any]: 20 | def _get_chain_id_with_provider(self, int) -> int: 21 | """ 22 | def check_providers_consistency(self) -> int | None: 23 | chain_id = None 24 | 25 | for provider_index in range(len(self.get_all_providers())): 26 | try: 27 | curr_chain_id = self._get_chain_id_with_provider(provider_index) 28 | except Exception as error: 29 | raise NotHealthyProvider(f'Provider [{provider_index}] does not responding.') from error 30 | 31 | if chain_id is None: 32 | chain_id = curr_chain_id 33 | elif chain_id != curr_chain_id: 34 | raise InconsistentProviders(f'Different chain ids detected for {provider_index=}. ' 35 | f'Expected {curr_chain_id=}, got {chain_id=}.') 36 | 37 | return chain_id 38 | 39 | @abstractmethod 40 | def get_all_providers(self) -> list[Any]: 41 | """Returns list of hosts or providers.""" 42 | raise NotImplementedError("get_all_providers should be implemented") 43 | 44 | @abstractmethod 45 | def _get_chain_id_with_provider(self, provider_index: int) -> int: 46 | """Does a health check call and returns chain_id for current host""" 47 | raise NotImplementedError("_get_chain_id_with_provider should be implemented") 48 | -------------------------------------------------------------------------------- /src/providers/execution/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/providers/execution/__init__.py -------------------------------------------------------------------------------- /src/providers/execution/base_interface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Any, Self, Type 4 | 5 | from web3 import Web3 6 | from web3.types import BlockIdentifier 7 | 8 | from src.web3py.contract_tweak import Contract 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class ContractInterface(Contract): 14 | abi_path: str 15 | 16 | @staticmethod 17 | def load_abi(abi_file: str) -> dict: 18 | with open(abi_file) as abi_json: 19 | return json.load(abi_json) 20 | 21 | @classmethod 22 | def factory(cls, w3: Web3, class_name: str | None = None, **kwargs: Any) -> Type[Self]: 23 | if cls.abi_path is None: 24 | raise AttributeError(f'abi_path attribute is missing in {cls.__name__} class') 25 | 26 | kwargs['abi'] = cls.load_abi(cls.abi_path) 27 | return super().factory(w3, class_name, **kwargs) 28 | 29 | def is_deployed(self, block: BlockIdentifier) -> bool: 30 | result = self.w3.eth.get_code(self.address, block_identifier=block) != b"" 31 | logger.info({"msg": f"Check that the contract {self.__class__.__name__} exists at {block=}", "value": result}) 32 | return result 33 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/accounting_oracle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from src.utils.cache import global_lru_cache as lru_cache 3 | 4 | from web3.contract.contract import ContractFunction 5 | from web3.types import BlockIdentifier 6 | 7 | from src.modules.accounting.types import AccountingProcessingState 8 | from src.providers.execution.contracts.base_oracle import BaseOracleContract 9 | from src.utils.abi import named_tuple_to_dataclass 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class AccountingOracleContract(BaseOracleContract): 16 | abi_path = './assets/AccountingOracle.json' 17 | 18 | @lru_cache(maxsize=1) 19 | def get_processing_state(self, block_identifier: BlockIdentifier = 'latest') -> AccountingProcessingState: 20 | """ 21 | Returns data processing state for the current reporting frame. 22 | """ 23 | response = self.functions.getProcessingState().call(block_identifier=block_identifier) 24 | response = named_tuple_to_dataclass(response, AccountingProcessingState) 25 | logger.info({ 26 | 'msg': 'Call `getProcessingState()`.', 27 | 'value': response, 28 | 'block_identifier': repr(block_identifier), 29 | 'to': self.address, 30 | }) 31 | return response 32 | 33 | def submit_report_extra_data_empty(self) -> ContractFunction: 34 | """ 35 | Triggers the processing required when no extra data is present in the report, 36 | i.e. when extra data format equals EXTRA_DATA_FORMAT_EMPTY. 37 | """ 38 | tx = self.functions.submitReportExtraDataEmpty() 39 | logger.info({'msg': 'Build `submitReportExtraDataEmpty()` tx.'}) 40 | return tx 41 | 42 | def submit_report_extra_data_list(self, extra_data: bytes) -> ContractFunction: 43 | """ 44 | Submits report extra data in the EXTRA_DATA_FORMAT_LIST format for processing. 45 | """ 46 | tx = self.functions.submitReportExtraDataList(extra_data) 47 | logger.info({'msg': f'Build `submitReportExtraDataList({extra_data.hex()})` tx.'}) 48 | return tx 49 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/burner.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from src.utils.cache import global_lru_cache as lru_cache 3 | 4 | from web3.types import BlockIdentifier 5 | 6 | from src.modules.accounting.types import SharesRequestedToBurn 7 | from src.providers.execution.base_interface import ContractInterface 8 | from src.utils.abi import named_tuple_to_dataclass 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class BurnerContract(ContractInterface): 15 | abi_path = './assets/Burner.json' 16 | 17 | @lru_cache(maxsize=1) 18 | def get_shares_requested_to_burn(self, block_identifier: BlockIdentifier = 'latest') -> SharesRequestedToBurn: 19 | """ 20 | Returns the current amount of shares locked on the contract to be burnt. 21 | """ 22 | response = self.functions.getSharesRequestedToBurn().call(block_identifier=block_identifier) 23 | 24 | response = named_tuple_to_dataclass(response, SharesRequestedToBurn) 25 | logger.info({ 26 | 'msg': 'Call `getSharesRequestedToBurn()`.', 27 | 'value': response, 28 | 'block_identifier': repr(block_identifier), 29 | 'to': self.address, 30 | }) 31 | return response 32 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/cs_accounting.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from eth_typing import ChecksumAddress 4 | from web3 import Web3 5 | from web3.types import BlockIdentifier 6 | 7 | from ..base_interface import ContractInterface 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class CSAccountingContract(ContractInterface): 13 | abi_path = "./assets/CSAccounting.json" 14 | 15 | def fee_distributor(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: 16 | """Returns the address of the CSFeeDistributor contract""" 17 | 18 | resp = self.functions.feeDistributor().call(block_identifier=block_identifier) 19 | logger.info( 20 | { 21 | "msg": "Call `feeDistributor()`.", 22 | "value": resp, 23 | "block_identifier": repr(block_identifier), 24 | } 25 | ) 26 | return Web3.to_checksum_address(resp) 27 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/cs_fee_distributor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from eth_typing import ChecksumAddress 4 | from hexbytes import HexBytes 5 | from web3 import Web3 6 | from web3.types import BlockIdentifier 7 | 8 | from ..base_interface import ContractInterface 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class CSFeeDistributorContract(ContractInterface): 14 | abi_path = "./assets/CSFeeDistributor.json" 15 | 16 | def oracle(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: 17 | """Returns the address of the CSFeeOracle contract""" 18 | 19 | resp = self.functions.ORACLE().call(block_identifier=block_identifier) 20 | logger.info( 21 | { 22 | "msg": "Call `ORACLE()`.", 23 | "value": resp, 24 | "block_identifier": repr(block_identifier), 25 | } 26 | ) 27 | return Web3.to_checksum_address(resp) 28 | 29 | def shares_to_distribute(self, block_identifier: BlockIdentifier = "latest") -> int: 30 | """Returns the amount of shares that are pending to be distributed""" 31 | 32 | resp = self.functions.pendingSharesToDistribute().call(block_identifier=block_identifier) 33 | logger.info( 34 | { 35 | "msg": "Call `pendingSharesToDistribute()`.", 36 | "value": resp, 37 | "block_identifier": repr(block_identifier), 38 | } 39 | ) 40 | return resp 41 | 42 | def tree_root(self, block_identifier: BlockIdentifier = "latest") -> HexBytes: 43 | """Root of the latest published Merkle tree""" 44 | 45 | resp = self.functions.treeRoot().call(block_identifier=block_identifier) 46 | logger.info( 47 | { 48 | "msg": "Call `treeRoot()`.", 49 | "value": resp, 50 | "block_identifier": repr(block_identifier), 51 | } 52 | ) 53 | return HexBytes(resp) 54 | 55 | def tree_cid(self, block_identifier: BlockIdentifier = "latest") -> str: 56 | """CID of the latest published Merkle tree""" 57 | 58 | resp = self.functions.treeCid().call(block_identifier=block_identifier) 59 | logger.info( 60 | { 61 | "msg": "Call `treeCid()`.", 62 | "value": resp, 63 | "block_identifier": repr(block_identifier), 64 | } 65 | ) 66 | return resp 67 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/cs_fee_oracle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from web3.types import BlockIdentifier 4 | 5 | from src.providers.execution.contracts.base_oracle import BaseOracleContract 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class CSFeeOracleContract(BaseOracleContract): 11 | abi_path = "./assets/CSFeeOracle.json" 12 | 13 | def is_paused(self, block_identifier: BlockIdentifier = "latest") -> bool: 14 | """Returns whether the contract is paused""" 15 | 16 | resp = self.functions.isPaused().call(block_identifier=block_identifier) 17 | logger.info( 18 | { 19 | "msg": "Call `isPaused()`.", 20 | "value": resp, 21 | "block_identifier": repr(block_identifier), 22 | } 23 | ) 24 | return resp 25 | 26 | def perf_leeway_bp(self, block_identifier: BlockIdentifier = "latest") -> int: 27 | """Performance threshold leeway used to determine underperforming validators""" 28 | 29 | resp = self.functions.avgPerfLeewayBP().call(block_identifier=block_identifier) 30 | logger.info( 31 | { 32 | "msg": "Call `avgPerfLeewayBP()`.", 33 | "value": resp, 34 | "block_identifier": repr(block_identifier), 35 | } 36 | ) 37 | return resp 38 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/cs_module.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import NamedTuple 3 | 4 | from eth_typing import ChecksumAddress 5 | from web3 import Web3 6 | from web3.types import BlockIdentifier 7 | 8 | from src.constants import UINT64_MAX 9 | 10 | from ..base_interface import ContractInterface 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class NodeOperatorSummary(NamedTuple): 16 | """getNodeOperatorSummary response, @see IStakingModule.sol""" 17 | 18 | targetLimitMode: int 19 | targetValidatorsCount: int 20 | stuckValidatorsCount: int 21 | refundedValidatorsCount: int 22 | stuckPenaltyEndTimestamp: int 23 | totalExitedValidators: int 24 | totalDepositedValidators: int 25 | depositableValidatorsCount: int 26 | 27 | 28 | class CSModuleContract(ContractInterface): 29 | abi_path = "./assets/CSModule.json" 30 | 31 | MAX_OPERATORS_COUNT = UINT64_MAX 32 | 33 | def accounting(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: 34 | """Returns the address of the CSAccounting contract""" 35 | 36 | resp = self.functions.accounting().call(block_identifier=block_identifier) 37 | logger.info( 38 | { 39 | "msg": "Call `accounting()`.", 40 | "value": resp, 41 | "block_identifier": repr(block_identifier), 42 | } 43 | ) 44 | return Web3.to_checksum_address(resp) 45 | 46 | def is_paused(self, block: BlockIdentifier = "latest") -> bool: 47 | resp = self.functions.isPaused().call(block_identifier=block) 48 | logger.info( 49 | { 50 | "msg": "Call `isPaused()`.", 51 | "value": resp, 52 | "block_identifier": repr(block), 53 | } 54 | ) 55 | return resp 56 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/deposit_contract.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from eth_typing import BlockIdentifier 4 | 5 | from ..base_interface import ContractInterface 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class DepositContract(ContractInterface): 11 | abi_path = "./assets/DepositContract.json" 12 | 13 | def get_deposit_count(self, block_identifier: BlockIdentifier) -> int: 14 | resp = self.functions.get_deposit_count().call(block_identifier=block_identifier) 15 | logger.info( 16 | { 17 | "msg": "Call `get_deposit_count()`.", 18 | "value": resp, 19 | "block_identifier": repr(block_identifier), 20 | } 21 | ) 22 | return int.from_bytes(resp, "little") 23 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/exit_bus_oracle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from itertools import batched 3 | 4 | from src.utils.cache import global_lru_cache as lru_cache 5 | from typing import Sequence 6 | 7 | from web3.types import BlockIdentifier 8 | 9 | from src.modules.ejector.types import EjectorProcessingState 10 | from src.providers.execution.contracts.base_oracle import BaseOracleContract 11 | from src.utils.abi import named_tuple_to_dataclass 12 | from src.variables import EL_REQUESTS_BATCH_SIZE 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class ExitBusOracleContract(BaseOracleContract): 18 | abi_path = './assets/ValidatorsExitBusOracle.json' 19 | 20 | @lru_cache(maxsize=1) 21 | def is_paused(self, block_identifier: BlockIdentifier = 'latest') -> bool: 22 | """ 23 | Returns whether the contract is paused. 24 | """ 25 | response = self.functions.isPaused().call(block_identifier=block_identifier) 26 | logger.info({ 27 | 'msg': 'Call `isPaused()`.', 28 | 'value': response, 29 | 'block_identifier': repr(block_identifier), 30 | 'to': self.address, 31 | }) 32 | return response 33 | 34 | @lru_cache(maxsize=1) 35 | def get_processing_state(self, block_identifier: BlockIdentifier = 'latest') -> EjectorProcessingState: 36 | """ 37 | Returns data processing state for the current reporting frame. 38 | """ 39 | response = self.functions.getProcessingState().call(block_identifier=block_identifier) 40 | response = named_tuple_to_dataclass(response, EjectorProcessingState) 41 | logger.info({ 42 | 'msg': 'Call `getProcessingState()`.', 43 | 'value': response, 44 | 'block_identifier': repr(block_identifier), 45 | 'to': self.address, 46 | }) 47 | return response 48 | 49 | def get_last_requested_validator_indices( 50 | self, 51 | module_id: int, 52 | node_operators_ids_in_module: Sequence[int], 53 | block_identifier: BlockIdentifier = 'latest', 54 | ) -> list[int]: 55 | """ 56 | Returns the latest validator indices that were requested to exit for the given 57 | `nodeOpIds` in the given `moduleId`. For node operators that were never requested to exit 58 | any validator, index is set to -1. 59 | """ 60 | result = [] 61 | 62 | for no_list in batched(node_operators_ids_in_module, EL_REQUESTS_BATCH_SIZE): 63 | response = self.functions.getLastRequestedValidatorIndices( 64 | module_id, 65 | no_list, 66 | ).call(block_identifier=block_identifier) 67 | 68 | logger.info({ 69 | 'msg': f'Call `getLastRequestedValidatorIndices({module_id}, {len(no_list)})`.', 70 | 'len': len(response), 71 | 'block_identifier': repr(block_identifier), 72 | 'to': self.address, 73 | }) 74 | 75 | result.extend(response) 76 | 77 | return result 78 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/oracle_daemon_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from src.utils.cache import global_lru_cache as lru_cache 4 | 5 | from web3 import Web3 6 | from web3.types import BlockIdentifier 7 | 8 | from src.providers.execution.base_interface import ContractInterface 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class OracleDaemonConfigContract(ContractInterface): 15 | abi_path = './assets/OracleDaemonConfig.json' 16 | 17 | def _get(self, param: str, block_identifier: BlockIdentifier = 'latest') -> int: 18 | response = Web3.to_int(self.functions.get(param).call(block_identifier=block_identifier)) 19 | 20 | logger.info({ 21 | 'msg': f'Call `get({param})`.', 22 | 'value': response, 23 | 'block_identifier': repr(block_identifier), 24 | 'to': self.address, 25 | }) 26 | return response 27 | 28 | @lru_cache(maxsize=1) 29 | def normalized_cl_reward_per_epoch(self, block_identifier: BlockIdentifier = 'latest') -> int: 30 | return self._get('NORMALIZED_CL_REWARD_PER_EPOCH', block_identifier) 31 | 32 | @lru_cache(maxsize=1) 33 | def normalized_cl_reward_mistake_rate_bp(self, block_identifier: BlockIdentifier = 'latest') -> int: 34 | return self._get('NORMALIZED_CL_REWARD_MISTAKE_RATE_BP', block_identifier) 35 | 36 | @lru_cache(maxsize=1) 37 | def rebase_check_nearest_epoch_distance(self, block_identifier: BlockIdentifier = 'latest') -> int: 38 | return self._get('REBASE_CHECK_NEAREST_EPOCH_DISTANCE', block_identifier) 39 | 40 | @lru_cache(maxsize=1) 41 | def rebase_check_distant_epoch_distance(self, block_identifier: BlockIdentifier = 'latest') -> int: 42 | return self._get('REBASE_CHECK_DISTANT_EPOCH_DISTANCE', block_identifier) 43 | 44 | @lru_cache(maxsize=1) 45 | def node_operator_network_penetration_threshold_bp(self, block_identifier: BlockIdentifier = 'latest') -> int: 46 | return self._get('NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP', block_identifier) 47 | 48 | @lru_cache(maxsize=1) 49 | def prediction_duration_in_slots(self, block_identifier: BlockIdentifier = 'latest') -> int: 50 | return self._get('PREDICTION_DURATION_IN_SLOTS', block_identifier) 51 | 52 | @lru_cache(maxsize=1) 53 | def finalization_max_negative_rebase_epoch_shift(self, block_identifier: BlockIdentifier = 'latest') -> int: 54 | return self._get('FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT', block_identifier) 55 | 56 | @lru_cache(maxsize=1) 57 | def validator_delayed_timeout_in_slots(self, block_identifier: BlockIdentifier = 'latest') -> int: 58 | return self._get('VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS', block_identifier) 59 | 60 | @lru_cache(maxsize=1) 61 | def validator_delinquent_timeout_in_slots(self, block_identifier: BlockIdentifier = 'latest') -> int: 62 | return self._get('VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS', block_identifier) 63 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/oracle_report_sanity_checker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from src.utils.cache import global_lru_cache as lru_cache 3 | 4 | from web3.types import BlockIdentifier 5 | 6 | from src.modules.accounting.types import OracleReportLimits 7 | from src.providers.execution.base_interface import ContractInterface 8 | from src.utils.abi import named_tuple_to_dataclass 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class OracleReportSanityCheckerContract(ContractInterface): 15 | abi_path = './assets/OracleReportSanityChecker.json' 16 | 17 | @lru_cache(maxsize=1) 18 | def get_oracle_report_limits(self, block_identifier: BlockIdentifier = 'latest') -> OracleReportLimits: 19 | """ 20 | Returns the limits list for the Lido's oracle report sanity checks 21 | """ 22 | response = self.functions.getOracleReportLimits().call(block_identifier=block_identifier) 23 | response = named_tuple_to_dataclass(response, OracleReportLimits.from_response) 24 | 25 | logger.info({ 26 | 'msg': 'Call `getOracleReportLimits()`.', 27 | 'value': response, 28 | 'block_identifier': repr(block_identifier), 29 | 'to': self.address, 30 | }) 31 | return response 32 | -------------------------------------------------------------------------------- /src/providers/execution/contracts/staking_router.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import abstractmethod 3 | 4 | from src.utils.cache import global_lru_cache as lru_cache 5 | 6 | from web3.types import BlockIdentifier 7 | 8 | from src.providers.execution.base_interface import ContractInterface 9 | from src.utils.dataclass import list_of_dataclasses 10 | from src.web3py.extensions.lido_validators import StakingModule, NodeOperator 11 | from src.variables import EL_REQUESTS_BATCH_SIZE 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class StakingRouterContract(ContractInterface): 17 | abi_path = './assets/StakingRouter.json' 18 | 19 | @lru_cache(maxsize=1) 20 | def get_contract_version(self, block_identifier: BlockIdentifier = 'latest') -> int: 21 | response = self.functions.getContractVersion().call(block_identifier=block_identifier) 22 | logger.debug( 23 | { 24 | 'msg': 'Call `getContractVersion()`.', 25 | 'value': response, 26 | 'block_identifier': repr(block_identifier), 27 | 'to': self.address, 28 | } 29 | ) 30 | return response 31 | 32 | def get_all_node_operator_digests(self, module: StakingModule, block_identifier: BlockIdentifier = 'latest') -> list[NodeOperator]: 33 | """ 34 | Returns node operator digests for each node operator in staking module 35 | """ 36 | response: list = [] 37 | i = 0 38 | 39 | while True: 40 | nos = self.functions.getNodeOperatorDigests( 41 | module.id, 42 | i * EL_REQUESTS_BATCH_SIZE, 43 | EL_REQUESTS_BATCH_SIZE, 44 | ).call(block_identifier=block_identifier) 45 | 46 | logger.info({ 47 | 'msg': f'Call `getNodeOperatorDigests({module.id}, {i * EL_REQUESTS_BATCH_SIZE}, {EL_REQUESTS_BATCH_SIZE})`.', 48 | # Too long response 49 | 'len': len(nos), 50 | 'block_identifier': repr(block_identifier), 51 | 'to': self.address, 52 | }) 53 | 54 | i += 1 55 | response.extend(nos) 56 | 57 | if len(nos) != EL_REQUESTS_BATCH_SIZE: 58 | break 59 | 60 | return [NodeOperator.from_response(no, module) for no in response] 61 | 62 | @lru_cache(maxsize=1) 63 | @list_of_dataclasses(StakingModule.from_response) 64 | def get_staking_modules(self, block_identifier: BlockIdentifier = 'latest') -> list[StakingModule]: 65 | """ 66 | Returns all registered staking modules 67 | """ 68 | response = self.functions.getStakingModules().call(block_identifier=block_identifier) 69 | 70 | logger.info({ 71 | 'msg': 'Call `getStakingModules()`.', 72 | 'value': response, 73 | 'block_identifier': repr(block_identifier), 74 | 'to': self.address, 75 | }) 76 | return response 77 | -------------------------------------------------------------------------------- /src/providers/execution/exceptions.py: -------------------------------------------------------------------------------- 1 | """Module for exceptions caused by the execution provider""" 2 | 3 | 4 | class InconsistentEvents(Exception): 5 | pass 6 | 7 | 8 | class InconsistentData(Exception): 9 | pass 10 | -------------------------------------------------------------------------------- /src/providers/ipfs/__init__.py: -------------------------------------------------------------------------------- 1 | from .cid import * 2 | from .dummy import * 3 | from .gw3 import * 4 | from .multi import * 5 | from .pinata import * 6 | from .public import * 7 | from .types import * 8 | -------------------------------------------------------------------------------- /src/providers/ipfs/cid.py: -------------------------------------------------------------------------------- 1 | from collections import UserString 2 | 3 | 4 | class CID(UserString): 5 | def __repr__(self): 6 | return f"{self.__class__.__name__}({self.data})" 7 | 8 | 9 | class CIDv0(CID): 10 | ... 11 | 12 | 13 | class CIDv1(CID): 14 | ... 15 | 16 | 17 | # @see https://github.com/multiformats/cid/blob/master/README.md#decoding-algorithm 18 | def is_cid_v0(cid: str) -> bool: 19 | return cid.startswith("Qm") and len(cid) == 46 20 | -------------------------------------------------------------------------------- /src/providers/ipfs/dummy.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from .cid import CID, CIDv0 4 | from .types import FetchError, IPFSProvider 5 | 6 | 7 | class DummyIPFSProvider(IPFSProvider): 8 | """Dummy IPFS provider which using the local filesystem as a backend""" 9 | 10 | # pylint: disable=unreachable 11 | 12 | mempool: dict[CID, bytes] 13 | 14 | def __init__(self) -> None: 15 | self.mempool = {} 16 | 17 | def fetch(self, cid: CID) -> bytes: 18 | try: 19 | return self.mempool[cid] 20 | except KeyError: 21 | try: 22 | with open(str(cid), mode="r") as f: 23 | return f.read().encode("utf-8") 24 | except Exception as e: 25 | raise FetchError(cid) from e 26 | 27 | def _upload(self, content: bytes, name: str | None = None) -> str: 28 | cid = "Qm" + hashlib.sha256(content).hexdigest() # XXX: Dummy. 29 | self.mempool[CIDv0(cid)] = content 30 | return cid 31 | 32 | def pin(self, cid: CID) -> None: 33 | content = self.fetch(cid) 34 | 35 | with open(str(cid), mode="w", encoding="utf-8") as f: 36 | f.write(content.decode()) 37 | -------------------------------------------------------------------------------- /src/providers/ipfs/pinata.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from json import JSONDecodeError 3 | 4 | import requests 5 | 6 | from .cid import CID 7 | from .types import FetchError, IPFSProvider, PinError, UploadError 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Pinata(IPFSProvider): 13 | """pinata.cloud IPFS provider""" 14 | 15 | API_ENDPOINT = "https://api.pinata.cloud" 16 | GATEWAY = "https://gateway.pinata.cloud" 17 | 18 | def __init__(self, jwt_token: str, *, timeout: int) -> None: 19 | super().__init__() 20 | self.timeout = timeout 21 | self.session = requests.Session() 22 | self.session.headers["Authorization"] = f"Bearer {jwt_token}" 23 | 24 | def fetch(self, cid: CID) -> bytes: 25 | url = f"{self.GATEWAY}/ipfs/{cid}" 26 | try: 27 | resp = requests.get(url, timeout=self.timeout) 28 | resp.raise_for_status() 29 | except requests.RequestException as ex: 30 | logger.error({"msg": "Request has been failed", "error": str(ex)}) 31 | raise FetchError(cid) from ex 32 | return resp.content 33 | 34 | def publish(self, content: bytes, name: str | None = None) -> CID: 35 | # NOTE: The content is pinned by the `upload` method. 36 | return self.upload(content) 37 | 38 | def _upload(self, content: bytes, name: str | None = None) -> str: 39 | """Pinata has no dedicated endpoint for uploading, so pinFileToIPFS is used""" 40 | 41 | url = f"{self.API_ENDPOINT}/pinning/pinFileToIPFS" 42 | try: 43 | with self.session as s: 44 | resp = s.post(url, files={"file": content}) 45 | resp.raise_for_status() 46 | except requests.RequestException as ex: 47 | logger.error({"msg": "Request has been failed", "error": str(ex)}) 48 | raise UploadError from ex 49 | try: 50 | cid = resp.json()["IpfsHash"] 51 | except JSONDecodeError as ex: 52 | raise UploadError from ex 53 | except KeyError as ex: 54 | raise UploadError from ex 55 | 56 | return cid 57 | 58 | def pin(self, cid: CID) -> None: 59 | """pinByHash is a paid feature""" 60 | raise PinError(cid) 61 | -------------------------------------------------------------------------------- /src/providers/ipfs/public.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import requests 4 | 5 | from .cid import CID 6 | from .types import FetchError, IPFSProvider, PinError, UploadError 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PublicIPFS(IPFSProvider): 12 | """Public IPFS gateway (fetch-only provider)""" 13 | 14 | # pylint:disable=duplicate-code 15 | 16 | GATEWAY = "https://ipfs.io" 17 | 18 | def __init__(self, *, timeout: int) -> None: 19 | super().__init__() 20 | self.timeout = timeout 21 | 22 | def fetch(self, cid: CID) -> bytes: 23 | url = f"{self.GATEWAY}/ipfs/{cid}" 24 | try: 25 | resp = requests.get(url, timeout=self.timeout) 26 | resp.raise_for_status() 27 | except requests.RequestException as ex: 28 | logger.error({"msg": "Request has been failed", "error": str(ex)}) 29 | raise FetchError(cid) from ex 30 | return resp.content 31 | 32 | def _upload(self, content: bytes, name: str | None = None) -> str: 33 | raise UploadError 34 | 35 | def pin(self, cid: CID) -> None: 36 | raise PinError(cid) 37 | -------------------------------------------------------------------------------- /src/providers/ipfs/types.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from multiformats_cid import make_cid, CIDv1 as MultiCIDv1 4 | 5 | from .cid import CID, CIDv0 6 | 7 | 8 | class IPFSError(Exception): 9 | """Base class for IPFS provider errors""" 10 | 11 | 12 | class FetchError(IPFSError): 13 | """Raised if no content found for the given CID""" 14 | 15 | cid: CID 16 | 17 | def __init__(self, cid: CID) -> None: 18 | super().__init__(self) 19 | self.cid = cid 20 | 21 | def __str__(self) -> str: 22 | return f"Unable to fetch {repr(self.cid)}" 23 | 24 | 25 | class UploadError(IPFSError): ... 26 | 27 | 28 | class PinError(IPFSError): 29 | cid: CID 30 | 31 | def __init__(self, cid: CID) -> None: 32 | super().__init__(self) 33 | self.cid = cid 34 | 35 | def __str__(self) -> str: 36 | return f"Unable to pin {repr(self.cid)}" 37 | 38 | 39 | class IPFSProvider(ABC): 40 | """Interface for all implementations of an [IPFS](https://docs.ipfs.tech) provider""" 41 | 42 | @abstractmethod 43 | def fetch(self, cid: CID) -> bytes: ... 44 | 45 | def publish(self, content: bytes, name: str | None = None) -> CID: 46 | cid = self.upload(content, name) 47 | self.pin(cid) 48 | return cid 49 | 50 | @abstractmethod 51 | def _upload(self, content: bytes, name: str | None = None) -> str: ... 52 | 53 | def upload(self, content: bytes, name: str | None = None) -> CIDv0: 54 | cid_str = self._upload(content, name) 55 | 56 | cid = make_cid(cid_str) 57 | 58 | if isinstance(cid, MultiCIDv1): 59 | cid = cid.to_v0() 60 | 61 | return CIDv0(cid) 62 | 63 | @abstractmethod 64 | def pin(self, cid: CID) -> None: 65 | """Pin the content, see https://docs.ipfs.tech/how-to/pin-files""" 66 | -------------------------------------------------------------------------------- /src/providers/keys/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/providers/keys/__init__.py -------------------------------------------------------------------------------- /src/providers/keys/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import cast, Self 3 | 4 | from eth_typing import ChecksumAddress, HexStr 5 | 6 | from src.types import NodeOperatorId 7 | from src.utils.dataclass import FromResponse 8 | 9 | 10 | @dataclass 11 | class LidoKey(FromResponse): 12 | key: HexStr 13 | depositSignature: HexStr 14 | operatorIndex: NodeOperatorId 15 | used: bool 16 | moduleAddress: ChecksumAddress 17 | 18 | @classmethod 19 | def from_response(cls, **kwargs) -> Self: 20 | response_lido_key = super().from_response(**kwargs) 21 | lido_key: Self = cast(Self, response_lido_key) 22 | lido_key.key = HexStr(lido_key.key.lower()) # pylint: disable=no-member 23 | return lido_key 24 | 25 | 26 | @dataclass 27 | class KeysApiStatus(FromResponse): 28 | appVersion: str 29 | chainId: int 30 | -------------------------------------------------------------------------------- /src/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/services/__init__.py -------------------------------------------------------------------------------- /src/services/bunker_cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/services/bunker_cases/__init__.py -------------------------------------------------------------------------------- /src/services/bunker_cases/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class BunkerConfig: 6 | normalized_cl_reward_per_epoch: int 7 | normalized_cl_reward_mistake_rate: float 8 | rebase_check_nearest_epoch_distance: int 9 | rebase_check_distant_epoch_distance: int 10 | -------------------------------------------------------------------------------- /src/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import StrEnum 3 | from typing import NewType 4 | 5 | from eth_typing import BlockNumber, ChecksumAddress, HexStr 6 | from web3.types import Timestamp, Wei 7 | 8 | 9 | class OracleModule(StrEnum): 10 | ACCOUNTING = 'accounting' 11 | EJECTOR = 'ejector' 12 | CHECK = 'check' 13 | CSM = 'csm' 14 | 15 | 16 | EpochNumber = NewType('EpochNumber', int) 17 | FrameNumber = NewType('FrameNumber', int) 18 | StateRoot = NewType('StateRoot', HexStr) 19 | BlockRoot = NewType('BlockRoot', HexStr) 20 | SlotNumber = NewType('SlotNumber', int) 21 | 22 | StakingModuleAddress = NewType('StakingModuleAddress', ChecksumAddress) 23 | StakingModuleId = NewType('StakingModuleId', int) 24 | NodeOperatorId = NewType('NodeOperatorId', int) 25 | NodeOperatorGlobalIndex = tuple[StakingModuleId, NodeOperatorId] 26 | 27 | BlockHash = NewType('BlockHash', HexStr) 28 | 29 | Gwei = NewType('Gwei', int) 30 | 31 | ValidatorIndex = NewType('ValidatorIndex', int) 32 | CommitteeIndex = NewType('CommitteeIndex', int) 33 | 34 | FinalizationBatches = NewType('FinalizationBatches', list[int]) 35 | WithdrawalVaultBalance = NewType('WithdrawalVaultBalance', Wei) 36 | ELVaultBalance = NewType('ELVaultBalance', Wei) 37 | 38 | type OperatorsValidatorCount = dict[NodeOperatorGlobalIndex, int] 39 | 40 | 41 | @dataclass(frozen=True) 42 | class BlockStamp: 43 | state_root: StateRoot 44 | slot_number: SlotNumber 45 | block_hash: BlockHash 46 | block_number: BlockNumber 47 | block_timestamp: Timestamp 48 | 49 | 50 | @dataclass(frozen=True) 51 | class ReferenceBlockStamp(BlockStamp): 52 | # Ref slot could differ from slot_number if ref_slot was missed slot_number will be previous first non-missed slot 53 | ref_slot: SlotNumber 54 | ref_epoch: EpochNumber 55 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/abi.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections.abc import Callable 3 | 4 | 5 | def camel_to_snake(name): 6 | name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 7 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() 8 | 9 | 10 | def named_tuple_to_dataclass[T](response, dataclass_factory: Callable[..., T] | type[T]) -> T: 11 | """ 12 | Converts ABIDecodedNamedTuple to provided dataclass 13 | Example: 14 | Input: ABIDecodedNamedTuple(slotsPerEpoch=32, secondsPerSlot=12, genesisTime=1675263480) 15 | Output: ChainConfig(slots_per_epoch=32, seconds_per_slot=12, genesis_time=1675263480) 16 | """ 17 | return dataclass_factory(**{camel_to_snake(key): value for key, value in response._asdict().items()}) 18 | -------------------------------------------------------------------------------- /src/utils/blockstamp.py: -------------------------------------------------------------------------------- 1 | from src.providers.consensus.types import BlockDetailsResponse 2 | from src.types import BlockStamp, EpochNumber, ReferenceBlockStamp, SlotNumber 3 | 4 | 5 | def build_reference_blockstamp( 6 | slot_details: BlockDetailsResponse, 7 | ref_slot: SlotNumber, 8 | ref_epoch: EpochNumber, 9 | ) -> ReferenceBlockStamp: 10 | return ReferenceBlockStamp( 11 | **_build_blockstamp_data(slot_details), 12 | ref_slot=ref_slot, 13 | ref_epoch=ref_epoch, 14 | ) 15 | 16 | 17 | def build_blockstamp(slot_details: BlockDetailsResponse): 18 | return BlockStamp(**_build_blockstamp_data(slot_details)) 19 | 20 | 21 | def _build_blockstamp_data( 22 | slot_details: BlockDetailsResponse, 23 | ) -> dict: 24 | execution_payload = slot_details.message.body.execution_payload 25 | 26 | return { 27 | "slot_number": slot_details.message.slot, 28 | "state_root": slot_details.message.state_root, 29 | "block_number": execution_payload.block_number, 30 | "block_hash": execution_payload.block_hash, 31 | "block_timestamp": execution_payload.timestamp, 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/build.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | UNKNOWN_BUILD_INFO = {"version": "unknown", "branch": "unknown", "commit": "unknown"} 4 | 5 | 6 | def get_build_info() -> dict: 7 | path = "./build-info.json" 8 | try: 9 | with open(path, "r") as f: 10 | return json.load(f) 11 | except (FileNotFoundError, json.JSONDecodeError): 12 | return UNKNOWN_BUILD_INFO 13 | -------------------------------------------------------------------------------- /src/utils/cache.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from inspect import signature 3 | from weakref import WeakKeyDictionary 4 | 5 | from src.providers.execution.base_interface import ContractInterface 6 | 7 | global_cache: WeakKeyDictionary = WeakKeyDictionary() 8 | 9 | 10 | def global_lru_cache(*args, **kwargs): 11 | def caching_decorator(func): 12 | cached_func = functools.lru_cache(*args, **kwargs)(func) 13 | 14 | def wrapper(*args, **kwargs): 15 | # if lru_cache used for caching ContractInterface method 16 | # Do not cache any requests with relative blocks 17 | # Like 'latest', 'earliest', 'pending', 'safe', 'finalized' or if default ('latest') arg provided 18 | args_list = signature(func).parameters 19 | 20 | if issubclass(args[0].__class__, ContractInterface) and args_list.get('block_identifier'): 21 | block = kwargs.get('block_identifier', None) 22 | if block is None: 23 | if len(args) == len(args_list): 24 | # block_identifier provided via kwargs and args 25 | block = args[-1] 26 | # Move to kwarg 27 | kwargs['block_identifier'] = block 28 | args = args[:-1] 29 | else: 30 | # block_identifier not provided 31 | return func(*args, **kwargs) 32 | 33 | if block in ['latest', 'earliest', 'pending', 'safe', 'finalized']: 34 | # block_identifier one of related markers 35 | return func(*args, **kwargs) 36 | 37 | result = cached_func(*args, **kwargs) 38 | global_cache[func] = cached_func 39 | return result 40 | 41 | def cache_clear(): 42 | cached_func.cache_clear() 43 | if func in global_cache: 44 | del global_cache[func] 45 | 46 | wrapper.cache_clear = cache_clear 47 | wrapper.cache_info = cached_func.cache_info 48 | return wrapper 49 | 50 | return caching_decorator 51 | 52 | 53 | def clear_global_cache(): 54 | for cached_func in global_cache.values(): 55 | cached_func.cache_clear() 56 | global_cache.clear() 57 | -------------------------------------------------------------------------------- /src/utils/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def from_file_or_env(env_name: str): 5 | """Return value read from `${env_name}_FILE` or `${env_name}` value directly""" 6 | 7 | filepath_env = f"{env_name}_FILE" 8 | if filepath := os.getenv(filepath_env): 9 | if not os.path.exists(filepath): 10 | raise ValueError(f'File {filepath} does not exist. Fix {filepath_env} variable or remove it.') 11 | 12 | with open(filepath) as f: 13 | return f.read().rstrip() 14 | 15 | return os.getenv(env_name) 16 | -------------------------------------------------------------------------------- /src/utils/exception.py: -------------------------------------------------------------------------------- 1 | class IncompatibleException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /src/utils/input.py: -------------------------------------------------------------------------------- 1 | def get_input(): 2 | return input() 3 | 4 | 5 | def prompt(prompt_message: str) -> bool: 6 | print(prompt_message, end='') 7 | while True: 8 | choice = get_input().lower() 9 | 10 | if choice in ['Y', 'y']: 11 | return True 12 | 13 | if choice in ['N', 'n']: 14 | return False 15 | 16 | print('Please respond with [y or n]: ', end='') 17 | -------------------------------------------------------------------------------- /src/utils/range.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, TypeVar, cast 2 | 3 | T = TypeVar("T", bound=int) 4 | 5 | 6 | def sequence(start: T, stop: T) -> Iterable[T]: 7 | """Returns inclusive range object [start;stop]""" 8 | if start > stop: 9 | raise ValueError(f"{start=} > {stop=}") 10 | return cast(Iterable, range(start, stop + 1)) 11 | -------------------------------------------------------------------------------- /src/utils/timeit.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import wraps 3 | from types import SimpleNamespace 4 | from typing import Callable 5 | 6 | 7 | type Arguments = SimpleNamespace 8 | type Duration = float 9 | 10 | 11 | def timeit(log_fn: Callable[[Arguments, Duration], None]): 12 | def decorator[T](func: Callable[..., T]): 13 | @wraps(func) 14 | def wrapped(*args, **kwargs) -> T: 15 | start_time = time.time() 16 | result = func(*args, **kwargs) 17 | execution_time = time.time() - start_time 18 | arguments = SimpleNamespace(**dict(zip(func.__code__.co_varnames, args)), **kwargs) 19 | log_fn(arguments, execution_time) 20 | return result 21 | 22 | return wrapped 23 | 24 | return decorator 25 | -------------------------------------------------------------------------------- /src/utils/types.py: -------------------------------------------------------------------------------- 1 | from eth_typing import HexStr 2 | 3 | 4 | def bytes_to_hex_str(b: bytes) -> HexStr: 5 | return HexStr('0x' + b.hex()) 6 | 7 | 8 | def hex_str_to_bytes(hex_str: str) -> bytes: 9 | return bytes.fromhex(hex_str[2:]) if hex_str.startswith("0x") else bytes.fromhex(hex_str) 10 | 11 | 12 | def is_4bytes_hex(s: str) -> bool: 13 | if not s.startswith("0x"): 14 | return False 15 | 16 | try: 17 | return len(bytes.fromhex(s[2:])) == 4 18 | except ValueError: 19 | return False 20 | -------------------------------------------------------------------------------- /src/utils/units.py: -------------------------------------------------------------------------------- 1 | """A set of utils to convert ether units""" 2 | 3 | from web3.types import Wei 4 | 5 | from src.constants import GWEI_TO_WEI 6 | from src.types import Gwei 7 | 8 | 9 | def wei_to_gwei(amount: Wei) -> Gwei: 10 | """Converts Wei to Gwei rounding down""" 11 | return Gwei(amount // GWEI_TO_WEI) 12 | 13 | 14 | def gwei_to_wei(amount: Gwei) -> Wei: 15 | """Converts Gwei to Wei""" 16 | return Wei(amount * GWEI_TO_WEI) 17 | -------------------------------------------------------------------------------- /src/utils/web3converter.py: -------------------------------------------------------------------------------- 1 | from src.types import SlotNumber, EpochNumber, FrameNumber 2 | from src.modules.submodules.types import ChainConfig, FrameConfig 3 | 4 | 5 | def epoch_from_slot(slot: SlotNumber, slots_per_epoch: int) -> EpochNumber: 6 | """ 7 | https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_epoch_at_slot 8 | """ 9 | return EpochNumber(slot // slots_per_epoch) 10 | 11 | 12 | class Web3Converter: 13 | """ 14 | The Web3Converter class contains methods for converting between slot, epoch, and frame numbers using chain and 15 | frame settings passed as arguments when the class instance is created. 16 | 17 | Frame is the distance between two oracle reports. 18 | """ 19 | 20 | chain_config: ChainConfig 21 | frame_config: FrameConfig 22 | 23 | def __init__(self, chain_config: ChainConfig, frame_config: FrameConfig): 24 | self.chain_config = chain_config 25 | self.frame_config = frame_config 26 | 27 | @property 28 | def slots_per_frame(self) -> int: 29 | return self.frame_config.epochs_per_frame * self.chain_config.slots_per_epoch 30 | 31 | def get_epoch_first_slot(self, epoch: EpochNumber) -> SlotNumber: 32 | return SlotNumber(epoch * self.chain_config.slots_per_epoch) 33 | 34 | def get_epoch_last_slot(self, epoch: EpochNumber) -> SlotNumber: 35 | return SlotNumber((epoch + 1) * self.chain_config.slots_per_epoch - 1) 36 | 37 | def get_frame_last_slot(self, frame: FrameNumber) -> SlotNumber: 38 | return SlotNumber(self.get_frame_first_slot(FrameNumber(frame + 1)) - 1) 39 | 40 | def get_frame_first_slot(self, frame: FrameNumber) -> SlotNumber: 41 | return SlotNumber( 42 | (self.frame_config.initial_epoch + frame * self.frame_config.epochs_per_frame) * self.chain_config.slots_per_epoch 43 | ) 44 | 45 | def get_epoch_by_slot(self, ref_slot: SlotNumber) -> EpochNumber: 46 | return EpochNumber(ref_slot // self.chain_config.slots_per_epoch) 47 | 48 | def get_epoch_by_timestamp(self, timestamp: int) -> EpochNumber: 49 | return EpochNumber(self.get_slot_by_timestamp(timestamp) // self.chain_config.slots_per_epoch) 50 | 51 | def get_slot_by_timestamp(self, timestamp: int) -> SlotNumber: 52 | return SlotNumber((timestamp - self.chain_config.genesis_time) // self.chain_config.seconds_per_slot) 53 | 54 | def get_frame_by_slot(self, slot: SlotNumber) -> FrameNumber: 55 | return self.get_frame_by_epoch(self.get_epoch_by_slot(slot)) 56 | 57 | def get_frame_by_epoch(self, epoch: EpochNumber) -> FrameNumber: 58 | return FrameNumber((epoch - self.frame_config.initial_epoch) // self.frame_config.epochs_per_frame) 59 | -------------------------------------------------------------------------------- /src/web3py/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/src/web3py/__init__.py -------------------------------------------------------------------------------- /src/web3py/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from src.web3py.extensions.keys_api import KeysAPIClientModule 2 | from src.web3py.extensions.tx_utils import TransactionUtils 3 | from src.web3py.extensions.consensus import ConsensusClientModule 4 | from src.web3py.extensions.contracts import LidoContracts 5 | from src.web3py.extensions.lido_validators import LidoValidatorsProvider 6 | from src.web3py.extensions.fallback import FallbackProviderModule 7 | from src.web3py.extensions.csm import CSM, LazyCSM 8 | -------------------------------------------------------------------------------- /src/web3py/extensions/consensus.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | from web3.module import Module 3 | 4 | from src.providers.consensus.client import ConsensusClient 5 | from src.variables import ( 6 | HTTP_REQUEST_TIMEOUT_CONSENSUS, 7 | HTTP_REQUEST_RETRY_COUNT_CONSENSUS, 8 | HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS_CONSENSUS, 9 | ) 10 | 11 | 12 | class ConsensusClientModule(ConsensusClient, Module): 13 | def __init__(self, hosts: list[str], w3: Web3): 14 | self.w3 = w3 15 | 16 | super(ConsensusClient, self).__init__( 17 | hosts, 18 | HTTP_REQUEST_TIMEOUT_CONSENSUS, 19 | HTTP_REQUEST_RETRY_COUNT_CONSENSUS, 20 | HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS_CONSENSUS, 21 | ) 22 | super(Module, self).__init__() 23 | -------------------------------------------------------------------------------- /src/web3py/extensions/fallback.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from web3_multi_provider import FallbackProvider 4 | from src.providers.consistency import ProviderConsistencyModule 5 | from web3 import Web3 6 | 7 | 8 | class FallbackProviderModule(ProviderConsistencyModule, FallbackProvider): 9 | def get_all_providers(self) -> list[Any]: 10 | return self._providers # type: ignore[attr-defined] 11 | 12 | def _get_chain_id_with_provider(self, provider_index: int) -> int: 13 | return Web3.to_int(hexstr=self._providers[provider_index].make_request("eth_chainId", []).get('result')) # type: ignore[attr-defined] 14 | -------------------------------------------------------------------------------- /src/web3py/extensions/keys_api.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | from web3.module import Module 3 | 4 | from src.providers.keys.client import KeysAPIClient 5 | from src.variables import ( 6 | HTTP_REQUEST_TIMEOUT_KEYS_API, 7 | HTTP_REQUEST_RETRY_COUNT_KEYS_API, 8 | HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS_KEYS_API 9 | ) 10 | 11 | 12 | class KeysAPIClientModule(KeysAPIClient, Module): 13 | def __init__(self, hosts: list[str], w3: Web3): 14 | self.w3 = w3 15 | 16 | super(KeysAPIClient, self).__init__( 17 | hosts, 18 | HTTP_REQUEST_TIMEOUT_KEYS_API, 19 | HTTP_REQUEST_RETRY_COUNT_KEYS_API, 20 | HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS_KEYS_API 21 | ) 22 | super(Module, self).__init__() 23 | -------------------------------------------------------------------------------- /src/web3py/types.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 as _Web3 2 | 3 | from src.providers.ipfs import IPFSProvider 4 | from src.web3py.extensions import ( 5 | CSM, 6 | ConsensusClientModule, 7 | KeysAPIClientModule, 8 | LidoContracts, 9 | LidoValidatorsProvider, 10 | TransactionUtils, 11 | ) 12 | 13 | 14 | class Web3(_Web3): 15 | lido_contracts: LidoContracts 16 | lido_validators: LidoValidatorsProvider 17 | transaction: TransactionUtils 18 | cc: ConsensusClientModule 19 | kac: KeysAPIClientModule 20 | csm: CSM 21 | ipfs: IPFSProvider 22 | -------------------------------------------------------------------------------- /stubs/lazy_object_proxy/__init__.pyi: -------------------------------------------------------------------------------- 1 | from .lazy_object_proxy import Proxy as Proxy 2 | -------------------------------------------------------------------------------- /stubs/lazy_object_proxy/lazy_object_proxy.pyi: -------------------------------------------------------------------------------- 1 | from typing import Generic, Type, TypeVar 2 | 3 | 4 | T = TypeVar("T") 5 | 6 | 7 | class Proxy(Generic[T]): 8 | def __init__(self, factory: Type[T]) -> None: ... 9 | -------------------------------------------------------------------------------- /stubs/timeout_decorator/__init__.pyi: -------------------------------------------------------------------------------- 1 | from .timeout_decorator import TimeoutError as TimeoutError 2 | from .timeout_decorator import timeout as timeout 3 | -------------------------------------------------------------------------------- /stubs/timeout_decorator/timeout_decorator.pyi: -------------------------------------------------------------------------------- 1 | from _typeshed import Incomplete 2 | 3 | class TimeoutError(AssertionError): 4 | value: Incomplete 5 | def __init__(self, value: str = ...) -> None: ... 6 | 7 | def timeout( 8 | seconds: int | None = ..., 9 | use_signals: bool = True, 10 | timeout_exception: type[TimeoutError] = TimeoutError, 11 | exception_message: str | None = ..., 12 | ): ... 13 | -------------------------------------------------------------------------------- /stubs/web3_multi_provider/__init__.pyi: -------------------------------------------------------------------------------- 1 | from .multi_http_provider import MultiHTTPProvider as MultiHTTPProvider 2 | from .multi_http_provider import FallbackProvider as FallbackProvider 3 | from .multi_http_provider import MultiProvider as MultiProvider 4 | from .multi_http_provider import NoActiveProviderError as NoActiveProviderError 5 | from .multi_http_provider import ProtocolNotSupported as ProtocolNotSupported 6 | -------------------------------------------------------------------------------- /stubs/web3_multi_provider/multi_http_provider.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional, Union 2 | 3 | from _typeshed import Incomplete 4 | from eth_typing import URI as URI 5 | from web3.providers import JSONBaseProvider 6 | from web3.types import RPCEndpoint as RPCEndpoint 7 | from web3.types import RPCResponse as RPCResponse 8 | 9 | logger: Incomplete 10 | 11 | class NoActiveProviderError(Exception): ... 12 | class ProtocolNotSupported(Exception): ... 13 | 14 | class MultiProvider(JSONBaseProvider): 15 | endpoint_uri: str 16 | def __init__( 17 | self, 18 | endpoint_urls: List[Union[URI, str]], 19 | request_kwargs: Optional[Any] = ..., 20 | session: Optional[Any] = ..., 21 | websocket_kwargs: Optional[Any] = ..., 22 | websocket_timeout: Optional[Any] = ..., 23 | ) -> None: ... 24 | def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: ... 25 | 26 | class MultiHTTPProvider(MultiProvider): 27 | def __init__( 28 | self, 29 | endpoint_urls: List[Union[URI, str]], 30 | request_kwargs: Optional[Any] = ..., 31 | session: Optional[Any] = ..., 32 | ) -> None: ... 33 | 34 | class FallbackProvider(MultiProvider): 35 | def __init__( 36 | self, 37 | endpoint_urls: List[Union[URI, str]], 38 | request_kwargs: Optional[Any] = ..., 39 | session: Optional[Any] = ..., 40 | ) -> None: ... 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if os.path.exists(".env"): 4 | with open(".env", "r") as f: 5 | for line in f.readlines(): 6 | line = line.strip() 7 | if line.startswith("#") or not line: 8 | continue 9 | key, value = line.split("=") 10 | os.environ[key] = value 11 | -------------------------------------------------------------------------------- /tests/e2e/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from logging.handlers import QueueHandler 4 | import pytest 5 | 6 | from pytest import Item 7 | from src.main import main 8 | from multiprocessing import Process, Queue 9 | from src.variables import EXECUTION_CLIENT_URI 10 | 11 | 12 | @pytest.hookimpl(hookwrapper=True) 13 | def pytest_collection_modifyitems(items: list[Item]): 14 | yield 15 | if any(not item.get_closest_marker("e2e") for item in items): 16 | for item in items: 17 | if item.get_closest_marker("e2e"): 18 | item.add_marker( 19 | pytest.mark.skip( 20 | reason="e2e tests are take a lot of time " "and skipped if any other tests are selected" 21 | ) 22 | ) 23 | 24 | 25 | def worker_process(queue, module_name, execution_client_uri): 26 | import src.variables 27 | 28 | src.variables.EXECUTION_CLIENT_URI = [execution_client_uri] 29 | qh = QueueHandler(queue) 30 | root = logging.getLogger() 31 | root.addHandler(qh) 32 | main(module_name) 33 | 34 | 35 | @pytest.fixture(scope="session", params=EXECUTION_CLIENT_URI) 36 | def execution_client_uri(request): 37 | return request.param 38 | 39 | 40 | @pytest.fixture 41 | def start_accounting(caplog, execution_client_uri): 42 | queue = Queue() 43 | listener = logging.handlers.QueueListener(queue, caplog.handler) 44 | listener.start() 45 | 46 | worker = Process(target=worker_process, args=(queue, "accounting", execution_client_uri)) 47 | worker.start() 48 | yield 49 | worker.terminate() 50 | 51 | 52 | def wait_for_message_appeared(caplog, message, timeout=600): 53 | start_time = time.time() 54 | while True: 55 | if message in caplog.messages: 56 | return 57 | if time.time() - start_time > timeout: 58 | break 59 | time.sleep(1) 60 | raise AssertionError(f"Message {message} not found in logs") 61 | -------------------------------------------------------------------------------- /tests/e2e/test_accounting.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.e2e.conftest import wait_for_message_appeared 4 | 5 | 6 | @pytest.mark.e2e 7 | def test_app(start_accounting, caplog): 8 | wait_for_message_appeared(caplog, "{'msg': 'Run module as daemon.'}", timeout=10) 9 | wait_for_message_appeared(caplog, "{'msg': 'Check if main data was submitted.', 'value': False}") 10 | wait_for_message_appeared(caplog, "{'msg': 'Check if contract could accept report.', 'value': True}") 11 | wait_for_message_appeared(caplog, "{'msg': 'Execute module.'}") 12 | wait_for_message_appeared(caplog, "{'msg': 'Checking bunker mode'}", timeout=1800) 13 | wait_for_message_appeared(caplog, "{'msg': 'Send report hash. Consensus version: [1]'}") 14 | -------------------------------------------------------------------------------- /tests/execution/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/execution/__init__.py -------------------------------------------------------------------------------- /tests/factory/base_oracle.py: -------------------------------------------------------------------------------- 1 | from src.modules.accounting.types import AccountingProcessingState 2 | from src.modules.ejector.types import EjectorProcessingState 3 | from tests.factory.web3_factory import Web3Factory 4 | 5 | 6 | class AccountingProcessingStateFactory(Web3Factory): 7 | __model__ = AccountingProcessingState 8 | 9 | 10 | class EjectorProcessingStateFactory(Web3Factory): 11 | __model__ = EjectorProcessingState 12 | -------------------------------------------------------------------------------- /tests/factory/bitarrays.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | from pydantic import BaseModel 4 | from pydantic_factories import ModelFactory 5 | 6 | 7 | class BitList(BaseModel): 8 | __root__: bytes 9 | 10 | def hex(self) -> str: 11 | return f"0x{self.__root__.hex()}" 12 | 13 | 14 | class BitListFactory(ModelFactory): 15 | __model__ = BitList 16 | 17 | @classmethod 18 | def build( 19 | cls, 20 | factory_use_construct: bool = False, 21 | set_indices: list[int] = [], 22 | bits_count: int = 0, 23 | **kwargs, 24 | ) -> BitList: 25 | bit_list: list[bool] = [] 26 | for n in sorted(set_indices): 27 | while len(bit_list) < n: 28 | bit_list += [False] 29 | bit_list += [True] 30 | 31 | model = cls._get_model() 32 | return model( 33 | __root__=get_serialized_bytearray( 34 | bit_list, 35 | bits_count=bits_count or len(bit_list), 36 | extra_byte=True, 37 | ) 38 | ) 39 | 40 | 41 | def get_serialized_bytearray(value: Sequence[bool], bits_count: int, extra_byte: bool) -> bytearray: 42 | """ 43 | Serialize a sequence either into a Bitlist or a Bitvector 44 | @see https://github.com/ethereum/py-ssz/blob/main/ssz/utils.py#L223 45 | """ 46 | 47 | if extra_byte: 48 | # Serialize Bitlist 49 | as_bytearray = bytearray(bits_count // 8 + 1) 50 | else: 51 | # Serialize Bitvector 52 | as_bytearray = bytearray((bits_count + 7) // 8) 53 | 54 | for i in range(bits_count): 55 | as_bytearray[i // 8] |= value[i] << (i % 8) 56 | 57 | if extra_byte: 58 | as_bytearray[bits_count // 8] |= 1 << (bits_count % 8) 59 | 60 | return as_bytearray 61 | -------------------------------------------------------------------------------- /tests/factory/blockstamp.py: -------------------------------------------------------------------------------- 1 | from eth_typing import BlockNumber, HexStr 2 | from web3.types import Timestamp 3 | 4 | from tests.factory.web3_factory import Web3Factory 5 | from src.types import BlockStamp, StateRoot, SlotNumber, BlockHash, ReferenceBlockStamp, EpochNumber 6 | 7 | 8 | class BlockStampFactory(Web3Factory): 9 | __model__ = BlockStamp 10 | 11 | state_root: StateRoot = StateRoot(HexStr('0xc4298fa1a4df250710d3e13d16fae7e4cc3ad52809745d86e1f1772abe04702b')) 12 | slot_number: SlotNumber = SlotNumber(294271) 13 | block_hash: BlockHash = BlockHash(HexStr('0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b')) 14 | block_number: BlockNumber = BlockNumber(281479) 15 | block_timestamp: Timestamp = Timestamp(1678794852) 16 | 17 | 18 | class ReferenceBlockStampFactory(Web3Factory): 19 | __model__ = ReferenceBlockStamp 20 | 21 | state_root: StateRoot = StateRoot(HexStr('0xc4298fa1a4df250710d3e13d16fae7e4cc3ad52809745d86e1f1772abe04702b')) 22 | slot_number: SlotNumber = SlotNumber(294271) 23 | block_hash: BlockHash = BlockHash(HexStr('0x0d339fdfa3018561311a39bf00568ed08048055082448d17091d5a4dc2fa035b')) 24 | block_number: BlockNumber = BlockNumber(281479) 25 | block_timestamp: Timestamp = Timestamp(1678794852) 26 | 27 | ref_slot: SlotNumber = SlotNumber(294271) 28 | ref_epoch: EpochNumber = EpochNumber(9195) 29 | -------------------------------------------------------------------------------- /tests/factory/configs.py: -------------------------------------------------------------------------------- 1 | from src.modules.accounting.types import OracleReportLimits 2 | from src.modules.submodules.types import ChainConfig, FrameConfig 3 | from src.providers.consensus.types import ( 4 | BeaconSpecResponse, 5 | SlotAttestationCommittee, 6 | BlockAttestationResponse, 7 | AttestationData, 8 | Checkpoint, 9 | ) 10 | from src.services.bunker_cases.types import BunkerConfig 11 | from tests.factory.web3_factory import Web3Factory 12 | 13 | 14 | class ChainConfigFactory(Web3Factory): 15 | __model__ = ChainConfig 16 | 17 | slots_per_epoch = 32 18 | seconds_per_slot = 12 19 | genesis_time = 0 20 | 21 | 22 | class FrameConfigFactory(Web3Factory): 23 | __model__ = FrameConfig 24 | 25 | initial_epoch = 0 26 | epochs_per_frame = 10 27 | 28 | 29 | class OracleReportLimitsFactory(Web3Factory): 30 | __model__ = OracleReportLimits 31 | 32 | churn_validators_per_day_limit = 0 33 | appeared_validators_per_day_limit = 0 34 | annual_balance_increase_bp_limit = 0 35 | simulated_share_rate_deviation_bp_limit = 0 36 | max_validator_exit_requests_per_report = 0 37 | max_items_per_extra_data_transaction = 0 38 | max_node_operators_per_extra_data_item = 0 39 | request_timestamp_margin = 0 40 | max_positive_token_rebase = 0 41 | 42 | 43 | class BunkerConfigFactory(Web3Factory): 44 | __model__ = BunkerConfig 45 | 46 | 47 | class BeaconSpecResponseFactory(Web3Factory): 48 | __model__ = BeaconSpecResponse 49 | 50 | SECONDS_PER_SLOT = 12 51 | SLOTS_PER_EPOCH = 32 52 | SLOTS_PER_HISTORICAL_ROOT = 8192 53 | 54 | 55 | class SlotAttestationCommitteeFactory(Web3Factory): 56 | __model__ = SlotAttestationCommittee 57 | 58 | slot = 0 59 | index = 0 60 | validators = [] 61 | 62 | 63 | class BlockAttestationFactory(Web3Factory): 64 | __model__ = BlockAttestationResponse 65 | 66 | aggregation_bits = "0x" 67 | committee_bits = None 68 | data = AttestationData( 69 | slot=0, 70 | index=0, 71 | beacon_block_root="0x", 72 | source=Checkpoint("0", "0x"), 73 | target=Checkpoint("0", "0x"), 74 | ) 75 | -------------------------------------------------------------------------------- /tests/factory/consensus.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Iterable 2 | 3 | from src.providers.consensus.types import BeaconStateView, Validator 4 | from tests.factory.web3_factory import Web3Factory 5 | 6 | 7 | class BeaconStateViewFactory(Web3Factory): 8 | __model__ = BeaconStateView 9 | 10 | @classmethod 11 | def build_with_validators(cls, validators: Iterable[Validator], **kwargs: Any): 12 | return cls.build( 13 | validators=[v.validator for v in validators], 14 | balances=[v.balance for v in validators], 15 | **kwargs, 16 | ) 17 | -------------------------------------------------------------------------------- /tests/factory/contract_responses.py: -------------------------------------------------------------------------------- 1 | from src.modules.accounting.types import LidoReportRebase 2 | from tests.factory.web3_factory import Web3Factory 3 | 4 | 5 | class LidoReportRebaseFactory(Web3Factory): 6 | __model__ = LidoReportRebase 7 | -------------------------------------------------------------------------------- /tests/factory/member_info.py: -------------------------------------------------------------------------------- 1 | from src.modules.submodules.types import MemberInfo 2 | from tests.factory.web3_factory import Web3Factory 3 | 4 | 5 | class MemberInfoFactory(Web3Factory): 6 | __model__ = MemberInfo 7 | -------------------------------------------------------------------------------- /tests/fork/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/fork/__init__.py -------------------------------------------------------------------------------- /tests/fork/test_lido_oracle_cycle.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.accounting.accounting import Accounting 4 | from src.modules.ejector.ejector import Ejector 5 | from src.modules.submodules.types import FrameConfig 6 | from src.utils.range import sequence 7 | from tests.fork.conftest import first_slot_of_epoch 8 | 9 | 10 | @pytest.fixture() 11 | def hash_consensus_bin(): 12 | with open('tests/fork/contracts/lido/HashConsensus_bin', 'r') as f: 13 | yield f.read() 14 | 15 | 16 | @pytest.fixture 17 | def accounting_module(web3): 18 | yield Accounting(web3) 19 | 20 | 21 | @pytest.fixture 22 | def ejector_module(web3): 23 | yield Ejector(web3) 24 | 25 | 26 | @pytest.fixture 27 | def start_before_initial_epoch(frame_config: FrameConfig): 28 | _from = frame_config.initial_epoch - 1 29 | _to = frame_config.initial_epoch + 2 30 | return [first_slot_of_epoch(i) for i in sequence(_from, _to)] 31 | 32 | 33 | @pytest.fixture 34 | def start_after_initial_epoch(frame_config: FrameConfig): 35 | _from = frame_config.initial_epoch + 1 36 | _to = frame_config.initial_epoch + 2 37 | return [first_slot_of_epoch(i) for i in sequence(_from, _to)] 38 | 39 | 40 | @pytest.fixture 41 | def missed_initial_frame(frame_config: FrameConfig): 42 | _from = frame_config.initial_epoch + frame_config.epochs_per_frame + 1 43 | _to = _from + 2 44 | return [first_slot_of_epoch(i) for i in sequence(_from, _to)] 45 | 46 | 47 | @pytest.mark.fork 48 | @pytest.mark.parametrize( 49 | 'module', 50 | [accounting_module, ejector_module], 51 | indirect=True, 52 | ) 53 | @pytest.mark.parametrize( 54 | 'running_finalized_slots', 55 | [start_before_initial_epoch, start_after_initial_epoch, missed_initial_frame], 56 | indirect=True, 57 | ) 58 | def test_lido_module_report(module, set_oracle_members, running_finalized_slots, account_from): 59 | assert module.report_contract.get_last_processing_ref_slot() == 0, "Last processing ref slot should be 0" 60 | members = set_oracle_members(count=2) 61 | 62 | report_frame = None 63 | 64 | switch_finalized, _ = running_finalized_slots 65 | while switch_finalized(): 66 | for _, private_key in members: 67 | with account_from(private_key): 68 | module.cycle_handler() 69 | report_frame = module.get_initial_or_current_frame( 70 | module._receive_last_finalized_slot() # pylint: disable=protected-access 71 | ) 72 | 73 | last_processing_after_report = module.report_contract.get_last_processing_ref_slot() 74 | assert ( 75 | last_processing_after_report == report_frame.ref_slot 76 | ), "Last processing ref slot should equal to report ref slot" 77 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/contracts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/integration/contracts/__init__.py -------------------------------------------------------------------------------- /tests/integration/contracts/contract_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from typing import Any, Callable 4 | 5 | from src.providers.execution.base_interface import ContractInterface 6 | 7 | 8 | HASH_REGREX = re.compile(r'^0x[0-9,A-F]{64}$', flags=re.IGNORECASE) 9 | ADDRESS_REGREX = re.compile('^0x[0-9,A-F]{40}$', flags=re.IGNORECASE) 10 | 11 | 12 | def check_contract( 13 | contract: ContractInterface, 14 | functions_spec: list[tuple[str, tuple | None, Callable[[Any], None]]], 15 | caplog, 16 | ): 17 | caplog.set_level(logging.DEBUG) 18 | 19 | for function in functions_spec: 20 | # get method 21 | method = contract.__getattribute__(function[0]) # pylint: disable=unnecessary-dunder-call 22 | # call method with args 23 | response = method(*function[1]) if function[1] is not None else method() 24 | # check response 25 | function[2](response) 26 | 27 | log_with_call = list(filter(lambda log: 'Call ' in log or 'Build ' in log, caplog.messages)) 28 | 29 | assert len(functions_spec) == len(log_with_call) 30 | 31 | 32 | def check_value_re(regrex, value) -> None: 33 | assert regrex.findall(value) 34 | 35 | 36 | def check_value_type(value, _type) -> None: 37 | assert isinstance(value, _type) 38 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_accounting_oracle.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from web3.contract.contract import ContractFunction 3 | 4 | from src.modules.accounting.types import AccountingProcessingState 5 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 6 | 7 | 8 | @pytest.mark.integration 9 | def test_accounting_oracle_contract(accounting_oracle_contract, caplog): 10 | check_contract( 11 | accounting_oracle_contract, 12 | [ 13 | ('get_processing_state', None, lambda response: check_value_type(response, AccountingProcessingState)), 14 | ('submit_report_extra_data_empty', None, lambda tx: check_value_type(tx, ContractFunction)), 15 | ('submit_report_extra_data_list', (b'',), lambda tx: check_value_type(tx, ContractFunction)), 16 | ], 17 | caplog, 18 | ) 19 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_bunker.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.accounting.types import SharesRequestedToBurn 4 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 5 | 6 | 7 | @pytest.mark.integration 8 | def test_burner(burner_contract, caplog): 9 | check_contract( 10 | burner_contract, 11 | [ 12 | ('get_shares_requested_to_burn', None, lambda response: check_value_type(response, SharesRequestedToBurn)), 13 | ], 14 | caplog, 15 | ) 16 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_lido.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.accounting.types import LidoReportRebase, BeaconStat 4 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 5 | 6 | 7 | @pytest.mark.integration 8 | def test_lido_contract_call(lido_contract, accounting_oracle_contract, burner_contract, caplog): 9 | check_contract( 10 | lido_contract, 11 | [ 12 | ('get_buffered_ether', None, lambda response: check_value_type(response, int)), 13 | ('total_supply', None, lambda response: check_value_type(response, int)), 14 | ('get_beacon_stat', None, lambda response: check_value_type(response, BeaconStat)), 15 | ( 16 | 'handle_oracle_report', 17 | ( 18 | 1721995211, # timestamp 19 | 86400, 20 | 368840, 21 | 9820580681659522000000000, 22 | 1397139100547000000000, 23 | 119464421677104745350, 24 | 0, 25 | accounting_oracle_contract.address, 26 | 20390705, 27 | # Call depends on contract state 28 | '0xfdc77ad0ea1ed99b1358beaca0d9c6fa831443f7f4c183302d9e2f76e4c9d0cb', 29 | ), 30 | lambda response: check_value_type(response, LidoReportRebase), 31 | ), 32 | ], 33 | caplog, 34 | ) 35 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_lido_locator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from eth_typing import ChecksumAddress 3 | 4 | from tests.integration.contracts.contract_utils import check_contract, check_value_type, check_value_re, ADDRESS_REGREX 5 | 6 | 7 | @pytest.mark.integration 8 | def test_lido_locator_contract(lido_locator_contract, caplog): 9 | check_contract( 10 | lido_locator_contract, 11 | [ 12 | ( 13 | 'lido', 14 | None, 15 | lambda response: check_value_re(ADDRESS_REGREX, response) 16 | and check_value_type(response, ChecksumAddress), 17 | ), 18 | ( 19 | 'accounting_oracle', 20 | None, 21 | lambda response: check_value_re(ADDRESS_REGREX, response) 22 | and check_value_type(response, ChecksumAddress), 23 | ), 24 | ( 25 | 'staking_router', 26 | None, 27 | lambda response: check_value_re(ADDRESS_REGREX, response) 28 | and check_value_type(response, ChecksumAddress), 29 | ), 30 | ( 31 | 'validator_exit_bus_oracle', 32 | None, 33 | lambda response: check_value_re(ADDRESS_REGREX, response) 34 | and check_value_type(response, ChecksumAddress), 35 | ), 36 | ( 37 | 'withdrawal_queue', 38 | None, 39 | lambda response: check_value_re(ADDRESS_REGREX, response) 40 | and check_value_type(response, ChecksumAddress), 41 | ), 42 | ( 43 | 'oracle_report_sanity_checker', 44 | None, 45 | lambda response: check_value_re(ADDRESS_REGREX, response) 46 | and check_value_type(response, ChecksumAddress), 47 | ), 48 | ( 49 | 'oracle_daemon_config', 50 | None, 51 | lambda response: check_value_re(ADDRESS_REGREX, response) 52 | and check_value_type(response, ChecksumAddress), 53 | ), 54 | ( 55 | 'burner', 56 | None, 57 | lambda response: check_value_re(ADDRESS_REGREX, response) 58 | and check_value_type(response, ChecksumAddress), 59 | ), 60 | ( 61 | 'withdrawal_vault', 62 | None, 63 | lambda response: check_value_re(ADDRESS_REGREX, response) 64 | and check_value_type(response, ChecksumAddress), 65 | ), 66 | ( 67 | 'el_rewards_vault', 68 | None, 69 | lambda response: check_value_re(ADDRESS_REGREX, response) 70 | and check_value_type(response, ChecksumAddress), 71 | ), 72 | ], 73 | caplog, 74 | ) 75 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_oracle_daemon_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.constants import TOTAL_BASIS_POINTS 4 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 5 | 6 | 7 | @pytest.mark.integration 8 | def test_oracle_daemon_config_contract(oracle_daemon_config_contract, caplog): 9 | check_contract( 10 | oracle_daemon_config_contract, 11 | [ 12 | ('normalized_cl_reward_per_epoch', None, lambda response: check_value_type(response, int)), 13 | ( 14 | 'normalized_cl_reward_mistake_rate_bp', 15 | None, 16 | lambda response: check_value_type(response, int) and response < TOTAL_BASIS_POINTS, 17 | ), 18 | ( 19 | 'rebase_check_nearest_epoch_distance', 20 | None, 21 | lambda response: check_value_type(response, int), 22 | ), 23 | ( 24 | 'rebase_check_distant_epoch_distance', 25 | None, 26 | lambda response: check_value_type(response, int), 27 | ), 28 | ( 29 | 'node_operator_network_penetration_threshold_bp', 30 | None, 31 | lambda response: check_value_type(response, int) and response < TOTAL_BASIS_POINTS, 32 | ), 33 | ( 34 | 'prediction_duration_in_slots', 35 | None, 36 | lambda response: check_value_type(response, int), 37 | ), 38 | ( 39 | 'finalization_max_negative_rebase_epoch_shift', 40 | None, 41 | lambda response: check_value_type(response, int), 42 | ), 43 | ( 44 | 'validator_delayed_timeout_in_slots', 45 | None, 46 | lambda response: check_value_type(response, int), 47 | ), 48 | ( 49 | 'validator_delinquent_timeout_in_slots', 50 | None, 51 | lambda response: check_value_type(response, int), 52 | ), 53 | ], 54 | caplog, 55 | ) 56 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_oracle_report_sanity_checker.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.accounting.types import OracleReportLimits 4 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 5 | 6 | 7 | @pytest.mark.integration 8 | def test_oracle_report_sanity_checker(oracle_report_sanity_checker_contract, caplog): 9 | check_contract( 10 | oracle_report_sanity_checker_contract, 11 | [ 12 | ('get_oracle_report_limits', None, lambda response: check_value_type(response, OracleReportLimits)), 13 | ], 14 | caplog, 15 | ) 16 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_staking_router.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.web3py.extensions.lido_validators import StakingModule, NodeOperator 4 | from tests.factory.no_registry import StakingModuleFactory 5 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 6 | 7 | 8 | @pytest.mark.integration 9 | def test_staking_router(staking_router_contract, caplog): 10 | check_contract( 11 | staking_router_contract, 12 | [ 13 | ( 14 | 'get_staking_modules', 15 | None, 16 | lambda response: check_value_type(response, list) 17 | and map(lambda sm: check_value_type(sm, StakingModule), response), 18 | ), 19 | ( 20 | 'get_all_node_operator_digests', 21 | (StakingModuleFactory.build(id=1),), 22 | lambda response: check_value_type(response, list) 23 | and map(lambda sm: check_value_type(sm, NodeOperator), response), 24 | ), 25 | ('get_contract_version', None, lambda response: check_value_type(response, int)), 26 | ], 27 | caplog, 28 | ) 29 | -------------------------------------------------------------------------------- /tests/integration/contracts/test_validator_exit_bus_oracle.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.ejector.types import EjectorProcessingState 4 | from tests.integration.contracts.contract_utils import check_contract, check_value_type 5 | 6 | 7 | @pytest.mark.integration 8 | def test_vebo(validators_exit_bus_oracle_contract, caplog): 9 | check_contract( 10 | validators_exit_bus_oracle_contract, 11 | [ 12 | ('is_paused', None, lambda response: check_value_type(response, bool)), 13 | ('get_processing_state', None, lambda response: check_value_type(response, EjectorProcessingState)), 14 | ( 15 | 'get_last_requested_validator_indices', 16 | (1, [1]), 17 | lambda response: check_value_type(response, list) and map(lambda val: check_value_type(val, int)), 18 | ), 19 | ], 20 | caplog, 21 | ) 22 | -------------------------------------------------------------------------------- /tests/metrics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/metrics/__init__.py -------------------------------------------------------------------------------- /tests/modules/accounting/test_withdrawal_integration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.submodules.types import FrameConfig, ChainConfig 4 | from src.services.withdrawal import Withdrawal 5 | from src.constants import SHARE_RATE_PRECISION_E27 6 | from tests.conftest import get_blockstamp_by_state 7 | 8 | 9 | @pytest.fixture 10 | def chain_config(): 11 | return ChainConfig(slots_per_epoch=32, seconds_per_slot=12, genesis_time=1675263480) 12 | 13 | 14 | @pytest.fixture 15 | def frame_config(): 16 | return FrameConfig(initial_epoch=0, epochs_per_frame=10, fast_lane_length_slots=0) 17 | 18 | 19 | @pytest.fixture 20 | def past_blockstamp(web3, consensus_client): 21 | return get_blockstamp_by_state(web3, 'finalized') 22 | 23 | 24 | @pytest.fixture 25 | def subject(web3, past_blockstamp, chain_config, frame_config, contracts, keys_api_client, consensus_client): 26 | return Withdrawal(web3, past_blockstamp, chain_config, frame_config) 27 | 28 | 29 | def test_happy_path(subject, past_blockstamp): 30 | withdrawal_vault_balance = subject.w3.lido_contracts.get_withdrawal_balance(past_blockstamp) 31 | el_rewards_vault_balance = subject.w3.lido_contracts.get_el_vault_balance(past_blockstamp) 32 | 33 | expected_min_withdrawal_id = subject.w3.lido_contracts.withdrawal_queue_nft.get_last_finalized_request_id( 34 | past_blockstamp.block_hash 35 | ) 36 | expected_max_withdrawal_id = subject.w3.lido_contracts.withdrawal_queue_nft.get_last_request_id( 37 | past_blockstamp.block_hash 38 | ) 39 | 40 | results = subject.get_finalization_batches( 41 | False, SHARE_RATE_PRECISION_E27, withdrawal_vault_balance, el_rewards_vault_balance 42 | ) 43 | 44 | for result in results: 45 | assert expected_min_withdrawal_id < result <= expected_max_withdrawal_id 46 | -------------------------------------------------------------------------------- /tests/modules/csm/test_log.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | 4 | from src.modules.csm.log import FramePerfLog 5 | from src.modules.csm.state import AttestationsAccumulator 6 | from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp 7 | from tests.factory.blockstamp import ReferenceBlockStampFactory 8 | 9 | 10 | @pytest.fixture() 11 | def ref_blockstamp() -> ReferenceBlockStamp: 12 | return ReferenceBlockStampFactory.build() 13 | 14 | 15 | @pytest.fixture() 16 | def frame() -> tuple[EpochNumber, EpochNumber]: 17 | return (EpochNumber(100), EpochNumber(500)) 18 | 19 | 20 | @pytest.fixture() 21 | def log(ref_blockstamp: ReferenceBlockStamp, frame: tuple[EpochNumber, EpochNumber]) -> FramePerfLog: 22 | return FramePerfLog(ref_blockstamp, frame) 23 | 24 | 25 | def test_fields_access(log: FramePerfLog): 26 | log.operators[NodeOperatorId(42)].validators["100500"].slashed = True 27 | log.operators[NodeOperatorId(17)].stuck = True 28 | 29 | 30 | def test_log_encode(log: FramePerfLog): 31 | # Fill in dynamic fields to make sure we have data in it to be encoded. 32 | log.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) 33 | log.operators[NodeOperatorId(42)].distributed = 17 34 | log.operators[NodeOperatorId(0)].distributed = 0 35 | 36 | encoded = log.encode() 37 | decoded = json.loads(encoded) 38 | 39 | assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 40 | assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 41 | assert decoded["operators"]["42"]["distributed"] == 17 42 | assert decoded["operators"]["0"]["distributed"] == 0 43 | 44 | assert decoded["blockstamp"]["block_hash"] == log.blockstamp.block_hash 45 | assert decoded["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot 46 | 47 | assert decoded["threshold"] == log.threshold 48 | assert decoded["frame"] == list(log.frame) 49 | -------------------------------------------------------------------------------- /tests/modules/csm/test_tree.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.constants import UINT64_MAX 4 | from src.modules.csm.tree import StandardMerkleTree, Tree, TreeJSONEncoder 5 | from src.types import NodeOperatorId 6 | 7 | 8 | @pytest.fixture() 9 | def tree(): 10 | return Tree.new( 11 | [ 12 | (NodeOperatorId(0), 0), 13 | (NodeOperatorId(1), 1), 14 | (NodeOperatorId(2), 42), 15 | (NodeOperatorId(UINT64_MAX), 0), 16 | ] 17 | ) 18 | 19 | 20 | def test_non_null_root(tree: Tree): 21 | assert tree.root 22 | 23 | 24 | def test_encode_decode(tree: Tree): 25 | decoded = Tree.decode(tree.encode()) 26 | assert decoded.root == tree.root 27 | 28 | 29 | def test_decode_plain_tree_dump(tree: Tree): 30 | decoded = Tree.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) 31 | assert decoded.root == tree.root 32 | 33 | 34 | def test_dump_compatibility(tree: Tree): 35 | loaded = StandardMerkleTree.load(tree.dump()) 36 | assert loaded.root == tree.root 37 | -------------------------------------------------------------------------------- /tests/modules/submodules/consensus/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.modules.submodules.consensus import ConsensusModule 4 | from src.types import BlockStamp, ReferenceBlockStamp 5 | 6 | 7 | class SimpleConsensusModule(ConsensusModule): 8 | COMPATIBLE_ONCHAIN_VERSIONS = [(2, 2)] 9 | 10 | def __init__(self, w3): 11 | self.report_contract = w3.lido_contracts.accounting_oracle 12 | super().__init__(w3) 13 | 14 | def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: 15 | return tuple() 16 | 17 | def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: 18 | return True 19 | 20 | def is_contract_reportable(self, blockstamp: BlockStamp) -> bool: 21 | return True 22 | 23 | def is_reporting_allowed(self, blockstamp: ReferenceBlockStamp) -> bool: 24 | return True 25 | 26 | 27 | @pytest.fixture() 28 | def consensus(web3, consensus_client, contracts): 29 | return SimpleConsensusModule(web3) 30 | -------------------------------------------------------------------------------- /tests/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/providers/__init__.py -------------------------------------------------------------------------------- /tests/providers/consensus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidofinance/lido-oracle/e6e5c69741f0a82d13f59a1011991bc18f23699f/tests/providers/consensus/__init__.py -------------------------------------------------------------------------------- /tests/providers/test_ipfs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.providers.ipfs import IPFSProvider, CID, CIDv0 4 | 5 | 6 | @pytest.mark.unit 7 | def test_ipfs_upload(): 8 | class TestIPFSProvider(IPFSProvider): 9 | def __init__(self, cid: str): 10 | self.cid = cid 11 | 12 | def _upload(self, *args): 13 | return self.cid 14 | 15 | def fetch(self, cid: CID) -> bytes: ... 16 | 17 | def pin(self, cid: CID) -> None: ... 18 | 19 | cid = TestIPFSProvider('QmPK1s3pNYLi9ERiq3BDxKa4XosgWwFRQUydHUtz4YgpqB').upload(b'hello world') 20 | 21 | assert isinstance(cid, CIDv0) 22 | assert cid == 'QmPK1s3pNYLi9ERiq3BDxKa4XosgWwFRQUydHUtz4YgpqB' 23 | 24 | cid = TestIPFSProvider('bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi').upload(b'hello world') 25 | assert isinstance(cid, CIDv0) 26 | assert cid == 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR' 27 | 28 | with pytest.raises(ValueError): 29 | # valid cid with json multicodec 30 | # Unsupported hash code 30 31 | TestIPFSProvider('bagaaihraf4oq2kddg6o5ewlu6aol6xab75xkwbgzx2dlot7cdun7iirve23a').upload(b'hello world') 32 | 33 | with pytest.raises(ValueError): 34 | # multihash is not a valid base58 encoded multihash 35 | TestIPFSProvider('invalidcid').upload(b'hello world') 36 | -------------------------------------------------------------------------------- /tests/providers_clients/test_keys_api_client.py: -------------------------------------------------------------------------------- 1 | """Simple tests for the keys api client responses validity.""" 2 | 3 | from unittest.mock import Mock 4 | 5 | import pytest 6 | 7 | import src.providers.keys.client as keys_api_client_module 8 | from src import variables 9 | from src.providers.keys.client import KeysAPIClient, KeysOutdatedException 10 | from src.variables import KEYS_API_URI 11 | from tests.factory.blockstamp import ReferenceBlockStampFactory 12 | 13 | 14 | @pytest.fixture() 15 | def keys_api_client(): 16 | return KeysAPIClient(KEYS_API_URI, 5 * 60, 5, 5) 17 | 18 | 19 | empty_blockstamp = ReferenceBlockStampFactory.build(block_number=0) 20 | 21 | 22 | @pytest.mark.integration 23 | def test_get_used_lido_keys(keys_api_client): 24 | lido_keys = keys_api_client.get_used_lido_keys(empty_blockstamp) 25 | assert lido_keys 26 | 27 | 28 | @pytest.mark.integration 29 | def test_get_status(keys_api_client): 30 | status = keys_api_client.get_status() 31 | assert status 32 | 33 | 34 | @pytest.mark.unit 35 | def test_get_with_blockstamp_retries_exhausted(keys_api_client, monkeypatch): 36 | variables.HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS = 1 37 | keys_api_client._get = Mock( 38 | return_value=( 39 | None, 40 | {"meta": {"elBlockSnapshot": {"blockNumber": empty_blockstamp.block_number - 1}}}, 41 | ) 42 | ) 43 | 44 | sleep_mock = Mock() 45 | 46 | with pytest.raises(KeysOutdatedException): 47 | with monkeypatch.context() as m: 48 | m.setattr(keys_api_client_module, "sleep", sleep_mock) 49 | keys_api_client.get_used_lido_keys(empty_blockstamp) 50 | 51 | assert sleep_mock.call_count == variables.HTTP_REQUEST_RETRY_COUNT_KEYS_API - 1 52 | sleep_mock.assert_called_with(variables.HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS_KEYS_API) 53 | -------------------------------------------------------------------------------- /tests/utils/test_abi.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from dataclasses import dataclass 3 | 4 | import pytest 5 | 6 | from src.utils.abi import camel_to_snake, named_tuple_to_dataclass 7 | 8 | 9 | pytestmark = pytest.mark.unit 10 | 11 | 12 | @dataclass 13 | class CarDataclass: 14 | car_name: str 15 | car_size: int 16 | super_car: bool 17 | 18 | 19 | CarTuple = namedtuple('Car', ['carName', 'carSize']) 20 | 21 | 22 | def test_named_tuple_to_dataclass(): 23 | Car = namedtuple('Car', ['carName', 'carSize', 'super_car']) 24 | car = Car('mazda', 1, True) 25 | 26 | supercar = named_tuple_to_dataclass(car, CarDataclass) 27 | assert supercar.car_name == car[0] 28 | assert supercar.car_size == car[1] 29 | assert supercar.super_car == car[2] 30 | 31 | 32 | def test_camel_to_snake(): 33 | assert 'camel_case' == camel_to_snake('CamelCase') 34 | assert 'get_http_response_code' == camel_to_snake('getHTTPResponseCode') 35 | assert 'http_response_code_xyz' == camel_to_snake('HTTPResponseCodeXYZ') 36 | -------------------------------------------------------------------------------- /tests/utils/test_build.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, mock_open 3 | import json 4 | 5 | from src.utils.build import get_build_info, UNKNOWN_BUILD_INFO 6 | 7 | 8 | class TestGetBuildInfo(unittest.TestCase): 9 | 10 | @patch( 11 | 'builtins.open', new_callable=mock_open, read_data='{"version": "1.0.0", "branch": "main", "commit": "abc123"}' 12 | ) 13 | def test_get_build_info_success(self, mock_open_file): 14 | """Test that get_build_info successfully reads from the JSON file.""" 15 | expected_build_info = {"version": "1.0.0", "branch": "main", "commit": "abc123"} 16 | 17 | # Call the function 18 | build_info = get_build_info() 19 | 20 | # Assertions 21 | mock_open_file.assert_called_once_with("./build-info.json", "r") 22 | self.assertEqual(build_info, expected_build_info, "Build info should match the data from the file") 23 | 24 | def test_get_build_info_file_not_exists(self): 25 | """Test that get_build_info returns UNKNOWN_BUILD_INFO when the file does not exist.""" 26 | build_info = get_build_info() 27 | 28 | # Assertions 29 | self.assertEqual(build_info, UNKNOWN_BUILD_INFO, "Should return UNKNOWN_BUILD_INFO when file doesn't exist") 30 | 31 | @patch('os.path.exists', return_value=True) 32 | @patch('builtins.open', new_callable=mock_open, read_data='invalid json') 33 | def test_get_build_info_json_decode_error(self, mock_open_file, mock_exists): 34 | """Test that get_build_info returns UNKNOWN_BUILD_INFO when there is a JSONDecodeError.""" 35 | # Simulate a JSONDecodeError by providing invalid JSON in the file 36 | with patch('json.load', side_effect=json.JSONDecodeError("Expecting value", "document", 0)): 37 | build_info = get_build_info() 38 | 39 | # Assertions 40 | mock_open_file.assert_called_once_with("./build-info.json", "r") 41 | self.assertEqual(build_info, UNKNOWN_BUILD_INFO, "Should return UNKNOWN_BUILD_INFO when JSON decode fails") 42 | -------------------------------------------------------------------------------- /tests/utils/test_cache.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from hexbytes import HexBytes 4 | from web3.types import BlockIdentifier 5 | 6 | from src.providers.execution.base_interface import ContractInterface 7 | from src.utils.cache import clear_global_cache, global_lru_cache 8 | 9 | 10 | class Calc: 11 | @global_lru_cache(maxsize=2) 12 | def get(self, a, b): 13 | return a + b 14 | 15 | 16 | def test_clear_global_cache(): 17 | calc = Calc() 18 | calc.get(1, 2) 19 | assert calc.get.cache_info().currsize == 1 20 | 21 | calc.get(2, 1) 22 | assert calc.get.cache_info().currsize == 2 23 | 24 | clear_global_cache() 25 | 26 | assert calc.get.cache_info().currsize == 0 27 | 28 | 29 | class Contract(ContractInterface): 30 | def __init__(self): 31 | pass 32 | 33 | @global_lru_cache(maxsize=5) 34 | def func(self, block_identifier: BlockIdentifier = 'latest'): 35 | pass 36 | 37 | @global_lru_cache(maxsize=1) 38 | def func_1(self, module_id: int, block_identifier: BlockIdentifier = 'latest'): 39 | return random.random() 40 | 41 | 42 | def test_cache_do_not_cache_contract_with_relative_blocks(): 43 | c = Contract() 44 | 45 | c.func() 46 | assert c.func.cache_info().currsize == 0 47 | c.func(block_identifier=HexBytes('11')) 48 | c.func(block_identifier=HexBytes('11')) 49 | c.func(block_identifier=HexBytes('22')) 50 | c.func(block_identifier='latest') 51 | c.func(block_identifier='finalized') 52 | c.func('finalized') 53 | assert c.func.cache_info().currsize == 2 54 | 55 | c.func(HexBytes('22')) 56 | c.func(HexBytes('11')) 57 | c.func(HexBytes('33')) 58 | c.func('finalized') 59 | assert c.func.cache_info().currsize == 3 60 | 61 | result_1 = c.func_1(1, 1) 62 | result_2 = c.func_1(1) 63 | result_3 = c.func_1(1, 1) 64 | 65 | assert result_1 != result_2 66 | assert result_1 == result_3 67 | -------------------------------------------------------------------------------- /tests/utils/test_env.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from src.utils.env import from_file_or_env 6 | 7 | 8 | class TestFromFileOrEnv: 9 | @pytest.fixture() 10 | def file_path(self, tmp_path: Path) -> Path: 11 | return tmp_path / "file" 12 | 13 | def test_from_env(self, monkeypatch: pytest.MonkeyPatch): 14 | with monkeypatch.context() as mp: 15 | env_name = "ENV_NAME" 16 | expected = "WHATEVER" 17 | 18 | mp.setenv(env_name, expected) 19 | actual = from_file_or_env(env_name) 20 | assert actual == expected 21 | 22 | def test_from_file(self, monkeypatch: pytest.MonkeyPatch, file_path: Path): 23 | with monkeypatch.context() as mp: 24 | env_name = "ENV_NAME" 25 | expected = "WHATEVER" 26 | 27 | mp.setenv(f"{env_name}_FILE", str(file_path)) 28 | with file_path.open("w") as f: 29 | f.write(expected) 30 | 31 | actual = from_file_or_env(env_name) 32 | assert actual == expected 33 | 34 | def test_file_overrides_env(self, monkeypatch: pytest.MonkeyPatch, file_path: Path): 35 | with monkeypatch.context() as mp: 36 | env_name = "ENV_NAME" 37 | expected = "WHATEVER" 38 | 39 | mp.setenv(f"{env_name}_FILE", str(file_path)) 40 | mp.setenv(env_name, "OVERRIDDEN") 41 | with file_path.open("w") as f: 42 | f.write(expected) 43 | 44 | actual = from_file_or_env(env_name) 45 | assert actual == expected 46 | 47 | def test_file_does_not_exist(self, monkeypatch: pytest.MonkeyPatch): 48 | with monkeypatch.context() as mp: 49 | env_name = "ENV_NAME" 50 | mp.setenv(f"{env_name}_FILE", "NONEXISTENT") 51 | with pytest.raises(ValueError, match="does not exist"): 52 | from_file_or_env(env_name) 53 | -------------------------------------------------------------------------------- /tests/utils/test_range.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.utils.range import sequence 4 | 5 | 6 | def test_sequence(): 7 | assert list(sequence(0, 3)) == [0, 1, 2, 3] 8 | assert list(sequence(1, 3)) == [1, 2, 3] 9 | assert list(sequence(3, 3)) == [3] 10 | 11 | assert list(sequence(-3, -3)) == [-3] 12 | assert list(sequence(-3, -1)) == [-3, -2, -1] 13 | assert list(sequence(-3, 0)) == [-3, -2, -1, 0] 14 | 15 | 16 | def test_sequence_raises(): 17 | with pytest.raises(ValueError, match="start=3 > stop=1"): 18 | sequence(3, 1) 19 | -------------------------------------------------------------------------------- /tests/utils/test_timeit.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from src.utils.timeit import timeit 7 | 8 | 9 | def test_timeit_log_fn_no_args(): 10 | log_fn = Mock() 11 | 12 | @timeit(log_fn) 13 | def fn(): ... 14 | 15 | fn() 16 | log_fn.assert_called_once() 17 | assert log_fn.call_args.args[0] == SimpleNamespace() 18 | 19 | 20 | def test_timeit_log_fn_args(): 21 | log_fn = Mock() 22 | 23 | @timeit(log_fn) 24 | def fn(a, b, k): ... 25 | 26 | fn(2, 0, k="any") 27 | log_fn.assert_called_once() 28 | assert log_fn.call_args.args[0] == SimpleNamespace(a=2, b=0, k="any") 29 | 30 | 31 | def test_timeit_log_fn_args_method(): 32 | log_fn = Mock() 33 | 34 | class Some: 35 | @timeit(log_fn) 36 | def fn(self, a, b): ... 37 | 38 | some = Some() 39 | some.fn(42, b="any") 40 | 41 | log_fn.assert_called_once() 42 | assert log_fn.call_args.args[0] == SimpleNamespace(a=42, b="any", self=some) 43 | 44 | 45 | def test_timeit_log_fn_called_on_exception(): 46 | log_fn = Mock() 47 | 48 | @timeit(log_fn) 49 | def fn(): 50 | raise ValueError 51 | 52 | with pytest.raises(ValueError): 53 | fn() 54 | 55 | log_fn.assert_not_called() 56 | 57 | 58 | def test_timeit_duration(monkeypatch: pytest.MonkeyPatch): 59 | log_fn = Mock() 60 | 61 | @timeit(log_fn) 62 | def fn(): ... 63 | 64 | time = Mock(side_effect=[1, 12.34]) 65 | with monkeypatch.context() as m: 66 | m.setattr("time.time", time) 67 | fn() 68 | 69 | assert time.call_count == 2 70 | log_fn.assert_called_once_with(SimpleNamespace(), 11.34) 71 | -------------------------------------------------------------------------------- /tests/utils/test_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.utils.types import bytes_to_hex_str, hex_str_to_bytes, is_4bytes_hex 4 | 5 | 6 | @pytest.mark.unit 7 | def test_bytes_to_hex_str(): 8 | assert bytes_to_hex_str(b"") == "0x" 9 | assert bytes_to_hex_str(b"\x00") == "0x00" 10 | assert bytes_to_hex_str(b"\x00\x01\x02") == "0x000102" 11 | 12 | 13 | @pytest.mark.unit 14 | def test_hex_str_to_bytes(): 15 | assert hex_str_to_bytes("") == b"" 16 | assert hex_str_to_bytes("00") == b"\x00" 17 | assert hex_str_to_bytes("000102") == b"\x00\x01\x02" 18 | assert hex_str_to_bytes("0x") == b"" 19 | assert hex_str_to_bytes("0x00") == b"\x00" 20 | assert hex_str_to_bytes("0x000102") == b"\x00\x01\x02" 21 | 22 | 23 | @pytest.mark.unit 24 | def test_is_4bytes_hex(): 25 | assert is_4bytes_hex("0x00000000") 26 | assert is_4bytes_hex("0x02000000") 27 | assert is_4bytes_hex("0x02000000") 28 | assert is_4bytes_hex("0x30637624") 29 | 30 | assert not is_4bytes_hex("") 31 | assert not is_4bytes_hex("0x") 32 | assert not is_4bytes_hex("0x00") 33 | assert not is_4bytes_hex("0x01") 34 | assert not is_4bytes_hex("0x01") 35 | assert not is_4bytes_hex("0xgg") 36 | assert not is_4bytes_hex("0x111") 37 | assert not is_4bytes_hex("0x02000000ff") 38 | --------------------------------------------------------------------------------