Reference

On this page

Four things are the whole system:

AnswersLives in
registry recipehow to resolve a toolregistry/*.toml, embedded in the binary
block.tomlwhat you wantyour repository
block.lockwhat was decidedyour repository
the storewhat is installed$BLOCK_HOME, shared by every project

Commands is the per-command reference; this page is the formats and the machinery underneath them.

block.toml #

# Optional. Platforms `block lock` resolves artifacts for.
# Default: the platform you run lock on.
platforms = ["linux/amd64", "darwin/arm64"]

[tools]
foundry = "1.7"      # newest 1.7.x
hermes = "1"         # newest 1.x.y

A version is a dotted prefix: "1" means the newest 1.x.y, "1.7" the newest 1.7.y, "1.7.1" exactly that release. There are no operators or ranges, and pre-releases (1.8.0-rc1) are never selected.

What a version may say, in full:

WrittenMeans
"1", "1.7", "1.7.1"a dotted prefix: the newest release under it
"1.8.0-rc1"that pre-release and nothing else; a pre-release is named, never floated onto
"nightly"a channel: the release line under a tag the upstream moves
"nightly-<commit>"one release of that line, named the way the upstream tags it

The last two are the same channel; only the first of them floats. See Channels.

Tool names are looked up in the built-in Tools. A tool that is not in the registry — or one you want to fetch from a fork — can be defined in the project itself:

[tools.foo]
version = "1.2"

[tools.foo.source]
type = "github_release"
repo = "example/foo"
asset = "foo_{version}_{os}_{arch}.tar.gz"   # {version} has no "v" prefix
bin = ["foo"]                                # executables inside the archive
# tag_prefix = "v"                           # text before the version in tags
# platforms = ["linux/amd64", "darwin/arm64"]
# strip_components = 1                       # drop a wrapping directory
# [tools.foo.source.target] "darwin/arm64" = "arm64-apple-darwin"  # {target}
# [tools.foo.source.os]   linux = "unknown-linux-gnu"   # rename {os}
# [tools.foo.source.arch] amd64 = "x86_64"              # rename {arch}

A project-local source uses exactly the same model as a registry recipe, so moving a definition into the registry is a copy. That is the intended path: define the tool in your project, use it, and promote it to the registry once it is useful to more than one project. Nobody waits for a registry merge.

Supported platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64. Which of them a tool can be installed for is the upstream’s decision, and the registry records it: most blockchain CLIs publish Unix builds only, and block says so rather than substituting something else.

block.lock #

# This file is generated by block. Do not edit it by hand.

version = 1

[[tools]]
name = "foundry"
constraint = "1.7"
version = "1.7.1"
bin = ["forge", "cast", "anvil", "chisel"]

[[tools.artifacts]]
platform = "darwin/arm64"
url = "https://github.com/foundry-rs/foundry/releases/download/v1.7.1/foundry_v1.7.1_darwin_arm64.tar.gz"
sha256 = "…"

[[tools.artifacts]]
platform = "linux/amd64"
url = "https://github.com/foundry-rs/foundry/releases/download/v1.7.1/foundry_v1.7.1_linux_amd64.tar.gz"
sha256 = "…"

Three files, three responsibilities:

AnswersLives in
registry recipehow to resolve a toolregistry/*.toml (embedded)
block.tomlwhat you wantyour repository
block.lockwhat was decidedyour repository

The lock holds facts only — exact version, executables, URL and digest per platform. An artifact of a private repository additionally records the asset’s API URL as api_url, which is where block sync downloads it from with GITHUB_TOKEN: the browser URL of a private asset answers a browser session alone. A public artifact records nothing more, whether or not a token was in hand when it was locked. A project-local source additionally records a fingerprint of its definition (source = "sha256:…"), so editing it makes the pin stale. A registry recipe change never affects an existing lock: sync needs only the URLs and digests already written down.

Where the checksum comes from depends on the upstream. GitHub records a sha256 for release assets uploaded since 2025, and lock writes that down without downloading anything. Otherwise lock downloads the artifact once and records what it got — trust on first use — and every later download, on every machine, must match. Re-locking reuses the recorded digest whenever the URL is unchanged, so an unchanged artifact is never fetched twice.

Store and cache #

Downloads and installs live under one directory shared by every project:

$BLOCK_HOME/                               Unix: ~/.local/share/block
  cache/sha256/<digest>                    Windows: %LOCALAPPDATA%\block
  tools/<name>/<version>-<digest12>/       extracted installs
  shims/<command>                          one per command, for running tools by name

Two projects that pin the same artifact share one download and one install. Caching this directory in CI, keyed by block.lock, makes sync an offline operation. XDG_DATA_HOME is honored when BLOCK_HOME is unset.

The store also holds the shims directory — one file per command, so that forge and friends can be run by name. It is the only directory you put on PATH, and it is the same one for every project.

How versions are resolved #

git tags of the repository           (GET /repos/{repo}/git/matching-refs/tags/{prefix})
  → drop tags that are not <prefix>MAJOR.MINOR.PATCH    (nightly-…, stable, …)
  → drop pre-releases, apply the constraint
  → newest first: fetch the release for that tag        (GET /repos/{repo}/releases/tags/{tag})
  → skip drafts, pre-release-flagged releases and tags without a release
  → pick the asset named by the recipe for each platform

Listing tags instead of paging through /releases matters for projects such as Foundry, whose release list is dominated by hundreds of nightly builds.

A channel — a release line published under a tag that moves — is resolved the other way round, because there is nothing to list:

the moving tag                       (GET /repos/{repo}/commits/{channel})
  → the commit it points at today
  → the release published for that commit  (GET /repos/{repo}/releases/tags/{channel}-{commit})
  → pick the asset named by the recipe's channel for each platform

That tag never moves again, so what reaches block.lock is as immutable as a version: an identity, a URL and a SHA-256. An upstream that moves a tag and publishes nothing for the commit beneath it cannot be pinned, and block refuses rather than recording a URL whose contents change.

A recipe declares its channels, because a channel names its assets after itself rather than after a version:

[source.channels.nightly]
asset = "foundry_nightly_{os}_{arch}.tar.gz"

Channels #

A channel can also be asked for by the tag of one of its releases:

[tools]
foundry = "nightly-e469863b1ac3f2d9d48f9d25d068a14861060cb3"

Written that way there is nothing to dereference — the constraint already names the tag — so lock fetches that release and pins it:

the named tag                        (GET /repos/{repo}/releases/tags/{channel}-{commit})
  → pick the asset named by the recipe's channel for each platform

The two forms differ in what re-running lock does, and in nothing else:

Writtenblock lock resolvesRe-running lock
foundry = "nightly"wherever the moving tag points nowmoves the pin when the upstream retagged
foundry = "nightly-<commit>"that releasewrites the same pin back

A commit is seven to sixty-four lower-case hex digits, which is how git writes one and how a tag carries it; anything else after the hyphen is part of a channel’s own name, so a release line called pre-release stays one. A channel name is at most 32 characters, and it becomes part of a directory name under $BLOCK_HOME by way of the tag it resolves to.

Both forms need the channel declared by the recipe, because the asset names come from it. A pre-release flag is no objection to either — a nightly is a pre-release by definition — but a draft release is, since a draft publishes no assets.

Registry and source types #

The built-in registry is a directory of TOML recipes, one per tool, embedded into the binary:

# registry/hermes.toml
name = "hermes"
ecosystems = ["cosmos", "ibc"]
description = "IBC relayer connecting Cosmos SDK chains, written in Rust"

[source]
type = "github_release"
repo = "informalsystems/hermes"
asset = "hermes-v{version}-{arch}-{os}.tar.gz"
platforms = ["linux/amd64", "linux/arm64", "darwin/amd64", "darwin/arm64"]
bin = ["hermes"]

[source.os]
linux = "unknown-linux-gnu"
darwin = "apple-darwin"

[source.arch]
amd64 = "x86_64"
arm64 = "aarch64"

A recipe changes only when an upstream renames its assets or moves repositories. New releases need nothing. Recipes are data, never commands.

Not every blockchain CLI is a GitHub Release asset, and block does not stop there. But where a recipe may download from is a rule, not a preference: the registry writes it down and its linter enforces it, so a catalogue that keeps growing cannot quietly grow the set of hosts block fetches binaries from.

typewhat it doeswhere it may point
github_releaseversions from git tags; download a release asset — a .tar.gz / .tar.bz2 / .zip archive or a single raw executable — using GitHub’s own sha256 when recordeda release of the same repository the tags come from, and nowhere else
httpversions from git tags; download a prebuilt artifact over HTTPS; {commit} and {target} cover vendors that name builds by commit or by their own platform stringsonly a host listed in block-registry’s policy/hosts.toml, which names the one repository each host serves and why a release asset will not do

48 of the 51 recipes take the first; three — Bitcoin Core, geth and the go-ethereum tools — take the second, because those projects build binaries and publish them on their own server rather than attaching them to a GitHub release. A github.com URL wearing type http is refused: it would be spelling out by hand what github_release does properly, and would throw away the digest GitHub publishes beside the asset.

There is no third type. No install = "curl … | bash", no command = "make install", no package-manager shell-out, and no arbitrary-script escape hatch — a recipe is data block interprets, and adding a tool can never add a way to run something. Building a tool from source is outside the model for the same reason; Security says why block draws the line there. block also does not manage the Go, Rust, Node or Python toolchains themselves, which is where a general-purpose version manager belongs; a blockchain CLI distributed only through npm, PyPI or crates.io is therefore not in the registry.

51 tools across 17 blockchain systems are installed today with those two types alone (block list <ecosystem> shows the same, from the binary):

ecosystemtools
bitcoinbitcoin-core (bitcoind, bitcoin-cli, …), btcd, ord
ethereumfoundry (forge, cast, anvil, chisel), solc, vyper, geth, geth-tools, erigon, reth, lighthouse, prysm, prysm-validator, nimbus-eth2, echidna, medusa, hevm, ethdo, anvil-zksync
solanaagave (solana, solana-keygen, solana-test-validator, …), anchor, surfpool, solana-verify
cosmosgaia, cometbft, osmosis, ignite, cosmovisor, hermes, cosmos-relayer, celestia-app, celestia-node
ibchermes, cosmos-relayer
celestiacelestia-app, celestia-node
aptos / nearaptos, near-cli
starknetscarb, starknet-foundry, starkli
cardano / stellar / avalanchecardano-node, stellar, avalanchego, avalanche-cli
icp / fabricdfx, fabric
zksync / zk / ipfsanvil-zksync, circom, kubo

Which platforms each of them has is the upstream’s decision, and the registry records it: geth and erigon ship Linux only, lighthouse has no macOS x86-64 build, gaia builds amd64 only. block reports anything else as an unsupported platform rather than substituting something else. Tools is the full catalogue with commands and platform coverage per tool — generated from the recipes, so it cannot drift from what the binary will actually do. See registry/README.md for the recipe schema.

The recipes are written and reviewed in their own repository, block-registry. registry/ here is a vendored copy of one revision of it — generated by make registry-sync, recorded in registry/SNAPSHOT, and checked on every push, so a recipe cannot be quietly fixed in the copy. block embeds that copy rather than depending on block-registry as a Go module, which is what keeps go install github.com/nao1215/block@latest a single self-contained download and block list and block lock usable with no network at all. A block version therefore always pairs with a registry revision it was tested against, and block version prints which one:

$ block version
block v0.1.0
registry df8fa5946e00 (51 recipes from https://github.com/nao1215/block-registry)

Non-goals #

block deliberately does not:

  • act as a general package manager (no npm, cargo, pip, apt, brew sources);
  • judge protocol compatibility between tools (client/consensus pairs, hard forks, chain IDs, RPC capabilities);
  • manage wallets, keys, validators, staking or nodes;
  • generate Docker Compose files or manage Solidity library dependencies;
  • run scripts from the registry;
  • offer shell hooks or eval-style integration, block env, block add/ remove, or any command that resolves or installs implicitly — a completion script is the whole of what block gives a shell;
  • grow into the tools beside the toolchain — a block explorer, a transaction tracer, a devnet manager, an RPC doctor, a terminal UI, a test framework. Each of those is somebody else’s project, and every one of them would blur what block is for.

If a feature would turn block into mise or aqua, it does not belong here.