Dependency and toolchain modernization: v1.12.x archive to v1.13.x¶
This document records the dependency and toolchain changes that take Core-Geth from its December 2024 archive state to the v1.13.x line. It is a companion to 2026-03-security-audit.md and 2026-08-security-followup.md, which cover the source-level CVE remediation; this one covers what changed underneath the code.
Baseline: commit 7ef3ecd7a, 2024-12-16, the last commit of the archived v1.12.x development line.
Work carried out by: White B0x
On this page: In short · Why this work was necessary · Toolchain · Dependency delta · What deliberately did not move · Linter · Build and release pipeline · Verification · Outstanding · Supporting this work
In short¶
Upgrade to v1.13.0 or later. The archived v1.12.x line was pinned to Go toolchains that left support in August 2024 and February 2025, and its module graph had not moved since December 2024. v1.13.0 builds on Go 1.26.8, behind it 83 module changes and a current linter. This page records what moved, what deliberately did not, and how each version was chosen.
Why this work was necessary¶
The archived line was pinned to Go 1.21 in go.mod and CI, and to Go 1.22 in its download path and Docker builder. Under Go’s support policy (“each major Go release is supported until there are two newer major releases”), Go 1.21 left support in August 2024 and Go 1.22 in February 2025, so every artifact built from that line runs a standard library that no longer receives security fixes.
That failure mode is silent. An end-of-life toolchain still compiles, still passes tests, and emits no deprecation warning; nothing in an ordinary build reports it. The same applies to a dependency that is merely old rather than known-vulnerable. Neither condition announces itself, which is why this pass was scheduled rather than triggered by an incident.
Toolchain¶
| Surface | Before | After |
|---|---|---|
go.mod directive | go 1.21 | go 1.26.0 |
| CI workflows | 1.21 | 1.26; the release workflow 1.26.8 |
| Docker builder | golang:1.22-alpine | golang:1.26.8-alpine |
build/checksums.txt (version:golang) | 1.22.1 | 1.26.8 |
There is no toolchain directive; the go directive is the whole statement.
Three Go versions were in force simultaneously on the archived line: 1.21 in CI, 1.22.1 for the -dlgo download path, and 1.22 in the Docker builder. They now agree. Go toolchain records which of them built each published archive, and the Go standard library advisories each archive carries.
build/checksums.txt carries a second Go pin, version:ppa-builder, which was deliberately left alone. It is read only by the Debian source-package path, which no active workflow invokes. It is knowingly insufficient: bootstrapping a modern Go from source requires a compiler two majors back, and closing that gap needs the recursive builder the file’s own comment anticipates rather than a version bump.
The linter pin moved separately, and last. version:golangci went from 1.55.2 to 2.13.1 in its own commit, after the dependency work, for the reason the deferral anticipated: a newer golangci-lint reports findings the pinned one does not, and mixing that into a dependency diff would obscure both. See “Linter” below for what it found.
The old pin was not merely stale. golangci-lint 1.55.2 cannot read Go 1.26 standard-library export data, so it failed on every package with a typecheck error and reported no real findings at all. A linter that cannot parse the tree reports clean by failing, which is the same shape as the end-of-life toolchain problem this document opens with.
Dependency delta¶
Measured against the baseline commit with go mod edit -json: 83 modules changed, 17 removed, 16 added. The manifest went from 176 modules (83 direct, 93 indirect) to 175 (82 direct, 93 indirect).
No direct dependency was added. All 16 additions are indirect, and that is the number worth noting: this was a currency and security pass, not a change in what the client depends on.
Eight of the 16 are not new code at all but import-path renames: they succeed seven entries in the removed list, one of which split into two modules. Each successor is the choice of the library that requires the module; core-geth imports none of them itself:
| Removed | Successor |
|---|---|
StackExchange/wmi | yusufpapurcu/wmi |
cockroachdb/sentry-go | getsentry/sentry-go |
deepmap/oapi-codegen | oapi-codegen/runtime |
go-fonts/liberation | codeberg.org/go-fonts/liberation |
go-latex/latex | codeberg.org/go-latex/latex |
go-pdf/fpdf | codeberg.org/go-pdf/fpdf |
rivo/uniseg | clipperhouse/displaywidth, clipperhouse/uax29/v2 |
yusufpapurcu/wmi is a personal-account fork of StackExchange/wmi, which is archived; shirou/gopsutil requires the fork, and it is compiled into Windows builds only. v1.13.0 does not fetch it from that account: a replace directive builds it from fukuii-project/archive-reference-material at the same commit, and the two etclabscore OpenRPC modules, go-openrpc-reflect and go-jsonschema-walk, are built from the same archive the same way. The archived source is identical to each pinned version.
The remaining eight arrived transitively behind modules that were updated: apapsch/go-jsonmerge/v2, cockroachdb/fifo, emicklei/dot, olekukonko/cat, olekukonko/errors, olekukonko/ll, go.mongodb.org/mongo-driver and go.yaml.in/yaml/v3.
Direct dependencies changed¶
| Module | Before | After |
|---|---|---|
golang.org/x/crypto | v0.17.0 | v0.55.0 |
golang.org/x/sys | v0.16.0 | v0.47.0 |
golang.org/x/tools | v0.15.0 | v0.49.0 |
golang.org/x/text | v0.14.0 | v0.41.0 |
golang.org/x/sync | v0.5.0 | v0.22.0 |
golang.org/x/time | v0.3.0 | v0.15.0 |
github.com/consensys/gnark-crypto | v0.12.1 | v0.21.0 |
github.com/crate-crypto/go-kzg-4844 | v0.7.0 | v1.1.0 |
github.com/supranational/blst | v0.3.11 | v0.3.17 |
github.com/holiman/uint256 | v1.2.4 | v1.3.2 |
github.com/btcsuite/btcd/btcec/v2 | v2.2.0 | v2.5.0 |
github.com/graph-gophers/graphql-go | v1.3.0 | v1.10.2 |
github.com/gorilla/websocket | v1.5.0 | v1.5.3 |
github.com/golang-jwt/jwt/v4 | v4.5.0 | v4.5.2 |
github.com/golang/protobuf | v1.5.3 | v1.5.4 |
github.com/stretchr/testify | v1.8.4 | v1.12.1 |
github.com/tidwall/gjson | v1.6.0 | v1.19.0 |
Four of these carry consensus weight: gnark-crypto, go-kzg-4844, blst and uint256. Each was read before being taken rather than accepted on version number alone, and the elliptic-curve and KZG changes were checked against the consensus test suites named under Verification below.
golang-jwt/jwt is an authentication boundary rather than a test dependency: it backs Engine API JWT verification. Its shortest module path runs through test tooling, which understates it.
Indirect dependencies changed¶
bits-and-blooms/bitset v1.10.0 to v1.24.6 · cespare/xxhash/v2 v2.2.0 to v2.3.0 · decred/dcrd/dcrec/secp256k1/v4 v4.0.1 to v4.4.0 · rogpeppe/go-internal v1.9.0 to v1.12.0 · tidwall/match v1.0.1 to v1.1.1 · tidwall/pretty v1.0.0 to v1.2.0 · golang.org/x/mod v0.14.0 to v0.39.0 · golang.org/x/net v0.18.0 to v0.58.0 · google.golang.org/protobuf v1.31.0 to v1.33.0
Modules removed¶
Ten modules left with no successor: consensys/bavard · mmcloughlin/addchain · rsc.io/tmplfunc · opentracing/opentracing-go · google/go-cmp · fjl/memsize · campoy/embedmd · hashicorp/go-cleanhttp · hashicorp/go-retryablehttp · pmezard/go-difflib
The other seven were renames rather than removals; see the table above.
All ten are unreferenced in source. bavard, addchain and tmplfunc were gnark-crypto’s code-generation dependencies, which its current release no longer requires. fjl/memsize is the one removed for cause rather than by tidy: it reaches into runtime internals through //go:linkname, which Go 1.23 restricted, and the link failed until it was dropped. The rest fell out of go mod tidy during the toolchain upgrade. None was removed by hand.
What deliberately did not move¶
Recording these matters as much as recording the changes, because an unexplained old pin invites a later contributor to “fix” it.
The AWS SDK v2 stack (twelve modules) is held at its archived versions, which match go-ethereum’s own pins exactly. These are reachable only from cmd/devp2p, the DNS node-list publishing tool, and not from the node itself. Advancing them independently would diverge from upstream for a peripheral tool with no exposure in the client.
c-kzg-4844 remains at v0.4.0. No advisory affects it. A major-version bump on a cgo binding is the highest-risk change available in the consensus tier, and upstream go-ethereum has since moved to a different module path (c-kzg-4844/v2), so catching up is an import-path migration rather than a version change. That belongs in its own release.
The Verkle modules (go-ipa and go-verkle) remain at their archived versions. Verkle trees are not live on any Ethereum network, including Ethereum Classic, and both modules are pre-1.0 upstream.
crypto/secp256k1/libsecp256k1 is unchanged and is tracked as outstanding work. It is vendored C, not a Go module, so it is invisible to go list, go.sum and govulncheck by construction, and on a normal cgo build it, rather than the Go elliptic-curve libraries, is the active signature-verification path. Its last content synchronization predates this fork’s divergence by years. Re-vendoring C that binds through cgo requires build and correctness verification beyond the scope of a dependency pass, so it is deliberately not bundled here.
Linter¶
Raising version:golangci from 1.55.2 to 2.13.1 required migrating .golangci.yml to the v2 schema, and the migration changed what runs in two ways that were not intended and had to be undone.
golangci-lint v2 folded gosimple, stylecheck and a new quickfix family into staticcheck. This tree enabled the first two under v1 and never enabled stylecheck; quickfix did not exist. The migration therefore switched on 34 style findings nobody had opted into, most of them in code inherited from upstream. ST* and QF* are disabled again, so the check set matches what was in force before.
exportloopref was replaced by copyloopvar, which has a much wider surface: it reports every now-unnecessary x := x loop-variable copy, obsolete since Go 1.22 changed loop scoping. Those 35 were taken rather than suppressed (the copies are dead weight in a tree targeting Go 1.26), along with the comments explaining them, which described a copy that no longer existed.
Several existing exclusions had silently stopped matching. staticcheck v2 reports fully-qualified names, so patterns written against event.TypeMux is deprecated no longer matched github.com/ethereum/go-ethereum/event.TypeMux is deprecated. They were rewritten to match loosely. A broken exclusion is worse than a missing one: it looks like a considered decision while doing nothing.
Fixed rather than excluded:
crypto/bn256/cloudflare/gfp_decl.go:// go:noescapecarried a space, so the compiler directive was inert andgfpNegsilently lost it. Its three siblings in the same file were correct.core/vm/evmc.go: an assignment tostatusthat every path below returns past.reflect.PtrTotoreflect.PointerToacrossrlp/, deprecated since Go 1.22. These were masked by thegovetinlineanalyzer, which reports where a call could not be inlined: a compiler diagnostic, not a defect, and one that fired only because the linter binary is built against a newer Go than this module targets. Disabling it surfaced the real findings underneath.- An
S1009redundant nil check, anS1011append loop, and assorted stray blank lines.
What is excluded, and why it is a deferral rather than a dismissal. Go 1.26 deprecated the raw big.Int fields on crypto/ecdsa keys and the low-level crypto/elliptic operations, directing callers to crypto/ecdh. That package covers the NIST curves only, and this client is secp256k1 throughout, so for most of the flagged sites there is no replacement to migrate to. Two of them are the CVE fixes recorded in the companion documents: the IsOnCurve calls in crypto/crypto.go and crypto/ecies/ecies.go answer CVE-2025-24883 and CVE-2026-26315, where the on-curve check is the fix. The sites in p2p/discover/v4wire and p2p/enode/urlv4 encode the devp2p discovery wire format, which is fixed by the protocol rather than chosen here.
runtime.GOROOT and go/parser.ParseDir are likewise excluded in internal/build/ and cmd/geth/misccmd.go. Both are correct there: that code runs from the source tree during a build, which is the case their replacements do not cover.
make lint reports zero issues on the resulting tree.
Build and release pipeline¶
The dependency work above covers what the client links against. It does not cover what builds it, and that surface had drifted further than the module graph had.
Documentation build. The requirements set was pinned at 2021-2022 versions and the workflow installing it asked for python-version: 3.x, which resolves to whichever interpreter the runner considers newest. regex==2021.8.3 publishes no binary wheel for CPython 3.12, 3.13 or 3.14, so that install either compiled a 2021 C extension against a modern header set or failed, decided by an image nothing pinned. The workflow is additionally self-triggering: a push touching the documentation, its configuration, or the requirements file runs it. The set is rebuilt from its five actual direct requirements, twelve packages are gone because nothing needs them, and the interpreter is pinned to the version the result was verified against.
Runner images. Eight workflows selected ubuntu-latest, and the release matrix selected macos-latest and windows-latest. A floating label means the same commit builds against whatever image the label currently maps to, which is the objection that already had every action pinned to a commit rather than a tag.
That label had already moved underneath the release build. macos-latest now resolves to an Arm64 image, while the archive name records no architecture, so an Intel user’s download path silently changes what it returns. The macOS build is now two entries: osx stays x86_64, and Apple Silicon publishes beside it as osx-arm64.
Corrected 2026-09: this had already shipped, and this section originally said it had not. The text here read that every published macOS archive was x86_64 and that the next release would have been the first Arm64 one. Measured directly from the published archives afterwards, the switch happened at v1.12.20 (June 2024): v1.12.19 contains an x86_64 binary and every release from v1.12.20 through v1.12.23 contains an Arm64 one, all four under the same architecture-free osx name. So the defect was not caught before it shipped. It had shipped four times. 2026-09-release-pipeline.md carries the measurement. The remedy described above is unchanged and correct; only the claim about when it was caught was wrong.
Container images. None were ever published from this repository. build/ci.go carries a complete implementation, inherited from upstream, that nothing has ever called – not a workflow, and not any of the retired CI configurations. The images under the previous namespace were produced by a registry-side integration configured outside the source tree, so the capability was absent rather than broken, and nothing in the repository looked wrong. Images are now built and published from here, per architecture on native runners and merged into a manifest list.
Build context. The container build sent 2.96 GB, most of it never read: fixture submodules, local build output, documentation sources, and untracked working directories. It is now 1.38 GB, of which 1.16 GB is .git, kept deliberately – internal/build/env.go guards its git reads on the directory existing, so removing it does not fail the build, it silently produces a binary that reports no commit.
Verification¶
Every change was gated on the consensus suites, not on a successful build:
1 2 3 4 5 | |
The ETC difficulty vectors bind each fork label to its own rule set, so a change in elliptic-curve or integer-arithmetic behavior surfaces as a specific label failing rather than as a single opaque total. Both networks were additionally synchronized from genesis on the resulting build.
govulncheck is run in both source and binary mode. A set of advisories is reported permanently and has been adjudicated; Go toolchain records the set and the reasoning. Two properties of that check are worth stating because they invert the usual reading:
- An exit status of 0 is a change, not a pass. Findings are expected here.
- An advisory disappearing is as much a finding as one appearing. It means either the adjudication has gone stale or the artifact scanned is not the one that was adjudicated.
Most reported advisories are structural. This client retains go-ethereum’s module path, so its pseudo-version always sorts below any upstream fixed tag, and govulncheck matches on symbol names while a backported fix adds guards inside the same function. Each was confirmed by reading the guard in this tree rather than by comparing version strings.
Outstanding¶
- The vendored
libsecp256k1resynchronization described above. - The Debian source path.
version:ppa-builderwas raised from a version that could no longer bootstrap the current toolchain to the minimum Go states as required, which is what that pin needed. It remains untested: no workflow invokes the Debian source path, which is why its insufficiency surfaced by reading rather than by failing. - The container runtime base is a floating tag,
alpine:latest, which can cross a major version without any change here. The builder names the same exact Go patch asbuild/checksums.txtand the release workflow, but as a tag rather than a digest. - The
crypto/ecdhmigration for the deprecatedcrypto/ecdsaandcrypto/ellipticcall sites, once a secp256k1 path exists or upstream go-ethereum moves first. Scoped per package rather than tree-wide: the keystore and scwallet sites are the most tractable, the wire-format ones the least. - Whether to adopt the
stylecheckandquickfixfamilies that the golangci-lint v2 migration switched on and this pass switched back off. They are reasonable checks; taking them means a large mechanical diff against code inherited from upstream, so it is a trade to weigh rather than an oversight.
Dependency automation is configured and deliberately disabled; .github/dependabot.yml and AGENTS.md carry that decision and its rationale. Go’s own tooling (go list -m -u all for retractions and govulncheck for advisories) is what is run against this repository, by hand.
Supporting this work¶
The modernization this document records was carried out by White B0x as unfunded public-goods work for Ethereum Classic. Mining pools, centralized exchanges, issuers of Ethereum Classic financial products, Etchash mining hardware manufacturers and large holders all depend on this client. If your operation relies on Ethereum Classic, please help fund its maintenance. Support this work has the routes: sponsorship on GitHub, an invoiced agreement for organizations, or a direct transfer.