Add local instaswap E2E workspace harness
This commit is contained in:
377
OPP-COHESIVE-DESIGN.md
Normal file
377
OPP-COHESIVE-DESIGN.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user