From b2ad189ca26278a56e8e3d7f4a9dba76d4e83304 Mon Sep 17 00:00:00 2001 From: Andreas Fackler Date: Thu, 23 Jan 2025 11:50:31 +0100 Subject: [PATCH] Add an oracle for the validation round. --- linera-base/src/data_types.rs | 18 +++++- linera-chain/src/chain.rs | 7 ++- linera-chain/src/unit_tests/chain_tests.rs | 17 ++--- linera-client/src/client_context.rs | 5 +- linera-core/src/chain_worker/actor.rs | 11 +++- .../chain_worker/state/attempted_changes.rs | 1 + linera-core/src/chain_worker/state/mod.rs | 5 +- .../chain_worker/state/temporary_changes.rs | 18 ++++-- linera-core/src/client/mod.rs | 63 +++++++++++++++---- linera-core/src/local_node.rs | 5 +- .../src/unit_tests/wasm_worker_tests.rs | 4 +- linera-core/src/unit_tests/worker_tests.rs | 23 +++---- linera-core/src/worker.rs | 7 ++- linera-execution/src/execution.rs | 11 +++- linera-execution/src/lib.rs | 13 +++- linera-execution/src/runtime.rs | 32 +++++++++- linera-execution/src/test_utils/mod.rs | 4 +- .../src/unit_tests/runtime_tests.rs | 3 +- .../src/unit_tests/system_tests.rs | 3 +- linera-execution/src/wasm/system_api.rs | 13 +++- linera-execution/tests/fee_consumption.rs | 3 +- .../tests/test_system_execution.rs | 4 +- linera-execution/tests/wasm.rs | 3 +- .../tests/snapshots/format__format.yaml.snap | 4 ++ .../src/contract/conversions_from_wit.rs | 13 +++- linera-sdk/src/contract/conversions_to_wit.rs | 15 ++++- linera-sdk/src/contract/runtime.rs | 8 ++- linera-sdk/src/contract/test_runtime.rs | 22 ++++++- linera-sdk/src/test/block.rs | 2 +- linera-sdk/wit/contract-system-api.wit | 8 +++ linera-service/src/linera/main.rs | 2 +- 31 files changed, 280 insertions(+), 67 deletions(-) diff --git a/linera-base/src/data_types.rs b/linera-base/src/data_types.rs index 86c0a383bfe..61d1008452f 100644 --- a/linera-base/src/data_types.rs +++ b/linera-base/src/data_types.rs @@ -105,7 +105,20 @@ pub struct BlockHeight(pub u64); /// An identifier for successive attempts to decide a value in a consensus protocol. #[derive( - Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Default, Debug, Serialize, Deserialize, + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Hash, + Default, + Debug, + Serialize, + Deserialize, + WitType, + WitLoad, + WitStore, )] pub enum Round { /// The initial fast round. @@ -763,6 +776,8 @@ pub enum OracleResponse { Blob(BlobId), /// An assertion oracle that passed. Assert, + /// The block's validation round. + Round(Round), } impl Display for OracleResponse { @@ -774,6 +789,7 @@ impl Display for OracleResponse { OracleResponse::Post(bytes) => write!(f, "Post:{}", STANDARD_NO_PAD.encode(bytes))?, OracleResponse::Blob(blob_id) => write!(f, "Blob:{}", blob_id)?, OracleResponse::Assert => write!(f, "Assert")?, + OracleResponse::Round(round) => write!(f, "Round:{round}")?, }; Ok(()) diff --git a/linera-chain/src/chain.rs b/linera-chain/src/chain.rs index 246f1baac72..1d88e22f4bf 100644 --- a/linera-chain/src/chain.rs +++ b/linera-chain/src/chain.rs @@ -13,7 +13,7 @@ use futures::stream::{self, StreamExt, TryStreamExt}; use linera_base::{ crypto::CryptoHash, data_types::{ - Amount, ArithmeticError, Blob, BlockHeight, OracleResponse, Timestamp, + Amount, ArithmeticError, Blob, BlockHeight, OracleResponse, Round, Timestamp, UserApplicationDescription, }, ensure, @@ -666,6 +666,7 @@ where &mut self, block: &ProposedBlock, local_time: Timestamp, + round: Option, replaying_oracle_responses: Option>>, ) -> Result { #[cfg(with_metrics)] @@ -785,6 +786,7 @@ where posted_message, incoming_bundle, block, + round, txn_index, local_time, &mut txn_tracker, @@ -803,6 +805,7 @@ where chain_id, height: block.height, index: Some(txn_index), + round, authenticated_signer: block.authenticated_signer, authenticated_caller_id: None, }; @@ -935,6 +938,7 @@ where posted_message: &PostedMessage, incoming_bundle: &IncomingBundle, block: &ProposedBlock, + round: Option, txn_index: u32, local_time: Timestamp, txn_tracker: &mut TransactionTracker, @@ -946,6 +950,7 @@ where chain_id: block.chain_id, is_bouncing: posted_message.is_bouncing(), height: block.height, + round, certificate_hash: incoming_bundle.bundle.certificate_hash, message_id, authenticated_signer: posted_message.authenticated_signer, diff --git a/linera-chain/src/unit_tests/chain_tests.rs b/linera-chain/src/unit_tests/chain_tests.rs index f7122f25918..8e788d6a203 100644 --- a/linera-chain/src/unit_tests/chain_tests.rs +++ b/linera-chain/src/unit_tests/chain_tests.rs @@ -160,7 +160,7 @@ async fn test_block_size_limit() { recipient: Recipient::root(0), amount: Amount::ONE, }); - let result = chain.execute_block(&invalid_block, time, None).await; + let result = chain.execute_block(&invalid_block, time, None, None).await; assert_matches!( result, Err(ChainError::ExecutionError( @@ -170,7 +170,10 @@ async fn test_block_size_limit() { ); // The valid block is accepted... - let outcome = chain.execute_block(&valid_block, time, None).await.unwrap(); + let outcome = chain + .execute_block(&valid_block, time, None, None) + .await + .unwrap(); let block = Block::new(valid_block, outcome); // ...because its size is exactly at the allowed limit. @@ -231,7 +234,7 @@ async fn test_application_permissions() -> anyhow::Result<()> { let invalid_block = make_first_block(chain_id) .with_incoming_bundle(bundle.clone()) .with_simple_transfer(chain_id, Amount::ONE); - let result = chain.execute_block(&invalid_block, time, None).await; + let result = chain.execute_block(&invalid_block, time, None, None).await; assert_matches!(result, Err(ChainError::AuthorizedApplications(app_ids)) if app_ids == vec![application_id] ); @@ -247,7 +250,7 @@ async fn test_application_permissions() -> anyhow::Result<()> { .with_incoming_bundle(bundle) .with_operation(app_operation.clone()); let executed_block = chain - .execute_block(&valid_block, time, None) + .execute_block(&valid_block, time, None, None) .await? .with(valid_block); let value = Hashed::new(ConfirmedBlock::new(executed_block)); @@ -256,14 +259,14 @@ async fn test_application_permissions() -> anyhow::Result<()> { let invalid_block = make_child_block(&value.clone()) .with_simple_transfer(chain_id, Amount::ONE) .with_operation(app_operation.clone()); - let result = chain.execute_block(&invalid_block, time, None).await; + let result = chain.execute_block(&invalid_block, time, None, None).await; assert_matches!(result, Err(ChainError::AuthorizedApplications(app_ids)) if app_ids == vec![application_id] ); // Also, blocks without an application operation or incoming message are forbidden. let invalid_block = make_child_block(&value.clone()); - let result = chain.execute_block(&invalid_block, time, None).await; + let result = chain.execute_block(&invalid_block, time, None, None).await; assert_matches!(result, Err(ChainError::MissingMandatoryApplications(app_ids)) if app_ids == vec![application_id] ); @@ -272,7 +275,7 @@ async fn test_application_permissions() -> anyhow::Result<()> { application.expect_call(ExpectedCall::execute_operation(|_, _, _| Ok(vec![]))); application.expect_call(ExpectedCall::default_finalize()); let valid_block = make_child_block(&value).with_operation(app_operation); - chain.execute_block(&valid_block, time, None).await?; + chain.execute_block(&valid_block, time, None, None).await?; Ok(()) } diff --git a/linera-client/src/client_context.rs b/linera-client/src/client_context.rs index 24016ff70f5..a76e61cb181 100644 --- a/linera-client/src/client_context.rs +++ b/linera-client/src/client_context.rs @@ -33,7 +33,7 @@ use { futures::{stream, StreamExt as _, TryStreamExt as _}, linera_base::{ crypto::PublicKey, - data_types::Amount, + data_types::{Amount, Round}, identifiers::{AccountOwner, ApplicationId, Owner}, }, linera_chain::data_types::{ @@ -972,11 +972,12 @@ where pub async fn stage_block_execution( &self, block: ProposedBlock, + round: Option, ) -> Result { Ok(self .client .local_node() - .stage_block_execution(block) + .stage_block_execution(block, round) .await? .0) } diff --git a/linera-core/src/chain_worker/actor.rs b/linera-core/src/chain_worker/actor.rs index 314f97d7295..e4d8cd720c7 100644 --- a/linera-core/src/chain_worker/actor.rs +++ b/linera-core/src/chain_worker/actor.rs @@ -12,7 +12,7 @@ use std::{ use custom_debug_derive::Debug; use linera_base::{ crypto::CryptoHash, - data_types::{Blob, BlockHeight, Timestamp, UserApplicationDescription}, + data_types::{Blob, BlockHeight, Round, Timestamp, UserApplicationDescription}, hashed::Hashed, identifiers::{BlobId, ChainId, UserApplicationId}, }; @@ -85,6 +85,7 @@ where /// Execute a block but discard any changes to the chain state. StageBlockExecution { block: ProposedBlock, + round: Option, #[debug(skip)] callback: oneshot::Sender>, }, @@ -286,8 +287,12 @@ where } => callback .send(self.worker.describe_application(application_id).await) .is_ok(), - ChainWorkerRequest::StageBlockExecution { block, callback } => callback - .send(self.worker.stage_block_execution(block).await) + ChainWorkerRequest::StageBlockExecution { + block, + round, + callback, + } => callback + .send(self.worker.stage_block_execution(block, round).await) .is_ok(), ChainWorkerRequest::ProcessTimeout { certificate, diff --git a/linera-core/src/chain_worker/state/attempted_changes.rs b/linera-core/src/chain_worker/state/attempted_changes.rs index 8e4d359e02c..e9e2eca18f6 100644 --- a/linera-core/src/chain_worker/state/attempted_changes.rs +++ b/linera-core/src/chain_worker/state/attempted_changes.rs @@ -359,6 +359,7 @@ where let verified_outcome = Box::pin(self.state.chain.execute_block( &executed_block.block, local_time, + None, Some(executed_block.outcome.oracle_responses.clone()), )) .await?; diff --git a/linera-core/src/chain_worker/state/mod.rs b/linera-core/src/chain_worker/state/mod.rs index 7888accdf5a..74aefa5a008 100644 --- a/linera-core/src/chain_worker/state/mod.rs +++ b/linera-core/src/chain_worker/state/mod.rs @@ -13,7 +13,7 @@ use std::{ use linera_base::{ crypto::CryptoHash, - data_types::{Blob, BlockHeight, UserApplicationDescription}, + data_types::{Blob, BlockHeight, Round, UserApplicationDescription}, ensure, hashed::Hashed, identifiers::{BlobId, ChainId, UserApplicationId}, @@ -179,10 +179,11 @@ where pub(super) async fn stage_block_execution( &mut self, block: ProposedBlock, + round: Option, ) -> Result<(ExecutedBlock, ChainInfoResponse), WorkerError> { ChainWorkerStateWithTemporaryChanges::new(self) .await - .stage_block_execution(block) + .stage_block_execution(block, round) .await } diff --git a/linera-core/src/chain_worker/state/temporary_changes.rs b/linera-core/src/chain_worker/state/temporary_changes.rs index a8307a3f809..ca4ef796d93 100644 --- a/linera-core/src/chain_worker/state/temporary_changes.rs +++ b/linera-core/src/chain_worker/state/temporary_changes.rs @@ -5,7 +5,7 @@ use linera_base::{ data_types::{ - ArithmeticError, Blob, BlobContent, CompressedBytecode, Timestamp, + ArithmeticError, Blob, BlobContent, CompressedBytecode, Round, Timestamp, UserApplicationDescription, }, ensure, @@ -126,14 +126,15 @@ where /// Executes a block without persisting any changes to the state. pub(super) async fn stage_block_execution( &mut self, - proposal: ProposedBlock, + block: ProposedBlock, + round: Option, ) -> Result<(ExecutedBlock, ChainInfoResponse), WorkerError> { let local_time = self.0.storage.clock().current_time(); - let signer = proposal.authenticated_signer; + let signer = block.authenticated_signer; - let executed_block = Box::pin(self.0.chain.execute_block(&proposal, local_time, None)) + let executed_block = Box::pin(self.0.chain.execute_block(&block, local_time, round, None)) .await? - .with(proposal); + .with(block); let mut response = ChainInfoResponse::new(&self.0.chain, None); if let Some(signer) = signer { @@ -238,7 +239,12 @@ where let outcome = if let Some(outcome) = outcome { outcome.clone() } else { - Box::pin(self.0.chain.execute_block(block, local_time, None)).await? + Box::pin( + self.0 + .chain + .execute_block(block, local_time, Some(*round), None), + ) + .await? }; let executed_block = outcome.with(block.clone()); diff --git a/linera-core/src/client/mod.rs b/linera-core/src/client/mod.rs index 1aa3b7e7d51..0a50ab0a27d 100644 --- a/linera-core/src/client/mod.rs +++ b/linera-core/src/client/mod.rs @@ -1910,9 +1910,10 @@ where async fn stage_block_execution_and_discard_failing_messages( &self, mut block: ProposedBlock, + round: Option, ) -> Result<(ExecutedBlock, ChainInfoResponse), ChainClientError> { loop { - let result = self.stage_block_execution(block.clone()).await; + let result = self.stage_block_execution(block.clone(), round).await; if let Err(ChainClientError::LocalNodeError(LocalNodeError::WorkerError( WorkerError::ChainError(chain_error), ))) = &result @@ -1951,12 +1952,13 @@ where async fn stage_block_execution( &self, block: ProposedBlock, + round: Option, ) -> Result<(ExecutedBlock, ChainInfoResponse), ChainClientError> { loop { let result = self .client .local_node - .stage_block_execution(block.clone()) + .stage_block_execution(block.clone(), round) .await; if let Err(LocalNodeError::BlobsNotFound(blob_ids)) = &result { self.receive_certificates_for_blobs(blob_ids.clone()) @@ -2129,8 +2131,18 @@ where }; // Make sure every incoming message succeeds and otherwise remove them. // Also, compute the final certified hash while we're at it. + + let info = self.chain_info().await?; + // Use the round number assuming there are oracle responses. + // Using the round number during execution counts as an oracle. + // Accessing the round number in single-leader rounds where we are not the leader + // is not currently supported. + let round = match Self::round_for_new_proposal(&info, &identity, &block, true)? { + Either::Left(round) => Some(round), + Either::Right(_) => None, + }; let (executed_block, _) = self - .stage_block_execution_and_discard_failing_messages(block) + .stage_block_execution_and_discard_failing_messages(block, round) .await?; let blobs = self .read_local_blobs(executed_block.required_blob_ids()) @@ -2277,7 +2289,7 @@ where timestamp, }; match self - .stage_block_execution_and_discard_failing_messages(block) + .stage_block_execution_and_discard_failing_messages(block, None) .await { Ok((_, response)) => Ok(( @@ -2430,6 +2442,7 @@ where { return self.finalize_locked_block(info).await; } + let identity = self.identity().await?; // Otherwise we have to re-propose the highest validated block, if there is one. let pending: Option = self.state().pending_proposal().clone(); @@ -2438,18 +2451,45 @@ where LockedBlock::Regular(certificate) => certificate.block().clone().into(), LockedBlock::Fast(proposal) => { let block = proposal.content.block.clone(); - self.stage_block_execution(block).await?.0 + self.stage_block_execution(block, None).await?.0 } } } else if let Some(block) = pending { // Otherwise we are free to propose our own pending block. - self.stage_block_execution(block).await?.0 + // Use the round number assuming there are oracle responses. + // Using the round number during execution counts as an oracle. + let (round, timeout) = + match Self::round_for_new_proposal(&info, &identity, &block, true)? { + Either::Left(round) => (Some(round), None), + Either::Right(timeout) => (None, Some(timeout)), + }; + match self.stage_block_execution(block, round).await { + Ok((executed_block, _)) => executed_block, + Err(ChainClientError::LocalNodeError(LocalNodeError::WorkerError( + WorkerError::ChainError(error), + ))) if matches!( + &*error, ChainError::ExecutionError(error, _) + if matches!(&**error, ExecutionError::MissingRound) + ) => + { + // During execution the round number was accessed, so the timeout applies. + let timeout = timeout.ok_or_else(|| { + ChainClientError::InternalError("Should have either timeout or round") + })?; + return Ok(ClientOutcome::WaitForTimeout(timeout)); + } + Err(error) => return Err(error), + } } else { return Ok(ClientOutcome::Committed(None)); // Nothing to do. }; - let identity = self.identity().await?; - let round = match Self::round_for_new_proposal(&info, &identity, &executed_block)? { + let round = match Self::round_for_new_proposal( + &info, + &identity, + &executed_block.block, + executed_block.outcome.has_oracle_responses(), + )? { Either::Left(round) => round, Either::Right(timeout) => return Ok(ClientOutcome::WaitForTimeout(timeout)), }; @@ -2552,17 +2592,16 @@ where fn round_for_new_proposal( info: &ChainInfo, identity: &Owner, - executed_block: &ExecutedBlock, + block: &ProposedBlock, + has_oracle_responses: bool, ) -> Result, ChainClientError> { let manager = &info.manager; - let block = &executed_block.block; // If there is a conflicting proposal in the current round, we can only propose if the // next round can be started without a timeout, i.e. if we are in a multi-leader round. // Similarly, we cannot propose a block that uses oracles in the fast round. let conflict = manager.requested_proposed.as_ref().is_some_and(|proposal| { proposal.content.round == manager.current_round && proposal.content.block != *block - }) || (manager.current_round.is_fast() - && executed_block.outcome.has_oracle_responses()); + }) || (manager.current_round.is_fast() && has_oracle_responses); let round = if !conflict { manager.current_round } else if let Some(round) = manager diff --git a/linera-core/src/local_node.rs b/linera-core/src/local_node.rs index 36128d019e0..348f7ee2166 100644 --- a/linera-core/src/local_node.rs +++ b/linera-core/src/local_node.rs @@ -9,7 +9,7 @@ use std::{ use futures::{future::Either, stream, StreamExt as _, TryStreamExt as _}; use linera_base::{ - data_types::{ArithmeticError, Blob, BlockHeight, UserApplicationDescription}, + data_types::{ArithmeticError, Blob, BlockHeight, Round, UserApplicationDescription}, identifiers::{BlobId, ChainId, MessageId, UserApplicationId}, }; use linera_chain::{ @@ -174,8 +174,9 @@ where pub async fn stage_block_execution( &self, block: ProposedBlock, + round: Option, ) -> Result<(ExecutedBlock, ChainInfoResponse), LocalNodeError> { - Ok(self.node.state.stage_block_execution(block).await?) + Ok(self.node.state.stage_block_execution(block, round).await?) } /// Reads blobs from storage. diff --git a/linera-core/src/unit_tests/wasm_worker_tests.rs b/linera-core/src/unit_tests/wasm_worker_tests.rs index ee569fc2395..dc877c87186 100644 --- a/linera-core/src/unit_tests/wasm_worker_tests.rs +++ b/linera-core/src/unit_tests/wasm_worker_tests.rs @@ -16,7 +16,8 @@ use assert_matches::assert_matches; use linera_base::{ crypto::KeyPair, data_types::{ - Amount, Blob, BlockHeight, Bytecode, OracleResponse, Timestamp, UserApplicationDescription, + Amount, Blob, BlockHeight, Bytecode, OracleResponse, Round, Timestamp, + UserApplicationDescription, }, hashed::Hashed, identifiers::{ @@ -273,6 +274,7 @@ where authenticated_signer: None, authenticated_caller_id: None, height: run_block.height, + round: Some(Round::MultiLeader(0)), index: Some(0), }; let mut controller = ResourceController::default(); diff --git a/linera-core/src/unit_tests/worker_tests.rs b/linera-core/src/unit_tests/worker_tests.rs index 2a7fc3c24c9..621428ce609 100644 --- a/linera-core/src/unit_tests/worker_tests.rs +++ b/linera-core/src/unit_tests/worker_tests.rs @@ -3234,7 +3234,7 @@ where timeout_config: TimeoutConfig::default(), }) .with_authenticated_signer(Some(owner0)); - let (executed_block0, _) = worker.stage_block_execution(block0).await?; + let (executed_block0, _) = worker.stage_block_execution(block0, None).await?; let value0 = Hashed::new(ConfirmedBlock::new(executed_block0)); let certificate0 = make_certificate(&committee, &worker, value0.clone()); let response = worker @@ -3283,7 +3283,7 @@ where // Now owner 0 can propose a block, but owner 1 can't. let block1 = make_child_block(&value0.clone()); - let (executed_block1, _) = worker.stage_block_execution(block1.clone()).await?; + let (executed_block1, _) = worker.stage_block_execution(block1.clone(), None).await?; let proposal1_wrong_owner = block1 .clone() .with_authenticated_signer(Some(owner1)) @@ -3322,7 +3322,7 @@ where // Create block2, also at height 1, but different from block 1. let amount = Amount::from_tokens(1); let block2 = make_child_block(&value0.clone()).with_simple_transfer(ChainId::root(1), amount); - let (executed_block2, _) = worker.stage_block_execution(block2.clone()).await?; + let (executed_block2, _) = worker.stage_block_execution(block2.clone(), None).await?; // Since round 3 is already over, a validated block from round 3 won't update the validator's // locked block; certificate1 (with block1) remains locked. @@ -3440,7 +3440,7 @@ where ..TimeoutConfig::default() }, }); - let (executed_block0, _) = worker.stage_block_execution(block0).await?; + let (executed_block0, _) = worker.stage_block_execution(block0, None).await?; let value0 = Hashed::new(ConfirmedBlock::new(executed_block0)); let certificate0 = make_certificate(&committee, &worker, value0.clone()); let response = worker @@ -3529,8 +3529,9 @@ where ..TimeoutConfig::default() }, }); - let (change_ownership_executed_block, _) = - worker.stage_block_execution(change_ownership_block).await?; + let (change_ownership_executed_block, _) = worker + .stage_block_execution(change_ownership_block, None) + .await?; let change_ownership_value = Hashed::new(ConfirmedBlock::new(change_ownership_executed_block)); let change_ownership_certificate = make_certificate(&committee, &worker, change_ownership_value.clone()); @@ -3553,7 +3554,7 @@ where let proposal = make_child_block(&change_ownership_value) .into_proposal_with_round(&KeyPair::generate(), Round::MultiLeader(0)); let (executed_block, _) = worker - .stage_block_execution(proposal.content.block.clone()) + .stage_block_execution(proposal.content.block.clone(), None) .await?; let value = Hashed::new(ConfirmedBlock::new(executed_block)); let (response, _) = worker.handle_block_proposal(proposal).await?; @@ -3592,7 +3593,7 @@ where ..TimeoutConfig::default() }, }); - let (executed_block0, _) = worker.stage_block_execution(block0).await?; + let (executed_block0, _) = worker.stage_block_execution(block0, None).await?; let value0 = Hashed::new(ConfirmedBlock::new(executed_block0)); let certificate0 = make_certificate(&committee, &worker, value0.clone()); let response = worker @@ -3608,7 +3609,7 @@ where let proposal1 = block1 .clone() .into_proposal_with_round(&key_pairs[0], Round::Fast); - let (executed_block1, _) = worker.stage_block_execution(block1.clone()).await?; + let (executed_block1, _) = worker.stage_block_execution(block1.clone(), None).await?; let value1 = Hashed::new(ConfirmedBlock::new(executed_block1)); let (response, _) = worker.handle_block_proposal(proposal1).await?; let vote = response.info.manager.pending.as_ref().unwrap(); @@ -3654,7 +3655,7 @@ where worker.handle_block_proposal(proposal3).await?; // A validated block certificate from a later round can override the locked fast block. - let (executed_block2, _) = worker.stage_block_execution(block2.clone()).await?; + let (executed_block2, _) = worker.stage_block_execution(block2.clone(), None).await?; let value2 = Hashed::new(ValidatedBlock::new(executed_block2.clone())); let certificate2 = make_certificate_with_round(&committee, &worker, value2.clone(), Round::MultiLeader(0)); @@ -3713,7 +3714,7 @@ where let block = make_first_block(chain_id) .with_simple_transfer(chain_id, Amount::ONE) .with_authenticated_signer(Some(key_pair.public().into())); - let (executed_block, _) = worker.stage_block_execution(block).await?; + let (executed_block, _) = worker.stage_block_execution(block, None).await?; let value = Hashed::new(ConfirmedBlock::new(executed_block)); let certificate = make_certificate(&committee, &worker, value); worker diff --git a/linera-core/src/worker.rs b/linera-core/src/worker.rs index d0dc07243f3..a3ec7b3c3d6 100644 --- a/linera-core/src/worker.rs +++ b/linera-core/src/worker.rs @@ -505,9 +505,14 @@ where pub async fn stage_block_execution( &self, block: ProposedBlock, + round: Option, ) -> Result<(ExecutedBlock, ChainInfoResponse), WorkerError> { self.query_chain_worker(block.chain_id, move |callback| { - ChainWorkerRequest::StageBlockExecution { block, callback } + ChainWorkerRequest::StageBlockExecution { + block, + round, + callback, + } }) .await } diff --git a/linera-execution/src/execution.rs b/linera-execution/src/execution.rs index 4a2f2b99499..210bb8fc8bc 100644 --- a/linera-execution/src/execution.rs +++ b/linera-execution/src/execution.rs @@ -8,7 +8,7 @@ use std::{ use futures::{stream::FuturesOrdered, FutureExt, StreamExt, TryStreamExt}; use linera_base::{ - data_types::{Amount, BlockHeight, Timestamp}, + data_types::{Amount, BlockHeight, Round, Timestamp}, identifiers::{Account, AccountOwner, ChainId, Destination, Owner}, }; use linera_views::{ @@ -75,6 +75,7 @@ where authenticated_signer: None, authenticated_caller_id: None, height: application_description.creation.height, + round: Some(Round::Fast), index: Some(0), }; @@ -148,6 +149,14 @@ impl UserAction { UserAction::Message(context, _) => context.height, } } + + pub(crate) fn round(&self) -> Option { + match self { + UserAction::Instantiate(context, _) => context.round, + UserAction::Operation(context, _) => context.round, + UserAction::Message(context, _) => context.round, + } + } } impl ExecutionStateView diff --git a/linera-execution/src/lib.rs b/linera-execution/src/lib.rs index 1b933e31755..c3237abf073 100644 --- a/linera-execution/src/lib.rs +++ b/linera-execution/src/lib.rs @@ -37,7 +37,7 @@ use linera_base::{ crypto::{BcsHashable, CryptoHash}, data_types::{ Amount, ApplicationPermissions, ArithmeticError, Blob, BlockHeight, DecompressionError, - Resources, SendMessageRequest, Timestamp, UserApplicationDescription, + Resources, Round, SendMessageRequest, Timestamp, UserApplicationDescription, }, doc_scalar, hex_debug, identifiers::{ @@ -271,6 +271,8 @@ pub enum ExecutionError { ServiceModuleSend(#[from] linera_base::task::SendError), #[error("Blobs not found: {0:?}")] BlobsNotFound(Vec), + #[error("Missing round; rounds are only available in blocks, not service queries")] + MissingRound, } impl From for ExecutionError { @@ -398,6 +400,8 @@ pub struct OperationContext { pub authenticated_caller_id: Option, /// The current block height. pub height: BlockHeight, + /// The consensus round. + pub round: Option, /// The current index of the operation. #[debug(skip_if = Option::is_none)] pub index: Option, @@ -417,6 +421,8 @@ pub struct MessageContext { pub refund_grant_to: Option, /// The current block height. pub height: BlockHeight, + /// The consensus round. + pub round: Option, /// The hash of the remote certificate that created the message. pub certificate_hash: CryptoHash, /// The ID of the message (based on the operation height and index in the remote @@ -433,6 +439,8 @@ pub struct FinalizeContext { pub authenticated_signer: Option, /// The current block height. pub height: BlockHeight, + /// The consensus round. + pub round: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -722,6 +730,9 @@ pub trait ContractRuntime: BaseRuntime { /// Writes a batch of changes. fn write_batch(&mut self, batch: Batch) -> Result<(), ExecutionError>; + + /// Returns the round in which this block was validated. + fn validation_round(&mut self) -> Result; } /// An operation to be executed in a block. diff --git a/linera-execution/src/runtime.rs b/linera-execution/src/runtime.rs index 8cb2e1f5571..c707f31b3c2 100644 --- a/linera-execution/src/runtime.rs +++ b/linera-execution/src/runtime.rs @@ -13,7 +13,7 @@ use linera_base::{ crypto::CryptoHash, data_types::{ Amount, ApplicationPermissions, ArithmeticError, BlockHeight, OracleResponse, Resources, - SendMessageRequest, Timestamp, + Round, SendMessageRequest, Timestamp, }, ensure, identifiers::{ @@ -65,6 +65,8 @@ pub struct SyncRuntimeInternal { /// The height of the next block that will be added to this chain. During operations /// and messages, this is the current block height. height: BlockHeight, + /// The current consensus round. Only available during block validation. + round: Option, /// The current local time. local_time: Timestamp, /// The authenticated signer of the operation or message, if any. @@ -283,6 +285,7 @@ impl SyncRuntimeInternal { fn new( chain_id: ChainId, height: BlockHeight, + round: Option, local_time: Timestamp, authenticated_signer: Option, executing_message: Option, @@ -294,6 +297,7 @@ impl SyncRuntimeInternal { Self { chain_id, height, + round, local_time, authenticated_signer, executing_message, @@ -443,6 +447,7 @@ impl SyncRuntimeInternal { authenticated_signer, authenticated_caller_id, height: self.height, + round: self.round, index: None, }; self.push_application(ApplicationStatus { @@ -1070,6 +1075,7 @@ impl ContractSyncRuntime { SyncRuntimeInternal::new( chain_id, action.height(), + action.round(), local_time, action.signer(), if let UserAction::Message(context, _) = action { @@ -1136,6 +1142,7 @@ impl ContractSyncRuntimeHandle { authenticated_signer: action.signer(), chain_id, height: action.height(), + round: action.round(), }; { @@ -1422,12 +1429,16 @@ impl ContractRuntime for ContractSyncRuntimeHandle { argument: Vec, required_application_ids: Vec, ) -> Result { - let chain_id = self.inner().chain_id; + let (chain_id, round) = { + let this = self.inner(); + (this.chain_id, this.round) + }; let context = OperationContext { chain_id, authenticated_signer: self.authenticated_signer()?, authenticated_caller_id: self.authenticated_caller_id()?, height: self.block_height()?, + round, index: None, }; @@ -1488,6 +1499,22 @@ impl ContractRuntime for ContractSyncRuntimeHandle { .recv_response()?; Ok(()) } + + fn validation_round(&mut self) -> Result { + let mut this = self.inner(); + let round = + if let Some(response) = this.transaction_tracker.next_replayed_oracle_response()? { + match response { + OracleResponse::Round(round) => round, + _ => return Err(ExecutionError::OracleResponseMismatch), + } + } else { + this.round.ok_or_else(|| ExecutionError::MissingRound)? + }; + this.transaction_tracker + .add_oracle_response(OracleResponse::Round(round)); + Ok(round) + } } impl ServiceSyncRuntime { @@ -1497,6 +1524,7 @@ impl ServiceSyncRuntime { SyncRuntimeInternal::new( context.chain_id, context.next_block_height, + None, context.local_time, None, None, diff --git a/linera-execution/src/test_utils/mod.rs b/linera-execution/src/test_utils/mod.rs index fd85dd273f5..a9f544581fa 100644 --- a/linera-execution/src/test_utils/mod.rs +++ b/linera-execution/src/test_utils/mod.rs @@ -12,7 +12,7 @@ use std::{collections::BTreeMap, sync::Arc, thread, vec}; use linera_base::{ crypto::{BcsSignable, CryptoHash}, - data_types::{Amount, Blob, BlockHeight, CompressedBytecode, OracleResponse, Timestamp}, + data_types::{Amount, Blob, BlockHeight, CompressedBytecode, OracleResponse, Round, Timestamp}, identifiers::{ AccountOwner, ApplicationId, BlobId, BlobType, BytecodeId, ChainId, MessageId, Owner, }, @@ -68,6 +68,7 @@ pub fn create_dummy_operation_context() -> OperationContext { OperationContext { chain_id: ChainId::root(0), height: BlockHeight(0), + round: Some(Round::MultiLeader(0)), index: Some(0), authenticated_signer: None, authenticated_caller_id: None, @@ -82,6 +83,7 @@ pub fn create_dummy_message_context(authenticated_signer: Option) -> Mess authenticated_signer, refund_grant_to: None, height: BlockHeight(0), + round: Some(Round::MultiLeader(0)), certificate_hash: CryptoHash::test_hash("block receiving a message"), message_id: MessageId { chain_id: ChainId::root(0), diff --git a/linera-execution/src/unit_tests/runtime_tests.rs b/linera-execution/src/unit_tests/runtime_tests.rs index 3cc24dafb5f..87bf441e1cb 100644 --- a/linera-execution/src/unit_tests/runtime_tests.rs +++ b/linera-execution/src/unit_tests/runtime_tests.rs @@ -13,7 +13,7 @@ use std::{ use futures::{channel::mpsc, StreamExt}; use linera_base::{ crypto::CryptoHash, - data_types::{BlockHeight, Timestamp}, + data_types::{BlockHeight, Round, Timestamp}, identifiers::{ApplicationId, BytecodeId, ChainDescription, MessageId}, }; use linera_views::batch::Batch; @@ -178,6 +178,7 @@ fn create_runtime() -> ( let runtime = SyncRuntimeInternal::new( chain_id, BlockHeight(0), + Some(Round::MultiLeader(0)), Timestamp::from(0), None, None, diff --git a/linera-execution/src/unit_tests/system_tests.rs b/linera-execution/src/unit_tests/system_tests.rs index df61286ff3e..649c73dc5bd 100644 --- a/linera-execution/src/unit_tests/system_tests.rs +++ b/linera-execution/src/unit_tests/system_tests.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use linera_base::{ - data_types::{Blob, BlockHeight, Bytecode}, + data_types::{Blob, BlockHeight, Bytecode, Round}, identifiers::ApplicationId, }; use linera_views::context::MemoryContext; @@ -22,6 +22,7 @@ async fn new_view_and_context() -> ( authenticated_signer: None, authenticated_caller_id: None, height: BlockHeight::from(7), + round: Some(Round::MultiLeader(0)), index: Some(2), }; let state = SystemExecutionState { diff --git a/linera-execution/src/wasm/system_api.rs b/linera-execution/src/wasm/system_api.rs index c8abea27c0b..cc2c394723b 100644 --- a/linera-execution/src/wasm/system_api.rs +++ b/linera-execution/src/wasm/system_api.rs @@ -5,7 +5,9 @@ use std::{any::Any, collections::HashMap, marker::PhantomData}; use linera_base::{ crypto::CryptoHash, - data_types::{Amount, ApplicationPermissions, BlockHeight, SendMessageRequest, Timestamp}, + data_types::{ + Amount, ApplicationPermissions, BlockHeight, Round, SendMessageRequest, Timestamp, + }, identifiers::{ Account, AccountOwner, ApplicationId, ChainId, ChannelName, MessageId, Owner, StreamName, }, @@ -424,6 +426,15 @@ where .consume_fuel(fuel) .map_err(|e| RuntimeError::Custom(e.into())) } + + /// Returns the round in which this block was validated. + fn validation_round(caller: &mut Caller) -> Result { + caller + .user_data_mut() + .runtime_mut() + .validation_round() + .map_err(|error| RuntimeError::Custom(error.into())) + } } /// An implementation of the system API made available to services. diff --git a/linera-execution/tests/fee_consumption.rs b/linera-execution/tests/fee_consumption.rs index f064fb57994..f1ac495cc26 100644 --- a/linera-execution/tests/fee_consumption.rs +++ b/linera-execution/tests/fee_consumption.rs @@ -9,7 +9,7 @@ use std::{sync::Arc, vec}; use linera_base::{ crypto::{CryptoHash, PublicKey}, - data_types::{Amount, BlockHeight, Timestamp}, + data_types::{Amount, BlockHeight, Round, Timestamp}, identifiers::{Account, AccountOwner, ChainDescription, ChainId, MessageId, Owner}, }; use linera_execution::{ @@ -190,6 +190,7 @@ async fn test_fee_consumption( authenticated_signer, refund_grant_to, height: BlockHeight(0), + round: Some(Round::MultiLeader(0)), certificate_hash: CryptoHash::default(), message_id: MessageId::default(), }; diff --git a/linera-execution/tests/test_system_execution.rs b/linera-execution/tests/test_system_execution.rs index 2957e5109d0..1ebc5ef6828 100644 --- a/linera-execution/tests/test_system_execution.rs +++ b/linera-execution/tests/test_system_execution.rs @@ -5,7 +5,7 @@ use linera_base::{ crypto::{CryptoHash, KeyPair}, - data_types::{Amount, BlockHeight, Timestamp}, + data_types::{Amount, BlockHeight, Round, Timestamp}, identifiers::{Account, AccountOwner, ChainDescription, ChainId, MessageId, Owner}, ownership::ChainOwnership, }; @@ -37,6 +37,7 @@ async fn test_simple_system_operation() -> anyhow::Result<()> { let context = OperationContext { chain_id: ChainId::root(0), height: BlockHeight(0), + round: Some(Round::MultiLeader(0)), index: Some(0), authenticated_signer: Some(owner), authenticated_caller_id: None, @@ -83,6 +84,7 @@ async fn test_simple_system_message() -> anyhow::Result<()> { chain_id: ChainId::root(0), is_bouncing: false, height: BlockHeight(0), + round: Some(Round::MultiLeader(0)), certificate_hash: CryptoHash::test_hash("certificate"), message_id: MessageId { chain_id: ChainId::root(1), diff --git a/linera-execution/tests/wasm.rs b/linera-execution/tests/wasm.rs index 4b860062940..b90176f0368 100644 --- a/linera-execution/tests/wasm.rs +++ b/linera-execution/tests/wasm.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use linera_base::{ - data_types::{Amount, BlockHeight, Timestamp}, + data_types::{Amount, BlockHeight, Round, Timestamp}, identifiers::{Account, ChainDescription, ChainId}, }; use linera_execution::{ @@ -69,6 +69,7 @@ async fn test_fuel_for_counter_wasm_application( let context = OperationContext { chain_id: ChainId::root(0), height: BlockHeight(0), + round: Some(Round::MultiLeader(0)), index: Some(0), authenticated_signer: None, authenticated_caller_id: None, diff --git a/linera-rpc/tests/snapshots/format__format.yaml.snap b/linera-rpc/tests/snapshots/format__format.yaml.snap index aeac3175c1a..90a6934ec4c 100644 --- a/linera-rpc/tests/snapshots/format__format.yaml.snap +++ b/linera-rpc/tests/snapshots/format__format.yaml.snap @@ -682,6 +682,10 @@ OracleResponse: TYPENAME: BlobId 3: Assert: UNIT + 4: + Round: + NEWTYPE: + TYPENAME: Round Origin: STRUCT: - sender: diff --git a/linera-sdk/src/contract/conversions_from_wit.rs b/linera-sdk/src/contract/conversions_from_wit.rs index eb4a88a0203..923c5817a46 100644 --- a/linera-sdk/src/contract/conversions_from_wit.rs +++ b/linera-sdk/src/contract/conversions_from_wit.rs @@ -5,7 +5,7 @@ use linera_base::{ crypto::CryptoHash, - data_types::{Amount, BlockHeight, TimeDelta, Timestamp}, + data_types::{Amount, BlockHeight, Round, TimeDelta, Timestamp}, identifiers::{ApplicationId, BytecodeId, ChainId, MessageId, Owner}, ownership::{ChainOwnership, CloseChainError, TimeoutConfig}, }; @@ -135,3 +135,14 @@ impl From for CloseChainError { } } } + +impl From for Round { + fn from(round: wit_system_api::Round) -> Self { + match round { + wit_system_api::Round::Fast => Self::Fast, + wit_system_api::Round::MultiLeader(r) => Self::MultiLeader(r), + wit_system_api::Round::SingleLeader(r) => Self::SingleLeader(r), + wit_system_api::Round::Validator(r) => Self::Validator(r), + } + } +} diff --git a/linera-sdk/src/contract/conversions_to_wit.rs b/linera-sdk/src/contract/conversions_to_wit.rs index 1a7d4e43755..d1397ad0cc4 100644 --- a/linera-sdk/src/contract/conversions_to_wit.rs +++ b/linera-sdk/src/contract/conversions_to_wit.rs @@ -6,8 +6,8 @@ use linera_base::{ crypto::CryptoHash, data_types::{ - Amount, ApplicationPermissions, BlockHeight, Resources, SendMessageRequest, TimeDelta, - Timestamp, + Amount, ApplicationPermissions, BlockHeight, Resources, Round, SendMessageRequest, + TimeDelta, Timestamp, }, identifiers::{ Account, AccountOwner, ApplicationId, BytecodeId, ChainId, ChannelName, Destination, @@ -255,3 +255,14 @@ impl From for wit_system_api::ChainOwnership { } } } + +impl From for wit_system_api::Round { + fn from(round: Round) -> Self { + match round { + Round::Fast => Self::Fast, + Round::MultiLeader(r) => Self::MultiLeader(r), + Round::SingleLeader(r) => Self::SingleLeader(r), + Round::Validator(r) => Self::Validator(r), + } + } +} diff --git a/linera-sdk/src/contract/runtime.rs b/linera-sdk/src/contract/runtime.rs index 43668d692c5..6996b91395b 100644 --- a/linera-sdk/src/contract/runtime.rs +++ b/linera-sdk/src/contract/runtime.rs @@ -6,7 +6,8 @@ use linera_base::{ abi::{ContractAbi, ServiceAbi}, data_types::{ - Amount, ApplicationPermissions, BlockHeight, Resources, SendMessageRequest, Timestamp, + Amount, ApplicationPermissions, BlockHeight, Resources, Round, SendMessageRequest, + Timestamp, }, identifiers::{ Account, AccountOwner, ApplicationId, BytecodeId, ChainId, ChannelName, Destination, @@ -319,6 +320,11 @@ where pub fn assert_data_blob_exists(&mut self, hash: DataBlobHash) { wit::assert_data_blob_exists(hash.0.into()) } + + /// Returns the round in which this block was validated. + pub fn validation_round(&mut self) -> Round { + wit::validation_round().into() + } } /// A helper type that uses the builder pattern to configure how a message is sent, and then diff --git a/linera-sdk/src/contract/test_runtime.rs b/linera-sdk/src/contract/test_runtime.rs index 41da736a9d9..2d9644bd19b 100644 --- a/linera-sdk/src/contract/test_runtime.rs +++ b/linera-sdk/src/contract/test_runtime.rs @@ -11,7 +11,8 @@ use std::{ use linera_base::{ abi::{ContractAbi, ServiceAbi}, data_types::{ - Amount, ApplicationPermissions, BlockHeight, Resources, SendMessageRequest, Timestamp, + Amount, ApplicationPermissions, BlockHeight, Resources, Round, SendMessageRequest, + Timestamp, }, identifiers::{ Account, AccountOwner, ApplicationId, BytecodeId, ChainId, ChannelName, Destination, @@ -42,6 +43,7 @@ where chain_id: Option, authenticated_signer: Option>, block_height: Option, + round: Option, message_id: Option>, message_is_bouncing: Option>, authenticated_caller_id: Option>, @@ -89,6 +91,7 @@ where chain_id: None, authenticated_signer: None, block_height: None, + round: None, message_id: None, message_is_bouncing: None, authenticated_caller_id: None, @@ -251,6 +254,18 @@ where self } + /// Configures the round number to return during the test. + pub fn with_round(mut self, round: Round) -> Self { + self.round = Some(round); + self + } + + /// Configures the round number to return during the test. + pub fn set_round(&mut self, round: Round) -> &mut Self { + self.round = Some(round); + self + } + /// Returns the height of the current block that is executing. pub fn block_height(&mut self) -> BlockHeight { self.block_height.expect( @@ -824,6 +839,11 @@ where assert_eq!(hash, expected_blob_hash); response.expect("Blob does not exist!"); } + + /// Returns the round in which this block was validated. + pub fn validation_round(&mut self) -> Round { + self.round.expect("Missing round number") + } } /// A type alias for the handler for cross-application calls. diff --git a/linera-sdk/src/test/block.rs b/linera-sdk/src/test/block.rs index d7cf1b278d9..cfed21cfc20 100644 --- a/linera-sdk/src/test/block.rs +++ b/linera-sdk/src/test/block.rs @@ -215,7 +215,7 @@ impl BlockBuilder { let (executed_block, _) = self .validator .worker() - .stage_block_execution(self.block) + .stage_block_execution(self.block, None) .await?; let value = Hashed::new(ConfirmedBlock::new(executed_block)); diff --git a/linera-sdk/wit/contract-system-api.wit b/linera-sdk/wit/contract-system-api.wit index 86a5bbfcbf9..56474edd881 100644 --- a/linera-sdk/wit/contract-system-api.wit +++ b/linera-sdk/wit/contract-system-api.wit @@ -31,6 +31,7 @@ interface contract-system-api { assert-data-blob-exists: func(hash: crypto-hash); log: func(message: string, level: log-level); consume-fuel: func(fuel: u64); + validation-round: func() -> round; record account { chain-id: chain-id, @@ -127,6 +128,13 @@ interface contract-system-api { storage-size-delta: u32, } + variant round { + fast, + multi-leader(u32), + single-leader(u32), + validator(u32), + } + record send-message-request { destination: destination, authenticated: bool, diff --git a/linera-service/src/linera/main.rs b/linera-service/src/linera/main.rs index 36ca5b5391f..92619f47358 100644 --- a/linera-service/src/linera/main.rs +++ b/linera-service/src/linera/main.rs @@ -775,7 +775,7 @@ impl Runnable for Job { for rpc_msg in &proposals { if let RpcMessage::BlockProposal(proposal) = rpc_msg { let executed_block = context - .stage_block_execution(proposal.content.block.clone()) + .stage_block_execution(proposal.content.block.clone(), None) .await?; let value = Hashed::new(ConfirmedBlock::new(executed_block)); values.insert(value.hash(), value);