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.
{
"version": "1",
"entrypoint": "example.sh",
"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 / file | Default value |
|---|---|
PATH | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
HOME | /root |
/etc/resolv.conf | Google (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:
#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.
{
"jsonrpc": "2.0",
"method": "processor_version",
"params": [],
"id": string
}
{
"jsonrpc": "2.0",
"result": {
"version": string
},
"id": string
}
Deployment
Android: 1.25.0+deployment_id
Returns the identifier of the active deployment.
{
"jsonrpc": "2.0",
"method": "deployment_id",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "deployment_ipfsHash",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "deployment_slot",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "deployment_publicKeys",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "deployment_encryptionKeys",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "deployment_assignedProcessors",
"params": [],
"id": string
}
{
"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.
signer_publicKey
Returns the public key for the given curve. Pass derivationPath for HD key derivation (supported on secp256k1).
{
"jsonrpc": "2.0",
"method": "signer_publicKey",
"params": [{
"curve": "p256" | "secp256k1" | "ed25519",
"derivationPath"?: string
}],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "signer_sign",
"params": [{
"curve": "p256" | "secp256k1" | "ed25519",
"bytes": string, // hex
"derivationPath"?: string
}],
"id": string
}
{
"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.
{
"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
}
{
"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.
{
"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
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_debugUrl",
"params": [],
"id": string
}
{
"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.
{
"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
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_surfaceSize",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_newTab",
"params": [{ "url": string }],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_closeTab",
"params": [{
"id": number,
"wholeTree"?: boolean // default: false
}],
"id": string
}
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Android: 1.25.0+
browser_openTabs
Returns a list of currently open tab IDs.
{
"jsonrpc": "2.0",
"method": "browser_openTabs",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_currentUrl",
"params": [{ "id"?: number }],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_loadCause",
"params": [{ "id"?: number }],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_startRefreshLoop",
"params": [{
"id"?: number,
"interval"?: number, // ms, default: 500
"wholeTree"?: boolean // default: false
}],
"id": string
}
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Android: 1.25.0+
browser_stopRefreshLoop
Stops the refresh loop for a tab.
{
"jsonrpc": "2.0",
"method": "browser_stopRefreshLoop",
"params": [{
"id"?: number,
"wholeTree"?: boolean // default: false
}],
"id": string
}
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Android: 1.25.0+
browser_useProxy
Configures the WebView to route traffic through a proxy server.
{
"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
}
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Android: 1.25.0+
browser_removeProxy
Removes the current proxy configuration from the WebView.
{
"jsonrpc": "2.0",
"method": "browser_removeProxy",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "browser_close",
"params": [],
"id": string
}
{
"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:
- Forward DNS + TXT - verifies that
_acu.<host>carries a TXT recordv=base64(sha256(deployment_source || host)), then resolves the hostname to one or more IP addresses (A/AAAA). - 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 recordv=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).
{
"jsonrpc": "2.0",
"method": "network_whitelist",
"params": [{ "host": string }],
"id": string
}
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Verification hash
Example scripts for computing the TXT record value:
// 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');
}
# 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>:8443) 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 tunnel quick start for details.
tunnel_start
Opens the tunnel and returns the public URL it serves.
{
"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
}
{
"jsonrpc": "2.0",
"result": {
"url": string, // https://<clientId>.<domainSuffix>:8443
"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.
{
"jsonrpc": "2.0",
"method": "tunnel_stop",
"params": [],
"id": string
}
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Android: 1.26.0+
tunnel_status
Returns the current tunnel status.
{
"jsonrpc": "2.0",
"method": "tunnel_status",
"params": [],
"id": string
}
{
"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.
{
"jsonrpc": "2.0",
"method": "tunnel_certPem",
"params": [],
"id": string
}
{
"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);
}