feat: publishing infernet-container-starter v0.1.0

This commit is contained in:
ritual-all
2024-03-29 10:49:24 -04:00
commit 41aaa152e6
24 changed files with 1135 additions and 0 deletions

View File

@ -0,0 +1,20 @@
FROM python:3.11-slim as builder
WORKDIR /app
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONPATH src
WORKDIR /app
RUN apt-get update
COPY src/requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY src src
ENTRYPOINT ["gunicorn", "app:create_app()"]
CMD ["-b", "0.0.0.0:3000"]

View File

@ -0,0 +1,20 @@
DOCKER_ORG := ritualnetwork
TAG := $(DOCKER_ORG)/hello-world-infernet:latest
.phony: build run publish
build:
@docker build -t $(TAG) .
update-tag:
jq ".containers[0].image = \"$(TAG)\"" config.json > updated_config.json && mv updated_config.json config.json
run: build
docker run \
-p 3000:3000 $(TAG)
# You may need to set up a docker builder, to do so run:
# docker buildx create --name mybuilder --bootstrap --use
# refer to https://docs.docker.com/build/building/multi-platform/#building-multi-platform-images for more info
build-multiplatform:
docker buildx build --platform linux/amd64,linux/arm64 -t $(TAG) --push .

View File

@ -0,0 +1,163 @@
# Creating an infernet-compatible `hello-world` container
In this tutorial, we'll create a simple hello-world container that can be used
with infernet.
> [!NOTE]
> This directory `containers/hello-world` already includes the final result
> of this tutorial. Run the following tutorial in a new directory.
Let's get started! 🎉
## Step 1: create a simple flask-app and a requirements.txt file
First, we'll create a simple flask-app that returns a hello-world message.
We begin by creating a `src` directory:
```
mkdir src
```
Inside `src`, we create a `app.py` file with the following content:
```python
from typing import Any
from flask import Flask, request
def create_app() -> Flask:
app = Flask(__name__)
@app.route("/")
def index() -> str:
return "Hello world service!"
@app.route("/service_output", methods=["POST"])
def inference() -> dict[str, Any]:
input = request.json
return {"output": f"hello, world!, your input was: {input}"}
return app
```
As you can see, the app has two endpoints: `/` and `/service_output`. The first
one is simply used to ping the service, while the second one is used for infernet.
We can see that our app uses the `flask` package. Additionally, we'll need to
install the `gunicorn` package to run the app. We'll create a `requirements.txt`
file with the following content:
```
Flask>=3.0.0,<4.0.0
gunicorn>=21.2.0,<22.0.0
```
## Step 2: create a Dockerfile
Next, we'll create a Dockerfile that builds the flask-app and runs it.
At the top-level directory, create a `Dockerfile` with the following content:
```dockerfile
FROM python:3.11-slim as builder
WORKDIR /app
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONPATH src
WORKDIR /app
RUN apt-get update
COPY src/requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY src src
ENTRYPOINT ["gunicorn", "app:create_app()"]
CMD ["-b", "0.0.0.0:3000"]
```
This is a simple Dockerfile that:
1. Uses the `python:3.11-slim` image as a base image
2. Installs the requirements
3. Copies the source code
4. Runs the app on port `3000`
> [!IMPORTANT]
> App must be exposed on port `3000`. Infernet's orchestrator
> will always assume that the container apps are exposed on that port within the container.
> Users can then remap this port to any port that they want on the host machine
> using the `port` parameter in the container specs.
By now, your project directory should look like this:
```
.
├── Dockerfile
├── README.md
└── src
├── __init__.py
└── app.py
└── requirements.txt
```
## Step 3: build the container
Now, we can build the container. At the top-level directory, run:
```
docker build -t hello-world .
```
## Step 4: run the container
Finally, we can run the container. In one terminal, run:
```
docker run --rm -p 3000:3000 --name hello hello-world
```
## Step 5: ping the container
In another terminal, run:
```
curl localhost:3000
```
It should return something like:
```
Hello world service!
```
Congratulations! You've created a simple hello-world container that can be
used with infernet. 🎉
## Step 6: request a service output
Now, let's request a service output. Note that this endpoint is called by
the infernet node, not by the user. For debugging purposes however, it's useful to
be able to call it manually.
In your terminal, run:
```
curl -X POST -H "Content-Type: application/json" -d '{"input": "hello"}' localhost:3000/service_output
```
The output should be something like:
```
{"output": "hello, world!, your input was: {'input': 'hello'}"}
```
Your users will never call this endpoint directly. Instead, they will:
1. Either [create an off-chain job request](../../../README.md#L36) through the node API
2. Or they will make a subscription on their contracts

View File

@ -0,0 +1,50 @@
{
"log_path": "infernet_node.log",
"server": {
"port": 4000
},
"chain": {
"enabled": true,
"trail_head_blocks": 0,
"rpc_url": "http://host.docker.internal:8545",
"coordinator_address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"wallet": {
"max_gas_limit": 4000000,
"private_key": "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
}
},
"startup_wait": 1.0,
"docker": {
"username": "your-username",
"password": ""
},
"redis": {
"host": "redis",
"port": 6379
},
"forward_stats": true,
"containers": [
{
"id": "hello-world",
"image": "ritualnetwork/hello-world-infernet:latest",
"external": true,
"port": "3000",
"allowed_delegate_addresses": [],
"allowed_addresses": [],
"allowed_ips": [],
"command": "--bind=0.0.0.0:3000 --workers=2",
"env": {}
},
{
"id": "anvil-node",
"image": "ritualnetwork/infernet-anvil:0.0.0",
"external": true,
"port": "8545",
"allowed_delegate_addresses": [],
"allowed_addresses": [],
"allowed_ips": [],
"command": "",
"env": {}
}
]
}

View File

@ -0,0 +1,47 @@
from time import sleep
import requests
def hit_server_directly():
print("hello")
r = requests.get("http://localhost:3000/")
print(r.status_code)
# server response
print("server response", r.text)
def poll_until_complete(id: str):
status = "running"
r = None
while status == "running":
r = requests.get(
"http://localhost:4000/api/jobs",
params={
"id": id,
},
).json()[0]
status = r.get("status")
print("status", status)
if status != "running":
return r
sleep(1)
def create_job_through_node():
r = requests.post(
"http://localhost:4000/api/jobs",
json={
"containers": ["hello-world"],
"data": {"some": "object"},
},
)
job_id = r.json().get("id")
result = poll_until_complete(job_id)
print("result", result)
if __name__ == "__main__":
create_job_through_node()

View File

@ -0,0 +1,18 @@
from typing import Any
from flask import Flask, request
def create_app() -> Flask:
app = Flask(__name__)
@app.route("/")
def index() -> str:
return "Hello world service!"
@app.route("/service_output", methods=["POST"])
def inference() -> dict[str, Any]:
input = request.json
return {"output": f"hello, world!, your input was: {input}"}
return app

View File

@ -0,0 +1,2 @@
Flask>=3.0.0,<4.0.0
gunicorn>=21.2.0,<22.0.0

View File

@ -0,0 +1,34 @@
name: test
on: workflow_dispatch
env:
FOUNDRY_PROFILE: ci
jobs:
check:
strategy:
fail-fast: true
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
- name: Run Forge tests
run: |
forge test -vvv
id: test

View File

@ -0,0 +1,14 @@
# Compiler files
cache/
out/
# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/
# Docs
docs/
# Dotenv file
.env

View File

@ -0,0 +1,14 @@
# phony targets are targets that don't actually create a file
.phony: deploy
# anvil's third default address
sender := 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
RPC_URL := http://localhost:8545
# deploying the contract
deploy:
@PRIVATE_KEY=$(sender) forge script script/Deploy.s.sol:Deploy --broadcast --rpc-url $(RPC_URL)
# calling sayGM()
call-contract:
@PRIVATE_KEY=$(sender) forge script script/CallContract.s.sol:CallContract --broadcast --rpc-url $(RPC_URL)

View File

@ -0,0 +1,44 @@
# `Hello-World` Consumer Contracts
This is a [foundry](https://book.getfoundry.sh/) project that implements a simple Consumer
contract, [`SaysGm`](./src/SaysGM.sol).
This readme explains how to compile and deploy the contract to the Infernet Anvil Testnet network.
For a detailed tutorial on how to write a consumer contract, refer to the [tutorial doc](./Tutorial.md).
> [!IMPORTANT]
> Ensure that you are running the following scripts with the Infernet Anvil Testnet network.
> The [tutorial](./../../../README.md) at the root of this repository explains how to
> bring up an infernet node.
### Installing the libraries
```bash
forge install
```
### Compiling the contracts
```bash
forge compile
```
### Deploying the contracts
The deploy script at `script/Deploy.s.sol` deploys the `SaysGM` contract to the Infernet Anvil Testnet network.
We have the [following make target](./Makefile#L9) to deploy the contract. Refer to the Makefile
for more understanding around the deploy scripts.
```bash
make deploy
```
### Requesting a job
We also have a script called `CallContract.s.sol` that requests a job to the `SaysGM` contract.
Refer to the [script](./script/CallContract.s.sol) for more details. Similar to deployment,
you can run that script using the following convenience make target.
```bash
make call-contract
```
Refer to the [Makefile](./Makefile#L14) for more details.

View File

@ -0,0 +1,229 @@
# `GM! 🤠`
In this tutorial we'll make a very simple consumer contract called `SaysGm`.
All this contract does is request compute from our `hello-world` container and
upon receiving a response, it prints everything to the console.
> [!NOTE]
> Run this tutorial in a new directory, the end result of this tutorial will
> be pretty much the same as the [contracts](.) project, so refer to that if
> you get stuck.
## Prerequisites
### Installing foundry
You'll need [foundry](https://book.getfoundry.sh/getting-started/installation) installed.
### Scaffolding a new project
Create a new directory, and run `forge init` in it. This will create a new
project with a `foundry.yaml` file in it.
```bash
mkdir says-gm
cd says-gm
forge init
```
### Installing Infernet sdk
Install our Infernet SDK via forge.
```bash
forge install ritual-net/infernet-sdk
```
### Specifying remappings
Create a new file called `remappings.txt` in the root of your project.
```
forge-std/=lib/forge-std/src
infernet-sdk/=lib/infernet-sdk/src
```
This'll make it easier to import our dependencies. More explanation on
remappings [here](https://book.getfoundry.sh/projects/dependencies?highlight=remappings#remapping-dependencies).
### `SaysGm` contract
Under the `src/` directory, create a new file called `SaysGm.sol` with the following content:
```solidity
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.13;
import {console2} from "forge-std/console2.sol";
import {CallbackConsumer} from "infernet-sdk/consumer/Callback.sol";
contract SaysGM is CallbackConsumer {
constructor(address coordinator) CallbackConsumer(coordinator) {}
function sayGM() public {
_requestCompute(
"hello-world",
bytes("Good morning!"),
20 gwei,
1_000_000,
1
);
}
function _receiveCompute(
uint32 subscriptionId,
uint32 interval,
uint16 redundancy,
address node,
bytes calldata input,
bytes calldata output,
bytes calldata proof
) internal override {
console2.log("\n\n"
"_____ _____ _______ _ _ _\n"
"| __ \\|_ _|__ __| | | | /\\ | |\n"
"| |__) | | | | | | | | | / \\ | |\n"
"| _ / | | | | | | | |/ /\\ \\ | |\n"
"| | \\ \\ _| |_ | | | |__| / ____ \\| |____\n"
"|_| \\_\\_____| |_| \\____/_/ \\_\\______|\n\n");
console2.log("subscription Id", subscriptionId);
console2.log("interval", interval);
console2.log("redundancy", redundancy);
console2.log("node", node);
console2.log("input:");
console2.logBytes(input);
console2.log("output:");
console2.logBytes(output);
console2.log("proof:");
console2.logBytes(proof);
}
}
```
All this contract does is request compute from our `hello-world` container via the `_requestCompute` function.
An Infernet node will pick up this subscription, execute the compute, and deliver the result to our contract via
the `_receiveCompute` function.
### Adding a Deploy Script
In the `scripts` directory, add a new file called `Deploy.s.sol`:
```solidity
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.13;
import {Script, console2} from "forge-std/Script.sol";
import {SaysGM} from "../src/SaysGM.sol";
contract Deploy is Script {
function run() public {
// Setup wallet
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Log address
address deployerAddress = vm.addr(deployerPrivateKey);
console2.log("Loaded deployer: ", deployerAddress);
address coordinator = 0x5FbDB2315678afecb367f032d93F642f64180aa3;
// Create consumer
SaysGM saysGm = new SaysGM(coordinator);
console2.log("Deployed SaysHello: ", address(saysGm));
// Execute
vm.stopBroadcast();
vm.broadcast();
}
}
```
The coordinator address is the address of the Infernet coordinator. Our
Infernet Anvil Node already has `Coordinator` pre-deployed to that address.
### Adding a Call Script
Create another file under the `script` directory called `CallContract.s.sol`
```solidity
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.0;
import {Script, console2} from "forge-std/Script.sol";
import {SaysGM} from "../src/SaysGM.sol";
contract CallContract is Script {
function run() public {
// Setup wallet
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
SaysGM saysGm = SaysGM(0x663F3ad617193148711d28f5334eE4Ed07016602);
saysGm.sayGM();
vm.stopBroadcast();
}
}
```
### Building the Project
Before building our project, we'll need to add this line to the `foundry.toml` file:
```
via_ir = true
```
So your `foundry.toml` file should look like [this](./foundry.toml). Otherwise the compiler will complain
about stack too deep errors.
Now, let's build our project.
```bash
forge build
```
The project should build successfully.
### Deploying the Contracts
**Deploy Infernet**
To deploy our contracts, and later be able to call and test them, we'll need to deploy infernet, as well as
our `hello-world` container! Refer to [the readme at the root of this project](../../README.md) for instructions on how
to do that.
After deploying an Infernet Node locally, we'll need to run the `Deploy` script.
```bash
PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a \
forge script script/Deploy.s.sol:Deploy --broadcast \
--rpc-url http://localhost:8545
```
The private key here is anvil's anvil's third default address which contains 10000 ETH.
### Calling the Contract
Similarly, to run our `CallContract.s.sol` script, we'll invoke it with `forge script`:
```bash
PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a \
forge script script/CallContract.s.sol:Deploy --broadcast \
--rpc-url http://localhost:8545
```
### Using a `Makefile`
To make running these commands easier, we can add them to a `Makefile`. This allows
us to run `make deploy` and `make call` instead of typing out the full command every time.
Refer to [this project's Makefile](./Makefile) for an example.
### 🎉 Done!
Congratulations! You've successfully created a contract that requests compute from
our `hello-world` container.

View File

@ -0,0 +1,7 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
via_ir = true
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

View File

@ -0,0 +1,2 @@
forge-std/=lib/forge-std/src
infernet-sdk/=lib/infernet-sdk/src

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.0;
import {Script, console2} from "forge-std/Script.sol";
import {SaysGM} from "../src/SaysGM.sol";
contract CallContract is Script {
function run() public {
// Setup wallet
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
SaysGM saysGm = SaysGM(0x663F3ad617193148711d28f5334eE4Ed07016602);
saysGm.sayGM();
vm.stopBroadcast();
}
}

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.13;
import {Script, console2} from "forge-std/Script.sol";
import {SaysGM} from "../src/SaysGM.sol";
contract Deploy is Script {
function run() public {
// Setup wallet
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Log address
address deployerAddress = vm.addr(deployerPrivateKey);
console2.log("Loaded deployer: ", deployerAddress);
address coordinator = 0x5FbDB2315678afecb367f032d93F642f64180aa3;
// Create consumer
SaysGM saysGm = new SaysGM(coordinator);
console2.log("Deployed SaysHello: ", address(saysGm));
// Execute
vm.stopBroadcast();
vm.broadcast();
}
}

View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.13;
import {console2} from "forge-std/console2.sol";
import {CallbackConsumer} from "infernet-sdk/consumer/Callback.sol";
contract SaysGM is CallbackConsumer {
constructor(address coordinator) CallbackConsumer(coordinator) {}
function sayGM() public {
_requestCompute(
"hello-world",
bytes("Good morning!"),
20 gwei,
1_000_000,
1
);
}
function _receiveCompute(
uint32 subscriptionId,
uint32 interval,
uint16 redundancy,
address node,
bytes calldata input,
bytes calldata output,
bytes calldata proof
) internal override {
console2.log("\n\n"
"_____ _____ _______ _ _ _\n"
"| __ \\|_ _|__ __| | | | /\\ | |\n"
"| |__) | | | | | | | | | / \\ | |\n"
"| _ / | | | | | | | |/ /\\ \\ | |\n"
"| | \\ \\ _| |_ | | | |__| / ____ \\| |____\n"
"|_| \\_\\_____| |_| \\____/_/ \\_\\______|\n\n");
console2.log("subscription Id", subscriptionId);
console2.log("interval", interval);
console2.log("redundancy", redundancy);
console2.log("node", node);
console2.log("input:");
console2.logBytes(input);
console2.log("output:");
console2.logBytes(output);
console2.log("proof:");
console2.logBytes(proof);
}
}