Skip to content

Commit

Permalink
Account impersonation (#436)
Browse files Browse the repository at this point in the history
* initial commit

* clippy

* fmt

* remove println

* changes

* integration tests

* clippy

* clippy

* transaction simulation test

* updated readme

* changed names of params

* updated supported features

* updated readme

* Update README.md

Co-authored-by: FabijanC <[email protected]>

* updated readme

* rename method to set_auto_impersonate

* add check for impersonate_account to not be able to impersonate an account that is in the state

* changed test name

* renamed utility method

* use assert contains

* renamed spawn_forkable_devnet with spawn_with_forking

* use to_hex_felt

* renamed methods auto_impersonate to set_auto_impersonate

* change variable name and changed comment

* edited utility function name 

* edited integration test

* test improvement

* typo [skip ci]

* refactored integration tests

* clippy

* updated comment

* fixed non compiling tests

* Revert "fixed non compiling tests"

This reverts commit 611b65c.

* Revert "Merge branch 'main' into impersonate"

This reverts commit 98de0f6, reversing
changes made to 66f3273.

* Revert "Revert "Merge branch 'main' into impersonate""

This reverts commit b038b20.

* Revert "Revert "fixed non compiling tests""

This reverts commit dc0eed2.

---------

Co-authored-by: FabijanC <[email protected]>
  • Loading branch information
marioiordanov and FabijanC authored May 21, 2024
1 parent a6fc0a1 commit de07790
Show file tree
Hide file tree
Showing 15 changed files with 680 additions and 59 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This repository is work in progress, please be patient. Please check below the s
- [x] [Aborting blocks](#abort-blocks)
- [x] [Creating an empty block](#create-an-empty-block)
- [x] [Creating blocks on demand](#creating-blocks-on-demand)
- [x] [Account impersonation](#account-impersonation)

## Installation and running

Expand Down Expand Up @@ -466,6 +467,68 @@ $ starknet-devnet --state-archive-capacity <CAPACITY>

All RPC endpoints that support querying the state at an old (non-latest) block only work with state archive capacity set to `full`.

## Account impersonation

Devnet allows you to use impersonated account from mainnet/testnet. This means that a transaction sent from an impersonated account, will not fail with an invalid signature error. In the general case a transaction send via an account that is not in the local state, fails with `invalid signature` error. To use the feature it is required to start Devnet in [forked mode](#forking).

Account impersonation supports 4 methods:
- Impersonate specific account address, not existing in the local state (devnet_impersonateAccount)
- Stop impersonation of an account (devnet_stopImpersonateAccount)
- Automatic impersonation of an account. Every account that does not exist in the local state, will be considered as impersonated(devnet_autoImpersonate)
- Stop auto impersonation (devnet_stopAutoImpersonate)

Notes:
- Only INVOKE and DECLARE transactions are supported. DEPLOY_ACCOUNT transaction is not supported, but you can create an INVOKE transaction to UDC.
- Overall fee, for transactions sent via impersonated account, will be lower compared to normal transactions. The reason is that validation part is skipped.
- Sending transactions with account that **does not** exist will return 1 of the errors: ContractNotFound, InsufficientAccountBalance. Most common way of sending a transaction is via starknet-rs/starknet.js or starkli. If a nonce is not hardcoded, during transaction construction it will query devnet for the account nonce and will return ContractNotFound error. Otherwise it will skip the part with fetching account nonce and fail with InsufficientAccountBalance error.

Account impersonation feature follows JSON-RPC method specification and each method returns an empty response:

devnet_impersonateAccount
```js
{
"jsonrpc": "2.0",
"id": "1",
"method": "devnet_impersonateAccount",
"params": {
"account_address": "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7"
}
}
```

devnet_stopImpersonateAccount
```js
{
"jsonrpc": "2.0",
"id": "1",
"method": "devnet_stopImpersonateAccount",
"params": {
"account_address": "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7"
}
}
```

devnet_autoImpersonate
```js
{
"jsonrpc": "2.0",
"id": "1",
"method": "devnet_autoImpersonate",
"params": {}
}
```

devnet_stopAutoImpersonate
```js
{
"jsonrpc": "2.0",
"id": "1",
"method": "devnet_stopAutoImpersonate",
"params": {}
}
```


## Fetch Devnet configuration

To retrieve the current configuration of Devnet, send a GET request to `/config`. Example response is attached below. It can be interpreted as a JSON mapping of CLI input parameters, both specified and default ones, with some irrelevant parameters omitted. So use `starknet-devnet --help` to better understand the meaning of each value, though keep in mind that some of the parameters have slightly modified names.
Expand Down
49 changes: 28 additions & 21 deletions crates/starknet-devnet-core/src/starknet/add_declare_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,43 @@ pub fn add_declare_transaction(
let transaction_hash = blockifier_declare_transaction.tx_hash().0.into();
let class_hash = blockifier_declare_transaction.class_hash().0.into();

let (declare_transaction, contract_class) = match broadcasted_declare_transaction {
BroadcastedDeclareTransaction::V1(ref v1) => {
let declare_transaction = Transaction::Declare(DeclareTransaction::V1(
DeclareTransactionV0V1::new(v1, class_hash),
));
let (declare_transaction, contract_class, sender_address) =
match broadcasted_declare_transaction {
BroadcastedDeclareTransaction::V1(ref v1) => {
let declare_transaction = Transaction::Declare(DeclareTransaction::V1(
DeclareTransactionV0V1::new(v1, class_hash),
));

(declare_transaction, v1.contract_class.clone().into())
}
BroadcastedDeclareTransaction::V2(ref v2) => {
let declare_transaction = Transaction::Declare(DeclareTransaction::V2(
DeclareTransactionV2::new(v2, class_hash),
));
(declare_transaction, v1.contract_class.clone().into(), &v1.sender_address)
}
BroadcastedDeclareTransaction::V2(ref v2) => {
let declare_transaction = Transaction::Declare(DeclareTransaction::V2(
DeclareTransactionV2::new(v2, class_hash),
));

(declare_transaction, v2.contract_class.clone().into())
}
BroadcastedDeclareTransaction::V3(ref v3) => {
let declare_transaction = Transaction::Declare(DeclareTransaction::V3(
DeclareTransactionV3::new(v3, class_hash),
));
(declare_transaction, v2.contract_class.clone().into(), &v2.sender_address)
}
BroadcastedDeclareTransaction::V3(ref v3) => {
let declare_transaction = Transaction::Declare(DeclareTransaction::V3(
DeclareTransactionV3::new(v3, class_hash),
));

(declare_transaction, v3.contract_class.clone().into())
}
};
(declare_transaction, v3.contract_class.clone().into(), &v3.sender_address)
}
};

let validate = !(Starknet::is_account_impersonated(
&mut starknet.state,
&starknet.cheats,
sender_address,
)?);

let transaction = TransactionWithHash::new(transaction_hash, declare_transaction);
let blockifier_execution_result =
blockifier::transaction::account_transaction::AccountTransaction::Declare(
blockifier_declare_transaction,
)
.execute(&mut starknet.state.state, &starknet.block_context, true, true);
.execute(&mut starknet.state.state, &starknet.block_context, true, validate);

starknet.handle_transaction_result(
transaction,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use blockifier::transaction::transactions::ExecutableTransaction;
use starknet_types::contract_address::ContractAddress;
use starknet_types::felt::TransactionHash;
use starknet_types::rpc::transactions::invoke_transaction_v1::InvokeTransactionV1;
use starknet_types::rpc::transactions::invoke_transaction_v3::InvokeTransactionV3;
Expand Down Expand Up @@ -38,6 +39,12 @@ pub fn add_invoke_transaction(
}
};

let validate = !(Starknet::is_account_impersonated(
&mut starknet.state,
&starknet.cheats,
&ContractAddress::from(blockifier_invoke_transaction.sender_address()),
)?);

let block_context = starknet.block_context.clone();

let state = &mut starknet.get_state().state;
Expand All @@ -46,7 +53,7 @@ pub fn add_invoke_transaction(
blockifier::transaction::account_transaction::AccountTransaction::Invoke(
blockifier_invoke_transaction,
)
.execute(state, &block_context, true, true);
.execute(state, &block_context, true, validate);

let transaction = TransactionWithHash::new(transaction_hash, invoke_transaction);

Expand Down
24 changes: 24 additions & 0 deletions crates/starknet-devnet-core/src/starknet/cheats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::collections::HashSet;

use starknet_types::contract_address::ContractAddress;

#[derive(Default, Clone)]
pub(crate) struct Cheats {
impersonated_accounts: HashSet<ContractAddress>,
auto_impersonate: bool,
}

impl Cheats {
pub(crate) fn impersonate_account(&mut self, account: ContractAddress) {
self.impersonated_accounts.insert(account);
}
pub(crate) fn stop_impersonating_account(&mut self, account: &ContractAddress) {
self.impersonated_accounts.remove(account);
}
pub(crate) fn is_impersonated(&self, account: &ContractAddress) -> bool {
self.auto_impersonate || self.impersonated_accounts.contains(account)
}
pub(crate) fn set_auto_impersonate(&mut self, auto_impersonation: bool) {
self.auto_impersonate = auto_impersonation;
}
}
29 changes: 22 additions & 7 deletions crates/starknet-devnet-core/src/starknet/estimations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,40 @@ pub fn estimate_fee(
) -> DevnetResult<Vec<FeeEstimateWrapper>> {
let chain_id = starknet.chain_id().to_felt();
let block_context = starknet.block_context.clone();
let cheats = starknet.cheats.clone();
let state = starknet.get_mut_state_at(block_id)?;
let mut transactional_state = CachedState::create_transactional(&mut state.state);

let transactions = transactions
.iter()
.map(|txn| Ok(txn.to_blockifier_account_transaction(&chain_id)?))
.collect::<DevnetResult<Vec<AccountTransaction>>>()?;
let transactions = {
transactions
.iter()
.map(|txn| {
Ok((
txn.to_blockifier_account_transaction(&chain_id)?,
Starknet::should_transaction_skip_validation_if_sender_is_impersonated(
state, &cheats, txn,
)?,
))
})
.collect::<DevnetResult<Vec<(AccountTransaction, bool)>>>()?
};

let mut transactional_state = CachedState::create_transactional(&mut state.state);

transactions
.into_iter()
.map(|transaction| {
.map(|(transaction, skip_validate_due_to_impersonation)| {
estimate_transaction_fee(
&mut transactional_state,
&block_context,
blockifier::transaction::transaction_execution::Transaction::AccountTransaction(
transaction,
),
charge_fee,
validate,
skip_validate_due_to_impersonation.then_some(false).or(validate), /* if skip validate is true, then
* this means that this transaction
* has to skip validation, because
* the sender is impersonated.
* Otherwise use the validate parameter that is passed to the estimateFee request */
)
})
.collect()
Expand Down
Loading

0 comments on commit de07790

Please sign in to comment.