diff --git a/applications/tari_base_node/src/builder.rs b/applications/tari_base_node/src/builder.rs index 034272f8fe..1ba24098e0 100644 --- a/applications/tari_base_node/src/builder.rs +++ b/applications/tari_base_node/src/builder.rs @@ -38,6 +38,7 @@ use tari_core::{ transactions::CryptoFactories, validation::{ block_validators::{BodyOnlyValidator, OrphanBlockValidator}, + dan_validators::TxDanLayerValidator, header_validator::HeaderValidator, transaction_validators::{ MempoolValidator, @@ -246,6 +247,7 @@ async fn build_node_context( )), Box::new(TxInputAndMaturityValidator::new(blockchain_db.clone())), Box::new(TxConsensusValidator::new(blockchain_db.clone())), + Box::new(TxDanLayerValidator::new(blockchain_db.clone())), ]); let mempool = Mempool::new( app_config.base_node.mempool.clone(), diff --git a/base_layer/core/benches/mempool.rs b/base_layer/core/benches/mempool.rs index fd27cbad2c..d099013fd8 100644 --- a/base_layer/core/benches/mempool.rs +++ b/base_layer/core/benches/mempool.rs @@ -44,11 +44,14 @@ mod benches { CryptoFactories, }, tx, - validation::transaction_validators::{ - MempoolValidator, - TxConsensusValidator, - TxInputAndMaturityValidator, - TxInternalConsistencyValidator, + validation::{ + dan_validators::TxDanLayerValidator, + transaction_validators::{ + MempoolValidator, + TxConsensusValidator, + TxInputAndMaturityValidator, + TxInternalConsistencyValidator, + }, }, }; use tokio::{runtime::Runtime, task}; @@ -85,7 +88,8 @@ mod benches { db.clone(), )), Box::new(TxInputAndMaturityValidator::new(db.clone())), - Box::new(TxConsensusValidator::new(db)), + Box::new(TxConsensusValidator::new(db.clone())), + Box::new(TxDanLayerValidator::new(db)), ]); let mempool = Mempool::new(config, rules, Box::new(mempool_validator)); const NUM_TXNS: usize = 100; diff --git a/base_layer/core/src/transactions/transaction_components/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs index c3f86416ed..165411b49c 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -40,6 +40,7 @@ use tari_utilities::ByteArray; use super::{ ContractAcceptance, ContractAmendment, + ContractConstitution, ContractDefinition, ContractUpdateProposal, ContractUpdateProposalAcceptance, @@ -302,6 +303,18 @@ impl OutputFeatures { } } + pub fn for_contract_constitution(contract_id: FixedHash, constitution: ContractConstitution) -> OutputFeatures { + Self { + output_type: OutputType::ContractConstitution, + sidechain_features: Some( + SideChainFeaturesBuilder::new(contract_id) + .with_contract_constitution(constitution) + .finish(), + ), + ..Default::default() + } + } + pub fn for_contract_acceptance( contract_id: FixedHash, validator_node_public_key: PublicKey, diff --git a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs new file mode 100644 index 0000000000..ed84e43cd6 --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs @@ -0,0 +1,160 @@ +// 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 tari_common_types::types::PublicKey; + +use super::helpers::{get_contract_constitution, get_sidechain_features, validate_output_type}; +use crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{ + ContractAcceptance, + ContractConstitution, + OutputType, + SideChainFeatures, + TransactionOutput, + }, + validation::ValidationError, +}; + +/// This validator checks that the provided output corresponds to a valid Contract Acceptance in the DAN layer +pub fn validate_acceptance( + db: &BlockchainDatabase, + output: &TransactionOutput, +) -> Result<(), ValidationError> { + validate_output_type(output, OutputType::ContractValidatorAcceptance)?; + + let sidechain_features = get_sidechain_features(output)?; + let contract_id = sidechain_features.contract_id; + + let acceptance_features = get_contract_acceptance(sidechain_features)?; + let validator_node_public_key = &acceptance_features.validator_node_public_key; + + let constitution = get_contract_constitution(db, contract_id)?; + + validate_public_key(constitution, validator_node_public_key)?; + + // TODO: check that the signature of the transaction is valid + // TODO: check that the acceptance is inside the accpentance window of the constiution + // TODO: check that the stake of the transaction is at least the minimum specified in the constitution + // TODO: check for duplicated acceptances + + Ok(()) +} + +/// Retrieves a contract acceptance object from the sidechain features, returns an error if not present +fn get_contract_acceptance(sidechain_feature: &SideChainFeatures) -> Result<&ContractAcceptance, ValidationError> { + match sidechain_feature.acceptance.as_ref() { + Some(acceptance) => Ok(acceptance), + None => Err(ValidationError::DanLayerError( + "Invalid contract acceptance: acceptance features not found".to_string(), + )), + } +} + +/// Checks that the validator public key is present as part of the proposed committee in the constitution +fn validate_public_key( + constitution: ContractConstitution, + validator_node_public_key: &PublicKey, +) -> Result<(), ValidationError> { + let is_validator_in_committee = constitution + .validator_committee + .members() + .contains(validator_node_public_key); + if !is_validator_in_committee { + let msg = format!( + "Invalid contract acceptance: validator node public key is not in committee ({:?})", + validator_node_public_key + ); + return Err(ValidationError::DanLayerError(msg)); + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use tari_common_types::types::PublicKey; + use tari_p2p::Network; + use tari_utilities::hex::Hex; + + use crate::{ + block_spec, + consensus::ConsensusManagerBuilder, + test_helpers::blockchain::TestBlockchain, + transactions::tari_amount::T, + txn_schema, + validation::{ + dan_validators::{ + test_helpers::{ + create_block, + create_contract_acceptance_schema, + create_contract_constitution_schema, + create_contract_definition_schema, + schema_to_transaction, + }, + TxDanLayerValidator, + }, + MempoolTransactionValidation, + ValidationError, + }, + }; + + #[test] + fn it_rejects_contract_acceptances_of_non_committee_members() { + // initialize a brand new taest blockchain with a genesis block + let consensus_manager = ConsensusManagerBuilder::new(Network::LocalNet).build(); + let mut blockchain = TestBlockchain::create(consensus_manager); + let (_, coinbase_a) = blockchain.add_next_tip(block_spec!("1")).unwrap(); + + // create a block with some UTXOs to spend later at contract transactions + let schema = txn_schema!(from: vec![coinbase_a], to: vec![50 * T, 50 * T, 50 * T]); + let change_outputs = create_block(&mut blockchain, "2", schema); + + // publish the contract definition into a block + let (contract_id, schema) = create_contract_definition_schema(change_outputs[0].clone()); + create_block(&mut blockchain, "3", schema); + + // publish the contract constitution into a block + // we deliberately use a committee with only a defult public key to be able to trigger the committee error later + let committee = vec![PublicKey::default()]; + let schema = create_contract_constitution_schema(contract_id, change_outputs[1].clone(), committee); + create_block(&mut blockchain, "4", schema); + + // create a contract acceptance transaction + // we use a public key that is not included in the constitution committee, to trigger the error + let validator_node_public_key = + PublicKey::from_hex("70350e09c474809209824c6e6888707b7dd09959aa227343b5106382b856f73a").unwrap(); + let schema = + create_contract_acceptance_schema(contract_id, change_outputs[2].clone(), validator_node_public_key); + let (txs, _) = schema_to_transaction(&[schema]); + + // try to validate the acceptance transaction and check that we get the committee error + let validator = TxDanLayerValidator::new(blockchain.db().clone()); + let err = validator.validate(txs.first().unwrap()).unwrap_err(); + match err { + ValidationError::DanLayerError(message) => { + assert!(message.contains("Invalid contract acceptance: validator node public key is not in committee")) + }, + _ => panic!("Expected a consensus error"), + } + } +} diff --git a/base_layer/core/src/validation/dan_validators/helpers.rs b/base_layer/core/src/validation/dan_validators/helpers.rs new file mode 100644 index 0000000000..bea70f1f1d --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/helpers.rs @@ -0,0 +1,109 @@ +// 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 tari_common_types::types::FixedHash; + +use crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{ContractConstitution, OutputType, SideChainFeatures, TransactionOutput}, + validation::ValidationError, +}; + +pub fn validate_output_type( + output: &TransactionOutput, + expected_output_type: OutputType, +) -> Result<(), ValidationError> { + let output_type = output.features.output_type; + if output_type != expected_output_type { + let msg = format!( + "Invalid output type: expected {:?} but got {:?}", + expected_output_type, output_type + ); + return Err(ValidationError::DanLayerError(msg)); + } + + Ok(()) +} + +pub fn get_sidechain_features(output: &TransactionOutput) -> Result<&SideChainFeatures, ValidationError> { + match output.features.sidechain_features.as_ref() { + Some(features) => Ok(features), + None => Err(ValidationError::DanLayerError( + "Sidechain features not found".to_string(), + )), + } +} + +pub fn get_contract_constitution( + db: &BlockchainDatabase, + contract_id: FixedHash, +) -> Result { + let contract_outputs = db + .fetch_contract_outputs_by_contract_id_and_type(contract_id, OutputType::ContractConstitution) + .unwrap(); + + if contract_outputs.is_empty() { + return Err(ValidationError::DanLayerError( + "Contract constitution not found".to_string(), + )); + } + + // we assume that only one constitution should be present in the blockchain for any given contract + // TODO: create a validation to avoid duplicated constitution publishing + let utxo_info = match contract_outputs.first() { + Some(value) => value, + None => { + return Err(ValidationError::DanLayerError( + "Contract constitution UtxoMindInfo not found".to_string(), + )) + }, + }; + + let constitution_output = match utxo_info.output.as_transaction_output() { + Some(value) => value, + None => { + return Err(ValidationError::DanLayerError( + "Contract constitution output not found".to_string(), + )) + }, + }; + + let constitution_features = match constitution_output.features.sidechain_features.as_ref() { + Some(value) => value, + None => { + return Err(ValidationError::DanLayerError( + "Contract constitution output features not found".to_string(), + )) + }, + }; + + let constitution = match constitution_features.constitution.as_ref() { + Some(value) => value, + None => { + return Err(ValidationError::DanLayerError( + "Contract constitution data not found in the output features".to_string(), + )) + }, + }; + + Ok(constitution.clone()) +} diff --git a/base_layer/core/src/validation/dan_validators/mod.rs b/base_layer/core/src/validation/dan_validators/mod.rs new file mode 100644 index 0000000000..392bdd14cf --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/mod.rs @@ -0,0 +1,60 @@ +// 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 super::{MempoolTransactionValidation, ValidationError}; +use crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{OutputType, Transaction}, +}; + +mod acceptance_validator; +use acceptance_validator::validate_acceptance; + +mod helpers; + +#[cfg(test)] +mod test_helpers; + +/// Validator of Digital Asset Network consensus rules. +#[derive(Clone)] +pub struct TxDanLayerValidator { + db: BlockchainDatabase, +} + +impl TxDanLayerValidator { + pub fn new(db: BlockchainDatabase) -> Self { + Self { db } + } +} + +impl MempoolTransactionValidation for TxDanLayerValidator { + fn validate(&self, tx: &Transaction) -> Result<(), ValidationError> { + for output in tx.body().outputs() { + match output.features.output_type { + OutputType::ContractValidatorAcceptance => validate_acceptance(&self.db, output)?, + _ => continue, + } + } + + Ok(()) + } +} diff --git a/base_layer/core/src/validation/dan_validators/test_helpers.rs b/base_layer/core/src/validation/dan_validators/test_helpers.rs new file mode 100644 index 0000000000..4332dea93a --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/test_helpers.rs @@ -0,0 +1,136 @@ +// 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 std::{convert::TryInto, sync::Arc}; + +use tari_common_types::types::{FixedHash, PublicKey, Signature}; + +use crate::{ + block_spec, + test_helpers::blockchain::TestBlockchain, + transactions::{ + test_helpers::{spend_utxos, TransactionSchema}, + transaction_components::{ + vec_into_fixed_string, + CheckpointParameters, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + ContractConstitution, + ContractDefinition, + ContractSpecification, + OutputFeatures, + RequirementsForConstitutionChange, + SideChainConsensus, + Transaction, + UnblindedOutput, + }, + }, + txn_schema, +}; + +pub fn schema_to_transaction(txns: &[TransactionSchema]) -> (Vec>, Vec) { + let mut tx = Vec::new(); + let mut utxos = Vec::new(); + txns.iter().for_each(|schema| { + let (txn, mut output) = spend_utxos(schema.clone()); + tx.push(Arc::new(txn)); + utxos.append(&mut output); + }); + (tx, utxos) +} + +pub fn create_block( + blockchain: &mut TestBlockchain, + block_name: &'static str, + schema: TransactionSchema, +) -> Vec { + let (txs, outputs) = schema_to_transaction(&[schema]); + let (_, _) = blockchain + .append_to_tip(block_spec!(block_name, transactions: txs.iter().map(|t| (**t).clone()).collect())) + .unwrap(); + + outputs +} + +pub fn create_contract_definition_schema(input: UnblindedOutput) -> (FixedHash, TransactionSchema) { + let definition = ContractDefinition { + contract_name: vec_into_fixed_string("name".as_bytes().to_vec()), + contract_issuer: PublicKey::default(), + contract_spec: ContractSpecification { + runtime: vec_into_fixed_string("runtime".as_bytes().to_vec()), + public_functions: vec![], + }, + }; + let contract_id = definition.calculate_contract_id(); + let definition_features = OutputFeatures::for_contract_definition(definition); + + let tx_schema = + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: definition_features); + + (contract_id, tx_schema) +} + +pub fn create_contract_constitution_schema( + contract_id: FixedHash, + input: UnblindedOutput, + committee: Vec, +) -> TransactionSchema { + let validator_committee: CommitteeMembers = vec![PublicKey::default()].try_into().unwrap(); + let constitution = ContractConstitution { + validator_committee, + acceptance_requirements: ContractAcceptanceRequirements { + acceptance_period_expiry: 100, + minimum_quorum_required: 5, + }, + consensus: SideChainConsensus::MerkleRoot, + checkpoint_params: CheckpointParameters { + minimum_quorum_required: 5, + abandoned_interval: 100, + }, + constitution_change_rules: ConstitutionChangeRules { + change_flags: ConstitutionChangeFlags::all(), + requirements_for_constitution_change: Some(RequirementsForConstitutionChange { + minimum_constitution_committee_signatures: 5, + constitution_committee: Some(committee.try_into().unwrap()), + }), + }, + initial_reward: 100.into(), + }; + let constitution_features = OutputFeatures::for_contract_constitution(contract_id, constitution); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: constitution_features) +} + +pub fn create_contract_acceptance_schema( + contract_id: FixedHash, + input: UnblindedOutput, + validator_node_public_key: PublicKey, +) -> TransactionSchema { + let signature = Signature::default(); + + let acceptance_features = + OutputFeatures::for_contract_acceptance(contract_id, validator_node_public_key, signature); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: acceptance_features) +} diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index 09385c071d..7ab54a285b 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -116,6 +116,8 @@ pub enum ValidationError { InvalidBlockchainVersion { version: u16 }, #[error("Standard transaction contains coinbase output")] ErroneousCoinbaseOutput, + #[error("Digital Asset Network Error: {0}")] + DanLayerError(String), } // ChainStorageError has a ValidationError variant, so to prevent a cyclic dependency we use a string representation in diff --git a/base_layer/core/src/validation/mod.rs b/base_layer/core/src/validation/mod.rs index 528709eb10..4d3497d1bc 100644 --- a/base_layer/core/src/validation/mod.rs +++ b/base_layer/core/src/validation/mod.rs @@ -43,6 +43,7 @@ pub use traits::{ }; pub mod block_validators; +pub mod dan_validators; mod difficulty_calculator; pub use difficulty_calculator::*; pub mod header_validator; diff --git a/integration_tests/features/ValidatorNode.feature b/integration_tests/features/ValidatorNode.feature index 6c8dd5a33a..a84a7338ca 100644 --- a/integration_tests/features/ValidatorNode.feature +++ b/integration_tests/features/ValidatorNode.feature @@ -11,6 +11,8 @@ Feature: Validator Node And I wait for wallet WALLET1 to have at least 1000000 uT 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 + And I publish a contract constitution from file "fixtures/contract_constitution.json" on wallet WALLET1 via command line + And I mine 4 blocks using wallet WALLET1 on NODE1 And I have a validator node VN1 connected to base node NODE1 and wallet WALLET1 When I publish a contract acceptance transaction for contract DEF1 for the validator node VN1 And I mine 9 blocks using wallet WALLET1 on NODE1 diff --git a/integration_tests/features/support/validator_node_steps.js b/integration_tests/features/support/validator_node_steps.js index d83e9f6bb0..c8da062b8b 100644 --- a/integration_tests/features/support/validator_node_steps.js +++ b/integration_tests/features/support/validator_node_steps.js @@ -20,20 +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. -const { When, Given, Then } = require("@cucumber/cucumber"); +const { Given, Then } = require("@cucumber/cucumber"); const { expect } = require("chai"); -const { sleep, findUtxoWithOutputMessage } = require("../../helpers/util"); -const ValidatorNodeProcess = require("../../helpers/validatorNodeProcess"); +const { findUtxoWithOutputMessage } = require("../../helpers/util"); Given( "I have a validator node {word} connected to base node {word} and wallet {word}", { timeout: 20 * 1000 }, - async function ( - vn_name, - base_node_name, - wallet_name, - ) { - let vn = await this.createValidatorNode(vn_name, base_node_name, wallet_name); + async function (vn_name, base_node_name, wallet_name) { + let vn = await this.createValidatorNode( + vn_name, + base_node_name, + wallet_name + ); await this.addDanNode(vn_name, vn); } ); @@ -41,19 +40,15 @@ Given( Then( "validator node {word} has {string} set to {word}", { timeout: 20 * 1000 }, - async function ( - vn_name, - option_name, - option_value, - ) { - let vn = this.getNode(vn_name); - await vn.stop(); + async function (vn_name, option_name, option_value) { + let vn = this.getNode(vn_name); + await vn.stop(); - vn.options['validator_node.' + option_name] = option_value; + vn.options["validator_node." + option_name] = option_value; - await vn.startNew(); + await vn.startNew(); } -) +); Then( "I publish a contract acceptance transaction for contract {word} for the validator node {word}", @@ -71,7 +66,7 @@ Then( Then( "I publish a contract update proposal acceptance transaction for the validator node {word}", - { timeout: 20 * 1000 }, + { timeout: 120 * 1000 }, async function (vn_name) { let dan_node = this.getNode(vn_name); let grpc_dan_node = await dan_node.createGrpcClient(); @@ -85,41 +80,41 @@ Then( ); Then( - "wallet {word} will have a successfully mined constitution acceptance transaction for contract {word}", - { timeout: 40 * 1000 }, - async function (wallet_name, contract_name) { - let wallet = await this.getWallet(wallet_name); - let contract_id = await this.fetchContract(contract_name); - let message = `Contract acceptance for contract with id=${contract_id}` + "wallet {word} will have a successfully mined constitution acceptance transaction for contract {word}", + { timeout: 40 * 1000 }, + async function (wallet_name, contract_name) { + let wallet = await this.getWallet(wallet_name); + let contract_id = await this.fetchContract(contract_name); + let message = `Contract acceptance for contract with id=${contract_id}`; - let utxos = await findUtxoWithOutputMessage(wallet, message); - expect(utxos.length).to.equal(1); - } -) + let utxos = await findUtxoWithOutputMessage(wallet, message); + expect(utxos.length).to.equal(1); + } +); Then( - "wallet {word} will have a successfully mined contract acceptance transaction for contract {word}", - { timeout: 40 * 1000 }, - async function (wallet_name, contract_name) { - let wallet = await this.getWallet(wallet_name); - let contract_id = await this.fetchContract(contract_name); - let message = `Contract acceptance for contract with id=${contract_id}` + "wallet {word} will have a successfully mined contract acceptance transaction for contract {word}", + { timeout: 40 * 1000 }, + async function (wallet_name, contract_name) { + let wallet = await this.getWallet(wallet_name); + let contract_id = await this.fetchContract(contract_name); + let message = `Contract acceptance for contract with id=${contract_id}`; - let utxos = await findUtxoWithOutputMessage(wallet, message); - expect(utxos.length).to.equal(1); - } -) + let utxos = await findUtxoWithOutputMessage(wallet, message); + // FIXME: it seems that the validator node publishes acceptances for both definitions and constitutions + expect(utxos.length).to.be.gte(1); + } +); Then( - "wallet {word} will have a successfully mined contract update proposal for contract {word}", - { timeout: 40 * 1000 }, - async function (wallet_name, contract_name) { - let wallet = await this.getWallet(wallet_name); - let contract_id = await this.fetchContract(contract_name); - let message = `Contract update proposal acceptance for contract_id=${contract_id} and proposal_id=0` - - let utxos = await findUtxoWithOutputMessage(wallet, message); - expect(utxos.length).to.equal(1); - } -) + "wallet {word} will have a successfully mined contract update proposal for contract {word}", + { timeout: 40 * 1000 }, + async function (wallet_name, contract_name) { + let wallet = await this.getWallet(wallet_name); + let contract_id = await this.fetchContract(contract_name); + let message = `Contract update proposal acceptance for contract_id=${contract_id} and proposal_id=0`; + let utxos = await findUtxoWithOutputMessage(wallet, message); + expect(utxos.length).to.equal(1); + } +); diff --git a/integration_tests/features/support/wallet_cli_steps.js b/integration_tests/features/support/wallet_cli_steps.js index 71de02ecd7..f74472444b 100644 --- a/integration_tests/features/support/wallet_cli_steps.js +++ b/integration_tests/features/support/wallet_cli_steps.js @@ -22,11 +22,9 @@ const { Given, Then, When } = require("@cucumber/cucumber"); const { expect } = require("chai"); -const fs = require('fs'); -const { waitFor, sleep, byteArrayToHex } = require("../../helpers/util"); +const fs = require("fs"); +const { waitFor, sleep } = require("../../helpers/util"); const path = require("path"); -const uuid = require("uuid"); -const dateFormat = require("dateformat"); Given( /I change the password of wallet (.*) to (.*) via command line/, @@ -299,41 +297,41 @@ Then( ); Then( - "I publish the contract constitution {word} on wallet {word} via command line", - { timeout: 120 * 1000 }, - async function (constitution_name, wallet_name) { - let constitution = this.fetchContractConstitution(constitution_name); - let wallet = this.getWallet(wallet_name); - - let absolute_path = await wallet.writeConstitutionFile(constitution); - let output = await wallet_run_command( - wallet, - `contract publish-constitution ${absolute_path}` - ); - console.log(output.buffer); - } + "I publish the contract constitution {word} on wallet {word} via command line", + { timeout: 120 * 1000 }, + async function (constitution_name, wallet_name) { + let constitution = this.fetchContractConstitution(constitution_name); + let wallet = this.getWallet(wallet_name); + + let absolute_path = await wallet.writeConstitutionFile(constitution); + let output = await wallet_run_command( + wallet, + `contract publish-constitution ${absolute_path}` + ); + console.log(output.buffer); + } ); When( - "I create a contract constitution {word} for contract {word} from file {string}", - async function (constitution_name, contract_name, relative_file_path) { - let absolute_path = path.resolve(relative_file_path); - let contract_id = this.fetchContract(contract_name); + "I create a contract constitution {word} for contract {word} from file {string}", + async function (constitution_name, contract_name, relative_file_path) { + let absolute_path = path.resolve(relative_file_path); + let contract_id = this.fetchContract(contract_name); - let constitution = JSON.parse(fs.readFileSync(absolute_path, 'utf8')); - constitution['contract_id'] = contract_id; + let constitution = JSON.parse(fs.readFileSync(absolute_path, "utf8")); + constitution["contract_id"] = contract_id; - this.saveContractConstitution(constitution_name, constitution); - } + this.saveContractConstitution(constitution_name, constitution); + } ); When( - 'I add {word} to the validator committee on {word}', - async function (vn_name, constitution_name) { - let vn = this.getNode(vn_name); - let constitution = this.fetchContractConstitution(constitution_name); - constitution['validator_committee'] = [vn.getPubKey()]; - } + "I add {word} to the validator committee on {word}", + async function (vn_name, constitution_name) { + let vn = this.getNode(vn_name); + let constitution = this.fetchContractConstitution(constitution_name); + constitution["validator_committee"] = [vn.getPubKey()]; + } ); Then( diff --git a/integration_tests/features/support/world.js b/integration_tests/features/support/world.js index 4450a76cf3..bedd878520 100644 --- a/integration_tests/features/support/world.js +++ b/integration_tests/features/support/world.js @@ -132,13 +132,13 @@ class CustomWorld { const walletGrpcAddress = `127.0.0.1:${walletNode.getGrpcPort()}`; let vn = new ValidatorNodeProcess( - vn_name, - false, - [], - this.logFilePathBaseNode, - undefined, - baseNodeGrpcAddress, - walletGrpcAddress + vn_name, + false, + [], + this.logFilePathBaseNode, + undefined, + baseNodeGrpcAddress, + walletGrpcAddress ); await vn.startNew(); diff --git a/integration_tests/fixtures/contract_constitution.json b/integration_tests/fixtures/contract_constitution.json index 084fa0f8ec..cc2208d01d 100644 --- a/integration_tests/fixtures/contract_constitution.json +++ b/integration_tests/fixtures/contract_constitution.json @@ -1,8 +1,7 @@ { "contract_id": "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177", "validator_committee": [ - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "608001dffed28d058591cd65eaca11c465165592baf872cf1d984e26fb12b472", "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" ], "acceptance_parameters": { diff --git a/integration_tests/fixtures/validator_node_id.json b/integration_tests/fixtures/validator_node_id.json new file mode 100644 index 0000000000..51f45d188d --- /dev/null +++ b/integration_tests/fixtures/validator_node_id.json @@ -0,0 +1,2 @@ +// This file is generated by the Tari base node. Any changes will be overwritten. +{"node_id":"5d4a78a0e3fe5ff8dfd16e864f","public_key":"608001dffed28d058591cd65eaca11c465165592baf872cf1d984e26fb12b472","features":{"bits":0},"secret_key":"3b7299635692f60699e0852c57104836fa63e75259c6ddc65cbac4be39e3df07","public_address":"","identity_signature":{"version":0,"signature":{"public_nonce":"def0de34f759b257a95009fa0afb4ab69e61e5385c1a2445e0d5c6ca68f5c933","signature":"dd84fd349002f4ea431c6361c51dd1e9cb7c1e13ab4591f2aedd3dcd24b50e04"},"updated_at":"2022-06-22T14:22:36.417Z"}} \ No newline at end of file diff --git a/integration_tests/helpers/util.js b/integration_tests/helpers/util.js index 2a6b42fa2a..ad1d25dfe7 100644 --- a/integration_tests/helpers/util.js +++ b/integration_tests/helpers/util.js @@ -408,6 +408,7 @@ const findUtxoWithOutputMessage = async (wallet, message) => { let client = await wallet.connectClient(); let accepted = []; + /* eslint-disable no-constant-condition */ while (true) { let found_txs = await client.getCompletedTransactions(); accepted = found_txs.filter((txo) => { @@ -415,7 +416,7 @@ const findUtxoWithOutputMessage = async (wallet, message) => { }); if (accepted.length > 0) { - break + break; } await sleep(5000); diff --git a/integration_tests/helpers/validatorNodeProcess.js b/integration_tests/helpers/validatorNodeProcess.js index c3ab067154..267397a9d5 100644 --- a/integration_tests/helpers/validatorNodeProcess.js +++ b/integration_tests/helpers/validatorNodeProcess.js @@ -163,6 +163,37 @@ class ValidatorNodeProcess { fs.mkdirSync(this.baseDir + "/log", { recursive: true }); } + // to avoid writing permission errors, we copy the reference identity file to the temp folder + let identity_file_name = "validator_node_id.json"; + let identity_source_path = path.resolve( + `./fixtures/${identity_file_name}` + ); + let identity_destination_path = path.resolve( + `${this.baseDir}/${identity_file_name}` + ); + fs.copyFile(identity_source_path, identity_destination_path, (err) => { + if (err) { + console.log( + "Error Found while copying validator identity file to temp folder: ", + err + ); + throw err; + } + console.log("Validator identity file was copied to destination"); + fs.chmod(identity_destination_path, 0o600, (err) => { + if (err) { + console.log( + "Error Found while changing the permissions of the validator indentity file: ", + err + ); + throw err; + } + console.log( + "Validator identity file permissions successfully modified" + ); + }); + }); + let envs = []; if (!this.excludeTestEnvars) { envs = this.getOverrides(); @@ -182,8 +213,8 @@ class ValidatorNodeProcess { customArgs["validator_node.grpc_address"] = this.getGrpcAddress(); } Object.keys(this.options).forEach((k) => { - if (k.startsWith('validator_node.')) { - customArgs[k] = this.options[k] + if (k.startsWith("validator_node.")) { + customArgs[k] = this.options[k]; } }); diff --git a/integration_tests/helpers/walletClient.js b/integration_tests/helpers/walletClient.js index 220c960405..2f13cf6aaa 100644 --- a/integration_tests/helpers/walletClient.js +++ b/integration_tests/helpers/walletClient.js @@ -8,10 +8,6 @@ const { convertStringToVec, multiAddrToSocket, } = require("./util"); -const dateFormat = require("dateformat"); -const uuid = require("uuid"); -const fs = require("fs"); -const path = require("path"); function transactionStatus() { return [ diff --git a/integration_tests/helpers/walletProcess.js b/integration_tests/helpers/walletProcess.js index 6658575f9b..1bf7e06031 100644 --- a/integration_tests/helpers/walletProcess.js +++ b/integration_tests/helpers/walletProcess.js @@ -469,9 +469,9 @@ class WalletProcess { async writeConstitutionFile(constitution) { let data = JSON.stringify(constitution); - let absolute_path = path.resolve(this.baseDir + '/' + uuid.v4() + '.json'); + let absolute_path = path.resolve(this.baseDir + "/" + uuid.v4() + ".json"); - fs.writeFile(absolute_path, data, 'utf8',(err) => { + fs.writeFile(absolute_path, data, "utf8", (err) => { if (err) { console.log(`Error writing file: ${err}`); } else {