Skip to content

Commit

Permalink
feat: add automatic deployment of EOA to RPC (#431)
Browse files Browse the repository at this point in the history
* feat: add automatic deployment of EOA

* dev: add Pooling Failed prefix to errors

* feat: update `fund_and_deploy_eoa` function, to add *DEPLOY_FEE automatically

---------

Co-authored-by: Harsh Bajpai <harshbajpai@Harshs-MacBook-Pro.local>
  • Loading branch information
bajpai244 and Harsh Bajpai authored Aug 22, 2023
1 parent dad7b16 commit 8592cb0
Show file tree
Hide file tree
Showing 24 changed files with 548 additions and 60 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ KATANA_PRIVATE_KEY=0x03000018000000003000001800000000000300000000000030060018000
MADARA_ACCOUNT_ADDRESS=0x3
MADARA_PRIVATE_KEY=0x00c1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d

# configuration for the eoa deployer account
DEPLOYER_ACCOUNT_ADDRESS=
DEPLOYER_ACCOUNT_PRIVATE_KEY=0x0288a51c164874bb6a1ca7bd1cb71823c234a86d0f7b150d70fa8f06de645396

# Kakarot Environment
KAKAROT_HTTP_RPC_ADDRESS=0.0.0.0:3030
## check `./deployments/katana/deployments.json` after running `make devnet`
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

### Added

- feat: add automatic deployment of eoa from RPC if an account doesn't exist on chain
- ci: decrease the rate which we send transactions in benchmark CI
- fix: update the hive genesis utils for missing Kakarot requirements.
- ci: use madara binary for benchmark CI
Expand Down
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,15 @@ Here is the list of all the available environment variables:

<!-- markdownlint-disable MD013 -->

| Name | Default value | Description |
| ------------------------ | ------------------------- | ------------------------ |
| TARGET_RPC_URL | <http://0.0.0.0:5050/rpc> | Target Starknet RPC URL |
| RUST_LOG | Debug | Log level |
| KAKAROT_HTTP_RPC_ADDRESS | 0.0.0.0:3030 | Kakarot RPC URL |
| KAKAROT_ADDRESS | see below | Kakarot address |
| PROXY_ACCOUNT_CLASS_HASH | see below | Proxy account class hash |
| Name | Default value | Description |
| ----------------------------- | ------------------------- | ---------------------------- |
| TARGET_RPC_URL | <http://0.0.0.0:5050/rpc> | Target Starknet RPC URL |
| RUST_LOG | Debug | Log level |
| KAKAROT_HTTP_RPC_ADDRESS | 0.0.0.0:3030 | Kakarot RPC URL |
| KAKAROT_ADDRESS | see below | Kakarot address |
| PROXY_ACCOUNT_CLASS_HASH | see below | Proxy account class hash |
| DEPLOYER_ACCOUNT_ADDRESS | N/A | Deployer Account Address |
| DEPLOYER_ACCOUNT_PRIVATE_KEY | see below | Deployer Account Private Key |

<!-- markdownlint-enable MD013 -->

Expand All @@ -227,6 +229,18 @@ Declared:
The Counter contract implementation can be found
[here](https://github.com/sayajin-labs/kakarot/blob/main/tests/integration/solidity_contracts/PlainOpcodes/Counter.sol)

### Deployer Account
The Kakarot RPC requires a funded deployer account to deploy ethereum EOAs whose on-chain smart contract don't exist, the role of
the deployer is to deploy these accounts for a smoother UX { the deployer recovers the amount spent of this deployments }

The kakarot [deploy scripts](https://github.com/kkrt-labs/kakarot/blob/9773e4d10a3c3a32fb8aa3cfbf6fdbff35d6985e/scripts/deploy_kakarot.py#L67) deploy and fund an account with the private key "0x0288a51c164874bb6a1ca7bd1cb71823c234a86d0f7b150d70fa8f06de645396" for [Katana](https://github.com/dojoengine/dojo/tree/main/crates/katana) and [Madara](https://github.com/keep-starknet-strange/madara), the address of this account can be found in the file `deployments/{network}/deployments.json` with the key `deployer_account` after running this script on [Kakarot](https://github.com/kkrt-labs/kakarot).

You can configure Kakarot RPC to run with a particular Deployer Account via the following environment variables:
- `DEPLOYER_ACCOUNT_ADDRESS`
- `DEPLOYER_ACCOUNT_PRIVATE_KEY`

When running in production on testnet and mainnet it is advised to have a separate pre-funded account for this.

### API

You can take a look at `rpc-call-examples` directory. Please note the following:
Expand Down
18 changes: 18 additions & 0 deletions crates/core/src/client/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ use reth_rpc_types::{
BlockTransactions, CallRequest, FeeHistory, Filter, FilterChanges, Index, RichBlock, SyncStatus,
Transaction as EtherTransaction, TransactionReceipt,
};
use starknet::accounts::SingleOwnerAccount;
use starknet::core::types::{
BlockId as StarknetBlockId, BroadcastedInvokeTransactionV1, EmittedEvent, EventFilterWithPage, FieldElement,
};
use starknet::providers::sequencer::models::TransactionSimulationInfo;
use starknet::providers::Provider;
use starknet::signers::LocalWallet;

use super::errors::EthApiError;
use crate::models::balance::TokenBalances;
Expand Down Expand Up @@ -88,6 +90,8 @@ pub trait KakarotStarknetApi<P: Provider + Send + Sync>: Send + Sync {

fn starknet_provider(&self) -> Arc<P>;

fn deployer_account(&self) -> &SingleOwnerAccount<Arc<P>, LocalWallet>;

async fn map_block_id_to_block_number(&self, block_id: &StarknetBlockId) -> Result<u64, EthApiError<P::Error>>;

async fn submit_starknet_transaction(
Expand Down Expand Up @@ -128,4 +132,18 @@ pub trait KakarotStarknetApi<P: Provider + Send + Sync>: Send + Sync {
) -> Result<TransactionSimulationInfo, EthApiError<P::Error>>;

async fn filter_events(&self, request: EventFilterWithPage) -> Result<Vec<EmittedEvent>, EthApiError<P::Error>>;

async fn check_eoa_account_exists(
&self,
ethereum_address: Address,
starknet_block_id: &StarknetBlockId,
) -> Result<bool, EthApiError<P::Error>>;

async fn deploy_eoa(&self, ethereum_address: Address) -> Result<FieldElement, EthApiError<P::Error>>;

async fn wait_for_confirmation_on_l2(
&self,
transaction_hash: FieldElement,
max_retries: u64,
) -> Result<bool, EthApiError<P::Error>>;
}
32 changes: 31 additions & 1 deletion crates/core/src/client/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::sync::Arc;

use eyre::Result;
use starknet::accounts::SingleOwnerAccount;
use starknet::core::types::FieldElement;
use starknet::providers::jsonrpc::{HttpTransport, JsonRpcTransport};
use starknet::providers::{JsonRpcClient, SequencerGatewayProvider};
use starknet::providers::{JsonRpcClient, Provider, SequencerGatewayProvider};
use starknet::signers::{LocalWallet, SigningKey};
use url::Url;

use super::constants::{KATANA_RPC_URL, MADARA_RPC_URL};
Expand Down Expand Up @@ -161,3 +165,29 @@ impl SequencerGatewayProviderBuilder {
self.0
}
}

pub async fn get_starknet_account_from_env<P: Provider + Send + Sync + 'static>(
provider: Arc<P>,
) -> Result<SingleOwnerAccount<Arc<P>, LocalWallet>> {
let (starknet_account_private_key, starknet_account_address) = {
let starknet_account_private_key = get_env_var("DEPLOYER_ACCOUNT_PRIVATE_KEY")?;
let starknet_account_private_key = FieldElement::from_hex_be(&starknet_account_private_key).map_err(|_| {
ConfigError::EnvironmentVariableSetWrong(format!(
"DEPLOYER_ACCOUNT_PRIVATE_KEY should be provided as a hex string, got {starknet_account_private_key}"
))
})?;

let starknet_account_address = get_env_var("DEPLOYER_ACCOUNT_ADDRESS")?;
let starknet_account_address = FieldElement::from_hex_be(&starknet_account_address).map_err(|_| {
ConfigError::EnvironmentVariableSetWrong(format!(
"DEPLOYER_ACCOUNT_ADDRESS should be provided as a hex string, got {starknet_account_private_key}"
))
})?;
(starknet_account_private_key, starknet_account_address)
};

let chain_id = provider.chain_id().await?;

let local_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar(starknet_account_private_key));
Ok(SingleOwnerAccount::new(provider.clone(), local_wallet, starknet_account_address, chain_id))
}
6 changes: 6 additions & 0 deletions crates/core/src/client/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod selectors {

pub const ETH_CALL: FieldElement = selector!("eth_call");
pub const ETH_SEND_TRANSACTION: FieldElement = selector!("eth_send_transaction");
pub const DEPLOY_EXTERNALLY_OWNED_ACCOUNT: FieldElement = selector!("deploy_externally_owned_account");
pub const COMPUTE_STARKNET_ADDRESS: FieldElement = selector!("compute_starknet_address");

pub const GET_EVM_ADDRESS: FieldElement = selector!("get_evm_address");
Expand Down Expand Up @@ -118,3 +119,8 @@ lazy_static! {
pub static ref COUNTER_CALL_TESTNET2: Call =
StarknetCall { to: *COUNTER_ADDRESS_TESTNET2, selector: *INC_SELECTOR, calldata: vec![] }.into();
}

// This module contains constants to be used for deployment of Kakarot System
lazy_static! {
pub static ref DEPLOY_FEE: FieldElement = FieldElement::from(100000_u64);
}
126 changes: 118 additions & 8 deletions crates/core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ use reth_rpc_types::{
BlockTransactions, CallRequest, FeeHistory, Filter, FilterChanges, Index, RichBlock, SyncInfo, SyncStatus,
Transaction as EtherTransaction, TransactionReceipt,
};
use starknet::accounts::SingleOwnerAccount;
use starknet::core::types::{
BlockId as StarknetBlockId, BlockTag, BroadcastedInvokeTransaction, BroadcastedInvokeTransactionV1, EmittedEvent,
Event, EventFilterWithPage, EventsPage, FieldElement, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs,
MaybePendingTransactionReceipt, ResultPageRequest, StarknetError, SyncStatusType, Transaction as TransactionType,
TransactionReceipt as StarknetTransactionReceipt,
TransactionReceipt as StarknetTransactionReceipt, TransactionStatus as StarknetTransactionStatus,
};
use starknet::providers::sequencer::models::{FeeEstimate, FeeUnit, TransactionSimulationInfo, TransactionTrace};
use starknet::providers::{Provider, ProviderError};
use starknet::signers::LocalWallet;
use tokio::time::{sleep, Duration};

use self::api::{KakarotEthApi, KakarotStarknetApi};
use self::config::{Network, StarknetConfig};
Expand Down Expand Up @@ -59,26 +62,29 @@ use crate::models::ConversionError;

pub struct KakarotClient<P: Provider + Send + Sync> {
starknet_provider: Arc<P>,
deployer_account: SingleOwnerAccount<Arc<P>, LocalWallet>,
kakarot_contract: KakarotContract<P>,
network: Network,
}

impl<P: Provider + Send + Sync> KakarotClient<P> {
impl<P: Provider + Send + Sync + 'static> KakarotClient<P> {
/// Create a new `KakarotClient`.
pub fn new(starknet_config: StarknetConfig, starknet_provider: P) -> Self {
pub fn new(
starknet_config: StarknetConfig,
starknet_provider: Arc<P>,
starknet_account: SingleOwnerAccount<Arc<P>, LocalWallet>,
) -> Self {
let StarknetConfig { kakarot_address, proxy_account_class_hash, network } = starknet_config;

let starknet_provider = Arc::new(starknet_provider);

let kakarot_contract =
KakarotContract::new(Arc::clone(&starknet_provider), kakarot_address, proxy_account_class_hash);

Self { starknet_provider, network, kakarot_contract }
Self { starknet_provider, network, kakarot_contract, deployer_account: starknet_account }
}
}

#[async_trait]
impl<P: Provider + Send + Sync> KakarotEthApi<P> for KakarotClient<P> {
impl<P: Provider + Send + Sync + 'static> KakarotEthApi<P> for KakarotClient<P> {
/// Returns the latest block number
async fn block_number(&self) -> Result<U64, EthApiError<P::Error>> {
let block_number = self.starknet_provider.block_number().await?;
Expand Down Expand Up @@ -400,6 +406,13 @@ impl<P: Provider + Send + Sync> KakarotEthApi<P> for KakarotClient<P> {

let starknet_block_id = StarknetBlockId::Tag(BlockTag::Latest);

let account_exists = self.check_eoa_account_exists(evm_address, &starknet_block_id).await?;
if !account_exists {
let starknet_transaction_hash: FieldElement =
Felt252Wrapper::from(self.deploy_eoa(evm_address).await?).into();
let _ = self.wait_for_confirmation_on_l2(starknet_transaction_hash, 10).await?;
}

let starknet_address = self.compute_starknet_address(evm_address, &starknet_block_id).await?;

let nonce = FieldElement::from(transaction.nonce());
Expand Down Expand Up @@ -560,7 +573,7 @@ impl<P: Provider + Send + Sync> KakarotEthApi<P> for KakarotClient<P> {
}

#[async_trait]
impl<P: Provider + Send + Sync> KakarotStarknetApi<P> for KakarotClient<P> {
impl<P: Provider + Send + Sync + 'static> KakarotStarknetApi<P> for KakarotClient<P> {
/// Returns the Kakarot contract address.
fn kakarot_address(&self) -> FieldElement {
self.kakarot_contract.address
Expand All @@ -576,6 +589,11 @@ impl<P: Provider + Send + Sync> KakarotStarknetApi<P> for KakarotClient<P> {
Arc::clone(&self.starknet_provider)
}

/// Returns a reference to the starknet account used for deployment
fn deployer_account(&self) -> &SingleOwnerAccount<Arc<P>, LocalWallet> {
&self.deployer_account
}

/// Returns the Starknet block number for a given block id.
async fn map_block_id_to_block_number(&self, block_id: &StarknetBlockId) -> Result<u64, EthApiError<P::Error>> {
match block_id {
Expand Down Expand Up @@ -757,4 +775,96 @@ impl<P: Provider + Send + Sync> KakarotStarknetApi<P> for KakarotClient<P> {

Ok(events)
}

async fn check_eoa_account_exists(
&self,
ethereum_address: Address,
starknet_block_id: &StarknetBlockId,
) -> Result<bool, EthApiError<P::Error>> {
let eoa_account_starknet_address = self.compute_starknet_address(ethereum_address, starknet_block_id).await?;

let result = self.get_evm_address(&eoa_account_starknet_address, starknet_block_id).await;

let result: Result<bool, EthApiError<<P as Provider>::Error>> = match result {
Ok(_) => Ok(true),
Err(error) => match error {
EthApiError::RequestError(error) => match error {
ProviderError::StarknetError(error) => match error {
StarknetError::ContractNotFound => Ok(false),
_ => Err(EthApiError::from(ProviderError::StarknetError(error))),
},
_ => Err(EthApiError::from(error)),
},
_ => Err(error),
},
};

Ok(result?)
}

async fn deploy_eoa(&self, ethereum_address: Address) -> Result<FieldElement, EthApiError<P::Error>> {
let ethereum_address: FieldElement = Felt252Wrapper::from(ethereum_address).into();
self.kakarot_contract.deploy_externally_owned_account(ethereum_address, &self.deployer_account).await
}

/// given a transaction hash, waits for it to be confirmed on L2
/// each retry is every 1 second, tries for max_retries*1s and then timeouts after that
/// returns a boolean indicating whether the transaction was accepted or not L2, it will always
/// be true, otherwise should return error
async fn wait_for_confirmation_on_l2(
&self,
transaction_hash: FieldElement,
max_retries: u64,
) -> Result<bool, EthApiError<P::Error>> {
let mut idx = 0;

loop {
if idx >= max_retries {
return Err(EthApiError::Other(anyhow::format_err!(
"timeout: max retries: {max_retries} attempted for polling the transaction receipt"
)));
}

let receipt = self.starknet_provider.get_transaction_receipt(transaction_hash).await;

match receipt {
Ok(receipt) => match receipt {
MaybePendingTransactionReceipt::Receipt(receipt) => match receipt {
StarknetTransactionReceipt::Invoke(receipt) => match receipt.status {
StarknetTransactionStatus::AcceptedOnL2 | StarknetTransactionStatus::AcceptedOnL1 => {
return Ok(true);
}
StarknetTransactionStatus::Rejected => {
return Err(EthApiError::Other(anyhow::anyhow!(
"Pooling Failed: the transaction {transaction_hash} has been rejected"
)));
}
_ => (),
},
_ => {
return Err(EthApiError::Other(anyhow::anyhow!(
"Pooling Failed: expected an invoke receipt, found {receipt:#?}"
)));
}
},
MaybePendingTransactionReceipt::PendingReceipt(_) => (),
},
Err(error) => {
match error {
ProviderError::StarknetError(StarknetError::TransactionHashNotFound) => {
// do nothing because to comply with json-rpc spec, even in case of
// TransactionStatus::Received an error will be returned, so we should
// continue polling see: https://github.com/xJonathanLEI/starknet-rs/blob/832c6cdc36e5899cf2c82f8391a4dd409650eed1/starknet-providers/src/sequencer/provider.rs#L134C1-L134C1
}
_ => {
return Err(error.into());
}
}
}
};

idx += 1;
sleep(Duration::from_secs(1)).await;
}
}
}
2 changes: 1 addition & 1 deletion crates/core/src/contracts/erc20/ethereum_erc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct EthereumErc20<'a, P> {
kakarot_contract: &'a KakarotContract<P>,
}

impl<'a, P: Provider + Send + Sync> EthereumErc20<'a, P> {
impl<'a, P: Provider + Send + Sync + 'static> EthereumErc20<'a, P> {
pub fn new(address: FieldElement, kakarot_contract: &'a KakarotContract<P>) -> Self {
Self { address, kakarot_contract }
}
Expand Down
Loading

0 comments on commit 8592cb0

Please sign in to comment.