use std::{
collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
};
use anyhow::{anyhow, bail, Context as _, Result};
use futures::future::join_all;
use linera_base::{
crypto::{CryptoHash, ValidatorPublicKey, ValidatorSignature},
data_types::{BlockHeight, Epoch, Round, Timestamp},
identifiers::{AccountOwner, ChainId},
};
use linera_cache::ValueCache;
use linera_chain::{
data_types::{BlockProposal, IncomingBundle, MessageBundle, ProposedBlock, Transaction},
types::{
CertificateKind, CertificateValue as _, ConfirmedBlock, ConfirmedBlockCertificate,
GenericCertificate,
},
};
use linera_core::{
client::Client as CoreClient,
data_types::ChainInfoQuery,
environment::Environment,
node::{CrossChainMessageDelivery, ValidatorNode},
remote_node::RemoteNode,
};
use linera_execution::{committee::Committee, Operation};
use linera_rpc::Client;
use tokio::sync::Mutex;
use tracing::warn;
use crate::benchmark::{BenchmarkClient, BenchmarkError};
fn cursor_of(bundle: &MessageBundle) -> (BlockHeight, u32) {
(bundle.height, bundle.transaction_index)
}
pub struct LiteChainClient<Env: Environment> {
chain_id: ChainId,
owner: AccountOwner,
epoch: Epoch,
height: linera_base::data_types::BlockHeight,
previous_block_hash: Option<CryptoHash>,
nodes: Vec<(ValidatorPublicKey, Client)>,
committee: Committee,
client: Arc<CoreClient<Env>>,
value_cache: ValueCache<CryptoHash, ConfirmedBlockCertificate>,
pending_bundles: Vec<IncomingBundle>,
light_certificates: bool,
}
impl<Env: Environment> LiteChainClient<Env> {
pub async fn seed(
chain_id: ChainId,
owner: AccountOwner,
nodes: Vec<(ValidatorPublicKey, Client)>,
committee: Committee,
client: Arc<CoreClient<Env>>,
light_certificates: bool,
) -> Result<Self> {
for (public_key, node) in &nodes {
let query = ChainInfoQuery::new(chain_id);
match node.handle_chain_info_query(query).await {
Ok(response) => {
let info = response.info;
return Ok(Self {
chain_id,
owner,
epoch: info.epoch,
height: info.next_block_height,
previous_block_hash: info.block_hash,
nodes,
committee,
client,
value_cache: ValueCache::new("lite-benchmark", 64, 60),
pending_bundles: Vec::new(),
light_certificates,
});
}
Err(error) => {
warn!(%public_key, %error, "validator did not answer the initial chain info query");
}
}
}
bail!("no validator answered the initial chain info query");
}
pub async fn propose_and_commit(
&mut self,
operations: Vec<linera_execution::Operation>,
process_messages: bool,
bundle_cap: usize,
) -> Result<usize> {
let bundles: Vec<IncomingBundle> = if process_messages {
self.pending_bundles
.iter()
.take(bundle_cap)
.cloned()
.collect()
} else {
Vec::new()
};
let num_bundles = bundles.len();
let consumed: HashSet<_> = bundles
.iter()
.map(|bundle| (bundle.origin, cursor_of(&bundle.bundle)))
.collect();
let transactions = bundles
.into_iter()
.map(Transaction::ReceiveMessages)
.chain(operations.into_iter().map(Transaction::ExecuteOperation))
.collect();
let block = ProposedBlock {
chain_id: self.chain_id,
epoch: self.epoch,
transactions,
height: self.height,
timestamp: Timestamp::now(),
authenticated_signer: Some(self.owner),
previous_block_hash: self.previous_block_hash,
};
let proposal =
BlockProposal::new_initial(self.owner, Round::Fast, block, self.client.signer())
.await
.map_err(|error| anyhow!("failed to sign the block proposal: {error}"))?;
let responses = join_all(self.nodes.iter().map(|(public_key, node)| {
let proposal = proposal.clone();
let public_key = *public_key;
let node = node.clone();
async move { (public_key, node.handle_block_proposal(proposal).await) }
}))
.await;
let votes = responses
.into_iter()
.filter_map(|(public_key, result)| match result {
Ok(response) => response.info.manager.pending.map(|vote| (public_key, vote)),
Err(error) => {
warn!(%public_key, %error, "validator rejected the block proposal");
None
}
});
let (value_hash, signatures) =
find_confirming_quorum(self.chain_id, votes, &self.committee)
.context("no quorum of validators voted to confirm the proposed block")?;
let (confirmed_block, next_pending) = self
.fetch_confirmed_and_pending(value_hash, process_messages, &consumed)
.await?;
let certificate = GenericCertificate::new(confirmed_block, Round::Fast, signatures);
let certificate_hash = certificate.hash();
let cached_certificate = self.value_cache.insert(&certificate_hash, certificate);
let light_certificates = self.light_certificates;
let results = join_all(self.nodes.iter().map(|(public_key, node)| {
let node = node.clone();
let cached_certificate = cached_certificate.clone();
async move {
if light_certificates {
let remote_node = RemoteNode {
public_key: *public_key,
node,
};
remote_node
.handle_optimized_confirmed_certificate(
&cached_certificate,
CrossChainMessageDelivery::NonBlocking,
)
.await
.map(|_| ())
} else {
node.handle_confirmed_certificate(
cached_certificate,
CrossChainMessageDelivery::NonBlocking,
)
.await
.map(|_| ())
}
}
}))
.await;
let mut committed = false;
for result in results {
if let Err(error) = result {
warn!(%error, "validator failed to process the confirmed certificate");
} else {
committed = true;
}
}
anyhow::ensure!(committed, "no validator accepted the confirmed certificate");
self.previous_block_hash = Some(certificate_hash);
self.height = self.height.try_add_one()?;
self.pending_bundles = next_pending;
Ok(num_bundles)
}
async fn fetch_confirmed_and_pending(
&self,
value_hash: CryptoHash,
process_messages: bool,
consumed: &HashSet<(ChainId, (BlockHeight, u32))>,
) -> Result<(ConfirmedBlock, Vec<IncomingBundle>)> {
let responses = join_all(self.nodes.iter().map(|(public_key, node)| {
let node = node.clone();
let mut query = ChainInfoQuery::new(self.chain_id);
query.request_manager_values = true;
if process_messages {
query = query.with_pending_message_bundles();
}
let public_key = *public_key;
async move {
match node.handle_chain_info_query(query).await {
Ok(response) => Some(response.info),
Err(error) => {
warn!(%public_key, %error, "validator did not answer the confirmed-value query");
None
}
}
}
}))
.await;
let mut confirmed_block: Option<ConfirmedBlock> = None;
let mut per_node: Vec<Vec<IncomingBundle>> = Vec::new();
for info in responses.into_iter().flatten() {
if process_messages {
per_node.push(info.requested_pending_message_bundles);
}
if confirmed_block.is_none() {
if let Some(value) = info.manager.requested_confirmed {
if value.hash() == value_hash {
confirmed_block = Some(*value);
}
}
}
}
let confirmed_block =
confirmed_block.context("could not fetch the confirmed block value")?;
let next_pending = if process_messages {
common_prefix_bundles(per_node)
.into_iter()
.filter(|bundle| !consumed.contains(&(bundle.origin, cursor_of(&bundle.bundle))))
.collect()
} else {
Vec::new()
};
Ok((confirmed_block, next_pending))
}
}
fn common_prefix_bundles(per_node: Vec<Vec<IncomingBundle>>) -> Vec<IncomingBundle> {
let Some((first, rest)) = per_node.split_first() else {
return Vec::new();
};
let group = |bundles: &[IncomingBundle]| -> BTreeMap<ChainId, Vec<IncomingBundle>> {
let mut by_origin: BTreeMap<ChainId, Vec<IncomingBundle>> = BTreeMap::new();
for bundle in bundles {
by_origin
.entry(bundle.origin)
.or_default()
.push(bundle.clone());
}
by_origin
};
let base = group(first);
let others: Vec<_> = rest.iter().map(|node| group(node)).collect();
let mut result = Vec::new();
for (origin, base_bundles) in base {
let mut prefix_len = base_bundles.len();
for other in &others {
let other_bundles = other.get(&origin).map_or(&[][..], Vec::as_slice);
let matching = base_bundles
.iter()
.zip(other_bundles)
.take_while(|(a, b)| cursor_of(&a.bundle) == cursor_of(&b.bundle))
.count();
prefix_len = prefix_len.min(matching);
if prefix_len == 0 {
break;
}
}
result.extend(base_bundles.into_iter().take(prefix_len));
}
result
}
fn find_confirming_quorum(
chain_id: ChainId,
votes: impl IntoIterator<Item = (ValidatorPublicKey, linera_chain::data_types::LiteVote)>,
committee: &Committee,
) -> Option<(CryptoHash, Vec<(ValidatorPublicKey, ValidatorSignature)>)> {
let mut signatures_by_hash: HashMap<CryptoHash, Vec<(ValidatorPublicKey, ValidatorSignature)>> =
HashMap::new();
let mut weight_by_hash: HashMap<CryptoHash, u64> = HashMap::new();
for (public_key, vote) in votes {
if vote.value.chain_id != chain_id || vote.value.kind != CertificateKind::Confirmed {
continue;
}
let hash = vote.value.value_hash;
signatures_by_hash
.entry(hash)
.or_default()
.push((public_key, vote.signature));
let weight = weight_by_hash.entry(hash).or_insert(0);
*weight += committee.weight(&public_key);
if *weight >= committee.quorum_threshold() {
let signatures = signatures_by_hash
.remove(&hash)
.expect("just inserted above");
return Some((hash, signatures));
}
}
None
}
pub struct LiteBenchmarkClient<Env: Environment> {
chain_id: ChainId,
owner: AccountOwner,
inner: Mutex<LiteChainClient<Env>>,
process_messages: bool,
bundle_cap: Option<usize>,
}
impl<Env: Environment> LiteBenchmarkClient<Env> {
pub fn new(
client: LiteChainClient<Env>,
process_messages: bool,
bundle_cap: Option<usize>,
) -> Self {
Self {
chain_id: client.chain_id,
owner: client.owner,
inner: Mutex::new(client),
process_messages,
bundle_cap,
}
}
}
#[async_trait::async_trait]
impl<Env: Environment> BenchmarkClient for LiteBenchmarkClient<Env> {
fn chain_id(&self) -> ChainId {
self.chain_id
}
async fn owner(&self) -> Result<AccountOwner, BenchmarkError> {
Ok(self.owner)
}
async fn commit_operations(&self, operations: Vec<Operation>) -> Result<(), BenchmarkError> {
let bundle_cap = self
.bundle_cap
.unwrap_or_else(|| operations.len().saturating_mul(2));
self.inner
.lock()
.await
.propose_and_commit(operations, self.process_messages, bundle_cap)
.await
.map_err(|error| BenchmarkError::LiteClient(error.to_string()))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use linera_base::{
crypto::{AccountSecretKey, CryptoHash, ValidatorKeypair},
data_types::BlockHeight,
};
use linera_chain::data_types::{LiteValue, LiteVote, MessageAction, MessageBundle};
use super::*;
fn bundle(origin: ChainId, height: u64, index: u32) -> IncomingBundle {
IncomingBundle {
origin,
bundle: MessageBundle {
height: BlockHeight(height),
timestamp: Timestamp::from(0),
certificate_hash: CryptoHash::test_hash("cert"),
transaction_index: index,
messages: Vec::new(),
},
action: MessageAction::Accept,
}
}
fn cursors(bundles: &[IncomingBundle]) -> Vec<(ChainId, u64, u32)> {
let mut cursors: Vec<_> = bundles
.iter()
.map(|b| (b.origin, b.bundle.height.0, b.bundle.transaction_index))
.collect();
cursors.sort();
cursors
}
fn sorted(mut cursors: Vec<(ChainId, u64, u32)>) -> Vec<(ChainId, u64, u32)> {
cursors.sort();
cursors
}
#[test]
fn common_prefix_takes_the_agreed_per_origin_prefix() {
let a = ChainId(CryptoHash::test_hash("a"));
let b = ChainId(CryptoHash::test_hash("b"));
assert!(common_prefix_bundles(Vec::new()).is_empty());
let only = vec![bundle(a, 0, 0), bundle(a, 1, 0), bundle(b, 0, 0)];
assert_eq!(
cursors(&common_prefix_bundles(vec![only.clone()])),
sorted(vec![(a, 0, 0), (a, 1, 0), (b, 0, 0)]),
);
assert_eq!(
common_prefix_bundles(vec![only.clone(), only.clone()]).len(),
3
);
let ahead = vec![bundle(a, 0, 0), bundle(a, 1, 0), bundle(b, 0, 0)];
let behind = vec![bundle(a, 0, 0), bundle(b, 0, 0)];
assert_eq!(
cursors(&common_prefix_bundles(vec![ahead, behind])),
sorted(vec![(a, 0, 0), (b, 0, 0)]),
);
let left = vec![bundle(a, 0, 0), bundle(a, 1, 0), bundle(a, 2, 0)];
let right = vec![bundle(a, 0, 0), bundle(a, 5, 0), bundle(a, 2, 0)];
assert_eq!(
cursors(&common_prefix_bundles(vec![left, right])),
vec![(a, 0, 0)],
);
let with_b = vec![bundle(a, 0, 0), bundle(b, 0, 0)];
let without_b = vec![bundle(a, 0, 0)];
assert_eq!(
cursors(&common_prefix_bundles(vec![with_b, without_b])),
vec![(a, 0, 0)],
);
}
fn committee_of(size: usize) -> (Committee, Vec<ValidatorPublicKey>) {
let keys: Vec<_> = (0..size)
.map(|_| {
(
ValidatorKeypair::generate().public_key,
AccountSecretKey::generate().public(),
)
})
.collect();
let public_keys = keys.iter().map(|(key, _)| *key).collect();
(Committee::make_simple(keys), public_keys)
}
fn vote(chain_id: ChainId, value_hash: CryptoHash) -> LiteVote {
LiteVote {
value: LiteValue {
value_hash,
chain_id,
kind: CertificateKind::Confirmed,
},
round: Round::Fast,
signature: ValidatorSignature::sign_prehash(
&ValidatorKeypair::generate().secret_key,
value_hash,
),
}
}
#[test]
fn quorum_is_reached_once_enough_weight_agrees() {
let chain_id = ChainId(CryptoHash::test_hash("chain"));
let value_hash = CryptoHash::test_hash("confirmed-block");
let (committee, keys) = committee_of(4);
let votes = keys[..2]
.iter()
.map(|key| (*key, vote(chain_id, value_hash)));
assert!(find_confirming_quorum(chain_id, votes, &committee).is_none());
let votes = keys[..3]
.iter()
.map(|key| (*key, vote(chain_id, value_hash)));
let (hash, signatures) = find_confirming_quorum(chain_id, votes, &committee)
.expect("3 out of 4 equally-weighted validators should reach the quorum threshold");
assert_eq!(hash, value_hash);
assert_eq!(signatures.len(), 3);
}
#[test]
fn votes_for_a_different_chain_are_ignored() {
let chain_id = ChainId(CryptoHash::test_hash("chain"));
let other_chain_id = ChainId(CryptoHash::test_hash("other-chain"));
let value_hash = CryptoHash::test_hash("confirmed-block");
let (committee, keys) = committee_of(4);
let votes = keys
.iter()
.map(|key| (*key, vote(other_chain_id, value_hash)));
assert!(find_confirming_quorum(chain_id, votes, &committee).is_none());
}
#[test]
fn a_split_vote_never_reaches_quorum_on_either_side() {
let chain_id = ChainId(CryptoHash::test_hash("chain"));
let hash_a = CryptoHash::test_hash("block-a");
let hash_b = CryptoHash::test_hash("block-b");
let (committee, keys) = committee_of(4);
let votes = vec![
(keys[0], vote(chain_id, hash_a)),
(keys[1], vote(chain_id, hash_a)),
(keys[2], vote(chain_id, hash_b)),
(keys[3], vote(chain_id, hash_b)),
];
assert!(find_confirming_quorum(chain_id, votes, &committee).is_none());
}
}