Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions linera-base/src/hashed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ impl<T> Hashed<T> {
Self { value, hash }
}

/// Creates a [`Hashed`] from a value and a precomputed hash, without recomputing it.
///
/// The caller is responsible for the hash being the canonical hash of `value`.
pub fn with_hash(value: T, hash: CryptoHash) -> Self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call it new_unchecked or with_hash_unchecked

@ma2bd ma2bd Jun 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit wrong to use Hashed in the first place if you need this method but maybe that's fine. After all, there is no verify method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that whatever is inside can have a hash computed and it's memoized.

@ma2bd ma2bd Jun 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. In that case, perhaps we want the container to be agnostic then: new should be rename with_crypto_hash and the manual hash (yours) with_unchecked_hash.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The container is agnostic – it accept any type as long as it implements BcsHashable. It's a textbook smart constructor. I will rename the new method to with_unchecked_hash.

Self { value, hash }
}

/// Returns the hash.
pub fn hash(&self) -> CryptoHash {
self.hash
Expand Down Expand Up @@ -99,3 +106,23 @@ impl<T> PartialEq for Hashed<T> {
}

impl<T> Eq for Hashed<T> {}

#[cfg(test)]
mod tests {
use crate::{
crypto::{BcsHashable, CryptoHash},
hashed::Hashed,
};

#[derive(serde::Serialize, serde::Deserialize)]
struct Dummy(u8);
impl BcsHashable<'_> for Dummy {}

#[test]
fn with_hash_stores_provided_hash() {
let forced = CryptoHash::from([9u8; 32]);
let hashed = Hashed::with_hash(Dummy(7), forced);
assert_eq!(hashed.hash(), forced);
assert_ne!(hashed.hash(), CryptoHash::new(&Dummy(7)));
}
}
1 change: 1 addition & 0 deletions linera-chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ anyhow = { workspace = true, optional = true }
async-graphql.workspace = true
axum = { workspace = true, optional = true }
custom_debug_derive.workspace = true
derive_more = { workspace = true, features = ["from"] }
futures.workspace = true
linera-base.workspace = true
linera-execution.workspace = true
Expand Down
86 changes: 79 additions & 7 deletions linera-chain/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,26 @@ use crate::{
};

/// Wrapper around a `Block` that has been validated.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Allocative)]
#[serde(transparent)]
#[derive(Debug, PartialEq, Eq, Clone, Allocative)]
pub struct ValidatedBlock(Hashed<Block>);

impl Serialize for ValidatedBlock {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for ValidatedBlock {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new(Block::deserialize(deserializer)?))
}
}

impl ValidatedBlock {
/// Creates a new `ValidatedBlock` from a `Block`.
pub fn new(block: Block) -> Self {
Self(Hashed::new(block))
let hash = block.hash();
Self(Hashed::with_hash(block, hash))
}

pub fn from_hashed(block: Hashed<Block>) -> Self {
Expand Down Expand Up @@ -75,10 +87,21 @@ impl ValidatedBlock {
}

/// Wrapper around a `Block` that has been confirmed.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Allocative)]
#[serde(transparent)]
#[derive(Debug, PartialEq, Eq, Clone, Allocative)]
pub struct ConfirmedBlock(Hashed<Block>);

impl Serialize for ConfirmedBlock {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for ConfirmedBlock {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new(Block::deserialize(deserializer)?))
}
}

#[async_graphql::Object(cache_control(no_cache))]
impl ConfirmedBlock {
#[graphql(derived(name = "block"))]
Expand All @@ -97,7 +120,8 @@ impl ConfirmedBlock {

impl ConfirmedBlock {
pub fn new(block: Block) -> Self {
Self(Hashed::new(block))
let hash = block.hash();
Self(Hashed::with_hash(block, hash))
}

pub fn from_hashed(block: Hashed<Block>) -> Self {
Expand Down Expand Up @@ -463,6 +487,11 @@ impl Block {
Self { header, body }
}

/// Returns the hash of this block, which commits to the entire block via its header.
pub fn hash(&self) -> CryptoHash {
CryptoHash::new(&self.header)
}

/// Returns the bundles of messages sent via the given medium to the specified
/// recipient. Messages originating from different transactions of the original block
/// are kept in separate bundles. If the medium is a channel, does not verify that the
Expand Down Expand Up @@ -671,7 +700,50 @@ impl Block {
}
}

impl BcsHashable<'_> for Block {}
/// A single field of a [`BlockBody`], paired with enough data to recompute its hash and
/// check it against the matching hash in a [`BlockHeader`]. This lets a holder of a header
/// prove that one body field belongs to the block without the rest of the body.
#[derive(derive_more::From)]
pub enum BlockBodyField {
Transactions(Vec<Transaction>),
Messages(Vec<Vec<OutgoingMessage>>),
PreviousMessageBlocks(BTreeMap<ChainId, (CryptoHash, BlockHeight)>),
PreviousEventBlocks(BTreeMap<StreamId, (CryptoHash, BlockHeight)>),
OracleResponses(Vec<Vec<OracleResponse>>),
Events(Vec<Vec<Event>>),
Blobs(Vec<Vec<Blob>>),
OperationResults(Vec<OperationResult>),
}

impl BlockHeader {
/// Returns whether `field` is the body field this header commits to.
pub fn verifies(&self, field: impl Into<BlockBodyField>) -> bool {
match field.into() {
BlockBodyField::Transactions(v) => hashing::hash_vec(v) == self.transactions_hash,
BlockBodyField::Messages(v) => hashing::hash_vec_vec(v) == self.messages_hash,
BlockBodyField::PreviousMessageBlocks(m) => {
CryptoHash::new(&PreviousMessageBlocksMap {
inner: Cow::Owned(m),
}) == self.previous_message_blocks_hash
}
BlockBodyField::PreviousEventBlocks(m) => {
CryptoHash::new(&PreviousEventBlocksMap {
inner: Cow::Owned(m),
}) == self.previous_event_blocks_hash
}
BlockBodyField::OracleResponses(v) => {
hashing::hash_vec_vec(v) == self.oracle_responses_hash
}
BlockBodyField::Events(v) => hashing::hash_vec_vec(v) == self.events_hash,
BlockBodyField::Blobs(v) => hashing::hash_vec_vec(v) == self.blobs_hash,
BlockBodyField::OperationResults(v) => {
hashing::hash_vec(v) == self.operation_results_hash
}
}
}
}

impl BcsHashable<'_> for BlockHeader {}

#[derive(Serialize, Deserialize)]
pub struct PreviousMessageBlocksMap<'a> {
Expand Down
96 changes: 90 additions & 6 deletions linera-chain/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@

mod http_server;

use std::collections::BTreeMap;

use linera_base::{
crypto::{AccountPublicKey, Signer, ValidatorPublicKey},
data_types::{Amount, BlockHeight, Epoch, Round, Timestamp},
crypto::{AccountPublicKey, CryptoHash, Signer, ValidatorPublicKey},
data_types::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
identifiers::{Account, AccountOwner, ChainId},
};
use linera_execution::{
committee::{Committee, ValidatorState},
Message, MessageKind, Operation, ResourceControlPolicy, SystemOperation,
Message, MessageKind, Operation, OutgoingMessage, ResourceControlPolicy, SystemOperation,
};

pub use self::http_server::HttpServer;
use crate::{
block::ConfirmedBlock,
block::{Block, ConfirmedBlock},
data_types::{
BlockProposal, IncomingBundle, PostedMessage, ProposedBlock, SignatureAggregator,
Transaction, Vote,
BlockExecutionOutcome, BlockProposal, IncomingBundle, OperationResult, PostedMessage,
ProposedBlock, SignatureAggregator, Transaction, Vote,
},
types::{CertificateValue, GenericCertificate},
};
Expand Down Expand Up @@ -52,6 +54,88 @@ pub fn make_first_block(chain_id: ChainId) -> ProposedBlock {
}
}

/// Builds a [`Block`] for tests with a header that stays consistent with its body (the
/// header is computed via [`Block::new`]), so it round-trips through serialization and
/// storage. Tests start from an empty body and add messages, events, and so on.
pub struct BlockBuilder {
block: ProposedBlock,
outcome: BlockExecutionOutcome,
}

impl BlockBuilder {
/// Starts building a block at the given chain and height with an empty body.
pub fn new(chain_id: ChainId, height: BlockHeight) -> Self {
BlockBuilder {
block: ProposedBlock {
epoch: Epoch::ZERO,
chain_id,
transactions: vec![],
previous_block_hash: None,
height,
authenticated_owner: None,
timestamp: Timestamp::default(),
},
outcome: BlockExecutionOutcome {
state_hash: CryptoHash::default(),
messages: vec![],
previous_message_blocks: BTreeMap::new(),
previous_event_blocks: BTreeMap::new(),
oracle_responses: vec![],
events: vec![],
blobs: vec![],
operation_results: vec![],
},
}
}

/// Sets the execution state hash recorded in the header.
pub fn with_state_hash(mut self, state_hash: CryptoHash) -> Self {
self.outcome.state_hash = state_hash;
self
}

/// Appends a transaction to the block's inputs.
pub fn with_transaction(mut self, transaction: Transaction) -> Self {
self.block.transactions.push(transaction);
self
}

/// Appends one transaction's outgoing messages to the body.
pub fn with_messages(mut self, messages: Vec<OutgoingMessage>) -> Self {
self.outcome.messages.push(messages);
self
}

/// Appends one transaction's events to the body.
pub fn with_events(mut self, events: Vec<Event>) -> Self {
self.outcome.events.push(events);
self
}

/// Appends one transaction's oracle responses to the body.
pub fn with_oracle_responses(mut self, oracle_responses: Vec<OracleResponse>) -> Self {
self.outcome.oracle_responses.push(oracle_responses);
self
}

/// Appends one transaction's created blobs to the body.
pub fn with_blobs(mut self, blobs: Vec<Blob>) -> Self {
self.outcome.blobs.push(blobs);
self
}

/// Appends an operation result to the body.
pub fn with_operation_result(mut self, operation_result: OperationResult) -> Self {
self.outcome.operation_results.push(operation_result);
self
}

/// Builds the block, computing a header that is consistent with the body.
pub fn build(self) -> Block {
self.outcome.with(self.block)
}
}

/// A helper trait to simplify constructing blocks for tests.
#[allow(async_fn_in_trait)]
pub trait BlockTestExt: Sized {
Expand Down
Loading
Loading