Skip to content
This repository has been archived by the owner on Dec 1, 2022. It is now read-only.

kurtosis-tech/zz-deprecated_kurtosis-onboarding-experience

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DEPRECATION NOTICE: This repo has been deprecated in favor of the official docs at https://docs.kurtosis.com/quickstart

Kurtosis Ethereum 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.

Step One: Set Up Prerequisites (2 minutes)

Install Docker

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

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

Install the Kurtosis CLI

Follow the steps on this installation page to install the CLI, or upgrade it to latest if it's already installed.

Step Two: Start An Enclave And Run One User Service Inside (3 minutes)

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

Step Three: Start An Ethereum Network (5 minutes)

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.

Step Four: Talk To Ethereum (5 minutes)

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

Step Five: Get An Ethereum Testsuite (5 minutes)

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.

Step Six: Test Ethereum (5 minutes)

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!