Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8353b00
chore: make sure rules validation returns a structured result
jeluard Apr 11, 2025
3882293
chore: cleanup
jeluard Apr 11, 2025
5e7d747
chore: address PR comment
jeluard Apr 15, 2025
952c0a1
chore: make rules validation return data
jeluard Apr 16, 2025
ce6fa51
chore: block validation fns share the same signature
jeluard Apr 16, 2025
bf02cb6
chore: remove unneeded enum
jeluard Apr 16, 2025
e7906ab
chore: use rust nightly
jeluard Apr 16, 2025
2c67b6c
feat: implement Try for BlockValidation
jeluard Apr 16, 2025
ef6b78d
chore: adapt rust-version
jeluard Apr 16, 2025
a65f00a
chore: added full test e2e make target
jeluard Apr 17, 2025
bbd1f3c
chore: flatten invalid block details
jeluard Apr 17, 2025
90fce18
chore: update cargo lock file
jeluard Apr 17, 2025
8193936
chore: git ignore llvm-cov generated file
jeluard Apr 17, 2025
bd88eae
chore: address PR comment
jeluard Apr 17, 2025
d38bfe4
chore: use latest nightly
jeluard Apr 17, 2025
5b349a9
chore: API cleanup
jeluard Apr 17, 2025
3b31683
feat: introduce trait for block validation fn execution
jeluard Apr 18, 2025
f769bd4
chore: fix merge issues
jeluard Apr 18, 2025
cb5c794
chore: address clippy comment
jeluard Apr 23, 2025
9a97099
fiix: merge issue
jeluard Apr 23, 2025
a45baef
Revert "feat: introduce trait for block validation fn execution"
jeluard Apr 23, 2025
e47695d
chore: make sure target failures are propagated
jeluard Apr 23, 2025
9237a86
chore: refect commit revert
jeluard Apr 23, 2025
0608748
Merge branch 'main' into jeluard/rules-validation-errors
jeluard Apr 23, 2025
ec96b89
chore: simplify Makefile
jeluard Apr 23, 2025
2e82638
chore: revert rule validation signature change
jeluard Apr 23, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ coverage/

# Cardano node support files
/cardano-node-config/

# llvm-cov generated file
lcov.info
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = ["Amaru Maintainers <amaru@pragma.builders>"]
repository = "https://github.com/pragma-org/amaru"
homepage = "https://github.com/pragma-org/amaru"
documentation = "https://docs.rs/amaru"
rust-version = "1.84" # ⚠️ Also change in .cargo/rust-toolchain.toml
rust-version = "1.88" # ⚠️ Also change in .cargo/rust-toolchain.toml

[workspace]
members = ["crates/*", "simulation/*"]
Expand Down
5 changes: 1 addition & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,7 @@ build-examples: ## Build all examples
@for dir in $(wildcard examples/*/.); do \
if [ -f $$dir/Makefile ]; then \
echo "Building $$dir"; \
$(MAKE) -C $$dir; \
if [ $$? != "0" ]; then \
exit $$?; \
fi; \
$(MAKE) -C $$dir || exit; \
fi; \
done

Expand Down
4 changes: 3 additions & 1 deletion crates/amaru-consensus/src/consensus/chain_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,10 @@ pub(crate) mod tests {
chain_selector.select_roll_forward(&alice, *header);
});

#[allow(clippy::double_ended_iterator_last)]
let result = chain2
.iter()
// TODO looks like this test relies on the fact that `select_roll_forward` is called on every element. `map` might not be correct then
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not understand this comment about map ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

last can be replaced by next_back in theory here but in practice it fails some test. This implies that this call expects map to traverse all element by calling a function with side-effects (select_roll_forward). map should be called with pure functions as a way to construct another iterator. for_each might help clarify the expectation here (but at the price of a more contrived usage as the last element should be tracked).

.map(|header| chain_selector.select_roll_forward(&bob, *header))
.last();

Expand Down Expand Up @@ -533,7 +535,7 @@ pub(crate) mod tests {
let result = chain1
.iter()
.map(|header| chain_selector.select_roll_forward(&alice, *header))
.last();
.next_back();

assert_eq!(ForwardChainSelection::NoChange, result.unwrap());
}
Expand Down
2 changes: 2 additions & 0 deletions crates/amaru-ledger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#![feature(try_trait_v2)]

use amaru_kernel::Point;
use tracing::Span;

Expand Down
26 changes: 13 additions & 13 deletions crates/amaru-ledger/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ pub(crate) mod tests {
use super::*;
use crate::{
context::assert::{AssertPreparationContext, AssertValidationContext},
rules,
rules::block::{InvalidBlock, InvalidBlockHeader},
rules::{
self,
block::{BlockValidation, InvalidBlockDetails},
},
tests::{fake_input, fake_output},
};
use amaru_kernel::protocol_parameters::ProtocolParameters;
Expand Down Expand Up @@ -174,12 +176,12 @@ pub(crate) mod tests {
prepare_block(&mut ctx, &block);

let results = rules::block::execute(
AssertValidationContext::from(ctx),
&mut AssertValidationContext::from(ctx),
ProtocolParameters::default(),
block,
&block,
);

assert!(results.is_ok())
assert!(matches!(results, BlockValidation::Valid));
}

#[test]
Expand All @@ -200,14 +202,12 @@ pub(crate) mod tests {

prepare_block(&mut ctx, &block);

assert!(
rules::block::execute(AssertValidationContext::from(ctx), pp, block).is_err_and(|e| {
matches!(
e,
InvalidBlock::Header(InvalidBlockHeader::SizeTooBig { .. })
)
})
)
let results = rules::block::execute(&mut AssertValidationContext::from(ctx), pp, &block);

assert!(matches!(
results,
BlockValidation::Invalid(InvalidBlockDetails::HeaderSizeTooBig { .. })
))
}

macro_rules! fixture_context {
Expand Down
122 changes: 78 additions & 44 deletions crates/amaru-ledger/src/rules/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,76 +16,109 @@ pub mod body_size;
pub mod ex_units;
pub mod header_size;

pub use crate::rules::block::{
body_size::InvalidBlockSize, ex_units::InvalidExUnits, header_size::InvalidBlockHeader,
};
use crate::{
context::ValidationContext,
rules::{transaction, transaction::InvalidTransaction},
state::FailedTransactions,
};
use amaru_kernel::{
protocol_parameters::ProtocolParameters, AuxiliaryData, HasExUnits, Hash, MintedBlock,
protocol_parameters::ProtocolParameters, AuxiliaryData, ExUnits, HasExUnits, Hash, MintedBlock,
OriginalHash, StakeCredential, TransactionPointer,
};
use std::ops::Deref;
use thiserror::Error;
use std::ops::{ControlFlow, Deref, FromResidual, Try};
use tracing::{instrument, Level};

#[derive(Debug, Error)]
pub enum InvalidBlock {
#[error("Invalid block's size: {0}")]
Size(#[from] InvalidBlockSize),

#[error("Invalid block's execution units: {0}")]
ExUnits(#[from] InvalidExUnits),

#[error("Invalid block header: {0}")]
Header(#[from] InvalidBlockHeader),

#[error(
"Invalid transaction (hash: {transaction_hash}, index: {transaction_index}): {violation} "
)]
#[derive(Debug)]
pub enum InvalidBlockDetails {
BlockSizeMismatch {
supplied: usize,
actual: usize,
},
TooManyExUnits {
provided: ExUnits,
max: ExUnits,
},
HeaderSizeTooBig {
supplied: usize,
max: usize,
},
Transaction {
transaction_hash: Hash<32>,
transaction_index: u32,
violation: InvalidTransaction,
},

// TODO: This error shouldn't exist, it's a placeholder for better error handling in less straight forward cases
#[error("Uncategorized error: {0}")]
UncategorizedError(String),
}

#[derive(Debug)]
pub enum BlockValidation {
Valid,
Invalid(InvalidBlockDetails),
}

impl Try for BlockValidation {
type Output = ();
type Residual = InvalidBlockDetails;

fn from_output((): Self::Output) -> Self {
BlockValidation::Valid
}

fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
match self {
BlockValidation::Valid => ControlFlow::Continue(()),
BlockValidation::Invalid(e) => ControlFlow::Break(e),
}
}
}

impl FromResidual for BlockValidation {
fn from_residual(residual: InvalidBlockDetails) -> Self {
BlockValidation::Invalid(residual)
}
}

impl From<BlockValidation> for Result<(), InvalidBlockDetails> {
fn from(validation: BlockValidation) -> Self {
match validation {
BlockValidation::Valid => Ok(()),
BlockValidation::Invalid(e) => Err(e),
}
}
}

#[instrument(level = Level::TRACE, skip_all)]
pub fn execute<C: ValidationContext<FinalState = S>, S: From<C>>(
mut context: C,
context: &mut C,
protocol_params: ProtocolParameters,
block: MintedBlock<'_>,
) -> Result<S, InvalidBlock> {
block: &MintedBlock<'_>,
) -> BlockValidation {
header_size::block_header_size_valid(block.header.raw_cbor(), &protocol_params)?;

body_size::block_body_size_valid(&block.header.header_body, &block)?;
body_size::block_body_size_valid(block)?;

ex_units::block_ex_units_valid(block.ex_units(), &protocol_params)?;

let failed_transactions = FailedTransactions::from_block(&block);
let failed_transactions = FailedTransactions::from_block(block);

let witness_sets = block.transaction_witness_sets.deref().to_vec();

let transactions = block.transaction_bodies.to_vec();
let transactions = block.transaction_bodies.deref().to_vec();

// using `zip` here instead of enumerate as it is safer to cast from u32 to usize than usize to u32
// Realistically, we're never gonna hit the u32 limit with the number of transactions in a block (a boy can dream)
for (i, transaction) in (0u32..).zip(transactions.into_iter()) {
let transaction_hash = transaction.original_hash();
Comment on lines 104 to 111
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid full Vec clones – iterate by reference

to_vec() allocates & copies every transaction/witness set, which is a bit of a wallet‑drain when blocks get chunky.

-let witness_sets = block.transaction_witness_sets.deref().to_vec();
-let transactions  = block.transaction_bodies.deref().to_vec();
+let witness_sets = &block.transaction_witness_sets;
+let transactions = &block.transaction_bodies;

Then rewrite the loop:

-for (i, transaction) in (0u32..).zip(transactions.into_iter()) {
+for (i, (transaction, witness_set)) in transactions.iter().zip(witness_sets).enumerate() {
+    let i = i as u32;

This keeps memory usage lean and sidesteps needless copies like Neo dodging bullets.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let witness_sets = block.transaction_witness_sets.deref().to_vec();
let transactions = block.transaction_bodies.to_vec();
let transactions = block.transaction_bodies.deref().to_vec();
// using `zip` here instead of enumerate as it is safer to cast from u32 to usize than usize to u32
// Realistically, we're never gonna hit the u32 limit with the number of transactions in a block (a boy can dream)
for (i, transaction) in (0u32..).zip(transactions.into_iter()) {
let transaction_hash = transaction.original_hash();
let witness_sets = &block.transaction_witness_sets;
let transactions = &block.transaction_bodies;
// using `zip` here instead of enumerate as it is safer to cast from u32 to usize than usize to u32
// Realistically, we're never gonna hit the u32 limit with the number of transactions in a block (a boy can dream)
for (i, (transaction, witness_set)) in transactions.iter().zip(witness_sets).enumerate() {
let i = i as u32;
let transaction_hash = transaction.original_hash();


let witness_set = witness_sets
.get(i as usize)
.ok_or(InvalidBlock::UncategorizedError(format!(
"Missing witness set for transaction index {}",
i
)))?;
let witness_set = match witness_sets.get(i as usize) {
Some(witness_set) => witness_set,
None => {
return BlockValidation::Invalid(InvalidBlockDetails::UncategorizedError(format!(
"Witness set not found for transaction index {}",
i
)));
}
};

let auxiliary_data: Option<&AuxiliaryData> = block
.auxiliary_data_set
Expand All @@ -108,21 +141,22 @@ pub fn execute<C: ValidationContext<FinalState = S>, S: From<C>>(
transaction_index: i as usize, // From u32
};

transaction::execute(
&mut context,
if let Err(err) = transaction::execute(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

transaction rules haven't been changed; this could be done in a follow up PR.

context,
&protocol_params,
pointer,
!failed_transactions.has(i),
transaction,
witness_set,
auxiliary_data,
)
.map_err(|err| InvalidBlock::Transaction {
transaction_hash,
transaction_index: i,
violation: err,
})?;
) {
return BlockValidation::Invalid(InvalidBlockDetails::Transaction {
transaction_hash,
transaction_index: i,
violation: err,
});
}
}

Ok(context.into())
BlockValidation::Valid
}
32 changes: 12 additions & 20 deletions crates/amaru-ledger/src/rules/block/body_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use amaru_kernel::{to_cbor, HeaderBody, MintedBlock};
use thiserror::Error;
use amaru_kernel::{to_cbor, MintedBlock};

#[derive(Debug, Error)]
pub enum InvalidBlockSize {
#[error("block body size mismatch: supplied {supplied}, actual {actual}")]
SizeMismatch { supplied: usize, actual: usize },
}
use super::{BlockValidation, InvalidBlockDetails};

/// This validation checks that the purported block body size matches the actual block body size.
/// The validation of the bounds happens in the networking layer
pub fn block_body_size_valid(
block_header: &HeaderBody,
block: &MintedBlock<'_>,
) -> Result<(), InvalidBlockSize> {
pub fn block_body_size_valid(block: &MintedBlock<'_>) -> BlockValidation {
let block_header = &block.header.header_body;
let bh_size = block_header.block_body_size as usize;
let actual_block_size = calculate_block_body_size(block);

if bh_size != actual_block_size {
Err(InvalidBlockSize::SizeMismatch {
BlockValidation::Invalid(InvalidBlockDetails::BlockSizeMismatch {
supplied: bh_size,
actual: actual_block_size,
})
} else {
Ok(())
BlockValidation::Valid
}
}

Expand All @@ -58,7 +51,7 @@ mod tests {
use amaru_kernel::{include_cbor, MintedBlock};
use test_case::test_case;

use super::InvalidBlockSize;
use crate::rules::block::InvalidBlockDetails;

macro_rules! fixture {
($number:literal) => {
Expand All @@ -70,12 +63,11 @@ mod tests {
}

#[test_case(fixture!("2667660"); "valid")]
#[test_case(fixture!("2667660", "invalid_block_body_size") =>
matches Err(InvalidBlockSize::SizeMismatch {supplied, actual})
#[test_case(fixture!("2667660", "invalid_block_body_size") =>
matches Err(InvalidBlockDetails::BlockSizeMismatch {supplied, actual})
if supplied == 0 && actual == 3411;
"block body size mismatch"
)]
fn test_block_size(block: MintedBlock<'_>) -> Result<(), InvalidBlockSize> {
super::block_body_size_valid(&block.header.header_body, &block)
"block body size mismatch")]
fn test_block_size(block: MintedBlock<'_>) -> Result<(), InvalidBlockDetails> {
super::block_body_size_valid(&block).into()
}
}
Loading