From 50308a44b4cf2cfc09eabffa6be1aac3d87b53f7 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Tue, 11 Jun 2024 14:15:40 -0700 Subject: [PATCH] initial commit --- .github/workflows/publish.yml | 47 +++++++++++ Dockerfile | 38 +++++++++ README.md | 27 +++++++ cmd/cpuminerd/main.go | 147 ++++++++++++++++++++++++++++++++++ go.mod | 26 ++++++ go.sum | 45 +++++++++++ 6 files changed, 330 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 cmd/cpuminerd/main.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4c81057 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Publish + +# Controls when the action will run. +on: + # Triggers the workflow on new SemVer tags + push: + branches: + - master + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-**' + +concurrency: + group: ${{ github.workflow }} + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/metadata-action@v5 + name: generate tags + id: meta + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=semver,pattern={{version}} + - uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cdff3d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM docker.io/library/golang:1.21 AS builder + +WORKDIR /cpuminerd + +# Install dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source +COPY . . + +# Enable CGO for sqlite3 support +ENV CGO_ENABLED=1 + +RUN go generate ./... +RUN go build -o bin/ -tags='netgo timetzdata' -trimpath -a -ldflags '-s -w' ./cmd/cpuminerd + +FROM scratch +LABEL maintainer="The Sia Foundation " \ + org.opencontainers.image.description.vendor="The Sia Foundation" \ + org.opencontainers.image.description="A basic cpu miner for mining using a walletd node" \ + org.opencontainers.image.source="https://github.com/SiaFoundation/cpuminer" \ + org.opencontainers.image.licenses=MIT + +ENV PUID=0 +ENV PGID=0 + +# copy binary and prepare data dir. +COPY --from=builder /cpuminerd/bin/* /usr/bin/ + +# API port +EXPOSE 9980/tcp +# RPC port +EXPOSE 9981/tcp + +USER ${PUID}:${PGID} + +ENTRYPOINT [ "cpuminerd" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c35e124 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# CPU Miner + +A very simple (and naive) single-threaded CPU miner for Siacoin testnet mining + +## Usage + +```bash +./cpuminerd --addr="addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" --http="http://localhost:9980/api" --password="sia is cool" +``` + +## Docker Compose +```yml +services: + walletd: + image: ghcr.io/siafoundation/walletd:master + ports: + - localhost:9980:9980 + - 9981: 9981 + volumes: + - ./wallet:/data + restart: unless-stopped + cpu-miner: + image: ghcr.io/siafoundation/cpuminer:master + command: --addr="addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" --http="http://walletd:9980/api" --password="sia is cool" + restart: unless-stopped + + diff --git a/cmd/cpuminerd/main.go b/cmd/cpuminerd/main.go new file mode 100644 index 0000000..c32c225 --- /dev/null +++ b/cmd/cpuminerd/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "flag" + "fmt" + "math/big" + "os" + "time" + + "go.sia.tech/core/types" + "go.sia.tech/coreutils" + "go.sia.tech/walletd/api" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "lukechampine.com/frand" +) + +func runCPUMiner(c *api.Client, minerAddr types.Address, log *zap.Logger) { + log.Info("starting cpu miner", zap.String("minerAddr", minerAddr.String())) + start := time.Now() + + check := func(msg string, err error) bool { + if err != nil { + log.Error(msg, zap.Error(err)) + time.Sleep(15 * time.Second) + return false + } + return true + } + + for { + elapsed := time.Since(start) + cs, err := c.ConsensusTipState() + if !check("failed to get consensus tip state", err) { + continue + } + + d, _ := new(big.Int).SetString(cs.Difficulty.String(), 10) + d.Mul(d, big.NewInt(int64(1+elapsed))) + log := log.With(zap.Uint64("height", cs.Index.Height+1), zap.Stringer("parentID", cs.Index.ID), zap.Stringer("difficulty", d)) + + log.Debug("mining block") + txns, v2txns, err := c.TxpoolTransactions() + if !check("failed to get txpool transactions", err) { + continue + } + + b := types.Block{ + ParentID: cs.Index.ID, + Nonce: cs.NonceFactor() * frand.Uint64n(100), + Timestamp: types.CurrentTimestamp(), + MinerPayouts: []types.SiacoinOutput{{Address: minerAddr, Value: cs.BlockReward()}}, + Transactions: txns, + } + for _, txn := range txns { + b.MinerPayouts[0].Value = b.MinerPayouts[0].Value.Add(txn.TotalFees()) + } + for _, txn := range v2txns { + b.MinerPayouts[0].Value = b.MinerPayouts[0].Value.Add(txn.MinerFee) + } + if len(v2txns) > 0 || cs.Index.Height+1 >= cs.Network.HardforkV2.RequireHeight { + b.V2 = &types.V2BlockData{ + Height: cs.Index.Height + 1, + Transactions: v2txns, + } + b.V2.Commitment = cs.Commitment(cs.TransactionsCommitment(b.Transactions, b.V2Transactions()), b.MinerPayouts[0].Address) + } + + if !coreutils.FindBlockNonce(cs, &b, time.Minute) { + log.Debug("failed to find nonce") + continue + } + log.Debug("found nonce", zap.Uint64("nonce", b.Nonce)) + index := types.ChainIndex{Height: cs.Index.Height + 1, ID: b.ID()} + tip, err := c.ConsensusTip() + if !check("failed to get consensus tip:", err) { + continue + } + + if tip != cs.Index { + log.Info("mined stale block", zap.Stringer("current", tip), zap.Stringer("original", cs.Index)) + } else if err := c.SyncerBroadcastBlock(b); err != nil { + log.Error("mined invalid block", zap.Error(err)) + } + log.Info("mined block", zap.Stringer("blockID", index.ID), zap.Stringer("fees", b.MinerPayouts[0].Value), zap.Int("transactions", len(b.Transactions)), zap.Int("v2transactions", len(b.V2Transactions()))) + } +} + +func parseLogLevel(level string) zap.AtomicLevel { + switch level { + case "debug": + return zap.NewAtomicLevelAt(zap.DebugLevel) + case "info": + return zap.NewAtomicLevelAt(zap.InfoLevel) + case "warn": + return zap.NewAtomicLevelAt(zap.WarnLevel) + case "error": + return zap.NewAtomicLevelAt(zap.ErrorLevel) + default: + fmt.Printf("invalid log level %q", level) + os.Exit(1) + } + panic("unreachable") +} + +func main() { + var ( + minerAddrStr string + + apiAddress string + apiPassword string + + logLevel string + ) + + flag.StringVar(&minerAddrStr, "address", "", "address to send mining rewards to") + flag.StringVar(&apiAddress, "api", "localhost:9980", "address of the walletd API") + flag.StringVar(&apiPassword, "password", "", "password for the walletd API") + flag.StringVar(&logLevel, "log.level", "info", "log level") + flag.Parse() + + var address types.Address + if err := address.UnmarshalText([]byte(minerAddrStr)); err != nil { + panic(err) + } + + c := api.NewClient(apiAddress, apiPassword) + if _, err := c.ConsensusTip(); err != nil { + panic(err) + } + + cfg := zap.NewProductionEncoderConfig() + cfg.EncodeTime = zapcore.RFC3339TimeEncoder + cfg.EncodeDuration = zapcore.StringDurationEncoder + cfg.EncodeLevel = zapcore.CapitalColorLevelEncoder + + cfg.StacktraceKey = "" + cfg.CallerKey = "" + encoder := zapcore.NewConsoleEncoder(cfg) + + log := zap.New(zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), parseLogLevel(logLevel))) + defer log.Sync() + + zap.RedirectStdLog(log) + + runCPUMiner(c, address, log) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9c2e77b --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module go.sia.tech/cpuminerd + +go 1.21.8 + +toolchain go1.21.11 + +require ( + go.sia.tech/core v0.2.7 + go.sia.tech/coreutils v0.0.5 + go.sia.tech/walletd v0.1.1-alpha.0.20240610172105-eb95d4161b91 + go.uber.org/zap v1.27.0 + lukechampine.com/frand v1.4.2 +) + +require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + go.etcd.io/bbolt v1.3.10 // indirect + go.sia.tech/jape v0.11.2-0.20240306154058-9832414a5385 // indirect + go.sia.tech/mux v1.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/tools v0.22.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d24e5c3 --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.sia.tech/core v0.2.7 h1:9Q/3BHL6ziAMPeiko863hhTD/Zs2s7OqEUiPKouDny8= +go.sia.tech/core v0.2.7/go.mod h1:BMgT/reXtgv6XbDgUYTCPY7wSMbspDRDs7KMi1vL6Iw= +go.sia.tech/coreutils v0.0.5 h1:Jj03VrqAayYHgA9fwV13+X88WB+Wr1p8wuLw2B8d2FI= +go.sia.tech/coreutils v0.0.5/go.mod h1:SkSpHeq3tBh2ff4HXuBk2WtlhkYQQtdcvU4Yv1Rd2bU= +go.sia.tech/jape v0.11.2-0.20240306154058-9832414a5385 h1:Gho1g6pkv56o6Ut9cez/Yu5o4xlA8WNkDbPn6RWXL7g= +go.sia.tech/jape v0.11.2-0.20240306154058-9832414a5385/go.mod h1:wU+h6Wh5olDjkPXjF0tbZ1GDgoZ6VTi4naFw91yyWC4= +go.sia.tech/mux v1.2.0 h1:ofa1Us9mdymBbGMY2XH/lSpY8itFsKIo/Aq8zwe+GHU= +go.sia.tech/mux v1.2.0/go.mod h1:Yyo6wZelOYTyvrHmJZ6aQfRoer3o4xyKQ4NmQLJrBSo= +go.sia.tech/walletd v0.1.1-alpha.0.20240610172105-eb95d4161b91 h1:9saK9WcYSK3DDwj6aWyM3MUYlyZaJFiYzdm7KHg4I+Y= +go.sia.tech/walletd v0.1.1-alpha.0.20240610172105-eb95d4161b91/go.mod h1:1CaApRlon10cIDnocnErbolh416CiUeP5jgEbVtmWx8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= +lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s=