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"