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..b0e4a6ff648 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,8 @@ dependencies = [ name = "data-contracts" version = "4.2.0-beta.3" dependencies = [ + "app-connect-contract", + "base58", "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..6aef3039086 --- /dev/null +++ b/packages/app-connect-contract/src/lib.rs @@ -0,0 +1,66 @@ +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)); +/// 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), + 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..87cc92e1318 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,7 @@ 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 } + +[dev-dependencies] +base58 = "0.2.0" 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..809df44bedf 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,37 @@ 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")), } } } + +#[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-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/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/v1/mod.rs index 0c867dcbaeb..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 @@ -71,6 +71,18 @@ impl Platform { ); } + // 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, @@ -116,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}; @@ -147,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/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-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-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/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..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,4 +10,7 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V1: SystemDataContractVersions = token_history: 1, keyword_search: 1, document_history: 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 9b87b90e94b..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,4 +15,7 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V2: SystemDataContractVersions = token_history: 1, keyword_search: 1, document_history: 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 3261a215403..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 @@ -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. 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, @@ -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..509b30cabc5 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 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: /// `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"