Reference
On this page
Four things are the whole system:
| Answers | Lives in | |
|---|---|---|
| registry recipe | how to resolve a tool | registry/*.toml, embedded in the binary |
block.toml | what you want | your repository |
block.lock | what was decided | your repository |
| the store | what 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:
| Written | Means |
|---|---|
"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:
| Answers | Lives in | |
|---|---|---|
| registry recipe | how to resolve a tool | registry/*.toml (embedded) |
block.toml | what you want | your repository |
block.lock | what was decided | your 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:
| Written | block lock resolves | Re-running lock |
|---|---|---|
foundry = "nightly" | wherever the moving tag points now | moves the pin when the upstream retagged |
foundry = "nightly-<commit>" | that release | writes 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.
| type | what it does | where it may point |
|---|---|---|
github_release | versions 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 recorded | a release of the same repository the tags come from, and nowhere else |
http | versions from git tags; download a prebuilt artifact over HTTPS; {commit} and {target} cover vendors that name builds by commit or by their own platform strings | only 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):
| ecosystem | tools |
|---|---|
| bitcoin | bitcoin-core (bitcoind, bitcoin-cli, …), btcd, ord |
| ethereum | foundry (forge, cast, anvil, chisel), solc, vyper, geth, geth-tools, erigon, reth, lighthouse, prysm, prysm-validator, nimbus-eth2, echidna, medusa, hevm, ethdo, anvil-zksync |
| solana | agave (solana, solana-keygen, solana-test-validator, …), anchor, surfpool, solana-verify |
| cosmos | gaia, cometbft, osmosis, ignite, cosmovisor, hermes, cosmos-relayer, celestia-app, celestia-node |
| ibc | hermes, cosmos-relayer |
| celestia | celestia-app, celestia-node |
| aptos / near | aptos, near-cli |
| starknet | scarb, starknet-foundry, starkli |
| cardano / stellar / avalanche | cardano-node, stellar, avalanchego, avalanche-cli |
| icp / fabric | dfx, fabric |
| zksync / zk / ipfs | anvil-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.