diff --git a/README.md b/README.md index 6d7e8fc..bb6a9f4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Oracle -Oracles are responsible for voting on the new ETH2 rewards for the StakeWise sETH2 tokens holders and calculating Merkle +Oracles are responsible for voting on the new rewards for the StakeWise staked tokens holders and calculating Merkle root and proofs for the additional token distributions through the [Merkle Distributor](https://github.com/stakewise/contracts/blob/master/contracts/merkles/MerkleDistributor.sol) contract. @@ -31,57 +31,37 @@ supports [ETH2 Beacon Node API specification](https://ethereum.github.io/beacon- - [Teku](https://launchpad.ethereum.org/en/teku) - [Infura](https://infura.io/docs/eth2) (hosted) -### Usage +### Oracle Usage -1. Move to `deploy` directory +1. Move to `deploy/<network>` directory ```shell script -cd deploy +cd deploy/mainnet ``` -2. Create an edit environment file (see `Oracle Settings` below) +2. Create an edit environment file ```shell script cp .env.example .env ``` -3. Enable `pushover` alerts in `configs/alertmanager.yml` +3. Enable `pushover` alerts in `deploy/configs/alertmanager.yml` -4. Run with [docker-compose](https://docs.docker.com/compose/) + 1. Register an account on [pushover](https://pushover.net/). + 2. Create an [Application/API Token](https://pushover.net/apps/build). + 3. Add `User Key` and `API Token` to `deploy/configs/alertmanager.yml` file. + +4. Run with [docker-compose](https://docs.docker.com/compose/). The docker-compose version must be **v1.27.0+**. ```shell script -docker-compose -f docker-compose.yml up -d +COMPOSE_PROFILES=lighthouse docker-compose up -d ``` -### Oracle Settings - -| Variable | Description | Required | Default | -|-------------------------|------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------| -| NETWORK | The network that the oracle is currently operating on. Choices are goerli, mainnet | No | mainnet | -| ENABLE_HEALTH_SERVER | Defines whether to enable health server | No | True | -| HEALTH_SERVER_PORT | The port where the health server will run | No | 8080 | -| HEALTH_SERVER_HOST | The host where the health server will run | No | 127.0.0.1 | -| IPFS_PIN_ENDPOINTS | The IPFS endpoint where the rewards will be uploaded | No | /dns/ipfs.infura.io/tcp/5001/https | -| IPFS_FETCH_ENDPOINTS | The IPFS endpoints from where the rewards will be fetched | No | https://gateway.pinata.cloud,http://cloudflare-ipfs.com,https://ipfs.io | -| IPFS_PINATA_API_KEY | The Pinata API key for uploading reward proofs for the redundancy | No | - | -| IPFS_PINATA_SECRET_KEY | The Pinata Secret key for uploading reward proofs for the redundancy | No | - | -| ETH2_ENDPOINT | The ETH2 node endpoint | No | http://localhost:3501 | -| ETH2_CLIENT | The ETH2 client used. Choices are prysm, lighthouse, teku. | No | prysm | -| ORACLE_PRIVATE_KEY | The ETH1 private key of the oracle | Yes | - | -| AWS_ACCESS_KEY_ID | The AWS access key used to make the oracle vote public | Yes | - | -| AWS_SECRET_ACCESS_KEY | The AWS secret access key used to make the oracle vote public | Yes | - | -| STAKEWISE_SUBGRAPH_URL | The StakeWise subgraph URL | No | https://api.thegraph.com/subgraphs/name/stakewise/stakewise-mainnet | -| UNISWAP_V3_SUBGRAPH_URL | The Uniswap V3 subgraph URL | No | https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-mainnet | -| RARI_FUSE_SUBGRAPH_URL | Rari Capital Fuse subgraph URL | No | https://api.thegraph.com/subgraphs/name/stakewise/rari-fuse-mainnet | -| ETHEREUM_SUBGRAPH_URL | The Ethereum subgraph URL | No | https://api.thegraph.com/subgraphs/name/stakewise/ethereum-mainnet | -| ORACLE_PROCESS_INTERVAL | How long to wait before processing again (in seconds) | No | 180 | -| CONFIRMATION_BLOCKS | The required number of confirmation blocks used to fetch the data | No | 15 | -| LOG_LEVEL | The log level of the oracle | No | INFO | - ## Keeper Keeper is an oracle that aggregates votes that were submitted by all the oracles and submits the update transaction. The keeper does not require any additional role, and can be executed by any of the oracles. +It helps save the gas cost and stability as there is no need for every oracle to submit vote. ### Dependencies @@ -91,49 +71,24 @@ The ETH1 node is used to submit the transactions on chain. Any of the ETH1 clien - [Go-ethereum](https://github.com/ethereum/go-ethereum) - [OpenEthereum](https://github.com/openethereum/openethereum) +- [Nethermind](https://github.com/NethermindEth/nethermind) - [Infura](https://infura.io/docs/eth2) (hosted) - [Alchemy](https://www.alchemy.com/) (hosted) -### Usage - -1. Move to `deploy` directory - -```shell script -cd deploy -``` +### Keeper Usage -2. Create an edit environment file (see `Keeper Settings` below) +1. Make sure keeper has enough balance to submit the transactions -```shell script -cp .env.example .env -``` +2. Go through the [oracle usage](#oracle-usage) steps above -3. Enable `pushover` alerts in `configs/alertmanager.yml` +3. Configure keeper section in the `deploy/<network>/.env` file 4. Uncomment `keeper` sections in the following files: - * configs/rules.yml - * configs/prometheus.yml - * configs/rules.yml - * docker-compose.yml + * `deploy/configs/prometheus.yml` + * `deploy/configs/rules.yml` -5. Run with [docker-compose](https://docs.docker.com/compose/) +5. Run with [docker-compose](https://docs.docker.com/compose/). The docker-compose version must be **v1.27.0+**. ```shell script -docker-compose -f docker-compose.yml up -d +COMPOSE_PROFILES=lighthouse,keeper docker-compose up -d ``` - -### Keeper Settings - -| Variable | Description | Required | Default | -|-------------------------|------------------------------------------------------------------------------------|----------|-----------| -| NETWORK | The network that the keeper is currently operating on. Choices are goerli, mainnet | No | mainnet | -| WEB3_ENDPOINT | The endpoint of the ETH1 node. | Yes | - | -| MAX_FEE_PER_GAS_GWEI | The max fee per gas keeper is willing to pay. Specified in GWEI. | No | 150 | -| ENABLE_HEALTH_SERVER | Defines whether to enable health server | No | True | -| HEALTH_SERVER_PORT | The port where the health server will run | No | 8080 | -| HEALTH_SERVER_HOST | The host where the health server will run | No | 127.0.0.1 | -| ORACLE_PRIVATE_KEY | The ETH1 private key of the oracle | Yes | - | -| KEEPER_PROCESS_INTERVAL | How long to wait before processing again (in seconds) | No | 180 | -| CONFIRMATION_BLOCKS | The required number of confirmation blocks used to fetch the data | No | 15 | -| KEEPER_MIN_BALANCE_WEI | The minimum balance keeper must have for votes submission | No | 0.1 ETH | -| LOG_LEVEL | The log level of the keeper | No | INFO | diff --git a/deploy/.env.example b/deploy/.env.example deleted file mode 100644 index 4d64b78..0000000 --- a/deploy/.env.example +++ /dev/null @@ -1,43 +0,0 @@ -# mainnet or goerli -NETWORK=mainnet -ENABLE_HEALTH_SERVER=true -HEALTH_SERVER_PORT=8080 -HEALTH_SERVER_HOST=0.0.0.0 -ORACLE_PRIVATE_KEY=0x<private_key> - -# ORACLE -IPFS_PINATA_API_KEY="" -IPFS_PINATA_SECRET_KEY="" -ETH2_ENDPOINT=http://lighthouse:5052 -# lighthouse, prysm or teku -ETH2_CLIENT=lighthouse -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" - -# Uncomment below lines if use self-hosted graph node -# STAKEWISE_SUBGRAPH_URL=http://graph-node:8000/subgraphs/name/stakewise/stakewise -# UNISWAP_V3_SUBGRAPH_URL=http://graph-node:8000/subgraphs/name/stakewise/uniswap-v3 -# ETHEREUM_SUBGRAPH_URL=http://graph-node:8000/subgraphs/name/stakewise/ethereum -# RARI_FUSE_SUBGRAPH_URL=http://graph-node:8000/subgraphs/name/stakewise/rari-fuse - -# KEEPER -WEB3_ENDPOINT="" -MAX_FEE_PER_GAS_GWEI=150 - -# GRAPH -postgres_host=postgres -postgres_user=graph -postgres_pass=strong-password -postgres_db=graph-node -ipfs=ipfs:5001 -ethereum=mainnet:http://geth:8545 -GRAPH_LOG=info -GRAPH_NODE_URL=http://graph-node:8020 -IPFS_URL=http://ipfs:5001 -IPFS_PROFILE=server -IPFS_FD_MAX=8192 - -# POSTGRESQL -POSTGRES_DB=graph-node -POSTGRES_USER=graph -POSTGRES_PASSWORD=strong-password diff --git a/deploy/README.md b/deploy/README.md deleted file mode 100644 index a5a5add..0000000 --- a/deploy/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Deploy instruction - -The deployment directory contains a set of docker-compose files for deploying `oracle` and `keeper` (by default, only `oracle` will be deployed). Oracle requires eth2 node, graph node and ipfs as dependencies, if you do not use cloud solutions, you can deploy self-hosted dependencies: - -```console -$ COMPOSE_PROFILES=oracle,geth,prysm,graph docker-compose up -d -``` - -If you want to run the keeper service: - -1. Uncomment `keeper` service in `docker-compose.yml` file. -1. Uncomment `keeper` job in `configs/prometheus.yml` file. -1. Uncomment `keeper` rule in `configs/rules.yml` file. - -## Monitoring - -If you want to receive notifications on one of your devices: - -1. Register an account on [pushover](https://pushover.net/). -1. Create an [Application/API Token](https://pushover.net/apps/build). -1. Add `User Key` and `API Token` to `configs/alertmanager.yml` file. -1. Restart `docker-compose`. diff --git a/deploy/configs/genesis.ssz b/deploy/configs/genesis.ssz new file mode 100644 index 0000000..3f03867 Binary files /dev/null and b/deploy/configs/genesis.ssz differ diff --git a/deploy/gnosis/.env.example b/deploy/gnosis/.env.example new file mode 100644 index 0000000..91be9a6 --- /dev/null +++ b/deploy/gnosis/.env.example @@ -0,0 +1,79 @@ +########## +# Oracle # +########## +LOG_LEVEL=INFO +ENABLED_NETWORKS=gnosis +ENABLE_HEALTH_SERVER=true +HEALTH_SERVER_PORT=8080 +HEALTH_SERVER_HOST=0.0.0.0 + +# Remove ",/dns/ipfs/tcp/5001/http" if you don't use "ipfs" profile +IPFS_PIN_ENDPOINTS=/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http + +# Optionally pin merkle proofs to the pinata service for redundancy +IPFS_PINATA_API_KEY=<pinata_api_key> +IPFS_PINATA_SECRET_KEY=<pinata_secret_key> + +# Change https://api.thegraph.com to http://graph-node:8000 if running local graph node +GNOSIS_STAKEWISE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/stakewise-gnosis +GNOSIS_ETHEREUM_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/ethereum-gnosis + +# Ethereum private key +# NB! You must use a different private key for every network +GNOSIS_ORACLE_PRIVATE_KEY=0x<private_key> + +# ETH2 (consensus) client endpoint +# Change if running an external ETH2 node +GNOSIS_ETH2_ENDPOINT=http://eth2-node:5052 + +# AWS bucket to publish oracle votes to +GNOSIS_AWS_ACCESS_KEY_ID=<access_id> +GNOSIS_AWS_SECRET_ACCESS_KEY=<secret_key> +GNOSIS_AWS_BUCKET_NAME=oracle-votes-gnosis +GNOSIS_AWS_REGION=us-east-2 + +########## +# Keeper # +########## +# Change if running an external ETH1 node +GNOSIS_KEEPER_ETH1_ENDPOINT=http://eth1-node:8545 +# Use https://eth-converter.com/ to calculate +GNOSIS_KEEPER_MIN_BALANCE_WEI=1000000000000000000 +GNOSIS_KEEPER_MAX_FEE_PER_GAS_GWEI=150 + +######## +# IPFS # +######## +IPFS_URL=http://ipfs:5001 +IPFS_PROFILE=server +IPFS_FD_MAX=8192 + +############## +# Graph Node # +############## +GRAPH_LOG=info +GRAPH_NODE_URL=http://graph-node:8020 +# Change if running remote IPFS node +ipfs=ipfs:5001 +# Change if running an external ETH1 node +# NB! If syncing graph node from scratch archive node must be used. +# It can be switched to fast-sync node once fully synced. +ethereum=xdai:http://eth1-node:8545 +# Postgres DB settings for graph node +postgres_host=postgres +postgres_user=graph +postgres_pass=strong-password +postgres_db=graph-node + +############ +# Postgres # +############ +# postgres is used by local graph node +POSTGRES_DB=graph-node +POSTGRES_USER=graph +POSTGRES_PASSWORD=strong-password + +############# +# ETH2 NODE # +############# +ETH1_ENDPOINT=http://eth1-node:8545 diff --git a/deploy/gnosis/docker-compose.yml b/deploy/gnosis/docker-compose.yml new file mode 100644 index 0000000..afbff74 --- /dev/null +++ b/deploy/gnosis/docker-compose.yml @@ -0,0 +1,177 @@ +version: "3.9" + +volumes: + prometheus: + driver: local + alertmanager: + driver: local + postgres: + driver: local + ipfs: + driver: local + openethereum: + driver: local + nethermind: + driver: local + lighthouse: + driver: local + +networks: + gnosis: + name: gnosis + driver: bridge + +services: + oracle: + container_name: oracle_gnosis + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.2.0 + restart: always + entrypoint: ["python"] + command: ["oracle/oracle/main.py"] + env_file: [".env"] + profiles: ["oracle"] + networks: + - gnosis + + keeper: + container_name: keeper_gnosis + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.2.0 + restart: always + entrypoint: ["python"] + command: ["oracle/keeper/main.py"] + env_file: [".env"] + profiles: ["keeper"] + networks: + - gnosis + + prometheus: + container_name: prometheus_gnosis + image: bitnami/prometheus:2 + restart: always + env_file: [".env"] + volumes: + - prometheus:/opt/bitnami/prometheus/data + - ../configs/prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml + - ../configs/rules.yml:/opt/bitnami/prometheus/conf/rules.yml + networks: + - gnosis + + alertmanager: + container_name: alertmanager_gnosis + image: bitnami/alertmanager:0 + restart: always + env_file: [".env"] + volumes: + - alertmanager:/opt/bitnami/alertmanager/data + - ../configs/alertmanager.yml:/opt/bitnami/alertmanager/conf/config.yml + depends_on: ["prometheus"] + networks: + - gnosis + + graph-node: + container_name: graph_node_gnosis + image: graphprotocol/graph-node:v0.25.0 + restart: always + env_file: [".env"] + depends_on: ["postgres","ipfs"] + profiles: ["graph"] + networks: + - gnosis + + postgres: + container_name: postgres_gnosis + image: postgres:14-alpine + restart: always + command: ["postgres", "-cshared_preload_libraries=pg_stat_statements"] + env_file: [".env"] + volumes: ["postgres:/var/lib/postgresql/data"] + profiles: ["graph"] + networks: + - gnosis + + subgraphs: + container_name: subgraphs_gnosis + image: europe-west4-docker.pkg.dev/stakewiselabs/public/subgraphs:v1.0.8 + command: > + /bin/sh -c "until nc -vz graph-node 8020; do echo 'Waiting graph-node'; sleep 2; done + && yarn build:${NETWORK} + && yarn create:local + && yarn deploy:local" + env_file: [".env"] + restart: "no" + depends_on: ["graph-node","ipfs"] + profiles: ["graph"] + networks: + - gnosis + + ipfs: + container_name: ipfs_gnosis + image: ipfs/go-ipfs:v0.10.0 + restart: always + env_file: [".env"] + ulimits: + nofile: + soft: 8192 + hard: 8192 + volumes: ["ipfs:/data/ipfs","../configs/ipfs-entrypoint.sh:/usr/local/bin/start_ipfs"] + profiles: ["ipfs"] + networks: + - gnosis + + openethereum: + container_name: openethereum_gnosis + image: openethereum/openethereum:v3.3.3 + restart: always + command: + - --chain=xdai + - --jsonrpc-interface=all + - --jsonrpc-hosts=all + - --jsonrpc-port=8545 + - --min-peers=50 + - --max-peers=100 + volumes: ["openethereum:/home/openethereum"] + profiles: ["openethereum"] + networks: + gnosis: + aliases: + - eth1-node + + + nethermind: + container_name: nethermind_gnosis + image: nethermind/nethermind:1.12.4 + restart: always + command: + - --config=xdai + - --datadir=/data/nethermind + - --JsonRpc.Enabled=true + - --JsonRpc.EnabledModules=Eth,Subscribe,Trace,TxPool,Web3,Personal,Proof,Net,Parity,Health + - --JsonRpc.Host=0.0.0.0 + - --JsonRpc.Port=8545 + volumes: ["nethermind:/data"] + profiles: ["nethermind"] + networks: + gnosis: + aliases: + - eth1-node + + lighthouse: + container_name: lighthouse_gnosis + image: sigp/lighthouse:v2.1.2 + restart: always + command: + - lighthouse + - --network + - gnosis + - beacon + - --http + - --http-address=0.0.0.0 + - --http-port=5052 + - --eth1-endpoints + - $ETH1_ENDPOINT + volumes: ["lighthouse:/root/.lighthouse"] + profiles: ["lighthouse"] + networks: + gnosis: + aliases: + - eth2-node diff --git a/deploy/goerli/.env.example b/deploy/goerli/.env.example new file mode 100644 index 0000000..cae0b5e --- /dev/null +++ b/deploy/goerli/.env.example @@ -0,0 +1,80 @@ +########## +# Oracle # +########## +LOG_LEVEL=INFO +ENABLED_NETWORKS=eth_goerli +ENABLE_HEALTH_SERVER=true +HEALTH_SERVER_PORT=8080 +HEALTH_SERVER_HOST=0.0.0.0 + +# Remove ",/dns/ipfs/tcp/5001/http" if you don't use "ipfs" profile +IPFS_PIN_ENDPOINTS=/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http + +# Optionally pin merkle proofs to the pinata service for redundancy +IPFS_PINATA_API_KEY=<pinata_api_key> +IPFS_PINATA_SECRET_KEY=<pinata_secret_key> + +# Change https://api.thegraph.com to http://graph-node:8000 if running local graph node +ETH_GOERLI_STAKEWISE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/stakewise-goerli +ETH_GOERLI_ETHEREUM_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/ethereum-goerli +ETH_GOERLI_UNISWAP_V3_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-goerli + +# Ethereum private key +# NB! You must use a different private key for every network +ETH_GOERLI_ORACLE_PRIVATE_KEY=0x<private_key> + +# ETH2 (consensus) client endpoint +# Change if running an external ETH2 node +ETH_GOERLI_ETH2_ENDPOINT=http://eth2-node:5052 + +# AWS bucket to publish oracle votes to +ETH_GOERLI_AWS_ACCESS_KEY_ID=<access_id> +ETH_GOERLI_AWS_SECRET_ACCESS_KEY=<secret_key> +ETH_GOERLI_AWS_BUCKET_NAME=oracle-votes-goerli +ETH_GOERLI_AWS_REGION=eu-central-1 + +########## +# Keeper # +########## +# Change if running an external ETH1 node +ETH_GOERLI_KEEPER_ETH1_ENDPOINT=http://eth1-node:8545 +# Use https://eth-converter.com/ to calculate +ETH_GOERLI_KEEPER_MIN_BALANCE_WEI=100000000000000000 +ETH_GOERLI_KEEPER_MAX_FEE_PER_GAS_GWEI=150 + +######## +# IPFS # +######## +IPFS_URL=http://ipfs:5001 +IPFS_PROFILE=server +IPFS_FD_MAX=8192 + +############## +# Graph Node # +############## +GRAPH_LOG=info +GRAPH_NODE_URL=http://graph-node:8020 +# Change if running remote IPFS node +ipfs=ipfs:5001 +# Change if running an external ETH1 node +# NB! If syncing graph node from scratch archive node must be used. +# It can be switched to fast-sync node once fully synced. +ethereum=goerli:http://eth1-node:8545 +# Postgres DB settings for graph node +postgres_host=postgres +postgres_user=graph +postgres_pass=strong-password +postgres_db=graph-node + +############ +# Postgres # +############ +# postgres is used by local graph node +POSTGRES_DB=graph-node +POSTGRES_USER=graph +POSTGRES_PASSWORD=strong-password + +############# +# ETH2 NODE # +############# +ETH1_ENDPOINT=http://eth1-node:8545 diff --git a/deploy/goerli/docker-compose.yml b/deploy/goerli/docker-compose.yml new file mode 100644 index 0000000..1954b8a --- /dev/null +++ b/deploy/goerli/docker-compose.yml @@ -0,0 +1,221 @@ +version: "3.9" + +volumes: + prometheus: + driver: local + alertmanager: + driver: local + postgres: + driver: local + ipfs: + driver: local + geth: + driver: local + erigon: + driver: local + prysm: + driver: local + lighthouse: + driver: local + +networks: + goerli: + name: goerli + driver: bridge + +services: + oracle: + container_name: oracle_goerli + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.2.0 + restart: always + entrypoint: ["python"] + command: ["oracle/oracle/main.py"] + env_file: [".env"] + profiles: ["oracle"] + networks: + - goerli + + keeper: + container_name: keeper_goerli + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.2.0 + restart: always + entrypoint: ["python"] + command: ["oracle/keeper/main.py"] + env_file: [".env"] + profiles: ["keeper"] + networks: + - goerli + + prometheus: + container_name: prometheus_goerli + image: bitnami/prometheus:2 + restart: always + env_file: [".env"] + volumes: + - prometheus:/opt/bitnami/prometheus/data + - ../configs/prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml + - ../configs/rules.yml:/opt/bitnami/prometheus/conf/rules.yml + networks: + - goerli + + alertmanager: + container_name: alertmanager_goerli + image: bitnami/alertmanager:0 + restart: always + env_file: [".env"] + volumes: + - alertmanager:/opt/bitnami/alertmanager/data + - ../configs/alertmanager.yml:/opt/bitnami/alertmanager/conf/config.yml + depends_on: ["prometheus"] + networks: + - goerli + + graph-node: + container_name: graph_node_goerli + image: graphprotocol/graph-node:v0.25.0 + restart: always + env_file: [".env"] + depends_on: ["postgres","ipfs"] + profiles: ["graph"] + networks: + - goerli + + postgres: + container_name: postgres_goerli + image: postgres:14-alpine + restart: always + command: ["postgres", "-cshared_preload_libraries=pg_stat_statements"] + env_file: [".env"] + volumes: ["postgres:/var/lib/postgresql/data"] + profiles: ["graph"] + networks: + - goerli + + subgraphs: + container_name: subgraphs_goerli + image: europe-west4-docker.pkg.dev/stakewiselabs/public/subgraphs:v1.0.8 + command: > + /bin/sh -c "until nc -vz graph-node 8020; do echo 'Waiting graph-node'; sleep 2; done + && yarn build:${NETWORK} + && yarn create:local + && yarn deploy:local" + env_file: [".env"] + restart: "no" + depends_on: ["graph-node","ipfs"] + profiles: ["graph"] + networks: + - goerli + + ipfs: + container_name: ipfs_goerli + image: ipfs/go-ipfs:v0.10.0 + restart: always + env_file: [".env"] + ulimits: + nofile: + soft: 8192 + hard: 8192 + volumes: ["ipfs:/data/ipfs","./configs/ipfs-entrypoint.sh:/usr/local/bin/start_ipfs"] + profiles: ["ipfs"] + networks: + - goerli + + geth: + container_name: geth_goerli + image: ethereum/client-go:v1.10.15 + restart: always + command: + - --goerli + - --syncmode=full + - --http + - --http.addr=0.0.0.0 + - --http.vhosts=* + - --http.api=web3,eth,net + - --datadir=/data/ethereum + - --ethash.dagdir=/data/ethereum/.ethash + - --ipcdisable + volumes: ["geth:/data"] + profiles: ["geth"] + networks: + goerli: + aliases: + - eth1-node + + erigon: + container_name: erigon_goerli + image: thorax/erigon:v2022.01.03 + restart: always + command: + - erigon + - --chain=goerli + - --private.api.addr=0.0.0.0:9090 + - --maxpeers=100 + - --datadir=/home/erigon/.local/share/erigon + - --batchSize=512M + - --prune.r.before=11184524 + - --prune=htc + volumes: ["erigon:/home/erigon/.local/share/erigon"] + profiles: ["erigon"] + networks: + - goerli + + erigon-rpcdaemon: + container_name: erigon-rpcdaemon_goerli + image: thorax/erigon:v2022.01.03 + restart: always + command: + - rpcdaemon + - --private.api.addr=erigon:9090 + - --http.addr=0.0.0.0 + - --http.vhosts=* + - --http.corsdomain=* + - --http.api=eth,erigon,web3,net,txpool + - --ws + depends_on: ["erigon"] + profiles: ["erigon"] + networks: + goerli: + aliases: + - eth1-node + + prysm: + container_name: prysm_prater + image: gcr.io/prysmaticlabs/prysm/beacon-chain:v2.0.6 + restart: always + command: + - --prater + - --genesis-state=/data/genesis.ssz + - --datadir=/data + - --rpc-host=0.0.0.0 + - --rpc-port=5052 + - --monitoring-host=0.0.0.0 + - --http-web3provider=$ETH1_ENDPOINT + - --slots-per-archive-point=64 + - --accept-terms-of-use + volumes: ["prysm:/data","../configs/genesis.ssz:/data/gensis.ssz"] + profiles: ["prysm"] + networks: + goerli: + aliases: + - eth2-node + + lighthouse: + container_name: lighthouse_prater + image: sigp/lighthouse:v2.1.2 + restart: always + command: + - lighthouse + - --network + - prater + - beacon + - --http + - --http-address=0.0.0.0 + - --http-port=5052 + - --eth1-endpoints + - $ETH1_ENDPOINT + volumes: ["lighthouse:/root/.lighthouse"] + profiles: ["lighthouse"] + networks: + goerli: + aliases: + - eth2-node diff --git a/deploy/mainnet/.env.example b/deploy/mainnet/.env.example new file mode 100644 index 0000000..ac19d79 --- /dev/null +++ b/deploy/mainnet/.env.example @@ -0,0 +1,81 @@ +########## +# Oracle # +########## +LOG_LEVEL=INFO +ENABLED_NETWORKS=eth_mainnet +ENABLE_HEALTH_SERVER=true +HEALTH_SERVER_PORT=8080 +HEALTH_SERVER_HOST=0.0.0.0 + +# Remove ",/dns/ipfs/tcp/5001/http" if you don't use "ipfs" profile +IPFS_PIN_ENDPOINTS=/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http + +# Optionally pin merkle proofs to the pinata service for redundancy +IPFS_PINATA_API_KEY=<pinata_api_key> +IPFS_PINATA_SECRET_KEY=<pinata_secret_key> + +# Change https://api.thegraph.com to http://graph-node:8000 if running local graph node +ETH_MAINNET_STAKEWISE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/stakewise-mainnet +ETH_MAINNET_ETHEREUM_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/ethereum-mainnet +ETH_MAINNET_UNISWAP_V3_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-mainnet +ETH_MAINNET_RARI_FUSE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/stakewise/rari-fuse-mainnet + +# Ethereum private key +# NB! You must use a different private key for every network +ETH_MAINNET_ORACLE_PRIVATE_KEY=0x<private_key> + +# ETH2 (consensus) client endpoint +# Change if running an external ETH2 node +ETH_MAINNET_ETH2_ENDPOINT=http://eth2-node:5052 + +# AWS bucket to publish oracle votes to +ETH_MAINNET_AWS_ACCESS_KEY_ID=<access_id> +ETH_MAINNET_AWS_SECRET_ACCESS_KEY=<secret_key> +ETH_MAINNET_AWS_BUCKET_NAME=oracle-votes-mainnet +ETH_MAINNET_AWS_REGION=eu-central-1 + +########## +# Keeper # +########## +# Change if running an external ETH1 node +ETH_MAINNET_KEEPER_ETH1_ENDPOINT=http://eth1-node:8545 +# Use https://eth-converter.com/ to calculate +ETH_MAINNET_KEEPER_MIN_BALANCE_WEI=100000000000000000 +ETH_MAINNET_KEEPER_MAX_FEE_PER_GAS_GWEI=150 + +######## +# IPFS # +######## +IPFS_URL=http://ipfs:5001 +IPFS_PROFILE=server +IPFS_FD_MAX=8192 + +############## +# Graph Node # +############## +GRAPH_LOG=info +GRAPH_NODE_URL=http://graph-node:8020 +# Change if running remote IPFS node +ipfs=ipfs:5001 +# Change if running an external ETH1 node +# NB! If syncing graph node from scratch archive node must be used. +# It can be switched to fast-sync node once fully synced. +ethereum=mainnet:http://eth1-node:8545 +# Postgres DB settings for graph node +postgres_host=postgres +postgres_user=graph +postgres_pass=strong-password +postgres_db=graph-node + +############ +# Postgres # +############ +# postgres is used by local graph node +POSTGRES_DB=graph-node +POSTGRES_USER=graph +POSTGRES_PASSWORD=strong-password + +############# +# ETH2 NODE # +############# +ETH1_ENDPOINT=http://eth1-node:8545 diff --git a/deploy/docker-compose.yml b/deploy/mainnet/docker-compose.yml similarity index 69% rename from deploy/docker-compose.yml rename to deploy/mainnet/docker-compose.yml index 0efdba1..6fe464f 100644 --- a/deploy/docker-compose.yml +++ b/deploy/mainnet/docker-compose.yml @@ -18,65 +18,81 @@ volumes: lighthouse: driver: local +networks: + mainnet: + name: mainnet + driver: bridge + services: oracle: - container_name: oracle - image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.1.2 + container_name: oracle_mainnet + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.2.0 restart: always entrypoint: ["python"] command: ["oracle/oracle/main.py"] env_file: [".env"] profiles: ["oracle"] + networks: + - mainnet keeper: - container_name: keeper - image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.1.2 + container_name: keeper_mainnet + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.2.0 restart: always entrypoint: ["python"] command: ["oracle/keeper/main.py"] env_file: [".env"] profiles: ["keeper"] + networks: + - mainnet prometheus: - container_name: prometheus + container_name: prometheus_mainnet image: bitnami/prometheus:2 restart: always env_file: [".env"] volumes: - prometheus:/opt/bitnami/prometheus/data - - ./configs/prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml - - ./configs/rules.yml:/opt/bitnami/prometheus/conf/rules.yml + - ../configs/prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml + - ../configs/rules.yml:/opt/bitnami/prometheus/conf/rules.yml + networks: + - mainnet alertmanager: - container_name: alertmanager + container_name: alertmanager_mainnet image: bitnami/alertmanager:0 restart: always - ports: ["127.0.0.1:9093:9093"] env_file: [".env"] volumes: - alertmanager:/opt/bitnami/alertmanager/data - - ./configs/alertmanager.yml:/opt/bitnami/alertmanager/conf/config.yml + - ../configs/alertmanager.yml:/opt/bitnami/alertmanager/conf/config.yml depends_on: ["prometheus"] + networks: + - mainnet graph-node: - container_name: graph-node + container_name: graph-node_mainnet image: graphprotocol/graph-node:v0.25.0 restart: always env_file: [".env"] depends_on: ["postgres","ipfs"] profiles: ["graph"] + networks: + - mainnet postgres: - container_name: postgres + container_name: postgres_mainnet image: postgres:14-alpine restart: always command: ["postgres", "-cshared_preload_libraries=pg_stat_statements"] env_file: [".env"] volumes: ["postgres:/var/lib/postgresql/data"] profiles: ["graph"] + networks: + - mainnet subgraphs: - container_name: subgraphs + container_name: subgraphs_mainnet image: europe-west4-docker.pkg.dev/stakewiselabs/public/subgraphs:v1.0.8 command: > /bin/sh -c "until nc -vz graph-node 8020; do echo 'Waiting graph-node'; sleep 2; done @@ -87,9 +103,11 @@ services: restart: "no" depends_on: ["graph-node","ipfs"] profiles: ["graph"] + networks: + - mainnet ipfs: - container_name: ipfs + container_name: ipfs_mainnet image: ipfs/go-ipfs:v0.10.0 restart: always env_file: [".env"] @@ -98,10 +116,12 @@ services: soft: 8192 hard: 8192 volumes: ["ipfs:/data/ipfs","./configs/ipfs-entrypoint.sh:/usr/local/bin/start_ipfs"] - profiles: ["oracle"] + profiles: ["ipfs"] + networks: + - mainnet geth: - container_name: geth + container_name: geth_mainnet image: ethereum/client-go:v1.10.15 restart: always command: @@ -115,9 +135,11 @@ services: - --ipcdisable volumes: ["geth:/data"] profiles: ["geth"] + networks: + - mainnet erigon: - container_name: erigon + container_name: erigon_mainnet image: thorax/erigon:v2022.01.03 restart: always command: @@ -130,9 +152,13 @@ services: - --prune=htc volumes: ["erigon:/home/erigon/.local/share/erigon"] profiles: ["erigon"] + networks: + mainnet: + aliases: + - eth1-node erigon-rpcdaemon: - container_name: erigon-rpcdaemon + container_name: erigon-rpcdaemon_mainnet image: thorax/erigon:v2022.01.03 restart: always command: @@ -145,24 +171,33 @@ services: - --ws depends_on: ["erigon"] profiles: ["erigon"] + networks: + mainnet: + aliases: + - eth1-node prysm: - container_name: prysm - image: gcr.io/prysmaticlabs/prysm/beacon-chain:v2.0.5 + container_name: prysm_mainnet + image: gcr.io/prysmaticlabs/prysm/beacon-chain:v2.0.6 restart: always command: - --datadir=/data - --rpc-host=0.0.0.0 + - --rpc-port=5052 - --monitoring-host=0.0.0.0 - - --http-web3provider=http://geth:8545 + - --http-web3provider=$ETH1_ENDPOINT - --slots-per-archive-point=64 - --accept-terms-of-use volumes: ["prysm:/data"] profiles: ["prysm"] + networks: + mainnet: + aliases: + - eth2-node lighthouse: - container_name: lighthouse - image: sigp/lighthouse:v2.1.1 + container_name: lighthouse_mainnet + image: sigp/lighthouse:v2.1.2 restart: always command: - lighthouse @@ -170,9 +205,13 @@ services: - mainnet - beacon - --http - - --http-address - - 0.0.0.0 + - --http-address=0.0.0.0 + - --http-port=5052 - --eth1-endpoints - - http://geth:8545 + - $ETH1_ENDPOINT volumes: ["lighthouse:/root/.lighthouse"] profiles: ["lighthouse"] + networks: + mainnet: + aliases: + - eth2-node diff --git a/oracle/common/__init__.py b/oracle/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oracle/common/settings.py b/oracle/common/settings.py deleted file mode 100644 index dbf8b2e..0000000 --- a/oracle/common/settings.py +++ /dev/null @@ -1,36 +0,0 @@ -from decouple import Choices, config - -LOG_LEVEL = config("LOG_LEVEL", default="INFO") - -REWARD_VOTE_FILENAME = "reward-vote.json" -DISTRIBUTOR_VOTE_FILENAME = "distributor-vote.json" -VALIDATOR_VOTE_FILENAME = "validator-vote.json" -TEST_VOTE_FILENAME = "test-vote.json" - -# supported networks -MAINNET = "mainnet" -GOERLI = "goerli" -GNOSIS = "gnosis" -# TODO: enable gnosis once supported -NETWORK = config( - "NETWORK", - default=MAINNET, - cast=Choices([MAINNET, GOERLI], cast=lambda net: net.lower()), -) - -if NETWORK == MAINNET: - AWS_S3_BUCKET_NAME = config("AWS_S3_BUCKET_NAME", default="oracle-votes-mainnet") -elif NETWORK == GNOSIS: - AWS_S3_BUCKET_NAME = config("AWS_S3_BUCKET_NAME", default="oracle-votes-gnosis") -elif NETWORK == GOERLI: - AWS_S3_BUCKET_NAME = config("AWS_S3_BUCKET_NAME", default="oracle-votes-goerli") - -AWS_S3_REGION = config("AWS_S3_REGION", default="eu-central-1") - -# health server settings -ENABLE_HEALTH_SERVER = config("ENABLE_HEALTH_SERVER", default=False, cast=bool) -HEALTH_SERVER_PORT = config("HEALTH_SERVER_PORT", default=8080, cast=int) -HEALTH_SERVER_HOST = config("HEALTH_SERVER_HOST", default="127.0.0.1", cast=str) - -# required confirmation blocks -CONFIRMATION_BLOCKS: int = config("CONFIRMATION_BLOCKS", default=15, cast=int) diff --git a/oracle/common/health_server.py b/oracle/health_server.py similarity index 86% rename from oracle/common/health_server.py rename to oracle/health_server.py index 8251cdb..e4f7a2e 100644 --- a/oracle/common/health_server.py +++ b/oracle/health_server.py @@ -2,7 +2,7 @@ from aiohttp import web -from oracle.common.settings import HEALTH_SERVER_HOST, HEALTH_SERVER_PORT +from oracle.settings import HEALTH_SERVER_HOST, HEALTH_SERVER_PORT def start_health_server(runner): diff --git a/oracle/keeper/clients.py b/oracle/keeper/clients.py index 0f3bf93..20934dd 100644 --- a/oracle/keeper/clients.py +++ b/oracle/keeper/clients.py @@ -1,41 +1,49 @@ import logging +from typing import Dict -import boto3 from web3 import Web3 from web3.middleware import construct_sign_and_send_raw_middleware, geth_poa_middleware -from oracle.keeper.settings import GOERLI, NETWORK, ORACLE_PRIVATE_KEY, WEB3_ENDPOINT +from oracle.networks import NETWORKS +from oracle.settings import ENABLED_NETWORKS logger = logging.getLogger(__name__) -def get_web3_client() -> Web3: +def get_web3_client(network: str) -> Web3: """Returns instance of the Web3 client.""" + network_config = NETWORKS[network] + endpoint = network_config["KEEPER_ETH1_ENDPOINT"] # Prefer WS over HTTP - if WEB3_ENDPOINT.startswith("ws"): - w3 = Web3(Web3.WebsocketProvider(WEB3_ENDPOINT, websocket_timeout=60)) - logger.warning(f"Web3 websocket endpoint={WEB3_ENDPOINT}") - elif WEB3_ENDPOINT.startswith("http"): - w3 = Web3(Web3.HTTPProvider(WEB3_ENDPOINT)) - logger.warning(f"Web3 HTTP endpoint={WEB3_ENDPOINT}") + if endpoint.startswith("ws"): + w3 = Web3(Web3.WebsocketProvider(endpoint, websocket_timeout=60)) + logger.warning(f"[{network}] Web3 websocket endpoint={endpoint}") + elif endpoint.startswith("http"): + w3 = Web3(Web3.HTTPProvider(endpoint)) + logger.warning(f"[{network}] Web3 HTTP endpoint={endpoint}") else: - w3 = Web3(Web3.IPCProvider(WEB3_ENDPOINT)) - logger.warning(f"Web3 HTTP endpoint={WEB3_ENDPOINT}") + w3 = Web3(Web3.IPCProvider(endpoint)) + logger.warning(f"[{network}] Web3 HTTP endpoint={endpoint}") - if NETWORK == GOERLI: + if network_config["IS_POA"]: w3.middleware_onion.inject(geth_poa_middleware, layer=0) - logger.warning("Injected POA middleware") + logger.warning(f"[{network}] Injected POA middleware") - account = w3.eth.account.from_key(ORACLE_PRIVATE_KEY) + account = w3.eth.account.from_key(network_config["ORACLE_PRIVATE_KEY"]) w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account)) - logger.warning("Injected middleware for capturing transactions and sending as raw") + logger.warning( + f"[{network}] Injected middleware for capturing transactions and sending as raw" + ) w3.eth.default_account = account.address - logger.info(f"Configured default account {w3.eth.default_account}") + logger.info(f"[{network}] Configured default account {w3.eth.default_account}") return w3 -s3_client = boto3.client("s3") -web3_client = get_web3_client() +def get_web3_clients() -> Dict[str, Web3]: + web3_clients = {} + for network in ENABLED_NETWORKS: + web3_clients[network] = get_web3_client(network) + return web3_clients diff --git a/oracle/keeper/contracts.py b/oracle/keeper/contracts.py index 8f6ed60..2749d47 100644 --- a/oracle/keeper/contracts.py +++ b/oracle/keeper/contracts.py @@ -1,13 +1,16 @@ +from typing import Dict + +from web3 import Web3 from web3.contract import Contract -from oracle.keeper.clients import web3_client -from oracle.keeper.settings import MULTICALL_CONTRACT_ADDRESS, ORACLES_CONTRACT_ADDRESS +from oracle.networks import NETWORKS +from oracle.settings import ENABLED_NETWORKS -def get_multicall_contract() -> Contract: +def get_multicall_contract(w3_client: Web3, network: str) -> Contract: """:returns instance of `Multicall` contract.""" - return web3_client.eth.contract( - address=MULTICALL_CONTRACT_ADDRESS, + return w3_client.eth.contract( + address=NETWORKS[network]["MULTICALL_CONTRACT_ADDRESS"], abi=[ { "constant": False, @@ -34,10 +37,10 @@ def get_multicall_contract() -> Contract: ) -def get_oracles_contract() -> Contract: +def get_oracles_contract(web3_client: Web3, network: str) -> Contract: """:returns instance of `Oracles` contract.""" return web3_client.eth.contract( - address=ORACLES_CONTRACT_ADDRESS, + address=NETWORKS[network]["ORACLES_CONTRACT_ADDRESS"], abi=[ { "inputs": [], @@ -193,5 +196,19 @@ def get_oracles_contract() -> Contract: ) -multicall_contract = get_multicall_contract() -oracles_contract = get_oracles_contract() +def get_multicall_contracts(web3_clients: Dict[str, Web3]) -> Dict[str, Contract]: + multicall_contracts = {} + for network in ENABLED_NETWORKS: + web3_client = web3_clients[network] + multicall_contracts[network] = get_multicall_contract(web3_client, network) + + return multicall_contracts + + +def get_oracles_contracts(web3_clients: Dict[str, Web3]) -> Dict[str, Contract]: + oracles_contracts = {} + for network in ENABLED_NETWORKS: + web3_client = web3_clients[network] + oracles_contracts[network] = get_oracles_contract(web3_client, network) + + return oracles_contracts diff --git a/oracle/keeper/health_server.py b/oracle/keeper/health_server.py index 0644074..1431b15 100644 --- a/oracle/keeper/health_server.py +++ b/oracle/keeper/health_server.py @@ -1,8 +1,9 @@ from aiohttp import web -from oracle.keeper.contracts import get_oracles_contract -from oracle.keeper.settings import KEEPER_MIN_BALANCE_WEI +from oracle.keeper.clients import get_web3_clients +from oracle.keeper.contracts import get_multicall_contracts, get_oracles_contracts from oracle.keeper.utils import get_keeper_params, get_oracles_votes +from oracle.networks import NETWORKS keeper_routes = web.RouteTableDef() @@ -10,27 +11,33 @@ @keeper_routes.get("/") async def health(request): try: - oracles = get_oracles_contract() - oracle = oracles.web3.eth.default_account - - # Check ETH1 node connection and oracle is part of the set - assert oracles.functions.isOracle(oracles.web3.eth.default_account).call() - - # Check oracle has enough balance - balance = oracles.web3.eth.get_balance(oracle) - assert balance > KEEPER_MIN_BALANCE_WEI - - # Can fetch oracle votes and is not paused - params = get_keeper_params() - if params.paused: - return web.Response(text="keeper 0") - - # Can resolve and fetch latest votes of the oracles - get_oracles_votes( - rewards_nonce=params.rewards_nonce, - validators_nonce=params.validators_nonce, - oracles=params.oracles, - ) + web3_clients = get_web3_clients() + oracles_contracts = get_oracles_contracts(web3_clients) + multicall_contracts = get_multicall_contracts(web3_clients) + for network, web3_client in web3_clients.items(): + oracles_contract = oracles_contracts[network] + multicall_contract = multicall_contracts[network] + + # Check ETH1 node connection and oracle is part of the set + oracle_account = web3_client.eth.default_account + assert oracles_contract.functions.isOracle(oracle_account).call() + + # Check oracle has enough balance + balance = web3_client.eth.get_balance(oracle_account) + assert balance > NETWORKS[network]["KEEPER_MIN_BALANCE"] + + # Can fetch oracle votes and is not paused + params = get_keeper_params(oracles_contract, multicall_contract) + if params.paused: + return web.Response(text="keeper 0") + + # Can resolve and fetch recent votes of the oracles + get_oracles_votes( + network=network, + rewards_nonce=params.rewards_nonce, + validators_nonce=params.validators_nonce, + oracles=params.oracles, + ) return web.Response(text="keeper 1") except: # noqa: E722 diff --git a/oracle/keeper/main.py b/oracle/keeper/main.py index bfe4c8f..a24063d 100644 --- a/oracle/keeper/main.py +++ b/oracle/keeper/main.py @@ -1,14 +1,20 @@ import logging -import signal import threading import time -from typing import Any -from oracle.common.health_server import create_health_server_runner, start_health_server -from oracle.common.settings import ENABLE_HEALTH_SERVER, LOG_LEVEL +from oracle.health_server import create_health_server_runner, start_health_server +from oracle.keeper.clients import get_web3_clients +from oracle.keeper.contracts import get_multicall_contracts, get_oracles_contracts from oracle.keeper.health_server import keeper_routes -from oracle.keeper.settings import KEEPER_PROCESS_INTERVAL from oracle.keeper.utils import get_keeper_params, submit_votes +from oracle.settings import ( + ENABLE_HEALTH_SERVER, + ENABLED_NETWORKS, + HEALTH_SERVER_PORT, + KEEPER_PROCESS_INTERVAL, + LOG_LEVEL, +) +from oracle.utils import InterruptHandler logging.basicConfig( format="%(asctime)s %(levelname)-8s %(message)s", @@ -20,39 +26,29 @@ logger = logging.getLogger(__name__) -class InterruptHandler: - """ - Tracks SIGINT and SIGTERM signals. - https://stackoverflow.com/a/31464349 - """ - - exit = False - - def __init__(self) -> None: - signal.signal(signal.SIGINT, self.exit_gracefully) - signal.signal(signal.SIGTERM, self.exit_gracefully) - - # noinspection PyUnusedLocal - def exit_gracefully(self, signum: int, frame: Any) -> None: - logger.info(f"Received interrupt signal {signum}, exiting...") - self.exit = True - - def main() -> None: # wait for interrupt interrupt_handler = InterruptHandler() + web3_clients = get_web3_clients() + multicall_contracts = get_multicall_contracts(web3_clients) + oracles_contracts = get_oracles_contracts(web3_clients) while not interrupt_handler.exit: # Fetch current nonces of the validators, rewards and the total number of oracles - params = get_keeper_params() - if params.paused: - time.sleep(KEEPER_PROCESS_INTERVAL) - continue + for network in ENABLED_NETWORKS: + web3_client = web3_clients[network] + multicall_contract = multicall_contracts[network] + oracles_contract = oracles_contracts[network] - # If nonces match the current for the majority, submit the transactions - submit_votes(params) + params = get_keeper_params(oracles_contract, multicall_contract) + if params.paused: + time.sleep(KEEPER_PROCESS_INTERVAL) + continue - time.sleep(KEEPER_PROCESS_INTERVAL) + # If nonces match the current for the majority, submit the transactions + submit_votes(network, web3_client, oracles_contract, params) + + time.sleep(KEEPER_PROCESS_INTERVAL) if __name__ == "__main__": @@ -62,5 +58,8 @@ def main() -> None: args=(create_health_server_runner(keeper_routes),), daemon=True, ) + logger.info( + f"Starting monitoring server at http://{ENABLE_HEALTH_SERVER}:{HEALTH_SERVER_PORT}" + ) t.start() main() diff --git a/oracle/keeper/settings.py b/oracle/keeper/settings.py deleted file mode 100644 index 73aafe3..0000000 --- a/oracle/keeper/settings.py +++ /dev/null @@ -1,35 +0,0 @@ -from decouple import config -from web3 import Web3 - -from oracle.common.settings import GOERLI, MAINNET, NETWORK - -WEB3_ENDPOINT = config("WEB3_ENDPOINT") - -ORACLE_PRIVATE_KEY = config("ORACLE_PRIVATE_KEY") - -KEEPER_PROCESS_INTERVAL = config("KEEPER_PROCESS_INTERVAL", default=10, cast=int) - -KEEPER_MIN_BALANCE_WEI = config( - "KEEPER_MIN_BALANCE_WEI", default=Web3.toWei(0.1, "ether"), cast=int -) - -TRANSACTION_TIMEOUT = config("TRANSACTION_TIMEOUT", default=900, cast=int) - -MAX_FEE_PER_GAS = config( - "MAX_FEE_PER_GAS_GWEI", default=150, cast=lambda x: Web3.toWei(x, "gwei") -) - -if NETWORK == MAINNET: - ORACLES_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x8a887282E67ff41d36C0b7537eAB035291461AcD" - ) - MULTICALL_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441" - ) -elif NETWORK == GOERLI: - ORACLES_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x531b9D9cb268E88D53A87890699bbe31326A6f08" - ) - MULTICALL_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e" - ) diff --git a/oracle/keeper/utils.py b/oracle/keeper/utils.py index c9c70ee..d92f285 100644 --- a/oracle/keeper/utils.py +++ b/oracle/keeper/utils.py @@ -4,41 +4,36 @@ from typing import List import backoff -import boto3 import requests from eth_account.messages import encode_defunct from eth_typing import BlockNumber, ChecksumAddress, HexStr from hexbytes import HexBytes from web3 import Web3 -from web3.contract import ContractFunction +from web3.contract import Contract, ContractFunction from web3.types import TxParams -from oracle.common.settings import ( - AWS_S3_BUCKET_NAME, - AWS_S3_REGION, +from oracle.keeper.typings import OraclesVotes, Parameters +from oracle.networks import NETWORKS +from oracle.oracle.distributor.types import DistributorVote +from oracle.oracle.rewards.types import RewardVote +from oracle.oracle.validators.types import ValidatorVote +from oracle.settings import ( CONFIRMATION_BLOCKS, DISTRIBUTOR_VOTE_FILENAME, REWARD_VOTE_FILENAME, + TRANSACTION_TIMEOUT, VALIDATOR_VOTE_FILENAME, ) -from oracle.keeper.clients import web3_client -from oracle.keeper.contracts import multicall_contract, oracles_contract -from oracle.keeper.settings import MAX_FEE_PER_GAS, TRANSACTION_TIMEOUT -from oracle.keeper.typings import OraclesVotes, Parameters -from oracle.oracle.distributor.types import DistributorVote -from oracle.oracle.rewards.types import RewardVote -from oracle.oracle.validators.types import ValidatorVote logger = logging.getLogger(__name__) -s3_client = boto3.client( - "s3", -) ORACLE_ROLE = Web3.solidityKeccak(["string"], ["ORACLE_ROLE"]) @backoff.on_exception(backoff.expo, Exception, max_time=900) -def get_keeper_params() -> Parameters: +def get_keeper_params( + oracles_contract: Contract, multicall_contract: Contract +) -> Parameters: """Returns keeper params for checking whether to submit the votes.""" calls = [ { @@ -90,7 +85,7 @@ def get_keeper_params() -> Parameters: def validate_vote_signature( - encoded_data: bytes, account: ChecksumAddress, signature: HexStr + web3_client: Web3, encoded_data: bytes, account: ChecksumAddress, signature: HexStr ) -> bool: """Checks whether vote was signed by specific Ethereum account.""" try: @@ -108,7 +103,9 @@ def validate_vote_signature( return True -def check_reward_vote(vote: RewardVote, oracle: ChecksumAddress) -> bool: +def check_reward_vote( + web3_client: Web3, vote: RewardVote, oracle: ChecksumAddress +) -> bool: """Checks whether oracle's reward vote is correct.""" try: encoded_data: bytes = web3_client.codec.encode_abi( @@ -119,24 +116,32 @@ def check_reward_vote(vote: RewardVote, oracle: ChecksumAddress) -> bool: int(vote["total_rewards"]), ], ) - return validate_vote_signature(encoded_data, oracle, vote["signature"]) + return validate_vote_signature( + web3_client, encoded_data, oracle, vote["signature"] + ) except: # noqa: E722 return False -def check_distributor_vote(vote: DistributorVote, oracle: ChecksumAddress) -> bool: +def check_distributor_vote( + web3_client: Web3, vote: DistributorVote, oracle: ChecksumAddress +) -> bool: """Checks whether oracle's distributor vote is correct.""" try: encoded_data: bytes = web3_client.codec.encode_abi( ["uint256", "string", "bytes32"], [int(vote["nonce"]), vote["merkle_proofs"], vote["merkle_root"]], ) - return validate_vote_signature(encoded_data, oracle, vote["signature"]) + return validate_vote_signature( + web3_client, encoded_data, oracle, vote["signature"] + ) except: # noqa: E722 return False -def check_validator_vote(vote: ValidatorVote, oracle: ChecksumAddress) -> bool: +def check_validator_vote( + web3_client: Web3, vote: ValidatorVote, oracle: ChecksumAddress +) -> bool: """Checks whether oracle's validator vote is correct.""" try: encoded_data: bytes = web3_client.codec.encode_abi( @@ -148,16 +153,24 @@ def check_validator_vote(vote: ValidatorVote, oracle: ChecksumAddress) -> bool: vote["validators_deposit_root"], ], ) - return validate_vote_signature(encoded_data, oracle, vote["signature"]) + return validate_vote_signature( + web3_client, encoded_data, oracle, vote["signature"] + ) except: # noqa: E722 return False def get_oracles_votes( - rewards_nonce: int, validators_nonce: int, oracles: List[ChecksumAddress] + network: str, + rewards_nonce: int, + validators_nonce: int, + oracles: List[ChecksumAddress], ) -> OraclesVotes: """Fetches oracle votes that match current nonces.""" votes = OraclesVotes(rewards=[], distributor=[], validator=[]) + network_config = NETWORKS[network] + aws_bucket_name = network_config["AWS_BUCKET_NAME"] + aws_region = network_config["AWS_REGION"] for oracle in oracles: for arr, filename, correct_nonce, vote_checker in [ @@ -179,7 +192,7 @@ def get_oracles_votes( bucket_key = f"{oracle}/{filename}" try: response = requests.get( - f"https://{AWS_S3_BUCKET_NAME}.s3.{AWS_S3_REGION}.amazonaws.com/{bucket_key}" + f"https://{aws_bucket_name}.s3.{aws_region}.amazonaws.com/{bucket_key}" ) response.raise_for_status() vote = response.json() @@ -187,7 +200,7 @@ def get_oracles_votes( continue if not vote_checker(vote, oracle): logger.warning( - f"Oracle {oracle} has submitted incorrect vote at {bucket_key}" + f"[{network}] Oracle {oracle} has submitted incorrect vote at {bucket_key}" ) continue @@ -203,7 +216,7 @@ def can_submit(signatures_count: int, total_oracles: int) -> bool: return signatures_count * 3 > total_oracles * 2 -def wait_for_transaction(tx_hash: HexBytes) -> None: +def wait_for_transaction(network: str, web3_client: Web3, tx_hash: HexBytes) -> None: """Waits for transaction to be confirmed.""" receipt = web3_client.eth.wait_for_transaction_receipt( transaction_hash=tx_hash, timeout=TRANSACTION_TIMEOUT, poll_latency=5 @@ -212,7 +225,7 @@ def wait_for_transaction(tx_hash: HexBytes) -> None: current_block: BlockNumber = web3_client.eth.block_number while confirmation_block > current_block: logger.info( - f"Waiting for {confirmation_block - current_block} confirmation blocks..." + f"[{network}] Waiting for {confirmation_block - current_block} confirmation blocks..." ) time.sleep(15) @@ -221,10 +234,12 @@ def wait_for_transaction(tx_hash: HexBytes) -> None: current_block = web3_client.eth.block_number -def get_transaction_params() -> TxParams: +def get_transaction_params(network: str, web3_client: Web3) -> TxParams: + network_config = NETWORKS[network] + max_fee_per_gas = network_config["KEEPER_MAX_FEE_PER_GAS"] account_nonce = web3_client.eth.getTransactionCount(web3_client.eth.default_account) latest_block = web3_client.eth.get_block("latest") - max_priority_fee = min(web3_client.eth.max_priority_fee, MAX_FEE_PER_GAS) + max_priority_fee = min(web3_client.eth.max_priority_fee, max_fee_per_gas) base_fee = latest_block["baseFeePerGas"] priority_fee = int(str(max_priority_fee), 16) @@ -233,12 +248,14 @@ def get_transaction_params() -> TxParams: return TxParams( nonce=account_nonce, maxPriorityFeePerGas=max_priority_fee, - maxFeePerGas=hex(min(max_fee_per_gas, MAX_FEE_PER_GAS)), + maxFeePerGas=hex(min(max_fee_per_gas, max_fee_per_gas)), ) -def submit_update(function_call: ContractFunction) -> None: - tx_params = get_transaction_params() +def submit_update( + network: str, web3_client: Web3, function_call: ContractFunction +) -> None: + tx_params = get_transaction_params(network, web3_client) estimated_gas = function_call.estimateGas(tx_params) # add 10% margin to the estimated gas @@ -246,15 +263,18 @@ def submit_update(function_call: ContractFunction) -> None: # execute transaction tx_hash = function_call.transact(tx_params) - logger.info(f"Submitted transaction: {Web3.toHex(tx_hash)}") - wait_for_transaction(tx_hash) + logger.info(f"[{network}] Submitted transaction: {Web3.toHex(tx_hash)}") + wait_for_transaction(network, web3_client, tx_hash) @backoff.on_exception(backoff.expo, Exception, max_time=900) -def submit_votes(params: Parameters) -> None: +def submit_votes( + network: str, web3_client: Web3, oracles_contract: Contract, params: Parameters +) -> None: """Submits aggregated votes in case they have majority.""" # resolve and fetch the latest votes of the oracles for validators and rewards votes = get_oracles_votes( + network=network, rewards_nonce=params.rewards_nonce, validators_nonce=params.validators_nonce, oracles=params.oracles, @@ -283,16 +303,18 @@ def submit_votes(params: Parameters) -> None: i += 1 logger.info( - f"Submitting total rewards update:" + f"[{network}] Submitting total rewards update:" f' rewards={Web3.fromWei(int(total_rewards), "ether")},' f" activated validators={activated_validators}" ) submit_update( + network, + web3_client, oracles_contract.functions.submitRewards( int(total_rewards), int(activated_validators), signatures ), ) - logger.info("Total rewards has been successfully updated") + logger.info(f"[{network}] Total rewards has been successfully updated") counter = Counter( [(vote["merkle_root"], vote["merkle_proofs"]) for vote in votes.distributor] @@ -312,14 +334,16 @@ def submit_votes(params: Parameters) -> None: i += 1 logger.info( - f"Submitting distributor update: merkle root={merkle_root}, merkle proofs={merkle_proofs}" + f"[{network}] Submitting distributor update: merkle root={merkle_root}, merkle proofs={merkle_proofs}" ) submit_update( + network, + web3_client, oracles_contract.functions.submitMerkleRoot( merkle_root, merkle_proofs, signatures ), ) - logger.info("Merkle Distributor has been successfully updated") + logger.info(f"[{network}] Merkle Distributor has been successfully updated") counter = Counter( [ @@ -353,12 +377,14 @@ def submit_votes(params: Parameters) -> None: ) ) logger.info( - f"Submitting validator registration: " + f"[{network}] Submitting validator registration: " f"operator={operator}, " f"public key={public_key}, " f"validator deposit root={validators_deposit_root}" ) submit_update( + network, + web3_client, oracles_contract.functions.registerValidator( dict( operator=validator_vote["operator"], @@ -372,4 +398,6 @@ def submit_votes(params: Parameters) -> None: signatures, ), ) - logger.info("Validator registration has been successfully submitted") + logger.info( + f"[{network}] Validator registration has been successfully submitted" + ) diff --git a/oracle/networks.py b/oracle/networks.py new file mode 100644 index 0000000..5579796 --- /dev/null +++ b/oracle/networks.py @@ -0,0 +1,223 @@ +from datetime import timedelta + +from decouple import config +from eth_typing import HexStr +from web3 import Web3 + +ETHEREUM_MAINNET = "eth_mainnet" +ETHEREUM_GOERLI = "eth_goerli" +GNOSIS_CHAIN = "gnosis" + +ETHEREUM_MAINNET_UPPER = ETHEREUM_MAINNET.upper() +ETHEREUM_GOERLI_UPPER = ETHEREUM_GOERLI.upper() +GNOSIS_CHAIN_UPPER = GNOSIS_CHAIN.upper() + +NETWORKS = { + ETHEREUM_MAINNET: dict( + STAKEWISE_SUBGRAPH_URL=config( + f"{ETHEREUM_MAINNET_UPPER}_STAKEWISE_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/stakewise-mainnet", + ), + ETHEREUM_SUBGRAPH_URL=config( + f"{ETHEREUM_MAINNET_UPPER}_ETHEREUM_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/ethereum-mainnet", + ), + UNISWAP_V3_SUBGRAPH_URL=config( + f"{ETHEREUM_MAINNET_UPPER}_UNISWAP_V3_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-mainnet", + ), + RARI_FUSE_SUBGRAPH_URL=config( + f"{ETHEREUM_MAINNET_UPPER}_RARI_FUSE_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/rari-fuse-mainnet", + ), + ETH2_ENDPOINT=config(f"{ETHEREUM_MAINNET_UPPER}_ETH2_ENDPOINT", default=""), + SLOTS_PER_EPOCH=32, + SECONDS_PER_SLOT=12, + ORACLES_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x8a887282E67ff41d36C0b7537eAB035291461AcD" + ), + MULTICALL_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441" + ), + SWISE_TOKEN_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2" + ), + REWARD_TOKEN_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x20BC832ca081b91433ff6c17f85701B6e92486c5" + ), + STAKED_TOKEN_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0xFe2e637202056d30016725477c5da089Ab0A043A" + ), + DISTRIBUTOR_FALLBACK_ADDRESS=Web3.toChecksumAddress( + "0x144a98cb1CdBb23610501fE6108858D9B7D24934" + ), + RARI_FUSE_POOL_ADDRESSES=[ + Web3.toChecksumAddress("0x18F49849D20Bc04059FE9d775df9a38Cd1f5eC9F"), + Web3.toChecksumAddress("0x83d534Ab1d4002249B0E6d22410b62CF31978Ca2"), + ], + WITHDRAWAL_CREDENTIALS=HexStr( + "0x0100000000000000000000002296e122c1a20fca3cac3371357bdad3be0df079" + ), + ORACLE_PRIVATE_KEY=config( + f"{ETHEREUM_MAINNET_UPPER}_ORACLE_PRIVATE_KEY", default="" + ), + AWS_BUCKET_NAME=config( + f"{ETHEREUM_MAINNET_UPPER}_AWS_BUCKET_NAME", default="oracle-votes-mainnet" + ), + AWS_REGION=config( + f"{ETHEREUM_MAINNET_UPPER}_AWS_REGION", default="eu-central-1" + ), + AWS_ACCESS_KEY_ID=config( + f"{ETHEREUM_MAINNET_UPPER}_AWS_ACCESS_KEY_ID", default="" + ), + AWS_SECRET_ACCESS_KEY=config( + f"{ETHEREUM_MAINNET_UPPER}_AWS_SECRET_ACCESS_KEY", default="" + ), + KEEPER_ETH1_ENDPOINT=config( + f"{ETHEREUM_MAINNET_UPPER}_KEEPER_ETH1_ENDPOINT", default="" + ), + KEEPER_MIN_BALANCE=config( + f"{ETHEREUM_MAINNET_UPPER}_KEEPER_MIN_BALANCE_WEI", + default=Web3.toWei(0.1, "ether"), + cast=int, + ), + KEEPER_MAX_FEE_PER_GAS=config( + f"{ETHEREUM_MAINNET_UPPER}_KEEPER_MAX_FEE_PER_GAS_GWEI", + default=150, + cast=lambda x: Web3.toWei(x, "gwei"), + ), + SYNC_PERIOD=timedelta(days=1), + IS_POA=False, + DEPOSIT_TOKEN_SYMBOL="ETH", + ), + ETHEREUM_GOERLI: dict( + STAKEWISE_SUBGRAPH_URL=config( + f"{ETHEREUM_GOERLI_UPPER}_STAKEWISE_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/stakewise-goerli", + ), + ETHEREUM_SUBGRAPH_URL=config( + f"{ETHEREUM_GOERLI_UPPER}_ETHEREUM_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/ethereum-goerli", + ), + UNISWAP_V3_SUBGRAPH_URL=config( + f"{ETHEREUM_GOERLI_UPPER}_UNISWAP_V3_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-goerli", + ), + # TODO: update once rari fuse pools is deployed to goerli chain + RARI_FUSE_SUBGRAPH_URL=config( + f"{ETHEREUM_GOERLI_UPPER}_RARI_FUSE_SUBGRAPH_URL", default="" + ), + ETH2_ENDPOINT=config(f"{ETHEREUM_GOERLI_UPPER}_ETH2_ENDPOINT", default=""), + SLOTS_PER_EPOCH=32, + SECONDS_PER_SLOT=12, + ORACLES_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x531b9D9cb268E88D53A87890699bbe31326A6f08" + ), + MULTICALL_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e" + ), + SWISE_TOKEN_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x0e2497aACec2755d831E4AFDEA25B4ef1B823855" + ), + REWARD_TOKEN_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x826f88d423440c305D9096cC1581Ae751eFCAfB0" + ), + STAKED_TOKEN_CONTRACT_ADDRESS=Web3.toChecksumAddress( + "0x221D9812823DBAb0F1fB40b0D294D9875980Ac19" + ), + DISTRIBUTOR_FALLBACK_ADDRESS=Web3.toChecksumAddress( + "0x1867c96601bc5fE24F685d112314B8F3Fe228D5A" + ), + RARI_FUSE_POOL_ADDRESSES=[], + WITHDRAWAL_CREDENTIALS=HexStr( + "0x010000000000000000000000040f15c6b5bfc5f324ecab5864c38d4e1eef4218" + ), + ORACLE_PRIVATE_KEY=config( + f"{ETHEREUM_GOERLI_UPPER}_ORACLE_PRIVATE_KEY", default="" + ), + AWS_BUCKET_NAME=config( + f"{ETHEREUM_GOERLI_UPPER}_AWS_BUCKET_NAME", default="oracle-votes-goerli" + ), + AWS_REGION=config( + f"{ETHEREUM_GOERLI_UPPER}_AWS_REGION", default="eu-central-1" + ), + AWS_ACCESS_KEY_ID=config( + f"{ETHEREUM_GOERLI_UPPER}_AWS_ACCESS_KEY_ID", default="" + ), + AWS_SECRET_ACCESS_KEY=config( + f"{ETHEREUM_GOERLI_UPPER}_AWS_SECRET_ACCESS_KEY", default="" + ), + KEEPER_ETH1_ENDPOINT=config( + f"{ETHEREUM_GOERLI_UPPER}_KEEPER_ETH1_ENDPOINT", default="" + ), + KEEPER_MIN_BALANCE=config( + f"{ETHEREUM_GOERLI_UPPER}_KEEPER_MIN_BALANCE_WEI", + default=Web3.toWei(0.1, "ether"), + cast=int, + ), + KEEPER_MAX_FEE_PER_GAS=config( + f"{ETHEREUM_GOERLI_UPPER}_KEEPER_MAX_FEE_PER_GAS_GWEI", + default=150, + cast=lambda x: Web3.toWei(x, "gwei"), + ), + SYNC_PERIOD=timedelta(hours=1), + IS_POA=True, + DEPOSIT_TOKEN_SYMBOL="ETH", + ), + GNOSIS_CHAIN: dict( + STAKEWISE_SUBGRAPH_URL=config( + f"{GNOSIS_CHAIN_UPPER}_STAKEWISE_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/stakewise-gnosis", + ), + ETHEREUM_SUBGRAPH_URL=config( + f"{GNOSIS_CHAIN_UPPER}_ETHEREUM_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/ethereum-gnosis", + ), + UNISWAP_V3_SUBGRAPH_URL=config( + f"{GNOSIS_CHAIN_UPPER}_UNISWAP_V3_SUBGRAPH_URL", + default="https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-gnosis", + ), + # TODO: update once rari fuse pools is deployed to gnosis chain + RARI_FUSE_SUBGRAPH_URL=config( + f"{GNOSIS_CHAIN_UPPER}_RARI_FUSE_SUBGRAPH_URL", default="" + ), + ETH2_ENDPOINT=config(f"{GNOSIS_CHAIN_UPPER}_ETH2_ENDPOINT", default=""), + SLOTS_PER_EPOCH=16, + SECONDS_PER_SLOT=5, + ORACLES_CONTRACT_ADDRESS="", + MULTICALL_CONTRACT_ADDRESS="", + SWISE_TOKEN_CONTRACT_ADDRESS="", + REWARD_TOKEN_CONTRACT_ADDRESS="", + STAKED_TOKEN_CONTRACT_ADDRESS="", + DISTRIBUTOR_FALLBACK_ADDRESS="", + RARI_FUSE_POOL_ADDRESSES=[], + WITHDRAWAL_CREDENTIALS="", + ORACLE_PRIVATE_KEY=config( + f"{GNOSIS_CHAIN_UPPER}_ORACLE_PRIVATE_KEY", default="" + ), + AWS_BUCKET_NAME=config( + f"{GNOSIS_CHAIN_UPPER}_AWS_BUCKET_NAME", default="oracle-votes-gnosis" + ), + AWS_REGION=config(f"{GNOSIS_CHAIN_UPPER}_AWS_REGION", default=""), + AWS_ACCESS_KEY_ID=config(f"{GNOSIS_CHAIN_UPPER}_AWS_ACCESS_KEY_ID", default=""), + AWS_SECRET_ACCESS_KEY=config( + f"{GNOSIS_CHAIN_UPPER}_AWS_SECRET_ACCESS_KEY", default="" + ), + KEEPER_ETH1_ENDPOINT=config( + f"{GNOSIS_CHAIN_UPPER}_KEEPER_ETH1_ENDPOINT", default="" + ), + KEEPER_MIN_BALANCE=config( + f"{GNOSIS_CHAIN_UPPER}_KEEPER_MIN_BALANCE_WEI", + default=Web3.toWei(1, "ether"), + cast=int, + ), + KEEPER_MAX_FEE_PER_GAS=config( + f"{GNOSIS_CHAIN_UPPER}_KEEPER_MAX_FEE_PER_GAS_GWEI", + default=150, + cast=lambda x: Web3.toWei(x, "gwei"), + ), + SYNC_PERIOD=timedelta(days=1), + IS_POA=True, + DEPOSIT_TOKEN_SYMBOL="mGNO", + ), +} diff --git a/oracle/oracle/clients.py b/oracle/oracle/clients.py index cc9158a..ca1c757 100644 --- a/oracle/oracle/clients.py +++ b/oracle/oracle/clients.py @@ -2,29 +2,14 @@ from typing import Any, Dict, List, Union import backoff -import boto3 import ipfshttpclient from aiohttp import ClientSession from gql import Client from gql.transport.aiohttp import AIOHTTPTransport from graphql import DocumentNode -from oracle.oracle.settings import ( - AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY, - ETHEREUM_SUBGRAPH_URL, - IPFS_FETCH_ENDPOINTS, - IPFS_PIN_ENDPOINTS, - RARI_FUSE_SUBGRAPH_URL, - STAKEWISE_SUBGRAPH_URL, - UNISWAP_V3_SUBGRAPH_URL, -) - -s3_client = boto3.client( - "s3", - aws_access_key_id=AWS_ACCESS_KEY_ID, - aws_secret_access_key=AWS_SECRET_ACCESS_KEY, -) +from oracle.networks import NETWORKS +from oracle.settings import IPFS_FETCH_ENDPOINTS, IPFS_PIN_ENDPOINTS gql_logger = logging.getLogger("gql_logger") gql_handler = logging.StreamHandler() @@ -36,35 +21,45 @@ @backoff.on_exception(backoff.expo, Exception, max_time=300, logger=gql_logger) -async def execute_sw_gql_query(query: DocumentNode, variables: Dict) -> Dict: +async def execute_sw_gql_query( + network: str, query: DocumentNode, variables: Dict +) -> Dict: """Executes GraphQL query.""" - transport = AIOHTTPTransport(url=STAKEWISE_SUBGRAPH_URL) + subgraph_url = NETWORKS[network]["STAKEWISE_SUBGRAPH_URL"] + transport = AIOHTTPTransport(url=subgraph_url) async with Client(transport=transport, execute_timeout=EXECUTE_TIMEOUT) as session: return await session.execute(query, variable_values=variables) @backoff.on_exception(backoff.expo, Exception, max_time=300, logger=gql_logger) -async def execute_uniswap_v3_gql_query(query: DocumentNode, variables: Dict) -> Dict: +async def execute_uniswap_v3_gql_query( + network: str, query: DocumentNode, variables: Dict +) -> Dict: """Executes GraphQL query.""" - transport = AIOHTTPTransport(url=UNISWAP_V3_SUBGRAPH_URL) + subgraph_url = NETWORKS[network]["UNISWAP_V3_SUBGRAPH_URL"] + transport = AIOHTTPTransport(url=subgraph_url) async with Client(transport=transport, execute_timeout=EXECUTE_TIMEOUT) as session: return await session.execute(query, variable_values=variables) @backoff.on_exception(backoff.expo, Exception, max_time=300, logger=gql_logger) -async def execute_ethereum_gql_query(query: DocumentNode, variables: Dict) -> Dict: +async def execute_ethereum_gql_query( + network: str, query: DocumentNode, variables: Dict +) -> Dict: """Executes GraphQL query.""" - transport = AIOHTTPTransport(url=ETHEREUM_SUBGRAPH_URL) + subgraph_url = NETWORKS[network]["ETHEREUM_SUBGRAPH_URL"] + transport = AIOHTTPTransport(url=subgraph_url) async with Client(transport=transport, execute_timeout=EXECUTE_TIMEOUT) as session: return await session.execute(query, variable_values=variables) @backoff.on_exception(backoff.expo, Exception, max_time=300, logger=gql_logger) async def execute_rari_fuse_pools_gql_query( - query: DocumentNode, variables: Dict + network: str, query: DocumentNode, variables: Dict ) -> Dict: """Executes GraphQL query.""" - transport = AIOHTTPTransport(url=RARI_FUSE_SUBGRAPH_URL) + subgraph_url = NETWORKS[network]["RARI_FUSE_SUBGRAPH_URL"] + transport = AIOHTTPTransport(url=subgraph_url) async with Client(transport=transport, execute_timeout=EXECUTE_TIMEOUT) as session: return await session.execute(query, variable_values=variables) diff --git a/oracle/oracle/distributor/controller.py b/oracle/oracle/distributor/controller.py index 3086bd3..f717a1d 100644 --- a/oracle/oracle/distributor/controller.py +++ b/oracle/oracle/distributor/controller.py @@ -5,10 +5,10 @@ from eth_typing import HexStr from web3 import Web3 -from oracle.common.settings import DISTRIBUTOR_VOTE_FILENAME +from oracle.networks import NETWORKS +from oracle.settings import DISTRIBUTOR_VOTE_FILENAME from ..eth1 import submit_vote -from ..settings import DISTRIBUTOR_FALLBACK_ADDRESS, REWARD_TOKEN_CONTRACT_ADDRESS from .eth1 import ( get_disabled_stakers_reward_token_distributions, get_distributor_claimed_accounts, @@ -30,9 +30,16 @@ class DistributorController(object): """Updates merkle root and submits proofs to the IPFS.""" - def __init__(self, oracle: LocalAccount) -> None: + def __init__(self, network: str, oracle: LocalAccount) -> None: self.last_to_block = None + self.network = network self.oracle = oracle + self.distributor_fallback_address = NETWORKS[network][ + "DISTRIBUTOR_FALLBACK_ADDRESS" + ] + self.reward_token_contract_address = NETWORKS[network][ + "REWARD_TOKEN_CONTRACT_ADDRESS" + ] async def process(self, voting_params: DistributorVotingParameters) -> None: """Submits vote for the new merkle root and merkle proofs to the IPFS.""" @@ -50,14 +57,16 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: return logger.info( - f"Voting for Merkle Distributor rewards: from block={from_block}, to block={to_block}" + f"[{self.network}] Voting for Merkle Distributor rewards: from block={from_block}, to block={to_block}" ) # fetch active periodic allocations active_allocations = await get_periodic_allocations( - from_block=from_block, to_block=to_block + network=self.network, from_block=from_block, to_block=to_block + ) + uniswap_v3_pools = await get_uniswap_v3_pools( + network=self.network, block_number=to_block ) - uniswap_v3_pools = await get_uniswap_v3_pools(to_block) # fetch uni v3 distributions all_distributions = await get_uniswap_v3_distributions( @@ -70,6 +79,7 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: # fetch disabled stakers distributions disabled_stakers_distributions = ( await get_disabled_stakers_reward_token_distributions( + network=self.network, distributor_reward=voting_params["distributor_reward"], from_block=from_block, to_block=to_block, @@ -81,7 +91,9 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: last_merkle_proofs = voting_params["last_merkle_proofs"] if w3.toInt(hexstr=last_merkle_root) and last_merkle_proofs: # fetch accounts that have claimed since last merkle root update - claimed_accounts = await get_distributor_claimed_accounts(last_merkle_root) + claimed_accounts = await get_distributor_claimed_accounts( + network=self.network, merkle_root=last_merkle_root + ) # calculate unclaimed rewards unclaimed_rewards = await get_unclaimed_balances( @@ -95,6 +107,7 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: tasks = [] for dist in all_distributions: distributor_rewards = DistributorRewards( + network=self.network, uniswap_v3_pools=uniswap_v3_pools, from_block=dist["from_block"], to_block=dist["to_block"], @@ -107,7 +120,11 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: tasks.append(task) # process one time rewards - tasks.append(get_one_time_rewards(from_block=from_block, to_block=to_block)) + tasks.append( + get_one_time_rewards( + network=self.network, from_block=from_block, to_block=to_block + ) + ) # merge results results = await asyncio.gather(*tasks) @@ -117,15 +134,21 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: protocol_reward = voting_params["protocol_reward"] operators_rewards, left_reward = await get_operators_rewards( - from_block=from_block, to_block=to_block, total_reward=protocol_reward + network=self.network, + from_block=from_block, + to_block=to_block, + total_reward=protocol_reward, ) partners_rewards, left_reward = await get_partners_rewards( - from_block=from_block, to_block=to_block, total_reward=left_reward + network=self.network, + from_block=from_block, + to_block=to_block, + total_reward=left_reward, ) if left_reward > 0: fallback_rewards: Rewards = { - DISTRIBUTOR_FALLBACK_ADDRESS: { - REWARD_TOKEN_CONTRACT_ADDRESS: str(left_reward) + self.distributor_fallback_address: { + self.reward_token_contract_address: str(left_reward) } } final_rewards = DistributorRewards.merge_rewards( @@ -144,10 +167,10 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: # calculate merkle root merkle_root, claims = calculate_merkle_root(final_rewards) - logger.info(f"Generated new merkle root: {merkle_root}") + logger.info(f"[{self.network}] Generated new merkle root: {merkle_root}") claims_link = await upload_claims(claims) - logger.info(f"Claims uploaded to: {claims_link}") + logger.info(f"[{self.network}] Claims uploaded to: {claims_link}") # submit vote encoded_data: bytes = w3.codec.encode_abi( @@ -161,11 +184,14 @@ async def process(self, voting_params: DistributorVotingParameters) -> None: merkle_proofs=claims_link, ) submit_vote( + network=self.network, oracle=self.oracle, encoded_data=encoded_data, vote=vote, name=DISTRIBUTOR_VOTE_FILENAME, ) - logger.info("Distributor vote has been successfully submitted") + logger.info( + f"[{self.network}] Distributor vote has been successfully submitted" + ) self.last_to_block = to_block diff --git a/oracle/oracle/distributor/eth1.py b/oracle/oracle/distributor/eth1.py index 1370902..226e43e 100644 --- a/oracle/oracle/distributor/eth1.py +++ b/oracle/oracle/distributor/eth1.py @@ -15,12 +15,8 @@ PARTNERS_QUERY, PERIODIC_DISTRIBUTIONS_QUERY, ) -from oracle.oracle.settings import ( - DISTRIBUTOR_FALLBACK_ADDRESS, - REWARD_TOKEN_CONTRACT_ADDRESS, - STAKED_TOKEN_CONTRACT_ADDRESS, -) +from ...networks import NETWORKS from .ipfs import get_one_time_rewards_allocations from .rewards import DistributorRewards from .types import ( @@ -36,11 +32,12 @@ async def get_periodic_allocations( - from_block: BlockNumber, to_block: BlockNumber + network: str, from_block: BlockNumber, to_block: BlockNumber ) -> TokenAllocations: """Fetches periodic allocations.""" last_id = "" result: Dict = await execute_sw_gql_query( + network=network, query=PERIODIC_DISTRIBUTIONS_QUERY, variables=dict(from_block=from_block, to_block=to_block, last_id=last_id), ) @@ -51,6 +48,7 @@ async def get_periodic_allocations( while len(distributions_chunk) >= 1000: last_id = distributions_chunk[-1]["id"] result: Dict = await execute_sw_gql_query( + network=network, query=PERIODIC_DISTRIBUTIONS_QUERY, variables=dict(from_block=from_block, to_block=to_block, last_id=last_id), ) @@ -80,14 +78,21 @@ async def get_periodic_allocations( async def get_disabled_stakers_reward_token_distributions( - distributor_reward: Wei, from_block: BlockNumber, to_block: BlockNumber + network: str, + distributor_reward: Wei, + from_block: BlockNumber, + to_block: BlockNumber, ) -> Distributions: """Fetches disabled stakers reward token distributions based on their staked token balances.""" if distributor_reward <= 0: return [] + reward_token_address = NETWORKS[network]["REWARD_TOKEN_CONTRACT_ADDRESS"] + staked_token_address = NETWORKS[network]["STAKED_TOKEN_CONTRACT_ADDRESS"] + last_id = "" result: Dict = await execute_sw_gql_query( + network=network, query=DISABLED_STAKER_ACCOUNTS_QUERY, variables=dict(block_number=to_block, last_id=last_id), ) @@ -98,6 +103,7 @@ async def get_disabled_stakers_reward_token_distributions( while len(stakers_chunk) >= 1000: last_id = stakers_chunk[-1]["id"] result: Dict = await execute_sw_gql_query( + network=network, query=DISABLED_STAKER_ACCOUNTS_QUERY, variables=dict(block_number=to_block, last_id=last_id), ) @@ -144,8 +150,8 @@ async def get_disabled_stakers_reward_token_distributions( contract=staker_address, from_block=from_block, to_block=to_block, - uni_v3_token=STAKED_TOKEN_CONTRACT_ADDRESS, - reward_token=REWARD_TOKEN_CONTRACT_ADDRESS, + uni_v3_token=staked_token_address, + reward_token=reward_token_address, reward=reward, ) distributions.append(distribution) @@ -154,10 +160,13 @@ async def get_disabled_stakers_reward_token_distributions( return distributions -async def get_distributor_claimed_accounts(merkle_root: HexStr) -> ClaimedAccounts: +async def get_distributor_claimed_accounts( + network: str, merkle_root: HexStr +) -> ClaimedAccounts: """Fetches addresses that have claimed their tokens from the `MerkleDistributor` contract.""" last_id = "" result: Dict = await execute_sw_gql_query( + network=network, query=DISTRIBUTOR_CLAIMED_ACCOUNTS_QUERY, variables=dict(merkle_root=merkle_root, last_id=last_id), ) @@ -168,6 +177,7 @@ async def get_distributor_claimed_accounts(merkle_root: HexStr) -> ClaimedAccoun while len(claims_chunk) >= 1000: last_id = claims_chunk[-1]["id"] result: Dict = await execute_sw_gql_query( + network=network, query=DISTRIBUTOR_CLAIMED_ACCOUNTS_QUERY, variables=dict(merkle_root=merkle_root, last_id=last_id), ) @@ -178,18 +188,21 @@ async def get_distributor_claimed_accounts(merkle_root: HexStr) -> ClaimedAccoun async def get_operators_rewards( + network: str, from_block: BlockNumber, to_block: BlockNumber, total_reward: Wei, ) -> Tuple[Rewards, Wei]: """Fetches operators rewards.""" result: Dict = await execute_sw_gql_query( + network=network, query=OPERATORS_REWARDS_QUERY, variables=dict( block_number=to_block, ), ) operators = result["operators"] + reward_token_address = NETWORKS[network]["REWARD_TOKEN_CONTRACT_ADDRESS"] # process operators points: Dict[ChecksumAddress, int] = {} @@ -234,23 +247,25 @@ async def get_operators_rewards( total_reward=operators_reward, points=points, total_points=total_points, - reward_token=REWARD_TOKEN_CONTRACT_ADDRESS, + reward_token=reward_token_address, ) return rewards, Wei(total_reward - operators_reward) async def get_partners_rewards( - from_block: BlockNumber, to_block: BlockNumber, total_reward: Wei + network: str, from_block: BlockNumber, to_block: BlockNumber, total_reward: Wei ) -> Tuple[Rewards, Wei]: """Fetches partners rewards.""" result: Dict = await execute_sw_gql_query( + network=network, query=PARTNERS_QUERY, variables=dict( block_number=to_block, ), ) partners = result["partners"] + reward_token_address = NETWORKS[network]["REWARD_TOKEN_CONTRACT_ADDRESS"] # process partners points: Dict[ChecksumAddress, int] = {} @@ -295,7 +310,7 @@ async def get_partners_rewards( total_reward=partners_reward, points=points, total_points=total_points, - reward_token=REWARD_TOKEN_CONTRACT_ADDRESS, + reward_token=reward_token_address, ) return rewards, Wei(total_reward - partners_reward) @@ -335,11 +350,14 @@ def calculate_points_based_rewards( async def get_one_time_rewards( - from_block: BlockNumber, to_block: BlockNumber + network: str, from_block: BlockNumber, to_block: BlockNumber ) -> Rewards: """Fetches one time rewards.""" + distributor_fallback_address = NETWORKS[network]["DISTRIBUTOR_FALLBACK_ADDRESS"] + last_id = "" result: Dict = await execute_sw_gql_query( + network=network, query=ONE_TIME_DISTRIBUTIONS_QUERY, variables=dict(from_block=from_block, to_block=to_block, last_id=last_id), ) @@ -350,6 +368,7 @@ async def get_one_time_rewards( while len(distributions_chunk) >= 1000: last_id = distributions_chunk[-1]["id"] result: Dict = await execute_sw_gql_query( + network=network, query=ONE_TIME_DISTRIBUTIONS_QUERY, variables=dict(from_block=from_block, to_block=to_block, last_id=last_id), ) @@ -380,18 +399,18 @@ async def get_one_time_rewards( if total_amount != distributed_amount: logger.warning( - f'Failed to process one time distribution: {distribution["id"]}. Invalid rewards.' + f'[{network}] Failed to process one time distribution: {distribution["id"]}. Invalid rewards.' ) rewards: Rewards = { - DISTRIBUTOR_FALLBACK_ADDRESS: {token: str(total_amount)} + distributor_fallback_address: {token: str(total_amount)} } except Exception as e: logger.error(e) logger.warning( - f'Failed to process one time distribution: {distribution["id"]}. Exception occurred.' + f'[{network}] Failed to process one time distribution: {distribution["id"]}. Exception occurred.' ) rewards: Rewards = { - DISTRIBUTOR_FALLBACK_ADDRESS: {token: str(total_amount)} + distributor_fallback_address: {token: str(total_amount)} } final_rewards = DistributorRewards.merge_rewards(final_rewards, rewards) diff --git a/oracle/oracle/distributor/ipfs.py b/oracle/oracle/distributor/ipfs.py index d056727..6222fc3 100644 --- a/oracle/oracle/distributor/ipfs.py +++ b/oracle/oracle/distributor/ipfs.py @@ -7,14 +7,14 @@ from aiohttp import ClientSession from eth_typing import ChecksumAddress -from oracle.oracle.settings import ( +from oracle.oracle.clients import ipfs_fetch +from oracle.settings import ( IPFS_PIN_ENDPOINTS, IPFS_PINATA_API_KEY, IPFS_PINATA_PIN_ENDPOINT, IPFS_PINATA_SECRET_KEY, ) -from ..clients import ipfs_fetch from .types import ClaimedAccounts, Claims, Rewards logger = logging.getLogger(__name__) diff --git a/oracle/oracle/distributor/rari.py b/oracle/oracle/distributor/rari.py index 87c471c..78a9d15 100644 --- a/oracle/oracle/distributor/rari.py +++ b/oracle/oracle/distributor/rari.py @@ -5,18 +5,22 @@ from web3 import Web3 from oracle.oracle.clients import execute_rari_fuse_pools_gql_query +from oracle.oracle.graphql_queries import RARI_FUSE_POOLS_CTOKENS_QUERY -from ..graphql_queries import RARI_FUSE_POOLS_CTOKENS_QUERY from .types import Balances async def get_rari_fuse_liquidity_points( - ctoken_address: ChecksumAddress, from_block: BlockNumber, to_block: BlockNumber + network: str, + ctoken_address: ChecksumAddress, + from_block: BlockNumber, + to_block: BlockNumber, ) -> Balances: """Fetches Rari Fuse pool accounts balances.""" lowered_ctoken_address = ctoken_address.lower() last_id = "" result: Dict = await execute_rari_fuse_pools_gql_query( + network=network, query=RARI_FUSE_POOLS_CTOKENS_QUERY, variables=dict( ctoken_address=lowered_ctoken_address, @@ -31,6 +35,7 @@ async def get_rari_fuse_liquidity_points( while len(positions_chunk) >= 1000: last_id = positions_chunk[-1]["id"] result: Dict = await execute_rari_fuse_pools_gql_query( + network=network, query=RARI_FUSE_POOLS_CTOKENS_QUERY, variables=dict( ctoken_address=lowered_ctoken_address, diff --git a/oracle/oracle/distributor/rewards.py b/oracle/oracle/distributor/rewards.py index fae3027..c4b8d3d 100644 --- a/oracle/oracle/distributor/rewards.py +++ b/oracle/oracle/distributor/rewards.py @@ -5,13 +5,7 @@ from ens.constants import EMPTY_ADDR_HEX from eth_typing import BlockNumber, ChecksumAddress -from oracle.oracle.settings import ( - DISTRIBUTOR_FALLBACK_ADDRESS, - RARI_FUSE_POOL_ADDRESSES, - REWARD_TOKEN_CONTRACT_ADDRESS, - STAKED_TOKEN_CONTRACT_ADDRESS, - SWISE_TOKEN_CONTRACT_ADDRESS, -) +from oracle.networks import NETWORKS from .rari import get_rari_fuse_liquidity_points from .types import Balances, Rewards, UniswapV3Pools @@ -27,12 +21,27 @@ class DistributorRewards(object): def __init__( self, + network: str, uniswap_v3_pools: UniswapV3Pools, from_block: BlockNumber, to_block: BlockNumber, reward_token: ChecksumAddress, uni_v3_token: ChecksumAddress, ) -> None: + self.network = network + self.rari_fuse_pool_addresses = NETWORKS[network]["RARI_FUSE_POOL_ADDRESSES"] + self.distributor_fallback_address = NETWORKS[network][ + "DISTRIBUTOR_FALLBACK_ADDRESS" + ] + self.staked_token_contract_address = NETWORKS[network][ + "STAKED_TOKEN_CONTRACT_ADDRESS" + ] + self.reward_token_contract_address = NETWORKS[network][ + "REWARD_TOKEN_CONTRACT_ADDRESS" + ] + self.swise_token_contract_address = NETWORKS[network][ + "SWISE_TOKEN_CONTRACT_ADDRESS" + ] self.uni_v3_staked_token_pools = uniswap_v3_pools["staked_token_pools"] self.uni_v3_reward_token_pools = uniswap_v3_pools["reward_token_pools"] self.uni_v3_swise_pools = uniswap_v3_pools["swise_pools"] @@ -48,8 +57,8 @@ def is_supported_contract(self, contract_address: ChecksumAddress) -> bool: """Checks whether the provided contract address is supported.""" return ( contract_address in self.uni_v3_pools - or contract_address == SWISE_TOKEN_CONTRACT_ADDRESS - or contract_address in RARI_FUSE_POOL_ADDRESSES + or contract_address == self.swise_token_contract_address + or contract_address in self.rari_fuse_pool_addresses ) @staticmethod @@ -96,7 +105,7 @@ async def get_rewards( rewards: Rewards = {} self.add_value( rewards=rewards, - to=DISTRIBUTOR_FALLBACK_ADDRESS, + to=self.distributor_fallback_address, reward_token=self.reward_token, amount=reward, ) @@ -106,37 +115,42 @@ async def get_rewards( async def get_balances(self, contract_address: ChecksumAddress) -> Balances: """Fetches balances and total supply of the contract.""" if ( - self.uni_v3_token == STAKED_TOKEN_CONTRACT_ADDRESS + self.uni_v3_token == self.staked_token_contract_address and contract_address in self.uni_v3_staked_token_pools ): logger.info( - f"Fetching Uniswap V3 staked token balances: pool={contract_address}" + f"[{self.network}] Fetching Uniswap V3 staked token balances: pool={contract_address}" ) return await get_uniswap_v3_single_token_balances( + network=self.network, pool_address=contract_address, - token=STAKED_TOKEN_CONTRACT_ADDRESS, + token=self.staked_token_contract_address, block_number=self.to_block, ) elif ( - self.uni_v3_token == REWARD_TOKEN_CONTRACT_ADDRESS + self.uni_v3_token == self.reward_token_contract_address and contract_address in self.uni_v3_reward_token_pools ): logger.info( - f"Fetching Uniswap V3 reward token balances: pool={contract_address}" + f"[{self.network}] Fetching Uniswap V3 reward token balances: pool={contract_address}" ) return await get_uniswap_v3_single_token_balances( + network=self.network, pool_address=contract_address, - token=REWARD_TOKEN_CONTRACT_ADDRESS, + token=self.reward_token_contract_address, block_number=self.to_block, ) elif ( - self.uni_v3_token == SWISE_TOKEN_CONTRACT_ADDRESS + self.uni_v3_token == self.swise_token_contract_address and contract_address in self.uni_v3_swise_pools ): - logger.info(f"Fetching Uniswap V3 SWISE balances: pool={contract_address}") + logger.info( + f"[{self.network}] Fetching Uniswap V3 SWISE balances: pool={contract_address}" + ) return await get_uniswap_v3_single_token_balances( + network=self.network, pool_address=contract_address, - token=SWISE_TOKEN_CONTRACT_ADDRESS, + token=self.swise_token_contract_address, block_number=self.to_block, ) elif ( @@ -144,9 +158,10 @@ async def get_balances(self, contract_address: ChecksumAddress) -> Balances: and contract_address in self.uni_v3_swise_pools ): logger.info( - f"Fetching Uniswap V3 full range liquidity points: pool={contract_address}" + f"[{self.network}] Fetching Uniswap V3 full range liquidity points: pool={contract_address}" ) return await get_uniswap_v3_range_liquidity_points( + network=self.network, tick_lower=-887220, tick_upper=887220, pool_address=contract_address, @@ -154,24 +169,26 @@ async def get_balances(self, contract_address: ChecksumAddress) -> Balances: ) elif contract_address in self.uni_v3_pools: logger.info( - f"Fetching Uniswap V3 liquidity points: pool={contract_address}" + f"[{self.network}] Fetching Uniswap V3 liquidity points: pool={contract_address}" ) return await get_uniswap_v3_liquidity_points( + network=self.network, pool_address=contract_address, block_number=self.to_block, ) - elif contract_address in RARI_FUSE_POOL_ADDRESSES: + elif contract_address in self.rari_fuse_pool_addresses: logger.info( - f"Fetching Rari Fuse Pool liquidity points: pool={contract_address}" + f"[{self.network}] Fetching Rari Fuse Pool liquidity points: pool={contract_address}" ) return await get_rari_fuse_liquidity_points( + network=self.network, ctoken_address=contract_address, from_block=self.from_block, to_block=self.to_block, ) raise ValueError( - f"Cannot get balances for unsupported contract address {contract_address}" + f"[{self.network}] Cannot get balances for unsupported contract address {contract_address}" ) async def _get_rewards( @@ -189,7 +206,7 @@ async def _get_rewards( # no recipients for the rewards -> assign reward to the rescue address self.add_value( rewards=rewards, - to=DISTRIBUTOR_FALLBACK_ADDRESS, + to=self.distributor_fallback_address, reward_token=self.reward_token, amount=total_reward, ) @@ -215,7 +232,7 @@ async def _get_rewards( # failed to assign reward -> return it to rescue address self.add_value( rewards=rewards, - to=DISTRIBUTOR_FALLBACK_ADDRESS, + to=self.distributor_fallback_address, reward_token=self.reward_token, amount=account_reward, ) diff --git a/oracle/oracle/distributor/uniswap_v3.py b/oracle/oracle/distributor/uniswap_v3.py index c5baf1a..383c7c1 100644 --- a/oracle/oracle/distributor/uniswap_v3.py +++ b/oracle/oracle/distributor/uniswap_v3.py @@ -6,6 +6,7 @@ from eth_typing import BlockNumber, ChecksumAddress from web3 import Web3 +from oracle.networks import NETWORKS from oracle.oracle.clients import execute_uniswap_v3_gql_query from oracle.oracle.graphql_queries import ( UNISWAP_V3_CURRENT_TICK_POSITIONS_QUERY, @@ -14,11 +15,6 @@ UNISWAP_V3_POSITIONS_QUERY, UNISWAP_V3_RANGE_POSITIONS_QUERY, ) -from oracle.oracle.settings import ( - REWARD_TOKEN_CONTRACT_ADDRESS, - STAKED_TOKEN_CONTRACT_ADDRESS, - SWISE_TOKEN_CONTRACT_ADDRESS, -) from .types import ( Balances, @@ -38,10 +34,14 @@ @backoff.on_exception(backoff.expo, Exception, max_time=900) -async def get_uniswap_v3_pools(block_number: BlockNumber) -> UniswapV3Pools: +async def get_uniswap_v3_pools( + network: str, block_number: BlockNumber +) -> UniswapV3Pools: """Fetches Uniswap V3 pools.""" + network_config = NETWORKS[network] last_id = "" result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_POOLS_QUERY, variables=dict(block_number=block_number, last_id=last_id), ) @@ -52,6 +52,7 @@ async def get_uniswap_v3_pools(block_number: BlockNumber) -> UniswapV3Pools: while len(pools_chunk) >= 1000: last_id = pools_chunk[-1]["id"] result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_POOLS_QUERY, variables=dict(block_number=block_number, last_id=last_id), ) @@ -68,11 +69,11 @@ async def get_uniswap_v3_pools(block_number: BlockNumber) -> UniswapV3Pools: pool_token0 = Web3.toChecksumAddress(pool["token0"]) pool_token1 = Web3.toChecksumAddress(pool["token1"]) for pool_token in [pool_token0, pool_token1]: - if pool_token == STAKED_TOKEN_CONTRACT_ADDRESS: + if pool_token == network_config["STAKED_TOKEN_CONTRACT_ADDRESS"]: uni_v3_pools["staked_token_pools"].add(pool_address) - elif pool_token == REWARD_TOKEN_CONTRACT_ADDRESS: + elif pool_token == network_config["REWARD_TOKEN_CONTRACT_ADDRESS"]: uni_v3_pools["reward_token_pools"].add(pool_address) - elif pool_token == SWISE_TOKEN_CONTRACT_ADDRESS: + elif pool_token == network_config["SWISE_TOKEN_CONTRACT_ADDRESS"]: uni_v3_pools["swise_pools"].add(pool_address) return uni_v3_pools @@ -151,11 +152,12 @@ async def get_uniswap_v3_distributions( @backoff.on_exception(backoff.expo, Exception, max_time=900) async def get_uniswap_v3_liquidity_points( - pool_address: ChecksumAddress, block_number: BlockNumber + network: str, pool_address: ChecksumAddress, block_number: BlockNumber ) -> Balances: """Fetches users' liquidity points of the Uniswap V3 pool in the current tick.""" lowered_pool_address = pool_address.lower() result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_POOL_QUERY, variables=dict(block_number=block_number, pool_address=lowered_pool_address), ) @@ -171,6 +173,7 @@ async def get_uniswap_v3_liquidity_points( last_id = "" result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_CURRENT_TICK_POSITIONS_QUERY, variables=dict( block_number=block_number, @@ -186,6 +189,7 @@ async def get_uniswap_v3_liquidity_points( while len(positions_chunk) >= 1000: last_id = positions_chunk[-1]["id"] result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_CURRENT_TICK_POSITIONS_QUERY, variables=dict( block_number=block_number, @@ -218,6 +222,7 @@ async def get_uniswap_v3_liquidity_points( @backoff.on_exception(backoff.expo, Exception, max_time=900) async def get_uniswap_v3_range_liquidity_points( + network: str, tick_lower: int, tick_upper: int, pool_address: ChecksumAddress, @@ -228,6 +233,7 @@ async def get_uniswap_v3_range_liquidity_points( last_id = "" result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_RANGE_POSITIONS_QUERY, variables=dict( block_number=block_number, @@ -244,6 +250,7 @@ async def get_uniswap_v3_range_liquidity_points( while len(positions_chunk) >= 1000: last_id = positions_chunk[-1]["id"] result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_RANGE_POSITIONS_QUERY, variables=dict( block_number=block_number, @@ -277,11 +284,15 @@ async def get_uniswap_v3_range_liquidity_points( @backoff.on_exception(backoff.expo, Exception, max_time=900) async def get_uniswap_v3_single_token_balances( - pool_address: ChecksumAddress, token: ChecksumAddress, block_number: BlockNumber + network: str, + pool_address: ChecksumAddress, + token: ChecksumAddress, + block_number: BlockNumber, ) -> Balances: """Fetches users' single token balances of the Uniswap V3 pair across all the ticks.""" lowered_pool_address = pool_address.lower() result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_POOL_QUERY, variables=dict(block_number=block_number, pool_address=lowered_pool_address), ) @@ -305,6 +316,7 @@ async def get_uniswap_v3_single_token_balances( last_id = "" result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_POSITIONS_QUERY, variables=dict( block_number=block_number, @@ -321,6 +333,7 @@ async def get_uniswap_v3_single_token_balances( while len(positions_chunk) >= 1000: last_id = positions_chunk[-1]["id"] result: Dict = await execute_uniswap_v3_gql_query( + network=network, query=UNISWAP_V3_POSITIONS_QUERY, variables=dict( block_number=block_number, diff --git a/oracle/oracle/eth1.py b/oracle/oracle/eth1.py index e4ee809..0941385 100644 --- a/oracle/oracle/eth1.py +++ b/oracle/oracle/eth1.py @@ -1,23 +1,24 @@ import json import logging -from typing import Dict, List, TypedDict, Union +from typing import Dict, TypedDict, Union import backoff +import boto3 from eth_account.messages import encode_defunct from eth_account.signers.local import LocalAccount from web3 import Web3 from web3.types import BlockNumber, Timestamp, Wei -from oracle.common.settings import AWS_S3_BUCKET_NAME, CONFIRMATION_BLOCKS - -from .clients import execute_ethereum_gql_query, execute_sw_gql_query, s3_client -from .distributor.types import DistributorVote, DistributorVotingParameters -from .graphql_queries import ( +from oracle.networks import NETWORKS +from oracle.oracle.clients import execute_ethereum_gql_query, execute_sw_gql_query +from oracle.oracle.graphql_queries import ( FINALIZED_BLOCK_QUERY, LATEST_BLOCK_QUERY, - ORACLE_QUERY, VOTING_PARAMETERS_QUERY, ) +from oracle.settings import CONFIRMATION_BLOCKS + +from .distributor.types import DistributorVote, DistributorVotingParameters from .rewards.types import RewardsVotingParameters, RewardVote from .validators.types import ValidatorVote @@ -34,9 +35,10 @@ class VotingParameters(TypedDict): distributor: DistributorVotingParameters -async def get_finalized_block() -> Block: +async def get_finalized_block(network: str) -> Block: """Gets the finalized block number and its timestamp.""" result: Dict = await execute_ethereum_gql_query( + network=network, query=FINALIZED_BLOCK_QUERY, variables=dict( confirmation_blocks=CONFIRMATION_BLOCKS, @@ -48,9 +50,10 @@ async def get_finalized_block() -> Block: ) -async def get_latest_block() -> Block: +async def get_latest_block(network: str) -> Block: """Gets the latest block number and its timestamp.""" result: Dict = await execute_ethereum_gql_query( + network=network, query=LATEST_BLOCK_QUERY, variables=dict( confirmation_blocks=CONFIRMATION_BLOCKS, @@ -62,9 +65,12 @@ async def get_latest_block() -> Block: ) -async def get_voting_parameters(block_number: BlockNumber) -> VotingParameters: +async def get_voting_parameters( + network: str, block_number: BlockNumber +) -> VotingParameters: """Fetches rewards voting parameters.""" result: Dict = await execute_sw_gql_query( + network=network, query=VOTING_PARAMETERS_QUERY, variables=dict( block_number=block_number, @@ -92,34 +98,22 @@ async def get_voting_parameters(block_number: BlockNumber) -> VotingParameters: return VotingParameters(rewards=rewards, distributor=distributor) -async def check_oracle_account(oracle: LocalAccount) -> None: - """Checks whether oracle is part of the oracles set.""" - oracle_lowered_address = oracle.address.lower() - result: List = ( - await execute_sw_gql_query( - query=ORACLE_QUERY, - variables=dict( - oracle_address=oracle_lowered_address, - ), - ) - ).get("oracles", []) - if result and result[0].get("id", "") == oracle_lowered_address: - logger.info(f"Oracle {oracle.address} is part of the oracles set") - else: - logger.warning( - f"NB! Oracle {oracle.address} is not part of the oracles set." - f" Please create DAO proposal to include it." - ) - - @backoff.on_exception(backoff.expo, Exception, max_time=900) def submit_vote( + network: str, oracle: LocalAccount, encoded_data: bytes, vote: Union[RewardVote, DistributorVote, ValidatorVote], name: str, ) -> None: """Submits vote to the votes' aggregator.""" + network_config = NETWORKS[network] + aws_bucket_name = network_config["AWS_BUCKET_NAME"] + s3_client = boto3.client( + "s3", + aws_access_key_id=network_config["AWS_ACCESS_KEY_ID"], + aws_secret_access_key=network_config["AWS_SECRET_ACCESS_KEY"], + ) # generate candidate ID candidate_id: bytes = Web3.keccak(primitive=encoded_data) message = encode_defunct(primitive=candidate_id) @@ -129,11 +123,9 @@ def submit_vote( # TODO: support more aggregators (GCP, Azure, etc.) bucket_key = f"{oracle.address}/{name}" s3_client.put_object( - Bucket=AWS_S3_BUCKET_NAME, + Bucket=aws_bucket_name, Key=bucket_key, Body=json.dumps(vote), ACL="public-read", ) - s3_client.get_waiter("object_exists").wait( - Bucket=AWS_S3_BUCKET_NAME, Key=bucket_key - ) + s3_client.get_waiter("object_exists").wait(Bucket=aws_bucket_name, Key=bucket_key) diff --git a/oracle/oracle/health_server.py b/oracle/oracle/health_server.py index edf8fbc..ed6782e 100644 --- a/oracle/oracle/health_server.py +++ b/oracle/oracle/health_server.py @@ -4,6 +4,7 @@ from oracle.oracle.clients import ipfs_fetch from oracle.oracle.eth1 import get_finalized_block, get_voting_parameters +from oracle.settings import ENABLED_NETWORKS logger = logging.getLogger(__name__) oracle_routes = web.RouteTableDef() @@ -13,13 +14,14 @@ async def health(request): try: # check graphQL connection - finalized_block = await get_finalized_block() - current_block_number = finalized_block["block_number"] - voting_params = await get_voting_parameters(current_block_number) - last_merkle_proofs = voting_params["distributor"]["last_merkle_proofs"] - - # check IPFS connection - await ipfs_fetch(last_merkle_proofs) + for network in ENABLED_NETWORKS: + finalized_block = await get_finalized_block(network) + current_block_number = finalized_block["block_number"] + voting_params = await get_voting_parameters(network, current_block_number) + last_merkle_proofs = voting_params["distributor"]["last_merkle_proofs"] + + # check IPFS connection + await ipfs_fetch(last_merkle_proofs) return web.Response(text="oracle 1") except Exception as e: # noqa: E722 diff --git a/oracle/oracle/main.py b/oracle/oracle/main.py index 25c5443..f960b01 100644 --- a/oracle/oracle/main.py +++ b/oracle/oracle/main.py @@ -1,34 +1,29 @@ import asyncio import logging -import signal import threading -from typing import Any +from typing import Dict from urllib.parse import urlparse import aiohttp -from eth_account import Account from eth_account.signers.local import LocalAccount -from oracle.common.health_server import create_health_server_runner, start_health_server -from oracle.common.settings import ENABLE_HEALTH_SERVER, LOG_LEVEL, TEST_VOTE_FILENAME +from oracle.health_server import create_health_server_runner, start_health_server +from oracle.networks import NETWORKS from oracle.oracle.distributor.controller import DistributorController -from oracle.oracle.eth1 import ( - check_oracle_account, - get_finalized_block, - get_voting_parameters, - submit_vote, -) +from oracle.oracle.eth1 import get_finalized_block, get_voting_parameters, submit_vote from oracle.oracle.health_server import oracle_routes from oracle.oracle.rewards.controller import RewardsController from oracle.oracle.rewards.eth2 import get_finality_checkpoints, get_genesis -from oracle.oracle.settings import ( - ETH2_CLIENT, - ETH2_ENDPOINT, - ETHEREUM_SUBGRAPH_URL, - ORACLE_PRIVATE_KEY, +from oracle.oracle.validators.controller import ValidatorsController +from oracle.settings import ( + ENABLE_HEALTH_SERVER, + ENABLED_NETWORKS, + HEALTH_SERVER_PORT, + LOG_LEVEL, ORACLE_PROCESS_INTERVAL, + TEST_VOTE_FILENAME, ) -from oracle.oracle.validators.controller import ValidatorsController +from oracle.utils import InterruptHandler, get_oracle_accounts logging.basicConfig( format="%(asctime)s %(levelname)-8s %(message)s", @@ -41,89 +36,86 @@ logger = logging.getLogger(__name__) -class InterruptHandler: - """ - Tracks SIGINT and SIGTERM signals. - https://stackoverflow.com/a/31464349 - """ - - exit = False - - def __init__(self) -> None: - signal.signal(signal.SIGINT, self.exit_gracefully) - signal.signal(signal.SIGTERM, self.exit_gracefully) - - # noinspection PyUnusedLocal - def exit_gracefully(self, signum: int, frame: Any) -> None: - logger.info(f"Received interrupt signal {signum}, exiting...") - self.exit = True - - async def main() -> None: - oracle: LocalAccount = Account.from_key(ORACLE_PRIVATE_KEY) + oracle_accounts: Dict[str, LocalAccount] = await get_oracle_accounts() # try submitting test vote - # noinspection PyTypeChecker - submit_vote( - oracle=oracle, - encoded_data=b"test data", - vote={"name": "test vote"}, - name=TEST_VOTE_FILENAME, - ) + for network, oracle in oracle_accounts.items(): + logger.info(f"[{network}] Submitting test vote for account {oracle.address}...") + # noinspection PyTypeChecker + submit_vote( + network=network, + oracle=oracle, + encoded_data=b"test data", + vote={"name": "test vote"}, + name=TEST_VOTE_FILENAME, + ) # check stakewise graphql connection - logger.info("Checking connection to graph node...") - await get_finalized_block() - parsed_uri = "{uri.scheme}://{uri.netloc}".format( - uri=urlparse(ETHEREUM_SUBGRAPH_URL) - ) - logger.info(f"Connected to graph node at {parsed_uri}") + for network in ENABLED_NETWORKS: + network_config = NETWORKS[network] + logger.info(f"[{network}] Checking connection to graph node...") + await get_finalized_block(network) + parsed_uri = "{uri.scheme}://{uri.netloc}".format( + uri=urlparse(network_config["ETHEREUM_SUBGRAPH_URL"]) + ) + logger.info(f"[{network}] Connected to graph node at {parsed_uri}") # aiohttp session session = aiohttp.ClientSession() # check ETH2 API connection - logger.info("Checking connection to ETH2 node...") - await get_finality_checkpoints(session) - parsed_uri = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(ETH2_ENDPOINT)) - logger.info(f"Connected to {ETH2_CLIENT} node at {parsed_uri}") - - # check whether oracle is part of the oracles set - await check_oracle_account(oracle) + for network in ENABLED_NETWORKS: + network_config = NETWORKS[network] + logger.info(f"[{network}] Checking connection to ETH2 node...") + await get_finality_checkpoints(network, session) + parsed_uri = "{uri.scheme}://{uri.netloc}".format( + uri=urlparse(network_config["ETH2_ENDPOINT"]) + ) + logger.info(f"[{network}] Connected to ETH2 node at {parsed_uri}") # wait for interrupt interrupt_handler = InterruptHandler() # fetch ETH2 genesis - genesis = await get_genesis(session) - - rewards_controller = RewardsController( - aiohttp_session=session, - genesis_timestamp=int(genesis["genesis_time"]), - oracle=oracle, - ) - distributor_controller = DistributorController(oracle) - validators_controller = ValidatorsController(oracle) + controllers = [] + for network in ENABLED_NETWORKS: + genesis = await get_genesis(network, session) + oracle = oracle_accounts[network] + rewards_controller = RewardsController( + network=network, + aiohttp_session=session, + genesis_timestamp=int(genesis["genesis_time"]), + oracle=oracle, + ) + distributor_controller = DistributorController(network, oracle) + validators_controller = ValidatorsController(network, oracle) + controllers.append( + (network, rewards_controller, distributor_controller, validators_controller) + ) while not interrupt_handler.exit: - # fetch current finalized ETH1 block data - finalized_block = await get_finalized_block() - current_block_number = finalized_block["block_number"] - current_timestamp = finalized_block["timestamp"] - voting_parameters = await get_voting_parameters(current_block_number) - - await asyncio.gather( - # check and update staking rewards - rewards_controller.process( - voting_params=voting_parameters["rewards"], - current_block_number=current_block_number, - current_timestamp=current_timestamp, - ), - # check and update merkle distributor - distributor_controller.process(voting_parameters["distributor"]), - # process validators registration - validators_controller.process(), - ) + for (network, rewards_ctrl, distributor_ctrl, validators_ctrl) in controllers: + # fetch current finalized ETH1 block data + finalized_block = await get_finalized_block(network) + current_block_number = finalized_block["block_number"] + current_timestamp = finalized_block["timestamp"] + voting_parameters = await get_voting_parameters( + network, current_block_number + ) + await asyncio.gather( + # check and update staking rewards + rewards_ctrl.process( + voting_params=voting_parameters["rewards"], + current_block_number=current_block_number, + current_timestamp=current_timestamp, + ), + # check and update merkle distributor + distributor_ctrl.process(voting_parameters["distributor"]), + # process validators registration + validators_ctrl.process(), + ) + # wait until next processing time await asyncio.sleep(ORACLE_PROCESS_INTERVAL) @@ -137,5 +129,8 @@ async def main() -> None: args=(create_health_server_runner(oracle_routes),), daemon=True, ) + logger.info( + f"Starting monitoring server at http://{ENABLE_HEALTH_SERVER}:{HEALTH_SERVER_PORT}" + ) t.start() asyncio.run(main()) diff --git a/oracle/oracle/rewards/controller.py b/oracle/oracle/rewards/controller.py index 2ddd6cc..2943d38 100644 --- a/oracle/oracle/rewards/controller.py +++ b/oracle/oracle/rewards/controller.py @@ -9,15 +9,13 @@ from web3 import Web3 from web3.types import Timestamp, Wei -from oracle.common.settings import REWARD_VOTE_FILENAME +from oracle.networks import NETWORKS from oracle.oracle.eth1 import submit_vote +from oracle.settings import REWARD_VOTE_FILENAME -from ..settings import STAKED_TOKEN_SYMBOL, SYNC_PERIOD from .eth1 import get_registered_validators_public_keys from .eth2 import ( PENDING_STATUSES, - SECONDS_PER_EPOCH, - SLOTS_PER_EPOCH, ValidatorStatus, get_finality_checkpoints, get_validators, @@ -28,22 +26,12 @@ w3 = Web3() -def format_ether(value: Union[str, int, Wei], sign=STAKED_TOKEN_SYMBOL) -> str: - """Converts Wei value.""" - _value = int(value) - if _value < 0: - formatted_value = f'-{Web3.fromWei(abs(_value), "ether")}' - else: - formatted_value = f'{Web3.fromWei(_value, "ether")}' - - return f"{formatted_value} {sign}" if sign else formatted_value - - class RewardsController(object): """Updates total rewards and activated validators number.""" def __init__( self, + network: str, aiohttp_session: ClientSession, genesis_timestamp: int, oracle: LocalAccount, @@ -52,6 +40,13 @@ def __init__( self.aiohttp_session = aiohttp_session self.genesis_timestamp = genesis_timestamp self.oracle = oracle + self.network = network + self.sync_period = NETWORKS[network]["SYNC_PERIOD"] + self.slots_per_epoch = NETWORKS[network]["SLOTS_PER_EPOCH"] + self.seconds_per_epoch = ( + self.slots_per_epoch * NETWORKS[network]["SECONDS_PER_SLOT"] + ) + self.deposit_token_symbol = NETWORKS[network]["DEPOSIT_TOKEN_SYMBOL"] self.last_vote_total_rewards = None async def process( @@ -65,17 +60,19 @@ async def process( last_update_time = datetime.utcfromtimestamp( voting_params["rewards_updated_at_timestamp"] ) - next_update_time: datetime = last_update_time + SYNC_PERIOD + next_update_time: datetime = last_update_time + self.sync_period current_time: datetime = datetime.utcfromtimestamp(current_timestamp) - while next_update_time + SYNC_PERIOD <= current_time: - next_update_time += SYNC_PERIOD + while next_update_time + self.sync_period <= current_time: + next_update_time += self.sync_period # skip submitting vote if too early or vote has been already submitted if next_update_time > current_time: return # fetch pool validator BLS public keys - public_keys = await get_registered_validators_public_keys(current_block_number) + public_keys = await get_registered_validators_public_keys( + self.network, current_block_number + ) # calculate current ETH2 epoch update_timestamp = int( @@ -83,26 +80,31 @@ async def process( ) update_epoch: int = ( update_timestamp - self.genesis_timestamp - ) // SECONDS_PER_EPOCH + ) // self.seconds_per_epoch logger.info( - f"Voting for new total rewards with parameters:" + f"[{self.network}] Voting for new total rewards with parameters:" f" timestamp={update_timestamp}, epoch={update_epoch}" ) # wait for the epoch to get finalized - checkpoints = await get_finality_checkpoints(self.aiohttp_session) + checkpoints = await get_finality_checkpoints(self.network, self.aiohttp_session) while update_epoch > int(checkpoints["finalized"]["epoch"]): - logger.info(f"Waiting for the epoch {update_epoch} to finalize...") + logger.info( + f"[{self.network}] Waiting for the epoch {update_epoch} to finalize..." + ) await asyncio.sleep(360) - checkpoints = await get_finality_checkpoints(self.aiohttp_session) + checkpoints = await get_finality_checkpoints( + self.network, self.aiohttp_session + ) - state_id = str(update_epoch * SLOTS_PER_EPOCH) + state_id = str(update_epoch * self.slots_per_epoch) total_rewards: Wei = Wei(0) activated_validators = 0 # fetch balances in chunks of 100 keys for i in range(0, len(public_keys), 100): validators = await get_validators( + network=self.network, session=self.aiohttp_session, public_keys=public_keys[i : i + 100], state_id=state_id, @@ -116,26 +118,24 @@ async def process( Web3.toWei(validator["balance"], "gwei") - self.deposit_amount ) - pretty_total_rewards = format_ether(total_rewards) - log_msg = f"Retrieved pool validator rewards: total={pretty_total_rewards}" - - if self.last_vote_total_rewards is not None: - log_msg += f", since last vote={format_ether((total_rewards - self.last_vote_total_rewards))}" - logger.info(log_msg) + pretty_total_rewards = self.format_ether(total_rewards) + logger.info( + f"[{self.network}] Retrieved pool validator rewards: total={pretty_total_rewards}" + ) if total_rewards < voting_params["total_rewards"]: # rewards were reduced -> don't mint new ones logger.warning( - f"Total rewards decreased since the previous update:" + f"[{self.network}] Total rewards decreased since the previous update:" f" current={pretty_total_rewards}," - f' previous={format_ether(voting_params["total_rewards"])}' + f' previous={self.format_ether(voting_params["total_rewards"])}' ) total_rewards = voting_params["total_rewards"] - pretty_total_rewards = format_ether(total_rewards) + pretty_total_rewards = self.format_ether(total_rewards) # submit vote logger.info( - f"Submitting rewards vote:" + f"[{self.network}] Submitting rewards vote:" f" nonce={voting_params['rewards_nonce']}," f" total rewards={pretty_total_rewards}," f" activated validators={activated_validators}" @@ -153,11 +153,22 @@ async def process( total_rewards=str(total_rewards), ) submit_vote( + network=self.network, oracle=self.oracle, encoded_data=encoded_data, vote=vote, name=REWARD_VOTE_FILENAME, ) - logger.info("Rewards vote has been successfully submitted") + logger.info(f"[{self.network}] Rewards vote has been successfully submitted") self.last_vote_total_rewards = total_rewards + + def format_ether(self, value: Union[str, int, Wei]) -> str: + """Converts Wei value.""" + _value = int(value) + if _value < 0: + formatted_value = f'-{Web3.fromWei(abs(_value), "ether")}' + else: + formatted_value = f'{Web3.fromWei(_value, "ether")}' + + return f"{formatted_value} {self.deposit_token_symbol}" diff --git a/oracle/oracle/rewards/eth1.py b/oracle/oracle/rewards/eth1.py index 10a2b2a..5c8b128 100644 --- a/oracle/oracle/rewards/eth1.py +++ b/oracle/oracle/rewards/eth1.py @@ -9,11 +9,13 @@ async def get_registered_validators_public_keys( + network: str, block_number: BlockNumber, ) -> RegisteredValidatorsPublicKeys: """Fetches pool validators public keys.""" last_id = "" result: Dict = await execute_sw_gql_query( + network=network, query=REGISTERED_VALIDATORS_QUERY, variables=dict(block_number=block_number, last_id=last_id), ) @@ -24,6 +26,7 @@ async def get_registered_validators_public_keys( while len(validators_chunk) >= 1000: last_id = validators_chunk[-1]["id"] result: Dict = await execute_sw_gql_query( + network=network, query=REGISTERED_VALIDATORS_QUERY, variables=dict(block_number=block_number, last_id=last_id), ) diff --git a/oracle/oracle/rewards/eth2.py b/oracle/oracle/rewards/eth2.py index 7e8dfc5..22603a7 100644 --- a/oracle/oracle/rewards/eth2.py +++ b/oracle/oracle/rewards/eth2.py @@ -5,13 +5,7 @@ from aiohttp import ClientSession from eth_typing import HexStr -from oracle.oracle.settings import ( - ETH2_CLIENT, - ETH2_ENDPOINT, - GNOSIS, - LIGHTHOUSE, - NETWORK, -) +from oracle.networks import NETWORKS class ValidatorStatus(Enum): @@ -30,22 +24,14 @@ class ValidatorStatus(Enum): PENDING_STATUSES = [ValidatorStatus.PENDING_INITIALIZED, ValidatorStatus.PENDING_QUEUED] -if NETWORK == GNOSIS: - SLOTS_PER_EPOCH = 16 - SECONDS_PER_SLOT = 5 -else: - SLOTS_PER_EPOCH = 32 - SECONDS_PER_SLOT = 12 - -SECONDS_PER_EPOCH = SECONDS_PER_SLOT * SLOTS_PER_EPOCH - @backoff.on_exception(backoff.expo, Exception, max_time=900) async def get_finality_checkpoints( - session: ClientSession, state_id: str = "head" + network: str, session: ClientSession, state_id: str = "head" ) -> Dict: """Fetches finality checkpoints.""" - endpoint = f"{ETH2_ENDPOINT}/eth/v1/beacon/states/{state_id}/finality_checkpoints" + network_config = NETWORKS[network] + endpoint = f"{network_config['ETH2_ENDPOINT']}/eth/v1/beacon/states/{state_id}/finality_checkpoints" async with session.get(endpoint) as response: response.raise_for_status() return (await response.json())["data"] @@ -53,17 +39,17 @@ async def get_finality_checkpoints( @backoff.on_exception(backoff.expo, Exception, max_time=900) async def get_validators( - session: ClientSession, public_keys: List[HexStr], state_id: str = "head" + network: str, + session: ClientSession, + public_keys: List[HexStr], + state_id: str = "head", ) -> List[Dict]: """Fetches validators.""" if not public_keys: return [] - # TODO: remove once https://github.com/gnosischain/gbc-lighthouse updated to 2.1.1 - if ETH2_CLIENT == LIGHTHOUSE: - endpoint = f"{ETH2_ENDPOINT}/eth/v1/beacon/states/{state_id}/validators?id={','.join(public_keys)}" - else: - endpoint = f"{ETH2_ENDPOINT}/eth/v1/beacon/states/{state_id}/validators?id={'&id='.join(public_keys)}" + _endpoint = NETWORKS[network]["ETH2_ENDPOINT"] + endpoint = f"{_endpoint}/eth/v1/beacon/states/{state_id}/validators?id={'&id='.join(public_keys)}" async with session.get(endpoint) as response: response.raise_for_status() @@ -71,9 +57,10 @@ async def get_validators( @backoff.on_exception(backoff.expo, Exception, max_time=900) -async def get_genesis(session: ClientSession) -> Dict: +async def get_genesis(network: str, session: ClientSession) -> Dict: """Fetches beacon chain genesis.""" - endpoint = f"{ETH2_ENDPOINT}/eth/v1/beacon/genesis" + network_config = NETWORKS[network] + endpoint = f"{network_config['ETH2_ENDPOINT']}/eth/v1/beacon/genesis" async with session.get(endpoint) as response: response.raise_for_status() return (await response.json())["data"] diff --git a/oracle/oracle/settings.py b/oracle/oracle/settings.py deleted file mode 100644 index 988970a..0000000 --- a/oracle/oracle/settings.py +++ /dev/null @@ -1,144 +0,0 @@ -from datetime import timedelta - -from decouple import Choices, Csv, config -from eth_typing import HexStr -from web3 import Web3 - -from oracle.common.settings import GNOSIS, GOERLI, MAINNET, NETWORK - -IPFS_PIN_ENDPOINTS = config( - "IPFS_PIN_ENDPOINTS", - cast=Csv(), - default="/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http", -) -IPFS_FETCH_ENDPOINTS = config( - "IPFS_FETCH_ENDPOINTS", - cast=Csv(), - default="https://gateway.pinata.cloud,http://cloudflare-ipfs.com,https://ipfs.io", -) - -# extra pins to pinata for redundancy -IPFS_PINATA_PIN_ENDPOINT = config( - "IPFS_PINATA_ENDPOINT", default="https://api.pinata.cloud/pinning/pinJSONToIPFS" -) -IPFS_PINATA_API_KEY = config("IPFS_PINATA_API_KEY", default="") -IPFS_PINATA_SECRET_KEY = config( - "IPFS_PINATA_SECRET_KEY", - default="", -) - -# ETH2 settings -ETH2_ENDPOINT = config("ETH2_ENDPOINT", default="http://localhost:3501") - -# TODO: remove once https://github.com/gnosischain/gbc-lighthouse updated to 2.1.1 -LIGHTHOUSE = "lighthouse" -PRYSM = "prysm" -TEKU = "teku" -ETH2_CLIENT = config( - "ETH2_CLIENT", - default=PRYSM, - cast=Choices([LIGHTHOUSE, PRYSM, TEKU], cast=lambda client: client.lower()), -) - -# credentials -ORACLE_PRIVATE_KEY = config("ORACLE_PRIVATE_KEY", default="") - -# S3 credentials -AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID", default="") -AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY", default="") - -ORACLE_PROCESS_INTERVAL = config("ORACLE_PROCESS_INTERVAL", default=10, cast=int) - -if NETWORK == MAINNET: - SYNC_PERIOD = timedelta(days=1) - SWISE_TOKEN_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2" - ) - REWARD_TOKEN_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x20BC832ca081b91433ff6c17f85701B6e92486c5" - ) - STAKED_TOKEN_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0xFe2e637202056d30016725477c5da089Ab0A043A" - ) - DISTRIBUTOR_FALLBACK_ADDRESS = Web3.toChecksumAddress( - "0x144a98cb1CdBb23610501fE6108858D9B7D24934" - ) - RARI_FUSE_POOL_ADDRESSES = [ - Web3.toChecksumAddress("0x18F49849D20Bc04059FE9d775df9a38Cd1f5eC9F"), - Web3.toChecksumAddress("0x83d534Ab1d4002249B0E6d22410b62CF31978Ca2"), - ] - WITHDRAWAL_CREDENTIALS: HexStr = HexStr( - "0x0100000000000000000000002296e122c1a20fca3cac3371357bdad3be0df079" - ) - STAKEWISE_SUBGRAPH_URL = config( - "STAKEWISE_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/stakewise-mainnet", - ) - UNISWAP_V3_SUBGRAPH_URL = config( - "UNISWAP_V3_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-mainnet", - ) - ETHEREUM_SUBGRAPH_URL = config( - "ETHEREUM_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/ethereum-mainnet", - ) - RARI_FUSE_SUBGRAPH_URL = config( - "RARI_FUSE_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/rari-fuse-mainnet", - ) - STAKED_TOKEN_SYMBOL = "ETH" -# TODO: fix addresses once gnosis deployed -elif NETWORK == GNOSIS: - SYNC_PERIOD = timedelta(days=1) - SWISE_TOKEN_CONTRACT_ADDRESS = "" - REWARD_TOKEN_CONTRACT_ADDRESS = "" - STAKED_TOKEN_CONTRACT_ADDRESS = "" - DISTRIBUTOR_FALLBACK_ADDRESS = "" - RARI_FUSE_POOL_ADDRESSES = [] - WITHDRAWAL_CREDENTIALS: HexStr = HexStr("") - STAKEWISE_SUBGRAPH_URL = config( - "STAKEWISE_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/stakewise-gnosis", - ) - # TODO: update once uniswap v3 is deployed to gnosis chain - UNISWAP_V3_SUBGRAPH_URL = config("UNISWAP_V3_SUBGRAPH_URL", default="") - ETHEREUM_SUBGRAPH_URL = config( - "ETHEREUM_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/ethereum-gnosis", - ) - # TODO: update once rari fuse pools is deployed to gnosis chain - RARI_FUSE_SUBGRAPH_URL = config("RARI_FUSE_SUBGRAPH_URL", default="") - STAKED_TOKEN_SYMBOL = "mGNO" -elif NETWORK == GOERLI: - SYNC_PERIOD = timedelta(hours=1) - SWISE_TOKEN_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x0e2497aACec2755d831E4AFDEA25B4ef1B823855" - ) - REWARD_TOKEN_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x826f88d423440c305D9096cC1581Ae751eFCAfB0" - ) - STAKED_TOKEN_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x221D9812823DBAb0F1fB40b0D294D9875980Ac19" - ) - DISTRIBUTOR_FALLBACK_ADDRESS = Web3.toChecksumAddress( - "0x1867c96601bc5fE24F685d112314B8F3Fe228D5A" - ) - RARI_FUSE_POOL_ADDRESSES = [] - WITHDRAWAL_CREDENTIALS: HexStr = HexStr( - "0x010000000000000000000000040f15c6b5bfc5f324ecab5864c38d4e1eef4218" - ) - STAKEWISE_SUBGRAPH_URL = config( - "STAKEWISE_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/stakewise-goerli", - ) - UNISWAP_V3_SUBGRAPH_URL = config( - "UNISWAP_V3_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/uniswap-v3-goerli", - ) - ETHEREUM_SUBGRAPH_URL = config( - "ETHEREUM_SUBGRAPH_URL", - default="https://api.thegraph.com/subgraphs/name/stakewise/ethereum-goerli", - ) - # TODO: update once rari fuse pools is deployed to goerli chain - RARI_FUSE_SUBGRAPH_URL = config("RARI_FUSE_SUBGRAPH_URL", default="") - STAKED_TOKEN_SYMBOL = "ETH" diff --git a/oracle/oracle/validators/controller.py b/oracle/oracle/validators/controller.py index 9908d1c..cbc59c4 100644 --- a/oracle/oracle/validators/controller.py +++ b/oracle/oracle/validators/controller.py @@ -6,9 +6,9 @@ from web3 import Web3 from web3.types import Wei -from oracle.common.settings import VALIDATOR_VOTE_FILENAME +from oracle.oracle.eth1 import submit_vote +from oracle.settings import VALIDATOR_VOTE_FILENAME -from ..eth1 import submit_vote from .eth1 import ( get_validators_deposit_root, get_voting_parameters, @@ -24,7 +24,8 @@ class ValidatorsController(object): """Submits new validators registrations to the IPFS.""" - def __init__(self, oracle: LocalAccount) -> None: + def __init__(self, network: str, oracle: LocalAccount) -> None: + self.network = network self.validator_deposit: Wei = Web3.toWei(32, "ether") self.last_vote_public_key = None self.last_vote_validators_deposit_root = None @@ -32,24 +33,30 @@ def __init__(self, oracle: LocalAccount) -> None: async def process(self) -> None: """Process validators registration.""" - voting_params = await get_voting_parameters() + voting_params = await get_voting_parameters(self.network) latest_block_number = voting_params["latest_block_number"] pool_balance = voting_params["pool_balance"] if pool_balance < self.validator_deposit: # not enough balance to register next validator return - while not (await has_synced_block(latest_block_number)): + while not (await has_synced_block(self.network, latest_block_number)): await asyncio.sleep(5) # select next validator # TODO: implement scoring system based on the operators performance - validator_deposit_data = await select_validator(latest_block_number) + validator_deposit_data = await select_validator( + self.network, latest_block_number + ) if validator_deposit_data is None: - logger.warning("Failed to find the next validator to register") + logger.warning( + f"[{self.network}] Failed to find the next validator to register" + ) return - validators_deposit_root = await get_validators_deposit_root(latest_block_number) + validators_deposit_root = await get_validators_deposit_root( + self.network, latest_block_number + ) public_key = validator_deposit_data["public_key"] if ( self.last_vote_validators_deposit_root == validators_deposit_root @@ -72,16 +79,17 @@ async def process(self) -> None: **validator_deposit_data, ) logger.info( - f"Voting for the next validator: operator={operator}, public key={public_key}" + f"[{self.network}] Voting for the next validator: operator={operator}, public key={public_key}" ) submit_vote( + network=self.network, oracle=self.oracle, encoded_data=encoded_data, vote=vote, name=VALIDATOR_VOTE_FILENAME, ) - logger.info("Submitted validator registration vote") + logger.info(f"[{self.network}] Submitted validator registration vote") # skip voting for the same validator and validators deposit root in the next check self.last_vote_public_key = public_key diff --git a/oracle/oracle/validators/eth1.py b/oracle/oracle/validators/eth1.py index 2d4fbfd..64206b2 100644 --- a/oracle/oracle/validators/eth1.py +++ b/oracle/oracle/validators/eth1.py @@ -20,9 +20,10 @@ from .types import ValidatorDepositData, ValidatorVotingParameters -async def get_voting_parameters() -> ValidatorVotingParameters: +async def get_voting_parameters(network: str) -> ValidatorVotingParameters: """Fetches validator voting parameters.""" result: Dict = await execute_sw_gql_query( + network=network, query=VALIDATOR_VOTING_PARAMETERS_QUERY, variables={}, ) @@ -37,10 +38,12 @@ async def get_voting_parameters() -> ValidatorVotingParameters: async def select_validator( + network: str, block_number: BlockNumber, ) -> Union[None, ValidatorDepositData]: """Selects the next validator to register.""" result: Dict = await execute_sw_gql_query( + network=network, query=OPERATORS_QUERY, variables=dict(block_number=block_number), ) @@ -60,7 +63,7 @@ async def select_validator( selected_deposit_data = deposit_datum[deposit_data_index] can_register = await can_register_validator( - block_number, selected_deposit_data["public_key"] + network, block_number, selected_deposit_data["public_key"] ) while deposit_data_index < max_deposit_data_index and not can_register: # the edge case when the validator was registered in previous merkle root @@ -68,7 +71,7 @@ async def select_validator( deposit_data_index += 1 selected_deposit_data = deposit_datum[deposit_data_index] can_register = await can_register_validator( - block_number, selected_deposit_data["public_key"] + network, block_number, selected_deposit_data["public_key"] ) if can_register: @@ -82,9 +85,12 @@ async def select_validator( ) -async def can_register_validator(block_number: BlockNumber, public_key: HexStr) -> bool: +async def can_register_validator( + network: str, block_number: BlockNumber, public_key: HexStr +) -> bool: """Checks whether it's safe to register the validator.""" result: Dict = await execute_ethereum_gql_query( + network=network, query=VALIDATOR_REGISTRATIONS_QUERY, variables=dict(block_number=block_number, public_key=public_key), ) @@ -93,8 +99,9 @@ async def can_register_validator(block_number: BlockNumber, public_key: HexStr) return len(registrations) == 0 -async def has_synced_block(block_number: BlockNumber) -> bool: +async def has_synced_block(network: str, block_number: BlockNumber) -> bool: result: Dict = await execute_ethereum_gql_query( + network=network, query=VALIDATOR_REGISTRATIONS_SYNC_BLOCK_QUERY, variables={}, ) @@ -103,9 +110,12 @@ async def has_synced_block(block_number: BlockNumber) -> bool: return block_number <= BlockNumber(int(meta["block"]["number"])) -async def get_validators_deposit_root(block_number: BlockNumber) -> HexStr: +async def get_validators_deposit_root( + network: str, block_number: BlockNumber +) -> HexStr: """Fetches validators deposit root for protecting against operator submitting deposit prior to registration.""" result: Dict = await execute_ethereum_gql_query( + network=network, query=VALIDATOR_REGISTRATIONS_LATEST_INDEX_QUERY, variables=dict(block_number=block_number), ) diff --git a/oracle/settings.py b/oracle/settings.py new file mode 100644 index 0000000..f4e8a91 --- /dev/null +++ b/oracle/settings.py @@ -0,0 +1,54 @@ +from decouple import Csv, config + +from oracle.networks import ETHEREUM_MAINNET + +# common +LOG_LEVEL = config("LOG_LEVEL", default="INFO") + +ENABLED_NETWORKS = config( + "ENABLED_NETWORKS", + default=ETHEREUM_MAINNET, + cast=Csv(), +) + +REWARD_VOTE_FILENAME = "reward-vote.json" +DISTRIBUTOR_VOTE_FILENAME = "distributor-vote.json" +VALIDATOR_VOTE_FILENAME = "validator-vote.json" +TEST_VOTE_FILENAME = "test-vote.json" + +# health server settings +ENABLE_HEALTH_SERVER = config("ENABLE_HEALTH_SERVER", default=False, cast=bool) +HEALTH_SERVER_PORT = config("HEALTH_SERVER_PORT", default=8080, cast=int) +HEALTH_SERVER_HOST = config("HEALTH_SERVER_HOST", default="127.0.0.1", cast=str) + +# required confirmation blocks +CONFIRMATION_BLOCKS: int = config("CONFIRMATION_BLOCKS", default=15, cast=int) + +# oracle +ORACLE_PROCESS_INTERVAL = config("ORACLE_PROCESS_INTERVAL", default=10, cast=int) + +IPFS_PIN_ENDPOINTS = config( + "IPFS_PIN_ENDPOINTS", + cast=Csv(), + default="/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http", +) +IPFS_FETCH_ENDPOINTS = config( + "IPFS_FETCH_ENDPOINTS", + cast=Csv(), + default="https://gateway.pinata.cloud,http://cloudflare-ipfs.com,https://ipfs.io", +) + +# extra pins to pinata for redundancy +IPFS_PINATA_PIN_ENDPOINT = config( + "IPFS_PINATA_ENDPOINT", default="https://api.pinata.cloud/pinning/pinJSONToIPFS" +) +IPFS_PINATA_API_KEY = config("IPFS_PINATA_API_KEY", default="") +IPFS_PINATA_SECRET_KEY = config( + "IPFS_PINATA_SECRET_KEY", + default="", +) + +# keeper +KEEPER_PROCESS_INTERVAL = config("KEEPER_PROCESS_INTERVAL", default=10, cast=int) + +TRANSACTION_TIMEOUT = config("TRANSACTION_TIMEOUT", default=900, cast=int) diff --git a/oracle/utils.py b/oracle/utils.py new file mode 100644 index 0000000..501bd63 --- /dev/null +++ b/oracle/utils.py @@ -0,0 +1,63 @@ +import logging +import signal +from typing import Any, Dict, List + +from eth_account import Account +from eth_account.signers.local import LocalAccount + +from oracle.networks import NETWORKS +from oracle.oracle.clients import execute_sw_gql_query +from oracle.oracle.graphql_queries import ORACLE_QUERY +from oracle.settings import ENABLED_NETWORKS + +logger = logging.getLogger(__name__) + + +class InterruptHandler: + """ + Tracks SIGINT and SIGTERM signals. + https://stackoverflow.com/a/31464349 + """ + + exit = False + + def __init__(self) -> None: + signal.signal(signal.SIGINT, self.exit_gracefully) + signal.signal(signal.SIGTERM, self.exit_gracefully) + + # noinspection PyUnusedLocal + def exit_gracefully(self, signum: int, frame: Any) -> None: + logger.info(f"Received interrupt signal {signum}, exiting...") + self.exit = True + + +async def check_oracle_account(network: str, oracle: LocalAccount) -> None: + """Checks whether oracle is part of the oracles set.""" + oracle_lowered_address = oracle.address.lower() + result: List = ( + await execute_sw_gql_query( + network=network, + query=ORACLE_QUERY, + variables=dict( + oracle_address=oracle_lowered_address, + ), + ) + ).get("oracles", []) + if result and result[0].get("id", "") == oracle_lowered_address: + logger.info(f"[{network}] Oracle {oracle.address} is part of the oracles set") + else: + logger.warning( + f"[{network}] NB! Oracle {oracle.address} is not part of the oracles set." + f" Please create DAO proposal to include it." + ) + + +async def get_oracle_accounts() -> Dict[str, LocalAccount]: + """Create oracle and verify oracle accounts.""" + oracle_accounts = {} + for network in ENABLED_NETWORKS: + oracle = Account.from_key(NETWORKS[network]["ORACLE_PRIVATE_KEY"]) + oracle_accounts[network] = oracle + await check_oracle_account(network, oracle) + + return oracle_accounts