Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(devnet): generate devnet in go
Browse files Browse the repository at this point in the history
Create a devnet docker image for local development.
It uses rollups-contracts own deployment scripts instead of foundry-rs
and replaces the shell script previousaly developed for the same purpose.
marcelstanley committed Feb 6, 2024

Verified

This commit was signed with the committer’s verified signature.
marcelstanley Marcel Moura
1 parent 5bd4be4 commit d1ccc10
Showing 15 changed files with 475 additions and 653 deletions.
69 changes: 51 additions & 18 deletions build/Dockerfile
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ ARG ROOTFS_VERSION
ARG LINUX_VERSION
ARG LINUX_KERNEL_VERSION
ARG ROM_VERSION
ARG ANVIL_IP_ADDR

# Build directories.
ARG SNAPSHOT_BUILD_PATH=/build/snapshot
@@ -121,30 +122,27 @@ CMD /bin/bash
# STAGE: devnet-base
#
# This stage installs Foundry.
FROM ${BASE_IMAGE} as devnet-base
FROM golang:${GO_VERSION}-bookworm as devnet-base

# Install system dependencies.
ARG DEBIAN_FRONTEND=noninteractive
RUN <<EOF
set -e
apt-get update
apt-get install -y --no-install-recommends \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
jq \
wget \
xxd
git
EOF

# Install Foundry.
# Install Foundry
RUN curl -L https://foundry.paradigm.xyz | bash
ENV PATH="~/.foundry/bin:${PATH}"
ARG FOUNDRY_COMMIT_VERSION
RUN <<EOF
set -e
bash -c foundryup -C ${FOUNDRY_COMMIT_VERSION}
ln -s ~/.foundry/bin/anvil /usr/bin/anvil
ln -s ~/.foundry/bin/cast /usr/bin/cast
EOF

# STAGE: devnet-builder
@@ -154,13 +152,42 @@ FROM devnet-base as devnet-builder
ARG DEVNET_BUILD_PATH
WORKDIR ${DEVNET_BUILD_PATH}

# Install dependencies needed to install yarn
RUN <<EOF
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gnupg
EOF

# Install yarn
RUN <<EOF
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
nodejs \
yarn
EOF

# Copy scripts
COPY . .

# Prepare environment
ARG ANVIL_IP_ADDR
ENV ANVIL_IP_ADDR="${ANVIL_IP_ADDR}"
ARG ANVIL_STATE_FILE="anvil_state.json"
ENV ANVIL_STATE_FILE="${ANVIL_STATE_FILE}"

# Copy machine snapshot hash.
ARG SNAPSHOT_BUILD_PATH
COPY --from=snapshot-builder ${SNAPSHOT_BUILD_PATH}/hash hash.bin

# Generate Anvil state.
COPY scripts scripts
RUN ./scripts/devnet/gen-devnet.sh -v -t hash.bin
# Config contract location
ENV ROLLUPS_CONTRACTS_PATH=./rollups-contracts

# Generate anvil state
RUN go run ./cmd/gen-devnet --silent --template-hash-file hash.bin > deployment.json


# STAGE: rollups-node-devnet
#
@@ -169,15 +196,21 @@ FROM devnet-base as rollups-node-devnet

# Copy anvil state file
ARG DEVNET_BUILD_PATH
ENV ANVIL_STATE_PATH=/usr/share/devnet/anvil_state.json
COPY --from=devnet-builder ${DEVNET_BUILD_PATH}/anvil_state.json ${ANVIL_STATE_PATH}

# Copy healthcheck script.
COPY scripts/devnet/lib/anvil_net_listening.sh /usr/bin/anvil_net_listening.sh
HEALTHCHECK --interval=1s --timeout=1s --retries=5 CMD anvil_net_listening.sh
ENV DEVNET_SHARE_PATH=/usr/share/devnet
COPY --from=devnet-builder ${DEVNET_BUILD_PATH}/anvil_state.json ${DEVNET_SHARE_PATH}/anvil_state.json
COPY --from=devnet-builder ${DEVNET_BUILD_PATH}/deployment.json ${DEVNET_SHARE_PATH}/deployment.json

# Make Anvil bind to the external network interface.
ENV ANVIL_IP_ADDR="0.0.0.0"
ARG ANVIL_IP_ADDR
ENV ANVIL_IP_ADDR="${ANVIL_IP_ADDR}"
ENV RPC_URL="http://${ANVIL_IP_ADDR}:8545"
HEALTHCHECK --interval=1s --timeout=1s --retries=5 CMD curl \
-X \
POST \
-s \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"net_listening","params":[]}' \
${RPC_URL}

# Start Anvil.
CMD anvil --block-time 1 --load-state $ANVIL_STATE_PATH
1 change: 1 addition & 0 deletions build/docker-bake.hcl
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ target "common" {
LINUX_VERSION = "0.17.0"
LINUX_KERNEL_VERSION = "5.15.63-ctsi-2-v0.17.0"
ROM_VERSION = "0.17.0"
ANVIL_IP_ADDR = "0.0.0.0"
}
}

30 changes: 30 additions & 0 deletions cmd/gen-devnet/anvil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

import (
"os"
"strconv"

"github.com/cartesi/rollups-node/internal/services"
)

const ANVIL_STATE_INTERVAL int = 1

func newAnvilService(depConfig DeploymentConfig) services.CommandService {
var s services.CommandService
s.Name = "anvil"
s.HealthcheckPort = 8545
s.Path = "anvil"

s.Args = append(s.Args, "--host", depConfig.AnvilIpAddr)
s.Args = append(s.Args, "--dump-state", depConfig.AnvilStateFilePath)
s.Args = append(s.Args, "--state-interval", strconv.Itoa(ANVIL_STATE_INTERVAL))
s.Args = append(s.Args, "--silent")

s.Env = append(s.Env, "ANVIL_IP_ADDR=0.0.0.0")
s.Env = append(s.Env, os.Environ()...)

return s
}
130 changes: 130 additions & 0 deletions cmd/gen-devnet/deployer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"

"github.com/cartesi/rollups-node/internal/config"
)

func deploy(ctx context.Context,
depConfig DeploymentConfig) (DeploymentInfo, error) {
var depInfo DeploymentInfo

config.InfoLogger.Printf("deployer: deploying %s", depConfig.RollupsContractsPath)
err := deployRollupsContracts(ctx, depConfig)
if err != nil {
return depInfo, fmt.Errorf("could not deploy rollups-contracts: %v", err)
}

config.InfoLogger.Println("deployer: creating application...")
depInfo, err = createApplication(ctx, depConfig)
if err != nil {
return depInfo, fmt.Errorf("could not create Application: %v", err)
}

config.InfoLogger.Println("deployer: gathering application info...")
return depInfo, nil
}

// Create a Rollups Application by calling the necessary factories
func createApplication(ctx context.Context, depConfig DeploymentConfig) (DeploymentInfo, error) {
var depInfo DeploymentInfo
if depConfig.Hash == "" {
return DeploymentInfo{}, fmt.Errorf("machine hash is missing")
}

// Create the Authority/History pair
addresses, _, err := execContract(ctx,
depConfig,
depConfig.AuthorityHistoryFactoryAddress,
"newAuthorityHistoryPair(address,bytes32)(address,address)",
depConfig.SignerAddress,
depConfig.Salt)
if err != nil {
return DeploymentInfo{}, fmt.Errorf("could not create authority/history pair: %v", err)
}

depInfo.AuthorityAddress = addresses[0]
depInfo.HistoryAddress = addresses[1]

// Create the Application, passing the address of the newly created Authority
addresses, blockNumber, err := execContract(ctx,
depConfig,
depConfig.ApplicationFactoryAddress,
"newApplication(address,address,bytes32,bytes32)(address)",
depInfo.AuthorityAddress,
depConfig.SignerAddress,
depConfig.Hash,
depConfig.Salt)
if err != nil {
return DeploymentInfo{}, fmt.Errorf("could not create application: %v", err)
}

depInfo.ApplicationAddress = addresses[0]
depInfo.BlockNumber = blockNumber

return depInfo, nil
}

// Call a contract factory, passing a factory function to be executed.
// Returns the resulting contract address(es) and the corresponding
// block number.
//
// Warning: a second call to a contract with the same arguments will fail.
func execContract(ctx context.Context,
depConfig DeploymentConfig,
args ...string) ([]string, string, error) {
rpcUrl := "http://" + depConfig.AnvilIpAddr + ":" + depConfig.AnvilPort
commonArgs := []string{"--rpc-url", rpcUrl}
commonArgs = append(commonArgs, args...)

var addresses []string
// Calculate the resulting deterministic address(es)
castCall := exec.CommandContext(ctx,
"cast",
"call")
castCall.Args = append(castCall.Args, commonArgs...)
var outStrBuilder strings.Builder
castCall.Stdout = &outStrBuilder
err := castCall.Run()
if err != nil {
return addresses, "", fmt.Errorf("command failed %v: %v", castCall.Args, err)
}
addresses = strings.Fields(outStrBuilder.String())

// Perform actual transaction on the contract
castSend := exec.CommandContext(ctx,
"cast",
"send",
"--json",
"--mnemonic",
depConfig.Mnemonic)
castSend.Args = append(castSend.Args, commonArgs...)
outStrBuilder.Reset()
castSend.Stdout = &outStrBuilder
err = castSend.Run()
if err != nil {
return addresses, "", fmt.Errorf("command failed %v: %v", castSend.Args, err)
}
if !depConfig.IsSilent {
config.InfoLogger.Printf("deployer: executed command %s", castSend.Args)
config.InfoLogger.Printf("deployer: output: %s", outStrBuilder.String())
}

// Extract blockNumber from JSON output
jsonMap := make(map[string](any))
err = json.Unmarshal([]byte([]byte(outStrBuilder.String())), &jsonMap)
if err != nil {
return addresses, "", fmt.Errorf("failed to extract block number, %s", err.Error())
}
blockNumber := jsonMap["blockNumber"].(string)

return addresses, blockNumber, nil
}
166 changes: 166 additions & 0 deletions cmd/gen-devnet/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"

"github.com/cartesi/rollups-node/internal/config"
"github.com/cartesi/rollups-node/internal/services"
"github.com/cartesi/rollups-node/pkg/addresses"
"github.com/cartesi/rollups-node/pkg/ethutil"
"github.com/ethereum/go-ethereum/common"
"github.com/spf13/cobra"
)

const CMD_NAME = "gen-devnet"

var Cmd = &cobra.Command{
Use: CMD_NAME,
Short: "Generates a devnet for testing",
Long: `Generates a devnet to be used for testing.
It uses a previously generated Cartesi Machine snapshot
and deploys an Application based upon it's hash file.`,
Run: run,
}

var (
hashFile string
anvilStateFilePath string
isSilent bool

depConfig = DeploymentConfig{
AnvilPort: "8545",
AuthorityHistoryFactoryAddress: "0x3890A047Cf9Af60731E80B2105362BbDCD70142D",
ApplicationFactoryAddress: common.Address.
Hex(addresses.GetTestBook().CartesiDAppFactory),
SignerAddress: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
Mnemonic: ethutil.FoundryMnemonic,
Salt: "0x0000000000000000000000000000000000000000000000000000000000000000",
}
)

func init() {
Cmd.Flags().StringVarP(&hashFile,
"template-hash-file",
"t",
"",
"path for a Cartesi Machine template hash file")
cobra.CheckErr(Cmd.MarkFlagRequired("template-hash-file"))

Cmd.Flags().StringVarP(&anvilStateFilePath,
"anvil-state-file",
"a",
"./anvil_state.json",
"path for the resulting anvil state file")
Cmd.Flags().BoolVarP(&isSilent,
"silent",
"s",
false,
"verbose output")
}

func main() {
err := Cmd.Execute()
if err != nil {
os.Exit(1)
}
}

func run(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

depConfig.AnvilStateFilePath = anvilStateFilePath
depConfig.IsSilent = isSilent

value, err := loadEnvVar("ROLLUPS_CONTRACTS_PATH")
if err != nil {
config.ErrorLogger.Println(err)
return
}
depConfig.RollupsContractsPath = value

value, err = loadEnvVar("ANVIL_IP_ADDR")
if err != nil {
config.ErrorLogger.Println(err)
return
}
depConfig.AnvilIpAddr = value

// Add machine hash to config
hash, err := readMachineHash(hashFile)
if err != nil {
config.ErrorLogger.Printf("failed to read hash: %v", err)
return
}
depConfig.Hash = hash

// Supervisor handles only Anvil
ready := make(chan struct{}, 1)
go func() {
var s []services.Service

s = append(s, newAnvilService(depConfig))

supervisor := newSupervisorService(s)
if err := supervisor.Start(ctx, ready); err != nil {
config.ErrorLogger.Printf("%s: %v", supervisor.Name, err)
cancel()
}
}()

// Deploy rollups-contracts and create Application
go func() {
defer cancel()
select {
case <-ready:
depInfo, err := deploy(ctx, depConfig) //, result)
if err != nil {
config.ErrorLogger.Printf("%s: deployment failed. %v", CMD_NAME, err)
}

jsonInfo, err := json.MarshalIndent(depInfo, "", "\t")
if err != nil {
config.ErrorLogger.Printf("%s: failed to parse deployment info. %v", CMD_NAME, err)
} else {
os.Stdout.Write(jsonInfo)
}

case <-ctx.Done():
}
}()

<-ctx.Done()
}

// Read template machine hash from file
func readMachineHash(hashPath string) (string, error) {
data, err := os.ReadFile(hashPath)
if err != nil {
return "", fmt.Errorf("error reading %v (%v)", hashPath, err)
}

return hex.EncodeToString(data), nil
}

func newSupervisorService(s []services.Service) services.SupervisorService {
return services.SupervisorService{
Name: CMD_NAME + "-supervisor",
Services: s,
}
}

func loadEnvVar(key string) (string, error) {
value, exist := os.LookupEnv(key)
if !exist {
return "", fmt.Errorf("required env var %v is not set", key)
}

return value, nil
}
67 changes: 67 additions & 0 deletions cmd/gen-devnet/rollups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

import (
"context"
"fmt"
"os"
"os/exec"

cp "github.com/otiai10/copy"
)

const (
PROJECT_NAME = "rollups-contracts"
ZIP_FILE = PROJECT_NAME + ".tar.gz"
)

// Deploy rollups-contracts to a local anvil instance
func deployRollupsContracts(ctx context.Context, depConfig DeploymentConfig) error {
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)

//Assumes gitmodule location relative to cmd/gen-devnet
if err := copyContracts(depConfig.RollupsContractsPath, tmpDir); err != nil {
return fmt.Errorf("failed to copy contracts: %v", err)
}

if err := deployContracts(ctx, depConfig, tmpDir); err != nil {
return fmt.Errorf("failed to deploy contracts: %v", err)
}
return nil
}

// Copy only the rollups hardhat project
func copyContracts(srcDir string, destDir string) error {
//Assumes hardhat project location from current rollups-contract version
srcPath := srcDir + string(os.PathSeparator) + "onchain" + string(os.PathSeparator) + "rollups"
destPath := destDir + string(os.PathSeparator) + "rollups"
return cp.Copy(srcPath, destPath)
}

// Deploy contracts by using the project deployment script
func deployContracts(ctx context.Context, depConfig DeploymentConfig, execDir string) error {
cmd := exec.CommandContext(ctx, "yarn", "install")
cmd.Dir = execDir + string(os.PathSeparator) + "rollups"
if err := cmd.Run(); err != nil {
return fmt.Errorf("command '%v' failed with %v", cmd.String(), err)
}

cmd = exec.CommandContext(ctx, "yarn", "deploy:development")
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "RPC_URL=http://"+depConfig.AnvilIpAddr+":"+depConfig.AnvilPort)
cmd.Dir = execDir + string(os.PathSeparator) + "rollups"
if !depConfig.IsSilent {
cmd.Stdout = os.Stdout
}

if err := cmd.Run(); err != nil {
return fmt.Errorf("command '%v' failed with %v", cmd.String(), err)
}
return nil
}
25 changes: 25 additions & 0 deletions cmd/gen-devnet/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

type DeploymentInfo struct {
AuthorityAddress string `json:"CARTESI_CONTRACTS_AUTHORITY_ADDRESS"`
HistoryAddress string `json:"CARTESI_CONTRACTS_HISTORY_ADDRESS"`
ApplicationAddress string `json:"CARTESI_CONTRACTS_APPLICATION_ADDRESS"`
BlockNumber string `json:"CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER"`
}

type DeploymentConfig struct {
AnvilIpAddr string
AnvilPort string
AnvilStateFilePath string
RollupsContractsPath string
AuthorityHistoryFactoryAddress string
ApplicationFactoryAddress string
SignerAddress string
Mnemonic string
Salt string
Hash string
IsSilent bool
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ require (
github.com/Khan/genqlient v0.6.0
github.com/deepmap/oapi-codegen/v2 v2.0.0
github.com/oapi-codegen/runtime v1.1.1
github.com/otiai10/copy v1.14.0
golang.org/x/sync v0.3.0
golang.org/x/text v0.14.0
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -242,6 +242,10 @@ github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
364 changes: 0 additions & 364 deletions scripts/devnet/gen-devnet.sh

This file was deleted.

60 changes: 0 additions & 60 deletions scripts/devnet/lib/anvil.sh

This file was deleted.

11 changes: 0 additions & 11 deletions scripts/devnet/lib/anvil_net_listening.sh

This file was deleted.

168 changes: 0 additions & 168 deletions scripts/devnet/lib/contracts.sh

This file was deleted.

30 changes: 0 additions & 30 deletions scripts/devnet/lib/util.sh

This file was deleted.

2 changes: 0 additions & 2 deletions scripts/devnet/remappings.txt

This file was deleted.

0 comments on commit d1ccc10

Please sign in to comment.