Files
skunk-net-e2e/OPP-COHESIVE-DESIGN.md
2026-04-03 17:17:40 -04:00

378 lines
13 KiB
Markdown

# Cohesive OPP Design
## Problem
The current repos already show the shape of the system, but not a single transport model that works cleanly for all four directions:
- `wire-ethereum` has a real outbound OPP sender and a partially implemented inbound verifier.
- `capital-staking` has several concrete business flows and several OPP placeholders.
- `wire-sysio` has protobuf ABI support and chain identity types, but no shared OPP depot/runtime in this checkout.
The main failure mode in the current shape is coupling unrelated traffic through global epoch state. That is the wrong abstraction for instant swaps and for the broader outpost/depot model.
The correct abstraction is:
- four independent directed OPP routes:
- `Ethereum -> Wire`
- `Wire -> Ethereum`
- `Solana -> Wire`
- `Wire -> Solana`
- optional sub-lanes inside each route so BAR traffic cannot stall Depositor traffic
- consensus on matching submissions of the same batch data
- request/response correlation by payload ID, not by shared epoch numbering across directions
## Design Goals
- No global OPP epoch across chains or directions.
- Ordering is only required inside a single directed stream.
- Batching exists for operator consensus and gas/tx sizing, not as a business rule.
- Multiple batch operators can submit the same batch independently; the depot accepts the batch once matching weight reaches threshold.
- Reverse-direction flows are independent streams. A `Wire -> Solana` completion is not transport-coupled to the `Solana -> Wire` request batch that caused it.
- Payloads are canonical protobuf messages, even if some origin repos still need a temporary translator while migrating.
- Delivery is idempotent and replay-safe.
## Route And Stream Model
There are four mandatory top-level OPP routes:
| Route | Meaning |
| --- | --- |
| `Ethereum -> Wire` | Ethereum outposts mirror facts into Wire |
| `Wire -> Ethereum` | Wire depot sends commands or acknowledgements to Ethereum outposts |
| `Solana -> Wire` | Solana outposts mirror facts into Wire |
| `Wire -> Solana` | Wire depot sends commands or acknowledgements to Solana outposts |
Each route may contain one or more logical lanes. Lanes are required because the current business surfaces already split into different domains:
- `depositor`
- `pretoken`
- `bar`
- `admin`
The minimum stream identity must therefore be:
```text
stream_key = (
from_chain,
to_chain,
origin_system,
destination_system,
lane
)
```
Where:
- `from_chain` and `to_chain` use `chain_kind_t`
- `origin_system` is the origin contract/program/depot identifier
- `destination_system` is the destination contract/program/depot identifier
- `lane` is a stable logical channel inside the route
This keeps the four chain directions independent while still allowing multiple application lanes per directed pair.
## Transport Units
The transport has three layers:
1. `Assertion`
2. `Message`
3. `Batch`
Definitions:
- An `Assertion` is one business fact or command.
- A `Message` is a deterministic ordered set of assertions derived from one origin transaction or one origin-side decision.
- A `Batch` is a transport seal over a contiguous message range in one stream.
Important rule:
- `Batch` replaces the old global `epoch` concept.
- Batch numbering is per `stream_key`.
- Batches can be sealed by time, size, or explicit flush.
- Failing to deliver batch `N` on one stream must not stall batch creation or delivery on any other stream.
## Protobuf Canonical Format
The canonical transport format should be defined once and shared by all three repos. The simplest shape that fits the current codebase is:
```proto
syntax = "proto3";
package wire.opp.v1;
message StreamKey {
uint32 from_chain = 1;
uint32 to_chain = 2;
bytes origin_system = 3;
bytes destination_system = 4;
uint32 lane = 5;
}
message Assertion {
uint32 assertion_type = 1;
bytes payload = 2;
}
message Uint256 {
bytes be_bytes = 1;
}
message MessageHeader {
StreamKey stream = 1;
uint64 sequence = 2;
bytes previous_message_hash = 3;
uint64 source_timestamp_ms = 4;
bytes source_event_id = 5;
bytes payload_hash = 6;
}
message Message {
MessageHeader header = 1;
repeated Assertion assertions = 2;
}
message BatchHeader {
StreamKey stream = 1;
uint64 batch_number = 2;
bytes previous_batch_hash = 3;
uint64 first_sequence = 4;
uint64 last_sequence = 5;
uint64 sealed_at_ms = 6;
bytes merkle_root = 7;
}
message Batch {
BatchHeader header = 1;
repeated Assertion summary_assertions = 2;
}
message BatchSubmission {
BatchHeader header = 1;
bytes batch_hash = 2;
bytes operator_id = 3;
bytes signature = 4;
}
```
Notes:
- `assertion_type` stays numeric because the current Ethereum code and manager wiring already depend on a numeric registry.
- `payload` becomes a protobuf-encoded business message specific to that assertion type.
- `source_event_id` is the deterministic origin reference used for replay protection and migration from legacy encoders.
- Protobuf has no native `uint256`, so business payload schemas should use a canonical `Uint256` wrapper. The wrapper should be encoded as exactly 32 unsigned big-endian bytes.
## Consensus Model
Consensus happens at the destination depot or endpoint, not at the sender.
For each `(stream_key, batch_number)` the destination keeps:
- every operator submission keyed by `(operator_id, batch_hash)`
- cumulative weight per `batch_hash`
- a single accepted `batch_hash` once threshold is met
Rules:
- A batch is accepted when matching submission weight reaches threshold.
- Conflicting submissions for the same `(stream_key, batch_number)` are retained as evidence.
- Acceptance is per stream only. No other stream is affected.
- Operators may submit late. Late submission is a liveness concern, not a protocol deadlock.
This is the core point the current design needs: consensus is "multiple operators submitted the same batch digest", not "the whole system advanced the next epoch".
## Delivery Model
Once a batch is accepted, any operator may deliver messages from that batch with Merkle proofs.
Execution state is per stream:
- `next_sequence`
- `last_applied_message_hash`
- `next_batch_number`
- optionally one currently accepted batch being drained
The simplest execution rule is:
- delivery chunks must be contiguous within the accepted batch
- the first delivered sequence must equal `next_sequence`
- successful execution increments `next_sequence`
That preserves deterministic ordering while still allowing large batches to be split across multiple destination transactions.
If arbitrary subset delivery is needed later, a per-batch bitmap can be added. It is not required for the first correct implementation.
## Request/Response Correlation
Transport ordering and business correlation are separate concerns.
Any payload that expects a reverse-direction response must include:
- `request_id`
- `origin_message_id` or `source_event_id`
Examples:
- `Solana -> Wire` withdraw request carries `request_id`
- `Wire -> Solana` withdraw completion references the same `request_id`
- `Ethereum -> Wire` unbond request carries `request_id`
- `Wire -> Ethereum` unbond completion or slash references the same `request_id`
This removes the false dependency on shared epoch numbers between opposite directions.
## Current Repo Surfaces Mapped To The Model
### Ethereum -> Wire
Current origin surfaces already emitting OPP payloads:
- `wire-ethereum/contracts/outpost/Depositor.sol`
- `3001` stake
- `3002` unstake
- `3004` liq pretoken purchase
- `3006` yield pretoken purchase
- `wire-ethereum/contracts/outpost/Pretoken.sol`
- `3005` pretoken purchase
- `wire-ethereum/contracts/outpost/BAR.sol`
- `2001` bonded actor
- `2002` unbonded actor
- `2003` bond slashed
Design meaning:
- This route is a facts-to-Wire route.
- Messages represent Ethereum-local state transitions that Wire mirrors.
- The sender can continue producing batches even if a prior batch has not yet been submitted to Wire.
### Wire -> Ethereum
Current transport surface exists in `wire-ethereum/contracts/outpost/OPPInbound.sol`, but it is still globally sequential and not yet attached to real business handlers.
Design meaning:
- This route is a commands/acknowledgements route.
- It needs a real endpoint contract per lane, or one endpoint with per-type handlers.
- State must move from one global queue to per-stream state keyed by `stream_key`.
Immediate commands that fit this route:
- approvals or rejections for cross-chain requests
- BAR role outcomes if Wire is authoritative
- settlement callbacks for future instant swap flows
### Solana -> Wire
Current business surfaces that should emit outbound OPP requests or facts:
- `capital-staking/.../wire_syndication/syndicate_liqsol.rs`
- stake mirror
- `capital-staking/.../wire_syndication/desyndicate_liqsol.rs`
- post-launch withdraw request to Wire is currently a TODO
- `capital-staking/.../wire_pretokens/purchase_pretoken.rs`
- pretoken purchase mirror
- `capital-staking/.../wire_pretokens/purchase_pretokens_from_yield.rs`
- yield purchase mirror
- `capital-staking/.../bar/bond_ops.rs`
- bond request
- unbond request
Design meaning:
- This route is the Solana mirror/request route.
- Solana should emit canonical protobuf messages, or a deterministic translator must derive them from Solana instruction data until native protobuf encoding exists.
### Wire -> Solana
Current destination-side business surfaces already exist:
- `capital-staking/.../wire_config/admin_instructions.rs`
- `complete_withdraw_handler`
- `capital-staking/.../bar/admin_instructions.rs`
- `complete_unbond_role_handler`
- `slash_bond_handler`
- `admin_force_unbond_role_handler`
Design meaning:
- This route is a commands/acknowledgements route.
- These handlers should stop being human-admin-only settlement paths.
- They should become callable by an OPP authority PDA or gateway that verifies accepted Wire batches and enforces idempotency.
## Authority And Idempotency Rules
Every destination endpoint needs the same guarantees:
- only accepted OPP deliveries can execute privileged effects
- the same `request_id` cannot execute twice
- the same `(stream_key, sequence)` cannot execute twice
Practical requirements by repo:
- Ethereum:
- replace global inbound state in `OPPInbound.sol` with per-stream state
- register real assertion handlers for business routes
- Solana:
- add an OPP authority PDA or gateway instruction layer
- move current admin settlement handlers behind that authority
- Wire:
- implement an actual depot contract or plugin state machine for stream state, operator submissions, accepted batches, and handler dispatch
## Migration Strategy
The clean migration path is:
1. Define the shared protobuf schema in one place and vendor it into all repos.
2. Keep the current numeric assertion type registry.
3. Canonicalize every observed origin event into the shared protobuf `Message`.
4. Run consensus on protobuf `Batch` hashes.
5. Dispatch protobuf payloads at the destination.
This allows migration even if some origin contracts still emit legacy byte payloads for a short time, because the operator canonicalization step can deterministically translate them into the protobuf form used for batch hashing and destination execution.
## Immediate Implementation Plan
### Phase 1: Shared Schema
- Add `wire.opp.v1` protobuf definitions to the shared contract/protocol area.
- Add a registry document mapping numeric `assertion_type` values to protobuf payload messages.
### Phase 2: Wire Depot
- Implement per-stream tables:
- `stream_state`
- `batch_submission`
- `accepted_batch`
- `executed_message`
- Implement threshold matching on `(stream_key, batch_number, batch_hash)`.
- Implement contiguous chunk delivery with Merkle proof validation.
### Phase 3: Ethereum Endpoint
- Refactor inbound state from one global queue to `mapping(stream_hash => StreamState)`.
- Keep the sender-side forward progress behavior already added in `OPP.sol`.
- Add real business handlers for `Wire -> Ethereum`.
### Phase 4: Solana Endpoint
- Add an OPP gateway account model and authority PDA.
- Convert current admin settlement instructions into OPP-executable handlers.
- Emit outbound request/fact messages for the existing TODO sites.
### Phase 5: End-To-End Instant Swap Tests
Build the first full-path tests around request/response pairs that prove route independence:
- `Solana -> Wire` withdraw request followed by `Wire -> Solana` completion
- `Ethereum -> Wire` BAR unbond request followed by `Wire -> Ethereum` completion or slash
- one route deliberately delayed while another route continues progressing
## Non-Goals
- No global epoch shared across all routes.
- No requirement that opposite directions share batch numbers.
- No requirement that batch sealing blocks new origin-side messages.
- No special transport rule for instant swaps beyond per-stream ordering and request/response correlation.
## Decision
The system should be built as independent directed OPP streams with per-stream batching and per-stream operator consensus. "Epoch" becomes a local batching mechanism, not a system-wide gating primitive. Reverse-direction actions are correlated by payload IDs, not by transport epoch state.