use std::{borrow::Cow, collections::HashSet, fmt};
use async_graphql::SimpleObject;
use linera_base::{
crypto::{BcsHashable, BcsSignable, CryptoError, CryptoHash, KeyPair, PublicKey, Signature},
data_types::{Amount, Blob, BlockHeight, OracleResponse, Round, Timestamp},
doc_scalar, ensure,
identifiers::{
Account, BlobId, ChainId, ChannelName, Destination, GenericApplicationId, MessageId, Owner,
StreamId,
},
};
use linera_execution::{
committee::{Committee, Epoch, ValidatorName},
system::OpenChainConfig,
Message, MessageKind, Operation, SystemOperation,
};
use serde::{de::Deserializer, Deserialize, Serialize};
use crate::ChainError;
#[cfg(test)]
#[path = "unit_tests/data_types_tests.rs"]
mod data_types_tests;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct Block {
pub chain_id: ChainId,
pub epoch: Epoch,
pub incoming_bundles: Vec<IncomingBundle>,
pub operations: Vec<Operation>,
pub height: BlockHeight,
pub timestamp: Timestamp,
pub authenticated_signer: Option<Owner>,
pub previous_block_hash: Option<CryptoHash>,
}
impl Block {
pub fn published_blob_ids(&self) -> HashSet<BlobId> {
let mut blob_ids = HashSet::new();
for operation in &self.operations {
if let Operation::System(SystemOperation::PublishDataBlob { blob_hash }) = operation {
blob_ids.insert(BlobId::new_data_from_hash(*blob_hash));
}
if let Operation::System(SystemOperation::PublishBytecode { bytecode_id }) = operation {
blob_ids.extend([
BlobId::new_contract_bytecode_from_hash(bytecode_id.contract_blob_hash),
BlobId::new_service_bytecode_from_hash(bytecode_id.service_blob_hash),
]);
}
}
blob_ids
}
pub fn has_only_rejected_messages(&self) -> bool {
self.operations.is_empty()
&& self
.incoming_bundles
.iter()
.all(|message| message.action == MessageAction::Reject)
}
pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
self.incoming_bundles
.iter()
.flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
}
pub fn message_count(&self) -> usize {
self.incoming_bundles
.iter()
.map(|im| im.bundle.messages.len())
.sum()
}
pub fn transactions(&self) -> impl Iterator<Item = (u32, Transaction<'_>)> {
let bundles = self
.incoming_bundles
.iter()
.map(Transaction::ReceiveMessages);
let operations = self.operations.iter().map(Transaction::ExecuteOperation);
(0u32..).zip(bundles.chain(operations))
}
pub fn starts_with_open_chain_message(
&self,
) -> Option<(&IncomingBundle, &PostedMessage, &OpenChainConfig)> {
let in_bundle = self.incoming_bundles.first()?;
if in_bundle.action != MessageAction::Accept {
return None;
}
let posted_message = in_bundle.bundle.messages.first()?;
let config = posted_message.message.matches_open_chain()?;
Some((in_bundle, posted_message, config))
}
}
#[derive(Debug, Clone)]
pub enum Transaction<'a> {
ReceiveMessages(&'a IncomingBundle),
ExecuteOperation(&'a Operation),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, SimpleObject)]
pub struct ChainAndHeight {
pub chain_id: ChainId,
pub height: BlockHeight,
}
impl ChainAndHeight {
pub fn to_message_id(&self, index: u32) -> MessageId {
MessageId {
chain_id: self.chain_id,
height: self.height,
index,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct IncomingBundle {
pub origin: Origin,
pub bundle: MessageBundle,
pub action: MessageAction,
}
impl IncomingBundle {
pub fn messages_and_ids(&self) -> impl Iterator<Item = (MessageId, &PostedMessage)> {
let chain_and_height = ChainAndHeight {
chain_id: self.origin.sender,
height: self.bundle.height,
};
let messages = self.bundle.messages.iter();
messages.map(move |posted_message| {
let message_id = chain_and_height.to_message_id(posted_message.index);
(message_id, posted_message)
})
}
}
#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub enum MessageAction {
Accept,
Reject,
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Serialize, Deserialize)]
pub struct Origin {
pub sender: ChainId,
pub medium: Medium,
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Serialize, Deserialize)]
pub struct Target {
pub recipient: ChainId,
pub medium: Medium,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)]
pub struct MessageBundle {
pub height: BlockHeight,
pub timestamp: Timestamp,
pub certificate_hash: CryptoHash,
pub transaction_index: u32,
pub messages: Vec<PostedMessage>,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct ChannelFullName {
pub application_id: GenericApplicationId,
pub name: ChannelName,
}
impl fmt::Display for ChannelFullName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = hex::encode(&self.name);
match self.application_id {
GenericApplicationId::System => write!(f, "system channel {name}"),
GenericApplicationId::User(app_id) => write!(f, "user channel {name} for app {app_id}"),
}
}
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Serialize, Deserialize)]
pub enum Medium {
Direct,
Channel(ChannelFullName),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(with_testing, derive(Eq, PartialEq))]
pub struct BlockProposal {
pub content: ProposalContent,
pub owner: Owner,
pub signature: Signature,
pub blobs: Vec<Blob>,
pub validated_block_certificate: Option<LiteCertificate<'static>>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct OutgoingMessage {
pub destination: Destination,
pub authenticated_signer: Option<Owner>,
pub grant: Amount,
pub refund_grant_to: Option<Account>,
pub kind: MessageKind,
pub message: Message,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct PostedMessage {
pub authenticated_signer: Option<Owner>,
pub grant: Amount,
pub refund_grant_to: Option<Account>,
pub kind: MessageKind,
pub index: u32,
pub message: Message,
}
impl OutgoingMessage {
pub fn has_destination(&self, medium: &Medium, recipient: ChainId) -> bool {
match (&self.destination, medium) {
(Destination::Recipient(_), Medium::Channel(_))
| (Destination::Subscribers(_), Medium::Direct) => false,
(Destination::Recipient(id), Medium::Direct) => *id == recipient,
(
Destination::Subscribers(dest_name),
Medium::Channel(ChannelFullName {
application_id,
name,
}),
) => *application_id == self.message.application_id() && name == dest_name,
}
}
pub fn into_posted(self, index: u32) -> PostedMessage {
let OutgoingMessage {
destination: _,
authenticated_signer,
grant,
refund_grant_to,
kind,
message,
} = self;
PostedMessage {
authenticated_signer,
grant,
refund_grant_to,
kind,
index,
message,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct ExecutedBlock {
pub block: Block,
pub outcome: BlockExecutionOutcome,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
#[cfg_attr(with_testing, derive(Default))]
pub struct BlockExecutionOutcome {
pub messages: Vec<Vec<OutgoingMessage>>,
pub state_hash: CryptoHash,
pub oracle_responses: Vec<Vec<OracleResponse>>,
pub events: Vec<Vec<EventRecord>>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct EventRecord {
pub stream_id: StreamId,
pub key: Vec<u8>,
pub value: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize, Serialize)]
pub enum CertificateValue {
ValidatedBlock {
executed_block: ExecutedBlock,
},
ConfirmedBlock {
executed_block: ExecutedBlock,
},
Timeout {
chain_id: ChainId,
height: BlockHeight,
epoch: Epoch,
},
}
#[async_graphql::Object(cache_control(no_cache))]
impl CertificateValue {
#[graphql(derived(name = "executed_block"))]
async fn _executed_block(&self) -> Option<ExecutedBlock> {
self.executed_block().cloned()
}
async fn status(&self) -> String {
match self {
CertificateValue::ValidatedBlock { .. } => "validated".to_string(),
CertificateValue::ConfirmedBlock { .. } => "confirmed".to_string(),
CertificateValue::Timeout { .. } => "timeout".to_string(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct HashedCertificateValue {
value: CertificateValue,
hash: CryptoHash,
}
#[async_graphql::Object(cache_control(no_cache))]
impl HashedCertificateValue {
#[graphql(derived(name = "hash"))]
async fn _hash(&self) -> CryptoHash {
self.hash
}
#[graphql(derived(name = "value"))]
async fn _value(&self) -> CertificateValue {
self.value.clone()
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct LiteValue {
pub value_hash: CryptoHash,
pub chain_id: ChainId,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
struct ValueHashAndRound(CryptoHash, Round);
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Vote {
pub value: HashedCertificateValue,
pub round: Round,
pub validator: ValidatorName,
pub signature: Signature,
}
impl Vote {
pub fn new(value: HashedCertificateValue, round: Round, key_pair: &KeyPair) -> Self {
let hash_and_round = ValueHashAndRound(value.hash, round);
let signature = Signature::new(&hash_and_round, key_pair);
Self {
value,
round,
validator: ValidatorName(key_pair.public()),
signature,
}
}
pub fn lite(&self) -> LiteVote {
LiteVote {
value: self.value.lite(),
round: self.round,
validator: self.validator,
signature: self.signature,
}
}
pub fn value(&self) -> &CertificateValue {
self.value.inner()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(with_testing, derive(Eq, PartialEq))]
pub struct LiteVote {
pub value: LiteValue,
pub round: Round,
pub validator: ValidatorName,
pub signature: Signature,
}
impl LiteVote {
pub fn with_value(self, value: HashedCertificateValue) -> Option<Vote> {
if self.value != value.lite() {
return None;
}
Some(Vote {
value,
round: self.round,
validator: self.validator,
signature: self.signature,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(with_testing, derive(Eq, PartialEq))]
pub struct LiteCertificate<'a> {
pub value: LiteValue,
pub round: Round,
pub signatures: Cow<'a, [(ValidatorName, Signature)]>,
}
impl<'a> LiteCertificate<'a> {
pub fn new(
value: LiteValue,
round: Round,
mut signatures: Vec<(ValidatorName, Signature)>,
) -> Self {
if !is_strictly_ordered(&signatures) {
signatures.sort_by_key(|&(validator_name, _)| validator_name)
}
let signatures = Cow::Owned(signatures);
Self {
value,
round,
signatures,
}
}
pub fn try_from_votes(votes: impl IntoIterator<Item = LiteVote>) -> Option<Self> {
let mut votes = votes.into_iter();
let LiteVote {
value,
round,
validator,
signature,
} = votes.next()?;
let mut signatures = vec![(validator, signature)];
for vote in votes {
if vote.value.value_hash != value.value_hash || vote.round != round {
return None;
}
signatures.push((vote.validator, vote.signature));
}
Some(LiteCertificate::new(value, round, signatures))
}
pub fn check(&self, committee: &Committee) -> Result<&LiteValue, ChainError> {
check_signatures(&self.value, self.round, &self.signatures, committee)?;
Ok(&self.value)
}
pub fn with_value(self, value: HashedCertificateValue) -> Option<Certificate> {
if self.value.chain_id != value.inner().chain_id() || self.value.value_hash != value.hash()
{
return None;
}
Some(Certificate {
value,
round: self.round,
signatures: self.signatures.into_owned(),
})
}
pub fn cloned(&self) -> LiteCertificate<'static> {
LiteCertificate {
value: self.value.clone(),
round: self.round,
signatures: Cow::Owned(self.signatures.clone().into_owned()),
}
}
}
#[derive(Clone, Debug, Serialize)]
#[cfg_attr(with_testing, derive(Eq, PartialEq))]
pub struct Certificate {
pub value: HashedCertificateValue,
pub round: Round,
signatures: Vec<(ValidatorName, Signature)>,
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.medium {
Medium::Direct => write!(f, "{:.8} (direct)", self.sender),
Medium::Channel(full_name) => write!(f, "{:.8} via {full_name:.8}", self.sender),
}
}
}
impl Origin {
pub fn chain(sender: ChainId) -> Self {
Self {
sender,
medium: Medium::Direct,
}
}
pub fn channel(sender: ChainId, name: ChannelFullName) -> Self {
Self {
sender,
medium: Medium::Channel(name),
}
}
}
impl Target {
pub fn chain(recipient: ChainId) -> Self {
Self {
recipient,
medium: Medium::Direct,
}
}
pub fn channel(recipient: ChainId, name: ChannelFullName) -> Self {
Self {
recipient,
medium: Medium::Channel(name),
}
}
}
impl Serialize for HashedCertificateValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.value.serialize(serializer)
}
}
impl<'a> Deserialize<'a> for HashedCertificateValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'a>,
{
Ok(CertificateValue::deserialize(deserializer)?.into())
}
}
impl From<CertificateValue> for HashedCertificateValue {
fn from(value: CertificateValue) -> HashedCertificateValue {
value.with_hash()
}
}
impl From<HashedCertificateValue> for CertificateValue {
fn from(hv: HashedCertificateValue) -> CertificateValue {
hv.value
}
}
impl CertificateValue {
pub fn chain_id(&self) -> ChainId {
match self {
CertificateValue::ConfirmedBlock { executed_block, .. }
| CertificateValue::ValidatedBlock { executed_block, .. } => {
executed_block.block.chain_id
}
CertificateValue::Timeout { chain_id, .. } => *chain_id,
}
}
pub fn height(&self) -> BlockHeight {
match self {
CertificateValue::ConfirmedBlock { executed_block, .. }
| CertificateValue::ValidatedBlock { executed_block, .. } => {
executed_block.block.height
}
CertificateValue::Timeout { height, .. } => *height,
}
}
pub fn epoch(&self) -> Epoch {
match self {
CertificateValue::ConfirmedBlock { executed_block, .. }
| CertificateValue::ValidatedBlock { executed_block, .. } => executed_block.block.epoch,
CertificateValue::Timeout { epoch, .. } => *epoch,
}
}
pub fn with_hash_checked(self, hash: CryptoHash) -> Result<HashedCertificateValue, ChainError> {
let hashed_certificate_value = self.with_hash();
ensure!(
hashed_certificate_value.hash == hash,
ChainError::CertificateValueHashMismatch {
expected: hash,
actual: hashed_certificate_value.hash
}
);
Ok(hashed_certificate_value)
}
pub fn with_hash(self) -> HashedCertificateValue {
let hash = CryptoHash::new(&self);
HashedCertificateValue { value: self, hash }
}
pub fn with_hash_unchecked(self, hash: CryptoHash) -> HashedCertificateValue {
HashedCertificateValue { value: self, hash }
}
pub fn has_message(&self, message_id: &MessageId) -> bool {
let Some(executed_block) = self.executed_block() else {
return false;
};
let Ok(index) = usize::try_from(message_id.index) else {
return false;
};
self.height() == message_id.height
&& self.chain_id() == message_id.chain_id
&& executed_block.messages().len() > index
}
pub fn is_confirmed(&self) -> bool {
matches!(self, CertificateValue::ConfirmedBlock { .. })
}
pub fn is_validated(&self) -> bool {
matches!(self, CertificateValue::ValidatedBlock { .. })
}
pub fn is_timeout(&self) -> bool {
matches!(self, CertificateValue::Timeout { .. })
}
#[cfg(with_testing)]
pub fn messages(&self) -> Option<&Vec<Vec<OutgoingMessage>>> {
Some(self.executed_block()?.messages())
}
pub fn executed_block(&self) -> Option<&ExecutedBlock> {
match self {
CertificateValue::ConfirmedBlock { executed_block, .. }
| CertificateValue::ValidatedBlock { executed_block, .. } => Some(executed_block),
CertificateValue::Timeout { .. } => None,
}
}
pub fn block(&self) -> Option<&Block> {
self.executed_block()
.map(|executed_block| &executed_block.block)
}
pub fn to_log_str(&self) -> &'static str {
match self {
CertificateValue::ConfirmedBlock { .. } => "confirmed_block",
CertificateValue::ValidatedBlock { .. } => "validated_block",
CertificateValue::Timeout { .. } => "timeout",
}
}
}
impl MessageBundle {
pub fn is_skippable(&self) -> bool {
self.messages.iter().all(PostedMessage::is_skippable)
}
pub fn is_tracked(&self) -> bool {
let mut tracked = false;
for posted_message in &self.messages {
match posted_message.kind {
MessageKind::Simple | MessageKind::Bouncing => {}
MessageKind::Protected => return false,
MessageKind::Tracked => tracked = true,
}
}
tracked
}
pub fn is_protected(&self) -> bool {
self.messages.iter().any(PostedMessage::is_protected)
}
pub fn goes_to_inbox(&self) -> bool {
self.messages
.iter()
.any(|posted_message| posted_message.message.goes_to_inbox())
}
}
impl PostedMessage {
pub fn is_skippable(&self) -> bool {
use MessageKind::*;
match self.kind {
Protected | Tracked => false,
Simple | Bouncing => self.grant == Amount::ZERO,
}
}
pub fn is_protected(&self) -> bool {
matches!(self.kind, MessageKind::Protected)
}
pub fn is_tracked(&self) -> bool {
matches!(self.kind, MessageKind::Tracked)
}
pub fn is_bouncing(&self) -> bool {
matches!(self.kind, MessageKind::Bouncing)
}
}
impl ExecutedBlock {
pub fn messages(&self) -> &Vec<Vec<OutgoingMessage>> {
&self.outcome.messages
}
pub fn message_id_for_operation(
&self,
operation_index: usize,
message_index: u32,
) -> Option<MessageId> {
let block = &self.block;
let transaction_index = block.incoming_bundles.len().checked_add(operation_index)?;
if message_index
>= u32::try_from(self.outcome.messages.get(transaction_index)?.len()).ok()?
{
return None;
}
let first_message_index = u32::try_from(
self.outcome
.messages
.iter()
.take(transaction_index)
.map(Vec::len)
.sum::<usize>(),
)
.ok()?;
let index = first_message_index.checked_add(message_index)?;
Some(self.message_id(index))
}
pub fn message_by_id(&self, message_id: &MessageId) -> Option<&OutgoingMessage> {
let MessageId {
chain_id,
height,
index,
} = message_id;
if self.block.chain_id != *chain_id || self.block.height != *height {
return None;
}
let mut index = usize::try_from(*index).ok()?;
for messages in self.messages() {
if let Some(message) = messages.get(index) {
return Some(message);
}
index -= messages.len();
}
None
}
pub fn message_id(&self, index: u32) -> MessageId {
MessageId {
chain_id: self.block.chain_id,
height: self.block.height,
index,
}
}
pub fn required_blob_ids(&self) -> HashSet<BlobId> {
self.outcome.required_blob_ids()
}
pub fn requires_blob(&self, blob_id: &BlobId) -> bool {
self.required_blob_ids().contains(blob_id)
}
}
impl BlockExecutionOutcome {
pub fn with(self, block: Block) -> ExecutedBlock {
ExecutedBlock {
block,
outcome: self,
}
}
pub fn required_blob_ids(&self) -> HashSet<BlobId> {
let mut required_blob_ids = HashSet::new();
for responses in &self.oracle_responses {
for response in responses {
if let OracleResponse::Blob(blob_id) = response {
required_blob_ids.insert(*blob_id);
}
}
}
required_blob_ids
}
}
impl HashedCertificateValue {
pub fn new_confirmed(executed_block: ExecutedBlock) -> HashedCertificateValue {
CertificateValue::ConfirmedBlock { executed_block }.into()
}
pub fn new_validated(executed_block: ExecutedBlock) -> HashedCertificateValue {
CertificateValue::ValidatedBlock { executed_block }.into()
}
pub fn new_timeout(
chain_id: ChainId,
height: BlockHeight,
epoch: Epoch,
) -> HashedCertificateValue {
CertificateValue::Timeout {
chain_id,
height,
epoch,
}
.into()
}
pub fn hash(&self) -> CryptoHash {
self.hash
}
pub fn lite(&self) -> LiteValue {
LiteValue {
value_hash: self.hash(),
chain_id: self.value.chain_id(),
}
}
pub fn validated_to_confirmed(&self) -> Option<HashedCertificateValue> {
match &self.value {
CertificateValue::ValidatedBlock { executed_block } => Some(
CertificateValue::ConfirmedBlock {
executed_block: executed_block.clone(),
}
.into(),
),
CertificateValue::ConfirmedBlock { .. } | CertificateValue::Timeout { .. } => None,
}
}
pub fn inner(&self) -> &CertificateValue {
&self.value
}
pub fn into_inner(self) -> CertificateValue {
self.value
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ProposalContent {
pub block: Block,
pub round: Round,
pub forced_oracle_responses: Option<Vec<Vec<OracleResponse>>>,
}
impl BlockProposal {
pub fn new_initial(round: Round, block: Block, secret: &KeyPair, blobs: Vec<Blob>) -> Self {
let content = ProposalContent {
round,
block,
forced_oracle_responses: None,
};
let signature = Signature::new(&content, secret);
Self {
content,
owner: secret.public().into(),
signature,
blobs,
validated_block_certificate: None,
}
}
pub fn new_retry(
round: Round,
validated_block_certificate: Certificate,
secret: &KeyPair,
blobs: Vec<Blob>,
) -> Self {
let lite_cert = validated_block_certificate.lite_certificate().cloned();
let CertificateValue::ValidatedBlock { executed_block } =
validated_block_certificate.value.into_inner()
else {
panic!("called new_retry with a certificate without a validated block");
};
let content = ProposalContent {
block: executed_block.block,
round,
forced_oracle_responses: Some(executed_block.outcome.oracle_responses),
};
let signature = Signature::new(&content, secret);
Self {
content,
owner: secret.public().into(),
signature,
blobs,
validated_block_certificate: Some(lite_cert),
}
}
pub fn check_signature(&self, public_key: PublicKey) -> Result<(), CryptoError> {
self.signature.check(&self.content, public_key)
}
}
impl LiteVote {
pub fn new(value: LiteValue, round: Round, key_pair: &KeyPair) -> Self {
let hash_and_round = ValueHashAndRound(value.value_hash, round);
let signature = Signature::new(&hash_and_round, key_pair);
Self {
value,
round,
validator: ValidatorName(key_pair.public()),
signature,
}
}
pub fn check(&self) -> Result<(), ChainError> {
let hash_and_round = ValueHashAndRound(self.value.value_hash, self.round);
Ok(self.signature.check(&hash_and_round, self.validator.0)?)
}
}
pub struct SignatureAggregator<'a> {
committee: &'a Committee,
weight: u64,
used_validators: HashSet<ValidatorName>,
partial: Certificate,
}
impl<'a> SignatureAggregator<'a> {
pub fn new(value: HashedCertificateValue, round: Round, committee: &'a Committee) -> Self {
Self {
committee,
weight: 0,
used_validators: HashSet::new(),
partial: Certificate {
value,
round,
signatures: Vec::new(),
},
}
}
pub fn append(
&mut self,
validator: ValidatorName,
signature: Signature,
) -> Result<Option<Certificate>, ChainError> {
let hash_and_round = ValueHashAndRound(self.partial.hash(), self.partial.round);
signature.check(&hash_and_round, validator.0)?;
ensure!(
!self.used_validators.contains(&validator),
ChainError::CertificateValidatorReuse
);
self.used_validators.insert(validator);
let voting_rights = self.committee.weight(&validator);
ensure!(voting_rights > 0, ChainError::InvalidSigner);
self.weight += voting_rights;
self.partial.add_signature((validator, signature));
if self.weight >= self.committee.quorum_threshold() {
self.weight = 0; Ok(Some(self.partial.clone()))
} else {
Ok(None)
}
}
}
fn is_strictly_ordered(values: &[(ValidatorName, Signature)]) -> bool {
values.windows(2).all(|pair| pair[0].0 < pair[1].0)
}
impl<'de> Deserialize<'de> for Certificate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Debug, Deserialize)]
#[serde(rename = "Certificate")]
struct CertificateHelper {
value: HashedCertificateValue,
round: Round,
signatures: Vec<(ValidatorName, Signature)>,
}
let helper: CertificateHelper = Deserialize::deserialize(deserializer)?;
if !is_strictly_ordered(&helper.signatures) {
Err(serde::de::Error::custom("Vector is not strictly sorted"))
} else {
Ok(Self {
value: helper.value,
round: helper.round,
signatures: helper.signatures,
})
}
}
}
impl Certificate {
pub fn new(
value: HashedCertificateValue,
round: Round,
mut signatures: Vec<(ValidatorName, Signature)>,
) -> Self {
if !is_strictly_ordered(&signatures) {
signatures.sort_by_key(|&(validator_name, _)| validator_name)
}
Self {
value,
round,
signatures,
}
}
pub fn signatures(&self) -> &Vec<(ValidatorName, Signature)> {
&self.signatures
}
pub fn add_signature(
&mut self,
signature: (ValidatorName, Signature),
) -> &Vec<(ValidatorName, Signature)> {
let index = self
.signatures
.binary_search_by(|(name, _)| name.cmp(&signature.0))
.unwrap_or_else(std::convert::identity);
self.signatures.insert(index, signature);
&self.signatures
}
pub fn check<'a>(
&'a self,
committee: &Committee,
) -> Result<&'a HashedCertificateValue, ChainError> {
check_signatures(&self.lite_value(), self.round, &self.signatures, committee)?;
Ok(&self.value)
}
pub fn lite_certificate(&self) -> LiteCertificate<'_> {
LiteCertificate {
value: self.lite_value(),
round: self.round,
signatures: Cow::Borrowed(&self.signatures),
}
}
pub fn lite_value(&self) -> LiteValue {
LiteValue {
value_hash: self.hash(),
chain_id: self.value().chain_id(),
}
}
pub fn value(&self) -> &CertificateValue {
&self.value.value
}
pub fn hash(&self) -> CryptoHash {
self.value.hash
}
pub fn is_signed_by(&self, validator_name: &ValidatorName) -> bool {
self.signatures
.binary_search_by(|(name, _)| name.cmp(validator_name))
.is_ok()
}
pub fn message_bundles_for<'a>(
&'a self,
medium: &'a Medium,
recipient: ChainId,
) -> impl Iterator<Item = (Epoch, MessageBundle)> + 'a {
let mut index = 0u32;
let maybe_executed_block = self.value().executed_block().into_iter();
maybe_executed_block.flat_map(move |executed_block| {
(0u32..).zip(executed_block.messages()).filter_map(
move |(transaction_index, txn_messages)| {
let messages = (index..)
.zip(txn_messages)
.filter(|(_, message)| message.has_destination(medium, recipient))
.map(|(idx, message)| message.clone().into_posted(idx))
.collect::<Vec<_>>();
index += txn_messages.len() as u32;
(!messages.is_empty()).then(|| {
let bundle = MessageBundle {
height: executed_block.block.height,
timestamp: executed_block.block.timestamp,
certificate_hash: self.hash(),
transaction_index,
messages,
};
(executed_block.block.epoch, bundle)
})
},
)
})
}
pub fn requires_blob(&self, blob_id: &BlobId) -> bool {
self.value()
.executed_block()
.is_some_and(|executed_block| executed_block.requires_blob(blob_id))
}
#[cfg(with_testing)]
pub fn outgoing_message_count(&self) -> usize {
let Some(executed_block) = self.value().executed_block() else {
return 0;
};
executed_block.messages().iter().map(Vec::len).sum()
}
}
fn check_signatures(
value: &LiteValue,
round: Round,
signatures: &[(ValidatorName, Signature)],
committee: &Committee,
) -> Result<(), ChainError> {
let mut weight = 0;
let mut used_validators = HashSet::new();
for (validator, _) in signatures {
ensure!(
!used_validators.contains(validator),
ChainError::CertificateValidatorReuse
);
used_validators.insert(*validator);
let voting_rights = committee.weight(validator);
ensure!(voting_rights > 0, ChainError::InvalidSigner);
weight += voting_rights;
}
ensure!(
weight >= committee.quorum_threshold(),
ChainError::CertificateRequiresQuorum
);
let hash_and_round = ValueHashAndRound(value.value_hash, round);
Signature::verify_batch(&hash_and_round, signatures.iter().map(|(v, s)| (&v.0, s)))?;
Ok(())
}
impl BcsSignable for ProposalContent {}
impl BcsSignable for ValueHashAndRound {}
impl BcsHashable for CertificateValue {}
doc_scalar!(
MessageAction,
"Whether an incoming message is accepted or rejected"
);
doc_scalar!(
ChannelFullName,
"A channel name together with its application ID"
);
doc_scalar!(
MessageBundle,
"A set of messages from a single block, for a single destination."
);
doc_scalar!(
Medium,
"The origin of a message coming from a particular chain. Used to identify each inbox."
);
doc_scalar!(
Origin,
"The origin of a message, relative to a particular application. Used to identify each inbox."
);
doc_scalar!(
Target,
"The target of a message, relative to a particular application. Used to identify each outbox."
);