1use std::{
13 collections::{BTreeMap, HashMap, HashSet},
14 sync::Arc,
15};
16
17use anyhow::{anyhow, bail, Context as _, Result};
18use futures::future::join_all;
19use linera_base::{
20 crypto::{CryptoHash, ValidatorPublicKey, ValidatorSignature},
21 data_types::{Epoch, Round, Timestamp},
22 identifiers::{AccountOwner, ChainId},
23};
24use linera_cache::ValueCache;
25use linera_chain::{
26 data_types::{BlockProposal, IncomingBundle, ProposedBlock, Transaction},
27 justification::JustificationChain,
28 types::{
29 CertificateKind, CertificateValue as _, ConfirmedBlock, ConfirmedBlockCertificate,
30 GenericCertificate,
31 },
32};
33use linera_core::{
34 client::Client as CoreClient,
35 data_types::ChainInfoQuery,
36 environment::Environment,
37 node::{CrossChainMessageDelivery, ValidatorNode},
38 remote_node::RemoteNode,
39};
40use linera_execution::{committee::Committee, Operation};
41use linera_rpc::Client;
42use tokio::sync::Mutex;
43use tracing::warn;
44
45use crate::benchmark::{BenchmarkClient, BenchmarkError};
46
47pub struct LiteChainClient<Env: Environment> {
50 chain_id: ChainId,
51 owner: AccountOwner,
52 epoch: Epoch,
53 height: linera_base::data_types::BlockHeight,
54 previous_block_hash: Option<CryptoHash>,
55 nodes: Vec<(ValidatorPublicKey, Client)>,
56 committee: Committee,
57 client: Arc<CoreClient<Env>>,
59 value_cache: ValueCache<CryptoHash, ConfirmedBlockCertificate>,
60 pending_bundles: Vec<IncomingBundle>,
65 light_certificates: bool,
68}
69
70impl<Env: Environment> LiteChainClient<Env> {
71 pub async fn seed(
73 chain_id: ChainId,
74 owner: AccountOwner,
75 nodes: Vec<(ValidatorPublicKey, Client)>,
76 committee: Committee,
77 client: Arc<CoreClient<Env>>,
78 light_certificates: bool,
79 ) -> Result<Self> {
80 for (public_key, node) in &nodes {
81 let query = ChainInfoQuery::new(chain_id);
82 match node.handle_chain_info_query(query).await {
83 Ok(response) => {
84 let info = response.info;
85 return Ok(Self {
86 chain_id,
87 owner,
88 epoch: info.epoch,
89 height: info.next_block_height,
90 previous_block_hash: info.block_hash,
91 nodes,
92 committee,
93 client,
94 value_cache: ValueCache::new("lite-benchmark", 64, 60),
95 pending_bundles: Vec::new(),
96 light_certificates,
97 });
98 }
99 Err(error) => {
100 warn!(%public_key, %error, "validator did not answer the initial chain info query");
101 }
102 }
103 }
104 bail!("no validator answered the initial chain info query");
105 }
106
107 pub async fn propose_and_commit(
123 &mut self,
124 operations: Vec<linera_execution::Operation>,
125 process_messages: bool,
126 bundle_cap: usize,
127 ) -> Result<usize> {
128 let bundles: Vec<IncomingBundle> = if process_messages {
129 self.pending_bundles
130 .iter()
131 .take(bundle_cap)
132 .cloned()
133 .collect()
134 } else {
135 Vec::new()
136 };
137 let num_bundles = bundles.len();
138 let consumed: HashSet<_> = bundles
142 .iter()
143 .map(|bundle| (bundle.origin, bundle.bundle.cursor()))
144 .collect();
145 let transactions = bundles
146 .into_iter()
147 .map(Transaction::ReceiveMessages)
148 .chain(operations.into_iter().map(Transaction::ExecuteOperation))
149 .collect();
150 let block = ProposedBlock {
151 chain_id: self.chain_id,
152 epoch: self.epoch,
153 transactions,
154 height: self.height,
155 timestamp: Timestamp::now(),
156 authenticated_owner: Some(self.owner),
157 previous_block_hash: self.previous_block_hash,
158 };
159 let proposal =
160 BlockProposal::new_initial(self.owner, Round::Fast, block, self.client.signer())
161 .await
162 .map_err(|error| anyhow!("failed to sign the block proposal: {error}"))?;
163
164 let responses = join_all(self.nodes.iter().map(|(public_key, node)| {
166 let proposal = proposal.clone();
167 let public_key = *public_key;
168 let node = node.clone();
169 async move { (public_key, node.handle_block_proposal(proposal).await) }
170 }))
171 .await;
172 let votes = responses
173 .into_iter()
174 .filter_map(|(public_key, result)| match result {
175 Ok(response) => response.info.manager.pending.map(|vote| (public_key, vote)),
176 Err(error) => {
177 warn!(%public_key, %error, "validator rejected the block proposal");
178 None
179 }
180 });
181 let (value_hash, signatures) =
182 find_confirming_quorum(self.chain_id, votes, &self.committee)
183 .context("no quorum of validators voted to confirm the proposed block")?;
184
185 let (confirmed_block, next_pending) = self
189 .fetch_confirmed_and_pending(value_hash, process_messages, &consumed)
190 .await?;
191
192 let quorum = GenericCertificate::new_with_payload(
196 confirmed_block,
197 Round::Fast,
198 None,
199 true,
200 None,
201 signatures,
202 );
203 let certificate =
204 ConfirmedBlockCertificate::from_parts(quorum, JustificationChain::default());
205 let certificate_hash = certificate.hash();
208 let cached_certificate = self.value_cache.insert(&certificate_hash, certificate);
209
210 let light_certificates = self.light_certificates;
222 let results = join_all(self.nodes.iter().map(|(public_key, node)| {
223 let node = node.clone();
224 let cached_certificate = cached_certificate.clone();
225 async move {
226 if light_certificates {
227 let remote_node = RemoteNode {
228 public_key: *public_key,
229 node,
230 };
231 remote_node
232 .handle_optimized_confirmed_certificate(
233 &cached_certificate,
234 CrossChainMessageDelivery::NonBlocking,
235 )
236 .await
237 .map(|_| ())
238 } else {
239 node.handle_confirmed_certificate(
240 cached_certificate,
241 CrossChainMessageDelivery::NonBlocking,
242 )
243 .await
244 .map(|_| ())
245 }
246 }
247 }))
248 .await;
249 let mut committed = false;
250 for result in results {
251 if let Err(error) = result {
252 warn!(%error, "validator failed to process the confirmed certificate");
253 } else {
254 committed = true;
255 }
256 }
257 anyhow::ensure!(committed, "no validator accepted the confirmed certificate");
258
259 self.previous_block_hash = Some(certificate_hash);
260 self.height = self.height.try_add_one()?;
261 self.pending_bundles = next_pending;
265 Ok(num_bundles)
266 }
267
268 async fn fetch_confirmed_and_pending(
284 &self,
285 value_hash: CryptoHash,
286 process_messages: bool,
287 consumed: &HashSet<(ChainId, linera_base::data_types::Cursor)>,
288 ) -> Result<(ConfirmedBlock, Vec<IncomingBundle>)> {
289 let responses = join_all(self.nodes.iter().map(|(public_key, node)| {
290 let node = node.clone();
291 let mut query = ChainInfoQuery::new(self.chain_id);
292 query.request_manager_values = true;
297 if process_messages {
298 query = query.with_pending_message_bundles();
299 }
300 let public_key = *public_key;
301 async move {
302 match node.handle_chain_info_query(query).await {
303 Ok(response) => Some(response.info),
304 Err(error) => {
305 warn!(%public_key, %error, "validator did not answer the confirmed-value query");
306 None
307 }
308 }
309 }
310 }))
311 .await;
312
313 let mut confirmed_block: Option<ConfirmedBlock> = None;
314 let mut per_node: Vec<Vec<IncomingBundle>> = Vec::new();
315 for info in responses.into_iter().flatten() {
316 if process_messages {
317 per_node.push(info.requested_pending_message_bundles);
318 }
319 if confirmed_block.is_none() {
320 if let Some(value) = info.manager.requested_confirmed {
321 if value.hash() == value_hash {
322 confirmed_block = Some(*value);
323 }
324 }
325 }
326 }
327 let confirmed_block =
328 confirmed_block.context("could not fetch the confirmed block value")?;
329
330 let next_pending = if process_messages {
331 common_prefix_bundles(per_node)
332 .into_iter()
333 .filter(|bundle| !consumed.contains(&(bundle.origin, bundle.bundle.cursor())))
334 .collect()
335 } else {
336 Vec::new()
337 };
338 Ok((confirmed_block, next_pending))
339 }
340}
341
342fn common_prefix_bundles(per_node: Vec<Vec<IncomingBundle>>) -> Vec<IncomingBundle> {
349 let Some((first, rest)) = per_node.split_first() else {
350 return Vec::new();
351 };
352 let group = |bundles: &[IncomingBundle]| -> BTreeMap<ChainId, Vec<IncomingBundle>> {
354 let mut by_origin: BTreeMap<ChainId, Vec<IncomingBundle>> = BTreeMap::new();
355 for bundle in bundles {
356 by_origin
357 .entry(bundle.origin)
358 .or_default()
359 .push(bundle.clone());
360 }
361 by_origin
362 };
363 let base = group(first);
364 let others: Vec<_> = rest.iter().map(|node| group(node)).collect();
365 let mut result = Vec::new();
366 for (origin, base_bundles) in base {
367 let mut prefix_len = base_bundles.len();
368 for other in &others {
369 let other_bundles = other.get(&origin).map_or(&[][..], Vec::as_slice);
370 let matching = base_bundles
371 .iter()
372 .zip(other_bundles)
373 .take_while(|(a, b)| a.bundle.cursor() == b.bundle.cursor())
374 .count();
375 prefix_len = prefix_len.min(matching);
376 if prefix_len == 0 {
377 break;
378 }
379 }
380 result.extend(base_bundles.into_iter().take(prefix_len));
381 }
382 result
383}
384
385fn find_confirming_quorum(
390 chain_id: ChainId,
391 votes: impl IntoIterator<Item = (ValidatorPublicKey, linera_chain::data_types::LiteVote)>,
392 committee: &Committee,
393) -> Option<(CryptoHash, Vec<(ValidatorPublicKey, ValidatorSignature)>)> {
394 let mut signatures_by_hash: HashMap<CryptoHash, Vec<(ValidatorPublicKey, ValidatorSignature)>> =
395 HashMap::new();
396 let mut weight_by_hash: HashMap<CryptoHash, u64> = HashMap::new();
397 for (public_key, vote) in votes {
398 if vote.value.chain_id != chain_id || vote.value.kind != CertificateKind::Confirmed {
399 continue;
400 }
401 let hash = vote.value.value_hash;
402 signatures_by_hash
403 .entry(hash)
404 .or_default()
405 .push((public_key, vote.signature));
406 let weight = weight_by_hash.entry(hash).or_insert(0);
407 *weight += committee.weight(&public_key);
408 if *weight >= committee.quorum_threshold() {
409 let signatures = signatures_by_hash
410 .remove(&hash)
411 .expect("just inserted above");
412 return Some((hash, signatures));
413 }
414 }
415 None
416}
417
418pub struct LiteBenchmarkClient<Env: Environment> {
425 chain_id: ChainId,
426 owner: AccountOwner,
427 inner: Mutex<LiteChainClient<Env>>,
428 process_messages: bool,
429 bundle_cap: Option<usize>,
430}
431
432impl<Env: Environment> LiteBenchmarkClient<Env> {
433 pub fn new(
436 client: LiteChainClient<Env>,
437 process_messages: bool,
438 bundle_cap: Option<usize>,
439 ) -> Self {
440 Self {
441 chain_id: client.chain_id,
442 owner: client.owner,
443 inner: Mutex::new(client),
444 process_messages,
445 bundle_cap,
446 }
447 }
448}
449
450#[async_trait::async_trait]
451impl<Env: Environment> BenchmarkClient for LiteBenchmarkClient<Env> {
452 fn chain_id(&self) -> ChainId {
453 self.chain_id
454 }
455
456 async fn owner(&self) -> Result<AccountOwner, BenchmarkError> {
457 Ok(self.owner)
458 }
459
460 async fn commit_operations(&self, operations: Vec<Operation>) -> Result<(), BenchmarkError> {
461 let bundle_cap = self
462 .bundle_cap
463 .unwrap_or_else(|| operations.len().saturating_mul(2));
464 self.inner
465 .lock()
466 .await
467 .propose_and_commit(operations, self.process_messages, bundle_cap)
468 .await
469 .map_err(|error| BenchmarkError::LiteClient(error.to_string()))?;
470 Ok(())
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use linera_base::{
477 crypto::{AccountSecretKey, CryptoHash, ValidatorKeypair},
478 data_types::BlockHeight,
479 };
480 use linera_chain::data_types::{LiteValue, LiteVote, MessageAction, MessageBundle};
481
482 use super::*;
483
484 fn bundle(origin: ChainId, height: u64, index: u32) -> IncomingBundle {
487 IncomingBundle {
488 origin,
489 bundle: MessageBundle {
490 height: BlockHeight(height),
491 timestamp: Timestamp::from(0),
492 certificate_hash: CryptoHash::test_hash("cert"),
493 transaction_index: index,
494 messages: Vec::new(),
495 },
496 action: MessageAction::Accept,
497 }
498 }
499
500 fn cursors(bundles: &[IncomingBundle]) -> Vec<(ChainId, u64, u32)> {
505 let mut cursors: Vec<_> = bundles
506 .iter()
507 .map(|b| (b.origin, b.bundle.height.0, b.bundle.transaction_index))
508 .collect();
509 cursors.sort();
510 cursors
511 }
512
513 fn sorted(mut cursors: Vec<(ChainId, u64, u32)>) -> Vec<(ChainId, u64, u32)> {
514 cursors.sort();
515 cursors
516 }
517
518 #[test]
519 fn common_prefix_takes_the_agreed_per_origin_prefix() {
520 let a = ChainId(CryptoHash::test_hash("a"));
521 let b = ChainId(CryptoHash::test_hash("b"));
522
523 assert!(common_prefix_bundles(Vec::new()).is_empty());
525
526 let only = vec![bundle(a, 0, 0), bundle(a, 1, 0), bundle(b, 0, 0)];
528 assert_eq!(
529 cursors(&common_prefix_bundles(vec![only.clone()])),
530 sorted(vec![(a, 0, 0), (a, 1, 0), (b, 0, 0)]),
531 );
532
533 assert_eq!(
535 common_prefix_bundles(vec![only.clone(), only.clone()]).len(),
536 3
537 );
538
539 let ahead = vec![bundle(a, 0, 0), bundle(a, 1, 0), bundle(b, 0, 0)];
542 let behind = vec![bundle(a, 0, 0), bundle(b, 0, 0)];
543 assert_eq!(
544 cursors(&common_prefix_bundles(vec![ahead, behind])),
545 sorted(vec![(a, 0, 0), (b, 0, 0)]),
546 );
547
548 let left = vec![bundle(a, 0, 0), bundle(a, 1, 0), bundle(a, 2, 0)];
551 let right = vec![bundle(a, 0, 0), bundle(a, 5, 0), bundle(a, 2, 0)];
552 assert_eq!(
553 cursors(&common_prefix_bundles(vec![left, right])),
554 vec![(a, 0, 0)],
555 );
556
557 let with_b = vec![bundle(a, 0, 0), bundle(b, 0, 0)];
560 let without_b = vec![bundle(a, 0, 0)];
561 assert_eq!(
562 cursors(&common_prefix_bundles(vec![with_b, without_b])),
563 vec![(a, 0, 0)],
564 );
565 }
566
567 fn committee_of(size: usize) -> (Committee, Vec<ValidatorPublicKey>) {
568 let keys: Vec<_> = (0..size)
569 .map(|_| {
570 (
571 ValidatorKeypair::generate().public_key,
572 AccountSecretKey::generate().public(),
573 )
574 })
575 .collect();
576 let public_keys = keys.iter().map(|(key, _)| *key).collect();
577 (Committee::make_simple(keys), public_keys)
578 }
579
580 fn vote(chain_id: ChainId, value_hash: CryptoHash) -> LiteVote {
581 LiteVote {
582 value: LiteValue {
583 value_hash,
584 chain_id,
585 kind: CertificateKind::Confirmed,
586 },
587 round: Round::Fast,
588 unlocking_round: None,
589 first_round: true,
590 justification_commitment: None,
591 signature: ValidatorSignature::sign_prehash(
592 &ValidatorKeypair::generate().secret_key,
593 value_hash,
594 ),
595 }
596 }
597
598 #[test]
599 fn quorum_is_reached_once_enough_weight_agrees() {
600 let chain_id = ChainId(CryptoHash::test_hash("chain"));
601 let value_hash = CryptoHash::test_hash("confirmed-block");
602 let (committee, keys) = committee_of(4);
603
604 let votes = keys[..2]
606 .iter()
607 .map(|key| (*key, vote(chain_id, value_hash)));
608 assert!(find_confirming_quorum(chain_id, votes, &committee).is_none());
609
610 let votes = keys[..3]
612 .iter()
613 .map(|key| (*key, vote(chain_id, value_hash)));
614 let (hash, signatures) = find_confirming_quorum(chain_id, votes, &committee)
615 .expect("3 out of 4 equally-weighted validators should reach the quorum threshold");
616 assert_eq!(hash, value_hash);
617 assert_eq!(signatures.len(), 3);
618 }
619
620 #[test]
621 fn votes_for_a_different_chain_are_ignored() {
622 let chain_id = ChainId(CryptoHash::test_hash("chain"));
623 let other_chain_id = ChainId(CryptoHash::test_hash("other-chain"));
624 let value_hash = CryptoHash::test_hash("confirmed-block");
625 let (committee, keys) = committee_of(4);
626
627 let votes = keys
628 .iter()
629 .map(|key| (*key, vote(other_chain_id, value_hash)));
630 assert!(find_confirming_quorum(chain_id, votes, &committee).is_none());
631 }
632
633 #[test]
634 fn a_split_vote_never_reaches_quorum_on_either_side() {
635 let chain_id = ChainId(CryptoHash::test_hash("chain"));
636 let hash_a = CryptoHash::test_hash("block-a");
637 let hash_b = CryptoHash::test_hash("block-b");
638 let (committee, keys) = committee_of(4);
639
640 let votes = vec![
641 (keys[0], vote(chain_id, hash_a)),
642 (keys[1], vote(chain_id, hash_a)),
643 (keys[2], vote(chain_id, hash_b)),
644 (keys[3], vote(chain_id, hash_b)),
645 ];
646 assert!(find_confirming_quorum(chain_id, votes, &committee).is_none());
647 }
648}