Skip to content

Commit

Permalink
chore(starknet_l1_provider): add l1 scraper test (#2857)
Browse files Browse the repository at this point in the history
Also add Anvil to the CI: moonrepo cannot cache binaries installed from
git, so this is _uncached_ and currently takes ~3 minutes to install.
Therefore, added a path filter to only run this installation when there
are changes in L1 crates.

Co-authored-by: Gilad Chase <gilad@starkware.com>
  • Loading branch information
giladchase and Gilad Chase authored Jan 16, 2025
1 parent 1e9cbaf commit a7b7801
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 5 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
changes:
runs-on: starkware-ubuntu-20-04-medium
permissions:
pull-requests: read
outputs:
l1: ${{ steps.filter.outputs.l1 }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
l1:
- 'crates/starknet_l1_provider/**'
- 'crates/papyrus_base_layer/**'
code_style:
runs-on: starkware-ubuntu-20-04-medium
steps:
Expand Down Expand Up @@ -99,6 +115,7 @@ jobs:
- run: cargo test -p workspace_tests

run-tests:
needs: changes
runs-on: starkware-ubuntu-latest-large
steps:
- uses: actions/checkout@v4
Expand All @@ -116,7 +133,16 @@ jobs:
- env:
LD_LIBRARY_PATH: ${{ env.Python3_ROOT_DIR }}/bin
run: echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> $GITHUB_ENV
# TODO(Gilad): only one test needs this (base_layer_test.rs), once it migrates to
# anvil, remove.
- run: npm install -g ganache@7.4.3

# Note: Anvil's not on crates.io, so we have to install from git, which moonrepo doesn't
# know how to cache, so we're forced to reinstall it every time :(
- name: "Maybe install Anvil"
run: cargo install --git https://github.com/foundry-rs/foundry anvil --locked
if: needs.changes.outputs.l1 == 'true'

- name: "Run tests pull request"
if: github.event_name == 'pull_request'
run: |
Expand Down
78 changes: 73 additions & 5 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ license-file = "LICENSE"
alloy-contract = "0.3.5"
alloy-dyn-abi = "0.8.3"
alloy-json-rpc = "0.3.5"
alloy-node-bindings = "0.8.3"
alloy-primitives = "0.8.3"
alloy-provider = "0.3.5"
alloy-rpc-types-eth = "0.3.5"
Expand Down
3 changes: 3 additions & 0 deletions crates/starknet_l1_provider/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ tracing.workspace = true
validator.workspace = true

[dev-dependencies]
alloy-node-bindings.workspace = true
alloy-primitives.workspace = true
assert_matches.workspace = true
pretty_assertions.workspace = true
rstest.workspace = true
starknet_api = { workspace = true, features = ["testing"] }

[lints]
Expand Down
4 changes: 4 additions & 0 deletions crates/starknet_l1_provider/src/l1_scraper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ use validator::Validate;

type L1ScraperResult<T, B> = Result<T, L1ScraperError<B>>;

#[cfg(test)]
#[path = "l1_scraper_tests.rs"]
pub mod l1_scraper_tests;

pub struct L1Scraper<B: BaseLayerContract> {
pub config: L1ScraperConfig,
pub base_layer: B,
Expand Down
155 changes: 155 additions & 0 deletions crates/starknet_l1_provider/src/l1_scraper_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use std::sync::Arc;

use alloy_node_bindings::{Anvil, AnvilInstance};
use alloy_primitives::U256;
use papyrus_base_layer::ethereum_base_layer_contract::{
EthereumBaseLayerConfig,
EthereumBaseLayerContract,
Starknet,
};
use rstest::{fixture, rstest};
use starknet_api::contract_address;
use starknet_api::core::{EntryPointSelector, Nonce};
use starknet_api::executable_transaction::L1HandlerTransaction as ExecutableL1HandlerTransaction;
use starknet_api::hash::StarkHash;
use starknet_api::transaction::fields::{Calldata, Fee};
use starknet_api::transaction::{L1HandlerTransaction, TransactionHasher, TransactionVersion};
use starknet_l1_provider_types::Event;

use crate::event_identifiers_to_track;
use crate::l1_scraper::{L1Scraper, L1ScraperConfig};
use crate::test_utils::FakeL1ProviderClient;

// TODO: move to global test_utils crate and use everywhere instead of relying on the
// confusing `#[ignore]` api to mark slow tests.
fn in_ci() -> bool {
std::env::var("CI").is_ok()
}

// Default funded account, there are more fixed funded accounts,
// see https://github.com/foundry-rs/foundry/tree/master/crates/anvil.
const DEFAULT_ANVIL_ACCOUNT_ADDRESS: StarkHash =
StarkHash::from_hex_unchecked("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
const DEFAULT_ANVIL_DEPLOY_ADDRESS: &str = "0x5fbdb2315678afecb367f032d93f642f64180aa3";

// Spin up Anvil instance, a local Ethereum node, dies when dropped.
#[fixture]
fn anvil() -> AnvilInstance {
Anvil::new().spawn()
}

// TODO: Replace EthereumBaseLayerContract with a mock that has a provider initialized with
// `with_recommended_fillers`, in order to be able to create txs from non-default users.
async fn scraper(
anvil: &AnvilInstance,
) -> (L1Scraper<EthereumBaseLayerContract>, Arc<FakeL1ProviderClient>) {
let fake_client = Arc::new(FakeL1ProviderClient::default());
let config = EthereumBaseLayerConfig {
node_url: anvil.endpoint_url(),
starknet_contract_address: DEFAULT_ANVIL_DEPLOY_ADDRESS.parse().unwrap(),
};
let base_layer = EthereumBaseLayerContract::new(config);

// Deploy a fresh Starknet contract on Anvil from the bytecode in the JSON file.
Starknet::deploy(base_layer.contract.provider().clone()).await.unwrap();

let scraper = L1Scraper::new(
L1ScraperConfig::default(),
fake_client.clone(),
base_layer,
event_identifiers_to_track(),
);

(scraper, fake_client)
}

#[rstest]
#[tokio::test]
// TODO: extract setup stuff into test helpers once more tests are added and patterns emerge.
async fn txs_happy_flow(anvil: AnvilInstance) {
if !in_ci() {
// To run the test _locally_, remove the `in_ci` check and install the anvil binary:
// cargo install --git https://github.com/foundry-rs/foundry anvil --locked
return;
}

// Setup.
let (mut scraper, fake_client) = scraper(&anvil).await;

// Test.
// Scrape multiple events.
let l2_contract_address = "0x12";
let l2_entry_point = "0x34";

let message_to_l2_0 = scraper.base_layer.contract.sendMessageToL2(
l2_contract_address.parse().unwrap(),
l2_entry_point.parse().unwrap(),
vec![U256::from(1_u8), U256::from(2_u8)],
);
let message_to_l2_1 = scraper.base_layer.contract.sendMessageToL2(
l2_contract_address.parse().unwrap(),
l2_entry_point.parse().unwrap(),
vec![U256::from(3_u8), U256::from(4_u8)],
);

// Send the transactions.
for msg in &[message_to_l2_0, message_to_l2_1] {
msg.send().await.unwrap().get_receipt().await.unwrap();
}

const EXPECTED_VERSION: TransactionVersion = TransactionVersion(StarkHash::ZERO);
let expected_internal_l1_tx = L1HandlerTransaction {
version: EXPECTED_VERSION,
nonce: Nonce(StarkHash::ZERO),
contract_address: contract_address!(l2_contract_address),
entry_point_selector: EntryPointSelector(StarkHash::from_hex_unchecked(l2_entry_point)),
calldata: Calldata(
vec![DEFAULT_ANVIL_ACCOUNT_ADDRESS, StarkHash::ONE, StarkHash::from(2)].into(),
),
};
let tx = ExecutableL1HandlerTransaction {
tx_hash: expected_internal_l1_tx
.calculate_transaction_hash(&scraper.config.chain_id, &EXPECTED_VERSION)
.unwrap(),
tx: expected_internal_l1_tx,
paid_fee_on_l1: Fee(0),
};
let first_expected_log = Event::L1HandlerTransaction(tx.clone());

let expected_internal_l1_tx_2 = L1HandlerTransaction {
nonce: Nonce(StarkHash::ONE),
calldata: Calldata(
vec![DEFAULT_ANVIL_ACCOUNT_ADDRESS, StarkHash::from(3), StarkHash::from(4)].into(),
),
..tx.tx
};
let second_expected_log = Event::L1HandlerTransaction(ExecutableL1HandlerTransaction {
tx_hash: expected_internal_l1_tx_2
.calculate_transaction_hash(&scraper.config.chain_id, &EXPECTED_VERSION)
.unwrap(),
tx: expected_internal_l1_tx_2,
..tx
});

// Assert.
scraper.fetch_events().await.unwrap();
fake_client.assert_add_events_received_with(&[first_expected_log, second_expected_log]);

// Previous events had been scraped, should no longer appear.
scraper.fetch_events().await.unwrap();
fake_client.assert_add_events_received_with(&[]);
}

#[tokio::test]
#[ignore = "Not yet implemented: generate an l1 and an cancel event for that tx, also check an \
abort for a different tx"]
async fn cancel_l1_handlers() {}

#[tokio::test]
#[ignore = "Not yet implemented: check that when the scraper resets all txs from the last T time
are processed"]
async fn reset() {}

#[tokio::test]
#[ignore = "Not yet implemented: check successful consume."]
async fn consume() {}

0 comments on commit a7b7801

Please sign in to comment.