diff --git a/Cargo.lock b/Cargo.lock index 52269c3ece..c009a77e19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1718,6 +1718,7 @@ dependencies = [ "tari_engine", "tari_engine_types", "tari_epoch_manager", + "tari_ootle_app_utilities", "tari_ootle_common_types", "tari_ootle_storage", "tari_shutdown", @@ -11536,6 +11537,7 @@ dependencies = [ "tari_common", "tari_common_types", "tari_crypto", + "tari_engine", "tari_engine_types", "tari_ootle_app_utilities", "tari_ootle_common_types", @@ -11544,7 +11546,6 @@ dependencies = [ "tari_ootle_wallet_sdk_services", "tari_ootle_wallet_storage_sqlite", "tari_shutdown", - "tari_template_abi", "tari_template_builtin", "tari_template_lib", "tari_transaction", diff --git a/applications/tari_indexer/src/network_client.rs b/applications/tari_indexer/src/network_client.rs index 8d7a6966ae..e44996089b 100644 --- a/applications/tari_indexer/src/network_client.rs +++ b/applications/tari_indexer/src/network_client.rs @@ -38,10 +38,6 @@ where } pub async fn submit_transaction(&self, transaction: Transaction) -> Result { - if !transaction.has_inputs() { - return Err(NetworkClientError::NoInputsProvided); - } - // Ensure initial scanning has completed to ensure an accurate epoch self.epoch_manager.wait_for_initial_scanning_to_complete().await?; @@ -210,6 +206,4 @@ pub enum NetworkClientError { }, #[error("No committee at present. Try again later")] NoCommitteeMembers, - #[error("No inputs provided in transaction.")] - NoInputsProvided, } diff --git a/applications/tari_indexer/src/rest_api/handlers/transactions.rs b/applications/tari_indexer/src/rest_api/handlers/transactions.rs index 6fb28c7798..1efa162785 100644 --- a/applications/tari_indexer/src/rest_api/handlers/transactions.rs +++ b/applications/tari_indexer/src/rest_api/handlers/transactions.rs @@ -76,9 +76,6 @@ pub async fn submit_transaction( transaction_id, details, } => ErrorResponse::bad_request(format!("Transaction {} is invalid: {}", transaction_id, details)), - TransactionManagerError::NetworkClientError(NetworkClientError::NoInputsProvided) => { - ErrorResponse::bad_request("Transaction has no inputs".to_string()) - }, e => ErrorResponse::anyhow(e), })?; diff --git a/applications/tari_indexer/web_ui/src/routes/VN/Components/NftRow.tsx b/applications/tari_indexer/web_ui/src/routes/VN/Components/NftRow.tsx index a07dc0a676..a2bc4b44ee 100644 --- a/applications/tari_indexer/web_ui/src/routes/VN/Components/NftRow.tsx +++ b/applications/tari_indexer/web_ui/src/routes/VN/Components/NftRow.tsx @@ -32,7 +32,6 @@ export interface NftData { address: string; version: number; nft: NonFungibleSubstate; - original_owner?: string; amount?: number; } @@ -59,10 +58,6 @@ function NftRow({ nftData }: { nftData: NftData }) { Original Owner: - - {nftData.original_owner ? shortenString(nftData.original_owner) : "No owner"} - {nftData.original_owner && } - Version: diff --git a/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx b/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx index 382675b166..f94ac35f5c 100644 --- a/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx +++ b/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx @@ -101,7 +101,7 @@ function Resources() { console.error("Error converting mutable CBOR value:", e); } - const { name, original_owner, amount } = nftData; + const { name, amount } = nftData; const { image_url } = mutableData || {}; const nftId = nft.address.id; const key = Object.keys(nftId)[0]; @@ -115,7 +115,6 @@ function Resources() { address, version: nft.version, nft, - original_owner, amount, }); } diff --git a/applications/tari_validator_node/log4rs_sample.yml b/applications/tari_validator_node/log4rs_sample.yml index cf1f3bb656..a38dd98a97 100644 --- a/applications/tari_validator_node/log4rs_sample.yml +++ b/applications/tari_validator_node/log4rs_sample.yml @@ -151,7 +151,6 @@ loggers: tari::ootle: level: debug appenders: - - ootle - stdout additive: false @@ -160,12 +159,27 @@ loggers: appenders: - consensus + tari::consensus: + level: debug + appenders: + - consensus + + tari::ootle::storage: + level: debug + appenders: + - consensus + tari::ootle::consensus: level: debug appenders: - consensus - tari::ootle::networking: + tari::ootle::hotstuff: + level: debug + appenders: + - consensus + + tari::networking: level: debug appenders: - network diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index e33bf9fbcd..6a973afcd4 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -90,9 +90,8 @@ use crate::{ }, state_store_template_provider::StateStoreTemplateProvider, transaction_validators::{ + BasicValidations, EpochRangeValidator, - FeeTransactionValidator, - IsShardApplicable, TemplateExistsValidator, TransactionDryRunValidator, TransactionNetworkValidator, @@ -214,7 +213,11 @@ pub async fn spawn_services( info!(target: LOG_TARGET, "State store initializing"); - let state_store = ValidatorNodeStateStore::open(&config.validator_node.state_db_path, DatabaseOptions::default())?; + let state_store = ValidatorNodeStateStore::open( + &config.validator_node.state_db_path, + // TODO: just enable it always for now, later make it configurable and default to true for testnets + DatabaseOptions::default().with_debugging_data(true), + )?; state_store.with_write_tx(|tx| create_genesis_state(tx, config.network, consensus_constants.num_preshards))?; @@ -445,8 +448,7 @@ pub fn create_mempool_transaction_validator( ) -> impl Validator { TransactionNetworkValidator::new(network) .and_then(TransactionDryRunValidator) - .and_then(IsShardApplicable::new()) - .and_then(FeeTransactionValidator) + .and_then(BasicValidations::new()) .and_then(TransactionSignatureValidator) .and_then(TemplateExistsValidator::new(template_manager)) } diff --git a/applications/tari_validator_node/src/p2p/rpc/mod.rs b/applications/tari_validator_node/src/p2p/rpc/mod.rs index 4237f0547c..510d8bf642 100644 --- a/applications/tari_validator_node/src/p2p/rpc/mod.rs +++ b/applications/tari_validator_node/src/p2p/rpc/mod.rs @@ -21,10 +21,10 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mod block_sync_task; -mod service_impl; +mod rpc_impl; mod state_sync_task; -pub use service_impl::ValidatorNodeRpcServiceImpl; +pub use rpc_impl::ValidatorNodeRpcServiceImpl; use tari_epoch_manager::service::EpochManagerHandle; use tari_ootle_common_types::PeerAddress; use tari_ootle_storage::StateStore; diff --git a/applications/tari_validator_node/src/p2p/rpc/service_impl.rs b/applications/tari_validator_node/src/p2p/rpc/rpc_impl.rs similarity index 99% rename from applications/tari_validator_node/src/p2p/rpc/service_impl.rs rename to applications/tari_validator_node/src/p2p/rpc/rpc_impl.rs index f03551346a..aad4cb2c84 100644 --- a/applications/tari_validator_node/src/p2p/rpc/service_impl.rs +++ b/applications/tari_validator_node/src/p2p/rpc/rpc_impl.rs @@ -402,7 +402,7 @@ impl ValidatorNodeRpcSe let value_filter_flags = SubstateValueFilterFlags::from_bits_truncate(req.value_filters); - info!( + debug!( target: LOG_TARGET, "🌍 peer initiated sync with this node (start: v{}, {}) to {} (values: {:?})", req.start_state_version, diff --git a/applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs b/applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs index a8cae8f172..58350ce310 100644 --- a/applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs +++ b/applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs @@ -76,7 +76,7 @@ impl StateSyncTask { // )))) // .await?; - info!(target: LOG_TARGET, "🌍sync complete ({}). {} update(s) sent.", current_state_version, counter); + debug!(target: LOG_TARGET, "🌍sync complete ({}). {} update(s) sent.", current_state_version, counter); // Finished return Ok(()); }, diff --git a/applications/tari_validator_node/src/p2p/services/mempool/service.rs b/applications/tari_validator_node/src/p2p/services/mempool/service.rs index 2d60616742..b9a79a6546 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/service.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/service.rs @@ -264,13 +264,6 @@ where return Err(e.into()); } - if !transaction.has_inputs() { - warn!(target: LOG_TARGET, "⚠ No involved shards for transaction {tx_id}"); - return Err(MempoolError::TransactionValidationError( - TransactionValidationError::NoInvolvedShards { transaction_id: tx_id }, - )); - } - let current_epoch = self.consensus_handle.current_view().get_epoch(); let local_committee_shard = self.epoch_manager.get_local_committee_info(current_epoch).await?; diff --git a/applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs b/applications/tari_validator_node/src/transaction_validators/basic.rs similarity index 57% rename from applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs rename to applications/tari_validator_node/src/transaction_validators/basic.rs index afa11ca32e..182eccca07 100644 --- a/applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs +++ b/applications/tari_validator_node/src/transaction_validators/basic.rs @@ -8,30 +8,32 @@ use crate::{transaction_validators::TransactionValidationError, validator::Valid const LOG_TARGET: &str = "tari::ootle::mempool::validators::is_shard_applicable"; -/// Refuse to process the transaction if it does not apply to any shard (i.e. does not have any inputs or claim burn -/// tombstones). +/// Basic validations for a transaction: +/// - Has at least one fee instruction #[derive(Debug, Clone, Default)] -pub struct IsShardApplicable; +pub struct BasicValidations; -impl IsShardApplicable { +impl BasicValidations { pub fn new() -> Self { Self } } -impl Validator for IsShardApplicable { +impl Validator for BasicValidations { type Context = (); type Error = TransactionValidationError; fn validate(&self, _context: &(), transaction: &Transaction) -> Result<(), Self::Error> { - if !transaction.has_inputs() { - warn!(target: LOG_TARGET, "HasInputs - FAIL: No input shards"); - return Err(TransactionValidationError::NoInputs { + if transaction.fee_instructions().is_empty() { + warn!(target: LOG_TARGET, "BasicValidations - FAIL: No fee instructions"); + return Err(TransactionValidationError::NoFeeInstructions { transaction_id: transaction.calculate_id(), }); } - debug!(target: LOG_TARGET, "HasInputs - OK"); + // TODO: additional checks? + + debug!(target: LOG_TARGET, "BasicValidations - OK"); Ok(()) } } diff --git a/applications/tari_validator_node/src/transaction_validators/error.rs b/applications/tari_validator_node/src/transaction_validators/error.rs index 25aacf248a..15632ed799 100644 --- a/applications/tari_validator_node/src/transaction_validators/error.rs +++ b/applications/tari_validator_node/src/transaction_validators/error.rs @@ -19,8 +19,8 @@ pub enum TransactionValidationError { // TODO: move these to MempoolValidationError type #[error("Template not found: {address}")] TemplateNotFound { address: TemplateAddress }, - #[error("No fee instructions")] - NoFeeInstructions, + #[error("{transaction_id} has no fee instructions")] + NoFeeInstructions { transaction_id: TransactionId }, #[error("Output substate exists in transaction {transaction_id}")] OutputSubstateExists { transaction_id: TransactionId }, #[error("Validator fee claim instruction in transaction {transaction_id} contained invalid epoch {given_epoch}")] @@ -32,10 +32,6 @@ pub enum TransactionValidationError { CurrentEpochLessThanMinimum { current_epoch: Epoch, min_epoch: Epoch }, #[error("Current epoch ({current_epoch}) is greater than maximum epoch ({max_epoch}) required for transaction")] CurrentEpochGreaterThanMaximum { current_epoch: Epoch, max_epoch: Epoch }, - #[error("Transaction {transaction_id} does not have any inputs")] - NoInputs { transaction_id: TransactionId }, - #[error("Executed transaction {transaction_id} does not involved any shards")] - NoInvolvedShards { transaction_id: TransactionId }, #[error("Invalid transaction signature")] InvalidSignature, #[error("Transaction {transaction_id} has no main signer")] diff --git a/applications/tari_validator_node/src/transaction_validators/fee.rs b/applications/tari_validator_node/src/transaction_validators/fee.rs deleted file mode 100644 index 7967835038..0000000000 --- a/applications/tari_validator_node/src/transaction_validators/fee.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2024 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use log::warn; -use tari_transaction::Transaction; - -use crate::{transaction_validators::TransactionValidationError, validator::Validator}; - -const LOG_TARGET: &str = "tari::ootle::mempool::validators::fee"; - -#[derive(Debug)] -pub struct FeeTransactionValidator; - -impl Validator for FeeTransactionValidator { - type Context = (); - type Error = TransactionValidationError; - - fn validate(&self, _context: &(), transaction: &Transaction) -> Result<(), TransactionValidationError> { - if transaction.fee_instructions().is_empty() { - warn!(target: LOG_TARGET, "FeeTransactionValidator - FAIL: No fee instructions"); - return Err(TransactionValidationError::NoFeeInstructions); - } - Ok(()) - } -} diff --git a/applications/tari_validator_node/src/transaction_validators/mod.rs b/applications/tari_validator_node/src/transaction_validators/mod.rs index dfa808cdc0..5c4a081dd4 100644 --- a/applications/tari_validator_node/src/transaction_validators/mod.rs +++ b/applications/tari_validator_node/src/transaction_validators/mod.rs @@ -1,19 +1,17 @@ // Copyright 2022 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +mod basic; mod epoch_range; -mod fee; -mod is_shard_applicable; mod network; mod signature; mod template_exists; mod dry_run; +pub use basic::*; pub use dry_run::*; pub use epoch_range::*; -pub use fee::*; -pub use is_shard_applicable::*; pub use network::*; pub use signature::*; pub use template_exists::*; diff --git a/applications/tari_validator_node/src/transaction_validators/network.rs b/applications/tari_validator_node/src/transaction_validators/network.rs index c7bd9edc2f..258ac39ae4 100644 --- a/applications/tari_validator_node/src/transaction_validators/network.rs +++ b/applications/tari_validator_node/src/transaction_validators/network.rs @@ -24,24 +24,22 @@ impl Validator for TransactionNetworkValidator { type Context = (); type Error = TransactionValidationError; - fn validate(&self, _context: &Self::Context, input: &Transaction) -> Result<(), Self::Error> { - match input { - Transaction::V1(tx) => { - let tx_network = Network::try_from(tx.network()).map_err(|error| Self::Error::UnknownNetwork { - byte: tx.network(), - details: error.to_string(), - })?; - if tx_network == self.network { - Ok(()) - } else { - warn!(target: LOG_TARGET, "TransactionNetworkValidator - FAIL: mismatching networks: TX: {} != Current: {}", tx_network, self.network); - Err(Self::Error::NetworkMismatch { - actual: self.network, - expected: tx_network, - }) - } - }, + fn validate(&self, _context: &Self::Context, tx: &Transaction) -> Result<(), Self::Error> { + let tx_network = + Network::try_from(tx.network()).map_err(|error| TransactionValidationError::UnknownNetwork { + byte: tx.network(), + details: error.to_string(), + })?; + + if tx_network != self.network { + warn!(target: LOG_TARGET, "TransactionNetworkValidator - FAIL: mismatching networks: TX: {} != Current: {}", tx_network, self.network); + return Err(Self::Error::NetworkMismatch { + actual: self.network, + expected: tx_network, + }); } + + Ok(()) } } diff --git a/applications/tari_walletd/Cargo.toml b/applications/tari_walletd/Cargo.toml index 2266467795..5724f9bb03 100644 --- a/applications/tari_walletd/Cargo.toml +++ b/applications/tari_walletd/Cargo.toml @@ -20,12 +20,12 @@ tari_ootle_wallet_sdk_services = { workspace = true, features = ["indexer_client tari_ootle_wallet_storage_sqlite = { workspace = true } tari_transaction = { workspace = true } tari_ootle_common_types = { workspace = true } +tari_engine = { workspace = true } tari_engine_types = { workspace = true } tari_wallet_daemon_client = { workspace = true } tari_template_builtin = { workspace = true } # TODO: Ideally we should not have to include the WASM template lib, we should perhaps extract the address types into a separate crate (e.g. template_types) tari_template_lib = { workspace = true } -tari_template_abi = { workspace = true } tari_transaction_manifest = { workspace = true } async-trait = { workspace = true } diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 025f8b2edb..8f95ab43ae 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -553,7 +553,6 @@ pub async fn handle_claim_burn( .claim_burn(claim_proof, output_data) .pay_fee_stealth(pay_fee_and_mint_output) }) - .add_input(XTR) .build(); let transaction = sdk.signer_api().sign(public_signer_key.key_id, transaction)?; diff --git a/applications/tari_walletd/src/handlers/validator.rs b/applications/tari_walletd/src/handlers/validator.rs index a6201ebdca..55d04a0871 100644 --- a/applications/tari_walletd/src/handlers/validator.rs +++ b/applications/tari_walletd/src/handlers/validator.rs @@ -10,7 +10,6 @@ use log::*; use tari_engine_types::{substate::SubstateId, ToByteType}; use tari_ootle_common_types::{derive_fee_pool_address, SubstateAddress, SubstateRequirement}; use tari_ootle_wallet_sdk::models::{KeyBranch, KeyId}; -use tari_template_lib::constants::XTR; use tari_transaction::args; use tari_wallet_daemon_client::{ permissions::JrpcPermission, @@ -177,7 +176,6 @@ pub async fn handle_claim_validator_fees( }) .with_inputs(inputs.into_iter().map(|input| input.into_unversioned())) .with_inputs(fee_pool_addresses.map(SubstateRequirement::unversioned)) - .add_input(XTR) .map(|builder| { if let Some(index) = req.claim_key_index { if claim_public_key == *account.address.account_public_key() { diff --git a/applications/tari_walletd/src/services/template_monitor.rs b/applications/tari_walletd/src/services/template_monitor.rs index 7edead4477..5ba3aee679 100644 --- a/applications/tari_walletd/src/services/template_monitor.rs +++ b/applications/tari_walletd/src/services/template_monitor.rs @@ -1,16 +1,13 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{ops::Add, time::Duration}; - -use anyhow::anyhow; use log::*; -use tari_ootle_common_types::{optional::Optional, substate_type::SubstateType}; -use tari_ootle_wallet_sdk::{models::WalletEvent, network::WalletNetworkInterface, WalletSdk, WalletSdkSpec}; +use tari_engine::wasm::WasmModule; +use tari_ootle_common_types::substate_type::SubstateType; +use tari_ootle_wallet_sdk::{models::WalletEvent, WalletSdk, WalletSdkSpec}; use tari_ootle_wallet_sdk_services::notify::Notify; use tari_shutdown::ShutdownSignal; -use tari_template_abi::TemplateDef; -use tari_template_lib::types::TemplateAddress; +use tokio::task; const LOG_TARGET: &str = "tari::ootle_wallet_daemon::services::template_monitor"; @@ -31,44 +28,13 @@ where TSpec: WalletSdkSpec } } - /// Fetching template definition with retry. - async fn fetch_template_definition(&self, template_address: TemplateAddress) -> anyhow::Result { - let min_wait_time = Duration::from_millis(100); - let max_wait_time = Duration::from_secs(5); - let wait_step = Duration::from_millis(500); - let mut current_wait_time = min_wait_time; - let network_interface = self.wallet_sdk.get_network_interface(); - loop { - match network_interface - .fetch_template_definition(template_address) - .await - .optional()? - { - Some(template_def) => { - info!(target: LOG_TARGET, "Fetched template definition for {template_address}"); - return Ok(template_def); - }, - None => { - info!(target: LOG_TARGET, "Template definition not found yet. retry after {:.2?}...", current_wait_time); - if self.shutdown_signal.is_triggered() { - return Err(anyhow!("shutdown during fetch template definition")); - } - tokio::time::sleep(current_wait_time).await; - if current_wait_time < max_wait_time { - current_wait_time = current_wait_time.add(wait_step); - } - }, - }; - } - } - async fn handle_wallet_event(&self, event: WalletEvent) -> anyhow::Result<()> { if let WalletEvent::TransactionFinalized(event) = event { - let Some(diff) = event.finalize.result.any_accept() else { + let Some(diff) = event.finalize.result.into_any_accept() else { return Ok(()); }; - for (id, substate) in diff.up_iter().filter(|(id, _)| id.is_template()) { + for (id, substate) in diff.into_up_iter().filter(|(id, _)| id.is_template()) { let template_address = id .as_template() .expect("is_template checked but as_template returned None"); @@ -82,22 +48,29 @@ where TSpec: WalletSdkSpec continue; } - let Some(template) = substate.substate_value().as_template() else { - error!(target: LOG_TARGET, "Diff contained a template substate ID {id} but the substate was type {}. This should not be possible", SubstateType::from(substate.substate_value())); + let substate_type = SubstateType::from(substate.substate_value()); + let Some(template) = substate.into_substate_value().into_template() else { + error!(target: LOG_TARGET, "Diff contained a template substate ID {id} but the substate was type {}. This should not be possible", substate_type); continue; }; - let template_definition = match self.fetch_template_definition(template_address.as_hash()).await { - Ok(template_definition) => template_definition, - Err(error) => { - error!(target: LOG_TARGET, "Failed to fetch template definition: {}", error); + + match task::spawn_blocking(move || { + WasmModule::load_template_from_code(&template.binary).map(|loaded| (template, loaded)) + }) + .await? + { + Ok((template, loaded)) => { + self.wallet_sdk.template_api().add_authored_template( + template.author, + template_address.as_hash(), + loaded.template_def().clone(), + )?; + }, + Err(err) => { + error!(target: LOG_TARGET, "Failed to load template {id} from transaction diff: {}", err); continue; }, - }; - self.wallet_sdk.template_api().add_authored_template( - template.author, - template_address.as_hash(), - template_definition, - )?; + } } } Ok(()) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx index 4b5167b5c8..a6b546eee7 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx @@ -107,7 +107,18 @@ function NftRow({ nft }: { nft: NonFungibleToken }) { const mutableData = convertCborValue(nft.mutable_data); const data = convertCborValue(nft.data); const imageUrl = mutableData?.image_url; - const originalOwner = data?.original_owner; + + const metadata: [string, string][] = []; + if (data && typeof data === "object") { + let limit = 5; + for (const [key, value] of Object.entries(data)) { + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + metadata.push([key, value.toString()]); + limit -= 1; + if (limit === 0) break; + } + } + } return ( @@ -137,7 +148,11 @@ function NftRow({ nft }: { nft: NonFungibleToken }) { - + {metadata.map(([key, value], index) => ( +
+ {key}: {value} +
+ ))}
diff --git a/clients/tari_indexer_client/src/rest_api_client.rs b/clients/tari_indexer_client/src/rest_api_client.rs index 01a1b23707..358ee87526 100644 --- a/clients/tari_indexer_client/src/rest_api_client.rs +++ b/clients/tari_indexer_client/src/rest_api_client.rs @@ -8,6 +8,7 @@ use tari_engine_types::{ template_lib_models::ResourceAddress, transaction_receipt::TransactionReceiptAddress, }; +use tari_template_lib_types::TemplateAddress; use crate::{ error::IndexerRestClientError, @@ -26,7 +27,6 @@ use crate::{ GetSubstateResponse, GetSubstatesRequest, GetSubstatesResponse, - GetTemplateDefinitionRequest, GetTemplateDefinitionResponse, GetTransactionReceiptResponse, GetTransactionResultRequest, @@ -145,9 +145,9 @@ impl IndexerRestApiClient { pub async fn get_template_definition( &mut self, - req: GetTemplateDefinitionRequest, + template_address: TemplateAddress, ) -> Result { - self.send_get(format!("templates/{}", req.template_address), ()).await + self.send_get(format!("templates/{template_address}"), ()).await } // pub async fn get_non_fungibles( diff --git a/clients/tari_indexer_client/src/types.rs b/clients/tari_indexer_client/src/types.rs index 916123042e..3a83ef0352 100644 --- a/clients/tari_indexer_client/src/types.rs +++ b/clients/tari_indexer_client/src/types.rs @@ -360,12 +360,6 @@ pub struct GetConnectionsResponse { pub connections: Vec, } -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] -pub struct GetTemplateDefinitionRequest { - pub template_address: TemplateAddress, -} - #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] pub struct GetTemplateDefinitionResponse { diff --git a/crates/common_types/src/versioned_substate_id.rs b/crates/common_types/src/versioned_substate_id.rs index 0a92e6b8ba..a2b9612900 100644 --- a/crates/common_types/src/versioned_substate_id.rs +++ b/crates/common_types/src/versioned_substate_id.rs @@ -18,7 +18,7 @@ pub struct SubstateRequirement { } impl SubstateRequirement { - pub fn new(address: SubstateId, version: Option) -> Self { + pub const fn new(address: SubstateId, version: Option) -> Self { Self { substate_id: address, version, diff --git a/crates/consensus/src/hotstuff/block_change_set.rs b/crates/consensus/src/hotstuff/block_change_set.rs index 0cdfe0f607..f49f47b2cf 100644 --- a/crates/consensus/src/hotstuff/block_change_set.rs +++ b/crates/consensus/src/hotstuff/block_change_set.rs @@ -55,7 +55,7 @@ const MEM_MAX_PROPOSED_UTXO_MINTS_SIZE: usize = 1000; #[derive(Debug, Clone)] pub struct BlockDecision { - pub quorum_decision: Option, + pub local_decision: Option, /// Contains newly-committed non-dummy blocks pub commit_blocks: Vec, pub finalized_transactions: Vec>, @@ -66,7 +66,7 @@ pub struct BlockDecision { impl BlockDecision { pub fn is_accept(&self) -> bool { - matches!(self.quorum_decision, Some(QuorumDecision::Accept)) + matches!(self.local_decision, Some(QuorumDecision::Accept)) } pub fn is_committed_epoch_end(&self) -> bool { @@ -90,7 +90,7 @@ impl BlockDecision { pub fn highest_qc_view(&self) -> NodeHeight { self.high_pc - .block_height() + .height() .max(self.new_high_tc.as_ref().map_or(NodeHeight(0), |tc| tc.height())) } } diff --git a/crates/consensus/src/hotstuff/common.rs b/crates/consensus/src/hotstuff/common.rs index be22b98344..7e48a4e014 100644 --- a/crates/consensus/src/hotstuff/common.rs +++ b/crates/consensus/src/hotstuff/common.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, iter, ops::ControlFlow}; use indexmap::IndexMap; use log::*; use tari_common_types::types::FixedHash; -use tari_consensus_types::{BlockId, HighPc, HighTc, LeafBlock, ProposalCertificate, QcId, ShardGroupAccumulatedData}; +use tari_consensus_types::{BlockId, HighPc, HighTc, LeafBlock, PcId, ProposalCertificate, ShardGroupAccumulatedData}; use tari_engine_types::{substate::SubstateDiff, ValidatorFeePool}; use tari_ootle_common_types::{ committee::{Committee, CommitteeInfo}, @@ -424,7 +424,7 @@ pub(crate) fn get_highest_seen_justified_view( ) -> Result { let high_pc = HighPc::get(tx, epoch) .optional()? - .map(|high_pc| high_pc.block_height()) + .map(|high_pc| high_pc.height()) .unwrap_or_default(); let high_tc = HighTc::get(tx, epoch) .optional()? @@ -437,7 +437,7 @@ pub(crate) fn get_highest_seen_justified_view( pub fn process_newly_justified_block( tx: &::ReadTransaction<'_>, new_leaf_block: &Block, - justify_id: QcId, + justify_id: PcId, local_committee_info: &CommitteeInfo, change_set: &mut ProposedBlockChangeSet, ) -> Result, HotStuffError> { @@ -446,7 +446,7 @@ pub fn process_newly_justified_block( tx: &::ReadTransaction<'_>, block: &Block, new_leaf_block: &LeafBlock, - justify_id: QcId, + justify_id: PcId, local_committee_info: &CommitteeInfo, change_set: &mut ProposedBlockChangeSet, ) -> Result<(), HotStuffError> { diff --git a/crates/consensus/src/hotstuff/error.rs b/crates/consensus/src/hotstuff/error.rs index 9430974ab6..f7540d7c40 100644 --- a/crates/consensus/src/hotstuff/error.rs +++ b/crates/consensus/src/hotstuff/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_common_types::types::FixedHash; -use tari_consensus_types::{BlockId, LeafBlock, QcId}; +use tari_consensus_types::{BlockId, LeafBlock, PcId, QcId}; use tari_epoch_manager::EpochManagerError; use tari_ootle_common_types::{Epoch, NodeHeight, ShardGroup, VersionedSubstateIdError, VotePower}; use tari_ootle_storage::{ @@ -167,6 +167,12 @@ pub enum ProposalValidationError { block_id: BlockId, details: String, }, + #[error("Justified block {justify_block} for proposed block {block_description} by {proposed_by} is parked")] + JustifyBlockParked { + proposed_by: String, + block_description: String, + justify_block: LeafBlock, + }, #[error("Candidate block {candidate_block_height} is not higher than justify {justify_block_height}")] CandidateBlockNotHigherThanJustify { justify_block_height: NodeHeight, @@ -286,7 +292,7 @@ pub enum ProposalValidationError { #[error("Invalid epoch in QC {qc_id} in {block_id}. Expected: {current_epoch}, given: {qc_epoch}")] InvalidEpochInQc { block_id: BlockId, - qc_id: QcId, + qc_id: PcId, qc_epoch: Epoch, current_epoch: Epoch, }, diff --git a/crates/consensus/src/hotstuff/foreign_proposal_processor.rs b/crates/consensus/src/hotstuff/foreign_proposal_processor.rs index ae818645a4..b3d705d113 100644 --- a/crates/consensus/src/hotstuff/foreign_proposal_processor.rs +++ b/crates/consensus/src/hotstuff/foreign_proposal_processor.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use log::*; -use tari_consensus_types::{BlockId, Decision, LeafBlock, ProposalCertificate, QcId, ValidatorSignatureBytes}; +use tari_consensus_types::{BlockId, Decision, LeafBlock, PcId, ProposalCertificate, ValidatorSignatureBytes}; use tari_crypto::tari_utilities::ByteArray; use tari_engine_types::commit_result::RejectReason; use tari_ootle_common_types::{ @@ -788,7 +788,7 @@ fn get_or_sequence_transaction( } } -fn calculate_qc_id_from_sidechain_qc(qc: &tari_sidechain::QuorumCertificate) -> QcId { +fn calculate_qc_id_from_sidechain_qc(qc: &tari_sidechain::QuorumCertificate) -> PcId { let signatures = qc .signatures .iter() diff --git a/crates/consensus/src/hotstuff/on_catch_up_sync.rs b/crates/consensus/src/hotstuff/on_catch_up_sync.rs index fa71300f4c..900241f75e 100644 --- a/crates/consensus/src/hotstuff/on_catch_up_sync.rs +++ b/crates/consensus/src/hotstuff/on_catch_up_sync.rs @@ -8,7 +8,7 @@ use tari_ootle_storage::{consensus_models::BookkeepingModel, StateStore}; use crate::{ hotstuff::{pacemaker_handle::PaceMakerHandle, HotStuffError}, - messages::{HotstuffMessage, SyncRequestMessage}, + messages::{CatchUpRequestMessage, HotstuffMessage}, traits::{ConsensusSpec, OutboundMessaging}, }; @@ -37,7 +37,7 @@ impl OnCatchUpSync { let high_qc = self.store.with_read_tx(|tx| HighPc::get(tx, epoch))?; let block_height = if high_qc.epoch() == epoch { - high_qc.block_height() + high_qc.height() } else { NodeHeight(1) }; @@ -60,7 +60,7 @@ impl OnCatchUpSync { .outbound_messaging .send( from, - HotstuffMessage::CatchUpSyncRequest(SyncRequestMessage { epoch, block_height }), + HotstuffMessage::CatchUpSyncRequest(CatchUpRequestMessage { epoch, block_height }), ) .await .is_err() diff --git a/crates/consensus/src/hotstuff/on_catch_up_sync_request.rs b/crates/consensus/src/hotstuff/on_catch_up_sync_request.rs index d86492b568..1d585bf566 100644 --- a/crates/consensus/src/hotstuff/on_catch_up_sync_request.rs +++ b/crates/consensus/src/hotstuff/on_catch_up_sync_request.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use log::*; -use tari_consensus_types::{LastProposed, LastSentVote, LeafBlock}; +use tari_consensus_types::{LastProposed, LeafBlock}; use tari_ootle_common_types::{optional::Optional, Epoch, NodeHeight}; use tari_ootle_storage::{ consensus_models::{Block, BookkeepingModel}, @@ -12,7 +12,7 @@ use tokio::task; use crate::{ hotstuff::HotStuffError, - messages::{HotstuffMessage, ProposalMessage, SyncRequestMessage}, + messages::{CatchUpRequestMessage, HotstuffMessage, ProposalMessage}, traits::{ConsensusSpec, OutboundMessaging}, }; @@ -33,7 +33,7 @@ impl OnSyncRequest { } #[allow(clippy::too_many_lines)] - pub fn handle(&self, from: TConsensusSpec::Addr, epoch: Epoch, msg: SyncRequestMessage) { + pub fn handle(&self, from: TConsensusSpec::Addr, epoch: Epoch, msg: CatchUpRequestMessage) { if msg.epoch != epoch { warn!( target: LOG_TARGET, @@ -66,12 +66,12 @@ impl OnSyncRequest { msg.epoch, leaf_block ); - return Ok(vec![]); + return Ok(None); } if leaf_block.height.is_zero() { info!(target: LOG_TARGET, "This node is at height 0 so cannot return any sync blocks. Ignoring request"); - return Ok(vec![]); + return Ok(None); } if leaf_block.height() < msg.block_height { @@ -91,89 +91,88 @@ impl OnSyncRequest { msg.block_height, leaf_block ); - // NOTE: We have to send dummy blocks, because the messaging will ignore heights > current_view + 1, - // until eventually the syncing node's pacemaker leader-fails a few times. - // TODO: A Block containing a higher QC that justifies the previous non-dummy block should be enough to cause a view change and allow the dummies to be generated locally. - // sending dummies is problematic as they are unsigned and generally, you cannot prove their validity. - let blocks = Block::get_all_blocks_between( - tx, - msg.epoch, - msg.block_height.max(NodeHeight(1)), - leaf_block.height(), - false, - 1000, - )?; - - Ok::<_, HotStuffError>(blocks) + Ok(Some(leaf_block)) }); - let blocks = match result { - Ok(mut blocks) => { - if let Some(pos) = blocks.iter().position(|b| b.is_genesis()) { - blocks.remove(pos); - } - blocks + let leaf_block = match result { + Ok(Some(leaf_block)) => leaf_block, + Ok(None) => { + return; }, Err(err) => { - warn!(target: LOG_TARGET, "Failed to fetch blocks for sync request: {}", err); + warn!(target: LOG_TARGET, "Failed to process sync request: {}", err); return; }, }; - info!( - target: LOG_TARGET, - "🌐 Sending {} block(s) ({} to {}) to {}", - blocks.len(), - blocks.first().map(|b| b.height()).unwrap_or_default(), - blocks.last().map(|b| b.height()).unwrap_or_default(), - from - ); + let mut start_height = msg.block_height.max(NodeHeight(1)); + while start_height < leaf_block.height() { + let result = store.with_read_tx(|tx| { + Block::get_all_blocks_between(tx, msg.epoch, start_height, leaf_block.height(), false, 100) + }); - for block in blocks { - info!( - target: LOG_TARGET, - "🌐 Sending block {} to {}", - block, - from - ); - // TODO(perf): O(n) queries - let foreign_proposals = match store.with_read_tx(|tx| block.get_foreign_proposals(tx)) { - Ok(foreign_proposals) => foreign_proposals, + let blocks = match result { + Ok(blocks) => blocks, Err(err) => { - warn!(target: LOG_TARGET, "Failed to fetch foreign proposals for block {}: {}", block, err); + warn!(target: LOG_TARGET, "Failed to fetch blocks for catch-up request: {}", err); return; }, }; - if let Err(err) = outbound_messaging - .send( - from.clone(), - HotstuffMessage::new_proposal(ProposalMessage { - block, - foreign_proposals: foreign_proposals.into_iter().map(|p| p.into_proposal()).collect(), - }), - ) - .await - { - warn!(target: LOG_TARGET, "Error sending SyncResponse: {err}"); + if blocks.is_empty() { + warn!( + target: LOG_TARGET, + "No blocks found between heights {} and {} for epoch {}", + start_height, + leaf_block.height(), + epoch + ); return; } - } + start_height = blocks + .last() + .map(|b| b.height() + NodeHeight(1)) + .unwrap_or(leaf_block.height()); - // Send last vote. - let maybe_last_vote = match store.with_read_tx(|tx| LastSentVote::get(tx, epoch)).optional() { - Ok(last_vote) => last_vote, - Err(err) => { - warn!(target: LOG_TARGET, "Failed to fetch last vote for catch-up request: {}", err); - return; - }, - }; - if let Some(last_vote) = maybe_last_vote { - if let Err(err) = outbound_messaging - .send(from.clone(), HotstuffMessage::Vote(last_vote.vote.into())) - .await - { - warn!(target: LOG_TARGET, "Failed to send LastVote {err}"); + info!( + target: LOG_TARGET, + "🌐 Sending {} block(s) ({} to {}) to {}", + blocks.len(), + blocks.first().map(|b| b.height()).unwrap_or_default(), + blocks.last().map(|b| b.height()).unwrap_or_default(), + from + ); + + for block in blocks { + // TODO(perf): O(n) queries + let foreign_proposals = match store.with_read_tx(|tx| block.get_foreign_proposals(tx)) { + Ok(foreign_proposals) => foreign_proposals, + Err(err) => { + warn!(target: LOG_TARGET, "Failed to fetch foreign proposals for block {}: {}", block, err); + return; + }, + }; + + debug!( + target: LOG_TARGET, + "🌐 Sending block {} to {}", + block, + from + ); + + if let Err(err) = outbound_messaging + .send( + from.clone(), + HotstuffMessage::new_proposal(ProposalMessage { + block, + foreign_proposals: foreign_proposals.into_iter().map(|p| p.into_proposal()).collect(), + }), + ) + .await + { + warn!(target: LOG_TARGET, "Error sending SyncResponse: {err}"); + return; + } } } }); diff --git a/crates/consensus/src/hotstuff/on_inbound_message.rs b/crates/consensus/src/hotstuff/on_inbound_message.rs index af9318decf..dc46820654 100644 --- a/crates/consensus/src/hotstuff/on_inbound_message.rs +++ b/crates/consensus/src/hotstuff/on_inbound_message.rs @@ -36,9 +36,13 @@ impl OnInboundMessage { &mut self, current_epoch: Epoch, current_height: NodeHeight, + has_processed_first_block: bool, ) -> Option> { // Then incoming messages for the current epoch/height - let result = self.message_buffer.next(current_epoch, current_height).await; + let result = self + .message_buffer + .next(current_epoch, current_height, has_processed_first_block) + .await; match result { Ok(Some((from, msg))) => { self.hooks.on_message_received(&msg); @@ -80,16 +84,28 @@ impl MessageBuffer { &mut self, current_epoch: Epoch, current_height: NodeHeight, + has_processed_first_block: bool, ) -> IncomingMessageResult { - // We listen for messages for the next view - let next_height = current_height; // + NodeHeight(1); - // Clear buffer with lower (epoch, heights) + let next_height = current_height + NodeHeight(1); + // Clear buffer with lower (epoch, heights) + let before_len = self.buffer.len(); self.buffer = self.buffer.split_off(&(current_epoch, next_height)); + let after_len = self.buffer.len(); - // Drain all buffered messages for the current view - if let Some(buffer) = self.buffer.get_mut(&(current_epoch, next_height)) { - if let Some(msg_tuple) = buffer.pop_front() { - return Ok(Some(msg_tuple)); + debug!( + target: LOG_TARGET, + "Next message for current view {}/{} (has_processed_first_block={}, discard={})", + current_epoch, + current_height, + has_processed_first_block, + before_len - after_len + ); + if has_processed_first_block { + // Drain all buffered messages for the current view + if let Some(buffer) = self.buffer.get_mut(&(current_epoch, next_height)) { + if let Some(msg_tuple) = buffer.pop_front() { + return Ok(Some(msg_tuple)); + } } } @@ -104,7 +120,7 @@ impl MessageBuffer { } } - match msg_relative_view(&msg, current_epoch, next_height) { + match msg_relative_view(&msg, current_epoch, current_height, has_processed_first_block) { MessageRelativeView::Current => { return Ok(Some((from, msg))); }, @@ -114,21 +130,21 @@ impl MessageBuffer { MessageRelativeView::Future { epoch, height } => { // Buffer message for future epoch/height if msg.proposal().is_some() { - info!(target: LOG_TARGET, "🔮 Proposal {msg} is for future view (Current view: {current_epoch}, {current_height})"); + info!(target: LOG_TARGET, "🔮 Proposal {msg} is for future view {height} (Current view: {current_epoch}, {current_height})"); } else { - info!(target: LOG_TARGET, "🔮 Message {msg} is for future view (Current view: {current_epoch}, {current_height})"); + info!(target: LOG_TARGET, "🔮 Message {msg} is for future view {height} (Current view: {current_epoch}, {current_height})"); } self.push_to_buffer(epoch, height, from, msg); }, MessageRelativeView::Discard => { - info!(target: LOG_TARGET, "🗑️ Discard non-applicable message {}. Current view {}/{}", msg, current_epoch, current_height); + warn!(target: LOG_TARGET, "🗑️ Discard non-applicable message {}. Current view {}/{}", msg, current_epoch, current_height); }, } } info!( target: LOG_TARGET, - "Inbound messaging has terminated. Current view: {}/{}", current_epoch, next_height + "Inbound messaging has terminated. Current view: {}/{}", current_epoch, current_height ); // Inbound messaging has terminated Ok(None) @@ -160,39 +176,87 @@ enum MessageRelativeView { } #[allow(clippy::too_many_lines)] -fn msg_relative_view(msg: &HotstuffMessage, current_epoch: Epoch, current_height: NodeHeight) -> MessageRelativeView { +fn msg_relative_view( + msg: &HotstuffMessage, + current_epoch: Epoch, + current_height: NodeHeight, + has_processed_first_block: bool, +) -> MessageRelativeView { match msg { HotstuffMessage::Proposal(msg) => { let next_height = current_height + NodeHeight(1); let epoch = msg.block.epoch(); let block_height = msg.block.height(); - let justify_height = msg.block.max_certificate_height(); - if epoch == current_epoch && - // Special case, the justify height and the current height are zero - ((current_height.is_zero() && justify_height.is_zero()) || - // The proposal (supposedly) justifies the next view change - justify_height >= next_height || - // The proposal is for the current view (catch up) - block_height == next_height || - // or, the timeout certificate height is higher than the current height - msg.block - .timeout_certificate() - .is_some_and(|t| t.height() >= current_height)) + let pc_height = msg.block.justify().height(); + + if epoch < current_epoch { + return MessageRelativeView::Past { + epoch: msg.block.epoch(), + height: pc_height, + }; + } + + if epoch == current_epoch + Epoch(1) { + return MessageRelativeView::Future { + epoch, + height: pc_height, + }; + } + + if epoch > current_epoch { + return MessageRelativeView::Discard; + } + + if pc_height <= current_height && + msg.block + .timeout_certificate() + .is_some_and(|tc| tc.height() > current_height) { return MessageRelativeView::Current; } - if epoch < current_epoch || (epoch == current_epoch && justify_height < next_height) { + if pc_height < current_height || (pc_height == current_height && block_height <= current_height) { return MessageRelativeView::Past { - epoch: msg.block.epoch(), - height: justify_height, + epoch, + height: pc_height, }; } - MessageRelativeView::Future { - epoch: msg.block.epoch(), - height: justify_height, + if has_processed_first_block { + if pc_height > next_height { + let msg_height = msg + .block + .timeout_certificate() + .map(|_| pc_height + NodeHeight(1)) + .unwrap_or(pc_height); + + return MessageRelativeView::Future { + epoch, + height: msg_height, + }; + } + } else { + // (a) Special case, the justify height and the current height are zero, and we have not processed the + // first block This is specifically to handle the case where we are starting from + // genesis and immediately run a catch up. The first and second blocks both are for view + // 1. If has_processed_first_block is false for the second block, we want to process it + // in the future not immediately (which would happen in (c) below). + // TODO: hacky + if pc_height == NodeHeight(1) && block_height == NodeHeight(2) { + return MessageRelativeView::Future { + epoch, + height: NodeHeight(1), + }; + } + if block_height > NodeHeight(1) { + return MessageRelativeView::Future { + epoch, + height: pc_height, + }; + } } + + MessageRelativeView::Current }, HotstuffMessage::Vote(msg) => { let vote = &msg.vote; diff --git a/crates/consensus/src/hotstuff/on_leader_timeout.rs b/crates/consensus/src/hotstuff/on_leader_timeout.rs index 9af5a4fbf1..6d2003611b 100644 --- a/crates/consensus/src/hotstuff/on_leader_timeout.rs +++ b/crates/consensus/src/hotstuff/on_leader_timeout.rs @@ -6,30 +6,45 @@ use std::sync::Arc; use tari_ootle_common_types::NodeHeight; use tokio::sync::watch; +#[derive(Debug, Clone, Copy, Default)] +pub struct LeaderTimeout { + pub current_height: NodeHeight, + pub current_high_pc: NodeHeight, + pub num_timeouts: u32, +} + +impl LeaderTimeout { + pub fn delta(&self) -> u64 { + self.current_height + .as_u64() + .saturating_sub(self.current_high_pc.as_u64()) + } +} + #[derive(Debug, Clone)] pub struct OnLeaderTimeout { // todo: consider using a different sync construct, like an mpsc channel - receiver: watch::Receiver, - sender: Arc>, + receiver: watch::Receiver, + sender: Arc>, } impl OnLeaderTimeout { pub fn new() -> Self { - let (sender, receiver) = watch::channel(NodeHeight::zero()); + let (sender, receiver) = watch::channel(LeaderTimeout::default()); Self { receiver, sender: Arc::new(sender), } } - pub async fn wait(&mut self) -> NodeHeight { + pub async fn wait(&mut self) -> LeaderTimeout { self.receiver.changed().await.expect("sender can never be dropped"); // This could lead to a more recent value being seen. Idk if that is ok... *self.receiver.borrow() } - pub fn leader_timed_out(&self, new_height: NodeHeight) { - self.sender.send(new_height).expect("receiver can never be dropped") + pub fn leader_timed_out(&self, timeout: LeaderTimeout) { + self.sender.send(timeout).expect("receiver can never be dropped") } } diff --git a/crates/consensus/src/hotstuff/on_next_sync_view.rs b/crates/consensus/src/hotstuff/on_next_sync_view.rs index 6b71de8ebe..44e2e73463 100644 --- a/crates/consensus/src/hotstuff/on_next_sync_view.rs +++ b/crates/consensus/src/hotstuff/on_next_sync_view.rs @@ -29,6 +29,7 @@ pub struct OnNextSyncViewHandler { outbound_messaging: TConsensusSpec::OutboundMessaging, leader_strategy: TConsensusSpec::LeaderStrategy, signer_service: TConsensusSpec::SignerService, + last_sent_new_view: Option<(Epoch, NodeHeight)>, } impl OnNextSyncViewHandler { @@ -43,6 +44,7 @@ impl OnNextSyncViewHandler { outbound_messaging, leader_strategy, signer_service, + last_sent_new_view: None, } } @@ -59,10 +61,9 @@ impl OnNextSyncViewHandler { let leaf_block = LeafBlock::get(tx, epoch)?; // If we leader failure more than once in a row, propose the next higher view - let last_sent_new_view = LastSentNewView::get(tx, epoch).optional()?; - if let Some(last_sent_new_view) = last_sent_new_view { - if last_sent_new_view.height() >= timeout_height { - timeout_height = last_sent_new_view.height() + NodeHeight(1); + if let Some((nv_epoch, last_sent_new_view)) = self.last_sent_new_view { + if nv_epoch == epoch && last_sent_new_view >= timeout_height { + timeout_height = last_sent_new_view + NodeHeight(1); } } let next_leader = get_leader_for_view( @@ -78,6 +79,7 @@ impl OnNextSyncViewHandler { let last_sent_vote = LastSentVote::get(tx, epoch) .optional()? .filter(|vote| high_pc.height() < vote.block_height()); + Ok::<_, HotStuffError>((next_leader, high_pc, last_sent_vote, timeout_height)) })?; @@ -115,6 +117,7 @@ impl OnNextSyncViewHandler { .send(next_leader.address.clone(), HotstuffMessage::new_newview(message)) .await?; + self.last_sent_new_view = Some((epoch, timeout_height)); self.store.with_write_tx(|tx| { LastSentNewView { epoch, diff --git a/crates/consensus/src/hotstuff/on_propose.rs b/crates/consensus/src/hotstuff/on_propose.rs index 4555785f56..db17567316 100644 --- a/crates/consensus/src/hotstuff/on_propose.rs +++ b/crates/consensus/src/hotstuff/on_propose.rs @@ -341,7 +341,7 @@ where TConsensusSpec: ConsensusSpec self.config.consensus_constants.num_preshards, ); - debug!(target: LOG_TARGET, "🌿 PROPOSE: {batch}"); + debug!(target: LOG_TARGET, "🌿 PROPOSE: {} (justify: {}) {batch}", highest_seen_block.height(), justify_block.height()); let mut executed_transactions = HashMap::new(); let mut commands = if can_propose_epoch_end { BTreeSet::from_iter([Command::EndEpoch]) diff --git a/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs b/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs index 93fe4d3c71..de378266dd 100644 --- a/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs +++ b/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs @@ -4,7 +4,7 @@ use std::num::NonZeroU64; use log::*; -use tari_consensus_types::{Decision, LastVoted, LeafBlock, QcId}; +use tari_consensus_types::{Decision, LastVoted, LeafBlock, PcId}; use tari_crypto::ristretto::RistrettoPublicKey; use tari_engine_types::commit_result::{AbortReason, RejectReason}; use tari_ootle_common_types::{committee::CommitteeInfo, optional::Optional, Epoch, ShardGroup}; @@ -166,8 +166,11 @@ where TConsensusSpec: ConsensusSpec } if self.should_vote(tx, valid_block.block())? { + let parent = valid_block.block().get_parent(&**tx)?; + self.decide_what_to_vote( tx, + &parent, valid_block.block(), local_committee_info, proposer_claim_public_key_bytes, @@ -178,35 +181,6 @@ where TConsensusSpec: ConsensusSpec change_set.set_no_vote(NoVoteReason::AlreadyVotedAtHeight); } - // if change_set.is_accept() { - // Update high TC - // maybe_high_tc = valid_block - // .block() - // .timeout_certificate() - // .map(|tc| tc.update_highest(tx)) - // .transpose()?; - // - // // Update nodes - // let high_qc = valid_block.block().update_nodes( - // tx, - // |tx, _prev_locked, block, _justify_qc| self.on_lock_block(tx, block), - // |tx, mut commit_block| { - // let committed = self.on_commit(tx, &block_qc_id, &commit_block, local_committee_info)?; - // // NOTE: update the commit QC in the local copy so that foreign proposals can obtain the commit QC - // // on_commit already sets the persisted commit_qc for the block - // commit_block.set_commit_qc(block_qc_id); - // if !commit_block.is_dummy() { - // commit_blocks.push(commit_block); - // } - // if !committed.is_empty() { - // finalized_transactions.push(committed); - // } - // Ok(()) - // }, - // )?; - // - // maybe_high_qc = Some(high_qc); - let quorum_decision = change_set.quorum_decision(); if change_set.is_accept() { info!( @@ -224,12 +198,8 @@ where TConsensusSpec: ConsensusSpec ); } - // let high_qc = maybe_high_qc - // .map(Ok) - // .unwrap_or_else(|| HighPc::get(&**tx, valid_block.epoch()))?; - Ok(BlockDecision { - quorum_decision, + local_decision: quorum_decision, commit_blocks, finalized_transactions, high_pc: high_qc, @@ -269,6 +239,7 @@ where TConsensusSpec: ConsensusSpec fn decide_what_to_vote( &self, tx: &::ReadTransaction<'_>, + parent: &Block, block: &Block, local_committee_info: &CommitteeInfo, proposer_claim_public_key_bytes: &RistrettoPublicKeyBytes, @@ -280,6 +251,7 @@ where TConsensusSpec: ConsensusSpec let mut substate_store = PendingSubstateStore::new(tx, block.as_leaf(), self.config.consensus_constants.num_preshards); let mut total_leader_fee = 0; + let mut total_exhaust_burn = parent.header().total_accumulated_exhaust_burn(); for cmd in block.commands() { match cmd { @@ -292,6 +264,7 @@ where TConsensusSpec: ConsensusSpec &mut substate_store, proposed_block_change_set, &mut total_leader_fee, + &mut total_exhaust_burn, )? { proposed_block_change_set.set_no_vote(reason); return Ok(()); @@ -332,6 +305,7 @@ where TConsensusSpec: ConsensusSpec &mut substate_store, proposed_block_change_set, &mut total_leader_fee, + &mut total_exhaust_burn, )? { proposed_block_change_set.set_no_vote(reason); return Ok(()); @@ -444,6 +418,18 @@ where TConsensusSpec: ConsensusSpec return Ok(()); } + if total_exhaust_burn != block.header().total_accumulated_exhaust_burn() { + warn!( + target: LOG_TARGET, + "❌ Exhaust burn disagreement for block {}. Leader proposed {}, we calculated {}", + block, + block.header().total_accumulated_exhaust_burn(), + total_exhaust_burn + ); + proposed_block_change_set.set_no_vote(NoVoteReason::TotalExhaustBurnDisagreement); + return Ok(()); + } + // Apply leader fee to substate store before we calculate the state root if total_leader_fee > 0 { apply_leader_fee_to_substate_store( @@ -505,6 +491,7 @@ where TConsensusSpec: ConsensusSpec substate_store: &mut PendingSubstateStore, proposed_block_change_set: &mut ProposedBlockChangeSet, total_leader_fee: &mut u64, + total_exhaust_burn: &mut u128, ) -> Result, HotStuffError> { let _timer = TraceTimer::info(LOG_TARGET, "Evaluate LocalOnly command"); let Some(mut pool_tx) = proposed_block_change_set @@ -620,6 +607,7 @@ where TConsensusSpec: ConsensusSpec } *total_leader_fee += calculated_leader_fee.fee(); + *total_exhaust_burn += u128::from(calculated_leader_fee.exhaust_burn()); } proposed_block_change_set.add_transaction_execution(*pool_tx.transaction_id(), execution)?; @@ -1137,6 +1125,7 @@ where TConsensusSpec: ConsensusSpec substate_store: &mut PendingSubstateStore, proposed_block_change_set: &mut ProposedBlockChangeSet, total_leader_fee: &mut u64, + total_exhaust_burn: &mut u128, ) -> Result, HotStuffError> { if atom.decision.is_abort() { warn!( @@ -1295,6 +1284,7 @@ where TConsensusSpec: ConsensusSpec })?; *total_leader_fee += leader_fee.fee(); + *total_exhaust_burn += u128::from(leader_fee.exhaust_burn()); substate_store.put_diff(&filter_diff_for_committee(local_committee_info, diff))?; @@ -1487,7 +1477,7 @@ where TConsensusSpec: ConsensusSpec fn on_commit( &self, tx: &mut ::WriteTransaction<'_>, - commit_qc_id: &QcId, + commit_qc_id: &PcId, block: &Block, ) -> Result, HotStuffError> { let committed_transactions = self.finalize_block(tx, commit_qc_id, block)?; @@ -1541,7 +1531,7 @@ where TConsensusSpec: ConsensusSpec fn finalize_block( &self, tx: &mut ::WriteTransaction<'_>, - commit_qc_id: &QcId, + commit_qc_id: &PcId, block: &Block, ) -> Result, HotStuffError> { if block.is_dummy() { diff --git a/crates/consensus/src/hotstuff/on_receive_local_proposal.rs b/crates/consensus/src/hotstuff/on_receive_local_proposal.rs index 7f35bf3c79..fe0e638d10 100644 --- a/crates/consensus/src/hotstuff/on_receive_local_proposal.rs +++ b/crates/consensus/src/hotstuff/on_receive_local_proposal.rs @@ -9,9 +9,9 @@ use tari_consensus_types::{ HighPc, HighestSeenBlock, LastSentVote, + PcId, ProposalCertificate, ProposalVote, - QcId, TimeoutVote, TimeoutVoteMessage, ValidatorSignatureBytes, @@ -35,6 +35,7 @@ use tari_ootle_storage::{ ValidBlock, }, StateStore, + StateStoreReadTransaction, }; use tari_sidechain::{ProposalCertificateSignatureFields, QuorumDecision}; use tari_template_lib_types::crypto::RistrettoPublicKeyBytes; @@ -369,13 +370,13 @@ impl OnReceiveLocalProposalHandler OnReceiveLocalProposalHandler OnReceiveLocalProposalHandler b, + None => { + if tx.parked_block_exists(&justify_block_id)? { + // This case shouldnt happen because the message buffer should not yet send the proposal through + warn!(target: LOG_TARGET, "⚠️ BUG: Justify block {} for candidate block {} is parked", justify_block_id, candidate_block); + return Err(ProposalValidationError::JustifyBlockParked { + proposed_by: candidate_block.proposed_by().to_string(), + block_description: candidate_block.to_string(), + justify_block: candidate_block.justify().as_leaf_block(), + } + .into()); + } // This will trigger a catch-up sync - ProposalValidationError::JustifyBlockNotFound { + return Err(ProposalValidationError::JustifyBlockNotFound { proposed_by: candidate_block.proposed_by().to_string(), block_description: candidate_block.to_string(), justify_block: candidate_block.justify().as_leaf_block(), } - })? + .into()); + }, + } }; if candidate_block.justifies_parent() && !candidate_block.parent_exists(tx)? { diff --git a/crates/consensus/src/hotstuff/on_receive_new_view.rs b/crates/consensus/src/hotstuff/on_receive_new_view.rs index db5a95c2aa..ebb2367a65 100644 --- a/crates/consensus/src/hotstuff/on_receive_new_view.rs +++ b/crates/consensus/src/hotstuff/on_receive_new_view.rs @@ -184,7 +184,11 @@ where TConsensusSpec: ConsensusSpec current_epoch: epoch_state.epoch(), }); } - check_quorum_certificate_signatures::(qc, epoch_state.local_committee(), vote_signing_service)?; + check_quorum_certificate_signatures::( + qc.into(), + epoch_state.local_committee(), + vote_signing_service, + )?; Ok(()) } } diff --git a/crates/consensus/src/hotstuff/on_receive_vote.rs b/crates/consensus/src/hotstuff/on_receive_vote.rs index 8be54954fd..d39ff8b3fc 100644 --- a/crates/consensus/src/hotstuff/on_receive_vote.rs +++ b/crates/consensus/src/hotstuff/on_receive_vote.rs @@ -55,10 +55,10 @@ where TConsensusSpec: ConsensusSpec .check_and_collect_vote(from, current_height, epoch_state, message.vote) .await { - Ok(Some((_, high_qc))) => { + Ok(Some((_, high_pc))) => { // Reset the leader timeout (not the block timer) - this mitigates the chance of our node sending a // NEWVIEW just before we are ready to propose - self.pacemaker.reset_leader_timeout(high_qc.block_height()).await?; + self.pacemaker.reset_leader_timeout(&high_pc).await?; // We've reached quorum, trigger a check to see if we should propose immediately self.pacemaker.beat(); }, diff --git a/crates/consensus/src/hotstuff/pacemaker.rs b/crates/consensus/src/hotstuff/pacemaker.rs index ae3229f6ef..84c1f5161c 100644 --- a/crates/consensus/src/hotstuff/pacemaker.rs +++ b/crates/consensus/src/hotstuff/pacemaker.rs @@ -14,7 +14,7 @@ use crate::hotstuff::{ current_view::CurrentView, on_beat::OnBeat, on_force_beat::OnForceBeat, - on_leader_timeout::OnLeaderTimeout, + on_leader_timeout::{LeaderTimeout, OnLeaderTimeout}, pacemaker_handle::{PaceMakerHandle, PacemakerRequest}, HotStuffError, }; @@ -72,6 +72,7 @@ impl PaceMaker { }); } + #[allow(clippy::too_many_lines)] pub async fn run( &mut self, on_beat: OnBeat, @@ -84,6 +85,7 @@ impl PaceMaker { tokio::pin!(leader_timeout); tokio::pin!(block_timer); + let mut num_timeouts = 0u32; let mut started = false; let mut leader_failure_suspended = false; let mut leader_failure_triggered_during_suspension = false; @@ -98,6 +100,7 @@ impl PaceMaker { if !started { continue; } + num_timeouts = 0; leader_failure_suspended = false; leader_failure_triggered_during_suspension = false; @@ -151,9 +154,15 @@ impl PaceMaker { leader_failure_triggered_during_suspension = false; leader_timeout.as_mut().reset(self.leader_timeout()); info!(target: LOG_TARGET, "⚠️ Resumed leader timeout! Current view: {}, Delta: {:.2?}", self.current_view, self.delta_time()); - on_leader_timeout.leader_timed_out(self.current_view.get_height()); + num_timeouts += 1; + on_leader_timeout.leader_timed_out(LeaderTimeout { + current_height: self.current_view.get_height(), + current_high_pc: self.current_high_pc_height, + num_timeouts, + }); + } - debug!(target: LOG_TARGET, "🧿 Pacemaker resume"); + debug!(target: LOG_TARGET, "🧿 Pacemaker resume {}", self.current_view); } } } else{ @@ -174,7 +183,12 @@ impl PaceMaker { leader_failure_triggered_during_suspension = true; } else { info!(target: LOG_TARGET, "⚠️ Leader timeout! Current view: {}, Delta: {:.2?}", self.current_view, self.delta_time()); - on_leader_timeout.leader_timed_out(self.current_view.get_height()); + num_timeouts += 1; + on_leader_timeout.leader_timed_out(LeaderTimeout { + current_height: self.current_view.get_height(), + current_high_pc: self.current_high_pc_height, + num_timeouts, + }); } }, diff --git a/crates/consensus/src/hotstuff/pacemaker_handle.rs b/crates/consensus/src/hotstuff/pacemaker_handle.rs index 21de6132ed..b5447018fa 100644 --- a/crates/consensus/src/hotstuff/pacemaker_handle.rs +++ b/crates/consensus/src/hotstuff/pacemaker_handle.rs @@ -1,6 +1,7 @@ // Copyright 2022 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use tari_consensus_types::HighPc; use tari_ootle_common_types::{Epoch, NodeHeight}; use tokio::sync::mpsc; @@ -99,14 +100,15 @@ impl PaceMakerHandle { self.on_leader_timeout.clone() } - pub async fn reset_leader_timeout(&self, high_pc_height: NodeHeight) -> Result<(), HotStuffError> { + pub async fn reset_leader_timeout(&self, high_pc: &HighPc) -> Result<(), HotStuffError> { self.sender .send(PacemakerRequest::Reset { - high_pc_height: Some(high_pc_height), + high_pc_height: Some(high_pc.height()), reset_block_time: false, }) .await - .map_err(|e| HotStuffError::PacemakerChannelDropped { details: e.to_string() }) + .map_err(|e| HotStuffError::PacemakerChannelDropped { details: e.to_string() })?; + Ok(()) } async fn reset(&self, high_pc_height: NodeHeight) -> Result<(), HotStuffError> { @@ -119,7 +121,8 @@ impl PaceMakerHandle { .map_err(|e| HotStuffError::PacemakerChannelDropped { details: e.to_string() }) } - /// Reset the leader timeout. This should be called when a valid leader proposal is received. + /// Reset the leader timeout. This should be called when a valid leader proposal is received. If the provided view < + /// current view, this is a no-op. pub async fn enter_view( &self, epoch: Epoch, diff --git a/crates/consensus/src/hotstuff/vote_collector/collector.rs b/crates/consensus/src/hotstuff/vote_collector/collector.rs index ea5268b72b..60ea9705e2 100644 --- a/crates/consensus/src/hotstuff/vote_collector/collector.rs +++ b/crates/consensus/src/hotstuff/vote_collector/collector.rs @@ -14,7 +14,7 @@ use tari_ootle_common_types::{committee::Committee, Epoch, NodeAddressable, Node use tari_sidechain::QuorumDecision; use tokio::sync::RwLock; -const LOG_TARGET: &str = "tari::consensus::hotstuff::vote_collector"; +const LOG_TARGET: &str = "tari::ootle::consensus::hotstuff::vote_collector"; #[derive(Clone)] pub struct VoteCollector { @@ -52,11 +52,11 @@ impl VoteCollector { return None; } - let quorum_threshold = committee.quorum_threshold(); let threshold_decision = access_mut.calculate_threshold_decision(epoch, height, &key, committee); + let quorum_threshold = committee.quorum_threshold(); let Some(quorum_decision) = threshold_decision.decision else { - info!( + debug!( target: LOG_TARGET, "🔥 Received {} from {} ({} of {}).", vote_display, @@ -70,7 +70,7 @@ impl VoteCollector { // We only generate the next qc once when we have a quorum of votes. Any votes received after this // are not included in the QC. if threshold_decision.total_power < quorum_threshold { - info!( + debug!( target: LOG_TARGET, "🔥 Received {} from {} ({} of {}).", vote_display, @@ -81,7 +81,7 @@ impl VoteCollector { return None; } - info!( + debug!( target: LOG_TARGET, "🔥 Received {} from {} ({} of {}). QUORUM!", vote_display, @@ -235,3 +235,91 @@ pub struct ThresholdDecision { pub decision: Option, pub total_power: VotePower, } + +#[cfg(test)] +mod tests { + use tari_consensus_types::{SignedMessage, ToSignatureMessage}; + use tari_template_lib_types::crypto::{RistrettoPublicKeyBytes, SchnorrSignatureBytes}; + + use super::*; + + const ZERO_SIG: SchnorrSignatureBytes = SchnorrSignatureBytes::zero(); + const ZERO_PUBKEY: RistrettoPublicKeyBytes = RistrettoPublicKeyBytes::zero(); + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + struct TestVote { + epoch: Epoch, + height: NodeHeight, + key: u32, + } + + impl Display for TestVote { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } + } + + impl ToSignatureMessage for TestVote { + fn to_signature_message(&self) -> FixedHash { + FixedHash::zero() + } + } + + impl SignedMessage for TestVote { + fn signature(&self) -> &SchnorrSignatureBytes { + &ZERO_SIG + } + + fn public_key(&self) -> &RistrettoPublicKeyBytes { + &ZERO_PUBKEY + } + } + + impl Vote for TestVote { + type Key = u32; + + fn epoch(&self) -> Epoch { + self.epoch + } + + fn height(&self) -> NodeHeight { + self.height + } + + fn key(&self) -> Self::Key { + self.key + } + + fn decision(&self) -> QuorumDecision { + QuorumDecision::Accept + } + } + + #[test] + fn it_saves_a_new_vote() { + let mut store = VoteStoreInner::::new(); + let vote = TestVote { + epoch: Epoch(1), + height: NodeHeight(1), + key: 42, + }; + let result = store.save_vote(FixedHash::zero(), vote.clone()); + assert!(result, "Expected to save a new vote successfully"); + } + + #[test] + fn it_detects_a_duplicate_vote() { + let mut store = VoteStoreInner::::new(); + let vote = TestVote { + epoch: Epoch(1), + height: NodeHeight(1), + key: 42, + }; + let result = store.save_vote(FixedHash::zero(), vote.clone()); + assert!(result, "Expected to save a new vote successfully"); + + // Try to save the same vote again + let result_duplicate = store.save_vote(FixedHash::zero(), vote); + assert!(!result_duplicate, "Expected duplicate vote to be rejected"); + } +} diff --git a/crates/consensus/src/hotstuff/vote_collector/proposal_collector.rs b/crates/consensus/src/hotstuff/vote_collector/proposal_collector.rs index c4d1dcf35a..2ae0d05278 100644 --- a/crates/consensus/src/hotstuff/vote_collector/proposal_collector.rs +++ b/crates/consensus/src/hotstuff/vote_collector/proposal_collector.rs @@ -68,6 +68,12 @@ where TConsensusSpec: ConsensusSpec let sender_vn = check_eligibility::(&self.epoch_manager, from, &vote, local_committee_info).await?; self.validate_vote(current_epoch, &vote)?; + debug!( + target: LOG_TARGET, + "✅ Vote from {} for block {} is valid", + sender_vn, + block_id + ); let sender_leaf_hash = sender_vn.get_node_hash(self.network); let Some((quorum_votes, quorum_decision)) = self .vote_collector diff --git a/crates/consensus/src/hotstuff/vote_collector/timeout_collector.rs b/crates/consensus/src/hotstuff/vote_collector/timeout_collector.rs index afb981ac88..fd78605edf 100644 --- a/crates/consensus/src/hotstuff/vote_collector/timeout_collector.rs +++ b/crates/consensus/src/hotstuff/vote_collector/timeout_collector.rs @@ -13,7 +13,7 @@ use crate::{ traits::{CertificateStore, ConsensusSpec, ValidatorSignatureVerifierService}, }; -const LOG_TARGET: &str = "tari::consensus::hotstuff::timeout_collector"; +const LOG_TARGET: &str = "tari::ootle::consensus::hotstuff::timeout_collector"; #[derive(Clone)] pub struct TimeoutVoteCollector { @@ -75,6 +75,7 @@ where TConsensusSpec: ConsensusSpec ) .await else { + debug!(target: LOG_TARGET, "🟡 No quorum reached yet for TimeoutVote at height {}", height); return Ok(None); }; diff --git a/crates/consensus/src/hotstuff/worker.rs b/crates/consensus/src/hotstuff/worker.rs index 73997eae55..af9a98b648 100644 --- a/crates/consensus/src/hotstuff/worker.rs +++ b/crates/consensus/src/hotstuff/worker.rs @@ -13,12 +13,14 @@ use tari_consensus_types::{ HighTc, HighestSeenBlock, LastProposed, + LastSentVote, + LeafBlock, + PcId, ProposalCertificate, - QcId, TimeoutCertificate, }; use tari_epoch_manager::{EpochManagerEvent, EpochManagerReader}; -use tari_ootle_common_types::{optional::Optional, Epoch, NodeHeight, ShardGroup}; +use tari_ootle_common_types::{displayable::Displayable, optional::Optional, Epoch, NodeHeight, ShardGroup}; use tari_ootle_storage::{ consensus_models::{ Block, @@ -51,6 +53,7 @@ use crate::{ on_catch_up_sync::OnCatchUpSync, on_catch_up_sync_request::OnSyncRequest, on_inbound_message::OnInboundMessage, + on_leader_timeout::LeaderTimeout, on_message_validate::{MessageValidationResult, OnMessageValidate}, on_next_sync_view::OnNextSyncViewHandler, on_propose::OnPropose, @@ -95,6 +98,7 @@ pub struct HotstuffWorker { on_propose: OnPropose, on_sync_request: OnSyncRequest, on_catch_up_sync: OnCatchUpSync, + worker_state: WorkerState, state_store: TConsensusSpec::StateStore, leader_strategy: TConsensusSpec::LeaderStrategy, @@ -220,6 +224,7 @@ impl HotstuffWorker { on_sync_request: OnSyncRequest::new(state_store.clone(), outbound_messaging.clone()), on_catch_up_sync: OnCatchUpSync::new(state_store.clone(), pacemaker.clone_handle(), outbound_messaging), + worker_state: WorkerState::default(), state_store, leader_strategy, @@ -244,22 +249,21 @@ impl HotstuffWorker { let local_committee_info = self.epoch_manager.get_local_committee_info(current_epoch).await?; self.create_genesis_block_if_required(current_epoch, current_epoch_hash, local_committee_info.shard_group())?; - // Resume pacemaker from the last epoch/height - let current_height = self - .state_store - .with_read_tx(|tx| get_highest_seen_justified_view(tx, current_epoch))?; + let (current_height, leaf_height) = self.state_store.with_read_tx(|tx| { + let height = get_highest_seen_justified_view(tx, current_epoch)?; + let leaf_block = LeafBlock::get(tx, current_epoch)?; + Ok::<_, HotStuffError>((height, leaf_block.height())) + })?; - info!( - target: LOG_TARGET, - "🚀 Pacemaker starting for epoch {}, height: {}", - current_epoch, - current_height, - ); + self.worker_state.has_processed_first_block = !leaf_height.is_zero(); self.pacemaker .start(current_epoch, current_height, current_height) .await?; + + info!(target: LOG_TARGET, "🚀 Pacemaker started at epoch {}, height: {}", current_epoch, current_height); + self.publish_event(HotstuffEvent::EpochChanged { epoch: current_epoch, registered_shard_group: Some(local_committee_info.shard_group()), @@ -300,14 +304,13 @@ impl HotstuffWorker { let mut prev_height = self.pacemaker.current_view().get_height(); let current_epoch = self.pacemaker.current_view().get_epoch(); - // self.request_initial_catch_up_sync(current_epoch).await?; let mut local_claim_public_key = self .epoch_manager .get_our_validator_node(current_epoch) .await? .fee_claim_public_key; - self.request_initial_catch_up_sync(&epoch_state).await?; + self.request_catch_up_sync(&epoch_state).await?; loop { let current_height = self.pacemaker.current_view().get_height(); @@ -363,7 +366,7 @@ impl HotstuffWorker { } }, - Some(result) = self.on_inbound_message.next_message(epoch_state.epoch(), current_height) => { + Some(result) = self.on_inbound_message.next_message(epoch_state.epoch(), current_height, self.worker_state.has_processed_first_block) => { if let Err(e) = self.on_unvalidated_message(&epoch_state, current_height, result).await { self.on_failure("on_unvalidated_message", &e).await; return Err(e); @@ -381,8 +384,8 @@ impl HotstuffWorker { } }, - _ = on_leader_timeout.wait() => { - if let Err(e) = self.on_leader_timeout(&epoch_state, current_height).await { + timeout = on_leader_timeout.wait() => { + if let Err(e) = self.on_leader_timeout(&epoch_state, current_height, Some(timeout)).await { self.on_failure("on_leader_timeout", &e).await; return Err(e); } @@ -422,7 +425,7 @@ impl HotstuffWorker { .dispatch_hotstuff_message(epoch_state, current_height, from, msg) .await { - return self.handle_hotstuff_error(epoch_state, None, e).await; + return self.handle_hotstuff_error(current_height, epoch_state, None, e).await; } Ok(()) }, @@ -476,7 +479,7 @@ impl HotstuffWorker { error!(target: LOG_TARGET, "🚨 Invalid message from {from}: {err} - {message}"); } - self.handle_hotstuff_error(epoch_state, None, err).await + self.handle_hotstuff_error(current_height, epoch_state, None, err).await }, } } @@ -596,7 +599,7 @@ impl HotstuffWorker { Ok(()) } - async fn request_initial_catch_up_sync( + async fn request_catch_up_sync( &mut self, epoch_state: &EpochState, ) -> Result<(), HotStuffError> { @@ -645,12 +648,31 @@ impl HotstuffWorker { &mut self, epoch_state: &EpochState, current_height: NodeHeight, + timeout: Option, ) -> Result<(), HotStuffError> { self.hooks.on_leader_timeout(current_height); - info!(target: LOG_TARGET, "⚠️ {} Leader failure: NEXTSYNCVIEW for epoch {} and current height {}", self.local_validator_addr, epoch_state.epoch(), current_height); + info!( + target: LOG_TARGET, + "⚠️ {} Leader failure: NEXTSYNCVIEW for epoch {} and current height {} (timeout: {})", + self.local_validator_addr, + epoch_state.epoch(), + current_height, + timeout.as_ref().map(|t| t.num_timeouts).display(), + ); self.on_next_sync_view .handle(epoch_state.epoch(), current_height, epoch_state.local_committee()) .await?; + // If we've gone into 3 leader failures in a row, request a catch up sync + if !self.worker_state.is_catching_up() && timeout.is_some_and(|t| t.num_timeouts.is_multiple_of(3)) { + warn!( + target: LOG_TARGET, + "⚠️ {} Leader timeout count is {}. Requesting catch up sync.", + self.local_validator_addr, + timeout.as_ref().map(|t| t.num_timeouts).display() + ); + self.request_catch_up_sync(epoch_state).await?; + } + self.publish_event(HotstuffEvent::LeaderTimeout { height: current_height }); Ok(()) } @@ -812,6 +834,31 @@ impl HotstuffWorker { is_timeout: bool, local_claim_public_key: RistrettoPublicKeyBytes, ) -> Result<(), HotStuffError> { + // If we're catching up, we don't propose + if self.worker_state.is_catching_up() { + info!( + target: LOG_TARGET, + "⤵️ [propose_now] {} Currently catching up, will not propose at height ({})", + self.local_validator_addr, + next_height + ); + return Ok(()); + } + let last_sent_vote = self + .state_store + .with_read_tx(|tx| LastSentVote::get(tx, epoch_state.epoch())) + .optional()?; + + if last_sent_vote.is_some_and(|vote| vote.block_height() >= next_height) { + info!( + target: LOG_TARGET, + "⤵️ [propose_now] {} Already sent vote at height ({}). Not proposing", + self.local_validator_addr, + next_height + ); + return Ok(()); + } + // We use the highest seen block - specifically to handle the case where a block is proposed and locally // accepted, however, for whatever reason, a new certificate could not be created for it. We still use // it at the parent for this block, subsequent certificates will justify it. @@ -969,17 +1016,60 @@ impl HotstuffWorker { "on_receive_local_proposal", self.on_receive_local_proposal.handle(epoch_state, msg).await, ) { - Ok(None) | Ok(Some(NoVoteReason::AlreadyVotedAtHeight)) => Ok(()), - Ok(Some(_)) => { - // We decided NOVOTE, so we immediately send a NEWVIEW - self.on_leader_timeout(epoch_state, current_height).await + Ok(no_vote) => { + if let Some(mut catch_up) = self.worker_state.catch_up.take() { + if current_height >= catch_up.expected_batch_height { + if catch_up.set_next_batch(current_height) { + info!( + target: LOG_TARGET, + "⏳ Still catching up... current height: {}, expected up to: {}", + current_height, + catch_up.expected_batch_height, + ); + // TODO: the sender should probably only send one batch, and we request more + // self.on_catch_up_sync + // .request_sync(epoch_state.epoch(), catch_up.from.clone()) + // .await?; + self.worker_state.catch_up = Some(catch_up); + } else { + info!( + target: LOG_TARGET, + "✅ Finished catch up at height {}. Resuming normal operation.", + current_height, + ); + } + } else { + debug!( + target: LOG_TARGET, + "⏳ Still catching up... current height: {}, expected up to: {}", + current_height, + catch_up.expected_batch_height, + ); + self.worker_state.catch_up = Some(catch_up); + } + } + + match no_vote { + None | Some(NoVoteReason::AlreadyVotedAtHeight) => { + self.worker_state.has_processed_first_block = true; + Ok(()) + }, + Some(_) => { + // We decided NOVOTE, so we immediately send a NEWVIEW + self.on_leader_timeout(epoch_state, current_height, None).await + }, + } + }, + Err(err) => { + self.handle_hotstuff_error(current_height, epoch_state, Some(proposed_by), err) + .await }, - Err(err) => self.handle_hotstuff_error(epoch_state, Some(proposed_by), err).await, } } async fn handle_hotstuff_error( &mut self, + current_height: NodeHeight, local_epoch_state: &EpochState, catch_up_from: Option, err: HotStuffError, @@ -1017,6 +1107,15 @@ impl HotstuffWorker { // Sync return Err(err); } + + if self.worker_state.is_catching_up() { + warn!( + target: LOG_TARGET, + "⏳ Already catching up. Ignoring additional catch up." + ); + return Ok(()); + } + // Otherwise, catch up let vn = match catch_up_from { Some(pk) => { @@ -1039,9 +1138,17 @@ impl HotstuffWorker { target: LOG_TARGET, "⚠️This node has fallen behind due to a missing justified block: {err}. Catching up" ); + self.on_catch_up_sync - .request_sync(local_epoch_state.epoch(), vn.address) + .request_sync(local_epoch_state.epoch(), vn.address.clone()) .await?; + + self.worker_state.catch_up = Some(CatchUp { + high_qc: remote_height, + // we get batches of 100 blocks which can only justify up to view h + 99 + expected_batch_height: current_height + NodeHeight(99), + }); + Ok(()) } @@ -1050,7 +1157,7 @@ impl HotstuffWorker { epoch: Epoch, epoch_hash: FixedHash, shard_group: ShardGroup, - ) -> Result<(), HotStuffError> { + ) -> Result { self.state_store.with_write_tx(|tx| { // The parent for genesis blocks refer to this zero block let mut zero_block = Block::zero_block(self.config.network, self.config.consensus_constants.num_preshards); @@ -1058,12 +1165,12 @@ impl HotstuffWorker { debug!(target: LOG_TARGET, "Creating zero block"); zero_block.justify().save(tx)?; zero_block.insert(tx)?; - zero_block.add_justify_qc(tx, &QcId::zero())?; + zero_block.add_justify_qc(tx, &PcId::zero())?; zero_block.commit_block_without_state_changes(tx, &zero_block.justify().calculate_id())?; } if !Block::get_ids_by_epoch_and_height(&**tx, epoch, NodeHeight::zero())?.is_empty() { - return Ok(()); + return Ok(false); } let state_merkle_root = ShardedStateTree::new(&**tx).calculate_state_root(shard_group)?; @@ -1081,7 +1188,7 @@ impl HotstuffWorker { info!(target: LOG_TARGET, "✨Creating genesis block {genesis}"); genesis.justify().save(tx)?; genesis.insert(tx)?; - genesis.add_justify_qc(tx, &QcId::zero())?; + genesis.add_justify_qc(tx, &PcId::zero())?; genesis.as_locked().set(tx)?; genesis.as_leaf().set(tx)?; genesis.as_highest_seen().set(tx)?; @@ -1090,7 +1197,7 @@ impl HotstuffWorker { genesis.justify().as_high_pc().set(tx)?; genesis.commit_block_without_state_changes(tx, &genesis.justify().calculate_id())?; - Ok(()) + Ok(true) }) } @@ -1117,3 +1224,28 @@ fn log_err(context: &'static str, result: Result) -> Result } result } + +#[derive(Debug, Default)] +struct WorkerState { + pub catch_up: Option, + pub has_processed_first_block: bool, +} + +impl WorkerState { + pub fn is_catching_up(&self) -> bool { + self.catch_up.is_some() + } +} + +#[derive(Debug)] +struct CatchUp { + pub high_qc: NodeHeight, + pub expected_batch_height: NodeHeight, +} + +impl CatchUp { + pub fn set_next_batch(&mut self, current_height: NodeHeight) -> bool { + self.expected_batch_height = (current_height + NodeHeight(99)).min(self.high_qc); + self.expected_batch_height < self.high_qc + } +} diff --git a/crates/consensus/src/messages/sync.rs b/crates/consensus/src/messages/catch_up.rs similarity index 87% rename from crates/consensus/src/messages/sync.rs rename to crates/consensus/src/messages/catch_up.rs index 7dbd0407ac..a23e7a3f0a 100644 --- a/crates/consensus/src/messages/sync.rs +++ b/crates/consensus/src/messages/catch_up.rs @@ -5,7 +5,7 @@ use serde::Serialize; use tari_ootle_common_types::{Epoch, NodeHeight}; #[derive(Debug, Clone, Serialize)] -pub struct SyncRequestMessage { +pub struct CatchUpRequestMessage { pub epoch: Epoch, pub block_height: NodeHeight, } diff --git a/crates/consensus/src/messages/message.rs b/crates/consensus/src/messages/message.rs index af34b96309..531e1f376d 100644 --- a/crates/consensus/src/messages/message.rs +++ b/crates/consensus/src/messages/message.rs @@ -15,20 +15,29 @@ use super::{ ProposalMessage, VoteMessage, }; -use crate::messages::{MissingTransactionsRequest, SyncRequestMessage}; +use crate::messages::{CatchUpRequestMessage, MissingTransactionsRequest}; // Serialize is implemented for the message logger #[derive(Debug, Clone, serde::Serialize)] pub enum HotstuffMessage { + /// New view message: a vote to move to a new view after a leader timeout NewView(Box), + /// Proposal message: a proposed block from the leader Proposal(Box), + /// Foreign proposal message: proposal from a foreign shard that involves this shard ForeignProposal(ForeignProposalMessage), + /// A broadcast notification that a foreign proposal should be requested if not already known ForeignProposalNotification(ForeignProposalNotificationMessage), + /// A request for a foreign proposal (after recieving a notification) ForeignProposalRequest(ForeignProposalRequestMessage), + /// Vote message: a vote for a proposed block Vote(VoteMessage), + /// Request for missing transactions MissingTransactionsRequest(MissingTransactionsRequest), + /// Response with missing transactions MissingTransactionsResponse(MissingTransactionsResponse), - CatchUpSyncRequest(SyncRequestMessage), + /// Request to send blocks from a given height + CatchUpSyncRequest(CatchUpRequestMessage), } impl HotstuffMessage { @@ -74,12 +83,19 @@ impl HotstuffMessage { _ => None, } } + + pub fn into_proposal(self) -> Option { + match self { + Self::Proposal(msg) => Some(*msg), + _ => None, + } + } } impl Display for HotstuffMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - HotstuffMessage::NewView(msg) => { + Self::NewView(msg) => { write!( f, "NewView({}, {}, high-qc: {})", @@ -88,7 +104,7 @@ impl Display for HotstuffMessage { msg.high_pc.height() ) }, - HotstuffMessage::Proposal(msg) => { + Self::Proposal(msg) => { write!( f, "Proposal(Epoch={},Height={},QC={},TC={},#foreign={})", @@ -99,10 +115,10 @@ impl Display for HotstuffMessage { msg.foreign_proposals.len() ) }, - HotstuffMessage::ForeignProposal(msg) => write!(f, "ForeignProposal({})", msg), - HotstuffMessage::ForeignProposalNotification(msg) => write!(f, "ForeignProposalNotification({})", msg), - HotstuffMessage::ForeignProposalRequest(msg) => write!(f, "ForeignProposalRequest({})", msg), - HotstuffMessage::Vote(VoteMessage { vote }) => write!( + Self::ForeignProposal(msg) => write!(f, "ForeignProposal({})", msg), + Self::ForeignProposalNotification(msg) => write!(f, "ForeignProposalNotification({})", msg), + Self::ForeignProposalRequest(msg) => write!(f, "ForeignProposalRequest({})", msg), + Self::Vote(VoteMessage { vote }) => write!( f, "Vote({}, {}, {}, {})", vote.height(), @@ -110,7 +126,7 @@ impl Display for HotstuffMessage { vote.block_id, vote.decision, ), - HotstuffMessage::MissingTransactionsRequest(msg) => { + Self::MissingTransactionsRequest(msg) => { write!( f, "RequestMissingTransactions({} transaction(s), block: {}, epoch: {})", @@ -119,14 +135,14 @@ impl Display for HotstuffMessage { msg.epoch ) }, - HotstuffMessage::MissingTransactionsResponse(msg) => write!( + Self::MissingTransactionsResponse(msg) => write!( f, "RequestedTransaction({} transaction(s), block: {}, epoch: {})", msg.transactions.len(), msg.block_id, msg.epoch ), - HotstuffMessage::CatchUpSyncRequest(msg) => write!(f, "SyncRequest({}/{})", msg.epoch, msg.block_height), + Self::CatchUpSyncRequest(msg) => write!(f, "SyncRequest({}/{})", msg.epoch, msg.block_height), } } } diff --git a/crates/consensus/src/messages/mod.rs b/crates/consensus/src/messages/mod.rs index 4155b0c56b..d045dcbe78 100644 --- a/crates/consensus/src/messages/mod.rs +++ b/crates/consensus/src/messages/mod.rs @@ -21,6 +21,6 @@ pub use request_missing_transaction::*; mod requested_transaction; pub use requested_transaction::*; -mod sync; +mod catch_up; -pub use sync::*; +pub use catch_up::*; diff --git a/crates/consensus/src/traits/block_store.rs b/crates/consensus/src/traits/block_store.rs index 77a6383538..fb80cad297 100644 --- a/crates/consensus/src/traits/block_store.rs +++ b/crates/consensus/src/traits/block_store.rs @@ -77,6 +77,7 @@ impl BlockStore for Block { // b <- b'.justify.node let commit_node = new_locked.justify().calculate_block_id(); + // b''.parent() == b'.id() && b'.parent() == b.id() if justified_node.parent() == new_locked.id() && *new_locked.parent() == commit_node { debug!( target: LOG_TARGET, diff --git a/crates/consensus/src/traits/certificate.rs b/crates/consensus/src/traits/certificate.rs index c8b36c7922..bda2014e62 100644 --- a/crates/consensus/src/traits/certificate.rs +++ b/crates/consensus/src/traits/certificate.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use log::info; -use tari_consensus_types::{HighPc, HighTc, ProposalCertificate, QcId, TcId, TimeoutCertificate}; +use tari_consensus_types::{HighPc, HighTc, PcId, ProposalCertificate, TcId, TimeoutCertificate}; use tari_ootle_common_types::{displayable::Displayable, optional::Optional, Epoch}; use tari_ootle_storage::{ consensus_models::BookkeepingModel, @@ -36,7 +36,7 @@ pub trait CertificateStore: Sized { impl CertificateStore for ProposalCertificate { type HighCertificate = HighPc; - type Id = QcId; + type Id = PcId; fn get(tx: &TTx, epoch: Epoch, id: &Self::Id) -> Result { tx.proposal_certificates_get(epoch, id) @@ -61,7 +61,7 @@ impl CertificateStore for ProposalCertificate { TTx::Target: StateStoreReadTransaction, { match HighPc::get(&**tx, self.epoch()).optional()? { - Some(high_pc) if high_pc.block_height() >= self.height() => { + Some(high_pc) if high_pc.height() >= self.height() => { // EDGE CASE: If we receive a new high PC, clear the last sent new view. This is because we could have // sent many unsuccessful NEWVIEWs (likely we're offline) and the chain progressed // without us. But then if we need to send NEWVIEWs again, it will be aligned with the network view. @@ -71,7 +71,7 @@ impl CertificateStore for ProposalCertificate { "🔥 HIGH_PC ({}, previous high PC: {} {}) - not new", self, high_pc.block_id(), - high_pc.block_height(), + high_pc.height(), ); Ok(high_pc) }, @@ -82,7 +82,7 @@ impl CertificateStore for ProposalCertificate { "🔥 NEW HIGH_PC ({}, previous high PC: {} {})", self, high_pc.block_id(), - high_pc.block_height(), + high_pc.height(), ); self.save(tx)?; diff --git a/crates/consensus/src/validations/block.rs b/crates/consensus/src/validations/block.rs index ea3264b91e..79a1981d41 100644 --- a/crates/consensus/src/validations/block.rs +++ b/crates/consensus/src/validations/block.rs @@ -18,11 +18,12 @@ use super::common::{ check_epoch_hash, check_height, check_network, + check_proposal_certificate, check_proposed_by_leader, - check_quorum_certificate, check_shard_group_bounds, check_shard_group_matches, check_sidechain_id, + check_timeout_certificate, }; use crate::{ hotstuff::{HotStuffError, HotstuffConfig, ProposalValidationError}, @@ -62,7 +63,8 @@ fn check_proposal( ) -> Result<(), HotStuffError> { check_header::(block.header(), expected_epoch_hash, config, signer_service)?; check_block(leader_strategy, committee_for_block, block)?; - check_quorum_certificate::(block, committee_for_block, signer_service)?; + check_proposal_certificate::(block, committee_for_block, signer_service)?; + check_timeout_certificate::(block, committee_for_block, signer_service)?; Ok(()) } diff --git a/crates/consensus/src/validations/common.rs b/crates/consensus/src/validations/common.rs index 56126d6279..d358fbb102 100644 --- a/crates/consensus/src/validations/common.rs +++ b/crates/consensus/src/validations/common.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use log::{debug, warn}; use tari_common_types::types::FixedHash; -use tari_consensus_types::ProposalCertificate; +use tari_consensus_types::{QuorumCertificateRef, TimeoutVote}; use tari_ootle_common_types::{ committee::Committee, DerivableFromPublicKey, @@ -200,7 +200,7 @@ pub(super) fn check_block_signature( +pub(super) fn check_proposal_certificate( candidate_block: &Block, committee: &Committee, signing_service: &TConsensusSpec::SignerService, @@ -213,7 +213,27 @@ pub(super) fn check_quorum_certificate( }); } - check_quorum_certificate_signatures::(qc, committee, signing_service)?; + check_quorum_certificate_signatures::(qc.into(), committee, signing_service)?; + + Ok(()) +} + +pub(super) fn check_timeout_certificate( + candidate_block: &Block, + committee: &Committee, + signing_service: &TConsensusSpec::SignerService, +) -> Result<(), ProposalValidationError> { + let Some(tc) = candidate_block.timeout_certificate() else { + return Ok(()); + }; + if candidate_block.height() <= tc.height() { + return Err(ProposalValidationError::CandidateBlockNotHigherThanJustify { + justify_block_height: tc.height(), + candidate_block_height: candidate_block.height(), + }); + } + + check_quorum_certificate_signatures::(tc.into(), committee, signing_service)?; Ok(()) } @@ -221,7 +241,7 @@ pub(super) fn check_quorum_certificate( /// Validates the signatures of the quorum certificate. // pub because used in on receive NEWVIEW pub fn check_quorum_certificate_signatures( - qc: &ProposalCertificate, + qc: QuorumCertificateRef<'_>, committee: &Committee, signing_service: &TConsensusSpec::SignerService, ) -> Result<(), ProposalValidationError> { @@ -239,9 +259,9 @@ pub fn check_quorum_certificate_signatures( return Err(ProposalValidationError::ValidatorNotInCommittee { validator: signature.public_key().to_string(), details: format!( - "QC signed with validator {} that is not in committee {}", + "QC {} signed with validator {} that is not in committee", + qc, signature.public_key(), - qc.shard_group(), ), }); }; @@ -253,16 +273,30 @@ pub fn check_quorum_certificate_signatures( }); } - let block_id = qc.calculate_block_id(); - let message = ProposalCertificateSignatureFields { - block_id: block_id.hash(), - decision: qc.decision(), - }; - let vote = SignedProposalVote { message, signature }; - - let is_valid = signing_service.verify(&vote); - if !is_valid { - return Err(ProposalValidationError::QcInvalidSignature { qc: qc.calculate_id() }); + match qc { + QuorumCertificateRef::ProposalCertificate(pc) => { + let block_id = pc.calculate_block_id(); + let message = ProposalCertificateSignatureFields { + block_id: block_id.hash(), + decision: pc.decision(), + }; + let vote = SignedProposalVote { message, signature }; + let is_valid = signing_service.verify(&vote); + if !is_valid { + return Err(ProposalValidationError::QcInvalidSignature { qc: qc.calculate_id() }); + } + }, + QuorumCertificateRef::TimeoutCertificate(tc) => { + let vote = TimeoutVote { + epoch: tc.epoch(), + height: tc.height(), + signature: signature.clone(), + }; + let is_valid = signing_service.verify(&vote); + if !is_valid { + return Err(ProposalValidationError::QcInvalidSignature { qc: qc.calculate_id() }); + } + }, } } diff --git a/crates/consensus_tests/Cargo.toml b/crates/consensus_tests/Cargo.toml index eb20922870..c92a67b1e7 100644 --- a/crates/consensus_tests/Cargo.toml +++ b/crates/consensus_tests/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [dependencies] [dev-dependencies] +tari_ootle_app_utilities = { workspace = true } tari_bor = { workspace = true } tari_common_types = { workspace = true } tari_consensus = { workspace = true } diff --git a/crates/consensus_tests/src/leader_failure.rs b/crates/consensus_tests/src/leader_failure.rs index 1d63ef59a7..f621444635 100644 --- a/crates/consensus_tests/src/leader_failure.rs +++ b/crates/consensus_tests/src/leader_failure.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::time::Duration; +use std::time::{Duration, Instant}; use tari_consensus::hotstuff::HotStuffError; use tari_consensus_types::Decision; @@ -38,9 +38,7 @@ async fn single_shard_node_goes_down() { // Take the VN offline - if we do it in the loop below, all transactions may have already been finalized (local // only) by committed block 1 log::info!("😴 {failure_node} is offline"); - test.network() - .go_offline(TestVnDestination::Address(failure_node.clone())) - .await; + test.network().go_offline(failure_node.clone()).await; test.start_epoch(Epoch(1)).await; @@ -114,12 +112,8 @@ async fn single_shard_neighbour_nodes_go_down() { // only) by committed block 1 log::info!("😴 {failure_node1} is offline"); log::info!("😴 {failure_node2} is offline"); - test.network() - .go_offline(TestVnDestination::Address(failure_node1.clone())) - .await; - test.network() - .go_offline(TestVnDestination::Address(failure_node2.clone())) - .await; + test.network().go_offline(failure_node1.clone()).await; + test.network().go_offline(failure_node2.clone()).await; test.start_epoch(Epoch(1)).await; @@ -193,9 +187,7 @@ async fn single_shard_node_goes_down_and_gets_evicted() { // Take the VN offline - if we do it in the loop below, all transactions may have already been finalized (local // only) by committed block 1 log::info!("😴 {failure_node} is offline"); - test.network() - .go_offline(TestVnDestination::Address(failure_node.clone())) - .await; + test.network().go_offline(failure_node.clone()).await; test.start_epoch(Epoch(1)).await; @@ -314,9 +306,7 @@ async fn multi_shard_node_goes_down() { // Take the VN offline - if we do it in the loop below, all transactions may have already been finalized (local // only) by committed block 1 log::info!("😴 {failure_node} is offline"); - test.network() - .go_offline(TestVnDestination::Address(failure_node.clone())) - .await; + test.network().go_offline(failure_node.clone()).await; test.start_epoch(Epoch(1)).await; @@ -360,3 +350,77 @@ async fn multi_shard_node_goes_down() { log::info!("total messages sent: {}", test.network().total_messages_sent()); test.assert_clean_shutdown_except(&[failure_node]).await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn single_shard_node_goes_down_and_catches_up() { + setup_logger(); + let mut test = Test::builder() + // Allow enough time for leader failures + .with_test_timeout(Duration::from_secs(60)) + .modify_consensus_constants(|constants_mut| { + constants_mut.missed_proposal_suspend_threshold = 10; + constants_mut.missed_proposal_evict_threshold = 10; + constants_mut.pacemaker_block_time = Duration::from_secs(5); + }) + .modify_config(|config_mut| { + config_mut.enable_eviction_proposal = false; + }) + .add_committee(0, vec!["1", "2", "3", "4", "5"]) + .start() + .await; + + let failure_node = TestAddress::new("4"); + + let mut tx_ids = Vec::with_capacity(12); + for _ in 0..10 { + let (tx, _, _) = test.send_transaction_to_all(Decision::Commit, 1, 2, 1).await; + tx_ids.push(*tx.id()); + } + + test.start_epoch(Epoch(1)).await; + let epoch_start = Instant::now(); + let mut is_back_online = false; + let mut had_gone_offline = false; + + loop { + let (_, _, _, committed_height) = test.on_block_committed().await; + + if !had_gone_offline && epoch_start.elapsed() >= Duration::from_secs(2) { + log::info!("😴 {failure_node} is offline"); + test.network().go_offline(failure_node.clone()).await; + let (tx, _, _) = test.send_transaction_to_all(Decision::Commit, 1, 2, 1).await; + tx_ids.push(*tx.id()); + had_gone_offline = true; + } + + if !is_back_online && epoch_start.elapsed() >= Duration::from_secs(13) { + log::info!("🚀 {failure_node} is online again"); + test.network().go_online(&failure_node).await; + let (tx, _, _) = test.send_transaction_to_all(Decision::Commit, 1, 2, 1).await; + is_back_online = true; + tx_ids.push(*tx.id()); + } + + if committed_height == NodeHeight(1) { + // This allows a few more leader failures to occur + test.send_transaction_to_all(Decision::Commit, 1, 2, 1).await; + test.wait_for_pool_count(TestVnDestination::All, 1).await; + } + + if is_back_online && + test.validators_iter() + .all(|v| tx_ids.iter().all(|tx_id| v.has_committed_substates(tx_id))) + { + break; + } + + if committed_height > NodeHeight(50) { + panic!("Not all transaction committed after {} blocks", committed_height); + } + } + + test.stop(); + + log::info!("total messages sent: {}", test.network().total_messages_sent()); + test.assert_clean_shutdown_except(&[failure_node]).await; +} diff --git a/crates/consensus_tests/src/substate_store.rs b/crates/consensus_tests/src/substate_store.rs index ac895f4a3a..ec4e535dc3 100644 --- a/crates/consensus_tests/src/substate_store.rs +++ b/crates/consensus_tests/src/substate_store.rs @@ -230,7 +230,7 @@ fn add_substate(store: &TestStore, seed: u8, version: u32) -> VersionedSubstateI fn create_store() -> (TestStore, TempDir) { let temp_dir = tempfile::tempdir().unwrap(); - let store = RocksDbStateStore::open(&temp_dir, DatabaseOptions::default()).unwrap(); + let store = RocksDbStateStore::open(&temp_dir, DatabaseOptions::default().with_debugging_data(true)).unwrap(); store .with_write_tx(|tx| { let zero = Block::zero_block(Network::LocalNet, NumPreshards::P256); diff --git a/crates/consensus_tests/src/support/harness.rs b/crates/consensus_tests/src/support/harness.rs index 5fdf5b0780..9bb4f9edd4 100644 --- a/crates/consensus_tests/src/support/harness.rs +++ b/crates/consensus_tests/src/support/harness.rs @@ -261,7 +261,7 @@ impl Test { } else { self.on_hotstuff_event().await }; - if self.network.is_offline(&address, self.num_committees).await { + if self.network.is_offline(&address).await { info!("[{}] Ignoring event for offline node: {:?}", address, event); continue; } @@ -650,8 +650,8 @@ impl TestBuilder { fee_exhaust_divisor: 20, epochs_per_era: Epoch(10), }, - state_tree_cleanup_interval: Duration::from_secs(60), - epoch_gc_interval: Duration::from_secs(60), + state_tree_cleanup_interval: Duration::from_secs(1000), + epoch_gc_interval: Duration::from_secs(1000), enable_eviction_proposal: true, }, } @@ -663,6 +663,11 @@ impl TestBuilder { self } + pub fn modify_config(mut self, f: F) -> Self { + f(&mut self.config); + self + } + #[allow(dead_code)] pub fn with_rocks_path>(mut self, path: T) -> Self { self.rocks_path = Some(path.into()); diff --git a/crates/consensus_tests/src/support/network.rs b/crates/consensus_tests/src/support/network.rs index 96d315b8b0..7b970bc7b0 100644 --- a/crates/consensus_tests/src/support/network.rs +++ b/crates/consensus_tests/src/support/network.rs @@ -8,6 +8,7 @@ use std::{ use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; use itertools::Itertools; +use log::info; use tari_consensus::messages::HotstuffMessage; use tari_ootle_common_types::ShardGroup; use tari_ootle_storage::consensus_models::TransactionRecord; @@ -112,7 +113,7 @@ pub struct TestNetwork { network_task_handle: task::JoinHandle<()>, tx_new_transaction: mpsc::Sender<(TestVnDestination, TransactionRecord)>, network_status: watch::Sender, - offline_destinations: Arc>>, + offline_destinations: Arc>>, num_sent_messages: Arc, num_filtered_messages: Arc, _on_message: watch::Receiver>, @@ -127,18 +128,25 @@ impl TestNetwork { &self.network_task_handle } - pub async fn go_offline(&self, destination: TestVnDestination) -> &Self { - if destination.is_shard() { - unimplemented!("Sorry :/ taking a bucket offline is not yet supported in the test harness"); + pub async fn go_offline(&self, node: TestAddress) -> &Self { + self.offline_destinations.write().await.push(node); + self + } + + pub async fn go_online(&self, node: &TestAddress) -> &Self { + let mut write = self.offline_destinations.write().await; + if let Some(pos) = write.iter().position(|d| d == node) { + info!("🟢 Bringing {} back online", node); + write.remove(pos); + } else { + panic!("🟡 Tried to bring {} online but it was not offline", node); } - self.offline_destinations.write().await.push(destination); self } - pub async fn is_offline(&self, address: &TestAddress, num_committees: u32) -> bool { + pub async fn is_offline(&self, address: &TestAddress) -> bool { let read = self.offline_destinations.read().await; - read.iter() - .any(|d| d.is_for(address, ShardGroup::all_shards(TEST_NUM_PRESHARDS), num_committees)) + read.iter().any(|d| d == address) } #[allow(dead_code)] @@ -164,7 +172,7 @@ impl TestNetwork { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum TestVnDestination { All, Address(TestAddress), @@ -186,10 +194,6 @@ impl TestVnDestination { pub fn is_for_vn(&self, vn: &Validator) -> bool { self.is_for(&vn.address, vn.shard_group, vn.num_committees) } - - pub fn is_shard(&self) -> bool { - matches!(self, TestVnDestination::Committee(_)) - } } pub struct TestNetworkWorker { @@ -215,7 +219,7 @@ pub struct TestNetworkWorker { num_filtered_messages: Arc, transaction_store: Arc>>, - offline_destinations: Arc>>, + offline_destinations: Arc>>, shutdown_signal: ShutdownSignal, message_filter: Option, } @@ -330,11 +334,7 @@ impl TestNetworkWorker { continue; } } - // TODO: support for taking a whole committee bucket offline - if to != from && - self.is_offline_destination(&from, &to, ShardGroup::all_shards(TEST_NUM_PRESHARDS)) - .await - { + if to != from && self.is_offline_destination(&from, &to).await { continue; } @@ -362,11 +362,8 @@ impl TestNetworkWorker { return; } } - if from != to && - self.is_offline_destination(&from, &to, ShardGroup::all_shards(TEST_NUM_PRESHARDS)) - .await - { - log::info!("🗑️ [TEST] Discarding message {msg} from {from}. Leader {to} is offline"); + if from != to && self.is_offline_destination(&from, &to).await { + log::info!("🗑️ [TEST] Discarding message {msg} from {from}. {from}/{to} is offline"); return; } log::debug!("✉️ Message {} sent from {} to {}", msg, from, to); @@ -376,10 +373,8 @@ impl TestNetworkWorker { self.tx_hs_message.get(&to).unwrap().send((from, msg)).await.unwrap(); } - async fn is_offline_destination(&self, from: &TestAddress, to: &TestAddress, shard: ShardGroup) -> bool { + async fn is_offline_destination(&self, from: &TestAddress, to: &TestAddress) -> bool { let lock = self.offline_destinations.read().await; - // 99999 is not used TODO: support for taking entire shard group offline - lock.iter() - .any(|d| d.is_for(from, shard, 99999) || d.is_for(to, shard, 99999)) + lock.iter().any(|d| d == from || d == to) } } diff --git a/crates/consensus_tests/src/support/transaction_executor.rs b/crates/consensus_tests/src/support/transaction_executor.rs index a64551c2de..914cc90e1b 100644 --- a/crates/consensus_tests/src/support/transaction_executor.rs +++ b/crates/consensus_tests/src/support/transaction_executor.rs @@ -10,11 +10,19 @@ use tari_engine_types::{ transaction_receipt::TransactionReceiptAddress, virtual_substate::{VirtualSubstate, VirtualSubstateId, VirtualSubstates}, }; -use tari_ootle_common_types::{displayable::Displayable, Epoch, LockIntent, SubstateRequirement, VersionedSubstateId}; +use tari_ootle_common_types::{ + displayable::Displayable, + Epoch, + LockIntent, + SubstateLockType, + SubstateRequirement, + VersionedSubstateId, +}; use tari_ootle_storage::{ consensus_models::{TransactionExecution, VersionedSubstateIdLockIntent}, StateStore, }; +use tari_template_lib::prelude::XTR; use tari_transaction::Transaction; use crate::support::{create_execution_result_for_transaction, executions_store::TestExecutionSpecStore, TestAddress}; @@ -90,6 +98,8 @@ impl BlockTransactionExecutor for TestBloc let resolved_inputs = spec .input_locks .into_iter() + // Implicitly add XTR as read lock for all transactions + .chain(iter::once((SubstateId::from(XTR), SubstateLockType::Read))) .map(|(substate_id, lock_type)| { let substate = resolved_inputs.get(&substate_id).unwrap_or_else(|| { panic!( diff --git a/crates/consensus_tests/src/support/validator/builder.rs b/crates/consensus_tests/src/support/validator/builder.rs index 3e5ca6ff4e..37a8cb2696 100644 --- a/crates/consensus_tests/src/support/validator/builder.rs +++ b/crates/consensus_tests/src/support/validator/builder.rs @@ -1,17 +1,30 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::path::{Path, PathBuf}; +use std::{ + ops::Deref, + path::{Path, PathBuf}, +}; +use serde::Serialize; use tari_common_types::types::PrivateKey; use tari_consensus::{ hotstuff::{ConsensusCurrentState, ConsensusWorker, ConsensusWorkerContext, HotstuffConfig, HotstuffWorker}, traits::hooks::NoopHooks, }; use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; -use tari_engine_types::ToByteType; -use tari_ootle_common_types::{ShardGroup, SubstateAddress}; -use tari_ootle_storage::consensus_models::TransactionPool; +use tari_engine_types::{ + substate::{SubstateId, SubstateValue}, + ToByteType, +}; +use tari_ootle_common_types::{Epoch, NodeAddressable, ShardGroup, SubstateAddress, VersionedSubstateIdRef}; +use tari_ootle_storage::{ + consensus_models::{SubstateRecord, SubstateTransition, SubstateUpdateBatch, TransactionPool}, + StateStore, + StateStoreReadTransaction, + StateStoreWriteTransaction, + StorageError, +}; use tari_shutdown::ShutdownSignal; use tari_state_store_rocksdb::DatabaseOptions; use tari_template_lib::prelude::RistrettoPublicKeyBytes; @@ -145,8 +158,14 @@ impl ValidatorBuilder { .as_deref() .unwrap_or_else(|| self.rocks_tmp_path.path()); log::info!("Rocksdb path {}", rocks_path.display()); - TestStore::open(rocks_path, DatabaseOptions::default()).unwrap() + TestStore::open(rocks_path, DatabaseOptions::default().with_debugging_data(true)).unwrap() }; + + // Add XTR to the store, since this is implicit for all transactions. + let (addr, xtr) = tari_ootle_app_utilities::genesis_resources::get_stealth_tari_resource( + self.config.as_ref().unwrap().network, + ); + store.with_write_tx(|tx| create_substate(tx, addr, xtr)).unwrap(); let signing_service = TestVoteSignatureService::new(self.address.clone()); let transaction_pool = TransactionPool::new(); let (tx_events, _) = broadcast::channel(100); @@ -214,3 +233,25 @@ impl ValidatorBuilder { (channels, validator) } } + +fn create_substate(tx: &mut TTx, substate_id: TId, value: TVal) -> Result<(), StorageError> +where + TTx: StateStoreWriteTransaction + Deref, + TTx::Target: StateStoreReadTransaction, + TTx::Addr: NodeAddressable + Serialize, + TId: Into, + TVal: Into, +{ + let substate_id = substate_id.into(); + let shard = VersionedSubstateIdRef::new(&substate_id, 0).to_shard(TEST_NUM_PRESHARDS); + let mut batch = SubstateUpdateBatch::new(Epoch::zero()); + batch.with_transition(shard, 0).push(SubstateTransition::Up { + id: substate_id, + version: 0, + substate_or_hash: value.into().into(), + }); + + SubstateRecord::commit_batch(tx, batch)?; + + Ok(()) +} diff --git a/crates/consensus_tests/src/support/validator/instance.rs b/crates/consensus_tests/src/support/validator/instance.rs index 0edc34f544..4c2f8f615e 100644 --- a/crates/consensus_tests/src/support/validator/instance.rs +++ b/crates/consensus_tests/src/support/validator/instance.rs @@ -95,8 +95,12 @@ impl Validator { pub fn has_committed_substates(&self, tx_id: &TransactionId) -> bool { let tx = self.state_store().create_read_tx().unwrap(); - let tx_rec = tx.transactions_get(tx_id).unwrap(); - let exec_result = tx_rec.get_finalized_execution(&tx).unwrap(); + let Some(tx_rec) = tx.transactions_get(tx_id).optional().unwrap() else { + return false; + }; + let Some(exec_result) = tx_rec.get_finalized_execution(&tx).optional().unwrap() else { + return false; + }; if let Some(diff) = exec_result.result().finalize.any_accept() { for (substate_id, substate) in diff.up_iter() { let id = VersionedSubstateIdRef::new(substate_id, substate.version()); diff --git a/crates/consensus_types/src/bookkeeping/high_pc.rs b/crates/consensus_types/src/bookkeeping/high_pc.rs index 5f8f46d141..a31378aee5 100644 --- a/crates/consensus_types/src/bookkeeping/high_pc.rs +++ b/crates/consensus_types/src/bookkeeping/high_pc.rs @@ -25,14 +25,14 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use tari_ootle_common_types::{Epoch, NodeHeight}; -use crate::ids::{BlockId, QcId}; +use crate::ids::{BlockId, PcId}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HighPc { pub block_id: BlockId, pub block_height: NodeHeight, pub epoch: Epoch, - pub qc_id: QcId, + pub qc_id: PcId, } impl HighPc { @@ -40,11 +40,11 @@ impl HighPc { &self.block_id } - pub fn block_height(&self) -> NodeHeight { + pub fn height(&self) -> NodeHeight { self.block_height } - pub fn id(&self) -> &QcId { + pub fn id(&self) -> &PcId { &self.qc_id } diff --git a/crates/consensus_types/src/bookkeeping/leaf_block.rs b/crates/consensus_types/src/bookkeeping/leaf_block.rs index 56f6189890..52cadbab96 100644 --- a/crates/consensus_types/src/bookkeeping/leaf_block.rs +++ b/crates/consensus_types/src/bookkeeping/leaf_block.rs @@ -36,6 +36,10 @@ pub struct LeafBlock { } impl LeafBlock { + pub fn is_genesis(&self) -> bool { + self.height.is_zero() + } + pub fn height(&self) -> NodeHeight { self.height } diff --git a/crates/consensus_types/src/certificates/mod.rs b/crates/consensus_types/src/certificates/mod.rs index d058ab2378..8edd534c9c 100644 --- a/crates/consensus_types/src/certificates/mod.rs +++ b/crates/consensus_types/src/certificates/mod.rs @@ -3,10 +3,12 @@ mod proposal_certificate; mod proposal_vote; +mod quorum_certificate; mod timeout_certificate; mod timeout_vote; pub use proposal_certificate::*; pub use proposal_vote::*; +pub use quorum_certificate::*; pub use timeout_certificate::*; pub use timeout_vote::*; diff --git a/crates/consensus_types/src/certificates/proposal_certificate.rs b/crates/consensus_types/src/certificates/proposal_certificate.rs index 397f08bebf..e933771dcb 100644 --- a/crates/consensus_types/src/certificates/proposal_certificate.rs +++ b/crates/consensus_types/src/certificates/proposal_certificate.rs @@ -12,7 +12,7 @@ use tari_sidechain::QuorumDecision; use crate::{ bookkeeping::{HighPc, LeafBlock}, - ids::{BlockId, QcId}, + ids::{BlockId, PcId}, validator_signature::ValidatorSignatureBytes, }; @@ -68,7 +68,7 @@ impl ProposalCertificate { /// Returns the hash of the QC. This is used to identify the QC and not for any secure purposes. /// However, we implement a secure hash (as opposed to a cheaper, non-collision-resistant hash e.g. siphash) to /// avoid any collision issues e.g. storage keys. - pub fn calculate_id(&self) -> QcId { + pub fn calculate_id(&self) -> PcId { // We use the same fields as tari_sidechain::QuorumCertificate. Since should calculate a consistent ID between // shards. Although, worth noting that in the current protocol, this does not matter because the foreign // QC id only needs to be consistent within a shard group. This may change in the future. @@ -80,7 +80,7 @@ impl ProposalCertificate { parent_id: &BlockId, signatures: &[ValidatorSignatureBytes], decision: &QuorumDecision, - ) -> QcId { + ) -> PcId { quorum_certificate_id_hasher() .chain(header_hash) .chain(parent_id) @@ -100,8 +100,8 @@ impl ProposalCertificate { self.epoch } - pub fn shard_group(&self) -> &ShardGroup { - &self.shard_group + pub fn shard_group(&self) -> ShardGroup { + self.shard_group } pub fn signatures(&self) -> &[ValidatorSignatureBytes] { diff --git a/crates/consensus_types/src/certificates/quorum_certificate.rs b/crates/consensus_types/src/certificates/quorum_certificate.rs new file mode 100644 index 0000000000..adaf3ae11d --- /dev/null +++ b/crates/consensus_types/src/certificates/quorum_certificate.rs @@ -0,0 +1,104 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use borsh::BorshSerialize; +use serde::Serialize; +use tari_ootle_common_types::{Epoch, NodeHeight}; + +use crate::{ProposalCertificate, QcId, TimeoutCertificate, ValidatorSignatureBytes}; + +#[derive(Debug, Clone, Serialize, BorshSerialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub enum QuorumCertificateRef<'a> { + ProposalCertificate(&'a ProposalCertificate), + TimeoutCertificate(&'a TimeoutCertificate), +} + +impl QuorumCertificateRef<'_> { + pub fn is_proposal_certificate(&self) -> bool { + matches!(self, Self::ProposalCertificate(_)) + } + + pub fn is_timeout_certificate(&self) -> bool { + matches!(self, Self::TimeoutCertificate(_)) + } + + pub fn justifies_zero_block(&self) -> bool { + match self { + Self::ProposalCertificate(pc) => pc.justifies_zero_block(), + Self::TimeoutCertificate(tc) => tc.height().is_zero(), + } + } + + pub fn signatures(&self) -> &[ValidatorSignatureBytes] { + match self { + Self::ProposalCertificate(pc) => pc.signatures(), + Self::TimeoutCertificate(tc) => tc.signatures(), + } + } + + pub fn calculate_id(&self) -> QcId { + match self { + Self::ProposalCertificate(pc) => pc.calculate_id().into(), + Self::TimeoutCertificate(tc) => tc.calculate_id().into(), + } + } + + pub fn type_str(&self) -> &'static str { + match self { + Self::ProposalCertificate(_) => "ProposalCertificate", + Self::TimeoutCertificate(_) => "TimeoutCertificate", + } + } + + pub fn epoch(&self) -> Epoch { + match self { + Self::ProposalCertificate(pc) => pc.epoch(), + Self::TimeoutCertificate(tc) => tc.epoch(), + } + } + + pub fn height(&self) -> NodeHeight { + match self { + Self::ProposalCertificate(pc) => pc.height(), + Self::TimeoutCertificate(tc) => tc.height(), + } + } + + pub fn as_proposal_certificate(&self) -> Option<&ProposalCertificate> { + if let Self::ProposalCertificate(pc) = self { + Some(pc) + } else { + None + } + } + + pub fn as_timeout_certificate(&self) -> Option<&TimeoutCertificate> { + if let Self::TimeoutCertificate(tc) = self { + Some(tc) + } else { + None + } + } +} + +impl<'a> From<&'a ProposalCertificate> for QuorumCertificateRef<'a> { + fn from(pc: &'a ProposalCertificate) -> Self { + Self::ProposalCertificate(pc) + } +} + +impl<'a> From<&'a TimeoutCertificate> for QuorumCertificateRef<'a> { + fn from(tc: &'a TimeoutCertificate) -> Self { + Self::TimeoutCertificate(tc) + } +} + +impl std::fmt::Display for QuorumCertificateRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ProposalCertificate(pc) => write!(f, "{}", pc), + Self::TimeoutCertificate(tc) => write!(f, "{}", tc), + } + } +} diff --git a/crates/consensus_types/src/ids/mod.rs b/crates/consensus_types/src/ids/mod.rs index b38f7d1a9e..10f8b4f384 100644 --- a/crates/consensus_types/src/ids/mod.rs +++ b/crates/consensus_types/src/ids/mod.rs @@ -5,10 +5,98 @@ mod block_id; mod macros; pub use block_id::*; +use tari_common_types::types::FixedHash; + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + serde::Serialize, + serde::Deserialize, + borsh::BorshSerialize, +)] +pub enum QcId { + PcId(PcId), + TcId(TcId), +} + +impl QcId { + pub const fn is_proposal_certificate(&self) -> bool { + matches!(self, Self::PcId(_)) + } + + pub const fn is_timeout_certificate(&self) -> bool { + matches!(self, Self::TcId(_)) + } + + pub const fn hash(&self) -> &FixedHash { + match self { + Self::PcId(pc_id) => pc_id.hash(), + Self::TcId(tc_id) => tc_id.hash(), + } + } + + /// Returns the bytes of the inner id type. i.e. these bytes do not include any enum discriminant. + pub fn as_inner_bytes(&self) -> &[u8] { + match self { + Self::PcId(pc_id) => pc_id.as_bytes(), + Self::TcId(tc_id) => tc_id.as_bytes(), + } + } + + pub fn is_zero(&self) -> bool { + match self { + Self::PcId(pc_id) => pc_id.is_zero(), + Self::TcId(tc_id) => tc_id.is_zero(), + } + } + + pub fn into_array(self) -> [u8; 32] { + match self { + Self::PcId(pc_id) => pc_id.into_array(), + Self::TcId(tc_id) => tc_id.into_array(), + } + } +} + +impl AsRef<[u8]> for QcId { + fn as_ref(&self) -> &[u8] { + match self { + Self::PcId(pc_id) => pc_id.as_ref(), + Self::TcId(tc_id) => tc_id.as_ref(), + } + } +} + +impl std::fmt::Display for QcId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PcId(pc_id) => write!(f, "pc_{}", pc_id), + Self::TcId(tc_id) => write!(f, "tc_{}", tc_id), + } + } +} + +impl From for QcId { + fn from(pc_id: PcId) -> Self { + Self::PcId(pc_id) + } +} + +impl From for QcId { + fn from(tc_id: TcId) -> Self { + Self::TcId(tc_id) + } +} crate::create_hash_type!( ///The ID of a Proposal Certificate - QcId + PcId ); crate::create_hash_type!( diff --git a/crates/engine_types/src/published_template.rs b/crates/engine_types/src/published_template.rs index 9293e2b84f..e0368042b0 100644 --- a/crates/engine_types/src/published_template.rs +++ b/crates/engine_types/src/published_template.rs @@ -56,7 +56,7 @@ impl PublishedTemplateAddress { Self::from_hash(address) } - pub fn as_object_key(&self) -> &ObjectKey { + pub const fn as_object_key(&self) -> &ObjectKey { self.0.inner() } diff --git a/crates/engine_types/src/substate.rs b/crates/engine_types/src/substate.rs index 42577436f5..4cddb004c4 100644 --- a/crates/engine_types/src/substate.rs +++ b/crates/engine_types/src/substate.rs @@ -134,49 +134,49 @@ pub enum SubstateId { } impl SubstateId { - pub fn as_component_address(&self) -> Option { + pub const fn as_component_address(&self) -> Option { match self { Self::Component(addr) => Some(*addr), _ => None, } } - pub fn as_vault_id(&self) -> Option { + pub const fn as_vault_id(&self) -> Option { match self { Self::Vault(id) => Some(*id), _ => None, } } - pub fn as_resource_address(&self) -> Option { + pub const fn as_resource_address(&self) -> Option { match self { Self::Resource(address) => Some(*address), _ => None, } } - pub fn as_unclaimed_confidential_output_address(&self) -> Option { + pub const fn as_unclaimed_confidential_output_address(&self) -> Option { match self { Self::ClaimedOutputTombstone(address) => Some(*address), _ => None, } } - pub fn as_template(&self) -> Option { + pub const fn as_template(&self) -> Option { match self { Self::Template(address) => Some(*address), _ => None, } } - pub fn as_transaction_receipt_address(&self) -> Option { + pub const fn as_transaction_receipt_address(&self) -> Option { match self { Self::TransactionReceipt(address) => Some(*address), _ => None, } } - pub fn as_validator_fee_pool_address(&self) -> Option { + pub const fn as_validator_fee_pool_address(&self) -> Option { match self { Self::ValidatorFeePool(address) => Some(*address), _ => None, @@ -234,22 +234,22 @@ impl SubstateId { self.to_string() } - pub fn as_non_fungible_address(&self) -> Option<&NonFungibleAddress> { + pub const fn as_non_fungible_address(&self) -> Option<&NonFungibleAddress> { match self { SubstateId::NonFungible(addr) => Some(addr), _ => None, } } - pub fn is_resource(&self) -> bool { + pub const fn is_resource(&self) -> bool { matches!(self, Self::Resource(_)) } - pub fn is_component(&self) -> bool { + pub const fn is_component(&self) -> bool { matches!(self, Self::Component(_)) } - pub fn is_root(&self) -> bool { + pub const fn is_root(&self) -> bool { // A component and utxo are "root" substates i.e. they may not have a parent node. NOTE: this concept isn't // well-defined right now, this is simply used to prevent components being detected as dangling. matches!( @@ -266,39 +266,39 @@ impl SubstateId { self.is_public_key_identity() } - pub fn is_vault(&self) -> bool { + pub const fn is_vault(&self) -> bool { matches!(self, Self::Vault(_)) } - pub fn is_non_fungible(&self) -> bool { + pub const fn is_non_fungible(&self) -> bool { matches!(self, Self::NonFungible(_)) } - pub fn is_layer1_commitment(&self) -> bool { + pub const fn is_claimed_output_tombstone(&self) -> bool { matches!(self, Self::ClaimedOutputTombstone(_)) } - pub fn is_transaction_receipt(&self) -> bool { + pub const fn is_transaction_receipt(&self) -> bool { matches!(self, Self::TransactionReceipt(_)) } - pub fn is_template(&self) -> bool { + pub const fn is_template(&self) -> bool { matches!(self, Self::Template(_)) } - pub fn is_validator_fee_pool(&self) -> bool { + pub const fn is_validator_fee_pool(&self) -> bool { matches!(self, Self::ValidatorFeePool(_)) } - pub fn is_utxo(&self) -> bool { + pub const fn is_utxo(&self) -> bool { matches!(self, Self::Utxo(_)) } - pub fn is_global(&self) -> bool { + pub const fn is_global(&self) -> bool { self.is_template() } - pub fn is_read_only(&self) -> bool { + pub const fn is_read_only(&self) -> bool { matches!(self, Self::TransactionReceipt(_) | Self::Template(_)) } } diff --git a/crates/engine_types/src/transaction_receipt.rs b/crates/engine_types/src/transaction_receipt.rs index 1d61ff3f73..3426295c60 100644 --- a/crates/engine_types/src/transaction_receipt.rs +++ b/crates/engine_types/src/transaction_receipt.rs @@ -53,7 +53,7 @@ impl TransactionReceiptAddress { Self(BorTag::new(key)) } - pub fn as_object_key(&self) -> &ObjectKey { + pub const fn as_object_key(&self) -> &ObjectKey { self.0.inner() } diff --git a/crates/engine_types/src/validator_fee.rs b/crates/engine_types/src/validator_fee.rs index 11fb7df3dd..567714d4c3 100644 --- a/crates/engine_types/src/validator_fee.rs +++ b/crates/engine_types/src/validator_fee.rs @@ -44,12 +44,12 @@ impl ValidatorFeePoolAddress { Self(BorTag::new(key)) } - pub fn as_object_key(&self) -> &ObjectKey { + pub const fn as_object_key(&self) -> &ObjectKey { self.0.inner() } - pub fn as_slice(&self) -> &[u8] { - self.0.inner() + pub const fn as_slice(&self) -> &[u8] { + self.0.inner().array() } pub fn from_hex(hex: &str) -> Result { diff --git a/crates/p2p/src/conversions/consensus.rs b/crates/p2p/src/conversions/consensus.rs index 767b0b525b..58937746ef 100644 --- a/crates/p2p/src/conversions/consensus.rs +++ b/crates/p2p/src/conversions/consensus.rs @@ -27,6 +27,7 @@ use std::{ use anyhow::{anyhow, Context}; use tari_consensus::messages::{ + CatchUpRequestMessage, ForeignProposalMessage, ForeignProposalNotificationMessage, ForeignProposalRequestMessage, @@ -35,15 +36,14 @@ use tari_consensus::messages::{ MissingTransactionsResponse, NewViewMessage, ProposalMessage, - SyncRequestMessage, VoteMessage, }; use tari_consensus_types::{ BlockId, Decision, + PcId, ProposalCertificate, ProposalVote, - QcId, ShardGroupAccumulatedData, TimeoutCertificate, TimeoutVote, @@ -488,7 +488,7 @@ impl From<&consensus_models::BlockHeader> for proto::consensus::BlockHeader { fn try_convert_proto_block_header( value: proto::consensus::BlockHeader, - justify_id: QcId, + justify_id: PcId, commands: &BTreeSet, ) -> Result { let network = u8::try_from(value.network) @@ -1034,8 +1034,8 @@ impl From for proto::consensus::SubstateDestroyedMetadata { // -------------------------------- SyncRequest -------------------------------- // -impl From<&SyncRequestMessage> for proto::consensus::SyncRequest { - fn from(value: &SyncRequestMessage) -> Self { +impl From<&CatchUpRequestMessage> for proto::consensus::SyncRequest { + fn from(value: &CatchUpRequestMessage) -> Self { Self { epoch: value.epoch.as_u64(), block_height: value.block_height.as_u64(), @@ -1043,7 +1043,7 @@ impl From<&SyncRequestMessage> for proto::consensus::SyncRequest { } } -impl TryFrom for SyncRequestMessage { +impl TryFrom for CatchUpRequestMessage { type Error = anyhow::Error; fn try_from(value: proto::consensus::SyncRequest) -> Result { diff --git a/crates/state_store_rocksdb/src/column_families/certificates.rs b/crates/state_store_rocksdb/src/column_families/certificates.rs index bad9df364a..0184677282 100644 --- a/crates/state_store_rocksdb/src/column_families/certificates.rs +++ b/crates/state_store_rocksdb/src/column_families/certificates.rs @@ -20,7 +20,7 @@ // 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_consensus_types::{ProposalCertificate, QcId, TcId, TimeoutCertificate}; +use tari_consensus_types::{PcId, ProposalCertificate, TcId, TimeoutCertificate}; use tari_ootle_common_types::Epoch; use crate::{ @@ -35,7 +35,7 @@ pub mod proposal { pub struct ProposalCertificateCf; impl Cf for ProposalCertificateCf { - type Key = (Epoch, QcId); + type Key = (Epoch, PcId); type KeyCodec = (EpochCodec, FixedBytesCodec32); type Value = ProposalCertificate; type ValueCodec = DefaultVersionedCodec; diff --git a/crates/state_store_rocksdb/src/column_families/diagnostic_no_vote.rs b/crates/state_store_rocksdb/src/column_families/diagnostic_no_vote.rs new file mode 100644 index 0000000000..bae3a92ae5 --- /dev/null +++ b/crates/state_store_rocksdb/src/column_families/diagnostic_no_vote.rs @@ -0,0 +1,28 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use serde::{Deserialize, Serialize}; +use tari_consensus_types::BlockId; + +use crate::{ + codecs::{BlockIdCodec, DefaultCodec}, + traits::Cf, +}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DiagnosticsNoVoteData { + pub reason: Box, +} + +pub struct DiagnosticsNoVoteCf; + +impl Cf for DiagnosticsNoVoteCf { + type Key = BlockId; + type KeyCodec = BlockIdCodec; + type Value = DiagnosticsNoVoteData; + type ValueCodec = DefaultCodec; + + fn name() -> &'static str { + "diagnostic_no_votes" + } +} diff --git a/crates/state_store_rocksdb/src/column_families/mod.rs b/crates/state_store_rocksdb/src/column_families/mod.rs index 93c47bdd74..d9927dee29 100644 --- a/crates/state_store_rocksdb/src/column_families/mod.rs +++ b/crates/state_store_rocksdb/src/column_families/mod.rs @@ -32,6 +32,7 @@ pub mod foreign_proposal; pub mod foreign_substate_pledge; pub mod certificates; +pub mod diagnostic_no_vote; pub mod finalized_transaction; pub mod lock_conflict; pub mod missing_transactions; diff --git a/crates/state_store_rocksdb/src/options.rs b/crates/state_store_rocksdb/src/options.rs index 67da6ba682..b3cb1a48a7 100644 --- a/crates/state_store_rocksdb/src/options.rs +++ b/crates/state_store_rocksdb/src/options.rs @@ -15,6 +15,18 @@ pub struct DatabaseOptions { /// The default is 1, which means we keep the previous epoch's data until this epoch has passed. It is not /// recommended to set this to 0. pub epoch_history_length: Epoch, + /// Whether to store additional debugging data in the database. This may increase storage requirements and slow + /// down some operations, so it should only be enabled for debugging purposes. + pub debugging_data: bool, +} + +impl DatabaseOptions { + /// Whether to store additional debugging data in the database. This may increase storage requirements and slow + /// down some operations, so it should only be enabled for debugging purposes. + pub fn with_debugging_data(mut self, debugging_data: bool) -> Self { + self.debugging_data = debugging_data; + self + } } impl Default for DatabaseOptions { @@ -22,6 +34,7 @@ impl Default for DatabaseOptions { Self { state_history_length: 100, epoch_history_length: Epoch(1), + debugging_data: false, } } } diff --git a/crates/state_store_rocksdb/src/reader.rs b/crates/state_store_rocksdb/src/reader.rs index 85a76e531f..6c6bea4e3b 100644 --- a/crates/state_store_rocksdb/src/reader.rs +++ b/crates/state_store_rocksdb/src/reader.rs @@ -40,8 +40,8 @@ use tari_consensus_types::{ LastVoted, LeafBlock, LockedBlock, + PcId, ProposalCertificate, - QcId, TcId, TimeoutCertificate, }; @@ -131,6 +131,7 @@ use crate::{ foreign_substate_pledge, foreign_substate_pledge::ForeignSubstatePledgeCf, lock_conflict, + parked_block, pending_state_tree_diff, state_transition, state_transition::StateTransitionType, @@ -589,6 +590,13 @@ impl<'tx, TAddr: NodeAddressable + Serialize + DeserializeOwned + 'tx> StateStor fn finalized_transaction_execution_get(&self, tx_id: &TransactionId) -> Result { const OPERATION: &str = "transaction_executions_get"; + let finalized_cf = self.db().cf(FinalizedTransactionLinkCf)?; + if !finalized_cf.exists(tx_id, OPERATION)? { + return Err(StorageError::NotFound { + item: "TransactionExecution", + key: format!("{tx_id}"), + }); + } let cf = self.db().cf(block_transaction_execution::ByTransactionIdQuery)?; let mut iter = cf.query_prefix_range_key_iterator(Ordering::default(), tx_id); let Some((tx_id, block_id, height)) = iter.next().transpose()? else { @@ -724,7 +732,7 @@ impl<'tx, TAddr: NodeAddressable + Serialize + DeserializeOwned + 'tx> StateStor const OPERATION: &str = "blocks_get_all_between"; // Prevent the possibility of memory exhaustion (defensive, not in response to an observed bug) - if limit > 1_000_000 { + if limit > 1_000 { return Err(StorageError::QueryError { reason: format!("{OPERATION}: limit {limit} is too large"), }); @@ -1091,7 +1099,7 @@ impl<'tx, TAddr: NodeAddressable + Serialize + DeserializeOwned + 'tx> StateStor }) } - fn proposal_certificates_get(&self, epoch: Epoch, qc_id: &QcId) -> Result { + fn proposal_certificates_get(&self, epoch: Epoch, qc_id: &PcId) -> Result { const OPERATION: &str = "proposal_certificates_get"; let qc = self.db().cf(ProposalCertificateCf)?.get(&(epoch, *qc_id), OPERATION)?; Ok(qc) @@ -1099,7 +1107,7 @@ impl<'tx, TAddr: NodeAddressable + Serialize + DeserializeOwned + 'tx> StateStor fn proposal_certificates_get_many<'a, I>(&self, qc_ids: I) -> Result, StorageError> where - I: IntoIterator, + I: IntoIterator, I::IntoIter: ExactSizeIterator, { const OPERATION: &str = "proposal_certificates_get_all"; @@ -1802,6 +1810,12 @@ impl<'tx, TAddr: NodeAddressable + Serialize + DeserializeOwned + 'tx> StateStor Ok(pledges) } + fn parked_block_exists(&self, block_id: &BlockId) -> Result { + const OPERATION: &str = "parked_block_exists"; + let exists = self.db().cf(parked_block::ParkedBlockCf)?.exists(block_id, OPERATION)?; + Ok(exists) + } + fn foreign_parked_blocks_exists(&self, block_id: &BlockId) -> Result { const OPERATION: &str = "foreign_parked_blocks_exists"; let cf = self.db().cf(ForeignParkedBlockCf)?; diff --git a/crates/state_store_rocksdb/src/store.rs b/crates/state_store_rocksdb/src/store.rs index b7edf65a7c..8abe9cb138 100644 --- a/crates/state_store_rocksdb/src/store.rs +++ b/crates/state_store_rocksdb/src/store.rs @@ -38,6 +38,7 @@ use crate::{ bookkeeping::DatabaseMigrationVersion, certificates::{proposal::ProposalCertificateCf, timeout::TimeoutCertificateCf}, chain, + diagnostic_no_vote::DiagnosticsNoVoteCf, epoch_checkpoint::EpochCheckpointCf, evicted_node::EvictedNodeCf, finalized_transaction::FinalizedTransactionLinkCf, @@ -123,6 +124,7 @@ pub fn all_column_families_iter() -> impl Iterator { lock_conflict::LockConflictBlockIdIndex::name(), EvictedNodeCf::name(), ValidatorNodeEpochStatsCf::name(), + DiagnosticsNoVoteCf::name(), ] .into_iter() } diff --git a/crates/state_store_rocksdb/src/writer.rs b/crates/state_store_rocksdb/src/writer.rs index 5beba01e0b..68f2492684 100644 --- a/crates/state_store_rocksdb/src/writer.rs +++ b/crates/state_store_rocksdb/src/writer.rs @@ -38,8 +38,8 @@ use tari_consensus_types::{ LastVoted, LeafBlock, LockedBlock, + PcId, ProposalCertificate, - QcId, TimeoutCertificate, }; use tari_engine_types::substate::SubstateId; @@ -117,6 +117,7 @@ use crate::{ certificates::{proposal::ProposalCertificateCf, timeout::TimeoutCertificateCf}, chain, chain::PendingChainIndex, + diagnostic_no_vote::{DiagnosticsNoVoteCf, DiagnosticsNoVoteData}, epoch_checkpoint::EpochCheckpointCf, evicted_node, evicted_node::{EvictedNodeCf, EvictedNodeData}, @@ -331,8 +332,8 @@ impl<'tx, TAddr: NodeAddressable + 'tx> StateStoreWriteTransaction for RocksDbSt fn blocks_set_qcs( &mut self, block_id: &BlockId, - commit_qc_id: Option<&QcId>, - justify_qc_id: Option<&QcId>, + commit_qc_id: Option<&PcId>, + justify_qc_id: Option<&PcId>, ) -> Result<(), StorageError> { const OPERATION: &str = "blocks_set_qcs"; if commit_qc_id.is_none() && justify_qc_id.is_none() { @@ -855,15 +856,16 @@ impl<'tx, TAddr: NodeAddressable + 'tx> StateStoreWriteTransaction for RocksDbSt cf.put(&(*block.block_id(), *update.transaction_id()), &value, OPERATION)?; - // TODO: remove CF - this is only used for debugging (or maybe make it configurable) - let cf = self - .db() - .cf(transaction_pool_state_update::TransactionPoolStateUpdateDebugHistoryCf)?; - cf.put( - &(block.epoch(), block.height(), *update.transaction_id()), - &value, - OPERATION, - )?; + if self.options.debugging_data { + let cf = self + .db() + .cf(transaction_pool_state_update::TransactionPoolStateUpdateDebugHistoryCf)?; + cf.put( + &(block.epoch(), block.height(), *update.transaction_id()), + &value, + OPERATION, + )?; + } // Set is_ready and pending_stage to the updated values. This allows has_uncommitted_transactions to return an // accurate value without querying records in the updates table. @@ -1721,8 +1723,18 @@ impl<'tx, TAddr: NodeAddressable + 'tx> StateStoreWriteTransaction for RocksDbSt Ok(()) } - fn diagnostics_add_no_vote(&mut self, _block_id: BlockId, _reason: NoVoteReason) -> Result<(), StorageError> { - // used for debugging. TODO: consider implementing as a user option or keeping in the global Sqlite db + fn diagnostics_add_no_vote(&mut self, block_id: BlockId, reason: NoVoteReason) -> Result<(), StorageError> { + const OPERATION: &str = "diagnostics_add_no_vote"; + if self.options.debugging_data { + self.db().cf(DiagnosticsNoVoteCf)?.insert( + &block_id, + &DiagnosticsNoVoteData { + reason: reason.to_string().into_boxed_str(), + }, + OPERATION, + )?; + } + Ok(()) } } diff --git a/crates/state_store_tests/src/blocks.rs b/crates/state_store_tests/src/blocks.rs index eca88b9a3f..559709d0cc 100644 --- a/crates/state_store_tests/src/blocks.rs +++ b/crates/state_store_tests/src/blocks.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_common_types::types::FixedHash; -use tari_consensus_types::{QcId, ShardGroupAccumulatedData}; +use tari_consensus_types::{PcId, ShardGroupAccumulatedData}; use tari_ootle_common_types::{ optional::Optional, Epoch, @@ -61,7 +61,7 @@ mod basic_block_operations { // set qcs let block1_from_db = tx.blocks_get(block1.id()).unwrap(); assert!(!block1_from_db.has_justify_qc()); - tx.blocks_set_qcs(block1_from_db.id(), None, Some(&QcId::zero())) + tx.blocks_set_qcs(block1_from_db.id(), None, Some(&PcId::zero())) .unwrap(); let block1_from_db = tx.blocks_get(block1.id()).unwrap(); assert!(block1_from_db.has_justify_qc()); @@ -69,7 +69,7 @@ mod basic_block_operations { // set is_commited flag let block1_from_db = tx.blocks_get(block1.id()).unwrap(); assert!(!block1_from_db.is_committed()); - tx.blocks_set_qcs(block1_from_db.id(), Some(&QcId::zero()), None) + tx.blocks_set_qcs(block1_from_db.id(), Some(&PcId::zero()), None) .unwrap(); let block1_from_db = tx.blocks_get(block1.id()).unwrap(); assert!(block1_from_db.is_committed()); @@ -179,9 +179,9 @@ mod block_parent_operations { assert_eq!(res, vec![]); // commit the blocks - tx.blocks_set_qcs(zero_block.id(), Some(&QcId::zero()), None).unwrap(); - tx.blocks_set_qcs(block1.id(), Some(&QcId::zero()), None).unwrap(); - tx.blocks_set_qcs(block2.id(), Some(&QcId::zero()), None).unwrap(); + tx.blocks_set_qcs(zero_block.id(), Some(&PcId::zero()), None).unwrap(); + tx.blocks_set_qcs(block1.id(), Some(&PcId::zero()), None).unwrap(); + tx.blocks_set_qcs(block2.id(), Some(&PcId::zero()), None).unwrap(); // blocks_get_all_by_parent let res = tx.blocks_get_committed_by_parent(zero_block.id()).unwrap(); @@ -229,7 +229,7 @@ mod block_query_operations { let zero_block = Block::zero_block(network, NumPreshards::P64); zero_block.insert(&mut tx).unwrap(); - tx.blocks_set_qcs(zero_block.id(), Some(&QcId::zero()), Some(&QcId::zero())) + tx.blocks_set_qcs(zero_block.id(), Some(&PcId::zero()), Some(&PcId::zero())) .unwrap(); let shard_group = ShardGroup::all_shards(NumPreshards::P64); @@ -255,7 +255,7 @@ mod block_query_operations { ) .unwrap(); block1.insert(&mut tx).unwrap(); - tx.blocks_set_qcs(block1.id(), Some(&QcId::zero()), Some(&QcId::zero())) + tx.blocks_set_qcs(block1.id(), Some(&PcId::zero()), Some(&PcId::zero())) .unwrap(); block1.as_locked().set(&mut tx).unwrap(); @@ -282,7 +282,7 @@ mod block_query_operations { ) .unwrap(); block2.insert(&mut tx).unwrap(); - tx.blocks_set_qcs(block2.id(), Some(&QcId::zero()), Some(&QcId::zero())) + tx.blocks_set_qcs(block2.id(), Some(&PcId::zero()), Some(&PcId::zero())) .unwrap(); tx.proposal_certificates_save(block2.justify()).unwrap(); diff --git a/crates/state_store_tests/src/helpers.rs b/crates/state_store_tests/src/helpers.rs index 334bb546c0..37aa9a8778 100644 --- a/crates/state_store_tests/src/helpers.rs +++ b/crates/state_store_tests/src/helpers.rs @@ -25,7 +25,7 @@ use std::{io::Write, ops::Deref}; use rand::{rngs::OsRng, Rng, RngCore}; use tari_bor::cbor; use tari_common_types::types::FixedHash; -use tari_consensus_types::{BlockId, Decision, LeafBlock, ProposalCertificate, QcId, ShardGroupAccumulatedData}; +use tari_consensus_types::{BlockId, Decision, LeafBlock, PcId, ProposalCertificate, ShardGroupAccumulatedData}; use tari_engine_types::{ component::{ComponentBody, ComponentHeader}, substate::{hash_substate, SubstateId, SubstateValue}, @@ -376,7 +376,7 @@ where chain[len - 3].as_locked().set(tx).unwrap(); for block in &chain[..len - 3] { - tx.blocks_set_qcs(block.id(), Some(&QcId::zero()), Some(&QcId::zero())) + tx.blocks_set_qcs(block.id(), Some(&PcId::zero()), Some(&PcId::zero())) .unwrap(); } diff --git a/crates/state_store_tests/src/misc.rs b/crates/state_store_tests/src/misc.rs index 5dd6eab74c..9796743a96 100644 --- a/crates/state_store_tests/src/misc.rs +++ b/crates/state_store_tests/src/misc.rs @@ -10,8 +10,8 @@ use tari_consensus_types::{ LastVoted, LeafBlock, LockedBlock, + PcId, ProposalVote, - QcId, ValidatorSignatureBytes, }; use tari_ootle_common_types::{optional::Optional, Epoch, Network, NodeHeight, ShardGroup}; @@ -151,7 +151,7 @@ fn miscellaneous_operations(db: impl StateStore) { block_id: BlockId::zero(), epoch, block_height: NodeHeight(123), - qc_id: QcId::zero(), + qc_id: PcId::zero(), }; tx.high_pc_set(&high_qc).unwrap(); let res = tx.high_pc_get(epoch).unwrap(); diff --git a/crates/state_store_tests/src/state_tree_diff.rs b/crates/state_store_tests/src/state_tree_diff.rs index d90155e2a3..afb2be86dd 100644 --- a/crates/state_store_tests/src/state_tree_diff.rs +++ b/crates/state_store_tests/src/state_tree_diff.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_consensus_types::QcId; +use tari_consensus_types::PcId; use tari_ootle_storage::{ consensus_models::{BookkeepingModel, PendingShardStateTreeDiff}, StateStore, @@ -23,16 +23,16 @@ fn pending_state_tree_diff_operations(db: impl StateStore) { // add some (committed) blocks to the database let mut genesis = create_block(None); - genesis.set_commit_qc(QcId::zero()); + genesis.set_commit_qc(PcId::zero()); genesis.insert(&mut tx).unwrap(); - tx.blocks_set_qcs(genesis.id(), Some(&QcId::zero()), Some(&QcId::zero())) + tx.blocks_set_qcs(genesis.id(), Some(&PcId::zero()), Some(&PcId::zero())) .unwrap(); tx.proposal_certificates_save(genesis.justify()).unwrap(); genesis.as_locked().set(&mut tx).unwrap(); genesis.as_leaf().set(&mut tx).unwrap(); let mut block_1 = create_block(Some(&genesis)); - block_1.set_commit_qc(QcId::zero()); + block_1.set_commit_qc(PcId::zero()); block_1.insert(&mut tx).unwrap(); let block_2 = create_block(Some(&block_1)); diff --git a/crates/state_store_tests/src/transactions.rs b/crates/state_store_tests/src/transactions.rs index 9ad479346c..3d2b876809 100644 --- a/crates/state_store_tests/src/transactions.rs +++ b/crates/state_store_tests/src/transactions.rs @@ -4,7 +4,7 @@ use std::time::Duration; use tari_common_types::types::{FixedHash, PrivateKey}; -use tari_consensus_types::{Decision, QcId, ShardGroupAccumulatedData}; +use tari_consensus_types::{Decision, PcId, ShardGroupAccumulatedData}; use tari_engine_types::{ commit_result::{ExecuteResult, FinalizeResult, TransactionResult}, fees::{FeeBreakdown, FeeReceipt}, @@ -55,7 +55,7 @@ mod confirm_all_transitions { let zero_block = Block::zero_block(network, TEST_NUM_PRESHARDS); zero_block.insert(&mut tx).unwrap(); tx.proposal_certificates_save(zero_block.justify()).unwrap(); - tx.blocks_set_qcs(zero_block.id(), Some(&QcId::zero()), Some(&QcId::zero())) + tx.blocks_set_qcs(zero_block.id(), Some(&PcId::zero()), Some(&PcId::zero())) .unwrap(); let shard_group = zero_block.shard_group(); diff --git a/crates/storage/src/consensus_models/block.rs b/crates/storage/src/consensus_models/block.rs index e1d63affad..341728c4d8 100644 --- a/crates/storage/src/consensus_models/block.rs +++ b/crates/storage/src/consensus_models/block.rs @@ -20,8 +20,8 @@ use tari_consensus_types::{ LastVoted, LeafBlock, LockedBlock, + PcId, ProposalCertificate, - QcId, ShardGroupAccumulatedData, TimeoutCertificate, }; @@ -99,10 +99,10 @@ pub struct Block { // Metadata - not included in the block hash /// The QC that justified this block #[cfg_attr(feature = "ts", ts(type = "string | null"))] - justify_qc_id: Option, + justify_qc_id: Option, /// The QC that caused this block to be committed #[cfg_attr(feature = "ts", ts(type = "string | null"))] - commit_qc_id: Option, + commit_qc_id: Option, #[cfg_attr(feature = "ts", ts(type = "number | null"))] block_time: Option, /// Timestamp when was this stored. @@ -401,7 +401,7 @@ impl Block { self.justify_qc_id.is_some() } - pub fn justify_qc_id(&self) -> Option { + pub fn justify_qc_id(&self) -> Option { self.justify_qc_id } @@ -446,15 +446,15 @@ impl Block { Ok(proof) } - pub fn set_justify_qc(&mut self, justify_qc_id: QcId) { + pub fn set_justify_qc(&mut self, justify_qc_id: PcId) { self.justify_qc_id = Some(justify_qc_id); } - pub fn set_commit_qc(&mut self, commit_qc_id: QcId) { + pub fn set_commit_qc(&mut self, commit_qc_id: PcId) { self.commit_qc_id = Some(commit_qc_id); } - pub fn commit_qc_id(&self) -> Option<&QcId> { + pub fn commit_qc_id(&self) -> Option<&PcId> { self.commit_qc_id.as_ref() } } @@ -576,7 +576,7 @@ impl Block { tx.blocks_delete(block_id) } - pub fn commit_block_without_state_changes(&self, tx: &mut TTx, commit_qc_id: &QcId) -> Result<(), StorageError> + pub fn commit_block_without_state_changes(&self, tx: &mut TTx, commit_qc_id: &PcId) -> Result<(), StorageError> where TTx: StateStoreWriteTransaction + Deref, TTx::Target: StateStoreReadTransaction, @@ -587,7 +587,7 @@ impl Block { pub fn commit_block( &self, tx: &mut TTx, - commit_qc_id: &QcId, + commit_qc_id: &PcId, version_updates: &HashMap, ) -> Result<(), StorageError> where @@ -676,7 +676,7 @@ impl Block { pub fn add_justify_qc( &mut self, tx: &mut TTx, - qc_id: &QcId, + qc_id: &PcId, ) -> Result<(), StorageError> { self.justify_qc_id = Some(*qc_id); tx.blocks_set_qcs(self.id(), None, Some(qc_id)) @@ -881,11 +881,13 @@ impl Block { let locked = LockedBlock::get(tx, self.epoch())?; // Liveness rules - if self.justify().height() > locked.height() { + // (qc.viewNumber > lockedQC.viewNumber) + if self.max_certificate_height() > locked.height() { return Ok(true); } // Safety rule + // (node extends from lockedQC.node) if self.extends_pending(tx, locked.block_id())? { return Ok(true); } diff --git a/crates/storage/src/consensus_models/block_header.rs b/crates/storage/src/consensus_models/block_header.rs index 0380ce4562..e3b4c87559 100644 --- a/crates/storage/src/consensus_models/block_header.rs +++ b/crates/storage/src/consensus_models/block_header.rs @@ -15,8 +15,8 @@ use tari_consensus_types::{ LastVoted, LeafBlock, LockedBlock, + PcId, ProposalCertificate, - QcId, ShardGroupAccumulatedData, SignedMessage, ToSignatureMessage, @@ -44,7 +44,7 @@ pub struct BlockHeader { parent: BlockId, /// The quorum certificate proposed in this block. Note that this QC justifies a previous block. #[cfg_attr(feature = "ts", ts(type = "string"))] - justify_id: QcId, + justify_id: PcId, /// Block height. height: NodeHeight, /// Epoch this block belongs to. @@ -87,7 +87,7 @@ impl BlockHeader { pub fn create( network: Network, parent: BlockId, - justify_id: QcId, + justify_id: PcId, height: NodeHeight, epoch: Epoch, shard_group: ShardGroup, @@ -127,7 +127,7 @@ impl BlockHeader { pub fn create_unsigned( network: Network, parent: BlockId, - justify_id: QcId, + justify_id: PcId, height: NodeHeight, epoch: Epoch, shard_group: ShardGroup, @@ -166,7 +166,7 @@ impl BlockHeader { pub fn genesis( network: Network, - justify_id: QcId, + justify_id: PcId, epoch: Epoch, shard_group: ShardGroup, state_merkle_root: FixedHash, @@ -225,7 +225,7 @@ impl BlockHeader { parent: BlockId, proposed_by: RistrettoPublicKeyBytes, height: NodeHeight, - justify_id: QcId, + justify_id: PcId, epoch: Epoch, shard_group: ShardGroup, parent_state_merkle_root: FixedHash, @@ -380,7 +380,7 @@ impl BlockHeader { &self.parent } - pub fn justify_id(&self) -> &QcId { + pub fn justify_id(&self) -> &PcId { &self.justify_id } @@ -436,6 +436,10 @@ impl BlockHeader { &self.accumulated_data } + pub fn total_accumulated_exhaust_burn(&self) -> u128 { + self.accumulated_data.total_exhaust_burn + } + pub fn epoch_hash(&self) -> &FixedHash { &self.epoch_hash } diff --git a/crates/storage/src/consensus_models/evidence.rs b/crates/storage/src/consensus_models/evidence.rs index b9c36d268b..da67c7f481 100644 --- a/crates/storage/src/consensus_models/evidence.rs +++ b/crates/storage/src/consensus_models/evidence.rs @@ -7,7 +7,7 @@ use borsh::BorshSerialize; use indexmap::IndexMap; use log::*; use serde::{Deserialize, Serialize}; -use tari_consensus_types::QcId; +use tari_consensus_types::PcId; use tari_engine_types::{serde_with, substate::SubstateId}; use tari_ootle_common_types::{ borsh::indexmap as indexmap_borsh, @@ -248,7 +248,7 @@ impl Evidence { self.evidence.contains_key(shard_group) } - pub fn qc_ids_iter(&self) -> impl Iterator + '_ { + pub fn qc_ids_iter(&self) -> impl Iterator + '_ { self.evidence .values() .flat_map(|e| e.prepare_qc.iter().chain(e.accept_qc.iter())) @@ -413,9 +413,9 @@ pub struct ShardGroupEvidence { #[cfg_attr(feature = "ts", ts(type = "Record"))] outputs: IndexMap, #[cfg_attr(feature = "ts", ts(type = "string | null"))] - prepare_qc: Option, + prepare_qc: Option, #[cfg_attr(feature = "ts", ts(type = "string | null"))] - accept_qc: Option, + accept_qc: Option, } impl ShardGroupEvidence { @@ -536,7 +536,7 @@ impl ShardGroupEvidence { self } - pub fn set_prepare_qc(&mut self, qc_id: QcId) -> &mut Self { + pub fn set_prepare_qc(&mut self, qc_id: PcId) -> &mut Self { debug!( target: LOG_TARGET, "set_prepare_qc: QC[{qc_id}]", @@ -545,11 +545,11 @@ impl ShardGroupEvidence { self } - pub fn prepare_qc(&self) -> Option<&QcId> { + pub fn prepare_qc(&self) -> Option<&PcId> { self.prepare_qc.as_ref() } - pub fn set_accept_qc(&mut self, qc_id: QcId) -> &mut Self { + pub fn set_accept_qc(&mut self, qc_id: PcId) -> &mut Self { debug!( target: LOG_TARGET, "set_accept_qc: QC[{qc_id}]", @@ -558,7 +558,7 @@ impl ShardGroupEvidence { self } - pub fn accept_qc(&self) -> Option<&QcId> { + pub fn accept_qc(&self) -> Option<&PcId> { self.accept_qc.as_ref() } } diff --git a/crates/storage/src/consensus_models/no_vote.rs b/crates/storage/src/consensus_models/no_vote.rs index 4dc5bdcd7a..198d7ee6ff 100644 --- a/crates/storage/src/consensus_models/no_vote.rs +++ b/crates/storage/src/consensus_models/no_vote.rs @@ -53,6 +53,8 @@ pub enum NoVoteReason { LeaderFeeDisagreement, #[error("Total leader fee disagreement")] TotalLeaderFeeDisagreement, + #[error("Total accumulated exhaust burn disagreement")] + TotalExhaustBurnDisagreement, #[error("No leader fee")] NoLeaderFee, #[error("Local only proposed for multi shard")] @@ -116,6 +118,7 @@ impl NoVoteReason { Self::AllAcceptMustBeCommit { .. } => "AllAcceptMustBeCommit", Self::FeeDisagreement => "FeeDisagreement", Self::LeaderFeeDisagreement => "LeaderFeeDisagreement", + Self::TotalExhaustBurnDisagreement => "TotalExhaustBurnDisagreement", Self::NoLeaderFee => "NoLeaderFee", Self::LocalOnlyProposedForMultiShard => "LocalOnlyProposedForMultiShard", Self::MultiShardProposedForLocalOnly => "MultiShardProposedForLocalOnly", diff --git a/crates/storage/src/state_store/mod.rs b/crates/storage/src/state_store/mod.rs index 30f2e46541..1c7d821e2f 100644 --- a/crates/storage/src/state_store/mod.rs +++ b/crates/storage/src/state_store/mod.rs @@ -18,8 +18,8 @@ use tari_consensus_types::{ LastVoted, LeafBlock, LockedBlock, + PcId, ProposalCertificate, - QcId, TcId, TimeoutCertificate, }; @@ -202,10 +202,10 @@ pub trait StateStoreReadTransaction: Sized { ) -> Result; // -------------------------------- ProposalCertificate -------------------------------- // - fn proposal_certificates_get(&self, epoch: Epoch, qc_id: &QcId) -> Result; + fn proposal_certificates_get(&self, epoch: Epoch, qc_id: &PcId) -> Result; fn proposal_certificates_get_many<'a, I>(&self, qc_ids: I) -> Result, StorageError> where - I: IntoIterator, + I: IntoIterator, I::IntoIter: ExactSizeIterator; // -------------------------------- TimeoutCertificate -------------------------------- // @@ -324,6 +324,8 @@ pub trait StateStoreReadTransaction: Sized { &self, transaction_id: &TransactionId, ) -> Result; + // -------------------------------- Parked blocks / Missing Transactions -------------------------------- // + fn parked_block_exists(&self, block_id: &BlockId) -> Result; // -------------------------------- Foreign parked block -------------------------------- // fn foreign_parked_blocks_exists(&self, block_id: &BlockId) -> Result; @@ -362,8 +364,8 @@ pub trait StateStoreWriteTransaction { fn blocks_set_qcs( &mut self, block_id: &BlockId, - commit_qc_id: Option<&QcId>, - justify_qc_id: Option<&QcId>, + commit_qc_id: Option<&PcId>, + justify_qc_id: Option<&PcId>, ) -> Result<(), StorageError>; // -------------------------------- BlockDiff -------------------------------- // diff --git a/crates/template_builtin/templates/faucet/src/lib.rs b/crates/template_builtin/templates/faucet/src/lib.rs index 9f949c6add..c70622e8e8 100644 --- a/crates/template_builtin/templates/faucet/src/lib.rs +++ b/crates/template_builtin/templates/faucet/src/lib.rs @@ -5,6 +5,7 @@ use tari_template_lib::prelude::*; #[template] mod template { + const FAUCET_MAX: u64 = 1_000_000_000; use super::*; pub struct XtrFaucet { @@ -13,6 +14,12 @@ mod template { impl XtrFaucet { pub fn take(&self, amount: Amount) -> Bucket { + assert!( + amount <= FAUCET_MAX, + "Requested amount {} exceeds faucet max of {}", + amount, + FAUCET_MAX + ); debug!("Withdrawing {} coins from faucet", amount); let signer = CallerContext::transaction_signer_public_key(); emit_event("take", [("amount", amount.to_string()), ("signer", signer.to_string())]); @@ -25,6 +32,12 @@ mod template { output: StealthOutputsStatement, balance_proof: Option, ) -> Option { + assert!( + amount <= FAUCET_MAX, + "Requested amount {} exceeds faucet max of {}", + amount, + FAUCET_MAX + ); let signer = CallerContext::transaction_signer_public_key(); let revealed_bucket = self.vault.withdraw(amount); let transfer = StealthTransferStatement { diff --git a/crates/template_builtin/templates/nft_faucet/src/lib.rs b/crates/template_builtin/templates/nft_faucet/src/lib.rs index a13d67135d..a1f8102f96 100644 --- a/crates/template_builtin/templates/nft_faucet/src/lib.rs +++ b/crates/template_builtin/templates/nft_faucet/src/lib.rs @@ -43,7 +43,7 @@ mod template { let owner = CallerContext::transaction_signer_public_key().to_string(); let mut metadata = Metadata::new(); - metadata.insert("original_owner", &owner); + metadata.insert("original_minter", &owner); let mut counter = 0; let amount_to_mint = amount.to_u64_checked().expect("Amount must be a positive"); diff --git a/crates/template_lib/src/models/claimed_output_tombstone.rs b/crates/template_lib/src/models/claimed_output_tombstone.rs index e6e8eafd5c..84925ef653 100644 --- a/crates/template_lib/src/models/claimed_output_tombstone.rs +++ b/crates/template_lib/src/models/claimed_output_tombstone.rs @@ -23,7 +23,7 @@ const TAG: u64 = BinaryTag::ClaimedOutputTombstoneAddress.as_u64(); pub struct ClaimedOutputTombstoneAddress(#[cfg_attr(feature = "ts", ts(type = "string"))] BorTag); impl ClaimedOutputTombstoneAddress { - pub fn new(key: ObjectKey) -> Self { + pub const fn new(key: ObjectKey) -> Self { Self(BorTag::new(key)) } @@ -31,20 +31,20 @@ impl ClaimedOutputTombstoneAddress { Ok(Self(BorTag::new(ObjectKey::from_hex(hex)?))) } - pub fn from_commitment(commitment_bytes: PedersenCommitmentBytes) -> Self { + pub const fn from_commitment(commitment_bytes: PedersenCommitmentBytes) -> Self { Self(BorTag::new(ObjectKey::from_array(commitment_bytes.into_array()))) } - pub fn as_object_key(&self) -> &ObjectKey { - &self.0 + pub const fn as_object_key(&self) -> &ObjectKey { + self.0.inner() } pub fn from_bytes(bytes: &[u8]) -> Result { Ok(Self(BorTag::new(ObjectKey::try_from(bytes)?))) } - pub fn as_bytes(&self) -> &[u8] { - self.0.inner() + pub const fn as_bytes(&self) -> &[u8] { + self.0.inner().array() } } diff --git a/crates/template_lib/src/models/component.rs b/crates/template_lib/src/models/component.rs index 33db983424..ee443fde8d 100644 --- a/crates/template_lib/src/models/component.rs +++ b/crates/template_lib/src/models/component.rs @@ -48,8 +48,8 @@ impl ComponentAddress { } /// Returns the underlying `ObjectKey` of this `ComponentAddress`. - pub fn as_object_key(&self) -> &ObjectKey { - &self.0 + pub const fn as_object_key(&self) -> &ObjectKey { + self.0.inner() } /// Returns the underlying byte slice. diff --git a/crates/template_lib/src/models/resource.rs b/crates/template_lib/src/models/resource.rs index 39a6b94a76..30be81a7a6 100644 --- a/crates/template_lib/src/models/resource.rs +++ b/crates/template_lib/src/models/resource.rs @@ -44,7 +44,7 @@ impl ResourceAddress { Self(BorTag::new(key)) } - pub fn as_object_key(&self) -> &ObjectKey { + pub const fn as_object_key(&self) -> &ObjectKey { self.0.inner() } diff --git a/crates/template_lib/src/models/vault.rs b/crates/template_lib/src/models/vault.rs index f2f082f27e..460bfaaf44 100644 --- a/crates/template_lib/src/models/vault.rs +++ b/crates/template_lib/src/models/vault.rs @@ -79,7 +79,7 @@ impl VaultId { Ok(Self::new(key)) } - pub fn as_object_key(&self) -> &ObjectKey { + pub const fn as_object_key(&self) -> &ObjectKey { self.0.inner() } diff --git a/crates/template_lib_types/src/crypto/commitment.rs b/crates/template_lib_types/src/crypto/commitment.rs index 0b4370c044..3e0e1e53c2 100644 --- a/crates/template_lib_types/src/crypto/commitment.rs +++ b/crates/template_lib_types/src/crypto/commitment.rs @@ -35,11 +35,11 @@ impl PedersenCommitmentBytes { Self([0u8; Self::length()]) } - pub fn from_public_key(commitment: RistrettoPublicKeyBytes) -> Self { + pub const fn from_public_key(commitment: RistrettoPublicKeyBytes) -> Self { Self(commitment.into_array()) } - pub fn from_array(bytes: [u8; Self::length()]) -> Self { + pub const fn from_array(bytes: [u8; Self::length()]) -> Self { Self(bytes) } @@ -61,15 +61,15 @@ impl PedersenCommitmentBytes { Ok(Self(bytes)) } - pub fn as_bytes(&self) -> &[u8] { + pub const fn as_bytes(&self) -> &[u8] { &self.0 } - pub fn into_array(self) -> [u8; Self::length()] { + pub const fn into_array(self) -> [u8; Self::length()] { self.0 } - pub fn as_hash(&self) -> Hash { + pub const fn as_hash(&self) -> Hash { Hash::from_array(self.0) } } diff --git a/crates/template_lib_types/src/crypto/ristretto.rs b/crates/template_lib_types/src/crypto/ristretto.rs index 8a056ec836..c2a3fa522b 100644 --- a/crates/template_lib_types/src/crypto/ristretto.rs +++ b/crates/template_lib_types/src/crypto/ristretto.rs @@ -24,7 +24,7 @@ impl RistrettoPublicKeyBytes { 32 } - pub fn zero() -> Self { + pub const fn zero() -> Self { Self([0u8; Self::length()]) } @@ -46,7 +46,7 @@ impl RistrettoPublicKeyBytes { Ok(Self(bytes)) } - pub fn as_bytes(&self) -> &[u8] { + pub const fn as_bytes(&self) -> &[u8] { &self.0 } @@ -54,11 +54,11 @@ impl RistrettoPublicKeyBytes { self.0.iter().all(|&b| b == 0) } - pub fn into_array(self) -> [u8; Self::length()] { + pub const fn into_array(self) -> [u8; Self::length()] { self.0 } - pub fn as_hash(&self) -> Hash { + pub const fn as_hash(&self) -> Hash { Hash::from_array(self.0) } } diff --git a/crates/template_lib_types/src/crypto/scalar.rs b/crates/template_lib_types/src/crypto/scalar.rs index c358313449..e9f15c7c71 100644 --- a/crates/template_lib_types/src/crypto/scalar.rs +++ b/crates/template_lib_types/src/crypto/scalar.rs @@ -21,7 +21,7 @@ impl Scalar32Bytes { 32 } - pub fn zero() -> Self { + pub const fn zero() -> Self { Self([0u8; Self::length()]) } diff --git a/crates/template_lib_types/src/crypto/schnorr.rs b/crates/template_lib_types/src/crypto/schnorr.rs index c3da781e71..d90ab7026e 100644 --- a/crates/template_lib_types/src/crypto/schnorr.rs +++ b/crates/template_lib_types/src/crypto/schnorr.rs @@ -19,7 +19,7 @@ impl SchnorrSignatureBytes { RistrettoPublicKeyBytes::length() + Scalar32Bytes::length() } - pub fn zero() -> Self { + pub const fn zero() -> Self { Self { public_nonce: RistrettoPublicKeyBytes::zero(), signature: Scalar32Bytes::zero(), diff --git a/crates/template_lib_types/src/entity_id.rs b/crates/template_lib_types/src/entity_id.rs index c7d5417555..ead663a16c 100644 --- a/crates/template_lib_types/src/entity_id.rs +++ b/crates/template_lib_types/src/entity_id.rs @@ -158,7 +158,7 @@ impl ObjectKey { self.0 } - pub fn array(&self) -> &[u8; Self::LENGTH] { + pub const fn array(&self) -> &[u8; Self::LENGTH] { &self.0 } diff --git a/crates/transaction/src/transaction.rs b/crates/transaction/src/transaction.rs index 4c3a3d696b..7009797521 100644 --- a/crates/transaction/src/transaction.rs +++ b/crates/transaction/src/transaction.rs @@ -176,10 +176,6 @@ impl Transaction { self.inputs().iter().map(|i| i.substate_id()) } - pub fn has_inputs(&self) -> bool { - !self.inputs().is_empty() - } - /// Returns true if the provided committee is involved in at least one input or known output of this transaction. /// A committee may be involved even if this function returns false if and only if it is involved in outputs only. pub fn is_involved(&self, committee_info: &CommitteeInfo) -> bool { diff --git a/crates/transaction/src/v1/transaction.rs b/crates/transaction/src/v1/transaction.rs index 6b0b4f7b9f..40786ec0de 100644 --- a/crates/transaction/src/v1/transaction.rs +++ b/crates/transaction/src/v1/transaction.rs @@ -1,7 +1,7 @@ // Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{collections::HashSet, fmt::Display}; +use std::{collections::HashSet, fmt::Display, iter}; use indexmap::IndexSet; use log::*; @@ -13,7 +13,10 @@ use tari_engine_types::{ substate::SubstateId, }; use tari_ootle_common_types::{Epoch, SubstateRequirement, SubstateRequirementRef}; -use tari_template_lib::models::{ComponentAddress, StealthTransferStatement}; +use tari_template_lib::{ + constants::XTR, + models::{ComponentAddress, StealthTransferStatement}, +}; use crate::{ args::InstructionArg, @@ -26,6 +29,8 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::transaction::transaction"; +static XTR_REQUIREMENT: SubstateRequirement = SubstateRequirement::new(SubstateId::Resource(XTR), None); + #[derive(Debug, Clone, Serialize, Deserialize, borsh::BorshSerialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct TransactionV1 { @@ -111,7 +116,12 @@ impl TransactionV1 { } pub fn all_inputs_iter(&self) -> impl Iterator> + '_ { - self.inputs().iter().map(Into::into) + self.inputs() + .iter() + .filter(|id| id.substate_id().as_resource_address() != Some(XTR)) + // Ensure XTR requirement is always included since every transaction needs to pay fees in XTR + .chain(iter::once(&XTR_REQUIREMENT)) + .map(Into::into) } pub fn all_published_templates_iter(&self) -> impl Iterator + '_ { diff --git a/crates/transaction/src/v1/unsealed.rs b/crates/transaction/src/v1/unsealed.rs index a2758842aa..21f1c847fe 100644 --- a/crates/transaction/src/v1/unsealed.rs +++ b/crates/transaction/src/v1/unsealed.rs @@ -43,7 +43,7 @@ impl UnsealedTransactionV1 { self.set_seal_signature(sig) } - pub fn set_seal_signature(self, signature: TransactionSealSignature) -> Transaction { + fn set_seal_signature(self, signature: TransactionSealSignature) -> Transaction { TransactionV1::new(self, signature).into() } diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs index dc38fcb022..b6f1487557 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -719,34 +719,27 @@ impl<'a, TSpec: WalletSdkSpec> StealthTransferApi<'a, TSpec> { // Badge if required match ¶ms.badge_usage { BadgeUsage::None => builder, - BadgeUsage::Resource(resx) => { - builder.call_method( - *owner_account.component_address(), - "create_proof_for_resource", - args![resx], - ) + BadgeUsage::Resource(resx) => builder + .call_method(*owner_account.component_address(), "create_proof_for_resource", args![ + resx + ]) .put_last_instruction_output_on_workspace("proof") - .add_input(*resx) - }, - BadgeUsage::NonFungible(nft) => { - builder.call_method( + .add_input(*resx), + BadgeUsage::NonFungible(nft) => builder + .call_method( *owner_account.component_address(), "create_proof_by_non_fungible", args![nft], ) .put_last_instruction_output_on_workspace("proof") .add_input(*nft.resource_address()) - .add_input(nft.clone()) - } - BadgeUsage::AmountOfResource { amount, resource } => { - builder.call_method( - *owner_account.component_address(), - "create_proof_by_amount", - args![resource, amount], - ) + .add_input(nft.clone()), + BadgeUsage::AmountOfResource { amount, resource } => builder + .call_method(*owner_account.component_address(), "create_proof_by_amount", args![ + resource, amount + ]) .put_last_instruction_output_on_workspace("proof") - .add_input(*resource) - } + .add_input(*resource), } }) .then(|builder| { @@ -778,7 +771,8 @@ impl<'a, TSpec: WalletSdkSpec> StealthTransferApi<'a, TSpec> { } let needs_to_split = params.outputs.len() > 1; - let dest_account = derive_account_address_from_public_key(output.address.account_public_key()); + let dest_account = + derive_account_address_from_public_key(output.address.account_public_key()); let need_to_create_account = accounts_to_create.contains(&dest_account); if needs_to_split { let sub_bucket_name = format!("output-sub-bucket-{i}"); @@ -787,26 +781,18 @@ impl<'a, TSpec: WalletSdkSpec> StealthTransferApi<'a, TSpec> { .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) .create_account_with_bucket( *output.address.account_public_key(), - sub_bucket_name + sub_bucket_name, ) } else { builder .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) - .call_method(dest_account, "deposit", args![Workspace( - sub_bucket_name - )]) + .call_method(dest_account, "deposit", args![Workspace(sub_bucket_name)]) } } else if need_to_create_account { builder - .create_account_with_bucket( - *output.address.account_public_key(), - "output_bucket" - ) + .create_account_with_bucket(*output.address.account_public_key(), "output_bucket") } else { - builder - .call_method(dest_account, "deposit", args![Workspace( - "output_bucket" - )]) + builder.call_method(dest_account, "deposit", args![Workspace("output_bucket")]) } }) }) @@ -819,8 +805,6 @@ impl<'a, TSpec: WalletSdkSpec> StealthTransferApi<'a, TSpec> { } }) .with_inputs(inputs) - // TODO: remove the need to add this input - .add_input(XTR) .build_unsigned_transaction(); Ok(transaction) diff --git a/crates/wallet/sdk/src/apis/transaction.rs b/crates/wallet/sdk/src/apis/transaction.rs index fa5a28a48f..eaf973048c 100644 --- a/crates/wallet/sdk/src/apis/transaction.rs +++ b/crates/wallet/sdk/src/apis/transaction.rs @@ -323,13 +323,14 @@ where ) -> Result<(), TransactionApiError> { let mut downed_substates_with_parents = HashMap::with_capacity(diff.down_len()); for (id, _) in diff.down_iter() { - if id.is_layer1_commitment() { - info!(target: LOG_TARGET, "Layer 1 commitment {} downed", id); + if id.is_claimed_output_tombstone() { + // Should never happen + warn!(target: LOG_TARGET, "❓️ Claimed tombstone {} downed", id); continue; } let Some(downed) = tx.substates_remove(id).optional()? else { - warn!(target: LOG_TARGET, "Downed substate {} not found", id); + debug!(target: LOG_TARGET, "Downed substate {} not found", id); continue; }; @@ -341,15 +342,15 @@ where let (components, mut other_substates) = diff.up_iter().partition::, _>(|(addr, _)| addr.is_component()); for (component_addr, substate) in components { - let header = substate.substate_value().component().unwrap(); - let indexed = IndexedWellKnownTypes::from_value(header.state())?; + let component = substate.substate_value().component().unwrap(); + let indexed = IndexedWellKnownTypes::from_value(component.state())?; debug!(target: LOG_TARGET, "Substate {} up", component_addr); tx.substates_upsert_root( VersionedSubstateIdRef::new(component_addr, substate.version()), indexed.referenced_substates().collect(), - Some(header.module_name.clone()), - Some(header.template_address), + Some(component.module_name.clone()), + Some(component.template_address), )?; for owned_id in indexed.referenced_substates() { @@ -442,22 +443,21 @@ where } }, None => { - // This should never happen because Vaults can't dangle and these are removed by the - // previous loop above - warn!(target: LOG_TARGET, "Vault {} does not have a parent", vault_id); - tx.substates_upsert_root( - VersionedSubstateIdRef::new(id, substate.version()), - [(*substate - .substate_value() - .vault() - .expect("should be vault") - .resource_address()) - .into()] - .into_iter() - .collect(), - None, - None, - )?; + // We don't know the parent account of this vault. + debug!(target: LOG_TARGET, "Vault {} does not have a parent", vault_id); + // tx.substates_upsert_root( + // VersionedSubstateIdRef::new(id, substate.version()), + // [(*substate + // .substate_value() + // .vault() + // .expect("should be vault") + // .resource_address()) + // .into()] + // .into_iter() + // .collect(), + // None, + // None, + // )?; }, } continue; diff --git a/crates/wallet/sdk/src/local_key_store.rs b/crates/wallet/sdk/src/local_key_store.rs index 5f26c28bb9..92815f3ff1 100644 --- a/crates/wallet/sdk/src/local_key_store.rs +++ b/crates/wallet/sdk/src/local_key_store.rs @@ -84,10 +84,7 @@ fn derive_private_key(seed: &CipherSeed, branch_seed: String, account: u64) -> P // At compile time, fail if the length of the derived key is not equal to the expected length which would lead to a // runtime panic - const { - assert_equal(RistrettoSecretKey::WIDE_REDUCTION_LEN, U64::INT); - } - // const _: () = assert_equal(RistrettoSecretKey::WIDE_REDUCTION_LEN, U64::INT); + const _: () = assert_equal(RistrettoSecretKey::WIDE_REDUCTION_LEN, U64::INT); PrivateKey::from_uniform_bytes(derive_key.as_ref()).expect("derived key length matches RistrettoSecretKey length") } diff --git a/crates/wallet/sdk_services/src/indexer_rest_api.rs b/crates/wallet/sdk_services/src/indexer_rest_api.rs index 0456b2a169..706aac3b91 100644 --- a/crates/wallet/sdk_services/src/indexer_rest_api.rs +++ b/crates/wallet/sdk_services/src/indexer_rest_api.rs @@ -169,10 +169,7 @@ impl WalletNetworkInterface for IndexerRestApiNetworkInterface { template_address: TemplateAddress, ) -> Result { let mut client = self.get_client()?; - let resp = client - .get_template_definition(tari_indexer_client::types::GetTemplateDefinitionRequest { template_address }) - .await?; - + let resp = client.get_template_definition(template_address).await?; Ok(resp.definition) } diff --git a/integration_tests/tests/steps/wallet_daemon.rs b/integration_tests/tests/steps/wallet_daemon.rs index afd42c6637..7c3d529a6c 100644 --- a/integration_tests/tests/steps/wallet_daemon.rs +++ b/integration_tests/tests/steps/wallet_daemon.rs @@ -121,7 +121,6 @@ async fn when_i_run_up_fees(world: &mut TariWorld, amount: u64, wallet_daemon_na let transaction = transaction_builder() .fee_transaction_pay_from_component(*account.component_address(), 100_000) .call_function(template.address, "new", args![payload]) - .add_input(XTR) .add_input(*account.component_address()) .build_unsigned_transaction(); diff --git a/networking/core/src/worker.rs b/networking/core/src/worker.rs index 2d1cc475d3..669841c2ea 100644 --- a/networking/core/src/worker.rs +++ b/networking/core/src/worker.rs @@ -66,7 +66,7 @@ use crate::{ ReachabilityMode, }; -const LOG_TARGET: &str = "tari::ootle::networking::service::worker"; +const LOG_TARGET: &str = "tari::networking::service::worker"; type ReplyTx = oneshot::Sender>; diff --git a/utilities/db_inspector/src/webserver/server.rs b/utilities/db_inspector/src/webserver/server.rs index 73fbe4db27..e6979b705b 100644 --- a/utilities/db_inspector/src/webserver/server.rs +++ b/utilities/db_inspector/src/webserver/server.rs @@ -110,7 +110,8 @@ pub async fn run(context: HandlerContext) -> anyhow::Result<()> { column_families::lock_conflict::LockConflictCf, column_families::lock_conflict::LockConflictBlockIdIndex, column_families::evicted_node::EvictedNodeCf, - column_families::validator_node_epoch_stats::ValidatorNodeEpochStatsCf + column_families::validator_node_epoch_stats::ValidatorNodeEpochStatsCf, + column_families::diagnostic_no_vote::DiagnosticsNoVoteCf ); let api = api.fallback(handlers::not_found); diff --git a/utilities/tariswap_test_bench/src/accounts.rs b/utilities/tariswap_test_bench/src/accounts.rs index b3f507db1c..b8662963dc 100644 --- a/utilities/tariswap_test_bench/src/accounts.rs +++ b/utilities/tariswap_test_bench/src/accounts.rs @@ -165,7 +165,7 @@ impl Runner { .accounts_api() .get_vault_by_resource(&fee_account.component_address, &XTR)?; - for accounts in all_accounts.chunks(100) { + for accounts in all_accounts.chunks(25) { let transaction = self .new_transaction_builder() .fee_transaction_pay_from_component(fee_account.component_address, 1000 * accounts.len()) @@ -182,7 +182,6 @@ impl Runner { }) }) .with_inputs([ - SubstateRequirement::unversioned(XTR), SubstateRequirement::unversioned(XTR_FAUCET_COMPONENT_ADDRESS), SubstateRequirement::unversioned(XTR_FAUCET_VAULT_ADDRESS), SubstateRequirement::unversioned(faucet.component_address), @@ -238,7 +237,7 @@ impl Runner { )?; } } - info!("✅ Funded 100 accounts"); + info!("✅ Funded 25 accounts"); } Ok(()) diff --git a/utilities/tariswap_test_bench/src/main.rs b/utilities/tariswap_test_bench/src/main.rs index 0c4ae0122e..0af580fce2 100644 --- a/utilities/tariswap_test_bench/src/main.rs +++ b/utilities/tariswap_test_bench/src/main.rs @@ -61,8 +61,8 @@ async fn run(cli: cli::CommonArgs, _args: cli::RunArgs) -> anyhow::Result<()> { info!("⏳️ Creating 1000 tariswap components..."); let mut tariswaps = vec![]; - for _ in 0..4 { - tariswaps.extend(runner.create_tariswaps(&primary_account, &faucet, 250).await?); + for _ in 0..40 { + tariswaps.extend(runner.create_tariswaps(&primary_account, &faucet, 25).await?); } info!("✅ Created 1000 tariswaps"); diff --git a/utilities/tariswap_test_bench/src/tariswap.rs b/utilities/tariswap_test_bench/src/tariswap.rs index eb8398146f..4cbbab2129 100644 --- a/utilities/tariswap_test_bench/src/tariswap.rs +++ b/utilities/tariswap_test_bench/src/tariswap.rs @@ -95,14 +95,16 @@ impl Runner { .sdk .key_manager_api() .get_public_key(primary_account.owner_key_id.expect("no owner key id"))?; - let mut tx_ids = Vec::with_capacity(200); let primary_account_pk = primary_account_key.public_key().to_byte_type(); - for i in 0..5 { + const BATCH_SIZE: usize = 100; + let mut tx_ids = Vec::with_capacity(BATCH_SIZE); + + for i in 0..(tariswaps.len() / BATCH_SIZE) { let _timer = TraceTimer::info("tariswap", "add_liquidity") - .with_iterations(200usize.min(tariswaps.len().saturating_sub(i * 200))); + .with_iterations(BATCH_SIZE.min(tariswaps.len().saturating_sub(i * BATCH_SIZE))); - for (i, tariswap) in tariswaps.iter().enumerate().skip(i * 200).take(200) { + for (i, tariswap) in tariswaps.iter().enumerate().skip(i * BATCH_SIZE).take(BATCH_SIZE) { let account = &accounts[i % accounts.len()]; let xtr_vault = self .sdk @@ -128,7 +130,6 @@ impl Runner { SubstateRequirement::unversioned(tariswap.component_address), SubstateRequirement::unversioned(tariswap.lp_resource_address), SubstateRequirement::unversioned(faucet.resource_address), - SubstateRequirement::unversioned(XTR), ]) .with_inputs(tariswap.vaults.values().map(|v| SubstateRequirement::unversioned(*v))) .fee_transaction_pay_from_component(account.component_address, 2000) @@ -171,7 +172,11 @@ impl Runner { )); } - info!("⏳️ Added liquidity to pools {}-{}", i * 200, (i + 1) * 200); + info!( + "⏳️ Added liquidity to pools {}-{}", + i * BATCH_SIZE, + (i + 1) * BATCH_SIZE + ); } info!("⏳️ Waiting for {} transactions to finalize", tariswaps.len());