Skip to main content
Omni-chain V1 shipped with Scale. Lite contracts and the Signer Daemon are live across supported chains; per-chain proxy addresses live in the launch manifest.
peaq stays canonical. Every other runs a thin, read-only mirror of peaq state, kept in sync by a Signer Daemon that watches finalized peaq events, packages them into EIP-712 batches, them with a per-chain push key, and pushes them to satellite Lite contracts. This means a or app on , Ethereum, Polygon, or any other supported chain can resolve a machine’s , identity, , and status without an hop to peaq, and without trusting an oracle on top.

What the mesh looks like

Domain separator: name = "PeaqosLite", version = "1.0.0". Each Lite is identified by (chainId, verifyingContract) per EIP-712.

DIDLite

DIDLite mirrors the peaq DID ’s per-attribute records onto every satellite chain. Consumers on the satellite resolve any peaq DID account’s attributes (including the locked "peaqID" attribute) without an extra hop. Records are written exclusively by signed batches from the Signer Daemon; consumers only read.

Public consumer views

readAttribute LitePaused while the Lite is paused. readAttributeRaw is an admin/debug carve-out that ignores the gate. lastHomeBlockApplied() is intentionally not pause-gated so consumers can still read the global watermark when reads are paused. To resolve a peaqID, compose: readAttribute(didAccount, "peaqID"). The orchestrator does not ship a dedicated peaqIDOf view; clients decode the 32-byte value themselves.

Consumer view errors

  • AttributeNotFound(address didAccount, bytes attrName)
  • AttributeRemoved(address didAccount, bytes attrName, uint64 removedAtHomeBlock)
  • LitePaused()

IdentityLite

IdentityLite mirrors the peaq IdentityRegistry per-machineId record. Consumers gate writes that depend on having seen a specific peaq approval by reading lastCursorPacked().

Public consumer views

Consumer view errors

  • MachineNotFound(uint256 machineId)
  • LiteUninitialized(address lite) — not raised by the Lite itself. Read (record, lastAt) and require(lastAt > 0, LiteUninitialized(address(this))).
  • LitePaused()

Cold-start pattern

The Lite returns (record, 0) when it has never accepted a batch, rather than reverting, so callers choose the policy:

StakingLite

StakingLite mirrors the peaq IdentityStaking per-machine stake record. Consumers gate authorisation or service eligibility on stake state without crossing chains.

Public consumer views

Same cold-start pattern as IdentityLite — require(lastBatchAcceptedAt_ > 0, LiteUninitialized(address(this))) before trusting reads. StakingLite has its own pause flag and its own EIP-712 schema (StakingEvent), but shares the PeaqosLite domain and the cross-language daemon parity gate.

EIP-712 schemas

7-field IdentityEvent

txIndex and logIndex were added vs the earlier 5-field shape so the on-chain cursor can be sub-block-precise. The daemon-side encoder is bit-identical to Solidity (parity-gated by the EIP-712 fixture suite).

DIDEvent

Both bytes fields are pre-hashed with keccak256(bytes(...)) per EIP-712 dynamic-bytes rule. kind ordinal: 0 = Add, 1 = Update, 2 = Remove. Removes must carry value.length == 0 and validity == 0.

Batch envelope (shared)

Schema version is per-Lite. A typehash bump requires lockstep upgrade of both the Lite and the daemon.

Signer Daemon

Off-chain. Python package, one instance per (home, satellite, Lite) triple. V1 shipped 2026-05-21 with a six-daemon fleet across two pipelines: three for peaq (home) → Agung (satellite) and three for Agung (home) → Base Sepolia (satellite). Each daemon binds to one LITE_NAME (IdentityLite | DIDLite | StakingLite), one push key (currentSigner on the Lite), one HEALTH_PORT, and one CURSOR_FILE_PATH. Six unique push keys total — reuse triggers nonce races. A direct peaq mainnet → Base Sepolia pipeline is registered in the satellite registry but not yet rolled out as a daemon deployment.

Health endpoint

Each daemon exposes a loopback-only /health (default port unique per instance, conventionally 8080–8085):
Operator runbooks (env vars, deploy steps, monitoring, troubleshooting) for the Signer Daemon will live in the Operate section: signer-daemon-deploy, signer-daemon-monitoring, and signer-daemon-troubleshooting (coming soon).

Pause and emergency model

Every Lite has two independent pause flags and one emergency flag, all external onlyOwner:
Routine setSigner rotation opens a GRACE_BLOCKS = 600 (~1h) window where the previous PUSH_KEY remains valid so in-flight signed batches do not fail mid-flight. Emergency rotation does not keep the previous key valid.

EmergencyMode

EmergencyMode is two booleans plus a snapshot , not an enum:
A first pauseLite or pauseApplyBatch sets inEmergencyMode = true and snapshots emergencyEnteredBySigner = currentSigner. To exit, the owner calls emergencyRotatePushKey(newKey) (rotation must actually change the signer) then exitEmergencyMode(). Pause flags are not auto-cleared.

Hot/cold key collapse defense

The cold key is owner() (admin and upgrade authority). The hot key is currentSigner (the PUSH_KEY on the daemon server). A single must never collapse them onto the same address. Enforcement points:
  • initialize(owner_, pushKey_) reverts if pushKey_ == owner_.
  • setSigner(newSigner) reverts if newSigner ∈ {owner(), pendingOwner()}.
  • emergencyRotatePushKey(newPushKey) reverts on the same membership check.
  • transferOwnership / _transferOwnership reject newOwner ∈ {currentSigner, previousSigner}.
  • renounceOwnership is permanently disabled.
A stolen daemon key cannot also seize upgrade authority.

Staleness policy

The Lite does not staleness-revert. SDK and ops layers apply staleness gates. Consumers should compare lastBatchAcceptedAt to block.timestamp and reject reads older than their own SLO. The Lite only blocks cold-start via the consumer-side sentinel pattern.

Soft delete (DIDLite)

Removed DID attributes stay in storage with removed = true and value = "". readAttribute reverts AttributeRemoved(...) for these. readAttributeRaw returns them as-is for admin/debug.

What an integrator does

  1. Resolve a peaqID on a satellite chain. Call readAttribute(didAccount, "peaqID") and decode the returned bytes as a bytes32. Always pair with require(lastBatchAcceptedAtTs > 0) to handle cold start.
  2. Read identity status. getIdentity(machineId) returns the full record. Same cold-start check. Or use the granular home-style getters: getOwnerIfExists, operatorOf, getMachineStatus, getMachineURI.
  3. Read stake state. StakingLite.getStake(machineId), isStaked(machineId), isAuthorized(wallet), or totalStaked().
  4. Gate on a specific peaq approval. Read IdentityLite.lastCursorPacked() >= myExpectedCursor before letting a satellite action depend on it. Use the unpaused lastCursor() view if you need the unpacked form.
  5. Apply your own staleness SLO. Compare lastBatchAcceptedAt to block.timestamp. The Lite intentionally does not enforce one.

Addresses

Per-chain proxy addresses are pasted into operator .env files generated from templates in signer-daemon/deploy/ and surfaced through the SDK satellite registry. There is no single deploy-manifest JSON in the repo. Track the launch announcement for the canonical Agung and Base Sepolia address list, or pull them from peaqos/concepts/contracts once published.