diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000000..f64e6c05804 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,59 @@ +# Use the official VS Code base image for dev containers +FROM mcr.microsoft.com/devcontainers/base:ubuntu + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + libssl-dev \ + pkg-config \ + clang \ + cmake \ + llvm \ + curl \ + gnupg \ + lsb-release \ + software-properties-common \ + unzip + +# Switch to clang +RUN rm /usr/bin/cc && ln -s /usr/bin/clang /usr/bin/cc + +# Install protoc - protobuf compiler +# The one shipped with Alpine does not work +ARG TARGETARCH +ARG PROTOC_VERSION=25.2 +RUN if [[ "$TARGETARCH" == "arm64" ]] ; then export PROTOC_ARCH=aarch_64; else export PROTOC_ARCH=x86_64; fi; \ + curl -Ls https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip \ + -o /tmp/protoc.zip && \ + unzip -qd /opt/protoc /tmp/protoc.zip && \ + rm /tmp/protoc.zip && \ + ln -s /opt/protoc/bin/protoc /usr/bin/ + +# Install protoc v25.2+ +RUN curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v25.2/protoc-25.2-linux-x86_64.zip \ + && unzip protoc-25.2-linux-x86_64.zip -d /usr/local \ + && rm protoc-25.2-linux-x86_64.zip + +# Switch to vscode user +USER vscode + +ENV CARGO_HOME=/home/vscode/.cargo +ENV PATH=$CARGO_HOME/bin:$PATH + +# TODO: It doesn't sharing PATH between stages, so we need "source $HOME/.cargo/env" everywhere +COPY rust-toolchain.toml . +RUN TOOLCHAIN_VERSION="$(grep channel rust-toolchain.toml | awk '{print $3}' | tr -d '"')" && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \ + --profile minimal \ + -y \ + --default-toolchain "${TOOLCHAIN_VERSION}" \ + --target wasm32-unknown-unknown + +# Install wasm-bindgen-cli in the same profile as other components, to sacrifice some performance & disk space to gain +# better build caching +RUN if [[ -z "${SCCACHE_MEMCACHED}" ]] ; then unset SCCACHE_MEMCACHED ; fi ; \ + RUSTFLAGS="-C target-feature=-crt-static" \ + # Meanwhile if you want to update wasm-bindgen you also need to update version in: + # - packages/wasm-dpp/Cargo.toml + # - packages/wasm-dpp/scripts/build-wasm.sh + cargo install wasm-bindgen-cli@0.2.86 --locked diff --git a/.devcontainer/devcontainer-build.json b/.devcontainer/devcontainer-build.json new file mode 100644 index 00000000000..df88d659340 --- /dev/null +++ b/.devcontainer/devcontainer-build.json @@ -0,0 +1,73 @@ +{ + "name": "Dash Platform Dev Container", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "settings": {}, + "extensions": [ + "arcanis.vscode-zipfs", + "chrmarti.regex", + "davidanson.vscode-markdownlint", + "ms-vscode.cmake-tools", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "zhangyue.rust-mod-generator", + "ms-azuretools.vscode-docker" + ] + } + }, + "remoteUser": "vscode", + "mounts": [ + { + "source": "devcontainer-platform-cargo-registry-index-${devcontainerId}", + "target": "/home/vscode/.cargo/registry", + "type": "volume" + }, + { + "source": "devcontainer-platform-cargo-registry-cache-${devcontainerId}", + "target": "/home/vscode/.cargo/registry/cache", + "type": "volume" + }, + { + "source": "devcontainer-platform-cargo-git-db-${devcontainerId}", + "target": "/home/vscode/.cargo/git/db", + "type": "volume" + }, + { + "source": "devcontainer-platform-target-${devcontainerId}", + "target": "${containerWorkspaceFolder}/target", + "type": "volume" + } + ], + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": 20, + "installYarnUsingApt": false + }, + "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/schlich/devcontainer-features/starship:0": {}, + }, + "postCreateCommand": { + "git-safe": "git config --global --add safe.directory ${containerWorkspaceFolder}", + "cargo-permissions": "sudo chown -R vscode:vscode /home/vscode/.cargo ${containerWorkspaceFolder}/target" + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..586571ba2b1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Dash Platform Dev Container", + "image": "ghcr.io/dashpay/platform/devcontainer:0.1.0" +} diff --git a/.github/workflows/prebuild-devcontainers.yml b/.github/workflows/prebuild-devcontainers.yml new file mode 100644 index 00000000000..794fa3d4a56 --- /dev/null +++ b/.github/workflows/prebuild-devcontainers.yml @@ -0,0 +1,58 @@ +name: Prebuild Dev Containers + +on: + push: + paths: + - '.devcontainer/**' + - '.github/workflows/prebuild-devcontainers.yml' + - rust-toolchain.toml + - Dockerfile + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and push devcontainer + runs-on: ["self-hosted", "linux", "x64", "ubuntu-platform"] + timeout-minutes: 60 + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install skopeo + run: | + sudo apt-get update + sudo apt-get install -y skopeo + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + with: + use: true + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: dashpay + password: ${{ secrets.GHCR_TOKEN }} + + - name: Build and push Platform devcontainer + uses: devcontainers/ci@v0.3 + with: + imageName: ghcr.io/dashpay/platform/devcontainer + imageTag: 0.1.0 + platform: linux/amd64,linux/arm64 + configFile: .devcontainer/devcontainer-build.json + push: always + cacheFrom: ghcr.io/dashpay/platform/devcontainer diff --git a/.yarn/cache/fsevents-patch-19706e7e35-10.zip b/.yarn/cache/fsevents-patch-19706e7e35-10.zip deleted file mode 100644 index aff1ab12ce5..00000000000 Binary files a/.yarn/cache/fsevents-patch-19706e7e35-10.zip and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 76bedf74c9d..3dd4e6d8930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +## [1.4.0-dev.1](https://github.com/dashpay/platform/compare/v1.3.0...v1.4.0-dev.1) (2024-09-27) + + +### ⚠ BREAKING CHANGES + +* **sdk:** change default network to mainnet (#2161) +* **dashmate:** confirm a node reset (#2160) +* **platform:** withdrawals polishing and fixes for mainnet (#2166) +* **platform:** do not switch to oldest quorums in validator set update (#2167) + +### Features + +* **dashmate:** confirm a node reset ([#2160](https://github.com/dashpay/platform/issues/2160)) +* **platform:** do not switch to oldest quorums in validator set update ([#2167](https://github.com/dashpay/platform/issues/2167)) +* **platform:** get current quorum info ([#2168](https://github.com/dashpay/platform/issues/2168)) +* **platform:** withdrawals polishing and fixes for mainnet ([#2166](https://github.com/dashpay/platform/issues/2166)) +* **sdk:** change default network to mainnet ([#2161](https://github.com/dashpay/platform/issues/2161)) + + +### Bug Fixes + +* **dapi:** getStatus cache invalidation ([#2155](https://github.com/dashpay/platform/issues/2155)) +* **dapi:** invalid mainnet seed ports ([#2173](https://github.com/dashpay/platform/issues/2173)) +* **dashmate:** cannot read properties of undefined (reading 'expires') ([#2164](https://github.com/dashpay/platform/issues/2164)) +* **dashmate:** colors[updated] is not a function ([#2157](https://github.com/dashpay/platform/issues/2157)) +* **dashmate:** doctor fails collecting to big logs ([#2158](https://github.com/dashpay/platform/issues/2158)) +* **dashmate:** port marks as closed if ipv6 is not disabled ([#2162](https://github.com/dashpay/platform/issues/2162)) +* **dashmate:** remove confusing short flag name ([#2165](https://github.com/dashpay/platform/issues/2165)) + + +### Continuous integration + +* build dashmate package on macos14 + + +### Documentation + +* **dashmate:** document logging configuration ([#2156](https://github.com/dashpay/platform/issues/2156)) + + +### Tests + +* **dashmate:** e2e tests failing due to DKG interval check ([#2171](https://github.com/dashpay/platform/issues/2171)) + + +### Miscellaneous Chores + +* **dashmate:** do not call mint on masternodes ([#2172](https://github.com/dashpay/platform/issues/2172)) +* **platform:** protocol version 4 creation ([#2153](https://github.com/dashpay/platform/issues/2153)) + + ## [1.3.0](https://github.com/dashpay/platform/compare/v1.2.0...v1.3.0) (2024-09-19) ### Features diff --git a/packages/dapi-grpc/build.rs b/packages/dapi-grpc/build.rs index 4bd36542e81..f70b685fbdc 100644 --- a/packages/dapi-grpc/build.rs +++ b/packages/dapi-grpc/build.rs @@ -47,7 +47,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { // Derive features for versioned messages // // "GetConsensusParamsRequest" is excluded as this message does not support proofs - const VERSIONED_REQUESTS: [&str; 29] = [ + const VERSIONED_REQUESTS: [&str; 30] = [ "GetDataContractHistoryRequest", "GetDataContractRequest", "GetDataContractsRequest", @@ -77,9 +77,13 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { "GetTotalCreditsInPlatformRequest", "GetEvonodesProposedEpochBlocksByIdsRequest", "GetEvonodesProposedEpochBlocksByRangeRequest", + "GetStatusRequest", ]; - // "GetConsensusParamsResponse" is excluded as this message does not support proofs + // The following responses are excluded as they don't support proofs: + // - "GetConsensusParamsResponse" + // - "GetStatusResponse" + // // "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests const VERSIONED_RESPONSES: [&str; 29] = [ "GetDataContractHistoryResponse", @@ -213,6 +217,7 @@ impl MappingConfig { /// /// * `protobuf_file` - Path to the protobuf file to use as input. /// * `out_dir` - Output directory where subdirectories for generated files will be created. + /// /// Depending on the features, either `client`, `server` or `client_server` subdirectory /// will be created inside `out_dir`. fn new(protobuf_file: PathBuf, out_dir: PathBuf) -> Self { diff --git a/packages/dapi/lib/externalApis/tenderdash/BlockchainListener.js b/packages/dapi/lib/externalApis/tenderdash/BlockchainListener.js index 6a0e1f5dc56..f6e3e7549fe 100644 --- a/packages/dapi/lib/externalApis/tenderdash/BlockchainListener.js +++ b/packages/dapi/lib/externalApis/tenderdash/BlockchainListener.js @@ -13,7 +13,12 @@ class BlockchainListener extends EventEmitter { */ constructor(tenderdashWsClient) { super(); + this.wsClient = tenderdashWsClient; + + this.processLogger = logger.child({ + process: 'BlockchainListener', + }); } /** @@ -30,14 +35,7 @@ class BlockchainListener extends EventEmitter { * Subscribe to blocks and transaction results */ start() { - const processLogger = logger.child({ - process: 'BlockchainListener', - }); - - processLogger.info('Subscribed to state transition results'); - // Emit transaction results - this.wsClient.subscribe(TX_QUERY); this.wsClient.on(TX_QUERY, (message) => { const [hashString] = (message.events || []).map((event) => { const hashAttribute = event.attributes.find((attribute) => attribute.key === 'hash'); @@ -53,15 +51,31 @@ class BlockchainListener extends EventEmitter { return; } - processLogger.trace(`received transaction result for ${hashString}`); + this.processLogger.trace(`Received transaction result for ${hashString}`); this.emit(BlockchainListener.getTransactionEventName(hashString), message); }); - // TODO: It's not using // Emit blocks and contained transactions - // this.wsClient.subscribe(NEW_BLOCK_QUERY); - // this.wsClient.on(NEW_BLOCK_QUERY, (message) => this.emit(EVENTS.NEW_BLOCK, message)); + this.wsClient.on(NEW_BLOCK_QUERY, (message) => { + this.processLogger.trace('Received new platform block'); + + this.emit(EVENTS.NEW_BLOCK, message); + }); + + this.wsClient.on('connect', () => { + this.#subscribe(); + }); + + if (this.wsClient.isConnected) { + this.#subscribe(); + } + } + + #subscribe() { + this.wsClient.subscribe(TX_QUERY); + this.wsClient.subscribe(NEW_BLOCK_QUERY); + this.processLogger.debug('Subscribed to platform blockchain events'); } } diff --git a/packages/dapi/lib/grpcServer/handlers/platform/getStatusHandlerFactory.js b/packages/dapi/lib/grpcServer/handlers/platform/getStatusHandlerFactory.js index 24249b044ee..af86ebb3120 100644 --- a/packages/dapi/lib/grpcServer/handlers/platform/getStatusHandlerFactory.js +++ b/packages/dapi/lib/grpcServer/handlers/platform/getStatusHandlerFactory.js @@ -9,6 +9,7 @@ const { } = require('@dashevo/dapi-grpc'); const BlockchainListener = require('../../../externalApis/tenderdash/BlockchainListener'); +const logger = require('../../../logger'); /** * @param {BlockchainListener} blockchainListener @@ -17,12 +18,23 @@ const BlockchainListener = require('../../../externalApis/tenderdash/BlockchainL * @return {getStatusHandler} */ function getStatusHandlerFactory(blockchainListener, driveClient, tenderdashRpcClient) { - // Clean cache when new platform block committed let cachedResponse = null; + let cleanCacheTimeout = null; - blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, () => { + function cleanCache() { cachedResponse = null; - }); + + // cancel scheduled cache cleanup + if (cleanCacheTimeout !== null) { + clearTimeout(cleanCacheTimeout); + cleanCacheTimeout = null; + } + + logger.trace({ endpoint: 'getStatus' }, 'cleanup cache'); + } + + // Clean cache when new platform block committed + blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, cleanCache); // DAPI Software version const packageJsonPath = path.resolve(__dirname, '..', '..', '..', '..', 'package.json'); @@ -210,6 +222,15 @@ function getStatusHandlerFactory(blockchainListener, driveClient, tenderdashRpcC cachedResponse = new GetStatusResponse(); cachedResponse.setV0(v0); + // Cancel any existing scheduled cache cleanup + if (cleanCacheTimeout !== null) { + clearTimeout(cleanCacheTimeout); + cleanCacheTimeout = null; + } + + // Clean cache in 3 minutes + cleanCacheTimeout = setTimeout(cleanCache, 3 * 60 * 1000); + return cachedResponse; } diff --git a/packages/dapi/test/integration/externalApis/tenderdash/BlockchainListener.spec.js b/packages/dapi/test/integration/externalApis/tenderdash/BlockchainListener.spec.js index cc2c5fdd7c8..b31b79e32b0 100644 --- a/packages/dapi/test/integration/externalApis/tenderdash/BlockchainListener.spec.js +++ b/packages/dapi/test/integration/externalApis/tenderdash/BlockchainListener.spec.js @@ -15,8 +15,8 @@ describe('BlockchainListener', () => { ({ sinon } = this); wsClientMock = new EventEmitter(); wsClientMock.subscribe = sinon.stub(); + blockchainListener = new BlockchainListener(wsClientMock); - blockchainListener.start(); sinon.spy(blockchainListener, 'on'); sinon.spy(blockchainListener, 'off'); @@ -84,19 +84,23 @@ describe('BlockchainListener', () => { }); describe('#start', () => { - it('should subscribe to transaction events from WS client', () => { - // TODO: We don't use it for now - // expect(wsClientMock.subscribe).to.be.calledTwice(); - expect(wsClientMock.subscribe).to.be.calledOnce(); + it('should subscribe to transaction events from WS client if it is connected', () => { + wsClientMock.isConnected = true; + + blockchainListener.start(); + + expect(wsClientMock.subscribe).to.be.calledTwice(); expect(wsClientMock.subscribe.firstCall).to.be.calledWithExactly( BlockchainListener.TX_QUERY, ); - // expect(wsClientMock.subscribe.secondCall).to.be.calledWithExactly( - // BlockchainListener.NEW_BLOCK_QUERY, - // ); + expect(wsClientMock.subscribe.secondCall).to.be.calledWithExactly( + BlockchainListener.NEW_BLOCK_QUERY, + ); }); - it.skip('should emit block when new block is arrived', (done) => { + it('should emit block when new block is arrived', (done) => { + blockchainListener.start(); + blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, (message) => { expect(message).to.be.deep.equal(blockMessageMock); @@ -107,6 +111,8 @@ describe('BlockchainListener', () => { }); it('should emit transaction when transaction is arrived', (done) => { + blockchainListener.start(); + const topic = BlockchainListener.getTransactionEventName(transactionHash); blockchainListener.on(topic, (message) => { diff --git a/packages/dashmate/src/commands/wallet/mint.js b/packages/dashmate/src/commands/wallet/mint.js index 2eeacabd35a..0f06ad0ecd1 100644 --- a/packages/dashmate/src/commands/wallet/mint.js +++ b/packages/dashmate/src/commands/wallet/mint.js @@ -51,6 +51,10 @@ Mint given amount of tDash to a new or specified address throw new Error('Only local network supports generation of dash'); } + if (config.get('core.masternode.enable')) { + throw new Error('A masternode doesn\'t support generation of dash'); + } + const tasks = new Listr( [ { diff --git a/packages/dashmate/src/doctor/analyse/analyseConfigFactory.js b/packages/dashmate/src/doctor/analyse/analyseConfigFactory.js index 7396668d439..32c4355bdec 100644 --- a/packages/dashmate/src/doctor/analyse/analyseConfigFactory.js +++ b/packages/dashmate/src/doctor/analyse/analyseConfigFactory.js @@ -134,6 +134,10 @@ and revoke the previous certificate in the ZeroSSL dashboard`, description: chalk`ZeroSSL certificate is not valid.`, solution: chalk`Please run {bold.cyanBright dashmate ssl zerossl obtain} to get a new one.`, }, + [ERRORS.ZERO_SSL_API_ERROR]: { + description: ssl?.data?.error?.message, + solution: chalk`Please contact ZeroSSL support if needed.`, + }, }[ssl.error] ?? {}; if (description) { @@ -156,13 +160,25 @@ and revoke the previous certificate in the ZeroSSL dashboard`, if (coreP2pPort && coreP2pPort !== 'OPEN') { const port = config.get('core.p2p.port'); const externalIp = config.get('externalIp'); - const problem = new Problem( - 'Core P2P port is unavailable for incoming connections.', - chalk`Please ensure that port ${port} on your public IP address ${externalIp} is open + + let solution = chalk`Please ensure that port ${port} on your public IP address ${externalIp} is open for incoming connections. You may need to configure your firewall to ensure this port is accessible from the public internet. If you are using Network Address Translation (NAT), please enable port forwarding for port 80 -and all Dash service ports listed above.`, +and all Dash service ports listed above.`; + if (externalIp) { + solution = chalk`Please ensure your configured IP address ${externalIp} is your public IP. +You can change it using {bold.cyanBright dashmate config set externalIp [IP]}. +Also, ensure that port ${port} on your public IP address is open +for incoming connections. You may need to configure your firewall to +ensure this port is accessible from the public internet. If you are using +Network Address Translation (NAT), please enable port forwarding for port 80 +and all Dash service ports listed above.`; + } + + const problem = new Problem( + 'Core P2P port is unavailable for incoming connections.', + solution, SEVERITY.HIGH, ); @@ -174,13 +190,25 @@ and all Dash service ports listed above.`, if (gatewayHttpPort && gatewayHttpPort !== 'OPEN') { const port = config.get('platform.gateway.listeners.dapiAndDrive.port'); const externalIp = config.get('externalIp'); - const problem = new Problem( - 'Gateway HTTP port is unavailable for incoming connections.', - chalk`Please ensure that port ${port} on your public IP address ${externalIp} is open + + let solution = chalk`Please ensure that port ${port} on your public IP address ${externalIp} is open for incoming connections. You may need to configure your firewall to ensure this port is accessible from the public internet. If you are using Network Address Translation (NAT), please enable port forwarding for port 80 -and all Dash service ports listed above.`, +and all Dash service ports listed above.`; + if (externalIp) { + solution = chalk`Please ensure your configured IP address ${externalIp} is your public IP. +You can change it using {bold.cyanBright dashmate config set externalIp [IP]}. +Also, ensure that port ${port} on your public IP address is open +for incoming connections. You may need to configure your firewall to +ensure this port is accessible from the public internet. If you are using +Network Address Translation (NAT), please enable port forwarding for port 80 +and all Dash service ports listed above.`; + } + + const problem = new Problem( + 'Gateway HTTP port is unavailable for incoming connections.', + solution, SEVERITY.HIGH, ); @@ -192,13 +220,25 @@ and all Dash service ports listed above.`, if (tenderdashP2pPort && tenderdashP2pPort !== 'OPEN') { const port = config.get('platform.drive.tenderdash.p2p.port'); const externalIp = config.get('externalIp'); - const problem = new Problem( - 'Tenderdash P2P port is unavailable for incoming connections.', - chalk`Please ensure that port ${port} on your public IP address ${externalIp} is open + + let solution = chalk`Please ensure that port ${port} on your public IP address ${externalIp} is open for incoming connections. You may need to configure your firewall to ensure this port is accessible from the public internet. If you are using Network Address Translation (NAT), please enable port forwarding for port 80 -and all Dash service ports listed above.`, +and all Dash service ports listed above.`; + if (externalIp) { + solution = chalk`Please ensure your configured IP address ${externalIp} is your public IP. +You can change it using {bold.cyanBright dashmate config set externalIp [IP]}. +Also, ensure that port ${port} on your public IP address is open +for incoming connections. You may need to configure your firewall to +ensure this port is accessible from the public internet. If you are using +Network Address Translation (NAT), please enable port forwarding for port 80 +and all Dash service ports listed above.`; + } + + const problem = new Problem( + 'Tenderdash P2P port is unavailable for incoming connections.', + solution, SEVERITY.HIGH, ); diff --git a/packages/dashmate/src/doctor/analyse/analyseServiceContainersFactory.js b/packages/dashmate/src/doctor/analyse/analyseServiceContainersFactory.js index 7cf6b6c6964..4edd993b62e 100644 --- a/packages/dashmate/src/doctor/analyse/analyseServiceContainersFactory.js +++ b/packages/dashmate/src/doctor/analyse/analyseServiceContainersFactory.js @@ -20,9 +20,12 @@ export default function analyseServiceContainersFactory( const servicesNotStarted = []; const servicesFailed = []; const servicesOOMKilled = []; + const servicesHighCpuUsage = []; + const servicesHighMemoryUsage = []; for (const service of services) { const dockerInspect = samples.getServiceInfo(service.name, 'dockerInspect'); + const dockerStats = samples.getServiceInfo(service.name, 'dockerStats'); if (!dockerInspect) { continue; @@ -47,6 +50,34 @@ export default function analyseServiceContainersFactory( service, }); } + + const cpuSystemUsage = dockerStats?.cpuStats?.system_cpu_usage ?? 0; + const cpuServiceUsage = dockerStats?.cpuStats?.cpu_usage?.total_usage ?? 0; + + if (cpuSystemUsage > 0) { + const cpuUsage = cpuServiceUsage / cpuSystemUsage; + + if (cpuUsage > 0.8) { + servicesHighCpuUsage.push({ + service, + cpuUsage, + }); + } + } + + const memoryLimit = dockerStats?.memoryStats?.limit ?? 0; + const memoryServiceUsage = dockerStats?.memoryStats?.usage ?? 0; + + if (memoryLimit > 0) { + const memoryUsage = memoryServiceUsage / memoryLimit; + + if (memoryUsage > 0.8) { + servicesHighMemoryUsage.push({ + service, + memoryUsage, + }); + } + } } const problems = []; @@ -103,6 +134,34 @@ export default function analyseServiceContainersFactory( problems.push(problem); } + if (servicesHighCpuUsage.length > 0) { + for (const highCpuService of servicesHighCpuUsage) { + const description = `Service ${highCpuService.service.title} is consuming ${(highCpuService.cpuUsage * 100).toFixed(2)}% CPU.`; + + const problem = new Problem( + description, + 'Consider upgrading CPU or report in case of misbehaviour.', + SEVERITY.MEDIUM, + ); + + problems.push(problem); + } + } + + if (servicesHighMemoryUsage.length > 0) { + for (const highMemoryService of servicesHighMemoryUsage) { + const description = `Service ${highMemoryService.service.title} is consuming ${(highMemoryService.memoryUsage * 100).toFixed(2)}% RAM.`; + + const problem = new Problem( + description, + 'Consider upgrading RAM or report in case of misbehaviour.', + SEVERITY.MEDIUM, + ); + + problems.push(problem); + } + } + return problems; } diff --git a/packages/dashmate/src/listr/tasks/doctor/collectSamplesTaskFactory.js b/packages/dashmate/src/listr/tasks/doctor/collectSamplesTaskFactory.js index 8e4ae5794a4..a3be7545091 100644 --- a/packages/dashmate/src/listr/tasks/doctor/collectSamplesTaskFactory.js +++ b/packages/dashmate/src/listr/tasks/doctor/collectSamplesTaskFactory.js @@ -2,6 +2,7 @@ import fs from 'fs'; import { Listr } from 'listr2'; import path from 'path'; import process from 'process'; +import si from 'systeminformation'; import obfuscateConfig from '../../../config/obfuscateConfig.js'; import { DASHMATE_VERSION } from '../../../constants.js'; import Certificate from '../../../ssl/zerossl/Certificate.js'; @@ -166,7 +167,7 @@ export default function collectSamplesTaskFactory( title: 'Core P2P port', task: async () => { const port = config.get('core.p2p.port'); - const response = await providers.mnowatch.checkPortStatus(port); + const response = await providers.mnowatch.checkPortStatus(port, config.get('externalIp')); ctx.samples.setServiceInfo('core', 'p2pPort', response); }, @@ -176,7 +177,7 @@ export default function collectSamplesTaskFactory( enabled: () => config.get('platform.enable'), task: async () => { const port = config.get('platform.gateway.listeners.dapiAndDrive.port'); - const response = await providers.mnowatch.checkPortStatus(port); + const response = await providers.mnowatch.checkPortStatus(port, config.get('externalIp')); ctx.samples.setServiceInfo('gateway', 'httpPort', response); }, @@ -185,7 +186,7 @@ export default function collectSamplesTaskFactory( title: 'Tenderdash P2P port', task: async () => { const port = config.get('platform.drive.tenderdash.p2p.port'); - const response = await providers.mnowatch.checkPortStatus(port); + const response = await providers.mnowatch.checkPortStatus(port, config.get('externalIp')); ctx.samples.setServiceInfo('drive_tenderdash', 'p2pPort', response); }, @@ -312,13 +313,10 @@ export default function collectSamplesTaskFactory( }, }, { - title: 'Logs', - task: async (ctx, task) => { + title: 'Docker containers info', + task: async (ctx) => { const services = await getServiceList(config); - // eslint-disable-next-line no-param-reassign - task.output = `Pulling logs from ${services.map((e) => e.name)}`; - await Promise.all( services.map(async (service) => { const [inspect, logs] = (await Promise.allSettled([ @@ -326,6 +324,12 @@ export default function collectSamplesTaskFactory( dockerCompose.logs(config, [service.name], { tail: 300000 }), ])).map((e) => e.value || e.reason); + const containerId = inspect?.Id; + let dockerStats; + if (containerId) { + dockerStats = await si.dockerContainerStats(containerId); + } + if (logs?.out) { // Hide username & external ip from logs logs.out = logs.out.replaceAll( @@ -354,6 +358,7 @@ export default function collectSamplesTaskFactory( ctx.samples.setServiceInfo(service.name, 'stdOut', logs?.out); ctx.samples.setServiceInfo(service.name, 'stdErr', logs?.err); ctx.samples.setServiceInfo(service.name, 'dockerInspect', inspect); + ctx.samples.setServiceInfo(service.name, 'dockerStats', dockerStats); }), ); }, diff --git a/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js b/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js index 86bb3db5ca4..ca679233d03 100644 --- a/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js +++ b/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js @@ -64,6 +64,9 @@ export default function obtainZeroSSLCertificateTaskFactory( case ERRORS.CERTIFICATE_ID_IS_NOT_SET: // eslint-disable-next-line no-param-reassign task.output = 'Certificate is not configured yet, creating a new one'; + + // We need to create a new certificate + ctx.certificate = null; break; case ERRORS.PRIVATE_KEY_IS_NOT_PRESENT: // If certificate exists but private key does not, then we can't set up TLS connection @@ -85,6 +88,9 @@ export default function obtainZeroSSLCertificateTaskFactory( case ERRORS.CERTIFICATE_EXPIRES_SOON: // eslint-disable-next-line no-param-reassign task.output = `Certificate exists but expires in less than ${ctx.expirationDays} days at ${ctx.certificate.expires}. Obtain a new one`; + + // We need to create a new certificate + ctx.certificate = null; break; case ERRORS.CERTIFICATE_IS_NOT_VALIDATED: // eslint-disable-next-line no-param-reassign @@ -93,7 +99,12 @@ export default function obtainZeroSSLCertificateTaskFactory( case ERRORS.CERTIFICATE_IS_NOT_VALID: // eslint-disable-next-line no-param-reassign task.output = 'Certificate is not valid. Create a new one'; + + // We need to create a new certificate + ctx.certificate = null; break; + case ERRORS.ZERO_SSL_API_ERROR: + throw ctx.error; default: throw new Error(`Unknown error: ${error}`); } diff --git a/packages/dashmate/src/ssl/zerossl/validateZeroSslCertificateFactory.js b/packages/dashmate/src/ssl/zerossl/validateZeroSslCertificateFactory.js index f4d9956a9d7..20b221216c5 100644 --- a/packages/dashmate/src/ssl/zerossl/validateZeroSslCertificateFactory.js +++ b/packages/dashmate/src/ssl/zerossl/validateZeroSslCertificateFactory.js @@ -11,6 +11,7 @@ export const ERRORS = { CERTIFICATE_EXPIRES_SOON: 'CERTIFICATE_EXPIRES_SOON', CERTIFICATE_IS_NOT_VALIDATED: 'CERTIFICATE_IS_NOT_VALIDATED', CERTIFICATE_IS_NOT_VALID: 'CERTIFICATE_IS_NOT_VALID', + ZERO_SSL_API_ERROR: 'ZERO_SSL_API_ERROR', }; /** @@ -68,9 +69,22 @@ export default function validateZeroSslCertificateFactory(homeDir, getCertificat data.isBundleFilePresent = fs.existsSync(data.bundleFilePath); // This function will throw an error if certificate with specified ID is not present - const certificate = await getCertificate(data.apiKey, certificateId); + try { + data.certificate = await getCertificate(data.apiKey, certificateId); + } catch (e) { + if (e.code) { + data.error = e; - data.isExpiresSoon = certificate.isExpiredInDays(expirationDays); + return { + error: ERRORS.ZERO_SSL_API_ERROR, + data, + }; + } + + throw e; + } + + data.isExpiresSoon = data.certificate.isExpiredInDays(expirationDays); // If certificate exists but private key does not, then we can't setup TLS connection // In this case we need to regenerate a certificate or put back this private key @@ -82,17 +96,16 @@ export default function validateZeroSslCertificateFactory(homeDir, getCertificat } // We need to make sure that external IP and certificate IP match - if (certificate.common_name !== data.externalIp) { + if (data.certificate.common_name !== data.externalIp) { return { error: ERRORS.EXTERNAL_IP_MISMATCH, data, }; } - if (['pending_validation', 'draft'].includes(certificate.status)) { + if (['pending_validation', 'draft'].includes(data.certificate.status)) { // Certificate is already created, so we just need to pass validation // and download certificate file - data.certificate = certificate; // We need to download new certificate bundle data.isBundleFilePresent = false; @@ -103,7 +116,7 @@ export default function validateZeroSslCertificateFactory(homeDir, getCertificat }; } - if (certificate.status !== 'issued' || data.isExpiresSoon) { + if (data.certificate.status !== 'issued' || data.isExpiresSoon) { // Certificate is going to expire soon, or current certificate is not valid // we need to obtain a new one @@ -128,8 +141,6 @@ export default function validateZeroSslCertificateFactory(homeDir, getCertificat } // Certificate is valid, so we might need only to download certificate bundle - data.certificate = certificate; - return { data, }; diff --git a/packages/dashmate/src/status/providers.js b/packages/dashmate/src/status/providers.js index a3a08b94b69..46c491c9202 100644 --- a/packages/dashmate/src/status/providers.js +++ b/packages/dashmate/src/status/providers.js @@ -1,6 +1,8 @@ +import https from 'https'; import { PortStateEnum } from './enums/portState.js'; const MAX_REQUEST_TIMEOUT = 5000; +const MAX_RESPONSE_SIZE = 1 * 1024 * 1024; // 1 MB const request = async (url) => { try { @@ -29,12 +31,6 @@ const requestJSON = async (url) => { return response; }; -const requestText = async (url) => { - const response = await request(url); - - return response.text(); -}; - const insightURLs = { testnet: 'https://testnet-insight.dashevo.org/insight-api', mainnet: 'https://insight.dash.org/insight-api', @@ -67,16 +63,80 @@ export default { }, }, mnowatch: { - checkPortStatus: async (port) => { - try { - return requestText(`https://mnowatch.org/${port}/`); - } catch (e) { - if (process.env.DEBUG) { - // eslint-disable-next-line no-console - console.warn(e); - } - return PortStateEnum.ERROR; - } + /** + * Check the status of a port and optionally validate an IP address. + * + * @param {number} port - The port number to check. + * @param {string} [ip] - Optional. The IP address to validate. + * @returns {Promise} A promise that resolves to the port status. + */ + checkPortStatus: async (port, ip = undefined) => { + // We use http request instead fetch function to force + // using IPv4 otherwise mnwatch could try to connect to IPv6 node address + // and fail (Core listens for IPv4 only) + // https://github.com/dashpay/platform/issues/2100 + + const options = { + hostname: 'mnowatch.org', + port: 443, + path: ip ? `/${port}/?validateIp=${ip}` : `/${port}/`, + method: 'GET', + family: 4, // Force IPv4 + timeout: MAX_REQUEST_TIMEOUT, + }; + + return new Promise((resolve) => { + const req = https.request(options, (res) => { + let data = ''; + + // Check if the status code is 200 + if (res.statusCode !== 200) { + if (process.env.DEBUG) { + // eslint-disable-next-line no-console + console.warn(`Port check request failed with status code ${res.statusCode}`); + } + // Consume response data to free up memory + res.resume(); + resolve(PortStateEnum.ERROR); + return; + } + + // Optionally set the encoding to receive strings directly + res.setEncoding('utf8'); + + // Collect data chunks + res.on('data', (chunk) => { + data += chunk; + + if (data.length > MAX_RESPONSE_SIZE) { + resolve(PortStateEnum.ERROR); + + if (process.env.DEBUG) { + // eslint-disable-next-line no-console + console.warn('Port check response size exceeded'); + } + + req.destroy(); + } + }); + + // Handle the end of the response + res.on('end', () => { + resolve(data); + }); + }); + + req.on('error', (e) => { + if (process.env.DEBUG) { + // eslint-disable-next-line no-console + console.warn(`Port check request failed: ${e}`); + } + + resolve(PortStateEnum.ERROR); + }); + + req.end(); + }); }, }, }; diff --git a/packages/dashmate/src/status/scopes/core.js b/packages/dashmate/src/status/scopes/core.js index d983bcd67c5..c460f305de5 100644 --- a/packages/dashmate/src/status/scopes/core.js +++ b/packages/dashmate/src/status/scopes/core.js @@ -116,7 +116,7 @@ export default function getCoreScopeFactory( const providersResult = await Promise.allSettled([ providers.github.release('dashpay/dash'), - providers.mnowatch.checkPortStatus(config.get('core.p2p.port')), + providers.mnowatch.checkPortStatus(config.get('core.p2p.port'), config.get('externalIp')), providers.insight(config.get('network')).status(), ]); diff --git a/packages/dashmate/src/status/scopes/platform.js b/packages/dashmate/src/status/scopes/platform.js index f18b2915498..7605104ecb4 100644 --- a/packages/dashmate/src/status/scopes/platform.js +++ b/packages/dashmate/src/status/scopes/platform.js @@ -89,8 +89,8 @@ export default function getPlatformScopeFactory( // Collecting platform data fails if Tenderdash is waiting for core to sync if (info.serviceStatus === ServiceStatusEnum.up) { const portStatusResult = await Promise.allSettled([ - providers.mnowatch.checkPortStatus(config.get('platform.gateway.listeners.dapiAndDrive.port')), - providers.mnowatch.checkPortStatus(config.get('platform.drive.tenderdash.p2p.port')), + providers.mnowatch.checkPortStatus(config.get('platform.gateway.listeners.dapiAndDrive.port'), config.get('externalIp')), + providers.mnowatch.checkPortStatus(config.get('platform.drive.tenderdash.p2p.port'), config.get('externalIp')), ]); const [httpPortState, p2pPortState] = portStatusResult.map((result) => (result.status === 'fulfilled' ? result.value : null)); diff --git a/packages/dashmate/test/e2e/localNetwork.spec.js b/packages/dashmate/test/e2e/localNetwork.spec.js index d1f8c7bb7b8..ab44a55991d 100644 --- a/packages/dashmate/test/e2e/localNetwork.spec.js +++ b/packages/dashmate/test/e2e/localNetwork.spec.js @@ -129,6 +129,7 @@ describe('Local Network', function main() { const task = restartNodeTask(config); await task.run({ isVerbose: true, + isForce: true, }); } @@ -146,6 +147,7 @@ describe('Local Network', function main() { const task = stopNodeTask(config); await task.run({ isVerbose: true, + isForce: true, }); } diff --git a/packages/dashmate/test/e2e/testnetEvonode.spec.js b/packages/dashmate/test/e2e/testnetEvonode.spec.js index 1e8f90d5ab4..5ffd9e3f94e 100644 --- a/packages/dashmate/test/e2e/testnetEvonode.spec.js +++ b/packages/dashmate/test/e2e/testnetEvonode.spec.js @@ -148,7 +148,7 @@ describe('Testnet Evonode', function main() { await task.run({ isVerbose: true, - isSafe: true, + isForce: true, }); // TODO: Assert all services are running @@ -232,7 +232,7 @@ describe('Testnet Evonode', function main() { await task.run({ isVerbose: true, - isSafe: true, + isForce: true, }); // TODO: Assert all services are running diff --git a/packages/dashmate/test/e2e/testnetFullnode.spec.js b/packages/dashmate/test/e2e/testnetFullnode.spec.js index 12329f3407f..01d36b61dbd 100644 --- a/packages/dashmate/test/e2e/testnetFullnode.spec.js +++ b/packages/dashmate/test/e2e/testnetFullnode.spec.js @@ -141,6 +141,7 @@ describe('Testnet Fullnode', function main() { await task.run({ isVerbose: true, + isSafe: true, }); await assertServiceRunning(config, 'core'); @@ -160,6 +161,7 @@ describe('Testnet Fullnode', function main() { await task.run({ isVerbose: true, + isSafe: true, }); await assertServiceRunning(config, 'core', false); diff --git a/packages/js-dapi-client/lib/networkConfigs.js b/packages/js-dapi-client/lib/networkConfigs.js index 7308016ee5c..4bc7c832136 100644 --- a/packages/js-dapi-client/lib/networkConfigs.js +++ b/packages/js-dapi-client/lib/networkConfigs.js @@ -53,10 +53,10 @@ module.exports = { }, mainnet: { seeds: [ - 'seed-1.mainnet.networks.dash.org:1443', - 'seed-2.mainnet.networks.dash.org:1443', - 'seed-3.mainnet.networks.dash.org:1443', - 'seed-4.mainnet.networks.dash.org:1443', + 'seed-1.mainnet.networks.dash.org', + 'seed-2.mainnet.networks.dash.org', + 'seed-3.mainnet.networks.dash.org', + 'seed-4.mainnet.networks.dash.org', ], network: 'mainnet', }, diff --git a/packages/js-dash-sdk/package.json b/packages/js-dash-sdk/package.json index f3f231678e5..457ce7b4dcf 100644 --- a/packages/js-dash-sdk/package.json +++ b/packages/js-dash-sdk/package.json @@ -1,6 +1,6 @@ { "name": "dash", - "version": "4.3.0", + "version": "4.4.0-dev.1", "description": "Dash library for JavaScript/TypeScript ecosystem (Wallet, DAPI, Primitives, BLS, ...)", "main": "build/index.js", "unpkg": "dist/dash.min.js", diff --git a/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts b/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts index b44f7cd02fd..bc6fa96bd42 100644 --- a/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts +++ b/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts @@ -37,6 +37,7 @@ describe('Dash - Client', function suite() { testHDKey = 'tprv8ZgxMBicQKsPeGi4CikhacVPz6UmErenu1PoD3S4XcEDSPP8auRaS8hG3DQtsQ2i9HACgohHwF5sgMVJNksoKqYoZbis8o75Pp1koCme2Yo'; client = new Client({ + network: 'testnet', wallet: { HDPrivateKey: testHDKey, }, @@ -77,7 +78,7 @@ describe('Dash - Client', function suite() { it('should be instantiable', () => { client = new Client(); expect(client).to.exist; - expect(client.network).to.be.equal('testnet'); + expect(client.network).to.be.equal('mainnet'); expect(client.getDAPIClient().constructor.name).to.be.equal('DAPIClient'); }); @@ -111,7 +112,7 @@ describe('Dash - Client', function suite() { wallet: { mnemonic: testMnemonic, offlineMode: true, - network: 'evonet', + network: 'mainnet', }, }); diff --git a/packages/js-dash-sdk/src/SDK/Client/Client.ts b/packages/js-dash-sdk/src/SDK/Client/Client.ts index 7f185677ea5..ee641901dfe 100644 --- a/packages/js-dash-sdk/src/SDK/Client/Client.ts +++ b/packages/js-dash-sdk/src/SDK/Client/Client.ts @@ -48,7 +48,7 @@ export interface ClientOpts { * and the Dash Platform (layer 2). */ export class Client extends EventEmitter { - public network: string = 'testnet'; + public network: string = 'mainnet'; public wallet: Wallet | undefined; @@ -74,7 +74,7 @@ export class Client extends EventEmitter { this.options = options; - this.network = this.options.network ? this.options.network.toString() : 'testnet'; + this.network = this.options.network ? this.options.network.toString() : 'mainnet'; // Initialize DAPI Client const dapiClientOptions = { diff --git a/packages/rs-dapi-client/src/address_list.rs b/packages/rs-dapi-client/src/address_list.rs index ab4e2ba0ebf..bf09b1af8c0 100644 --- a/packages/rs-dapi-client/src/address_list.rs +++ b/packages/rs-dapi-client/src/address_list.rs @@ -85,7 +85,7 @@ pub enum AddressListError { /// A structure to manage DAPI addresses to select from /// for [DapiRequest](crate::DapiRequest) execution. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AddressList { addresses: HashSet
, base_ban_period: Duration, @@ -221,3 +221,12 @@ impl FromIterator for AddressList { address_list } } + +impl IntoIterator for AddressList { + type Item = Address; + type IntoIter = std::collections::hash_set::IntoIter
; + + fn into_iter(self) -> Self::IntoIter { + self.addresses.into_iter() + } +} diff --git a/packages/rs-dapi-client/src/dapi_client.rs b/packages/rs-dapi-client/src/dapi_client.rs index 8e5a3d660b2..17748ab2b02 100644 --- a/packages/rs-dapi-client/src/dapi_client.rs +++ b/packages/rs-dapi-client/src/dapi_client.rs @@ -112,6 +112,11 @@ impl DapiClient { dump_dir: None, } } + + /// Return the [DapiClient] address list. + pub fn address_list(&self) -> &Arc> { + &self.address_list + } } #[async_trait] diff --git a/packages/rs-dapi-client/src/dump.rs b/packages/rs-dapi-client/src/dump.rs index c81399395bf..a1e23d1ff20 100644 --- a/packages/rs-dapi-client/src/dump.rs +++ b/packages/rs-dapi-client/src/dump.rs @@ -189,7 +189,6 @@ impl DapiClient { response: &MockResult, dump_dir: Option, ) where - R: Mockable, ::Response: Mockable, { let path = match dump_dir { diff --git a/packages/rs-dapi-client/src/lib.rs b/packages/rs-dapi-client/src/lib.rs index e4f5836e29e..976537097eb 100644 --- a/packages/rs-dapi-client/src/lib.rs +++ b/packages/rs-dapi-client/src/lib.rs @@ -14,6 +14,7 @@ pub mod transport; pub use address_list::Address; pub use address_list::AddressList; +pub use connection_pool::ConnectionPool; pub use dapi_client::DapiRequestExecutor; pub use dapi_client::{DapiClient, DapiClientError}; use dapi_grpc::mock::Mockable; diff --git a/packages/rs-dapi-client/src/transport/grpc.rs b/packages/rs-dapi-client/src/transport/grpc.rs index 4cfa0c26495..98976ed08e8 100644 --- a/packages/rs-dapi-client/src/transport/grpc.rs +++ b/packages/rs-dapi-client/src/transport/grpc.rs @@ -421,3 +421,12 @@ impl_transport_request_grpc!( }, subscribe_to_transactions_with_proofs ); + +// rpc getStatus(GetStatusRequest) returns (GetStatusResponse); +impl_transport_request_grpc!( + platform_proto::GetStatusRequest, + platform_proto::GetStatusResponse, + PlatformGrpcClient, + RequestSettings::default(), + get_status +); diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 57dec66d31f..89fef813601 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -2,13 +2,16 @@ use crate::from_request::TryFromRequest; use crate::provider::DataContractProvider; use crate::verify::verify_tenderdash_proof; use crate::{types, types::*, ContextProvider, Error}; +use dapi_grpc::platform::v0::get_evonodes_proposed_epoch_blocks_by_range_request::get_evonodes_proposed_epoch_blocks_by_range_request_v0::Start; use dapi_grpc::platform::v0::get_identities_contract_keys_request::GetIdentitiesContractKeysRequestV0; use dapi_grpc::platform::v0::get_path_elements_request::GetPathElementsRequestV0; use dapi_grpc::platform::v0::get_protocol_version_upgrade_vote_status_request::{ self, GetProtocolVersionUpgradeVoteStatusRequestV0, }; use dapi_grpc::platform::v0::security_level_map::KeyKindRequestType as GrpcKeyKind; -use dapi_grpc::platform::v0::{get_contested_resource_identity_votes_request, get_data_contract_history_request, get_data_contract_request, get_data_contracts_request, get_epochs_info_request, get_evonodes_proposed_epoch_blocks_by_ids_request, get_evonodes_proposed_epoch_blocks_by_range_request, get_identities_balances_request, get_identities_contract_keys_request, get_identity_balance_and_revision_request, get_identity_balance_request, get_identity_by_public_key_hash_request, get_identity_contract_nonce_request, get_identity_keys_request, get_identity_nonce_request, get_identity_request, get_path_elements_request, get_prefunded_specialized_balance_request, GetContestedResourceVotersForIdentityRequest, GetContestedResourceVotersForIdentityResponse, GetPathElementsRequest, GetPathElementsResponse, GetProtocolVersionUpgradeStateRequest, GetProtocolVersionUpgradeStateResponse, GetProtocolVersionUpgradeVoteStatusRequest, GetProtocolVersionUpgradeVoteStatusResponse, Proof, ResponseMetadata}; +use dapi_grpc::platform::v0::{ + get_contested_resource_identity_votes_request, get_data_contract_history_request, get_data_contract_request, get_data_contracts_request, get_epochs_info_request, get_evonodes_proposed_epoch_blocks_by_ids_request, get_evonodes_proposed_epoch_blocks_by_range_request, get_identities_balances_request, get_identities_contract_keys_request, get_identity_balance_and_revision_request, get_identity_balance_request, get_identity_by_public_key_hash_request, get_identity_contract_nonce_request, get_identity_keys_request, get_identity_nonce_request, get_identity_request, get_path_elements_request, get_prefunded_specialized_balance_request, GetContestedResourceVotersForIdentityRequest, GetContestedResourceVotersForIdentityResponse, GetPathElementsRequest, GetPathElementsResponse, GetProtocolVersionUpgradeStateRequest, GetProtocolVersionUpgradeStateResponse, GetProtocolVersionUpgradeVoteStatusRequest, GetProtocolVersionUpgradeVoteStatusResponse, Proof, ResponseMetadata +}; use dapi_grpc::platform::{ v0::{self as platform, key_request_type, KeyRequestType as GrpcKeyType}, VersionedGrpcResponse, @@ -35,6 +38,7 @@ use drive::drive::identity::key::fetch::{ use drive::drive::Drive; use drive::error::proof::ProofError; use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery; +use drive::query::proposer_block_count_query::ProposerQueryType; use drive::query::vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery; use drive::query::vote_poll_vote_state_query::ContestedDocumentVotePollDriveQuery; use drive::query::vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery; @@ -42,8 +46,6 @@ use drive::query::{DriveDocumentQuery, VotePollsByEndDateDriveQuery}; use std::array::TryFromSliceError; use std::collections::BTreeMap; use std::num::TryFromIntError; -use dapi_grpc::platform::v0::get_evonodes_proposed_epoch_blocks_by_range_request::get_evonodes_proposed_epoch_blocks_by_range_request_v0::Start; -use drive::query::proposer_block_count_query::ProposerQueryType; /// Parse and verify the received proof and retrieve the requested object, if any. /// @@ -78,7 +80,7 @@ pub trait FromProof { /// /// * `Ok(Some(object, metadata))` when the requested object was found in the proof. /// * `Ok(None)` when the requested object was not found in the proof; this can be interpreted as proof of non-existence. - /// For collections, returns Ok(None) if none of the requested objects were found. + /// For collections, returns Ok(None) if none of the requested objects were found. /// * `Err(Error)` when either the provided data is invalid or proof validation failed. fn maybe_from_proof<'a, I: Into, O: Into>( request: I, @@ -108,7 +110,7 @@ pub trait FromProof { /// /// * `Ok(Some((object, metadata)))` when the requested object was found in the proof. /// * `Ok(None)` when the requested object was not found in the proof; this can be interpreted as proof of non-existence. - /// For collections, returns Ok(None) if none of the requested objects were found. + /// For collections, returns Ok(None) if none of the requested objects were found. /// * `Err(Error)` when either the provided data is invalid or proof validation failed. fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( request: I, @@ -1714,7 +1716,6 @@ impl FromProof for TotalCreditsInPla )) } } - impl FromProof for ProposerBlockCounts { type Request = platform::GetEvonodesProposedEpochBlocksByIdsRequest; type Response = platform::GetEvonodesProposedEpochBlocksResponse; diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index c1778ab536b..d49301720b3 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -5,6 +5,10 @@ //! In this case, the [FromProof](crate::FromProof) trait is implemented for dedicated object type //! defined in this module. +mod evonode_status; + +use dpp::block::block_info::BlockInfo; +use dpp::core_types::validator_set::ValidatorSet; use dpp::data_contract::document_type::DocumentType; use dpp::fee::Credits; use dpp::platform_value::Value; @@ -13,6 +17,7 @@ use dpp::version::PlatformVersion; pub use dpp::version::ProtocolVersionVoteCount; use dpp::voting::contender_structs::{Contender, ContenderWithSerializedDocument}; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::resource_vote::ResourceVote; @@ -24,13 +29,10 @@ use dpp::{ prelude::{DataContract, Identifier, IdentityPublicKey, Revision}, util::deserializer::ProtocolVersion, }; +use drive::grovedb::query_result_type::Path; use drive::grovedb::Element; use std::collections::{BTreeMap, BTreeSet}; -use dpp::block::block_info::BlockInfo; -use dpp::core_types::validator_set::ValidatorSet; -use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; -use drive::grovedb::query_result_type::Path; #[cfg(feature = "mocks")] use { bincode::{Decode, Encode}, @@ -39,6 +41,8 @@ use { platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}, }; +pub use evonode_status::*; + /// A data structure that holds a set of objects of a generic type `O`, indexed by a key of type `K`. /// /// This type is typically returned by functions that operate on multiple objects, such as fetching multiple objects diff --git a/packages/rs-drive-proof-verifier/src/types/evonode_status.rs b/packages/rs-drive-proof-verifier/src/types/evonode_status.rs new file mode 100644 index 00000000000..87831de0f6f --- /dev/null +++ b/packages/rs-drive-proof-verifier/src/types/evonode_status.rs @@ -0,0 +1,382 @@ +//! Status details of EvoNode, like version, current height, etc. + +use crate::Error; +use dapi_grpc::platform::v0::{ + get_status_response::{self}, + GetStatusResponse, +}; + +#[cfg(feature = "mocks")] +use { + bincode::{Decode, Encode}, + dpp::{version as platform_version, ProtocolError}, + platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}, +}; + +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +/// The status of an EvoNode. +pub struct EvoNodeStatus { + /// Information about protocol and software components versions. + pub version: Version, + /// Information about the node. + pub node: Node, + /// Layer 2 blockchain information + pub chain: Chain, + /// Node networking information. + pub network: Network, + /// Information about state synchronization progress. + pub state_sync: StateSync, + /// Information about current time used by the node. + pub time: Time, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] + +/// Information about protocol and software components versions. +pub struct Version { + /// Information about software components versions. + pub software: Option, + /// Information about protocol version. + pub protocol: Option, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +/// Information about software components versions. +pub struct Software { + /// DAPI version, semver-compatible string. + pub dapi: String, + /// Drive version, semver-compatible string. + pub drive: Option, + /// Tenderdash version, semver-compatible string. + pub tenderdash: Option, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +/// Information about protocol-level versions used by the node +pub struct Protocol { + /// Tenderdash protocols version. + pub tenderdash: Option, + /// Drive protocols version. + pub drive: Option, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +/// Tenderdash protocol versions. +pub struct TenderdashProtocol { + /// Tenderdash P2P protocol version. + pub p2p: u32, + /// Tenderdash block protocol version. + pub block: u32, +} + +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] + +/// Drive protocol versions. +pub struct DriveProtocol { + /// Latest version supported by the node. + pub latest: u32, + /// Current version used by the node. + pub current: u32, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] + +/// Information about current time used by the node. +pub struct Time { + /// Local time of the node. Unix timestamp since epoch. + pub local: u64, + /// Time of the last block. Unix timestamp since epoch. + pub block: Option, + /// Genesis time. Unix timestamp since epoch. + pub genesis: Option, + /// Epoch number + pub epoch: Option, +} + +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] + +/// Evo node identification information. +pub struct Node { + /// Node ID + pub id: Vec, + /// ProTxHash of masternode; None for full nodes + pub pro_tx_hash: Option>, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] + +/// Layer 2 blockchain information +pub struct Chain { + /// Whether the node is catching up with the network. + pub catching_up: bool, + /// Block hash of the latest block on the node. + pub latest_block_hash: Vec, + /// Latest app hash of the node, as visible in the latest block. + pub latest_app_hash: Vec, + /// Block hash of the earliest block available on the node. + pub earliest_block_hash: Vec, + /// Earliest app hash of the node, as visible in the earliest block. + pub earliest_app_hash: Vec, + /// Height of the latest block available on the node. + pub latest_block_height: u64, + /// Height of the earliest block available on the node. + pub earliest_block_height: u64, + /// Maximum block height of the peers connected to the node. + pub max_peer_block_height: u64, + /// Current core chain locked height. + pub core_chain_locked_height: Option, +} +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +/// Node networking information. +pub struct Network { + /// Identifier of chain the node is member of. + pub chain_id: String, + /// Number of peers in the address book. + pub peers_count: u32, + /// Whether the node is listening for incoming connections. + pub listening: bool, +} + +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +/// Information about state synchronization progress. +pub struct StateSync { + /// Total time spent on state synchronization. + pub total_synced_time: u64, + /// Estimated remaining time to finish state synchronization. + pub remaining_time: u64, + /// Total number of snapshots available. + pub total_snapshots: u32, + /// Average time spent on processing a chunk of snapshot data. + pub chunk_process_avg_time: u64, + /// Height of the latest snapshot. + pub snapshot_height: u64, + /// Number of chunks in the latest snapshot. + pub snapshot_chunks_count: u64, + /// Number of backfilled blocks. + pub backfilled_blocks: u64, + /// Total number of blocks to backfill. + pub backfill_blocks_total: u64, +} + +impl TryFrom for EvoNodeStatus { + type Error = Error; + + fn try_from(response: GetStatusResponse) -> Result { + Ok(Self { + version: Version::try_from(&response)?, + node: Node::try_from(&response)?, + chain: Chain::try_from(&response)?, + network: Network::try_from(&response)?, + state_sync: StateSync::try_from(&response)?, + time: Time::try_from(&response)?, + }) + } +} + +impl TryFrom<&GetStatusResponse> for Version { + type Error = Error; + + fn try_from(response: &GetStatusResponse) -> Result { + match &response.version { + Some(get_status_response::Version::V0(v0)) => { + let software = v0 + .version + .as_ref() + .and_then(|v| v.software.clone()) + .map(|s| Software { + dapi: s.dapi, + drive: s.drive, + tenderdash: s.tenderdash, + }); + + let protocol = v0 + .version + .as_ref() + .and_then(|v| v.protocol.clone()) + .map(|p| Protocol { + tenderdash: p.tenderdash.map(|t| TenderdashProtocol { + p2p: t.p2p, + block: t.block, + }), + drive: p.drive.map(|d| DriveProtocol { + latest: d.latest, + current: d.current, + }), + }); + + Ok(Self { software, protocol }) + } + _ => Err(Error::EmptyVersion), + } + } +} + +impl TryFrom<&GetStatusResponse> for Node { + type Error = Error; + + fn try_from(response: &GetStatusResponse) -> Result { + match &response.version { + Some(get_status_response::Version::V0(v0)) => { + let node = v0 + .node + .as_ref() + .map(|n| Self { + id: n.id.clone(), + pro_tx_hash: n.pro_tx_hash.clone(), + }) + .unwrap_or_default(); + Ok(node) + } + _ => Err(Error::EmptyVersion), + } + } +} + +impl TryFrom<&GetStatusResponse> for Chain { + type Error = Error; + + fn try_from(response: &GetStatusResponse) -> Result { + match &response.version { + Some(get_status_response::Version::V0(v0)) => { + let chain = v0 + .chain + .as_ref() + .map(|c| Self { + catching_up: c.catching_up, + latest_block_hash: c.latest_block_hash.clone(), + latest_app_hash: c.latest_app_hash.clone(), + earliest_block_hash: c.earliest_block_hash.clone(), + earliest_app_hash: c.earliest_app_hash.clone(), + latest_block_height: c.latest_block_height, + earliest_block_height: c.earliest_block_height, + max_peer_block_height: c.max_peer_block_height, + core_chain_locked_height: c.core_chain_locked_height, + }) + .unwrap_or_default(); + Ok(chain) + } + _ => Err(Error::EmptyVersion), + } + } +} + +impl TryFrom<&GetStatusResponse> for Network { + type Error = Error; + + fn try_from(response: &GetStatusResponse) -> Result { + match &response.version { + Some(get_status_response::Version::V0(v0)) => { + let network = v0 + .network + .as_ref() + .map(|n| Self { + chain_id: n.chain_id.clone(), + peers_count: n.peers_count, + listening: n.listening, + }) + .unwrap_or_default(); + Ok(network) + } + _ => Err(Error::EmptyVersion), + } + } +} + +impl TryFrom<&GetStatusResponse> for StateSync { + type Error = Error; + + fn try_from(response: &GetStatusResponse) -> Result { + match &response.version { + Some(get_status_response::Version::V0(v0)) => { + let state_sync = v0 + .state_sync + .as_ref() + .map(|s| Self { + total_synced_time: s.total_synced_time, + remaining_time: s.remaining_time, + total_snapshots: s.total_snapshots, + chunk_process_avg_time: s.chunk_process_avg_time, + snapshot_height: s.snapshot_height, + snapshot_chunks_count: s.snapshot_chunks_count, + backfilled_blocks: s.backfilled_blocks, + backfill_blocks_total: s.backfill_blocks_total, + }) + .unwrap_or_default(); + Ok(state_sync) + } + _ => Err(Error::EmptyVersion), + } + } +} + +impl TryFrom<&GetStatusResponse> for Time { + type Error = Error; + + fn try_from(response: &GetStatusResponse) -> Result { + match &response.version { + Some(get_status_response::Version::V0(v0)) => { + let time = v0 + .time + .as_ref() + .map(|t| Self { + local: t.local, + block: t.block, + genesis: t.genesis, + epoch: t.epoch, + }) + .unwrap_or_default(); + Ok(time) + } + _ => Err(Error::EmptyVersion), + } + } +} diff --git a/packages/rs-drive-proof-verifier/src/unproved.rs b/packages/rs-drive-proof-verifier/src/unproved.rs index c2f30c64526..d9fd37009c1 100644 --- a/packages/rs-drive-proof-verifier/src/unproved.rs +++ b/packages/rs-drive-proof-verifier/src/unproved.rs @@ -1,7 +1,8 @@ -use crate::types::CurrentQuorumsInfo; +use crate::types::{CurrentQuorumsInfo, EvoNodeStatus}; use crate::Error; use dapi_grpc::platform::v0::ResponseMetadata; use dapi_grpc::platform::v0::{self as platform}; +use dapi_grpc::tonic::async_trait; use dpp::bls_signatures::PublicKey as BlsPublicKey; use dpp::core_types::validator::v0::ValidatorV0; use dpp::core_types::validator_set::v0::ValidatorSetV0; @@ -273,3 +274,23 @@ impl FromUnproved for CurrentQuorumsInfo Ok((Some(info), metadata)) } } + +#[async_trait] +impl FromUnproved for EvoNodeStatus { + type Request = platform::GetStatusRequest; + type Response = platform::GetStatusResponse; + + fn maybe_from_unproved_with_metadata, O: Into>( + _request: I, + response: O, + _network: Network, + _platform_version: &PlatformVersion, + ) -> Result<(Option, ResponseMetadata), Error> + where + Self: Sized, + { + let status = Self::try_from(response.into())?; + // we use default response metadata, as this request does not return any metadata + Ok((Some(status), Default::default())) + } +} diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 94853d3627d..b53cbdca568 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -26,7 +26,7 @@ serde = { version = "1.0.197", default-features = false, features = [ ], optional = true } serde_json = { version = "1.0", features = ["preserve_order"], optional = true } tracing = { version = "0.1.40" } -hex = { version = "0.4.3"} +hex = { version = "0.4.3" } dotenvy = { version = "0.15.7", optional = true } envy = { version = "0.4.2", optional = true } futures = { version = "0.3.30" } diff --git a/packages/rs-sdk/README.md b/packages/rs-sdk/README.md index 4d543c5857b..c14b9bbd592 100644 --- a/packages/rs-sdk/README.md +++ b/packages/rs-sdk/README.md @@ -97,9 +97,9 @@ Run the offline test using the following command: cargo test -p dash-platform-sdk ``` -## Implementing Fetch and FetchAny on new objects +## Implementing Fetch and FetchMany on new objects -How to implement `Fetch` and `FetchAny` trait on new object types (`Object`). +How to implement `Fetch` and `FetchMany` trait on new object types (`Object`). It's basically copy-paste and tweaking of existing implementation for another object type. @@ -114,7 +114,7 @@ Definitions: Checklist: 1. [ ] Ensure protobuf messages are defined in `packages/dapi-grpc/protos/platform/v0/platform.proto` and generated - correctly in `packages/dapi-grpc/src/platform/proto/org.dash.platform.dapi.v0.rs`. + correctly in `packages/dapi-grpc/src/platform/client/org.dash.platform.dapi.v0.rs`. 2. [ ] In `packages/dapi-grpc/build.rs`, add `Request` to `VERSIONED_REQUESTS` and response `Response` to `VERSIONED_RESPONSES`. This should add derive of `VersionedGrpcMessage` (and some more) in `org.dash.platform.dapi.v0.rs`. 3. [ ] Link request and response type to dapi-client by adding appropriate invocation of `impl_transport_request_grpc!` macro @@ -123,7 +123,7 @@ in `packages/rs-dapi-client/src/transport/grpc.rs`. used internally. If you intend to implement `FetchMany`, you should define type returned by `fetch_many()` using `RetrievedObjects` - that will store collection of returned objects, indexd by some key. + that will store collection of returned objects, indexed by some key. 5. [ ] Implement `FromProof` trait for the `Object` (or type defined in `types.rs`) in `packages/rs-drive-proof-verifier/src/proof.rs`. 6. [ ] Implement `Query` trait for the `Request` in `packages/rs-sdk/src/platform/query.rs`. 7. [ ] Implement `Fetch` trait for the `Object` (or type defined in `types.rs`), with inner type Request = `Request`, diff --git a/packages/rs-sdk/src/lib.rs b/packages/rs-sdk/src/lib.rs index e09eb4d3bb5..d165a211a51 100644 --- a/packages/rs-sdk/src/lib.rs +++ b/packages/rs-sdk/src/lib.rs @@ -32,8 +32,8 @@ //! //! 1. [`Identifier`](crate::platform::Identifier) - fetches an object by its identifier //! 2. [`DocumentQuery`](crate::platform::DocumentQuery) - fetches documents based on search conditions; see -//! [query syntax documentation](https://docs.dash.org/projects/platform/en/stable/docs/reference/query-syntax.html) -//! for more details. +//! [query syntax documentation](https://docs.dash.org/projects/platform/en/stable/docs/reference/query-syntax.html) +//! for more details. //! 3. [`DriveQuery`](crate::platform::DriveDocumentQuery) - can be used to build more complex queries //! //! ## Testability diff --git a/packages/rs-sdk/src/mock.rs b/packages/rs-sdk/src/mock.rs index b3f1b69c639..7ad4dc7ccd4 100644 --- a/packages/rs-sdk/src/mock.rs +++ b/packages/rs-sdk/src/mock.rs @@ -32,7 +32,6 @@ pub mod sdk; // Otherwise dapi_grpc_macros::Mockable fails. // TODO: move Mockable to some crate that can be shared between dapi-grpc, rs-dapi-client, and dash-sdk pub use dapi_grpc::mock::Mockable; - // MockResponse is needed even if mocks feature is disabled - it just does nothing. #[cfg(not(feature = "mocks"))] pub use noop::MockResponse; diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 879fe88bb45..d29e48d4103 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -15,9 +15,10 @@ use dpp::{ voting::votes::{resource_vote::ResourceVote, Vote}, }; use drive_proof_verifier::types::{ - Contenders, ContestedResources, ElementFetchRequestItem, IdentityBalanceAndRevision, - MasternodeProtocolVote, PrefundedSpecializedBalance, ProposerBlockCounts, - RetrievedIntegerValue, TotalCreditsInPlatform, VotePollsGroupedByTimestamp, Voters, + Contenders, ContestedResources, CurrentQuorumsInfo, ElementFetchRequestItem, EvoNodeStatus, + IdentityBalanceAndRevision, MasternodeProtocolVote, PrefundedSpecializedBalance, + ProposerBlockCounts, RetrievedIntegerValue, TotalCreditsInPlatform, + VotePollsGroupedByTimestamp, Voters, }; use std::collections::BTreeMap; @@ -243,3 +244,5 @@ impl_mock_response!(VotePollsGroupedByTimestamp); impl_mock_response!(PrefundedSpecializedBalance); impl_mock_response!(TotalCreditsInPlatform); impl_mock_response!(ElementFetchRequestItem); +impl_mock_response!(EvoNodeStatus); +impl_mock_response!(CurrentQuorumsInfo); diff --git a/packages/rs-sdk/src/mock/sdk.rs b/packages/rs-sdk/src/mock/sdk.rs index 0f37c7dd364..02258c0cd13 100644 --- a/packages/rs-sdk/src/mock/sdk.rs +++ b/packages/rs-sdk/src/mock/sdk.rs @@ -2,7 +2,10 @@ //! //! See [MockDashPlatformSdk] for more details. use crate::{ - platform::{types::identity::IdentityRequest, DocumentQuery, Fetch, FetchMany, Query}, + platform::{ + types::{evonode::EvoNode, identity::IdentityRequest}, + DocumentQuery, Fetch, FetchMany, Query, + }, Error, Sdk, }; use arc_swap::ArcSwapOption; @@ -201,9 +204,10 @@ impl MockDashPlatformSdk { self.load_expectation::(filename) .await? } + "EvoNode" => self.load_expectation::(filename).await?, _ => { return Err(Error::Config(format!( - "unknown request type {} in {}", + "unknown request type {} in {}, missing match arm in load_expectations?", request_type, filename.display() ))) diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index aab3d2153af..a4c0fb83f4f 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -32,5 +32,6 @@ pub use { document_query::DocumentQuery, fetch::Fetch, fetch_many::FetchMany, + fetch_unproved::FetchUnproved, query::{LimitQuery, Query, QueryStartInfo, DEFAULT_EPOCH_QUERY_LIMIT}, }; diff --git a/packages/rs-sdk/src/platform/fetch_unproved.rs b/packages/rs-sdk/src/platform/fetch_unproved.rs index d25fc86b50e..9e89ad163a6 100644 --- a/packages/rs-sdk/src/platform/fetch_unproved.rs +++ b/packages/rs-sdk/src/platform/fetch_unproved.rs @@ -1,8 +1,12 @@ +use super::{types::evonode::EvoNode, Query}; use crate::error::Error; -use crate::platform::proto; +use crate::mock::MockResponse; use crate::Sdk; -use dapi_grpc::platform::v0::get_current_quorums_info_request::GetCurrentQuorumsInfoRequestV0; -use dapi_grpc::platform::v0::{self as platform_proto}; +use dapi_grpc::platform::v0::{ + self as platform_proto, GetStatusRequest, GetStatusResponse, ResponseMetadata, +}; +use dpp::{dashcore::Network, version::PlatformVersion}; +use drive_proof_verifier::types::EvoNodeStatus; use drive_proof_verifier::unproved::FromUnproved; use rs_dapi_client::{transport::TransportRequest, DapiRequest, RequestSettings}; use std::fmt::Debug; @@ -10,7 +14,7 @@ use std::fmt::Debug; #[async_trait::async_trait] pub trait FetchUnproved where - Self: Sized + Debug, + Self: Sized + Debug + MockResponse, { /// Type of request used to fetch data from Platform. type Request: TransportRequest; @@ -19,20 +23,34 @@ where /// /// ## Parameters /// - `sdk`: An instance of [Sdk]. + /// - `query`: Query used to fetch data from the Platform. /// /// ## Returns /// Returns: /// * `Ok(Some(Self))` when object is found. /// * `Ok(None)` when object is not found. /// * [`Err(Error)`](Error) when an error occurs. - async fn fetch_unproved(sdk: &Sdk) -> Result, Error> { - Self::fetch_unproved_with_settings(sdk, RequestSettings::default()).await + async fn fetch_unproved::Request>>( + sdk: &Sdk, + query: Q, + ) -> Result, Error> + where + Self: FromUnproved< + ::Request, + Request = ::Request, + Response = <::Request as TransportRequest>::Response, + >, + { + let (obj, _mtd) = + Self::fetch_unproved_with_settings(sdk, query, RequestSettings::default()).await?; + Ok(obj) } /// Fetch unproved data from the Platform with custom settings. /// /// ## Parameters /// - `sdk`: An instance of [Sdk]. + /// - `query`: Query used to fetch data from the Platform. /// - `settings`: Request settings for the connection to Platform. /// /// ## Returns @@ -40,36 +58,61 @@ where /// * `Ok(Some(Self))` when object is found. /// * `Ok(None)` when object is not found. /// * [`Err(Error)`](Error) when an error occurs. - async fn fetch_unproved_with_settings( + async fn fetch_unproved_with_settings::Request>>( sdk: &Sdk, + query: Q, settings: RequestSettings, - ) -> Result, Error>; + ) -> Result<(Option, ResponseMetadata), Error> + where + Self: FromUnproved< + ::Request, + Request = ::Request, + Response = <::Request as TransportRequest>::Response, + >, + { + // Default implementation + let request: ::Request = query.query(false)?; + + // Execute the request using the Sdk instance + let response = request.clone().execute(sdk, settings).await?; + + // Parse the response into the appropriate type along with metadata + let (object, mtd): (Option, platform_proto::ResponseMetadata) = + Self::maybe_from_unproved_with_metadata(request, response, sdk.network, sdk.version())?; + + Ok((object, mtd)) + } } -#[async_trait::async_trait] impl FetchUnproved for drive_proof_verifier::types::CurrentQuorumsInfo { type Request = platform_proto::GetCurrentQuorumsInfoRequest; +} - async fn fetch_unproved_with_settings( - sdk: &Sdk, - settings: RequestSettings, - ) -> Result, Error> { - // Create the request from the query - let request = Self::Request { - version: Some(proto::get_current_quorums_info_request::Version::V0( - GetCurrentQuorumsInfoRequestV0 {}, - )), - }; +impl FetchUnproved for EvoNodeStatus { + type Request = EvoNode; +} - // Execute the request using the Sdk instance - let response = request.clone().execute(sdk, settings).await?; +// We need to delegate FromUnproved for the impl FetchUnproved for EvonodeStatus. +#[async_trait::async_trait] +impl FromUnproved for EvoNodeStatus { + type Request = EvoNode; + type Response = GetStatusResponse; - // Parse the response into a CurrentQuorumsInfo object along with metadata - match Self::maybe_from_unproved_with_metadata(request, response, sdk.network, sdk.version()) - { - Ok((Some(info), _metadata)) => Ok(Some(info)), - Ok((None, _metadata)) => Ok(None), - Err(err) => Err(err.into()), - } + fn maybe_from_unproved_with_metadata, O: Into>( + request: I, + response: O, + network: Network, + platform_version: &PlatformVersion, + ) -> Result<(Option, ResponseMetadata), drive_proof_verifier::Error> + where + Self: Sized, + { + // delegate to the FromUnproved + >::maybe_from_unproved_with_metadata( + request.into(), + response, + network, + platform_version, + ) } } diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index e55f0e3e6a2..287f8b6951a 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -2,6 +2,7 @@ //! //! [Query] trait is used to specify individual objects as well as search criteria for fetching multiple objects from Platform. use super::types::epoch::EpochQuery; +use super::types::evonode::EvoNode; use crate::{error::Error, platform::document_query::DocumentQuery}; use dapi_grpc::mock::Mockable; use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::GetContestedResourceIdentityVotesRequestV0; @@ -9,6 +10,7 @@ use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request: use dapi_grpc::platform::v0::get_contested_resources_request::GetContestedResourcesRequestV0; use dapi_grpc::platform::v0::get_evonodes_proposed_epoch_blocks_by_range_request::GetEvonodesProposedEpochBlocksByRangeRequestV0; use dapi_grpc::platform::v0::get_path_elements_request::GetPathElementsRequestV0; +use dapi_grpc::platform::v0::get_status_request::GetStatusRequestV0; use dapi_grpc::platform::v0::get_total_credits_in_platform_request::GetTotalCreditsInPlatformRequestV0; use dapi_grpc::platform::v0::{ self as proto, get_identity_keys_request, get_identity_keys_request::GetIdentityKeysRequestV0, @@ -20,8 +22,8 @@ use dapi_grpc::platform::v0::{ GetTotalCreditsInPlatformRequest, KeyRequestType, }; use dapi_grpc::platform::v0::{ - GetContestedResourceIdentityVotesRequest, GetPrefundedSpecializedBalanceRequest, - GetVotePollsByEndDateRequest, + get_status_request, GetContestedResourceIdentityVotesRequest, + GetPrefundedSpecializedBalanceRequest, GetStatusRequest, GetVotePollsByEndDateRequest, }; use dashcore_rpc::dashcore::{hashes::Hash, ProTxHash}; use dpp::version::PlatformVersionError; @@ -98,7 +100,7 @@ where { fn query(self, prove: bool) -> Result { if !prove { - unimplemented!("queries without proofs are not supported yet"); + tracing::warn!(request= ?self, "sending query without proof, ensure data is trusted"); } Ok(self) } @@ -645,3 +647,15 @@ impl Query for LimitQuery for EvoNode { + fn query(self, _prove: bool) -> Result { + // ignore proof + + let request: GetStatusRequest = GetStatusRequest { + version: Some(get_status_request::Version::V0(GetStatusRequestV0 {})), + }; + + Ok(request) + } +} diff --git a/packages/rs-sdk/src/platform/types.rs b/packages/rs-sdk/src/platform/types.rs index 97a12c40b56..e4fbab2d47e 100644 --- a/packages/rs-sdk/src/platform/types.rs +++ b/packages/rs-sdk/src/platform/types.rs @@ -1,5 +1,6 @@ //! Type-specific implementation for various dpp object types to make queries more convenient and intuitive. pub mod epoch; +pub mod evonode; pub mod identity; pub mod proposed_blocks; mod total_credits_in_platform; diff --git a/packages/rs-sdk/src/platform/types/evonode.rs b/packages/rs-sdk/src/platform/types/evonode.rs new file mode 100644 index 00000000000..01c0630b493 --- /dev/null +++ b/packages/rs-sdk/src/platform/types/evonode.rs @@ -0,0 +1,108 @@ +//! Evo Node represents a network node (server). + +use dapi_grpc::mock::Mockable; +use dapi_grpc::platform::v0::get_status_request::GetStatusRequestV0; +use dapi_grpc::platform::v0::{self as proto, get_status_request, GetStatusRequest}; +use dapi_grpc::tonic::IntoRequest; +pub use drive_proof_verifier::types::EvoNodeStatus; +use futures::future::BoxFuture; +use futures::{FutureExt, TryFutureExt}; +use rs_dapi_client::transport::{ + AppliedRequestSettings, PlatformGrpcClient, TransportClient, TransportRequest, +}; +use rs_dapi_client::{Address, ConnectionPool, RequestSettings}; +#[cfg(feature = "mocks")] +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +/// EvoNode allows querying the status of a single node using DAPI. +/// +/// ## Example +/// +/// ```rust,no_run +/// use dash_sdk::{platform::types::evonode::EvoNode,platform::FetchUnproved, Sdk}; +/// use drive_proof_verifier::types::EvoNodeStatus; +/// use futures::executor::block_on; +/// +/// let sdk = Sdk::new_mock(); +/// let uri: http::Uri = "http://127.0.0.1:1".parse().unwrap(); +/// let node = EvoNode::new(uri.into()); +/// let status = block_on(EvoNodeStatus::fetch_unproved(&sdk, node)).unwrap(); +/// ``` + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "mocks", derive(Serialize, Deserialize))] +pub struct EvoNode(Address); + +impl EvoNode { + /// Creates a new `EvoNode` with the given address. + pub fn new(address: Address) -> Self { + Self(address) + } +} + +impl Mockable for EvoNode { + #[cfg(feature = "mocks")] + fn mock_deserialize(data: &[u8]) -> Option { + serde_json::de::from_slice(data).ok() + } + + #[cfg(feature = "mocks")] + fn mock_serialize(&self) -> Option> { + serde_json::ser::to_vec(self).ok() + } +} +impl TransportRequest for EvoNode { + type Client = PlatformGrpcClient; + type Response = proto::GetStatusResponse; + + const SETTINGS_OVERRIDES: rs_dapi_client::RequestSettings = RequestSettings::default(); + + fn method_name(&self) -> &'static str { + "get_status" + } + + fn execute_transport<'c>( + self, + _client: &'c mut Self::Client, + settings: &AppliedRequestSettings, + ) -> BoxFuture<'c, Result::Error>> { + let uri = self.0.uri(); + // As this is single node connection case, we create a new connection pool with space for a single connection + // and we drop it after use. + // + // We also create a new client to use with this request, so that the user does not need to + // reconfigure SDK to use a single node. + let pool = ConnectionPool::new(1); + let mut client = Self::Client::with_uri_and_settings(uri.clone(), settings, &pool); + let mut grpc_request = GetStatusRequest { + version: Some(get_status_request::Version::V0(GetStatusRequestV0 {})), + } + .into_request(); + + // we need to establish connection only with provided node, so we override client + + if !settings.timeout.is_zero() { + grpc_request.set_timeout(settings.timeout); + } + + async move { + let response = client + .get_status(grpc_request) + .map_ok(|response| response.into_inner()) + .await; + + drop(client); + drop(pool); + response + } + .boxed() + } +} + +impl From for GetStatusRequest { + fn from(_node: EvoNode) -> Self { + // we don't need to send any data to the node, and address is handled in impl TrasportRequest + GetStatusRequestV0 {}.into() + } +} diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index fb8bd7fed8b..abd02e184cc 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -507,6 +507,23 @@ impl Sdk { pub fn shutdown(&self) { self.cancel_token.cancel(); } + + /// Return the [DapiClient] address list + pub fn address_list(&self) -> Result { + match &self.inner { + SdkInstance::Dapi { dapi, version: _ } => { + let address_list_arc = dapi.address_list(); + let address_list_lock = address_list_arc + .read() + .map_err(|e| format!("Failed to read address list: {e}"))?; + Ok(address_list_lock.clone()) + } + #[cfg(feature = "mocks")] + SdkInstance::Mock { .. } => { + unimplemented!("mock Sdk does not have address list") + } + } + } } #[async_trait::async_trait] @@ -765,9 +782,6 @@ impl SdkBuilder { if sdk.context_provider.is_none() { #[cfg(feature = "mocks")] if !self.core_ip.is_empty() { - tracing::warn!("ContextProvider not set; mocking with Dash Core. \ - Please provide your own ContextProvider with SdkBuilder::with_context_provider()."); - let mut context_provider = GrpcContextProvider::new(None, &self.core_ip, self.core_port, &self.core_user, &self.core_password, self.data_contract_cache_size, self.quorum_public_keys_cache_size)?; diff --git a/packages/rs-sdk/tests/fetch/evonode.rs b/packages/rs-sdk/tests/fetch/evonode.rs new file mode 100644 index 00000000000..0d35d5be9f3 --- /dev/null +++ b/packages/rs-sdk/tests/fetch/evonode.rs @@ -0,0 +1,71 @@ +//! Test evo node status and other node-related functionality. + +use super::{common::setup_logs, config::Config}; +use dash_sdk::platform::{types::evonode::EvoNode, FetchUnproved}; +use dpp::dashcore::{hashes::Hash, ProTxHash}; +use drive_proof_verifier::types::EvoNodeStatus; +use http::Uri; +use std::time::Duration; +/// Given some existing evonode URIs, WHEN we connect to them, THEN we get status. +use tokio::time::timeout; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_evonode_status() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("test_evonode_status").await; + + let addresses = cfg.address_list(); + + for address in addresses { + let node = EvoNode::new(address.clone()); + match timeout( + Duration::from_secs(3), + EvoNodeStatus::fetch_unproved(&sdk, node), + ) + .await + { + Ok(Ok(Some(status))) => { + tracing::debug!(?status, ?address, "evonode status"); + // Add assertions here to verify the status contents + assert!( + status.chain.latest_block_height > 0, + "latest block height must be positive" + ); + assert!( + status.node.pro_tx_hash.unwrap_or_default().len() == ProTxHash::LEN, + "latest block hash must be non-empty" + ); + // Add more specific assertions based on expected status properties + } + Ok(Ok(None)) => { + tracing::warn!(?address, "No status found for evonode"); + panic!("No status found for evonode"); + } + Ok(Err(e)) => { + tracing::error!(?address, error = ?e, "Error fetching evonode status"); + } + Err(_) => { + tracing::error!(?address, "Timeout while fetching evonode status"); + } + } + } +} + +/// Given invalid evonode URI, when we request status, we get error. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_evonode_status_refused() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("test_evonode_status_refused").await; + + let uri: Uri = "http://127.0.0.1:1".parse().unwrap(); + + let node = EvoNode::new(uri.clone().into()); + let result = EvoNodeStatus::fetch_unproved(&sdk, node).await; + tracing::debug!(?result, ?uri, "evonode status"); + + assert!(result.is_err()); +} diff --git a/packages/rs-sdk/tests/fetch/mod.rs b/packages/rs-sdk/tests/fetch/mod.rs index 76e6c84c69c..363e35f069f 100644 --- a/packages/rs-sdk/tests/fetch/mod.rs +++ b/packages/rs-sdk/tests/fetch/mod.rs @@ -17,6 +17,7 @@ mod contested_resource_voters; mod data_contract; mod document; mod epoch; +mod evonode; mod identity; mod identity_contract_nonce; mod mock_fetch; diff --git a/packages/rs-sdk/tests/vectors/test_evonode_status/.gitkeep b/packages/rs-sdk/tests/vectors/test_evonode_status/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rs-sdk/tests/vectors/test_evonode_status/msg_EvoNode_244e3005966550cd3cb2837d3bca1c40d35547373d23f3ba329df2b6d993b374.json b/packages/rs-sdk/tests/vectors/test_evonode_status/msg_EvoNode_244e3005966550cd3cb2837d3bca1c40d35547373d23f3ba329df2b6d993b374.json new file mode 100644 index 00000000000..6eafe3314ea Binary files /dev/null and b/packages/rs-sdk/tests/vectors/test_evonode_status/msg_EvoNode_244e3005966550cd3cb2837d3bca1c40d35547373d23f3ba329df2b6d993b374.json differ diff --git a/packages/rs-sdk/tests/vectors/test_evonode_status_refused/.gitkeep b/packages/rs-sdk/tests/vectors/test_evonode_status_refused/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rs-sdk/tests/vectors/test_evonode_status_refused/msg_EvoNode_6db392ff1869b56ecc7de9ace5864123671ed14d3f0c537aa8e878d24e529de5.json b/packages/rs-sdk/tests/vectors/test_evonode_status_refused/msg_EvoNode_6db392ff1869b56ecc7de9ace5864123671ed14d3f0c537aa8e878d24e529de5.json new file mode 100644 index 00000000000..c80da24adbe Binary files /dev/null and b/packages/rs-sdk/tests/vectors/test_evonode_status_refused/msg_EvoNode_6db392ff1869b56ecc7de9ace5864123671ed14d3f0c537aa8e878d24e529de5.json differ