diff --git a/packages/rs-drive-abci/src/config.rs b/packages/rs-drive-abci/src/config.rs index e004e14df60..afa20e34f5d 100644 --- a/packages/rs-drive-abci/src/config.rs +++ b/packages/rs-drive-abci/src/config.rs @@ -677,6 +677,7 @@ impl Default for ExecutionConfig { Self { use_document_triggers: ExecutionConfig::default_use_document_triggers(), verify_sum_trees: ExecutionConfig::default_verify_sum_trees(), + verify_token_sum_trees: ExecutionConfig::default_verify_token_sum_trees(), epoch_time_length_s: ExecutionConfig::default_epoch_time_length_s(), } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0.rs b/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0.rs index c1defaa1236..34362477603 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0.rs @@ -1,12 +1,10 @@ -use dpp::block::epoch::Epoch; -use drive::drive::Drive; use drive::grovedb::Transaction; use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::block_execution_context::BlockExecutionContext; use crate::platform_types::platform::Platform; -use platform_version::version::PlatformVersion; +use dpp::version::PlatformVersion; impl Platform { /// Adds operations to GroveDB op batch related to processing @@ -22,25 +20,24 @@ impl Platform { ) -> Result<(), Error> { if self.config.execution.verify_token_sum_trees { // Verify sum trees - let credits_verified = self + let token_balance = self .drive - .calculate_total_token_balance(Some(transaction), &platform_version.drive) + .calculate_total_tokens_balance(Some(transaction), &platform_version.drive) .map_err(Error::Drive)?; - if !credits_verified.ok()? { + if !token_balance.ok()? { return Err(Error::Execution( ExecutionError::CorruptedCreditsNotBalanced(format!( "credits are not balanced after block execution {:?} off by {}", - credits_verified, - credits_verified - .total_in_trees() - .unwrap() - .abs_diff(credits_verified.total_credits_in_platform) + token_balance, + token_balance + .total_identity_token_balances + .abs_diff(token_balance.total_tokens_in_platform) )), )); } } - Ok(outcome) + Ok(()) } } diff --git a/packages/rs-drive/src/drive/identity/update/methods/add_new_unique_keys_to_identity/v0/mod.rs b/packages/rs-drive/src/drive/identity/update/methods/add_new_unique_keys_to_identity/v0/mod.rs index 70de562e499..7ca4f44d18a 100644 --- a/packages/rs-drive/src/drive/identity/update/methods/add_new_unique_keys_to_identity/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/update/methods/add_new_unique_keys_to_identity/v0/mod.rs @@ -45,6 +45,7 @@ impl Drive { &mut drive_operations, &platform_version.drive, )?; + println!("{:?}", drive_operations); let fees = Drive::calculate_fee( None, Some(drive_operations), diff --git a/packages/rs-drive/src/drive/initialization/v0/mod.rs b/packages/rs-drive/src/drive/initialization/v0/mod.rs index 1d5b0d17831..71a52818d8f 100644 --- a/packages/rs-drive/src/drive/initialization/v0/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v0/mod.rs @@ -284,8 +284,6 @@ mod tests { let platform_version = PlatformVersion::first(); let drive = setup_drive_with_initial_state_structure(Some(platform_version)); - let _db_transaction = drive.grove.start_transaction(); - let platform_version = PlatformVersion::first(); let drive_version = &platform_version.drive; @@ -574,8 +572,6 @@ mod tests { fn test_initial_state_structure_proper_heights_in_latest_protocol_version() { let drive = setup_drive_with_initial_state_structure(None); - let _db_transaction = drive.grove.start_transaction(); - let platform_version = PlatformVersion::latest(); let drive_version = &platform_version.drive; diff --git a/packages/search-contract/.eslintrc b/packages/search-contract/.eslintrc new file mode 100644 index 00000000000..cb6c7636b60 --- /dev/null +++ b/packages/search-contract/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": "airbnb-base", + "rules": { + "no-plusplus": 0, + "eol-last": [ + "error", + "always" + ], + "class-methods-use-this": "off", + "curly": [ + "error", + "all" + ] + }, + "globals": { + "BigInt": true + } +} diff --git a/packages/search-contract/.mocharc.yml b/packages/search-contract/.mocharc.yml new file mode 100644 index 00000000000..164b941c1b6 --- /dev/null +++ b/packages/search-contract/.mocharc.yml @@ -0,0 +1,2 @@ +require: test/bootstrap.js +recursive: true diff --git a/packages/search-contract/Cargo.toml b/packages/search-contract/Cargo.toml new file mode 100644 index 00000000000..3625f97a47e --- /dev/null +++ b/packages/search-contract/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "token-history-contract" +description = "Token history data contract schema and tools" +version = "1.6.2" +edition = "2021" +rust-version.workspace = true +license = "MIT" + +[dependencies] +thiserror = "1.0.64" +platform-version = { path = "../rs-platform-version" } +serde_json = { version = "1.0" } +platform-value = { path = "../rs-platform-value" } diff --git a/packages/search-contract/LICENSE b/packages/search-contract/LICENSE new file mode 100644 index 00000000000..3be95833750 --- /dev/null +++ b/packages/search-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/search-contract/README.md b/packages/search-contract/README.md new file mode 100644 index 00000000000..6f6caf4d9d6 --- /dev/null +++ b/packages/search-contract/README.md @@ -0,0 +1,26 @@ +# Search Contract + +[![Build Status](https://github.com/dashpay/platform/actions/workflows/release.yml/badge.svg)](https://github.com/dashpay/platform/actions/workflows/release.yml) +[![NPM version](https://img.shields.io/npm/v/@dashevo/wallet-contract.svg?style=flat-square)](https://npmjs.org/package/@dashevo/wallet-contract) + +JSON Contracts for Searching Data Contracts + +## Table of Contents + +- [Install](#install) +- [Contributing](#contributing) +- [License](#license) + +## Install + +```sh +npm install @dashevo/search-contract +``` + +## Contributing + +Feel free to dive in! [Open an issue](https://github.com/dashpay/platform/issues/new/choose) or submit PRs. + +## License + +[MIT](LICENSE) © Dash Core Group, Inc. diff --git a/packages/search-contract/lib/systemIds.js b/packages/search-contract/lib/systemIds.js new file mode 100644 index 00000000000..7335d3d1635 --- /dev/null +++ b/packages/search-contract/lib/systemIds.js @@ -0,0 +1,4 @@ +module.exports = { + ownerId: '11111111111111111111111111111111', + contractId: '8v8CoKCDdBcQu1Y7GDNJjR7a5vkMmgpXycJURkaUhfU9', +}; diff --git a/packages/search-contract/package.json b/packages/search-contract/package.json new file mode 100644 index 00000000000..82782f0d627 --- /dev/null +++ b/packages/search-contract/package.json @@ -0,0 +1,29 @@ +{ + "name": "@dashevo/search-contract", + "version": "1.6.2", + "description": "A contract that allows searching for contracts", + "scripts": { + "lint": "eslint .", + "test": "yarn run test:unit", + "test:unit": "mocha 'test/unit/**/*.spec.js'" + }, + "contributors": [ + { + "name": "Samuel Westrich", + "email": "sam@dash.org", + "url": "https://github.com/quantumexplorer" + } + ], + "license": "MIT", + "devDependencies": { + "@dashevo/wasm-dpp": "workspace:*", + "chai": "^4.3.10", + "dirty-chai": "^2.0.1", + "eslint": "^8.53.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.29.0", + "mocha": "^10.2.0", + "sinon": "^17.0.1", + "sinon-chai": "^3.7.0" + } +} diff --git a/packages/search-contract/schema/v1/search-contract-documents.json b/packages/search-contract/schema/v1/search-contract-documents.json new file mode 100644 index 00000000000..f146ecb3e6c --- /dev/null +++ b/packages/search-contract/schema/v1/search-contract-documents.json @@ -0,0 +1,62 @@ +{ + "contract": { + "type": "object", + "documentsMutable": false, + "canBeDeleted": false, + "referenceType": "contract", + "creationRestrictionMode": 2, + "indices": [ + { + "name": "byKeyword", + "properties": [ + { + "keyword": "asc" + } + ] + } + ], + "properties": { + "keyword": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "position": 0 + } + }, + "required": [ + "$contractId", + "keyword" + ], + "additionalProperties": false + }, + "token": { + "type": "object", + "documentsMutable": false, + "canBeDeleted": false, + "referenceType": "token", + "creationRestrictionMode": 2, + "indices": [ + { + "name": "byKeyword", + "properties": [ + { + "keyword": "asc" + } + ] + } + ], + "properties": { + "keyword": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "position": 0 + } + }, + "required": [ + "$tokenId", + "keyword" + ], + "additionalProperties": false + } +} diff --git a/packages/search-contract/src/error.rs b/packages/search-contract/src/error.rs new file mode 100644 index 00000000000..d01bbcc91cf --- /dev/null +++ b/packages/search-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/search-contract/src/lib.rs b/packages/search-contract/src/lib.rs new file mode 100644 index 00000000000..f7767ea6c26 --- /dev/null +++ b/packages/search-contract/src/lib.rs @@ -0,0 +1,37 @@ +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] = [ + 92, 20, 14, 101, 92, 2, 101, 187, 194, 168, 8, 113, 109, 225, 132, 121, 133, 19, 89, 24, 173, + 81, 205, 253, 11, 118, 102, 75, 169, 91, 163, 124, +]; + +pub const OWNER_ID_BYTES: [u8; 32] = [0; 32]; + +pub const ID: Identifier = Identifier(IdentifierBytes32(ID_BYTES)); +pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES)); +pub fn load_definitions(platform_version: &PlatformVersion) -> Result, Error> { + match platform_version.system_data_contracts.withdrawals { + 1 => Ok(None), + version => Err(Error::UnknownVersionMismatch { + method: "search_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.withdrawals { + 1 => v1::load_documents_schemas(), + version => Err(Error::UnknownVersionMismatch { + method: "search_contract::load_documents_schemas".to_string(), + known_versions: vec![1], + received: version, + }), + } +} diff --git a/packages/search-contract/src/v1/mod.rs b/packages/search-contract/src/v1/mod.rs new file mode 100644 index 00000000000..e5d6a067712 --- /dev/null +++ b/packages/search-contract/src/v1/mod.rs @@ -0,0 +1,27 @@ +use crate::Error; +use serde_json::Value; + +pub mod document_types { + pub mod contract { + pub const NAME: &str = "contract"; + + pub mod properties { + pub const KEY_INDEX: &str = "byKeyword"; + } + } + + pub mod token { + pub const NAME: &str = "token"; + + pub mod properties { + pub const KEY_INDEX: &str = "byKeyword"; + } + } +} + +pub fn load_documents_schemas() -> Result { + serde_json::from_str(include_str!( + "../../schema/v1/search-contract-documents.json" + )) + .map_err(Error::InvalidSchemaJson) +} diff --git a/packages/search-contract/test/.eslintrc b/packages/search-contract/test/.eslintrc new file mode 100644 index 00000000000..720ced73852 --- /dev/null +++ b/packages/search-contract/test/.eslintrc @@ -0,0 +1,12 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "rules": { + "import/no-extraneous-dependencies": "off" + }, + "globals": { + "expect": true + } +} diff --git a/packages/search-contract/test/bootstrap.js b/packages/search-contract/test/bootstrap.js new file mode 100644 index 00000000000..7af04f464d7 --- /dev/null +++ b/packages/search-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/search-contract/test/unit/searchContract.spec.js b/packages/search-contract/test/unit/searchContract.spec.js new file mode 100644 index 00000000000..2fd94a879b7 --- /dev/null +++ b/packages/search-contract/test/unit/searchContract.spec.js @@ -0,0 +1,187 @@ +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 walletContractDocumentsSchema = require('../../schema/v1/search-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('Search 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), walletContractDocumentsSchema); + }); + + it('should have a valid contract definition', async () => { + expect(() => dpp.dataContract.create(identityId, BigInt(1), walletContractDocumentsSchema)) + .to + .not + .throw(); + }); + + describe('documents', () => { + describe('txMetadata', () => { + let rawTxMetadataDocument; + + beforeEach(() => { + rawTxMetadataDocument = { + keyIndex: 0, + encryptionKeyIndex: 100, + encryptedMetadata: crypto.randomBytes(64), + }; + }); + + describe('keyIndex', () => { + it('should be defined', async () => { + delete rawTxMetadataDocument.keyIndex; + + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword) + .to + .equal('required'); + expect(error.params.missingProperty) + .to + .equal('keyIndex'); + }); + + it('should be a non-negative integer', async () => { + rawTxMetadataDocument.keyIndex = -1; + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + expect(error.keyword).to.equal('minimum'); + }); + }); + + describe('encryptionKeyIndex', () => { + it('should be defined', async () => { + delete rawTxMetadataDocument.encryptionKeyIndex; + + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword) + .to + .equal('required'); + expect(error.params.missingProperty) + .to + .equal('encryptionKeyIndex'); + }); + + it('should be a non-negative integer', async () => { + rawTxMetadataDocument.encryptionKeyIndex = -1; + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + expect(error.keyword).to.equal('minimum'); + }); + }); + + describe('encryptedMetadata', () => { + it('should be defined', async () => { + delete rawTxMetadataDocument.encryptedMetadata; + + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword) + .to + .equal('required'); + expect(error.params.missingProperty) + .to + .equal('encryptedMetadata'); + }); + + it('should be not shorter than 32 bytes', async () => { + rawTxMetadataDocument.encryptedMetadata = crypto.randomBytes(31); + + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword) + .to + .equal('minItems'); + expect(error.instancePath) + .to + .equal('/encryptedMetadata'); + }); + + it('should be not longer than 4096 bytes', async () => { + rawTxMetadataDocument.encryptedMetadata = crypto.randomBytes(4097); + + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + const validationResult = document.validate(dpp.protocolVersion); + const error = expectJsonSchemaError(validationResult); + + expect(error.keyword) + .to + .equal('maxItems'); + expect(error.instancePath) + .to + .equal('/encryptedMetadata'); + }); + }); + + it('should not have additional properties', async () => { + rawTxMetadataDocument.someOtherProperty = 42; + + const document = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + 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 txMetadata = dpp.document.create(dataContract, identityId, 'txMetadata', rawTxMetadataDocument); + + const result = await txMetadata.validate(dpp.protocolVersion); + + expect(result.isValid()) + .to + .be + .true(); + }); + }); + }); +});