From 34ca795b01fee9878170df492de90e7be6e1223f Mon Sep 17 00:00:00 2001 From: stringhandler Date: Tue, 28 Jun 2022 19:43:08 +0200 Subject: [PATCH 1/3] chore: delete package-lock.json from wallet grpc --- clients/wallet_grpc_client/package-lock.json | 262 ------------------- 1 file changed, 262 deletions(-) delete mode 100644 clients/wallet_grpc_client/package-lock.json diff --git a/clients/wallet_grpc_client/package-lock.json b/clients/wallet_grpc_client/package-lock.json deleted file mode 100644 index 768729da2a..0000000000 --- a/clients/wallet_grpc_client/package-lock.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "name": "@tari/wallet-grpc-client", - "version": "0.0.1", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "@tari/wallet-grpc-client", - "version": "0.0.1", - "dependencies": { - "@grpc/grpc-js": "^1.3.6", - "@grpc/proto-loader": "^0.5.5", - "grpc-promise": "^1.4.0" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.6.tgz", - "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", - "dependencies": { - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz", - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "node_modules/@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "node_modules/@types/node": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", - "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" - }, - "node_modules/grpc-promise": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/grpc-promise/-/grpc-promise-1.4.0.tgz", - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/protobufjs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - } - }, - "dependencies": { - "@grpc/grpc-js": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.6.tgz", - "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", - "requires": { - "@types/node": ">=12.12.47" - } - }, - "@grpc/proto-loader": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz", - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/node": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", - "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" - }, - "grpc-promise": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/grpc-promise/-/grpc-promise-1.4.0.tgz", - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "protobufjs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - } - } - } -} From 92ae4abf2d59e675f2f6c48df053e3273076fbd8 Mon Sep 17 00:00:00 2001 From: Brian Pearce Date: Wed, 29 Jun 2022 09:54:48 +0200 Subject: [PATCH 2/3] feat(vn): record contract states (#4241) Description --- Add a contracts table to the global vn db so we can store the state of contracts the vn is a member of. Motivation and Context --- It will make it easier to go back and check known contracts that are awaiting quorum acceptance. How Has This Been Tested? --- Manually, and by fixing the previously broken spec. Commits --- * Add the constitutions table to the globaldb * Add contract saving call * Refactor the naming from constitution to contract * Save the contract in a pending state * Store expired contracts * Update contract states * Only auto accept if the setting is set * Make the sleep length configurable The long sleep length was causing the specs to fail. We also don't want to wait in minutes for polling to occur. Make the option configurable and use a much shorter time in testing. * Rename to state for consistency * Fix clippy warnings * No more lockfiles, npm install it --- .../workflows/dan_layer_integration_tests.yml | 2 +- Cargo.lock | 2 + .../tari_validator_node/src/config.rs | 4 +- .../src/contract_worker_manager.rs | 43 ++++++++++++----- dan_layer/core/Cargo.toml | 4 +- .../core/src/storage/global/global_db.rs | 21 ++++++++- .../global/global_db_backend_adapter.rs | 26 +++++++++++ dan_layer/core/src/storage/global/mod.rs | 2 +- dan_layer/core/src/storage/mocks/global_db.rs | 17 ++++++- .../down.sql | 21 +++++++++ .../2022-06-28-120617_create_contracts/up.sql | 29 ++++++++++++ .../src/global/models/contract.rs | 30 ++++++++++++ .../storage_sqlite/src/global/models/mod.rs | 1 + dan_layer/storage_sqlite/src/global/schema.rs | 10 ++++ .../sqlite_global_db_backend_adapter.rs | 46 ++++++++++++++++++- integration_tests/config/config.toml | 3 +- .../features/ValidatorNode.feature | 2 +- 17 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/down.sql create mode 100644 dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/up.sql create mode 100644 dan_layer/storage_sqlite/src/global/models/contract.rs diff --git a/.github/workflows/dan_layer_integration_tests.yml b/.github/workflows/dan_layer_integration_tests.yml index a655a6ed4e..8c61df48a6 100644 --- a/.github/workflows/dan_layer_integration_tests.yml +++ b/.github/workflows/dan_layer_integration_tests.yml @@ -83,7 +83,7 @@ jobs: command: build args: --release --bin tari_validator_node -Z unstable-options - name: npm ci - run: cd integration_tests && npm ci && cd node_modules/wallet-grpc-client && npm ci + run: cd integration_tests && npm ci && cd node_modules/wallet-grpc-client && npm install - name: Run integration tests run: cd integration_tests && mkdir -p cucumber_output && node_modules/.bin/cucumber-js --profile "non-critical" --tags "@dan and not @broken" --format json:cucumber_output/tests.cucumber --exit --retry 2 --retry-tag-filter "@flaky and not @broken" - name: Generate report diff --git a/Cargo.lock b/Cargo.lock index 3111f69c49..ff2e00b207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6981,6 +6981,8 @@ dependencies = [ "futures 0.3.21", "lmdb-zero", "log", + "num-derive", + "num-traits", "patricia_tree", "prost", "prost-types", diff --git a/applications/tari_validator_node/src/config.rs b/applications/tari_validator_node/src/config.rs index 09de83d123..eb58b8b9b7 100644 --- a/applications/tari_validator_node/src/config.rs +++ b/applications/tari_validator_node/src/config.rs @@ -74,8 +74,9 @@ pub struct ValidatorNodeConfig { pub p2p: P2pConfig, pub constitution_auto_accept: bool, /// Constitution polling interval in block height - pub constitution_management_polling_interval: u64, pub constitution_management_confirmation_time: u64, + pub constitution_management_polling_interval: u64, + pub constitution_management_polling_interval_in_seconds: u64, pub grpc_address: Option, } @@ -116,6 +117,7 @@ impl Default for ValidatorNodeConfig { constitution_auto_accept: false, constitution_management_confirmation_time: 20, constitution_management_polling_interval: 120, + constitution_management_polling_interval_in_seconds: 60, p2p, grpc_address: Some("/ip4/127.0.0.1/tcp/18144".parse().unwrap()), } diff --git a/applications/tari_validator_node/src/contract_worker_manager.rs b/applications/tari_validator_node/src/contract_worker_manager.rs index 7dcedab425..79c90c6ff7 100644 --- a/applications/tari_validator_node/src/contract_worker_manager.rs +++ b/applications/tari_validator_node/src/contract_worker_manager.rs @@ -49,7 +49,7 @@ use tari_dan_core::{ WalletClient, }, storage::{ - global::{GlobalDb, GlobalDbMetadataKey}, + global::{ContractState, GlobalDb, GlobalDbMetadataKey}, StorageError, }, workers::ConsensusWorker, @@ -134,6 +134,9 @@ impl ContractWorkerManager { // TODO: Uncomment line to scan from previous block height once we can // start up asset workers for existing contracts. // self.load_initial_state()?; + if self.config.constitution_auto_accept { + info!("constitution_auto_accept is true") + } if !self.config.scan_for_assets { info!( @@ -155,7 +158,7 @@ impl ContractWorkerManager { next_scan_height ); tokio::select! { - _ = time::sleep(Duration::from_secs(60)) => {}, + _ = time::sleep(Duration::from_secs(self.config.constitution_management_polling_interval_in_seconds)) => {}, _ = &mut self.shutdown => break, } continue; @@ -170,20 +173,29 @@ impl ContractWorkerManager { info!(target: LOG_TARGET, "{} new contract(s) found", active_contracts.len()); for contract in active_contracts { - info!( - target: LOG_TARGET, - "Posting acceptance transaction for contract {}", contract.contract_id - ); - self.post_contract_acceptance(&contract).await?; - // TODO: Scan for acceptances and once enough are present, start working on the contract - // for now, we start working immediately. - let kill = self.spawn_asset_worker(contract.contract_id, &contract.constitution); - self.active_workers.insert(contract.contract_id, kill); + self.global_db + .save_contract(contract.contract_id, contract.mined_height, ContractState::Pending)?; + + if self.config.constitution_auto_accept { + info!( + target: LOG_TARGET, + "Posting acceptance transaction for contract {}", contract.contract_id + ); + self.post_contract_acceptance(&contract).await?; + + self.global_db + .update_contract_state(contract.contract_id, ContractState::Accepted)?; + + // TODO: Scan for acceptances and once enough are present, start working on the contract + // for now, we start working immediately. + let kill = self.spawn_asset_worker(contract.contract_id, &contract.constitution); + self.active_workers.insert(contract.contract_id, kill); + } } self.set_last_scanned_block(tip)?; tokio::select! { - _ = time::sleep(Duration::from_secs(60)) => {}, + _ = time::sleep(Duration::from_secs(self.config.constitution_management_polling_interval_in_seconds)) => {}, _ = &mut self.shutdown => break, } } @@ -234,6 +246,7 @@ impl ContractWorkerManager { let mut new_contracts = vec![]; for utxo in outputs { let output = some_or_continue!(utxo.output.into_unpruned_output()); + let mined_height = utxo.mined_height; let sidechain_features = some_or_continue!(output.features.sidechain_features); let contract_id = sidechain_features.contract_id; let constitution = some_or_continue!(sidechain_features.constitution); @@ -258,12 +271,17 @@ impl ContractWorkerManager { constitution.acceptance_requirements.acceptance_period_expiry, tip.height_of_longest_chain ); + + self.global_db + .save_contract(contract_id, mined_height, ContractState::Expired)?; + continue; } new_contracts.push(ActiveContract { contract_id, constitution, + mined_height, }); } @@ -435,4 +453,5 @@ pub enum WorkerManagerError { struct ActiveContract { pub contract_id: FixedHash, pub constitution: ContractConstitution, + pub mined_height: u64, } diff --git a/dan_layer/core/Cargo.toml b/dan_layer/core/Cargo.toml index 83d4b45dc2..ecb17a3fbb 100644 --- a/dan_layer/core/Cargo.toml +++ b/dan_layer/core/Cargo.toml @@ -28,8 +28,10 @@ blake2 = "0.9.2" clap = "3.1.8" digest = "0.9.0" futures = { version = "^0.3.1" } -log = { version = "0.4.8", features = ["std"] } lmdb-zero = "0.4.4" +log = { version = "0.4.8", features = ["std"] } +num-derive = "0.3.3" +num-traits = "0.2.15" prost = "0.9" prost-types = "0.9" rand = "0.8.4" diff --git a/dan_layer/core/src/storage/global/global_db.rs b/dan_layer/core/src/storage/global/global_db.rs index e38452ca3f..b0514925d6 100644 --- a/dan_layer/core/src/storage/global/global_db.rs +++ b/dan_layer/core/src/storage/global/global_db.rs @@ -22,8 +22,10 @@ use std::sync::Arc; +use tari_common_types::types::FixedHash; + use crate::storage::{ - global::{GlobalDbBackendAdapter, GlobalDbMetadataKey}, + global::{ContractState, GlobalDbBackendAdapter, GlobalDbMetadataKey}, StorageError, }; @@ -48,4 +50,21 @@ impl GlobalDb Result>, StorageError> { self.adapter.get_data(key).map_err(TGlobalDbBackendAdapter::Error::into) } + + pub fn save_contract( + &self, + contract_id: FixedHash, + mined_height: u64, + state: ContractState, + ) -> Result<(), StorageError> { + self.adapter + .save_contract(contract_id, mined_height, state) + .map_err(TGlobalDbBackendAdapter::Error::into) + } + + pub fn update_contract_state(&self, contract_id: FixedHash, state: ContractState) -> Result<(), StorageError> { + self.adapter + .update_contract_state(contract_id, state) + .map_err(TGlobalDbBackendAdapter::Error::into) + } } diff --git a/dan_layer/core/src/storage/global/global_db_backend_adapter.rs b/dan_layer/core/src/storage/global/global_db_backend_adapter.rs index 4947379f4c..2404dc5a1f 100644 --- a/dan_layer/core/src/storage/global/global_db_backend_adapter.rs +++ b/dan_layer/core/src/storage/global/global_db_backend_adapter.rs @@ -20,6 +20,10 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use tari_common_types::types::FixedHash; + use crate::storage::StorageError; pub trait GlobalDbBackendAdapter: Send + Sync + Clone { @@ -35,6 +39,9 @@ pub trait GlobalDbBackendAdapter: Send + Sync + Clone { key: &GlobalDbMetadataKey, connection: &Self::BackendTransaction, ) -> Result>, Self::Error>; + fn save_contract(&self, contract_id: FixedHash, mined_height: u64, state: ContractState) + -> Result<(), Self::Error>; + fn update_contract_state(&self, contract_id: FixedHash, state: ContractState) -> Result<(), Self::Error>; } #[derive(Debug, Clone, Copy)] @@ -51,3 +58,22 @@ impl GlobalDbMetadataKey { } } } + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, FromPrimitive)] +#[repr(u8)] +pub enum ContractState { + Pending = 0, + Accepted = 1, + Expired = 2, +} + +impl ContractState { + pub fn as_byte(self) -> u8 { + self as u8 + } + + /// Returns the Status that corresponds to the byte. None is returned if the byte does not correspond + pub fn from_byte(value: u8) -> Option { + FromPrimitive::from_u8(value) + } +} diff --git a/dan_layer/core/src/storage/global/mod.rs b/dan_layer/core/src/storage/global/mod.rs index b4d849d85e..8ee36752bd 100644 --- a/dan_layer/core/src/storage/global/mod.rs +++ b/dan_layer/core/src/storage/global/mod.rs @@ -23,4 +23,4 @@ mod global_db; pub use global_db::GlobalDb; mod global_db_backend_adapter; -pub use global_db_backend_adapter::{GlobalDbBackendAdapter, GlobalDbMetadataKey}; +pub use global_db_backend_adapter::{ContractState, GlobalDbBackendAdapter, GlobalDbMetadataKey}; diff --git a/dan_layer/core/src/storage/mocks/global_db.rs b/dan_layer/core/src/storage/mocks/global_db.rs index 0adc59a4a9..8f8d455eeb 100644 --- a/dan_layer/core/src/storage/mocks/global_db.rs +++ b/dan_layer/core/src/storage/mocks/global_db.rs @@ -20,8 +20,10 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use tari_common_types::types::FixedHash; + use crate::storage::{ - global::{GlobalDbBackendAdapter, GlobalDbMetadataKey}, + global::{ContractState, GlobalDbBackendAdapter, GlobalDbMetadataKey}, StorageError, }; @@ -55,4 +57,17 @@ impl GlobalDbBackendAdapter for MockGlobalDbBackupAdapter { ) -> Result>, Self::Error> { todo!() } + + fn save_contract( + &self, + _contract_id: FixedHash, + _mined_height: u64, + _status: ContractState, + ) -> Result<(), Self::Error> { + todo!() + } + + fn update_contract_state(&self, _contract_id: FixedHash, _state: ContractState) -> Result<(), Self::Error> { + todo!() + } } diff --git a/dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/down.sql b/dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/down.sql new file mode 100644 index 0000000000..2c804585e0 --- /dev/null +++ b/dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/down.sql @@ -0,0 +1,21 @@ +-- // Copyright 2022. The Tari Project +-- // +-- // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +-- // following conditions are met: +-- // +-- // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +-- // disclaimer. +-- // +-- // 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +-- // following disclaimer in the documentation and/or other materials provided with the distribution. +-- // +-- // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +-- // products derived from this software without specific prior written permission. +-- // +-- // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +-- // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +-- // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +-- // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +-- // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +-- // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/up.sql b/dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/up.sql new file mode 100644 index 0000000000..f9fd18ef90 --- /dev/null +++ b/dan_layer/storage_sqlite/global_db_migrations/2022-06-28-120617_create_contracts/up.sql @@ -0,0 +1,29 @@ +-- // Copyright 2022. The Tari Project +-- // +-- // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +-- // following conditions are met: +-- // +-- // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +-- // disclaimer. +-- // +-- // 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +-- // following disclaimer in the documentation and/or other materials provided with the distribution. +-- // +-- // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +-- // products derived from this software without specific prior written permission. +-- // +-- // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +-- // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +-- // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +-- // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +-- // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +-- // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +create table contracts ( + id blob primary key not null, + height bigint not null, + state integer not null +); + +create index contracts_state_index on contracts (state); diff --git a/dan_layer/storage_sqlite/src/global/models/contract.rs b/dan_layer/storage_sqlite/src/global/models/contract.rs new file mode 100644 index 0000000000..d99e648039 --- /dev/null +++ b/dan_layer/storage_sqlite/src/global/models/contract.rs @@ -0,0 +1,30 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::global::schema::*; + +#[derive(Queryable, Insertable, Identifiable)] +pub struct Contract { + pub id: Vec, + pub state: i32, + pub height: i64, +} diff --git a/dan_layer/storage_sqlite/src/global/models/mod.rs b/dan_layer/storage_sqlite/src/global/models/mod.rs index 64826ec9ef..030337a27c 100644 --- a/dan_layer/storage_sqlite/src/global/models/mod.rs +++ b/dan_layer/storage_sqlite/src/global/models/mod.rs @@ -20,4 +20,5 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +pub mod contract; pub mod metadata; diff --git a/dan_layer/storage_sqlite/src/global/schema.rs b/dan_layer/storage_sqlite/src/global/schema.rs index a17b0f45d5..19e0b1767d 100644 --- a/dan_layer/storage_sqlite/src/global/schema.rs +++ b/dan_layer/storage_sqlite/src/global/schema.rs @@ -20,9 +20,19 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +table! { + contracts (id) { + id -> Binary, + height -> BigInt, + state -> Integer, + } +} + table! { metadata (key_name) { key_name -> Binary, value -> Binary, } } + +allow_tables_to_appear_in_same_query!(contracts, metadata,); diff --git a/dan_layer/storage_sqlite/src/global/sqlite_global_db_backend_adapter.rs b/dan_layer/storage_sqlite/src/global/sqlite_global_db_backend_adapter.rs index 4f37c7a5a4..01742c5496 100644 --- a/dan_layer/storage_sqlite/src/global/sqlite_global_db_backend_adapter.rs +++ b/dan_layer/storage_sqlite/src/global/sqlite_global_db_backend_adapter.rs @@ -21,7 +21,8 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use diesel::{prelude::*, Connection, RunQueryDsl, SqliteConnection}; -use tari_dan_core::storage::global::{GlobalDbBackendAdapter, GlobalDbMetadataKey}; +use tari_common_types::types::FixedHash; +use tari_dan_core::storage::global::{ContractState, GlobalDbBackendAdapter, GlobalDbMetadataKey}; use crate::{error::SqliteStorageError, global::models::metadata::Metadata, SqliteTransaction}; @@ -133,4 +134,47 @@ impl GlobalDbBackendAdapter for SqliteGlobalDbBackendAdapter { })?; Ok(()) } + + fn save_contract( + &self, + contract_id: FixedHash, + mined_height: u64, + state: ContractState, + ) -> Result<(), Self::Error> { + use crate::global::schema::contracts; + let tx = self.create_transaction()?; + + diesel::insert_into(contracts::table) + .values(( + contracts::id.eq(contract_id.to_vec()), + contracts::height.eq(mined_height as i64), + contracts::state.eq(i32::from(state.as_byte())), + )) + .execute(tx.connection()) + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "insert::contract".to_string(), + })?; + + self.commit(&tx)?; + + Ok(()) + } + + fn update_contract_state(&self, contract_id: FixedHash, state: ContractState) -> Result<(), Self::Error> { + use crate::global::schema::contracts; + let tx = self.create_transaction()?; + + diesel::update(contracts::table.filter(contracts::id.eq(contract_id.to_vec()))) + .set(contracts::state.eq(i32::from(state.as_byte()))) + .execute(tx.connection()) + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "update::contract_state".to_string(), + })?; + + self.commit(&tx)?; + + Ok(()) + } } diff --git a/integration_tests/config/config.toml b/integration_tests/config/config.toml index 1bf3ac8ce8..569d3b05c8 100644 --- a/integration_tests/config/config.toml +++ b/integration_tests/config/config.toml @@ -360,8 +360,9 @@ new_asset_scanning_interval = 10 constitution_auto_accept = false +constitution_management_polling_interval_in_seconds = 10 constitution_management_polling_interval = 5 -constitution_management_confirmation_time = 20 +constitution_management_confirmation_time = 50 ######################################################################################################################## # # # Collectibles Configuration Options # diff --git a/integration_tests/features/ValidatorNode.feature b/integration_tests/features/ValidatorNode.feature index a84a7338ca..ad5f813b28 100644 --- a/integration_tests/features/ValidatorNode.feature +++ b/integration_tests/features/ValidatorNode.feature @@ -18,7 +18,6 @@ Feature: Validator Node And I mine 9 blocks using wallet WALLET1 on NODE1 Then wallet WALLET1 will have a successfully mined contract acceptance transaction for contract DEF1 - @broken Scenario: Contract constitution auto acceptance Given I have a seed node NODE1 And I have wallet WALLET1 connected to all seed nodes @@ -27,6 +26,7 @@ Feature: Validator Node And I have a validator node VN1 connected to base node NODE1 and wallet WALLET1 And validator node VN1 has "constitution_auto_accept" set to true And validator node VN1 has "constitution_management_polling_interval" set to 5 + And validator node VN1 has "constitution_management_polling_interval_in_seconds" set to 5 And I publish a contract definition DEF1 from file "fixtures/contract_definition.json" on wallet WALLET1 via command line And I mine 4 blocks using wallet WALLET1 on NODE1 When I create a contract constitution COM1 for contract DEF1 from file "fixtures/contract_constitution.json" From 90a5ec32bd4f746b29f06d70bc9737a9cacf4538 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Wed, 29 Jun 2022 10:04:09 +0200 Subject: [PATCH 3/3] feat: wallet selects previous checkpoint for spending (#4236) Description --- - adds ContractOutput filter to UtxoSelectionCriteria - adds output features param to send_transaction functions - fixes input metadata mismatch when spending contract utxos - changes dan to submit initial checkpoint for first checkpoint (todo: dan needs to keep track of checkpoints) Motivation and Context --- Allows wallet to select contract utxos when spending and allows for particular output features to be specified for the non-change output. How Has This Been Tested? --- Basic unit test. Manually - VN posts initial and follow up checkpoint that spends previous checkpoint Commits --- * feat(wallet): add ContractOutput selection filter * feat(wallet): spend contract utxos and allow output features to be specified * chore(lmdb-db): add more debug info to contract index * fix(dan): use new contract output metadata for checkpoints * fix(dan): create initial checkpoint --- .../src/automation/commands.rs | 17 ++- .../src/grpc/wallet_grpc_server.rs | 20 ++- .../src/ui/state/app_state.rs | 19 ++- .../tari_console_wallet/src/ui/state/tasks.rs | 13 +- .../src/grpc/services/wallet_client.rs | 33 ++-- .../chain_storage/lmdb_db/contract_index.rs | 32 +++- .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 1 + base_layer/core/src/proto/transaction.rs | 1 + .../side_chain/committee_signatures.rs | 7 + .../unblinded_output_builder.rs | 4 + base_layer/wallet/src/assets/asset_manager.rs | 33 ++-- .../infrastructure/asset_manager_service.rs | 4 +- .../src/output_manager_service/error.rs | 5 +- .../src/output_manager_service/handle.rs | 34 ++--- .../output_manager_service/input_selection.rs | 44 +++++- .../src/output_manager_service/service.rs | 144 +++++++++--------- .../storage/database/backend.rs | 2 +- .../storage/database/mod.rs | 2 +- .../storage/sqlite_db/mod.rs | 2 +- .../storage/sqlite_db/output_sql.rs | 57 +++---- .../wallet/src/transaction_service/handle.rs | 68 +-------- .../protocols/transaction_send_protocol.rs | 15 +- .../wallet/src/transaction_service/service.rs | 47 +++--- .../output_manager_service_tests/service.rs | 142 +++++++++++++---- .../transaction_service_tests/service.rs | 34 ++++- base_layer/wallet/tests/wallet.rs | 2 + base_layer/wallet_ffi/src/lib.rs | 3 + .../core/src/models/base_layer_output.rs | 8 +- .../core/src/services/checkpoint_manager.rs | 10 +- dan_layer/core/src/services/mocks/mod.rs | 1 + dan_layer/core/src/services/wallet_client.rs | 1 + .../core/src/workers/consensus_worker.rs | 1 + 32 files changed, 492 insertions(+), 314 deletions(-) diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 8537517a77..eec187243d 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -55,6 +55,7 @@ use tari_core::transactions::{ ContractAmendment, ContractDefinition, ContractUpdateProposal, + OutputFeatures, SideChainConsensus, SideChainFeatures, TransactionOutput, @@ -141,7 +142,13 @@ pub async fn send_tari( message: String, ) -> Result { wallet_transaction_service - .send_transaction(dest_pubkey, amount, fee_per_gram * uT, message) + .send_transaction( + dest_pubkey, + amount, + OutputFeatures::default(), + fee_per_gram * uT, + message, + ) .await .map_err(CommandError::TransactionServiceError) } @@ -205,7 +212,13 @@ pub async fn send_one_sided( message: String, ) -> Result { wallet_transaction_service - .send_one_sided_transaction(dest_pubkey, amount, fee_per_gram * uT, message) + .send_one_sided_transaction( + dest_pubkey, + amount, + OutputFeatures::default(), + fee_per_gram * uT, + message, + ) .await .map_err(CommandError::TransactionServiceError) } diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index 3eb41a80f0..8bc09b6235 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -458,7 +458,13 @@ impl wallet_server::Wallet for WalletGrpcServer { ( address, transaction_service - .send_transaction(pk, amount.into(), fee_per_gram.into(), message) + .send_transaction( + pk, + amount.into(), + OutputFeatures::default(), + fee_per_gram.into(), + message, + ) .await, ) }); @@ -467,7 +473,13 @@ impl wallet_server::Wallet for WalletGrpcServer { ( address, transaction_service - .send_one_sided_transaction(pk, amount.into(), fee_per_gram.into(), message) + .send_one_sided_transaction( + pk, + amount.into(), + OutputFeatures::default(), + fee_per_gram.into(), + message, + ) .await, ) }); @@ -857,8 +869,8 @@ impl wallet_server::Wallet for WalletGrpcServer { .map_err(|e| Status::internal(e.to_string()))?; let message = format!("Sidechain state checkpoint for {}", contract_id); - let _ = transaction_service - .submit_transaction(tx_id, transaction, 0.into(), message) + transaction_service + .submit_transaction(tx_id, transaction, 10.into(), message) .await .map_err(|e| Status::internal(e.to_string()))?; diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index e11c551d7e..63d84f8deb 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -45,6 +45,7 @@ use tari_comms::{ }; use tari_core::transactions::{ tari_amount::{uT, MicroTari}, + transaction_components::OutputFeatures, weight::TransactionWeight, }; use tari_crypto::ristretto::RistrettoPublicKey; @@ -295,13 +296,18 @@ impl AppState { Err(_) => EmojiId::str_to_pubkey(public_key.as_str()).map_err(|_| UiError::PublicKeyParseError)?, }; + let output_features = OutputFeatures { + unique_id, + parent_public_key, + ..Default::default() + }; + let fee_per_gram = fee_per_gram * uT; let tx_service_handle = inner.wallet.transaction_service.clone(); tokio::spawn(send_transaction_task( public_key, MicroTari::from(amount), - unique_id, - parent_public_key, + output_features, message, fee_per_gram, tx_service_handle, @@ -327,13 +333,18 @@ impl AppState { Err(_) => EmojiId::str_to_pubkey(public_key.as_str()).map_err(|_| UiError::PublicKeyParseError)?, }; + let output_features = OutputFeatures { + unique_id, + parent_public_key, + ..Default::default() + }; + let fee_per_gram = fee_per_gram * uT; let tx_service_handle = inner.wallet.transaction_service.clone(); tokio::spawn(send_one_sided_transaction_task( public_key, MicroTari::from(amount), - unique_id, - parent_public_key, + output_features, message, fee_per_gram, tx_service_handle, diff --git a/applications/tari_console_wallet/src/ui/state/tasks.rs b/applications/tari_console_wallet/src/ui/state/tasks.rs index 8dce160d5e..0a69ca97f5 100644 --- a/applications/tari_console_wallet/src/ui/state/tasks.rs +++ b/applications/tari_console_wallet/src/ui/state/tasks.rs @@ -20,9 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use tari_common_types::types::PublicKey; use tari_comms::types::CommsPublicKey; -use tari_core::transactions::tari_amount::MicroTari; +use tari_core::transactions::{tari_amount::MicroTari, transaction_components::OutputFeatures}; use tari_wallet::transaction_service::handle::{TransactionEvent, TransactionSendStatus, TransactionServiceHandle}; use tokio::sync::{broadcast, watch}; @@ -33,8 +32,7 @@ const LOG_TARGET: &str = "wallet::console_wallet::tasks "; pub async fn send_transaction_task( public_key: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + output_features: OutputFeatures, message: String, fee_per_gram: MicroTari, mut transaction_service_handle: TransactionServiceHandle, @@ -44,7 +42,7 @@ pub async fn send_transaction_task( let mut event_stream = transaction_service_handle.get_event_stream(); let mut send_status = TransactionSendStatus::default(); match transaction_service_handle - .send_transaction_or_token(public_key, amount, unique_id, parent_public_key, fee_per_gram, message) + .send_transaction(public_key, amount, output_features, fee_per_gram, message) .await { Err(e) => { @@ -102,8 +100,7 @@ pub async fn send_transaction_task( pub async fn send_one_sided_transaction_task( public_key: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + output_features: OutputFeatures, message: String, fee_per_gram: MicroTari, mut transaction_service_handle: TransactionServiceHandle, @@ -112,7 +109,7 @@ pub async fn send_one_sided_transaction_task( let _result = result_tx.send(UiTransactionSendStatus::Initiated); let mut event_stream = transaction_service_handle.get_event_stream(); match transaction_service_handle - .send_one_sided_transaction_or_token(public_key, amount, unique_id, parent_public_key, fee_per_gram, message) + .send_one_sided_transaction(public_key, amount, output_features, fee_per_gram, message) .await { Err(e) => { diff --git a/applications/tari_validator_node/src/grpc/services/wallet_client.rs b/applications/tari_validator_node/src/grpc/services/wallet_client.rs index dac42263bb..208fdc7077 100644 --- a/applications/tari_validator_node/src/grpc/services/wallet_client.rs +++ b/applications/tari_validator_node/src/grpc/services/wallet_client.rs @@ -27,6 +27,7 @@ use tari_app_grpc::{ tari_rpc as grpc, tari_rpc::{ CreateFollowOnAssetCheckpointRequest, + CreateInitialAssetCheckpointRequest, SubmitContractAcceptanceRequest, SubmitContractUpdateProposalAcceptanceRequest, }, @@ -66,18 +67,32 @@ impl WalletClient for GrpcWalletClient { &mut self, contract_id: &FixedHash, state_root: &StateRoot, + is_initial: bool, ) -> Result<(), DigitalAssetError> { let inner = self.connection().await?; - let request = CreateFollowOnAssetCheckpointRequest { - contract_id: contract_id.to_vec(), - merkle_root: state_root.as_bytes().to_vec(), - }; - - let _res = inner - .create_follow_on_asset_checkpoint(request) - .await - .map_err(|e| DigitalAssetError::FatalError(format!("Could not create checkpoint:{}", e)))?; + if is_initial { + let request = CreateInitialAssetCheckpointRequest { + contract_id: contract_id.to_vec(), + merkle_root: state_root.as_bytes().to_vec(), + committee: vec![], + }; + + let _res = inner + .create_initial_asset_checkpoint(request) + .await + .map_err(|e| DigitalAssetError::FatalError(format!("Could not create checkpoint:{}", e)))?; + } else { + let request = CreateFollowOnAssetCheckpointRequest { + contract_id: contract_id.to_vec(), + merkle_root: state_root.as_bytes().to_vec(), + }; + + let _res = inner + .create_follow_on_asset_checkpoint(request) + .await + .map_err(|e| DigitalAssetError::FatalError(format!("Could not create checkpoint:{}", e)))?; + } Ok(()) } diff --git a/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs b/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs index 3d4758a932..52fdd5cdd4 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs @@ -23,7 +23,7 @@ use std::{ collections::{hash_map::DefaultHasher, HashSet}, convert::{TryFrom, TryInto}, - fmt::Debug, + fmt::{Debug, Display, Formatter}, hash::{BuildHasherDefault, Hash}, ops::Deref, }; @@ -215,6 +215,7 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> { output_hash: FixedHash, ) -> Result<(), ChainStorageError> { let contract_key = ContractIndexKey::new(contract_id, output_type); + debug!(target: LOG_TARGET, "Adding contract key {} to index", contract_key,); let block_key = BlockContractIndexKey::new(block_hash, output_type, contract_id); match output_type { OutputType::ContractDefinition => { @@ -267,6 +268,7 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> { ) -> Result<(), ChainStorageError> { let contract_key = ContractIndexKey::new(contract_id, output_type); + debug!(target: LOG_TARGET, "Removing contract key {} from index", contract_key,); match output_type { OutputType::ContractDefinition => { if self.has_dependent_outputs(&contract_key)? { @@ -488,6 +490,22 @@ impl ContractIndexKey { key.key[FixedHash::byte_size() + 1] = output_type.as_byte(); key } + + pub fn key_type(&self) -> KeyType { + match self.key[0] { + 0 => KeyType::PerContract, + 1 => KeyType::PerBlock, + _ => unreachable!(), + } + } + + pub fn contract_id(&self) -> FixedHash { + FixedHash::try_from(&self.key[1..=32]).expect("32 bytes cannot fail") + } + + pub fn output_type(&self) -> OutputType { + OutputType::from_byte(self.key[33]).expect("Contract key set with invalid OutputType") + } } impl Deref for ContractIndexKey { @@ -504,6 +522,18 @@ impl AsLmdbBytes for ContractIndexKey { } } +impl Display for ContractIndexKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:?}, {}, {}", + self.key_type(), + self.contract_id(), + self.output_type() + ) + } +} + #[cfg(test)] mod tests { use digest::Digest; diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index 70e0de0c2f..2a0bd8e876 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -762,6 +762,7 @@ impl LMDBDatabase { } if input.features()?.is_sidechain_contract() { + // TODO: 0-conf not supported for contract outputs self.get_contract_index(txn).spend(input)?; } diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index fcaa457301..75d02a2d08 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -216,6 +216,7 @@ impl TryFrom for proto::types::TransactionInput { .map_err(|_| "Non-compact Transaction input should contain sender_offset_public_key".to_string())? .as_bytes() .to_vec(), + // Output hash is only used in compact form output_hash: Vec::new(), covenant: input .covenant() diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs b/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs index 3cbbf0c4f2..96c6365c47 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs @@ -45,6 +45,13 @@ impl CommitteeSignatures { Self { signatures } } + pub fn empty() -> Self { + Self { + // Panic: vec is size 0 < 512 + signatures: vec![].try_into().unwrap(), + } + } + pub fn signatures(&self) -> Vec { self.signatures.to_vec() } diff --git a/base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs b/base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs index 3e8eb93d7d..865af8f821 100644 --- a/base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs +++ b/base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs @@ -183,6 +183,10 @@ impl UnblindedOutputBuilder { self.script_private_key = Some(script_private_key); self } + + pub fn covenant(&self) -> &Covenant { + &self.covenant + } } #[cfg(test)] diff --git a/base_layer/wallet/src/assets/asset_manager.rs b/base_layer/wallet/src/assets/asset_manager.rs index ef635d2753..6bdbfdb4be 100644 --- a/base_layer/wallet/src/assets/asset_manager.rs +++ b/base_layer/wallet/src/assets/asset_manager.rs @@ -46,6 +46,7 @@ use crate::{ database::{OutputManagerBackend, OutputManagerDatabase}, models::DbUnblindedOutput, }, + UtxoSelectionCriteria, }, }; @@ -122,7 +123,7 @@ impl AssetManager { debug!(target: LOG_TARGET, "Created output: {:?}", output); let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) } @@ -153,7 +154,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(outputs, ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(outputs, ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) } @@ -166,7 +167,7 @@ impl AssetManager { let output = self .output_manager .create_output_with_features( - 0.into(), + 10.into(), OutputFeatures::for_checkpoint( contract_id, merkle_root, @@ -203,15 +204,13 @@ impl AssetManager { // TODO: Fee is proportional to tx weight, so does not need to be different for contract // transactions - should be chosen by the user ASSET_FPG.into(), - // TODO: Spend previous checkpoint - None, - None, + UtxoSelectionCriteria::default(), ) .await?; Ok((tx_id, transaction)) } - pub async fn create_follow_on_asset_checkpoint( + pub async fn create_follow_on_contract_checkpoint( &mut self, contract_id: FixedHash, merkle_root: FixedHash, @@ -219,7 +218,7 @@ impl AssetManager { let output = self .output_manager .create_output_with_features( - 0.into(), + 10.into(), OutputFeatures::for_checkpoint( contract_id, merkle_root, @@ -254,15 +253,13 @@ impl AssetManager { .create_send_to_self_with_output( vec![output], ASSET_FPG.into(), - // TODO: Spend previous checkpoint - None, - None, + UtxoSelectionCriteria::for_contract(contract_id, OutputType::ContractCheckpoint), ) .await?; Ok((tx_id, transaction)) } - pub async fn create_constitution_definition( + pub async fn create_contract_constitution( &mut self, constitution_definition: &SideChainFeatures, ) -> Result<(TxId, Transaction), WalletError> { @@ -277,7 +274,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) @@ -294,7 +291,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) @@ -316,7 +313,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) @@ -344,7 +341,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) @@ -365,7 +362,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) @@ -383,7 +380,7 @@ impl AssetManager { let (tx_id, transaction) = self .output_manager - .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), UtxoSelectionCriteria::default()) .await?; Ok((tx_id, transaction)) diff --git a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs index 9f753f240c..eec7235f2b 100644 --- a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs +++ b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs @@ -152,7 +152,7 @@ impl AssetManagerService { } => { let (tx_id, transaction) = self .manager - .create_follow_on_asset_checkpoint(contract_id, merkle_root) + .create_follow_on_contract_checkpoint(contract_id, merkle_root) .await?; Ok(AssetManagerResponse::CreateFollowOnCheckpoint { transaction: Box::new(transaction), @@ -164,7 +164,7 @@ impl AssetManagerService { } => { let (tx_id, transaction) = self .manager - .create_constitution_definition(&constitution_definition) + .create_contract_constitution(&constitution_definition) .await?; Ok(AssetManagerResponse::CreateConstitutionDefinition { transaction: Box::new(transaction), diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 0cb6ab98cf..adfee9bc0b 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -39,6 +39,7 @@ use crate::{ base_node_service::error::BaseNodeServiceError, error::WalletStorageError, key_manager_service::KeyManagerServiceError, + output_manager_service::UtxoSelectionCriteria, }; #[derive(Debug, Error)] @@ -115,8 +116,8 @@ pub enum OutputManagerError { MasterSeedMismatch, #[error("Private Key is not found in the current Key Chain")] KeyNotFoundInKeyChain, - #[error("Token with unique id not found")] - TokenUniqueIdNotFound, + #[error("No UTXOs selected as inputs for {criteria}")] + NoUtxosSelected { criteria: UtxoSelectionCriteria }, #[error("Connectivity error: {source}")] ConnectivityError { #[from] diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index a49209c517..325592cf56 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -57,6 +57,7 @@ use crate::output_manager_service::{ database::OutputBackendQuery, models::{KnownOneSidedPaymentScript, SpendingPriority}, }, + UtxoSelectionCriteria, }; /// API Request enum @@ -78,8 +79,8 @@ pub enum OutputManagerRequest { PrepareToSendTransaction { tx_id: TxId, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + utxo_selection: UtxoSelectionCriteria, + output_features: OutputFeatures, fee_per_gram: MicroTari, lock_height: Option, message: String, @@ -89,8 +90,8 @@ pub enum OutputManagerRequest { CreatePayToSelfTransaction { tx_id: TxId, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + utxo_selection: UtxoSelectionCriteria, + output_features: OutputFeatures, fee_per_gram: MicroTari, lock_height: Option, message: String, @@ -98,8 +99,7 @@ pub enum OutputManagerRequest { CreatePayToSelfWithOutputs { outputs: Vec, fee_per_gram: MicroTari, - spending_unique_id: Option>, - spending_parent_public_key: Option, + input_selection: UtxoSelectionCriteria, }, CancelTransaction(TxId), GetSpentOutputs, @@ -518,8 +518,8 @@ impl OutputManagerHandle { &mut self, tx_id: TxId, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + utxo_selection: UtxoSelectionCriteria, + output_features: OutputFeatures, fee_per_gram: MicroTari, lock_height: Option, message: String, @@ -531,8 +531,8 @@ impl OutputManagerHandle { .call(OutputManagerRequest::PrepareToSendTransaction { tx_id, amount, - unique_id, - parent_public_key, + utxo_selection, + output_features, fee_per_gram, lock_height, message, @@ -759,16 +759,14 @@ impl OutputManagerHandle { &mut self, outputs: Vec, fee_per_gram: MicroTari, - spending_unique_id: Option>, - spending_parent_public_key: Option, + input_selection: UtxoSelectionCriteria, ) -> Result<(TxId, Transaction), OutputManagerError> { match self .handle .call(OutputManagerRequest::CreatePayToSelfWithOutputs { outputs, fee_per_gram, - spending_unique_id, - spending_parent_public_key, + input_selection, }) .await?? { @@ -781,8 +779,8 @@ impl OutputManagerHandle { &mut self, tx_id: TxId, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + utxo_selection: UtxoSelectionCriteria, + output_features: OutputFeatures, fee_per_gram: MicroTari, lock_height: Option, message: String, @@ -792,11 +790,11 @@ impl OutputManagerHandle { .call(OutputManagerRequest::CreatePayToSelfTransaction { tx_id, amount, + utxo_selection, + output_features, fee_per_gram, lock_height, message, - unique_id, - parent_public_key, }) .await?? { diff --git a/base_layer/wallet/src/output_manager_service/input_selection.rs b/base_layer/wallet/src/output_manager_service/input_selection.rs index 026ebe5616..aa24e5bd9d 100644 --- a/base_layer/wallet/src/output_manager_service/input_selection.rs +++ b/base_layer/wallet/src/output_manager_service/input_selection.rs @@ -25,7 +25,8 @@ use std::{ fmt::{Display, Formatter}, }; -use tari_common_types::types::PublicKey; +use tari_common_types::types::{Commitment, FixedHash, PublicKey}; +use tari_core::transactions::transaction_components::OutputType; use crate::output_manager_service::storage::models::DbUnblindedOutput; @@ -33,13 +34,23 @@ use crate::output_manager_service::storage::models::DbUnblindedOutput; pub struct UtxoSelectionCriteria { pub filter: UtxoSelectionFilter, pub ordering: UtxoSelectionOrdering, + pub excluding: Vec, } impl UtxoSelectionCriteria { + pub fn smallest_first() -> Self { + Self { + filter: UtxoSelectionFilter::Standard, + ordering: UtxoSelectionOrdering::SmallestFirst, + ..Default::default() + } + } + pub fn largest_first() -> Self { Self { filter: UtxoSelectionFilter::Standard, ordering: UtxoSelectionOrdering::LargestFirst, + ..Default::default() } } @@ -49,7 +60,17 @@ impl UtxoSelectionCriteria { unique_id, parent_public_key, }, - ordering: UtxoSelectionOrdering::Default, + ..Default::default() + } + } + + pub fn for_contract(contract_id: FixedHash, output_type: OutputType) -> Self { + Self { + filter: UtxoSelectionFilter::ContractOutput { + contract_id, + output_type, + }, + ..Default::default() } } } @@ -99,9 +120,25 @@ pub enum UtxoSelectionFilter { unique_id: Vec, parent_public_key: Option, }, + /// Select matching contract outputs. Additional Standard outputs may be included if necessary. + ContractOutput { + /// Contract ID to select + contract_id: FixedHash, + /// Type of contract output to select. + output_type: OutputType, + }, /// Selects specific outputs. All outputs must be exist and be spendable. SpecificOutputs { outputs: Vec }, } +impl UtxoSelectionFilter { + pub fn is_standard(&self) -> bool { + matches!(self, UtxoSelectionFilter::Standard) + } + + pub fn is_contract_output(&self) -> bool { + matches!(self, UtxoSelectionFilter::ContractOutput { .. }) + } +} impl Default for UtxoSelectionFilter { fn default() -> Self { @@ -121,6 +158,9 @@ impl Display for UtxoSelectionFilter { UtxoSelectionFilter::SpecificOutputs { outputs } => { write!(f, "Specific({} output(s))", outputs.len()) }, + UtxoSelectionFilter::ContractOutput { contract_id, .. } => { + write!(f, "ContractOutput({})", contract_id) + }, } } } diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index d95780cb8f..fc86a236d2 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -283,8 +283,8 @@ where OutputManagerRequest::PrepareToSendTransaction { tx_id, amount, - unique_id, - parent_public_key, + utxo_selection, + output_features, fee_per_gram, lock_height, message, @@ -294,11 +294,11 @@ where .prepare_transaction_to_send( tx_id, amount, - unique_id, - parent_public_key, + utxo_selection, fee_per_gram, lock_height, message, + output_features, script, covenant, ) @@ -307,8 +307,8 @@ where OutputManagerRequest::CreatePayToSelfTransaction { tx_id, amount, - unique_id, - parent_public_key, + utxo_selection, + output_features, fee_per_gram, lock_height, message, @@ -316,8 +316,8 @@ where .create_pay_to_self_transaction( tx_id, amount, - unique_id, - parent_public_key, + utxo_selection, + output_features, fee_per_gram, lock_height, message, @@ -414,16 +414,10 @@ where OutputManagerRequest::CreatePayToSelfWithOutputs { outputs, fee_per_gram, - spending_unique_id, - spending_parent_public_key, + input_selection, } => { let (tx_id, transaction) = self - .create_pay_to_self_containing_outputs( - outputs, - fee_per_gram, - spending_unique_id.as_ref(), - spending_parent_public_key.as_ref(), - ) + .create_pay_to_self_containing_outputs(outputs, fee_per_gram, input_selection) .await?; Ok(OutputManagerResponse::CreatePayToSelfWithOutputs { transaction: Box::new(transaction), @@ -844,56 +838,35 @@ where &mut self, tx_id: TxId, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + utxo_selection: UtxoSelectionCriteria, fee_per_gram: MicroTari, lock_height: Option, message: String, + recipient_output_features: OutputFeatures, recipient_script: TariScript, recipient_covenant: Covenant, ) -> Result { debug!( target: LOG_TARGET, - "Preparing to send transaction. Amount: {}. Unique id : {:?}. Fee per gram: {}. ", + "Preparing to send transaction. Amount: {}. UTXO Selection: {}. Fee per gram: {}. ", amount, - unique_id, + utxo_selection, fee_per_gram, ); - let output_features_estimate = OutputFeatures::default(); let metadata_byte_size = self .resources .consensus_constants .transaction_weight() .round_up_metadata_size( - output_features_estimate.consensus_encode_exact_size() + + recipient_output_features.consensus_encode_exact_size() + recipient_script.consensus_encode_exact_size() + recipient_covenant.consensus_encode_exact_size(), ); - // TODO: Some(unique_id) means select the unique_id AND use the features of UTXOs with the unique_id. These - // should be able to be specified independently. - let selection_criteria = match unique_id.as_ref() { - Some(unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), parent_public_key.as_ref().cloned()), - None => UtxoSelectionCriteria::default(), - }; - let input_selection = self - .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, selection_criteria) + .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, utxo_selection) .await?; - // TODO: improve this logic #LOGGED - let recipient_output_features = match unique_id { - Some(ref _unique_id) => match input_selection - .utxos - .iter() - .find(|output| output.unblinded_output.features.unique_id.is_some()) - { - Some(output) => output.unblinded_output.features.clone(), - _ => OutputFeatures::default(), - }, - _ => OutputFeatures::default(), - }; - let offset = PrivateKey::random(&mut OsRng); let nonce = PrivateKey::random(&mut OsRng); @@ -1065,8 +1038,7 @@ where &mut self, outputs: Vec, fee_per_gram: MicroTari, - spending_unique_id: Option<&Vec>, - spending_parent_public_key: Option<&PublicKey>, + selection_criteria: UtxoSelectionCriteria, ) -> Result<(TxId, Transaction), OutputManagerError> { let total_value = MicroTari(outputs.iter().fold(0u64, |running, out| running + out.value.as_u64())); let nop_script = script![Nop]; @@ -1075,6 +1047,7 @@ where total + weighting.round_up_metadata_size({ output.features.consensus_encode_exact_size() + + output.covenant().consensus_encode_exact_size() + output .script .as_ref() @@ -1083,11 +1056,6 @@ where }) }); - let selection_criteria = match spending_unique_id { - Some(unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), spending_parent_public_key.cloned()), - None => UtxoSelectionCriteria::default(), - }; - let input_selection = self .select_utxos( total_value, @@ -1229,35 +1197,26 @@ where &mut self, tx_id: TxId, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + utxo_selection: UtxoSelectionCriteria, + mut output_features: OutputFeatures, fee_per_gram: MicroTari, lock_height: Option, message: String, ) -> Result<(MicroTari, Transaction), OutputManagerError> { let script = script!(Nop); let covenant = Covenant::default(); - let output_features_estimate = OutputFeatures { - unique_id: unique_id.clone(), - ..Default::default() - }; let metadata_byte_size = self .resources .consensus_constants .transaction_weight() .round_up_metadata_size( - output_features_estimate.consensus_encode_exact_size() + + output_features.consensus_encode_exact_size() + script.consensus_encode_exact_size() + covenant.consensus_encode_exact_size(), ); - let selection_criteria = match unique_id { - Some(ref unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), parent_public_key), - None => UtxoSelectionCriteria::default(), - }; - let input_selection = self - .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, selection_criteria) + .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, utxo_selection) .await?; let offset = PrivateKey::random(&mut OsRng); @@ -1286,11 +1245,7 @@ where let (spending_key, script_private_key) = self.get_spend_and_script_keys().await?; let recovery_byte = self.calculate_recovery_byte(spending_key.clone(), amount.as_u64(), true)?; - let output_features = OutputFeatures { - recovery_byte, - unique_id: unique_id.clone(), - ..Default::default() - }; + output_features.set_recovery_byte(recovery_byte); let encrypted_value = EncryptedValue::todo_encrypt_from(amount); let metadata_signature = TransactionOutput::create_final_metadata_signature( TransactionOutputVersion::get_current_version(), @@ -1412,6 +1367,7 @@ where /// Select which unspent transaction outputs to use to send a transaction of the specified amount. Use the specified /// selection strategy to choose the outputs. It also determines if a change output is required. + #[allow(clippy::too_many_lines)] async fn select_utxos( &mut self, amount: MicroTari, @@ -1432,9 +1388,6 @@ where ); let mut utxos = Vec::new(); - let mut utxos_total_value = MicroTari::from(0); - let mut fee_without_change = MicroTari::from(0); - let mut fee_with_change = MicroTari::from(0); let fee_calc = self.get_fee_calc(); // Attempt to get the chain tip height @@ -1445,20 +1398,59 @@ where "select_utxos selection criteria: {}", selection_criteria ); let tip_height = chain_metadata.as_ref().map(|m| m.height_of_longest_chain()); - let uo = self + let mut uo = self .resources .db - .fetch_unspent_outputs_for_spending(selection_criteria, amount, tip_height)?; - trace!(target: LOG_TARGET, "We found {} UTXOs to select from", uo.len()); + .fetch_unspent_outputs_for_spending(&selection_criteria, amount, tip_height)?; + + // For non-standard queries, we want to ensure that the intended UTXOs are selected + if !selection_criteria.filter.is_standard() && uo.is_empty() { + return Err(OutputManagerError::NoUtxosSelected { + criteria: selection_criteria, + }); + } // Assumes that default Outputfeatures are used for change utxo let output_features_estimate = OutputFeatures::default(); let default_metadata_size = fee_calc.weighting().round_up_metadata_size( - output_features_estimate.consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size(), + output_features_estimate.consensus_encode_exact_size() + + Covenant::new().consensus_encode_exact_size() + + script![Nop].consensus_encode_exact_size(), ); + + if selection_criteria.filter.is_contract_output() { + let fee_with_change = fee_calc.calculate( + fee_per_gram, + 1, + uo.len(), + num_outputs + 1, + output_metadata_byte_size + default_metadata_size, + ); + + // If the initial selection was not able to select enough UTXOs, fill in the difference with standard UTXOs + let total_utxo_value = uo.iter().map(|uo| uo.unblinded_output.value).sum::(); + if total_utxo_value < amount + fee_with_change { + let mut query = UtxoSelectionCriteria::smallest_first(); + query.excluding = uo.iter().map(|o| o.commitment.clone()).collect(); + let additional = self.resources.db.fetch_unspent_outputs_for_spending( + &query, + amount + fee_with_change - total_utxo_value, + tip_height, + )?; + uo.extend(additional); + } + } + + trace!(target: LOG_TARGET, "We found {} UTXOs to select from", uo.len()); + let mut requires_change_output = false; + let mut utxos_total_value = MicroTari::from(0); + let mut fee_without_change = MicroTari::from(0); + let mut fee_with_change = MicroTari::from(0); for o in uo { utxos_total_value += o.unblinded_output.value; + + error!(target: LOG_TARGET, "-- utxos_total_value = {:?}", utxos_total_value); utxos.push(o); // The assumption here is that the only output will be the payment output and change if required fee_without_change = @@ -1473,6 +1465,8 @@ where num_outputs + 1, output_metadata_byte_size + default_metadata_size, ); + + error!(target: LOG_TARGET, "-- amt+fee = {:?} {}", amount, fee_with_change); if utxos_total_value > amount + fee_with_change { requires_change_output = true; break; diff --git a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs index 0c925a6e01..70bd6e3ada 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs @@ -110,7 +110,7 @@ pub trait OutputManagerBackend: Send + Sync + Clone { fn add_unvalidated_output(&self, output: DbUnblindedOutput, tx_id: TxId) -> Result<(), OutputManagerStorageError>; fn fetch_unspent_outputs_for_spending( &self, - selection_criteria: UtxoSelectionCriteria, + selection_criteria: &UtxoSelectionCriteria, amount: u64, current_tip_height: Option, ) -> Result, OutputManagerStorageError>; diff --git a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs index a3586d204f..ed37283cd7 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs @@ -251,7 +251,7 @@ where T: OutputManagerBackend + 'static /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. pub fn fetch_unspent_outputs_for_spending( &self, - selection_criteria: UtxoSelectionCriteria, + selection_criteria: &UtxoSelectionCriteria, amount: MicroTari, tip_height: Option, ) -> Result, OutputManagerStorageError> { diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs index a90351314e..a74cd432c6 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs @@ -1192,7 +1192,7 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. fn fetch_unspent_outputs_for_spending( &self, - selection_criteria: UtxoSelectionCriteria, + selection_criteria: &UtxoSelectionCriteria, amount: u64, tip_height: Option, ) -> Result, OutputManagerStorageError> { diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index 47ad656505..046cbdf3d1 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -28,13 +28,13 @@ use diesel::{prelude::*, sql_query, SqliteConnection}; use log::*; use tari_common_types::{ transaction::TxId, - types::{ComSignature, Commitment, FixedHash, PrivateKey, PublicKey}, + types::{ComSignature, Commitment, PrivateKey, PublicKey}, }; use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction_components::{EncryptedValue, OutputFeatures, OutputType, SideChainFeatures, UnblindedOutput}, + transaction_components::{EncryptedValue, OutputFeatures, OutputType, UnblindedOutput}, CryptoFactories, }, }; @@ -183,7 +183,7 @@ impl OutputSql { /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. #[allow(clippy::cast_sign_loss)] pub fn fetch_unspent_outputs_for_spending( - selection_criteria: UtxoSelectionCriteria, + selection_criteria: &UtxoSelectionCriteria, amount: u64, tip_height: Option, conn: &SqliteConnection, @@ -193,7 +193,7 @@ impl OutputSql { .filter(outputs::status.eq(OutputStatus::Unspent as i32)) .order_by(outputs::spending_priority.desc()); - match selection_criteria.filter { + match &selection_criteria.filter { UtxoSelectionFilter::Standard => { query = query.filter( outputs::output_type @@ -209,11 +209,23 @@ impl OutputSql { .filter(outputs::features_unique_id.eq(unique_id)) .filter(outputs::features_parent_public_key.eq(parent_public_key.as_ref().map(|pk| pk.to_vec()))); }, + UtxoSelectionFilter::ContractOutput { + contract_id, + output_type, + } => { + query = query + .filter(outputs::contract_id.eq(contract_id.as_slice())) + .filter(outputs::output_type.eq(i32::from(output_type.as_byte()))); + }, UtxoSelectionFilter::SpecificOutputs { outputs } => { - query = query.filter(outputs::hash.eq_any(outputs.into_iter().map(|o| o.hash))) + query = query.filter(outputs::hash.eq_any(outputs.iter().map(|o| &o.hash))) }, } + for exclude in &selection_criteria.excluding { + query = query.filter(outputs::commitment.ne(exclude.as_bytes())); + } + match selection_criteria.ordering { UtxoSelectionOrdering::SmallestFirst => { query = query.then_order_by(outputs::value.asc()); @@ -250,6 +262,7 @@ impl OutputSql { } }, }; + match tip_height { Some(tip_height) => { let i64_tip_height = i64::try_from(tip_height).unwrap_or(i64::MAX); @@ -263,6 +276,11 @@ impl OutputSql { query = query.then_order_by(outputs::maturity.asc()); }, } + // debug!( + // target: LOG_TARGET, + // "Executing UTXO select query: {}", + // diesel::debug_query(&query) + // ); Ok(query.load(conn)?) } @@ -594,38 +612,11 @@ impl TryFrom for DbUnblindedOutput { #[allow(clippy::too_many_lines)] fn try_from(o: OutputSql) -> Result { - let mut features: OutputFeatures = + let features: OutputFeatures = serde_json::from_str(&o.features_json).map_err(|s| OutputManagerStorageError::ConversionError { reason: format!("Could not convert json into OutputFeatures:{}", s), })?; - let output_type = o - .output_type - .try_into() - .map_err(|_| OutputManagerStorageError::ConversionError { - reason: format!("Unable to convert flag bits with value {} to OutputType", o.output_type), - })?; - features.output_type = - OutputType::from_byte(output_type).ok_or(OutputManagerStorageError::ConversionError { - reason: "Flags could not be converted from bits".to_string(), - })?; - features.maturity = o.maturity as u64; - features.metadata = o.metadata.unwrap_or_default(); - features.sidechain_features = o - .features_unique_id - .as_ref() - .map(|v| FixedHash::try_from(v.as_slice())) - .transpose() - .map_err(|_| OutputManagerStorageError::ConversionError { - reason: "Invalid contract ID".to_string(), - })? - // TODO: Add side chain features to wallet db - .map(SideChainFeatures::new); - features.parent_public_key = o - .features_parent_public_key - .map(|p| PublicKey::from_bytes(&p)) - .transpose()?; - features.recovery_byte = u8::try_from(o.recovery_byte).unwrap(); let encrypted_value = EncryptedValue::from_bytes(&o.encrypted_value)?; let unblinded_output = UnblindedOutput::new_current_version( MicroTari::from(o.value as u64), diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 141f094c85..71cd6ebab0 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -38,7 +38,7 @@ use tari_core::{ proto, transactions::{ tari_amount::MicroTari, - transaction_components::{Transaction, TransactionOutput}, + transaction_components::{OutputFeatures, Transaction, TransactionOutput}, }, }; use tari_service_framework::reply_channel::SenderService; @@ -75,16 +75,14 @@ pub enum TransactionServiceRequest { SendTransaction { dest_pubkey: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + output_features: OutputFeatures, fee_per_gram: MicroTari, message: String, }, SendOneSidedTransaction { dest_pubkey: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + output_features: OutputFeatures, fee_per_gram: MicroTari, message: String, }, @@ -398,6 +396,7 @@ impl TransactionServiceHandle { &mut self, dest_pubkey: CommsPublicKey, amount: MicroTari, + output_features: OutputFeatures, fee_per_gram: MicroTari, message: String, ) -> Result { @@ -406,34 +405,7 @@ impl TransactionServiceHandle { .call(TransactionServiceRequest::SendTransaction { dest_pubkey, amount, - unique_id: None, - parent_public_key: None, - fee_per_gram, - message, - }) - .await?? - { - TransactionServiceResponse::TransactionSent(tx_id) => Ok(tx_id), - _ => Err(TransactionServiceError::UnexpectedApiResponse), - } - } - - pub async fn send_transaction_or_token( - &mut self, - dest_pubkey: CommsPublicKey, - amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, - fee_per_gram: MicroTari, - message: String, - ) -> Result { - match self - .handle - .call(TransactionServiceRequest::SendTransaction { - dest_pubkey, - amount, - unique_id, - parent_public_key, + output_features, fee_per_gram, message, }) @@ -448,6 +420,7 @@ impl TransactionServiceHandle { &mut self, dest_pubkey: CommsPublicKey, amount: MicroTari, + output_features: OutputFeatures, fee_per_gram: MicroTari, message: String, ) -> Result { @@ -456,34 +429,7 @@ impl TransactionServiceHandle { .call(TransactionServiceRequest::SendOneSidedTransaction { dest_pubkey, amount, - unique_id: None, - parent_public_key: None, - fee_per_gram, - message, - }) - .await?? - { - TransactionServiceResponse::TransactionSent(tx_id) => Ok(tx_id), - _ => Err(TransactionServiceError::UnexpectedApiResponse), - } - } - - pub async fn send_one_sided_transaction_or_token( - &mut self, - dest_pubkey: CommsPublicKey, - amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, - fee_per_gram: MicroTari, - message: String, - ) -> Result { - match self - .handle - .call(TransactionServiceRequest::SendOneSidedTransaction { - dest_pubkey, - amount, - unique_id, - parent_public_key, + output_features, fee_per_gram, message, }) diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 35badf317a..556df671ad 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -27,7 +27,7 @@ use futures::FutureExt; use log::*; use tari_common_types::{ transaction::{TransactionDirection, TransactionStatus, TxId}, - types::{HashOutput, PublicKey}, + types::HashOutput, }; use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; use tari_comms_dht::{ @@ -38,7 +38,7 @@ use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction_components::KernelFeatures, + transaction_components::{KernelFeatures, OutputFeatures}, transaction_protocol::{ proto::protocol as proto, recipient::RecipientSignedMessage, @@ -56,6 +56,7 @@ use tokio::{ use crate::{ connectivity_service::WalletConnectivityInterface, + output_manager_service::UtxoSelectionCriteria, transaction_service::{ config::TransactionRoutingMechanism, error::{TransactionServiceError, TransactionServiceProtocolError}, @@ -87,8 +88,6 @@ pub struct TransactionSendProtocol { id: TxId, dest_pubkey: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, fee_per_gram: MicroTari, message: String, service_request_reply_channel: Option>>, @@ -113,8 +112,6 @@ where cancellation_receiver: oneshot::Receiver<()>, dest_pubkey: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, fee_per_gram: MicroTari, message: String, service_request_reply_channel: Option< @@ -132,8 +129,6 @@ where cancellation_receiver: Some(cancellation_receiver), dest_pubkey, amount, - unique_id, - parent_public_key, fee_per_gram, message, service_request_reply_channel, @@ -223,8 +218,8 @@ where .prepare_transaction_to_send( self.id, self.amount, - self.unique_id.clone(), - self.parent_public_key.clone(), + UtxoSelectionCriteria::default(), + OutputFeatures::default(), self.fee_per_gram, None, self.message.clone(), diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 53ed6bf64f..cd1d6954ce 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -45,7 +45,14 @@ use tari_core::{ proto::base_node as base_node_proto, transactions::{ tari_amount::MicroTari, - transaction_components::{EncryptedValue, KernelFeatures, Transaction, TransactionOutput, UnblindedOutput}, + transaction_components::{ + EncryptedValue, + KernelFeatures, + OutputFeatures, + Transaction, + TransactionOutput, + UnblindedOutput, + }, transaction_protocol::{ proto::protocol as proto, recipient::RecipientSignedMessage, @@ -75,6 +82,7 @@ use crate::{ output_manager_service::{ handle::{OutputManagerEvent, OutputManagerHandle}, storage::models::SpendingPriority, + UtxoSelectionCriteria, }, storage::database::{WalletBackend, WalletDatabase}, transaction_service::{ @@ -562,8 +570,7 @@ where TransactionServiceRequest::SendTransaction { dest_pubkey, amount, - unique_id, - parent_public_key, + output_features, fee_per_gram, message, } => { @@ -571,8 +578,7 @@ where self.send_transaction( dest_pubkey, amount, - unique_id, - parent_public_key, + output_features, fee_per_gram, message, send_transaction_join_handles, @@ -585,16 +591,14 @@ where TransactionServiceRequest::SendOneSidedTransaction { dest_pubkey, amount, - unique_id, - parent_public_key, + output_features, fee_per_gram, message, } => self .send_one_sided_transaction( dest_pubkey, amount, - unique_id, - parent_public_key, + output_features, fee_per_gram, message, transaction_broadcast_join_handles, @@ -839,7 +843,7 @@ where } } - /// Sends a new transaction to a recipient + /// Sends a new transaction to a single recipient /// # Arguments /// 'dest_pubkey': The Comms pubkey of the recipient node /// 'amount': The amount of Tari to send to the recipient @@ -848,8 +852,7 @@ where &mut self, dest_pubkey: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + output_features: OutputFeatures, fee_per_gram: MicroTari, message: String, join_handles: &mut FuturesUnordered< @@ -874,8 +877,9 @@ where .create_pay_to_self_transaction( tx_id, amount, - unique_id.clone(), - parent_public_key.clone(), + // TODO: allow customization of selected inputs and outputs + UtxoSelectionCriteria::default(), + output_features, fee_per_gram, None, message.clone(), @@ -929,8 +933,6 @@ where cancellation_receiver, dest_pubkey, amount, - unique_id, - parent_public_key, fee_per_gram, message, Some(reply_channel), @@ -987,8 +989,8 @@ where .prepare_transaction_to_send( tx_id, amount, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, message.clone(), @@ -1129,8 +1131,7 @@ where &mut self, dest_pubkey: CommsPublicKey, amount: MicroTari, - unique_id: Option>, - parent_public_key: Option, + output_features: OutputFeatures, fee_per_gram: MicroTari, message: String, transaction_broadcast_join_handles: &mut FuturesUnordered< @@ -1152,8 +1153,8 @@ where .prepare_transaction_to_send( tx_id, amount, - unique_id.clone(), - parent_public_key.clone(), + UtxoSelectionCriteria::default(), + output_features, fee_per_gram, None, message.clone(), @@ -1569,8 +1570,6 @@ where cancellation_receiver, tx.destination_public_key, tx.amount, - None, - None, tx.fee, tx.message, None, diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index 87c4c9ec68..9a90cf01ce 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -24,7 +24,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use rand::{rngs::OsRng, Rng, RngCore}; use tari_common_types::{ transaction::TxId, - types::{ComSignature, PrivateKey, PublicKey}, + types::{ComSignature, FixedHash, PrivateKey, PublicKey}, }; use tari_comms::{ peer_manager::{NodeIdentity, PeerFeatures}, @@ -41,7 +41,14 @@ use tari_core::{ fee::Fee, tari_amount::{uT, MicroTari}, test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, - transaction_components::{EncryptedValue, OutputFeatures, OutputType, TransactionOutput, UnblindedOutput}, + transaction_components::{ + CommitteeSignatures, + EncryptedValue, + OutputFeatures, + OutputType, + TransactionOutput, + UnblindedOutput, + }, transaction_protocol::{sender::TransactionSenderMessage, RewindData}, weight::TransactionWeight, CryptoFactories, @@ -86,6 +93,7 @@ use tari_wallet::{ sqlite_db::OutputManagerSqliteDatabase, OutputStatus, }, + UtxoSelectionCriteria, }, test_utils::create_consensus_constants, transaction_service::handle::TransactionServiceHandle, @@ -437,8 +445,8 @@ async fn test_utxo_selection_no_chain_metadata() { .prepare_transaction_to_send( TxId::new_random(), amount, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -470,11 +478,11 @@ async fn test_utxo_selection_no_chain_metadata() { .prepare_transaction_to_send( TxId::new_random(), amount, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, - "".to_string(), + String::new(), script!(Nop), Covenant::default(), ) @@ -551,8 +559,8 @@ async fn test_utxo_selection_with_chain_metadata() { .prepare_transaction_to_send( TxId::new_random(), amount, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -613,8 +621,8 @@ async fn test_utxo_selection_with_chain_metadata() { .prepare_transaction_to_send( TxId::new_random(), amount, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -640,8 +648,8 @@ async fn test_utxo_selection_with_chain_metadata() { .prepare_transaction_to_send( TxId::new_random(), 6 * amount, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -713,8 +721,8 @@ async fn test_utxo_selection_with_tx_priority() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(1000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -732,6 +740,77 @@ async fn test_utxo_selection_with_tx_priority() { assert_ne!(utxos[0].features.output_type, OutputType::Coinbase); } +#[tokio::test] +async fn utxo_selection_for_contract_checkpoint() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let contract_id = FixedHash::hash_bytes(b"test_utxo_selection_for_contract_checkpoint"); + + let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); + // setup with chain metadata at a height of 6 + let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( + OutputManagerSqliteDatabase::new(connection, None), + Some(6), + server_node_identity, + ) + .await; + + let amount = MicroTari::from(2000); + let fee_per_gram = MicroTari::from(2); + + // we create two outputs, one as coinbase-high priority one as normal so we can track them + let (_, uo) = make_input_with_features( + &mut OsRng.clone(), + amount, + &factories.commitment, + Some(OutputFeatures::for_checkpoint( + contract_id, + FixedHash::zero(), + CommitteeSignatures::empty(), + )), + oms.clone(), + ) + .await; + oms.add_rewindable_output(uo, None, None).await.unwrap(); + let (_, uo) = make_input_with_features( + &mut OsRng.clone(), + amount, + &factories.commitment, + Some(OutputFeatures { + maturity: 1, + ..Default::default() + }), + oms.clone(), + ) + .await; + oms.add_rewindable_output(uo, None, None).await.unwrap(); + + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 2); + + // test transactions + let stp = oms + .prepare_transaction_to_send( + TxId::new_random(), + // Spend more than the selected contract output, this will cause the other UTXO to be included + MicroTari::from(2500), + UtxoSelectionCriteria::for_contract(contract_id, OutputType::ContractCheckpoint), + OutputFeatures::for_checkpoint(contract_id, FixedHash::zero(), CommitteeSignatures::empty()), + fee_per_gram, + None, + String::new(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); + assert!(stp.get_tx_id().is_ok()); + + // test that the utxo with the lowest priority was left + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 0); +} + #[tokio::test] async fn send_not_enough_funds() { let factories = CryptoFactories::default(); @@ -758,8 +837,8 @@ async fn send_not_enough_funds() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(num_outputs * 2000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(4), None, "".to_string(), @@ -817,8 +896,8 @@ async fn send_no_change() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(value1 + value2) - fee_without_change, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -838,6 +917,7 @@ async fn send_no_change() { MicroTari::from(0) ); } + #[tokio::test] async fn send_not_enough_for_change() { let (connection, _tempdir) = get_temp_sqlite_database_connection(); @@ -881,8 +961,8 @@ async fn send_not_enough_for_change() { .prepare_transaction_to_send( TxId::new_random(), value1 + value2 + uT - fee_without_change, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), fee_per_gram, None, "".to_string(), @@ -922,8 +1002,8 @@ async fn cancel_transaction() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(1000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(4), None, "".to_string(), @@ -1015,8 +1095,8 @@ async fn test_get_balance() { .prepare_transaction_to_send( TxId::new_random(), send_value, - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(4), None, "".to_string(), @@ -1071,8 +1151,8 @@ async fn sending_transaction_persisted_while_offline() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(1000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(4), None, "".to_string(), @@ -1102,8 +1182,8 @@ async fn sending_transaction_persisted_while_offline() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(1000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(4), None, "".to_string(), @@ -1409,8 +1489,8 @@ async fn test_txo_validation() { .prepare_transaction_to_send( 4u64.into(), MicroTari::from(900_000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(10), None, "".to_string(), diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index b69477231c..4b6edcb385 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -119,6 +119,7 @@ use tari_wallet::{ sqlite_db::OutputManagerSqliteDatabase, }, OutputManagerServiceInitializer, + UtxoSelectionCriteria, }, storage::{ database::WalletDatabase, @@ -546,6 +547,7 @@ fn manage_single_transaction() { .block_on(alice_ts.send_transaction( bob_node_identity.public_key().clone(), value, + OutputFeatures::default(), MicroTari::from(4), "".to_string() )) @@ -557,6 +559,7 @@ fn manage_single_transaction() { .block_on(alice_ts.send_transaction( bob_node_identity.public_key().clone(), value, + OutputFeatures::default(), MicroTari::from(4), message, )) @@ -681,6 +684,7 @@ fn single_transaction_to_self() { .send_transaction( alice_node_identity.public_key().clone(), value, + OutputFeatures::default(), 20.into(), message.clone(), ) @@ -772,6 +776,7 @@ fn send_one_sided_transaction_to_other() { .send_one_sided_transaction( bob_node_identity.public_key().clone(), value, + OutputFeatures::default(), 20.into(), message.clone(), ) @@ -911,6 +916,7 @@ fn recover_one_sided_transaction() { .send_one_sided_transaction( bob_node_identity.public_key().clone(), value, + OutputFeatures::default(), 20.into(), message.clone(), ) @@ -1137,6 +1143,7 @@ fn send_one_sided_transaction_to_self() { .send_one_sided_transaction( alice_node_identity.public_key().clone(), value, + OutputFeatures::default(), 20.into(), message.clone(), ) @@ -1275,6 +1282,7 @@ fn manage_multiple_transactions() { .block_on(alice_ts.send_transaction( bob_node_identity.public_key().clone(), value_a_to_b_1, + OutputFeatures::default(), MicroTari::from(20), "a to b 1".to_string(), )) @@ -1285,6 +1293,7 @@ fn manage_multiple_transactions() { .block_on(alice_ts.send_transaction( carol_node_identity.public_key().clone(), value_a_to_c_1, + OutputFeatures::default(), MicroTari::from(20), "a to c 1".to_string(), )) @@ -1297,6 +1306,7 @@ fn manage_multiple_transactions() { .block_on(bob_ts.send_transaction( alice_node_identity.public_key().clone(), value_b_to_a_1, + OutputFeatures::default(), MicroTari::from(20), "b to a 1".to_string(), )) @@ -1305,6 +1315,7 @@ fn manage_multiple_transactions() { .block_on(alice_ts.send_transaction( bob_node_identity.public_key().clone(), value_a_to_b_2, + OutputFeatures::default(), MicroTari::from(20), "a to b 2".to_string(), )) @@ -1436,6 +1447,7 @@ fn test_accepting_unknown_tx_id_and_malformed_reply() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), MicroTari::from(5000), + OutputFeatures::default(), MicroTari::from(20), "".to_string(), )) @@ -1543,8 +1555,8 @@ fn finalize_tx_with_incorrect_pubkey() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(5000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(25), None, "".to_string(), @@ -1664,8 +1676,8 @@ fn finalize_tx_with_missing_output() { .prepare_transaction_to_send( TxId::new_random(), MicroTari::from(5000), - None, - None, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), MicroTari::from(20), None, "".to_string(), @@ -1840,6 +1852,7 @@ fn discovery_async_return_test() { .block_on(alice_ts.send_transaction( bob_node_identity.public_key().clone(), value_a_to_c_1, + OutputFeatures::default(), MicroTari::from(20), "Discovery Tx!".to_string(), )) @@ -1876,6 +1889,7 @@ fn discovery_async_return_test() { .block_on(alice_ts.send_transaction( carol_node_identity.public_key().clone(), value_a_to_c_1, + OutputFeatures::default(), MicroTari::from(20), "Discovery Tx2!".to_string(), )) @@ -2150,6 +2164,7 @@ fn test_transaction_cancellation() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message".to_string(), )) @@ -2497,6 +2512,7 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message".to_string(), )) @@ -2679,6 +2695,7 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message".to_string(), )) @@ -2800,6 +2817,7 @@ fn test_tx_direct_send_behaviour() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message1".to_string(), )) @@ -2841,6 +2859,7 @@ fn test_tx_direct_send_behaviour() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message2".to_string(), )) @@ -2886,6 +2905,7 @@ fn test_tx_direct_send_behaviour() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message3".to_string(), )) @@ -2931,6 +2951,7 @@ fn test_tx_direct_send_behaviour() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message4".to_string(), )) @@ -4152,6 +4173,7 @@ fn test_transaction_resending() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message".to_string(), )) @@ -4653,6 +4675,7 @@ fn test_replying_to_cancelled_tx() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 100 * uT, "Testing Message".to_string(), )) @@ -4775,6 +4798,7 @@ fn test_transaction_timeout_cancellation() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent, + OutputFeatures::default(), 20 * uT, "Testing Message".to_string(), )) @@ -5027,6 +5051,7 @@ fn transaction_service_tx_broadcast() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent1, + OutputFeatures::default(), 100 * uT, "Testing Message".to_string(), )) @@ -5084,6 +5109,7 @@ fn transaction_service_tx_broadcast() { .block_on(alice_ts_interface.transaction_service_handle.send_transaction( bob_node_identity.public_key().clone(), amount_sent2, + OutputFeatures::default(), 20 * uT, "Testing Message2".to_string(), )) diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index ba6957d187..cadb77a9dc 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -293,6 +293,7 @@ async fn test_wallet() { .send_transaction( bob_identity.public_key().clone(), value, + OutputFeatures::default(), MicroTari::from(5), "".to_string(), ) @@ -609,6 +610,7 @@ async fn test_store_and_forward_send_tx() { .send_transaction( carol_identity.public_key().clone(), value, + OutputFeatures::default(), MicroTari::from(3), "Store and Forward!".to_string(), ) diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 80fea4d457..a3b45d894f 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -102,6 +102,7 @@ use tari_core::transactions::{ AssetOutputFeatures, CommitteeDefinitionFeatures, MintNonFungibleFeatures, + OutputFeatures, OutputFeaturesVersion, OutputType, SideChainCheckpointFeatures, @@ -4893,6 +4894,7 @@ pub unsafe extern "C" fn wallet_send_transaction( .block_on((*wallet).wallet.transaction_service.send_one_sided_transaction( (*dest_public_key).clone(), MicroTari::from(amount), + OutputFeatures::default(), MicroTari::from(fee_per_gram), message_string, )) { @@ -4909,6 +4911,7 @@ pub unsafe extern "C" fn wallet_send_transaction( .block_on((*wallet).wallet.transaction_service.send_transaction( (*dest_public_key).clone(), MicroTari::from(amount), + OutputFeatures::default(), MicroTari::from(fee_per_gram), message_string, )) { diff --git a/dan_layer/core/src/models/base_layer_output.rs b/dan_layer/core/src/models/base_layer_output.rs index 35a4de8bdb..3bae121e89 100644 --- a/dan_layer/core/src/models/base_layer_output.rs +++ b/dan_layer/core/src/models/base_layer_output.rs @@ -42,7 +42,11 @@ impl BaseLayerOutput { } pub fn get_checkpoint_merkle_root(&self) -> Option { - self.features.sidechain_checkpoint.as_ref().map(|cp| cp.merkle_root) + self.features + .sidechain_features + .as_ref() + .and_then(|cp| cp.checkpoint.as_ref()) + .map(|cp| cp.merkle_root) } pub fn get_parent_public_key(&self) -> Option<&PublicKey> { @@ -65,7 +69,7 @@ impl TryFrom for CheckpointOutput { type Error = ModelError; fn try_from(output: BaseLayerOutput) -> Result { - if output.features.output_type != OutputType::SidechainCheckpoint { + if output.features.output_type != OutputType::ContractCheckpoint { return Err(ModelError::NotCheckpointOutput); } diff --git a/dan_layer/core/src/services/checkpoint_manager.rs b/dan_layer/core/src/services/checkpoint_manager.rs index a1ae232c8e..bd5b7a3c04 100644 --- a/dan_layer/core/src/services/checkpoint_manager.rs +++ b/dan_layer/core/src/services/checkpoint_manager.rs @@ -41,6 +41,7 @@ pub struct ConcreteCheckpointManager { asset_definition: AssetDefinition, wallet: TWallet, num_calls: u32, + is_initial_checkpoint: bool, checkpoint_interval: u32, } @@ -50,6 +51,8 @@ impl ConcreteCheckpointManager { asset_definition, wallet, num_calls: 0, + // TODO: VNs need to be aware of the checkpoint state + is_initial_checkpoint: true, checkpoint_interval: 100, } } @@ -65,8 +68,13 @@ impl CheckpointManager for ConcreteCheckpoi "Creating checkpoint for contract {}", self.asset_definition.contract_id ); self.wallet - .create_new_checkpoint(&self.asset_definition.contract_id, &state_root) + .create_new_checkpoint( + &self.asset_definition.contract_id, + &state_root, + self.is_initial_checkpoint, + ) .await?; + self.is_initial_checkpoint = false; self.num_calls = 0; } Ok(()) diff --git a/dan_layer/core/src/services/mocks/mod.rs b/dan_layer/core/src/services/mocks/mod.rs index fbed3ea1c4..ccf61e5fd3 100644 --- a/dan_layer/core/src/services/mocks/mod.rs +++ b/dan_layer/core/src/services/mocks/mod.rs @@ -340,6 +340,7 @@ impl WalletClient for MockWalletClient { &mut self, _contract_id: &FixedHash, _state_root: &StateRoot, + _is_initial: bool, ) -> Result<(), DigitalAssetError> { Ok(()) } diff --git a/dan_layer/core/src/services/wallet_client.rs b/dan_layer/core/src/services/wallet_client.rs index f2aeb55915..42d9537a83 100644 --- a/dan_layer/core/src/services/wallet_client.rs +++ b/dan_layer/core/src/services/wallet_client.rs @@ -31,6 +31,7 @@ pub trait WalletClient: Send + Sync { &mut self, contract_id: &FixedHash, state_root: &StateRoot, + is_initial: bool, ) -> Result<(), DigitalAssetError>; async fn submit_contract_acceptance( diff --git a/dan_layer/core/src/workers/consensus_worker.rs b/dan_layer/core/src/workers/consensus_worker.rs index 260420afbd..487e0ca585 100644 --- a/dan_layer/core/src/workers/consensus_worker.rs +++ b/dan_layer/core/src/workers/consensus_worker.rs @@ -308,6 +308,7 @@ impl<'a, T: ServiceSpecification> ConsensusWorkerProcessor<'a, &mut self.worker.payload_provider, ) .await?; + unit_of_work.commit()?; if let Some(mut state_tx) = self.worker.state_db_unit_of_work.take() { state_tx.commit()?;