A local testnet for Starknet... in Rust!
This repository is work in progress, please be patient. Please check below the status of features compared with the Pythonic Devnet:
- RPC v0.5.1
- Dump & Load
- Mint token - Local faucet
- Customizable predeployed accounts
- Starknet.js test suite passes 100%
- Advancing time
- RPC v0.6.0
- Availability as a package (crate)
- Forking
- L1-L2 Postman integration
- Block manipulation
- Create an empty block
Make sure to have installed Rust.
The required Rust version is specified in rust-toolchain.toml and handled automatically by cargo
.
After git-cloning this repository, install and run the project with:
$ cargo run
Installing and running as a binary is achievable via cargo install
, but until Devnet is released as a crate, it comes with some caveats. You need to:
- Populate your environment with the variables defined in the config file
- Use the
--locked
flag to ensure using the dependencies listed in the lock file - Preferrably familiarize yourself with the
cargo install
command (docs)
$ cargo install --git https://github.com/0xSpaceShard/starknet-devnet-rs.git --locked
When the installation finishes, follow the output in your terminal.
This application is available as a Docker image (Docker Hub link). To download the latest
image, run:
$ docker pull shardlabs/starknet-devnet-rs
Supported architectures: arm64 and amd64.
Running a container is done like this (see port publishing for more info):
$ docker run -p [HOST:]<PORT>:5050 shardlabs/starknet-devnet-rs [OPTIONS]
Commits to the main
branch of this repository are mostly available as images tagged with their commit hash (the full 40-lowercase-hex-digits SHA1 digest):
$ docker pull shardlabs/starknet-devnet-rs:<COMMIT_HASH>
By appending the -seed0
suffix, you can use images which predeploy funded accounts with --seed 0
, thus always predeploying the same set of accounts:
$ docker pull shardlabs/starknet-devnet-rs:<VERSION>-seed0
$ docker pull shardlabs/starknet-devnet-rs:latest-seed0
If on a Linux host machine, you can use --network host
. This way, the port used internally by the container is also available on your host machine. The --port
option can be used (as well as other CLI options).
$ docker run --network host shardlabs/starknet-devnet-rs [--port <PORT>]
If not on Linux, you need to publish the container's internally used port to a desired <PORT>
on your host machine. The internal port is 5050
by default (probably not your concern, but can be overridden with --port
).
$ docker run -p [HOST:]<PORT>:5050 shardlabs/starknet-devnet-rs
E.g. if you want to use your host machine's 127.0.0.1:5050
, you need to run:
$ docker run -p 127.0.0.1:5050:5050 shardlabs/starknet-devnet-rs
You may ignore any address-related output logged on container startup (e.g. Starknet Devnet listening on 0.0.0.0:5050
). What you will use is what you specified with the -p
argument.
If you don't specify the HOST
part, the server will indeed be available on all of your host machine's addresses (localhost, local network IP, etc.), which may present a security issue if you don't want anyone from the local network to access your Devnet instance.
Check out the CLI options with:
$ cargo run -- --help
Or if using dockerized Devnet:
$ docker run --rm shardlabs/starknet-devnet-rs --help
By default, the logging level is INFO, but this can be changed via the RUST_LOG
environment variable.
All logging levels: TRACE
, DEBUG
, INFO
, WARN
, ERROR
To specify the logging level and run Devnet on the same line:
$ RUST_LOG=<LEVEL> cargo run
or if using dockerized Devnet:
$ docker run -e RUST_LOG=<LEVEL> shardlabs/starknet-devnet-rs
Unlike Pythonic Devnet, which supported the gateway and feeder gateway API, Devnet in Rust only supports JSON-RPC, which at the time of writing this is synchronized with specification v0.4.0.
The JSON-RPC API is reachable via /rpc
and /
(e.g. if spawning Devnet with default settings, these URLs have the equivalent functionality: http://127.0.0.1:5050/rpc
and http://127.0.0.1:5050/
)
Note:
Out of Starknet trace API RPC methods, only
starknet_simulateTransactions
is supported.
Devnet predeploys a UDC, an ERC20 (fee token) contract and a set of predeployed funded accounts.
The set of accounts can be controlled via CLI options: --accounts <NUMBER_OF>
, --initial-balance <WEI>
, --seed <VALUE>
.
Choose between predeploying Cairo 0 (OpenZeppelin 0.5.1) or Cairo 1 (OpenZeppelin 0.7.0) accounts by using --account-class [cairo0 | cairo1]
. Alternatively, provide a path to the Sierra artifact of your custom account using --account-class-custom <SIERRA_PATH>
.
The predeployment information is logged on Devnet startup. Predeployed accounts can be retrieved in JSON format by sending a GET
request to /predeployed_accounts
of your Devnet.
For now, you can consult the Pythonic Devnet docs on minting, with the difference of lite minting not being supported anymore.
To preserve your Devnet instance for future use, these are the options:
- Dumping on exit (handles Ctrl+C, i.e. SIGINT, doesn't handle SIGKILL):
cargo run -- --dump-on exit --dump-path <PATH>
- Dumping after each transaction:
cargo run -- --dump-on transaction --dump-path <PATH>
- Dumping on request (replace , and with your own):
curl -X POST http://<HOST>:<PORT>/dump -d '{ "path": <PATH> }' -H "Content-Type: application/json"
To load a preserved Devnet instance, the options are:
- Loading on startup (note the argument name is not
--load-path
as it was in Devnet-py):
cargo run -- --dump-path <PATH>
- Loading on request:
curl -X POST http://<HOST>:<PORT>/load -d '{ "path": <PATH> }' -H "Content-Type: application/json"
Currently, dumping produces a list of received transactions that is stored on disk.
Conversely, loading is implemented as the re-execution of transactions from a dump.
This means that timestamps of StarknetBlock
will be different.
Dumping and loading is not guaranteed to work cross-version. I.e. if you dumped one version of Devnet, do not expect it to be loadable with a different version.
If you dumped a Devnet utilizing one class for account predeployment (e.g. the default --account-class cairo0
), you should use the same option when loading.
Devnet can be restarted by making a POST /restart
request (no body required). All of the deployed contracts (including predeployed), blocks and storage updates will be restarted to the original state, without the transactions and requests from a dump file you may have provided on startup.
If you're using the Hardhat plugin, restart with starknet.devnet.restart()
.
A new block is generated with each new transaction, and you can create an empty block by yourself.
To create an empty block without transactions, POST a request to /create_block:
POST /create_block
Response:
{'block_hash': '0x115e1b390cafa7942b6ab141ab85040defe7dee9bef3bc31d8b5b3d01cc9c67'}
Block timestamp can be manipulated by setting the exact time or setting the time offset. Timestamps methods /set_time
and /increase_time
will generate a new block. All values should be set in Unix time seconds Unix time seconds.
Sets the exact time and generates a new block.
POST /set_time
{
"time": TIME_IN_SECONDS
}
Warning: block time can be set in the past which might lead to unexpected behavior!
Increases the block timestamp by the provided amount and generates a new block. All subsequent blocks will keep this increment.
POST /increase_time
{
"time": TIME_IN_SECONDS
}
Devnet can be started with the --start-time
argument, where START_TIME_IN_SECONDS
should be greater than 0.
cargo run -- --start-time START_TIME_IN_SECONDS
Timeout can be passed to Devnet's HTTP server. This makes it easier to deploy and manage large contracts that take longer to execute.
cargo run -- --timeout TIMEOUT
With state archive capacity set to full
, Devnet will store full state history. The default mode is none
, where no old states are stored.
cargo run -- --state-archive-capacity CAPACITY
It is highly recommended to get familiar with Visual Studio Code Dev Containers and install rust-analyzer extension.
Run the linter with:
./scripts/clippy_check.sh
Run the formatter with:
./scripts/format.sh
If you encounter an error like
error: toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed
Resolve it with:
rustup default nightly
To check for unused dependencies, run:
./scripts/check_unused_deps.sh
If you think this reports a dependency as a false-positive (i.e. isn't unused), check here.
Some tests require the anvil
command, so you need to install Foundry. The anvil
command might not be usable by tests if you run them using VS Code's Run Test
button available just above the test case. Either run tests using a shell which has foundry/anvil in PATH
, or modify the BackgroundAnvil Command to specify anvil
by its path on your system.
To ensure that integration tests pass, be sure to have run cargo build --release
or cargo run --release
prior to testing. This builds the production target used in integration tests, so spawning BackgroundDevnet won't time out.
Run all tests using all available CPUs with:
cargo test
The previous command might cause your testing to die along the way due to memory issues. In that case, limiting the number of jobs helps, but depends on your machine (rule of thumb: N=6):
cargo test --jobs <N>
Due to internal needs, images with arch suffix are built and pushed to Docker Hub, but this is not mentioned in the user docs as users should NOT be needing it.
This is what happens under the hood on main
:
- build
shardlabs/starknet-devnet-rs-<COMMIT_SHA1>-amd
- build
shardlabs/starknet-devnet-rs-<COMMIT_SHA1>-arm
- create and push joint docker manifest called
shardlabs/starknet-devnet-rs-<COMMIT_SHA1>
- same for
latest
- same for
In the image, tini
is used to properly handle killing of dockerized Devnet with Ctrl+C
To test Starknet messaging, Devnet exposes endpoints prefixed with postman/
which are dedicated to the messaging feature.
You can find a full guide to test the messaging feature in the contracts/l1-l2-messaging README.
Devnet exposes the following endpoints:
/postman/load_l1_messaging_contract
: deploys theMockStarknetMessaging
contract on L1 (requires L1 node to be running)./postman/flush
: fetches and executes L1 -> L2 messages, and sends L2 -> L1 messages (requires L1 node to be running ifdry_run
option is not used)./postman/send_message_to_l2
: sends and executes a message on L2 (L1 node not required)./postman/consume_message_from_l2
: consumes a message on L1 node from the L2 (requires L1 node to be running).
We ❤️ and encourage all contributions!
Click here for the development guide.
Special thanks to all the contributors!