Skip to content

Commit

Permalink
Add eth_call and eth_send_transaction entrypoints to kakarot (kkrt-la…
Browse files Browse the repository at this point in the history
…bs#554)

Time spent on this PR: 2.5

## Pull request type

Please check the type of change your PR introduces:

- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [x] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

The EOAccount owns the logic of transferring native tokens and
dispatching between contract deployment and contract invocation.
On the other hand, the kakarot contract exposes two methods
(execute_at_address & deploy_contract_account) that require the caller
to know what they want to do and how they need to send the data. This is
an extra complexity compared to what the EVM does: the kakarot contract
should be able to handle a plain (encoded or decoded) EVM tx without
requiring the caller to do any logic themselves.

As a matter of fact, the EOAccount responsibility is (almost) narrowed
to a standard AA responsibility: accepte a sign tx, verify signature and
propagate the call. Almost because it still needs to **decode** the tx,
and actually Yoav (from Braavos) thinks that we should go further and
remove this **decoding** part from the EOA: only validate a signature
and send the payload (encoded) to the kakarot contract. This would allow
us to use Braavos contract out of the box (with a proper signer type)

Resolves kkrt-labs#551 kkrt-labs#411 

## What is the new behavior?

The Kakarot contract has two new entrypoints that behave as in the
[ethereum RPC
doc](https://ethereum.org/en/developers/docs/apis/json-rpc)
- `eth_call`: Executes a new message call immediately without creating a
transaction (`@view` function)
- `eth_send_transaction`: Creates new message call transaction or a
contract creation, if the data field contains code. (`@external`
function)

Along the way, the test refactoring (kkrt-labs#441) was eventually done, and the
`ExecutionContext` `init` & `init_at_address` have been merged into one
single general purpose `init` method.

## Other information

Following a community call discussion, we could put more of these
entrypoints in the main Kakarot contract:
- eth_getBalance
- eth_getStorageAt
- eth_getTransactionCount
- eth_getCode
- etc.

to put more logic in the core contract and less in the RPC thay may
eventually require a complete refacto depending on where we actually
make this kakarot live.

I have added a check
`compute_starknet_address(IAccount.get_evm_address(contract_address)) !=
contract_address` to avoid unexpected behaviors resulting from a
starknet contract implementing `get_evm_address` without being **the**
`ContractAccount`. I'd like to remove this sooner or later but still
need to think about all the edge cases.
  • Loading branch information
ClementWalter authored Mar 31, 2023
1 parent 662f251 commit 1ddafaf
Show file tree
Hide file tree
Showing 106 changed files with 1,077 additions and 1,074 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ test-no-log: build-sol
test-integration: build-sol
poetry run pytest tests/integration --log-cli-level=INFO -n logical

test-unit: build-sol
poetry run pytest tests/unit --log-cli-level=INFO
test-unit:
poetry run pytest tests/src --log-cli-level=INFO

run-test-log: build-sol
poetry run pytest -k $(test) --log-cli-level=INFO -vvv
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ Test architecture is the following:
function with forged bytecode
- tests/integration/solidity_contracts contains python tests for solidity
contracts that are compiled, deployed on kakarot local node and interacted
with kakarot execute_at_address()
with kakarot eth_call() and invoke()
- the project also contains some forge tests (e.g. `PlainOpcodes.t.sol`) whose
purpose is to test easily the solidity functions meant to be tested with
kakarot, i.e. quickly making sure that they return the expected output so that
Expand Down
87 changes: 59 additions & 28 deletions docs/general/kakarot_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,85 @@ The entire Kakarot protocol is composed of 4 different contracts:

## Kakarot

The main Kakarot contract is located at: `./src/kakarot/kakarot.cairo`.
The main Kakarot contract is located at:
[`./src/kakarot/kakarot.cairo`](../../src/kakarot/kakarot.cairo).

This is the core contract which is capable of parsing and executing ethereum bytecode.
This is the core contract which is capable of executing decoded ethereum
transactions thanks to its `invoke` entrypoint.

Its functionality is accessed via two functions:
Currently, Argent or Braavos accounts contracts don't work with Kakarot.
Consequently, the `deploy_externally_owned_account` entrypoint has been added to
let the owner of an Ethereum address get their corresponding starknet contract.

- `deploy(bytecode_len: felt, bytecode: felt*)`: Deploys a new contract (a *Contract Account*) that is initialized using the bytecode that is accompanied with the method call as a parameter.
The mapping between EVM addresses and Starknet addresses of the deployed
contracts is stored as follows:

- `execute_at_address(address: felt, value: felt, gas_limit: felt, calldata_len: felt, calldata: felt*)`: Executes the code held by a previously deployed *Account Contract*.
- each deployed contract has a `get_evm_address` entrypoint
- only the Kakarot contract deploys accounts and provides a
`compute_starknet_address(evm_address)` entrypoint that returns the
corresponding starknet address

## Contract Accounts
For this latter computation to be account agnostic, Kakarot indeed uses a
transparent proxy.

A *Contract Account* is a StarkNet contract. However, it also acts as an Ethereum style contract account within the Kakarot EVM.
In isolation it is not more than a StarkNet contract that stores some bytecode as well as some key-value pairs which were assigned to it on creation.
It is only addressable via its StarkNet address and not an EVM address (which it is associated with inside the Kakarot EVM).
## Contract Accounts

## Account Registry
A _Contract Account_ is a StarkNet contract. However, it also acts as an
Ethereum style contract account within the Kakarot EVM. In isolation it is not
more than a StarkNet contract that stores some bytecode as well as some
key-value pairs which were assigned to it on creation. It is only addressable
via its StarkNet address and not an EVM address (which it is associated with
inside the Kakarot EVM).

The account registry contract maps StarkNet addresses of deployed *Contract Accounts* to their EVM addresses (which are their identifiers within the Kakarot EVM).
## Externally Owned Account

The mapping is created on deployment of the *Contract Account* by the core Kakarot contract (after its EVM address is computed).
Each Externally Owned Account in the EVM world has its counterpart in Starknet
by the mean of a specific account contract deployed by Kakarot.

When the Kakarot core contract needs to execute a *Contract Accounts* bytecode or needs to write/read its storage it will use the account registry contract to convert its EVM address to the StarkNet address. Using the StarkNet address it is now capable of addressing the *Contract Accounts* methods.
This contract is a regular account contract in the Starknet sense with
`__validate__` and `__execute__` entrypoint. However, it does decode and
validate an EVM signed transaction and redirect it only to Kakarot. Further
development will allow the user to have one single Starknet account for both
Starknet native and Kakarot deployed dApp. For a general introduction to EVM
transactions, see
[the official doc](https://ethereum.org/en/developers/docs/transactions/).

## Blockhash Registry

The EVM computes a hash from the contents of each created block (the so called *blockhash*).
The [BLOCKHASH](https://www.evm.codes/#40) opcode is a particular opcode that
requires the EVM to be aware of past blocks
([see also](https://ethresear.ch/t/the-curious-case-of-blockhash-and-stateless-ethereum/7304/7)).
Since this is not feasible from within Starknet, we deployed a block hash
registry contract on Starknet to make this data accessible on-chain.

As Kakarot core is not aware of which transactions are within one block, it cannot compute the hash itself.
However, Kakarot needs to be able to access the blockhash for a given block number in order to be EVM compatible.

The blockhash registry enables this by holding a `block_number -> block_hash` mapping that admins can write to and Kakarot core can read from.
The blockhash registry enables this by holding a `block_number -> block_hash`
mapping that admins can write to and Kakarot core can read from.

## Deploying Kakarot

With the above information in mind the Kakarot EVM can be deployed and configured on StarkNet with the following steps:
With the above information in mind the Kakarot EVM can be deployed and
configured on StarkNet with the following steps:

1. Declare the account proxy, the contract account and the externally owner
account contracts.

1. Declare the *Contract Account*.
- This generates class hashes which will be used by the core Kakarot contract
to deploy _accounts_.

This generates a class hash which will be used by the core Kakarot contract to deploy *Contract Accounts*.
1. Deploy Kakarot (core), with the following constructor arguments:

2. Deploy Kakarot (core), with the following constructor arguments:
- StarkNet address of the owner/admin account that controls the Kakarot core contract.
- Address of the ETH token contract (Which is also used as ether within the Kakarot EVM)
- *Contract Account* class hash.
- StarkNet address of the owner/admin account that controls the Kakarot core
contract.
- Address of the ETH token contract (Which is also used as ether within the
Kakarot EVM)
- _Contract Account_ class hash.
- _Externally Owned Account_ class hash.
- _Account Proxy_ class hash.

3. Deploy Account Registry
1. Deploy Blockhash Registry

4. Deploy Blockhash Registry
1. Store the addresses of the blockhash registry contracts in Kakarot core using
`set_blockhash_registry`. This is required for Kakarot to access the last 256
bock hashes.

5. Store the addresses of the account and blockhash registry contracts in Kakarot core using `set_account_registry` and `set_blockhash_registry` respectively. This is required for Kakarot to access the functionality of the registries.
This flow can be seen in the [deploy script](../../scripts/deploy_kakarot.py).
45 changes: 30 additions & 15 deletions scripts/get_latest_blockhashes.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
import asyncio
import logging
import json
from pathlib import Path
import logging
from argparse import ArgumentParser
from pathlib import Path

from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import FeederGatewayClient
from services.external_api.client import RetryConfig
from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import (
FeederGatewayClient,
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

NETWORKS = {
"goerli": "https://alpha4.starknet.io/feeder_gateway", # alpha-goerli
"mainnet": "https://alpha-mainnet.starknet.io/feeder_gateway/" # mainnet
"goerli": "https://alpha4.starknet.io/feeder_gateway", # alpha-goerli
"mainnet": "https://alpha-mainnet.starknet.io/feeder_gateway/", # mainnet
}


parser = ArgumentParser(description="Get block information from sequencer")
parser.add_argument("--network", "-n", default="goerli", type=str, help=f"Select network, one of {list(NETWORKS.keys())}")
parser.add_argument(
"--network",
"-n",
default="goerli",
type=str,
help=f"Select network, one of {list(NETWORKS.keys())}",
)
args = parser.parse_args()


async def main():
# Instantiate a FeederGatewayClient object
# -1 means unlimited retries
retry_config = RetryConfig(n_retries=-1)
feeder_gateway_client = FeederGatewayClient(url=NETWORKS[args.network], retry_config=retry_config)
feeder_gateway_client = FeederGatewayClient(
url=NETWORKS[args.network], retry_config=retry_config
)

# Get the latest block
# Sometimes get_block returns "null" as value
Expand All @@ -36,26 +47,30 @@ async def main():
# Get the last 256 blocks excluding the current block
# Convert the list of blockhashes in to dictionary { block_number: block_hash }
last_256_blocks = [
await feeder_gateway_client.get_block(block_number=latest_block.block_number - i)
await feeder_gateway_client.get_block(
block_number=latest_block.block_number - i
)
for i in range(1, 257)
]
last_256_blockhashes = {
block.block_number: block.block_hash
for block in last_256_blocks
block.block_number: block.block_hash for block in last_256_blocks
}

# Dump JSON to file
# If you want to use the created file for testing, rename that file to mock_blockhashes.json
with open(Path("sequencer") / "blockhashes.json", "w") as file:
context = {
"current_block": {
"block_number": latest_block.block_number,
"timestamp": latest_block.timestamp
"timestamp": latest_block.timestamp,
},
"last_256_blocks": last_256_blockhashes
"last_256_blocks": last_256_blockhashes,
}
json.dump(context, file)
logger.info(f"Blockhashes retrieved from block number {min(last_256_blockhashes.keys())} to {max(last_256_blockhashes.keys())}")
logger.info(
f"Blockhashes retrieved from block number {min(last_256_blockhashes.keys())} to {max(last_256_blockhashes.keys())}"
)


if __name__== "__main__":
if __name__ == "__main__":
asyncio.run(main())
1 change: 0 additions & 1 deletion src/kakarot/accounts/contract/contract_account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ from kakarot.accounts.library import Accounts
from openzeppelin.access.ownable.library import Ownable

// @title EVM smart contract account representation.
// @author @abdelhamidbakhta

// Contract initializer
@external
Expand Down
7 changes: 2 additions & 5 deletions src/kakarot/accounts/contract/library.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ from starkware.cairo.common.math import unsigned_div_rem
from starkware.cairo.common.registers import get_label_location
from starkware.cairo.common.uint256 import Uint256, uint256_not

// @title ContractAccount main library file.
// @notice This file contains the EVM smart contract account representation logic.
// @author @abdelhamidbakhta
// @custom:namespace ContractAccount

// Storage

@storage_var
Expand All @@ -39,6 +34,8 @@ func is_initialized_() -> (res: felt) {
func evm_address() -> (evm_address: felt) {
}

// @title ContractAccount main library file.
// @notice This file contains the EVM smart contract account representation logic.
namespace ContractAccount {
// Define the number of bytes per felt. Above 16, the following code won't work as it uses unsigned_div_rem
// which is bounded by RC_BOUND = 2 ** 128 ~ uint128 ~ bytes16
Expand Down
73 changes: 14 additions & 59 deletions src/kakarot/accounts/eoa/library.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -117,76 +117,31 @@ namespace ExternallyOwnedAccount {
return (response_len=0);
}

Accounts.increment_nonce();

let (
gas_limit, destination, amount, payload_len, payload, tx_hash, v, r, s
gas_price, gas_limit, destination, amount, payload_len, payload, tx_hash, v, r, s
) = EthTransaction.decode([call_array].data_len, calldata + [call_array].data_offset);

let (_kakarot_address) = kakarot_address.read();
local retdata_size;

// Increment EOA nonce
Accounts.increment_nonce();

// If destination is 0, we are deploying a contract
if (destination == 0) {
// deploy_contract_account signature is
// calldata_len: felt, calldata: felt*
IKakarot.deploy_contract_account(
contract_address=_kakarot_address, bytecode_len=payload_len, bytecode=payload
);
// Else run the bytecode of the destination contract
} else {
// execute_at_address signature
IKakarot.execute_at_address(
contract_address=_kakarot_address,
address=destination,
value=amount,
gas_limit=gas_limit,
calldata_len=payload_len,
calldata=payload,
);
}

// copy retdata into response using the syscall_ptr
tempvar syscall_ptr = syscall_ptr;
tempvar range_check_ptr = range_check_ptr;
let res = [cast(syscall_ptr - CallContract.SIZE, CallContract*)];
memcpy(response, res.response.retdata, res.response.retdata_size);
assert retdata_size = res.response.retdata_size;

if (amount == 0) {
let (response_len) = execute(
call_array_len - 1,
call_array + CallArray.SIZE,
calldata_len,
calldata,
response + retdata_size,
);
return (response_len=retdata_size + response_len);
}

// transfer the amount from the EOA to the destination
// get kakarot native token address,
// compute starknet address from evm address,
let (native_token_address_) = IKakarot.get_native_token(contract_address=_kakarot_address);
let (starknet_address_) = IKakarot.compute_starknet_address(
contract_address=_kakarot_address, evm_address=destination
let (return_data_len, return_data) = IKakarot.eth_send_transaction(
contract_address=_kakarot_address,
to=destination,
gas_limit=gas_limit,
gas_price=gas_price,
value=amount,
data_len=payload_len,
data=payload,
);
let amount_u256 = Helpers.to_uint256(amount);
let (success) = IEth.transfer(
contract_address=native_token_address_, recipient=starknet_address_, amount=amount_u256
);
with_attr error_message("Kakarot: EOA: failed to transfer token to {destination}") {
assert success = TRUE;
}
memcpy(response, return_data, return_data_len);

let (response_len) = execute(
call_array_len - 1,
call_array + CallArray.SIZE,
calldata_len,
calldata,
response + retdata_size,
response + return_data_len,
);
return (response_len=retdata_size + response_len);
return (response_len=return_data_len + response_len);
}
}
11 changes: 2 additions & 9 deletions src/kakarot/accounts/library.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,8 @@ from starkware.cairo.common.hash_state import (
hash_update_single,
hash_update_with_hashchain,
)
// Kakarot dependencies
from kakarot.constants import (
native_token_address,
contract_account_class_hash,
externally_owned_account_class_hash,
blockhash_registry_address,
Constants,
account_proxy_class_hash,
)

from kakarot.constants import Constants, account_proxy_class_hash
from kakarot.interfaces.interfaces import IAccount

@storage_var
Expand Down
2 changes: 0 additions & 2 deletions src/kakarot/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ func account_proxy_class_hash() -> (res: felt) {

// @title Constants file.
// @notice This file contains global constants.
// @author @abdelhamidbakhta
// @custom:namespace Constants
namespace Constants {
// Define constants
Expand Down
Loading

0 comments on commit 1ddafaf

Please sign in to comment.