Skip to content

Commit e35c25f

Browse files
authored
Validation round number oracle (#3175)
## Motivation For most uses of permissionless rounds (#3162), applications need to have access to the round number, at least in multi-leader rounds. ## Proposal Expose the round number in which the block is being validated to the contract. This is only `Some` in multi-leader rounds. ## Test Plan An example application using this will be added in a separate PR. ## Release Plan - Nothing to do / These changes follow the usual release cycle. ## Links - Closes #3164. - Closes #3162. - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 452e5cd commit e35c25f

File tree

29 files changed

+200
-44
lines changed

29 files changed

+200
-44
lines changed

linera-base/src/data_types.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,14 @@ impl Round {
603603
matches!(self, Round::MultiLeader(_))
604604
}
605605

606+
/// Returns the round number if this is a multi-leader round, `None` otherwise.
607+
pub fn multi_leader(&self) -> Option<u32> {
608+
match self {
609+
Round::MultiLeader(number) => Some(*number),
610+
_ => None,
611+
}
612+
}
613+
606614
/// Whether the round is the fast round.
607615
pub fn is_fast(&self) -> bool {
608616
matches!(self, Round::Fast)
@@ -763,6 +771,8 @@ pub enum OracleResponse {
763771
Blob(BlobId),
764772
/// An assertion oracle that passed.
765773
Assert,
774+
/// The block's validation round.
775+
Round(Option<u32>),
766776
}
767777

768778
impl Display for OracleResponse {
@@ -774,6 +784,8 @@ impl Display for OracleResponse {
774784
OracleResponse::Post(bytes) => write!(f, "Post:{}", STANDARD_NO_PAD.encode(bytes))?,
775785
OracleResponse::Blob(blob_id) => write!(f, "Blob:{}", blob_id)?,
776786
OracleResponse::Assert => write!(f, "Assert")?,
787+
OracleResponse::Round(Some(round)) => write!(f, "Round:{round}")?,
788+
OracleResponse::Round(None) => write!(f, "Round:None")?,
777789
};
778790

779791
Ok(())

linera-chain/src/chain.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@ where
666666
&mut self,
667667
block: &ProposedBlock,
668668
local_time: Timestamp,
669+
round: Option<u32>,
669670
replaying_oracle_responses: Option<Vec<Vec<OracleResponse>>>,
670671
) -> Result<BlockExecutionOutcome, ChainError> {
671672
#[cfg(with_metrics)]
@@ -785,6 +786,7 @@ where
785786
posted_message,
786787
incoming_bundle,
787788
block,
789+
round,
788790
txn_index,
789791
local_time,
790792
&mut txn_tracker,
@@ -803,6 +805,7 @@ where
803805
chain_id,
804806
height: block.height,
805807
index: Some(txn_index),
808+
round,
806809
authenticated_signer: block.authenticated_signer,
807810
authenticated_caller_id: None,
808811
};
@@ -935,6 +938,7 @@ where
935938
posted_message: &PostedMessage,
936939
incoming_bundle: &IncomingBundle,
937940
block: &ProposedBlock,
941+
round: Option<u32>,
938942
txn_index: u32,
939943
local_time: Timestamp,
940944
txn_tracker: &mut TransactionTracker,
@@ -946,6 +950,7 @@ where
946950
chain_id: block.chain_id,
947951
is_bouncing: posted_message.is_bouncing(),
948952
height: block.height,
953+
round,
949954
certificate_hash: incoming_bundle.bundle.certificate_hash,
950955
message_id,
951956
authenticated_signer: posted_message.authenticated_signer,

linera-chain/src/unit_tests/chain_tests.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ async fn test_block_size_limit() {
160160
recipient: Recipient::root(0),
161161
amount: Amount::ONE,
162162
});
163-
let result = chain.execute_block(&invalid_block, time, None).await;
163+
let result = chain.execute_block(&invalid_block, time, None, None).await;
164164
assert_matches!(
165165
result,
166166
Err(ChainError::ExecutionError(
@@ -170,7 +170,10 @@ async fn test_block_size_limit() {
170170
);
171171

172172
// The valid block is accepted...
173-
let outcome = chain.execute_block(&valid_block, time, None).await.unwrap();
173+
let outcome = chain
174+
.execute_block(&valid_block, time, None, None)
175+
.await
176+
.unwrap();
174177
let block = Block::new(valid_block, outcome);
175178

176179
// ...because its size is exactly at the allowed limit.
@@ -231,7 +234,7 @@ async fn test_application_permissions() -> anyhow::Result<()> {
231234
let invalid_block = make_first_block(chain_id)
232235
.with_incoming_bundle(bundle.clone())
233236
.with_simple_transfer(chain_id, Amount::ONE);
234-
let result = chain.execute_block(&invalid_block, time, None).await;
237+
let result = chain.execute_block(&invalid_block, time, None, None).await;
235238
assert_matches!(result, Err(ChainError::AuthorizedApplications(app_ids))
236239
if app_ids == vec![application_id]
237240
);
@@ -247,7 +250,7 @@ async fn test_application_permissions() -> anyhow::Result<()> {
247250
.with_incoming_bundle(bundle)
248251
.with_operation(app_operation.clone());
249252
let executed_block = chain
250-
.execute_block(&valid_block, time, None)
253+
.execute_block(&valid_block, time, None, None)
251254
.await?
252255
.with(valid_block);
253256
let value = Hashed::new(ConfirmedBlock::new(executed_block));
@@ -256,14 +259,14 @@ async fn test_application_permissions() -> anyhow::Result<()> {
256259
let invalid_block = make_child_block(&value.clone())
257260
.with_simple_transfer(chain_id, Amount::ONE)
258261
.with_operation(app_operation.clone());
259-
let result = chain.execute_block(&invalid_block, time, None).await;
262+
let result = chain.execute_block(&invalid_block, time, None, None).await;
260263
assert_matches!(result, Err(ChainError::AuthorizedApplications(app_ids))
261264
if app_ids == vec![application_id]
262265
);
263266

264267
// Also, blocks without an application operation or incoming message are forbidden.
265268
let invalid_block = make_child_block(&value.clone());
266-
let result = chain.execute_block(&invalid_block, time, None).await;
269+
let result = chain.execute_block(&invalid_block, time, None, None).await;
267270
assert_matches!(result, Err(ChainError::MissingMandatoryApplications(app_ids))
268271
if app_ids == vec![application_id]
269272
);
@@ -272,7 +275,7 @@ async fn test_application_permissions() -> anyhow::Result<()> {
272275
application.expect_call(ExpectedCall::execute_operation(|_, _, _| Ok(vec![])));
273276
application.expect_call(ExpectedCall::default_finalize());
274277
let valid_block = make_child_block(&value).with_operation(app_operation);
275-
chain.execute_block(&valid_block, time, None).await?;
278+
chain.execute_block(&valid_block, time, None, None).await?;
276279

277280
Ok(())
278281
}

linera-client/src/client_context.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,11 +972,12 @@ where
972972
pub async fn stage_block_execution(
973973
&self,
974974
block: ProposedBlock,
975+
round: Option<u32>,
975976
) -> Result<ExecutedBlock, Error> {
976977
Ok(self
977978
.client
978979
.local_node()
979-
.stage_block_execution(block)
980+
.stage_block_execution(block, round)
980981
.await?
981982
.0)
982983
}

linera-core/src/chain_worker/actor.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ where
8585
/// Execute a block but discard any changes to the chain state.
8686
StageBlockExecution {
8787
block: ProposedBlock,
88+
round: Option<u32>,
8889
#[debug(skip)]
8990
callback: oneshot::Sender<Result<(ExecutedBlock, ChainInfoResponse), WorkerError>>,
9091
},
@@ -286,8 +287,12 @@ where
286287
} => callback
287288
.send(self.worker.describe_application(application_id).await)
288289
.is_ok(),
289-
ChainWorkerRequest::StageBlockExecution { block, callback } => callback
290-
.send(self.worker.stage_block_execution(block).await)
290+
ChainWorkerRequest::StageBlockExecution {
291+
block,
292+
round,
293+
callback,
294+
} => callback
295+
.send(self.worker.stage_block_execution(block, round).await)
291296
.is_ok(),
292297
ChainWorkerRequest::ProcessTimeout {
293298
certificate,

linera-core/src/chain_worker/state/attempted_changes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ where
359359
let verified_outcome = Box::pin(self.state.chain.execute_block(
360360
&executed_block.block,
361361
local_time,
362+
None,
362363
Some(executed_block.outcome.oracle_responses.clone()),
363364
))
364365
.await?;

linera-core/src/chain_worker/state/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,11 @@ where
179179
pub(super) async fn stage_block_execution(
180180
&mut self,
181181
block: ProposedBlock,
182+
round: Option<u32>,
182183
) -> Result<(ExecutedBlock, ChainInfoResponse), WorkerError> {
183184
ChainWorkerStateWithTemporaryChanges::new(self)
184185
.await
185-
.stage_block_execution(block)
186+
.stage_block_execution(block, round)
186187
.await
187188
}
188189

linera-core/src/chain_worker/state/temporary_changes.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,15 @@ where
126126
/// Executes a block without persisting any changes to the state.
127127
pub(super) async fn stage_block_execution(
128128
&mut self,
129-
proposal: ProposedBlock,
129+
block: ProposedBlock,
130+
round: Option<u32>,
130131
) -> Result<(ExecutedBlock, ChainInfoResponse), WorkerError> {
131132
let local_time = self.0.storage.clock().current_time();
132-
let signer = proposal.authenticated_signer;
133+
let signer = block.authenticated_signer;
133134

134-
let executed_block = Box::pin(self.0.chain.execute_block(&proposal, local_time, None))
135+
let executed_block = Box::pin(self.0.chain.execute_block(&block, local_time, round, None))
135136
.await?
136-
.with(proposal);
137+
.with(block);
137138

138139
let mut response = ChainInfoResponse::new(&self.0.chain, None);
139140
if let Some(signer) = signer {
@@ -238,7 +239,12 @@ where
238239
let outcome = if let Some(outcome) = outcome {
239240
outcome.clone()
240241
} else {
241-
Box::pin(self.0.chain.execute_block(block, local_time, None)).await?
242+
Box::pin(
243+
self.0
244+
.chain
245+
.execute_block(block, local_time, round.multi_leader(), None),
246+
)
247+
.await?
242248
};
243249

244250
let executed_block = outcome.with(block.clone());

linera-core/src/client/mod.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1903,9 +1903,10 @@ where
19031903
async fn stage_block_execution_and_discard_failing_messages(
19041904
&self,
19051905
mut block: ProposedBlock,
1906+
round: Option<u32>,
19061907
) -> Result<(ExecutedBlock, ChainInfoResponse), ChainClientError> {
19071908
loop {
1908-
let result = self.stage_block_execution(block.clone()).await;
1909+
let result = self.stage_block_execution(block.clone(), round).await;
19091910
if let Err(ChainClientError::LocalNodeError(LocalNodeError::WorkerError(
19101911
WorkerError::ChainError(chain_error),
19111912
))) = &result
@@ -1944,12 +1945,13 @@ where
19441945
async fn stage_block_execution(
19451946
&self,
19461947
block: ProposedBlock,
1948+
round: Option<u32>,
19471949
) -> Result<(ExecutedBlock, ChainInfoResponse), ChainClientError> {
19481950
loop {
19491951
let result = self
19501952
.client
19511953
.local_node
1952-
.stage_block_execution(block.clone())
1954+
.stage_block_execution(block.clone(), round)
19531955
.await;
19541956
if let Err(LocalNodeError::BlobsNotFound(blob_ids)) = &result {
19551957
self.receive_certificates_for_blobs(blob_ids.clone())
@@ -2122,8 +2124,18 @@ where
21222124
};
21232125
// Make sure every incoming message succeeds and otherwise remove them.
21242126
// Also, compute the final certified hash while we're at it.
2127+
2128+
let info = self.chain_info().await?;
2129+
// Use the round number assuming there are oracle responses.
2130+
// Using the round number during execution counts as an oracle.
2131+
// Accessing the round number in single-leader rounds where we are not the leader
2132+
// is not currently supported.
2133+
let round = match Self::round_for_new_proposal(&info, &identity, &block, true)? {
2134+
Either::Left(round) => round.multi_leader(),
2135+
Either::Right(_) => None,
2136+
};
21252137
let (executed_block, _) = self
2126-
.stage_block_execution_and_discard_failing_messages(block)
2138+
.stage_block_execution_and_discard_failing_messages(block, round)
21272139
.await?;
21282140
let blobs = self
21292141
.read_local_blobs(executed_block.required_blob_ids())
@@ -2270,7 +2282,7 @@ where
22702282
timestamp,
22712283
};
22722284
match self
2273-
.stage_block_execution_and_discard_failing_messages(block)
2285+
.stage_block_execution_and_discard_failing_messages(block, None)
22742286
.await
22752287
{
22762288
Ok((_, response)) => Ok((
@@ -2424,6 +2436,7 @@ where
24242436
{
24252437
return self.finalize_locking_block(info).await;
24262438
}
2439+
let identity = self.identity().await?;
24272440

24282441
// Otherwise we have to re-propose the highest validated block, if there is one.
24292442
let pending: Option<ProposedBlock> = self.state().pending_proposal().clone();
@@ -2432,18 +2445,28 @@ where
24322445
LockingBlock::Regular(certificate) => certificate.block().clone().into(),
24332446
LockingBlock::Fast(proposal) => {
24342447
let block = proposal.content.block.clone();
2435-
self.stage_block_execution(block).await?.0
2448+
self.stage_block_execution(block, None).await?.0
24362449
}
24372450
}
24382451
} else if let Some(block) = pending {
24392452
// Otherwise we are free to propose our own pending block.
2440-
self.stage_block_execution(block).await?.0
2453+
// Use the round number assuming there are oracle responses.
2454+
// Using the round number during execution counts as an oracle.
2455+
let round = match Self::round_for_new_proposal(&info, &identity, &block, true)? {
2456+
Either::Left(round) => round.multi_leader(),
2457+
Either::Right(_) => None,
2458+
};
2459+
self.stage_block_execution(block, round).await?.0
24412460
} else {
24422461
return Ok(ClientOutcome::Committed(None)); // Nothing to do.
24432462
};
24442463

2445-
let identity = self.identity().await?;
2446-
let round = match Self::round_for_new_proposal(&info, &identity, &executed_block)? {
2464+
let round = match Self::round_for_new_proposal(
2465+
&info,
2466+
&identity,
2467+
&executed_block.block,
2468+
executed_block.outcome.has_oracle_responses(),
2469+
)? {
24472470
Either::Left(round) => round,
24482471
Either::Right(timeout) => return Ok(ClientOutcome::WaitForTimeout(timeout)),
24492472
};
@@ -2546,17 +2569,16 @@ where
25462569
fn round_for_new_proposal(
25472570
info: &ChainInfo,
25482571
identity: &Owner,
2549-
executed_block: &ExecutedBlock,
2572+
block: &ProposedBlock,
2573+
has_oracle_responses: bool,
25502574
) -> Result<Either<Round, RoundTimeout>, ChainClientError> {
25512575
let manager = &info.manager;
2552-
let block = &executed_block.block;
25532576
// If there is a conflicting proposal in the current round, we can only propose if the
25542577
// next round can be started without a timeout, i.e. if we are in a multi-leader round.
25552578
// Similarly, we cannot propose a block that uses oracles in the fast round.
25562579
let conflict = manager.requested_proposed.as_ref().is_some_and(|proposal| {
25572580
proposal.content.round == manager.current_round && proposal.content.block != *block
2558-
}) || (manager.current_round.is_fast()
2559-
&& executed_block.outcome.has_oracle_responses());
2581+
}) || (manager.current_round.is_fast() && has_oracle_responses);
25602582
let round = if !conflict {
25612583
manager.current_round
25622584
} else if let Some(round) = manager

linera-core/src/local_node.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ where
174174
pub async fn stage_block_execution(
175175
&self,
176176
block: ProposedBlock,
177+
round: Option<u32>,
177178
) -> Result<(ExecutedBlock, ChainInfoResponse), LocalNodeError> {
178-
Ok(self.node.state.stage_block_execution(block).await?)
179+
Ok(self.node.state.stage_block_execution(block, round).await?)
179180
}
180181

181182
/// Reads blobs from storage.

0 commit comments

Comments
 (0)