378 lines
13 KiB
Markdown
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.
|