Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Block structure #3101

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

New Block structure #3101

wants to merge 22 commits into from

Conversation

deuszx
Copy link
Contributor

@deuszx deuszx commented Jan 8, 2025

Motivation

Verifying Linera (micro)chains with a light client requires following the chain's blocks and subsequently confirming whether an event, a message, state change, were part of that block. Our current block definition - ExecutedBlock type - was not made for this purpose:

  • there is no distinction between block's "metadata" and the "body" - which would allow for light verification of the blocks.
  • the events/messages/operations are all layed out, flat, in the BlockExecutionOutcome w/o a way to easily verify whether a particular event is part of that set other than sending the whole vector.

Proposal

In this PR we introduce the new block structures. The new Block type consists of two elements:

  • (block) header
  • (block) body.

BlockHeader is a succint representation of the chain's new block. It contains metadata that suffices to track the chain's progress:

pub struct BlockHeader {
    /// The block version.
    pub version: u8, // TODO: More granular versioning. #3078
    /// The chain to which this block belongs.
    pub chain_id: ChainId,
    /// The number identifying the current configuration.
    pub epoch: Epoch,
    /// The block height.
    pub height: BlockHeight,
    /// The timestamp when this block was created.
    pub timestamp: Timestamp,
    /// The hash of the chain's execution state after this block.
    pub state_hash: CryptoHash,
    /// Certified hash of the previous block in the
    /// chain, if any.
    pub previous_block_hash: Option<CryptoHash>,
    /// The user signing for the operations in the block and paying for their execution
    /// fees. If set, this must be the `owner` in the block proposal. `None` means that
    /// the default account of the chain is used. This value is also used as recipient of
    /// potential refunds for the message grants created by the operations.
    pub authenticated_signer: Option<Owner>,

    // Inputs to the block, chosen by the block proposer.
    /// Cryptographic hash of all the incoming bundles in the block.
    pub bundles_hash: CryptoHash,
    /// Cryptographic hash of all the operations in the block.
    pub operations_hash: CryptoHash,

    // Outcome of the block execution.
    /// Cryptographic hash of all the messages in the block.
    pub messages_hash: CryptoHash,
    /// Cryptographic hash of all the oracle responses in the block.
    pub oracle_responses_hash: CryptoHash,
    /// Cryptographic hash of all the events in the block.
    pub events_hash: CryptoHash,
}

BlockBody holds the data that is not necessary for (light) verification but was either an input to the block's execution (incoming bundles, operations) or its output (oracle responses, messages sent, events emitted). Each vector is deterministically hashed (into CryptoHash) and the result of that hashing is put into the BlockHeader. This way it's possible to verify inclusion of any specific element w/o having to sent all of the body.

/// The body of a block containing all the data included in the block.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct BlockBody {
    /// A selection of incoming messages to be executed first. Successive messages of same
    /// sender and height are grouped together for conciseness.
    pub incoming_bundles: Vec<IncomingBundle>,
    /// The operations to execute.
    pub operations: Vec<Operation>,
    /// The list of outgoing messages for each transaction.
    pub messages: Vec<Vec<OutgoingMessage>>,
    /// The record of oracle responses for each transaction.
    pub oracle_responses: Vec<Vec<OracleResponse>>,
    /// The list of events produced by each transaction.
    pub events: Vec<Vec<EventRecord>>,
}

Test Plan

All of the tests have been updated to work with the new types.

Release Plan

  • These changes follow the usual release cycle. Although we may want to defer this update since this change is not backwards compatible.

Links

@deuszx deuszx force-pushed the new-block-structure branch from 96cbde5 to 6ab368e Compare January 8, 2025 14:30
@deuszx deuszx force-pushed the new-block-structure branch from 6ab368e to facb07d Compare January 9, 2025 12:01
let new_round = certificate.round;
if let Some(Vote { value, round, .. }) = &self.confirmed_vote {
if value.inner().executed_block().block == *new_block && *round == new_round {
if value.inner().block() == new_block && *round == new_round {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: Double-check. Previously we were comparing the Proposal and now we compare the new Block type. It means we also compare the execution results now.

if certificate.executed_block().block
== confirmed_value.inner().executed_block().block =>
if certificate.block() == confirmed_value.inner().block() =>
// TODO: double check this. Previously we were comparing the Proposal only.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: Double-check. Same here as above.

linera-chain/src/chain.rs Outdated Show resolved Hide resolved
@@ -1235,5 +1241,5 @@ fn empty_executed_block_size() {
outcome: crate::data_types::BlockExecutionOutcome::default(),
};
let size = bcs::serialized_size(&executed_block).unwrap();
assert_eq!(size, EMPTY_EXECUTED_BLOCK_SIZE);
assert_eq!(size, EMPTY_BLOCK_SIZE);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: Test for empty block (new) size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

What's the point of testing a test-only variable? (But as I said I think we should just use this one in production.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right - it doesn't make sense to keep this test for the ExecutedBlock type.

linera-base/src/crypto.rs Outdated Show resolved Hide resolved
@deuszx deuszx force-pushed the new-block-structure branch from 6a9cccf to bb50374 Compare January 9, 2025 18:33
@deuszx deuszx changed the title New block structure New Block structure Jan 10, 2025
where
S: serde::ser::Serializer,
{
serializer.serialize_newtype_struct("CryptoHashVec", &self.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Another #[transparent]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in dca5da3.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, this won't work, b/c we will start getting the errors that we can call type_name() only on struct or newtype (b/c we'll call Deserialize on the Vec<_> and that will fail).

Copy link
Contributor

Choose a reason for hiding this comment

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

Also why do we need this in the first place?

Copy link
Contributor Author

@deuszx deuszx Jan 10, 2025

Choose a reason for hiding this comment

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

When constructing the new Block, we are hashing the contents of the BlockBody - we iterate over elements of incoming_bundles, operations, etc. turning them into CryptoHash-es and then turning resulting Vec<CryptoHash> into a CryptoHash - here. When calling CryptoHash::new(Vec<CryptoHash>) the code would fail b/c calling serde_name::trace_name on a Vec throws. To work around that we have this newtype struct that wraps the Vec<CryptoHash> first.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't deriving Serialize and Deserialize (without #[transparent]) do exactly the same as this implementation?

}

/// Returns whether this value contains the message with the specified ID.
pub fn has_message(&self, message_id: &MessageId) -> bool {
self.executed_block().message_by_id(message_id).is_some()
self.block().message_by_id(message_id).is_some()
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense the handle the renaming executed_block -> block first in a separate PR?

Copy link
Contributor Author

@deuszx deuszx Jan 10, 2025

Choose a reason for hiding this comment

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

Actually, I didn't get rid of ExecutedBlock. I tried but the Proposal/BlockExecutionOutcome is used in a couple of places (we execute before proposing, we execute when we receive a proposal, we re-execute when we receive a confirmed certificate, etc.) so I kept the ExecutedBlock as an internal data structure that caries the result of execution.

This particular change that you commented on is a follow-up to changing the ConfirmedBlock(ExecutedBlock) to ConfirmedBlock(Block) so it's not a rename only, it's actually a new Block type inside.

@deuszx deuszx force-pushed the new-block-structure branch from dca5da3 to d13da68 Compare January 10, 2025 18:21
@deuszx deuszx marked this pull request as ready for review January 10, 2025 18:45
@deuszx deuszx requested review from afck and bart-linera January 10, 2025 18:47
linera-sdk/src/test/block.rs Outdated Show resolved Hide resolved
linera-indexer/example/tests/test.rs Outdated Show resolved Hide resolved
linera-core/src/unit_tests/worker_tests.rs Outdated Show resolved Hide resolved
linera-chain/src/block.rs Outdated Show resolved Hide resolved
linera-chain/src/block.rs Show resolved Hide resolved
linera-chain/src/block.rs Outdated Show resolved Hide resolved
@deuszx deuszx force-pushed the new-block-structure branch from 200252e to f3c8e49 Compare January 13, 2025 13:22
@afck afck self-requested a review January 13, 2025 13:24
When (de)serializing a Block, don't (de)serialize the hashes from the
BlockHeader but rather compute them from the elements of the BlockBody.
This makes sure that we never simply _trust_ the values that are being
sent and rather compute them ourselves.
@deuszx deuszx force-pushed the new-block-structure branch from f3c8e49 to 652604e Compare January 13, 2025 13:42
@@ -395,16 +399,16 @@ impl OutgoingMessage {
}
}

/// A [`Block`], together with the outcome from its execution.
/// A [`Proposal`], together with the outcome from its execution.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
pub struct ExecutedBlock {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe ProposalWithOutcome. Or even inline it, if it's not used in many places anymore?

@@ -76,7 +76,7 @@ pub struct Block {
pub previous_block_hash: Option<CryptoHash>,
}

impl Block {
impl Proposal {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now a BlockProposal contains a ProposalContent which contains a Proposal.

Maybe call this type ProposedBlock?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't call it ProposedBlock on purpose b/c we have the BlockProposal which sounds basically equivalent that is the encompassing type. It seemed weird that BlockProposal contains ProposedBlock.

Copy link
Contributor

Choose a reason for hiding this comment

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

But isn't a BlockProposal containing a Proposal even worse?

Maybe BlockInput? BlockTemplate? BlockPlan? 😕

@deuszx deuszx force-pushed the new-block-structure branch from 51161cf to 543802f Compare January 13, 2025 16:35
@deuszx deuszx requested review from ma2bd and afck January 13, 2025 16:38
@deuszx deuszx force-pushed the new-block-structure branch from 543802f to 5cbe42e Compare January 13, 2025 16:42
where
S: serde::ser::Serializer,
{
serializer.serialize_newtype_struct("CryptoHashVec", &self.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't deriving Serialize and Deserialize (without #[transparent]) do exactly the same as this implementation?


/// Block defines the atomic unit of growth of the Linera chain.
/// As part of the block body, contains all the incoming messages
/// and operations to executed which define state transition of the chain.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// and operations to executed which define state transition of the chain.
/// and operations to execute which define a state transition of the chain.

@@ -192,3 +206,362 @@ pub enum ConversionError {
#[error("Expected a `TimeoutCertificate` value")]
Timeout,
}

/// Block defines the atomic unit of growth of the Linera chain.
/// As part of the block body, contains all the incoming messages
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's common to add a blank line (i.e. new paragraph in markdown; also below).

Suggested change
/// As part of the block body, contains all the incoming messages
///
/// As part of the block body, contains all the incoming messages

@@ -147,9 +148,13 @@ static STATE_HASH_COMPUTATION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(||
)
});

/// The BCS-serialized size of an empty `ExecutedBlock`.
/// The BCS-serialized size of an empty [`crate::data_types::ExecutedBlock`].
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need to serialize ExtecutedBlocks at all? Can't we always use Block instead?

And this constant is about the block size limit: I'd say that should apply to Block, not to ExecutedBlock?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. We don't need them anymore.

@@ -698,7 +707,7 @@ where
account: block.authenticated_signer,
};
resource_controller
.track_executed_block_size(EMPTY_EXECUTED_BLOCK_SIZE)
.track_block_size(EMPTY_EXECUTED_BLOCK_SIZE)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rather use EMPTY_BLOCK_SIZE here.

Suggested change
.track_block_size(EMPTY_EXECUTED_BLOCK_SIZE)
.track_block_size(EMPTY_EXECUTED_BLOCK_SIZE)

@@ -1235,5 +1241,5 @@ fn empty_executed_block_size() {
outcome: crate::data_types::BlockExecutionOutcome::default(),
};
let size = bcs::serialized_size(&executed_block).unwrap();
assert_eq!(size, EMPTY_EXECUTED_BLOCK_SIZE);
assert_eq!(size, EMPTY_BLOCK_SIZE);
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the point of testing a test-only variable? (But as I said I think we should just use this one in production.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add new Block and BlockHeader structs
3 participants