Skip to content

How plugins load

A plugin JAR is checked twice: by tools before you deploy it, and by every node when it starts. Neither check makes plugin code safe to run. This page shows what each check covers, what the node’s class loader is, and where the trust boundaries are.

  • You’ll learn: which command catches which mistake, the order of the node’s start-up checks, what the shared loader does and does not isolate, and how to find a member that runs a different JAR.
  • Before you start: read SPI and manifest for the files a plugin JAR carries.

These commands read plugin JARs as data. None of them runs the plugin’s code.

CommandRun itIt checks
./yano.sh appchain plugin validate <jar> --trust-key <id>=<key> (or inspect)after gradle jarThe Ed25519 envelope over the catalog, runtime manifest and configuration metadata. validate --output exports a data-only catalog snapshot.
tools/yano-plugins/bin/yano-plugins validate <jar>before copying JARs to a nodeThe manifest, the match between manifest and ServiceLoader entries, no packaged Yano API classes, and API compatibility with this host. --api-major and --api-level target another host. Exit 2 means invalid.
./yano.sh appchain init --plugin-jar <jar> --trust-key …when you create the projectThe signed snapshot; it then pins the catalog, manifest, metadata and JAR digests in appchain.lock. render and doctor re-verify the snapshot.
./yano.sh appchain doctor <project> --distribution <dir>before starting nodesEvery pinned JAR is present in the distribution and unchanged.
./yano.sh appchain drift <project> --peer <api-base-url>against running nodesEach node’s identity against the project, including its plugin catalog fingerprint.

yano-plugins uses the same strict scanner as the node, so a JAR it rejects as malformed would also stop a node from starting. It is the quickest check after a build.

A JVM node builds its plugin catalog once, at start-up, before any plugin code runs. Step through the checks in order, then try a “What if” to see which check stops a broken bundle.

From JAR to running providerIllustrative data

What you're seeing: a JVM node starts with one plugin JAR in its plugin directory. Step through the checks in the order the host runs them, then try a “What if” to see which check stops a broken bundle.

  1. Capture At start-up the node copies every JAR in yano.plugins.directory into a private snapshot before it reads or loads anything: at most 256 JARs, 1 GiB each and 4 GiB in total. Replacing a JAR later does not change the running node.
  2. Scan The scanner reads each snapshot as data and loads no code. It needs one manifest, no copy of the host API, ServiceLoader entries that match the manifest exactly, and no Class-Path outside the recorded JARs.
    • passes: Exactly one qualified schema-v1 manifest
    • passes: No packaged class under org/yanoproject/api/
    • passes: Manifest contributions and ServiceLoader entries agree
    • passes: No JAR-manifest Class-Path
  3. Check the API range The manifest’s yanoApi {min, max, minLevel} must include the host’s API major, 3, and the host’s level, 12, must be at least minLevel. There is a maximum major but no maximum level.
    • passes: min ≤ 3 ≤ max, and 12 ≥ minLevel
  4. Validate in isolation Each JAR is opened alone in a short-lived loader to resolve its provider types, even JARs that policy will filter out. No provider constructor or static initializer runs.
  5. Apply policy yano.plugins.allow-list and yano.plugins.deny-list select bundles. A filtered bundle stays in the inventory but never reaches the executable loader. A JAR that mixes selected and filtered bundles is refused.
  6. Build the shared loader Selected snapshots join one parent-first class loader, in SHA-256 order. Host classes win over any plugin copy. Plugins are trusted, in-process code: this loader is not a sandbox.
  7. Correlate providers On the shared loader, every manifested provider must have a matching ServiceLoader entry, and it must come from its own JAR.
    • passes: Each manifested provider has a ServiceLoader entry from its own JAR
  8. Order bundles Declared bundle dependencies, versions and cycles are checked, and the selected bundles are put in dependency order.
  9. Fingerprint Every JAR is hashed again and must be unchanged. The catalog fingerprint covers the host API level and each bundle’s id, version, digest and yanoApi. The node logs it at start-up and reports it as pluginCatalogFingerprint in the chain identity.
  10. Publish and activate The immutable registry is published. Only then do lifecycle callbacks run, and each provider is constructed when the node first needs it, for example when a chain selects its state machine.

What if: It embeds a host API class

The bundle was packaged with a copy of yano-core-api inside it.

  1. Capture At start-up the node copies every JAR in yano.plugins.directory into a private snapshot before it reads or loads anything: at most 256 JARs, 1 GiB each and 4 GiB in total. Replacing a JAR later does not change the running node.
  2. Scan The JAR contains classes under org/yanoproject/api/. Two copies of one interface would break type identity, so the scanner refuses the artifact before any code is loaded.
    • passes: Exactly one qualified schema-v1 manifest
    • fails: No packaged class under org/yanoproject/api/ plugin artifact packages Yano API class
    • not reached: Manifest contributions and ServiceLoader entries agree
    • not reached: No JAR-manifest Class-Path
  3. Start-up fails Keep the host API compileOnly and rebuild. yano-plugins validate reports the same error offline, with exit code 2. Any structural catalog error stops start-up, whatever the contribution.

What if: A ServiceLoader entry is missing

The manifest declares a provider that META-INF/services does not list.

  1. Capture At start-up the node copies every JAR in yano.plugins.directory into a private snapshot before it reads or loads anything: at most 256 JARs, 1 GiB each and 4 GiB in total. Replacing a JAR later does not change the running node.
  2. Scan The manifest names a contribution whose provider has no META-INF/services entry. The scanner requires the two to agree exactly, in the same artifact.
    • passes: Exactly one qualified schema-v1 manifest
    • passes: No packaged class under org/yanoproject/api/
    • fails: Manifest contributions and ServiceLoader entries agree ServiceLoader entries differ
    • not reached: No JAR-manifest Class-Path
  3. Start-up fails Add the service file entry, or remove the contribution from the manifest. Any structural catalog error stops start-up, whatever the contribution.

What if: minLevel is too high

The manifest declares minLevel: 13, but this host is at level 12.

  1. Capture At start-up the node copies every JAR in yano.plugins.directory into a private snapshot before it reads or loads anything: at most 256 JARs, 1 GiB each and 4 GiB in total. Replacing a JAR later does not change the running node.
  2. Scan The scanner reads each snapshot as data and loads no code. It needs one manifest, no copy of the host API, ServiceLoader entries that match the manifest exactly, and no Class-Path outside the recorded JARs.
    • passes: Exactly one qualified schema-v1 manifest
    • passes: No packaged class under org/yanoproject/api/
    • passes: Manifest contributions and ServiceLoader entries agree
    • passes: No JAR-manifest Class-Path
  3. Check the API range The host’s level, 12, is below the bundle’s minLevel of 13. The bundle needs API that this host does not have.
    • fails: min ≤ 3 ≤ max, and 12 ≥ minLevel level 12 < 13
  4. Start-up fails The node reports that the bundle does not support Yano plugin API major 3 level 12. Use the host version the bundle was built for. Any structural catalog error stops start-up, whatever the contribution.

What if: A different JAR on one member

Member C has a rebuilt JAR with the same id and version as members A and B.

  1. Capture At start-up the node copies every JAR in yano.plugins.directory into a private snapshot before it reads or loads anything: at most 256 JARs, 1 GiB each and 4 GiB in total. Replacing a JAR later does not change the running node.
  2. Scan The scanner reads each snapshot as data and loads no code. It needs one manifest, no copy of the host API, ServiceLoader entries that match the manifest exactly, and no Class-Path outside the recorded JARs.
    • passes: Exactly one qualified schema-v1 manifest
    • passes: No packaged class under org/yanoproject/api/
    • passes: Manifest contributions and ServiceLoader entries agree
    • passes: No JAR-manifest Class-Path
  3. Check the API range The manifest’s yanoApi {min, max, minLevel} must include the host’s API major, 3, and the host’s level, 12, must be at least minLevel. There is a maximum major but no maximum level.
    • passes: min ≤ 3 ≤ max, and 12 ≥ minLevel
  4. Validate in isolation Each JAR is opened alone in a short-lived loader to resolve its provider types, even JARs that policy will filter out. No provider constructor or static initializer runs.
  5. Apply policy yano.plugins.allow-list and yano.plugins.deny-list select bundles. A filtered bundle stays in the inventory but never reaches the executable loader. A JAR that mixes selected and filtered bundles is refused.
  6. Build the shared loader Selected snapshots join one parent-first class loader, in SHA-256 order. Host classes win over any plugin copy. Plugins are trusted, in-process code: this loader is not a sandbox.
  7. Correlate providers On the shared loader, every manifested provider must have a matching ServiceLoader entry, and it must come from its own JAR.
    • passes: Each manifested provider has a ServiceLoader entry from its own JAR
  8. Order bundles Declared bundle dependencies, versions and cycles are checked, and the selected bundles are put in dependency order.
  9. Fingerprint Every check passes on member C. Only its fingerprint differs from A and B, because the JAR’s digest differs. Nothing at start-up compares fingerprints across members.
  10. Diverge Member C starts. If its code computes a different result, its state root differs and it does not vote for that block, so it stalls. Catch drift before traffic: appchain drift compares cluster.plugin-catalog across members, and appchain doctor checks each JAR’s digest against the project lock.
  • artifact
  • data-only checks
  • executable loader
  • running
  • refused

Any structural catalog error stops start-up. The node does not start with part of a broken plugin set.

All selected plugin JARs share one class loader, with the host’s own class loader as its parent:

  • Parent-first. A class the host already has always comes from the host. That keeps one copy of the Yano API, and it is why a plugin must not package org/yanoproject/api/** classes. Do not also put plugin JARs on the host’s class path.
  • Shared. Bundles see each other’s classes. There is no per-bundle isolation: if two bundles carry different versions of one library, both get whichever copy the loader finds first.
  • Snapshotted. The loader reads the private copies made at start-up. Replacing a JAR in the plugin directory changes nothing until the node restarts, and there is no hot reload.
  • Not a sandbox. Plugins are trusted, in-process Java code with the node’s permissions. A broken callback can ignore interruption; if a plugin blocks shutdown, stop the process and remove or replace the bundle.
QuestionAnswer
Does the node verify the publisher’s signature?No. Signatures are checked by tooling: plugin validate, inspect, metadata verify, project rendering and doctor. Signed manifests and JARs at runtime are deferred work.
What does the node trust?Any JAR in its plugin directory that passes the structural checks and the allow and deny lists. Protect that directory like the node’s binaries.
Does a valid signature approve the code?No. It authenticates the catalog and manifest bytes, nothing more.
What must match across members?The same bundles, the same machine or profile ids and the same committed settings. A member with a different JAR can compute a different state root and stall.
Which contributions affect agreement?The host’s CONSENSUS tier: state machines, sequencer modes and L1 observers. Executors, signers and domain APIs are PRIVILEGED_LOCAL; sinks, health and metrics are AUXILIARY_LOCAL.

The same caution applies to authoring tools that construct plugins: appchain bindings catalog, validate and profile-check run the installed plugins’ code, unsandboxed. Run them only on bundles you trust.

  • Start-up failure. Start-up stops with the reason. Common ones:

    Message containsCauseFix
    plugin artifact packages Yano API classThe JAR includes host API classesMake the host API compileOnly and rebuild
    manifest and supported ServiceLoader entries differA provider is in the manifest but not in META-INF/services, or the reverseMake the two agree
    does not support Yano plugin API majorThe host’s major is outside min–max, or its level is below minLevelUse the host version the bundle was built for
    cannot mix selected and policy-filtered bundlesOne JAR holds bundles that the allow or deny list splitsPut each bundle in its own JAR
  • Which catalog is running. Each node logs a YANO_PLUGIN_CATALOG_PROVENANCE line with its catalog fingerprint at start-up, and reports it as pluginCatalogFingerprint from /api/v1/app-chain/chains/<chain-id>/identity (under the configured API prefix).

  • Find the member that differs. ./yano.sh appchain drift <project> --peer http://<member>:8080/api/v1/ --api-key-env <variable> compares every node’s identity with the project. The variable holds one of the nodes’ operator API keys. A plugin difference shows as cluster.plugin-catalog and the result is DRIFT_DETECTED.

  • Plugin health and metrics. See Yano’s plugin operations guide.

Next: Consensus rules.