> ## Documentation Index
> Fetch the complete documentation index at: https://docs.peaq.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Omni-chain

> How peaqOS mirrors identity, DID, and credit state from peaq to every supported chain via signed-push Lite contracts and an off-chain Signer Daemon.

<Info>
  **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.
</Info>

peaq stays canonical. Every other <Tooltip tip={G.blockchain.def}>chain</Tooltip> 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, <Tooltip tip={G.sign.def}>signs</Tooltip> them with a per-chain push key, and pushes them to satellite Lite contracts.

This means a <Tooltip tip={G.smartContract.def}>contract</Tooltip> or app on <Tooltip tip={G.base.def}>Base</Tooltip>, Ethereum, Polygon, or any other supported chain can resolve a machine's <Tooltip tip={G.did.def}>peaqID</Tooltip>, identity, <Tooltip tip={G.mcr.def}>MCR</Tooltip>, and <Tooltip tip={G.bond.def}>bond</Tooltip> status without an <Tooltip tip={G.rpcUrl.def}>RPC</Tooltip> hop to peaq, and without trusting an <Tooltip tip={G.onchain.def}>off-chain</Tooltip> oracle on top.

## What the mesh looks like

```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
                 peaq home chain
                 ┌────────────────────────┐
                 │ IdentityRegistry       │
                 │ peaq DID precompile    │
                 │ IdentityStaking        │
                 │ MCR pipeline           │
                 └──────────┬─────────────┘
                            │ finalized events
                            ▼
                 ┌────────────────────────┐
                 │ Signer Daemon          │
                 │ (off-chain, peaq-side) │
                 │ batches + EIP-712 sign │
                 └─┬────────┬─────────────┘
                   │        │
        ┌──────────┘        └──────────┐
        ▼                              ▼
  ┌───────────────┐              ┌───────────────┐
  │ Base satellite│              │ other chains  │
  │ IdentityLite  │              │ IdentityLite  │
  │ DIDLite       │              │ DIDLite       │
  │ StakingLite   │              │ StakingLite   │
  └───────────────┘              └───────────────┘
```

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

## DIDLite

DIDLite mirrors the peaq DID <Tooltip tip={G.precompile.def}>precompile</Tooltip>'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 <Tooltip tip={G.omniChain.def}>cross-chain</Tooltip> hop. Records are written exclusively by signed batches from the Signer Daemon; consumers only read.

### Public consumer views

```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
function readAttribute(address didAccount, bytes calldata attrName)
    external view
    returns (
        bytes memory value,
        uint32 validity,
        uint64 lastUpdatedHomeBlock,
        uint64 lastBatchAcceptedAtTs
    );

function readAttributeRaw(address didAccount, bytes calldata attrName)
    external view
    returns (DIDAttributeRecord memory record, uint64 lastBatchAcceptedAtTs);
    // admin/debug only; ignores the pause gate; returns soft-deleted records as-is

function lastHomeBlockApplied() external view returns (uint64);

struct DIDAttributeRecord {
    bytes  value;                  // raw bytes from peaq; empty after soft-delete
    uint32 validity;               // peaq DID validity field
    uint64 lastUpdatedHomeBlock;
    bool   removed;                // soft-delete sentinel
}
```

`readAttribute` <Tooltip tip={G.revert.def}>reverts</Tooltip> `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

```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
function getIdentity(uint256 machineId)
    external view
    returns (IdentityRecord memory record, uint64 lastBatchAcceptedAt);
    // pause-gated; lastBatchAcceptedAt == 0 is the cold-start sentinel

function getIdentityRaw(uint256 machineId)
    external view
    returns (IdentityRecord memory record, uint64 lastBatchAcceptedAt);
    // admin/debug only; ignores pause gate

function lastCursor()
    external view
    returns (uint64 blockNumber, uint32 txIndex, uint32 logIndex);
    // unpacked; NOT pause-gated (cross-Lite read invariant)

function lastCursorPacked() external view returns (uint128);
    // packed = (block << 64) | (txIndex << 32) | logIndex
    // NOT pause-gated

function lastUpdatedHomeBlock(uint256 machineId) external view returns (uint64);
    // per-record cursor; pause-gated

// Home-style accessors (additive to getIdentity)
function getOwnerIfExists(uint256 machineId)
    external view
    returns (address owner_, bool exists_);

function operatorOf(uint256 machineId) external view returns (address);

function getMachineStatus(uint256 machineId) external view returns (MachineStatus);

function getMachineURI(uint256 machineId) external view returns (string memory);

struct IdentityRecord {
    address owner;        // immutable post-Registered (soulbound on peaq)
    address operator;     // mutable; address(0) clears
    string  machineURI;   // immutable post-Registered
    MachineStatus status;
    uint64  lastHomeBlock;
}

enum MachineStatus { None, Pending, Verified, Rejected, Deactivated }
// Ordinals match peaq IdentityRegistry. Do not reorder.
```

### 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:

```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
(IdentityRecord memory rec, uint64 lastAt) = identityLite.getIdentity(machineId);
require(lastAt > 0, LiteUninitialized(address(identityLite)));
require(rec.status == MachineStatus.Verified, "not verified");
```

## 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

```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
function getStake(uint256 machineId)
    external view
    returns (StakeRecord memory record, uint64 lastBatchAcceptedAt_);
    // pause-gated; lastBatchAcceptedAt_ == 0 is the cold-start sentinel

function getStakeRaw(uint256 machineId)
    external view
    returns (StakeRecord memory record, uint64 lastBatchAcceptedAt_);
    // admin/debug only; ignores pause gate

function isStaked(uint256 machineId) external view returns (bool);
    // pause-gated

function isAuthorized(address wallet)
    external view
    returns (bool isAuth, uint64 lastBatchAcceptedAt_);
    // pause-gated; cold-start sentinel applies

function totalStaked() external view returns (uint256);
    // pause-gated; aggregate of applied Staked.amount events
```

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

```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
IdentityEvent(uint8 kind,uint256 machineId,address subject,string machineURI,uint64 homeBlockNumber,uint32 txIndex,uint32 logIndex)
```

`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

```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
DIDEvent(uint8 kind,address didAccount,bytes attrName,bytes attrValue,uint32 validity,uint64 homeBlockNumber)
```

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)

```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
Batch(uint64 nonce,uint64 deadline,uint8 schemaVersion,bytes32 eventsRoot,uint64 cursorLo_block,uint32 cursorLo_txIndex,uint32 cursorLo_logIndex,uint64 cursorHi_block,uint32 cursorHi_txIndex,uint32 cursorHi_logIndex)
```

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 <Tooltip tip={G.mainnet.def}>mainnet</Tooltip> (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 <Tooltip tip={G.sdk.def}>SDK</Tooltip> satellite registry but not yet rolled out as a daemon deployment.

| Property                | Behaviour                                                                                                                                                                                                                                                                                                                                                                                 |
| :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Finality                | GRANDPA-finalized only. No `latest-N` fallback. Daemon freezes if GRANDPA stalls rather than serve unfinalized state.                                                                                                                                                                                                                                                                     |
| Batching                | Critical events flush immediately; non-critical aggregate up to `flush_after_sec=30` or `max_batch_size=50`. `MAX_BATCH_SIZE` env override bounded `[1, 50]`; required `=10` for DIDLite (byte-payload events exceed Agung block gas at 50).                                                                                                                                              |
| Nonce ordering          | Per-chain EVM nonce lock + per-Lite monotonic batch nonce. `AlreadyApplied` → skip-advance. `NonceOutOfOrder` → backfill missing nonce(s).                                                                                                                                                                                                                                                |
| Restart catchup         | On boot, `resume_cursor = max(localCursor, liteCursor)`. Prevents replay after a crash between Lite acceptance and local commit.                                                                                                                                                                                                                                                          |
| Pause retries           | `WritesPaused` from `applyBatch` triggers exponential backoff `(1, 5, 15, 60, 300)s` rather than fatal exit, so admin pauses do not crash the fleet.                                                                                                                                                                                                                                      |
| Poison events (DIDLite) | Orphan `Update`/`Remove` events whose `Add` predates the deploy block trigger `AttributeDoesNotExist` or `AttributeAlreadySoftDeleted`. The daemon decodes the revert, isolates the offending event, pushes the rest of the batch, and increments `poison_event_skipped_per_lite[lite]` on `/health`. Under partial-history replay the satellite is best-effort cache, not authoritative. |
| Cross-language parity   | EIP-712 typehashes are constants in Solidity (`EVENT_TYPEHASH_V1`, `BATCH_TYPEHASH_V1`) and in Python. A Hardhat task dumps canonical fixtures; Python recomputes identical hashes (17 fixtures, all 65-byte signatures bit-identical).                                                                                                                                                   |
| Hot-wallet floor        | `HOT_WALLET_MIN_BALANCE_WEI` (default `0.02` native). Below the floor the daemon stops signing and `/health.hot_wallet_low[chain_id] = true`.                                                                                                                                                                                                                                             |

### Health endpoint

Each daemon exposes a loopback-only `/health` (default port unique per instance, conventionally 8080–8085):

```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
  signer_key_id,
  last_push_at,
  hot_wallet_low: { [chain_id]: bool },
  queue_depth,
  poison_event_skipped_per_lite: { [lite_name]: int }
}
```

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`:

```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
pauseLite(string reason)         // pause user-facing reads
unpauseLite()                     // resume reads (blocked while in emergency)
pauseApplyBatch(string reason)    // pause inbound writes
unpauseApplyBatch()               // resume writes (allowed even in emergency)
setSigner(address newSigner)      // routine PUSH_KEY rotation
emergencyRotatePushKey(address)   // incident-response rotation
exitEmergencyMode()                // requires currentSigner != snapshot
adminReplayEvents(...)             // owner-driven replay/rewind
```

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 <Tooltip tip={G.address.def}>address</Tooltip>, not an enum:

```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
bool    inEmergencyMode;
address emergencyEnteredBySigner;   // snapshot at first incident pause
bool    litePaused;                 // read pause
bool    applyBatchPaused;           // write pause
```

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 <Tooltip tip={G.transaction.def}>transaction</Tooltip> 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.

## Related

* Satellite mirrors — propagation and cursor-independence model (coming soon)
* Signer daemon — runtime surface (coming soon)
* Satellite SDK overview — JS/Python bindings (coming soon)
* Lite views (Solidity) — Solidity consumer surface (coming soon)
* Operate → Signer daemon deploy — production runbooks (coming soon)
* [Scale function](/peaqos/functions/scale)
* [Activate function](/peaqos/functions/activate)
* [Qualify function](/peaqos/functions/qualify)
* [Machine NFT concept](/peaqos/concepts/machine-nft)
* [peaqID concept](/peaqos/concepts/peaqid)
* [Roadmap](/roadmap)
