Build, testing and native image

Hedgehog is a program that people run to take part in the Unigrid network. This page describes how that program is built from source, how it is tested, and how it reaches users as a single download for Linux, macOS and Windows that runs without installing Java first. For what the program does once it runs, see Architecture overview.

What gets built

The build is a Maven project of six modules. The project requires Java 25 or newer; the build refuses to start on an older version.

Module Role
application The program itself: command line, network, REST interface, sporks and storage. It pulls in the three modules below it.
common A very small library that knows the program’s name, version and per-user directories.
ledger The ledger: its state, rules and consensus engine.
proxy The SOCKS5 front end, chains, sessions and exits of the SOCKS proxy.
s3 The S3 endpoint: its server, authentication, namespace index and cache.
native-image The launcher that packages the program as one executable per operating system.

The ledger, proxy and s3 modules are on the master branch and not part of a release yet. Release 0.0.9 builds them in, but a daemon runs them only with the hidden --preview switch, which turns on the parts the release leaves off.

One mvn clean install produces three kinds of output:

  • A thin jar, which holds only Hedgehog’s own code.
  • A fat jar, which holds Hedgehog and every library it needs. It runs anywhere with Java 25 through java -jar, and it is the download for platforms without an executable, such as Intel Macs.
  • A native executable for Linux, macOS on Apple Silicon and Windows.

Why the native executable is not truly native

The executable is built with GraalVM, a compiler that turns Java into machine code. Hedgehog cannot be compiled that way as a whole, because it relies on a dependency-injection container (CDI container and component lifecycle) that inspects and builds objects at run time, which GraalVM’s ahead-of-time analysis does not support without extensive configuration. The project therefore takes another route: GraalVM compiles a small launcher, and the launcher carries a trimmed-down Java runtime and the real program inside it.

flowchart LR
    C[common] --> A[application]
    M[ledger, proxy, s3] --> A
    A -->|assembly| FAT[fat jar]
    A --> N[native-image]
    C --> N
    N -->|jlink| Z[trimmed Java runtime<br/>and Hedgehog as a zip]
    Z -->|embedded by GraalVM| B[hedgehog.bin / hedgehog.exe]

The first time the executable starts, the launcher unpacks the embedded runtime into the user’s data directory, in a folder named after the SHA-1 hash of the bundle, and then starts Hedgehog on it with the arguments the user gave. A later build has a different hash and unpacks next to the old copy instead of overwriting it, and --force-unpack repeats the unpacking on demand. The cost is size: one earlier Linux build came to about 109 MB for the executable and roughly 175 MB on disk once unpacked. The launcher ends with 0 when the program does, and hands on the program’s exit codes 1, for a command that failed, and 2, for one given something it cannot use such as a refused option, without a stack trace of its own, so scripts and service managers see the same codes as with java -jar.

Running it

Fact Value
Commands daemon (a network node), cli, util and bootstrap; s3, hidden from --help, manages the access keys of the S3 endpoint, which runs only with --preview
Peer-to-peer port 52883, listening on 0.0.0.0 by default
REST port 52884, listening on localhost by default
Needed to build Java 25 and Maven; the native build also needs GraalVM 25 in GRAALVM_HOME
Native targets Linux x86_64, macOS Apple Silicon, Windows x86_64

A node is started with hedgehog daemon, or with java -jar on the fat jar. The ports are explained in Peer-to-peer network protocol and REST interface.

How it is tested

Almost every test is a property-based test written with jqwik: instead of a handful of fixed examples, the test states a rule and the framework tries it against many generated inputs. This suits a network daemon well. For example, every network packet type has a test that encodes generated packets, decodes them again and checks that nothing was lost or left unread (Peer-to-peer network protocol).

Many tests start real servers. The test harness brings up as many as twenty independent nodes inside one test run, each with its own container, so that handlers, schedules and the REST interface are exercised over real connections rather than against stand-ins. The tests replace the user’s config, data and log directories with temporary ones, so a test run never touches a developer’s real spork database.

The --preview switch in tests. PreviewMockUp fakes the switch as on for a test of a part that the release leaves off. BaseServerTest installs it for the whole class rather than in the expectations of each try, as the threads of its servers read the switch whenever they take, send or act on a spork, and a read during a try that records its expectations can take the recorded answer. BaseRestClientTest runs without it, as a node of the release, and a class that needs a part left off opts in with a @BeforeContainer method that creates a PreviewMockUp.

Tests of a default node. A test of the node as a daemon without --preview extends BaseDefaultNodeTest. It starts every eager component of such a daemon, on ports of its own and with no seeds. It tells which parts of the preview (LedgerService, S3Gateway, RepairService, ChainHost, ChainExit and SocksServer) and which of the components they build the container created. It also tells which threads of the preview run.

Lost topology rounds. The topology thread logs a round that fails and carries on, so nothing else would fail a test for it. TopologyRoundsHook, which every Weld test gets through BaseMockedWeldTest, fails a property during which the thread logs a lost round at warning level or above. A property that makes rounds fail on purpose is marked @FailsTopologyRounds.

rehearsal/ledger-rehearsal.sh runs four ledger validators from the built jar on one machine, through keys, genesis, sporks, a mint, transfers and a change of the board keys. Its nodes start with --preview --no-s3, as the ledger is left off in the release and the nodes share one host; its README.md gives the ports.

Code style is enforced as part of the build. The Checkstyle rules in checkstyle.xml run at the verify step and fail the build on a violation, so mvn install and mvn verify check style while mvn test alone does not.

Continuous integration and releases

Three GitHub workflows cover the work:

  • build.yml builds and tests every push and pull request to master on Linux, and publishes the test coverage figure shown on the project page.
  • release.yml runs when a tag such as v0.0.8 is pushed. It tests on Linux, macOS and Windows, builds the native executable on each of the three systems, smoke-tests it by starting a node and asking it for its version over REST, and prepares a draft release. The smoke test, .github/smoke-test.sh, also checks that GET /version without the token answers 401, that GET /gridspork/mint-supply answers 200 or 204, and that the node stops within 30 seconds of POST /stop. It checks that the parts the release leaves off stay off: their commands and options, --preview among them, are missing from --help, daemon --ledger exits with 2, /ledger, /storage, /gridnode and /gridspork/communication answer 503, and nothing listens on port 52885. The help is read with its ANSI colour codes removed, as the Windows executable colours it. Started by hand, the workflow only builds and keeps the outputs as run artefacts.
  • activity.yml draws the chart of lines of code by month on every push to master.

A draft becomes a release on the maintainer’s machine with release.sh. There the assets are signed with the Unigrid Foundation release key, whose public half is release-key.asc in the repository, and a signed chain snapshot is attached (Legacy chain snapshot). The signing steps are described in the repository README.md under Releases.

License

The code is under the GNU Affero General Public License version 3, together with an addendum from The Unigrid Foundation that adds attribution terms. Among them, the software may not be used to run a separate network that communicates independently of the Hedgehog network, and the Foundation must stay visibly credited in the code and in the command-line header. The terms are in COPYING and COPYING.addendum.

In the source

  • pom.xml - the parent build: modules, pinned versions, the Java 25 requirement.
  • application/pom.xml - the program’s dependencies, the fat jar and the test configuration.
  • application/src/test/java/org/unigrid/hedgehog/jqwik/ - the test harness that boots nodes and mocks.
  • application/src/test/java/org/unigrid/hedgehog/model/option/PreviewMockUp.java - the --preview switch turned on in a test.
  • application/src/test/java/org/unigrid/hedgehog/server/BaseDefaultNodeTest.java - the base of a test of a default node.
  • .github/smoke-test.sh - the smoke test of a native executable.
  • rehearsal/ledger-rehearsal.sh - the ledger rehearsal.
  • native-image/src/main/java/org/unigrid/hedgehog/nativeimage/NativeImage.java - the launcher.
  • native-image/src/main/java/org/unigrid/hedgehog/nativeimage/BundleFeature.java - embeds the runtime.
  • .github/workflows/ - the three workflows.
  • release.sh - cutting and publishing a release.
  • checkstyle.xml - the style rules the build enforces.