This commit is contained in:
mortimr 2023-06-16 15:39:35 +02:00
parent 78a3c32ef4
commit afb48d4edc
15 changed files with 477 additions and 4 deletions

View file

@ -3,3 +3,6 @@
build/_workspace
build/_bin
tests/testdata
plugins/**/
!plugins/build.sh

View file

@ -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}

4
.gitignore vendored
View file

@ -50,3 +50,7 @@ profile.cov
logs/
tests/spec-tests/
#pgeth
!plugin/.gitkeep
plugins/*/

47
Dockerfile.plugins Normal file
View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)...)

View file

@ -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
}

View file

@ -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
}

27
eth/api_backend.pgeth.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

71
log/format.pgeth.go Normal file
View file

@ -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()
})
}

206
pgeth/engine.go Normal file
View file

@ -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
}

0
plugins/.gitkeep Normal file
View file

17
plugins/build.sh Executable file
View file

@ -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

15
pull_plugins.sh Executable file
View file

@ -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