DEPRECATION NOTICE: This repo has been deprecated in favor of the official docs at https://docs.kurtosis.com/quickstart
The instructions below will walk you through spinning up an Ethereum network in a Kurtosis sandbox, interacting with it, and migrating the logic into a test. By the end of this tutorial, you will have a rudimentary Ethereum test in Typescript that you can begin to modify on your own.
Verify that you have the Docker daemon installed and running on your local machine by running (you can copy this code by hovering over it and clicking the clipboard in the top-right corner):
docker image ls
- If you don't have Docker installed, do so by following the installation instructions
- If Docker is installed but not running, start it
NOTE: DockerHub restricts downloads from users who aren't logged in to 100 images downloaded per 6 hours, so if at any point in this tutorial you see the following error message:
Error response from daemon: toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit
you can fix it by creating a DockerHub account (if you don't have one already) and registering it with your local Docker engine like so:
docker login
Follow the steps on this installation page to install the CLI, or upgrade it to latest if it's already installed.
The Kurtosis engine provides you isolated environments called "enclaves" to run your services inside. Let's use the CLI to create a new enclave:
kurtosis enclave new --id demo
The Kurtosis images that run the engine will take a few seconds to pull the first time, but once done you'll have a new enclave ready to be used.
To see the enclave you just created, run:
kurtosis enclave ls
You should see the enclave list, showing the new enclave:
EnclaveID Status
demo EnclaveContainersStatus_RUNNING
You've created your first enclave! To check the contents of the enclave:
kurtosis enclave inspect demo
You should see something like this:
Enclave ID: demo
Enclave Status: EnclaveContainersStatus_RUNNING
API Container Status: EnclaveAPIContainerStatus_RUNNING
API Container Host GRPC Port: 127.0.0.1:55783
API Container Host GRPC Proxy Port: 127.0.0.1:55784
========================================= Kurtosis Modules =========================================
GUID Ports
========================================== User Services ==========================================
GUID ID Ports
Notice how the enclave doesn't yet have any user services or Kurtosis modules. This is because enclaves are created empty by default.
Now that we have an enclave, let's put something in it. Execute the following command to run a web server (using the httpd
Docker image) inside the enclave and give it the ID webserver
:
kurtosis service add demo webserver httpd --ports http=80
Your output should look something like this:
Service ID: webserver
Ports Bindings:
http: 80/tcp -> 127.0.0.1:63825
Now, you can go to your browser and check that the web server is up and running through the local URL provided in the http
port binding (e.g. 127.0.0.1:63825
). Note that the port number is ephemeral and yours will be different.
Let's inspect the enclave again:
kurtosis enclave inspect demo
Now, you should see the web server listed in the "User Services" section, like so:
========================================== User Services ==========================================
GUID ID Ports
webserver-1649186256 webserver http: 80/tcp -> 127.0.0.1:63825
Finally, you can remove the service with the following:
kurtosis service rm demo webserver
Now that we know how to create an enclave and add a service, we can proceed to one of the most important features in Kurtosis: modules.
Ethereum is the most popular smart contract blockchain in the world, so let's create a private Ethereum network in Kurtosis:
kurtosis module exec --enclave-id demo 'kurtosistech/ethereum-kurtosis-module'
This will take approximately a minute to run, with the majority of the time spent pulling the Ethereum images. After the final console.log
line executes, you'll see a result with information about the services running inside your enclave:
{
bootnode_service_id: 'bootnode',
node_info: {
bootnode: {
ip_addr_inside_network: '154.18.224.5',
ip_addr_on_host_machine: '127.0.0.1',
rpc_port_id: 'rpc',
ws_port_id: 'ws',
tcp_discovery_port_id: 'tcp-discovery',
udp_discovery_port_id: 'udp-discovery'
},
'ethereum-node-1': {
ip_addr_inside_network: '154.18.224.7',
ip_addr_on_host_machine: '127.0.0.1',
rpc_port_id: 'rpc',
ws_port_id: 'ws',
tcp_discovery_port_id: 'tcp-discovery',
udp_discovery_port_id: 'udp-discovery'
},
'ethereum-node-2': {
ip_addr_inside_network: '154.18.224.9',
ip_addr_on_host_machine: '127.0.0.1',
rpc_port_id: 'rpc',
ws_port_id: 'ws',
tcp_discovery_port_id: 'tcp-discovery',
udp_discovery_port_id: 'udp-discovery'
}
},
signer_keystore_content: '{"address":"14f6136b48b74b147926c9f24323d16c1e54a026","crypto":{"cipher":"aes-128-ctr","ciphertext":"39fb1d86c1082c0103ece1c5f394321f127bf1b65e6c471edcfb181058a3053a","cipherparams":{"iv":"c366d1eed33e8693fec7a85fad65d19f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"f210bc3b55117197f62a7ab8d85f2172342085f1daafa31034016163b8bc7db6"},"mac":"2ff8aa24d9b73ccfdb99cfd15fcdbcc8f640aaa7861e6813d53efaf550725fac"},"id":"6c5ac271-d24a-4971-b365-49490cc4befc","version":3}',
signer_account_password: 'passphrase'
}
And if we inspect the enclave again....
kurtosis enclave inspect demo
...we'll see that our enclave now has the Kurtosis module and three Ethereum nodes with local port bindings:
========================================= Kurtosis Modules =========================================
GUID Ports
ethereum-kurtosis-module.1649269281-1649269281 grpc: 1111/tcp -> 127.0.0.1:55863
========================================== User Services ==========================================
GUID ID Ports
bootnode-1649269284 bootnode ws: 8546/tcp -> 127.0.0.1:55879
tcpDiscovery: 30303/tcp -> 127.0.0.1:55877
udpDiscovery: 30303/udp -> 127.0.0.1:59562
rpc: 8545/tcp -> 127.0.0.1:55878
ethereum-node-1-1649269292 ethereum-node-1 udpDiscovery: 30303/udp -> 127.0.0.1:55356
rpc: 8545/tcp -> 127.0.0.1:55883
ws: 8546/tcp -> 127.0.0.1:55884
tcpDiscovery: 30303/tcp -> 127.0.0.1:55885
ethereum-node-2-1649269296 ethereum-node-2 udpDiscovery: 30303/udp -> 127.0.0.1:64347
rpc: 8545/tcp -> 127.0.0.1:56013
ws: 8546/tcp -> 127.0.0.1:56014
tcpDiscovery: 30303/tcp -> 127.0.0.1:56012
webserver-1649268254 webserver http: 80/tcp
But what just happened?
Starting networks is a very common task in Kurtosis, so we provide a framework called "modules" for making it dead simple. An executable module is basically a chunk of code that responds to an "execute" command, packaged as a Docker image, that runs inside a Kurtosis enclave - sort of like Docker Compose on steroids. In the steps above, we executed kurtosis module exec
to load the Ethereum module into the demo enclave. The Ethereum module doesn't take any parameters at load or execute time, but other modules do via the --execute-params
flag.
Now that you have a pet Ethereum network, let's do something with it.
Now let's connect to the Ethereum bootnode to verify the network is producing blocks.
Find the Ethereum node with ID bootnode
in the enclave contents, find its RPC port declared on 8545/tcp
, and copy the public IP and port that it's bound to on your machine (e.g. 127.0.0.1:55878
).
Then, slot it into RPC_URL_HERE in the below command:
curl -X POST RPC_URL_HERE --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":83}' -H "Content-Type: application/json"
You should get a response similar to the following, where result
is the current block number in hexadecimal:
{"jsonrpc":"2.0","id":83,"result":"0x93"}
And that's it! Anything doable on the testnet is now doable against your private Ethereum network running in your Kurtosis enclave.
To destroy the enclave we created and everything inside it, run:
kurtosis enclave rm -f demo
To destroy all enclaves, run:
kurtosis clean -a
Manually verifying against an enclave is nice, but it'd be great if we could take our logic and run it as part of CI. Fortunately, Kurtosis was designed from the beginning with testing in mind.
Normally, you'd have a project that you'd add the Kurtosis tests to. For the purposes of this onboarding though, we've created a sample Typescript project with testing ready to go. Go ahead and clone it from here, and we'll take a look around.
The first thing to notice is the test/basic_eth_test.ts
file. This contains a Mocha test that connects to the Kurtosis engine, spins up an enclave for the test, does nothing (right now), and stops it when it's done.
The second thing to notice is the kurtosis-engine-api-lib
dev dependency in the package.json
. This is the client library for connecting to the Kurtosis engine for creating, manipulating, stopping, & destroying enclaves.
Now let's see the testing framework in action. From the root of the repo, run:
scripts/build.sh
The testsuite will run, and you'll see that our basic test passed!
If we go ahead and run the enclave-listing command again:
kurtosis enclave ls
you'll notice a new stopped basic-ethereum-test_XXXXXXXXXXXXX
enclave. Our current test is set to stop enclaves after it's done with them so debugging information stays around, though the test could easily be switched to destroy the enclave instead.
We now have a test running in the testing framework, but our test doesn't currently do anything. Let's fix that.
First, inside the test, replace the // TODO Replace with Ethereum network setup
line with the following code:
log.info("Setting up Ethereum network...")
const loadEthModuleResult: Result<ModuleContext, Error> = await enclaveCtx.loadModule(ETH_MODULE_ID, ETH_MODULE_IMAGE, "{}");
if (loadEthModuleResult.isErr()) {
throw loadEthModuleResult.error;
}
const ethModuleCtx: ModuleContext = loadEthModuleResult.value;
const executeEthModuleResult: Result<string, Error> = await ethModuleCtx.execute("{}")
if (executeEthModuleResult.isErr()) {
throw executeEthModuleResult.error;
}
const executeEthModuleResultObj = JSON.parse(executeEthModuleResult.value);
log.info("Ethereum network set up successfully");
This code uses the Kurtosis SDK, which is the same SDK that the Kurtosis CLI uses to communicate with the Kurtosis engine. The only new bits to pay attention to are the error-checking: all EnclaveContext
and ModuleContext
methods return a Result object (much like in Rust). If result.isErr()
, we'll throw the result which will cause Mocha to mark the test as failed.
Second, replace the // TODO Replace with block number check
line with this code:
log.info("Verifying block number is increasing...");
// Grab the bootnode's service context
const bootnodeServiceId = executeEthModuleResultObj.bootnode_service_id
const bootnodeNodeObj = executeEthModuleResultObj.node_info[bootnodeServiceId]
const getBootnodeServiceCtxResult = await enclaveCtx.getServiceContext(bootnodeServiceId)
if (getBootnodeServiceCtxResult.isErr()) {
throw getBootnodeServiceCtxResult.error;
}
const bootnodeServiceCtx = getBootnodeServiceCtxResult.value;
// Get the IP & port of the bootnode, *outside* the enclave
const bootnodeRpcPortId = bootnodeNodeObj.rpc_port_id
const bootnodeRpcPort = bootnodeServiceCtx.getPublicPorts().get(bootnodeRpcPortId)
if (bootnodeRpcPort === undefined) {
throw new Error("We expected the boot node to have a public RPC port, but it was undefined");
}
const bootnodePublicIp = bootnodeServiceCtx.getMaybePublicIPAddress()
// Instantiate the Ethers client
const bootnodeRpcProvider = new ethers.providers.JsonRpcProvider(`http://${bootnodePublicIp}:${bootnodeRpcPort.number}`)
const blockNumber: number = await bootnodeRpcProvider.getBlockNumber();
if (blockNumber === 0) {
throw new Error("We expected the Ethereum cluster to be producing blocks, but the block number is still 0");
}
log.info("Verified that block number is increasing");
Finally, build and run the testsuite again:
scripts/build.sh
The test will pass, indicating that our test set up an Ethereum network and ran our block count verification logic against it!