kkrt-labs / kakarot-rpc

Kakarot ZK EVM Ethereum RPC adapter
MIT License
131 stars 87 forks source link

feat: add execute from outside architecture #1310

Open tcoratger opened 1 month ago

tcoratger commented 1 month ago

In view of our launch on Starknet Testnet, we want to be able to execute transactions from outside on Kakarot.

To be able to execute such transactions through the RPC, we need to make some modifications to its architecture which will be described here:

Accounts setup

Account management (deployment and rebalancing)

#[derive(Clone)]
pub struct OZAccountDeployer {
    provider: Arc<dyn Provider>,
    deployment_code: Vec<u8>,
}

impl OZAccountDeployer {
    pub fn new(provider: Arc<dyn Provider>, deployment_code: Vec<u8>) -> Self {
        OZAccountDeployer {
            provider,
            deployment_code,
        }
    }

    /// Deploys a new OpenZeppelin account on the fly
    pub async fn deploy_new_account(&self) -> Result<FieldElement, SendTransactionError> {
        // Construct the transaction to deploy the account
        let deploy_tx = self.create_deploy_transaction()?;

        // Send the transaction to StarkNet
        let result = self.provider.send_transaction(&deploy_tx).await?;

        // Get the deployed account address from the result
        let account_address = result.contract_address;

        Ok(account_address)
    }

    /// Creates a deploy transaction for an OpenZeppelin account
    fn create_deploy_transaction(&self) -> Result<StarknetTransaction, SendTransactionError> {
        // Build the deploy transaction using the deployment code
        // Should be something like
        Ok(StarknetTransaction {
            calldata: self.deployment_code.clone(),
            ..Default::default()
        })
    }
}
#[derive(Clone)]
pub struct RebalancerService {
    provider: Arc<dyn Provider>,
    main_wallet: FieldElement,
    min_balance: u64,
    top_up_amount: u64,
    deployed_accounts: Arc<Mutex<Vec<FieldElement>>>, // To maintain a list with the deployed accounts
}

impl RebalancerService {
    pub fn new(
        provider: Arc<dyn Provider>,
        main_wallet: FieldElement, // Our source wallet
        min_balance: u64,
        top_up_amount: u64,
        deployed_accounts: Arc<Mutex<Vec<FieldElement>>>,
    ) -> Self {
        Self {
            provider,
            main_wallet,
            min_balance,
            top_up_amount,
            deployed_accounts,
        }
    }

    /// Starts the rebalancing service
    pub async fn start(&self) {
        let mut interval = time::interval(Duration::from_secs(60)); // Check every 60 seconds (or something different)
        loop {
            interval.tick().await;
            self.rebalance().await;
        }
    }

    /// Rebalances ETH across deployed accounts
    async fn rebalance(&self) {
        let accounts = self.deployed_accounts.lock().await;
        for account in accounts.iter() {
            if let Ok(balance) = self.get_account_balance(*account).await {
                if balance < self.min_balance {
                    self.top_up_account()
                }
            }
        }
    }

    /// Gets the balance of a specific account
    async fn get_account_balance(&self, account: FieldElement) -> Result<u64, SendTransactionError> {
    }

    /// Tops up a specific account from the main wallet
    async fn top_up_account(&self, account: FieldElement) -> Result<(), SendTransactionError> {
        // Construct the transaction to transfer ETH from the main wallet to the account
        let top_up_tx = self.create_top_up_transaction(account)?;

        // Send the transaction to StarkNet
        self.provider.send_transaction(&top_up_tx).await.map(|_| ())
    }

    /// Creates a top-up transaction
    fn create_top_up_transaction(&self, account: FieldElement) -> Result<StarknetTransaction, SendTransactionError> {
        // Build the top-up transaction using the main wallet and target account
    }
}

Transaction handling

// Helper function to select an OpenZeppelin account (implement our own selection strategy)
async fn select_oz_account(&self) -> Result<OzAccount, SomeError> {
    // Implement your logic to select an OZ account
}

We can construct the transaction via something like:

fn create_execute_from_outside_transaction(
    oz_account: OzAccount,
    payload: Bytes,
    timestamp: u64,
    signature: Signature,
) -> Result<StarknetTransaction, SomeError> {
}
Eikix commented 1 month ago

A few notes:

  1. For deployment of OZ accounts, we can check starkli's code
  2. For rebalancing, I think a simple CRON (out of repo) script might do the trick?
  3. How do we implement selection?
  4. Where does the private key live in this case?
  5. How many private keys for the OZ accounts?
  6. Where does the "list" of OZ account address live?
tcoratger commented 1 month ago

A few notes:

  1. For deployment of OZ accounts, we can check starkli's code
  2. For rebalancing, I think a simple CRON (out of repo) script might do the trick?
  3. How do we implement selection?
  4. Where does the private key live in this case?
  5. How many private keys for the OZ accounts?
  6. Where does the "list" of OZ account address live?
Eikix commented 1 month ago

A few notes:

  1. For deployment of OZ accounts, we can check starkli's code
  2. For rebalancing, I think a simple CRON (out of repo) script might do the trick?
  3. How do we implement selection?
  4. Where does the private key live in this case?
  5. How many private keys for the OZ accounts?
  6. Where does the "list" of OZ account address live?
  • yes would say that a cron is fine
  • for selection I would say either random if we are 100% sure the balance is always sufficient for all accounts (some risks if multiple txs at same time with same account for ex) or select the richest account but less arbitrary

The problem with selection afaik is the probability to hit nonce errors because two transactions that are too close (in time) to one another might send two txs with same nonce

greged93 commented 1 month ago

Design overview for the execute_from_outside in the RPC. execute_from_outside

The issue which I am not sure yet how to solve is that the retry service becomes a big problem now with execute_from_outside, since before the nonce of the account was checked by the gateway and quickly rejected in case it was invalid. Now with execute_from_outside, the nonce gets checked in the __execute__ of the relayer, which means:

Eikix commented 1 month ago

Design overview for the execute_from_outside in the RPC. execute_from_outside

The issue which I am not sure yet how to solve is that the retry service becomes a big problem now with execute_from_outside, since before the nonce of the account was checked by the gateway and quickly rejected in case it was invalid. Now with execute_from_outside, the nonce gets checked in the __execute__ of the relayer, which means:

  • it gets rejected at the __execute__ level which is way later in the transaction.
  • relayer has to pay for the execution of a transaction for which the nonce is incorrect, meaning relayer now pays for each retry.

Nice!! Where did you find this idea?

Can we check that it is a good idea? Maybe with someone from Lambda, Pimlico and Biconomy?

greged93 commented 1 month ago

Where did you find this idea?

Just brainstorming alone and checking the tokio sync docs.

I guess we could ask Lambda, I don't think Pimlico actually do nonce management afaik, because in the EVM world, the mempool takes care of that for you

Eikix commented 1 month ago

Where did you find this idea?

Just brainstorming alone and checking the tokio sync docs.

I guess we could ask Lambda, I don't think Pimlico actually do nonce management afaik, because in the EVM world, the mempool takes care of that for you

Not sure, since you still need to sign the correct nonce? So something or someone is supposed to increment nonces

Eikix commented 1 month ago

Theoretically we could just store a mapping of Account -> Nonce, and increment optimistically a nonce every time we post a tx, and if the tx is rejected (nonce shouldnt be incremented) we decrease it

You can technically send 10 txs in a row without cooldown on SN, granted you increment nonce