haskoin-store-1.9.0: integration/Haskoin/Store/Integration/Image.hs
-- |
-- Module : Haskoin.Store.Integration.Image
-- Copyright : (c) Haskoin Developers
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- The container image of the integration tests, built from pinned and
-- verified sources only.
--
-- The tests use no third-party images. The image starts from the official
-- Ubuntu 26.04 image, pinned by the digest of its multi-architecture index,
-- installs Valkey (the Redis-compatible server Ubuntu ships) and wrk from the
-- Ubuntu archive at a fixed snapshot date (apt checks the signatures of the
-- archive; the certificates needed to reach the snapshot service over HTTPS
-- come from the regular archive first), and downloads Bitcoin Core and
-- Bitcoin Cash Node from their official release locations, checking each
-- tarball against a SHA-256 hash pinned here. A tarball that does not match
-- stops the build.
--
-- The hashes were taken from the signed checksum files of each release and
-- checked against the release signing keys of each project when they were
-- pinned:
--
-- * Bitcoin Core 32.1: @SHA256SUMS@ from bitcoincore.org, with good
-- signatures from eight of the builders whose keys are in the
-- @bitcoin-core/guix.sigs@ repository;
-- * Bitcoin Cash Node 29.2.0: @SHA256SUMS.29.2.0.asc.*@ from the
-- @bitcoin-cash-node/announcements@ repository, with good signatures from
-- two maintainers whose keys are in @contrib/gitian-signing/pubkeys.txt@.
--
-- To move to new releases, change the versions and hashes below after
-- checking them the same way. The tag of the image is a hash of the whole
-- recipe, so a change builds a new image and the old one is no longer used.
module Haskoin.Store.Integration.Image
( testImage,
coreBin,
bchnBin,
valkeyBin,
wrkBin,
)
where
import Data.ByteString.Char8 qualified as C8
import Data.List (intercalate)
import Data.Serialize (encode)
import Data.Text qualified as T
import Haskoin (encodeHex, sha256)
import System.Exit (ExitCode (..))
import System.Process (readProcessWithExitCode)
import UnliftIO.Temporary (withSystemTempDirectory)
-- | Ubuntu 26.04, by the digest of its multi-architecture index.
base :: String
base = "docker.io/library/ubuntu@sha256:6a06111c3b0a8600feadb270aec771dc239bf407b25625f93f44ab00e11f4d3c"
-- | Date of the snapshot of the Ubuntu archive to install packages from, so
-- that the same versions are installed every time.
snapshot :: String
snapshot = "20261101T000000Z"
-- | Bitcoin Core release and the hashes of its Linux tarballs for x86_64 and
-- aarch64.
coreVersion, coreAmd64, coreArm64 :: String
coreVersion = "32.1"
coreAmd64 = "61545c17c3120fb7412f896d248b11993af04ec8817aad3649c55b2afb044963"
coreArm64 = "7ed37c7373eb6aa3e3f72842596a1afeba1b223717033cade9842b25773e1922"
-- | Bitcoin Cash Node release and the hashes of its Linux tarballs for x86_64
-- and aarch64.
bchnVersion, bchnAmd64, bchnArm64 :: String
bchnVersion = "29.2.0"
bchnAmd64 = "23bbe9ff749136a9b832a2a59023ce76a5391322c1ea2f4f24b5b91f035081bc"
bchnArm64 = "27e177e5a07dc78c2c0d18798ea2e3925111955509ecdc476e828e7ce8f46383"
-- | Daemon of Bitcoin Core in the image.
coreBin :: String
coreBin = "/opt/bitcoin-core/bin/bitcoind"
-- | Daemon of Bitcoin Cash Node in the image.
bchnBin :: String
bchnBin = "/opt/bitcoin-cash-node/bin/bitcoind"
-- | Valkey server in the image.
valkeyBin :: String
valkeyBin = "/usr/bin/valkey-server"
-- | The HTTP load generator wrk in the image, for the stress test of the
-- cache.
wrkBin :: String
wrkBin = "/usr/bin/wrk"
-- | Recipe of the image.
--
-- Every download is its own layer, ordered from the one that changes least
-- often: the packages of the snapshot, then Bitcoin Core, then Bitcoin Cash
-- Node, then wrk for the stress test, which came last. Podman keeps the
-- layers it builds and reuses each one whose instruction, and those before
-- it, did not change, so moving one node to a new release downloads only
-- that release. Nothing in the image depends on the time it is built.
--
-- The recipe stays here rather than in a Containerfile of its own: it is
-- made from the versions and hashes defined in this module, and the tag of
-- the image is a hash of it, so the tests and the stress program, which may
-- run from any directory, need no file to find. It is written one
-- instruction and one shell command per line.
containerfile :: String
containerfile =
unlines $
[ "FROM " <> base,
"ARG TARGETARCH",
-- the snapshot archive is only served over HTTPS, so first get the
-- certificates to reach it from the regular archive (over HTTP, with
-- the packages checked by the signatures of the archive), then install
-- everything, the certificates included, from the snapshot
"RUN apt-get update --error-on=any \\",
" && apt-get install -y --no-install-recommends ca-certificates \\",
" && apt-get update --error-on=any --snapshot " <> snapshot <> " \\",
" && apt-get install -y --no-install-recommends --snapshot "
<> snapshot
<> " ca-certificates curl valkey-server \\",
" && rm -rf /var/lib/apt/lists/*"
]
<> release
"/opt/bitcoin-core"
( "https://bitcoincore.org/bin/bitcoin-core-"
<> coreVersion
<> "/bitcoin-"
<> coreVersion
<> "-$a-linux-gnu.tar.gz"
)
coreAmd64
coreArm64
<> release
"/opt/bitcoin-cash-node"
( "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v"
<> bchnVersion
<> "/bitcoin-cash-node-"
<> bchnVersion
<> "-$a-linux-gnu.tar.gz"
)
bchnAmd64
bchnArm64
<> [ "RUN apt-get update --error-on=any --snapshot " <> snapshot <> " \\",
" && apt-get install -y --no-install-recommends --snapshot " <> snapshot <> " wrk \\",
" && rm -rf /var/lib/apt/lists/*"
]
where
-- download a release for the architecture of the image, check its hash,
-- and unpack it; @$a@ in the URL stands for the architecture
release dir url amd64 arm64 =
[ "RUN set -eu; \\",
" case \"$TARGETARCH\" in \\",
" amd64) a=x86_64; h=" <> amd64 <> ";; \\",
" arm64) a=aarch64; h=" <> arm64 <> ";; \\",
" *) echo \"unsupported architecture $TARGETARCH\"; exit 1;; \\",
" esac; \\",
" curl -fsSL -o /tmp/release.tar.gz \"" <> url <> "\"; \\",
" echo \"$h /tmp/release.tar.gz\" | sha256sum -c -; \\",
" mkdir -p " <> dir <> "; \\",
" tar -xzf /tmp/release.tar.gz -C " <> dir <> " --strip-components=1; \\",
" rm /tmp/release.tar.gz"
]
-- | Tag of the image: a hash of its recipe.
tag :: String
tag =
"localhost/haskoin-store-test:"
<> take 16 (T.unpack (encodeHex (encode (sha256 (C8.pack containerfile)))))
-- | Build the image unless it exists already, and return its tag. The first
-- build downloads the base image, the packages and the two releases, and may
-- take a few minutes; later builds of a changed recipe reuse the layers that
-- did not change (see the recipe above). Images and layers are never removed by
-- the tests. The build may use 1 GiB of memory.
testImage :: IO String
testImage = do
(exists, _, _) <- readProcessWithExitCode "podman" ["image", "exists", tag] ""
case exists of
ExitSuccess -> return tag
_ -> withSystemTempDirectory "haskoin-store-image" $ \ctx -> do
(code, out, err) <-
readProcessWithExitCode
"podman"
["build", "--layers", "--memory", "1g", "--tag", tag, "--file", "-", ctx]
containerfile
case code of
ExitSuccess -> return tag
_ ->
fail $
intercalate
"\n"
["Could not build the test image " <> tag <> ":", lastLines out, lastLines err]
where
lastLines = unlines . reverse . take 30 . reverse . lines