This guide explains how to create and maintain protocol implementations for the BlockJoy platform.
- File Structure
- BlockJoy API Integration
- Configuration Files
- Testing and Deploying Protocols
- Best Practices
- Example Implementation
base-images/
└── base_image/
├── config/ # (Optional)
└── config_files.yaml # Base image with config files
└── templates/ # (Optional)
└── file_base.template # Common templates for all images
└── base.rhai # Common base rhai configuration
└── Dockerfile # Base Dockerfile with common utilities
clients/
└── consensus/
├── consensus_client/
└── Dockerfile # Client-specific Docker configuration
└── exec/
├── exec_client/
└── Dockerfile # Client-specific Docker configuration
└── ... # Other client types (e.g., load-balancer, observability, etc.)
protocols/
└── protocols.yaml # Root entity for all protocol implementations
└── your_protocol/
└── your_protocol-exec_client/
└── templates/
├── babel.yaml # Protocol configuration and metadata
├── main.rhai # Main protocol configuration
├── aux.rhai # Auxiliary functions and configurations
└── Dockerfile # Protocol-specific Docker configuration
└── ... # Other protocols (e.g., ethereum, optimism, etc.)The protocol implementation interacts with the BlockJoy API through a combination of metadata configuration (aka babel.yaml) and runtime interface files (aka Rhai scripts).
First make sure that the protocol you're creating an image for is defined in protocols.yaml. This is the root entity that groups all implementations of the same protocol.
The babel.yaml file defines the protocol image's metadata that the BlockJoy API needs to:
- Define available image variants
- Set up resource requirements (CPU, memory, disk)
- Configure networking and firewall rules
- Establish container settings
- Set variant visibility and access properties
This metadata is used by the API for deployment planning and resource allocation, but the actual protocol image configuration and runtime behavior are controlled through the RHAI files.
The Rhai scripts (main.rhai and other imported scripts like aux.rhai) serve as the primary configuration interface between your protocol image and the BlockJoy API. These files:
- Access the protocol image metadata from
babel.yamlthrough thenode_env()function - Configure protocol image behavior based on the selected
babel.yamlvariant - Initialize and manage protocol services
- Report node status back to the API
The API provides access to deployment configuration through the node_env() function in Rhai script. This function exposes metadata from babel.yaml along with runtime information:
pub struct NodeEnv {
/// Node id.
pub node_id: String,
/// Node name.
pub node_name: String,
/// Node version.
pub node_version: String,
/// Node protocol.
pub node_protocol: String,
/// Node variant.
pub node_variant: String, // Maps to variant key in babel.yaml
/// Node IP.
pub node_ip: String,
/// Node gateway IP.
pub node_gateway: String,
/// Indicate if node run in dev mode.
pub dev_mode: bool,
/// Host id.
pub bv_host_id: String,
/// Host name.
pub bv_host_name: String,
/// API url used by host.
pub bv_api_url: String,
/// Organisation id to which node belongs to.
pub org_id: String,
/// Absolute path to directory where data drive is mounted.
pub data_mount_point: String,
/// Absolute path to directory where protocol data are stored.
pub protocol_data_path: String,
}When implementing a new blockchain protocol image:
-
Create client(s) used by the protocol being implemented:
- Define Dockerfile for each client
- Setup build for client(s)
- Copy client binaries to common location for future use
- Copy client specific libraries to common location for future use
-
Define Protocol Image Metadata (
babel.yaml):- Set protocol image identification (version, SKU, description)
- Define available variants and their resource requirements
- Configure network access rules
- Set visibility and access properties
-
Create Runtime Interface (
main.rhai):- Import base configurations
- Define protocol-specific constants
- Map
node_env().node_variantto protocol configurations - Configure services and initialization steps
- Implement required status functions
-
Add Auxiliary Functions (
aux.rhai):- Define reusable configurations
- Set up template processing
- Configure additional services
-
Set Up Container (
Dockerfile):- Use appropriate base image
- Copy related client binaries from the previously built client images (or use external images where required)
- Add protocol-specific dependencies
- Configure runtime environment
- Place all necessary Rhai scripts (
main.rhaiin particular) into the container (/var/lib/babel/plugin/)
The BlockJoy API uses the metadata from babel.yaml to plan and create node deployments, while the RHAI files control how the node actually operates within those parameters.
- Define available variants in
babel.yaml:
variants:
- key: client-mainnet-full # This key is accessed via node_env().node_variant
min_cpu: 4
min_memory_mb: 16000
min_disk_gb: 1000
sku_code: EXPL-MF- Configure variant-specific behavior in the protocol's
main.rhai:
// Map node_env().node_variant to protocol configuration
const VARIANTS = #{
"client-mainnet-full": #{
network: "mainnet",
extra_args: "--syncmode full",
},
};
// Access current variant configuration
const VARIANT = VARIANTS[node_env().node_variant];
// Use in service configuration
fn plugin_config() {#{
aux_services: base::aux_services() + aux::aux_services(), // explained below
config_files: base::config_files() + aux::config_files(global::METRICS_PORT,global::METRICS_PATH,global::RPC_PORT,global::WS_PORT, global::AUTHRPC_PORT,global::OP_RPC_PORT,global::CADDY_DIR), // explained below
services: [
#{
name: "example-node",
run_sh: `/usr/bin/example-node \
--network=${global::VARIANT.network} \
${global::VARIANT.extra_args}`,
},
],
}}
See docs and examples with comments, delivered with BV bundle in /opt/blockvisor/current/docs/ for more details on babel.yaml and Rhai scripts.
The base.rhai file is part of the base image and provides common services and their configurations that are used by all protocols. It is located at /usr/lib/babel/plugin/base.rhai in the container:
// Base configuration that protocols can extend
fn config_files() {
[
#{
template: "/some/template.template",
destination: "/some/destination/config",
params: #{
param1: "value1",
param2: "value2",
},
},
]
}
fn aux_services() {
[
#{
name: "binary1",
run_sh: "/usr/bin/binary1 run",
},
#{
name: "binary2",
run_sh: "/usr/bin/binary2 run",
}
]
}
fn config_files() {
[
#{
template: "/var/lib/babel/templates/Caddyfile.template",
destination: "/etc/caddy/Caddyfile",
params: #{
rpc_port: `${rpc_port}`,
ws_port: `${ws_port}`,
metrics_port: `${metrics_port}`,
hostname: node_env().node_name,
tld: ".n0des.xyz",
data_dir: `${caddy_dir}`,
}
}
]
}
fn aux_services() {
[
#{
name: "caddy",
run_sh: `/usr/bin/caddy run --config /etc/caddy/Caddyfile`,
},
]
}
Base and aux functions are imported into the protocol's main.rhai using:
import "base" as base; // Inherited from the base-image used
import "aux" as aux; // Inherited from the aux.rhai in protocol directory
// Import auxiliary configuration
fn plugin_config() {
aux_services: base::aux_services() + aux::aux_services(), // pull from base.rhai and aux.rhai
config_files: base::config_files() + aux::config_files(global::METRICS_PORT,global::METRICS_PATH,global::RPC_PORT,global::WS_PORT,global::AUTHRPC_PORT,global::OP_RPC_PORT,global::CADDY_DIR), // use global variables to interpolate into configs and pull them in the `main.rhai`
services : [
#{
name: "erigon",
run_sh: `/root/bin/erigon \
--network=${global::VARIANT.network} \
${global::VARIANT.extra_args}`,
},
]
}
The main.rhai also contains protocol specific functions that the API uses to communicate with the running services and assess the node's health or status:
fn protocol_status() {
let resp = parse_hex(run_jrpc(#{host: global::API_HOST, method: "eth_chainId"}).expect(200).result);
if resp == 1 { // Example chain ID
#{state: "broadcasting", health: "healthy"}
} else {
#{state: "delinquent", health: "healthy"}
}
}
fn height() {
parse_hex(run_jrpc(#{ host: global::API_HOST, method: "eth_blockNumber"}).expect(200).result)
}
fn sync_status() {
let resp = run_jrpc(#{host: global::API_HOST, method: "eth_syncing"}).expect(200);
if resp.result == false {
"synced"
} else {
"syncing"
}
}
Comprehensive documentation on the plugin's configuration and supported functions can be found here: Protocol RHAI Plugin Guide.
The protocol implementation may include template files that are processed during node initialization. These templates are used to create configuration files for node services, such as the reverse proxy, and may also include additional configuration for the node.
Caddyfile.template - Reverse proxy configuration:
{hostname}{tld} {
reverse_proxy /debug/metrics/prometheus localhost:{metrics_port}
reverse_proxy /ws localhost:{ws_port}
reverse_proxy localhost:{rpc_port}
tls {
dns cloudflare {env.CF_API_TOKEN}
}
log {
output file {data_dir}/access.log
format json
}
}These templates are referenced in the auxiliary configuration (aux.rhai) and are processed with values from the node main.rhai to ensure consistency across services.
FROM privaterepo/erigon as erigon
FROM privaterepo/lighthouse as lighthouse
FROM privaterepo/debian-bookworm-base
RUN mkdir -p /root/bin /root/lib
COPY --from=erigon /root/bin/erigon /root/bin/
COPY --from=erigon /root/lib/libsilkworm_capi.so /root/lib/
COPY --from=lighthouse /root/bin/lighthouse /root/bin/
COPY aux.rhai /var/lib/babel/plugin/
COPY main.rhai /var/lib/babel/plugin/Before deploying your protocol, you should check its syntax and configuration using nib. This tool is used in our CI/CD pipeline to validate protocols. Please refer to the documentation here for installation and authentication instructions.
To check a protocol's syntax:
nib image check --variant <variant-name> --path <path-to-babel-yaml> pluginFor example:
nib image check --variant mainnet --path protocols/your_protocol/your_protocol-exec_client/babel.yaml pluginThis will validate:
- The
babel.yamlfile structure and required fields - The presence and syntax of required Rhai scripts
- The configuration templates and their variables
Once the syntax check passes, you can test your protocol nodes. There are two main types of checks:
- Service Status Checks - Verify that all services defined in your configuration start correctly (doesn't check for service restarts, only service startup):
nib image check --variant <variant-name> --path <path-to-babel-yaml> --cleanup jobs-status- Service Restart Checks - Verify that the node services run properly once started (besides checking for proper service startup, it will also check if the services fail and then get restarted):
nib image check --variant <variant-name> --path <path-to-babel-yaml> --cleanup jobs-restartsNotes:
babel (the component running on the node as part of the blockvisor suite) is responsible for starting and monitoring the node services. If a service isn't configured properly, it will start and eventually fail, so babel will restart start it according to the restart policy of the service. This will register as a service restart and is detected by the job-restarts check (job-status will only detect the initial failure to execute), but shouldn't be considered a critical error since some jobs won't start without specific requirements (for example, some execution clients won't start without a dataset since syncing from genesis isn't possible). These restarts are expected and can be normal behavior of the node, but they need to be checked thoroughly in order to identify configuration issues early on. In case of automated workflows, it's recommended to not fail the workflow on these errors, instead they should trigger a soft alert and get double-checked.
When you've added a new protocol or modified protocol metadata in protocols.yaml, you need to push these changes to the API:
nib protocol push --path protocols/protocols.yamlTo deploy a specific protocol implementation:
nib image push --path <path-to-babel-yaml>For example:
nib image push --path protocols/your_protocol/your_protocol-exec_client/babel.yamlNotes:
- The
nibCLI tool is part ofbvbundle released in the bv-host-setup repository, please refer to the documentation here for installation and authentication instructions. - The
--pathargument is optional and added throughout the documentation for clarity.
-
Version Management:
- Use semantic versioning in
babel.yaml - Update the version when making any changes to the protocol implementation
- Use semantic versioning in
-
Configuration:
- Use environment variables for configurable values
- Follow the example protocols for structure
-
Service Management:
- Use appropriate timeouts
- Ensure all functions required by the API are implemented
- Test functions to ensure they work as expected
-
Metrics:
- Expose Prometheus metrics when possible
The example protocol implementation in example/ demonstrates how to implement a protocol.
See also docs and examples delivered with BV bundle in /opt/blockvisor/current/docs/.