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

13 KiB

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:

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:

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.