Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion crates/agglayer-aggregator-notifier/src/certifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,11 @@ where

let l1_aggchain_hash = self
.l1_rpc
.get_aggchain_hash(rollup_address, certificate.custom_chain_data.clone().into())
.get_aggchain_hash(
rollup_address,
certificate.custom_chain_data.clone().into(),
certificate_tx_hash.map(|digest| digest.0.into()),
)
.await
.map_err(CertificationError::UnableToFindAggchainHash)?
.into();
Expand Down
1 change: 1 addition & 0 deletions crates/agglayer-aggregator-notifier/src/certifier/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ mockall::mock! {
&self,
rollup_address: agglayer_types::primitives::Address,
aggchain_data: Bytes,
before_tx_hash: Option<TxHash>,
) -> Result<[u8; 32], L1RpcError>;

async fn get_multisig_context(
Expand Down
10 changes: 3 additions & 7 deletions crates/agglayer-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,8 @@ impl Config {

let path = path
.parent()
.ok_or_else(|| ConfigurationError::UnableToReadConfigFile {
path: path.to_path_buf(),
source: std::io::Error::other(
"Unable to determine the parent folder of the configuration file",
),
})?;
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));

let config_with_path = ConfigDeserializer { path };

Expand Down Expand Up @@ -248,7 +244,7 @@ impl<'de> DeserializeSeed<'de> for ConfigDeserializer<'_> {
.storage
.path_contextualized(&self.path.canonicalize().map_err(|error| {
serde::de::Error::custom(format!(
"Unable to canonicalize the storage path: {error}"
"Unable to canonicalize the configuration directory: {error}"
))
})?);

Expand Down
27 changes: 27 additions & 0 deletions crates/agglayer-config/tests/bare_filename_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::path::{Path, PathBuf};

use agglayer_config::Config;
use pretty_assertions::assert_eq;

struct CurrentDirGuard(PathBuf);

impl Drop for CurrentDirGuard {
fn drop(&mut self) {
std::env::set_current_dir(&self.0).unwrap();
}
}

#[test]
fn bare_filename_config_path_uses_current_directory() {
let original_dir = std::env::current_dir().unwrap();
let tests_dir = original_dir.join("tests");
std::env::set_current_dir(&tests_dir).unwrap();
let _current_dir_guard = CurrentDirGuard(original_dir);

let config = Config::try_load(Path::new("bare_filename_config.toml")).unwrap();

assert_eq!(
config.storage.state_db_path,
tests_dir.canonicalize().unwrap().join("storage/state")
);
}
1 change: 1 addition & 0 deletions crates/agglayer-config/tests/bare_filename_config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[full-node-rpcs]
33 changes: 30 additions & 3 deletions crates/agglayer-contracts/src/aggchain.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
pub use agglayer_primitives::vkey_hash::VKeyHash;
use agglayer_primitives::{Address, U256};
use alloy::primitives::Bytes;
use alloy::{
eips::BlockId,
primitives::{Bytes, TxHash},
};
use tracing::error;

use crate::{contracts::AggchainBase, L1RpcClient, L1RpcError};
use crate::{block_pinning::block_before_tx, contracts::AggchainBase, L1RpcClient, L1RpcError};

#[async_trait::async_trait]
pub trait AggchainContract {
Expand All @@ -13,10 +16,22 @@ pub trait AggchainContract {
aggchain_vkey_selector: u16,
) -> Result<VKeyHash, L1RpcError>;

/// Fetch the aggchain hash for `aggchain_data` from the rollup's aggchain
/// contract on L1.
///
/// When `before_tx_hash` is `Some`, the call is pinned to the L1 block
/// immediately preceding that transaction's inclusion block. This is used
/// to reconcile an already-settled certificate: stateful aggchain
/// contracts (e.g. `AggchainFEP`) revert `getAggchainHash` once their
/// `nextBlockNumber` has advanced past the certificate's range, so the
/// hash must be queried at the pre-settlement state where it is still
/// served. When `None`, or when the transaction is not yet mined
/// successfully, the query targets `latest`.
async fn get_aggchain_hash(
&self,
rollup_address: Address,
aggchain_data: Bytes,
before_tx_hash: Option<TxHash>,
) -> Result<[u8; 32], L1RpcError>;

async fn get_multisig_context(
Expand Down Expand Up @@ -55,14 +70,26 @@ where
&self,
rollup_address: Address,
aggchain_data: Bytes,
before_tx_hash: Option<TxHash>,
) -> Result<[u8; 32], L1RpcError> {
let at_block = match before_tx_hash {
// A transaction that did not successfully advance the state we depend
// on (not mined, reverted, or whose receipt could not be fetched)
// leaves the current state as the one to query.
Some(tx_hash) => block_before_tx(&self.rpc, tx_hash)
.await
.unwrap_or_else(|_| BlockId::latest()),
None => BlockId::latest(),
};

AggchainBase::new(rollup_address.into(), self.rpc.clone())
.getAggchainHash(aggchain_data)
.block(at_block)
.call()
.await
.map(Into::into)
.map_err(|error| {
error!(?error, "Unable to fetch the aggchain hash");
error!(?error, ?at_block, "Unable to fetch the aggchain hash");

L1RpcError::AggchainHashFetchFailed
})
Expand Down
123 changes: 123 additions & 0 deletions crates/agglayer-contracts/src/block_pinning.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Resolve the L1 block at which to evaluate a view call so that it observes
//! the state immediately *before* a given transaction was included.
//!
//! Stateful aggchain contracts (e.g. `AggchainFEP`) mutate their on-chain state
//! as certificates settle, so a view call such as `getAggchainHash` or
//! `rollupIDToRollupDataV2` must sometimes be pinned to the pre-settlement
//! block to observe the value a now-settled certificate was checked against.
//!
//! Both [`crate::aggchain`] and [`crate::rollup`] share this resolution; they
//! differ only in how they treat a transaction that did not successfully
//! advance the state (not mined, reverted, or whose receipt could not be
//! fetched). Each caller decides that policy by mapping [`UnresolvedBlock`]:
//! the aggchain path falls back to `latest`, while the rollup path surfaces a
//! typed error.

use alloy::{eips::BlockId, primitives::TxHash, providers::Provider};

/// Reason a pre-transaction block could not be resolved from a receipt.
///
/// Returned by [`block_before_tx`] when the transaction did not successfully
/// advance L1 state. Callers decide whether this is a hard error or a cue to
/// fall back to the current (`latest`) state.
#[derive(Debug)]
pub(crate) enum UnresolvedBlock {
/// The transaction receipt could not be fetched from L1.
FetchFailed(eyre::Error),
/// The transaction has no receipt yet, i.e. it is not mined.
NotMined,
/// The transaction is mined but reverted.
Reverted,
}

/// Resolve the block immediately preceding `tx_hash`'s inclusion block.
///
/// On success returns the block before inclusion (saturating at zero), or
/// [`BlockId::latest`] when the inclusion block number is unknown. Returns an
/// [`UnresolvedBlock`] when the receipt cannot be fetched, the transaction is
/// not yet mined, or it reverted.
pub(crate) async fn block_before_tx<P: Provider>(
rpc: &P,
tx_hash: TxHash,
) -> Result<BlockId, UnresolvedBlock> {
let receipt = rpc
.get_transaction_receipt(tx_hash)
.await
.map_err(|err| UnresolvedBlock::FetchFailed(err.into()))?
.map(|receipt| (receipt.status(), receipt.block_number));

block_before_inclusion(receipt)
}

/// Resolve the query block from a transaction's `(succeeded, inclusion_block)`
/// receipt projection, or `None` when the transaction has no receipt.
///
/// A successful transaction included in block `n` resolves to block `n - 1`
/// (saturating at zero); a successful transaction with an unknown inclusion
/// block resolves to [`BlockId::latest`]. A reverted or unmined transaction
/// yields the corresponding [`UnresolvedBlock`].
fn block_before_inclusion(
receipt: Option<(bool, Option<u64>)>,
) -> Result<BlockId, UnresolvedBlock> {
match receipt {
Some((true, Some(block))) => Ok(BlockId::number(block.saturating_sub(1))),
Some((true, None)) => Ok(BlockId::latest()),
Some((false, _)) => Err(UnresolvedBlock::Reverted),
None => Err(UnresolvedBlock::NotMined),
}
}

#[cfg(test)]
mod tests {
use alloy::eips::BlockId;

use super::{block_before_inclusion, UnresolvedBlock};

#[test]
fn successful_receipt_resolves_to_preceding_block() {
assert_eq!(
block_before_inclusion(Some((true, Some(100)))).unwrap(),
BlockId::number(99),
);
}

#[test]
fn inclusion_in_genesis_block_saturates_at_zero() {
assert_eq!(
block_before_inclusion(Some((true, Some(0)))).unwrap(),
BlockId::number(0),
);
}

#[test]
fn successful_receipt_without_block_resolves_to_latest() {
assert_eq!(
block_before_inclusion(Some((true, None))).unwrap(),
BlockId::latest(),
);
}

#[test]
fn reverted_transaction_is_unresolved() {
assert!(matches!(
block_before_inclusion(Some((false, Some(100)))),
Err(UnresolvedBlock::Reverted),
));
}

#[test]
fn reverted_transaction_without_block_is_unresolved() {
assert!(matches!(
block_before_inclusion(Some((false, None))),
Err(UnresolvedBlock::Reverted),
));
}

#[test]
fn missing_receipt_is_not_mined() {
assert!(matches!(
block_before_inclusion(None),
Err(UnresolvedBlock::NotMined),
));
}
}
1 change: 1 addition & 0 deletions crates/agglayer-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use eyre::{eyre, Context as _};
use tracing::{debug, info};

pub mod aggchain;
mod block_pinning;
pub mod contracts;
pub mod rollup;
pub mod settler;
Expand Down
32 changes: 13 additions & 19 deletions crates/agglayer-contracts/src/rollup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use num_traits::FromPrimitive;
use tracing::{debug, error, trace};

use crate::{
block_pinning::{block_before_tx, UnresolvedBlock},
contracts::{PolygonRollupManager::RollupDataReturnV2, PolygonZkEvm},
L1RpcClient, L1RpcError,
};
Expand Down Expand Up @@ -250,27 +251,20 @@ where
) -> Result<[u8; 32], L1RpcError> {
let at_block = if let Some(tx_hash) = before_tx_hash {
let settlement_tx_hash = SettlementTxHash::from(tx_hash);
let receipt = self
.rpc
.get_transaction_receipt(tx_hash)
block_before_tx(&self.rpc, tx_hash)
.await
.map_err(|err| L1RpcError::UnableToFetchTransactionReceipt {
tx_hash: settlement_tx_hash,
source: err.into(),
.map_err(|unresolved| match unresolved {
UnresolvedBlock::FetchFailed(source) => {
L1RpcError::UnableToFetchTransactionReceipt {
tx_hash: settlement_tx_hash,
source,
}
}
UnresolvedBlock::NotMined => {
L1RpcError::TransactionNotYetMined(settlement_tx_hash)
}
UnresolvedBlock::Reverted => L1RpcError::TransactionReceiptFailedOnL1(tx_hash),
})?
.ok_or_else(|| L1RpcError::TransactionNotYetMined(settlement_tx_hash))?;

if receipt.status() {
receipt
.block_number
.map(|block| {
let block = block.saturating_sub(1);
BlockId::number(block)
})
.unwrap_or_else(BlockId::latest)
} else {
return Err(L1RpcError::TransactionReceiptFailedOnL1(tx_hash));
}
} else {
BlockId::latest()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl AggchainContract for L1Rpc {
&self,
_rollup_address: agglayer_types::Address,
_aggchain_data: alloy::primitives::Bytes,
_before_tx_hash: Option<alloy::primitives::TxHash>,
) -> Result<[u8; 32], L1RpcError> {
unreachable!("invalid certificates are rejected before L1 access")
}
Expand Down
Loading
Loading