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

feat: L3 support #437

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

feat: L3 support #437

wants to merge 58 commits into from

Conversation

ocdbytes
Copy link
Member

@ocdbytes ocdbytes commented Dec 20, 2024

Added support for L3s. Starknet client is added in this PR and tests are added for several client functions. for now we are assuming that the l3 gas prices will be zero only.

Pull Request type

Please add the labels corresponding to the type of changes your PR introduces:

  • Feature
  • Testing

What is the current behavior?

Currently we didn't support the L3 appchain spec.

What is the new behavior?

Included the starknet client and updated the cli arguments for running madara in appchain mode. For now for all the gas prices and metrics we are still using the l1 modules. We can cover this refactoring in another PR.

Key Changes

  1. Initially we had only one client for settlement (crates/madara/client/eth) but now the folder structure is little bit changed in order to add two clients. crates/madara/client/eth is renamed to crates/madara/client/settlement_client :
├── Cargo.toml
├── README.md
└── src /
    ├── client.rs
    ├── error.rs
    ├── eth /
    │   ├── event.rs
    │   ├── mod.rs
    │   └── starknet_core.json
    ├── gas_price.rs
    ├── lib.rs
    ├── messaging.rs
    ├── starknet /
    │   ├── event.rs
    │   ├── mod.rs
    │   └── utils.rs
    ├── state_update.rs
    ├── sync.rs
    └── utils.rs
  1. Added automock for client testing. automock in client
  2. Also Added separate tests based on mocking the client for messaging and state update listeners.
  3. In this PR wrappers for event stream is added for ETH which basically takes the alloy eth event stream and puts a stream wrapper implemented in order to make event processing easier. Stream Wrapper.Here the stream wrapper is not generalised and is hard coded to return the Messaging stream in another PR we should make the event stream generalise din order to be used with any event listener from alloy or any other lib we use in future.
  4. We have implemented separate event streams for eth and starknet, as starknet does not have .watch() function for listening to events we have added a custom stream implementation for starknet (Starknet Stream Implementation)
  5. We have also changes the input type of eth_core_contract_address and eth_gps_statement_verifier in chain config in order to be more accommodating to other types we might use in future and we are currently using.

Important

For now as the PR is itself very long and cannot be broken into components because of inter component and e2e messaging tests. All the variables which are related to l1 are not renamed right now as it will be more time consuming and has no impact on functionality as of now.

Simple Diagram for messaging components

image

Does this introduce a breaking change?

No

@apoorvsadana apoorvsadana marked this pull request as draft December 23, 2024 03:40
@Trantorian1 Trantorian1 added the research Research and exploration required before implementation label Dec 31, 2024
Comment on lines 54 to 74
while !page_indicator {
let events = provider
.get_events(
EventFilter {
from_block: filter.from_block,
to_block: filter.to_block,
address: filter.address,
keys: filter.keys.clone(),
},
continuation_token.clone(),
1000,
)
.await?;

event_vec.extend(events.events);
if let Some(token) = events.continuation_token {
continuation_token = Some(token);
} else {
page_indicator = true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

this code exists in get_events in mod.rs as well, can we use that?

Copy link
Member Author

Choose a reason for hiding this comment

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

That cannot be used unfortunately as there are some constraints in stream implementation.
So when I used the function it kills the stream when no event is returned. So there are no new calls to the stream.
I am checking more on this behaviour on why this is happening.

Copy link
Member

@Mohiiit Mohiiit left a comment

Choose a reason for hiding this comment

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

I need some more context in order to understand the event structure a bit better, by structure I mean, how we are fetching for both cases and how is cancelling of messages is working.


// Initialize database service
let db = Arc::new(
DatabaseService::new(&base_path, backup_dir, false, chain_info.clone(), Default::default())
Copy link
Member

Choose a reason for hiding this comment

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

open_for_testing can be used instead of new

devnet: bool,
mempool: Arc<Mempool>,
) -> anyhow::Result<Box<dyn Service>> {
match config.settlement_layer {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add the if !config.l1_sync_disabled && (config.l1_endpoint.is_some() || !devnet) else {} part that is common in eth and starknet here? we can have this match statement inside the if part and it will return the client

Copy link
Contributor

Choose a reason for hiding this comment

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

as discussed this common logic should be in the create function

@Mohiiit Mohiiit assigned Mohiiit and unassigned ocdbytes Feb 6, 2025
Copy link
Collaborator

@Trantorian1 Trantorian1 left a comment

Choose a reason for hiding this comment

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

SettlementClientError::Other seems to be used a lot. We either add more error variants or break this down into more error types, perhaps allow implementors of SettlementClientTrait to define their own error type as part of the trait (might conflict with dyn uses of the type, idk).

I'm also not sure that having SettlementClientError::Other wrap anyhow::Error is a good idea. It seems like we are missing out on a lot of constant errors this way and this should probably be moved to a wrap a String instead.

There also are a few uses of Arc<Box<dyn>> which seems strange. These should also probably be reduced to Arc<dyn>. The issue seems to come from the usage of Box::pin instead of Arc::pin but I have not checked out every instance where this is being used.

Comment on lines 13 to 47
#[derive(thiserror::Error, Debug)]
pub enum SettlementClientError {
#[error("Missing required field: {0}")]
MissingField(&'static str),

#[error("Value exceeds Felt max value for field: {0}")]
ValueExceedsFeltRange(&'static str),

#[error("Invalid log: {0}")]
InvalidLog(String),

#[error("Invalid contract: {0}")]
InvalidContract(String),

#[error("Conversion error: {0}")]
ConversionError(String),

#[error("Ethereum RPC error: {0}")]
EthereumRpcError(#[from] alloy::sol_types::Error),

#[error(transparent)]
Other(#[from] anyhow::Error),

#[error("Invalid event: {0}")]
InvalidEvent(String),

#[error("Invalid response: {0}")]
InvalidResponse(String),

#[error("Database error: {0}")]
DatabaseError(String),

#[error("Invalid data: {0}")]
InvalidData(String),
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice :D

})?),
transaction_hash: Felt::from_bytes_be_slice(
log.transaction_hash
.ok_or_else(|| SettlementClientError::MissingField("transaction_hash"))?
Copy link
Collaborator

Choose a reason for hiding this comment

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

This probably doesn't need to be behind a ok_or_else since this is essentially a compile-time constant. Same for the other uses below.

#[fixture]
async fn setup_test_env() -> TestRunner {
// Start Anvil instance
let anvil = Anvil::new().block_time(1).chain_id(1337).try_spawn().expect("failed to spawn anvil instance");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense, thanks for the info 👍

/// 8. Assert that the event is successfully pushed to the db
/// 9. TODO : Assert that the tx was correctly executed
#[rstest]
#[traced_test]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair!

@@ -26,48 +27,51 @@ pub struct CommonMessagingEventData {
}

pub async fn sync<C, S>(
settlement_client: Arc<Box<dyn ClientTrait<Config = C, StreamType = S>>>,
settlement_client: Arc<Box<dyn SettlementClientTrait<Config = C, StreamType = S>>>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't Arc<dyn SettlementClientTrait<Config = C, StreamType = S>> be enough here?

Ok(self.get_state_call().await?[0])
}

async fn get_last_verified_block_hash(&self) -> anyhow::Result<Felt> {
// Block Hash index in call response : 2
async fn get_last_verified_block_hash(&self) -> Result<Felt, SettlementClientError> {
Ok(self.get_state_call().await?[2])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here, if this is safe add a comment.

async fn fail_create_new_client_contract_does_not_exists(
#[future] test_context: anyhow::Result<TestContext>,
) -> anyhow::Result<()> {
let _guard = get_state_update_lock().lock().await;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we could have the guard as a fixture


// These functions can now be marked as deprecated as they're replaced by the context system
#[deprecated(note = "Use init_test_context() instead")]
pub async fn prepare_starknet_client_test() -> anyhow::Result<(StarknetAccount, Felt, MadaraProcess)> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we still using this somewhere in the codebase?

&STATE_UPDATE_LOCK
}

pub async fn cleanup_test_context() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like this should be implemented through some king of drop guard so that this method is always called even if a test errors or panics.

Comment on lines 13 to 22
pub struct SyncWorkerConfig<C: 'static, S> {
pub backend: Arc<MadaraBackend>,
pub settlement_client: Arc<Box<dyn SettlementClientTrait<Config = C, StreamType = S>>>,
pub l1_gas_provider: GasPriceProvider,
pub gas_price_sync_disabled: bool,
pub gas_price_poll_ms: Duration,
pub mempool: Arc<Mempool>,
pub ctx: ServiceContext,
pub l1_block_metrics: Arc<L1BlockMetrics>,
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
node Related to the full node implementation research Research and exploration required before implementation sequencer Related to the sequencing logic and implementation
Projects
Status: In progress
Development

Successfully merging this pull request may close these issues.

5 participants