commit 16d5eb3a8617b6ef7f6ba24f2e39dc6a743f3943 Author: Daniel Taghavi Date: Fri Apr 3 17:17:40 2026 -0400 Add local instaswap E2E workspace harness diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..1d32222 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,79 @@ +COMPOSE_PROJECT_NAME=skunk-net + +# Compose runtime user mapping. `./local-compose.sh` overwrites these with your +# current uid/gid at runtime so generated files are not owned by root. +LOCAL_UID=1000 +LOCAL_GID=1000 + +# Branch-aligned repo paths. For coworker handoff, clone each dependency repo +# into these top-level directories and check out the branch recorded in +# WORKSPACE-MANIFEST.md. +WIRE_SYSIO_REPO_DIR=wire-sysio +WIRE_SYSIO_CONTEXT=./wire-sysio +WIRE_ETHEREUM_REPO_DIR=wire-ethereum +CAPITAL_STAKING_REPO_DIR=capital-staking +CAPITAL_STAKING_BUILD_CONTEXT=./capital-staking +WIRE_HUB_WEBAPP_REPO_DIR=wire-hub-webapp + +# Wire SysIO +WIRE_SYSIO_IMAGE=wire/sysio:platform-dev-batchop-local +WIRE_NODE_ROOT=/srv/wire-node/chain-001 +WIRE_HTTP_PORT=8887 +WIRE_P2P_PORT=4443 +WIRE_ETH_DEPOT_ACCOUNT=sysio.dpeth +WIRE_SOL_DEPOT_ACCOUNT=sysio.dpsol +WIRE_INSTASWAP_ACCOUNT=sysio.iswap +WIRE_WYIELD_ACCOUNT=sysio.wyield +WIRE_TEST_USER_ACCOUNT=userinsta1 +WIRE_BATCH_OPERATOR_ETH_ACCOUNT=bopeth111111 +WIRE_BATCH_OPERATOR_SOL_ACCOUNT=bopsol111111 +WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID=wire-bopeth-1 +WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID=wire-bopsol-1 + +# Solana / Anchor +CAPITAL_STAKING_IMAGE=skunk-net/capital-staking-dev:local +SOLANA_DOCKER_IMAGE=skunk-net/solana-validator:local +SOLANA_RPC=http://solana-validator:8899 +RPC_URL=http://solana-validator:8899 +ANCHOR_PROVIDER_URL=http://solana-validator:8899 +ANCHOR_WALLET=/workspace/capital-staking/wallets/deploymentWallet/universalDeploymentWallet.json +REQUIRED_VALIDATORS=1 +SOLANA_SLOTS_PER_EPOCH=64 +SOLANA_GOSSIP_PORT=8001 +SOLANA_RPC_PORT=8899 +SOLANA_WS_PORT=8900 +SOLANA_FAUCET_PORT=9900 +SOLANA_RUN_SH_VALIDATOR_ARGS= + +# Ethereum / Hardhat +HARDHAT_PORT=8545 + +# Batch operator relay +WIRE_BATCH_OPERATOR_WIRE_RPC_URL=http://wire-nodeop:8887 +WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS=5000 + +# ETH route operator +WIRE_BATCH_OPERATOR_ETH_WIRE_DEPOT_ACCOUNT=sysio.dpeth +WIRE_BATCH_OPERATOR_ETH_WIRE_ACCOUNT= +WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID= +WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_SPEC= +WIRE_BATCH_OPERATOR_ETH_CLIENT_ID=eth-local-1 +WIRE_BATCH_OPERATOR_ETH_PROVIDER_ID=eth-local-1 +WIRE_BATCH_OPERATOR_ETH_PROVIDER_SPEC= +WIRE_BATCH_OPERATOR_ETH_RPC_URL=http://hardhat:8545 +WIRE_BATCH_OPERATOR_ETH_CHAIN_ID=31337 +WIRE_BATCH_OPERATOR_ETH_ABI_FILE=/wire/.local/e2e/ethereum-outpost-abi.json +WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS= +WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS= + +# SOL route operator +WIRE_BATCH_OPERATOR_SOL_WIRE_DEPOT_ACCOUNT=sysio.dpsol +WIRE_BATCH_OPERATOR_SOL_WIRE_ACCOUNT= +WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID= +WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_SPEC= +WIRE_BATCH_OPERATOR_SOL_CLIENT_ID=sol-local-1 +WIRE_BATCH_OPERATOR_SOL_PROVIDER_ID=sol-local-1 +WIRE_BATCH_OPERATOR_SOL_PROVIDER_SPEC= +WIRE_BATCH_OPERATOR_SOL_RPC_URL=http://solana-validator:8899 +WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID= +WIRE_BATCH_OPERATOR_SOL_IDL_FILE=/wire/capital-staking/target/idl/liqsol_core.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28e421e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.env.local +.codex +.local/ +.branch-worktrees/ +capital-staking/ +wire-ethereum/ +wire-sysio/ +wire-hub-webapp/ +wire-cdt/ +solana-docker-setup/ diff --git a/CHECKPOINT-2026-04-03.md b/CHECKPOINT-2026-04-03.md new file mode 100644 index 0000000..c99acd6 --- /dev/null +++ b/CHECKPOINT-2026-04-03.md @@ -0,0 +1,124 @@ +# Checkpoint 2026-04-03 + +This checkpoint captures the latest live state of the local instant-swap E2E bring-up. + +## What Is Working + +- Clean end-to-end bootstrap through `./local-e2e-up.sh` after resetting local state. +- `wire-nodeop` comes up cleanly and the Wire batch operator services stay running. +- Real `Eth -> Wire` delivery is working through the live ETH batch operator. +- The Wire depot emits a real outbound `Wire -> Sol` epoch for the instaswap settlement. +- Solana `ingest_epoch` succeeds for that outbound epoch on the fresh chain. + +## Current Blocker + +The Solana batch operator fails on `process_messages` for the first live +`instaswap_settlement` message. + +Observed error: + +```text +Transaction simulation failed: Error processing Instruction 1: +custom program error: 0xbc4 +``` + +`0xbc4` is decimal `3012`, which maps to Anchor `AccountNotInitialized`. + +## Current Diagnosis + +The transport path is not the blocker anymore. + +What we proved on the fresh cluster: + +- ETH emits `OPPMessage` / `OPPEpoch` +- ETH batch operator ingests that epoch into Wire +- Wire depot creates the `xswap` +- Wire depot emits outbound Solana epoch `0` +- Solana `ingest_epoch` accepts that epoch + +The remaining failure is inside Solana settlement execution for +`handle_instaswap_settlement()` in: + +- `capital-staking/programs/liqsol-core/src/instructions/opp/yield_hub.rs` + +The most likely issue is one missing or wrongly-derived account in the +batch-operator `process_messages` remaining account list, or one account that +the live deploy path did not initialize even though the test harness does. + +Primary suspects: + +- `extra_account_meta_list` +- `distribution_state` +- `bridge_vault_ata` +- `bucket_token_account` +- `liqsol_mint` + +## Reproduction + +Fresh bring-up: + +```bash +./local-compose.sh down -v +rm -rf .local/wire-node .local/wire-batch-operators .local/e2e +./local-e2e-up.sh +``` + +Trigger the live ETH instaswap: + +```bash +./local-compose.sh exec -T hardhat bash -lc \ + 'cd /workspace/wire-ethereum && \ + HH_SCRIPT_ARGS="instaswap 5.0000 1.0000 B3bVKtqAJxRp4RUbzmUXVUdF4CRtaHfjnNfKzCd9Fy7T" \ + npx hardhat run src/scripts/outpost/wire_yield_hub_live.ts --network anvil_local' +``` + +Inspect the Solana batch operator log: + +```bash +tail -n 120 .local/wire-batch-operators/sol/1/log/relay.log +``` + +Expected current failure: + +```text +Submitting solana process_messages for epoch 0 with 1 message(s) and 15 remaining account(s) +batch operator pass failed: -32002 ... custom program error: 0xbc4 +``` + +## Useful Live Checks + +Wire chain health: + +```bash +./local-compose.sh exec -T wire-nodeop clio -u http://127.0.0.1:8887 get info +``` + +Wire ETH depot inbound epochs: + +```bash +./local-compose.sh exec -T wire-nodeop \ + clio -u http://127.0.0.1:8887 get table sysio.dpeth ............2 oppepochin --limit 20 +``` + +Wire xswap rows: + +```bash +./local-compose.sh exec -T wire-nodeop \ + clio -u http://127.0.0.1:8887 get table sysio.iswap sysio.iswap xswaps --limit 20 +``` + +Wire Sol outbound epochs: + +```bash +./local-compose.sh exec -T wire-nodeop \ + clio -u http://127.0.0.1:8887 get table sysio.dpsol ............3 oppepochout --limit 20 +``` + +## Suggested Next Step + +Reproduce the failing Solana `process_messages` call with the same 15 remaining +accounts in a TypeScript helper or test so the full simulation logs identify the +exact uninitialized account. Once that account is known, patch either: + +- the local Solana deploy/bootstrap path, or +- the batch operator account derivation / account ordering. diff --git a/LOCAL-E2E.md b/LOCAL-E2E.md new file mode 100644 index 0000000..3eb8fa9 --- /dev/null +++ b/LOCAL-E2E.md @@ -0,0 +1,187 @@ +# Local Docker-First Stack + +This workspace can be run locally on Arch without shell-profile exports. + +For the current live checkpoint, known blocker, and exact repo matrix, see +`CHECKPOINT-2026-04-03.md` and `WORKSPACE-MANIFEST.md`. + +The setup below uses top-level dependency checkouts: + +- `wire-sysio` in Docker, using the maintained Dockerfile under `wire-sysio/etc/docker/Dockerfile` +- `wire-ethereum` in a Node 20 container on a local Hardhat network +- `capital-staking` in a custom Anchor/Solana toolchain container against the local `solana-docker-setup` genesis validator + +## What I Could Confirm + +- The remote setup document at `swarm.gitgo.app/.../TESTNET-SETUP-README.md` returned `Not found` on April 2, 2026, so this runbook is based on the local repos and `OPP.pdf`. +- `OPP.pdf` is still useful for the protocol shape, but message encoding details are stale because the live code is moving to protobuf-backed payloads. +- `wire-ethereum` still models OPP delivery around sequential epoch envelopes. +- `capital-staking` is the Solana-side outpost/wire harness in this workspace. +- `solana-docker-setup` is now the intended local validator path so epoch timing is configurable through `SOLANA_SLOTS_PER_EPOCH`. +- `wire-sysio` contains the local node/bootstrap path plus the batch-operator relay implementation we are exercising in this workspace. +- The current Solana withdraw flow still has a placeholder instead of real OPP emission: + `capital-staking/programs/liqsol-core/src/instructions/wire_syndication/desyndicate_liqsol.rs` + +## One-Time Setup + +```bash +cd /home/dtaghavi/Documents/Projects/skunk-net +cp .env.local.example .env.local +``` + +Then use the checked-in wrapper: + +```bash +./local-compose.sh ps +``` + +Nothing in this flow depends on `~/.bashrc`, and the wrapper injects your live uid/gid at runtime so container-generated files stay owned by your Arch user. Adjust `.env.local` if you want different ports, wallet path, or ledger location. + +Important Solana knobs: + +- `SOLANA_SLOTS_PER_EPOCH=64` keeps local epoch turnover fast for tests. +- `SOLANA_RUN_SH_VALIDATOR_ARGS` lets you append raw `agave-validator` flags from `.env.local`. + +## Build The Containers + +```bash +./local-compose.sh build capital-staking-dev wire-sysio-dev +``` + +If you also need to rebuild the Solana validator image explicitly: + +```bash +./local-compose.sh build solana-validator +``` + +`wire-sysio-dev` is the expensive image because it builds the Wire toolchain and Clang 18 layer. The upstream Wire docs call out a 32 GiB RAM expectation for this build path. + +## Solana / Capital Staking + +Start the local validator: + +```bash +./local-compose.sh up -d solana-validator +``` + +Check the local epoch cadence: + +```bash +./local-compose.sh run --rm --no-deps capital-staking-dev bash -lc ' + solana -u "$ANCHOR_PROVIDER_URL" epoch-info +' +``` + +Reset the local Solana ledger when you want a fresh cluster: + +```bash +./local-compose.sh down -v +./local-compose.sh up -d solana-validator +``` + +Prepare program IDs, build, deploy, and initialize the local cluster: + +```bash +./local-compose.sh run --rm capital-staking-dev bash -lc ' + solana config set --url "$ANCHOR_PROVIDER_URL" --keypair "$ANCHOR_WALLET" && + ./bash-scripts/prep-anchor-toml.sh && + npm ci && + ./bash-scripts/reset-local-cluster.sh --wait-for-cluster +' +``` + +Run the Wire-related Solana suites: + +```bash +./local-compose.sh run --rm capital-staking-dev bash -lc ' + solana config set --url "$ANCHOR_PROVIDER_URL" --keypair "$ANCHOR_WALLET" && + npm ci && + npm run test:wire-syndication && + npm run test:wire-pretokens && + npm run test:bar +' +``` + +Those are the closest existing local tests to the instant-swap / outpost lifecycle in this checkout. + +## Ethereum / OPP + +For contract-level OPP validation you can either run one-shot tests or keep a local Hardhat node up. + +One-shot OPP tests: + +```bash +./local-compose.sh run --rm wire-ethereum-dev bash -lc ' + cd /workspace/wire-ethereum && + npm ci && + npx hardhat test src/test/outpost/OPPSend.ts src/test/outpost/OPPRecv.ts +' +``` + +Long-lived local JSON-RPC: + +```bash +./local-compose.sh up -d hardhat +``` + +Then run additional tests or scripts from a disposable shell: + +```bash +./local-compose.sh run --rm wire-ethereum-dev bash +``` + +Inside the container: + +```bash +cd /workspace/wire-ethereum +npm ci +npx hardhat test src/test/outpost/Depositor.integration.ts +``` + +## Wire SysIO + +Make sure the Wire submodules are present once on the host: + +```bash +git -C wire-sysio submodule update --init --recursive +``` + +Bring up the local Wire node: + +```bash +./local-compose.sh up wire-nodeop +``` + +That service now: + +- builds the required `wire-sysio` targets inside Docker +- applies the local BoringSSL/OpenSSL pkg-config shim needed by this checkout +- runs `sys-util chain-configure` +- boots `kiod` and `nodeop` with the canonical 5-part `--signature-provider` format + +If you want it detached: + +```bash +./local-compose.sh up -d wire-nodeop +``` + +Check the node: + +```bash +curl -s http://127.0.0.1:${WIRE_HTTP_PORT:-8887}/v1/chain/get_info -X POST +``` + +## Current Protocol Gap + +From the code currently in this directory: + +- Ethereum OPP is still epoch-based and sequential, but outbound epoch rollover no longer stalls on an unsubmitted prior epoch. +- Solana wire syndication is still mostly local-accounting plus admin completion hooks. +- The Solana PostLaunch withdraw path logs that it should send an OPP message, but does not do it yet. +- I do not see the inbound Wire-side OPP gate you described inside `wire-sysio` itself in this checkout. + +That means the fastest path to a real end-to-end instant-swap test is: + +1. Stand up the local Solana and Ethereum stacks above. +2. Use the existing wire-related Solana tests and OPP Ethereum tests as the starting harness. +3. Identify where the missing Wire-side depot / batch-operator logic actually lives if it is not in these repos. diff --git a/OPP-COHESIVE-DESIGN.md b/OPP-COHESIVE-DESIGN.md new file mode 100644 index 0000000..0ded495 --- /dev/null +++ b/OPP-COHESIVE-DESIGN.md @@ -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. diff --git a/OPP.pdf b/OPP.pdf new file mode 100644 index 0000000..2b77005 Binary files /dev/null and b/OPP.pdf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e54b162 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# skunk-net-e2e + +This repo is the cross-repo local E2E harness for the instant-swap / OPP work. + +It is intentionally not a monorepo mirror of the underlying projects. The real +code stays in the original repos; this workspace repo tracks: + +- Docker compose and bootstrap scripts +- local environment examples +- protocol notes used for this bring-up +- a dated checkpoint with the current blocker and reproduction steps +- the dependency repo matrix and expected branches + +## Start Here + +1. Read `WORKSPACE-MANIFEST.md`. +2. Clone each dependency repo into the top-level paths listed there. +3. Check out the recorded branches in those repos. +4. Copy `.env.local.example` to `.env.local` if you need overrides. +5. Use `./local-e2e-up.sh` or the individual commands in `LOCAL-E2E.md`. + +## Key Files + +- `WORKSPACE-MANIFEST.md`: remotes, branches, and current local heads +- `CHECKPOINT-2026-04-03.md`: current live state, blocker, and next steps +- `LOCAL-E2E.md`: bring-up and runtime commands +- `OPP-COHESIVE-DESIGN.md`: directional OPP design notes +- `protocol/opp/v1/opp.proto`: shared protobuf draft used for the cohesive design diff --git a/WORKSPACE-MANIFEST.md b/WORKSPACE-MANIFEST.md new file mode 100644 index 0000000..5a55147 --- /dev/null +++ b/WORKSPACE-MANIFEST.md @@ -0,0 +1,66 @@ +# Workspace Manifest + +This workspace expects the dependency repos to be cloned into fixed top-level +paths under the workspace root. + +## Dependency Repos + +| Path | Remote | Working Branch | Current Head | +| --- | --- | --- | --- | +| `wire-sysio` | `https://swarm.gitgo.app/experimental/wire-sysio.git` | `codex/local-kv-batchop` | `0525ce01789c4fc5f8799a1c89f881908ed77b64` | +| `wire-ethereum` | `https://swarm.gitgo.app/experimental/wire-ethereum.git` | `runtime/opp-parity-ethereum` | `cd240e751330356dfe59511e0d97405194cedd2b` | +| `capital-staking` | `https://swarm.gitgo.app/experimental/capital-staking.git` | `runtime/opp-parity-solana` | `b9bd80d9b436c60d8010a1a119f37dab824742f7` | +| `wire-hub-webapp` | `https://swarm.gitgo.app/experimental/wire-hub-webapp.git` | `runtime/instaswap-ui-dev-clean` | `87d9ea1c3fadc169b494f4599ae4e27324539206` | +| `solana-docker-setup` | `https://swarm.gitgo.app/experimental/solana-docker-setup` | `master` | `5c9d912e4e1e4dbd1d84402f9210d7daf09ca420` | +| `wire-cdt` | `https://swarm.gitgo.app/experimental/wire-cdt.git` | `master` | `c81b0b90a7118975dcfdb53ca4da225226aae505` | + +## Local Change Summary + +`wire-sysio` +- KV migration for the depot / instaswap path. +- batch operator relay implementation for ETH and SOL flows. +- Wire bootstrap / nodeop local harness adjustments. +- Local branch does not have an upstream configured yet. + +`wire-ethereum` +- Live local outpost script updated for a real user/admin split and Solana base58 destination parsing. + +`capital-staking` +- Docker-validator Anchor target support. +- local deploy now initializes OPP epoch state. +- relay artifact writer added. +- local tranche-state init adjusted for the Docker validator path. + +`wire-hub-webapp` +- No local changes in this checkpoint. + +## Workspace Files Tracked Here + +- `docker-compose.local.yml` +- `local-compose.sh` +- `local-e2e-up.sh` +- `bootstrap-ethereum-local.sh` +- `bootstrap-solana-local.sh` +- `wire-nodeop-local.sh` +- `wire-batch-operator-local.sh` +- `.env.local.example` +- `LOCAL-E2E.md` +- `CHECKPOINT-2026-04-03.md` +- `OPP-COHESIVE-DESIGN.md` +- `protocol/opp/v1/opp.proto` +- `protocol/opp/v1/assertion-registry.md` + +## Expected Directory Layout + +```text +skunk-net/ + README.md + docker-compose.local.yml + local-e2e-up.sh + wire-sysio/ + wire-ethereum/ + capital-staking/ + wire-hub-webapp/ + solana-docker-setup/ + wire-cdt/ +``` diff --git a/bootstrap-ethereum-local.sh b/bootstrap-ethereum-local.sh new file mode 100755 index 0000000..597309d --- /dev/null +++ b/bootstrap-ethereum-local.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WIRE_ETHEREUM_DIR="${WIRE_ETHEREUM_DIR:-$(pwd)}" +WIRE_ETHEREUM_REPO_REL="${WIRE_ETHEREUM_REPO_REL:-$(realpath --relative-to "${ROOT_DIR}" "${WIRE_ETHEREUM_DIR}")}" +LOCAL_DEPLOY_DIR="${WIRE_ETHEREUM_DIR}/.local/deployments" +ARTIFACTS_DIR="${ROOT_DIR}/.local/e2e" +HARDHAT_RPC_URL="${HARDHAT_RPC_URL:-http://127.0.0.1:8545}" +WIRE_BATCH_ETH_RPC_URL="${WIRE_BATCH_ETH_RPC_URL:-http://hardhat:8545}" +HARDHAT_DEPLOYER_KEY="${HARDHAT_DEPLOYER_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" +ETH_CLIENT_ID="${ETH_CLIENT_ID:-eth-local-1}" +ETH_PROVIDER_ID="${ETH_PROVIDER_ID:-eth-local-1}" +ETH_ABI_FILE="${ETH_ABI_FILE:-}" + +wait_for_rpc() { + for _ in $(seq 1 60); do + if curl -fsS -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ + "${HARDHAT_RPC_URL}" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + + echo "Hardhat RPC did not become ready at ${HARDHAT_RPC_URL}" >&2 + exit 1 +} + +write_deploy_configs() { + mkdir -p "${LOCAL_DEPLOY_DIR}" + + cat >"${LOCAL_DEPLOY_DIR}/liqeth.json" <"${LOCAL_DEPLOY_DIR}/outpost.json" <"${out_file}" +const fs = require("fs"); +const path = require("path"); +const { ethers } = require("ethers"); + +const rootDir = process.env.ROOT_DIR; +const repoDir = process.env.WIRE_ETHEREUM_DIR; +const repoRel = process.env.WIRE_ETHEREUM_REPO_REL; +const providerId = process.env.ETH_PROVIDER_ID; +const clientId = process.env.ETH_CLIENT_ID; +const privateKey = process.env.HARDHAT_DEPLOYER_KEY; +const rpcUrl = process.env.WIRE_BATCH_ETH_RPC_URL; +const abiFileOverride = process.env.ETH_ABI_FILE; +const addrsPath = path.join(repoDir, ".local", "deployments", "outpost-addrs.json"); +const addrs = JSON.parse(fs.readFileSync(addrsPath, "utf8")); +const publicKey = new ethers.SigningKey(privateKey).publicKey; +const abiOutputPath = path.join(rootDir, ".local", "e2e", "ethereum-outpost-abi.json"); + +let abiFile = abiFileOverride; +if (!abiFile) { + const artifactPaths = [ + path.join(repoDir, "artifacts", "contracts", "outpost", "OPP.sol", "OPP.json"), + path.join(repoDir, "artifacts", "contracts", "outpost", "OPPInbound.sol", "OPPInbound.json"), + ]; + const combinedAbi = artifactPaths.flatMap((artifactPath) => { + const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8")); + if (!Array.isArray(artifact.abi)) { + throw new Error(`Artifact missing abi array: ${artifactPath}`); + } + return artifact.abi; + }); + fs.mkdirSync(path.dirname(abiOutputPath), { recursive: true }); + fs.writeFileSync(abiOutputPath, `${JSON.stringify(combinedAbi, null, 2)}\n`); + abiFile = "/wire/.local/e2e/ethereum-outpost-abi.json"; +} + +console.log(`LOCAL_ETH_CLIENT_ID=${clientId}`); +console.log(`LOCAL_ETH_PROVIDER_ID=${providerId}`); +console.log(`LOCAL_ETH_PROVIDER_SPEC=${providerId},ethereum,ethereum,${publicKey},KEY:${privateKey}`); +console.log(`LOCAL_ETH_RPC_URL=${rpcUrl}`); +console.log(`LOCAL_ETH_OPP_ADDRESS=${addrs.OPP}`); +console.log(`LOCAL_ETH_OPP_INBOUND_ADDRESS=${addrs.OPPInbound}`); +console.log(`LOCAL_ETH_ABI_FILE=${abiFile}`); +console.log(`LOCAL_ETH_REPO_DIR=${repoRel}`); +console.log(`LOCAL_ETH_OUTPOST_ADDRS=${path.join(rootDir, repoRel, ".local", "deployments", "outpost-addrs.json")}`); +EOF +} + +main() { + cd "${WIRE_ETHEREUM_DIR}" + wait_for_rpc + deploy_local_stack +write_artifacts +} + +main "$@" diff --git a/bootstrap-solana-local.sh b/bootstrap-solana-local.sh new file mode 100755 index 0000000..6ce7c23 --- /dev/null +++ b/bootstrap-solana-local.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CAPITAL_STAKING_DIR="${CAPITAL_STAKING_DIR:-$(pwd)}" +CAPITAL_STAKING_REPO_REL="${CAPITAL_STAKING_REPO_REL:-$(realpath --relative-to "${ROOT_DIR}" "${CAPITAL_STAKING_DIR}")}" +ARTIFACTS_DIR="${ROOT_DIR}/.local/e2e" +ANCHOR_PROVIDER_URL="${ANCHOR_PROVIDER_URL:-http://solana-validator:8899}" +ANCHOR_WALLET="${ANCHOR_WALLET:-${CAPITAL_STAKING_DIR}/wallets/deploymentWallet/universalDeploymentWallet.json}" +WIRE_BATCH_SOL_RPC_URL="${WIRE_BATCH_SOL_RPC_URL:-http://solana-validator:8899}" +AIRDROP_AMOUNT="${AIRDROP_AMOUNT:-100}" +LOCAL_BOOTSTRAP_ROOT="${LOCAL_BOOTSTRAP_ROOT:-${ROOT_DIR}/.local/solana-bootstrap}" +LOCAL_CARGO_HOME="${LOCAL_CARGO_HOME:-${LOCAL_BOOTSTRAP_ROOT}/cargo-home}" +LOCAL_SOLANA_HOME="${LOCAL_SOLANA_HOME:-${LOCAL_BOOTSTRAP_ROOT}/home}" +LOCAL_RUSTUP_HOME="${LOCAL_RUSTUP_HOME:-${LOCAL_BOOTSTRAP_ROOT}/rustup-home}" +LOCAL_SBF_SDK_PATH="${LOCAL_SBF_SDK_PATH:-${LOCAL_SOLANA_HOME}/sdk/sbf}" +CAPITAL_STAKING_FEATURES="${CAPITAL_STAKING_FEATURES:-development}" +SOL_CLIENT_ID="${SOL_CLIENT_ID:-sol-local-1}" +SOL_PROVIDER_ID="${SOL_PROVIDER_ID:-sol-local-1}" +SOL_PROGRAM_ID="${SOL_PROGRAM_ID:-}" +SOL_IDL_FILE="/wire/${CAPITAL_STAKING_REPO_REL}/target/idl/liqsol_core.json" + +prepare_sbf_sdk() { + local default_sbf_sdk="/opt/solana/.local/share/solana/install/active_release/bin/sdk/sbf" + + mkdir -p "$(dirname "${LOCAL_SBF_SDK_PATH}")" + if [[ ! -d "${LOCAL_SBF_SDK_PATH}" ]]; then + cp -a "${default_sbf_sdk}" "${LOCAL_SBF_SDK_PATH}" + fi + + export SBF_SDK_PATH="${LOCAL_SBF_SDK_PATH}" +} + +prepare_rustup_home() { + if [[ ! -d "${LOCAL_RUSTUP_HOME}/toolchains" ]]; then + mkdir -p "${LOCAL_RUSTUP_HOME}" + cp -a /opt/rustup/. "${LOCAL_RUSTUP_HOME}/" + fi + + export RUSTUP_HOME="${LOCAL_RUSTUP_HOME}" +} + +resolve_sol_program_id() { + if [[ -n "${SOL_PROGRAM_ID}" ]]; then + printf '%s\n' "${SOL_PROGRAM_ID}" + return 0 + fi + + node <<'EOF' +const fs = require("fs"); +const path = require("path"); +const { Keypair } = require("@solana/web3.js"); + +const keypairPath = path.resolve("target/deploy/liqsol_core-keypair.json"); +if (fs.existsSync(keypairPath)) { + const secret = Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, "utf8"))); + console.log(Keypair.fromSecretKey(secret).publicKey.toBase58()); + process.exit(0); +} + +const idlPath = path.resolve("target/idl/liqsol_core.json"); +if (fs.existsSync(idlPath)) { + const idl = JSON.parse(fs.readFileSync(idlPath, "utf8")); + if (idl.address) { + console.log(idl.address); + process.exit(0); + } +} + +process.exit(1); +EOF +} + +deploy_local_cluster() { + export HOME="${LOCAL_SOLANA_HOME}" + export XDG_CACHE_HOME="${HOME}/.cache" + export XDG_CONFIG_HOME="${HOME}/.config" + npm ci + export ANCHOR_PROVIDER_URL + export ANCHOR_WALLET + export AIRDROP_AMOUNT + export CAPITAL_STAKING_FEATURES + export CARGO_HOME="${LOCAL_CARGO_HOME}" + export SOLANA_RPC="${ANCHOR_PROVIDER_URL}" + mkdir -p "${HOME}" "${XDG_CACHE_HOME}" "${XDG_CONFIG_HOME}" "${CARGO_HOME}" "${LOCAL_BOOTSTRAP_ROOT}" + prepare_rustup_home + prepare_sbf_sdk + solana config set --url "${ANCHOR_PROVIDER_URL}" --keypair "${ANCHOR_WALLET}" >/dev/null + bash bash-scripts/local-deploy.sh --wait-for-cluster --features "${CAPITAL_STAKING_FEATURES}" + SOL_PROGRAM_ID="$(resolve_sol_program_id)" + export SOL_PROGRAM_ID +} + +set_signature_weight() { + ANCHOR_WALLET="${ANCHOR_WALLET}" \ + ANCHOR_PROVIDER_URL="${ANCHOR_PROVIDER_URL}" \ + SOL_PROGRAM_ID="${SOL_PROGRAM_ID}" \ + node <<'EOF' +const fs = require("fs"); +const anchor = require("@coral-xyz/anchor"); +const { Connection, Keypair, PublicKey } = require("@solana/web3.js"); +const idl = require("./target/idl/liqsol_core.json"); + +const walletPath = process.env.ANCHOR_WALLET; +const providerUrl = process.env.ANCHOR_PROVIDER_URL; +const programId = new PublicKey(process.env.SOL_PROGRAM_ID || idl.address); +idl.address = programId.toBase58(); +const secret = Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "utf8"))); +const wallet = new anchor.Wallet(Keypair.fromSecretKey(secret)); +const provider = new anchor.AnchorProvider(new Connection(providerUrl, "confirmed"), wallet, { + commitment: "confirmed", +}); +anchor.setProvider(provider); + +const program = new anchor.Program(idl, provider); +const [globalConfig] = PublicKey.findProgramAddressSync([Buffer.from("global_config")], programId); +const [controllerState] = PublicKey.findProgramAddressSync([Buffer.from("stake_controller")], programId); + +async function main() { + await program.methods + .updateConfigU16({ oppRequiredSignatureWeight: {} }, 1) + .accounts({ + globalConfig, + controllerState, + authority: wallet.publicKey, + }) + .rpc(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); +EOF +} + +write_artifacts() { + local out_file="${ARTIFACTS_DIR}/solana.env" + + mkdir -p "${ARTIFACTS_DIR}" + + ANCHOR_WALLET="${ANCHOR_WALLET}" \ + SOL_PROVIDER_ID="${SOL_PROVIDER_ID}" \ + SOL_CLIENT_ID="${SOL_CLIENT_ID}" \ + WIRE_BATCH_SOL_RPC_URL="${WIRE_BATCH_SOL_RPC_URL}" \ + SOL_PROGRAM_ID="${SOL_PROGRAM_ID}" \ + SOL_IDL_FILE="${SOL_IDL_FILE}" \ + CAPITAL_STAKING_REPO_REL="${CAPITAL_STAKING_REPO_REL}" \ + node <<'EOF' >"${out_file}" +const fs = require("fs"); +const bs58 = require("bs58"); +const { Keypair } = require("@solana/web3.js"); + +const walletPath = process.env.ANCHOR_WALLET; +const providerId = process.env.SOL_PROVIDER_ID; +const clientId = process.env.SOL_CLIENT_ID; +const rpcUrl = process.env.WIRE_BATCH_SOL_RPC_URL; +const programId = process.env.SOL_PROGRAM_ID; +const idlFile = process.env.SOL_IDL_FILE; +const repoRel = process.env.CAPITAL_STAKING_REPO_REL; + +const secret = Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "utf8"))); +const keypair = Keypair.fromSecretKey(secret); +const secretBase58 = bs58.encode(Buffer.from(keypair.secretKey)); + +console.log(`LOCAL_SOL_CLIENT_ID=${clientId}`); +console.log(`LOCAL_SOL_PROVIDER_ID=${providerId}`); +console.log(`LOCAL_SOL_PROVIDER_SPEC=${providerId},solana,solana,${keypair.publicKey.toBase58()},KEY:${secretBase58}`); +console.log(`LOCAL_SOL_RPC_URL=${rpcUrl}`); +console.log(`LOCAL_SOL_PROGRAM_ID=${programId}`); +console.log(`LOCAL_SOL_IDL_FILE=${idlFile}`); +console.log(`LOCAL_SOL_REPO_DIR=${repoRel}`); +console.log(`LOCAL_SOL_OPP_REQUIRED_SIGNATURE_WEIGHT=1`); +EOF +} + +main() { + cd "${CAPITAL_STAKING_DIR}" + deploy_local_cluster + set_signature_weight + write_artifacts +} + +main "$@" diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..2c4b171 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,215 @@ +services: + wire-sysio-dev: + build: + context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + dockerfile: etc/docker/Dockerfile + target: platform-dev + additional_contexts: + clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18 + app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local} + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /wire + volumes: + - .:/wire + - ./.local/wire-node:/srv/wire-node + environment: + WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + command: ["bash", "-lc", "sleep infinity"] + + wire-nodeop: + build: + context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + dockerfile: etc/docker/Dockerfile + target: platform-dev + additional_contexts: + clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18 + app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local} + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /wire + init: true + volumes: + - .:/wire + - ./.local/wire-node:/srv/wire-node + environment: + WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_NODE_ROOT: ${WIRE_NODE_ROOT:-/srv/wire-node/chain-001} + WIRE_ARTIFACTS_DIR: /wire/.local/e2e + WIRE_ETH_DEPOT_ACCOUNT: ${WIRE_ETH_DEPOT_ACCOUNT:-sysio.dpeth} + WIRE_SOL_DEPOT_ACCOUNT: ${WIRE_SOL_DEPOT_ACCOUNT:-sysio.dpsol} + WIRE_INSTASWAP_ACCOUNT: ${WIRE_INSTASWAP_ACCOUNT:-sysio.iswap} + WIRE_WYIELD_ACCOUNT: ${WIRE_WYIELD_ACCOUNT:-sysio.wyield} + WIRE_TEST_USER_ACCOUNT: ${WIRE_TEST_USER_ACCOUNT:-userinsta1} + WIRE_BATCH_OPERATOR_ETH_ACCOUNT: ${WIRE_BATCH_OPERATOR_ETH_ACCOUNT:-bopeth111111} + WIRE_BATCH_OPERATOR_SOL_ACCOUNT: ${WIRE_BATCH_OPERATOR_SOL_ACCOUNT:-bopsol111111} + WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID:-wire-bopeth-1} + WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID:-wire-bopsol-1} + ports: + - "${WIRE_HTTP_PORT:-8887}:8887" + - "${WIRE_P2P_PORT:-4443}:4443" + command: ["bash", "-lc", "bash /wire/wire-nodeop-local.sh"] + + batch-operator-eth-1: + build: + context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + dockerfile: etc/docker/Dockerfile + target: platform-dev + additional_contexts: + clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18 + app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local} + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /wire + init: true + volumes: + - .:/wire + environment: + WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_BATCH_OPERATOR_ROUTE: eth + WIRE_BATCH_OPERATOR_INDEX: "1" + WIRE_BATCH_OPERATOR_ROOT: /wire/.local/wire-batch-operators + WIRE_BATCH_OPERATOR_ARTIFACTS_DIR: /wire/.local/e2e + WIRE_BATCH_OPERATOR_WIRE_RPC_URL: ${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-http://wire-nodeop:8887} + WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT: ${WIRE_BATCH_OPERATOR_ETH_WIRE_DEPOT_ACCOUNT:-sysio.dpeth} + WIRE_BATCH_OPERATOR_WIRE_ACCOUNT: ${WIRE_BATCH_OPERATOR_ETH_WIRE_ACCOUNT:-} + WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID:-} + WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_SPEC:-} + WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID: ${WIRE_BATCH_OPERATOR_ETH_CLIENT_ID:-eth-local-1} + WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_ETH_PROVIDER_ID:-eth-local-1} + WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_ETH_PROVIDER_SPEC:-} + WIRE_BATCH_OPERATOR_SOURCE_RPC_URL: ${WIRE_BATCH_OPERATOR_ETH_RPC_URL:-http://hardhat:8545} + WIRE_BATCH_OPERATOR_ETH_CHAIN_ID: ${WIRE_BATCH_OPERATOR_ETH_CHAIN_ID:-31337} + WIRE_BATCH_OPERATOR_ETH_ABI_FILE: ${WIRE_BATCH_OPERATOR_ETH_ABI_FILE:-/wire/.local/e2e/ethereum-outpost-abi.json} + WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS: ${WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS:-} + WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS: ${WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS:-} + WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS: ${WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS:-5000} + depends_on: + wire-nodeop: + condition: service_started + hardhat: + condition: service_started + command: ["bash", "-lc", "bash /wire/wire-batch-operator-local.sh eth 1"] + + batch-operator-sol-1: + build: + context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + dockerfile: etc/docker/Dockerfile + target: platform-dev + additional_contexts: + clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18 + app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio} + image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local} + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /wire + init: true + volumes: + - .:/wire + environment: + WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio} + WIRE_BATCH_OPERATOR_ROUTE: sol + WIRE_BATCH_OPERATOR_INDEX: "1" + WIRE_BATCH_OPERATOR_ROOT: /wire/.local/wire-batch-operators + WIRE_BATCH_OPERATOR_ARTIFACTS_DIR: /wire/.local/e2e + WIRE_BATCH_OPERATOR_WIRE_RPC_URL: ${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-http://wire-nodeop:8887} + WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT: ${WIRE_BATCH_OPERATOR_SOL_WIRE_DEPOT_ACCOUNT:-sysio.dpsol} + WIRE_BATCH_OPERATOR_WIRE_ACCOUNT: ${WIRE_BATCH_OPERATOR_SOL_WIRE_ACCOUNT:-} + WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID:-} + WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_SPEC:-} + WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID: ${WIRE_BATCH_OPERATOR_SOL_CLIENT_ID:-sol-local-1} + WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_SOL_PROVIDER_ID:-sol-local-1} + WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_SOL_PROVIDER_SPEC:-} + WIRE_BATCH_OPERATOR_SOURCE_RPC_URL: ${WIRE_BATCH_OPERATOR_SOL_RPC_URL:-http://solana-validator:8899} + WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID: ${WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID:-} + WIRE_BATCH_OPERATOR_SOL_IDL_FILE: ${WIRE_BATCH_OPERATOR_SOL_IDL_FILE:-/wire/.branch-worktrees/capital-staking-opp-parity/target/idl/liqsol_core.json} + WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS: ${WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS:-5000} + depends_on: + wire-nodeop: + condition: service_started + solana-validator: + condition: service_healthy + command: ["bash", "-lc", "bash /wire/wire-batch-operator-local.sh sol 1"] + + wire-ethereum-dev: + image: node:20-bookworm + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /workspace/${WIRE_ETHEREUM_REPO_DIR:-wire-ethereum} + volumes: + - .:/workspace + command: ["bash", "-lc", "sleep infinity"] + + hardhat: + image: node:20-bookworm + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /workspace/${WIRE_ETHEREUM_REPO_DIR:-wire-ethereum} + volumes: + - .:/workspace + ports: + - "${HARDHAT_PORT:-8545}:8545" + command: ["bash", "-lc", "npm ci && npx hardhat node --hostname 0.0.0.0"] + + capital-staking-dev: + build: + context: ${CAPITAL_STAKING_BUILD_CONTEXT:-./capital-staking} + dockerfile: docker/anchor-toolchain.Dockerfile + image: ${CAPITAL_STAKING_IMAGE:-skunk-net/capital-staking-dev:local} + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + working_dir: /workspace/${CAPITAL_STAKING_REPO_DIR:-capital-staking} + volumes: + - .:/workspace + environment: + ANCHOR_PROVIDER_URL: ${ANCHOR_PROVIDER_URL:-http://solana-validator:8899} + ANCHOR_WALLET: ${ANCHOR_WALLET:-/workspace/.branch-worktrees/capital-staking-opp-parity/wallets/deploymentWallet/universalDeploymentWallet.json} + DOCKER_CLUSTER: "1" + RPC_URL: ${RPC_URL:-http://solana-validator:8899} + REQUIRED_VALIDATORS: ${REQUIRED_VALIDATORS:-1} + SOLANA_RPC: ${SOLANA_RPC:-http://solana-validator:8899} + depends_on: + solana-validator: + condition: service_healthy + command: ["bash", "-lc", "sleep infinity"] + + solana-validator: + build: + context: ./solana-docker-setup + dockerfile: Dockerfile + image: ${SOLANA_DOCKER_IMAGE:-skunk-net/solana-validator:local} + working_dir: /solana + volumes: + - solana-validator-data:/solana-data + ports: + - "${SOLANA_GOSSIP_PORT:-8001}:8001/udp" + - "${SOLANA_RPC_PORT:-8899}:8899" + - "${SOLANA_WS_PORT:-8900}:8900" + - "${SOLANA_FAUCET_PORT:-9900}:9900" + environment: + IS_GENESIS: "true" + NODE_NAME: genesis + GOSSIP_HOST: solana-validator + GOSSIP_PORT: ${SOLANA_GOSSIP_PORT:-8001} + RPC_PORT: 8899 + RPC_BIND_ADDRESS: 0.0.0.0 + BIND_ADDRESS: 0.0.0.0 + FAUCET_ENABLE: "true" + FAUCET_ADDRESS: 0.0.0.0 + FAUCET_PORT: 9900 + SLOTS_PER_EPOCH: ${SOLANA_SLOTS_PER_EPOCH:-64} + DATA_DIR: /solana-data/genesis + SOLANA_RUN_SH_VALIDATOR_ARGS: ${SOLANA_RUN_SH_VALIDATOR_ARGS:-} + ulimits: + nofile: + soft: 1000000 + hard: 1000000 + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8899/health >/dev/null"] + interval: 5s + timeout: 5s + retries: 60 + start_period: 120s + +volumes: + solana-validator-data: diff --git a/local-compose.sh b/local-compose.sh new file mode 100755 index 0000000..4794231 --- /dev/null +++ b/local-compose.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${ROOT_DIR}/.env.local" + +if [[ ! -f "${ENV_FILE}" ]]; then + if [[ -f "${ROOT_DIR}/.env.local.example" ]]; then + cp "${ROOT_DIR}/.env.local.example" "${ENV_FILE}" + echo "Created ${ENV_FILE} from .env.local.example" >&2 + else + echo "Missing ${ENV_FILE} and .env.local.example" >&2 + exit 1 + fi +fi + +exec env \ + LOCAL_UID="$(id -u)" \ + LOCAL_GID="$(id -g)" \ + docker compose \ + --env-file "${ENV_FILE}" \ + -f "${ROOT_DIR}/docker-compose.local.yml" \ + "$@" diff --git a/local-e2e-up.sh b/local-e2e-up.sh new file mode 100755 index 0000000..5534864 --- /dev/null +++ b/local-e2e-up.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${ROOT_DIR}/.env.local" + +if [[ -f "${ENV_FILE}" ]]; then + set -a + # shellcheck disable=SC1090 + source "${ENV_FILE}" + set +a +fi + +WIRE_ETHEREUM_REPO_DIR="${WIRE_ETHEREUM_REPO_DIR:-wire-ethereum}" +CAPITAL_STAKING_REPO_DIR="${CAPITAL_STAKING_REPO_DIR:-capital-staking}" + +"${ROOT_DIR}/local-compose.sh" up -d hardhat solana-validator wire-nodeop + +"${ROOT_DIR}/local-compose.sh" exec hardhat bash -lc \ + "cd /workspace/${WIRE_ETHEREUM_REPO_DIR} && ROOT_DIR=/workspace WIRE_ETHEREUM_DIR=/workspace/${WIRE_ETHEREUM_REPO_DIR} bash /workspace/bootstrap-ethereum-local.sh" + +"${ROOT_DIR}/local-compose.sh" run --rm --no-deps capital-staking-dev bash -lc \ + "cd /workspace/${CAPITAL_STAKING_REPO_DIR} && ROOT_DIR=/workspace CAPITAL_STAKING_DIR=/workspace/${CAPITAL_STAKING_REPO_DIR} bash /workspace/bootstrap-solana-local.sh" + +"${ROOT_DIR}/local-compose.sh" up -d batch-operator-eth-1 batch-operator-sol-1 diff --git a/protocol/opp/v1/assertion-registry.md b/protocol/opp/v1/assertion-registry.md new file mode 100644 index 0000000..b74a988 --- /dev/null +++ b/protocol/opp/v1/assertion-registry.md @@ -0,0 +1,53 @@ +# OPP Assertion Registry + +This file is the numeric assertion registry for the shared OPP protobuf schema in `protocol/opp/v1/opp.proto`. + +## Rules + +- Numeric assertion IDs remain stable once assigned. +- Direction is part of the assignment. The same business meaning in opposite directions gets a different numeric ID. +- Payload bytes are protobuf messages from `wire.opp.v1`. +- Request/response flows correlate through `request_id`, not through shared batch numbers. + +## Assigned Ranges + +| Range | Route | Notes | +| --- | --- | --- | +| `2000-2099` | `Ethereum -> Wire` BAR | Existing Ethereum BAR assertions | +| `3000-3099` | `Ethereum -> Wire` Depositor/Pretoken | Existing Ethereum depositor and pretoken assertions | +| `4000-4199` | `Solana -> Wire` | Solana facts and requests | +| `5000-5199` | `Wire -> Solana` | Wire settlement and admin commands | +| `6000-6199` | `Wire -> Ethereum` | Wire settlement and admin commands | + +## Active Assignments + +| ID | Route | Lane | Payload | Current surface | +| --- | --- | --- | --- | --- | +| `2001` | `Ethereum -> Wire` | `bar` | `BondedActorMirror` | `wire-ethereum/contracts/outpost/BAR.sol` | +| `2002` | `Ethereum -> Wire` | `bar` | `UnbondedActorMirror` | `wire-ethereum/contracts/outpost/BAR.sol` | +| `2003` | `Ethereum -> Wire` | `bar` | `BondSlashMirror` | `wire-ethereum/contracts/outpost/BAR.sol` | +| `3001` | `Ethereum -> Wire` | `depositor` | `StakeMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` | +| `3002` | `Ethereum -> Wire` | `depositor` | `UnstakeMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` | +| `3004` | `Ethereum -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` | +| `3005` | `Ethereum -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `wire-ethereum/contracts/outpost/Pretoken.sol` | +| `3006` | `Ethereum -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` | +| `4001` | `Solana -> Wire` | `depositor` | `StakeMirror` | `capital-staking/.../wire_syndication/syndicate_liqsol.rs` | +| `4002` | `Solana -> Wire` | `depositor` | `WithdrawRequest` | `capital-staking/.../wire_syndication/desyndicate_liqsol.rs` | +| `4003` | `Solana -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `capital-staking/.../wire_pretokens/purchase_pretoken.rs` | +| `4004` | `Solana -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `capital-staking/.../wire_pretokens/purchase_pretokens_from_yield.rs` | +| `4101` | `Solana -> Wire` | `bar` | `RoleBondRequest` | `capital-staking/.../bar/bond_ops.rs` | +| `4102` | `Solana -> Wire` | `bar` | `RoleUnbondRequest` | `capital-staking/.../bar/bond_ops.rs` | +| `5001` | `Wire -> Solana` | `admin` | `CompleteWithdrawCommand` | `capital-staking/.../wire_config/admin_instructions.rs` | +| `5002` | `Wire -> Solana` | `admin` | `CompleteUnbondCommand` | `capital-staking/.../bar/admin_instructions.rs` | +| `5003` | `Wire -> Solana` | `admin` | `SlashBondCommand` | `capital-staking/.../bar/admin_instructions.rs` | +| `5004` | `Wire -> Solana` | `admin` | `AdminForceUnbondCommand` | `capital-staking/.../bar/admin_instructions.rs` | +| `5009` | `Wire -> Solana` | `admin` | `RequestRejected` | Reserved for failed Solana request outcomes | +| `6001` | `Wire -> Ethereum` | `admin` | `CompleteUnbondCommand` | Reserved for Ethereum endpoint implementation | +| `6002` | `Wire -> Ethereum` | `admin` | `SlashBondCommand` | Reserved for Ethereum endpoint implementation | +| `6009` | `Wire -> Ethereum` | `admin` | `RequestRejected` | Reserved for Ethereum endpoint implementation | + +## Notes + +- `3004`, `3005`, and `3006` all currently describe pretoken-related state, but they remain distinct numeric IDs because they originate from different Ethereum business actions. +- `4002` and the `500x` series are the first Solana request/response pair that should be implemented for end-to-end testing. +- `600x` is intentionally small right now because `Wire -> Ethereum` business handlers do not yet exist in this checkout. diff --git a/protocol/opp/v1/opp.proto b/protocol/opp/v1/opp.proto new file mode 100644 index 0000000..0a34f32 --- /dev/null +++ b/protocol/opp/v1/opp.proto @@ -0,0 +1,215 @@ +syntax = "proto3"; + +package wire.opp.v1; + +enum ChainKind { + CHAIN_KIND_UNKNOWN = 0; + CHAIN_KIND_WIRE = 1; + CHAIN_KIND_ETHEREUM = 2; + CHAIN_KIND_SOLANA = 3; + CHAIN_KIND_SUI = 4; +} + +enum OppLane { + OPP_LANE_UNSPECIFIED = 0; + OPP_LANE_DEPOSITOR = 1; + OPP_LANE_PRETOKEN = 2; + OPP_LANE_BAR = 3; + OPP_LANE_ADMIN = 4; + OPP_LANE_SWAP = 5; +} + +enum Role { + ROLE_UNSPECIFIED = 0; + ROLE_YIELD_OPERATOR = 1; + ROLE_BATCH_OPERATOR = 2; + ROLE_UNDERWRITER = 4; + ROLE_POOL_OPERATOR = 8; +} + +enum AssertionType { + ASSERTION_TYPE_UNSPECIFIED = 0; + + ASSERTION_TYPE_ETH_BAR_BONDED_ACTOR = 2001; + ASSERTION_TYPE_ETH_BAR_UNBONDED_ACTOR = 2002; + ASSERTION_TYPE_ETH_BAR_BOND_SLASHED = 2003; + + ASSERTION_TYPE_ETH_DEPOSITOR_STAKE = 3001; + ASSERTION_TYPE_ETH_DEPOSITOR_UNSTAKE = 3002; + ASSERTION_TYPE_ETH_DEPOSITOR_LIQ_PRETOKEN_PURCHASE = 3004; + ASSERTION_TYPE_ETH_PRETOKEN_PURCHASE = 3005; + ASSERTION_TYPE_ETH_DEPOSITOR_YIELD_PRETOKEN_PURCHASE = 3006; + + ASSERTION_TYPE_SOL_STAKE = 4001; + ASSERTION_TYPE_SOL_WITHDRAW_REQUEST = 4002; + ASSERTION_TYPE_SOL_PRETOKEN_PURCHASE = 4003; + ASSERTION_TYPE_SOL_YIELD_PRETOKEN_PURCHASE = 4004; + ASSERTION_TYPE_SOL_BAR_BOND_ROLE = 4101; + ASSERTION_TYPE_SOL_BAR_UNBOND_REQUEST = 4102; + + ASSERTION_TYPE_WIRE_SOL_COMPLETE_WITHDRAW = 5001; + ASSERTION_TYPE_WIRE_SOL_COMPLETE_UNBOND = 5002; + ASSERTION_TYPE_WIRE_SOL_SLASH_BOND = 5003; + ASSERTION_TYPE_WIRE_SOL_ADMIN_FORCE_UNBOND = 5004; + ASSERTION_TYPE_WIRE_SOL_REQUEST_REJECTED = 5009; + + ASSERTION_TYPE_WIRE_ETH_COMPLETE_UNBOND = 6001; + ASSERTION_TYPE_WIRE_ETH_SLASH_BOND = 6002; + ASSERTION_TYPE_WIRE_ETH_REQUEST_REJECTED = 6009; +} + +message StreamKey { + ChainKind from_chain = 1; + ChainKind to_chain = 2; + bytes origin_system = 3; + bytes destination_system = 4; + OppLane lane = 5; +} + +message Assertion { + AssertionType assertion_type = 1; + bytes payload = 2; +} + +message Uint256 { + // Exactly 32 bytes, big-endian, unsigned. + 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; +} + +message RequestRef { + bytes request_id = 1; + bytes origin_message_id = 2; + bytes source_event_id = 3; +} + +message StakeMirror { + bytes user = 1; + Uint256 principal = 2; + Uint256 shares = 3; + Uint256 index_at_mint = 4; +} + +message UnstakeMirror { + bytes user = 1; + Uint256 amount = 2; + Uint256 shares = 3; + Uint256 index_at_burn = 4; + bytes receipt_id = 5; +} + +message PretokenPurchaseMirror { + bytes buyer = 1; + Uint256 principal = 2; + Uint256 shares = 3; + Uint256 index_at_mint = 4; + Uint256 pretokens_out = 5; +} + +message BondedActorMirror { + bytes actor = 1; + bytes owner = 2; + bytes bond_level_id = 3; + Uint256 token_id = 4; + uint64 bonded_at_unix_seconds = 5; +} + +message UnbondedActorMirror { + bytes actor = 1; + bytes bond_level_id = 2; + Uint256 token_id = 3; + uint64 unbonded_at_unix_seconds = 4; +} + +message BondSlashMirror { + bytes actor = 1; + bytes bond_level_id = 2; + Uint256 slashed_amount = 3; +} + +message WithdrawRequest { + bytes request_id = 1; + bytes user = 2; + Uint256 amount = 3; +} + +message RoleBondRequest { + bytes request_id = 1; + bytes user = 2; + Role role = 3; + Uint256 principal = 4; + uint64 warmup_ends_at_unix_seconds = 5; +} + +message RoleUnbondRequest { + bytes request_id = 1; + bytes user = 2; + Role role = 3; +} + +message CompleteWithdrawCommand { + RequestRef request = 1; + bytes user = 2; + Uint256 amount = 3; +} + +message CompleteUnbondCommand { + RequestRef request = 1; + bytes user = 2; + Role role = 3; + Uint256 principal = 4; +} + +message SlashBondCommand { + RequestRef request = 1; + bytes user = 2; + Role role = 3; + Uint256 slashed_amount = 4; +} + +message AdminForceUnbondCommand { + RequestRef request = 1; + bytes user = 2; + Role role = 3; +} + +message RequestRejected { + RequestRef request = 1; + uint32 code = 2; + string reason = 3; +} diff --git a/wire-batch-operator-local.sh b/wire-batch-operator-local.sh new file mode 100644 index 0000000..0efae0e --- /dev/null +++ b/wire-batch-operator-local.sh @@ -0,0 +1,345 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WIRE_SYSIO_DIR="${WIRE_SYSIO_DIR:-${ROOT_DIR}/wire-sysio}" +BUILD_DIR="${WIRE_SYSIO_BUILD_DIR:-${WIRE_SYSIO_DIR}/build/debug-docker}" +VCPKG_DIR="${BUILD_DIR}/vcpkg_installed/x64-linux" +WIRE_CDT_DIR="${WIRE_CDT_DIR:-${ROOT_DIR}/wire-cdt}" +WIRE_CDT_BUILD_DIR="${WIRE_CDT_BUILD_DIR:-${WIRE_CDT_DIR}/build/debug-docker}" +WIRE_SYSIO_SUBMODULE_FALLBACK_DIR="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio}" +WIRE_SYSIO_BUILD_FALLBACK_DIR="${WIRE_SYSIO_BUILD_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio/build/debug-docker}" +ROUTE="${1:-${WIRE_BATCH_OPERATOR_ROUTE:-}}" +INDEX="${2:-${WIRE_BATCH_OPERATOR_INDEX:-1}}" +OP_ROOT="${WIRE_BATCH_OPERATOR_ROOT:-/srv/wire-batch-operators}" +WIRE_BATCH_OPERATOR_ARTIFACTS_DIR="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR:-${ROOT_DIR}/.local/e2e}" +CONFIG_DIR="${OP_ROOT}/${ROUTE}/${INDEX}/config" +DATA_DIR="${OP_ROOT}/${ROUTE}/${INDEX}/data" +LOG_DIR="${OP_ROOT}/${ROUTE}/${INDEX}/log" +LOG_FILE="${LOG_DIR}/relay.log" +BUILD_LOCK_FILE="${BUILD_DIR}/batch-operator-relay.lock" + +CMAKE_COMMON_ARGS=( + -S "${WIRE_SYSIO_DIR}" + -B "${BUILD_DIR}" + -G Ninja + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_TOOLCHAIN_FILE="${WIRE_SYSIO_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" + -DCMAKE_C_COMPILER=/opt/clang/clang-18/bin/clang + -DCMAKE_CXX_COMPILER=/opt/clang/clang-18/bin/clang++ + -DCMAKE_INSTALL_PREFIX=/opt/wire + -DCMAKE_PREFIX_PATH="/opt/wire;/opt/clang/clang-18;${WIRE_CDT_BUILD_DIR}" + -DENABLE_TESTS=OFF + -DBUILD_SYSTEM_CONTRACTS=ON + -DBUILD_TEST_CONTRACTS=OFF + -DCDT_ROOT="${WIRE_CDT_BUILD_DIR}" +) + +dir_is_empty() { + local path="$1" + [[ -d "${path}" ]] && [[ -z "$(find "${path}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]] +} + +link_submodule_from_fallback() { + local relative_path="$1" + local target_path="${WIRE_SYSIO_DIR}/${relative_path}" + local source_path="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR}/${relative_path}" + local link_source + + if [[ ! -e "${source_path}" ]]; then + return + fi + + link_source="$(realpath --relative-to "$(dirname "${target_path}")" "${source_path}")" + + if [[ -L "${target_path}" ]] && [[ "$(readlink "${target_path}")" == "${link_source}" ]]; then + return + fi + + if [[ -L "${target_path}" ]]; then + ln -sfn "${link_source}" "${target_path}" + return + fi + + if dir_is_empty "${target_path}"; then + rmdir "${target_path}" + fi + + if [[ ! -e "${target_path}" ]]; then + ln -sfn "${link_source}" "${target_path}" + fi +} + +hydrate_wire_submodules() { + link_submodule_from_fallback "libraries/appbase" + link_submodule_from_fallback "vcpkg" +} + +hydrate_wire_build_cache() { + local target_install_dir="${BUILD_DIR}/vcpkg_installed" + local source_install_dir="${WIRE_SYSIO_BUILD_FALLBACK_DIR}/vcpkg_installed" + local link_source + + if [[ ! -d "${source_install_dir}" ]]; then + return + fi + + mkdir -p "$(dirname "${target_install_dir}")" + link_source="$(realpath --relative-to "$(dirname "${target_install_dir}")" "${source_install_dir}")" + + if [[ -L "${target_install_dir}" ]] && [[ "$(readlink "${target_install_dir}")" == "${link_source}" ]]; then + return + fi + + if [[ -L "${target_install_dir}" ]]; then + ln -sfn "${link_source}" "${target_install_dir}" + return + fi + + if [[ -d "${target_install_dir}" ]] && [[ ! -f "${BUILD_DIR}/bin/batch-operator-relay" ]]; then + mv "${target_install_dir}" "${target_install_dir}.partial.$(date +%s)" + fi + + if [[ ! -e "${target_install_dir}" ]]; then + ln -s "${link_source}" "${target_install_dir}" + fi +} + +require_wire_submodules() { + hydrate_wire_submodules + + if [[ ! -f "${WIRE_SYSIO_DIR}/vcpkg/bootstrap-vcpkg.sh" ]] || [[ ! -f "${WIRE_SYSIO_DIR}/libraries/appbase/CMakeLists.txt" ]]; then + echo "wire-sysio submodules are missing. Run: git -C ${WIRE_SYSIO_DIR} submodule update --init --recursive" >&2 + exit 1 + fi +} + +write_pkgconfig_shims() { + local pc_dir="$1" + local lib_dir="$2" + + mkdir -p "${pc_dir}" + + cat >"${pc_dir}/libssl.pc" <"${pc_dir}/libcrypto.pc" </dev/null 2>&1 || true + + write_pkgconfig_shims "${VCPKG_DIR}/lib/pkgconfig" "${VCPKG_DIR}/lib" + write_pkgconfig_shims "${VCPKG_DIR}/debug/lib/pkgconfig" "${VCPKG_DIR}/debug/lib" + + cmake \ + "${CMAKE_COMMON_ARGS[@]}" \ + -DOPENSSL_INCLUDE_DIR="${VCPKG_DIR}/include" \ + -DOPENSSL_SSL_LIBRARY="${VCPKG_DIR}/debug/lib/libssl.a" \ + -DOPENSSL_CRYPTO_LIBRARY="${VCPKG_DIR}/debug/lib/libbscrypto.a" +} + +build_batch_operator() { + cmake --build "${BUILD_DIR}" --target batch-operator-relay -- -j"$(nproc)" +} + +require_var() { + local name="$1" + if [[ -z "${!name:-}" ]]; then + echo "Missing required environment variable: ${name}" >&2 + exit 1 + fi +} + +prepare_dirs() { + mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${LOG_DIR}" +} + +append_log() { + printf '[%s] %s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$*" >>"${LOG_FILE}" +} + +configure_and_build_batch_operator() { + mkdir -p "${BUILD_DIR}" + + append_log "Waiting for build lock: ${BUILD_LOCK_FILE}" + { + flock 9 + append_log "Acquired build lock" + + configure_wire_sysio >>"${LOG_FILE}" 2>&1 + build_batch_operator >>"${LOG_FILE}" 2>&1 + } 9>"${BUILD_LOCK_FILE}" +} + +wait_for_artifact() { + local path="$1" + local attempts="${2:-120}" + + for _ in $(seq 1 "${attempts}"); do + if [[ -f "${path}" ]]; then + return 0 + fi + sleep 1 + done + + echo "Timed out waiting for artifact: ${path}" >&2 + exit 1 +} + +source_env_file() { + local path="$1" + set -a + # shellcheck disable=SC1090 + source "${path}" + set +a +} + +load_generated_artifacts() { + local wire_env="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR}/wire.env" + local source_env + + wait_for_artifact "${wire_env}" + source_env_file "${wire_env}" + + case "${ROUTE}" in + eth) + source_env="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR}/ethereum.env" + ;; + sol) + source_env="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR}/solana.env" + ;; + *) + echo "Unsupported route '${ROUTE}'. Expected 'eth' or 'sol'." >&2 + exit 1 + ;; + esac + + wait_for_artifact "${source_env}" + source_env_file "${source_env}" + + if [[ "${ROUTE}" == "eth" ]]; then + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_RPC_URL="${LOCAL_WIRE_RPC_URL:-http://wire-nodeop:8887}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT="${LOCAL_WIRE_ETH_DEPOT_ACCOUNT:-}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_ACCOUNT="${LOCAL_WIRE_ETH_OPERATOR_ACCOUNT:-}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID="${LOCAL_WIRE_ETH_OPERATOR_PROVIDER_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC="${LOCAL_WIRE_ETH_OPERATOR_PROVIDER_SPEC:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID="${LOCAL_ETH_CLIENT_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID="${LOCAL_ETH_PROVIDER_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC="${LOCAL_ETH_PROVIDER_SPEC:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_RPC_URL="${LOCAL_ETH_RPC_URL:-}" + [[ -n "${WIRE_BATCH_OPERATOR_ETH_ABI_FILE:-}" ]] || WIRE_BATCH_OPERATOR_ETH_ABI_FILE="${LOCAL_ETH_ABI_FILE:-}" + [[ -n "${WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS:-}" ]] || WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS="${LOCAL_ETH_OPP_ADDRESS:-}" + [[ -n "${WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS:-}" ]] || WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS="${LOCAL_ETH_OPP_INBOUND_ADDRESS:-}" + else + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_RPC_URL="${LOCAL_WIRE_RPC_URL:-http://wire-nodeop:8887}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT="${LOCAL_WIRE_SOL_DEPOT_ACCOUNT:-}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_ACCOUNT="${LOCAL_WIRE_SOL_OPERATOR_ACCOUNT:-}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID="${LOCAL_WIRE_SOL_OPERATOR_PROVIDER_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC="${LOCAL_WIRE_SOL_OPERATOR_PROVIDER_SPEC:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID="${LOCAL_SOL_CLIENT_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID="${LOCAL_SOL_PROVIDER_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC="${LOCAL_SOL_PROVIDER_SPEC:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_RPC_URL="${LOCAL_SOL_RPC_URL:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID="${LOCAL_SOL_PROGRAM_ID:-}" + [[ -n "${WIRE_BATCH_OPERATOR_SOL_IDL_FILE:-}" ]] || WIRE_BATCH_OPERATOR_SOL_IDL_FILE="${LOCAL_SOL_IDL_FILE:-}" + fi +} + +build_args() { + local -n out_args=$1 + local route="$2" + + require_var WIRE_BATCH_OPERATOR_WIRE_ACCOUNT + require_var WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID + require_var WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC + require_var WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID + require_var WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID + require_var WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC + require_var WIRE_BATCH_OPERATOR_SOURCE_RPC_URL + + out_args=( + --config-dir "${CONFIG_DIR}" + --data-dir "${DATA_DIR}" + --batch-operator-wire-rpc-url "${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-http://wire-nodeop:8887}" + --batch-operator-wire-depot-account "${WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT:-sysio.depot}" + --batch-operator-wire-operator "${WIRE_BATCH_OPERATOR_WIRE_ACCOUNT},${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID}" + --batch-operator-poll-interval-ms "${WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS:-5000}" + --signature-provider "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC}" + --signature-provider "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC}" + ) + + if [[ "${route}" == "eth" ]]; then + require_var WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS + require_var WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS + require_var WIRE_BATCH_OPERATOR_ETH_ABI_FILE + + out_args+=( + --outpost-ethereum-client "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID},${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID},${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL},${WIRE_BATCH_OPERATOR_ETH_CHAIN_ID:-31337}" + --ethereum-abi-file "${WIRE_BATCH_OPERATOR_ETH_ABI_FILE}" + --batch-operator-ethereum-client-id "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID}" + --batch-operator-ethereum-opp-address "${WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS}" + --batch-operator-ethereum-opp-inbound-address "${WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS}" + ) + elif [[ "${route}" == "sol" ]]; then + require_var WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID + require_var WIRE_BATCH_OPERATOR_SOL_IDL_FILE + + out_args+=( + --outpost-solana-client "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID},${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID},${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL}" + --solana-idl-file "${WIRE_BATCH_OPERATOR_SOL_IDL_FILE}" + --batch-operator-solana-client-id "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID}" + --batch-operator-solana-program-id "${WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID}" + ) + else + echo "Unsupported route '${route}'. Expected 'eth' or 'sol'." >&2 + exit 1 + fi +} + +main() { + require_wire_submodules + prepare_dirs + load_generated_artifacts + append_log "Loaded generated artifacts for route=${ROUTE} index=${INDEX}" + configure_and_build_batch_operator + + local binary="${BUILD_DIR}/bin/batch-operator-relay" + local args=() + build_args args "${ROUTE}" + + append_log "Starting batch-operator route=${ROUTE} index=${INDEX}" + append_log "Config dir: ${CONFIG_DIR}" + append_log "Data dir: ${DATA_DIR}" + + exec "${binary}" "${args[@]}" >>"${LOG_FILE}" 2>&1 +} + +main "$@" diff --git a/wire-nodeop-local.sh b/wire-nodeop-local.sh new file mode 100644 index 0000000..8037480 --- /dev/null +++ b/wire-nodeop-local.sh @@ -0,0 +1,567 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WIRE_SYSIO_DIR="${WIRE_SYSIO_DIR:-${ROOT_DIR}/wire-sysio}" +BUILD_DIR="${WIRE_SYSIO_BUILD_DIR:-${WIRE_SYSIO_DIR}/build/debug-docker}" +VCPKG_DIR="${BUILD_DIR}/vcpkg_installed/x64-linux" +WIRE_CDT_DIR="${WIRE_CDT_DIR:-${ROOT_DIR}/wire-cdt}" +WIRE_CDT_BUILD_DIR="${WIRE_CDT_BUILD_DIR:-${WIRE_CDT_DIR}/build/debug-docker}" +TARGET_ROOT="${WIRE_NODE_ROOT:-/srv/wire-node/chain-001}" +CONTRACTS_DIR="${WIRE_SYSIO_CONTRACTS_DIR:-${BUILD_DIR}/contracts}" +WIRE_ARTIFACTS_DIR="${WIRE_ARTIFACTS_DIR:-${ROOT_DIR}/.local/e2e}" +WIRE_SYSIO_SUBMODULE_FALLBACK_DIR="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio}" +WIRE_SYSIO_BUILD_FALLBACK_DIR="${WIRE_SYSIO_BUILD_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio/build/debug-docker}" +WIRE_ETH_DEPOT_ACCOUNT="${WIRE_ETH_DEPOT_ACCOUNT:-sysio.dpeth}" +WIRE_SOL_DEPOT_ACCOUNT="${WIRE_SOL_DEPOT_ACCOUNT:-sysio.dpsol}" +WIRE_INSTASWAP_ACCOUNT="${WIRE_INSTASWAP_ACCOUNT:-sysio.iswap}" +WIRE_WYIELD_ACCOUNT="${WIRE_WYIELD_ACCOUNT:-sysio.wyield}" +WIRE_TEST_USER_ACCOUNT="${WIRE_TEST_USER_ACCOUNT:-userinsta1}" +WIRE_BATCH_OPERATOR_ETH_ACCOUNT="${WIRE_BATCH_OPERATOR_ETH_ACCOUNT:-bopeth111111}" +WIRE_BATCH_OPERATOR_SOL_ACCOUNT="${WIRE_BATCH_OPERATOR_SOL_ACCOUNT:-bopsol111111}" +WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID="${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID:-wire-bopeth-1}" +WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID="${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID:-wire-bopsol-1}" +WIRE_DEPOT_ACCOUNT_RAM_BYTES="${WIRE_DEPOT_ACCOUNT_RAM_BYTES:-8388608}" +WIRE_APP_ACCOUNT_RAM_BYTES="${WIRE_APP_ACCOUNT_RAM_BYTES:-8388608}" +WIRE_OPERATOR_ACCOUNT_RAM_BYTES="${WIRE_OPERATOR_ACCOUNT_RAM_BYTES:-1048576}" +WIRE_USER_ACCOUNT_RAM_BYTES="${WIRE_USER_ACCOUNT_RAM_BYTES:-1048576}" + +CMAKE_COMMON_ARGS=( + -S "${WIRE_SYSIO_DIR}" + -B "${BUILD_DIR}" + -G Ninja + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_TOOLCHAIN_FILE="${WIRE_SYSIO_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" + -DCMAKE_C_COMPILER=/opt/clang/clang-18/bin/clang + -DCMAKE_CXX_COMPILER=/opt/clang/clang-18/bin/clang++ + -DCMAKE_INSTALL_PREFIX=/opt/wire + -DCMAKE_PREFIX_PATH="/opt/wire;/opt/clang/clang-18;${WIRE_CDT_BUILD_DIR}" + -DENABLE_TESTS=OFF + -DBUILD_SYSTEM_CONTRACTS=ON + -DCDT_ROOT="${WIRE_CDT_BUILD_DIR}" +) + +cleanup() { + if [[ -n "${NODEOP_PID:-}" ]]; then + kill "${NODEOP_PID}" >/dev/null 2>&1 || true + wait "${NODEOP_PID}" >/dev/null 2>&1 || true + fi + if [[ -n "${KIOD_PID:-}" ]]; then + kill "${KIOD_PID}" >/dev/null 2>&1 || true + wait "${KIOD_PID}" >/dev/null 2>&1 || true + fi +} + +dir_is_empty() { + local path="$1" + [[ -d "${path}" ]] && [[ -z "$(find "${path}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]] +} + +link_submodule_from_fallback() { + local relative_path="$1" + local target_path="${WIRE_SYSIO_DIR}/${relative_path}" + local source_path="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR}/${relative_path}" + local link_source + + if [[ ! -e "${source_path}" ]]; then + return + fi + + link_source="$(realpath --relative-to "$(dirname "${target_path}")" "${source_path}")" + + if [[ -L "${target_path}" ]] && [[ "$(readlink "${target_path}")" == "${link_source}" ]]; then + return + fi + + if [[ -L "${target_path}" ]]; then + ln -sfn "${link_source}" "${target_path}" + return + fi + + if dir_is_empty "${target_path}"; then + rmdir "${target_path}" + fi + + if [[ ! -e "${target_path}" ]]; then + ln -sfn "${link_source}" "${target_path}" + fi +} + +hydrate_wire_submodules() { + link_submodule_from_fallback "libraries/appbase" + link_submodule_from_fallback "vcpkg" +} + +hydrate_wire_build_cache() { + local target_install_dir="${BUILD_DIR}/vcpkg_installed" + local source_install_dir="${WIRE_SYSIO_BUILD_FALLBACK_DIR}/vcpkg_installed" + local link_source + + if [[ ! -d "${source_install_dir}" ]]; then + return + fi + + mkdir -p "$(dirname "${target_install_dir}")" + link_source="$(realpath --relative-to "$(dirname "${target_install_dir}")" "${source_install_dir}")" + + if [[ -L "${target_install_dir}" ]] && [[ "$(readlink "${target_install_dir}")" == "${link_source}" ]]; then + return + fi + + if [[ -L "${target_install_dir}" ]]; then + ln -sfn "${link_source}" "${target_install_dir}" + return + fi + + if [[ -d "${target_install_dir}" ]] && [[ ! -f "${BUILD_DIR}/bin/nodeop" ]]; then + mv "${target_install_dir}" "${target_install_dir}.partial.$(date +%s)" + fi + + if [[ ! -e "${target_install_dir}" ]]; then + ln -s "${link_source}" "${target_install_dir}" + fi +} + +require_wire_submodules() { + hydrate_wire_submodules + + if [[ ! -f "${WIRE_SYSIO_DIR}/vcpkg/bootstrap-vcpkg.sh" ]] || [[ ! -f "${WIRE_SYSIO_DIR}/libraries/appbase/CMakeLists.txt" ]]; then + echo "wire-sysio submodules are missing. Run: git -C ${WIRE_SYSIO_DIR} submodule update --init --recursive" >&2 + exit 1 + fi +} + +write_pkgconfig_shims() { + local pc_dir="$1" + local lib_dir="$2" + + mkdir -p "${pc_dir}" + + cat >"${pc_dir}/libssl.pc" <"${pc_dir}/libcrypto.pc" </dev/null 2>&1 || true + + write_pkgconfig_shims "${VCPKG_DIR}/lib/pkgconfig" "${VCPKG_DIR}/lib" + write_pkgconfig_shims "${VCPKG_DIR}/debug/lib/pkgconfig" "${VCPKG_DIR}/debug/lib" + + cmake \ + "${CMAKE_COMMON_ARGS[@]}" \ + -DOPENSSL_INCLUDE_DIR="${VCPKG_DIR}/include" \ + -DOPENSSL_SSL_LIBRARY="${VCPKG_DIR}/debug/lib/libssl.a" \ + -DOPENSSL_CRYPTO_LIBRARY="${VCPKG_DIR}/debug/lib/libbscrypto.a" +} + +build_wire_sysio() { + cmake --build \ + "${BUILD_DIR}" \ + --target contracts_project-build sys-util clio kiod nodeop \ + -- -j"$(nproc)" +} + +configure_chain() { + export WIRE_ROOT="${WIRE_ROOT:-${WIRE_SYSIO_DIR}}" + export PATH="${BUILD_DIR}/bin:${PATH}" + + sys-util chain-configure \ + --contracts="${CONTRACTS_DIR}" \ + --target="${TARGET_ROOT}" \ + --template=aio \ + --overwrite +} + +extract_secret() { + local pattern="$1" + local file="$2" + sed -n "s/^${pattern}: //p" "${file}" +} + +run_clio() { + clio -u http://127.0.0.1:8887 "$@" +} + +run_clio_wallet() { + clio wallet "$@" +} + +key_file_for_account() { + local account_name="$1" + echo "${TARGET_ROOT}/secrets/${account_name}_key.txt" +} + +ensure_local_key() { + local account_name="$1" + local key_file + local output + + key_file="$(key_file_for_account "${account_name}")" + if [[ -f "${key_file}" ]]; then + return + fi + + output="$(clio create key --to-console)" + printf '%s\n' "${output}" >"${key_file}" +} + +import_key_if_needed() { + local key_file="$1" + local private_key + + private_key="$(extract_secret "Private key" "${key_file}")" + if [[ -z "${private_key}" ]]; then + echo "Missing private key in ${key_file}" >&2 + exit 1 + fi + + run_clio_wallet import --name default --private-key "${private_key}" >/dev/null 2>&1 || true +} + +ensure_account_exists() { + local account_name="$1" + local key_file + local public_key + + key_file="$(key_file_for_account "${account_name}")" + public_key="$(extract_secret "Public key" "${key_file}")" + if [[ -z "${public_key}" ]]; then + echo "Missing public key in ${key_file}" >&2 + exit 1 + fi + + if run_clio get account "${account_name}" >/dev/null 2>&1; then + return + fi + + run_clio create account sysio "${account_name}" "${public_key}" >/dev/null +} + +ensure_contract_deployed() { + local account_name="$1" + local contract_dir="$2" + + run_clio set contract "${account_name}" "${contract_dir}" >/dev/null +} + +ensure_account_ram() { + local account_name="$1" + local ram_bytes="$2" + + run_clio push action \ + sysio \ + setacctram \ + "[\"${account_name}\",${ram_bytes}]" \ + -p sysio@active >/dev/null +} + +ensure_account_code_permission() { + local account_name="$1" + + run_clio set account permission \ + "${account_name}" \ + active \ + --add-code \ + -p "${account_name}@active" >/dev/null +} + +table_has_rows() { + local account="$1" + local scope="$2" + local table="$3" + ! run_clio get table "${account}" "${scope}" "${table}" | grep -Eq '"rows": *\[[[:space:]]*\]' +} + +currency_exists() { + local code="$1" + local output + + output="$(run_clio get currency stats sysio.token "${code}" 2>/dev/null || true)" + grep -q "\"${code}\"" <<<"${output}" +} + +wire_env_path() { + echo "${WIRE_ARTIFACTS_DIR}/wire.env" +} + +write_wire_artifacts() { + local eth_key_file + local sol_key_file + local eth_pub + local eth_priv + local sol_pub + local sol_priv + local out_file + + mkdir -p "${WIRE_ARTIFACTS_DIR}" + + eth_key_file="$(key_file_for_account "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}")" + sol_key_file="$(key_file_for_account "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}")" + eth_pub="$(extract_secret "Public key" "${eth_key_file}")" + eth_priv="$(extract_secret "Private key" "${eth_key_file}")" + sol_pub="$(extract_secret "Public key" "${sol_key_file}")" + sol_priv="$(extract_secret "Private key" "${sol_key_file}")" + out_file="$(wire_env_path)" + + cat >"${out_file}" </dev/null + fi + + if ! table_has_rows "${WIRE_SOL_DEPOT_ACCOUNT}" "${WIRE_SOL_DEPOT_ACCOUNT}" depotstate; then + run_clio push action \ + "${WIRE_SOL_DEPOT_ACCOUNT}" \ + init \ + "[3,\"sysio.token\"]" \ + -p "${WIRE_SOL_DEPOT_ACCOUNT}@active" >/dev/null + fi + + if ! currency_exists WIRE; then + run_clio push action \ + sysio.token \ + create \ + "[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000000.0000 WIRE\"]" \ + -p sysio.token@active >/dev/null + fi + + if ! currency_exists LIQETH; then + run_clio push action \ + sysio.token \ + create \ + "[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000000.0000 LIQETH\"]" \ + -p sysio.token@active >/dev/null + fi + + if ! currency_exists LIQSOL; then + run_clio push action \ + sysio.token \ + create \ + "[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000000.0000 LIQSOL\"]" \ + -p sysio.token@active >/dev/null + fi + + if ! run_clio get currency balance sysio.token "${WIRE_INSTASWAP_ACCOUNT}" WIRE | grep -q "WIRE"; then + run_clio push action \ + sysio.token \ + issue \ + "[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000.0000 WIRE\",\"local pool seed\"]" \ + -p "${WIRE_INSTASWAP_ACCOUNT}@active" >/dev/null + fi + + if ! table_has_rows "${WIRE_INSTASWAP_ACCOUNT}" "${WIRE_INSTASWAP_ACCOUNT}" config; then + run_clio push action \ + "${WIRE_INSTASWAP_ACCOUNT}" \ + init \ + "[\"sysio.token\",\"${WIRE_ETH_DEPOT_ACCOUNT}\",\"${WIRE_SOL_DEPOT_ACCOUNT}\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"4,WIRE\",\"4,LIQETH\",\"4,LIQSOL\",30]" \ + -p "${WIRE_INSTASWAP_ACCOUNT}@active" >/dev/null + fi + + if ! table_has_rows "${WIRE_WYIELD_ACCOUNT}" "${WIRE_WYIELD_ACCOUNT}" config; then + run_clio push action \ + "${WIRE_WYIELD_ACCOUNT}" \ + init \ + "[\"sysio.token\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"${WIRE_ETH_DEPOT_ACCOUNT}\",\"${WIRE_SOL_DEPOT_ACCOUNT}\",\"4,WIRE\",\"4,LIQETH\",\"4,LIQSOL\"]" \ + -p "${WIRE_WYIELD_ACCOUNT}@active" >/dev/null + fi + + run_clio push action \ + "${WIRE_INSTASWAP_ACCOUNT}" \ + ondeposit \ + "[2,\"${eth_seed_id}\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"10000.0000 LIQETH\"]" \ + -p "${WIRE_ETH_DEPOT_ACCOUNT}@active" >/dev/null + + run_clio push action \ + "${WIRE_INSTASWAP_ACCOUNT}" \ + ondeposit \ + "[3,\"${sol_seed_id}\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"10000.0000 LIQSOL\"]" \ + -p "${WIRE_SOL_DEPOT_ACCOUNT}@active" >/dev/null + + if ! run_clio get table "${WIRE_ETH_DEPOT_ACCOUNT}" 2 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\""; then + run_clio push action \ + "${WIRE_ETH_DEPOT_ACCOUNT}" \ + regoperator \ + "[\"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\",1,\"010101010101010101010101010101010101010101010101010101010101010101\",\"0202020202020202020202020202020202020202020202020202020202020202\",\"1000.0000 WIRE\"]" \ + -p "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}@active" >/dev/null + fi + + if ! run_clio get table "${WIRE_SOL_DEPOT_ACCOUNT}" 3 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\""; then + run_clio push action \ + "${WIRE_SOL_DEPOT_ACCOUNT}" \ + regoperator \ + "[\"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\",1,\"030303030303030303030303030303030303030303030303030303030303030303\",\"0404040404040404040404040404040404040404040404040404040404040404\",\"1000.0000 WIRE\"]" \ + -p "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}@active" >/dev/null + fi + + if ! run_clio get table "${WIRE_ETH_DEPOT_ACCOUNT}" 2 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\",.*\"status\": 1"; then + run_clio push action \ + "${WIRE_ETH_DEPOT_ACCOUNT}" \ + activateop \ + "[\"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\"]" \ + -p "${WIRE_ETH_DEPOT_ACCOUNT}@active" >/dev/null + fi + + if ! run_clio get table "${WIRE_SOL_DEPOT_ACCOUNT}" 3 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\",.*\"status\": 1"; then + run_clio push action \ + "${WIRE_SOL_DEPOT_ACCOUNT}" \ + activateop \ + "[\"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\"]" \ + -p "${WIRE_SOL_DEPOT_ACCOUNT}@active" >/dev/null + fi + + write_wire_artifacts +} + +start_nodeop() { + local key_file="${TARGET_ROOT}/secrets/sysio_key.txt" + local wallet_pw + local sys_public_key + local sys_private_key + local sys_bls_public_key + local sys_bls_private_key + local ready=0 + + wallet_pw="$(cat "${TARGET_ROOT}/secrets/sysio_wallet_pw.txt")" + sys_public_key="$(extract_secret "Public key" "${key_file}")" + sys_private_key="$(extract_secret "Private key" "${key_file}")" + sys_bls_public_key="$(extract_secret "BLS Pub key" "${key_file}")" + sys_bls_private_key="$(extract_secret "BLS Priv key" "${key_file}")" + + pkill -9 kiod >/dev/null 2>&1 || true + kiod --wallet-dir "${TARGET_ROOT}/wallet" >"${TARGET_ROOT}/kiod.log" 2>&1 & + KIOD_PID=$! + + sleep 1 + clio wallet unlock --name default --password "${wallet_pw}" >/dev/null + + nodeop \ + --config-dir "${TARGET_ROOT}/config" \ + --data-dir "${TARGET_ROOT}/data" \ + --genesis-json "${TARGET_ROOT}/config/genesis.json" \ + --contracts-console \ + --signature-provider "wire-${sys_public_key},wire,wire,${sys_public_key},KEY:${sys_private_key}" \ + --signature-provider "wire-bls-${sys_bls_public_key},wire,wire_bls,${sys_bls_public_key},KEY:${sys_bls_private_key}" \ + >"${TARGET_ROOT}/nodeop.log" 2>&1 & + NODEOP_PID=$! + + for _ in $(seq 1 30); do + if clio -u http://127.0.0.1:8887 get info >/dev/null 2>&1; then + ready=1 + break + fi + sleep 1 + done + + if [[ "${ready}" -ne 1 ]]; then + echo "nodeop did not become ready within 30 seconds" >&2 + tail -n 80 "${TARGET_ROOT}/nodeop.log" >&2 || true + return 1 + fi + + bootstrap_wire_stack + + wait "${NODEOP_PID}" +} + +trap cleanup EXIT + +require_wire_submodules +configure_wire_sysio +build_wire_sysio +configure_chain +start_nodeop