Skip to content

Commit

Permalink
dev: update nonce for evm accounts (#513)
Browse files Browse the repository at this point in the history
* feat: add eoa and ca class hash to config

* dev: update nonce method for eoa and ca

* mock get_implementation

* dev: fix handling of get_implementation

* dev: use contract account class hash

* dev: add get_nonce integration test

* dev: refactor env var

* dev: modify test

* refactor: use ethers

* chore: fix comment

* dev: rename to account due to entrypoints

* bump: kakarot

* chore: avoid format! to improve error

* chore: improve docs

* chore: use Felt252Wrapper

* chore: simplify conversion

* test: add unit test for nonce and implementation

* chore: remove padding

* dev: propagate error for EnvironmentVariableSetWrong

* dev: use proper implementation

* dev: handle other errors

* chore: improve naming

* dev: remove redundant ContractNotFound

---------

Co-authored-by: Gregory Edison <gregory.edison1993@gmail.com>
  • Loading branch information
ftupas and greged93 authored Sep 15, 2023
1 parent 85ec7e8 commit 388f807
Show file tree
Hide file tree
Showing 26 changed files with 699 additions and 101 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ KAKAROT_HTTP_RPC_ADDRESS=0.0.0.0:3030
## check `./deployments/katana/deployments.json` after running `make devnet`
KAKAROT_ADDRESS=
PROXY_ACCOUNT_CLASS_HASH=0xba8f3f34eb92f56498fdf14ecac1f19d507dcc6859fa6d85eb8545370654bd
EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH=0x4730612e9d26ebca8dd27be1af79cea613f7dee43f5b1584a172040e39f4063
CONTRACT_ACCOUNT_CLASS_HASH=0x5599cc38b46f92273f8c3566810c292352beb857816d0d90b05afa967995b21

## configurations for testing
COMPILED_KAKAROT_PATH=lib/kakarot/build
Expand Down
74 changes: 45 additions & 29 deletions crates/core/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ use url::Url;
use super::constants::{KATANA_RPC_URL, MADARA_RPC_URL};
use super::errors::ConfigError;

fn get_env_var(name: &str) -> Result<String, ConfigError> {
fn env_var(name: &str) -> Result<String, ConfigError> {
std::env::var(name).map_err(|_| ConfigError::EnvironmentVariableMissing(name.into()))
}

fn field_element_from_env(var_name: &str) -> Result<FieldElement, ConfigError> {
let env_var = env_var(var_name)?;

FieldElement::from_hex_be(&env_var)
.map_err(|err| ConfigError::EnvironmentVariableSetWrong(var_name.into(), err.to_string()))
}

#[derive(Default, Clone, Debug)]
pub enum Network {
#[default]
Expand Down Expand Up @@ -61,19 +68,35 @@ pub struct KakarotRpcConfig {
pub kakarot_address: FieldElement,
/// Proxy account class hash.
pub proxy_account_class_hash: FieldElement,
/// EOA class hash.
pub externally_owned_account_class_hash: FieldElement,
/// Contract Account class hash.
pub contract_account_class_hash: FieldElement,
}

impl KakarotRpcConfig {
pub fn new(network: Network, kakarot_address: FieldElement, proxy_account_class_hash: FieldElement) -> Self {
KakarotRpcConfig { network, kakarot_address, proxy_account_class_hash }
pub fn new(
network: Network,
kakarot_address: FieldElement,
proxy_account_class_hash: FieldElement,
externally_owned_account_class_hash: FieldElement,
contract_account_class_hash: FieldElement,
) -> Self {
KakarotRpcConfig {
network,
kakarot_address,
proxy_account_class_hash,
externally_owned_account_class_hash,
contract_account_class_hash,
}
}

/// Create a new `StarknetConfig` from environment variables.
/// When using non-standard providers (i.e. not "katana", "madara", "mainnet"), the
/// `STARKNET_NETWORK` environment variable should be set the URL of a JsonRpc
/// starknet provider, e.g. https://starknet-goerli.g.alchemy.com/v2/some_key.
pub fn from_env() -> Result<Self, ConfigError> {
let network = get_env_var("STARKNET_NETWORK")?;
let network = env_var("STARKNET_NETWORK")?;
let network = match network.to_lowercase().as_str() {
"katana" => Network::Katana,
"madara" => Network::Madara,
Expand All @@ -85,21 +108,18 @@ impl KakarotRpcConfig {
network_url => Network::JsonRpcProvider(Url::parse(network_url)?),
};

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

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

Ok(KakarotRpcConfig::new(network, kakarot_address, proxy_account_class_hash))
let kakarot_address = field_element_from_env("KAKAROT_ADDRESS")?;
let proxy_account_class_hash = field_element_from_env("PROXY_ACCOUNT_CLASS_HASH")?;
let externally_owned_account_class_hash = field_element_from_env("EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH")?;
let contract_account_class_hash = field_element_from_env("CONTRACT_ACCOUNT_CLASS_HASH")?;

Ok(KakarotRpcConfig::new(
network,
kakarot_address,
proxy_account_class_hash,
externally_owned_account_class_hash,
contract_account_class_hash,
))
}
}

Expand Down Expand Up @@ -170,18 +190,14 @@ 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_private_key = env_var("DEPLOYER_ACCOUNT_PRIVATE_KEY")?;
let starknet_account_private_key = FieldElement::from_hex_be(&starknet_account_private_key).map_err(|err| {
ConfigError::EnvironmentVariableSetWrong("DEPLOYER_ACCOUNT_PRIVATE_KEY".into(), err.to_string())
})?;

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}"
))
let starknet_account_address = env_var("DEPLOYER_ACCOUNT_ADDRESS")?;
let starknet_account_address = FieldElement::from_hex_be(&starknet_account_address).map_err(|err| {
ConfigError::EnvironmentVariableSetWrong("DEPLOYER_ACCOUNT_ADDRESS".into(), err.to_string())
})?;
(starknet_account_private_key, starknet_account_address)
};
Expand Down
2 changes: 2 additions & 0 deletions crates/core/src/client/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub mod selectors {

pub const BYTECODE: FieldElement = selector!("bytecode");
pub const STORAGE: FieldElement = selector!("storage");
pub const GET_IMPLEMENTATION: FieldElement = selector!("get_implementation");
pub const GET_NONCE: FieldElement = selector!("get_nonce");

pub const ETH_CALL: FieldElement = selector!("eth_call");
pub const ETH_SEND_TRANSACTION: FieldElement = selector!("eth_send_transaction");
Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/client/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ pub enum ConfigError {
#[error("Missing mandatory environment variable: {0}")]
EnvironmentVariableMissing(String),
/// Environment variable set wrong error.
#[error("{0}")]
EnvironmentVariableSetWrong(String),
#[error("Environment variable {0} set wrong: {1}")]
EnvironmentVariableSetWrong(String, String),
/// Invalid URL error.
#[error("Invalid URL: {0}")]
InvalidUrl(#[from] url::ParseError),
Expand Down
52 changes: 36 additions & 16 deletions crates/core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,21 @@ impl<P: Provider + Send + Sync + 'static> KakarotClient<P> {
starknet_provider: Arc<P>,
starknet_account: SingleOwnerAccount<Arc<P>, LocalWallet>,
) -> Self {
let KakarotRpcConfig { kakarot_address, proxy_account_class_hash, network } = starknet_config;

let kakarot_contract =
KakarotContract::new(Arc::clone(&starknet_provider), kakarot_address, proxy_account_class_hash);
let KakarotRpcConfig {
kakarot_address,
proxy_account_class_hash,
externally_owned_account_class_hash,
contract_account_class_hash,
network,
} = starknet_config;

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

Self { starknet_provider, network, kakarot_contract, deployer_account: starknet_account }
}
Expand Down Expand Up @@ -322,26 +333,35 @@ impl<P: Provider + Send + Sync + 'static> KakarotEthApi<P> for KakarotClient<P>
}

/// Returns the nonce for a given ethereum address
/// if it's an EOA, use native nonce and if it's a contract account, use managed nonce
/// if ethereum -> stark mapping doesn't exist in the starknet provider, we translate
/// ContractNotFound errors into zeros
async fn nonce(&self, ethereum_address: Address, block_id: BlockId) -> Result<U256, EthApiError<P::Error>> {
let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?;
let starknet_address = self.compute_starknet_address(ethereum_address, &starknet_block_id).await?;

self.starknet_provider
.get_nonce(starknet_block_id, starknet_address)
.await
.map(|nonce| {
let nonce: Felt252Wrapper = nonce.into();
nonce.into()
})
.or_else(|err| match err {
ProviderError::StarknetError(StarknetErrorWithMessage {
// Get the implementation of the account
let account = KakarotAccount::new(starknet_address, &self.starknet_provider);
let class_hash = match account.implementation(&starknet_block_id).await {
Ok(class_hash) => class_hash,
Err(err) => match err {
EthApiError::RequestError(ProviderError::StarknetError(StarknetErrorWithMessage {
code: MaybeUnknownErrorCode::Known(StarknetError::ContractNotFound),
..
}) => Ok(U256::from(0)),
_ => Err(EthApiError::from(err)),
})
})) => return Ok(U256::from(0)), // Return 0 if the account doesn't exist
_ => return Err(err), // Propagate the error
},
};

if class_hash == self.kakarot_contract.contract_account_class_hash {
// Get the nonce of the contract account
let contract_account = ContractAccount::new(starknet_address, &self.starknet_provider);
contract_account.nonce(&starknet_block_id).await
} else {
// Get the nonce of the EOA
let nonce = self.starknet_provider.get_nonce(starknet_block_id, starknet_address).await?;
Ok(Felt252Wrapper::from(nonce).into())
}
}

/// Returns the balance in Starknet's native token of a specific EVM address.
Expand Down
6 changes: 5 additions & 1 deletion crates/core/src/client/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ async fn test_block_number() {
#[tokio::test]
async fn test_nonce() {
// Given
let fixtures = fixtures(vec![wrap_kakarot!(JsonRpcMethod::GetNonce), AvailableFixtures::ComputeStarknetAddress]);
let fixtures = fixtures(vec![
wrap_kakarot!(JsonRpcMethod::GetNonce),
AvailableFixtures::ComputeStarknetAddress,
AvailableFixtures::GetImplementation,
]);
let client = init_mock_client(Some(fixtures));

// When
Expand Down
22 changes: 21 additions & 1 deletion crates/core/src/contracts/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use starknet::core::types::{BlockId, FunctionCall, StarknetError};
use starknet::providers::{MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage};
use starknet_crypto::FieldElement;

use crate::client::constants::selectors::{BYTECODE, GET_EVM_ADDRESS};
use crate::client::constants::selectors::{BYTECODE, GET_EVM_ADDRESS, GET_IMPLEMENTATION};
use crate::client::errors::EthApiError;
use crate::client::helpers::{vec_felt_to_bytes, DataDecodingError};
use crate::models::felt::Felt252Wrapper;
Expand Down Expand Up @@ -54,6 +54,26 @@ pub trait Account<'a, P: Provider + Send + Sync + 'a> {
// TODO: Remove Manual Decoding
Ok(vec_felt_to_bytes(bytecode[1..].to_vec()))
}

/// Returns the class hash of account implementation of the contract.
async fn implementation(&self, block_id: &BlockId) -> Result<FieldElement, EthApiError<P::Error>> {
// Prepare the calldata for the get_implementation function call
let calldata = vec![];
let request = FunctionCall {
contract_address: self.starknet_address(),
entry_point_selector: GET_IMPLEMENTATION,
calldata,
};

// Make the function call to get the Starknet contract address
let class_hash = self.provider().call(request, block_id).await?;
let class_hash = *class_hash.first().ok_or_else(|| DataDecodingError::InvalidReturnArrayLength {
entrypoint: "get_implementation".into(),
expected: 1,
actual: 0,
})?;
Ok(class_hash)
}
}

pub struct KakarotAccount<'a, P> {
Expand Down
25 changes: 24 additions & 1 deletion crates/core/src/contracts/contract_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use starknet::providers::Provider;
use starknet_crypto::FieldElement;

use super::account::Account;
use crate::client::constants::selectors::STORAGE;
use crate::client::constants::selectors::{GET_NONCE, STORAGE};
use crate::client::errors::EthApiError;
use crate::client::helpers::DataDecodingError;
use crate::models::felt::Felt252Wrapper;
Expand Down Expand Up @@ -58,4 +58,27 @@ impl<'a, P: Provider + Send + Sync> ContractAccount<'a, P> {
let value = Into::<U256>::into(low) + (Into::<U256>::into(high) << 128);
Ok(value)
}

/// Returns the nonce of the contract account.
/// In Kakarot EVM, there are two types of accounts: EOA and Contract Account.
/// EOA nonce is handled by Starknet protocol.
/// Contract Account nonce is handled by Kakarot through a dedicated storage, this function
/// returns that storage value.
pub async fn nonce(&self, block_id: &BlockId) -> Result<U256, EthApiError<P::Error>> {
// Prepare the calldata for the get_nonce function call
let calldata = vec![];
let request = FunctionCall { contract_address: self.address, entry_point_selector: GET_NONCE, calldata };

let result = self.provider.call(request, block_id).await?;
if result.len() != 1 {
return Err(DataDecodingError::InvalidReturnArrayLength {
entrypoint: "get_nonce".into(),
expected: 1,
actual: result.len(),
}
.into());
}

Ok(Felt252Wrapper::from(result[0]).into())
}
}
18 changes: 16 additions & 2 deletions crates/core/src/contracts/kakarot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,26 @@ use crate::client::waiter::TransactionWaiter;
pub struct KakarotContract<P> {
pub address: FieldElement,
pub proxy_account_class_hash: FieldElement,
pub externally_owned_account_class_hash: FieldElement,
pub contract_account_class_hash: FieldElement,
provider: Arc<P>,
}

impl<P: Provider + Send + Sync + 'static> KakarotContract<P> {
pub fn new(provider: Arc<P>, address: FieldElement, proxy_account_class_hash: FieldElement) -> Self {
Self { address, proxy_account_class_hash, provider }
pub fn new(
provider: Arc<P>,
address: FieldElement,
proxy_account_class_hash: FieldElement,
externally_owned_account_class_hash: FieldElement,
contract_account_class_hash: FieldElement,
) -> Self {
Self {
address,
proxy_account_class_hash,
externally_owned_account_class_hash,
contract_account_class_hash,
provider,
}
}

pub async fn compute_starknet_address(
Expand Down
38 changes: 38 additions & 0 deletions crates/core/src/contracts/tests/account.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,53 @@
#[cfg(test)]
mod tests {

use std::sync::Arc;

use kakarot_rpc_core::client::api::KakarotStarknetApi;
use kakarot_test_utils::deploy_helpers::{
get_contract, get_contract_deployed_bytecode, KakarotTestEnvironmentContext,
};
use kakarot_test_utils::fixtures::kakarot_test_env_ctx;
use reth_primitives::U256;
use rstest::*;
use starknet::core::types::{BlockId, BlockTag};
use starknet_crypto::FieldElement;

use crate::contracts::account::{Account, KakarotAccount};
use crate::contracts::contract_account::ContractAccount;
use crate::mock::constants::ABDEL_STARKNET_ADDRESS;
use crate::mock::mock_starknet::{fixtures, mock_starknet_provider, AvailableFixtures};

#[tokio::test]
async fn test_nonce() {
// Given
let fixtures = fixtures(vec![AvailableFixtures::GetNonce]);
let starknet_provider = Arc::new(mock_starknet_provider(Some(fixtures)));
let contract_account = ContractAccount::new(*ABDEL_STARKNET_ADDRESS, &starknet_provider);

// When
let nonce = contract_account.nonce(&BlockId::Tag(BlockTag::Latest)).await.unwrap();

// Then
assert_eq!(U256::from(1), nonce);
}

#[tokio::test]
async fn test_implementation() {
// Given
let fixtures = fixtures(vec![AvailableFixtures::GetImplementation]);
let starknet_provider = Arc::new(mock_starknet_provider(Some(fixtures)));
let account = KakarotAccount::new(*ABDEL_STARKNET_ADDRESS, &starknet_provider);

// When
let implementation = account.implementation(&BlockId::Tag(BlockTag::Latest)).await.unwrap();

// Then
assert_eq!(
FieldElement::from_hex_be("0x4730612e9d26ebca8dd27be1af79cea613f7dee43f5b1584a172040e39f4063").unwrap(),
implementation
);
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
Expand Down
Loading

0 comments on commit 388f807

Please sign in to comment.