diff --git a/OVERVIEW.md b/OVERVIEW.md index 88d9644cd9..3f38fc3b21 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -2,16 +2,16 @@ ## Wire Network -Wire Network is a multi-chain consensus and interoperability platform built on the AntelopeIO protocol. It enables secure, trustless cross-chain communication through a specialized consensus mechanism and unified proof protocol. The network connects the Wire blockchain (the coordination layer) with external chains — Ethereum, Solana, and Sui — via on-chain **Outpost** contracts deployed on each supported chain. +Wire Network is a multi-chain consensus and interoperability platform built on the AntelopeIO protocol. It enables secure, trustless cross-chain communication through a specialized consensus mechanism and unified proof protocol. The network connects the Wire blockchain (the coordination layer, or **depot**) with external chains — Ethereum (EVM) and Solana (SVM) — via on-chain **Outpost** contracts deployed on each supported chain. ### Architecture at a Glance | Layer | Components | |-------|-----------| -| **Wire Blockchain (Depot)** | Block producers, system contracts (`sysio.msgch`, `sysio.epoch`, `sysio.uwrit`, `sysio.chalg`) | -| **Operator Network** | 21 batch operators (3 groups of 7), underwriters | -| **External Chains** | Outpost smart contracts on Ethereum, Solana, Sui | -| **Protocol Layer** | OPP — protobuf-encoded messages, merkle proofs, multi-sig consensus | +| **Wire Blockchain (Depot)** | Block producers, system contracts (`sysio.msgch`, `sysio.epoch`, `sysio.opreg`, `sysio.chalg`, `sysio.synd`, `sysio.bond`, `sysio.liq`, `sysio.swap`, `sysio.dclaim`, `sysio.andon`, …) | +| **Operator Network** | Batch operators relaying OPP envelopes in rotating groups (default 21 operators, 3 groups of 7); bond providers underwriting syndication statements on the depot | +| **External Chains** | Outpost contracts on Ethereum and Solana | +| **Protocol Layer** | OPP — protobuf-encoded envelopes, per-outpost envelope hash chains, group consensus | --- @@ -20,23 +20,23 @@ Wire Network is a multi-chain consensus and interoperability platform built on t OPP is the cryptographic wire protocol that enables deterministic, consensus-backed proof transmission across blockchains. It uses Protocol Buffers for serialization and defines a hierarchical message structure: ``` -Envelope (epoch-spanning container) - └─ Message (individual message within an epoch) +Envelope (one per outpost per epoch, per direction) + └─ Message (individual message within an envelope) └─ Attestation (typed data entry within a message) ``` ### Key Concepts -- **Epochs**: Time-bounded intervals managed by `sysio.epoch`. Each epoch assigns one group of 7 batch operators as active relayers. -- **Envelopes**: Epoch-level containers carrying a merkle root, epoch index, timestamp, and the range of messages they cover. -- **Messages**: Individual cross-chain payloads containing a header (message ID, checksum, encoding flags) and a payload (an array of attestation entries). -- **Attestations**: Typed data units that represent cross-chain actions — swaps, stake updates, operator actions, underwriting intents, and more. +- **Epochs**: Time-bounded intervals managed by `sysio.epoch` — one global epoch clock for every chain. Each epoch has one group of batch operators on duty as relayers. +- **Envelopes**: Epoch-level containers. Each envelope carries the epoch index and timestamp, its route endpoints, and `previous_envelope_hash` — the digest of the previous envelope on the same (depot, outpost) stream — so each direction forms a hash chain. An envelope is capped at 32,768 bytes on every chain. +- **Messages**: Individual cross-chain payloads containing a header (message ID, sequence, checksum, encoding flags) and a payload (an array of attestation entries). +- **Attestations**: Typed data units that represent cross-chain actions — operator rosters, liquidity syndication and desyndication, yield reports, node-owner registrations, staking rewards. ### Chain Addressing OPP uses a universal chain-agnostic address system: -- **ChainKind** enum: `WIRE`, `ETHEREUM`, `SOLANA`, `SUI` +- **ChainKind** enum: `WIRE`, `EVM`, `SVM` - **ChainAddress**: `(ChainKind, raw_bytes_address)` — no translation or mapping needed - **ChainSignature**: `(actor, key_type, signature_bytes)` — supports native chain signatures @@ -48,53 +48,55 @@ This allows a single unified message format across all supported chains. ### Inbound (External Chain → Wire) -1. A user action on an external chain (e.g., depositing ETH on the Ethereum Outpost) triggers an `OPPMessage` event containing protobuf-encoded attestations. -2. All 7 active batch operators independently observe the event via chain-specific client plugins (`outpost_ethereum_client_plugin`, `outpost_solana_client_plugin`). +1. A user action on an external chain (e.g. syndicating liqETH into the Ethereum `SyndicationPool`) queues a protobuf-encoded attestation in the outpost's outbound buffer, which the outpost seals into its envelope for the epoch. +2. Every batch operator in the active group independently reads that envelope through its chain-specific client plugin (`outpost_ethereum_client_plugin`, `outpost_solana_client_plugin`). 3. Each operator delivers the envelope to Wire's `sysio.msgch` contract via the `deliver()` action. -4. `sysio.msgch` evaluates consensus — when all operators in the active group submit identical envelope checksums, the messages are accepted. -5. Attestations are extracted, parsed, and routed to the appropriate system contract handlers. +4. `sysio.msgch` evaluates consensus: every eligible operator delivering an identical envelope is accepted immediately; otherwise a strict majority is accepted once the epoch boundary has passed. A split with no strict majority opens a `sysio.chalg` dispute. +5. Attestations are extracted, validated, and routed to the appropriate system contract handlers. ### Outbound (Wire → External Chain) 1. System contracts queue attestations for a target outpost via `sysio.msgch::queueout()`. -2. When the epoch advances, `buildenv()` packs all `READY` attestations into a protobuf-encoded outbound envelope with a merkle root. -3. Batch operators read the outbound envelope and submit it to the target chain's Outpost contract. -4. The Outpost contract (`OPPInbound.sol` on Ethereum) verifies consensus, checks merkle proofs, and routes attestations to registered handler contracts. +2. When the epoch advances, `buildenv()` packs the `READY` attestations into a protobuf-encoded outbound envelope; anything that does not fit stays queued for the next epoch. +3. Batch operators read the outbound envelope and submit it to the target chain's Outpost — chunked on both chains (`OPPInbound.epochIn` on Ethereum, `epoch_in` on Solana). +4. The Outpost verifies the envelope chain and group consensus, then routes attestations to their registered handlers. An unhandled type is skipped, never reverted. --- ## Key Attestation Types -| Category | Types | Purpose | -|----------|-------|---------| -| **Operator Management** | `OPERATOR_ACTION`, `BATCH_OPERATOR_NEXT_GROUP`, `SLASH_OPERATOR` | Operator registration, rotation, penalties | -| **Staking** | `STAKING_REWARD` | Per-staker cross-chain yield distribution (validator-staking lifecycle types are reserved but deferred post-launch) | -| **Swaps & Liquidity** | `SWAP`, `UNDERWRITE_INTENT`, `UNDERWRITE_CONFIRM`, `REMIT` | Cross-chain swaps backed by underwriter collateral | -| **Governance** | `CHALLENGE_REQUEST`, `CHALLENGE_RESPONSE`, `EPOCH_SYNC`, `ROSTER_UPDATE` | Dispute resolution and epoch synchronization | -| **Reserves** | `RESERVE_BALANCE_SHEET` | Collateral snapshots across chains | +| Category | Types | Direction | Purpose | +|----------|-------|-----------|---------| +| **Operator Rosters** | `OPERATORS`, `BATCH_OPERATOR_GROUPS` | depot → outpost | Full operator roster and the next epoch's batch-operator group, sent every epoch | +| **Liquidity Syndication** | `SYNDICATE_LIQ`, `LIQ_YIELD` | outpost → depot | A user syndicating liq tokens into outpost custody; realized custody yield | +| **Liquidity Desyndication** | `DESYNDICATE_LIQ` | depot → outpost | Return syndicated liq tokens to a holder on the outpost | +| **Node Owners** | `NODE_OWNER_REG` | outpost → depot | Node-owner NFT committed on Ethereum, registering the owner on the depot | +| **Staking** | `STAKING_REWARD` | outpost → depot | Per-staker reward credited to the `sysio.dclaim` claim ledger (validator-staking lifecycle types are reserved but deferred) | + +Cross-chain swaps routed through outposts, outpost reserves and outpost-side operator collateral have been removed. Swaps are a depot-local AMM (`sysio.swap`) with no outpost participation; operator collateral is held on the depot by `sysio.opreg`. --- ## Participant Roles ### Block Producers -Elected via Appointed Proof of Stake (APoS). They validate transactions and produce blocks on the Wire blockchain — the standard Antelope producer role. +Elected via Appointed Proof of Stake (APoS). They validate transactions and produce blocks on the Wire blockchain — the standard Antelope producer role. Producers register with `sysio.opreg` and post depot-native collateral there. ### Batch Operators -21 operators split into 3 groups of 7. One group is active per epoch. They run the `batch_operator_plugin` and are responsible for: -- Observing events on external chain Outposts -- Delivering inbound envelopes to `sysio.msgch` -- Building and submitting outbound envelopes to external chains -- Responding to challenges if disputed +Operators scheduled in groups of `operators_per_epoch` (default 21 operators in 3 groups of 7); one group is on duty per epoch, and the schedule slides forward every epoch. They run the `batch_operator_plugin` and are responsible for: +- Reading each outpost's outbound envelope and delivering it to `sysio.msgch` +- Relaying the depot's outbound envelopes to each outpost +- Cranking the depot: `sysio.msgch::chkcons` (epoch advance), `sysio.chalg::chkdispute`, `sysio.swap::tickyield`, `sysio.liq::queueyield` + +Batch operators post depot-native collateral on `sysio.opreg`; a non-canonical delivery is slashed and missed deliveries lead to termination. Signing keys for outbound submissions can be held locally (`KEY:`) or in a `kiod` daemon (`KIOD:`) — both built into `signature_provider_manager_plugin`. AWS KMS support (`KMS:` — secp256k1/ethereum keys only, remote signing so the key never leaves KMS) is provided by `signature_provider_kms_plugin`, enabled per config with `plugin = sysio::signature_provider_kms_plugin`. See `plugins/signature_provider_kms_plugin/test/README.md` for KMS key setup, IAM requirements, the `KMS:` spec format, and operational notes. AWS SSM Parameter Store support (`SSM:` — all key types) is `signature_provider_ssm_plugin`, enabled with `plugin = sysio::signature_provider_ssm_plugin`: the private key is fetched from a KMS-encrypted `SecureString` parameter once at startup and signing is local thereafter, fast enough for every signing path including block production. See `plugins/signature_provider_ssm_plugin/test/README.md` for parameter setup, IAM requirements, the `SSM:` spec format, and rotation notes. A `KMS:`/`SSM:` spec whose plugin is not enabled fails the boot with an error naming the `plugin =` line to add; host applications that embed the manager without the plugins can still register handlers from `main()` via `register_spec_handler()`. -### Underwriters -Independent operators running the `underwriter_plugin`. They provide collateral-backed liquidity for cross-chain swaps: -1. Monitor `sysio.msgch` for `SWAP` attestations -2. Verify deposits on external chains -3. Submit underwriting intent to `sysio.uwrit` with locked collateral -4. Earn fees on successful settlement; face slashing on failure +### Bond Providers (Underwriters) +Depot accounts that underwrite provable statements on `sysio.bond` with depot-native tokens (WIRE or shadow LIQ). Today the issuer is `sysio.synd`: each syndication envelope is released only after its statement is bonded. A bond provider accepts a share of a request, earns a pro-rata share of the bounty when the statement is approved, and forfeits the bond to the issuer if the statement is resolved invalid. No plugin or outpost interaction is involved. + +### Node Owners +Holders of a node-owner NFT who commit it on the Ethereum outpost (`NODE_OWNER_REG`). They are registered on `sysio.roa`, receive node-owner WIRE distributions from `sysio.system`, and form the Tier-1 electorate that votes on `sysio.chalg` envelope disputes. --- @@ -102,10 +104,20 @@ Independent operators running the `underwriter_plugin`. They provide collateral- | Contract | Role | |----------|------| -| **sysio.msgch** | Central message router — receives inbound envelopes, evaluates consensus, extracts attestations, queues outbound messages | -| **sysio.epoch** | Manages epoch cycles, assigns operator groups, registers outposts | -| **sysio.uwrit** | Underwriting ledger — tracks collateral, locks/releases funds, distributes fees, handles slashing | -| **sysio.chalg** | Challenge handler — manages dispute resolution with economic penalties | +| **sysio.msgch** | OPP channel — receives inbound envelopes, evaluates consensus, routes attestations, queues and builds outbound envelopes, cranks the epoch | +| **sysio.epoch** | Global epoch clock, sliding-window batch-operator schedule, emissions gate | +| **sysio.opreg** | Operator registry and depot-native collateral ledger (deposit, withdraw queue, pull claims, slashing, termination) | +| **sysio.chalg** | Envelope-dispute vote by Tier-1 node owners; slash execution | +| **sysio.synd** | Liquidity syndication ledger: syndication envelopes, bonded release, desyndication, rate limits | +| **sysio.bond** | Generic bonded underwriting of provable statements | +| **sysio.liq** | Shadow liq tokens (`LIQETH`, `LIQSOL`) minted against outpost custody, with a WIRE yield index | +| **sysio.swap** | Depot-local constant-product AMM; every pair's second leg is WIRE | +| **sysio.dclaim** | WIRE claim ledger (launch import, staking rewards) | +| **sysio.andon** | Emergency stop: one cord freezing depot fund egress | +| **sysio.roa** | Resources and node-owner registry | +| **sysio.authex** | Links a depot account to its external-chain keys | +| **sysio.chains** / **sysio.tokens** | Chain (outpost) and token registries | +| **sysio.system** | Emissions and producer / batch-operator / node-owner pay | --- @@ -113,20 +125,24 @@ Independent operators running the `underwriter_plugin`. They provide collateral- | Contract | Role | |----------|------| -| **OPP.sol** | Message sender — collects attestations in send mode, builds merkle trees, emits `OPPMessage` / `OPPEpoch` events | -| **OPPInbound.sol** | Message receiver — validates consensus, verifies merkle proofs, routes attestations to handlers | -| **OperatorRegistry.sol** | Maps operators to roles, keys, and status for identity validation | -| **BAR.sol** (Bond Agreement Registry) | Records validator / node-owner bonds; operator-collateral `OPERATOR_ACTION` emission has moved to `OperatorRegistry.sol` | -| **IOPPReceiver implementations** | Domain-specific handlers (Depositor, Pool, Pretoken) that process individual attestation types | +| **OPP.sol** | Outbound — queues attestations, seals and emits the outpost's per-epoch envelope | +| **OPPInbound.sol** | Inbound — accepts chunked `epochIn` deliveries, validates group consensus and the envelope chain, routes attestations to handlers | +| **SyndicationPool.sol** | liqETH custody: `syndicate` (`SYNDICATE_LIQ`), `realizeYield` (`LIQ_YIELD`), `DESYNDICATE_LIQ` payout | +| **BAR.sol** | Node-owner NFT escrow emitting `NODE_OWNER_REG` | +| **OutpostManager.sol** | OPP endpoint and role wiring | +| **StakingManager.sol** | Placeholder for the deferred staking track; its entry points revert | + +On Solana the outpost lives in the `liqsol-core` program (`instructions/opp/`), alongside liqSOL staking and syndication. --- ## Consensus & Security -- **Multi-signature consensus**: All 7 active operators must deliver identical envelope checksums for acceptance. -- **Merkle proofs**: Each message is a leaf in the epoch merkle tree; inbound processors verify proofs before accepting. -- **Challenge mechanism**: Any party can challenge a disputed epoch via `sysio.chalg`. Operators must respond with correct hashes or face slashing. -- **Collateralized underwriting**: Swaps are backed by locked collateral with a full lifecycle (intent → submit → confirm → release/slash). +- **Group consensus**: An envelope is accepted when every eligible operator in the active group delivers it identically, or by strict majority once the epoch boundary has passed. +- **Envelope hash chains**: Each envelope carries the digest of its predecessor on the same stream, and messages carry sequence numbers, so envelopes and messages cannot be reordered or replayed. +- **Disputes**: A post-boundary split with no strict majority opens a `sysio.chalg` dispute that pauses the epoch until Tier-1 node owners vote a canonical envelope. Operators that delivered a non-canonical envelope are slashed. +- **Bonded syndication**: Syndicated value is released on the depot only after `sysio.bond` underwriters bond the syndication statement; anyone may challenge it during the challenge window. +- **Emergency stop**: `sysio.andon` freezes depot fund egress; outposts pause independently (`SyndicationPool` pause on Ethereum, `frozen` on Solana). --- @@ -136,6 +152,6 @@ Wire Network implements a trustless, consensus-backed omnichain infrastructure w 1. **OPP** provides a single deterministic message encoding across all blockchains 2. **Batch operators** collectively relay and validate messages with no single point of failure -3. **Underwriters** provide collateral-backed liquidity for cross-chain swaps -4. **On-chain governance** handles scheduling (epochs), routing (message channel), and economic enforcement (underwriting + challenges) -5. **Atomic cross-chain settlement** — no bridging overhead, no wrapped tokens, no trusted intermediaries +3. **Bond providers** stand behind every syndication statement with slashable bonds +4. **On-chain governance** handles scheduling (epochs), routing (message channel), and economic enforcement (collateral, bonds, disputes) +5. **Atomic cross-chain settlement** — no bridging overhead, no trusted intermediaries diff --git a/PLUGINS.md b/PLUGINS.md index 4fce634a4b..78a5bf0c58 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -31,7 +31,7 @@ documents what it does, how it works, and how to configure it. The plugin lifecy | `query_engine_plugin` | Opt-in SQL queries over the node's current chain state, answered by an in-process C++ service and, when HTTP is enabled, `POST /v1/query/execute`; runs only on a non-producing node in head or irreversible read mode | [README](plugins/query_engine_plugin/README.md) | | `resource_monitor_plugin` | Watches free space on every file system nodeop writes to and gracefully shuts the node down before it runs out | [README](plugins/resource_monitor_plugin/README.md) | | `signature_provider_kms_plugin` | Adds the `KMS:` signature-provider scheme, signing Ethereum keys remotely through AWS KMS so the key never leaves AWS | [README](plugins/signature_provider_kms_plugin/README.md) | -| `signature_provider_manager_plugin` | Owns every signing key the node uses, parsing each `--signature-provider` spec and publishing the providers to the chain, producer, net, outpost, and underwriter plugins | [README](plugins/signature_provider_manager_plugin/README.md) | +| `signature_provider_manager_plugin` | Owns every signing key the node uses, parsing each `--signature-provider` spec and publishing the providers to the chain, producer, net, and outpost plugins | [README](plugins/signature_provider_manager_plugin/README.md) | | `signature_provider_ssm_plugin` | Adds the `SSM:` signature-provider scheme, fetching a private key once from an AWS SSM Parameter Store SecureString and signing locally thereafter | [README](plugins/signature_provider_ssm_plugin/README.md) | | `snapshot_api_plugin` | Public, read-only HTTP endpoints for snapshot discovery and download, so nodes can bootstrap from each other | [README](plugins/snapshot_api_plugin/README.md) | | `state_history_plugin` | Records per-block traces, chain-state deltas, and finality data to append-only logs and streams them to clients over a binary websocket protocol | [README](plugins/state_history_plugin/README.md) | @@ -39,7 +39,6 @@ documents what it does, how it works, and how to configure it. The plugin lifecy | `test_control_api_plugin` | HTTP surface for `test_control_plugin`, registering the `/v1/test_control/*` write endpoints in their own `test_control` API category | [README](plugins/test_control_api_plugin/README.md) | | `test_control_plugin` | Test-only plugin that arms deliberate node misbehavior: shut down inside a named producer's round, throw from a controller signal handler, or republish the next block with one action swapped | [README](plugins/test_control_plugin/README.md) | | `trace_api_plugin` | Full-history action trace store with HTTP endpoints for querying traces, transactions, actions, and token transfers | [trace_api_plugin.md](plugins/trace_api_plugin/trace_api_plugin.md) | -| `underwriter_plugin` | Autonomous underwriter daemon that polls pending swaps and submits signed underwrite-intent commits for the outpost legs that still lack one | [README](plugins/underwriter_plugin/README.md) | | `wallet_api_plugin` | Publishes the `kiod` key store as the `/v1/wallet/*` HTTP endpoints that `clio` and a `KIOD:` signature provider call. Linked and initialized by `kiod`, not `nodeop` | [README](plugins/wallet_api_plugin/README.md) | | `wallet_plugin` | The `kiod` key store: a locked directory of AES-encrypted wallet files with inactivity auto-lock and transaction and digest signing. Linked and initialized by `kiod`, not `nodeop` | [README](plugins/wallet_plugin/README.md) | diff --git a/contracts/sysio.chalg/README.md b/contracts/sysio.chalg/README.md index 89a10a1c91..4131b70e4f 100644 --- a/contracts/sysio.chalg/README.md +++ b/contracts/sysio.chalg/README.md @@ -53,6 +53,7 @@ OPP envelope dispute resolution and slash-execution contract. - Triggered by `sysio.msgch::evalcons`; dispatches the winning envelope via `sysio.msgch::resolvedisp` - Tier-1 electorate snapshot from `sysio.roa::nodeowners`; the active `network_gen` is read through the shared canonical ROA accessor -- Slashes via `sysio.opreg::slash` (opreg routes the unlocked bond to the matching LP and defers the - locked portion through `sysio.uwrit::release`) +- Slashes via `sysio.opreg::slash`: opreg marks the operator `SLASHED` and removes its whole + depot-native collateral balance from the operator's row; the slashed tokens stay in + `sysio.opreg`'s custody - Pauses / unpauses via `sysio.epoch::pause` / `sysio.epoch::unpause` diff --git a/contracts/sysio.epoch/README.md b/contracts/sysio.epoch/README.md index 803acc918a..011fbdec97 100644 --- a/contracts/sysio.epoch/README.md +++ b/contracts/sysio.epoch/README.md @@ -1,41 +1,58 @@ # sysio.epoch -Epoch lifecycle and batch operator scheduling contract for the OPP (Outpost Protocol) system. +Global epoch clock and batch operator scheduling contract for the OPP (Outpost Protocol) system. ## Responsibility -- Manages the synthetic epoch clock (default: 6-minute epochs) -- Maintains the batch operator roster (registration, warmup, cooldown, blacklist) -- Assigns operators into 3 rotation groups of 7 via `schbatchgps` -- Advances epochs with deterministic group rotation (`epoch_index % 3`) -- Supports in-place operator replacement when an operator is slashed or deregistered -- Registers external chain outposts (ETH, SOL, etc.) -- Provides global pause/unpause (controlled by `sysio.chalg`) +- Owns the single global epoch clock (`epoch_duration_sec`, default 360 s, bounded to 60 s .. 30 days) +- Builds the initial batch operator schedule with `schbatchgps` and maintains it as a sliding window + of `batch_op_groups` groups of `operators_per_epoch` members; the group on duty is always the front + of the window +- Advances epochs (`advance`), crank-driven by `sysio.msgch::chkcons` after genesis +- Gates every advance on emissions readiness: if `sysio.system` cannot pay the epoch, the advance is + recorded in `blocklog` and skipped (never thrown), and retried on the next `chkcons` +- Provides the global pause/unpause used by `sysio.chalg` while an envelope dispute is open + +Operator registration, collateral and status live on `sysio.opreg`; the outpost set lives on +`sysio.chains`. This contract reads both and mirrors neither. ## Tables | Table | Type | Description | |-------|------|-------------| -| `epochcfg` | Singleton | Epoch duration, group count, warmup/cooldown config | -| `epochstate` | Singleton | Current epoch index, active group, 3 groups of 7, pause flag | -| `operators` | Multi-index | Operator roster with type, status, collateral, group assignment | -| `outposts` | Multi-index | Registered external chain outposts | +| `epochcfg` | `kv::global` | `epoch_duration_sec`, `operators_per_epoch` (default 7), `batch_operator_minimum_active` (default 21), `batch_op_groups` (default 3), `epoch_retention_envelope_log_count` (default 200) | +| `epochstate` | `kv::global` | Current epoch index, current / next epoch start, active group cursor, the resident group window, last consensus hash, pause flag | +| `blocklog` | `kv::table` | One row per epoch the emissions gate blocked: reason, attempted emission, treasury and balance figures, first/last retry and retry count. Pruned when the gate passes | ## Actions | Action | Auth | Description | |--------|------|-------------| -| `setconfig` | `sysio.epoch` | Set epoch configuration | -| `regoperator` | `sysio.epoch` | Register an operator | -| `unregoper` | operator | Begin deregistration (cooldown) | -| `advance` | permissionless | Advance epoch if duration elapsed | -| `schbatchgps` | `sysio.epoch` | One-time group assignment (21 operators) | -| `replaceop` | `sysio.epoch` | Replace operator in-place within group | -| `regoutpost` | `sysio.epoch` | Register an outpost chain | -| `pause` | `sysio.chalg` | Set global pause | -| `unpause` | `sysio.chalg` | Clear global pause | +| `setconfig` | `sysio.epoch` | Set the epoch configuration. Requires `operators_per_epoch * batch_op_groups == batch_operator_minimum_active` and enforces the size caps (`MAX_OPERATORS_PER_EPOCH` 100, `MAX_BATCH_OP_GROUPS` 255, `MAX_SCHEDULED_BATCH_OPERATORS` 1000) | +| `schbatchgps` | `sysio.epoch` | Build the initial window from the `ACTIVE` batch operators on `sysio.opreg` (non-bootstrapped first, then bootstrapped, trimmed to `batch_operator_minimum_active`) | +| `advance` | permissionless at genesis (epoch 0 → 1); afterwards `sysio.msgch` or `sysio.epoch` | Advance the epoch once the wall-clock boundary has passed and the contract is not paused | +| `pause` | `sysio.chalg` | Set the global pause | +| `unpause` | `sysio.chalg` | Clear the global pause | + +## What `advance` does + +1. Emissions readiness gate (block-and-retry via `blocklog`, never a throw). +2. Delivery audit of the expiring group for every active outpost: every operator that delivered a + non-canonical envelope is slashed through `sysio.chalg::slashop`; each member's delivery result is + recorded with `sysio.opreg::recorddel`, followed by `sysio.opreg::termcheck`. +3. Bump the epoch index, drop the expired group from the front of the window and append a new tail + group drawn from `ACTIVE` batch operators that are not already resident (non-bootstrapped first). + The resident groups stay disjoint, so an operator serves at most once every `batch_op_groups` + epochs. +4. `sysio.opreg::flushwtdw` matures queued collateral withdrawals. +5. Queue `OPERATORS` and `BATCH_OPERATOR_GROUPS` to every active outpost through + `sysio.msgch::queueout` (an empty group is withheld), then `sysio.msgch::buildenv` per outpost. +6. `sysio.system::accrueepoch` and `rcrdbatch`, plus `payepoch` on pay epochs. ## Dependencies -- Notifies `sysio.msgch` on epoch advancement +- Advanced by `sysio.msgch` (`chkcons`, and `bootstrap` at epoch 0) +- Reads operators from `sysio.opreg`, authex links from `sysio.authex`, outposts from `sysio.chains` +- Calls `sysio.chalg::slashop`, `sysio.opreg::{recorddel,termcheck,flushwtdw}`, + `sysio.msgch::{queueout,buildenv}` and `sysio.system::{accrueepoch,rcrdbatch,payepoch}` - Pause/unpause controlled by `sysio.chalg` diff --git a/contracts/sysio.msgch/README.md b/contracts/sysio.msgch/README.md index fd0de66419..2c93121160 100644 --- a/contracts/sysio.msgch/README.md +++ b/contracts/sysio.msgch/README.md @@ -4,43 +4,65 @@ Inbound/outbound OPP message chain management and consensus tracking contract. ## Responsibility -- Manages inbound chain requests from external outposts -- Collects deliveries from all 7 elected batch operators per epoch -- Evaluates consensus (Option A: unanimous 7/7, Option B: 4+ majority at epoch boundary) -- Stores and routes individual messages by attestation type -- Queues outbound messages and builds OPP envelopes for delivery to outposts +- Accepts inbound OPP envelopes delivered by the batch operators of the active group, one delivery per + operator per outpost per epoch (`MAX_ENVELOPE_BYTES` = 32,768; larger deliveries are rejected) +- Evaluates consensus per (outpost, epoch): every eligible operator delivers an identical envelope + (fast path), or a strict majority of the group agrees once the epoch boundary has passed. A + post-boundary split with no strict majority opens a `sysio.chalg` dispute +- Validates the per-outpost envelope chain and message sequence, stores the accepted messages and + attestations, and routes each attestation to its depot handler +- Queues outbound attestations and packs them into one OPP envelope per outpost per epoch; whatever + does not fit under the envelope cap stays queued for the next epoch +- Drives the epoch: `chkcons` advances `sysio.epoch` once every active outpost has reached + consensus and the wall-clock boundary has passed ## Tables | Table | Type | Description | |-------|------|-------------| -| `inchainreq` | Multi-index | Inbound chain requests per outpost/epoch | -| `deliveries` | Multi-index | Per-operator delivery records with chain hashes | -| `messages` | Multi-index | Individual OPP messages (inbound + outbound) | -| `outenvelopes` | Multi-index | Built outbound envelopes pending delivery | +| `envelopes` | `kv::table` | Inbound deliveries, one row per batch operator per outpost per epoch (working state, pruned to the current and previous epoch) | +| `messages` | `kv::table` | Individual OPP messages extracted from consensus-verified envelopes | +| `attestations` | `kv::table` | Attestations extracted inbound, and outbound attestations queued (`READY`) for envelope packing | +| `outenvelopes` | `kv::table` | Built outbound envelopes awaiting relay, with their canonical epoch digest | +| `outpcons` | `kv::table` | Per-outpost consensus record: accepted envelope digest and inbound message-chain tip | +| `attseq` | `kv::table` | Attestation sequence counter | +| `envlog` | `kv::table` | Metadata-only envelope audit log, capped at `active_outposts * 2 * epoch_retention_envelope_log_count` rows | ## Actions | Action | Auth | Description | |--------|------|-------------| -| `crank` | permissionless | Main depot crank, advances processing | -| `createreq` | `sysio.msgch` | Create inbound chain request for an outpost | -| `deliver` | operator | Batch operator delivers a chain with hash + messages | -| `evalcons` | `sysio.msgch` | Evaluate consensus on a chain request | -| `processmsg` | `sysio.msgch` | Process a READY message (unpack, route attestations) | -| `queueout` | `sysio.epoch`, `sysio.opreg`, `sysio.uwrit`, `sysio.reserv`, `sysio.synd`, or `sysio.msgch` | Queue an outbound message to an outpost | -| `buildenv` | `sysio.msgch` | Build outbound envelope from queued messages | +| `bootstrap` | `sysio.msgch` | Epoch-0 only: asserts the `sysio.system` emissions config is set, then triggers the first `sysio.epoch::advance` | +| `deliver` | batch operator (resident active group, `ACTIVE` in `sysio.opreg`) | Deliver an inbound envelope for an outpost; calls `evalcons` inline | +| `evalcons` | `sysio.msgch` | Evaluate consensus for an (outpost, epoch); on consensus store and dispatch the attestations; on an eligible split open a `sysio.chalg` dispute | +| `chkcons` | permissionless | If no dispute is open, every active outpost has consensus and the boundary has passed, call `sysio.epoch::advance` | +| `resolvedisp` | `sysio.chalg` | Dispatch the winning envelope of a resolved dispute | +| `queueout` | `sysio.epoch`, `sysio.opreg`, `sysio.synd`, or `sysio.msgch` | Queue an outbound attestation for an outpost | +| `buildenv` | `sysio.epoch` | Pack `READY` attestations into the outpost's outbound envelope for the epoch | + +## Attestation routing + +Inbound (outpost → depot): + +- `SYNDICATE_LIQ` → `sysio.synd::onsynd`, `LIQ_YIELD` → `sysio.synd::onyield`. Each carries the + envelope's epoch and canonical digest, and is forwarded only once the payload's token is an active + `TOKEN_KIND_LIQ` row on `sysio.tokens` bound to the proven outpost and, for a syndication, the + user's key family is the outpost's own. After the envelope's last attestation it sends + `sysio.synd::closeenv`, only when at least one of them reached `sysio.synd`. +- `NODE_OWNER_REG` → `sysio.roa` (account creation and node-owner registration, recording the + outpost link). +- `STAKING_REWARD` → `sysio.dclaim::onreward`, after binding the reward's chain to the proven + delivering outpost. +- Every other type is dropped. Every refusal is a logged drop, never an abort. + +Outbound (depot → outpost), queued through `queueout`: + +- `OPERATORS` and `BATCH_OPERATOR_GROUPS` from `sysio.epoch::advance`, to every active outpost +- `DESYNDICATE_LIQ` from `sysio.synd::desyndicate` ## Dependencies -- Reads epoch state from `sysio.epoch` -- Notifies `sysio.chalg` on consensus failure -- Routes attestations to `sysio.epoch`, `sysio.uwrit`, `sysio.chalg` -- Routes `SYNDICATE_LIQ` and `LIQ_YIELD` to `sysio.synd` (`onsynd` for a syndication, - linked user or not, `onyield` for a yield report), each carrying the envelope's epoch and - canonical digest, once the payload's token is an active `TOKEN_KIND_LIQ` row on - `sysio.tokens` bound to the proven outpost and, for a syndication, the user's key family - is the outpost's own. After the envelope's last attestation it sends `sysio.synd::closeenv`, - only when at least one of them reached `sysio.synd`. Every refusal is a logged drop, never - an abort. `DESYNDICATE_LIQ` is outbound only: - `sysio.synd::desyndicate` queues it through `queueout`. +- Reads epoch state from `sysio.epoch` and operator status from `sysio.opreg` +- Opens disputes on `sysio.chalg` (`opendispute`); `sysio.chalg::chkdispute` calls back into `resolvedisp` +- Triggers `sysio.epoch::advance` from `chkcons` (and from `bootstrap` at epoch 0) +- Routes inbound attestations to `sysio.synd`, `sysio.roa` and `sysio.dclaim` diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 3c01a35222..dacb318981 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -827,14 +827,14 @@ void dispatch_node_owner_reg(const std::vector& data, uint64_t chain_code) // task alongside liqEth / liqsol-token wiring. break; - // Outbound-only types (depot emits these, never receives them inbound) - // and deprecated pre-launch types are dropped silently. SLASH was - // formerly its own attestation type; it now rides on OPERATOR_ACTION - // with action_type=SLASH and is gated inside `dispatch_operator_action`. + // Outbound-only types (depot emits these, never receives them inbound) are dropped silently; + // an outpost relaying one back is a benign no-op. case AttestationType::ATTESTATION_TYPE_SWAP_REVERT: case AttestationType::ATTESTATION_TYPE_DEPOSIT_REVERT: case AttestationType::ATTESTATION_TYPE_OPERATORS: case AttestationType::ATTESTATION_TYPE_BATCH_OPERATOR_GROUPS: + break; + case AttestationType::ATTESTATION_TYPE_NODE_OWNER_REG: // NFT node-owner claim: create the account + register + record the ETH link. Self-contained // (decodes NodeOwnerRegistration, inline-sends sysio.roa); soft-drops a malformed envelope. diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index 42e3a7677a..f7ab14b023 100755 Binary files a/contracts/sysio.msgch/sysio.msgch.wasm and b/contracts/sysio.msgch/sysio.msgch.wasm differ diff --git a/contracts/sysio.system/EMISSIONS.md b/contracts/sysio.system/EMISSIONS.md index 9f17581828..ddddb3ec8c 100644 --- a/contracts/sysio.system/EMISSIONS.md +++ b/contracts/sysio.system/EMISSIONS.md @@ -53,8 +53,7 @@ the CATEGORY, not the trace: carrying neither category. And `epochlog` does not recover the split either. It records top-level PERIOD aggregates -— `total_emission`, `compute_amount`, `capex_amount`, `governance_amount`, -`fee_distributed` — where `compute_amount` is the COMBINED producer + batch-operator +— `total_emission`, `compute_amount`, `capex_amount`, `governance_amount` — where `compute_amount` is the COMBINED producer + batch-operator pool. Neither operator category's actually-credited total is stored, so once both kinds of credit accumulate in one `payclaims` row, **the producer-versus-batch-operator split is not available anywhere on chain**. What you can attribute: period totals per top-level @@ -79,21 +78,16 @@ by the full group size). Equal rosters are coalesced before their weighted slice is calculated, preserving one group-level rounding step per roster. A credit is made only for members that are opreg-ACTIVE, so the slices of skipped (inactive / slashed / terminated) members stay in the treasury rather than being redistributed -to the active ones. When roster history is complete and includes at least one -non-empty roster, swap-fee rewards from `sysio.reserv`'s `rewards_bucket` are -swept in (`drainrewards`) and allocated -**exclusively to the batch-operator -distribution**, on top of their emission share and weighted by that same -historical active-epoch count. Producers are not paid out of swap fees, so -`producer_bps` / `batch_op_bps` govern the emission split only. +to the active ones. Batch operators are paid from emission only; no swap-fee +pool is folded into the distribution, so `producer_bps` / `batch_op_bps` split +the whole compute share. Allocated is not the same as paid: only **eligible** shares are actually credited. Emission WIRE stays in the treasury when an **empty historical roster** owns an accrued epoch, when a **member is not opreg-ACTIVE**, as the **remainder** of the two integer divisions (per-roster weighting, then the even per-member split), or when roster history is incomplete and the whole batch -emission slice takes the bounded recovery path. Incomplete or entirely empty -history leaves swap fees in `sysio.reserv` as described below. Every +emission slice takes the bounded recovery path. Every `batchepochs` row represents one accrued epoch, so a zero-epoch historical roster cannot arise. @@ -127,8 +121,7 @@ largest accepted period, so even a malformed gapped table cannot turn recovery into an unbounded transaction; stale history drains monotonically. If `payepoch` sees missing, stale, non-contiguous, or over-cap history, it does not guess a roster or abort the enclosing `advance`: it retains that period's -batch-emission slice in the treasury, leaves the swap-fee bucket in -`sysio.reserv` for the next complete period, drains the unusable history within +batch-emission slice in the treasury, drains the unusable history within the cleanup bound, and begins clean history after stale rows are gone. A complete history remains required to credit batch-operator rewards. Every `epochlog` row records whether history was complete and the exact batch-emission amount @@ -137,12 +130,9 @@ retained, making recovery distinguishable from an ordinary zero-eligible payout. `epoch_log_retention_count` counts payment rows, not elapsed epoch indexes, so cadence values greater than one retain the configured number of audit records. -`epochlog.fee_distributed` records what was actually paid, while -`batch_fee_retained` records a swept amount left in treasury because an empty -roster alongside a non-empty roster, an inactive member, or a division remainder -prevented its distribution. It is zero for incomplete or entirely empty history -because those fees remain in `sysio.reserv` for a later payable period. The -analogous `batch_emission_retained` field records undistributed batch emission. +`epochlog.batch_emission_retained` records undistributed batch emission. +`epochlog.fee_distributed` and `epochlog.batch_fee_retained` are reserved +fields that `payepoch` always writes as zero; no swap-fee sweep feeds them. ## Retrieved via a claim action (pulled by recipient) @@ -249,5 +239,4 @@ period_emission - Driven inline by `sysio.epoch::advance` (`accrueepoch` + `rcrdbatch` + `payepoch`). - Reads producer eligibility and operator status from `sysio.opreg`. - Reads the canonical epoch duration from `sysio.epoch::epochcfg`. -- Folds swap-fee rewards from `sysio.reserv` (`drainrewards`). - Funds `sysio.dclaim` (staking rewards) and `sysio.liq` (the yield kicker) on demand via `fundclaim`. diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 20f3c5fa07..6f54e5f4aa 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1033,6 +1033,36 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_node_owner_reg_to_roa, sysio_dispatch_te BOOST_REQUIRE_EQUAL(pending["balance"].as().get_amount(), 4321); } FC_LOG_AND_RETHROW() } +// Only ATTESTATION_TYPE_NODE_OWNER_REG reaches the node-owner handler. The outbound-only types +// (OPERATORS, BATCH_OPERATOR_GROUPS, SWAP_REVERT, DEPOSIT_REVERT) are dropped on receipt even when +// the payload is a well-formed NodeOwnerRegistration from the node-owner source outpost: a payload +// that would register CLAIM_ACCOUNT under the real type must leave no owner row and no audit row. +BOOST_FIXTURE_TEST_CASE(outbound_only_types_never_reach_node_owner_dispatch, sysio_dispatch_tester) { try { + bootstrap_for_dispatch("ETHEREUM"); + + const auto eth_code = fc::slug_name{"ETHEREUM"}.value; + auto wire_key = k1_pubkey_bytes(get_public_key(CLAIM_ACCOUNT, "active")); + auto eth_pub = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::em).get_public_key(); + auto eth_bytes = em_uncompressed_pubkey_bytes(eth_pub); + auto eth_address = fc::crypto::ethereum::address_to_bytes(eth_pub); + const auto payload = encode_node_owner_registration( + CLAIM_ACCOUNT.to_string(), /*tier=*/2, + sysio::opp::types::WIRE_KEY_TYPE_K1, wire_key, eth_bytes, eth_address); + + std::vector entries; + for (auto type : {ATTESTATION_TYPE_OPERATORS, ATTESTATION_TYPE_BATCH_OPERATOR_GROUPS, + ATTESTATION_TYPE_SWAP_REVERT, ATTESTATION_TYPE_DEPOSIT_REVERT}) { + entries.emplace_back(type, payload); + } + const auto trace = deliver_trace(eth_code, + encode_envelope_with_mixed_attestations(current_epoch(), entries)); + BOOST_REQUIRE(trace != nullptr); + BOOST_REQUIRE(!trace->except); + + BOOST_CHECK(get_nodeowner(CLAIM_ACCOUNT).is_null()); + BOOST_CHECK(get_nodeownerreg(CLAIM_ACCOUNT).is_null()); +} FC_LOG_AND_RETHROW() } + /// One slot and two fresh-name claims must commit the first, reject the second, and keep epochs moving. BOOST_FIXTURE_TEST_CASE(dispatch_node_owner_tier_cap_preserves_epoch_progress, sysio_dispatch_tester) { try { namespace owners = sysio_system::test_support::nodeowners; diff --git a/docs/becoming-a-block-producer.md b/docs/becoming-a-block-producer.md index ed6918cdd0..caf87df47a 100644 --- a/docs/becoming-a-block-producer.md +++ b/docs/becoming-a-block-producer.md @@ -9,21 +9,21 @@ There are two ways onto the schedule, and this guide covers the second: - **Genesis producers** are placed in the schedule when a chain is bootstrapped. They are registered as *bootstrapped* operators, which only the registry contract itself can do. -- **Collateral-backed producers** post a bond on the outpost chains and earn a schedule position - by rank. That is the open path, and it is the one described here. +- **Collateral-backed producers** post a bond in `sysio.opreg` on WIRE and earn a schedule + position by rank. That is the open path, and it is the one described here. ## What you need before you start | | | |---|---| | A WIRE account | The account that will produce blocks. Its `active` permission signs everything below. | -| An Ethereum wallet | Funded with the required bond plus gas. | -| A Solana keypair | Funded with the required bond plus fees. | +| An Ethereum key and a Solana key | Used only to link your WIRE account to each active outpost chain (step 1). No collateral is posted on those chains. | +| The required collateral, held on WIRE | The tokens `sysio.opreg`'s configuration lists, typically shadow LIQ (`LIQETH`, `LIQSOL`) and/or `WIRE`. | | A machine running `nodeop` | Reachable by the peer network, with your signing keys available to it. | -You must bond on **every** chain the network requires, not just one. The requirement lives in -`sysio.opreg`'s configuration as a per-chain minimum bond, and eligibility takes the **minimum** -across all of them. Posting extra on the cheapest chain buys you nothing. +You must bond **every** token the network requires, not just one. The requirement lives in +`sysio.opreg`'s configuration (`opconfig.req_prod_collat`) as a minimum per token, and +eligibility needs every one of them met. Posting extra of one token buys you nothing for another. ## Step 1 — Link your outpost addresses @@ -31,9 +31,9 @@ across all of them. Posting extra on the cheapest chain buys you nothing. sysio.authex::createlink(chain_kind, account, sig, pub_key, nonce) ``` -Sign this once per chain, with your Ethereum and Solana keys respectively. The link is what makes -a deposit you send on an outpost attributable to your WIRE account; without it the chain has no way -to know the bond is yours. +Sign this once per active outpost chain, with your Ethereum and Solana keys respectively. +`regoperator` refuses an account that lacks a link for any active outpost chain, and the links are +what the depot publishes for you in the `OPERATORS` roster it sends each outpost. The `nonce` is a millisecond timestamp and is rejected if it is more than ten minutes old, so generate it at signing time. @@ -51,24 +51,21 @@ the bootstrap process explicitly pushes those privileged `regoperator` actions a You are now registered but not yet eligible. Your status stays `UNKNOWN` until the bond arrives. -## Step 3 — Post your collateral on each outpost +## Step 3 — Post your collateral on WIRE -Deposit on the outpost chains themselves, signed by the wallets you linked in step 1: +Read `opconfig.req_prod_collat` for the tokens and minimums, hold those tokens on WIRE, and bond +each one: -- **Ethereum** — `OperatorRegistry.deposit(...)` -- **Solana** — the outpost program's `deposit` instruction - -Each deposit travels to WIRE over the cross-chain protocol and credits your balance in -`sysio.opreg`. +``` +sysio.opreg::deposit(account, token_code, amount) +``` -If the configured requirements include the native WIRE pair, separately call -`sysio.opreg::deposit(account, token_code, amount)` on WIRE with `token_code` `WIRE`, signed by -your WIRE account. This native action requires an existing non-bootstrapped operator row, -transfers the token inline, and credits only the matching WIRE-chain pair; it does not replace -the Ethereum or Solana outpost deposits. The same action bonds a shadow LIQ symbol such as -`LIQETH` or `LIQSOL` held on WIRE, one `token_code` per call. +Signed by your WIRE account, one `token_code` per call: `WIRE` or a shadow LIQ symbol such as +`LIQETH` or `LIQSOL`. The action requires an existing non-bootstrapped operator row, transfers the +token into `sysio.opreg` inline, and credits your balance. Collateral is held only on WIRE; +nothing is deposited on the outpost chains. -When every required chain is at or above its minimum, your operator status flips to `ACTIVE` on +When every required token is at or above its minimum, your operator status flips to `ACTIVE` on its own. No producer rank exists yet: the first score is written only after the producer row and active finalizer key are created in steps 4 and 5. @@ -176,7 +173,7 @@ arrive. Nobody has to vote them out, and there is no flag day. | Factor | What it measures | |---|---| -| Collateral | Your bond divided by the required minimum, taken as the **minimum** across every required chain. Linear and uncapped, so more collateral always outranks less. | +| Collateral | Your bond divided by the required minimum, taken as the **minimum** across every required token. Linear and uncapped, so more collateral always outranks less. | | Participation | Falls with each consecutive unserved round and recovers when you serve one. | | Snapshot service | Snapshot attestations that reached quorum in the current pay period. Weighted at a tenth of collateral, so it separates producers the bond has left tied rather than outranking a larger bond. | @@ -266,17 +263,11 @@ under it. It is not applied only to rounds missed from that point on. simply hold no schedule position. `regproducer` brings you back at the position your collateral earns after rechecking current admission; a minimum raised while you were parked requires a top-up first. -- **Withdraw** from the chain that holds the bond. An outpost bond is released through that - outpost's own withdrawal entry point, the counterpart of the deposit you made in step 3, which - travels to WIRE and settles against your registry balance. - `sysio.opreg::withdraw(account, token_code, amount)` is **not** that path: it applies only to - your WIRE-native balance in `token_code` (`WIRE` or a shadow LIQ symbol), so calling it for an - Ethereum or Solana bond fails for insufficient balance - and leaves the outpost collateral untouched. Once a WIRE-native withdrawal flushes, the funds - are credited to a per-token claim that you pull with `sysio.opreg::claimremit(account, - token_code)`. Either way the request is queued rather than immediate, and `cancelwtdw` - cancels it before it flushes. Once your balance falls below the minimum on any required chain you - leave `ACTIVE` and the schedule drops you at the next rebuild. +- **Withdraw** with `sysio.opreg::withdraw(account, token_code, amount)`. The request is queued + rather than immediate (`cancelwtdw` cancels it before it flushes); once it flushes at an epoch + advance, the funds are credited to a per-token claim that you pull with + `sysio.opreg::claimremit(account, token_code)`. Once your balance falls below the minimum for + any required token you leave `ACTIVE` and the schedule drops you at the next rebuild. - **Collect shadow yield.** Bonded shadow LIQ keeps earning WIRE yield: each bonded unit earns what one unit of that shadow token earns in `sysio.liq`, from the moment it is bonded until it leaves your balance. `sysio.opreg::claimyield(account, token_code)` credits what you have diff --git a/docs/contract-upgrade-order.md b/docs/contract-upgrade-order.md index 0c05505eb8..2a091600d5 100644 --- a/docs/contract-upgrade-order.md +++ b/docs/contract-upgrade-order.md @@ -1,7 +1,7 @@ # System-contract upgrade order The system contracts are not independent deployables. `sysio.epoch::advance` -inlines actions into six other contracts, and the emissions gate reads two more +inlines actions into four other contracts, and the emissions gate reads two contracts' tables, so a release's contract builds are only correct **as a set**. Upgrading them one at a time creates windows in which a new caller meets an old callee. @@ -16,22 +16,19 @@ Contracts outside the coupled set stage separately, in any order. ## The deployment recipe "Just `setcode` everything in one transaction" fails twice over — once on which -action deploys each account, once on transaction size — and both bite this -release specifically. +action deploys each account, once on transaction size. **1. The deploy action depends on WHICH account holds the contract — the transaction mixes both.** -*Separate system accounts* (`sysio.epoch`, `sysio.reserv`, `sysio.opreg`, -`sysio.uwrit`, …) deploy through **`sysio.roa::setsyscode(account, vmtype, -vmversion, code)` / `sysio.roa::setsysabi(account, abi)`**. Those set the +*Separate system accounts* (`sysio.epoch`, `sysio.opreg`, `sysio.msgch`, +`sysio.chalg`, `sysio.synd`, …) deploy through **`sysio.roa::setsyscode(account, +vmtype, vmversion, code)` / `sysio.roa::setsysabi(account, abi)`**. Those set the code/abi AND reconcile the account's gifted RAM to its exact new usage out of sysio's pool via `giftram`, measured after the write (re-callable: a smaller re-deploy reclaims). A raw `setcode` here skips that reconciliation and leaves -the account paying for the new size out of a finite quota it does not have — -not theoretical for this release, where `sysio.reserv.wasm` grows **6,040 -bytes** and `sysio.opreg.wasm` grows **2,447 bytes** against a system account's -small creation allowance. +the account paying for any growth out of a finite quota it does not have — a +system account only has a small creation allowance. *The root `sysio` account* — which holds `sysio.system` and **is the RAM pool itself** — deploys with the **native `setcode` / `setabi`**. `giftram` cannot @@ -45,37 +42,33 @@ lower than the actual quota, and it fails silently: sysio's limit is finite, so production bootstrap deploys `system` on `sysio` natively (the harness encodes the same split as `ContractSteps.DeployMode.raw` for bios/system/roa). -So the atomic transaction for this release's trio carries **native -`setcode`/`setabi` for `sysio`** plus **ROA `setsyscode`/`setsysabi` for -`sysio.epoch` and `sysio.reserv`**. +So an atomic transaction that includes `sysio.system` carries **native +`setcode`/`setabi` for `sysio`** plus **ROA `setsyscode`/`setsysabi` for every +other account in the set**. -**2. The whole release does not fit in one transaction.** The five changed -WASMs total **579,408 bytes**, already past the default -`max_transaction_net_usage` of **524,288** (`config::default_max_block_net_usage -/ 2`) before ABIs or action wrapping. `sysio.msig::propose` receives the -complete inner transaction before it chunks storage, so proposing through msig -does not dodge that input NET. - -What fits is the **compatibility-coupled trio** — `sysio.reserv`, -`sysio.epoch`, `sysio.system` — the three the edges below actually couple: +**2. A large set does not fit in one transaction.** The default +`max_transaction_net_usage` is **524,288** bytes (`config::default_max_block_net_usage +/ 2`), and `sysio.msig::propose` receives the complete inner transaction before it +chunks storage, so proposing through msig does not dodge that input NET. The +contracts on `advance`'s path are not small (sizes of the builds in this tree): | | code | abi | total | |---|---|---|---| -| `sysio.epoch` | 75,579 | 6,758 | 82,337 | -| `sysio.system` | 174,138 | 61,916 | 236,054 | -| `sysio.reserv` | 84,026 | 24,647 | 108,673 | -| **trio** | **333,743** | **93,321** | **427,064** | - -That leaves ~97 KB against the 524,288 ceiling — enough, but not by so much -that it can be assumed. **Preflight the packed size of the actual proposal** -rather than trusting this table, which is a snapshot of one release. - -`sysio.opreg` and `sysio.uwrit` are uncoupled (they gain no action another -contract inlines, and no other contract reads their new tables), so they stage -in their own transaction, before or after the trio. - -When a future release's coupled set does NOT fit, the options are: split off -whatever is genuinely uncoupled and stage it, or raise +| `sysio.epoch` | 95,314 | 6,758 | 102,072 | +| `sysio.system` | 277,387 | 66,071 | 343,458 | +| `sysio.opreg` | 115,210 | 27,979 | 143,189 | +| `sysio.msgch` | 177,916 | 21,421 | 199,337 | +| `sysio.chalg` | 32,009 | 6,797 | 38,806 | + +All five together exceed the ceiling, so a release that changes every one of +them cannot deploy them atomically. Put in the atomic set only the contracts a +release's changes actually couple (see [The cross-contract +edges](#the-cross-contract-edges)), and **preflight the packed size of the actual +proposal** rather than trusting this table, which is a snapshot of one build. + +When a release's coupled set does NOT fit, the options are: split off whatever is +genuinely uncoupled and stage it (see [Staged +rollout](#staged-rollout-when-one-transaction-is-not-possible)), or raise `max_transaction_net_usage` via `setparams` first — as a **tested** path, with the raise proposed and confirmed before the deploy proposal, never assumed to work on the day. @@ -121,17 +114,18 @@ Everything `sysio.epoch::advance` inlines, directly: | Callee | Actions | |---|---| -| `sysio.reserv` | `sweepclaims` | -| `sysio.uwrit` | `chklocks`, `pruneuwreqs`, `drainfwq` | | `sysio.opreg` | `recorddel`, `termcheck`, `flushwtdw` | | `sysio.chalg` | `slashop` | | `sysio.msgch` | `queueout`, `buildenv` | | `sysio` | `accrueepoch`, `rcrdbatch`, `payepoch` | -Those callees inline further (`drainfwq` → `sysio.reserv::refundwire`, -`termcheck` → the `sysio.opreg` remit path, `payepoch` → `sysio.token::transfer`), -so the transitive subtree — not just the table above — sits inside `advance`'s -abort surface. +Those callees inline further (`slashop` → `sysio.opreg::slash`, `termcheck` → the +`sysio.opreg` termination path that credits `remitclaims`, `payepoch` → +`sysio.token::transfer` for the pushed capex and governance buckets), so the +transitive subtree — not just the table above — sits inside `advance`'s abort +surface. `advance` itself is inlined by `sysio.msgch::chkcons` (and by +`sysio.msgch::bootstrap` at epoch 0), so `sysio.msgch` is both a caller and a +callee of `sysio.epoch`. Independently of inlines, the emissions readiness gate in `sysio.epoch` **reads** `sysio.system`'s `emitcfg`, `t5state` and `payclaimtot`, and `sysio.token`'s @@ -201,8 +195,7 @@ mixed or mid-period deployment. `rcrdbatch` prunes the exact oldest retained roster, and `payepoch` removes at most 20 history rows per payment, so even a malformed gapped table cannot turn cleanup into an unbounded `advance`. If `payepoch` sees missing, stale, non-contiguous, or over-cap history, it -retains that period's batch-emission slice in the treasury, leaves the swap-fee -bucket in `sysio.reserv` for the next complete period, records the retained +retains that period's batch-emission slice in the treasury, records the retained emission and incomplete-history status in `epochlog`, and drains stale history monotonically before resuming batch payouts. Producer, capex, and governance processing still completes. The runtime also shortens a legacy cadence when @@ -236,28 +229,25 @@ state that aborts every advance. ## Staged rollout (when one transaction is not possible) -For the SEC-150 claimable-payout release the order is: - -``` -sysio.reserv -> sysio.epoch -> sysio.system -``` - -`sysio.opreg` and `sysio.uwrit` are free to land anywhere in the sequence: they -gain no action any other contract inlines (`claimremit` is user-initiated), and -no other contract reads their new tables. +Derive the order from the two rules above for the edges a release actually +changes. For the edges on `advance`'s path today: | Edge | Why | |---|---| -| `sysio.reserv` before `sysio.epoch` | The new `advance` inlines `sysio.reserv::sweepclaims`, guarded only on the account existing. An old `sysio.reserv` build has the account and not the action, so the inline asserts and every advance aborts. | -| `sysio.epoch` before `sysio.system` | The new `payepoch` retains WIRE in `payclaims` and reserves it in `payclaimtot`. The old gate counts that backing as spendable, so a later pay period can double-commit it and leave credited claims underfunded. | +| `sysio.opreg`, `sysio.chalg`, `sysio.msgch` before `sysio.epoch`, when the release adds or changes an action `advance` inlines into them | Call edge: the new `advance` would otherwise reach a callee that cannot dispatch the action, and every advance aborts. | +| `sysio` (`sysio.system`) before `sysio.epoch`, when the release adds or changes `accrueepoch` / `rcrdbatch` / `payepoch` | Call edge — but an action `sysio.system` does not implement is silently ignored rather than asserted, so this skew drops the effect instead of stalling. | +| `sysio.epoch` before `sysio.system`, when `sysio.system` gains state the emissions gate must reserve (as `payclaimtot` reserves unclaimed `payclaims`) | Table edge: the old gate counts the new backing as spendable, so a later pay period can double-commit it and leave credited claims underfunded. | +| `sysio.epoch` before `sysio.msgch`, when the release adds or changes an action `chkcons` inlines into `sysio.epoch` | Call edge in the other direction. | + +When a release changes both directions of one pair, the two contracts are a +compatibility-coupled set and must deploy in one transaction; if they do not fit, +make the change layout-compatible so that either intermediate state is safe, or +raise `max_transaction_net_usage` first. -Every intermediate state of that order is safe. A new `sysio.reserv` under an old -`sysio.epoch` is simply never asked to sweep — the retention deadline then rests -on `credit_wire_claim`'s opportunistic sweep until epoch catches up. A new -`sysio.epoch` under an old `sysio.system` reads a `payclaimtot` whose KV key does -not exist yet, so `get_or_default` yields a zero reserve — the correct answer -while nothing is credited, and the absent-key case rather than the -short-decode one (see [above](#why-a-mixed-version-is-not-merely-degraded)). +A reader that meets a KV key the new writer has not created yet reads the +default from `get_or_default` — the correct answer while nothing is written, and +the absent-key case rather than the short-decode one (see +[above](#why-a-mixed-version-is-not-merely-degraded)). ## Downgrades @@ -271,13 +261,12 @@ short-decode one (see [above](#why-a-mixed-version-is-not-merely-degraded)). **A downgrade is not the upgrade run backwards. It is a data-migration problem first, and a code-ordering problem second** — because by the time you want to -roll back, all three claim tables may hold value that only the NEW code can pay -out: +roll back, the claim tables may hold value that only the NEW code can pay out: | Table | Contract | Paid out by | Stranded when that contract rolls back | |---|---|---|---| | `payclaims` | `sysio.system` | `claimpay` | earned epoch pay | -| `wireclaims` | `sysio.reserv` | `claimwire` | swap payouts + refunds already withheld from recipients | +| `wireclaims` | `sysio.bond` | `claimwire` | WIRE yield earned by underwriters and banked at `claim` | | `remitclaims` | `sysio.opreg` | `claimremit` | debited operator collateral | Every one of those balances is value already taken from someone's spendable @@ -292,14 +281,14 @@ resumes double-committing while the pile of unreachable claims grows. The safe procedure is therefore: 1. **Quiesce the credit writers** so no new claim rows appear. -2. **Drain or migrate all three claim tables** — claimants pull, or the balances +2. **Drain or migrate the claim tables** — claimants pull, or the balances are migrated. This step is the one that actually gates the rollback, and it cannot be completed unilaterally: a claimant who never claims holds it open. -3. **Roll back the coupled trio in the order `sysio.system` → `sysio.epoch` → - `sysio.reserv`** — the mirror of the upgrade order, so the writer is retired - before the reader that accounts for it, and the caller before the callee it - would otherwise inline into. -4. **Roll `sysio.opreg` back only once its remits are handled.** +3. **Roll back the coupled set in the mirror of the upgrade order** — the writer + is retired before the reader that accounts for it, and the caller before the + callee it would otherwise inline into. +4. **Roll `sysio.opreg` back only once its remits are handled, and `sysio.bond` + only once its `wireclaims` are drained.** **A live chain with uncooperative claimants is not safely downgradeable** by code order alone. If a rollback has to happen anyway, the outstanding balances diff --git a/docs/emergency-stop-playbook.md b/docs/emergency-stop-playbook.md index c86720edce..b8d10847e4 100644 --- a/docs/emergency-stop-playbook.md +++ b/docs/emergency-stop-playbook.md @@ -14,7 +14,7 @@ chain. The contracts behind it are described in [`sysio-synd.md`](sysio-synd.md) |---|---|---|---| | Depot | `sysio.andon` `cord` row, `pulled` | `sysio.andon::pull`: the panic account, `sysio`, or a registered puller | `sysio.andon::clear`: the panic account or `sysio` | | Solana | liqsol-core `GlobalState.frozen` | `set_frozen(true)`: `GlobalConfig.panic` or `GlobalConfig.admin` | `set_frozen(false)`: the same two | -| Ethereum | OpenZeppelin `paused()` on `SyndicationPool`, `ReserveManager`, `LiqEthToken`, `StakingModule` | `pause()`: any holder of the `panic` role (the deploy's `panicAccount` and the admin) | `unpause()`: the same | +| Ethereum | OpenZeppelin `paused()` on `SyndicationPool`, `LiqEthToken`, `StakingModule` | `pause()`: any holder of the `panic` role (the deploy's `panicAccount` and the admin) | `unpause()`: the same | The three are independent: pulling one changes nothing on the others. When unsure whether a fault crossed chains, pull all three. @@ -33,7 +33,7 @@ playbook; they do not change between incidents unless an account is replaced. | `SOL_ADMIN_KEYPAIR` | keypair file of the Solana admin (`GlobalConfig.admin`) | | `SOL_CRANK_KEYPAIR` | any funded Solana keypair: it signs `pay_pending_desyndication` | | `ETH_RPC_URL` | Ethereum RPC URL of the outpost's network | -| `SYNDICATION_POOL`, `RESERVE_MANAGER`, `OPP_INBOUND`, `OUTPOST_AUTHORITY`, `OUTPOST_MANAGER` | the `SyndicationPool`, `ReserveManager`, `OPPInbound`, `OutpostManagerAuthority` and `OutpostManager` entries of the outpost deploy's `addressFile` | +| `SYNDICATION_POOL`, `OPP_INBOUND`, `OUTPOST_AUTHORITY`, `OUTPOST_MANAGER` | the `SyndicationPool`, `OPPInbound`, `OutpostManagerAuthority` and `OutpostManager` entries of the outpost deploy's `addressFile` | | `LIQETH_TOKEN`, `STAKING_MODULE`, `LIQETH_AUTHORITY` | the `LiqEthToken`, `StakingModule` and `LiqEthAuthority` entries of the liqEth deploy's `addressFile` | A few values belong to the incident itself and are set when it happens: `REASON` (why the cord was @@ -79,9 +79,11 @@ Each item is a deploy or upgrade step. An item not done is a freeze that cannot (`set-panic`, [Solana commands](#solana-commands)) and read it back with `status`. 3. **Ethereum.** Set `panicAccount` in the deploy (or upgrade) config. A fresh deploy maps `pause` and `unpause` to the `panic` role from `permissions/outpost.yaml` and `permissions/liqeth.yaml` and - grants the role to `panicAccount` and to the applying admin. An environment deployed earlier gets - the mapping from the permissions-only steps `outpost_20260930.01_permissions` and - `liqeth_20260930.01_permissions` (wire-ethereum `docs/deploy.md`, Rolling the panic role out). + grants the role to `panicAccount` and to the applying admin. On the outpost only `SyndicationPool` + is pausable, and since it is not deployed anywhere yet its mapping always comes from + `permissions/outpost.yaml`. A liqEth environment deployed earlier gets the mapping from the + permissions-only step `liqeth_20260930.01_permissions` (wire-ethereum `docs/deploy.md`, Rolling the + panic role out). 4. **Solana relay funding.** Fund the Solana key each `outpost_solana_client` relay signs `dispatch_attestations` with (the public key in its `--signature-provider`, logged at start as `Signer public key:`) for the rent of the payouts the outpost may store: on a freeze, a custody @@ -114,7 +116,8 @@ collateral and the other depot payouts are a separate concern and are not frozen - `sysio.opreg::claimremit(account, token_code)` for a shadow LIQ `token_code` pays through `sysio.liq::transfer` to the operator, which is not a custody contract, so it IS refused while the cord is pulled; the `remitclaims` row stays and is claimed after the clear. -- The WIRE payouts of `sysio.chalg`, `sysio.uwrit` and `sysio.reserv` read no cord and run. +- The WIRE payouts of `sysio.system` (`claimpay`, `claimnodedis`) and `sysio.dclaim` (`claim`) read no + cord and run. If an incident involves one of these, the andon cord does not stop it; the owning contract's own controls (or a contract redeploy) do. @@ -143,7 +146,6 @@ the outpost have diverged. The relay skips the yield report of a frozen outpost | Contract | `pause()` stops | Keeps running | |---|---|---| | `SyndicationPool` | `syndicate` and `realizeYield` (`EnforcedPause`); a depot `DESYNDICATE_LIQ` is stored as a pending desyndication (`DesyndicationDeferred`, reason `OUTPOST_FROZEN`); `payPendingDesyndication` | OPP delivery; views | -| `ReserveManager` | `create_reserve`, `requestReserveCreateErc20WithPermit`, `requestReserveCreateErc20WithApproval`, `requestSwap`, `requestSwapErc20WithPermit`, `requestSwapErc20WithApproval`, `withdraw` | OPP delivery and attestation handling, including `SWAP_REMIT` and `SWAP_REVERT` payouts. While `LiqEthToken` is paused a liqETH payout fails: a `SWAP_REMIT` is skipped with `SwapRemitUnpayable` ("transfer failed") and a `SWAP_REVERT` refund with `SwapRevertError`. The depot counts both as paid. | | `LiqEthToken` | every liqETH transfer, mint and burn | views | | `StakingModule` | `batchDeposit`, `topUpValidator` | the rest | @@ -164,9 +166,8 @@ pauses itself and stores the release with reason `CUSTODY_SHORTFALL` `OUTPOST_FROZEN` for the whole freeze instead of attempting transfers against a paused token. **Pause `LiqEthToken` only when the token itself is at fault.** A paused liqETH blocks every transfer, -so `ReserveManager` cannot pay a liqETH `SWAP_REMIT` or `SWAP_REVERT` that arrives during the pause. It -skips the payout and logs it (`SwapRemitUnpayable`, `SwapRevertError`), while the depot counts it as -paid. Each one found in step 9 is reconciled by hand. +mint and burn, including the liqETH staking flows; desyndications arriving while only the token is +paused are stored as `SETTLEMENT_REFUSED` and paid by `payPendingDesyndication` after the unpause. ## Pulling the cord @@ -197,9 +198,8 @@ Pull `SyndicationPool` first: ```bash cast send --rpc-url "$ETH_RPC_URL" --account panic "$SYNDICATION_POOL" 'pause()' -cast send --rpc-url "$ETH_RPC_URL" --account panic "$RESERVE_MANAGER" 'pause()' cast send --rpc-url "$ETH_RPC_URL" --account panic "$STAKING_MODULE" 'pause()' -for c in "$SYNDICATION_POOL" "$RESERVE_MANAGER" "$STAKING_MODULE"; do +for c in "$SYNDICATION_POOL" "$STAKING_MODULE"; do cast call --rpc-url "$ETH_RPC_URL" "$c" 'paused()(bool)' done ``` @@ -274,8 +274,8 @@ share wait for the clear. `revokeRole` call to the authority. The liqEth side has no handoff, so its admin calls `LiqEthAuthority` directly: ```bash - # Outpost access manager (ReserveManager, SyndicationPool), through OutpostManager - ROLE=$(cast call --rpc-url "$ETH_RPC_URL" "$OUTPOST_AUTHORITY" 'getTargetFunctionRole(address,bytes4)(uint64)' "$RESERVE_MANAGER" "$(cast sig 'pause()')") + # Outpost access manager (SyndicationPool), through OutpostManager + ROLE=$(cast call --rpc-url "$ETH_RPC_URL" "$OUTPOST_AUTHORITY" 'getTargetFunctionRole(address,bytes4)(uint64)' "$SYNDICATION_POOL" "$(cast sig 'pause()')") cast send --rpc-url "$ETH_RPC_URL" --account admin "$OUTPOST_MANAGER" 'grantRole(uint64,address)' "$ROLE" "$NEW_ETH_PANIC" cast send --rpc-url "$ETH_RPC_URL" --account admin "$OUTPOST_MANAGER" 'execute(address,bytes)' "$OUTPOST_AUTHORITY" \ "$(cast calldata 'revokeRole(uint64,address)' "$ROLE" "$OLD_ETH_PANIC")" @@ -341,14 +341,6 @@ and `status` on Solana (`liq_sequence`, the pool ATA balance). What to check: outpost; a skipped one is reconciled by the recredit rule ([Reconciling](#reconciling-a-desyndication-the-outpost-did-not-pay)). - Between two readings during a freeze, no outpost custody balance falls. -- If `LiqEthToken` was paused: look for liqETH payouts `ReserveManager` skipped between `PAUSE_BLOCK` - and the unpause. The depot counts each of them as paid, so each one found is reconciled by hand: - ```bash - cast logs --rpc-url "$ETH_RPC_URL" --from-block "$PAUSE_BLOCK" --address "$RESERVE_MANAGER" \ - 'SwapRemitUnpayable(uint64 tokenCode, uint64 reserveCode, uint64 depotAmount, bytes32 originalId, string reason)' - cast logs --rpc-url "$ETH_RPC_URL" --from-block "$PAUSE_BLOCK" --address "$RESERVE_MANAGER" \ - 'SwapRevertError(address indexed depositor, uint64 tokenCode, uint64 reserveCode, uint256 amount, string amountUnit, bytes32 originalSwapMessageId, bytes errData)' - ``` ### Reconciling a desyndication the outpost did not pay @@ -466,7 +458,7 @@ cast logs --rpc-url "$ETH_RPC_URL" --address "$SYNDICATION_POOL" \ cast call --rpc-url "$ETH_RPC_URL" "$SYNDICATION_POOL" 'paused()(bool)' # true ``` -Only `SyndicationPool` is paused; pause `ReserveManager`, `StakingModule` or `LiqEthToken` by hand if the +Only `SyndicationPool` is paused; pause `StakingModule` or `LiqEthToken` by hand if the fault may reach them ([Pulling the cord](#pulling-the-cord)). `payPendingDesyndication` pays the stored release after `unpause`, with the same rule as Solana: unpause only once the pool covers the depot's outstanding plus every release still stored. @@ -504,7 +496,6 @@ Unpause `SyndicationPool` last, so the liqETH it pays out can move: ```bash cast send --rpc-url "$ETH_RPC_URL" --account panic "$LIQETH_TOKEN" 'unpause()' # only if it was paused cast send --rpc-url "$ETH_RPC_URL" --account panic "$STAKING_MODULE" 'unpause()' -cast send --rpc-url "$ETH_RPC_URL" --account panic "$RESERVE_MANAGER" 'unpause()' cast send --rpc-url "$ETH_RPC_URL" --account panic "$SYNDICATION_POOL" 'unpause()' ``` diff --git a/docs/opp-two-phase-dispatch-design.md b/docs/opp-two-phase-dispatch-design.md index b87924974c..90ffe689e8 100644 --- a/docs/opp-two-phase-dispatch-design.md +++ b/docs/opp-two-phase-dispatch-design.md @@ -3,12 +3,18 @@ Status: design, approved 2026-08-10. Supersedes the fused terminal-call design currently on `feat/resumable-opp-dispatch` (wire-sysio #552, wire-solana #419). +Cross-chain swaps through outposts (`SWAP_REMIT` / `SWAP_REVERT`) and outpost-side +operator collateral (`OPERATOR_ACTION` remits) have since been removed. The +remit-class inbound effect on Solana today is the `DESYNDICATE_LIQ` payout; the +dispatch mechanics below are unchanged by that. + ## Problem A Solana transaction cannot carry the effect accounts for every attestation in an OPP envelope. The packet limit is 1232 bytes serialized and every dynamic account -costs 32 bytes in the message, so a large envelope — many SPL swap remits across -distinct reserves — needs more accounts than one transaction can declare. Only +costs 32 bytes in the message, so a large envelope — many attestations whose +effects each touch their own accounts (desyndication payouts to distinct +recipients, for example) — needs more accounts than one transaction can declare. Only the relay can size a batch, because only the relay supplies the accounts and therefore knows the transaction's real byte cost. @@ -260,10 +266,9 @@ deliberately deleted the SEC-94 estimator allow-list, which was correct; the scaffolding to learn `chain.kind`, so it fell out as collateral rather than by decision. Restore the bare registration assert with no estimator and no `kind` branch. Separately, `buildenv`'s chains fetch now uses the bare `.get()` overload -and aborts with `"key not found"`; give it the message overload. Three stale -references describe the deleted guard and must be corrected or a future reader -will conclude msgch is the backstop: `sysio.reserv.cpp:93-95`, -`sysio.msgch_tests.cpp:164-170`, `sysio.reserv_tests.cpp:411-414`. +and aborts with `"key not found"`; give it the message overload. Any remaining +comment or test that describes the deleted guard (`sysio.msgch_tests.cpp` carried +one) must be corrected, or a future reader will conclude msgch is the backstop. Any change under `contracts/**` runs `contracts_unit_test -- --sys-vm` before the commit lands, and follows the rebuild + WASM/ABI copy sequence @@ -308,7 +313,8 @@ Order: nonzero cursor, stall break, round exhaustion alarming, and the terminal-shape packet-limit measurement. - Contracts: `contracts_unit_test -- --sys-vm` for the `queueout` change. -- Flow: a SOL swap-remit flow whose envelope needs ≥ 2 dispatch rounds, asserting +- Flow: a SOL flow whose inbound envelope needs ≥ 2 dispatch rounds (for example + many desyndication payouts in one epoch), asserting the epoch does not advance until the cursor drains. Run via the canonical pair (`run-flow.mjs` + `flow-heartbeat-monitor.mjs`) per `run-flows-via-canonical-scripts.md`. diff --git a/docs/platform-bootstrap-config.md b/docs/platform-bootstrap-config.md index ae1fbca36f..95444d6ed4 100644 --- a/docs/platform-bootstrap-config.md +++ b/docs/platform-bootstrap-config.md @@ -1,22 +1,14 @@ # Platform bootstrap configuration -Launch-day chains, tokens, reserves, and swap settings are described by a -single human-authored JSON file validated against a protobuf schema. The -bootstrap tool replays that file onto the chain inside the epoch-0 bootstrap -window, replacing the dataset that was previously hard-coded in the cluster -tooling. - -> This is the WIRE **platform** bootstrap config — the launch-day registry -> state (chains, tokens, reserves, global swap settings). The swap reserves it -> seeds are one part of that state; it is not itself a DEX. (Earlier drafts -> called the top-level message `DexConfig`; it is now `BootstrapPlatformConfig`.) - -This addresses three launch needs: +Launch-day chains, tokens, shadow-liq yield pools and pre-launch syndicated +positions are described by a single human-authored JSON file validated against +a protobuf schema. The bootstrap tool replays that file onto the chain inside +the epoch-0 bootstrap window (`sysio.epoch` `current_epoch_index == 0`), where +every registration is privilege-gated and lands active inline. | Need | Artifact | |---|---| -| A T5 allocation set aside to back the WIRE side of reserves | `BootstrapPlatformConfig.t5_reserve_allocation` — a genesis-config WIRE amount; config + bootstrap arithmetic, **no contract change** | -| A T5 allocation set aside for launch DEX integration | `BootstrapPlatformConfig.t5_dex_allocation` — reserved earmark (default 0); the on-chain DEX-seeding mechanism is TBD (launch total set by Kyle / Ken) | +| A T5 allocation set aside to back the WIRE side of the launch yield pools | `BootstrapPlatformConfig.t5_dex_allocation` — drained by `sysio.liq::regliqpool` | | A file the bootstrap tool reads for these settings | `etc/config/dex/dex-config.*.json` | | A defined input shape for that file | `libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto` → message `BootstrapPlatformConfig` | @@ -31,29 +23,30 @@ canonical protobuf-JSON encoding of `BootstrapPlatformConfig`. - `etc/config/dex/dex-config.launch.example.json` — strawman mainnet launch config (placeholder economics; mainnet token addresses marked VERIFY-BEFORE-LAUNCH). -- `etc/config/dex/dex-config.dev.json` — 1:1 mirror of the dev-cluster dataset - (9 tokens, 8 reserves), proving the schema carries the full set and driving - the test. +- `etc/config/dex/dex-config.dev.json` — the dev-cluster dataset (3 chains, + 9 tokens, 2 liq pools, 3 syndications). - `libraries/opp/test/test_bootstrap_platform_config.cpp` — strict-parse + invariant test in the `test_opp` binary. +- `contracts/tests/sysio.synd_tests.cpp` — parses the dev config the same + strict way to drive its launch-replay cases. ## Schema -`BootstrapPlatformConfig` holds the schema version, a deployment label, the two -T5 earmarks (`t5_reserve_allocation`, `t5_dex_allocation`), and repeated -`ChainSpec` / `TokenSpec` / `ReserveSpec` plus a single `UwritConfig`. +`BootstrapPlatformConfig` holds the schema version, a deployment label, the T5 +earmarks, and repeated `ChainSpec` / `TokenSpec` / `LiqPoolSpec` / +`SyndicationSpec`. The spec messages deliberately differ from the registry carriers in -`sysio/opp/types/types.proto` (`Chain` / `Token` / `ChainToken` / `Reserve`), -which are wire messages with packed-uint64 codes, raw `bytes` addresses, and -lifecycle fields that are outputs. A hand-authored config wants the opposite: +`sysio/opp/types/types.proto` (`Chain` / `Token` / `ChainToken`), which are wire +messages with packed-uint64 codes, raw `bytes` addresses, and lifecycle fields +that are outputs. A hand-authored config wants the opposite: -- **Codes are strings** (`"ETHEREUM"`, `"USDC"`, `"PRIMARY"`); the tool packs +- **Codes are strings** (`"ETHEREUM"`, `"USDC"`, `"LIQETH"`); the tool packs them via `slug_name` (`[A-Z][A-Z0-9_]{0,7}` -- a code must START with a letter, ≤ 8 chars). -- **Addresses are strings** in chain-native display form (`0x`-hex for EVM, - base58 for SVM) so each is verifiable against a block explorer. `bytes` would - render as base64 in JSON. +- **Addresses and pubkeys are strings** in chain-native display form (`0x`-hex + for EVM, base58 for SVM) so each is verifiable against a block explorer. + `bytes` would render as base64 in JSON. - **Amounts are uint64 subunits** (9-decimal unless `precision` says otherwise). Proto3 canonical JSON renders 64-bit integers as **quoted strings** — amounts must be quoted in authored JSON, since launch-scale @@ -71,108 +64,74 @@ lifecycle fields that are outputs. A hand-authored config wants the opposite: |---|---| | `ChainSpec` | `sysio.chains::regchain(kind, code, external_chain_id, name, description)` | | `TokenSpec` | `sysio.tokens::regtoken(kind, code, symbol_name, description, precision, address)` then `sysio.tokens::regctok(chain_code, token_code, contract_addr, is_native)` | -| `ReserveSpec` | `sysio.reserv::regreserve(chain_code, token_code, reserve_code, name, description, initial_chain_amount, initial_wire_amount, source_token_precision, connector_weight_bps, is_private, owner)` — `source_token_precision` is **not** a `ReserveSpec` field: it comes from the referenced `TokenSpec.precision` (see below) | -| `UwritConfig` | `sysio.uwrit::setconfig(fee_bps, collateral_lock_duration_ms, min_fromwire_amount, fromwire_revert_fee_bps, uwreq_pending_timeout_epochs, uwreq_retention_epochs)` — the spec pins the first two; the caller supplies the rest (see below) | -| `t5_reserve_allocation` | none — feeds the `setemitcfg` arithmetic below | -| `t5_dex_allocation` | none yet — reserved earmark; carved out of T5 alongside `t5_reserve_allocation` once the DEX-seeding path lands | - -The `regreserve` signature (with `is_private` + `owner`) and the ms-based -`setconfig` are the reserve-and-swap-beta surface; this config slots directly -onto them. `owner` is a WIRE account name (`sysio::name`): empty for public -reserves, the owning account for private ones. - -`regreserve`'s eleven-argument signature includes `source_token_precision` as its -**eighth** argument, which no `ReserveSpec` field supplies. It is the -**depot-frame precision of the paired -token** — i.e. the referenced `TokenSpec.precision`, which is itself -`min(native precision, 9)`. So the mapping is a lookup, not a new config field: -resolve `(chain_code, token_code)` to its `TokenSpec` and pass that spec's -`precision`. `regreserve` rejects a value above 9 (`WIRE_PRECISION`) with -*"source_token_precision exceeds the depot frame (9) — the outpost must downscale -to min(native, 9)"*, which is why V4 bounds `precision` at 9 rather than 18: -a token declared at, say, 18 would satisfy a wider validator and then abort the -irreversible bootstrap at `regtoken`/`regreserve`. Tokens whose native precision -exceeds the frame (ETH at 18) declare the frame value and are downscaled at the -outpost boundary. - -`UwritConfig` carries only `fee_bps` and `collateral_lock_duration_ms`, while -`setconfig` takes six arguments. The remaining four — -`min_fromwire_amount`, `fromwire_revert_fee_bps`, `uwreq_pending_timeout_epochs`, -`uwreq_retention_epochs` — are **not part of this spec**: a bootstrap caller -passes the contract's `uw_config` in-struct defaults (5 WIRE floor, 500 bps -revert fee, and the two uwreq lifecycle windows) unless it has a reason to -override them. - -`fee_bps` is the **network** fee and not the whole effective swap fee. Each -participating non-WIRE leg's reserve independently charges its own -`owner_fee_bps` off the same WIRE leg; that rate is **not** in `ReserveSpec` and -is set post-bootstrap via `sysio.reserv::setrsvfee`. The network fee itself -splits 50/50 between the winning underwriter and a rewards pool -(`sysio.reserv::FEE_UNDERWRITER_SHARE_BPS`), and that pool splits again by the -optional `reservcfg` dial `fee_emissions_share_bps` — which nothing seeds, so it -reads zero until `sysio.reserv::setconfig` first persists a row. - -## T5 reserve earmark - -With `A` = the launch T5 allotment, `E` = `t5_reserve_allocation`, and -`W` = Σ `reserves[].initial_wire_amount`: - -`regreserve` drains each reserve's WIRE side from the `sysio` emissions -treasury at registration (a real inline `sysio.token::transfer` authorized by -`sysio@active`). The emissions formula gates on +| `LiqPoolSpec` | `sysio.liq::regliqpool(chain_code, token_code, pair_symbol, initial_chain_amount, initial_wire_amount, fee, locked_shares, conversion_horizon_sec, depth_cap_bps, clip_floor)` — mints the LCO shadow to `sysio`, deposits it with the WIRE side from the `sysio` treasury into `sysio.swap`, and creates the pair with the shadow as its yield leg. The shadow symbol must already exist; exactly one pool per shadow | +| `SyndicationSpec` | `sysio.synd::importsynd(chain_code, token_code, credits[] {pubkey, amount})`, batched by the tool; `sysio.synd::importdone` closes the import | +| `t5_dex_allocation` | none — feeds the `setemitcfg` arithmetic below | + +`TokenSpec.precision` is the **depot-frame precision**, `min(native precision, +9)`. `sysio.tokens::regtoken` rejects anything above 9, which is why V4 bounds +`precision` at 9 rather than 18: a token declared at, say, 18 would satisfy a +wider validator and then abort the irreversible bootstrap at `regtoken`. Tokens +whose native precision exceeds the frame (ETH at 18) declare the frame value and +are downscaled at the outpost boundary. + +`SyndicationSpec.pubkey` is the identity `SyndicateLIQ.user` carries: base58 of +the 32-byte Ed25519 key on SVM, 0x-hex of the 33-byte **compressed** secp256k1 +point on EVM (never the 20-byte address). Each credit mints to the account that +pubkey has linked through `sysio.authex`, or parks it until the link is made. + +### Defined but not consumed + +`bootstrap.proto` still defines `ReserveSpec`, `UwritConfig`, +`BootstrapPlatformConfig.reserves`, `BootstrapPlatformConfig.uwrit` and +`t5_reserve_allocation`, and both JSON files still carry values for them. The +contracts they targeted (`sysio.reserv`, `sysio.uwrit`) have been removed, so no +bootstrap action consumes them; only `test_bootstrap_platform_config.cpp` still +parses and validates them (V6–V9). + +## T5 DEX earmark + +With `A` = the launch T5 allotment, `D` = `t5_dex_allocation`, and +`W` = Σ `liq_pools[].initial_wire_amount`: + +`regliqpool` drains each pool's WIRE side from the `sysio` emissions treasury at +registration. The emissions formula gates on `t5_distributable − t5_floor − total_distributed`, and the per-epoch readiness gate independently checks the real treasury balance, so the earmark must sit **outside** the distributable pool: -1. Config invariant: `0 < W ≤ E`. -2. Bootstrap sets `setemitcfg.t5_distributable = A − E` (`t5_floor` unchanged — +1. Config invariant: `W ≤ D`, and `D > 0` whenever any pool is seeded. +2. Bootstrap sets `setemitcfg.t5_distributable = A − D` (`t5_floor` unchanged — the floor is inside the distributable pool). -3. End-of-bootstrap `sysio` balance ≥ `t5_distributable + E`; after reserves +3. End-of-bootstrap `sysio` balance ≥ `t5_distributable + D`; after the pools drain `W`, balance ≥ `t5_distributable`, so the readiness gate never trips - because of reserve funding. -4. Remainder `E − W` stays in the treasury, inert and outside emissions + because of pool funding. +4. Remainder `D − W` stays in the treasury, inert and outside emissions accounting. -Putting `E` *inside* `t5_distributable` instead would make the emissions math +Putting `D` *inside* `t5_distributable` instead would make the emissions math count WIRE that has physically left the treasury — the readiness gate blocks at launch scale, and effective emissions headroom silently shrinks by `W`. Hence the outside-the-pool earmark. -Each bootstrap `regreserve` transaction must carry `sysio@active` (for the -treasury drain) alongside `sysio.reserv@active`. - -## T5 DEX earmark - -`t5_dex_allocation` (`D`) is a second, parallel carve-out of the T5 allotment, -reserved for launch DEX integration — at least one DEX is seeded at launch, with -more added later by council vote. It follows the same outside-the-pool rule as -the reserve earmark, so the full T5 carve-out is `E + D` and -`t5_distributable = A − E − D`. - -It is **reserved**: the on-chain DEX-seeding mechanism is not finalized, so the -bootstrap tool does not yet drain `D` — the field is carried for forward -compatibility and defaults to `0` (earmark disabled). The launch total is a -per-deployment economics decision (Kyle / Ken own the figure). When the seeding -path lands, the `A − E` arithmetic above generalizes to `A − E − D` with no -shape change to this config. - ## Validation Parsing is **strict**: unknown / misspelled keys are rejected, not dropped, -because the file is hand-authored and drives irreversible actions. A validator -(reproduced in the test, and to be shared with the bootstrap tool) enforces: +because the file is hand-authored and drives irreversible actions. The +validator in `test_bootstrap_platform_config.cpp` enforces: | # | Invariant | |---|---| | V1 | `schema_version == 1`; `network` non-empty | | V2 | every code is a valid slug: `[A-Z][A-Z0-9_]{0,7}` -- leading character must be a letter, ≤ 8 chars | | V3 | chain codes unique; exactly one `CHAIN_KIND_WIRE` chain, code `WIRE` | -| V4 | token codes unique; `chain_code` declared; `precision` ∈ 1..9 (the depot frame — `sysio.tokens::regtoken` rejects anything higher, so a wider bound here would pass validation and then fail mid-bootstrap); native ⇔ kind `NATIVE` + empty address; non-native address well-formed for the chain kind (EVM `0x`+40 hex; SVM base58 → 32 bytes) | +| V4 | token codes unique; `chain_code` declared; `precision` ∈ 1..9 (the depot frame); native ⇔ kind `NATIVE` + empty address; non-native address well-formed for the chain kind (EVM `0x`+40 hex; SVM base58 → 32 bytes) | | V5 | exactly one native token per non-depot chain | -| V6 | reserve `(chain, token, code)` unique; references a declared binding; not on the depot; `0 < connector_weight_bps ≤ 9999`; amounts > 0 | -| V7 | Σ `initial_wire_amount` ≤ `t5_reserve_allocation` > 0 | -| V8 | `is_private` ⇒ `owner` a valid account name; else `owner` empty | -| V9 | `uwrit` present; `fee_bps ≤ 9999`; lock duration > 0 | +| V6–V9 | checks on the unconsumed `reserves` / `uwrit` / `t5_reserve_allocation` fields (see [Defined but not consumed](#defined-but-not-consumed)) | +| V10 | `t5_dex_allocation > 0` when any liq pool is seeded; Σ `liq_pools[].initial_wire_amount` ≤ `t5_dex_allocation` | +| V11 | each liq pool references a declared `TOKEN_KIND_LIQ` token on its chain and is unique; `pair_symbol` 1..7 characters `[A-Z]`; seeds > 0; `fee ≤ 9999`; `conversion_horizon_sec > 0`; `depth_cap_bps` ∈ 1..10000; `clip_floor > 0` | +| V12 | each syndication references a declared liq token on its chain; `pubkey` fits the chain family; `amount > 0` | +| V13 | a non-zero `custody_total` equals the pool's `initial_chain_amount` plus the sum of that token's `syndications` | Run the test: @@ -183,24 +142,17 @@ ninja -C build test_opp ## Follow-ups -- Wire the bootstrap tool to consume `BootstrapPlatformConfig` (the cluster phases become a - loop over the parsed config, same order: chains → tokens → bindings → - reserves → uwrit), injecting per-deployment contract addresses from - deployment artifacts into the dev config. -- Regenerate the TS/Solidity/Solana model bundles so the tool imports the - generated `BootstrapPlatformConfig` type. -- Fill in launch economics (`t5_reserve_allocation` + per-reserve amounts) and - verified mainnet addresses; freeze `dex-config.mainnet.json`. +- Wire the bootstrap tool to consume `BootstrapPlatformConfig` (the cluster + phases become a loop over the parsed config, in order: chains → tokens → + bindings → liq pools → syndication import), injecting per-deployment contract + addresses from deployment artifacts into the dev config. +- Fill in launch economics (`t5_dex_allocation` + per-pool amounts) and verified + mainnet addresses; freeze `dex-config.mainnet.json`. ## Open questions -- The earmark `E` and per-reserve amounts are economics decisions - (placeholders today). -- Launch token set: Solana SPL stables? Liquid-staking tokens (addresses exist - only once those contracts deploy)? -- Whether to seed any private reserves directly at launch, or create them all - post-launch via the authex-gated outpost flow (default assumption: all - post-launch). +- The earmark `D` and per-pool amounts are economics decisions (placeholders + today). - Whether to unify the emissions genesis numbers (`A`, `t5_distributable`, - `t5_floor`) with this config so the `A − E` arithmetic is checkable in one + `t5_floor`) with this config so the `A − D` arithmetic is checkable in one place. diff --git a/plugins/batch_operator_plugin/README.md b/plugins/batch_operator_plugin/README.md index 94edd7a820..0453c9441e 100644 --- a/plugins/batch_operator_plugin/README.md +++ b/plugins/batch_operator_plugin/README.md @@ -4,27 +4,34 @@ Cranks Depot and Outpost contracts, ferrying OPP message chains between the WIRE ## Overview -All 21 batch operators run this plugin in perpetuity. The epoch scheduler (`sysio.epoch`) assigns operators into 3 fixed groups of 7. Each epoch (every 6 minutes), one group is elected; those 7 execute the full epoch cycle. +Every batch operator runs this plugin. `sysio.epoch` keeps a sliding window of +`batch_op_groups` groups of `operators_per_epoch` operators (default 3 groups of 7, epochs +of 6 minutes); the group at the front of the window is elected for the current epoch and +its members execute the epoch cycle. Each poll the plugin also reads its own +`sysio.opreg` status and stops relaying once the operator is `SLASHED` or `TERMINATED`. ## Epoch Cycle -**Phase 1 — Outbound (WIRE → Outposts):** -1. Crank Depot (`sysio.msgch::crank`) to produce outbound OPP Message Chain -2. Read the produced chain from Depot tables -3. Deliver chain to Outpost contract (ETH: `OPPInbound.epochIn()`, SOL: `epoch_in()`) -4. All 7 independently verify the delivered chain +Run by every member of the elected group, for every active outpost chain: -**Phase 2 — Inbound (Outposts → WIRE):** +**Outbound (WIRE → Outposts):** +1. Read the outpost's outbound envelope that `sysio.epoch::advance` built + (`sysio.msgch::buildenv`) from the depot's `outenvelopes` table +2. Deliver it to the Outpost contract, chunked (ETH: `OPPInbound.epochIn()`, SOL: `epoch_in`) +3. The outpost reaches consensus once enough group members have delivered identical bytes + +**Inbound (Outposts → WIRE):** 1. The consensus-reaching delivery emits the outpost's outbound envelope 2. Read the latest outbound envelope from Outpost storage 3. Deliver its raw protobuf bytes to Depot (`sysio.msgch::deliver`) -4. Depot evaluates consensus across all 7 deliveries +4. Depot evaluates consensus across the group's deliveries ## Depot cranks Every epoch poll (`--batch-epoch-poll-ms`) also pushes the depot actions nothing on -chain schedules. `sysio.msgch::chkcons` comes only from the elected operator; the -rest come from every ACTIVE operator, because the elected one may be the operator +chain schedules. `sysio.msgch::chkcons` — which advances the epoch once every active +outpost has consensus and the boundary has passed — comes only from elected operators; +the rest come from every ACTIVE operator, because the elected one may be the operator that is offline, and each is a cheap no-op once its work is done: | Action | When | Why nothing else drives it | @@ -34,7 +41,8 @@ that is offline, and each is a cheap no-op once its work is done: | `sysio.liq::queueyield(sym)` | every shadow with yield pending from an outpost `LIQ_YIELD` report | the report lands in `sysio.liq`'s pending balance; queuing it into the swap is a separate, permissionless step | The two yield cranks stay idle, without logging a read failure per poll, until both -`sysio.swap` and `sysio.liq` are deployed on the depot. +`sysio.swap` and `sysio.liq` are deployed on the depot. The plugin does not crank +`sysio.synd::crank`; syndication release is driven by keepers. ## Configuration @@ -79,5 +87,5 @@ address rather than left pointing at the old one. - `chain_plugin` — blockchain state access - `cron_plugin` — irreversible block event subscription - `signature_provider_manager_plugin` — signing key management -- `outpost_ethereum_client_plugin` — ETH RPC calls (future) -- `outpost_solana_client_plugin` — SOL RPC calls (future) +- `outpost_ethereum_client_plugin` — Ethereum outpost relay +- `outpost_solana_client_plugin` — Solana outpost relay diff --git a/plugins/chain_interface/README.md b/plugins/chain_interface/README.md index ef0644b968..c4fbe6dd8b 100644 --- a/plugins/chain_interface/README.md +++ b/plugins/chain_interface/README.md @@ -39,9 +39,9 @@ returned `handle` for the lifetime of its subscription or registration. | `channels::applied_transaction` | `transaction_trace_ptr` | `chain_plugin`, relaying `controller::applied_transaction` | `low` | | `compat::channels::transaction_ack` | `std::pair` | `producer_plugin` after a speculative transaction is applied or rejected, and `chain_plugin`'s transaction-retry database | `low` | -`irreversible_block` is the one most non-chain plugins consume: `batch_operator_plugin` and -`underwriter_plugin` each subscribe to it as their sync gate, arming their work only once the node has caught -up. `net_plugin` subscribes to `compat::channels::transaction_ack` so it can relay or drop a peer's +`irreversible_block` is the one most non-chain plugins consume: `batch_operator_plugin` subscribes to it as +its sync gate, arming its work only once the node has caught up, and `status_monitor_plugin` renders one +document per irreversible block. `net_plugin` subscribes to `compat::channels::transaction_ack` so it can relay or drop a peer's transaction according to the result. ### Methods @@ -81,9 +81,8 @@ sync_gate_subscription = ``` [`usage_pattern.md`](../usage_pattern.md) lists connecting to other plugins' signals and registering -provider methods among the typical `plugin_initialize` actions; the sync-gate consumers in this tree -(`batch_operator_plugin`, `underwriter_plugin`) subscribe from `plugin_startup` instead, after their own state -is built. Either way, keep the returned handle — dropping it cancels the subscription. +provider methods among the typical `plugin_initialize` actions; the sync-gate consumer in this tree +(`batch_operator_plugin`) subscribes from `plugin_startup` instead, after its own state is built. Either way, keep the returned handle — dropping it cancels the subscription. ## Options @@ -113,6 +112,7 @@ to the ack channel. Both run under their own plugin's test target. this include path public to the rest of the tree. - `net_plugin` — subscribes to `transaction_ack`, and feeds every transaction it accepts from a peer into `transaction_async` via `chain_plugin::accept_transaction`. -- `batch_operator_plugin`, `underwriter_plugin` — subscribe to `irreversible_block` as their sync gate. +- `batch_operator_plugin` — subscribes to `irreversible_block` as its sync gate. +- `status_monitor_plugin` — subscribes to `irreversible_block` to ship one snapshot per irreversible block. - [`plugins/usage_pattern.md`](../usage_pattern.md) — when to connect, register, and release these handles across the appbase lifecycle. diff --git a/plugins/chain_plugin/README.md b/plugins/chain_plugin/README.md index f2546e0e78..84b21966dc 100644 --- a/plugins/chain_plugin/README.md +++ b/plugins/chain_plugin/README.md @@ -3,9 +3,9 @@ `chain_plugin` owns the blockchain itself. It constructs and configures the `controller` — block log, chain state database, fork database, WASM runtime, protocol features, whitelists and blacklists — brings it up from genesis, a snapshot, or existing state, and relays the controller's signals onto the appbase channels -declared in `chain_interface`. Those channels have few subscribers: `batch_operator_plugin` and -`underwriter_plugin` take `irreversible_block` as their sync gate, and nothing else subscribes to any of -them — `net_plugin` connects to the controller's signals directly instead. `nodeop` initializes it on every +declared in `chain_interface`. Those channels have few subscribers: `batch_operator_plugin` takes +`irreversible_block` as its sync gate and `status_monitor_plugin` ships one snapshot per irreversible block; +nothing else subscribes to any of them — `net_plugin` connects to the controller's signals directly instead. `nodeop` initializes it on every run alongside `resource_monitor_plugin`, `net_plugin`, and `producer_plugin`, so there is no `plugin =` line to add; it pulls in `signature_provider_manager_plugin` through `APPBASE_PLUGIN_REQUIRES`, which in turn pulls in `http_client_plugin`. It registers no HTTP endpoints of its own — `chain_api_plugin` publishes the diff --git a/plugins/cron_plugin/README.md b/plugins/cron_plugin/README.md index a8f4430203..bb9c39eba4 100644 --- a/plugins/cron_plugin/README.md +++ b/plugins/cron_plugin/README.md @@ -2,9 +2,9 @@ `cron_plugin` provides `nodeop` with an in-process cron scheduler: other plugins register functions against cron-style schedules and the plugin fires them on a worker pool. It is infrastructure, not an operator-facing -feature -- it exposes no HTTP endpoint and does nothing on its own. Operators rarely name it: both -`batch_operator_plugin` and `underwriter_plugin` declare it in `APPBASE_PLUGIN_REQUIRES`, so enabling either one -loads it automatically. It has no dependencies of its own (`APPBASE_PLUGIN_REQUIRES()` is empty). +feature -- it exposes no HTTP endpoint and does nothing on its own. Operators rarely name it: +`batch_operator_plugin` declares it in `APPBASE_PLUGIN_REQUIRES`, so enabling the batch operator loads it +automatically. It has no dependencies of its own (`APPBASE_PLUGIN_REQUIRES()` is empty). ## How it works @@ -74,7 +74,7 @@ discovered, so its OPP polling jobs do not contend with anything else on the sha ## Enabling / configuration -No `plugin =` line is normally needed -- `batch_operator_plugin` and `underwriter_plugin` both require it. +No `plugin =` line is normally needed -- `batch_operator_plugin` requires it. Naming it explicitly is harmless. `config.ini`: @@ -147,7 +147,5 @@ trigger pre-computation, the scheduler waking when a new job is added, and day-o - [`batch_operator_plugin`](../batch_operator_plugin) -- requires this plugin, and additionally creates its own private `cron_service` sized from the discovered outpost count for OPP polling jobs. -- [`underwriter_plugin`](../underwriter_plugin) -- requires this plugin and schedules its scan job on the shared - service. - [`resource_monitor_plugin`](../resource_monitor_plugin) -- a plugin with its own dedicated timer thread rather than a cron job; the two are independent. diff --git a/plugins/external_debugging_plugin/README.md b/plugins/external_debugging_plugin/README.md index f3468ecf4c..dcea6b8e11 100644 --- a/plugins/external_debugging_plugin/README.md +++ b/plugins/external_debugging_plugin/README.md @@ -175,5 +175,3 @@ it. No test dials a real server. plugin's `ext-debugging-*` triple layers over. - [`prometheus_plugin`](../prometheus_plugin) -- its `nodeop_outbound_http_*` counters include this plugin's outbound traffic, since all outbound callers share one transport. -- [`underwriter_plugin`](../underwriter_plugin) -- the other OPP-side plugin; it has its own - `/v1/underwriter/*` diagnostics rather than an external sink. diff --git a/plugins/http_plugin/README.md b/plugins/http_plugin/README.md index fb0d75c77f..194c535cc0 100644 --- a/plugins/http_plugin/README.md +++ b/plugins/http_plugin/README.md @@ -94,7 +94,6 @@ be named in a `plugin` option, or startup fails with `plugin_config_exception`. | `trace_api` | `sysio::trace_api_plugin` | | `prometheus` | `sysio::prometheus_plugin` | | `test_control` | `sysio::test_control_api_plugin` | -| `underwriter` | `sysio::underwriter_plugin` | `node` is the category of endpoints served on every listener; it is what `http-server-address` and `unix-socket-path` bind, and it is not a value accepted by `--http-category-address`. @@ -162,7 +161,7 @@ All of `http_plugin`'s options are config-file options, so each is equally valid |---|---|---| | `http-server-address` | `127.0.0.1:8888` | Local IP and port to listen on for incoming HTTP connections. Set to the literal `http-category-address` to enable the `http-category-address` option; leave blank to disable. Under `kiod` the option is registered with no default. | | `unix-socket-path` | unset | Filename, relative to the data dir, of a unix socket for HTTP RPC; blank disables it. Must not be set when `http-category-address` is used. Under `kiod` it defaults to `kiod.sock`. | -| `http-category-address` | unset | `category,address` pair binding one API category to one listen address; may be repeated. The address is `:port`, `:port`, or a unix socket path starting with `/`, `./`, or `../`. Valid categories are `chain_ro`, `chain_rw`, `db_size`, `net_ro`, `net_rw`, `producer_ro`, `producer_rw`, `snapshot`, `trace_api`, `prometheus`, `test_control`, `snapshot_ro`, and `underwriter`. `kiod` does not register this option. | +| `http-category-address` | unset | `category,address` pair binding one API category to one listen address; may be repeated. The address is `:port`, `:port`, or a unix socket path starting with `/`, `./`, or `../`. Valid categories are `chain_ro`, `chain_rw`, `db_size`, `net_ro`, `net_rw`, `producer_ro`, `producer_rw`, `snapshot`, `trace_api`, `prometheus`, `test_control`, and `snapshot_ro`. `kiod` does not register this option. | ### Limits and threads @@ -234,5 +233,5 @@ request-body reservation. - `producer_api_plugin` — registers the `producer_ro`, `producer_rw`, and `snapshot` endpoints. - [`snapshot_api_plugin`](../snapshot_api_plugin/README.md) — registers the `snapshot_ro` endpoints, and is the consumer of `add_raw_handler` for file downloads. -- `db_size_api_plugin`, `trace_api_plugin`, `prometheus_plugin`, `test_control_api_plugin`, - `underwriter_plugin` — the remaining category owners. +- `db_size_api_plugin`, `trace_api_plugin`, `prometheus_plugin`, `test_control_api_plugin` — the remaining + category owners. diff --git a/plugins/outpost_client_plugin/README.md b/plugins/outpost_client_plugin/README.md index 556684f25c..c3b385af5c 100644 --- a/plugins/outpost_client_plugin/README.md +++ b/plugins/outpost_client_plugin/README.md @@ -20,7 +20,7 @@ before a chain-specific client tries to resolve one. ### `sysio/outpost_client/outpost_client.hpp` — the SPI `outpost_client` is the abstract interface between the plugins that orchestrate OPP work -(`batch_operator_plugin`, `underwriter_plugin`) and the chain-specific concretes. An orchestrator holds an +(`batch_operator_plugin`) and the chain-specific concretes. An orchestrator holds an `outpost_client_ptr` (a `std::shared_ptr`) and calls only these virtuals; it never interprets an EVM address, a Solana public key, a PDA, or a signature-provider format. @@ -33,7 +33,7 @@ an EVM address, a Solana public key, a PDA, or a signature-provider format. | `to_string()` | `{chain_code}:{ChainKind_Name}:{chain_id}`. The default prints `chain_code()` as the raw packed integer, so the row registered as `ETH` against a local chain renders `23373212024832:CHAIN_KIND_EVM:31337`. Virtual, with a default derived from the three getters | | `deliver_outbound_envelope(epoch_index, envelope_bytes, deadline)` | OPP outbound — submit one envelope to the remote chain; returns the chain-native transaction id | | `read_inbound_envelope(epoch_index, deadline)` | OPP inbound — return the envelope the remote chain produced for this epoch, or an empty vector when the latest slot does not match | -| `uw_commit(uw_request_id, uic_bytes, deadline)` | Underwriter commit — relay a signed `UnderwriteIntentCommit` through the outpost; returns only after on-chain confirmation | +| `crank_outpost(epoch_index, deadline)` | Drive the outpost's permissionless per-epoch instructions right after this operator's delivery lands; best-effort, retried next epoch. The default cranks nothing; Solana overrides it with `report_liq_yield`, Ethereum with `SyndicationPool.realizeYield` | Two rules bind every implementation. Each RPC-bound call must enforce the `deadline` it is passed, so a hung remote chain cannot starve a cron worker; the protected helper `throw_if_past_deadline(deadline_abs, op)` @@ -136,4 +136,4 @@ so the base class's default implementation is not under test. Higher-fidelity mo - [`outpost_solana_client_plugin`](../outpost_solana_client_plugin/README.md) — the Solana concrete. - `signature_provider_manager_plugin` — required dependency; supplies the signers the concretes resolve by name. - `http_client_plugin` — owns the process-wide `--outbound-http-*` transport options this plugin's RPC policy reads. -- `batch_operator_plugin`, `underwriter_plugin` — the SPI's consumers. +- `batch_operator_plugin` — the SPI's consumer. diff --git a/plugins/outpost_ethereum_client_plugin/README.md b/plugins/outpost_ethereum_client_plugin/README.md index 097c8b1dfc..65b6f47c29 100644 --- a/plugins/outpost_ethereum_client_plugin/README.md +++ b/plugins/outpost_ethereum_client_plugin/README.md @@ -3,10 +3,10 @@ `outpost_ethereum_client_plugin` owns this node's connections to EVM chains. It builds one policy-enforcing `ethereum_client` per configured endpoint — each bound to a named Ethereum signature provider and to a verified numeric chain id — loads the contract ABIs those connections are driven through, and hands out -`outpost_client` concretes that speak the chain-agnostic OPP SPI on top of `OPP.sol`, `OPPInbound.sol`, and -`OperatorRegistry.sol`. An operator enables it whenever the node runs an OPP daemon against an EVM outpost: -`batch_operator_plugin` and `underwriter_plugin` both name it in `APPBASE_PLUGIN_REQUIRES`, so either of -those pulls it in, and it can also be loaded alone (as the bundled RPC tool does) to talk to an EVM endpoint +`outpost_client` concretes that speak the chain-agnostic OPP SPI on top of `OPP.sol` and `OPPInbound.sol` +(plus the outpost's `SyndicationPool.sol` for the per-epoch crank). An operator enables it whenever the node +runs an OPP daemon against an EVM outpost: `batch_operator_plugin` names it in `APPBASE_PLUGIN_REQUIRES`, so +the batch operator pulls it in, and it can also be loaded alone (as the bundled RPC tool does) to talk to an EVM endpoint directly. ## How it works @@ -27,19 +27,19 @@ plugin_initialize v publish the client map (only after every client succeeded) -batch_operator_plugin / underwriter_plugin - | create_outpost_client(client-id, chain_code, chain_id, opp, oppInbound, operatorRegistry) +batch_operator_plugin + | create_outpost_client(client-id, chain_code, chain_id, opp, oppInbound) v -outpost_ethereum_client -- deliver_outbound_envelope / read_inbound_envelope / uw_commit +outpost_ethereum_client -- deliver_outbound_envelope / read_inbound_envelope / crank_outpost ``` ### Endpoints, signers, and chain ids Clients come from exactly one of two sources. The check is an exclusive-or, so configuring both **and** configuring neither are rejected with the same message, `Configure exactly one of ---outpost-ethereum-client-config-file or --outpost-ethereum-client`. Because `batch_operator_plugin` and -`underwriter_plugin` name this plugin in `APPBASE_PLUGIN_REQUIRES`, a node that loads either of them must -configure an Ethereum client source or it will not start, even if it only intends to serve a Solana +--outpost-ethereum-client-config-file or --outpost-ethereum-client`. Because `batch_operator_plugin` names +this plugin in `APPBASE_PLUGIN_REQUIRES`, a node that loads it must configure an Ethereum client source or it +will not start, even if it only intends to serve a Solana outpost. A **command-line spec** is `,,[,]`. With three fields @@ -87,9 +87,9 @@ endpoint cannot occupy a cron worker past its budget. ### The contract clients -Three typed wrappers are built over the shared connection, each only when its address was supplied to -`create_outpost_client`; passing an empty string for the others is normal, and calling an SPI method whose -wrapper was not provisioned asserts with a message naming the missing address. A fourth, the syndication +Two typed wrappers are built over the shared connection, each only when its address was supplied to +`create_outpost_client`; passing an empty string for the other is normal, and calling an SPI method whose +wrapper was not provisioned asserts with a message naming the missing address. A third, the syndication pool's, is bound by the crank once the outpost names the pool (see [Outpost cranks](#outpost-cranks)). State-changing calls go through `create_tx_and_confirm`, which returns only after on-chain inclusion plus confirmations — OPP writes are consensus-critical and must not silently drop. @@ -98,7 +98,6 @@ confirmations — OPP writes are consensus-critical and must not silently drop. |---|---|---| | `opp_contract_client` | `OPP.sol` | `emitOutboundEnvelope(uint32)` (recovery-only write; no in-tree steady-state caller), `getLatestOutboundEnvelope()` view | | `opp_inbound_contract_client` | `OPPInbound.sol` | `epochIn(uint32,uint16,uint16,uint32,bytes)`, `discardEnvelopeChunks()`, `nextEpochIndex()` view, `envelopeChunkState(address)` view, `attestationHandlers(uint16)` view | -| `operator_registry_contract_client` | `OperatorRegistry.sol` | `commit(bytes)` | | `syndication_pool_contract_client` | `SyndicationPool.sol` (Wire-Network/wire-ethereum#207) | `realizeYield()` | ### Outbound delivery and chunking @@ -127,7 +126,7 @@ malformed state decodes as all-zero and degrades to starting fresh, which the co no-ops. A mid-sequence failure simply abandons the tick; the next one restarts from the on-chain high-water mark. -### Inbound reads and underwriter commits +### Inbound reads `read_inbound_envelope` makes one `getLatestOutboundEnvelope` view call at the **`finalized`** block tag, not `latest`. WIRE consensus on inbound is committed forward against this read, so an operator that read at @@ -135,11 +134,6 @@ mark. compared with the requested one and a mismatch returns an empty vector at debug level, since observing the preceding epoch is normal until the consensus-reaching delivery overwrites the slot. -`uw_commit` hex-encodes the canonical `UnderwriteIntentCommit` bytes and calls `OperatorRegistry.commit`, -returning the transaction hash only after confirmation. The outpost binds the signed EVM caller and the -claimed ACTIVE roster identity before queuing the unchanged bytes; the WIRE depot remains authoritative for -the embedded permission signature and bond. - ### Outpost cranks Once per epoch, right after this operator's envelope delivery lands, the outbound relay job calls @@ -264,7 +258,6 @@ Per-client runtime lines are prefixed with the SPI label `outpost_ethereum_clien - `resuming epoch=... delivery at chunk ...`, `discarded a superseded epoch=... staging header`, and `skipping epoch=... delivery — the outpost has ...` on the chunk-resume paths. - `read inbound envelope epoch=... bytes=...` after a successful inbound read. -- `uw_commit confirmed uwreq=... tx_hash=... bytes=...` after a confirmed underwriter commit. - Warning-level lines for every malformed view result (`envelopeChunkState returned non-string variant`, `latestOutboundEnvelope data_ not a string`, and siblings); an epoch mismatch on the inbound slot stays at debug level so steady-state polling is not noisy. @@ -313,4 +306,3 @@ counter address. - `signature_provider_manager_plugin` — required dependency; supplies the Ethereum signer resolved by name. - `http_client_plugin` — owns the process-wide `--outbound-http-*` transport fallbacks. - `batch_operator_plugin` — calls `create_outpost_client` for the OPP envelope path (`OPP.sol` + `OPPInbound.sol`). -- `underwriter_plugin` — calls `create_outpost_client` for the `OperatorRegistry.sol` commit relay. diff --git a/plugins/outpost_solana_client_plugin/README.md b/plugins/outpost_solana_client_plugin/README.md index 21902732b5..01855b4d67 100644 --- a/plugins/outpost_solana_client_plugin/README.md +++ b/plugins/outpost_solana_client_plugin/README.md @@ -94,9 +94,10 @@ Loads one or more Anchor IDL JSON files for use with `solana_program_client`: Anchor IDL program name of the Solana OPP outpost. When an outpost client is constructed, the loaded `--solana-idl-file` set is filtered to programs whose -IDL name matches this value. The default targets the standalone `opp_outpost` -program; the clean-room outpost implementation is hosted inside the -`liqsol-core` program, whose generated IDL is named `liqsol_core`: +IDL name matches this value. The default names a standalone `opp_outpost` +program, which wire-solana no longer builds: the OPP outpost is hosted inside the +`liqsol-core` program, whose generated IDL is named `liqsol_core`, so deployments +pass: ``` --solana-outpost-program-name liqsol_core @@ -451,25 +452,23 @@ Settlement of a consensus envelope is a separate instruction (`dispatch_attestat driven from the on-chain cursor, and every effect account a handler resolves out of `remaining_accounts` must be derived by this relay. `extract_inbound_effects` walks the envelope once, in dispatch order, and `build_dispatch_manifests` derives one manifest per -attestation. The shapes and what each derives: +attestation. Attestations that need no effect account (`OPERATORS`, `BATCH_OPERATOR_GROUPS`) +keep an empty manifest so the indices stay aligned with the on-chain cursor. The one shape +that needs effect accounts, and what it derives: | `effect_shape` | Attestation | Accounts derived | |---|---|---| -| `withdraw_remit`, `slash`, `deposit_revert` | `OPERATOR_ACTION` | the operator / depositor, their `CollateralPosition` PDA, and under SPL custody the collateral vault, the destination ATA and the token program | -| `swap_remit`, `swap_revert` | `SWAP_REMIT`, `SWAP_REVERT` | the `Reserve` PDA, and under SPL custody the reserve vault, the recipient's ATA, the custody mint, its token program and any transfer-hook metas | -| `reserve_ready`, `reserve_create_cancelled` | `RESERVE_READY`, `RESERVE_CREATE_CANCELLED` | the `Reserve` PDA, plus the creator's refund accounts for the cancel | | `desyndicate_liq` | `DESYNDICATE_LIQ` | the liqSOL pool's `GlobalState` and `DistributionState` singletons, the pool authority, the pool and user Token-2022 ATAs with their `UserRecord`s, the bucket ATA, the liqSOL mint, Token-2022, the bucket authority, the mint's transfer-hook program and extra-metas PDA, and liqsol-core itself; plus, for every non-zero request id and even when `DistributionState` cannot be read, the writable `PendingPayout` PDA (`["pending_desyndication", request_id_le8]`) the handler stores the payout at whenever it cannot pay inline: the outpost is frozen, the solvency check finds a custody shortfall, a share record is still the legacy layout, or the settlement meets any other chain-state refusal (wire-solana `PendingPayoutReason`: `OutpostFrozen`, `CustodyShortfall`, `LegacyUserRecord`, `SettlementRefused`). Without it any of these aborts the dispatch with `EffectAccountMissing` | -Custody is read from the account the on-chain handler branches on (`Reserve`, -`CollateralPosition`, `DistributionState`), never from the mutable `OutpostConfig` token -map. An absent account degrades to the accounts the handler needs to log-and-skip; a +Custody is read from the account the on-chain handler branches on (`DistributionState`), +never from the mutable `OutpostConfig` token map. An absent account degrades to the accounts the handler needs to log-and-skip; a present but unreadable one throws, because a guessed manifest is one the program is guaranteed to abort on. Each shape's account list is in lock-step with the program's `require_remaining_account` calls in wire-solana `inbound.rs`: a program-side change to what a handler requires and the matching shape here must move together. The relay -boot-checks the declarations it decodes (`Reserve`, `CollateralPosition`, -`EpochDeliveries`, `LatestOutboundEnvelope`, and `DistributionState` on a program that -declares it) so a drifted IDL fails at startup rather than on the first drain. +boot-checks the declarations it decodes (`EpochDeliveries`, `LatestOutboundEnvelope`, and +`DistributionState` on a program that declares it) so a drifted IDL fails at startup rather +than on the first drain. ## Outpost Cranks diff --git a/plugins/signature_provider_manager_plugin/README.md b/plugins/signature_provider_manager_plugin/README.md index 215616e7fa..cbce5bd6a9 100644 --- a/plugins/signature_provider_manager_plugin/README.md +++ b/plugins/signature_provider_manager_plugin/README.md @@ -3,7 +3,7 @@ `signature_provider_manager_plugin` owns every signing key the node uses. It parses each `--signature-provider` spec, builds the matching signer, and publishes the whole set through one lookup API (by name, by public key, by chain kind, by key type) that `chain_plugin`, `producer_plugin`, -`net_plugin`, the outpost clients, and `underwriter_plugin` query. An operator never enables it by hand: +`net_plugin`, and the outpost clients query. An operator never enables it by hand: every one of those plugins names it in `APPBASE_PLUGIN_REQUIRES`, so appbase loads and initializes it before any consumer. It requires `http_client_plugin`, which it uses for the `KIOD:` scheme. @@ -59,7 +59,7 @@ there is no flag to skip it. The provider set is immutable once the node is running. Every mutator asserts it was called before startup, and all mutation happens on the main thread during the sequential initialize/startup phases, so runtime -lookups from producer, batch-operator, underwriter, and outpost threads need no synchronization. +lookups from producer, batch-operator, and outpost threads need no synchronization. ### Generated default providers @@ -171,6 +171,6 @@ on the generated defaults file. The AWS-backed schemes are tested with their own the `kiod` side of a `KIOD:` provider. - `http_client_plugin` — the HTTP client a `KIOD:` provider signs through; required by this plugin. - `chain_plugin`, `producer_plugin`, `net_plugin`, `outpost_client_plugin`, - `outpost_ethereum_client_plugin`, `outpost_solana_client_plugin`, `underwriter_plugin` — consumers that - require this plugin and therefore load it automatically. + `outpost_ethereum_client_plugin`, `outpost_solana_client_plugin` — consumers that require this plugin and + therefore load it automatically. - `docs/signature-provider-manager-plugin.md` — the spec reference with worked examples.