From afb48d4edcede45186723563e3a76e495340d237 Mon Sep 17 00:00:00 2001 From: mortimr Date: Fri, 16 Jun 2023 15:39:35 +0200 Subject: [PATCH] pgeth --- .dockerignore | 3 + .github/workflows/sync-and-build-plugins.yaml | 55 +++++ .gitignore | 4 + Dockerfile.plugins | 47 ++++ Makefile | 7 +- README.md | 12 +- build/ci.go | 3 + cmd/geth/main.go | 6 + cmd/geth/misccmd.go | 8 + eth/api_backend.pgeth.go | 27 +++ log/format.pgeth.go | 71 ++++++ pgeth/engine.go | 206 ++++++++++++++++++ plugins/.gitkeep | 0 plugins/build.sh | 17 ++ pull_plugins.sh | 15 ++ 15 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/sync-and-build-plugins.yaml create mode 100644 Dockerfile.plugins create mode 100644 eth/api_backend.pgeth.go create mode 100644 log/format.pgeth.go create mode 100644 pgeth/engine.go create mode 100644 plugins/.gitkeep create mode 100755 plugins/build.sh create mode 100755 pull_plugins.sh diff --git a/.dockerignore b/.dockerignore index 0c013d18b1..bfef708858 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,6 @@ build/_workspace build/_bin tests/testdata + +plugins/**/ +!plugins/build.sh diff --git a/.github/workflows/sync-and-build-plugins.yaml b/.github/workflows/sync-and-build-plugins.yaml new file mode 100644 index 0000000000..14e941b5a9 --- /dev/null +++ b/.github/workflows/sync-and-build-plugins.yaml @@ -0,0 +1,55 @@ +name: sync + +on: + push: + branches: + - "pgeth" + schedule: + - cron: "0 * * * *" # every hour + +permissions: write-all + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: "prepare sync operations" + run: | + git remote add geth https://github.com/ethereum/go-ethereum + git checkout master + git fetch geth master + - name: "check if diff exists" + id: diffcheck + run: echo "stdout=$(git diff master geth/master -- | wc -c)" >> $GITHUB_OUTPUT + - name: "perform sync operations" + if: steps.diffcheck.outputs.stdout != '0' + run: | + git reset --hard geth/master + git checkout pgeth + git config --global user.email "iulian@rotaru.fr" + git config --global user.name "mortimr" + git rebase --stat geth/master + - name: "build the docker image to ensure project still builds" + if: steps.diffcheck.outputs.stdout != '0' + run: docker build . --tag ghcr.io/kilnfi/pgeth:latest --tag ghcr.io/kilnfi/pgeth:${GITHUB_SHA} + - name: "push rebased master branch" + if: steps.diffcheck.outputs.stdout != '0' + run: git push origin master --force + - name: "push rebased pgeth branch" + if: steps.diffcheck.outputs.stdout != '0' + run: git push origin pgeth --force + - name: "login to ghcr" + if: steps.diffcheck.outputs.stdout != '0' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: "push to ghcr" + if: steps.diffcheck.outputs.stdout != '0' + run: | + docker push ghcr.io/kilnfi/pgeth:latest + docker push ghcr.io/kilnfi/pgeth:${GITHUB_SHA} diff --git a/.gitignore b/.gitignore index 3f27cdc00f..409a8f0204 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ profile.cov logs/ tests/spec-tests/ +#pgeth + +!plugin/.gitkeep +plugins/*/ diff --git a/Dockerfile.plugins b/Dockerfile.plugins new file mode 100644 index 0000000000..95bfac74a2 --- /dev/null +++ b/Dockerfile.plugins @@ -0,0 +1,47 @@ +# Support setting various labels on the final image +ARG COMMIT="" +ARG VERSION="" +ARG BUILDNUM="" + +# Build Geth in a stock Go builder container +FROM golang:1.21-alpine as builder + +ARG PLUGIN_REPOSITORIES="" + +RUN apk add --no-cache gcc musl-dev linux-headers git bash + +# Get dependencies - will also be cached if we won't change go.mod/go.sum +COPY go.mod /go-ethereum/ +COPY go.sum /go-ethereum/ +RUN cd /go-ethereum && go mod download + +ADD . /go-ethereum +RUN cd /go-ethereum + +ENV PLUGIN_REPOSITORIES=${PLUGIN_REPOSITORIES} + +RUN env + +RUN cd /go-ethereum && /go-ethereum/pull_plugins.sh + +RUN cd /go-ethereum && /go-ethereum/plugins/build.sh +RUN cd /go-ethereum && go run build/ci.go install ./cmd/geth + +# Pull Geth into a second stage deploy alpine container +FROM alpine:latest + +RUN apk add --no-cache ca-certificates +COPY --from=builder /go-ethereum/build/bin/geth /usr/local/bin/ +COPY --from=builder /go-ethereum/plugins /pgeth-plugins + +ENV PGETH_DIRECTORY="/pgeth-plugins" + +EXPOSE 8545 8546 30303 30303/udp +ENTRYPOINT ["geth"] + +# Add some metadata labels to help programatic image consumption +ARG COMMIT="" +ARG VERSION="" +ARG BUILDNUM="" + +LABEL commit="$COMMIT" version="$VERSION" buildnum="$BUILDNUM" diff --git a/Makefile b/Makefile index d736ef61c0..64dc9cd476 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,13 @@ # with Go source code. If you know what GOPATH is then you probably # don't need to bother with make. -.PHONY: geth android ios evm all test clean +.PHONY: geth android ios evm all test clean plugin GOBIN = ./build/bin GO ?= latest GORUN = go run -geth: +geth: plugin $(GORUN) build/ci.go install ./cmd/geth @echo "Done building." @echo "Run \"$(GOBIN)/geth\" to launch geth." @@ -36,3 +36,6 @@ devtools: env GOBIN= go install ./cmd/abigen @type "solc" 2> /dev/null || echo 'Please install solc' @type "protoc" 2> /dev/null || echo 'Please install protoc' + +plugin: + plugins/build.sh \ No newline at end of file diff --git a/README.md b/README.md index d6bc1af05c..5ba77dd7aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ -## Go Ethereum +## Pluggable Go Ethereum -Official Golang execution layer implementation of the Ethereum protocol. +Unofficial Golang execution layer implementation of the Ethereum protocol with plugin support + +The goal is to allow anyone to build [go plugins](https://pkg.go.dev/plugin) that follow our standard plugin interface, and run code alongside the node while accessing core geth capabilities. + +Plugins are running in an isolated mode, crashing with a plugin won't stop the node. Of course, playing with internal geth stuff can break your node. + +Example use cases: +- create a plugin that performs better evm simulations, extracting traces quickly +- create a block builder [![API Reference]( https://pkg.go.dev/badge/github.com/ethereum/go-ethereum diff --git a/build/ci.go b/build/ci.go index 1ffbf3074d..1ca7b1640b 100644 --- a/build/ci.go +++ b/build/ci.go @@ -209,6 +209,9 @@ func doInstall(cmdline []string) { buildTags = append(buildTags, "ckzg") } + // PGETH: ADD PUREGO BUILD TAG + buildTags = append(buildTags, "purego") + // Configure the build. gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags)...) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 4438cef560..67f51c86cc 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -45,6 +45,7 @@ import ( _ "github.com/ethereum/go-ethereum/eth/tracers/js" _ "github.com/ethereum/go-ethereum/eth/tracers/native" + "github.com/ethereum/go-ethereum/pgeth" "github.com/urfave/cli/v2" ) @@ -339,6 +340,11 @@ func geth(ctx *cli.Context) error { defer stack.Close() startNode(ctx, stack, backend, false) + pe := pgeth.NewEngine(&pgeth.PluginEngineConfig{ + Node: stack, + Backend: backend, + }) + pe.Start(ctx.Context) stack.Wait() return nil } diff --git a/cmd/geth/misccmd.go b/cmd/geth/misccmd.go index f3530c30fb..db912bfc6a 100644 --- a/cmd/geth/misccmd.go +++ b/cmd/geth/misccmd.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/internal/version" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/pgeth" "github.com/urfave/cli/v2" ) @@ -85,6 +86,13 @@ func printVersion(ctx *cli.Context) error { fmt.Println("Operating System:", runtime.GOOS) fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) + + pe := pgeth.NewEngine(&pgeth.PluginEngineConfig{ + Node: nil, + Backend: nil, + }) + pe.Version(ctx.Context) + return nil } diff --git a/eth/api_backend.pgeth.go b/eth/api_backend.pgeth.go new file mode 100644 index 0000000000..98366f12d1 --- /dev/null +++ b/eth/api_backend.pgeth.go @@ -0,0 +1,27 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eth + +import "github.com/ethereum/go-ethereum/core" + +func (b *EthAPIBackend) Ethereum() *Ethereum { + return b.eth +} + +func (e *Ethereum) Blockchain() *core.BlockChain { + return e.blockchain +} diff --git a/log/format.pgeth.go b/log/format.pgeth.go new file mode 100644 index 0000000000..d6968b69f8 --- /dev/null +++ b/log/format.pgeth.go @@ -0,0 +1,71 @@ +package log + +import ( + "bytes" + "fmt" + "strings" + "unicode/utf8" +) + +func PgethFormat(usecolor bool) Format { + return FormatFunc(func(r *Record) []byte { + var color = 0 + if usecolor { + switch r.Lvl { + case LvlCrit: + color = 35 + case LvlError: + color = 31 + case LvlWarn: + color = 33 + case LvlInfo: + color = 34 + case LvlDebug: + color = 36 + case LvlTrace: + color = 34 + } + } + + b := &bytes.Buffer{} + lvl := r.Lvl.AlignedString() + if lvl == "INFO " { + lvl = "PLUG " + } + if locationEnabled.Load() { + // Log origin printing was requested, format the location path and line number + location := fmt.Sprintf("%+v", r.Call) + for _, prefix := range locationTrims { + location = strings.TrimPrefix(location, prefix) + } + // Maintain the maximum location length for fancyer alignment + align := int(locationLength.Load()) + if align < len(location) { + align = len(location) + locationLength.Store(uint32(align)) + } + padding := strings.Repeat(" ", align-len(location)) + + // Assemble and print the log heading + if color > 0 { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s|%s]%s %s ", color, lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) + } else { + fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) + } + } else { + if color > 0 { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), r.Msg) + } else { + fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg) + } + } + // try to justify the log output for short messages + length := utf8.RuneCountInString(r.Msg) + if len(r.Ctx) > 0 && length < termMsgJust { + b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length)) + } + // print the keys logfmt style + logfmt(b, r.Ctx, color, true) + return b.Bytes() + }) +} diff --git a/pgeth/engine.go b/pgeth/engine.go new file mode 100644 index 0000000000..f5e3cf68e3 --- /dev/null +++ b/pgeth/engine.go @@ -0,0 +1,206 @@ +package pgeth + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "plugin" + "runtime/debug" + + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/mattn/go-colorable" + "gopkg.in/yaml.v2" +) + +type PluginToolkit struct { + Node *node.Node + Backend ethapi.Backend + Logger log.Logger +} + +type PluginDetails struct { + Name string `yaml:"name"` + Config map[string]interface{} `yaml:"config"` +} + +type Plugin struct { + Details PluginDetails + Start func(*PluginToolkit, map[string]interface{}, context.Context, chan (error)) + Version func() +} + +type PluginEngineConfig struct { + Node *node.Node + Backend ethapi.Backend +} + +type PluginEngine struct { + node *node.Node + backend ethapi.Backend + logger log.Logger +} + +func NewEngine(cfg *PluginEngineConfig) *PluginEngine { + logger := log.New() + + var ostream log.Handler + output := colorable.NewColorableStdout() + ostream = log.StreamHandler(output, log.PgethFormat(true)) + logger.SetHandler(ostream) + return &PluginEngine{ + node: cfg.Node, + backend: cfg.Backend, + logger: logger, + } +} + +func (p *PluginEngine) Version(ctx context.Context) error { + pluginDirectory := os.Getenv("PGETH_DIRECTORY") + + if len(pluginDirectory) == 0 { + p.logger.Warn("Skipping plugin engine startup: PGETH_DIRECTORY is empty") + return nil + } + + p.logger.Info("Starting plugin engine") + + files, err := ioutil.ReadDir(pluginDirectory) + if err != nil { + return err + } + + plugins := []*Plugin{} + + for _, file := range files { + if file.IsDir() { + newPlugin, err := p.loadPluginDetailsFromDirectory(filepath.Join(pluginDirectory, file.Name())) + if err != nil { + return err + } + plugins = append(plugins, newPlugin) + } + } + + errChan := make(chan error) + + for _, plug := range plugins { + plug.Version() + } + + go func() { + for { + select { + case <-ctx.Done(): + return + case err := <-errChan: + p.logger.Error(err.Error()) + } + } + }() + + return nil +} + +func (p *PluginEngine) Start(ctx context.Context) error { + pluginDirectory := os.Getenv("PGETH_DIRECTORY") + + if len(pluginDirectory) == 0 { + p.logger.Warn("Skipping plugin engine startup: PGETH_DIRECTORY is empty") + return nil + } + + p.logger.Info("Starting plugin engine") + + files, err := ioutil.ReadDir(pluginDirectory) + if err != nil { + return err + } + + plugins := []*Plugin{} + + for _, file := range files { + if file.IsDir() { + newPlugin, err := p.loadPluginDetailsFromDirectory(filepath.Join(pluginDirectory, file.Name())) + if err != nil { + return err + } + plugins = append(plugins, newPlugin) + } + } + + var toolkit *PluginToolkit = &PluginToolkit{ + Node: p.node, + Backend: p.backend, + Logger: p.logger, + } + + errChan := make(chan error) + + for _, plug := range plugins { + go func(_plug *Plugin) { + defer func() { + if err := recover(); err != nil { + p.logger.Error("Plugin crashed", "error", err, "plugin", _plug.Details.Name) + // logs entire trace + debug.PrintStack() + } + }() + _plug.Start(toolkit, _plug.Details.Config, ctx, errChan) + }(plug) + p.logger.Info(fmt.Sprintf("Starting \"%s\" plugin", plug.Details.Name)) + } + + go func() { + for { + select { + case <-ctx.Done(): + return + case err := <-errChan: + p.logger.Error(err.Error()) + } + } + }() + + return nil +} + +func (p *PluginEngine) loadPluginDetailsFromDirectory(pluginDirectoryPath string) (*Plugin, error) { + p.logger.Info(fmt.Sprintf("Loading plugin from %s", pluginDirectoryPath)) + + var plug Plugin + + yamlFile, err := ioutil.ReadFile(filepath.Join(pluginDirectoryPath, "config.yaml")) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(yamlFile, &plug.Details) + if err != nil { + return nil, err + } + + p.logger.Info(fmt.Sprintf("Loading config for \"%s\" = %+v", plug.Details.Name, plug.Details.Config)) + + pluginSo, err := plugin.Open(filepath.Join(pluginDirectoryPath, "plugin.so")) + if err != nil { + panic(err) + } + + startFunc, err := pluginSo.Lookup("Start") + if err != nil { + panic(err) + } + + versionFunc, err := pluginSo.Lookup("Version") + if err != nil { + panic(err) + } + + plug.Start = startFunc.(func(*PluginToolkit, map[string]interface{}, context.Context, chan (error))) + plug.Version = versionFunc.(func()) + + return &plug, nil +} diff --git a/plugins/.gitkeep b/plugins/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/build.sh b/plugins/build.sh new file mode 100755 index 0000000000..96e90e09e1 --- /dev/null +++ b/plugins/build.sh @@ -0,0 +1,17 @@ +#! /bin/bash + +for d in plugins/*/ ; do + CWD=$(pwd) + echo "fetch dependencies $d" + cd "$d" + ./dependencies.sh + cd "$CWD" +done + +for d in plugins/*/ ; do + CWD=$(pwd) + echo "building $d" + cd "$d" + /usr/local/go/bin/go build -buildmode=plugin -ldflags "-extldflags '-Wl,-z,stack-size=0x800000'" -tags "urfave_cli_no_docs,ckzg,purego" -trimpath -v -o plugin.so + cd "$CWD" +done diff --git a/pull_plugins.sh b/pull_plugins.sh new file mode 100755 index 0000000000..57c9f7d5ca --- /dev/null +++ b/pull_plugins.sh @@ -0,0 +1,15 @@ +#! /bin/bash + +arrPLUGINS=(${PLUGIN_REPOSITORIES//;/ }) + +mkdir -p /tmp/plugins +CWD=$(pwd) +cd ./plugins + +for i in "${arrPLUGINS[@]}" +do + echo "cloning $i" + git clone "$i" +done + +cd $CWD \ No newline at end of file