Browse the beta.3 manual
docs/OPERATIONAL-PROFILE.mdHistorical record / original scope retained

Operational profile record

Historical beta.1 profile and verification record. Not a beta.3 verification.

This manual targets 0.1.0-beta.3. Read the source and availability policy before installing. Historical reports retain their original scope.

Historical evidence, not a new 0.1.0-beta.3 verification. Read the record's date and version before interpreting any PASS result.

Date: 2026-08-30

Historical beta.1 measurements and evidence. For current commands, demo, checkout and client helpers, use the documentation index and changelog. This record is not a new beta.2 verification.

Status: beta implementation profile. This document describes behavior exercised by the repository; it is not a production-readiness or privacy-absoluteness claim.

Product boundary

PPOps is a single-merchant, self-hosted reconciler for one configured RAILGUN network and one configured ERC-20 token. It creates local payment intents, signs payer-facing descriptors, scans an incoming view-only wallet, derives settlement eligibility and emits local/HMAC-authenticated events.

PPOps does not create or submit spends. It has no RAILGUN mnemonic/spending-key configuration, Solidity, Request Network adapter, UI, hosted relay, HPKE, second privacy rail or generic adapter framework.

Descriptor profile

Name: PPOpsPaymentDescriptorV1

EIP-712 domain:

name:    PPOps Payment Descriptor
version: 1
chainId: configured chain

Signed fields:

version        uint8
chainId        uint256
rail           string       # railgun
tokenAddress   address
decimals       uint8
amountAtomic   uint256
recipient0zk   string
reference      bytes32
expiresAt      uint64
nonce          bytes32
merchantSigner address

The reference and nonce are independently generated random 32-byte values. The commercial reference is not signed into the descriptor and remains in local SQLite. A verifier must compare both the recovered signer and embedded merchantSigner to an address obtained independently of the descriptor.

Settlement normalization

Only positive ERC-20 TXOs with a memo matching this exact expression enter PPOps reconciliation:

^ppops:v1:(0x[0-9a-f]{64})$

The scanner immediately reduces the decrypted memo to the opaque reference; it does not persist the memo text. It records:

uniqueSettlementId
chainId
txidVersion
transactionHash
tree
position
optional railgunTxid
tokenAddress
amountAtomic
blockNumber
blockTimestamp
balanceBucket
rawPPOIStatuses
chainStatus
poiStatus
reference

Stable identity:

chainId:txidVersion:normalizedPublicTransactionHash:tree:position

The public transaction hash and output coordinates were stable across the gate restart. transactCreationRailgunTxid is optional because the controlled and sampled V2 TXOs returned it as null in relevant cases.

Orthogonal settlement state

chainStatus:

OBSERVED | CONFIRMED | FINALIZED | REVERTED

Finality is one policy per instance:

  • finalized: transaction block is at or below the RPC’s finalized tag.
  • confirmations: latest - transactionBlock + 1 reaches the configured count.

The Arbitrum mainnet profile requires finalized; confirmation-count finality is rejected even at a high count. Receipt, block and finalized-height reads use a majority quorum across all configured RPCs. Two configured providers must both agree; three tolerate one failure or inconsistent response. Height selection is conservative and excessive divergence fails closed. Non-final and recently finalized persisted settlements are rechecked by transaction receipt so a TXO that disappears from the wallet scan can be marked REVERTED.

poiStatus:

RAILGUN balance bucket PPOps status
Spendable SPENDABLE
ShieldBlocked BLOCKED
ShieldPending, ProofSubmitted, MissingInternalPOI, MissingExternalPOI PENDING
Spent with a raw Valid status SPENDABLE as historical eligibility
Spent without Valid UNKNOWN, preserving prior spendability if PPOps observed it

matchStatus:

  • UNMATCHED: no local intent for the opaque reference.
  • MATCHED: reference, chain and token match.
  • CONFLICT: the reference exists, but chain or token differs.

A settlement contributes value only while MATCHED, FINALIZED and SPENDABLE.

Intent projection

Intent state is recalculated transactionally from settlements rather than maintained as an independent state machine. Atomic integer strings are converted to JavaScript bigint for all sums and comparisons.

Condition Status
no credited value, before expiry OPEN
credited value is positive and below expected PARTIAL
credited value reaches expected; crossing settlement timestamp is not late PAID
no credited value at/after expiry EXPIRED
crossing settlement block timestamp is after expiry PAID_LATE

Overpayment is max(received - expected, 0). Pending matched value is reported separately and never counted as received.

Idempotency and event delivery

Intent creation requires an Idempotency-Key. SQLite stores a SHA-256 request fingerprint and the resulting intent in the same transaction. Identical retries return the original intent; reuse with a changed request is rejected.

SQLite uniqueness enforces one settlement row per stable identifier. Immutable identity fields are compared on rediscovery; a collision with different data is rejected. Projection recalculation, revision increment and outbox insertion run inside one SQLite transaction.

Event IDs are deterministic SHA-256-derived identifiers over a persisted dedupe key. Event ordering uses SQLite insertion order when timestamps match. Events do not include externalReference.

For the optional outbound webhook:

signature = HMAC-SHA256(key, timestamp + "." + keyId + "." + eventId + "." + rawPayload)

Deliveries include a configured key ID for receiver-side rotation. An authenticated endpoint can reschedule one explicitly selected dead-lettered event; it cannot change the webhook URL or replay an event that is still live.

The controlled pilot receiver persists event IDs and counts duplicate delivery attempts per event type without retaining payloads. mainnet-gate-replay reconstructs and freshly signs the single confirmation event, then fails unless the receiver recognizes it as an identical persisted event. Three API-token-keyed redacted snapshots bind the paid state before restart, after restart and from an isolated restore; the final verifier requires distinct daemon instances and stable intent, settlement and confirmation fingerprints. Every capture also requeries the receipt and block through the configured RPC quorum and checks the current finalized height. The resulting redacted report is EIP-191 signed with the existing merchant identity key for public verification against the independently distributed signer. Operator snapshots retain exact amounts and timestamps and are private evidence; only the final metadata-minimal report is intended for publication.

The URL exists only in operator configuration. There is no registration API. Non-loopback HTTP URLs are rejected; remote delivery requires HTTPS. Failed events use exponential retry and become dead-lettered at the configured maximum.

Storage and recovery

  • PPOps intents/projections/settlements/outbox: SQLite with WAL, foreign keys and synchronous=FULL.
  • RAILGUN engine/wallet state: LevelDOWN using the pinned SDK and a file-provided 32-byte encryption key.
  • Viewing key and every service key: separate mode-0600 file, never returned by API or application logs.
  • Runtime lock: prevents a second daemon and makes offline backup fail closed.

Backup uses SQLite’s backup API only after verifying the daemon is stopped, recursively copies the encrypted LevelDB and wallet state, inventories every file with SHA-256 and records high-entropy secret fingerprints. Secret values are excluded unless the operator explicitly supplies --include-secrets. Restore validates inventory, network/token and secret identity; forced restore moves prior files aside instead of deleting them.

Tested surfaces and measurements

Pinned dependencies:

@railgun-community/wallet        10.9.0
@railgun-community/engine         9.6.0
@railgun-community/shared-models  8.0.1
@railgun-community/waku-broadcaster-client-node 9.1.1 # payer only
ethers                            6.14.3

Primitive gate:

  • Controlled RAILGUN V2 1-input/2-output Groth16 transfer on a local Hardhat chain: memo/token/amount/output identity/PPOI recovered by the view-only receiver and spending rejected.
  • Live Sepolia fixture: 70 ERC-20 TXOs and real PPOI-derived buckets; test-only journal emitted two memo outputs on first run and zero on restart.

Verification evidence:

  • Public CI run 33337865296 passed both verify and docker for commit 207837c, including the final populated-calldata simulation, prepare-time nullifier admission and tested shield-key derivation remediations.
  • Automated merchant suite on 2026-08-30: 19 test files and 58 tests, including 1,000 property-based runs across reconciliation conservation/order invariance and opaque memo round trips. Enforced V8 coverage is 67.54% statements, 62.74% branches, 72.23% functions and 70.38% lines across all src/**/*.ts; core database, reconciliation, descriptor and webhook paths are substantially higher than the RAILGUN engine wrapper that requires the live gate.
  • Automated reference-payer suite on 2026-08-30: 15 test files and 82 tests, including request freshness, self-signed/Broadcaster transaction bounds, pre-Waku nullifier journaling, reported-versus-canonical hash separation, adversarial hash mismatch, proof-compatible quote rotation, exact-submission-quote persistence, classified rejection/ambiguity, exact-nullifier bounded retries, alternate-Broadcaster exclusion, cross-intent nullifier collision rejection, bounded RPC reads, gas outlier handling, exact populated-calldata simulation quorum, receipt agreement, prepare-time nullifier admission, no-broadcast preparation and clean CLI-process termination.
  • Dry-run npm tarballs contained no secret/config/data paths; the merchant tarball contained no payer runtime and was limited to dist, package metadata, README and license files.
  • Clean tarballs for both packages were installed outside the repository and their npm-generated binaries executed. This exposed and then verified the fix for the merchant CLI’s former symlink-sensitive direct-execution guard.
  • Arbitrum quorum preflight against two public RPC origins: chain ID 42161, latest block 499967929, finalized block 499964670; both providers agreed on the conservative finalized block and its hash.
  • A later three-origin profile passed chain/finality preflight with 2-of-3 majority semantics. The payer subsequently synchronized and completed the same bounded no-broadcast preparation in 6.3 seconds; no submission record was created. This is fault-tolerance evidence for that run, not an availability SLA or proof of provider organizational independence.
  • PPOI preflight against the community aggregator documented by the Wallet SDK: 1/1 node returned the exact ppoi_health JSON-RPC success response on 2026-08-30. This proves reachability at that time, not future availability or independence.
  • Fresh strict Sepolia scan from the public fixture viewing key: 21.2 seconds, zero PPOps-format references as expected.
  • Restart scan against the same encrypted LevelDB: 9.9 seconds, zero duplicate reconciliation events.
  • Local API: unauthenticated operational request returned 401; intent creation returned 201; descriptor verification returned valid; webhook registration route returned 404; SIGINT completed graceful shutdown.
  • Loopback pilot receiver: the first correctly signed event returned 204 with idempotent-replayed: false; an identical retry returned 204 with idempotent-replayed: true; /stats retained exactly one payment.confirmed event and no payload.
  • Mainnet evidence tooling: authenticated redacted snapshots passed across three distinct test daemon identities and separate restore origin; modified state, reused instances and raw invoice/reference/transaction leakage were rejected.
  • Offline state backup completed and produced an integrity manifest.
  • Controlled Arbitrum mainnet self-pilot: after the no-broadcast preparations, one approved 0.01 USDC transaction mined once with a populated maximum gas cost of 54267840000000 wei under a 0.001 ETH ceiling. PPOps first recorded CONFIRMED + PENDING + MATCHED, then FINALIZED + SPENDABLE + MATCHED -> PAID after the payer submitted its output PPOI. Exact-once webhook replay, restart and an isolated restore all passed.
  • Non-financial Gate B preflight: the pinned Waku client became ready in about twelve seconds, found at least five LightPush and at least five Filter peers, and selected a native-USDC quote with observed reliability between 0.84 and 1 and roughly five to nine minutes remaining. The command loaded no wallet or EVM key, generated no proof and submitted no payment.
  • Non-financial Gate B full preparation: the payer generated a proof for a 10000-atomic (0.01 USDC) request, obtained a 1226761 gas estimate with three-provider agreement and observed an exact 70373-atomic (0.070373 USDC) Broadcaster fee at 0.84 reliability. A lower 50000-atomic ceiling had already rejected the quote safely before proof. The passing preparation submitted no payment, wrote no journal record and left the intent open at zero received. The fee is point-in-time evidence, not a later quote. The value-bearing Gate B remained unproven at that stage.
  • Post-trial no-send diagnosis: the pre-proof SDK estimate was 1128365, and all three configured RPCs accepted the exact final populated calldata at an upper-median 1123239 estimate. The point-in-time fee was 64892 atomic (0.064892 USDC). The run submitted no payment and wrote no journal. This narrows the failed value-bearing path to off-chain Broadcaster processing or its sanitized response boundary; by itself it did not pass Gate B.
  • Prepare-time reservation diagnosis: a later fresh-intent proof selected at least one nullifier from the unresolved lineage. The read-only journal check returned SUBMISSION_ALREADY_RECORDED before final simulation, Waku or a new journal record. Existing Wallet SDK Spendable balance is therefore not treated as safe fresh-input capacity.
  • Controlled value-bearing Gate B trial: one initial Waku submission and three bounded same-nullifier variants used fee quotes between 0.058867 and 0.071154 USDC, each below the operator’s 0.08 USDC ceiling. The first three attempts addressed one Broadcaster identity. The final retry observed 18 valid quotes from 14 unique identities, excluded the prior identity and selected a second. Both attempted identities returned post-send failures without a reported transaction hash; one was the upstream-sanitized UNKNOWN_ERROR, while another remained an unclassified client/transport failure. Final wallet synchronization more than 15 minutes after the last attempt found no canonical transaction for the reserved nullifiers; the private balance remained 0.1895 USDC and the merchant intent remained open with zero received/pending value. The retry cap is exhausted, the nullifiers remain reserved, no fee was observed as charged and that original lineage remains unresolved.
  • Subsequent isolated Gate B pass: a fresh payer waited for 149625 atomic to become fully Spendable. No-send preparation quoted 67110 atomic and quorum-simulated 1191953 gas for the exact final calldata. The authorized value-bearing command paid a 66912-atomic fee, resolved the Broadcaster-reported hash independently from fresh reserved nullifiers, mined on its first attempt and used no public EVM self-signer. PPOI returned all relevant statuses to Valid; PPOps reached FINALIZED + SPENDABLE + MATCHED -> PAID; webhook replay, restart and isolated restore passed. This is one controlled success, not a relay SLO.
  • After moving to one explicit scan owner, the merchant reached readiness in approximately 6 seconds and, after the final observability correction, completed five subsequent scheduled scans at roughly 34-second cadence. Earlier transient RPC-quorum failures had been mislabeled STORAGE_LOCKED because the classifier matched lock inside the word block; no LevelDB lock failure was established.

These measurements use public third-party RPC/PPOI endpoints and reflect controlled mainnet test runs rather than fixed latency SLOs.

Reproducible evidence

npm run verify

Machine-readable reports:

  • artifacts/primitive-gate-report.json
  • artifacts/privacy-report.json
  • artifacts/mainnet-gate-report.json

Dependencies and residual operational risk

The pinned RAILGUN packages are a large security-sensitive dependency. As of this snapshot, compatible transitive overrides reduce the merchant npm audit --omit=dev to 36 findings (6 moderate, 30 low) and the separate Waku-enabled payer to 40 findings (10 moderate, 30 low); both have zero critical/high findings. Overrides cover Axios, WebSocket, form-data, tar, cookie/query parsers and YAML/object setters. The runtime, privacy gate and test suite pass with the override set. Legacy web3 -> web3-bzz -> swarm-js, wallet GraphQL/React-Native packages and the Waku transport tree remain present, so package presence is still a supply-chain and future-reachability risk. CI rejects high/critical production findings and exports a CycloneDX SBOM. Blind npm audit fix --force would still propose incompatible RAILGUN changes and is not applied.

The pinned SDK’s historical refresh defers the event consumed by awaitWalletScan, while its listener poller may schedule delayed TXID work. PPOps therefore does not combine those two completion models: it pauses the SDK listener poller, owns explicit refresh scheduling and reads current TXO/PPOI state after refreshBalances. The daemon drains its active refresh before LevelDB shutdown. The payer performs the same clean shutdown, flushes output, then terminates prover worker threads that otherwise keep a finite CLI alive. These lifecycle assumptions must be revalidated on every SDK upgrade.

Operational progress treats an SDK Complete scan event as ratio 1 even when the wrapper omits its numeric progress field. RPC quorum disagreement is exposed as RPC_QUORUM, not as a storage fault. Every quorum read gets one bounded retry; persistent disagreement still fails closed and the next owned scan retries without loosening finality or accepting a single-provider answer.

Docker packaging is built in CI. The Node base image is digest-pinned, GitHub Actions are SHA-pinned, and a tag-triggered workflow first requires v<package version>, then publishes version and commit-addressed GHCR images. The release records the immutable image digest and attaches both SBOMs and the primitive, privacy and Mainnet Gate reports. Release CI parses both generated SBOMs and requires their expected root component plus a nonempty CycloneDX component inventory, even when the generator tolerates known overridden transitive-tree warnings. Compose bounds memory, CPU and PID usage; the production runbook defines readiness, metrics, alert and egress policy.

Public GitHub Actions run 33337865296 passed both the full verify job and the Docker build job for implementation commit 207837c on 2026-08-30. This proves that commit’s CI build, not the still-pending tag-triggered GHCR publication.