From 8183a34e9abb6f2dd079eae6ea51a27f16f4cff5 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 08:28:01 +0700 Subject: [PATCH 1/3] feat(platform)!: add app-connect login response system contract --- .codecov.yml | 1 + .../package-filters/js-packages-direct.yml | 3 + .../js-packages-no-workflows.yml | 4 + .github/package-filters/js-packages.yml | 5 + .../package-filters/rs-packages-direct.yml | 5 + .../rs-packages-no-workflows.yml | 6 + .github/package-filters/rs-packages.yml | 7 + .../package-filters/test-suite-triggers.yml | 1 + .github/workflows/tests-rs-workspace.yml | 1 + .github/workflows/tests.yml | 1 + .pnp.cjs | 129 +++++++----- Cargo.lock | 12 ++ Cargo.toml | 1 + Dockerfile | 5 + book/src/architecture/overview.md | 2 +- docs/protocol/app-connect.md | 86 ++++++++ package.json | 2 + packages/app-connect-contract/.mocharc.yml | 2 + packages/app-connect-contract/Cargo.toml | 15 ++ packages/app-connect-contract/LICENSE | 20 ++ packages/app-connect-contract/README.md | 39 ++++ .../app-connect-contract/eslint.config.mjs | 10 + .../app-connect-contract/lib/systemIds.js | 4 + packages/app-connect-contract/package.json | 27 +++ .../v1/app-connect-contract-documents.json | 55 +++++ packages/app-connect-contract/src/error.rs | 17 ++ packages/app-connect-contract/src/lib.rs | 63 ++++++ packages/app-connect-contract/src/v1/mod.rs | 25 +++ .../app-connect-contract/test/bootstrap.js | 30 +++ .../test/unit/appConnectContract.spec.js | 199 ++++++++++++++++++ packages/data-contracts/Cargo.toml | 3 + packages/data-contracts/src/error.rs | 18 ++ packages/data-contracts/src/lib.rs | 26 ++- packages/rs-dpp/Cargo.toml | 2 + packages/rs-dpp/src/system_data_contracts.rs | 94 +++++++++ .../create_genesis_state/mod.rs | 10 +- .../create_genesis_state/v2/mod.rs | 173 +++++++++++++++ .../mod.rs | 114 ++++++++++ .../v0/mod.rs | 116 +++++++++- .../batch/tests/document/deletion.rs | 4 +- .../batch/tests/document/dpns.rs | 4 +- .../batch/tests/document/index_only.rs | 47 +++-- .../batch/tests/document/replacement.rs | 4 +- .../rs-drive/src/cache/system_contracts.rs | 32 +++ .../index_only_scalar_terminal_e2e_tests.rs | 131 ++++++++++++ .../drive_abci_method_versions/v10.rs | 2 +- .../feature_initial_protocol_versions.rs | 5 + .../system_data_contract_versions/mod.rs | 1 + .../system_data_contract_versions/v1.rs | 1 + .../system_data_contract_versions/v2.rs | 1 + .../system_data_contract_versions/v3.rs | 7 + .../rs-platform-version/src/version/v14.rs | 7 + packages/rs-sdk-ffi/Cargo.toml | 1 + .../Cargo.toml | 2 + .../src/provider.rs | 51 +++++ packages/rs-sdk/Cargo.toml | 1 + yarn.lock | 14 ++ 57 files changed, 1570 insertions(+), 78 deletions(-) create mode 100644 docs/protocol/app-connect.md create mode 100644 packages/app-connect-contract/.mocharc.yml create mode 100644 packages/app-connect-contract/Cargo.toml create mode 100644 packages/app-connect-contract/LICENSE create mode 100644 packages/app-connect-contract/README.md create mode 100644 packages/app-connect-contract/eslint.config.mjs create mode 100644 packages/app-connect-contract/lib/systemIds.js create mode 100644 packages/app-connect-contract/package.json create mode 100644 packages/app-connect-contract/schema/v1/app-connect-contract-documents.json create mode 100644 packages/app-connect-contract/src/error.rs create mode 100644 packages/app-connect-contract/src/lib.rs create mode 100644 packages/app-connect-contract/src/v1/mod.rs create mode 100644 packages/app-connect-contract/test/bootstrap.js create mode 100644 packages/app-connect-contract/test/unit/appConnectContract.spec.js create mode 100644 packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs diff --git a/.codecov.yml b/.codecov.yml index 432c1cff2a2..712578c8e50 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -52,6 +52,7 @@ ignore: - "packages/data-contracts/src/**" - "packages/dpns-contract/src/**" - "packages/keyword-search-contract/src/**" + - "packages/app-connect-contract/src/**" - "packages/masternode-reward-shares-contract/src/**" - "packages/token-history-contract/src/**" - "packages/wallet-utils-contract/src/**" diff --git a/.github/package-filters/js-packages-direct.yml b/.github/package-filters/js-packages-direct.yml index a3e768e6901..37f0c53aec4 100644 --- a/.github/package-filters/js-packages-direct.yml +++ b/.github/package-filters/js-packages-direct.yml @@ -10,6 +10,9 @@ '@dashevo/keyword-search-contract': - packages/keyword-search-contract/** +'@dashevo/app-connect-contract': + - packages/app-connect-contract/** + '@dashevo/dashpay-contract': - packages/dashpay-contract/** diff --git a/.github/package-filters/js-packages-no-workflows.yml b/.github/package-filters/js-packages-no-workflows.yml index 983ffd5f1d2..f14c729cd08 100644 --- a/.github/package-filters/js-packages-no-workflows.yml +++ b/.github/package-filters/js-packages-no-workflows.yml @@ -10,6 +10,9 @@ '@dashevo/keyword-search-contract': &keyword-search-contract - packages/keyword-search-contract/** +'@dashevo/app-connect-contract': &app-connect-contract + - packages/app-connect-contract/** + '@dashevo/dashpay-contract': &dashpay-contract - packages/dashpay-contract/** @@ -35,6 +38,7 @@ - *token-history-contract - *document-history-contract - *keyword-search-contract + - *app-connect-contract - packages/rs-platform-serialization/** - packages/rs-platform-serialization-derive/** - packages/rs-platform-value/** diff --git a/.github/package-filters/js-packages.yml b/.github/package-filters/js-packages.yml index d41985c9d1e..087abed9a45 100644 --- a/.github/package-filters/js-packages.yml +++ b/.github/package-filters/js-packages.yml @@ -14,6 +14,10 @@ - .github/workflows/tests* - packages/keyword-search-contract/** +'@dashevo/app-connect-contract': &app-connect-contract + - .github/workflows/tests* + - packages/app-connect-contract/** + '@dashevo/dashpay-contract': &dashpay-contract - .github/workflows/tests* - packages/dashpay-contract/** @@ -45,6 +49,7 @@ - *token-history-contract - *document-history-contract - *keyword-search-contract + - *app-connect-contract - packages/rs-platform-serialization/** - packages/rs-platform-serialization-derive/** - packages/rs-platform-value/** diff --git a/.github/package-filters/rs-packages-direct.yml b/.github/package-filters/rs-packages-direct.yml index 441c8137023..e0db8b9abc6 100644 --- a/.github/package-filters/rs-packages-direct.yml +++ b/.github/package-filters/rs-packages-direct.yml @@ -18,6 +18,11 @@ keyword-search-contract: - packages/keyword-search-contract/schema/** - packages/keyword-search-contract/Cargo.toml +app-connect-contract: + - packages/app-connect-contract/src/** + - packages/app-connect-contract/schema/** + - packages/app-connect-contract/Cargo.toml + dashpay-contract: - packages/dashpay-contract/src/** - packages/dashpay-contract/schema/** diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 90835d0429f..b0c596ec3a8 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -18,6 +18,11 @@ keyword-search-contract: &keyword-search-contract - packages/keyword-search-contract/schema/** - packages/keyword-search-contract/Cargo.toml +app-connect-contract: &app-connect-contract + - packages/app-connect-contract/src/** + - packages/app-connect-contract/schema/** + - packages/app-connect-contract/Cargo.toml + dashpay-contract: &dashpay-contract - packages/dashpay-contract/src/** - packages/dashpay-contract/schema/** @@ -51,6 +56,7 @@ data-contracts: &data-contracts - *token-history-contract - *keyword-search-contract - *document-history-contract + - *app-connect-contract dpp: &dpp - packages/rs-dpp/** diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index 6fae2aa84ab..1174da0c998 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -22,6 +22,12 @@ keyword-search-contract: &keyword-search-contract - packages/keyword-search-contract/schema/** - packages/keyword-search-contract/Cargo.toml +app-connect-contract: &app-connect-contract + - .github/workflows/tests* + - packages/app-connect-contract/src/** + - packages/app-connect-contract/schema/** + - packages/app-connect-contract/Cargo.toml + dashpay-contract: &dashpay-contract - .github/workflows/tests* - packages/dashpay-contract/src/** @@ -61,6 +67,7 @@ data-contracts: &data-contracts - *token-history-contract - *keyword-search-contract - *document-history-contract + - *app-connect-contract dpp: &dpp - .github/workflows/tests* diff --git a/.github/package-filters/test-suite-triggers.yml b/.github/package-filters/test-suite-triggers.yml index 7f814cd8811..1f60d0ec9f9 100644 --- a/.github/package-filters/test-suite-triggers.yml +++ b/.github/package-filters/test-suite-triggers.yml @@ -30,6 +30,7 @@ run: - packages/token-history-contract/** - packages/document-history-contract/** - packages/keyword-search-contract/** + - packages/app-connect-contract/** - packages/wallet-utils-contract/** # Local network scripts and action - .github/actions/local-network/** diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index f6a979e1402..67b44711e3c 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -419,6 +419,7 @@ jobs: --package token-history-contract \ --package wallet-utils-contract \ --package keyword-search-contract \ + --package app-connect-contract \ --all-features \ --locked \ -E 'not test(~shield) and (not binary_id(=drive-abci::strategy_tests) or test(~comprehensive_mixed_operations) or test(~process_proposal_collision))' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ef36f1b9c79..e4a34213637 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -318,6 +318,7 @@ jobs: - .github/workflows/swift-sdk-freeze-release.yml - .github/workflows/tests.yml - packages/swift-sdk/** + - packages/app-connect-contract/** - packages/dapi-grpc/** - packages/dashpay-contract/** - packages/data-contracts/** diff --git a/.pnp.cjs b/.pnp.cjs index f2c82efb91a..1843bea9e57 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -14,6 +14,10 @@ const RAW_RUNTIME_STATE = "name": "@dashevo/platform",\ "reference": "workspace:."\ },\ + {\ + "name": "@dashevo/app-connect-contract",\ + "reference": "workspace:packages/app-connect-contract"\ + },\ {\ "name": "@dashevo/bench-suite",\ "reference": "workspace:packages/bench-suite"\ @@ -111,6 +115,7 @@ const RAW_RUNTIME_STATE = "ignorePatternData": "(^(?:\\\\.yarn\\\\/sdks(?:\\\\/(?!\\\\.{1,2}(?:\\\\/|$))(?:(?:(?!(?:^|\\\\/)\\\\.{1,2}(?:\\\\/|$)).)*?)|$))$)",\ "pnpZipBackend": "libzip",\ "fallbackExclusionList": [\ + ["@dashevo/app-connect-contract", ["workspace:packages/app-connect-contract"]],\ ["@dashevo/bench-suite", ["workspace:packages/bench-suite"]],\ ["@dashevo/dapi", ["workspace:packages/dapi"]],\ ["@dashevo/dapi-client", ["workspace:packages/js-dapi-client"]],\ @@ -2493,6 +2498,22 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@dashevo/app-connect-contract", [\ + ["workspace:packages/app-connect-contract", {\ + "packageLocation": "./packages/app-connect-contract/",\ + "packageDependencies": [\ + ["@dashevo/app-connect-contract", "workspace:packages/app-connect-contract"],\ + ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ + ["chai", "npm:4.3.10"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ + ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ + ["mocha", "npm:11.1.0"],\ + ["sinon", "npm:18.0.1"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@dashevo/bench-suite", [\ ["workspace:packages/bench-suite", {\ "packageLocation": "./packages/bench-suite/",\ @@ -2545,8 +2566,8 @@ const RAW_RUNTIME_STATE = ["bs58", "npm:4.0.1"],\ ["cbor", "npm:8.1.0"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["dotenv", "npm:8.6.0"],\ ["dotenv-expand", "npm:5.1.0"],\ ["dotenv-safe", "npm:8.2.0"],\ @@ -2562,7 +2583,7 @@ const RAW_RUNTIME_STATE = ["pino-pretty", "npm:10.2.3"],\ ["semver", "npm:7.5.3"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"],\ ["swagger-jsdoc", "npm:3.7.0"],\ ["ws", "virtual:b375dcefccef90d9158d5f197a75395cffedb61772e66f2efcf31c6c8e30c82a6423e0d52b091b15b4fa72cda43a09256ed00b6ce89b9cfb14074f087b9c8496#npm:8.17.1"]\ ],\ @@ -2586,11 +2607,11 @@ const RAW_RUNTIME_STATE = ["buffer", "npm:6.0.3"],\ ["cbor", "npm:8.1.0"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ ["comment-parser", "npm:0.7.6"],\ ["core-js", "npm:3.33.2"],\ ["crypto-browserify", "npm:3.12.1"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["events", "npm:3.3.0"],\ ["google-protobuf", "npm:3.19.1"],\ @@ -2607,7 +2628,7 @@ const RAW_RUNTIME_STATE = ["path-browserify", "npm:1.0.1"],\ ["process", "npm:0.11.10"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"],\ ["stream-browserify", "npm:3.0.0"],\ ["string_decoder", "npm:1.3.0"],\ ["undici", "npm:6.25.0"],\ @@ -2630,15 +2651,15 @@ const RAW_RUNTIME_STATE = ["@grpc/grpc-js", "npm:1.14.3"],\ ["@improbable-eng/grpc-web", "virtual:c60802fb91064892a66eac238372b1f92273bed401eb316b63f9eae73923158c5dcd2982eb1e735f7e36e089d74b3ee3773666256e3b50594593c762aa939877#npm:0.15.0"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["google-protobuf", "npm:3.19.1"],\ ["long", "npm:5.2.0"],\ ["mocha", "npm:11.1.0"],\ ["mocha-sinon", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.1.2"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2731,11 +2752,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/dashpay-contract", "workspace:packages/dashpay-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2757,11 +2778,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/document-history-contract", "workspace:packages/document-history-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2787,11 +2808,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/dpns-contract", "workspace:packages/dpns-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2849,8 +2870,8 @@ const RAW_RUNTIME_STATE = ["@grpc/proto-loader", "npm:0.5.6"],\ ["cbor", "npm:8.1.0"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["lodash", "npm:4.17.23"],\ ["long", "npm:5.2.0"],\ @@ -2859,7 +2880,7 @@ const RAW_RUNTIME_STATE = ["nyc", "npm:15.1.0"],\ ["semver", "npm:7.5.3"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2871,11 +2892,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/keyword-search-contract", "workspace:packages/keyword-search-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2887,11 +2908,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/masternode-reward-shares-contract", "workspace:packages/masternode-reward-shares-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -2941,10 +2962,10 @@ const RAW_RUNTIME_STATE = ["buffer", "npm:6.0.3"],\ ["bufferutil", "npm:4.0.6"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ ["crypto-browserify", "npm:3.12.1"],\ ["dash", "workspace:packages/js-dash-sdk"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["dotenv-safe", "npm:8.2.0"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["events", "npm:3.3.0"],\ @@ -2969,7 +2990,7 @@ const RAW_RUNTIME_STATE = ["semver", "npm:7.5.3"],\ ["setimmediate", "npm:1.0.5"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"],\ ["stream-browserify", "npm:3.0.0"],\ ["stream-http", "npm:3.2.0"],\ ["string_decoder", "npm:1.3.0"],\ @@ -3020,11 +3041,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/token-history-contract", "workspace:packages/token-history-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -3045,10 +3066,10 @@ const RAW_RUNTIME_STATE = ["buffer", "npm:6.0.3"],\ ["cbor", "npm:8.1.0"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ ["crypto-browserify", "npm:3.12.1"],\ ["crypto-js", "npm:4.2.0"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["dotenv-safe", "npm:8.2.0"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["events", "npm:3.3.0"],\ @@ -3071,7 +3092,7 @@ const RAW_RUNTIME_STATE = ["process", "npm:0.11.10"],\ ["setimmediate", "npm:1.0.5"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"],\ ["stream-browserify", "npm:3.0.0"],\ ["stream-http", "npm:3.2.0"],\ ["string_decoder", "npm:1.3.0"],\ @@ -3093,11 +3114,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/wallet-utils-contract", "workspace:packages/wallet-utils-contract"],\ ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -3122,11 +3143,11 @@ const RAW_RUNTIME_STATE = ["bs58", "npm:4.0.1"],\ ["buffer", "npm:6.0.3"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ ["chai-exclude", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:2.1.0"],\ ["chai-string", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:1.5.0"],\ ["crypto-browserify", "npm:3.12.1"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["events", "npm:3.3.0"],\ ["fast-json-patch", "npm:3.1.1"],\ @@ -3145,7 +3166,7 @@ const RAW_RUNTIME_STATE = ["path-browserify", "npm:1.0.1"],\ ["process", "npm:0.11.10"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"],\ ["stream-browserify", "npm:3.0.0"],\ ["stream-http", "npm:3.2.0"],\ ["string_decoder", "npm:1.3.0"],\ @@ -3243,11 +3264,11 @@ const RAW_RUNTIME_STATE = ["@dashevo/wasm-dpp", "workspace:packages/wasm-dpp"],\ ["@dashevo/withdrawals-contract", "workspace:packages/withdrawals-contract"],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["eslint", "virtual:de32c10d523830f1843784ae863166d6ef2e074b6da9615f2b3296a1f90385ed3f59e274e3957326ba7cf3442d82470d9e1ec01e6720989a570c075c95d90dbc#npm:9.39.2"],\ ["mocha", "npm:11.1.0"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -8398,12 +8419,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1", {\ - "packageLocation": "./.yarn/__virtual__/chai-as-promised-virtual-8795af412c/0/cache/chai-as-promised-npm-7.1.1-cdc17e4612-5d9ecab37b.zip/node_modules/chai-as-promised/",\ + ["virtual:98d1afeac78a19485e4cb7428abff692e58b6fc468d8040035b560ed49383fc95857be6b5014af27e53063e6f08b654690c2b945f3443c22dd60c6b083684b3c#npm:7.1.1", {\ + "packageLocation": "./.yarn/__virtual__/chai-as-promised-virtual-d444a37be5/0/cache/chai-as-promised-npm-7.1.1-cdc17e4612-5d9ecab37b.zip/node_modules/chai-as-promised/",\ "packageDependencies": [\ - ["@types/chai", null],\ + ["@types/chai", "npm:4.3.20"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ + ["chai-as-promised", "virtual:98d1afeac78a19485e4cb7428abff692e58b6fc468d8040035b560ed49383fc95857be6b5014af27e53063e6f08b654690c2b945f3443c22dd60c6b083684b3c#npm:7.1.1"],\ ["check-error", "npm:1.0.3"]\ ],\ "packagePeers": [\ @@ -8412,12 +8433,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:98d1afeac78a19485e4cb7428abff692e58b6fc468d8040035b560ed49383fc95857be6b5014af27e53063e6f08b654690c2b945f3443c22dd60c6b083684b3c#npm:7.1.1", {\ - "packageLocation": "./.yarn/__virtual__/chai-as-promised-virtual-d444a37be5/0/cache/chai-as-promised-npm-7.1.1-cdc17e4612-5d9ecab37b.zip/node_modules/chai-as-promised/",\ + ["virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1", {\ + "packageLocation": "./.yarn/__virtual__/chai-as-promised-virtual-d5c799738c/0/cache/chai-as-promised-npm-7.1.1-cdc17e4612-5d9ecab37b.zip/node_modules/chai-as-promised/",\ "packageDependencies": [\ - ["@types/chai", "npm:4.3.20"],\ + ["@types/chai", null],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:98d1afeac78a19485e4cb7428abff692e58b6fc468d8040035b560ed49383fc95857be6b5014af27e53063e6f08b654690c2b945f3443c22dd60c6b083684b3c#npm:7.1.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ ["check-error", "npm:1.0.3"]\ ],\ "packagePeers": [\ @@ -9671,11 +9692,11 @@ const RAW_RUNTIME_STATE = ["begoo", "npm:2.0.2"],\ ["bs58", "npm:4.0.1"],\ ["chai", "npm:4.3.10"],\ - ["chai-as-promised", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:7.1.1"],\ + ["chai-as-promised", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:7.1.1"],\ ["chalk", "npm:4.1.2"],\ ["cron", "npm:2.1.0"],\ ["dashmate", "workspace:packages/dashmate"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"],\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"],\ ["diskusage-ng", "npm:1.0.4"],\ ["dockerode", "npm:4.0.9"],\ ["dot", "npm:1.1.3"],\ @@ -9704,7 +9725,7 @@ const RAW_RUNTIME_STATE = ["rxjs", "npm:6.6.7"],\ ["semver", "npm:7.5.3"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"],\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"],\ ["systeminformation", "npm:5.31.1"],\ ["table", "npm:6.8.1"],\ ["tar", "npm:7.5.10"],\ @@ -10228,12 +10249,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1", {\ - "packageLocation": "./.yarn/__virtual__/dirty-chai-virtual-a3086bc2f4/0/cache/dirty-chai-npm-2.0.1-acaf82c8df-b4f3d1ea01.zip/node_modules/dirty-chai/",\ + ["virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1", {\ + "packageLocation": "./.yarn/__virtual__/dirty-chai-virtual-7efea90668/0/cache/dirty-chai-npm-2.0.1-acaf82c8df-b4f3d1ea01.zip/node_modules/dirty-chai/",\ "packageDependencies": [\ ["@types/chai", null],\ ["chai", "npm:4.3.10"],\ - ["dirty-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:2.0.1"]\ + ["dirty-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:2.0.1"]\ ],\ "packagePeers": [\ "@types/chai",\ @@ -19781,14 +19802,14 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0", {\ - "packageLocation": "./.yarn/__virtual__/sinon-chai-virtual-9ba2472305/0/cache/sinon-chai-npm-3.7.0-8e6588805e-028853eb8a.zip/node_modules/sinon-chai/",\ + ["virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0", {\ + "packageLocation": "./.yarn/__virtual__/sinon-chai-virtual-68fbcb0797/0/cache/sinon-chai-npm-3.7.0-8e6588805e-028853eb8a.zip/node_modules/sinon-chai/",\ "packageDependencies": [\ ["@types/chai", null],\ ["@types/sinon", null],\ ["chai", "npm:4.3.10"],\ ["sinon", "npm:18.0.1"],\ - ["sinon-chai", "virtual:595d7482cc8ddf98ee6aef33fc48b46393554ab5f17f851ef62e6e39315e53666c3e66226b978689aa0bc7f1e83a03081511a21db1c381362fe67614887077f9#npm:3.7.0"]\ + ["sinon-chai", "virtual:5066f1efd4c78a5ddf1dc175fd2039811919d09bb6f7aa5f2b46141ac45f2e6a675ff6260802f91c4f0e827a9565804d3931db690e7aa741774d17536ffb79fb#npm:3.7.0"]\ ],\ "packagePeers": [\ "@types/chai",\ diff --git a/Cargo.lock b/Cargo.lock index 95988364714..e46f069202c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,17 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "app-connect-contract" +version = "4.2.0-beta.3" +dependencies = [ + "base58", + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "apple-native-keyring-store" version = "1.0.0" @@ -1859,6 +1870,7 @@ dependencies = [ name = "data-contracts" version = "4.2.0-beta.3" dependencies = [ + "app-connect-contract", "dashpay-contract", "document-history-contract", "dpns-contract", diff --git a/Cargo.toml b/Cargo.toml index c4baeee1181..09d41d94b47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "packages/token-history-contract", "packages/document-history-contract", "packages/keyword-search-contract", + "packages/app-connect-contract", "packages/rs-sdk-ffi", "packages/wasm-drive-verify", "packages/dash-platform-balance-checker", diff --git a/Dockerfile b/Dockerfile index 13a762334ce..df982026d38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -401,6 +401,7 @@ COPY --parents \ packages/token-history-contract \ packages/document-history-contract \ packages/keyword-search-contract \ + packages/app-connect-contract \ packages/data-contracts \ packages/strategy-tests \ packages/simple-signer \ @@ -523,6 +524,7 @@ COPY --parents \ packages/token-history-contract \ packages/document-history-contract \ packages/keyword-search-contract \ + packages/app-connect-contract \ packages/withdrawals-contract \ packages/masternode-reward-shares-contract \ packages/dpns-contract \ @@ -705,6 +707,7 @@ COPY --parents \ packages/token-history-contract \ packages/document-history-contract \ packages/keyword-search-contract \ + packages/app-connect-contract \ packages/masternode-reward-shares-contract \ packages/dpns-contract \ packages/data-contracts \ @@ -847,6 +850,7 @@ COPY --from=build-dashmate-helper /platform/packages/wallet-utils-contract packa COPY --from=build-dashmate-helper /platform/packages/token-history-contract packages/token-history-contract COPY --from=build-dashmate-helper /platform/packages/document-history-contract packages/document-history-contract COPY --from=build-dashmate-helper /platform/packages/keyword-search-contract packages/keyword-search-contract +COPY --from=build-dashmate-helper /platform/packages/app-connect-contract packages/app-connect-contract COPY --from=build-dashmate-helper /platform/packages/withdrawals-contract packages/withdrawals-contract COPY --from=build-dashmate-helper /platform/packages/masternode-reward-shares-contract packages/masternode-reward-shares-contract COPY --from=build-dashmate-helper /platform/packages/dpns-contract packages/dpns-contract @@ -950,6 +954,7 @@ COPY --parents \ packages/token-history-contract \ packages/document-history-contract \ packages/keyword-search-contract \ + packages/app-connect-contract \ packages/withdrawals-contract \ packages/masternode-reward-shares-contract \ packages/dpns-contract \ diff --git a/book/src/architecture/overview.md b/book/src/architecture/overview.md index f0d24e394f7..c2e61bedba8 100644 --- a/book/src/architecture/overview.md +++ b/book/src/architecture/overview.md @@ -281,7 +281,7 @@ Here is a simplified view of every Rust workspace member, grouped by role: | **gRPC definitions** | `dapi-grpc` | | **WASM bindings** | `wasm-dpp`, `wasm-dpp2`, `wasm-sdk`, `wasm-drive-verify` | | **iOS/FFI** | `rs-sdk-ffi` | -| **System contracts** | `dpns-contract`, `dashpay-contract`, `withdrawals-contract`, `masternode-reward-shares-contract`, `wallet-utils-contract`, `token-history-contract`, `keyword-search-contract`, `data-contracts` | +| **System contracts** | `dpns-contract`, `dashpay-contract`, `withdrawals-contract`, `masternode-reward-shares-contract`, `wallet-utils-contract`, `token-history-contract`, `keyword-search-contract`, `document-history-contract`, `app-connect-contract`, `data-contracts` | | **Tooling** | `dashmate` (JS), `strategy-tests`, `simple-signer`, `check-features`, `json-schema-compatibility-validator` | | **Other** | `dash-platform-macros`, `rs-dash-event-bus`, `rs-platform-wallet`, `dash-platform-balance-checker`, `rs-dapi` | diff --git a/docs/protocol/app-connect.md b/docs/protocol/app-connect.md new file mode 100644 index 00000000000..37d5ac62177 --- /dev/null +++ b/docs/protocol/app-connect.md @@ -0,0 +1,86 @@ +# App Connect login responses + +The App Connect system contract gives wallet-to-app login responses a fixed +home on every network. It activates at protocol version 14, both at genesis +and when an existing chain upgrades from version 13. + +- Contract ID: `H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ` +- Owner: the all-zero system identity +- Registry entry: `SystemDataContract::AppConnect = 9` +- Schema version: 1 +- Document type: `loginKeyResponse` + +## Response schema + +A wallet publishes a response under the identity whose keys the user approved +sharing with the app. Ordinary identities may create responses. + +| Property | Type | Meaning | +| --- | --- | --- | +| `appEphemeralPubKeyHash` | 20 bytes, required | `hash160` of the ephemeral public key in the app's request; identifies that request | +| `walletEphemeralPubKey` | 33 bytes, required | Compressed wallet ephemeral public key; the app combines it with its own ephemeral private key to derive the shared secret | +| `encryptedPayload` | 60–572 bytes, required | Granted private keys encrypted to the app: a 28-byte envelope plus 32 bytes per key, for 1–17 keys (session key plus up to eight bindings with both purposes) | + +Additional properties are rejected. The schema enforces lengths, but does not +parse secp256k1 points, validate an encryption tag, or enforce 32-byte increments +inside the payload range. Those are client protocol checks. The app's contract +ID, requested permissions and any manifest are outside this response schema. + +## Storage and lookup + +The type is immutable, deletable and `indexOnly`: + +```json +{ + "indexOnly": true, + "documentsMutable": false, + "canBeDeleted": true, + "creationRestrictionMode": 0, + "indices": [{ + "name": "byRequest", + "terminal": ["appEphemeralPubKeyHash", "$ownerId"] + }], + "entryPayload": ["walletEphemeralPubKey", "encryptedPayload"] +} +``` + +The flat index stores one entry at +`[…, loginKeyResponse, "\0appEphemeralPubKeyHash\0$ownerId", 0, hash ‖ owner]`. +Its value contains the row commitment followed by the framed payload +properties in name order (`encryptedPayload`, then `walletEphemeralPubKey`). +There is no primary document row, document-ID lookup, or tree per request. + +The composite key enforces one response per request **per identity**. The +request hash is public, so uniqueness on the hash alone would allow an +observer to block the wallet. Including `$ownerId` lets other identities +respond without reserving the wallet's entry. + +Apps query `appEphemeralPubKeyHash == requestHash` to retrieve candidates and +use `$ownerId` as the next ordered component for pagination. Once the identity +is known, equality on both fields selects one response. Query proofs +reconstruct all three properties and the owner from the entry. A synthesized +`$id` is not a primary row identifier or sufficient input for deletion. + +## Approval and re-login + +1. The app creates a fresh ephemeral key pair and keeps the request context. +2. After user approval, the wallet provisions the granted identity keys and + waits for them to be confirmed before publishing the response. The wallet + and app agree on encryption and how the grant binds to the request context. +3. The app checks each candidate's encryption tag and framing, then verifies + that the granted keys are live on that candidate's `$ownerId`, with the + approved purposes, bounds, expiry and budget. ECDH to a public ephemeral key + alone does not authenticate the responding user; pairing and approval must + establish which identity the app intends to log in. +4. The wallet retains its previous response values per identity and app. On + re-login it deletes that response using an index-only delete carrying + **all three original values**, then creates a response for the fresh + request. An ordinary document replace is not supported. Deletion checks + the stored commitment, so a changed ciphertext cannot delete the old entry. +5. If local tracking is lost, the wallet can create the fresh response. An old + entry remains until its owner deletes it with the original values. There is + no owner-only or per-app secondary index and no automatic cleanup. + +Platform treats responses as per-request entries. The wallet manages their +per-app lifecycle and any identity-key revocation separately; deleting a +response does not revoke keys that an app has already received. diff --git a/package.json b/package.json index c493b06cb7d..497e861f4dd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:dpns-contract": "ultra -r --filter \"packages/@(dpns-contract|js-dash-sdk|js-drive|js-dapi-client|wasm-dpp|wallet-lib|dapi|platform-test-suite)\" test", "test:token-history-contract": "ultra -r --filter \"packages/@(token-history-contract|js-dash-sdk|js-drive|js-dapi-client|wasm-dpp|wallet-lib|dapi|platform-test-suite)\" test", "test:document-history-contract": "ultra -r --filter \"packages/@(document-history-contract|js-dash-sdk|js-drive|js-dapi-client|wasm-dpp|wallet-lib|dapi|platform-test-suite)\" test", + "test:app-connect-contract": "ultra -r --filter \"packages/@(app-connect-contract|js-dash-sdk|js-drive|js-dapi-client|wasm-dpp|wallet-lib|dapi|platform-test-suite)\" test", "test:dapi-client": "ultra -r --filter \"packages/@(js-dapi-client|wallet-lib|js-dash-sdk|platform-test-suite)\" test", "test:sdk": "ultra -r --filter \"packages/@(js-dash-sdk|platform-test-suite)\" test", "test:spv": "ultra -r --filter \"packages/@(dash-spv|js-dapi-client)\" test", @@ -77,6 +78,7 @@ "packages/token-history-contract", "packages/document-history-contract", "packages/keyword-search-contract", + "packages/app-connect-contract", "packages/wasm-drive-verify", "packages/wasm-sdk", "packages/js-evo-sdk" diff --git a/packages/app-connect-contract/.mocharc.yml b/packages/app-connect-contract/.mocharc.yml new file mode 100644 index 00000000000..164b941c1b6 --- /dev/null +++ b/packages/app-connect-contract/.mocharc.yml @@ -0,0 +1,2 @@ +require: test/bootstrap.js +recursive: true diff --git a/packages/app-connect-contract/Cargo.toml b/packages/app-connect-contract/Cargo.toml new file mode 100644 index 00000000000..439a431aca4 --- /dev/null +++ b/packages/app-connect-contract/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "app-connect-contract" +description = "App Connect data contract schema and tools. The wallet's encrypted response to an app login request" +version.workspace = true +edition = "2021" +rust-version.workspace = true +license = "MIT" + +[dependencies] +thiserror = "2.0.12" +platform-version = { path = "../rs-platform-version" } +serde_json = { version = "1.0" } +platform-value = { path = "../rs-platform-value" } +[dev-dependencies] +base58 = "0.2.0" diff --git a/packages/app-connect-contract/LICENSE b/packages/app-connect-contract/LICENSE new file mode 100644 index 00000000000..3be95833750 --- /dev/null +++ b/packages/app-connect-contract/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2019 Dash Core Group, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/app-connect-contract/README.md b/packages/app-connect-contract/README.md new file mode 100644 index 00000000000..c2289e5971c --- /dev/null +++ b/packages/app-connect-contract/README.md @@ -0,0 +1,39 @@ +# App Connect Contract + +The App Connect system contract carries the wallet's encrypted response to an +app login request. It activates at protocol version 14 and has the same ID on +every network: `H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ`. + +Its only document type is `loginKeyResponse`, written by the wallet's identity +after the user approves: + +| Property | Bytes | Meaning | +| --- | --- | --- | +| `appEphemeralPubKeyHash` | 20 | `hash160` of the ephemeral public key in the app's request | +| `walletEphemeralPubKey` | 33 | Compressed ephemeral public key used to derive the shared secret | +| `encryptedPayload` | 60–572 | Encrypted granted private keys: 28-byte envelope plus 32 bytes per key, for 1–17 keys (session key plus up to eight bindings with both purposes) | + +All three properties are required. The contract checks their byte lengths; +clients validate the public key, ciphertext framing and decrypted grant. + +The type is `indexOnly`, immutable and deletable. Its `byRequest` index is a +flat composite terminal `(appEphemeralPubKeyHash, $ownerId)`. The wallet key and +ciphertext are `entryPayload` values, so there is no primary document row or +per-request tree. Each identity may answer a request once; another identity's +answer cannot reserve the wallet's entry. Apps query by request hash and +validate each candidate's ciphertext and granted keys against its `$ownerId`. + +On re-login the wallet deletes its old response **by values** and creates the +new response. It retains the previous request hash, wallet key and ciphertext +for deletion; retaining only a document ID is insufficient for an index-only +type. See [the protocol guide](../../docs/protocol/app-connect.md) for details. + +## Install + +```sh +npm install @dashevo/app-connect-contract +``` + +## License + +[MIT](LICENSE) © Dash Core Group, Inc. diff --git a/packages/app-connect-contract/eslint.config.mjs b/packages/app-connect-contract/eslint.config.mjs new file mode 100644 index 00000000000..cdf50e57d0d --- /dev/null +++ b/packages/app-connect-contract/eslint.config.mjs @@ -0,0 +1,10 @@ +import baseConfig from '../../eslint/base.mjs'; +import mochaTestConfig from '../../eslint/mocha-tests.mjs'; + +export default [ + ...baseConfig, + mochaTestConfig, + { + ignores: ['dist/**', 'node_modules/**'], + }, +]; diff --git a/packages/app-connect-contract/lib/systemIds.js b/packages/app-connect-contract/lib/systemIds.js new file mode 100644 index 00000000000..8fd6cbcd21b --- /dev/null +++ b/packages/app-connect-contract/lib/systemIds.js @@ -0,0 +1,4 @@ +module.exports = { + ownerId: '11111111111111111111111111111111', + contractId: 'H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ', +}; diff --git a/packages/app-connect-contract/package.json b/packages/app-connect-contract/package.json new file mode 100644 index 00000000000..2b4b4be7f81 --- /dev/null +++ b/packages/app-connect-contract/package.json @@ -0,0 +1,27 @@ +{ + "name": "@dashevo/app-connect-contract", + "version": "4.2.0-beta.3", + "description": "A system contract for encrypted wallet-to-app login responses", + "scripts": { + "lint": "eslint .", + "test": "yarn run test:unit", + "test:unit": "mocha 'test/unit/**/*.spec.js'" + }, + "contributors": [ + { + "name": "Pasta", + "email": "pasta@dashboost.org", + "url": "https://github.com/PastaPastaPasta" + } + ], + "license": "MIT", + "devDependencies": { + "@dashevo/wasm-dpp": "workspace:*", + "chai": "^4.3.10", + "dirty-chai": "^2.0.1", + "eslint": "^9.18.0", + "mocha": "^11.1.0", + "sinon": "^18.0.1", + "sinon-chai": "^3.7.0" + } +} diff --git a/packages/app-connect-contract/schema/v1/app-connect-contract-documents.json b/packages/app-connect-contract/schema/v1/app-connect-contract-documents.json new file mode 100644 index 00000000000..2148a46efab --- /dev/null +++ b/packages/app-connect-contract/schema/v1/app-connect-contract-documents.json @@ -0,0 +1,55 @@ +{ + "loginKeyResponse": { + "type": "object", + "documentsMutable": false, + "canBeDeleted": true, + "creationRestrictionMode": 0, + "indices": [ + { + "name": "byRequest", + "terminal": [ + "appEphemeralPubKeyHash", + "$ownerId" + ] + } + ], + "properties": { + "appEphemeralPubKeyHash": { + "type": "array", + "byteArray": true, + "minItems": 20, + "maxItems": 20, + "description": "hash160 of the ephemeral public key the app put in its connect request; identifies the request", + "position": 0 + }, + "walletEphemeralPubKey": { + "type": "array", + "byteArray": true, + "minItems": 33, + "maxItems": 33, + "description": "The wallet's compressed ephemeral public key; the app derives the shared secret from it and its own ephemeral private key", + "position": 1 + }, + "encryptedPayload": { + "type": "array", + "byteArray": true, + "minItems": 60, + "maxItems": 572, + "description": "The granted private keys encrypted to the app: a 28-byte envelope plus 32 bytes per key, one to seventeen keys (the session key plus up to eight bindings with both purposes)", + "position": 2 + } + }, + "required": [ + "appEphemeralPubKeyHash", + "walletEphemeralPubKey", + "encryptedPayload" + ], + "description": "A wallet's response after approving a login request. The flat entry is keyed by the request hash and wallet identity, so other identities cannot block its write. On re-login the wallet deletes its previous response by values and creates a response for the new request.", + "additionalProperties": false, + "indexOnly": true, + "entryPayload": [ + "walletEphemeralPubKey", + "encryptedPayload" + ] + } +} diff --git a/packages/app-connect-contract/src/error.rs b/packages/app-connect-contract/src/error.rs new file mode 100644 index 00000000000..d01bbcc91cf --- /dev/null +++ b/packages/app-connect-contract/src/error.rs @@ -0,0 +1,17 @@ +use platform_version::version::FeatureVersion; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Platform expected some specific versions + #[error("platform unknown version on {method}, received: {received}")] + UnknownVersionMismatch { + /// method + method: String, + /// the allowed versions for this method + known_versions: Vec, + /// requested core height + received: FeatureVersion, + }, + #[error("schema deserialize error: {0}")] + InvalidSchemaJson(#[from] serde_json::Error), +} diff --git a/packages/app-connect-contract/src/lib.rs b/packages/app-connect-contract/src/lib.rs new file mode 100644 index 00000000000..072758ee54a --- /dev/null +++ b/packages/app-connect-contract/src/lib.rs @@ -0,0 +1,63 @@ +mod error; +pub mod v1; + +pub use crate::error::Error; +use platform_value::{Identifier, IdentifierBytes32}; +use platform_version::version::PlatformVersion; +use serde_json::Value; + +pub const ID_BYTES: [u8; 32] = [ + 239, 150, 14, 165, 105, 114, 235, 173, 190, 248, 162, 126, 247, 218, 92, 129, 255, 75, 179, + 138, 2, 150, 151, 69, 126, 36, 218, 66, 183, 155, 84, 183, +]; + +pub const OWNER_ID_BYTES: [u8; 32] = [0; 32]; + +pub const ID: Identifier = Identifier(IdentifierBytes32(ID_BYTES)); +pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES)); +pub fn load_definitions(platform_version: &PlatformVersion) -> Result, Error> { + match platform_version.system_data_contracts.app_connect { + 1 => Ok(None), + version => Err(Error::UnknownVersionMismatch { + method: "app_connect_contract::load_definitions".to_string(), + known_versions: vec![1], + received: version, + }), + } +} +pub fn load_documents_schemas(platform_version: &PlatformVersion) -> Result { + match platform_version.system_data_contracts.app_connect { + 1 => v1::load_documents_schemas(), + version => Err(Error::UnknownVersionMismatch { + method: "app_connect_contract::load_documents_schemas".to_string(), + known_versions: vec![1], + received: version, + }), + } +} + +#[cfg(test)] +mod tests { + use base58::FromBase58; + + use super::*; + + #[test] + /// Ensure that the ID constant matches the expected value + /// and that it can be encoded to base58 correctly. + fn should_match_the_published_system_contract_id() { + assert_eq!( + ID, + Identifier(IdentifierBytes32(ID_BYTES)), + "ID should match the expected value" + ); + + let base58_decoded = "H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ" + .from_base58() + .unwrap(); + assert_eq!( + base58_decoded, ID_BYTES, + "ID should match the base58 decoded value" + ); + } +} diff --git a/packages/app-connect-contract/src/v1/mod.rs b/packages/app-connect-contract/src/v1/mod.rs new file mode 100644 index 00000000000..0d320637f6c --- /dev/null +++ b/packages/app-connect-contract/src/v1/mod.rs @@ -0,0 +1,25 @@ +use crate::Error; +use serde_json::Value; + +pub mod document_types { + pub mod login_key_response { + pub const NAME: &str = "loginKeyResponse"; + + pub mod properties { + pub const APP_EPHEMERAL_PUB_KEY_HASH: &str = "appEphemeralPubKeyHash"; + pub const WALLET_EPHEMERAL_PUB_KEY: &str = "walletEphemeralPubKey"; + pub const ENCRYPTED_PAYLOAD: &str = "encryptedPayload"; + } + + pub mod indexes { + pub const BY_REQUEST: &str = "byRequest"; + } + } +} + +pub fn load_documents_schemas() -> Result { + serde_json::from_str(include_str!( + "../../schema/v1/app-connect-contract-documents.json" + )) + .map_err(Error::InvalidSchemaJson) +} diff --git a/packages/app-connect-contract/test/bootstrap.js b/packages/app-connect-contract/test/bootstrap.js new file mode 100644 index 00000000000..7af04f464d7 --- /dev/null +++ b/packages/app-connect-contract/test/bootstrap.js @@ -0,0 +1,30 @@ +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const { expect, use } = require('chai'); +const dirtyChai = require('dirty-chai'); + +const { + default: loadWasmDpp, +} = require('@dashevo/wasm-dpp'); + +use(dirtyChai); +use(sinonChai); + +exports.mochaHooks = { + beforeAll: loadWasmDpp, + + beforeEach() { + if (!this.sinon) { + this.sinon = sinon.createSandbox(); + } else { + this.sinon.restore(); + } + }, + + afterEach() { + this.sinon.restore(); + }, +}; + +global.expect = expect; diff --git a/packages/app-connect-contract/test/unit/appConnectContract.spec.js b/packages/app-connect-contract/test/unit/appConnectContract.spec.js new file mode 100644 index 00000000000..0d4e197f439 --- /dev/null +++ b/packages/app-connect-contract/test/unit/appConnectContract.spec.js @@ -0,0 +1,199 @@ +const crypto = require('crypto'); + +const { + DashPlatformProtocol, + JsonSchemaError, +} = require('@dashevo/wasm-dpp'); +const generateRandomIdentifier = require('@dashevo/wasm-dpp/lib/test/utils/generateRandomIdentifierAsync'); + +const { expect } = require('chai'); +const appConnectContractDocumentsSchema = require('../../schema/v1/app-connect-contract-documents.json'); + +const expectJsonSchemaError = (validationResult, errorCount = 1) => { + const errors = validationResult.getErrors(); + expect(errors) + .to + .have + .length(errorCount); + + const error = validationResult.getErrors()[0]; + expect(error) + .to + .be + .instanceof(JsonSchemaError); + + return error; +}; + +describe('App Connect Contract', () => { + let dpp; + let dataContract; + let identityId; + + beforeEach(async () => { + dpp = new DashPlatformProtocol( + { generate: () => crypto.randomBytes(32) }, + ); + + identityId = await generateRandomIdentifier(); + + dataContract = dpp.dataContract.create( + identityId, + BigInt(1), + appConnectContractDocumentsSchema, + ); + }); + + it('should have a valid contract definition', async () => { + expect(() => dpp.dataContract.create( + identityId, + BigInt(1), + appConnectContractDocumentsSchema, + )) + .to + .not + .throw(); + }); + + describe('documents', () => { + describe('loginKeyResponse', () => { + let rawLoginKeyResponseDocument; + + beforeEach(() => { + rawLoginKeyResponseDocument = { + appEphemeralPubKeyHash: crypto.randomBytes(20), + walletEphemeralPubKey: crypto.randomBytes(33), + encryptedPayload: crypto.randomBytes(60), + }; + }); + + describe('appEphemeralPubKeyHash', () => { + it('should be defined', async () => { + delete rawLoginKeyResponseDocument.appEphemeralPubKeyHash; + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('required'); + expect(error.params.missingProperty).to.equal('appEphemeralPubKeyHash'); + }); + + it('should be not shorter than 20 bytes', async () => { + rawLoginKeyResponseDocument.appEphemeralPubKeyHash = crypto.randomBytes(19); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('minItems'); + }); + + it('should be not longer than 20 bytes', async () => { + rawLoginKeyResponseDocument.appEphemeralPubKeyHash = crypto.randomBytes(21); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('maxItems'); + }); + }); + + describe('walletEphemeralPubKey', () => { + it('should be defined', async () => { + delete rawLoginKeyResponseDocument.walletEphemeralPubKey; + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('required'); + expect(error.params.missingProperty).to.equal('walletEphemeralPubKey'); + }); + + it('should be not shorter than 33 bytes', async () => { + rawLoginKeyResponseDocument.walletEphemeralPubKey = crypto.randomBytes(32); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('minItems'); + }); + + it('should be not longer than 33 bytes', async () => { + rawLoginKeyResponseDocument.walletEphemeralPubKey = crypto.randomBytes(34); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('maxItems'); + }); + }); + + describe('encryptedPayload', () => { + it('should be defined', async () => { + delete rawLoginKeyResponseDocument.encryptedPayload; + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('required'); + expect(error.params.missingProperty).to.equal('encryptedPayload'); + }); + + it('should be not shorter than 60 bytes', async () => { + rawLoginKeyResponseDocument.encryptedPayload = crypto.randomBytes(59); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('minItems'); + }); + + it('should be not longer than 572 bytes', async () => { + rawLoginKeyResponseDocument.encryptedPayload = crypto.randomBytes(573); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('maxItems'); + }); + + it('should accept the maximum grant of seventeen keys', async () => { + // Session key plus eight bindings asking for both + // purposes: 28-byte envelope + 17 * 32 = 572 bytes. + rawLoginKeyResponseDocument.encryptedPayload = crypto.randomBytes(28 + (17 * 32)); + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + + expect(validationResult.isValid()).to.be.true(); + }); + }); + + it('should not have additional properties', async () => { + rawLoginKeyResponseDocument.someOtherProperty = 42; + + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword).to.equal('additionalProperties'); + expect(error.params.additionalProperties).to.deep.equal(['someOtherProperty']); + }); + + it('should be valid', async () => { + const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument); + const validationResult = document.validate(dpp.protocolVersion); + + expect(validationResult.isValid()).to.be.true(); + }); + }); + }); +}); diff --git a/packages/data-contracts/Cargo.toml b/packages/data-contracts/Cargo.toml index da907fd418c..0f331adf20a 100644 --- a/packages/data-contracts/Cargo.toml +++ b/packages/data-contracts/Cargo.toml @@ -18,6 +18,7 @@ all-contracts = [ "token-history", "keyword-search", "document-history", + "app-connect", ] # Individual contract features @@ -29,6 +30,7 @@ wallet-utils = ["dep:wallet-utils-contract"] token-history = ["dep:token-history-contract"] keyword-search = ["dep:keyword-search-contract"] document-history = ["dep:document-history-contract"] +app-connect = ["dep:app-connect-contract"] [dependencies] thiserror = "2.0.12" @@ -43,3 +45,4 @@ wallet-utils-contract = { path = "../wallet-utils-contract", optional = true } token-history-contract = { path = "../token-history-contract", optional = true } keyword-search-contract = { path = "../keyword-search-contract", optional = true } document-history-contract = { path = "../document-history-contract", optional = true } +app-connect-contract = { path = "../app-connect-contract", optional = true } diff --git a/packages/data-contracts/src/error.rs b/packages/data-contracts/src/error.rs index f5a89053bca..ce469e36239 100644 --- a/packages/data-contracts/src/error.rs +++ b/packages/data-contracts/src/error.rs @@ -165,3 +165,21 @@ impl From for Error { } } } + +#[cfg(feature = "app-connect")] +impl From for Error { + fn from(e: app_connect_contract::Error) -> Self { + match e { + app_connect_contract::Error::UnknownVersionMismatch { + method, + known_versions, + received, + } => Error::UnknownVersionMismatch { + method, + known_versions, + received, + }, + app_connect_contract::Error::InvalidSchemaJson(e) => Error::InvalidSchemaJson(e), + } + } +} diff --git a/packages/data-contracts/src/lib.rs b/packages/data-contracts/src/lib.rs index 0172c6205ab..26c824d014c 100644 --- a/packages/data-contracts/src/lib.rs +++ b/packages/data-contracts/src/lib.rs @@ -4,6 +4,9 @@ use serde_json::Value; use crate::error::Error; +#[cfg(feature = "app-connect")] +pub use app_connect_contract; + #[cfg(feature = "dashpay")] pub use dashpay_contract; @@ -46,6 +49,7 @@ pub enum SystemDataContract { TokenHistory = 6, KeywordSearch = 7, DocumentHistory = 8, + AppConnect = 9, } pub struct DataContractSource { @@ -62,7 +66,7 @@ impl SystemDataContract { /// Deliberately kept beside the enum so that adding a variant and adding it here are the /// same edit. `assert_every_variant_is_listed` below makes that mechanical rather than /// remembered: a new variant makes its match non-exhaustive and the crate stops compiling. - pub const ALL: [SystemDataContract; 9] = [ + pub const ALL: [SystemDataContract; 10] = [ SystemDataContract::Withdrawals, SystemDataContract::MasternodeRewards, SystemDataContract::FeatureFlags, @@ -72,6 +76,7 @@ impl SystemDataContract { SystemDataContract::TokenHistory, SystemDataContract::KeywordSearch, SystemDataContract::DocumentHistory, + SystemDataContract::AppConnect, ]; /// A new variant must also be added to [`SystemDataContract::ALL`]; this match is where the @@ -149,6 +154,14 @@ impl SystemDataContract { 88, 18, 140, 208, 179, 231, 242, 57, 225, 203, 4, 210, 245, 95, 136, 92, 160, 167, 112, 118, 173, 238, 83, 62, 234, 230, 222, 16, 231, 30, 99, 98, ], + + #[cfg(feature = "app-connect")] + SystemDataContract::AppConnect => app_connect_contract::ID_BYTES, + #[cfg(not(feature = "app-connect"))] + SystemDataContract::AppConnect => [ + 239, 150, 14, 165, 105, 114, 235, 173, 190, 248, 162, 126, 247, 218, 92, 129, 255, + 75, 179, 138, 2, 150, 151, 69, 126, 36, 218, 66, 183, 155, 84, 183, + ], }; Identifier::new(bytes) } @@ -258,6 +271,17 @@ impl SystemDataContract { SystemDataContract::DocumentHistory => { Err(Error::ContractNotIncluded("document-history")) } + + #[cfg(feature = "app-connect")] + SystemDataContract::AppConnect => Ok(DataContractSource { + id_bytes: app_connect_contract::ID_BYTES, + owner_id_bytes: app_connect_contract::OWNER_ID_BYTES, + version: platform_version.system_data_contracts.app_connect as u32, + definitions: app_connect_contract::load_definitions(platform_version)?, + document_schemas: app_connect_contract::load_documents_schemas(platform_version)?, + }), + #[cfg(not(feature = "app-connect"))] + SystemDataContract::AppConnect => Err(Error::ContractNotIncluded("app-connect")), } } } diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index bf9bba16c3c..d98e26f03b6 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -268,6 +268,7 @@ all-system_contracts = [ "token-history-contract", "keywords-contract", "document-history-contract", + "app-connect-contract", ] # Individual data contract features @@ -282,6 +283,7 @@ wallet-utils-contract = ["data-contracts", "data-contracts/wallet-utils"] token-history-contract = ["data-contracts", "data-contracts/token-history"] keywords-contract = ["data-contracts", "data-contracts/keyword-search"] document-history-contract = ["data-contracts", "data-contracts/document-history"] +app-connect-contract = ["data-contracts", "data-contracts/app-connect"] fixtures-and-mocks = ["all-system_contracts", "platform-value/json"] random-public-keys = ["bls-signatures", "ed25519-dalek"] random-identities = ["random-public-keys"] diff --git a/packages/rs-dpp/src/system_data_contracts.rs b/packages/rs-dpp/src/system_data_contracts.rs index e5504da4d21..66b865d0012 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -69,6 +69,11 @@ impl ConfigurationForSystemContract for SystemDataContract { config.set_sized_integer_types_enabled(true); Ok(config) } + SystemDataContract::AppConnect => { + let mut config = DataContractConfig::default_for_version(platform_version)?; + config.set_sized_integer_types_enabled(true); + Ok(config) + } } } } @@ -202,3 +207,92 @@ mod tests { assert_eq!(v9_bytes.len(), 1758); // this will use a config v1 in serialization with sized_integer_types } } + +#[cfg(all(test, feature = "app-connect-contract", feature = "validation"))] +mod app_connect_tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::random_document::CreateRandomDocument; + use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0; + use crate::document::{Document, DocumentV0Getters, DocumentV0Setters}; + use platform_value::Value; + + fn response(contract: &DataContract) -> Document { + let mut document = contract + .document_type_for_name("loginKeyResponse") + .expect("response type") + .random_document(Some(42), PlatformVersion::latest()) + .expect("response document"); + document.set_properties(BTreeMap::from([ + ( + "appEphemeralPubKeyHash".into(), + Value::Bytes(vec![0x11; 20]), + ), + ("walletEphemeralPubKey".into(), Value::Bytes(vec![0x22; 33])), + ("encryptedPayload".into(), Value::Bytes(vec![0x33; 60])), + ])); + document + } + + #[test] + fn should_validate_app_connect_response_lengths() { + let platform_version = PlatformVersion::latest(); + let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version) + .expect("system contract"); + for (property, length, expected) in [ + ("appEphemeralPubKeyHash", 19, false), + ("appEphemeralPubKeyHash", 20, true), + ("appEphemeralPubKeyHash", 21, false), + ("walletEphemeralPubKey", 32, false), + ("walletEphemeralPubKey", 33, true), + ("walletEphemeralPubKey", 34, false), + ("encryptedPayload", 59, false), + ("encryptedPayload", 60, true), + ("encryptedPayload", 572, true), + ("encryptedPayload", 573, false), + ] { + let mut document = response(&contract); + document.set(property, Value::Bytes(vec![0x44; length])); + let result = contract + .validate_document("loginKeyResponse", &document, platform_version) + .expect("validation executes"); + assert_eq!( + result.is_valid(), + expected, + "{property}: {length} bytes: {result:?}" + ); + } + } + + #[test] + fn should_require_only_the_three_app_connect_response_properties() { + let platform_version = PlatformVersion::latest(); + let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version) + .expect("system contract"); + assert_eq!(contract.document_types().len(), 1); + for property in [ + "appEphemeralPubKeyHash", + "walletEphemeralPubKey", + "encryptedPayload", + ] { + let mut document = response(&contract); + document.properties_mut().remove(property); + assert!( + !contract + .validate_document("loginKeyResponse", &document, platform_version) + .expect("validation executes") + .is_valid(), + "{property} is required" + ); + } + let mut document = response(&contract); + document.set("contractId", Value::Bytes(vec![0x55; 32])); + assert!( + !contract + .validate_document("loginKeyResponse", &document, platform_version) + .expect("validation executes") + .is_valid(), + "the former contractId field is not admitted" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs index 7afeeeaf7c9..331d5550eee 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs @@ -11,6 +11,7 @@ mod common; mod test; pub mod v0; pub mod v1; +pub mod v2; impl Platform { /// Creates trees and populates them with necessary identities, contracts and documents @@ -41,9 +42,16 @@ impl Platform { transaction, platform_version, ), + // V2 (protocol version 14 and later) also registers the app-connect contract + 2 => self.create_genesis_state_v2( + genesis_core_height, + genesis_time, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "create_genesis_state".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), }?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs new file mode 100644 index 00000000000..27f76ea39bc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs @@ -0,0 +1,173 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; + +use drive::dpp::identity::TimestampMillis; + +use dpp::block::block_info::BlockInfo; +use dpp::prelude::CoreBlockHeight; +use dpp::system_data_contracts::load_system_data_contract; +use dpp::version::PlatformVersion; +use drive::dpp::system_data_contracts::SystemDataContract; +use drive::query::TransactionArg; +use std::collections::BTreeMap; + +impl Platform { + /// Creates trees and populates them with necessary identities, contracts and documents. + /// + /// v2 is v1 (protocol versions 6 to 13) plus the app-connect contract, which activates with + /// protocol version 14. Both the document history contract (v1 registered it from protocol + /// version 13 behind a version check) and the app-connect contract are unconditional here: + /// this generation is only ever selected by the tables of protocol version 14 and later. + #[inline(always)] + pub(super) fn create_genesis_state_v2( + &self, + genesis_core_height: CoreBlockHeight, + genesis_time: TimestampMillis, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + //versioned call + self.drive + .create_initial_state_structure(transaction, platform_version)?; + + self.drive + .store_genesis_core_height(genesis_core_height, transaction, platform_version)?; + + let mut operations = vec![]; + + // Create system identities and contracts + + let system_data_contracts = &self.drive.cache.system_data_contracts; + + let system_data_contract_types = BTreeMap::from_iter([ + ( + SystemDataContract::DPNS, + system_data_contracts.load_dpns(platform_version)?, + ), + ( + SystemDataContract::Withdrawals, + system_data_contracts.load_withdrawals(platform_version)?, + ), + ( + SystemDataContract::Dashpay, + system_data_contracts.load_dashpay(platform_version)?, + ), + ( + SystemDataContract::MasternodeRewards, + system_data_contracts.load_masternode_reward_shares(platform_version)?, + ), + ( + SystemDataContract::TokenHistory, + system_data_contracts.load_token_history(platform_version)?, + ), + ( + SystemDataContract::KeywordSearch, + system_data_contracts.load_keyword_search(platform_version)?, + ), + ( + SystemDataContract::DocumentHistory, + system_data_contracts.load_document_history(platform_version)?, + ), + ( + SystemDataContract::AppConnect, + system_data_contracts.load_app_connect(platform_version)?, + ), + ]); + + for data_contract in system_data_contract_types.values() { + self.register_system_data_contract_operations( + data_contract, + &mut operations, + platform_version, + )?; + } + + let wallet_utils_contract = + load_system_data_contract(SystemDataContract::WalletUtils, platform_version)?; + + self.register_system_data_contract_operations( + &wallet_utils_contract, + &mut operations, + platform_version, + )?; + + let dpns_contract = system_data_contracts.load_dpns(platform_version)?; + + self.register_dpns_top_level_domain_operations( + &dpns_contract, + genesis_time, + &mut operations, + )?; + + let block_info = BlockInfo::default_with_time(genesis_time); + + self.drive.apply_drive_operations( + operations, + true, + &block_info, + transaction, + platform_version, + None, // No previous_fee_versions needed for genesis state creation + )?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + mod create_genesis_state { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contracts::SystemDataContract; + use platform_version::version::PlatformVersion; + + /// The app-connect contract is part of the genesis state from protocol version 14 + /// on, where the tables select this generation, and absent from the genesis state of + /// every earlier version, which still runs v1 and which a replaying node must + /// reproduce byte for byte. Both sides go through the `create_genesis_state` + /// dispatcher. + #[test] + pub fn should_register_the_app_connect_contract_only_from_protocol_version_14() { + let app_connect_id = SystemDataContract::AppConnect.id(); + + for (platform_version, expected) in [ + (PlatformVersion::get(13).expect("protocol 13"), false), + (PlatformVersion::latest(), true), + ] { + let initial_protocol_version = platform_version.protocol_version; + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(initial_protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let stored = platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + None, + platform_version, + ) + .value + .expect("expected to query the app-connect contract"); + + assert_eq!( + stored.is_some(), + expected, + "app-connect contract presence in a genesis state born at protocol version {initial_protocol_version}" + ); + + if let Some(stored) = stored { + assert_eq!(stored.contract.id(), app_connect_id); + assert!(stored + .contract + .document_type_for_name("loginKeyResponse") + .is_ok()); + assert_eq!(stored.contract.document_types().len(), 1); + } + } + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs index 20fa1e72758..7bdecf92795 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs @@ -88,6 +88,7 @@ mod tests { use crate::test::helpers::setup::TestPlatformBuilder; use dpp::block::block_info::BlockInfo; use dpp::block::epoch::Epoch; + use dpp::data_contracts::SystemDataContract; #[test] fn test_perform_events_when_version_method_is_none() { @@ -171,4 +172,117 @@ mod tests { _ => panic!("expected UnknownVersionMismatch error"), } } + + #[test] + fn should_rollback_and_retry_app_connect_registration_through_the_upgrade_dispatcher() { + let previous_version = PlatformVersion::get(13).expect("protocol 13"); + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(13) + .build_with_mock_rpc() + .set_genesis_state(); + let platform_state = platform.state.load(); + let block_info = BlockInfo { + time_ms: 1_000_000, + height: 100, + core_height: 100, + epoch: Epoch::new(1).expect("epoch"), + }; + let id = SystemDataContract::AppConnect.id(); + let original_root = platform + .drive + .grove + .root_hash(None, &previous_version.drive.grove_version) + .unwrap() + .expect("original root"); + let mut first_candidate_root = None; + + for commit in [false, true] { + // The block proposal path clears this cache before retrying a candidate. + platform.drive.cache.data_contracts.clear_block_cache(); + let transaction = platform.drive.grove.start_transaction(); + platform + .perform_events_on_first_block_of_protocol_change( + &platform_state, + &block_info, + &transaction, + 13, + platform_version, + ) + .expect("protocol upgrade through the dispatcher"); + assert_eq!( + platform + .drive + .fetch_contract_version(id.to_buffer(), Some(&transaction), platform_version) + .expect("registered version"), + Some(1) + ); + let candidate_root = platform + .drive + .grove + .root_hash(Some(&transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("candidate root"); + assert_ne!(candidate_root, original_root); + // Compiling the new schema must not expose it to old-version cache readers. + assert!(platform + .drive + .cache + .system_data_contracts + .find_by_id(id, platform_version) + .expect("new cache lookup") + .is_some()); + assert!(platform + .drive + .cache + .system_data_contracts + .find_by_id(id, previous_version) + .expect("old cache lookup") + .is_none()); + if commit { + assert_eq!( + Some(candidate_root), + first_candidate_root, + "retry after rollback must produce the same state" + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit retry"); + platform + .drive + .cache + .data_contracts + .merge_and_clear_block_cache(); + } else { + first_candidate_root = Some(candidate_root); + drop(transaction); + platform.drive.cache.data_contracts.clear_block_cache(); + assert_eq!( + platform + .drive + .grove + .root_hash(None, &previous_version.drive.grove_version) + .unwrap() + .expect("committed root after rollback"), + original_root + ); + assert!(platform + .drive + .fetch_contract(id.to_buffer(), None, None, None, previous_version) + .value + .expect("old committed state") + .is_none()); + } + } + assert_eq!( + platform + .drive + .fetch_contract_version(id.to_buffer(), None, platform_version) + .expect("committed contract version"), + Some(1) + ); + } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index d95899c4124..5820f88c0c3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -695,8 +695,10 @@ impl Platform { /// When transitioning to version 14 we re-store the DashPay contract whose /// v2 schema adds the optional public payment address fields to the - /// `profile` document type (DIP-33), and the withdrawals contract whose v2 - /// schema admits the terminal FAILED value of the `status` property. + /// `profile` document type (DIP-33), the withdrawals contract whose v2 + /// schema admits the terminal FAILED value of the `status` property, and + /// register the app-connect contract that carries the wallet-to-app login + /// handshake. fn transition_to_version_14( &self, block_info: &BlockInfo, @@ -730,6 +732,20 @@ impl Platform { platform_version, )?; + // App-connect contract: the wallet's encrypted login response gets one system + // contract id on every network from this version. Fresh + // chains register it at genesis (`create_genesis_state` v2). + let app_connect_contract = + load_system_data_contract(SystemDataContract::AppConnect, platform_version)?; + + self.drive.insert_contract( + &app_connect_contract, + *block_info, + true, + Some(transaction), + platform_version, + )?; + // Total credits history under the withdrawals tree: the daily withdrawal limit becomes // a share of the total credits Platform held a day ago, recorded here every block. self.drive.grove_insert_if_not_exists( @@ -1219,6 +1235,102 @@ mod tests { assert!(profile.iter().any(|p| p == "shieldedAddress")); } + #[test] + fn should_insert_app_connect_on_transition_to_version_14() { + use dpp::data_contract::accessors::v0::DataContractV0Getters; + + // A chain born at protocol version 13 has no app-connect contract: it + // is neither in that genesis state nor active for the system contract + // cache. + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(13) + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_version_13 = PlatformVersion::get(13).expect("expected platform version 13"); + let platform_version = PlatformVersion::latest(); + let app_connect_id = SystemDataContract::AppConnect.id(); + + let transaction = platform.drive.grove.start_transaction(); + + assert!( + platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + Some(&transaction), + platform_version_13, + ) + .value + .expect("expected to query the app-connect contract") + .is_none(), + "the app-connect contract must not exist before transition_to_version_14" + ); + assert!(platform + .drive + .cache + .system_data_contracts + .find_by_id(app_connect_id, platform_version_13) + .expect("expected the pre-activation lookup to succeed") + .is_none()); + + let block_info = BlockInfo { + time_ms: 1_000_000, + height: 100, + core_height: 100, + epoch: Epoch::new(1).expect("expected epoch"), + }; + + platform + .transition_to_version_14(&block_info, &transaction, platform_version) + .expect("expected the transition to succeed"); + + let stored = platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + Some(&transaction), + platform_version, + ) + .value + .expect("expected to fetch the app-connect contract") + .expect("the app-connect contract must exist after transition_to_version_14"); + + assert_eq!(stored.contract.id(), app_connect_id); + assert_eq!(stored.contract.owner_id(), Identifier::from([0u8; 32])); + assert!(stored + .contract + .document_type_for_name("loginKeyResponse") + .is_ok()); + assert_eq!(stored.contract.document_types().len(), 1); + + // Stored beside its version item, like every contract from this version on. + assert_eq!( + platform + .drive + .fetch_contract_version( + app_connect_id.to_buffer(), + Some(&transaction), + platform_version + ) + .expect("expected to read the version item"), + Some(stored.contract.version()), + "the app-connect contract has its version item after the transition" + ); + + assert!(platform + .drive + .cache + .system_data_contracts + .find_by_id(app_connect_id, platform_version) + .expect("expected the post-activation lookup to succeed") + .is_some()); + } + /// Reads the `status` enum of the stored withdrawals contract's `withdrawal` document /// type, the contract-level record of which statuses a withdrawal may carry. fn stored_withdrawal_status_enum( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs index 2ebdedb7734..1ba8d5de0ed 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs @@ -15,7 +15,9 @@ mod deletion_tests { // Protocol version 14 adds +740 per document write (the contract's version // item is one more node to rehash) and the larger DashPay v2 schema // increases byte-billed contract-tree reads. - 1721520, + // The app-connect contract adds one sibling to the genesis contracts tree, + // increasing the bytes billed when reading that tree (protocol 14 only). + 1770160, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs index a72055cfeda..94bdf6fe6b2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs @@ -33,7 +33,9 @@ mod dpns_tests { // inserts touch a cheaper set of nodes (v13 moves by the same amount // when the seed reorders its ids). +10_000 per create: the nonce // derived id is billed both SHA-256 passes, 4 blocks instead of 2. - 5_895_280, + // The app-connect contract adds one sibling to the genesis contracts tree, + // increasing the bytes billed when reading that tree (protocol 14 only). + 6_049_360, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/index_only.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/index_only.rs index 50d6d71ff29..5410b384280 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/index_only.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/index_only.rs @@ -1437,6 +1437,7 @@ mod index_only_executed_proof_tests { use dpp::prelude::DataContract; use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::StateTransition; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use drive::drive::Drive; use simple_signer::signer::SimpleSigner; use std::sync::Arc; @@ -2656,6 +2657,15 @@ mod index_only_executed_proof_tests { /// recomputed commitment, and the executed delete proves it absent. #[tokio::test] async fn test_executed_flat_composite_create_and_delete_proofs() { + assert_flat_composite_create_and_delete_proofs(false).await; + } + + #[tokio::test] + async fn should_create_and_delete_app_connect_response_with_proofs() { + assert_flat_composite_create_and_delete_proofs(true).await; + } + + async fn assert_flat_composite_create_and_delete_proofs(system_contract: bool) { use dpp::data_contract::accessors::v0::DataContractV0Setters; let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() @@ -2666,21 +2676,28 @@ mod index_only_executed_proof_tests { let (alice, alice_signer, alice_key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - let mut contract = - json_document_to_contract(SCALAR_TERMINAL_CONTRACT, true, platform_version) - .expect("expected to parse the scalar-terminal contract"); - contract.set_owner_id(alice.id()); - platform - .drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply the scalar-terminal contract"); + let contract = if system_contract { + // Genesis registers the real system schema; ordinary identities write to it. + load_system_data_contract(SystemDataContract::AppConnect, platform_version) + .expect("expected the app-connect system contract") + } else { + let mut contract = + json_document_to_contract(SCALAR_TERMINAL_CONTRACT, true, platform_version) + .expect("expected to parse the scalar-terminal contract"); + contract.set_owner_id(alice.id()); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply the scalar-terminal contract"); + contract + }; let response_type = contract .document_type_for_name("loginKeyResponse") .expect("loginKeyResponse doctype exists"); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 920da412f13..0bbf619b864 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -517,7 +517,9 @@ mod replacement_tests { // Protocol version 14 adds +740 per document write (the contract's version // item is one more node to rehash) and the larger DashPay v2 schema // increases byte-billed contract-tree reads. - 1450220, + // The app-connect contract adds one sibling to the genesis contracts tree, + // increasing the bytes billed when reading that tree (protocol 14 only). + 1498860, ) .await; } diff --git a/packages/rs-drive/src/cache/system_contracts.rs b/packages/rs-drive/src/cache/system_contracts.rs index 2d2655be964..8c562a5bce3 100644 --- a/packages/rs-drive/src/cache/system_contracts.rs +++ b/packages/rs-drive/src/cache/system_contracts.rs @@ -3,6 +3,7 @@ use arc_swap::ArcSwap; use dpp::data_contract::DataContract; use dpp::prelude::Identifier; use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use platform_version::version::feature_initial_protocol_versions::APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION; use platform_version::version::{PlatformVersion, ProtocolVersion}; use std::collections::BTreeMap; use std::sync::Arc; @@ -159,6 +160,14 @@ impl SystemDataContracts { self.load(SystemDataContract::DocumentHistory, platform_version) } + /// Returns the app-connect contract materialized for `platform_version`. + pub fn load_app_connect( + &self, + platform_version: &PlatformVersion, + ) -> Result, Error> { + self.load(SystemDataContract::AppConnect, platform_version) + } + /// Returns the system contract whose deterministic identifier matches `id`, materialized /// for `platform_version`. /// @@ -197,6 +206,8 @@ impl SystemDataContracts { SystemDataContract::TokenHistory | SystemDataContract::KeywordSearch => 9, // Written to state by the transition to protocol version 13. SystemDataContract::DocumentHistory => 13, + // Written to state by the transition to protocol version 14. + SystemDataContract::AppConnect => APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION, // Never served from this cache: `WalletUtils` is only ever read from grovedb, and // the reserved `FeatureFlags` slot has no implementation. SystemDataContract::WalletUtils | SystemDataContract::FeatureFlags => return Ok(None), @@ -399,6 +410,27 @@ mod tests { .is_some()); } + #[test] + fn should_serve_app_connect_only_from_its_activation_version() { + let contracts = SystemDataContracts::new(); + + assert!(contracts + .find_by_id(SystemDataContract::AppConnect.id(), platform_version(13)) + .expect("expected the pre-activation lookup to succeed") + .is_none()); + assert!(contracts + .find_by_id( + SystemDataContract::AppConnect.id(), + PlatformVersion::latest() + ) + .expect("expected the v14 lookup to succeed") + .is_some()); + assert!(contracts + .find_by_id(SystemDataContract::AppConnect.id(), platform_version(13)) + .expect("an old-version lookup after materialization must succeed") + .is_none()); + } + fn memoized_protocol_versions(contracts: &SystemDataContracts) -> Vec { let materialized = contracts.materialized.load(); let mut protocol_versions: Vec = materialized diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/index_only_scalar_terminal_e2e_tests.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/index_only_scalar_terminal_e2e_tests.rs index 8fff41cb91f..7745a166684 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/index_only_scalar_terminal_e2e_tests.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/index_only_scalar_terminal_e2e_tests.rs @@ -54,6 +54,7 @@ use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; use dpp::fee::fee_result::FeeResult; use dpp::platform_value::{Identifier, Value}; use dpp::prelude::DataContract; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use dpp::tests::json_document::json_document_to_contract; use grovedb::Element; use std::collections::BTreeMap; @@ -1781,3 +1782,133 @@ fn composite_tail_in_clauses_on_last_and_leading_components() { assert_grovedb_is_consistent(&drive); } + +/// Exercise the shipped system schema, not just the generic feature fixture. +#[test] +fn should_query_and_relogin_with_the_app_connect_system_contract() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = platform_version(); + let contract = load_system_data_contract(SystemDataContract::AppConnect, pv) + .expect("app-connect system contract"); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("apply system contract"); + + let original = build_login_response( + &contract, + REQUEST_HASH, + WALLET_KEY_1, + cipher(0xC1, 60), + OWNER_1, + 1, + ); + let other_owner = build_login_response( + &contract, + REQUEST_HASH, + WALLET_KEY_2, + cipher(0xC2, 572), + OWNER_2, + 2, + ); + for document in [&other_owner, &original] { + insert(&drive, &contract, "loginKeyResponse", document, true) + .expect("an earlier response by another identity cannot squat the request"); + } + let duplicate = build_login_response( + &contract, + REQUEST_HASH, + WALLET_KEY_3, + cipher(0xC3, 92), + OWNER_1, + 3, + ); + assert!( + insert(&drive, &contract, "loginKeyResponse", &duplicate, true).is_err(), + "each owner may answer a request only once" + ); + + let by_request = query( + &contract, + "loginKeyResponse", + vec![equal( + "appEphemeralPubKeyHash", + Value::Bytes(REQUEST_HASH.to_vec()), + )], + Some(10), + ); + let (proof, _) = by_request + .clone() + .execute_with_proof(&drive, None, None, pv) + .expect("request proof"); + let (_, documents) = by_request + .clone() + .verify_proof(&proof, pv) + .expect("verify request proof"); + assert_eq!(documents.len(), 2); + for document in documents { + let expected = if document.owner_id().to_buffer() == OWNER_1 { + &original + } else { + &other_owner + }; + assert_eq!(document.properties(), expected.properties()); + } + + assert!( + delete(&drive, &contract, "loginKeyResponse", duplicate, true).is_err(), + "deletion must supply the original ciphertext and wallet key" + ); + delete(&drive, &contract, "loginKeyResponse", original, true).expect("delete old response"); + let renewed = build_login_response( + &contract, + OTHER_REQUEST_HASH, + WALLET_KEY_3, + cipher(0xC3, 572), + OWNER_1, + 4, + ); + insert(&drive, &contract, "loginKeyResponse", &renewed, true).expect("publish re-login"); + + assert!(login_entry(&drive, &contract, REQUEST_HASH, OWNER_1).is_none()); + assert!(login_entry(&drive, &contract, REQUEST_HASH, OWNER_2).is_some()); + assert!(login_entry(&drive, &contract, OTHER_REQUEST_HASH, OWNER_1).is_some()); + let (proof, _) = by_request + .clone() + .execute_with_proof(&drive, None, None, pv) + .expect("old request proof"); + let (_, documents) = by_request + .verify_proof(&proof, pv) + .expect("verify old request"); + assert_eq!(documents.len(), 1); + assert_eq!(documents[0].owner_id().to_buffer(), OWNER_2); + + let new_request = query( + &contract, + "loginKeyResponse", + vec![ + equal( + "appEphemeralPubKeyHash", + Value::Bytes(OTHER_REQUEST_HASH.to_vec()), + ), + equal("$ownerId", Value::Identifier(OWNER_1)), + ], + Some(1), + ); + let (proof, _) = new_request + .clone() + .execute_with_proof(&drive, None, None, pv) + .expect("new request proof"); + let (_, documents) = new_request + .verify_proof(&proof, pv) + .expect("verify new request"); + assert_eq!(documents.len(), 1); + assert_eq!(documents[0].properties(), renewed.properties()); + assert_grovedb_is_consistent(&drive); +} diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index bc98d8b3a97..0913b20823a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -30,7 +30,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet }, initialization: DriveAbciInitializationMethodVersions { initial_core_height_and_time: 0, - create_genesis_state: 1, + create_genesis_state: 2, // registers the app-connect contract at genesis }, core_based_updates: DriveAbciCoreBasedUpdatesMethodVersions { update_core_info: 0, diff --git a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs index 0a8ade18b58..c283d55151e 100644 --- a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs +++ b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs @@ -17,3 +17,8 @@ pub const CONTRACT_USER_MODERATION_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1 /// The protocol version that introduces document action fees and the `ContractFeeClaim` state /// transition, which pays out the fee pots they collect in. pub const CONTRACT_FEE_CLAIM_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; + +/// The app-connect system contract is written to state by the upgrade to protocol +/// version 14 and registered at genesis from that version on; below it the contract does +/// not exist and lookups must report it absent. +pub const APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/mod.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/mod.rs index 1f01823d67a..62b8a6ea477 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/mod.rs @@ -14,4 +14,5 @@ pub struct SystemDataContractVersions { pub token_history: FeatureVersion, pub keyword_search: FeatureVersion, pub document_history: FeatureVersion, + pub app_connect: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs index 5e814cc4b78..5759ca91273 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs @@ -10,4 +10,5 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V1: SystemDataContractVersions = token_history: 1, keyword_search: 1, document_history: 1, + app_connect: 1, }; diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs index 9b87b90e94b..3c7877fe426 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs @@ -15,4 +15,5 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V2: SystemDataContractVersions = token_history: 1, keyword_search: 1, document_history: 1, + app_connect: 1, }; diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs index 3261a215403..4bd6c11c210 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs @@ -10,6 +10,12 @@ use crate::version::system_data_contract_versions::SystemDataContractVersions; // `distributionType`, written for once-per-identity distribution claims. // v2 (dashpay: 1, withdrawals: 1, token_history: 1) remains for // PROTOCOL_VERSION_13 chain replay. +// +// The app-connect contract (app_connect: 1) also activates with +// PROTOCOL_VERSION_14: it is registered at genesis from that version on and +// inserted by `transition_to_version_14` on chains upgrading from 13. Its +// feature version is listed in the earlier tables only because the struct has +// no optional fields; before 14 the contract is never loaded or served. pub const SYSTEM_DATA_CONTRACT_VERSIONS_V3: SystemDataContractVersions = SystemDataContractVersions { withdrawals: 2, @@ -20,4 +26,5 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V3: SystemDataContractVersions = token_history: 2, keyword_search: 1, document_history: 1, + app_connect: 1, }; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 345d23f805c..761d932b055 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -456,6 +456,13 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// whose epoch's multiplier rose beyond the tolerance (40134), so a /// contract whose fees change cannot make a signed transition pay them. /// +/// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) +/// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by +/// the app's ephemeral key hash and the responding identity, with the wallet's +/// ephemeral key and encrypted grant in `entryPayload`. Genesis generation 2 +/// registers it on fresh chains; `transition_to_version_14` inserts it on upgrade. +/// The Drive and trusted SDK caches serve it only from protocol version 14. +/// /// * `ShieldFromIdentity` (state transition type 21) activates: /// `SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION = 14` gates it in /// `is_allowed`, and `DRIVE_ABCI_VALIDATION_VERSIONS_V10` is the first diff --git a/packages/rs-sdk-ffi/Cargo.toml b/packages/rs-sdk-ffi/Cargo.toml index a25212abc97..a95d87e5d5f 100644 --- a/packages/rs-sdk-ffi/Cargo.toml +++ b/packages/rs-sdk-ffi/Cargo.toml @@ -24,6 +24,7 @@ rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider", "token-history-contract", "keywords-contract", "document-history-contract", + "app-connect-contract", ] } simple-signer = { path = "../simple-signer" } async-trait = { version = "0.1.83" } diff --git a/packages/rs-sdk-trusted-context-provider/Cargo.toml b/packages/rs-sdk-trusted-context-provider/Cargo.toml index 365e8e6e167..69d488e11b9 100644 --- a/packages/rs-sdk-trusted-context-provider/Cargo.toml +++ b/packages/rs-sdk-trusted-context-provider/Cargo.toml @@ -42,6 +42,7 @@ all-system-contracts = [ "token-history-contract", "keywords-contract", "document-history-contract", + "app-connect-contract", ] # Individual contract features - these enable specific contracts in DPP @@ -52,6 +53,7 @@ wallet-utils-contract = ["dpp/wallet-utils-contract"] token-history-contract = ["dpp/token-history-contract"] keywords-contract = ["dpp/keywords-contract"] document-history-contract = ["dpp/document-history-contract"] +app-connect-contract = ["dpp/app-connect-contract"] [target.'cfg(not(target_os = "android"))'.dependencies] reqwest = { version = "0.12", features = ["json"] } diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index e88bcd650b8..eb520875fe4 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -18,9 +18,12 @@ use dpp::data_contract::TokenConfiguration; feature = "token-history-contract", feature = "keywords-contract", feature = "document-history-contract", + feature = "app-connect-contract", feature = "all-system-contracts" ))] use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +#[cfg(any(feature = "app-connect-contract", feature = "all-system-contracts"))] +use dpp::version::feature_initial_protocol_versions::APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION; use dpp::version::PlatformVersion; use lru::LruCache; @@ -781,6 +784,7 @@ impl ContextProvider for TrustedHttpContextProvider { feature = "token-history-contract", feature = "keywords-contract", feature = "document-history-contract", + feature = "app-connect-contract", feature = "all-system-contracts" ))] { @@ -886,6 +890,23 @@ impl ContextProvider for TrustedHttpContextProvider { )) }); } + + #[cfg(any(feature = "app-connect-contract", feature = "all-system-contracts"))] + // Below protocol version 14 the app-connect contract is + // absent, so the lookup falls through to the fallback provider (or `None`). + if *id == SystemDataContract::AppConnect.id() + && platform_version.protocol_version + >= APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION + { + return load_system_data_contract(SystemDataContract::AppConnect, platform_version) + .map(|contract| Some(Arc::new(contract))) + .map_err(|e| { + ContextProviderError::Generic(format!( + "Failed to load AppConnect contract: {}", + e + )) + }); + } } // If not found in known contracts or system contracts, delegate to fallback provider if available @@ -1552,6 +1573,36 @@ mod tests { // The builder pattern is more appropriate since contracts are only added during initialization } + /// The app-connect system contract is served only from its activation version on, + /// matching Drive's `SystemDataContracts::find_by_id`: below it the contract does not + /// exist in state, and its schema would not even parse under the older meta-schema. + #[cfg(any(feature = "app-connect-contract", feature = "all-system-contracts"))] + #[test] + fn should_serve_app_connect_only_from_protocol_14() { + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::version::PlatformVersion; + + // A numeric loopback URL avoids DNS; contract lookups make no HTTP requests. + let provider = TrustedHttpContextProvider::new_with_url( + Network::Testnet, + "https://127.0.0.1".to_string(), + NonZeroUsize::new(100).unwrap(), + ) + .unwrap(); + let id = SystemDataContract::AppConnect.id(); + + assert!(provider + .get_data_contract(&id, PlatformVersion::get(13).unwrap()) + .expect("a pre-activation lookup must not error") + .is_none()); + + let contract = provider + .get_data_contract(&id, PlatformVersion::latest()) + .expect("the lookup must succeed at protocol version 14") + .expect("the app-connect contract must be served at protocol version 14"); + assert_eq!(contract.id(), id); + } + #[test] fn test_domain_resolution_check() { // Test with a domain that should resolve (using localhost) diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 30e6a1bae7b..0a75a9f36c6 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -143,6 +143,7 @@ wallet-utils-contract = ["dpp/wallet-utils-contract"] token-history-contract = ["dpp/token-history-contract"] keywords-contract = ["dpp/keywords-contract"] document-history-contract = ["dpp/document-history-contract"] +app-connect-contract = ["dpp/app-connect-contract"] token_reward_explanations = ["dpp/token-reward-explanations"] diff --git a/yarn.lock b/yarn.lock index 659b772ef72..9bf457de492 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1483,6 +1483,20 @@ __metadata: languageName: node linkType: hard +"@dashevo/app-connect-contract@workspace:packages/app-connect-contract": + version: 0.0.0-use.local + resolution: "@dashevo/app-connect-contract@workspace:packages/app-connect-contract" + dependencies: + "@dashevo/wasm-dpp": "workspace:*" + chai: "npm:^4.3.10" + dirty-chai: "npm:^2.0.1" + eslint: "npm:^9.18.0" + mocha: "npm:^11.1.0" + sinon: "npm:^18.0.1" + sinon-chai: "npm:^3.7.0" + languageName: unknown + linkType: soft + "@dashevo/bench-suite@workspace:packages/bench-suite": version: 0.0.0-use.local resolution: "@dashevo/bench-suite@workspace:packages/bench-suite" From 071c120fe7c8f33cc2cca2fc9473c32f492e74d0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 16:25:24 +0700 Subject: [PATCH 2/3] refactor(platform): share the genesis body between generations and gate app-connect by its version table - Genesis generations v1 and v2 now call one shared body (`create_genesis_state_with_system_data_contracts`) over their own contract lists; v2 is v1's list plus the app-connect contract, so the identity, contract, DPNS top-level-domain and apply steps exist once. The registered operations and their order are unchanged (the pinned protocol-14 fee expectations still hold). - The system contract version tables below protocol 14 carry `app_connect: 0`, which the crate refuses like any unknown version, so the table itself gates the contract instead of a constant checked at each lookup site. - Tests: the feature-less fallback id bytes match the published id under both feature settings; a strategy test upgrades a live chain from 13 to 14 through real blocks, checks the contract is absent while locked in, written by the activation block, served by the cache from 14 only, and present after Drive is reopened. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 1 + packages/app-connect-contract/src/lib.rs | 3 + packages/data-contracts/Cargo.toml | 3 + packages/data-contracts/src/lib.rs | 20 ++ .../create_genesis_state/common.rs | 74 +++++ .../create_genesis_state/v1/mod.rs | 67 ++--- .../create_genesis_state/v2/mod.rs | 105 ++----- .../test_cases/upgrade_fork_tests.rs | 275 ++++++++++++++++++ .../system_data_contract_versions/v1.rs | 4 +- .../system_data_contract_versions/v2.rs | 4 +- .../system_data_contract_versions/v3.rs | 6 +- 11 files changed, 420 insertions(+), 142 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e46f069202c..b0e4a6ff648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1871,6 +1871,7 @@ name = "data-contracts" version = "4.2.0-beta.3" dependencies = [ "app-connect-contract", + "base58", "dashpay-contract", "document-history-contract", "dpns-contract", diff --git a/packages/app-connect-contract/src/lib.rs b/packages/app-connect-contract/src/lib.rs index 072758ee54a..6aef3039086 100644 --- a/packages/app-connect-contract/src/lib.rs +++ b/packages/app-connect-contract/src/lib.rs @@ -15,6 +15,9 @@ pub const OWNER_ID_BYTES: [u8; 32] = [0; 32]; pub const ID: Identifier = Identifier(IdentifierBytes32(ID_BYTES)); pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES)); +/// The contract's definitions for `platform_version`. Version 0, the value the tables of +/// protocol versions below 14 carry, names no schema generation and is refused like any +/// unknown version: the contract does not exist before its activation. pub fn load_definitions(platform_version: &PlatformVersion) -> Result, Error> { match platform_version.system_data_contracts.app_connect { 1 => Ok(None), diff --git a/packages/data-contracts/Cargo.toml b/packages/data-contracts/Cargo.toml index 0f331adf20a..87cc92e1318 100644 --- a/packages/data-contracts/Cargo.toml +++ b/packages/data-contracts/Cargo.toml @@ -46,3 +46,6 @@ token-history-contract = { path = "../token-history-contract", optional = true } keyword-search-contract = { path = "../keyword-search-contract", optional = true } document-history-contract = { path = "../document-history-contract", optional = true } app-connect-contract = { path = "../app-connect-contract", optional = true } + +[dev-dependencies] +base58 = "0.2.0" diff --git a/packages/data-contracts/src/lib.rs b/packages/data-contracts/src/lib.rs index 26c824d014c..809df44bedf 100644 --- a/packages/data-contracts/src/lib.rs +++ b/packages/data-contracts/src/lib.rs @@ -285,3 +285,23 @@ impl SystemDataContract { } } } + +#[cfg(test)] +mod tests { + use super::SystemDataContract; + use base58::FromBase58; + + /// `id()` spells the published identifier under both feature settings: with the crate + /// feature on it reads the crate's constant, without it the bytes copied above. A typo + /// in either copy would give feature-less builds a different contract id. + #[test] + fn app_connect_id_matches_the_published_id() { + let published = "H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ" + .from_base58() + .expect("the published id is base58"); + assert_eq!( + SystemDataContract::AppConnect.id().to_buffer().to_vec(), + published + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs index aa28c7fa3bf..2ae13df22d3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs @@ -4,20 +4,94 @@ use crate::platform_types::platform::Platform; use dpp::platform_value::platform_value; use dpp::ProtocolError; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::DataContract; use dpp::document::DocumentV0; +use dpp::prelude::CoreBlockHeight; use dpp::serialization::PlatformSerializableWithPlatformVersion; use dpp::system_data_contracts::dpns_contract::DPNS_DASH_TLD_DOCUMENT_ID; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use dpp::version::PlatformVersion; use drive::dpp::identity::TimestampMillis; +use drive::query::TransactionArg; use drive::util::batch::{DataContractOperationType, DocumentOperationType, DriveOperation}; use drive::util::object_size_info::{ DataContractInfo, DocumentInfo, DocumentTypeInfo, OwnedDocumentInfo, }; use std::borrow::Cow; +use std::collections::BTreeMap; +use std::sync::Arc; impl Platform { + /// The genesis state every generation from v1 on writes: the initial tree structure, + /// the genesis core height, then one batch registering `system_data_contract_types` + /// (in the map's order), the wallet utils contract and the DPNS `dash` top-level + /// domain. Generations differ only in the contract list they pass, so a fix to the + /// shared steps reaches every generation, while each generation's list stays frozen + /// with it. + pub(in crate::execution::platform_events::initialization::create_genesis_state) fn create_genesis_state_with_system_data_contracts( + &self, + genesis_core_height: CoreBlockHeight, + genesis_time: TimestampMillis, + system_data_contract_types: BTreeMap>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + //versioned call + self.drive + .create_initial_state_structure(transaction, platform_version)?; + + self.drive + .store_genesis_core_height(genesis_core_height, transaction, platform_version)?; + + let mut operations = vec![]; + + // Create system identities and contracts + + for data_contract in system_data_contract_types.values() { + self.register_system_data_contract_operations( + data_contract, + &mut operations, + platform_version, + )?; + } + + let wallet_utils_contract = + load_system_data_contract(SystemDataContract::WalletUtils, platform_version)?; + + self.register_system_data_contract_operations( + &wallet_utils_contract, + &mut operations, + platform_version, + )?; + + let dpns_contract = self + .drive + .cache + .system_data_contracts + .load_dpns(platform_version)?; + + self.register_dpns_top_level_domain_operations( + &dpns_contract, + genesis_time, + &mut operations, + )?; + + let block_info = BlockInfo::default_with_time(genesis_time); + + self.drive.apply_drive_operations( + operations, + true, + &block_info, + transaction, + platform_version, + None, // No previous_fee_versions needed for genesis state creation + )?; + + Ok(()) + } + pub(in crate::execution::platform_events::initialization::create_genesis_state) fn register_system_data_contract_operations< 'a, >( diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs index 0c867dcbaeb..86e12ffedfd 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs @@ -3,13 +3,13 @@ use crate::platform_types::platform::Platform; use drive::dpp::identity::TimestampMillis; -use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; use dpp::prelude::CoreBlockHeight; -use dpp::system_data_contracts::load_system_data_contract; use dpp::version::PlatformVersion; use drive::dpp::system_data_contracts::SystemDataContract; use drive::query::TransactionArg; use std::collections::BTreeMap; +use std::sync::Arc; impl Platform { /// Creates trees and populates them with necessary identities, contracts and documents @@ -21,17 +21,22 @@ impl Platform { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - //versioned call - self.drive - .create_initial_state_structure(transaction, platform_version)?; - - self.drive - .store_genesis_core_height(genesis_core_height, transaction, platform_version)?; - - let mut operations = vec![]; - - // Create system identities and contracts + self.create_genesis_state_with_system_data_contracts( + genesis_core_height, + genesis_time, + self.genesis_v1_system_data_contracts(platform_version)?, + transaction, + platform_version, + ) + } + /// The system contracts genesis generation v1 registers, in registration order: the + /// four original contracts plus tokens and keyword search, and from protocol version + /// 13 the document history contract. Later generations extend this list. + pub(super) fn genesis_v1_system_data_contracts( + &self, + platform_version: &PlatformVersion, + ) -> Result>, Error> { let system_data_contracts = &self.drive.cache.system_data_contracts; let mut system_data_contract_types = BTreeMap::from_iter([ @@ -71,43 +76,7 @@ impl Platform { ); } - for data_contract in system_data_contract_types.values() { - self.register_system_data_contract_operations( - data_contract, - &mut operations, - platform_version, - )?; - } - - let wallet_utils_contract = - load_system_data_contract(SystemDataContract::WalletUtils, platform_version)?; - - self.register_system_data_contract_operations( - &wallet_utils_contract, - &mut operations, - platform_version, - )?; - - let dpns_contract = system_data_contracts.load_dpns(platform_version)?; - - self.register_dpns_top_level_domain_operations( - &dpns_contract, - genesis_time, - &mut operations, - )?; - - let block_info = BlockInfo::default_with_time(genesis_time); - - self.drive.apply_drive_operations( - operations, - true, - &block_info, - transaction, - platform_version, - None, // No previous_fee_versions needed for genesis state creation - )?; - - Ok(()) + Ok(system_data_contract_types) } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs index 27f76ea39bc..03916adeb4e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs @@ -3,21 +3,18 @@ use crate::platform_types::platform::Platform; use drive::dpp::identity::TimestampMillis; -use dpp::block::block_info::BlockInfo; use dpp::prelude::CoreBlockHeight; -use dpp::system_data_contracts::load_system_data_contract; use dpp::version::PlatformVersion; use drive::dpp::system_data_contracts::SystemDataContract; use drive::query::TransactionArg; -use std::collections::BTreeMap; impl Platform { /// Creates trees and populates them with necessary identities, contracts and documents. /// /// v2 is v1 (protocol versions 6 to 13) plus the app-connect contract, which activates with - /// protocol version 14. Both the document history contract (v1 registered it from protocol - /// version 13 behind a version check) and the app-connect contract are unconditional here: - /// this generation is only ever selected by the tables of protocol version 14 and later. + /// protocol version 14: the same genesis body over v1's contract list with one more entry. + /// This generation is only ever selected by the tables of protocol version 14 and later, + /// where v1's list already carries the document history contract. #[inline(always)] pub(super) fn create_genesis_state_v2( &self, @@ -26,91 +23,23 @@ impl Platform { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - //versioned call - self.drive - .create_initial_state_structure(transaction, platform_version)?; - - self.drive - .store_genesis_core_height(genesis_core_height, transaction, platform_version)?; - - let mut operations = vec![]; - - // Create system identities and contracts - - let system_data_contracts = &self.drive.cache.system_data_contracts; - - let system_data_contract_types = BTreeMap::from_iter([ - ( - SystemDataContract::DPNS, - system_data_contracts.load_dpns(platform_version)?, - ), - ( - SystemDataContract::Withdrawals, - system_data_contracts.load_withdrawals(platform_version)?, - ), - ( - SystemDataContract::Dashpay, - system_data_contracts.load_dashpay(platform_version)?, - ), - ( - SystemDataContract::MasternodeRewards, - system_data_contracts.load_masternode_reward_shares(platform_version)?, - ), - ( - SystemDataContract::TokenHistory, - system_data_contracts.load_token_history(platform_version)?, - ), - ( - SystemDataContract::KeywordSearch, - system_data_contracts.load_keyword_search(platform_version)?, - ), - ( - SystemDataContract::DocumentHistory, - system_data_contracts.load_document_history(platform_version)?, - ), - ( - SystemDataContract::AppConnect, - system_data_contracts.load_app_connect(platform_version)?, - ), - ]); - - for data_contract in system_data_contract_types.values() { - self.register_system_data_contract_operations( - data_contract, - &mut operations, - platform_version, - )?; - } - - let wallet_utils_contract = - load_system_data_contract(SystemDataContract::WalletUtils, platform_version)?; - - self.register_system_data_contract_operations( - &wallet_utils_contract, - &mut operations, - platform_version, - )?; - - let dpns_contract = system_data_contracts.load_dpns(platform_version)?; - - self.register_dpns_top_level_domain_operations( - &dpns_contract, + let mut system_data_contract_types = + self.genesis_v1_system_data_contracts(platform_version)?; + system_data_contract_types.insert( + SystemDataContract::AppConnect, + self.drive + .cache + .system_data_contracts + .load_app_connect(platform_version)?, + ); + + self.create_genesis_state_with_system_data_contracts( + genesis_core_height, genesis_time, - &mut operations, - )?; - - let block_info = BlockInfo::default_with_time(genesis_time); - - self.drive.apply_drive_operations( - operations, - true, - &block_info, + system_data_contract_types, transaction, platform_version, - None, // No previous_fee_versions needed for genesis state creation - )?; - - Ok(()) + ) } } diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs index f1a5c8a8093..3a91a25015f 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs @@ -916,6 +916,281 @@ mod tests { .is_some()); } + /// The app-connect contract only comes into existence through the upgrade to protocol + /// version 14 (or a version-14 genesis). Driven through real blocks: absent while the + /// chain still runs 13, written by the activation block, served by the system contract + /// cache from 14 only, and still there after Drive is closed and reopened. + #[stack_size(4 * 1024 * 1024)] + #[test] + async fn run_chain_v13_to_v14_registers_the_app_connect_contract() { + let strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![], + start_identities: StartIdentities::default(), + start_addresses: StartAddresses::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: None, + }, + total_hpmns: 50, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: Some(UpgradingInfo { + current_protocol_version: 13, + proposed_protocol_versions_with_weight: vec![(14, 1)], + upgrade_three_quarters_life: 0.0, + }), + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: false, + ..Default::default() + }; + let config = PlatformConfig { + validator_set: ValidatorSetConfig { + quorum_size: 30, + ..Default::default() + }, + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + epoch_time_length_s: 60, + ..Default::default() + }, + block_spacing_ms: 1_000, + testing_configs: PlatformTestConfig { + store_platform_state: true, + ..PlatformTestConfig::default_minimal_verifications() + }, + ..Default::default() + }; + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .with_initial_protocol_version(13) + .build_with_mock_rpc(); + + let platform_version_13 = PlatformVersion::get(13).expect("platform version 13"); + let platform_version_14 = PlatformVersion::get(14).expect("platform version 14"); + let app_connect_id = SystemDataContract::AppConnect.id(); + + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums, + current_validator_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + .. + } = run_chain_for_strategy( + &mut platform, + 60, + strategy.clone(), + config.clone(), + 13, + &mut None, + &mut None, + ) + .await; + + let state = abci_app.platform.state.load(); + assert_eq!(state.current_protocol_version_in_consensus(), 13); + assert_eq!(state.next_epoch_protocol_version(), 13); + let block_start = state + .last_committed_block_info() + .as_ref() + .expect("expected committed block info") + .basic_info() + .height + + 1; + drop(state); + + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums, + current_validator_quorum_hash, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + .. + } = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start, + core_height_start: 1, + block_count: 1, + proposers, + validator_quorums, + current_validator_quorum_hash, + current_proposer_versions: Some(current_proposer_versions.clone()), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: 1681094380000, + current_time_ms: end_time_ms, + instant_lock_quorums, + current_identities: Vec::new(), + current_addresses_with_balance: AddressesWithBalance::default(), + }, + strategy.clone(), + config.clone(), + StrategyRandomness::SeedEntropy(7), + ) + .await; + + let state = abci_app.platform.state.load(); + assert_eq!(state.last_committed_block_epoch().index, 1); + assert_eq!(state.current_protocol_version_in_consensus(), 13); + assert_eq!(state.next_epoch_protocol_version(), 14); + let block_start = state + .last_committed_block_info() + .as_ref() + .expect("expected committed block info") + .basic_info() + .height + + 1; + drop(state); + + // Locked in but not yet active: nothing has written the contract. + assert!(abci_app + .platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + None, + platform_version_13, + ) + .value + .expect("query the app-connect contract before activation") + .is_none()); + assert!(abci_app + .platform + .drive + .cache + .system_data_contracts + .find_by_id(app_connect_id, platform_version_13) + .expect("expected the pre-activation lookup to succeed") + .is_none()); + + drop(abci_app); + let TempPlatform { + platform: mut platform_before_activation_restart, + tempdir, + } = platform; + let core_rpc = std::mem::take(&mut platform_before_activation_restart.core_rpc); + drop(platform_before_activation_restart); + platform = TempPlatform::open_with_tempdir(tempdir, config.clone()); + platform.platform.core_rpc = core_rpc; + let abci_app = FullAbciApplication::new(&platform.platform); + + let ChainExecutionOutcome { abci_app, .. } = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start, + core_height_start: 1, + block_count: 60, + proposers, + validator_quorums, + current_validator_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: 1681094380000, + current_time_ms: end_time_ms, + instant_lock_quorums, + current_identities: Vec::new(), + current_addresses_with_balance: AddressesWithBalance::default(), + }, + strategy, + config.clone(), + StrategyRandomness::SeedEntropy(18), + ) + .await; + + let state = abci_app.platform.state.load(); + assert_eq!(state.last_committed_block_epoch().index, 2); + assert_eq!(state.current_protocol_version_in_consensus(), 14); + assert_eq!(state.next_epoch_protocol_version(), 14); + drop(state); + + let stored = abci_app + .platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + None, + platform_version_14, + ) + .value + .expect("fetch the app-connect contract after activation") + .expect("the activation block must write the app-connect contract"); + assert_eq!(stored.contract.id(), app_connect_id); + assert!(stored + .contract + .document_type_for_name("loginKeyResponse") + .is_ok()); + assert!(abci_app + .platform + .drive + .cache + .system_data_contracts + .find_by_id(app_connect_id, platform_version_14) + .expect("expected the post-activation lookup to succeed") + .is_some()); + assert!(abci_app + .platform + .drive + .cache + .system_data_contracts + .find_by_id(app_connect_id, platform_version_13) + .expect("an old-version lookup must still succeed") + .is_none()); + drop(abci_app); + + let TempPlatform { + platform: platform_before_restart, + tempdir, + } = platform; + drop(platform_before_restart); + + let reopened_platform = TempPlatform::open_with_tempdir(tempdir, config); + let state = reopened_platform.state.load(); + assert_eq!(state.current_protocol_version_in_consensus(), 14); + drop(state); + assert!(reopened_platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + None, + platform_version_14, + ) + .value + .expect("fetch the app-connect contract after restart") + .is_some()); + assert!(reopened_platform + .drive + .cache + .system_data_contracts + .find_by_id(app_connect_id, platform_version_14) + .expect("expected the lookup after restart to succeed") + .is_some()); + } + /// What a node has committed about the protocol upgrade, captured to compare a node that /// never restarted with one whose Drive was closed and reopened. #[derive(Debug, PartialEq)] diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs index 5759ca91273..b81a0bfb104 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v1.rs @@ -10,5 +10,7 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V1: SystemDataContractVersions = token_history: 1, keyword_search: 1, document_history: 1, - app_connect: 1, + // The app-connect contract does not exist before protocol version 14: no + // schema generation is selected here, so loading it is refused. + app_connect: 0, }; diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs index 3c7877fe426..c4a4f8e8035 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v2.rs @@ -15,5 +15,7 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V2: SystemDataContractVersions = token_history: 1, keyword_search: 1, document_history: 1, - app_connect: 1, + // The app-connect contract does not exist before protocol version 14: no + // schema generation is selected here, so loading it is refused. + app_connect: 0, }; diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs index 4bd6c11c210..57f8f319595 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs @@ -13,9 +13,9 @@ use crate::version::system_data_contract_versions::SystemDataContractVersions; // // The app-connect contract (app_connect: 1) also activates with // PROTOCOL_VERSION_14: it is registered at genesis from that version on and -// inserted by `transition_to_version_14` on chains upgrading from 13. Its -// feature version is listed in the earlier tables only because the struct has -// no optional fields; before 14 the contract is never loaded or served. +// inserted by `transition_to_version_14` on chains upgrading from 13. The +// earlier tables carry 0, which no schema generation answers to, so the table +// itself refuses to load the contract before 14. pub const SYSTEM_DATA_CONTRACT_VERSIONS_V3: SystemDataContractVersions = SystemDataContractVersions { withdrawals: 2, From 76b5140958d6aaa7f45a9257b2bc5006f7e17c6a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 17:27:51 +0700 Subject: [PATCH 3/3] refactor(platform): register app-connect from genesis v1 behind its activation version Genesis v0 is what mainnet and testnet replay; v1 is only ever replayed by chains born at protocol versions 9 to 13, all of them ephemeral networks re-cut since. So the app-connect contract joins genesis v1 behind a `>= 14` check, the way document history did at 13, instead of a new generation and a method-version bump. The generation test moves with it; the registered operations are unchanged. Co-Authored-By: Claude Fable 5.1 --- .../create_genesis_state/common.rs | 74 ---------- .../create_genesis_state/mod.rs | 10 +- .../create_genesis_state/v1/mod.rs | 129 +++++++++++++++--- .../create_genesis_state/v2/mod.rs | 102 -------------- .../drive_abci_method_versions/v10.rs | 2 +- .../rs-platform-version/src/version/v14.rs | 4 +- 6 files changed, 115 insertions(+), 206 deletions(-) delete mode 100644 packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs index 2ae13df22d3..aa28c7fa3bf 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/common.rs @@ -4,94 +4,20 @@ use crate::platform_types::platform::Platform; use dpp::platform_value::platform_value; use dpp::ProtocolError; -use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::DataContract; use dpp::document::DocumentV0; -use dpp::prelude::CoreBlockHeight; use dpp::serialization::PlatformSerializableWithPlatformVersion; use dpp::system_data_contracts::dpns_contract::DPNS_DASH_TLD_DOCUMENT_ID; -use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use dpp::version::PlatformVersion; use drive::dpp::identity::TimestampMillis; -use drive::query::TransactionArg; use drive::util::batch::{DataContractOperationType, DocumentOperationType, DriveOperation}; use drive::util::object_size_info::{ DataContractInfo, DocumentInfo, DocumentTypeInfo, OwnedDocumentInfo, }; use std::borrow::Cow; -use std::collections::BTreeMap; -use std::sync::Arc; impl Platform { - /// The genesis state every generation from v1 on writes: the initial tree structure, - /// the genesis core height, then one batch registering `system_data_contract_types` - /// (in the map's order), the wallet utils contract and the DPNS `dash` top-level - /// domain. Generations differ only in the contract list they pass, so a fix to the - /// shared steps reaches every generation, while each generation's list stays frozen - /// with it. - pub(in crate::execution::platform_events::initialization::create_genesis_state) fn create_genesis_state_with_system_data_contracts( - &self, - genesis_core_height: CoreBlockHeight, - genesis_time: TimestampMillis, - system_data_contract_types: BTreeMap>, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - //versioned call - self.drive - .create_initial_state_structure(transaction, platform_version)?; - - self.drive - .store_genesis_core_height(genesis_core_height, transaction, platform_version)?; - - let mut operations = vec![]; - - // Create system identities and contracts - - for data_contract in system_data_contract_types.values() { - self.register_system_data_contract_operations( - data_contract, - &mut operations, - platform_version, - )?; - } - - let wallet_utils_contract = - load_system_data_contract(SystemDataContract::WalletUtils, platform_version)?; - - self.register_system_data_contract_operations( - &wallet_utils_contract, - &mut operations, - platform_version, - )?; - - let dpns_contract = self - .drive - .cache - .system_data_contracts - .load_dpns(platform_version)?; - - self.register_dpns_top_level_domain_operations( - &dpns_contract, - genesis_time, - &mut operations, - )?; - - let block_info = BlockInfo::default_with_time(genesis_time); - - self.drive.apply_drive_operations( - operations, - true, - &block_info, - transaction, - platform_version, - None, // No previous_fee_versions needed for genesis state creation - )?; - - Ok(()) - } - pub(in crate::execution::platform_events::initialization::create_genesis_state) fn register_system_data_contract_operations< 'a, >( diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs index 331d5550eee..7afeeeaf7c9 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/mod.rs @@ -11,7 +11,6 @@ mod common; mod test; pub mod v0; pub mod v1; -pub mod v2; impl Platform { /// Creates trees and populates them with necessary identities, contracts and documents @@ -42,16 +41,9 @@ impl Platform { transaction, platform_version, ), - // V2 (protocol version 14 and later) also registers the app-connect contract - 2 => self.create_genesis_state_v2( - genesis_core_height, - genesis_time, - transaction, - platform_version, - ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "create_genesis_state".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, })), }?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs index 86e12ffedfd..2af05278cba 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs @@ -3,13 +3,13 @@ use crate::platform_types::platform::Platform; use drive::dpp::identity::TimestampMillis; -use dpp::data_contract::DataContract; +use dpp::block::block_info::BlockInfo; use dpp::prelude::CoreBlockHeight; +use dpp::system_data_contracts::load_system_data_contract; use dpp::version::PlatformVersion; use drive::dpp::system_data_contracts::SystemDataContract; use drive::query::TransactionArg; use std::collections::BTreeMap; -use std::sync::Arc; impl Platform { /// Creates trees and populates them with necessary identities, contracts and documents @@ -21,22 +21,17 @@ impl Platform { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - self.create_genesis_state_with_system_data_contracts( - genesis_core_height, - genesis_time, - self.genesis_v1_system_data_contracts(platform_version)?, - transaction, - platform_version, - ) - } + //versioned call + self.drive + .create_initial_state_structure(transaction, platform_version)?; + + self.drive + .store_genesis_core_height(genesis_core_height, transaction, platform_version)?; + + let mut operations = vec![]; + + // Create system identities and contracts - /// The system contracts genesis generation v1 registers, in registration order: the - /// four original contracts plus tokens and keyword search, and from protocol version - /// 13 the document history contract. Later generations extend this list. - pub(super) fn genesis_v1_system_data_contracts( - &self, - platform_version: &PlatformVersion, - ) -> Result>, Error> { let system_data_contracts = &self.drive.cache.system_data_contracts; let mut system_data_contract_types = BTreeMap::from_iter([ @@ -76,7 +71,55 @@ impl Platform { ); } - Ok(system_data_contract_types) + // The app-connect contract activates with protocol version 14, the same + // way: a chain born at 14 registers it at genesis, an older chain gets it + // from `transition_to_version_14`. Mainnet and testnet replay v0, and every + // chain born at 9 to 13 is an ephemeral network, so the branch changes no + // genesis a live node reproduces. + if platform_version.protocol_version >= 14 { + system_data_contract_types.insert( + SystemDataContract::AppConnect, + system_data_contracts.load_app_connect(platform_version)?, + ); + } + + for data_contract in system_data_contract_types.values() { + self.register_system_data_contract_operations( + data_contract, + &mut operations, + platform_version, + )?; + } + + let wallet_utils_contract = + load_system_data_contract(SystemDataContract::WalletUtils, platform_version)?; + + self.register_system_data_contract_operations( + &wallet_utils_contract, + &mut operations, + platform_version, + )?; + + let dpns_contract = system_data_contracts.load_dpns(platform_version)?; + + self.register_dpns_top_level_domain_operations( + &dpns_contract, + genesis_time, + &mut operations, + )?; + + let block_info = BlockInfo::default_with_time(genesis_time); + + self.drive.apply_drive_operations( + operations, + true, + &block_info, + transaction, + platform_version, + None, // No previous_fee_versions needed for genesis state creation + )?; + + Ok(()) } } @@ -85,6 +128,8 @@ mod tests { mod create_genesis_state { use crate::config::PlatformConfig; use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contracts::SystemDataContract; use drive::config::DriveConfig; use platform_version::version::{PlatformVersion, INITIAL_PROTOCOL_VERSION}; @@ -116,5 +161,53 @@ mod tests { "dc5b0d4be407428adda2315db7d782e64015cbe2d2b7df963f05622390dc3c9f" ) } + + /// The app-connect contract is part of the genesis state from protocol version 14 + /// on, where the tables select this generation, and absent from the genesis state of + /// every earlier version, which still runs v1 and which a replaying node must + /// reproduce byte for byte. Both sides go through the `create_genesis_state` + /// dispatcher. + #[test] + pub fn should_register_the_app_connect_contract_only_from_protocol_version_14() { + let app_connect_id = SystemDataContract::AppConnect.id(); + + for (platform_version, expected) in [ + (PlatformVersion::get(13).expect("protocol 13"), false), + (PlatformVersion::latest(), true), + ] { + let initial_protocol_version = platform_version.protocol_version; + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(initial_protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let stored = platform + .drive + .fetch_contract( + app_connect_id.to_buffer(), + None, + None, + None, + platform_version, + ) + .value + .expect("expected to query the app-connect contract"); + + assert_eq!( + stored.is_some(), + expected, + "app-connect contract presence in a genesis state born at protocol version {initial_protocol_version}" + ); + + if let Some(stored) = stored { + assert_eq!(stored.contract.id(), app_connect_id); + assert!(stored + .contract + .document_type_for_name("loginKeyResponse") + .is_ok()); + assert_eq!(stored.contract.document_types().len(), 1); + } + } + } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs deleted file mode 100644 index 03916adeb4e..00000000000 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v2/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::error::Error; -use crate::platform_types::platform::Platform; - -use drive::dpp::identity::TimestampMillis; - -use dpp::prelude::CoreBlockHeight; -use dpp::version::PlatformVersion; -use drive::dpp::system_data_contracts::SystemDataContract; -use drive::query::TransactionArg; - -impl Platform { - /// Creates trees and populates them with necessary identities, contracts and documents. - /// - /// v2 is v1 (protocol versions 6 to 13) plus the app-connect contract, which activates with - /// protocol version 14: the same genesis body over v1's contract list with one more entry. - /// This generation is only ever selected by the tables of protocol version 14 and later, - /// where v1's list already carries the document history contract. - #[inline(always)] - pub(super) fn create_genesis_state_v2( - &self, - genesis_core_height: CoreBlockHeight, - genesis_time: TimestampMillis, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - let mut system_data_contract_types = - self.genesis_v1_system_data_contracts(platform_version)?; - system_data_contract_types.insert( - SystemDataContract::AppConnect, - self.drive - .cache - .system_data_contracts - .load_app_connect(platform_version)?, - ); - - self.create_genesis_state_with_system_data_contracts( - genesis_core_height, - genesis_time, - system_data_contract_types, - transaction, - platform_version, - ) - } -} - -#[cfg(test)] -mod tests { - mod create_genesis_state { - use crate::test::helpers::setup::TestPlatformBuilder; - use dpp::data_contract::accessors::v0::DataContractV0Getters; - use dpp::data_contracts::SystemDataContract; - use platform_version::version::PlatformVersion; - - /// The app-connect contract is part of the genesis state from protocol version 14 - /// on, where the tables select this generation, and absent from the genesis state of - /// every earlier version, which still runs v1 and which a replaying node must - /// reproduce byte for byte. Both sides go through the `create_genesis_state` - /// dispatcher. - #[test] - pub fn should_register_the_app_connect_contract_only_from_protocol_version_14() { - let app_connect_id = SystemDataContract::AppConnect.id(); - - for (platform_version, expected) in [ - (PlatformVersion::get(13).expect("protocol 13"), false), - (PlatformVersion::latest(), true), - ] { - let initial_protocol_version = platform_version.protocol_version; - let platform = TestPlatformBuilder::new() - .with_initial_protocol_version(initial_protocol_version) - .build_with_mock_rpc() - .set_genesis_state(); - - let stored = platform - .drive - .fetch_contract( - app_connect_id.to_buffer(), - None, - None, - None, - platform_version, - ) - .value - .expect("expected to query the app-connect contract"); - - assert_eq!( - stored.is_some(), - expected, - "app-connect contract presence in a genesis state born at protocol version {initial_protocol_version}" - ); - - if let Some(stored) = stored { - assert_eq!(stored.contract.id(), app_connect_id); - assert!(stored - .contract - .document_type_for_name("loginKeyResponse") - .is_ok()); - assert_eq!(stored.contract.document_types().len(), 1); - } - } - } - } -} diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 0913b20823a..bc98d8b3a97 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -30,7 +30,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet }, initialization: DriveAbciInitializationMethodVersions { initial_core_height_and_time: 0, - create_genesis_state: 2, // registers the app-connect contract at genesis + create_genesis_state: 1, }, core_based_updates: DriveAbciCoreBasedUpdatesMethodVersions { update_core_info: 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 761d932b055..509b30cabc5 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -459,8 +459,8 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) /// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by /// the app's ephemeral key hash and the responding identity, with the wallet's -/// ephemeral key and encrypted grant in `entryPayload`. Genesis generation 2 -/// registers it on fresh chains; `transition_to_version_14` inserts it on upgrade. +/// ephemeral key and encrypted grant in `entryPayload`. Genesis registers it on +/// chains born at this version; `transition_to_version_14` inserts it on upgrade. /// The Drive and trusted SDK caches serve it only from protocol version 14. /// /// * `ShieldFromIdentity` (state transition type 21) activates: