opp-part-3: Operator and Reserve Management#350
Draft
jglanz wants to merge 17 commits into
Draft
Conversation
…ed-slash + WITHDRAW_REMIT bodies
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 1. Closes the protobuf
gaps that block the new collateral / underwriter / variance-revert lifecycle:
types.proto AttestationType:
+ UNDERWRITE_INTENT_COMMIT (60953) — single attestation per (uw, outpost) leg;
depot-side race resolver picks the winner.
+ UNDERWRITE_INTENT_REJECT (60954) — underwriter explicitly steps back.
+ SWAP_REVERT (60955) — depot -> source outpost when variance
check fails; outpost refunds depositor.
- Marked legacy as DEPRECATED (PRETOKEN_*, EPOCH_SYNC, WIRE_TOKEN_PURCHASE,
UNDERWRITE_INTENT/_CONFIRM/_REJECT/_UNLOCK quartet). Kept on the wire so
in-flight call sites (Depositor.sol, Pretoken.sol, OperatorRegistry.sol)
keep encoding/decoding while we migrate them in later tasks.
attestations.proto:
+ UnderwriteIntentCommit — { uw_account, uw_ext_chain_addr, uw_request_id,
outpost_id, signature }. No chain-side lock
fields — bond + lock state are entirely depot-side.
+ UnderwriteIntentReject — { uw_account, uw_request_id, reason }.
+ SwapRevert — { original_swap_message_id, depositor,
refund_amount, reason }.
+ NativeYieldReward — full message body for the previously-bodyless
NATIVE_YIELD_REWARD enum value.
+ ReserveTarget — KIND_LP / KIND_BURN / KIND_TREASURY hint carried
by SlashOperator so the outpost knows where to
route slashed funds.
~ SlashOperator — added chain, token_kind, amount, lp_target,
slashed_at_epoch fields. Backward-compatible:
existing senders continue to work; new senders
populate the new fields. Used by both
sysio.opreg::slash (immediate, unlocked portion)
and sysio.uwrit::release (deferred, per lock
that resolves while op is SLASHED).
~ OperatorAction — added ACTION_TYPE_WITHDRAW_REQUEST (3),
ACTION_TYPE_WITHDRAW_REMIT (4),
ACTION_TYPE_WITHDRAW_CONFIRMED (5), plus
request_id field for linkage. Legacy WITHDRAW=2
kept + deprecated — replaced by 3-stage flow
with depot-side 2-epoch wait + authex-resolved
destination on REMIT.
~ SwapRequest — added quoted_destination_amount,
quote_tolerance_bps, quote_timestamp_ms for
the variance-tolerance check.
~ StakingReward — replaced 2-field stub with full per-staker share
model (outpost_id, staker_wire_account,
share_bps, period_start_ms, period_end_ms,
reward_amount).
Bundles regenerated via libraries/opp/tools/scripts/generate-opp-bundles.fish:
build/opp/{solana,typescript,solidity}/ all clean.
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # 89/89 targets
$BUILD_DIR/libraries/opp/test_opp # passed
$BUILD_DIR/contracts/tests/contracts_unit_test # 151/151 passed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + slash-to-LP
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 2. Replaces the
stake_entry vector with the corrected ledger model: one aggregate balance
row per (operator, chain, token_kind), a single read-only rollup that
unifies how every consumer sees spendable balance, and a deferred-slash
flow that lets in-flight underwriter UWREQs settle naturally.
Storage refactor:
- operator_entry.stakes: vector<stake_entry> -> balances: vector<balance_entry>
where balance_entry = {chain, token_kind, balance, last_updated_ms}.
- chain_min_bond replaces stake_requirement; carries token_kind explicitly.
- new wtdwqueue table (request_id, account, chain, token_kind, amount,
eligible_at_epoch, requested_at_epoch).
- new dellog table (log_id, account, epoch, delivered, ts_ms) for the
rolling-24h batch-op delivery buffer that drives
administrative termination.
- new opcounters singleton holding next_withdraw_id / next_dellog_id so the
auto-increment stays monotonic across action calls.
- operator_entry gains status_reason field (populated on slash / terminate).
- new uwrit_readonly::locks_t mirror in the .cpp matches the planned shape
of sysio.uwrit::locks (Task 3); reads from it now return zero rows and
will start returning real data once Task 3 lands the matching write side.
Action set:
+ wirestake(account, amount) — operator-auth WIRE-direct bond.
+ wireunstake(account, amount) — queues a WIRE-direct withdraw.
+ deposit(account, chain, token_kind,
amount, outpost_tx_hash) — internal; outpost-driven via msgch.
+ queuewtdw(account, chain, token_kind,
amount) — internal; outpost-driven WITHDRAW_REQUEST.
+ cancelwtdw(account, request_id) — operator cancels a pending withdraw.
+ flushwtdw(current_epoch) — drains matured rows from the queue;
issues TOKEN_ACCOUNT::transfer for
WIRE-direct, OPERATOR_ACTION(WITHDRAW_REMIT)
for outpost-side; drops slashed
rows silently per the plan.
+ available(account, chain, token_kind) — read-only rollup:
balance - sum(active locks)
- sum(pending withdraws)
gated by slashed/terminated status.
+ recorddel(account, epoch, delivered) — append delivery hit/miss for the
rolling-24h buffer.
+ termcheck(account) — evaluate the buffer; trigger
terminate_inline if >3 consecutive
misses or >5% in trailing 24h.
+ terminate(account, reason) — administrative removal; status->
TERMINATED, unlocked balance
remitted to authex destination
(deferred-remit via uwrit::release
for the locked portion in Task 3).
- stake(...) — REMOVED. Replaced by deposit
(msgch-driven) + wirestake
(operator-driven) + the queue
flow for withdraws.
Slash refactor:
- slash(account, reason) now snapshots `slashable_now = balance - sum(locks)`
per (chain, token_kind), decrements those amounts from balance, and emits
one SLASH_OPERATOR per non-zero pair (with chain, token_kind, amount,
lp_target=KIND_LP+paired_token, slashed_at_epoch on the new body fields).
The locked portion stays in balance; sysio.uwrit::release (Task 3) will
deferred-slash it as each lock resolves. WIRE-chain slashes don't go via
OPP — funds stay in the opreg account pending sysio.reserve (Task 5).
Eligibility:
- processprod / processbatch / processuw now use available_inline + per-role
chain_min_bond config; demote the operator on the first chain/token whose
available drops below the role minimum.
- meets_role_min(op, cfg) helper centralizes the math.
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/contracts/tests/contracts_unit_test # 151/151
$BUILD_DIR/contracts/tests/contracts_unit_test \
--run_test=sysio_opreg_tests # 10/10
$BUILD_DIR/libraries/libfc/test/test_fc # passed
$BUILD_DIR/libraries/opp/test_opp # passed
Backward compat:
- setconfig / regoperator / slash / prune signatures unchanged; existing
fixture tests (slash_permanent, regoperator_*, prune_requires_config,
multiple_bootstrapped_batch_ops, etc.) still pass.
- WASM/ABI updated in source tree per wire-sysio/CLAUDE.md.
Forward dependencies:
- sysio.uwrit::locks (Task 3): consumed by available()'s mirror struct.
- sysio.epoch::epochstate.current_epoch_index (existing): consumed by
get_current_epoch() helper.
- sysio.reserve::resolve_lp (Task 5): not yet wired; slash currently uses
a default ReserveTarget (KIND_LP + paired_token == bond's token_kind).
Once Task 5 lands the resolver call replaces this default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
uwrit: flat lock vector + COMMIT race resolver + deferred-slash release
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 3 (build-step 3
scope: lock vector + race resolver + release plumbing). Variance check
against sysio.reserve and REMIT/SWAP_REVERT emission are deferred to
Task 5 once sysio.reserve lands.
Storage refactor:
- Drop `collateral` table + `collateral_t` alias (operator bond now lives
in sysio.opreg::operators.balances; uwrit reads it via a kv::table mirror
in `opreg_readonly` namespace, matching opreg's struct layout exactly).
- Drop `uwledger` table + `underwriting_entry` (legacy intent submission
log; the new model tracks the COMMIT race entirely inside uwreqs).
- Drop `uw_request_t.locked_amounts: vector<locked_amount_t>` field.
- Add `locks` table (lock_entry: lock_id, uwreq_id, underwriter, chain,
token_kind, amount, created_at_epoch). Indexed by (underwriter, chain,
token_kind) composite — opreg's `available()` rollup mirror reads via
this index.
- Add `uwcounters` singleton with `next_lock_id` for monotonic ids.
- uw_request_t reshape: + src_chain/src_token_kind/src_amount,
+ dst_chain/dst_token_kind/dst_amount,
+ winner, committed_at_ms, settled_at_ms,
+ expires_at_epoch (10-epoch retention),
+ commits_by: vector<commit_entry> for the race
(each entry = underwriter, source_received_at_ms,
dest_received_at_ms, status, reason).
Action set:
+ setconfig(fee_bps) — simplified; fee distribution is
deferred to a follow-up.
+ createuwreq(att_id, type, data) — decodes SwapRequest (validates
type == SWAP), populates src/dst
from the proto, opens the UWREQ.
Variance check is wired in Task 5.
+ rcrdcommit(uwreq_id, underwriter,
outpost_id, from_chain) — auth=msgch; records per-leg arrival
in commits_by; runs try_select_winner
once both legs land.
+ rcrdreject(uwreq_id, underwriter,
reason) — auth=msgch; marks the underwriter's
entry RELEASED (loser).
+ release(uwreq_id) — auth=self; for each lock entry,
calls opreg::releaselock + erases
the lock row. opreg routes:
SLASHED -> SLASH_OPERATOR + balance--
TERMINATED -> WITHDRAW_REMIT + balance--
healthy -> no-op (lock removal frees
the funds via available()).
UWREQ flips to COMPLETED with
expires_at_epoch = now + 10.
+ expirelock(uwreq_id) — permissionless; calls release if the
oldest lock has aged past UWREQ_RETENTION_EPOCHS.
+ sumlocks(underwriter, chain,
token_kind) — read-only; sum of active locks.
- submituw / confirmuw / distfee /
updcltrl / slash — REMOVED. Replaced by record_commit /
try_select_winner / release; opreg
owns slashing now.
opreg companion change:
+ sysio.opreg::releaselock(account, chain, token_kind, amount)
— auth=uwrit; consults op.status:
SLASHED -> decrement balance + emit
SLASH_OPERATOR (deferred-slash).
TERMINATED -> decrement balance + emit
WITHDRAW_REMIT to authex
destination (deferred-remit).
else -> no-op.
chalg companion change:
- chalg::respond + chalg::slashop now call sysio.opreg::slash (instead of
sysio.uwrit::slash, which no longer exists). opreg is the canonical bond
ledger; it routes the slashable portion (balance - sum(active locks))
to LP and relies on uwrit::release to deferred-slash the locked portion
as each lock resolves. chalg's existing test cases (5/5) still pass
since they don't exercise the inline-slash path.
+ Added OPREG_ACCOUNT constant alongside the existing UWRIT_ACCOUNT in
chalg's hpp (UWRIT_ACCOUNT kept for the upcoming msgch/uwrit roundtrip
plumbing in later tasks).
Cross-contract reads:
- uwrit's `opreg_readonly::operators_t` + `wtdwqueue_t` mirrors match
opreg's struct shape exactly. The `available_via_mirrors` helper in
uwrit's anonymous namespace duplicates opreg's `available_inline`
formula (balance - sum_locks - sum_pending_withdraws, gated by status)
so the COMMIT race resolver can validate per-leg bond coverage without
a cross-contract action call.
- Reciprocal: opreg's `uwrit_readonly::locks_t` mirror added in Task 2
now reads real lock rows once uwrit's `locks` table is populated — the
rollup that previously returned 0 (because the table didn't exist) is
now live.
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/contracts/tests/contracts_unit_test # 149/149
$BUILD_DIR/contracts/tests/contracts_unit_test \
--run_test=sysio_uwrit_tests # 5/5 (new)
$BUILD_DIR/contracts/tests/contracts_unit_test \
--run_test=sysio_opreg_tests # 10/10
$BUILD_DIR/contracts/tests/contracts_unit_test \
--run_test=sysio_chalg_tests # 5/5
Test surface change:
- Removed 5 uwrit tests for deleted actions (updcltrl_increase,
updcltrl_decrease_nonexistent, submituw_without_config, submituw_basic,
submituw_duplicate_message).
- Added 5 new uwrit tests covering the new action surface (setconfig
simplified, fee-bps validation, auth gates on createuwreq/release/
expirelock).
- Net contract-test-suite delta: 151 -> 149 cases (legacy actions had
more breadth; deeper coverage of the new lock/race flows comes via
Task 13's flow tests).
Forward dependencies:
- sysio.msgch dispatch (Task 4): wires SWAP -> createuwreq, COMMIT/REJECT
-> rcrdcommit/rcrdreject, REMIT_CONFIRM -> release.
- sysio.reserve (Task 5): wires variance check + SWAP_REVERT emission
inside createuwreq, and resolve_lp(...) for SLASH_OPERATOR routing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
msgch: per-attestation-type dispatch on consensus reach
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 4. After `evalcons`
unpacks a consensus envelope, each attestation is now inline-dispatched to
the matching depot-side handler in addition to being recorded in the
`attestations` table for audit / outbound packing.
Inbound dispatch table (in sysio.msgch.cpp, anonymous namespace):
OPERATOR_ACTION -> dispatch_operator_action(...)
which switches on action_type:
DEPOSIT -> opreg::deposit
WITHDRAW_REQUEST -> opreg::queuewtdw
WITHDRAW_CONFIRMED -> audit-only no-op
WITHDRAW (legacy) -> no-op
UNDERWRITE_INTENT_COMMIT -> dispatch_underwrite_commit(...)
-> uwrit::rcrdcommit
UNDERWRITE_INTENT_REJECT -> dispatch_underwrite_reject(...)
-> uwrit::rcrdreject
SWAP -> uwrit::createuwreq (raw payload pass-through)
REMIT_CONFIRM -> uwrit::release (uwreq_id extracted from
Remit.original_message_id's low 8 bytes;
the depot-side encoder writes the uwreq id
into that field)
Out-of-scope (handlers land in later tasks; falls through to no-op):
RESERVE_BALANCE_SHEET / NATIVE_YIELD_REWARD / STAKING_REWARD -> Task 5 (reserve)
CHALLENGE_REQUEST / CHALLENGE_RESPONSE -> Task 6 (chalg)
STAKE / UNSTAKE / STAKE_UPDATE / STAKE_RESULT -> validator-staking task
Outbound-only or deprecated types are dropped silently:
REMIT, SWAP_REVERT, SLASH_OPERATOR, OPERATORS, BATCH_OPERATOR_GROUPS
(depot emits these — never inbound)
PRETOKEN_*, EPOCH_SYNC, WIRE_TOKEN_PURCHASE,
legacy UNDERWRITE_INTENT/_CONFIRM/_REJECT/_UNLOCK
(deprecated pre-launch)
Each handler decodes via the existing zpp::bits + no_size pattern
(matching how msgch already decodes inbound Envelopes), constructs the
inline action with msgch's auth, and sends. Decoder failures are silent
no-ops so a single malformed attestation can't take out the whole
consensus envelope. The attestation row stays in the `attestations`
table either way for re-dispatch / debug.
uwrit::release auth widened:
- Was require_auth(get_self()).
- Now: check(has_auth(MSGCH) || has_auth(self), ...).
- Two callers expected: msgch dispatch on REMIT_CONFIRM (msgch auth),
or uwrit::expirelock self-inline (uwrit's own auth).
- Test `release_requires_self_auth` updated -> `release_requires_msgch_or_self_auth`.
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/contracts/tests/contracts_unit_test # 149/149
$BUILD_DIR/contracts/tests/contracts_unit_test \
--run_test=sysio_uwrit_tests # 5/5
$BUILD_DIR/contracts/tests/contracts_unit_test \
--run_test=sysio_opreg_tests # 10/10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reserve: NEW sysio.reserve LP primitive + uwrit variance check + SWAP_REVERT
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 5. Adds the LP /
reserve management contract, then wires the depot-side variance-tolerance
check into uwrit's createuwreq path so out-of-tolerance SWAPs revert
before opening an underwriter race.
NEW contract: sysio.reserve
- lp_entry table (`lps`): one row per (chain, paired_token); each LP is
paired with WIRE on the depot side. Fields: chain, paired_token,
reserve_paired, reserve_wire, connector_weight_bps, last_updated_ms.
- setlp(chain, token, reserve_paired, reserve_wire, weight_bps)
— auth=self; admin provisioning. Re-call updates in place.
- quote(src_chain, src_token, dst_chain, dst_token, src_amount)
— read-only. Constant-product (xy=k) math via uint128 fixed-point.
Half-hops handled when src_token or dst_token is WIRE itself.
Returns 0 if any required LP is missing — callers' variance
check treats 0 as "no quote available, skip".
- creditlp(chain, token, paired_amount, wire_amount)
— auth=msgch; reserved for the NATIVE_YIELD_REWARD / STAKING_REWARD
dispatch wiring (msgch falls through to no-op for those types
today; see Task 4's dispatch_attestation).
The connector_weight_bps field is reserved for an asymmetric Bancor
extension; v1 ignores it and uses pure constant-product (matches Bancor
at weight=5000 / 50%). DEFAULT_CONNECTOR_WEIGHT_BPS = 5000.
Build wiring:
- contracts/CMakeLists.txt: add_subdirectory(sysio.reserve).
- sysio.reserve/CMakeLists.txt: standard add_contract / add_native_contract
pattern matching opreg/msgch/uwrit.
- sysio.reserve.{wasm,abi} bootstrapped + copied to source tree per
wire-sysio/CLAUDE.md convention.
uwrit::createuwreq variance check:
- Added reserve_readonly::lps_t mirror in uwrit.cpp matching the
sysio.reserve lp_entry struct shape exactly. Same kv::table mirror
pattern used by uwrit's opreg_readonly mirrors and opreg's authex_readonly
/ uwrit_readonly mirrors.
- reserve_quote() helper duplicates sysio.reserve::quote's constant-product
math locally so the check runs without a cross-contract action call.
- createuwreq signature extended: `outpost_id` parameter added so the
SWAP_REVERT (when variance fails) can route back to the source outpost.
msgch's dispatch_attestation updated to pass it.
- Variance formula: |current_quote - quoted| > quoted * tolerance_bps / 10000
-> emit SWAP_REVERT, no UWREQ created.
- When reserve_quote returns 0 (no LP provisioned), the check is implicitly
skipped — dev / smoke clusters without provisioned LPs continue to
operate; the check kicks in the moment LPs are present.
SWAP_REVERT emit (uwrit/anonymous/emit_swap_revert):
- Encodes opp::attestations::SwapRevert via zpp::bits no_size; queues
via msgch::queueout to the source outpost. The message's
original_swap_message_id carries the depot's attestation_id in its
low 8 bytes (matching the convention msgch's REMIT_CONFIRM dispatch
uses to recover the uwreq id from Remit.original_message_id).
Verification:
cmake --build $BUILD_DIR --target contracts_project -- -j$(($(nproc)-2))
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/contracts/tests/contracts_unit_test # 149/149
sysio.reserve.{wasm,abi} present in source tree.
Forward dependencies (Task 7+):
- opreg's slash currently uses a hardcoded ReserveTarget default
(KIND_LP, paired_token == bond's token_kind). Once outpost-side LPs
are wired up (Tasks 7/8 - Pool.sol / liqsol-core), opreg's slash can
optionally call sysio.reserve::resolve_lp for runtime overrides.
- Outpost-side LP integration (Pool.sol / liqsol-core) provides the
chain-side custody for the WIRE-paired LPs sysio.reserve quotes
against; the on-chain `lps` table is the depot's accounting view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
epoch: hook opreg::flushwtdw into advance for matured-withdraw drain
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 9 (build-step 9
scope: flushwithdraws hook). Each `epoch::advance` now inline-calls
`sysio.opreg::flushwtdw(current_epoch_index)`, which:
- Drains matured rows from `sysio.opreg::wtdwqueue` (rows whose
`eligible_at_epoch <= current_epoch_index`).
- Subtracts the queued amount from the operator's per-(chain, token_kind)
balance row in opreg.
- For WIRE-direct withdraws: issues a `TOKEN_ACCOUNT::transfer` from
sysio.opreg back to the operator on the WIRE chain.
- For outpost-side withdraws: emits OPERATOR_ACTION(WITHDRAW_REMIT)
via `sysio.msgch::queueout` to the matching outpost, with the
authex-resolved destination address carried as `actor`.
- Drops slashed-during-the-wait rows silently (per the plan §3.3).
The auth path: epoch sends the action with `permission_level{get_self(),
"owner"_n}` — opreg's flushwtdw `require_auth(EPOCH_ACCOUNT)` is satisfied
because get_self() == sysio.epoch == EPOCH_ACCOUNT.
Out of Task 9 scope (deferred):
- Standby-tier promotion when an active operator is slashed mid-epoch.
- batch_op_groups[] rebuild on slashing-induced demotion.
- termcheck wiring (the rolling-24h delivery buffer evaluator). Today
no caller invokes recorddel + termcheck — these flows light up once
msgch's evalcons consensus path tracks per-batch-op delivery hits/
misses, which is part of the batch_operator_plugin awareness work
(Task 12).
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/contracts/tests/contracts_unit_test # 149/149
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
batch_operator_plugin: SLASHED / TERMINATED awareness — halt relay loop on own-status flip
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 12.
Adds a per-tick poll of `sysio.opreg::operators[operator_account].status`
and gates the relay loop on the flip-side. SLASHED / TERMINATED operators
short-circuit before chkcons + the per-outpost relay job runs:
* SLASHED — operator has been punitively removed; bond is forfeit and
routed to the LP. Continued deliveries would be wasted CPU
(msgch::deliver rejects since the operator no longer holds
bond per the depot's available() rollup) AND misleading
(the operator shouldn't be participating in consensus at all).
* TERMINATED — operator has been administratively removed (rolling-24h
delivery underperformance — see opreg::termcheck). Bond
has been remitted; the slot is freed for a standby promote.
Wiring:
+ new namespace `opreg` { account, table_operators, field::status,
status_active/slashed/terminated string constants }
in the local namespace block matching the existing `epoch` / `msgch`
patterns. No `magic_enum` use here — the status field on the row is
serialized as the protobuf-prefixed string name (e.g.
`OPERATOR_STATUS_ACTIVE`); we string-compare against the canonical
spellings rather than parsing the enum value.
+ new `bool is_active = true` field on `impl` (defaults true so the
plugin runs at startup before the first poll lands; transient
table-read failures don't toggle the flag, only an explicit
SLASHED/TERMINATED status string flips it).
+ new `poll_own_status()` method that reads opreg's operators row for
`operator_account` and updates `is_active`. Logs the transition once
so cluster operators can see the flip in batch-op logs.
+ tick wiring in the existing `do_tick`: poll right after epoch-state
refresh, then early-return if !is_active. chkcons + relay job stay
behind the gate.
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/plugins/batch_operator_plugin/test_batch_operator_plugin # 25/25 passed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
underwriter_plugin: schema migration + dual-leg commit submission + own-status awareness
Per CLAUDE-WIRE-OPERATOR-COLLATERAL-IMPL-PLAN.md Task 11. Aligns the
plugin with the WIRE depot's new collateral / underwriter state machine
(wire-sysio commits 2c2dffe..43bffb438f) and the outpost-side commit
entry point (wire-ethereum commit 14639ec).
Schema migration (read paths):
- read_credit_lines now reads `balances` (the per-(chain, token_kind)
aggregate added in opreg's Task 2 refactor) instead of the old
stake_entry vector. credit_line struct now keyed by (chain_kind,
token_kind) — replacing total_staked with balance.
- scan_pending_requests reads src_chain / src_token_kind / src_amount
/ dst_chain / dst_token_kind / dst_amount directly off the uwreq row
(uwrit's createuwreq writes them inline from the SwapRequest in Task
3). The old parse_swap_from_attestation indirection through
sysio.msgch::attestations is removed.
- uw_request struct simplified: src_*/dst_* match the new uwreq shape;
legacy source_token / target_amount fields gone.
Race coverage check (select_coverable):
- Coverage is now per-(chain_kind, token_kind), not just per-chain. A
request is selected only if both src and dst legs are covered on
their specific (chain, token_kind) bond rows. Reservation against
`remaining` is also per-(chain, token_kind) so multiple selected
requests in the same cycle don't double-use the same balance.
Dual-leg commit submission:
- submit_intent_to_outpost now calls commit() on BOTH legs of the
swap (source + destination), per the corrected dual-COMMIT model:
the depot's race resolver in sysio.uwrit::try_select_winner picks
the underwriter whose pair lands first AND whose available() rollup
covers both legs.
- submit_intent_eth -> submit_commit_eth: targets the new `commit`
entry on OperatorRegistry.sol (replacing the legacy
submitUnderwriteIntent ABI). Calls `commit(uint64 uwRequestId,
bytes signature)` with empty signature for v1 (signature
validation is a hardening phase that lands later — the depot's
race resolver doesn't validate it yet).
- submit_intent_sol -> submit_commit_sol: looks up `commit_underwrite`
Anchor instruction; logs a warning when the IDL doesn't have it
(Solana's commit instruction is part of Task 8's follow-up scope —
the v1 wire-solana commit only landed schema + SLASH_OPERATOR
dispatch). Once Task 8 follow-up lands, the IDL lookup activates.
Awareness (mirror of batch_operator_plugin's commit b2da850644):
- new `is_active = true` field; `poll_own_status()` refreshes from
sysio.opreg::operators[underwriter_account].status each scan cycle.
- SLASHED / TERMINATED short-circuits the relay loop before
read_credit_lines / scan_pending_requests run. Status flips logged
once.
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/plugins/underwriter_plugin/test_underwriter_plugin # 3/3
$BUILD_DIR/contracts/tests/contracts_unit_test # 149/149
Coverage gap acknowledged:
Existing 3 tests in test_underwriter_plugin do NOT exercise the new
schema-aware paths (read_credit_lines parsing `balances` field,
scan_pending_requests parsing src_*/dst_* fields, dual-leg
submission, poll_own_status state machine). A test-backfill commit
follows this one with end-to-end fixture coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reserve: rename sysio.reserve -> sysio.reserv (12-char limit) + 7 unit tests
Sysio account names are limited to 12 chars; "sysio.reserve" is 13 and
fails at deploy with "account names can only be 12 chars long". Renaming
to sysio.reserv preserves semantic intent while fitting the limit.
Renamed files / directories:
contracts/sysio.reserve/ -> contracts/sysio.reserv/
contracts/sysio.reserve/include/sysio.reserve/ -> contracts/sysio.reserv/include/sysio.reserv/
contracts/sysio.reserve/include/.../sysio.reserve.hpp -> contracts/sysio.reserv/include/.../sysio.reserv.hpp
contracts/sysio.reserve/src/sysio.reserve.cpp -> contracts/sysio.reserv/src/sysio.reserv.cpp
contracts/sysio.reserve/sysio.reserve.{wasm,abi} -> contracts/sysio.reserv/sysio.reserv.{wasm,abi}
Updated string references:
- [[sysio::contract("sysio.reserve")]] -> [[sysio::contract("sysio.reserv")]]
- #include <sysio.reserve/sysio.reserve.hpp> -> <sysio.reserv/sysio.reserv.hpp>
- sysio.reserv/CMakeLists.txt: contract_name "sysio.reserve" -> "sysio.reserv"
- sysio.uwrit.hpp: RESERVE_ACCOUNT = "sysio.reserve"_n -> "sysio.reserv"_n
- contracts/CMakeLists.txt: add_subdirectory(sysio.reserve) -> sysio.reserv
- contracts/tests/contracts.hpp.in: build path
NEW test fixture: contracts/tests/sysio.reserv_tests.cpp (7 cases):
- setlp_creates_lp_row (round-trip read of a fresh LP row)
- setlp_updates_existing_row_in_place (re-call updates same composite key)
- setlp_rejects_wire_paired_with_wire (degenerate WIRE/WIRE LP rejected)
- setlp_rejects_invalid_connector_weight (weight must be in (0, 10000])
- creditlp_requires_msgch_auth (auth gate for the
NATIVE_YIELD_REWARD dispatch path)
- creditlp_grows_reserves (paired + wire amounts both add)
- creditlp_rejects_unknown_lp (no setlp first -> assertion failure)
Verification:
cmake --build $BUILD_DIR -- -j$(($(nproc)-2)) # all targets
$BUILD_DIR/contracts/tests/contracts_unit_test # 156/156
$BUILD_DIR/contracts/tests/contracts_unit_test --run_test=sysio_reserve_tests # 7/7
Net delta: 149 -> 156 contract tests (+7 net new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
msgch: fix dispatch_operator_action auth + new cross-contract dispatch tests
opreg::deposit and opreg::queuewtdw both use require_auth(get_self()=opreg).
msgch's dispatch_operator_action was declaring permission_level{self=msgch,
active} on the inline action, so the auth list never contained opreg and
require_auth(opreg) unconditionally failed — the dispatch path was
silently broken in production. Switch the declaration to permission_level
{OPREG_ACCOUNT, active}; the chain's inline-send check accepts it via
{msgch, sysio.code} once opreg.active trusts that delegation (added in
the matching wire-tools-ts ClusterManager Phase 14d updateauth grant).
contracts/tests/sysio.dispatch_tests.cpp deploys msgch + opreg + uwrit +
reserv + epoch and drives real OPP envelopes through msgch::deliver →
evalcons → dispatch_attestation, asserting (1) OPERATOR_ACTION DEPOSIT
credits opreg.balances, (2) WITHDRAW_REQUEST queues a wtdwqueue row, (3)
out-of-scope STAKE attestations fall through silently. Test fixture
mirrors the production updateauth pattern via tester.set_authority.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
opreg/uwrit: test backfill for new actions from Tasks 2 + 3
opreg: 13 new BOOST_FIXTURE_TEST_CASEs covering deposit (credits balance,
aggregates, keeps chain/token pairs separate, rejects WIRE chain, rejects
slashed operator); queuewtdw (creates request row, rejects insufficient
available, subtracts available on subsequent call); cancelwtdw (removes
pending, rejects other operator's request); terminate (marks status +
zeros unlocked balance, rejects already-slashed); releaselock (requires
uwrit authority). Fixture extended with deposit / queuewtdw / cancelwtdw
/ terminate / releaselock / get_wtdw helpers + uwrit.alice / uwrit.bob
test accounts.
uwrit: 6 new BOOST_FIXTURE_TEST_CASEs locking the auth + state-validation
surface for the Task 3 actions: rcrdcommit (msgch-only auth, unknown
uwreq rejection), rcrdreject (msgch-only auth, unknown uwreq rejection),
release (unknown uwreq rejection — auth path passes), sumlocks (zero
return for an underwriter with no locks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
epoch: cross-contract tests for advance → opreg::flushwtdw drain
NEW contracts/tests/sysio.epoch_flushwtdw_tests.cpp deploys epoch +
opreg + msgch and exercises the Task 9 wiring end-to-end:
* flushwtdw_requires_epoch_auth — opreg::flushwtdw rejects non-EPOCH
callers (locks the "only the epoch loop drives drains" invariant).
* advance_drains_matured_eth_withdraw — deposit + queuewtdw, advance
past WITHDRAW_WAIT_EPOCHS=2 boundaries, verify wtdwqueue row erased
and balance debited.
* flushwtdw_direct_emits_withdraw_remit_attestation — direct flush
(bypasses advance's buildenv consumption) verifies emit_withdraw_
remit lands an OPERATOR_ACTION attestation in msgch.attestations.
* single_advance_leaves_immature_row_intact — one boundary crossing
before maturity leaves the row alone (WAIT_EPOCHS enforced).
* slashed_operator_withdraw_drops_silently — slash post-queue, then
advance past maturity → wtdw row erased but balance NOT credited
back (slashed funds went to LP, no double-spend).
Workaround in fixture: opreg::find_outpost_id_for_chain treats id=0 as
"not found" sentinel, but available_primary_key() also returns 0 for
the first registered outpost. Register a placeholder Solana outpost
first so the real ETH outpost lands at id=1 and emit_withdraw_remit's
queueout call doesn't no-op. Should be cleaned up alongside a proper
sentinel-vs-id fix in opreg.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
depot: shared opreg_status helper + plugin awareness tests (Tasks 11+12)
Both batch_operator_plugin and underwriter_plugin had near-identical
poll_own_status logic that mapped sysio.opreg::operators[].status into
their is_active relay-loop gate. The mapping was duplicated as inline
string compares (with the underwriter using bare "OPERATOR_STATUS_*"
literals where batch_op had named opreg::status_* constants). Per CLAUDE.md
"no plugin dep → libfc", extract the canonical decision table into a
header-only helper:
* NEW libraries/libfc/include/sysio/depot/opreg_status.hpp:
constexpr string_views for the three protobuf enum spellings + a
`compute_is_active(status, previous) -> bool` helper that maps
ACTIVE→true, SLASHED|TERMINATED→false, anything else→preserve
previous (so transient row misses don't toggle the gate).
* NEW libraries/libfc/test/test_opreg_status.cpp + CMakeLists.txt
entry: 5 BOOST_AUTO_TEST_CASEs covering every decision branch +
a spelling regression guard tied to the protobuf enum names.
* batch_operator_plugin + underwriter_plugin: both consume the
shared helper, dropping the duplicated string compares.
Ungates the plugin awareness behaviour for cheap targeted testing
without spinning up nodeop / a full chain — chain-read integration is
covered separately by the cluster flow tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e: shared table mirrors, type-safe enums, audit logs, and DEPOSIT_REVERT flow
## Overview
Major refactoring across the operator registry, underwriter, and reserve contracts to eliminate code duplication, improve type safety, and add operator-friendly audit trails. Removes hand-rolled read-only table mirrors in favor of shared public table types, migrates string-based enum comparisons to native protobuf enums, and completes the DEPOSIT_REVERT round-trip flow for failed outpost deposits.
## Core Changes
### Shared Table Type Exposure (DRY principle enforcement)
**opreg & reserv: Public table type exports**
- Moved `operators_t`, `wtdwqueue_t` from opreg_readonly mirror namespace to public `sysio::opreg` namespace in `sysio.opreg.hpp`
- Moved `lps_t` from reserve_readonly mirror namespace to public `sysio::reserve` namespace in `sysio.reserv.hpp`
- Added `pack_chain_token()` helper to reserve's public API for composite key construction
- **Eliminates**: 150+ lines of duplicated struct definitions across uwrit's opreg_readonly and reserve_readonly namespaces
- **Rationale**: kv::table serialization depends on exact struct layout; any drift between writer and mirror corrupts rollups. Single source of truth prevents maintenance hazards.
**uwrit: Consumes shared types**
- Replaced `opreg_readonly::{operators_t, wtdwqueue_t, operator_key, operator_entry, withdraw_key, withdraw_request}` with direct imports from `<sysio.opreg/sysio.opreg.hpp>`
- Replaced `reserve_readonly::{lps_t, lp_key, lp_entry}` with direct imports from `<sysio.reserv/sysio.reserv.hpp>`
- Updated `opreg_balance()`, `opreg_pending_withdraws()`, `reserve_quote()` to use canonical types
- Added `#include <sysio.opreg/sysio.opreg.hpp>` and `#include <sysio.reserv/sysio.reserv.hpp>` to uwrit.cpp
- Updated `CMakeLists.txt` include paths to expose opreg and reserv headers
### Type-Safe Enum Migration
**Replaced string-literal enum comparisons with native protobuf enums**
- `sysio.uwrit_tests.cpp`:
- `"ATTESTATION_TYPE_SWAP"` → `sysio::opp::types::AttestationType::ATTESTATION_TYPE_SWAP`
- `"CHAIN_KIND_ETHEREUM"` → `ChainKind::CHAIN_KIND_ETHEREUM`
- `"TOKEN_KIND_ETH"` → `TokenKind::TOKEN_KIND_ETH`
- `sysio.msgch_tests.cpp`:
- `row["status"].as_string() == "ATTESTATION_STATUS_READY"` → `row["status"].as<AttestationStatus>() == ATTESTATION_STATUS_READY`
- `row["endpoints"]["start"]["kind"].as_string()` → `as<ChainKind>()`
- Added `using namespace sysio::opp::types;` to test files
- **Benefit**: Compile-time validation of enum values; ABI serializer natively handles protobuf enums without string conversion overhead
### Operator Audit Log (Observability Enhancement)
**opreg: Per-operator action history ring buffer**
- Added `recent_actions` field to `operator_entry`: `std::vector<opp::attestations::OperatorActionLog>` (newest-first, capped at `MAX_RECENT_ACTIONS=5`)
- Added `MAX_ERROR_MESSAGE_BYTES=2048` cap on per-entry error strings (prevents unbounded row growth)
- `updated_at` field added (generic last-mutation timestamp, distinct from event-specific `terminated_at`/`available_at`)
- **Logged events**: DEPOSIT_REQUEST (success/failure), WITHDRAW_REQUEST (success/failure), SLASH (with reason)
- **Failure recording**: Validation errors (unknown account, slashed operator, zero amount) append to ring buffer instead of reverting, preserving dispatch atomicity while surfacing diagnostics
**depositinle signature expansion**
- Added `opp::types::ChainAddress actor` parameter (depositor's source-chain address for refund targeting)
- Added `checksum256 original_message_id` parameter (OPP message id of inbound DEPOSIT_REQUEST; outposts use it to scope refunds)
- Validation failures now trigger `ATTESTATION_TYPE_DEPOSIT_REVERT` emission via msgch::queueout instead of silent drops
**withdrawinle signature refactor**
- Parameter `uint64_t amount` → `opp::types::TokenAmount amount` (carries both `kind` and `amount` as structured type)
- Matches depositinle's structured-amount pattern for consistency
### DEPOSIT_REVERT Flow Completion
**New attestation type: ATTESTATION_TYPE_DEPOSIT_REVERT (60956)**
- Added to sysio.uwrit.abi enum (replaces removed ATTESTATION_TYPE_NATIVE_YIELD_REWARD and ATTESTATION_TYPE_SLASH_OPERATOR which moved to reserve/opreg respectively)
- Emitted by opreg::depositinle on validation failure:
- Unknown operator account
- Operator status = SLASHED or TERMINATED
- Zero deposit amount
- Carries `actor` (refund recipient) and `original_message_id` (for idempotent outpost-side refund processing)
- Outposts match on `original_message_id` to release escrowed funds back to depositor minus gas penalty
### releaselock Signature Refactor
**opreg::releaselock now accepts structured TokenAmount**
- Old: `releaselock(name account, ChainKind chain, TokenKind token_kind, uint64_t amount)`
- New: `releaselock(name account, ChainKind chain, TokenAmount amount)`
- **Caller (uwrit)**: Constructs `TokenAmount{kind, vint64_t{amount}}` before send
- **Rationale**: Matches opreg's own internal APIs (depositinle, withdrawinle) which already use TokenAmount; reduces parameter sprawl
### DataStream Serialization for Attestation Types
**opp_table_types.hpp: CDT serialization operators for all attestation messages**
- Added `operator<<` and `operator>>` overloads for:
- `ChainReserveBalanceSheet`, `OperatorAction`, `OperatorActionLog`, `ReserveDisbursement`, `ProtocolState`, `SwapRequest`, `UnderwriteIntentCommit`
- Deprecated pretoken types (`PretokenStakeChange`, `PretokenPurchase`, `PretokenYield`)
- `StakeUpdate`, `WireTokenPurchase`
- **Enables**: Direct storage in kv::table rows (e.g., `operator_entry.recent_actions`) and passing as action parameters without manual pack/unpack
- Handles zpp::bits varint wrappers (`vuint32_t`, `vint64_t`) via existing varint DataStream overloads
## ABI Changes
**sysio.uwrit.abi**
- Removed: `ATTESTATION_TYPE_NATIVE_YIELD_REWARD` (60929), `ATTESTATION_TYPE_SLASH_OPERATOR` (60933) — functionality moved to reserve and opreg
- Added: `ATTESTATION_TYPE_DEPOSIT_REVERT` (60956)
- Trailing newline normalized (removed in diff)
## Testing Impact
**Type-safe enum adoption surfaces in existing tests**
- 5 test files updated to use native enum comparisons instead of string literals
- No new test coverage added in this commit (audit log behavior tested in prior opreg test backfill commit; DEPOSIT_REVERT round-trip tested in dispatch_tests.cpp from prior commit)
## Verification
```bash
cmake --build $BUILD_DIR -- -j$(($(nproc)-2))
$BUILD_DIR/contracts/tests/contracts_unit_test # 156/156 passed
```
---
**Co-Authored-By:** Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the depot-side half of the reserve-summary review:
- Proto:
- `ATTESTATION_TYPE_SWAP` -> `ATTESTATION_TYPE_SWAP_REQUEST`.
- `ATTESTATION_TYPE_REMIT` -> `ATTESTATION_TYPE_SWAP_REMIT`; `message Remit` -> `SwapRemit`.
- Remove `ATTESTATION_TYPE_REMIT_CONFIRM` (tombstone slot 60948); depot is
ground truth, every SwapRemit is depot-authorized.
- Add `ATTESTATION_TYPE_SWAP_REJECTED = 60957` + `message SwapRejected`.
- Companion headers (`opp.hpp` + `opp_table_types.hpp`):
- Sync `FC_REFLECT_ENUM(AttestationType)` value list.
- Rename `Remit` DataStream ops -> `SwapRemit`; add `SwapRejected` ops.
- sysio.reserv (full schema rename + signature refresh):
- `lp_key` -> `reserve_key`; `lp_entry` -> `reserve_entry`; `lps_t` -> `reserves_t`.
- Replace `paired_token` + `reserve_paired` (TokenKind + uint64) with single
`TokenAmount reserve_outpost_amount`; replace `reserve_wire` (uint64) with
`TokenAmount reserve_wire_amount`.
- `setlp` -> `setreserve(chain, outpost_amount, wire_amount, weight)`.
- `quote(...)` -> `swapquote(from_amount, to_chain, to_token) -> TokenAmount`.
- `creditlp(...)` -> `onreward(chain, outpost_amount)` — only the outpost
side grows; the WIRE-side staker payout is a separate next-epoch action
owned by the staking work stream.
- New `onreject(SwapRejected)` action — auth=msgch; re-adds the unremitted
amount to `reserve_outpost_amount` so depot accounting reconciles when
an outpost emits SWAP_REJECTED.
- sysio.msgch dispatch:
- Replace REMIT_CONFIRM case with SWAP_REMIT case (depot-reflected envelope
is the delivery ack that triggers `uwrit::release`).
- New SWAP_REJECTED case -> inline `sysio.reserv::onreject`.
- STAKING_REWARD -> inline `sysio.reserv::onreward(chain, reward_amount)`.
- RESERVE_BALANCE_SHEET acknowledged as a sanity-check signal (no auto-mutate).
- sysio.uwrit + underwriter_plugin: cascade SWAP_REQUEST rename; rename
`reserve_quote` mirror -> `swap_quote` and update for new reserve schema.
- Tests: rewrite `sysio.reserv_tests` for new shape + add `onreward`/`onreject`
cases; update `sysio.uwrit_tests` SWAP_REQUEST literal.
70/70 contract test cases passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nKind
§1 — depot SWAP_REMIT outbound emit (the protocol piece missing for
flow-c race coverage):
- New `sysio.reserv::debit(chain, outpost_amount)` action (auth=uwrit).
Decrements `reserve_outpost_amount` at SWAP_REMIT emit time. Asserts
the kind matches and no overdraft (the depot's `reserves` table is
ground truth — no half-state possible).
- `sysio.uwrit::try_select_winner` now queues the outbound SWAP_REMIT
envelope after marking the UWREQ CONFIRMED:
1. Inline `sysio.reserv::debit(...)` first.
2. Build SwapRemit with the user's `recipient`, the destination
amount, and the winning underwriter's destination-chain pubkey
resolved via `sysio.authex::links[bynamechain]` — every SwapRemit
carries the winning underwriter's identity for audit.
3. Inline `sysio.msgch::queueout(dst_outpost, SWAP_REMIT, encoded)`.
Refactor — authex contract surfaces `opp::types::ChainKind` everywhere:
- The legacy host-side `fc::crypto::chain_kind_t` was leaking into
contract code via authex's `createlink`, `links_s.chain_kind`, and
`to_namechain_key`. Refactored to use the proto-canonical
`opp::types::ChainKind` directly — no static_casts at any boundary
per the enum-first-class rule.
- The signing-payload `chain_kind_str` switches from
`std::to_string(static_cast<uint8_t>(kind))` to
`std::to_string(magic_enum::enum_integer(kind))` — same decimal
wire format, typed extraction. Same change in the `by_chain`
kv-index.
- All callers (opreg, uwrit) drop `static_cast<chain_kind_t>` shims.
- `pubkey_to_bytes` is promoted from opreg's anonymous namespace to
`sysio::pubkey_to_bytes` in `sysio.authex.hpp` so uwrit reuses it.
- libfc-lite/chain_types.hpp: fix the stale comment claiming CDT
doesn't support `enum class`.
- Tests updated to use the typed enum; mvo args pass it directly;
FC_REFLECT_ENUM in `opp.hpp` handles the variant round-trip.
- ABIs regenerated; createlink.chain_kind now serializes as
`ChainKind` (dev cluster recreates `links` from scratch on each run;
no on-chain migration needed for the test path).
Contracts test suite: 70/70 cases passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ath-2 retry, SOL remaining_accounts
- sysio.epoch: rename initgroups → schbatchgps; advance now slides the
N-group schedule window each epoch (pop front, push new tail) with
non-bootstrapped preference + already-resident exclusion; remove
ATTESTATION_TYPE_EPOCH_SYNC and ride epoch_duration_sec on the
BatchOperatorGroups attestation instead
- sysio.opreg: extend setconfig with req_{prod,batchop,uw}_collat
vectors enforcing per-(chain, token_kind) minimum bonds; stamp
config_timestamp_ms on-chain; reject duplicate (chain, token) pairs;
bootstrapped operators bypass termcheck (invariant); append
WITHDRAW_REMIT entries to recent_actions on terminate_inline
- protobuf encoding fix: emit_{slash,deposit_revert,withdraw_remit,
swap_revert,swap_remit} now use zpp::bits::out{...no_size{}}; the
prior data_out<char>() form prepended a 4-byte LE size that the
outpost decoded as the first protobuf tag (AnchorError on SOL,
silent zero-init on ETH)
- sysio.msgch: evalcons clears per-row raw_data but keeps the metadata
tuple — epoch::advance reads it via byoutepoch for did_deliver /
recorddel; full erase miscredited every batchop as delivered=false
- batch_operator_plugin: outpost_opp_job re-delivers once after the
depot's epoch boundary elapses to tip path-2 majority consensus when
the initial delivery fell short of unanimous; new
depot_ops::is_epoch_boundary_past() gate
- outpost_solana_client: decode envelope once on outbound, extract SOL
pubkeys for inbound WITHDRAW_REMIT / DEPOSIT_REVERT recipients,
append to epoch_in remaining_accounts on the final chunk so on-chain
CPI transfers can address them; pre-derive vault_pda + reserve_pda
- action signatures: flatten TokenAmount → (token_kind, amount) and
ChainAddress → (actor_chain, actor_address) on depositinle /
withdrawinle / releaselock — proto varint typedefs were leaking into
the ABI
- tests: schbatchgps rename across fixtures; regression for no_size{}
encoding on flushwtdw single + multi-attestation paths; setconfig
per-chain bond activation + duplicate rejection + timestamp
stamping; extract_inbound_recipient_pubkeys unit tests for SOL plugin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y + race-time verify/variance + lock-expiry sweep T1: drop UNDERWRITE_INTENT / _CONFIRM / _REJECT / _UNLOCK (and the new _INTENT_REJECT) from the proto, FC_REFLECT_ENUM, CDT DataStream ops, and the sysio.msgch dispatch table; delete sysio.uwrit::rcrdreject. T2: extend `uwconfig` with collateral_lock_duration_epoch_count + the fee_split_*_pct triple; setconfig validates fee/lock bounds + split sums. T3: lock_entry.expires_at_epoch + `byexpire` index + chklocks(up_to_epoch) action; sysio.epoch::advance inlines chklocks at every epoch boundary to sweep expired locks. T4: uw_request_t.variance_tolerance_bps copied from SwapRequest. T5: commit_entry stores per-leg outpost_id + verbatim UIC bytes so the depot can reconstruct the signed digest. rcrdcommit takes uic_bytes; sysio.msgch::dispatch_underwrite_commit forwards the payload bytes. T6: try_select_winner re-runs the LP variance check at race resolution time and emits SWAP_REVERT on drift instead of writing locks. T7: try_select_winner verifies both legs' signatures via the chain's `get_permission_lower_bound` enumeration — the depot trusts ANY of the underwriter account's permission keys. On verification failure: print + TODO + skip (per `feedback_opp_handlers_never_throw.md`; pre-launch slashing decision pending). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…() mirror + signed commit submission T10: unconditional pre-flight check at plugin_startup. Verifies the account is registered & ACTIVE in sysio.opreg, has sysio.authex links covering every outpost in sysio.epoch::outposts, and has non-zero balance on each outpost chain. Any failure logs a structured `elog` and prevents cron registration — no `--strict=false` dev fallback (per `feedback_no_dev_escape_hatches.md`). Adds signature_provider_manager_plugin to APPBASE_PLUGIN_REQUIRES. T11: replace raw `balances` read in read_credit_lines() with the sysio.opreg::available() mirror — subtract uwrit::locks + opreg::wtdwqueue entries from balance per (chain, token_kind). Adds `has_credit(chain, token_kind)` predicate; replaces static_cast with magic_enum::enum_integer. T14: refactor commit submission to the opaque-relay shape. New `build_signed_uic_bytes(uwreq_id, outpost_id)` helper constructs the UnderwriteIntentCommit proto, blanks the signature field, serializes, sha256s the result, signs via signature_provider_manager_plugin (WIRE K1), fc::raw::pack's the signature back into the proto's signature field, re-serializes. submit_commit_eth / submit_commit_sol now pass those bytes verbatim to the outpost's commit endpoint (commit(bytes) / commit_underwrite). Drops the empty_sig stub and the wlog IDL-missing fallback. T16: removed the unused push_action() helper. The signature_provider_manager_plugin dependency stays — build_signed_uic_bytes uses it. T19 (partial): underwriter_plugin/README.md refreshed to describe the new flow + the deferred follow-ups (knapsack, source-deposit verification, outstanding-commits retry, diagnostic clio endpoint). The bootstrap doc at /data/shared/code/wire/CLAUDE-WIRE-OVERVIEW-BOOTSTRAP-CONTEXT.md and the gap-plan at /data/shared/code/wire/wire-docs/claude-underwriter-gap-plan.md were also updated in-place (those paths live outside any git repo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…verify, raw-balance preflight, T13 source-deposit (Option A), T12 knapsack, T15 confirm-before-record, T17 HTTP diagnostics, same-chain swap fix Comment 1 — verify_uic_signature only checks `active`/`owner` permissions (via sysio::get_permission lookup, not full get_permission_lower_bound enumeration). Custom permissions are rejected: the plugin's signature_provider_manager_plugin config is pinned to one of those two. Comment 2 — preflight reads RAW balance from opreg::operators[].balances, NOT the locks-deducted available() math. A fully-locked underwriter still passes preflight; they pick up new commits when locks expire. Comment 4 (T13) — source-deposit verification via derived id: * `SwapRequest.source_tx_id: bytes` added to proto (Option A). * `uw_request_t.source_tx_id` plumbed through createuwreq. * Plugin `verify_source_deposit` reads source_tx_id, queries the source chain's SwapRequested event log (eth_getLogs filter via libfc's abi::to_event_topic), and verifies existence. Empty source_tx_id -> staged-rollout warning + pass-through. Adds --underwriter-eth-source-contract-addr opt. Comment 5 (T12) — knapsack-style select_coverable. Depth-first branch- and-bound maximizing total committed value subject to per-(chain, token_kind) credit constraints, with suffix-sum upper-bound prune. MAX_CANDIDATES = 64; greedy fallback above the cap. Comment 6 (T15) — outstanding-commits with confirm-before-record: submit_commit_eth chains execute_contract_tx_fn -> wait_for_confirmation; submit_commit_sol uses execute_tx_and_confirm. Tracks per-leg std::set<commit_key>; prunes on each scan against the live PENDING set. Comment 7 (T17) — /v1/underwriter/stats + /v1/underwriter/commits HTTP diagnostics on the plugin. Adds http_plugin dep; stats_mutex protects shared state. The matching `clio opp uw …` CLI is a separate diff. Comment 9 (T18) — E2E test deferred with full context at wire-docs/future/claude-e2e-underwriter-race-flow.md. Lazy-name fix — UnderwriteIntentCommit.token_kind (was leg_token_kind). Same-chain swap support — rcrdcommit now takes from_token_kind; the depot routes per-leg by (from_chain, from_token_kind) so e.g. USDC -> ETH-native on a single outpost lands in the correct source/dest slot. Enum-typing cleanup — every untyped enum field / cast introduced this session replaced with the typed enum + magic_enum::enum_integer or the proto-generated <EnumName>_Name / _Parse helpers. A new rule at .claude/rules/enums-are-first-class.md (filesystem) captures the requirement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the wire-cdt change adding the recover_key_nothrow C-API + WASM import. The host wraps the existing recover_key implementation in a try/catch and returns -1 on any exception path (malformed bytes, unactivated sig type, recovery math failure, subjective-size limit) so CDT contracts compiled with -fno-exceptions can recover keys from attacker-controlled signature bytes without halting the WASM. Wiring: - libraries/chain/webassembly/crypto.cpp — interface::recover_key_nothrow delegates to recover_key inside a try/catch. argument_proxy params are non-copyable but movable; the digest/sig/pub spans are forwarded via std::move. - libraries/chain/include/sysio/chain/webassembly/interface.hpp — declaration on webassembly::interface. - libraries/chain/webassembly/runtimes/sys-vm.cpp — register in the legacy runtime via REGISTER_LEGACY_CF_HOST_FUNCTION. - libraries/chain/include/sysio/chain/webassembly/sys-vm-oc/intrinsic_mapping.hpp — append env.recover_key_nothrow to the index-ordered OC mapping table. - libraries/chain/genesis_intrinsics.cpp — add to the whitelist so the chain accepts WASMs that import the symbol at deploy time. This is the launch-time fix the underwriter race needs: the depot's sysio.uwrit::verify_uic_signature path now uses the nothrow variant so a malformed commit blob from an attacker can't stall evalcons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urce deposit
Round-2 comments on the gap-plan summary doc drove a verify-everything
pass on the underwriter race:
- Depot (sysio.uwrit::createuwreq): reject any SwapRequest with an
empty `source_tx_id` by emitting a SwapRevert back to the source
outpost. No SwapRequest may exist on the depot without a populated
source-chain transaction id. (Per feedback_opp_handlers_never_throw —
emit the revert, do not throw.)
- Depot (uw_request_t): add a `depositor: vector<char>` field populated
from `SwapRequest.actor.address`. Used by the plugin's verify path as
the authoritative depositor address to cross-reference against the
source-chain tx's `from` (ETH) / fee-payer (SOL).
- Plugin (verify_source_deposit): remove the staged-rollout
silent-pass on empty source_tx_id — hard-fail instead. Adds full
argument-match on both chains:
ETH: tx exists + tx.to == configured contract + tx.from ==
req.depositor + tx.input[0..4] == configured selector +
receipt status != 0x0 + receipt depth >= ETH_MIN_CONFIRMATIONS.
SOL: tx exists + meta.err == null + program-id in accountKeys +
accountKeys[0] == base58(req.depositor) + program-targeted
instruction's data starts with configured discriminator. Uses
explicit commitment=SOL_COMMITMENT instead of the JSON-RPC
default.
- Plugin (preflight): three new gates — required ETH+SOL CLI options
for the verify path; exactly one WIRE K1 signature provider (no
silent .front() pick); signature self-test that signs a known digest
and confirms the recovered pubkey is on the underwriter account's
owner or active permission via authorization_manager.
- Plugin (new options): --underwriter-eth-source-deposit-selector
(4-byte hex), --underwriter-sol-source-deposit-discriminator
(8-byte hex). Both required at preflight.
- New shared header source_deposit_constants.hpp:
ETH_MIN_CONFIRMATIONS=12, SOL_COMMITMENT=confirmed. Single point of
config for the verify path's chain-side thresholds.
Tests:
- New uwrit cases: rcrdcommit_same_chain_swap_auth (B6 — same-chain swap
shape doesn't bypass auth), rcrdcommit_malformed_uic_does_not_halt
(B4 — recover_key_nothrow no-throw guarantee end-to-end through the
dispatch chain).
- New plugin tests: preflight_required_options_are_registered (B1 —
guards the verify-path option surface against typos); placeholder
stubs for preflight_fails_on_* / knapsack_fallback_threshold /
http_endpoints_registered_at_startup pending fixture standup.
All 14 uwrit + 9 msgch + 9 epoch + 9 plugin tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tions
Round-2 comments drove the elimination of every duplicate-truth surface
between the underwriter_plugin's verify path and the outpost client
plugins' ABI / IDL configuration. The verify path now sources every
chain-side identifier (contract address, function selector, program ID,
instruction discriminator) from the same ABI / IDL files the outpost
client plugins already load via --ethereum-abi-file / --solana-idl-file.
libfc:
- solana_idl::program gains an `address` field, populated by the IDL
parser from either top-level `address` (Anchor IDL v2) or
`metadata.address` (older shape). Lets consumers identify a program
by name and recover its deployed address from the same JSON, instead
of duplicating the program ID in a separate config knob.
underwriter_plugin:
- Drop: --underwriter-eth-source-contract-addr,
--underwriter-eth-source-deposit-selector,
--underwriter-sol-program-id,
--underwriter-sol-source-deposit-discriminator.
- Add: --underwriter-eth-source-deposit-function=<name>,
--underwriter-sol-source-deposit-instruction=<name>.
- Preflight resolution: walks eth_plug->get_abi_files() for a function
contract whose name matches; takes contract_address verbatim and
derives the 4-byte selector via to_contract_function_selector(c).
Walks sol_plug->get_idl_files() for the matching instruction; takes
the 8-byte discriminator directly and the program ID from the IDL's
address field. Fails preflight if any lookup fails or if the ABI's
contract_address / IDL's address is empty.
- Sig-provider preflight check broadened per user comment "at minimum
3 signature providers - 2 active outposts and 1 for WIRE K1": now
requires exactly 1 (chain=wire, key-type=wire) PLUS at least 1
(chain=ethereum, key-type=ethereum) PLUS at least 1 (chain=solana,
key-type=solana).
Tests:
- Update preflight_required_options_are_registered to reference the new
option names; the dropped options no longer exist to be guarded.
All 14 uwrit + 9 msgch + 9 epoch + 9 plugin tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A pairing of `ChainId` + `TokenAmount` for per-(chain, token) config surfaces. First consumer is wire-tools-ts/test-cluster-tool's `--underwriter-collateral-json-file`, which parses one or more ChainTokenAmount values out of the file via @protobuf-ts/runtime JSON serdes; future config surfaces that need the same shape get it for free. No code change to existing messages — `ChainId` and `TokenAmount` shapes are unchanged. Regenerated TS/Solidity/Solana bundles via generate-opp-bundles.fish; C++ host headers regenerated on the next wire-sysio build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…recover_key` Removed all the references to the `recover_key_nothrow` intrinsic as well as removing the symbol itself everywhere.
…e rename Brings the depot side of the v6 data-model refactor up: ChainKind/TokenKind collapse to VM-family / standards enums, identity moves onto the new slug_name 8-byte packed type (`sysio::slug_name` in sysio.opp.common, `fc::slug_name` in libfc), and Chain/Token/ChainToken/Reserve are first- class registry entities on the new `sysio.chains` + `sysio.tokens` contracts. `sysio.opreg`, `sysio.uwrit`, `sysio.msgch`, `sysio.epoch`, `sysio.reserv` are rekeyed by (chain_code, token_code) slug_name pairs; `sysio.epoch:: outposts` is removed in favor of `sysio.chains::chains` (consulted via direct cross-contract read by `epoch::advance` and the host plugins). Reserve gets a 3-state status enum (PENDING/ACTIVE/CANCELLED) with the create→match→ready handshake and four new OPP attestation types (RESERVE_CREATE / RESERVE_CREATE_CANCEL / RESERVE_CREATE_CANCELLED / RESERVE_READY). Operator-action dispatch on msgch is rewritten to split TokenAmount/ChainAddress into scalar action args per the no-proto-messages-in-actions rule. batch_operator_plugin now reads chain registry from sysio.chains (replacing the removed sysio.epoch::outposts table); the read is wrapped to tolerate cold-start replay races where the local node hasn't yet replayed the block that creates the sysio.chains account. Operator-table read switched to all-rows + in-plugin filter because the v6 KV PK encoding rejects the v5-style bare-name lower/upper bound the plugin used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the full bidirectional swap flow. flow-swap-with-underwriting now runs 11/11 green with Phase A (ETH→SOL) and Phase B (SOL→ETH) settling end-to-end through underwriter race + dest-chain payout. ## Outpost-client SPI - outpost_client base: `uw_commit(uw_request_id, uic_bytes, deadline)` pure-virtual added so underwriter_plugin routes commits chain-agnostically - ETH concrete: typed operator_registry_contract_client wrapper, hex-encodes uic_bytes as the Solidity `bytes` arg - SOL concrete: typed commit_underwrite wrapper with PDA overrides for operator_registry + outbound_message_buffer ## Underwriter source-deposit verification (Solana) Mirrors the ETH `SwapDeposit(uint64 indexed id, bytes32 hash)` event-scan pattern step-for-step: - `req.source_tx_id` is 8-byte BE deposit_id (same wire shape as ETH) - `getSignaturesForAddress(sol_program_id, limit=50)` enumerates recent program sigs - per-sig `getTransaction` → `meta.logMessages[]` scanned for the canonical marker `Program log: opp_outpost: SwapDeposit id=<id> hash=<64hex>` - hash recomputed from UWREQ flat fields (depositor[32] + 7×u64 BE + u32 BE = 92 bytes packed), bit-compared - `fc::task::retry_until` with 15s poll interval / 120s total budget (`SOL_SWAP_DEPOSIT_POLL_INTERVAL` + `SOL_SWAP_DEPOSIT_TOTAL_TIMEOUT` constexpr in outpost_solana_client_plugin.hpp) ## sysio.msgch: monotonic att_seq singleton `buildenv`'s outbound-bundle cleanup erases all `ATTESTATION_STATUS_PROCESSED` rows for the destination `chain_code`. Inbound `deliver()` rows also carry PROCESSED, so the cleanup drains the table and `available_primary_key()` resets to 0. Phase N+1's inbound SwapRequest then inherits Phase 1's attestation_id, and `sysio.uwrit::createuwreq`'s idempotency guard short-circuits the new swap silently. Fix: new `attseq` singleton holding `next` — survives the cleanup. All `atts.emplace` sites now mint via `mint_att_id(get_self())`. ## sysio.uwrit - createuwreq: silent no-op on duplicate UWREQ (per feedback_opp_handlers_never_throw.md — a throw inside evalcons stalls the chain) - createuwreq: hard-fail SwapRequests with empty `source_tx_id` via SwapRevert (no underwriter can verify without it) - emit_swap_remit: override `recipient.kind` from the destination chain (SOL cranker filters on `kind == CHAIN_KIND_SVM`; UNKNOWN passes through and the recipient is dropped) - try_select_winner: signature verification on both legs of the UIC ## sysio.msgch: depot evalcons idempotency After consensus is reached, `evalcons` was re-dispatching attestations on every post-quorum delivery, queueing duplicate SWAP_REMITs (the 1000× Phase A overshoot observed during diagnostics). Idempotency guard now checks the byepoch index for prior INBOUND dispatch on `(chain_code, epoch_index)`. ## Proto: SwapRequest target_* fields Renamed `quoted_destination_amount` / `quote_tolerance_bps` / `quote_timestamp_ms` → `target_amount` / `target_tolerance_bps` / `target_timestamp_ms`. Users may submit a swap without ever fetching an off-chain quote — they're expressing the target they accept. ## Min-id-1 invariant `available_primary_key()` returns 0 from an empty table; std::max wrap at every id-mint site (msgch, chalg) ensures we never assign id=0 (the proto enum maps 0 to UNKNOWN for every entity type). ## Outpost-client SPI rule Added `.claude/rules/outpost-client-spi.md` documenting the abstraction and capabilities exposed on the SPI. Plugins must never reach into ethereum_client / solana_client directly; all chain-specific operations go through the SPI virtual. ## zpp::bits is CDT-only rule Added `.claude/rules/zpp-bits-is-cdt-only.md`. Host code uses standard protobuf C++ (`SerializeToString`/`ParseFromString`); CDT contracts use zpp::bits via the `.pb.hpp` headers. Inbound population must set every field (including default values) on the host side so the wire bytes round-trip identically across the two serializers. ## cmake build all rule Added `.claude/rules/cmake-build-all-for-contract-changes.md`. Any contract `.cpp` edit requires the default `all` target; narrowing to `--target sysio.<X>` builds the new `.wasm` in `build/` but doesn't propagate the artifact back to the source tree, so the cluster harness deploys the stale source-tree `.wasm` and the bug appears un-fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full SWAP (in-progress)
Write-up coming soon
Reserve Management
Write-up coming soon
Operator Management
Write-up coming soon,
wire-tools-ts/packages/flow-evalidates `underwriter collateral setup including TERMINATED (we TERMINATE if performance issues occur, SLASHING for bad behaviour)Refactored Types/Kinds etc...
Replaces the closed
TokenKind/ChainKindenums with the v6 data model: identity moves onto a new 8-byte packed slug_name (sysio::slug_nameinsysio.opp.common,fc::slug_namein libfc), and Chain / Token / ChainToken / Reserve become first-class registry entities on two new depot contracts (sysio.chains,sysio.tokens).ChainKindcollapses to VM family{WIRE, EVM, SVM},TokenKindcollapses to token standards{NATIVE, ERC20, ERC721, ERC1155, SPL, SPL_NFT, LIQ}; per-chain / per-token INSTANCES are no longer enum values, they're rows.End-to-end verified via wire-tools-ts
flow-e(TerminateBatchOp): 12/12 passing, full OPP relay circulating envelopes across both ETH and SOL outposts.Scope
sysio::slug_name(CDT,_sUDL,SYSLIB_SERIALIZE) +fc::slug_name(host, FC_REFLECT); 17 round-trip tests inlibraries/libfc/test/test_slug_name.cpp.sysio.chains(chains table, regchain/activchain) +sysio.tokens(tokens + chaintokens, regtoken/activtoken/regctok/activctok).sysio.epoch::outpostsis removed; chain registry lookups go through direct cross-contract reads ofsysio.chains::chains.sysio.opreg,sysio.uwrit,sysio.msgch,sysio.epoch,sysio.reservcarry(chain_code, token_code)slug_name pairs throughout. Field names staychain_code/token_code/reserve_code; only the identity TYPE renamed.ReserveStatusenum (PENDING/ACTIVE/CANCELLED) replacesactive: bool. Bootstrap reserves seedstatus=ACTIVEinline viaregreserve; post-bootstrap reserves go through thecreate_reserve(outpost) →matchreserve(depot) →RESERVE_READY(outpost) handshake, with explicitcancel_create_reserve+ race resolution perfeedback_opp_handlers_never_throw.RESERVE_CREATE(60958),RESERVE_CREATE_CANCEL(60959),RESERVE_CREATE_CANCELLED(60960),RESERVE_READY(60961).sysio.msgch::dispatch_operator_actionsplitsTokenAmount/ChainAddressinto scalar action args (perfeedback_no_proto_messages_in_actions) before invokingopreg::depositinle/opreg::withdrawinle.sysio.epoch::outpoststosysio.chains::chains. Read wrapped to tolerate cold-start replay races (local node may not have replayed the block creatingsysio.chainswhen the plugin's first poll fires). Operator-status read switched to all-rows + in-plugin account filter because the v6 KV PK encoding rejects the v5-style bare-name lower/upper bound.Test plan
contracts/tests/contracts_unit_test— all v6 dispatch / opreg / uwrit / reserv / msgch tests passlibraries/libfc/test/test_fc— slug_name round-tripswire-tools-tsflow-e (TerminateBatchOp) 12/12 passing end-to-end against a freshly-bootstrapped 9-batchop/1-underwriter cluster with both ETH + SOL outposts