Skip to main content

Cargo Runtime Environment

Deployments in the Cargo runtime run as native binaries inside a Linux distro image on the processor, isolated via PRoot. Standard system environment variables are available directly. Host services - deployment metadata, cryptographic signing, and browser control - are accessed through an RPC API using JSON-RPC 2.0 over an abstract Unix domain socket.

Distro Image​

Each Cargo deployment must specify a Linux distro image in its manifest, along with the SHA256 hash of the image for verification. The processor downloads and extracts the image before running the deployment.

{
"name": "example",
"version": 1,
"entrypoint": "acurast.sh",
"restartPolicy": "no",
"image": {
"url": "https://example.com/distro.tar.xz",
"sha256": "abc123..."
}
}

Supported distro images can be found in the Termux proot-distro repository.

Environment Variables​

Env vars declared in your deployment config are injected as standard system environment variables. See Environment Variables for how to declare them.

let api_key = std::env::var("API_KEY").unwrap();

PRoot Quirks​

The PRoot container starts with a minimal environment. The processor injects the following defaults before the entrypoint runs:

Variable / fileDefault value
PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME/root
/etc/resolv.confGoogle (8.8.8.8, 8.8.4.4), Cloudflare (1.1.1.1, 1.0.0.1), Quad9 (9.9.9.9) - skipped if the file already contains a non-loopback nameserver

These can be overridden in your entrypoint script if your deployment requires different values.

DNS resolution​

Android has no system-level /etc/resolv.conf. Without it, DNS resolution inside the chroot fails silently - apt-get update downloads nothing, package lists stay empty, and subsequent apt-get install calls report packages as not found. The processor writes a set of public resolvers to /etc/resolv.conf at prepare time to avoid this, unless the file already contains a non-loopback nameserver entry.

To use different resolvers, write them yourself before any network calls:

echo "nameserver <your-resolver>" > /etc/resolv.conf

Network interfaces (getifaddrs)​

Programs running inside the chroot that call getifaddrs() - SSH daemons, some package managers - receive Android's real network interfaces (wlan0, rmnet_data0, etc.) instead of a standard Linux interface list. This happens because PRoot bind-mounts /proc from the Android host and glibc queries the kernel via netlink, which responds with Android interfaces. Programs may fail to bind or behave unexpectedly as a result.

To fix this, override getifaddrs and freeifaddrs in a shared library and preload it via LD_PRELOAD:

getifaddrs_override.c
#include <ifaddrs.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <net/if.h>

int getifaddrs(struct ifaddrs **ifap) {
struct ifaddrs *ifa = calloc(1, sizeof(struct ifaddrs));
if (!ifa) return -1;
ifa->ifa_next = NULL;
ifa->ifa_name = strdup("lo");
ifa->ifa_flags = IFF_UP | IFF_RUNNING | IFF_LOOPBACK;

struct sockaddr_in *addr = calloc(1, sizeof(struct sockaddr_in));
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = htonl(0x7f000001);
ifa->ifa_addr = (struct sockaddr *)addr;

struct sockaddr_in *netmask = calloc(1, sizeof(struct sockaddr_in));
netmask->sin_family = AF_INET;
netmask->sin_addr.s_addr = htonl(0xff000000);
ifa->ifa_netmask = (struct sockaddr *)netmask;

*ifap = ifa;
return 0;
}

void freeifaddrs(struct ifaddrs *ifa) {
while (ifa) {
struct ifaddrs *next = ifa->ifa_next;
free(ifa->ifa_name);
free(ifa->ifa_addr);
free(ifa->ifa_netmask);
free(ifa);
ifa = next;
}
}

Compile and preload it before starting the affected program:

gcc -shared -fPIC -o /usr/local/lib/getifaddrs_override.so getifaddrs_override.c
export LD_PRELOAD=/usr/local/lib/getifaddrs_override.so

RPC API​

The processor injects a BRIDGE_SOCKET environment variable at runtime containing the name of an abstract Unix socket. To call a host API, open a new socket connection, send a single JSON-RPC 2.0 request line, and read back the response line. Each connection carries exactly one request/response exchange.

socket address: \0<BRIDGE_SOCKET> (abstract namespace - null-byte prefix)
protocol: JSON-RPC 2.0, newline-delimited, one call per connection

Processor​

Android: 1.25.0+

processor_version​

Returns the version of the processor runtime.

Request
{
"jsonrpc": "2.0",
"method": "processor_version",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"version": string
},
"id": string
}

Deployment​

Android: 1.25.0+

deployment_id​

Returns the identifier of the active deployment.

Request
{
"jsonrpc": "2.0",
"method": "deployment_id",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"id": string,
"origin": {
"kind": string,
"source": string // hex
}
},
"id": string
}

Android: 1.25.0+

deployment_ipfsHash​

Returns the IPFS CID of the deployment app.

Request
{
"jsonrpc": "2.0",
"method": "deployment_ipfsHash",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"ipfsHash": string
},
"id": string
}

Android: 1.25.0+

deployment_slot​

Returns the execution slot index assigned to this processor, or null if no slot is assigned.

Request
{
"jsonrpc": "2.0",
"method": "deployment_slot",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"slot": number | null
},
"id": string
}

Android: 1.25.0+

deployment_publicKeys​

Returns the signing public keys of this processor for the active deployment, keyed by curve. Only curves for which a key exists are included.

Request
{
"jsonrpc": "2.0",
"method": "deployment_publicKeys",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"publicKeys": {
"p256"?: string, // hex
"secp256k1"?: string, // hex
"ed25519"?: string // hex
}
},
"id": string
}

Android: 1.25.0+

deployment_encryptionKeys​

Returns the ECDH encryption public keys of this processor for the active deployment. Supported curves: p256, secp256k1.

Request
{
"jsonrpc": "2.0",
"method": "deployment_encryptionKeys",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"encryptionKeys": {
"p256"?: string, // hex
"secp256k1"?: string // hex
}
},
"id": string
}

Android: 1.25.0+

deployment_assignedProcessors​

Returns all processors assigned to this deployment, keyed by their SS58 address, along with their public keys per curve.

Request
{
"jsonrpc": "2.0",
"method": "deployment_assignedProcessors",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"processors": {
[ss58: string]: {
"p256"?: string, // hex, signing key
"secp256k1"?: string, // hex, signing key
"ed25519"?: string, // hex, signing key
"encP256"?: string, // hex, encryption key
"encSecp256k1"?: string // hex, encryption key
}
}
},
"id": string
}

Signer​

Supported curves: "p256", "secp256k1", "ed25519". Encryption and decryption support "p256" and "secp256k1" only.

Android: 1.25.0+

signer_publicKey​

Returns the public key for the given curve. Pass derivationPath for HD key derivation (supported on secp256k1).

Request
{
"jsonrpc": "2.0",
"method": "signer_publicKey",
"params": [{
"curve": "p256" | "secp256k1" | "ed25519",
"derivationPath"?: string
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"publicKey": string, // hex
"derivationPath"?: string // present only if requested
},
"id": string
}

Android: 1.25.0+

signer_sign​

Signs a hex-encoded byte string with the given curve key. Pass derivationPath for HD signing.

:::warning ECDSA curves sign a digest, not a message For p256 and secp256k1 the input is not hashed: the processor performs a raw ECDSA signature over exactly the bytes you pass. Hash your message first (usually SHA-256) and pass the 32-byte digest. The result is raw r || s (64 bytes), not DER; secp256k1 signatures are low-s normalized with no recovery byte. ed25519 hashes internally, so pass the full message. See Signing requests from a deployment. :::

Request
{
"jsonrpc": "2.0",
"method": "signer_sign",
"params": [{
"curve": "p256" | "secp256k1" | "ed25519",
"bytes": string, // hex
"derivationPath"?: string
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"bytes": string // hex
},
"id": string
}

Android: 1.25.0+

signer_encrypt​

Encrypts bytes using ECDH key agreement with the receiver's public key. Supported curves: p256, secp256k1.

Request
{
"jsonrpc": "2.0",
"method": "signer_encrypt",
"params": [{
"curve": "p256" | "secp256k1",
"publicKey": string, // hex, receiver's public key
"salt": string, // hex
"bytes": string // hex, plaintext
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"bytes": string // hex, ciphertext
},
"id": string
}

Android: 1.25.0+

signer_decrypt​

Decrypts bytes using ECDH key agreement with the sender's public key. Supported curves: p256, secp256k1.

Request
{
"jsonrpc": "2.0",
"method": "signer_decrypt",
"params": [{
"curve": "p256" | "secp256k1",
"publicKey": string, // hex, sender's public key
"salt": string, // hex
"bytes": string // hex, ciphertext
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"bytes": string // hex, plaintext
},
"id": string
}

Browser​

These methods control an embedded WebView on the processor device and are only available on Android processors.

Android: 1.25.0+

browser_debugUrl​

Returns the Chrome DevTools remote debugging URL for the WebView.

Request
{
"jsonrpc": "2.0",
"method": "browser_debugUrl",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"url": string
},
"id": string
}

Android: 1.27.0

browser_initSurface​

Sets the size of the surface the browser paints its pages to, applied when the surface is created as the first tab is opened. Only takes effect if called before any tab is open - without it, the surface is created at the device's display size.

Fails if the surface size has already been initialized, if a surface already exists, or if the requested size is outside the supported bounds: each side between 240 and 2560 device pixels, total pixel count no more than 2560x1440, and deviceScaleFactor between 0.5 and 4.

Request
{
"jsonrpc": "2.0",
"method": "browser_initSurface",
"params": [{
"width": number, // device pixels
"height": number, // device pixels
"deviceScaleFactor"?: number // device pixels per CSS pixel, default: 1
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.27.0

browser_surfaceSize​

Returns the size of the surface the browser paints its pages to, or null if the browser currently has no surface.

Request
{
"jsonrpc": "2.0",
"method": "browser_surfaceSize",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"size": {
"width": number, // device pixels
"height": number, // device pixels
"deviceScaleFactor": number // device pixels per CSS pixel
} | null
},
"id": string
}

Android: 1.25.0+

browser_newTab​

Opens a new tab in the WebView and returns its ID.

Request
{
"jsonrpc": "2.0",
"method": "browser_newTab",
"params": [{ "url": string }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"id": number
},
"id": string
}

Android: 1.25.0+

browser_closeTab​

Closes a tab by ID. Set wholeTree to true to also close all tabs spawned by this tab.

Request
{
"jsonrpc": "2.0",
"method": "browser_closeTab",
"params": [{
"id": number,
"wholeTree"?: boolean // default: false
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_openTabs​

Returns a list of currently open tab IDs.

Request
{
"jsonrpc": "2.0",
"method": "browser_openTabs",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"tabs": Array<number | null>
},
"id": string
}

Android: 1.25.0+

browser_currentUrl​

Returns the current URL of a tab, or null if it has not loaded yet.

Request
{
"jsonrpc": "2.0",
"method": "browser_currentUrl",
"params": [{ "id"?: number }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"url": string | null
},
"id": string
}

Android: 1.25.0+

browser_loadCause​

Returns why the tab last loaded: "manual" (opened via browser_newTab) or "auto" (opened by a tab autonomously), or null if not yet loaded.

Request
{
"jsonrpc": "2.0",
"method": "browser_loadCause",
"params": [{ "id"?: number }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"cause": "manual" | "auto" | null
},
"id": string
}

Android: 1.25.0+

browser_startRefreshLoop​

Starts a periodic refresh loop for a tab. Stop with browser_stopRefreshLoop when no longer needed.

Request
{
"jsonrpc": "2.0",
"method": "browser_startRefreshLoop",
"params": [{
"id"?: number,
"interval"?: number, // ms, default: 500
"wholeTree"?: boolean // default: false
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_stopRefreshLoop​

Stops the refresh loop for a tab.

Request
{
"jsonrpc": "2.0",
"method": "browser_stopRefreshLoop",
"params": [{
"id"?: number,
"wholeTree"?: boolean // default: false
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_useProxy​

Configures the WebView to route traffic through a proxy server.

Request
{
"jsonrpc": "2.0",
"method": "browser_useProxy",
"params": [{
"url": string,
"config"?: {
"username"?: string,
"password"?: string,
"fallback"?: boolean // default: false — connect directly if proxy fails
}
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_removeProxy​

Removes the current proxy configuration from the WebView.

Request
{
"jsonrpc": "2.0",
"method": "browser_removeProxy",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_close​

Closes all open tabs and destroys the WebView, clearing storage, cookies, and proxy settings.

Request
{
"jsonrpc": "2.0",
"method": "browser_close",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Network​

Android: 1.25.0+

network_whitelist​

Verifies host via DNS TXT records and, if valid, whitelists its resolved IP addresses so they bypass the rate-limiter in the network extension.

The processor performs the following verification steps:

  1. Forward DNS + TXT - verifies that _acu.<host> carries a TXT record v=base64(sha256(deployment_source || host)), then resolves the hostname to one or more IP addresses (A/AAAA).
  2. Reverse DNS + TXT - for each resolved IP, performs a PTR lookup to obtain the PTR hostname and verifies that _acu.<ptr_hostname> carries a TXT record v=base64(sha256(deployment_source || ptr_hostname)).

Both steps must pass for an IP to be added to the whitelist. Connections to non-whitelisted hosts are rate-limited.

deployment_source is the raw 32-byte Substrate Account ID of the deployment's owner. host is the bare hostname (no scheme, port, or path — e.g. example.com).

Request
{
"jsonrpc": "2.0",
"method": "network_whitelist",
"params": [{ "host": string }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Verification hash​

Example scripts for computing the TXT record value:

Node.js
// npm install @polkadot/util-crypto

const { createHash } = require('crypto');
const { decodeAddress } = require('@polkadot/util-crypto');

function verificationHash(source, host) {
// Decode hex Account ID directly, or convert SS58 address to raw bytes
const sourceBytes = source.length === 64
? Buffer.from(source, 'hex')
: Buffer.from(decodeAddress(source));
// SHA-256 over concatenation of source bytes and UTF-8 encoded host
const digest = createHash('sha256')
.update(sourceBytes)
.update(host)
.digest();
// Return the hash as a base64 string
return digest.toString('base64');
}
Python
# pip install substrate-interface

import hashlib
import base64

from substrateinterface.utils.ss58 import ss58_decode


def verification_hash(source: str, host: str) -> str:
# Decode hex Account ID directly, or convert SS58 address to raw bytes
source_bytes = bytes.fromhex(source) if len(source) == 64 else bytes.fromhex(ss58_decode(source))
# SHA-256 over concatenation of source bytes and UTF-8 encoded host
digest = hashlib.sha256(source_bytes + host.encode()).digest()
# Return the hash as a base64 string
return base64.b64encode(digest).decode()

Tunnel​

Opens a reverse tunnel that forwards inbound TLS connections from a public URL (https://<clientId>.<domainSuffix>, port 443) to a local address inside the deployment. Only one tunnel may be active per deployment.

Before calling tunnel_start, the <domainSuffix> must have the required wildcard and verification TXT records — see the quic-tunnel README for details.

Android: 1.26.0+

tunnel_start​

Opens the tunnel and returns the public URL it serves.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_start",
"params": [{
"serverAddrs": string[], // relay endpoints, e.g. ["relay-2.canary.acurast.com:4433"]
"domainSuffix": string, // DNS suffix you control
"localAddr": string, // local "host:port" to forward to
"primaryKey": {
"algorithm": "Secp256r1", // only P-256 is accepted
"bytes": string // base64 of PKCS#8 DER (no wrap)
},
"acmeStaging"?: boolean, // default false
"acmeEmail"?: string, // Let's Encrypt account contact
"certPem"?: string, // pre-supplied PEM chain; when set, ACME is skipped
"forceH2"?: boolean, // skip QUIC, use HTTP/2 fallback pool (default false)
"poolSize"?: number // H2 pool size when forceH2 is on (default 4)
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"url": string, // https://<clientId>.<domainSuffix> (port 443)
"clientId": string, // the deployment's tunnel identifier
"secondaryUrl"?: string,
"secondaryClientId"?: string
},
"id": string
}

Persist primaryKey.bytes across deployment restarts to keep the same clientId. Persist the cert PEM from tunnel_certPem and pass it back here as certPem to skip ACME on subsequent starts.


Android: 1.26.0+

tunnel_stop​

Closes the active tunnel. The cached ACME credentials and certificate are preserved on disk so a subsequent tunnel_start with the same identity can reuse them. No-op if no tunnel is running.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_stop",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.26.0+

tunnel_status​

Returns the current tunnel status.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_status",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"status": number // 0 = Starting, 1 = Running, 2 = Stopped, 3 = Failed, -1 = no tunnel active
},
"id": string
}

Android: 1.26.0+

tunnel_certPem​

Returns the PEM-encoded full certificate chain currently issued for the tunnel.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_certPem",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"pem"?: string // omitted if no certificate has been issued yet (or no tunnel is active)
},
"id": string
}

Examples​

The examples below call processor_version to show how to connect to the bridge and make a request in different languages.

Node.js​

const net = require('net');

const client = net.createConnection('\0' + process.env.BRIDGE_SOCKET);
const request = JSON.stringify({
jsonrpc: '2.0',
method: 'processor_version',
params: [],
id: '1',
});

client.write(request + '\n');
client.once('data', (data) => {
const response = JSON.parse(data.toString());
console.log(response);

client.end();
});

Python​

import json, os, socket

request = json.dumps({
'jsonrpc': '2.0',
'method': 'processor_version',
'params': [],
'id': '1'
})

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect('\0' + os.environ['BRIDGE_SOCKET'])
s.sendall((request + '\n').encode())
response = json.loads(s.recv(65536))

print(response)

Rust​

use std::io::{BufRead, BufReader, Write};
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixStream};

fn main() {
let socket = std::env::var("BRIDGE_SOCKET").unwrap();
let addr = SocketAddr::from_abstract_name(socket.as_bytes()).unwrap();
let mut stream = UnixStream::connect_addr(&addr).unwrap();

let request = serde_json::json!({
"jsonrpc": "2.0",
"method": "processor_version",
"params": [],
"id": "1"
}).to_string();
stream.write_all(format!("{request}\n").as_bytes()).unwrap();

let mut response = String::new();
BufReader::new(&stream).read_line(&mut response).unwrap();

let value: serde_json::Value = serde_json::from_str(&response).unwrap();
println!("{}", value);
}