diff --git a/.gitignore b/.gitignore index 49afb2f8..907f72cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ target .bsp .idea +docker/data +docker/logs +docker/secrets.env diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..a274f9c8 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,35 @@ +# Launching the Units Network node +Units Network node consists of Waves blockchain node, Consensus Client extension, and an execution client (either [besu](https://besu.hyperledger.org) or [geth](https://geth.ethereum.org)). This directory contains sample docker compose files for running the node. + +## Prerequisites +* Install [Docker Compose](https://docs.docker.com/compose/install/). +* Generate JWT secret and execution client keys by running `./gen-keys.sh`. This script requires `openssl` and `xxd`. +* Optional: get waves node [state](https://docs.waves.tech/en/waves-node/options-for-getting-actual-blockchain/state-downloading-and-applying) and place it inside the `./data/waves` directory. +* Optional: get execution client state. + +## Configuring Waves Node +* Create `./secrets.env` file with the base58-encoded [seed and password](https://docs.waves.tech/en/waves-node/how-to-work-with-node-wallet): + ``` + WAVES_WALLET_SEED= + WAVES_WALLET_PASSWORD= + ``` + This wallet seed will be used for mining both waves and ethereum blocks, so make sure it's the correct one. +* Specify the proper declared addresses in the environment file (`testnet.env` for testnet, etc.). Make sure these declared addresses have distinct ports, otherwise your node will be banned from the network! + +## Launching +To run besu on Linux, you need to manually create data & log directories and set appropriate permissions: +``` +install -d -o 1000 -g 1000 data/besu logs/besu +``` +Running, stopping and updating with besu in testnet: +``` +docker compose --env-file=testnet.env up -d +docker compose --env-file=testnet.env down +docker compose --env-file=testnet.env pull +``` +Running, stopping and updating with geth in testnet: +``` +docker compose -f docker-compose-geth.yml --env-file=testnet.env up -d +docker compose -f docker-compose-geth.yml --env-file=testnet.env down +docker compose -f docker-compose-geth.yml --env-file=testnet.env pull +``` diff --git a/docker/docker-compose-geth.yml b/docker/docker-compose-geth.yml new file mode 100644 index 00000000..f74b3a74 --- /dev/null +++ b/docker/docker-compose-geth.yml @@ -0,0 +1,20 @@ +services: + geth-init: + extends: + file: ./services/geth.yml + service: geth-init + + geth: + extends: + file: ./services/geth.yml + service: geth + depends_on: + geth-init: + condition: service_completed_successfully + + waves-node: + extends: + file: ./services/waves-node.yml + service: waves-node + environment: + EXECUTION_CLIENT: geth diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..eebb3c26 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,11 @@ +services: + besu: + extends: + file: ./services/besu.yml + service: besu + waves-node: + extends: + file: ./services/waves-node.yml + service: waves-node + environment: + EXECUTION_CLIENT: besu diff --git a/docker/gen-keys.sh b/docker/gen-keys.sh new file mode 100755 index 00000000..c9aa23dd --- /dev/null +++ b/docker/gen-keys.sh @@ -0,0 +1,4 @@ +#!/bin/sh +mkdir -p data/secrets +openssl rand 32 | xxd -p -c 32 > data/secrets/p2p-key +openssl rand 32 | xxd -p -c 32 > data/secrets/jwtsecret diff --git a/docker/genesis-testnet.json b/docker/genesis-testnet.json new file mode 100644 index 00000000..a6a395b4 --- /dev/null +++ b/docker/genesis-testnet.json @@ -0,0 +1,38 @@ +{ + "alloc": {}, + "baseFeePerGas": "0x3b9aca00", + "blobGasUsed": null, + "coinbase": "0x0000000000000000000000000000000000000000", + "config": { + "arrowGlacierBlock": 0, + "berlinBlock": 0, + "byzantiumBlock": 0, + "cancunTime": 0, + "chainId": 88817, + "constantinopleBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "ethash": {}, + "grayGlacierBlock": 0, + "homesteadBlock": 0, + "istanbulBlock": 0, + "londonBlock": 0, + "muirGlacierBlock": 0, + "petersburgBlock": 0, + "shanghaiTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true + }, + "difficulty": "0x0", + "excessBlobGas": null, + "extraData": "0x", + "gasLimit": "0x1000000", + "gasUsed": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "number": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x660e5e00" +} diff --git a/docker/init-geth.sh b/docker/init-geth.sh new file mode 100755 index 00000000..39e25e74 --- /dev/null +++ b/docker/init-geth.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ ! -d /root/.ethereum/geth ] ; then + geth init /tmp/genesis.json +else + echo geth already initialized +fi diff --git a/docker/log4j2.xml b/docker/log4j2.xml new file mode 100644 index 00000000..800b4d1a --- /dev/null +++ b/docker/log4j2.xml @@ -0,0 +1,31 @@ + + + + + %date %-5level [%-25.25thread] %35.35c{1.} - %msg%n%throwable + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker/logback.xml b/docker/logback.xml new file mode 100644 index 00000000..08bd8909 --- /dev/null +++ b/docker/logback.xml @@ -0,0 +1,3 @@ + + + diff --git a/docker/services/besu.yml b/docker/services/besu.yml new file mode 100644 index 00000000..04fb24c2 --- /dev/null +++ b/docker/services/besu.yml @@ -0,0 +1,35 @@ +services: + besu: + container_name: besu + image: hyperledger/besu:latest + pull_policy: always + stop_grace_period: 5m + command: + - --logging=ALL + - --host-allowlist=* + - --rpc-http-enabled + - --rpc-http-api=ETH,NET,WEB3,TXPOOL,TRACE + - --rpc-http-cors-origins=all + - --rpc-ws-enabled + - --discovery-dns-url=enrtree://AIRIZFFZSCSIVHXTKA44WYZQJMR75FLTGWJ5TUNEW5IP7QKZDLBRK@${NETWORK}-nodes.unit0.dev + - --discovery-enabled=true + - --engine-rpc-enabled + - --engine-jwt-secret=/etc/secrets/jwtsecret + - --engine-host-allowlist=* + - --node-private-key-file=/etc/secrets/p2p-key + - --data-path=/var/lib/besu + - --genesis-file=/etc/besu/genesis.json + - --data-storage-format=BONSAI + - --network-id=${NETWORK_ID} + volumes: + - ../genesis-${NETWORK}.json:/etc/besu/genesis.json + - ../data/secrets:/etc/secrets:ro + - ../log4j2.xml:/etc/besu/log4j2.xml + - ../data/besu:/var/lib/besu + - ../logs/besu:/opt/besu/logs + ports: + - '30303:30303/tcp' + - '30303:30303/udp' + - '8545:8545' + environment: + - LOG4J_CONFIGURATION_FILE=/etc/besu/log4j2.xml diff --git a/docker/services/geth.yml b/docker/services/geth.yml new file mode 100644 index 00000000..3c7b7655 --- /dev/null +++ b/docker/services/geth.yml @@ -0,0 +1,48 @@ +services: + geth-init: + container_name: geth-init + image: ethereum/client-go:stable + entrypoint: /tmp/init-geth.sh + volumes: + - ../genesis-${NETWORK}.json:/tmp/genesis.json + - ../data/geth:/root/.ethereum + - ../init-geth.sh:/tmp/init-geth.sh + geth: + container_name: geth + image: ethereum/client-go:stable + pull_policy: always + stop_grace_period: 5m + command: + - --verbosity=4 + - --http + - --http.addr=0.0.0.0 + - --http.vhosts=* + - --http.api=eth,web3,txpool,net,debug,engine + - --http.corsdomain=* + - --ws + - --ws.addr=0.0.0.0 + - --ws.api=eth,web3,txpool,net,debug + - --ws.rpcprefix=/ + - --ws.origins=* + - --authrpc.addr=0.0.0.0 + - --authrpc.vhosts=* + - --discovery.dns=enrtree://AIRIZFFZSCSIVHXTKA44WYZQJMR75FLTGWJ5TUNEW5IP7QKZDLBRK@${NETWORK}-nodes.unit0.dev + - --networkid=88817 + - --authrpc.jwtsecret=/etc/secrets/jwtsecret + - --nodekey=/etc/secrets/p2p-key + logging: + driver: local + options: + max-size: 1g + max-file: 5 + volumes: + - ../data/secrets:/etc/secrets:ro + - ../data/geth:/root/.ethereum + ports: + - '30303:30303/tcp' + - '30303:30303/udp' + healthcheck: + test: 'wget -qO /dev/null --header "content-type: application/json" --post-data {\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1} http://127.0.0.1:8545' + interval: 5s + timeout: 1s + retries: 10 diff --git a/docker/services/waves-node.yml b/docker/services/waves-node.yml new file mode 100644 index 00000000..a1921bf5 --- /dev/null +++ b/docker/services/waves-node.yml @@ -0,0 +1,20 @@ +services: + waves-node: + container_name: waves-node + image: ghcr.io/unitsnetwork/consensus-client:${WAVES_NODE_TAG:-${NETWORK}} + stop_grace_period: 5m + ports: + - "6869:6869" + - "6868:6868" + - "6865:6865" + environment: + - JAVA_OPTS=-Dwaves.config.directory=/etc/waves -Dlogback.file.level=TRACE -Dwaves.blockchain.type=$NETWORK + env_file: + - path: ../secrets.env + required: false + volumes: + - ../data/secrets:/etc/secrets:ro + - ../data/waves:/var/lib/waves/data + - ../waves-${NETWORK}.conf:/etc/waves/waves.conf:ro + - ../logback.xml:/etc/waves/logback.xml + - ../logs/waves:/var/log/waves diff --git a/docker/testnet.env b/docker/testnet.env new file mode 100644 index 00000000..be67a404 --- /dev/null +++ b/docker/testnet.env @@ -0,0 +1,5 @@ +NETWORK=testnet +NETWORK_ID=88817 +WAVES_NODE_TAG=L2-test +WAVES_DECLARED_ADDRESS=1.2.3.4:6868 +UNITS_DECLARED_ADDRESS=1.2.3.4:6865 diff --git a/docker/waves-testnet.conf b/docker/waves-testnet.conf new file mode 100644 index 00000000..c1640323 --- /dev/null +++ b/docker/waves-testnet.conf @@ -0,0 +1,39 @@ +waves { + extensions = [ + units.ConsensusClient + ] + + wallet { + seed = ${WAVES_WALLET_SEED} + password = ${WAVES_WALLET_PASSWORD} + } + + network { + bind-address = "0.0.0.0" + port = 6868 + declared-address = ${?WAVES_DECLARED_ADDRESS} + } + + rest-api { + enable = yes + bind-address = "0.0.0.0" + port = 6869 + api-key-hash = ${?WAVES_API_KEY_HASH} + } + + l2 { + chain-contract = 3MsqKJ6o1ABE37676cHHBxJRs6huYTt72ch + execution-client-address = "http://${EXECUTION_CLIENT}:8551" + jwt-secret-file = /etc/secrets/jwtsecret + + network { + port = 6865 + declared-address = ${?UNITS_DECLARED_ADDRESS} + known-peers = [ + "testnet-l2-htz-hel1-2.wavesnodes.com:6865" + "testnet-htz-nbg1-1.wavesnodes.com:6865" + ] + } + mining-enable = no + } +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 361f6a1b..14dc35d0 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -2,14 +2,12 @@ waves { l2 { chain-contract = "" - execution-client-address = "127.0.0.1" - engine-api-port = 8551 - http-api-port = 8545 + execution-client-address = "http://127.0.0.1:8551" api-request-retries = 2 api-request-retry-wait-time = 2s block-delay = 6s - block-sync-request-timeout = 2s + block-sync-request-timeout = 500ms network = ${waves.network} network { diff --git a/src/main/scala/units/ClientConfig.scala b/src/main/scala/units/ClientConfig.scala index 58668bae..f77b5d3a 100644 --- a/src/main/scala/units/ClientConfig.scala +++ b/src/main/scala/units/ClientConfig.scala @@ -13,8 +13,6 @@ import scala.concurrent.duration.FiniteDuration case class ClientConfig( chainContract: String, executionClientAddress: String, - engineApiPort: Int, - httpApiPort: Int, apiRequestRetries: Int, apiRequestRetryWaitTime: FiniteDuration, blockDelay: FiniteDuration, diff --git a/src/main/scala/units/ConsensusClient.scala b/src/main/scala/units/ConsensusClient.scala index 216cf42c..00d5fc1d 100644 --- a/src/main/scala/units/ConsensusClient.scala +++ b/src/main/scala/units/ConsensusClient.scala @@ -110,19 +110,18 @@ class ConsensusClientDependencies(context: ExtensionContext) extends AutoCloseab val eluScheduler: SchedulerService = Scheduler.singleThread("el-updater", reporter = { e => log.warn("Exception in ELUpdater", e) }) private val httpClientBackend = new LoggingBackend(HttpClientSyncBackend()) - val engineApiClient = new HttpEngineApiClient( - config, - config.jwtSecretFile match { - case Some(secretFile) => - val src = Source.fromFile(secretFile) - try new JwtAuthenticationBackend(src.getLines().next(), httpClientBackend) - finally src.close() - case _ => - log.warn("JWT secret is not set") - httpClientBackend - } - ) - val httpApiClient = new HttpEcApiClient(config, httpClientBackend) + private val maybeAuthenticatedBackend = config.jwtSecretFile match { + case Some(secretFile) => + val src = Source.fromFile(secretFile) + try new JwtAuthenticationBackend(src.getLines().next(), httpClientBackend) + finally src.close() + case _ => + log.warn("JWT secret is not set") + httpClientBackend + } + + val engineApiClient = new HttpEngineApiClient(config, maybeAuthenticatedBackend) + val httpApiClient = new HttpEcApiClient(config, maybeAuthenticatedBackend) val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE) val peerDatabase = new PeerDatabaseImpl(config.network) diff --git a/src/main/scala/units/client/engine/HttpEngineApiClient.scala b/src/main/scala/units/client/engine/HttpEngineApiClient.scala index d1fadc95..12c1a0cb 100644 --- a/src/main/scala/units/client/engine/HttpEngineApiClient.scala +++ b/src/main/scala/units/client/engine/HttpEngineApiClient.scala @@ -9,12 +9,13 @@ import units.eth.EthAddress import units.{BlockHash, ClientConfig, ClientError, Job} import play.api.libs.json.* import sttp.client3.* +import sttp.model.Uri import scala.concurrent.duration.{DurationInt, FiniteDuration} class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Identity, ?]) extends EngineApiClient with JsonRpcClient { - val apiUrl = uri"http://${config.executionClientAddress}:${config.engineApiPort}" + val apiUrl: Uri = Uri(config.executionClientAddress) def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): Job[String] = { sendEngineRequest[ForkChoiceUpdatedRequest, ForkChoiceUpdatedResponse](ForkChoiceUpdatedRequest(blockHash, finalizedBlockHash, None), BlockExecutionTimeout)