From e806545191f187f517330355c7255cafe2a33ef5 Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Thu, 8 Oct 2026 17:07:00 -0400 Subject: [PATCH 1/5] Align launch bootstrap and flows with contracts Remove retired reward consumers and capital-action callers, use the shared gross return minimum, and align native Andon permissions and retained flows with the current SDK. Verify raw fallback for removed reward and reserve-creation wire values without blocking active attestation decoding. --- CLAUDE.md | 4 +- README.md | 23 +- docs/local-setup.md | 2 +- docs/production-bootstrap.md | 4 +- packages/cluster-tool/README.md | 3 +- .../src/orchestration/ClusterBuildDefaults.ts | 21 +- .../src/orchestration/steps/RegistrySteps.ts | 5 +- .../steps/contracts/sysio/LiqContractSteps.ts | 65 +-- .../ethereum/EthereumYieldEmitterTool.ts | 165 ------- .../cluster-tool/src/tools/ethereum/index.ts | 1 - .../cluster-tool/src/utils/ethereumUtils.ts | 2 +- .../tests/flow/SyndicationScenario.test.ts | 3 +- .../ClusterBuildDefaultsMockLiqPools.test.ts | 9 +- .../ClusterBuildDefaultsSyndication.test.ts | 3 +- .../orchestration/steps/RegistrySteps.test.ts | 7 + .../contracts/sysio/LiqContractSteps.test.ts | 27 +- .../contracts/sysio/SyndContractSteps.test.ts | 9 +- .../src/features/opp/util/AttestationCodec.ts | 2 - .../opp/util/AttestationCodec.test.ts | 24 + packages/flow-emissions-soak/README.md | 13 +- .../src/EmissionsSoakScenario.ts | 46 +- .../src/EmissionsSoakScenarioConstants.ts | 3 +- .../SyndicationChallengeScenarioConstants.ts | 4 +- .../SyndicationRateLimitScenarioConstants.ts | 4 +- ...yndicationUnderwritingScenarioConstants.ts | 4 +- packages/flow-yield-distribution/README.md | 60 --- packages/flow-yield-distribution/package.json | 21 - .../src/YieldDistributionScenario.ts | 440 ------------------ .../src/YieldDistributionScenarioConstants.ts | 68 --- packages/flow-yield-distribution/src/index.ts | 10 - .../YieldDistributionScenarioEmitSteps.ts | 261 ----------- .../src/steps/index.ts | 1 - .../flow-yield-distribution/tsconfig.json | 11 - .../flow-yield-distribution/tsconfig.src.json | 14 - tsconfig.json | 1 - 35 files changed, 102 insertions(+), 1238 deletions(-) delete mode 100644 packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts delete mode 100644 packages/flow-yield-distribution/README.md delete mode 100644 packages/flow-yield-distribution/package.json delete mode 100644 packages/flow-yield-distribution/src/YieldDistributionScenario.ts delete mode 100644 packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts delete mode 100644 packages/flow-yield-distribution/src/index.ts delete mode 100644 packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts delete mode 100644 packages/flow-yield-distribution/src/steps/index.ts delete mode 100644 packages/flow-yield-distribution/tsconfig.json delete mode 100644 packages/flow-yield-distribution/tsconfig.src.json diff --git a/CLAUDE.md b/CLAUDE.md index 95a55b526..451ce72f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,8 +369,8 @@ the backing and custody to cover it before the epoch relay can realize yield. **Flow authoring — underwriting, syndication and the emergency stop.** The bootstrap deploys `sysio.andon`, `sysio.bond` and `sysio.synd` and configures them unconditionally: `PanicAccount` + `EmergencyStop` (after `BootstrapNodeOwner`: the -panic account `Constants.PANIC_ACCOUNT`, `sysio.andon::setpanic`, -`addpuller(sysio.synd)`) and `SyndicationConfig` (after `LiqConfig`: +panic account `Constants.PANIC_ACCOUNT`, funded native `pull`/`clear` permissions +and action links) and `SyndicationConfig` (after `ShadowLiqTokens`: `sysio.bond::setconfig` and one `sysio.synd::setconfig` per shadow pair, from `Steps.registry.SyndicationConfigRegistrations`, with no fees). A flow that needs a fee or another bucket sets its own `synd::setconfig` in `plan()` — governance, not diff --git a/README.md b/README.md index 0380b35d3..a743f581b 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,6 @@ pnpm workspace (no nx/turbo/lerna); everything lives under `packages/`. | `cluster-tool` | `@wireio/cluster-tool` | Core harness: process managers, chain clients, bootstrap, **`wire-cluster-tool` CLI** | | `flow-operator-collateral-deposit` | `@wireio/test-flow-operator-collateral-deposit` | Node-operator collateral deposit + withdraw remit | | `flow-batch-operator-termination` | `@wireio/test-flow-batch-operator-termination` | Batch-operator termination via delivery underperformance | -| `flow-yield-distribution` | `@wireio/test-flow-yield-distribution` | `STAKING_REWARD` (Ethereum) → `sysio.dclaim::onreward` → `fundclaim` | | `flow-liq-syndication` | `@wireio/test-flow-liq-syndication` | Real syndication reaches the destination wallet; reported yield is fully released | | `flow-liq-yield` | `@wireio/test-flow-liq-yield` | Syndicated liqSOL parked → linked → credited; reported yield minted, sold through `sysio.swap`, claimed as WIRE; `DESYNDICATE_LIQ` paid on the outpost | | `flow-emissions-soak` | `@wireio/test-flow-emissions-soak` | Multi-hour emissions + `sysio.dclaim` payout soak | @@ -157,10 +156,9 @@ and outbound queue entries are intermediate checks. | `flow-syndication-rate-limit` | Actual bond and FIFO/bucket assertions; accepted redemption must reach the external wallet | | `flow-emergency-stop` | Actual bond/challenge and deferred payouts; repaired-custody probe deposits must also reach the wallet | -The other seven enabled flows (`flow-batch-operator-slashing`, +The other six enabled flows (`flow-batch-operator-slashing`, `flow-batch-operator-termination`, `flow-emissions-soak`, `flow-node-owner-nft`, -`flow-operator-collateral-deposit`, `flow-producer-registration`, -`flow-yield-distribution`) submit no user LIQ syndication/desyndication. Imported +`flow-operator-collateral-deposit`, `flow-producer-registration`) exercise their own paths. Imported bootstrap collateral and reward funding have their own assertions. The eight former disabled swap/reserve flows have been removed. @@ -424,19 +422,10 @@ depot needs them too: |---|---| | `OPPContracts` | deploys `sysio.andon` before the contracts that read its cord (`sysio.swap`, `sysio.liq`, `sysio.bond`, `sysio.synd`), and `sysio.bond` before `sysio.synd`, all privileged through `setsyscode`; `OPPCodeGrants` then grants `@sysio.code` to `sysio.bond` and `sysio.synd` (`sysio.andon` sends no inline action) | | `PanicAccount` | creates `andon.panic`, the account that may pull and clear the cord besides `sysio` | -| `EmergencyStop` | `sysio.andon::setpanic(andon.panic)`, then `addpuller(sysio.synd)`, so a custody shortfall pulls the cord — both before any liq token is registered | -| `SyndicationConfig` | after `LiqConfig`: `sysio.bond::setconfig` (hold bond 1000 bps), one `sysio.synd::setconfig` per shadow pair (`ETHEREUM`/`LIQETH`, `SOLANA`/`LIQSOL`: no fees, buckets of one million tokens, a 60 s challenge window, a one-token challenge charge), then verifies that each shadow is at the depot's 9 decimals and each pair is an active liq token with an active binding | - -`setpanic` and `addpuller` run after the four cord readers are deployed, not -before them as `docs/contract-upgrade-order.md` orders a live upgrade: the panic -account must exist first, and the bootstrap creates it only once the bootstrap -node owner can grant its resource policy, which is after `OPPContracts`. The -order is still safe, because what the rule protects is traffic: a reader that -runs before `setpanic` reads the cord as clear, and during the bootstrap no -traffic reaches the readers: the liq tokens are registered only after -`EmergencyStop`, and no envelope is accepted until `EpochBootstrap` starts the -first epoch. By then the panic account is named and `sysio.synd` is a registered -puller. +| `EmergencyStop` | funds and delegates native `pull`/`clear` permissions to `andon.panic` and `sysio`, then links each action before any liq token is registered | +| `SyndicationConfig` | after `ShadowLiqTokens`: `sysio.bond::setconfig` (hold bond 1000 bps), one `sysio.synd::setconfig` per shadow pair (`ETHEREUM`/`LIQETH`, `SOLANA`/`LIQSOL`: no fees, buckets of one million tokens, a 60 s challenge window, a one-token challenge charge and shared 1,000,000-unit gross return minimum), then verifies that each shadow is at the depot's 9 decimals and each pair is an active liq token with an active binding | + +Native action permissions are armed after the cord readers deploy and before registry setup or epoch traffic. Privileged `sysio.synd` can pull the cord without a contract-managed puller registry. Each outpost gets its own emergency stop and per-transfer syndication maximum in local mode: diff --git a/docs/local-setup.md b/docs/local-setup.md index 9c45a69eb..76a964fad 100644 --- a/docs/local-setup.md +++ b/docs/local-setup.md @@ -319,7 +319,7 @@ Each `flow-*` package is a standalone scenario that bootstraps its own cluster. | `flow-batch-operator-slashing` | `flow-liq-syndication` | `flow-syndication-challenge` | | `flow-batch-operator-termination` | `flow-liq-yield` | `flow-syndication-rate-limit` | | `flow-emergency-stop` | `flow-node-owner-nft` | `flow-syndication-underwriting` | -| `flow-emissions-soak` | `flow-operator-collateral-deposit` | `flow-yield-distribution` | +| `flow-emissions-soak` | `flow-operator-collateral-deposit` | | | `flow-producer-registration` | | | Launch one with `scripts/run-flow.mjs` and watch it with `scripts/flow-heartbeat-monitor.mjs` (one monitor diff --git a/docs/production-bootstrap.md b/docs/production-bootstrap.md index 228845571..6dce878e9 100644 --- a/docs/production-bootstrap.md +++ b/docs/production-bootstrap.md @@ -327,7 +327,7 @@ before the first node-owner link in Stage 10 and operator `createlink` calls in | `compute_bps` | `4000` | 40% → producers + batch ops | | `capex_bps` | `2000` | 20% → `sysio.ops` | | `governance_bps` | `1000` | 10% → `sysio.gov` | -| *(implicit capital reserve)* | `3000` | `10000 − compute − capex − governance`; stays on `sysio`, drained by `fundclaim` | +| *(implicit capital reserve)* | `3000` | `10000 − compute − capex − governance`; stays on `sysio`; no automatic LIQ or DClaim capital draw | | `producer_bps` | `7000` | compute split: 70% producers | | `batch_op_bps` | `3000` | compute split: 30% batch ops | | `standby_end_rank` | `28` | producers ranked ≤28 are standby-eligible | @@ -444,7 +444,6 @@ contract/mint addresses via the same shapes. registered liq token: `{sym:"9,LIQETH", chain_code:slug("ETHEREUM"), token_code:slug("LIQETH")}` and `{sym:"9,LIQSOL", chain_code:slug("SOLANA"), token_code:slug("LIQSOL")}`. Syndicated outpost custody is minted 1:1 into these symbols. -36. `sysio.liq::setkicker({bps:200})` — `[sysio.liq@active]` — the T5 yield kicker. 37. **Underwriting + syndication rules:** - `sysio.bond::setconfig({hold_bps:1000})` — `[sysio.bond@active]` — the hold bond, in bps of a request's covered amount (the contract default, set explicitly). @@ -459,6 +458,7 @@ contract/mint addresses via the same shapes. | `window_sec` | `60` | challenge window before `sysio.bond::approve` can succeed **(cluster; production: longer)** | | `bounty` | `0` | bounty posted on each envelope's `sysio.bond` request | | `challenge_extra` | `1000000000` | one token charged to a challenger on top of the hold bond (the contract refuses `0`) | +| `min_desyndicate` | `1000000` | required gross return floor: 0.001 at nine decimals, shared by both pairs; an arbitrary launch choice based on 0.001 SOL | The tooling then verifies that every shadow symbol is at the depot frame's precision and that each `(chain, token)` pair is an active `TOKEN_KIND_LIQ` token with an active binding (otherwise `sysio.msgch` diff --git a/packages/cluster-tool/README.md b/packages/cluster-tool/README.md index a3235db47..db98c9d1e 100644 --- a/packages/cluster-tool/README.md +++ b/packages/cluster-tool/README.md @@ -384,8 +384,7 @@ A flow is a `FlowScenario` composed onto the same engine via `flow-emergency-stop`, `flow-emissions-soak`, `flow-liq-syndication`, `flow-liq-yield`, `flow-node-owner-nft`, `flow-operator-collateral-deposit`, `flow-producer-registration`, `flow-syndication-challenge`, -`flow-syndication-rate-limit`, `flow-syndication-underwriting`, -`flow-yield-distribution` — for end-to-end examples. +`flow-syndication-rate-limit`, `flow-syndication-underwriting` — for end-to-end examples. --- diff --git a/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts b/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts index e9442caeb..24fad4910 100644 --- a/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts +++ b/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts @@ -52,12 +52,6 @@ const ProducerSysGrant = "1000000.0000 SYS" const WireSupply = "1000000000.000000000 WIRE" /** WIRE's ABI symbol — the system token every swap pair quotes its second leg in. */ const WireSymbol = "9,WIRE" -/** - * `sysio.liq`'s T5 kicker: the share of every yield intake requested from T5 - * and folded into the same index bump, in basis points. Mirrors the contract - * default, so a cluster pays yield the way the network will. - */ -const LiqKickerBps = 200 /** * WIRE the panic account is funded with: none. It only signs `sysio.andon::pull` * and `clear`, which move no funds; its resource policy pays for them. @@ -972,7 +966,7 @@ export namespace ClusterBuildDefaults { ) // The shadow-liq system, in the order sysio.liq's README gives a deployment: // the swap's governance + system token, one shadow symbol per registered liq - // token, the T5 kicker. Registry setup a real depot performs too — unconditional. + // token. Registry setup a real depot performs too — unconditional. ClusterBuildPhase.create( prerequisites, "SwapConfig", @@ -998,19 +992,6 @@ export namespace ClusterBuildDefaults { "Open the shadow liq symbols on sysio.liq", {} ) - ClusterBuildPhase.create( - prerequisites, - "LiqConfig", - "Configure sysio.liq" - ).push( - Steps.contracts.sysio.liq.planSetkicker( - Actor.Sysio, - "configure-liq-kicker", - "set the T5 yield kicker", - {}, - { bps: LiqKickerBps } - ) - ) // Underwriting and syndication: sysio.bond's hold bond, each shadow pair's // sysio.synd rules, and the verify of the syndication preconditions. A pair with // no syndconfig row releases nothing, so a real depot configures it too — diff --git a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts index 31588b1c1..e727e0b67 100644 --- a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts @@ -118,6 +118,8 @@ export namespace RegistrySteps { * contract refuses a challenge whose charge is zero, so this is never 0. */ export const SyndicationChallengeExtra = 1_000_000_000 + /** Shared gross return floor: 0.001 SOL at nine decimals, also used for LIQETH. */ + export const MinimumDesyndication = 1_000_000 /** Durable bonder identity; flows resolve its ED/EM keys from readMockSyndicationBonder. */ export const MockSyndicationBonderLabel = "mock-syndication-bonder" @@ -474,7 +476,8 @@ export namespace RegistrySteps { desynd_refill: SyndicationBucketSize, window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: SyndicationBounty, - challenge_extra: SyndicationChallengeExtra + challenge_extra: SyndicationChallengeExtra, + min_desyndicate: MinimumDesyndication })) /** Seed chains + tokens + chain-token bindings. */ diff --git a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts index ee9d81c0a..701f79019 100644 --- a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts @@ -8,14 +8,13 @@ import { } from "../../../ClusterBuildStep.js" import type { StepInput } from "../../../StepRunner.js" -const { SysioContractName, SysioContractAccount } = SysioContracts +const { SysioContractName } = SysioContracts /** * Steps for `sysio.liq` actions — the shadow liq token. * * The contract-signed actions (`create`, `regliqpool`, `recredit`) ride the - * client's default authorization, the contract account. `setkicker` is - * governance's: the contract requires the system account. `claim` is signed by + * client's default authorization, the contract account. `claim` is signed by * the holder named in its data. Sweeping parked shadow and desyndicating are * `sysio.synd`'s actions (`Steps.contracts.sysio.synd`). */ @@ -30,7 +29,9 @@ export namespace LiqContractSteps { * `sysio.liq::create` — open one shadow symbol, bound to an active liq token * of the chain and token registries. Signed by the contract. */ - export function planCreate( + export function planCreate< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -59,50 +60,6 @@ export namespace LiqContractSteps { .actions.create.invoke(input.data) } - /** Input for {@link planSetkicker} — the generated `liq::setkicker` data. */ - export interface SetkickerInput extends StepInput { - readonly kind: "LiqContractSteps.SetkickerInput" - readonly data: SysioContracts.SysioLiqSetkickerAction - } - - /** - * `sysio.liq::setkicker` — set the T5 kicker folded into every yield intake, - * in basis points of the intake. Governance's action: signed by the system - * account, the authority council proposals execute as. - */ - export function planSetkicker( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioLiqSetkickerAction - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "LiqContractSteps.SetkickerInput", data }, - runSetkicker - ) - } - - /** Named runner — `sysio.liq::setkicker`, signed by the system account. */ - export async function runSetkicker( - ctx: C, - input: SetkickerInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.liq) - .actions.setkicker.invoke(input.data, { - authorization: WireClient.activeAuthorization( - SysioContractAccount[SysioContractName.system] - ) - }) - } - /** Input for {@link planRegliqpool} — the generated `liq::regliqpool` data. */ export interface RegliqpoolInput extends StepInput { readonly kind: "LiqContractSteps.RegliqpoolInput" @@ -116,7 +73,9 @@ export namespace LiqContractSteps { * gates it to the epoch-0 bootstrap window, so it only ever runs * pre-EpochBootstrap. Signed by the contract. */ - export function planRegliqpool( + export function planRegliqpool< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -155,7 +114,9 @@ export namespace LiqContractSteps { * `sysio.liq::claim` — pay a holder every subunit of WIRE owed to their * shadow row and settle it at the current index. Signed by the holder. */ - export function planClaim( + export function planClaim< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -198,7 +159,9 @@ export namespace LiqContractSteps { * a release the outpost neither paid nor stored (`docs/sysio-synd.md`, the * recredit rule). Signed by the contract. */ - export function planRecredit( + export function planRecredit< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, diff --git a/packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts b/packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts deleted file mode 100644 index b3872e9bf..000000000 --- a/packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * EthYieldEmitterTool — wraps `MockYieldEmitter.sol` (deployed by - * wire-ethereum's `deployLocal.ts` script under `contracts/test/outpost/`) - * with an ergonomic helper for the flow-yield-distribution test. - * - * The on-chain contract is a permissioned poke-emit fake: an admin - * records synthetic per-staker positions, then triggers a single tx - * that fans STAKING_REWARD attestations onto OPP's outbound queue — - * the same queue the post-launch StakingManager will use. Once an - * attestation lands there, the batch-operator nodeop plugin picks it - * up, ferries it through OPP envelope consensus, and the depot - * dispatches it as `sysio.dclaim::onreward`. - */ - -import Assert from "node:assert" -import { ethers } from "ethers" - -import { loadOutpostContract, resolveLatestNonce } from "../../utils/ethereumUtils.js" - -/** - * Minimal `ethers` surface of `MockYieldEmitter.sol`. Typed structurally - * so the caller can pass any compatible contract instance (typechain, - * mock, hand-rolled stub). - */ -export interface MockYieldEmitterContract extends ethers.BaseContract { - setChainAndTokenCode: ( - chainCode: bigint, - tokenCode: bigint, - overrides?: ethers.Overrides - ) => Promise - stake: ( - staker: string, - amount: bigint, - overrides?: ethers.Overrides - ) => Promise - emitYield: ( - stakers: string[], - wireAccounts: string[], - rewardAmounts: bigint[], - shareBpses: number[], - externalEpochRef: bigint, - rewardEpochIndex: number, - overrides?: ethers.Overrides - ) => Promise - stakeOf: (staker: string) => Promise - totalStaked: () => Promise - lastExternalEpochRef: (staker: string) => Promise - getAddress: () => Promise -} - -/** Per-staker entry in an `emitYield` batch. */ -export interface YieldEntry { - /** ETH address (0x-prefixed) of the staker. */ - staker: string - /** - * WIRE account name the depot will credit. May be `""` for - * not-yet-AuthEx-linked stakers — the depot then parks the reward by - * `staker_native_address` (the ETH addr) until the link sweep moves - * it (`sysio.dclaim::linkswept`). - */ - wireAccount: string - /** Reward amount in native base units (wei for ETH; the contract - * passes through to the depot which converts via PrecisionLib). */ - rewardAmount: bigint - /** Informational share-in-bps for the depot's audit logging. */ - shareBps: number -} - -/** - * Load the hardhat-emitted `MockYieldEmitter.json` artifact from the - * wire-ethereum repo, look up its deployed address from the matching - * `outpost-addrs.json`, and return an ethers Contract instance bound - * to `signer`. The address file is the canonical handoff the rest of - * the harness already consumes; we read it the same way. - */ -export function loadMockYieldEmitter( - ethereumPath: string, - outpostAddrs: Record, - signer: ethers.Signer -): MockYieldEmitterContract { - return loadOutpostContract( - ethereumPath, - outpostAddrs, - "MockYieldEmitter", - ["test", "outpost"], - signer - ) -} - -/** - * Record a stake entry on the emitter. Idempotent in the sense that - * subsequent calls add to the staker's recorded balance. - * - * @param contract Emitter bound to a signer holding the AccessManager - * admin / configured role. - * @param staker ETH address whose stake is being recorded. - * @param amount Native base-unit amount to add to `staker`'s position. - */ -export async function recordStake( - contract: MockYieldEmitterContract, - staker: string, - amount: bigint -): Promise { - Assert.ok(amount > 0n, "EthYieldEmitterTool: stake amount must be positive") - const nonce = await resolveLatestNonce(contract) - const tx = await contract.stake(staker, amount, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthYieldEmitterTool: stake tx reverted (status=${receipt?.status ?? "null"})` - ) - return receipt -} - -/** - * Fan a batch of STAKING_REWARD attestations through OPP outbound. The - * batch must: - * - * * Be non-empty. - * * Use a single monotonic `externalEpochRef` across the batch — the - * contract enforces strict monotonicity per staker (`onreward` - * dedupes against this on the depot side). - * * Have at most one entry per staker (the contract's monotonic - * check would reject a same-`externalEpochRef` repeat). - * - * @param contract Emitter bound to admin signer. - * @param entries Per-staker reward triples. - * @param externalEpochRef Monotonic-per-staker reference. The depot - * rejects any inbound STAKING_REWARD whose - * `external_epoch_ref` is `<=` the staker's - * last processed value. - * @param rewardEpochIndex WIRE epoch index — informational; the - * depot logs it on the credit row. - */ -export async function emitYieldBatch( - contract: MockYieldEmitterContract, - entries: YieldEntry[], - externalEpochRef: bigint, - rewardEpochIndex: number -): Promise { - Assert.ok(entries.length > 0, "EthYieldEmitterTool: empty entries") - Assert.ok(externalEpochRef > 0n, "EthYieldEmitterTool: externalEpochRef must be positive") - - const stakers = entries.map(e => e.staker) - const wireAccounts = entries.map(e => e.wireAccount) - const rewardAmounts = entries.map(e => e.rewardAmount) - const shareBpses = entries.map(e => e.shareBps) - - const nonce = await resolveLatestNonce(contract) - const tx = await contract.emitYield( - stakers, - wireAccounts, - rewardAmounts, - shareBpses, - externalEpochRef, - rewardEpochIndex, - { nonce } - ) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthYieldEmitterTool: emitYield tx reverted (status=${receipt?.status ?? "null"})` - ) - return receipt -} diff --git a/packages/cluster-tool/src/tools/ethereum/index.ts b/packages/cluster-tool/src/tools/ethereum/index.ts index a99e201f1..ef43ebe07 100644 --- a/packages/cluster-tool/src/tools/ethereum/index.ts +++ b/packages/cluster-tool/src/tools/ethereum/index.ts @@ -1,3 +1,2 @@ -export * from "./EthereumYieldEmitterTool.js" export * from "./EthereumNodeOwnerNftTool.js" export * from "./EthereumSyndicationTool.js" diff --git a/packages/cluster-tool/src/utils/ethereumUtils.ts b/packages/cluster-tool/src/utils/ethereumUtils.ts index 2a41b4496..d31adf64d 100644 --- a/packages/cluster-tool/src/utils/ethereumUtils.ts +++ b/packages/cluster-tool/src/utils/ethereumUtils.ts @@ -37,7 +37,7 @@ export const EvmAddressPattern = /^0x[0-9a-fA-F]{40}$/ * `/artifacts/contracts/<...artifactSubpath>/.sol/ * .json`, and bind it to `runner` via {@link contractView}. The * ONE artifact-loading path every per-contract loader (`loadBar`, - * `loadMockWireNodes`, `loadMockYieldEmitter`, …) delegates to. + * `loadMockWireNodes`, …) delegates to. * * @param ethereumPath - The wire-ethereum repo root (artifact tree parent). * @param outpostAddrs - The `outpost-addrs.json` address map. diff --git a/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts b/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts index 45dc90532..8ff87d9e0 100644 --- a/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts +++ b/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts @@ -145,7 +145,8 @@ describe("syndication scenario accounting", () => { desynd_refill: 4, window_sec: 60, bounty: 5, - challenge_extra: 6 + challenge_extra: 6, + min_desyndicate: 1 } ctx.outputs.set(SyndicationScenario.OriginalConfigKey, config) await step.runner(ctx, step.input, signal) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts index c7caad732..6c2414464 100644 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts +++ b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts @@ -27,7 +27,7 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { } } - it("always configures the swap, opens the shadow symbols and sets the kicker, in that order", async () => { + it("always configures the swap, opens the shadow symbols and configures syndication, in that order", async () => { const cluster = await ClusterBuildDefaults.create(baseOptions()) const names = collectPhaseNames(cluster.children) expect(names.indexOf("SwapConfig")).toBeGreaterThan( @@ -36,11 +36,8 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { expect(names.indexOf("ShadowLiqTokens")).toBe( names.indexOf("SwapConfig") + 1 ) - expect(names.indexOf("LiqConfig")).toBe( - names.indexOf("ShadowLiqTokens") + 1 - ) expect(names.indexOf("SyndicationConfig")).toBeGreaterThan( - names.indexOf("LiqConfig") + names.indexOf("ShadowLiqTokens") ) expect(names).not.toContain("UnderwriterConfig") expect(names).not.toContain("ReserveConfig") @@ -60,7 +57,7 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { }) const names = collectPhaseNames(cluster.children) expect(names).toContain("MockLiqPools") - // gated phase follows the kicker and the per-pair syndication config, pre-EpochBootstrap + // gated phase follows the per-pair syndication config, pre-EpochBootstrap expect(names.indexOf("MockLiqPools")).toBe( names.indexOf("SyndicationConfig") + 1 ) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts index 8cd0b909a..088e2a1d6 100644 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts +++ b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts @@ -58,7 +58,6 @@ describe("ClusterBuildDefaults — underwriting, syndication and the emergency s "EmergencyStop", "Registry", "ShadowLiqTokens", - "LiqConfig", "SyndicationConfig", "EpochBootstrap" ]) @@ -69,7 +68,7 @@ describe("ClusterBuildDefaults — underwriting, syndication and the emergency s names.indexOf("PanicAccount") + 1 ) expect(names.indexOf("SyndicationConfig")).toBe( - names.indexOf("LiqConfig") + 1 + names.indexOf("ShadowLiqTokens") + 1 ) }) diff --git a/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts index 61273e3b8..b28f1acd0 100644 --- a/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts @@ -143,6 +143,13 @@ describe("Steps.registry — underwriting and syndication configuration", () => }) }) + it("uses the same 0.001 SOL equivalent gross minimum for both shadow tokens", () => { + expect(Steps.registry.MinimumDesyndication).toBe(1_000_000) + rows.forEach(row => + expect(row.min_desyndicate).toBe(Steps.registry.MinimumDesyndication) + ) + }) + it("takes the challenge window from ProtocolTiming and a non-zero challenge charge", () => { rows.forEach(row => { expect(row.window_sec).toBe( diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts index fb8d6b524..dd0b6fdc9 100644 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts @@ -31,21 +31,6 @@ describe("Steps.contracts.sysio.liq", () => { expect(typeof step.runner).toBe("function") }) - it("setkicker carries the liq::setkicker data", () => { - const data: SysioContracts.SysioLiqSetkickerAction = { bps: 200 } - const step = Steps.contracts.sysio.liq.planSetkicker( - Report.Actor.Sysio, - "configure-liq-kicker", - "set the T5 yield kicker", - {}, - data - ) - expect(step.input.kind).toBe("LiqContractSteps.SetkickerInput") - expect(step.input.data).toBe(data) - expect(step.input.data.bps).toBe(200) - expect(typeof step.runner).toBe("function") - }) - it("regliqpool carries the liq::regliqpool data", () => { const data: SysioContracts.SysioLiqRegliqpoolAction = { chain_code: "SOLANA", @@ -116,7 +101,9 @@ describe("Steps.contracts.sysio.liq", () => { /** A fixture context whose `getSysioContract` hands back one shared `sysio.liq` client. */ function liqContext() { const ctx = fixtureContext(), - contract = ctx.wire.getSysioContract(SysioContracts.SysioContractName.liq) + contract = ctx.wire.getSysioContract( + SysioContracts.SysioContractName.liq + ) jest.spyOn(ctx.wire, "getSysioContract").mockReturnValue(contract) return { ctx, contract } } @@ -125,7 +112,9 @@ describe("Steps.contracts.sysio.liq", () => { it("pushes liq::recredit under the contract's own authority", async () => { const { ctx, contract } = liqContext(), - invoke = jest.spyOn(contract.actions.recredit, "invoke").mockResolvedValue(undefined) + invoke = jest + .spyOn(contract.actions.recredit, "invoke") + .mockResolvedValue(undefined) await Steps.contracts.sysio.liq.runRecredit( ctx, { kind: "LiqContractSteps.RecreditInput", data }, @@ -136,7 +125,9 @@ describe("Steps.contracts.sysio.liq", () => { it("pushes nothing once the step is aborted", async () => { const { ctx, contract } = liqContext(), - invoke = jest.spyOn(contract.actions.recredit, "invoke").mockResolvedValue(undefined), + invoke = jest + .spyOn(contract.actions.recredit, "invoke") + .mockResolvedValue(undefined), controller = new AbortController() controller.abort() await expect( diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts index 0d77b07bf..79b34312c 100644 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts @@ -40,7 +40,8 @@ describe("Steps.contracts.sysio.synd", () => { desynd_refill: 1_000_000_000_000, window_sec: 60, bounty: 0, - challenge_extra: 1_000_000_000 + challenge_extra: 1_000_000_000, + min_desyndicate: 1_000_000 } const step = Steps.contracts.sysio.synd.planSetconfig( Report.Actor.Sysio, @@ -68,7 +69,8 @@ describe("Steps.contracts.sysio.synd", () => { desynd_refill: 1_000_000_000_000, window_sec: 60, bounty: 0, - challenge_extra: 1_000_000_000 + challenge_extra: 1_000_000_000, + min_desyndicate: 1_000_000 } await Steps.contracts.sysio.synd.runSetconfig( ctx, @@ -313,7 +315,8 @@ describe("Steps.contracts.sysio.synd", () => { desynd_refill: 1_000_000_000_000, window_sec: 60, bounty: 0, - challenge_extra: 1_000_000_000 + challenge_extra: 1_000_000_000, + min_desyndicate: 1_000_000 } controller.abort() await expect( diff --git a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts index 4f1bf7c23..3253d0fea 100644 --- a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts +++ b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts @@ -13,7 +13,6 @@ import { PretokenYield, StakeResult, StakeUpdate, - StakingReward, SwapRemit, SwapRequest, SwapRevert, @@ -61,7 +60,6 @@ export const AttestationDecoders: Partial< // commitNode emits and sysio.msgch's dispatch_node_owner_reg decodes; the // legacy 3-field NodeOwnerReg message is no longer emitted by any outpost. [AttestationType.NODE_OWNER_REG]: NodeOwnerRegistration, - [AttestationType.STAKING_REWARD]: StakingReward, [AttestationType.STAKE_RESULT]: StakeResult, [AttestationType.ATTESTATION_PROCESSING_ERROR]: AttestationProcessingError, [AttestationType.UNDERWRITE_INTENT_COMMIT]: UnderwriteIntentCommit, diff --git a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts index e957539df..4b64b8339 100644 --- a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts +++ b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts @@ -79,6 +79,30 @@ describe("decodeAttestation", () => { expect(result.reason).toMatch(/no decoder/) }) + it.each([60950, 60958])( + "renders removed wire type %s as raw without blocking active decoding", + type => { + const entry: AttestationEntry = { + type: type as AttestationType, + dataSize: 1, + data: new Uint8Array([0x0a]) + } + expect(decodeAttestation(entry)).toEqual({ + kind: "raw", + reason: `no decoder registered for AttestationType=${type}`, + entry + }) + const bytes = encodeBatchOperatorGroups() + expect( + decodeAttestation({ + type: AttestationType.BATCH_OPERATOR_GROUPS, + dataSize: bytes.length, + data: bytes + }).kind + ).toBe("decoded") + } + ) + it("falls back to `raw` when decode throws on bogus bytes", () => { const entry: AttestationEntry = { type: AttestationType.BATCH_OPERATOR_GROUPS, diff --git a/packages/flow-emissions-soak/README.md b/packages/flow-emissions-soak/README.md index b823af063..6582e134a 100644 --- a/packages/flow-emissions-soak/README.md +++ b/packages/flow-emissions-soak/README.md @@ -20,17 +20,6 @@ end-to-end. ## What it does NOT verify -- **`sysio.system::fundclaim` cap semantics from PR 354.** That code path - fires only on `sysio.dclaim::onreward`, which is driven by - STAKING_REWARD attestations arriving from the outposts. As of writing, - wire-ethereum `StakingManager.sol` is a rename-only placeholder - (`revert()` bodies); outpost reward emission is on a separate - developer track. A companion exhaust flow will be added in a follow-up - PR once that emission track lands. - - `capital_shortfall_total` is asserted to stay at `0` in this soak — - trivially true today because no `fundclaim` calls occur. - - **Pre-funding of `sysio.dclaim`.** At production launch the dclaim account is credited with the pre-launch capital allocation up-front. This soak replicates that with an in-test `sysio.token::transfer` @@ -105,7 +94,7 @@ SOAK_DURATION_MS=$((30 * 60 * 1000)) node scripts/run-flow.mjs flow-emissions-so **Scaffold only.** The bootstrap wiring (`ClusterManager.ts` Phase 15b/c) is in place; the seed + sample-claim test bodies are stubbed as `it.todo` pending the regen of `@wireio/sdk-core` types against the -post-PR-354 wire-sysio ABIs (`sysio.system::setemitcfg`, `fundclaim`, +post-PR-354 wire-sysio ABIs (`sysio.system::setemitcfg`, `sysio.dclaim::*`). The `convertImportSeed` helper has full unit-test coverage in this file diff --git a/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts b/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts index 1f5f34d10..24503b819 100644 --- a/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts +++ b/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts @@ -84,13 +84,7 @@ async function readCapConfig( * holds their ETH wallets) complete AuthEx linking, whose inline * `linkswept` sweeps `unmapped_tokens` into `pending_claims` → `claim` * pays each staker its exact seeded WIRE. dclaim is pre-funded from - * `sysio` for the synthetic load (the importseed path never calls - * `fundclaim`; only the onreward path does). - * - * **Out of scope:** `sysio.system::fundclaim` cap semantics — that path fires - * only on `sysio.dclaim::onreward` (STAKING_REWARD attestations), pending the - * outpost emission track. `capital_shortfall_total` is asserted `== 0` - * throughout (trivially true today because no `fundclaim` calls occur). + * `sysio` for the synthetic load; imports and claims use only that funding. * * Phases: `ConfigureEmissions` → `GenerateAndImport` → `SetupClaimers` → * `StabilityLoop` → `Claim`. @@ -193,18 +187,13 @@ export class EmissionsSoakScenario extends FlowScenario { verifyStep( Actor.Sysio, "t5-state-initialized", - "t5state exists with non-negative distribution and zero capital shortfall", + "t5state exists with non-negative distribution", async ctx => { const t5 = await readT5State(ctx) Assert.ok( Number(t5.total_distributed) >= 0, "total_distributed must be non-negative" ) - Assert.strictEqual( - Number(t5.capital_shortfall_total), - 0, - "capital_shortfall_total must start at 0" - ) } ) ) @@ -372,18 +361,18 @@ export class EmissionsSoakScenario extends FlowScenario { ) // ── 4. StabilityLoop — sample t5state across the soak window; monotonic - // accrual, zero shortfall, headroom respected. ── + // accrual, headroom respected. ── ClusterBuildPhase.create( cluster, "StabilityLoop", - `Sample t5state for ${Constants.SoakDurationMs}ms; monotonic accrual + zero shortfall`, + `Sample t5state for ${Constants.SoakDurationMs}ms; monotonic accrual within headroom`, [], soakOptions ).push( verifyStep( Actor.Sysio, "soak-monotonic-accrual", - "total_distributed advances monotonically within headroom; capital_shortfall_total stays 0", + "total_distributed advances monotonically within headroom", async (ctx, signal) => { const emissionConfig = await readEmissionConfig(ctx), headroom = @@ -399,20 +388,12 @@ export class EmissionsSoakScenario extends FlowScenario { await sleep(Constants.SampleIntervalMs) const t5 = await readT5State(ctx), distributed = BigInt(t5.total_distributed), - shortfall = BigInt(t5.capital_shortfall_total), elapsedSec = Math.round((Date.now() - startWallMs) / 1000) - log.info( - `[soak] +${elapsedSec}s distributed=${distributed} shortfall=${shortfall}` - ) + log.info(`[soak] +${elapsedSec}s distributed=${distributed}`) Assert.ok( distributed >= lastDistributed, `total_distributed regressed: ${distributed} < ${lastDistributed}` ) - Assert.strictEqual( - shortfall, - 0n, - "unexpected capital shortfall during the soak" - ) lastDistributed = distributed sampleCount += 1 } @@ -434,7 +415,7 @@ export class EmissionsSoakScenario extends FlowScenario { ) // ── 5. Claim — snapshot balances, claim per staker, verify EXACT deltas - // and a still-zero capital shortfall. ── + // and treasury distribution within headroom. ── ClusterBuildPhase.create( cluster, "Claim", @@ -492,19 +473,6 @@ export class EmissionsSoakScenario extends FlowScenario { }) ) } - ), - verifyStep( - Actor.Sysio, - "final-shortfall-zero", - "capital_shortfall_total is still 0 after every claim (no fundclaim calls on this path)", - async ctx => { - const t5 = await readT5State(ctx) - Assert.strictEqual( - BigInt(t5.capital_shortfall_total), - 0n, - "capital_shortfall_total moved without an onreward-driven claim" - ) - } ) ) } diff --git a/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts b/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts index d8c2dfba8..4a54cb243 100644 --- a/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts +++ b/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts @@ -77,8 +77,7 @@ export namespace EmissionsSoakScenarioConstants { ControlledStakerSourceUnits / WireAtomicDivisor /** * WIRE atomic pre-funded from `sysio` to `sysio.dclaim` to cover every - * controlled-staker claim (the importseed path never calls `fundclaim`; only - * the onreward path does — a real launch pre-funds dclaim the same way). + * controlled-staker claim; a real launch pre-funds DClaim the same way. */ export const ClaimPreFundAtomic = PerStakerClaimAtomic * BigInt(ControlledStakerCount) diff --git a/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts b/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts index 1b8a3bd35..e4192d78c 100644 --- a/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts +++ b/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts @@ -3,6 +3,7 @@ import { SysioContracts } from "@wireio/sdk-core" import { outputKey, ProtocolTiming, + Steps, SyndicationScenario } from "@wireio/cluster-tool" @@ -46,7 +47,8 @@ export namespace SyndicationChallengeScenarioConstants { desynd_refill: 0, window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: String(Bounty), - challenge_extra: String(Extra) + challenge_extra: String(Extra), + min_desyndicate: Steps.registry.MinimumDesyndication } /** Fee-pot seed envelope. */ export const SeedEpoch = outputKey( diff --git a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts index 758c7d4fc..27a6ebb15 100644 --- a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts +++ b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts @@ -3,6 +3,7 @@ import { SysioContracts } from "@wireio/sdk-core" import { outputKey, ProtocolTiming, + Steps, SyndicationScenario } from "@wireio/cluster-tool" @@ -47,7 +48,8 @@ export namespace SyndicationRateLimitScenarioConstants { desynd_refill: 0, window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: 0, - challenge_extra: "1000000000" + challenge_extra: "1000000000", + min_desyndicate: Steps.registry.MinimumDesyndication } /** A's envelope epoch. */ export const Epoch = outputKey("rate.epoch", "burst envelope epoch") diff --git a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts index ace464623..0daccb007 100644 --- a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts +++ b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts @@ -3,6 +3,7 @@ import { SysioContracts } from "@wireio/sdk-core" import { outputKey, ProtocolTiming, + Steps, SyndicationScenario } from "@wireio/cluster-tool" @@ -38,7 +39,8 @@ export namespace SyndicationUnderwritingScenarioConstants { desynd_refill: "100000000000", window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: 0, - challenge_extra: "1000000000" + challenge_extra: "1000000000", + min_desyndicate: Steps.registry.MinimumDesyndication } /** Initial ledger and custody balances. */ export const Before = outputKey( diff --git a/packages/flow-yield-distribution/README.md b/packages/flow-yield-distribution/README.md deleted file mode 100644 index b74baae96..000000000 --- a/packages/flow-yield-distribution/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# flow-yield-distribution - -Validates the cross-chain yield-distribution path end-to-end against a -fresh cluster: - -``` -MockYieldEmitter.sol::emitYield (ETH outpost — Solidity fake) - → STAKING_REWARD attestation - → batch operator ferries via OPP envelope - → sysio.msgch dispatches as - sysio.dclaim::onreward - → sysio.system::fundclaim - → sysio.token::transfer (sysio → sysio.dclaim) - → credit into sysio.dclaim::pclaims (if AuthEx-linked) - OR sysio.dclaim::unmapped_tokens (if not yet linked) -``` - -Asserts: - -1. **Reward arrives in pclaims/unmapped.** After a yield emission, a - pclaim row appears for the linked staker, OR an unmapped row for the - unlinked staker. -2. **Fundclaim caps fire correctly.** `sysio` balance decreases by - `min(emission, accounting_available, balance_available)` — never - over-spending. `t5_state.capital_shortfall_total` accrues when the - accounting bucket can't cover the credit. -3. **Dedupe.** Re-emitting the same `external_epoch_ref` is a no-op on - the depot (the dclaim reward-cursor row guards against replay). - -## Ethereum only, for now - -The flow drives the ETH outpost alone. `MockYieldEmitter.sol` stands in -for a real emitter because `StakingManager.sol` is a rename-only -placeholder with no STAKING_REWARD path. - -There is no Solana leg: the SOL staking surface is a separate developer -track and `liqsol_core` emits no STAKING_REWARD, so a SOL leg could only -be a harness-side attestation injection — which the harness no longer -does. It returns when the outpost produces the attestation itself. - -## Running - -Like every flow, run it with the canonical runner + heartbeat monitor pair -(see the repo README's "Running flows" / "Monitoring a live flow run" and -`wire-platform-manifest/.claude/rules/run-flows-via-canonical-scripts.md`): - -```bash -# From the wire-tools-ts root: -node scripts/run-flow.mjs flow-yield-distribution \ - --cluster-path /tmp/wire-flow-yield \ - --wire-build-path $HOME/code/wire/wire-sysio/build \ - --ethereum-path $HOME/code/wire/wire-ethereum \ - --solana-path $HOME/code/wire/wire-solana - -# In a second terminal — the mandatory six-probe heartbeat: -node scripts/flow-heartbeat-monitor.mjs --cluster-path /tmp/wire-flow-yield -``` - -Cluster data lands under `--cluster-path` (env `WIRE_CLUSTER_PATH`); omit it -and the harness generates a fresh temp dir per run. diff --git a/packages/flow-yield-distribution/package.json b/packages/flow-yield-distribution/package.json deleted file mode 100644 index d6d52619f..000000000 --- a/packages/flow-yield-distribution/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@wireio/test-flow-yield-distribution", - "version": "0.1.8", - "private": true, - "type": "commonjs", - "description": "Flow: fake-yield STAKING_REWARD → sysio.dclaim::onreward → sysio.system::fundclaim", - "scripts": { - "build": "tsc -b tsconfig.json", - "test": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", - "@wireio/opp-typescript-models": "^1.0.51", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-yield-distribution/src/YieldDistributionScenario.ts b/packages/flow-yield-distribution/src/YieldDistributionScenario.ts deleted file mode 100644 index d7422a894..000000000 --- a/packages/flow-yield-distribution/src/YieldDistributionScenario.ts +++ /dev/null @@ -1,440 +0,0 @@ -import Assert from "node:assert" -import { ethers } from "ethers" -import { ChainKind } from "@wireio/opp-typescript-models" -import { SysioContracts } from "@wireio/sdk-core" -import { - AuthExLinkTool, - ClusterBuildPhase, - ClusterBuildStep, - FlowScenario, - Report, - ethereumPrivateKeyFromWallet, - outputKey, - pollUntil, - provisionWireUser, - sleep, - verifyStep, - type ClusterBuild, - type ClusterBuildContext, - type ClusterBuildOptions, - type StepInput -} from "@wireio/cluster-tool" -import { YieldDistributionScenarioConstants as Constants } from "./YieldDistributionScenarioConstants.js" -import { YieldDistributionScenarioEmitSteps as EmitSteps } from "./steps/index.js" - -const { SysioContractName } = SysioContracts -const { Actor } = Report - -// ── reads (execute freely inside verify steps) ────────────────────────────── - -/** All `sysio.dclaim::pclaims` rows (a read). */ -async function readPclaimsRows( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.dclaim) - .tables.pclaims.query({ limit: Constants.TableQueryLimit }) - return rows -} - -/** How many `pclaims` rows credit `wireAccount` (a read — the dedupe metric). */ -async function readPclaimsCount( - ctx: ClusterBuildContext, - wireAccount: string -): Promise { - return (await readPclaimsRows(ctx)).filter( - row => row.wire_account === wireAccount - ).length -} - -/** All `sysio.dclaim::unmapped` rows (a read). */ -async function readUnmappedRows( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.dclaim) - .tables.unmapped.query({ limit: Constants.TableQueryLimit }) - return rows -} - -/** The `sysio::t5state` emissions-accounting singleton (a read). */ -async function readT5State( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.system) - .tables.t5state.query({ limit: 1 }) - Assert.ok( - rows.length >= 1, - "sysio::t5state has no row — was initt5 run at bootstrap?" - ) - return rows[0] -} - -/** - * `capital_shortfall_total` as a number (a read). Large int64 columns can - * arrive as JSON strings, so the conversion is runtime normalization, not - * ceremony — the expected value here is 0 either way. - */ -async function readCapitalShortfallTotal( - ctx: ClusterBuildContext -): Promise { - return Number((await readT5State(ctx)).capital_shortfall_total) -} - -/** Lowercased, `0x`-stripped hex of an ETH address — dclaim's `unmapped.native_pubkey` spelling. */ -function ethereumNativePubkey(address: string): string { - return address.toLowerCase().replace(/^0x/, "") -} - -// ── SetupStakers step inputs + named runners ──────────────────────────────── - -/** Input for the linked-staker provisioning step. */ -interface ProvisionLinkedStakerInput extends StepInput { - readonly kind: "YieldDistributionScenario.ProvisionLinkedStakerInput" - /** WIRE account name to create + resource-policy. */ - readonly account: string -} - -/** - * Named runner — mint the linked staker's ETH wallet into `ctx.outputs`, then - * provision its WIRE account (creation under the dev key + the standard - * resource policy, via the shared harness helper). - */ -async function runProvisionLinkedStaker( - ctx: ClusterBuildContext, - input: ProvisionLinkedStakerInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - ctx.outputs.set( - YieldDistributionScenario.LinkedStakerWalletKey, - ethers.Wallet.createRandom() - ) - await provisionWireUser(ctx.wire, input.account) -} - -/** Input for the authex-link step. */ -interface AuthexLinkLinkedStakerInput extends StepInput { - readonly kind: "YieldDistributionScenario.AuthexLinkLinkedStakerInput" - /** WIRE account being linked to its ETH wallet. */ - readonly account: string -} - -/** Named runner — ONE `sysio.authex::createlink` write binding the staker's ETH wallet to its WIRE account. */ -async function runAuthexLinkLinkedStaker( - ctx: ClusterBuildContext, - input: AuthexLinkLinkedStakerInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const wallet = ctx.outputs.assert( - YieldDistributionScenario.LinkedStakerWalletKey - ) - await AuthExLinkTool.createLink(ctx.wire, { - chainKind: ChainKind.EVM, - account: input.account, - privateKey: ethereumPrivateKeyFromWallet(wallet), - ethereumWallet: wallet - }) -} - -/** - * Named runner — mint the UNLINKED staker's ETH wallet into `ctx.outputs`. - * Deliberately NO WIRE account and NO authex link: the depot parking its - * reward in `unmapped` IS the scenario under test. - */ -async function runProvisionUnlinkedStaker( - ctx: ClusterBuildContext, - _input: null, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - ctx.outputs.set( - YieldDistributionScenario.UnlinkedStakerWalletKey, - ethers.Wallet.createRandom() - ) -} - -/** - * Yield distribution — synthetic STAKING_REWARD attestations driven through - * the ETHEREUM outpost into the depot's reward-distribution path - * (`sysio.dclaim::onreward` → `sysio.system::fundclaim` → `sysio.token`), - * porting the old jest suite's four tests 1:1: - * - * 1. **SetupStakers** — the AuthEx-LINKED staker (WIRE account + `createlink`) - * and the UNLINKED staker (a bare ETH wallet), one provisioning step each. - * 2. **EmitEthereumLinked** — `MockYieldEmitter.emitYield` → batchop ferry → - * `pclaims` row for the linked account; `capital_shortfall_total` stays - * unchanged (emissions cover the credit). - * 3. **EmitEthereumUnlinked** — the same emit with an empty WIRE account → - * the depot parks the credit in `unmapped` keyed by the ETH address. - * 4. **DedupReplay** — snapshot the linked `pclaims` count, re-emit the SAME - * `external_epoch_ref` (the emitter's monotonic check reverts), and after a - * settle window the count is unchanged. - */ -export class YieldDistributionScenario extends FlowScenario { - readonly name = "flow-yield-distribution" - readonly description = - "Fake STAKING_REWARD emissions on the Ethereum outpost: pclaims credit (linked), unmapped park (unlinked), and external_epoch_ref replay dedupe" - - override readonly defaults: ClusterBuildOptions = { - epochDurationSec: Constants.EpochDurationSec, - producerCount: Constants.ProducerCount, - batchOperatorCount: Constants.BatchOperatorCount, - underwriterCount: Constants.UnderwriterCount - } - - plan(cluster: ClusterBuild): void { - const emitStepOptions = { timeoutMs: Constants.EmitStepTimeoutMs }, - propagationStepOptions = { - timeoutMs: - Constants.PropagationTimeoutMs + Constants.PollDeadlineBufferMs - }, - dedupeStepOptions = { - timeoutMs: Constants.dedupeSettleMs() + Constants.PollDeadlineBufferMs - } - - // ── 1. SetupStakers — linked (account + authex link) vs unlinked (bare wallet) ── - ClusterBuildPhase.create( - cluster, - "SetupStakers", - "Provision the AuthEx-linked staker (WIRE account + createlink) and the unlinked staker" - ).push( - ClusterBuildStep.create( - Actor.User, - "provision-linked-staker", - `provision WIRE account ${Constants.LinkedStakerAccount} + mint its ETH wallet`, - {}, - { - kind: "YieldDistributionScenario.ProvisionLinkedStakerInput", - account: Constants.LinkedStakerAccount - }, - runProvisionLinkedStaker - ), - ClusterBuildStep.create( - Actor.User, - "authex-link-linked-staker", - `sysio.authex::createlink binds ${Constants.LinkedStakerAccount} to its ETH wallet`, - {}, - { - kind: "YieldDistributionScenario.AuthexLinkLinkedStakerInput", - account: Constants.LinkedStakerAccount - }, - runAuthexLinkLinkedStaker - ), - ClusterBuildStep.create( - Actor.User, - "provision-unlinked-staker", - "mint the unlinked staker's ETH wallet (no WIRE account, no authex link)", - {}, - null, - runProvisionUnlinkedStaker - ) - ) - - // ── 2. EmitEthereumLinked — emitYield → pclaims row; shortfall unchanged ── - ClusterBuildPhase.create( - cluster, - "EmitEthereumLinked", - "MockYieldEmitter emits a linked-staker STAKING_REWARD; the depot credits pclaims" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-capital-shortfall", - "record t5state.capital_shortfall_total before the emission", - async ctx => { - ctx.outputs.set( - YieldDistributionScenario.CapitalShortfallBeforeKey, - await readCapitalShortfallTotal(ctx) - ) - } - ), - EmitSteps.planEthereumEmit( - Actor.EthereumOutpost, - "emit-ethereum-linked", - `emit ${Constants.EthereumRewardPerStaker} wei STAKING_REWARD for ${Constants.LinkedStakerAccount}`, - emitStepOptions, - YieldDistributionScenario.LinkedStakerWalletKey, - Constants.LinkedStakerAccount, - Constants.EthereumRewardPerStaker, - Constants.FullShareBps, - Constants.LinkedStakerExternalEpochRef, - Constants.RewardEpochIndex - ), - verifyStep( - Actor.Sysio, - "pclaims-row-linked", - `a NEW pclaims row appears for ${Constants.LinkedStakerAccount} (new account — any row is new)`, - async ctx => { - await pollUntil( - `pclaims row for ${Constants.LinkedStakerAccount} (ETH)`, - async () => - (await readPclaimsCount(ctx, Constants.LinkedStakerAccount)) >= 1, - Constants.PropagationTimeoutMs, - Constants.PropagationPollMs - ) - }, - propagationStepOptions - ), - verifyStep( - Actor.Sysio, - "capital-shortfall-unchanged", - "t5state.capital_shortfall_total is unchanged (emissions cover the credit)", - async ctx => { - const before = ctx.outputs.assert( - YieldDistributionScenario.CapitalShortfallBeforeKey - ) - Assert.strictEqual( - await readCapitalShortfallTotal(ctx), - before, - "capital_shortfall_total moved — emissions did not cover the credit" - ) - } - ) - ) - - // ── 3. EmitEthereumUnlinked — emitYield("") → unmapped park ── - ClusterBuildPhase.create( - cluster, - "EmitEthereumUnlinked", - "MockYieldEmitter emits an unlinked-staker STAKING_REWARD; the depot parks it in unmapped" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-unmapped-ethereum", - "record the unmapped row count before the emission", - async ctx => { - ctx.outputs.set( - YieldDistributionScenario.EthereumUnmappedCountBeforeKey, - (await readUnmappedRows(ctx)).length - ) - } - ), - EmitSteps.planEthereumEmit( - Actor.EthereumOutpost, - "emit-ethereum-unlinked", - `emit ${Constants.EthereumRewardPerStaker} wei STAKING_REWARD for the unlinked staker`, - emitStepOptions, - YieldDistributionScenario.UnlinkedStakerWalletKey, - Constants.UnlinkedWireAccount, - Constants.EthereumRewardPerStaker, - Constants.FullShareBps, - Constants.UnlinkedStakerExternalEpochRef, - Constants.RewardEpochIndex - ), - verifyStep( - Actor.Sysio, - "unmapped-row-unlinked", - "an unmapped row appears keyed by the unlinked staker's ETH address", - async ctx => { - const wallet = ctx.outputs.assert( - YieldDistributionScenario.UnlinkedStakerWalletKey - ), - expectedPubkey = ethereumNativePubkey(wallet.address) - await pollUntil( - "unmapped row for the unlinked ETH staker", - async () => - (await readUnmappedRows(ctx)).some( - row => row.native_pubkey.toLowerCase() === expectedPubkey - ), - Constants.PropagationTimeoutMs, - Constants.PropagationPollMs - ) - }, - propagationStepOptions - ), - verifyStep( - Actor.Sysio, - "unmapped-count-grew-ethereum", - "the unmapped row count grew past the snapshot", - async ctx => { - const before = ctx.outputs.assert( - YieldDistributionScenario.EthereumUnmappedCountBeforeKey - ) - const after = (await readUnmappedRows(ctx)).length - Assert.ok( - after > before, - `unmapped count ${after} did not grow past ${before}` - ) - } - ) - ) - - // ── 4. DedupReplay — same external_epoch_ref reverts; nothing credits ── - ClusterBuildPhase.create( - cluster, - "DedupReplay", - "Replaying the linked staker's external_epoch_ref reverts at the emitter and credits nothing" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-pclaims-count", - `record ${Constants.LinkedStakerAccount}'s pclaims row count before the replay`, - async ctx => { - ctx.outputs.set( - YieldDistributionScenario.ReplayPclaimsCountBeforeKey, - await readPclaimsCount(ctx, Constants.LinkedStakerAccount) - ) - } - ), - EmitSteps.planEthereumEmitReplay( - Actor.EthereumOutpost, - "emit-ethereum-replay", - "re-emit the SAME external_epoch_ref — the emitter's monotonic check must revert", - emitStepOptions, - YieldDistributionScenario.LinkedStakerWalletKey, - Constants.LinkedStakerAccount, - Constants.EthereumRewardPerStaker, - Constants.FullShareBps, - Constants.RewardEpochIndex - ), - verifyStep( - Actor.Sysio, - "pclaims-count-unchanged", - `pclaims count unchanged after a ${Constants.DedupeSettleEpochs}-epoch settle window`, - async ctx => { - const before = ctx.outputs.assert( - YieldDistributionScenario.ReplayPclaimsCountBeforeKey - ) - await sleep(Constants.dedupeSettleMs()) - Assert.strictEqual( - await readPclaimsCount(ctx, Constants.LinkedStakerAccount), - before, - "replay was not deduped — the linked staker's pclaims count changed" - ) - }, - dedupeStepOptions - ) - ) - } -} - -/** Typed cross-step output keys for the yield-distribution scenario. */ -export namespace YieldDistributionScenario { - /** The AuthEx-linked staker's ETH wallet (minted by SetupStakers). */ - export const LinkedStakerWalletKey = outputKey( - "yieldDistribution.linkedStakerWallet", - "the AuthEx-linked staker's ETH wallet" - ) - /** The UNLINKED staker's ETH wallet (no WIRE account, no authex link). */ - export const UnlinkedStakerWalletKey = outputKey( - "yieldDistribution.unlinkedStakerWallet", - "the unlinked staker's ETH wallet" - ) - /** `t5state.capital_shortfall_total` snapshotted before the linked emission. */ - export const CapitalShortfallBeforeKey = outputKey( - "yieldDistribution.capitalShortfallBefore", - "t5state.capital_shortfall_total before the linked emission" - ) - /** `unmapped` row count snapshotted before the unlinked ETH emission. */ - export const EthereumUnmappedCountBeforeKey = outputKey( - "yieldDistribution.ethereumUnmappedCountBefore", - "unmapped row count before the unlinked ETH emission" - ) - /** The linked staker's `pclaims` row count snapshotted before the replay. */ - export const ReplayPclaimsCountBeforeKey = outputKey( - "yieldDistribution.replayPclaimsCountBefore", - "the linked staker's pclaims row count before the replayed emission" - ) -} diff --git a/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts b/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts deleted file mode 100644 index 7d9619278..000000000 --- a/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the yield-distribution flow. Rewards, share bps, external - * epoch refs, and timing carry over from the previously-validated jest suite - * (2026-06): per-staker rewards are sized so the depot's emission-accounting - * bucket comfortably covers them, the propagation deadline budgets a full - * emitter → OPP ferry → depot dispatch round trip, and the dedupe settle - * window derives from the epoch duration so the flow scales with it. - */ -export namespace YieldDistributionScenarioConstants { - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** Producer nodes stood up by the bootstrap (the old suite's `producerCount`). */ - export const ProducerCount = 3 - /** Batch operators ferrying OPP envelopes (the old suite's `batchOperatorCount`). */ - export const BatchOperatorCount = 3 - /** Underwriters provisioned by the bootstrap (the old suite's `underwriterCount`). */ - export const UnderwriterCount = 0 - - /** The AuthEx-linked staker's WIRE account — its reward lands in `sysio.dclaim::pclaims`. */ - export const LinkedStakerAccount = "yield.lnk" - /** - * The emitter's "unlinked" marker: an empty WIRE account makes the depot park - * the reward in `sysio.dclaim::unmapped` keyed by the staker's native address. - */ - export const UnlinkedWireAccount = "" - - /** Per-staker ETH-side reward (wei — the depot scales via PrecisionLib). */ - export const EthereumRewardPerStaker = 1_000_000n - /** Informational share-in-bps stamped on every emission (100%). */ - export const FullShareBps = 10_000 - /** Informational WIRE epoch index stamped on every emission. */ - export const RewardEpochIndex = 1 - - /** The linked staker's `external_epoch_ref` (the old suite's per-run counter, value 1). */ - export const LinkedStakerExternalEpochRef = 1n - /** The unlinked ETH staker's `external_epoch_ref` (counter value 2). */ - export const UnlinkedStakerExternalEpochRef = 2n - - /** Deadline for an attestation to round-trip emitter → batchop ferry → depot - * table — a single outpost→depot hop (envelope class). */ - export const PropagationTimeoutMs = ProtocolTiming.SingleHopBudgetMs - /** Interval between depot-table polls (ms). */ - export const PropagationPollMs = 2_000 - /** Row page size for `sysio.dclaim` table scans (matches the old suite's read limit). */ - export const TableQueryLimit = 5_000 - /** Ceiling on each emit write step (one outpost tx + confirmation). */ - export const EmitStepTimeoutMs = 60_000 - /** Buffer added on top of each poll/settle deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - /** 1 s in ms — multiplies epoch counts into ms deadlines. */ - export const MsPerSecond = 1_000 - /** Epochs the dedupe verify waits before re-counting `pclaims` (settle window). */ - export const DedupeSettleEpochs = 2 - - /** - * Rejection signature of a replayed `external_epoch_ref` — the - * `MockYieldEmitter` per-staker monotonic check reverts the tx. - */ - export const ReplayRejectionPattern = - /externalEpochRef not monotonic|reverted/i - - /** Settle window slept before asserting the replayed emission credited nothing. */ - export function dedupeSettleMs(): number { - return EpochDurationSec * DedupeSettleEpochs * MsPerSecond - } -} diff --git a/packages/flow-yield-distribution/src/index.ts b/packages/flow-yield-distribution/src/index.ts deleted file mode 100644 index fb2ce8d09..000000000 --- a/packages/flow-yield-distribution/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { YieldDistributionScenario } from "./YieldDistributionScenario.js" - -/** Run the yield-distribution flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(YieldDistributionScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts b/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts deleted file mode 100644 index 6bb7cbd55..000000000 --- a/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * YieldDistributionScenarioEmitSteps — Step factories for the flow's synthetic - * STAKING_REWARD writes. Every emit is its OWN {@link ClusterBuildStep} so the - * `Report` records it: {@link YieldDistributionScenarioEmitSteps.planEthereumEmit} - * (one `MockYieldEmitter.emitYield(...)` tx) and {@link - * YieldDistributionScenarioEmitSteps.planEthereumEmitReplay} (the SAME - * `external_epoch_ref` re-emitted — the emitter's per-staker monotonic check - * MUST revert it). Emitter / contract loading are pure value helpers executed - * INSIDE the runners. - * - * The flow is Ethereum-only: the Solana outpost emits STAKING_REWARD from no - * real path (its staking surface is a separate developer track), and the - * harness no longer injects attestations. The SOL leg returns when the outpost - * produces one. - */ - -/** - * YieldDistributionScenarioEmitSteps — Step factories for the flow's synthetic - * STAKING_REWARD writes. Every emit is its OWN {@link ClusterBuildStep} so the - * `Report` records it: {@link YieldDistributionScenarioEmitSteps.planEthereumEmit} - * (one `MockYieldEmitter.emitYield(...)` tx) and {@link - * YieldDistributionScenarioEmitSteps.planEthereumEmitReplay} (the SAME - * `external_epoch_ref` re-emitted — the emitter's per-staker monotonic check - * MUST revert it). Emitter / contract loading are pure value helpers executed - * INSIDE the runners. - * - * The flow is Ethereum-only: the Solana outpost emits STAKING_REWARD from no - * real path (its staking surface is a separate developer track), and the - * harness no longer injects attestations. The SOL leg returns when the outpost - * produces one. - */ -import Assert from "node:assert" -import { ethers } from "ethers" -import { - OperatorDaemonArtifactsKey, - Report, - emitYieldBatch, - loadMockYieldEmitter, - ClusterBuildStep, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type MockYieldEmitterContract, - type OutputKey, - type StepInput -} from "@wireio/cluster-tool" -import { YieldDistributionScenarioConstants as Constants } from "../YieldDistributionScenarioConstants.js" - -export namespace YieldDistributionScenarioEmitSteps { - // ── Step: ETH-side STAKING_REWARD (`MockYieldEmitter.emitYield`) ────────── - - /** Input for {@link planEthereumEmit} — one `MockYieldEmitter.emitYield(...)` write. */ - export interface EthereumEmitInput extends StepInput { - readonly kind: "YieldDistributionScenarioEmitSteps.EthereumEmitInput" - /** Output key holding the staker's ETH wallet (minted by SetupStakers). */ - readonly stakerWalletKey: OutputKey - /** WIRE account the depot credits — `""` parks the reward in `unmapped`. */ - readonly wireAccount: string - /** Reward in wei (the depot scales via PrecisionLib). */ - readonly rewardAmount: bigint - /** Informational share-in-bps for the depot's audit logging. */ - readonly shareBps: number - /** Monotonic-per-staker reference the emitter + depot dedupe against. */ - readonly externalEpochRef: bigint - /** Informational WIRE epoch index logged on the credit row. */ - readonly rewardEpochIndex: number - } - - /** - * A single `MockYieldEmitter.emitYield(...)` STAKING_REWARD write, signed by - * the run deployer (anvil #0 — holds the AccessManager role granted by - * `deployLocal.ts`). The attestation lands on OPP's outbound queue and the - * batch operators ferry it to the depot's `sysio.dclaim::onreward`. - * - * @param actor - The narrative subject (the Ethereum outpost emits). - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param stakerWalletKey - Output key of the staker's ETH wallet. - * @param wireAccount - WIRE account to credit (`""` → unmapped park). - * @param rewardAmount - Reward in wei. - * @param shareBps - Informational share-in-bps. - * @param externalEpochRef - Monotonic-per-staker reference. - * @param rewardEpochIndex - Informational WIRE epoch index. - * @returns The definition step. - */ - export function planEthereumEmit< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - stakerWalletKey: OutputKey, - wireAccount: string, - rewardAmount: bigint, - shareBps: number, - externalEpochRef: bigint, - rewardEpochIndex: number - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "YieldDistributionScenarioEmitSteps.EthereumEmitInput", - stakerWalletKey, - wireAccount, - rewardAmount, - shareBps, - externalEpochRef, - rewardEpochIndex - }, - runEthereumEmit - ) - } - - /** Named runner — ONE `MockYieldEmitter.emitYield(...)` write for one staker. */ - export async function runEthereumEmit( - ctx: C, - input: EthereumEmitInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const staker = ctx.outputs.assert(input.stakerWalletKey) - await emitYieldBatch( - loadEmitter(ctx), - [ - { - staker: staker.address, - wireAccount: input.wireAccount, - rewardAmount: input.rewardAmount, - shareBps: input.shareBps - } - ], - input.externalEpochRef, - input.rewardEpochIndex - ) - } - - // ── Step: replayed ETH-side emission (same `external_epoch_ref`) ────────── - - /** Input for {@link planEthereumEmitReplay} — the replayed-`external_epoch_ref` write attempt. */ - export interface EthereumEmitReplayInput extends StepInput { - readonly kind: "YieldDistributionScenarioEmitSteps.EthereumEmitReplayInput" - /** Output key holding the staker's ETH wallet (the linked staker). */ - readonly stakerWalletKey: OutputKey - /** WIRE account of the original emission being replayed. */ - readonly wireAccount: string - /** Reward in wei (same as the original emission). */ - readonly rewardAmount: bigint - /** Informational share-in-bps. */ - readonly shareBps: number - /** Informational WIRE epoch index. */ - readonly rewardEpochIndex: number - } - - /** - * Re-emit the staker's LAST `external_epoch_ref` (read back from the emitter's - * `lastExternalEpochRef`, exactly like the old suite) and assert the tx - * REVERTS on the per-staker monotonic check. The write is attempted; the - * revert is the expected outcome, so a replay that LANDS fails the step. - * - * @param actor - The narrative subject (the Ethereum outpost emits). - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param stakerWalletKey - Output key of the linked staker's ETH wallet. - * @param wireAccount - WIRE account of the original emission. - * @param rewardAmount - Reward in wei (same as the original emission). - * @param shareBps - Informational share-in-bps. - * @param rewardEpochIndex - Informational WIRE epoch index. - * @returns The definition step. - */ - export function planEthereumEmitReplay< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - stakerWalletKey: OutputKey, - wireAccount: string, - rewardAmount: bigint, - shareBps: number, - rewardEpochIndex: number - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "YieldDistributionScenarioEmitSteps.EthereumEmitReplayInput", - stakerWalletKey, - wireAccount, - rewardAmount, - shareBps, - rewardEpochIndex - }, - runEthereumEmitReplay - ) - } - - /** Named runner — attempt ONE replayed `emitYield(...)` write; assert it reverts. */ - export async function runEthereumEmitReplay( - ctx: C, - input: EthereumEmitReplayInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const staker = ctx.outputs.assert(input.stakerWalletKey) - const emitter = loadEmitter(ctx) - // The staker's last processed ref (a read) — the exact value the original - // emission recorded, so re-submitting it violates strict monotonicity. - const replayedRef = await emitter.lastExternalEpochRef(staker.address) - Assert.ok( - replayedRef > 0n, - `YieldDistributionScenarioEmitSteps.ethereumEmitReplay: no prior emission recorded for ${staker.address}` - ) - await Assert.rejects( - () => - emitYieldBatch( - emitter, - [ - { - staker: staker.address, - wireAccount: input.wireAccount, - rewardAmount: input.rewardAmount, - shareBps: input.shareBps - } - ], - replayedRef, - input.rewardEpochIndex - ), - Constants.ReplayRejectionPattern, - `expected the replayed external_epoch_ref ${replayedRef} to revert on the emitter's monotonic check` - ) - } - - // ── value helpers (artifact loads — executed INSIDE runners) ────────────── - - /** - * Resolve `MockYieldEmitter` from the run's deploy artifacts, bound to the - * run deployer signer (anvil #0 — the AccessManager admin `deployLocal.ts` - * configured). Address from `outpost-addrs.json`; ABI from the hardhat - * artifact — both via the harness loaders. - * - * @param ctx - The build context (supplies `ethereumPath` + the deployer signer). - * @returns The emitter bound to the deployer. - */ - export function loadEmitter( - ctx: C - ): MockYieldEmitterContract { - return loadMockYieldEmitter( - ctx.config.ethereumPath, - ctx.outputs.assert(OperatorDaemonArtifactsKey).ethereumAddresses, - ctx.ethereum.wallet.signer - ) - } -} diff --git a/packages/flow-yield-distribution/src/steps/index.ts b/packages/flow-yield-distribution/src/steps/index.ts deleted file mode 100644 index 6ab1064dd..000000000 --- a/packages/flow-yield-distribution/src/steps/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./YieldDistributionScenarioEmitSteps.js" diff --git a/packages/flow-yield-distribution/tsconfig.json b/packages/flow-yield-distribution/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-yield-distribution/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - - }, - "include": [], - "references": [ - { "path": "../cluster-tool/tsconfig.json" }, - { "path": "./tsconfig.src.json" } - ] -} diff --git a/packages/flow-yield-distribution/tsconfig.src.json b/packages/flow-yield-distribution/tsconfig.src.json deleted file mode 100644 index e67621b57..000000000 --- a/packages/flow-yield-distribution/tsconfig.src.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": ["../cluster-tool/src"] - } - }, - "include": ["src"], - "references": [ - { "path": "../cluster-tool/tsconfig.json" } - ] -} diff --git a/tsconfig.json b/tsconfig.json index a71e1fd4d..c2b1efd25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,6 @@ { "path": "./packages/flow-batch-operator-slashing/tsconfig.json" }, { "path": "./packages/flow-node-owner-nft/tsconfig.json" }, { "path": "./packages/flow-emissions-soak/tsconfig.json" }, - { "path": "./packages/flow-yield-distribution/tsconfig.json" }, { "path": "./packages/flow-liq-syndication/tsconfig.json" }, { "path": "./packages/flow-liq-yield/tsconfig.json" }, { "path": "./packages/flow-syndication-underwriting/tsconfig.json" }, From e3d76da15c6cb248beba8268d97b72e2234707b6 Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Fri, 9 Oct 2026 15:37:51 -0400 Subject: [PATCH 2/5] Require published SDK 1.0.97 for the min_desyndicate config Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SUTuRushtiEZgUXLRaoEiv --- package.json | 2 +- packages/cluster-tool-shared/package.json | 2 +- packages/cluster-tool/package.json | 2 +- packages/debugging-client-shared/package.json | 2 +- packages/debugging-client-tool-tui/package.json | 2 +- packages/debugging-client-tool/package.json | 2 +- packages/debugging-server/package.json | 2 +- packages/flow-batch-operator-slashing/package.json | 2 +- packages/flow-batch-operator-termination/package.json | 2 +- packages/flow-emergency-stop/package.json | 2 +- packages/flow-emissions-soak/package.json | 2 +- packages/flow-liq-syndication/package.json | 2 +- packages/flow-liq-yield/package.json | 2 +- packages/flow-node-owner-nft/package.json | 2 +- packages/flow-operator-collateral-deposit/package.json | 2 +- packages/flow-producer-registration/package.json | 2 +- packages/flow-syndication-challenge/package.json | 2 +- packages/flow-syndication-rate-limit/package.json | 2 +- packages/flow-syndication-underwriting/package.json | 2 +- packages/test-app-server/package.json | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 0b4e19e7f..af17a6e23 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@types/node": "25.5.0", "@types/proper-lockfile": "^4.1.4", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", "eslint": "^10.7.0", diff --git a/packages/cluster-tool-shared/package.json b/packages/cluster-tool-shared/package.json index 17dd57a23..0b8a8b206 100644 --- a/packages/cluster-tool-shared/package.json +++ b/packages/cluster-tool-shared/package.json @@ -34,7 +34,7 @@ "dependencies": { "@3fv/prelude-ts": "^0.8.42", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "zod": "^4.4.3" }, diff --git a/packages/cluster-tool/package.json b/packages/cluster-tool/package.json index 93e4b55c1..7b94b79e8 100644 --- a/packages/cluster-tool/package.json +++ b/packages/cluster-tool/package.json @@ -40,7 +40,7 @@ "@wireio/debugging-server": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", "archiver": "^8.0.0", diff --git a/packages/debugging-client-shared/package.json b/packages/debugging-client-shared/package.json index 0c2473636..ffe3f7598 100644 --- a/packages/debugging-client-shared/package.json +++ b/packages/debugging-client-shared/package.json @@ -37,7 +37,7 @@ "@wireio/cluster-tool-shared": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "bluebird": "^3.7.2", "eventemitter3": "^5.0.4", diff --git a/packages/debugging-client-tool-tui/package.json b/packages/debugging-client-tool-tui/package.json index 11bc934dc..3607c66d1 100644 --- a/packages/debugging-client-tool-tui/package.json +++ b/packages/debugging-client-tool-tui/package.json @@ -40,7 +40,7 @@ "@wireio/debugging-client-shared": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "bluebird": "^3.7.2", "eventemitter3": "^5.0.4", diff --git a/packages/debugging-client-tool/package.json b/packages/debugging-client-tool/package.json index b19695d80..431aabe63 100644 --- a/packages/debugging-client-tool/package.json +++ b/packages/debugging-client-tool/package.json @@ -36,7 +36,7 @@ "@wireio/debugging-client-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "get-port": "^7.2.0", "lodash": "^4.17.21", "ts-pattern": "^5.9.0", diff --git a/packages/debugging-server/package.json b/packages/debugging-server/package.json index 90c8263a0..a741ce3bc 100644 --- a/packages/debugging-server/package.json +++ b/packages/debugging-server/package.json @@ -38,7 +38,7 @@ "@wireio/cluster-tool-shared": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "bluebird": "^3.7.2", "express": "^5.2.1", diff --git a/packages/flow-batch-operator-slashing/package.json b/packages/flow-batch-operator-slashing/package.json index 43e529816..b3a533357 100644 --- a/packages/flow-batch-operator-slashing/package.json +++ b/packages/flow-batch-operator-slashing/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "ethers": "^6.14.0" diff --git a/packages/flow-batch-operator-termination/package.json b/packages/flow-batch-operator-termination/package.json index 66276f3c7..970855f2f 100644 --- a/packages/flow-batch-operator-termination/package.json +++ b/packages/flow-batch-operator-termination/package.json @@ -13,7 +13,7 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96" }, diff --git a/packages/flow-emergency-stop/package.json b/packages/flow-emergency-stop/package.json index 2a92ffd7f..bc41566a5 100644 --- a/packages/flow-emergency-stop/package.json +++ b/packages/flow-emergency-stop/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21", "@coral-xyz/anchor": "^0.31.0", diff --git a/packages/flow-emissions-soak/package.json b/packages/flow-emissions-soak/package.json index 1fb4fcdc2..8d67d9e94 100644 --- a/packages/flow-emissions-soak/package.json +++ b/packages/flow-emissions-soak/package.json @@ -12,7 +12,7 @@ "dependencies": { "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "ethers": "^6.14.0" diff --git a/packages/flow-liq-syndication/package.json b/packages/flow-liq-syndication/package.json index e49e1957b..c20865cd7 100644 --- a/packages/flow-liq-syndication/package.json +++ b/packages/flow-liq-syndication/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51" }, "devDependencies": { diff --git a/packages/flow-liq-yield/package.json b/packages/flow-liq-yield/package.json index 381af15d8..b717fca0b 100644 --- a/packages/flow-liq-yield/package.json +++ b/packages/flow-liq-yield/package.json @@ -12,7 +12,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51" }, "devDependencies": { diff --git a/packages/flow-node-owner-nft/package.json b/packages/flow-node-owner-nft/package.json index 59c95771c..6756d7c7d 100644 --- a/packages/flow-node-owner-nft/package.json +++ b/packages/flow-node-owner-nft/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96" + "@wireio/sdk-core": "^1.0.97" }, "devDependencies": { "@types/node": "25.5.0" diff --git a/packages/flow-operator-collateral-deposit/package.json b/packages/flow-operator-collateral-deposit/package.json index e4530332e..89c57d879 100644 --- a/packages/flow-operator-collateral-deposit/package.json +++ b/packages/flow-operator-collateral-deposit/package.json @@ -13,7 +13,7 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", diff --git a/packages/flow-producer-registration/package.json b/packages/flow-producer-registration/package.json index 404ab1826..cc3ae5d35 100644 --- a/packages/flow-producer-registration/package.json +++ b/packages/flow-producer-registration/package.json @@ -14,7 +14,7 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", diff --git a/packages/flow-syndication-challenge/package.json b/packages/flow-syndication-challenge/package.json index bab0e3624..0c9b8eca6 100644 --- a/packages/flow-syndication-challenge/package.json +++ b/packages/flow-syndication-challenge/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21", "@wireio/shared": "^1.0.96" diff --git a/packages/flow-syndication-rate-limit/package.json b/packages/flow-syndication-rate-limit/package.json index 69c538258..b7e1bf77f 100644 --- a/packages/flow-syndication-rate-limit/package.json +++ b/packages/flow-syndication-rate-limit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21" }, diff --git a/packages/flow-syndication-underwriting/package.json b/packages/flow-syndication-underwriting/package.json index f3e6f8adf..d3d920fc9 100644 --- a/packages/flow-syndication-underwriting/package.json +++ b/packages/flow-syndication-underwriting/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21" }, diff --git a/packages/test-app-server/package.json b/packages/test-app-server/package.json index 37e8f43c8..676801105 100644 --- a/packages/test-app-server/package.json +++ b/packages/test-app-server/package.json @@ -11,7 +11,7 @@ "clean": "rm -rf lib dist" }, "dependencies": { - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "@wireio/wallet-ext-sdk": "^0.1.84", "buffer": "^6.0.3", From 60d79b6e7029e0c9863f974409d4b14b4b699da8 Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Fri, 9 Oct 2026 20:13:56 -0400 Subject: [PATCH 3/5] Drop retired OPP carriers from the harness wire-sysio PR #674 removes the retired reserve, swap, underwriting, pretoken, staking and processing-error OPP carriers, and the underwriter outpost roles on the sysio.chains registry. The harness still wrote those registry fields, decoded the removed payloads for display, and scanned for SwapRevert. Stop writing operator_registry_addr / source_deposit_addr in RegistrySteps and drop the fixtures that carried OperatorRegistry and ReserveManager addresses for the deleted underwriter daemon. Remove the retired display decoders (StakeUpdate, StakeResult and AttestationProcessingError among them) and the swap-revert scanner; the codec test now pins that removed wire values still render raw beside decoded entries, and the envelope-scan tests use live attestation types. Native sysio.swap tooling, the Solana OperatorRegistry PDA, and the node-owner and pretoken purchase history surfaces stay: they are live. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ARSeK35wwkiAy4et9mCkVm --- .../config/ExternalOutpostConfig.test.ts | 2 +- .../cluster-tool/src/flow/oppEnvelopeScan.ts | 27 +------ .../src/orchestration/steps/RegistrySteps.ts | 17 ++--- .../src/tools/solana/SolanaFundingTool.ts | 4 -- .../tests/flow/oppEnvelopeScan.test.ts | 71 ++++++++++++------- .../steps/ExternalClusterConfigSteps.test.ts | 9 +-- .../steps/ExternalOutpostSteps.test.ts | 4 +- .../sysio/ChainsContractSteps.test.ts | 4 +- .../processes/NodeopProcessSteps.test.ts | 4 +- .../tools/wire/OperatorDaemonTool.test.ts | 4 +- .../src/features/opp/util/AttestationCodec.ts | 37 ++-------- .../opp/util/AttestationCodec.test.ts | 5 +- 12 files changed, 71 insertions(+), 117 deletions(-) diff --git a/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts b/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts index 3e5e8f20c..b71c058ba 100644 --- a/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts +++ b/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts @@ -13,7 +13,7 @@ describe("ExternalOutpostConfig", () => { const config: ExternalOutpostConfig = { ethereum: { addressFile: "outpost-addrs.json", - abiFiles: ["eth-abis/OPP.json", "eth-abis/OperatorRegistry.json"], + abiFiles: ["eth-abis/OPP.json", "eth-abis/OPPInbound.json"], chainId: 1 }, solana: { idlFile: "solana-idls/opp_outpost.json" } diff --git a/packages/cluster-tool/src/flow/oppEnvelopeScan.ts b/packages/cluster-tool/src/flow/oppEnvelopeScan.ts index ae04d5ca9..db9892449 100644 --- a/packages/cluster-tool/src/flow/oppEnvelopeScan.ts +++ b/packages/cluster-tool/src/flow/oppEnvelopeScan.ts @@ -14,10 +14,9 @@ import { * serialized OPP envelope bytes each side of a cross-chain edge emitted or * consumed (file naming: `--.data`). * - * Negative-assertion flows (variance revert, private-reserve gating) use these - * to prove a `SWAP_REVERT` attestation circulated without pulling the full - * proto decoder into the scenario: the attestation's type tag has a fixed, - * unambiguous byte encoding inside the envelope payload. + * Flows use these to prove an attestation of a given type circulated without + * pulling the full proto decoder into the scenario: the attestation's type tag + * has a fixed, unambiguous byte encoding inside the envelope payload. */ /** @@ -87,26 +86,6 @@ export function envelopeDataContains( ) } -/** - * Whether a `SWAP_REVERT` attestation has circulated on `direction` — the - * canonical negative-path proof for the variance-revert and private-reserve - * gating flows. - * - * @param oppDebuggingDirectory - The cluster's `data/opp-debugging/` path. - * @param direction - The cross-chain edge to scan (default: depot → Ethereum). - * @returns Whether any matching envelope carries a SWAP_REVERT attestation. - */ -export function containsSwapRevert( - oppDebuggingDirectory: string, - direction: DebugOutpostEndpointsType = DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM -): boolean { - return envelopeDataContains( - oppDebuggingDirectory, - direction, - attestationEntryTag(AttestationType.SWAP_REVERT) - ) -} - /** * Whether a `SYNDICATE_LIQ` attestation has circulated on `direction` — the * outpost → depot proof that a syndication enqueued on the outpost reached an diff --git a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts index e727e0b67..3ba7bb0b4 100644 --- a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts @@ -551,20 +551,17 @@ export namespace RegistrySteps { code: "ETHEREUM", outpost: { opp_addr: ethAddress("OPP"), - opp_inbound_addr: ethAddress("OPPInbound"), - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: ethAddress("OPPInbound") } }) - // One Solana program serves every role, so it goes in `opp_addr` alone — - // `sysio.chains` rejects an SVM row that fills the role-specific fields. + // One Solana program receives inbound and emits outbound envelopes, so it + // goes in `opp_addr` alone — `sysio.chains` rejects an SVM row that sets + // `opp_inbound_addr`. await chains.actions.setoutpost.invoke({ code: "SOLANA", outpost: { opp_addr: artifacts.solanaProgramId, - opp_inbound_addr: "", - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: "" } }) } @@ -754,9 +751,7 @@ export namespace RegistrySteps { // daemons skip a chain whose addresses are not set yet. const emptyOutpost: SysioContracts.SysioChainsOutpostAddrsType = { opp_addr: "", - opp_inbound_addr: "", - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: "" } const chainRegistrations: SysioContracts.SysioChainsRegchainAction[] = [ { diff --git a/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts b/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts index 54bf4ba1e..3a64d6d6b 100644 --- a/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts +++ b/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts @@ -40,10 +40,6 @@ const log = getLogger(__filename) * `@solana/spl-token`'s `createMint`, which relies on a WebSocket subscription * for confirmation the test validator doesn't reliably serve) and confirmed via * the polling {@link confirmSignature}. - * - * Recipient ATAs for swap *destinations* are NOT pre-created — the on-chain - * `handle_swap_remit` SPL branch creates them on demand with the Reserve PDA as - * rent payer. */ export namespace SolanaFundingTool { /** Minimum SPL mint decimal scale. */ diff --git a/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts b/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts index 35805d3c6..9c264988d 100644 --- a/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts +++ b/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts @@ -13,15 +13,12 @@ import { attestationEntryTag, containsDesyndicateLIQ, containsLIQYield, - containsSwapRevert, containsSyndicateLIQ, envelopeDataContains, readEnvelopeAttestations, varintBytes } from "@wireio/cluster-tool/flow" -/** The known wire encoding of `ATTESTATION_TYPE_SWAP_REVERT` (60955). */ -const SwapRevertTagBytes = [0x08, 0x9b, 0xdc, 0x03] /** The known wire encoding of `ATTESTATION_TYPE_SYNDICATE_LIQ` (60963). */ const SyndicateLIQTagBytes = [0x08, 0xa3, 0xdc, 0x03] /** The known wire encoding of `ATTESTATION_TYPE_LIQ_YIELD` (60964). */ @@ -83,19 +80,14 @@ describe("oppEnvelopeScan", () => { expect(varintBytes(0x7f)).toEqual([0x7f]) }) it("encodes multi-group values least-significant group first", () => { - expect(varintBytes(AttestationType.SWAP_REVERT)).toEqual( - SwapRevertTagBytes.slice(1) + expect(varintBytes(AttestationType.SYNDICATE_LIQ)).toEqual( + SyndicateLIQTagBytes.slice(1) ) }) }) describe("attestationEntryTag", () => { - it("prefixes the field-1 varint tag to the enum's varint", () => { - expect([...attestationEntryTag(AttestationType.SWAP_REVERT)]).toEqual( - SwapRevertTagBytes - ) - }) - it("encodes each liq-syndication type distinctly", () => { + it("prefixes the field-1 varint tag to each liq-syndication type's varint", () => { expect([...attestationEntryTag(AttestationType.SYNDICATE_LIQ)]).toEqual( SyndicateLIQTagBytes ) @@ -108,58 +100,83 @@ describe("oppEnvelopeScan", () => { }) }) - describe("envelopeDataContains / containsSwapRevert", () => { + describe("envelopeDataContains", () => { it("is false for a missing directory", () => { - expect(containsSwapRevert(Path.join(oppDirectory, "absent"))).toBe(false) + expect( + envelopeDataContains( + Path.join(oppDirectory, "absent"), + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(false) }) it("is false when no artifact carries the pattern", () => { writeArtifact( - DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([0x01, 0x02, 0x03]) ) - expect(containsSwapRevert(oppDirectory)).toBe(false) + expect( + envelopeDataContains( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(false) }) - it("finds the SWAP_REVERT tag inside a matching-direction artifact", () => { + it("finds the tag inside a matching-direction artifact", () => { writeArtifact( - DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM, - Buffer.from([0xff, ...SwapRevertTagBytes, 0xff]) + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + Buffer.from([0xff, ...SyndicateLIQTagBytes, 0xff]) ) - expect(containsSwapRevert(oppDirectory)).toBe(true) + expect( + envelopeDataContains( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(true) }) it("ignores artifacts from other directions", () => { writeArtifact( DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, - Buffer.from(SwapRevertTagBytes) + Buffer.from(SyndicateLIQTagBytes) ) - expect(containsSwapRevert(oppDirectory)).toBe(false) expect( - containsSwapRevert( + envelopeDataContains( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(false) + expect( + envelopeDataContains( oppDirectory, - DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA + DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) ) ).toBe(true) }) - it("scans for arbitrary attestation tags via envelopeDataContains", () => { + it("distinguishes attestation types by their tag", () => { writeArtifact( DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - Buffer.from([...attestationEntryTag(AttestationType.SWAP_REQUEST)]) + Buffer.from([...attestationEntryTag(AttestationType.LIQ_YIELD)]) ) expect( envelopeDataContains( oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - attestationEntryTag(AttestationType.SWAP_REQUEST) + attestationEntryTag(AttestationType.LIQ_YIELD) ) ).toBe(true) expect( envelopeDataContains( oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - attestationEntryTag(AttestationType.SWAP_REVERT) + attestationEntryTag(AttestationType.DESYNDICATE_LIQ) ) ).toBe(false) }) diff --git a/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts index 32f62c6d9..f9f79df22 100644 --- a/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts @@ -102,15 +102,12 @@ const ExternalEthereumRpcUrl = "https://ethereum-rpc.external.example/" const ExternalSolanaRpcUrl = "https://solana-rpc.external.example/" /** - * The deployed ETH outpost addresses the operator daemons' argv resolves - * through — every key `OperatorDaemonTool.assertAddress` demands (batch reads - * OPP + OPPInbound, underwriter reads OperatorRegistry + ReserveManager). + * The deployed ETH outpost addresses the batch-operator daemon's argv resolves + * through (OPP + OPPInbound). */ const OutpostAddresses = { OPP: "0x1111111111111111111111111111111111111111", - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" } /** The one hardhat artifact the ABI generation needs (`ethereumAbiFiles.length > 0`). */ diff --git a/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts index 841d9332b..3184dd3b0 100644 --- a/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts @@ -37,9 +37,7 @@ describe("Steps.externalOutpost (materialize + publish)", () => { addressFile, JSON.stringify({ OPP: OppAddress, - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" }) ) Fs.writeFileSync( diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts index 2573bb91b..d7241c872 100644 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts @@ -15,9 +15,7 @@ describe("Steps.contracts.sysio.chains", () => { // setoutpost once the daemon artifacts resolve. outpost: { opp_addr: "", - opp_inbound_addr: "", - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: "" } } const step = Steps.contracts.sysio.chains.planRegchain( diff --git a/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts index 581463656..6368bb76b 100644 --- a/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts @@ -65,9 +65,7 @@ const artifactsFixture: OperatorDaemonArtifacts = { ethereumAbiFiles: ["/cluster/data/eth-abis/OPP.json"], ethereumAddresses: { OPP: "0x1111111111111111111111111111111111111111", - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" }, solanaProgramId: "GrqvbZLCLkfeSQqvE7rL8XKHVWjNhAG2faLsY8yr9tD5", solanaIdlFile: "/cluster/data/solana-idls/liqsol_core.json" diff --git a/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts b/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts index 29a40322b..650a92602 100644 --- a/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts +++ b/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts @@ -74,9 +74,7 @@ const artifacts: OperatorDaemonArtifacts = { ], ethereumAddresses: { OPP: "0x1111111111111111111111111111111111111111", - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" }, solanaProgramId: "GrqvbZLCLkfeSQqvE7rL8XKHVWjNhAG2faLsY8yr9tD5", solanaIdlFile: "/cluster/data/solana-idls/liqsol_core.json" diff --git a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts index 3253d0fea..b466f3ae4 100644 --- a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts +++ b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts @@ -1,23 +1,11 @@ import { - AttestationProcessingError, AttestationType, BatchOperatorGroups, - ReserveBalanceSheet, ChallengeOperatorHash, ChallengeRequest, - DepositRevert, NodeOwnerRegistration, OperatorAction, Operators, - PretokenPurchase, - PretokenYield, - StakeResult, - StakeUpdate, - SwapRemit, - SwapRequest, - SwapRevert, - UnderwriteIntentCommit, - WireTokenPurchase, type AttestationEntry } from "@wireio/opp-typescript-models" @@ -34,9 +22,9 @@ interface AttestationMessageType { /** * Single source of truth mapping every `AttestationType` enum value → the * `MessageType` that knows how to decode its `data` bytes. Unmapped values - * (e.g. `UNSPECIFIED`, the few `STAKE`/`UNSTAKE` / pre-launch names for - * which no companion class is exported by the generated package) fall - * through to a raw-record render in the panel — no decode is attempted. + * (e.g. `UNSPECIFIED`, or a type whose payload has no registered message + * class here) fall through to a raw-record render in the panel — no decode is + * attempted. * * Keep the keys aligned with `AttestationType`'s identifiers; renames in the * generated package surface as compile errors here. @@ -45,26 +33,13 @@ export const AttestationDecoders: Partial< Record > = { [AttestationType.OPERATOR_ACTION]: OperatorAction, - [AttestationType.PRETOKEN_PURCHASE]: PretokenPurchase, - [AttestationType.PRETOKEN_YIELD]: PretokenYield, - [AttestationType.RESERVE_BALANCE_SHEET]: ReserveBalanceSheet, - [AttestationType.STAKE_UPDATE]: StakeUpdate, - [AttestationType.WIRE_TOKEN_PURCHASE]: WireTokenPurchase, [AttestationType.CHALLENGE_RESPONSE]: ChallengeOperatorHash, - [AttestationType.SWAP_REQUEST]: SwapRequest, - [AttestationType.SWAP_REMIT]: SwapRemit, [AttestationType.CHALLENGE_REQUEST]: ChallengeRequest, [AttestationType.OPERATORS]: Operators, [AttestationType.BATCH_OPERATOR_GROUPS]: BatchOperatorGroups, - // The FULL registration payload (actor, keys, account, tier) — what BAR's - // commitNode emits and sysio.msgch's dispatch_node_owner_reg decodes; the - // legacy 3-field NodeOwnerReg message is no longer emitted by any outpost. - [AttestationType.NODE_OWNER_REG]: NodeOwnerRegistration, - [AttestationType.STAKE_RESULT]: StakeResult, - [AttestationType.ATTESTATION_PROCESSING_ERROR]: AttestationProcessingError, - [AttestationType.UNDERWRITE_INTENT_COMMIT]: UnderwriteIntentCommit, - [AttestationType.SWAP_REVERT]: SwapRevert, - [AttestationType.DEPOSIT_REVERT]: DepositRevert + // The registration payload (actor, keys, account, tier) — what BAR's + // commitNode emits and sysio.msgch's dispatch_node_owner_reg decodes. + [AttestationType.NODE_OWNER_REG]: NodeOwnerRegistration } /** Successful decode — the typed message the UI pretty-prints. */ diff --git a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts index 4b64b8339..adadf3666 100644 --- a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts +++ b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts @@ -79,7 +79,10 @@ describe("decodeAttestation", () => { expect(result.reason).toMatch(/no decoder/) }) - it.each([60950, 60958])( + it.each([ + 60950, 60958, 3001, 3002, 3004, 43520, 60928, 60934, 60951, 60952, 60955, + 60962 + ])( "renders removed wire type %s as raw without blocking active decoding", type => { const entry: AttestationEntry = { From 5801a6baef567f92539a56e949f00d9e4d4ca482 Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Fri, 9 Oct 2026 22:25:24 -0400 Subject: [PATCH 4/5] Wait for the decoded syndication in intake checks The syndication-underwriting flow's later-intake-held step failed in e2e run 38009710729 with "missing decoded syndication". The step polled until the depot held the intake envelope, then read the relayed SyndicateLIQ from the OPP debugging artifacts once. The debugging plugin writes that artifact after the depot accepts the envelope; in that run it landed 85 ms after the read. Read the decoded attestation inside the poll, so the step waits for both the held row and the persisted artifact before it records the epoch and bond request. The custody check against the attestation's own total is unchanged. readDecodedSyndications is exported so the read can be tested on its own. Move the artifact-writing test helpers out of the envelope-scan tests into a shared fixture, and cover the filter, the wait and the custody refusal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ARSeK35wwkiAy4et9mCkVm --- .../src/flow/SyndicationScenario.ts | 49 +++-- .../tests/flow/SyndicationScenario.test.ts | 182 +++++++++++++++++- .../tests/flow/oppArtifactFixture.ts | 79 ++++++++ .../tests/flow/oppEnvelopeScan.test.ts | 111 ++++------- 4 files changed, 337 insertions(+), 84 deletions(-) create mode 100644 packages/cluster-tool/tests/flow/oppArtifactFixture.ts diff --git a/packages/cluster-tool/src/flow/SyndicationScenario.ts b/packages/cluster-tool/src/flow/SyndicationScenario.ts index fbb6f7458..2a38ab8a9 100644 --- a/packages/cluster-tool/src/flow/SyndicationScenario.ts +++ b/packages/cluster-tool/src/flow/SyndicationScenario.ts @@ -371,6 +371,36 @@ export namespace SyndicationScenario { Assert.ok(row, "missing scenario envelope") return row } + /** + * The decoded `SyndicateLIQ` attestations the Solana outpost relayed to the depot for `user`'s + * wallet and `amount`, read from the cluster's OPP debugging artifacts. + * + * @param ctx - The build context whose cluster path holds the artifacts. + * @param user - The syndicating user, identified by their persisted wallet. + * @param amount - The syndicated amount, in base units. + * @return Every matching attestation; empty until the debugging plugin has persisted the + * envelope that carries one. + */ + export async function readDecodedSyndications( + ctx: ClusterBuildContext, + user: User, + amount: bigint + ): Promise { + const wallet = publicKey(ctx, user) + return ( + await readEnvelopeAttestations( + oppDebuggingPath(ctx.config.clusterPath), + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + AttestationType.SYNDICATE_LIQ + ) + ) + .map(bytes => SyndicateLIQ.fromBinary(bytes)) + .filter( + message => + message.amount?.amount === amount && + Buffer.from(message.user?.address).toString("hex") === wallet + ) + } /** Wait for intake, prove it is held and capture the consensus epoch and request. */ export function planVerifyIntake( actor: Report.Actor, @@ -387,6 +417,7 @@ export namespace SyndicationScenario { name, description, async ctx => { + let matching: SyndicateLIQ[] = [] await pollUntil( name, async () => { @@ -399,6 +430,10 @@ export namespace SyndicationScenario { publicKey(ctx, user) ) if (!envelope) return false + // The debugging plugin persists the relayed envelope after the depot accepts it, so + // the decoded attestation can trail the held row; wait for both. + matching = await readDecodedSyndications(ctx, user, amount) + if (matching.length === 0) return false ctx.outputs.set(epoch, envelope.epoch_index) ctx.outputs.set(request, envelope.request_id) return true @@ -406,20 +441,6 @@ export namespace SyndicationScenario { ProtocolTiming.SingleHopBudgetMs, PollMs ) - const messages = ( - await readEnvelopeAttestations( - oppDebuggingPath(ctx.config.clusterPath), - DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, - AttestationType.SYNDICATE_LIQ - ) - ).map(bytes => SyndicateLIQ.fromBinary(bytes)), - matching = messages.filter( - message => - message.amount?.amount === amount && - Buffer.from(message.user?.address).toString("hex") === - publicKey(ctx, user) - ) - Assert.ok(matching.length > 0, "missing decoded syndication") Assert.ok( matching.every(message => message.totalSyndicated >= amount), "custody attestation is below its own syndication" diff --git a/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts b/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts index 8ff87d9e0..cff361116 100644 --- a/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts +++ b/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts @@ -1,5 +1,15 @@ +import Fs from "node:fs" +import Os from "node:os" +import Path from "node:path" import { AuthExLinkTool } from "@wireio/cluster-tool/tools/all" import { Keypair } from "@solana/web3.js" +import { oppDebuggingPath } from "@wireio/debugging-shared" +import { + AttestationType, + ChainKind, + DebugOutpostEndpointsType, + SyndicateLIQ +} from "@wireio/opp-typescript-models" import { SysioContracts } from "@wireio/sdk-core" import { SyndicationScenario } from "@wireio/cluster-tool/flow" import { @@ -12,11 +22,22 @@ import { Report } from "@wireio/cluster-tool/report" import { SolanaFundingTool } from "@wireio/cluster-tool/tools/solana" import { WireSyndicationTool } from "@wireio/cluster-tool/tools/wire" import { fixtureContext } from "../config/clusterBuildContextFixture.js" +import { + attestationEntry, + envelopeBytes, + writeOppArtifact +} from "./oppArtifactFixture.js" const signal = new AbortController().signal, { Actor } = Report, { SysioContractName } = SysioContracts, - account = "synd.user" + account = "synd.user", + /** The syndicated amount every decoded-intake case relays, in base units. */ + intakeAmount = 2_000_000_001n, + /** The epoch the held fixture envelope reached consensus in. */ + intakeEpoch = 4, + /** The bond request the held fixture envelope carries. */ + intakeRequest = 11 /** Expose shared composition to exercise its production setup and cleanup boundaries. */ class Scenario extends SyndicationScenario { @@ -240,3 +261,162 @@ describe("shared syndication user writes", () => { expect(link).toHaveBeenCalledTimes(1) }) }) + +describe("decoded syndication intake", () => { + const user: SyndicationScenario.User = { + account, + keypairName: account, + linked: true + } + let clusterPath: string + + /** A `SyndicateLIQ` from `wallet` for `amount`, as the Solana outpost relays it. */ + function syndication( + wallet: Keypair, + amount: bigint, + totalSyndicated: bigint + ): Uint8Array { + return SyndicateLIQ.toBinary({ + chainCode: 0n, + user: { kind: ChainKind.SVM, address: wallet.publicKey.toBytes() }, + amount: { tokenCode: SyndicationScenario.TokenCode, amount }, + sequence: 1n, + totalSyndicated + }) + } + + /** Persist one Solana → depot envelope carrying `syndications`. */ + function relay(...syndications: Uint8Array[]): void { + writeOppArtifact( + oppDebuggingPath(clusterPath), + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + envelopeBytes( + syndications.map(data => + attestationEntry(AttestationType.SYNDICATE_LIQ, data) + ) + ) + ) + } + + /** The depot's held envelope row for the fixture intake. */ + function heldEnvelope(): SysioContracts.SysioSyndEnvelopeRowType { + return { + chain_code: SyndicationScenario.Chain, + token_code: SyndicationScenario.Token, + epoch_index: intakeEpoch, + digest: "", + synd_total: intakeAmount.toString(), + yield_total: 0, + item_count: 1, + state: SysioContracts.SysioSyndEnvelopeState.HELD, + request_id: intakeRequest, + released: 0, + burned: 0, + outcome: SysioContracts.SysioSyndRequestOutcome.PENDING, + forfeit: 0, + bounty_returned: 0, + hold_share: 0, + hold_beneficiary: "", + share_pending: false + } + } + + beforeEach(() => { + clusterPath = Fs.mkdtempSync(Path.join(Os.tmpdir(), "synd-intake-test-")) + }) + afterEach(() => { + Fs.rmSync(clusterPath, { recursive: true, force: true }) + }) + + it("decodes only the user's syndication of the amount", async () => { + const ctx = fixtureContext({ clusterPath }), + wallet = Keypair.generate() + jest.spyOn(SolanaFundingTool, "loadKeypair").mockReturnValue(wallet) + relay( + syndication(wallet, intakeAmount, intakeAmount), + syndication(wallet, intakeAmount + 1n, intakeAmount + 1n), + syndication(Keypair.generate(), intakeAmount, intakeAmount) + ) + const decoded = await SyndicationScenario.readDecodedSyndications( + ctx, + user, + intakeAmount + ) + expect(decoded).toHaveLength(1) + expect(decoded[0].amount.amount).toBe(intakeAmount) + expect(Buffer.from(decoded[0].user.address)).toEqual( + wallet.publicKey.toBuffer() + ) + }) + + it("decodes nothing before the relayed envelope is persisted", async () => { + const ctx = fixtureContext({ clusterPath }) + jest + .spyOn(SolanaFundingTool, "loadKeypair") + .mockReturnValue(Keypair.generate()) + relay() + await expect( + SyndicationScenario.readDecodedSyndications(ctx, user, intakeAmount) + ).resolves.toEqual([]) + }) + + it("waits for the persisted attestation after the depot holds the envelope", async () => { + const ctx = fixtureContext({ clusterPath }), + wallet = Keypair.generate(), + epoch = outputKey("test.intake.epoch", "consensus epoch"), + request = outputKey( + "test.intake.request", + "bond request" + ), + step = SyndicationScenario.planVerifyIntake( + Actor.Sysio, + "verify-intake", + "verify intake", + {}, + user, + intakeAmount, + epoch, + request + ) + jest.spyOn(SolanaFundingTool, "loadKeypair").mockReturnValue(wallet) + // The depot holds the envelope from the first poll; the debugging plugin persists the + // relayed artifact only after that first read. + const read = jest + .spyOn(WireSyndicationTool, "readHeldEnvelope") + .mockImplementationOnce(async () => heldEnvelope()) + .mockImplementation(async () => { + relay(syndication(wallet, intakeAmount, intakeAmount)) + return heldEnvelope() + }) + await step.runner(ctx, step.input, signal) + expect(read.mock.calls.length).toBeGreaterThan(1) + expect(ctx.outputs.get(epoch)).toBe(intakeEpoch) + expect(ctx.outputs.get(request)).toBe(intakeRequest) + }) + + it("refuses a custody attestation below its own syndication", async () => { + const ctx = fixtureContext({ clusterPath }), + wallet = Keypair.generate(), + step = SyndicationScenario.planVerifyIntake( + Actor.Sysio, + "verify-intake", + "verify intake", + {}, + user, + intakeAmount, + outputKey("test.intake.epoch", "consensus epoch"), + outputKey( + "test.intake.request", + "bond request" + ) + ) + jest.spyOn(SolanaFundingTool, "loadKeypair").mockReturnValue(wallet) + jest + .spyOn(WireSyndicationTool, "readHeldEnvelope") + .mockResolvedValue(heldEnvelope()) + relay(syndication(wallet, intakeAmount, intakeAmount - 1n)) + await expect(step.runner(ctx, step.input, signal)).rejects.toThrow( + "below its own syndication" + ) + }) +}) diff --git a/packages/cluster-tool/tests/flow/oppArtifactFixture.ts b/packages/cluster-tool/tests/flow/oppArtifactFixture.ts new file mode 100644 index 000000000..ddfa3c2b1 --- /dev/null +++ b/packages/cluster-tool/tests/flow/oppArtifactFixture.ts @@ -0,0 +1,79 @@ +import Fs from "node:fs" +import Path from "node:path" +import { + DebugEnvelopeMetadataRecord, + DebugOutpostEndpointsType, + Envelope, + type AttestationType, + type AttestationEntry +} from "@wireio/opp-typescript-models" + +/** The fixed checksum segment of every fixture artifact key. */ +export const FixtureArtifactChecksum = "abcdef0123456789" +/** Digits the epoch segment of an artifact key is zero-padded to. */ +export const ArtifactEpochDigits = 8 + +/** + * Write one OPP debugging artifact PAIR for `direction` carrying `payload`. + * + * Both halves, because the cluster writes both and the scanners consume different ones: the + * byte-tag scan reads `.data` alone, while `readEnvelopeAttestations` enumerates `.metadata` + * keys and reads the pair. + * + * @param directory - The OPP debugging directory; created when missing. + * @param direction - The relay direction the artifact key names. + * @param payload - The serialized envelope bytes written as `.data`. + * @param epoch - The epoch the artifact key names. + */ +export function writeOppArtifact( + directory: string, + direction: DebugOutpostEndpointsType, + payload: Buffer, + epoch = 1 +): void { + const baseKey = `${String(epoch).padStart(ArtifactEpochDigits, "0")}-${DebugOutpostEndpointsType[direction]}-${FixtureArtifactChecksum}` + Fs.mkdirSync(directory, { recursive: true }) + Fs.writeFileSync(Path.join(directory, `${baseKey}.data`), payload) + Fs.writeFileSync( + Path.join(directory, `${baseKey}.metadata`), + Buffer.from( + DebugEnvelopeMetadataRecord.toBinary({ + checksum: BigInt(payload.length), + batchOpNames: [] + }) + ) + ) +} + +/** + * One `AttestationEntry` with its `dataSize` derived from the payload. + * + * @param type - The attestation's wire discriminant. + * @param data - The serialized attestation payload. + * @return The entry an envelope message carries. + */ +export function attestationEntry( + type: AttestationType, + data: Uint8Array +): AttestationEntry { + return { type, dataSize: data.length, data } +} + +/** + * A one-message envelope carrying `attestations`, serialized as a `.data` artifact would be. + * + * @param attestations - The entries the single message carries. + * @return The envelope's wire bytes. + */ +export function envelopeBytes(attestations: AttestationEntry[]): Buffer { + return Buffer.from( + Envelope.toBinary({ + envelopeHash: new Uint8Array(), + epochTimestamp: 0n, + epochIndex: 1, + epochEnvelopeIndex: 0, + previousEnvelopeHash: new Uint8Array(), + messages: [{ header: undefined, payload: { version: 0, attestations } }] + }) + ) +} diff --git a/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts b/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts index 9c264988d..4e5be09bc 100644 --- a/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts +++ b/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts @@ -3,11 +3,8 @@ import Os from "node:os" import Path from "node:path" import { AttestationType, - DebugEnvelopeMetadataRecord, DebugOutpostEndpointsType, - Envelope, - LIQYield, - type AttestationEntry + LIQYield } from "@wireio/opp-typescript-models" import { attestationEntryTag, @@ -18,6 +15,11 @@ import { readEnvelopeAttestations, varintBytes } from "@wireio/cluster-tool/flow" +import { + attestationEntry, + envelopeBytes, + writeOppArtifact +} from "./oppArtifactFixture.js" /** The known wire encoding of `ATTESTATION_TYPE_SYNDICATE_LIQ` (60963). */ const SyndicateLIQTagBytes = [0x08, 0xa3, 0xdc, 0x03] @@ -41,32 +43,6 @@ const FixtureLIQYieldTotalSyndicated = 500_000_000n describe("oppEnvelopeScan", () => { let oppDirectory: string - /** - * Write one artifact PAIR for `direction` carrying `payload`. - * - * Both halves, because the cluster writes both and the two scanners consume - * different ones: the byte-tag scan reads `.data` alone, while - * `readEnvelopeAttestations` goes through `readEnvelopeRecordsFromDir`, which - * enumerates `.metadata` keys and reads the pair. - */ - function writeArtifact( - direction: DebugOutpostEndpointsType, - payload: Buffer, - epoch = 1 - ): void { - const baseKey = `${String(epoch).padStart(8, "0")}-${DebugOutpostEndpointsType[direction]}-abcdef0123456789` - Fs.writeFileSync(Path.join(oppDirectory, `${baseKey}.data`), payload) - Fs.writeFileSync( - Path.join(oppDirectory, `${baseKey}.metadata`), - Buffer.from( - DebugEnvelopeMetadataRecord.toBinary({ - checksum: BigInt(payload.length), - batchOpNames: [] - }) - ) - ) - } - beforeEach(() => { oppDirectory = Fs.mkdtempSync(Path.join(Os.tmpdir(), "opp-scan-test-")) }) @@ -112,7 +88,8 @@ describe("oppEnvelopeScan", () => { }) it("is false when no artifact carries the pattern", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([0x01, 0x02, 0x03]) ) @@ -126,7 +103,8 @@ describe("oppEnvelopeScan", () => { }) it("finds the tag inside a matching-direction artifact", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([0xff, ...SyndicateLIQTagBytes, 0xff]) ) @@ -140,7 +118,8 @@ describe("oppEnvelopeScan", () => { }) it("ignores artifacts from other directions", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, Buffer.from(SyndicateLIQTagBytes) ) @@ -161,7 +140,8 @@ describe("oppEnvelopeScan", () => { }) it("distinguishes attestation types by their tag", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, Buffer.from([...attestationEntryTag(AttestationType.LIQ_YIELD)]) ) @@ -184,7 +164,8 @@ describe("oppEnvelopeScan", () => { describe("containsDesyndicateLIQ", () => { it("defaults to the depot → Solana direction the redemption travels", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, Buffer.from(DesyndicateLIQTagBytes) ) @@ -202,7 +183,8 @@ describe("oppEnvelopeScan", () => { }) it("does not match the two inbound liq types", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, Buffer.from([...SyndicateLIQTagBytes, ...LIQYieldTagBytes]) ) @@ -212,7 +194,8 @@ describe("oppEnvelopeScan", () => { describe("containsSyndicateLIQ / containsLIQYield", () => { it("default to the direction each type actually travels", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([...SyndicateLIQTagBytes, ...LIQYieldTagBytes]) ) @@ -227,7 +210,8 @@ describe("oppEnvelopeScan", () => { }) it("does not confuse the three adjacent enum values", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from(SyndicateLIQTagBytes) ) @@ -236,7 +220,8 @@ describe("oppEnvelopeScan", () => { }) it("does not match a syndication tag on the WRONG direction", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, Buffer.from(SyndicateLIQTagBytes) ) @@ -250,7 +235,8 @@ describe("oppEnvelopeScan", () => { }) it("does not match a DESYNDICATE_LIQ tag as either outbound liq type", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from(DesyndicateLIQTagBytes) ) @@ -260,27 +246,6 @@ describe("oppEnvelopeScan", () => { }) describe("readEnvelopeAttestations", () => { - /** One `AttestationEntry` with its `dataSize` derived from the payload. */ - function entry(type: AttestationType, data: Uint8Array): AttestationEntry { - return { type, dataSize: data.length, data } - } - - /** A one-message envelope carrying `attestations`, serialized as a `.data` artifact would be. */ - function envelopeBytes(attestations: AttestationEntry[]): Buffer { - return Buffer.from( - Envelope.toBinary({ - envelopeHash: new Uint8Array(), - epochTimestamp: 0n, - epochIndex: 1, - epochEnvelopeIndex: 0, - previousEnvelopeHash: new Uint8Array(), - messages: [ - { header: undefined, payload: { version: 0, attestations } } - ] - }) - ) - } - /** The fixture `LIQYield` payload every case below round-trips. */ const liqYieldPayload = LIQYield.toBinary({ chainCode: FixtureLIQYieldChainCode, @@ -304,11 +269,12 @@ describe("oppEnvelopeScan", () => { }) it("returns the payload of every matching attestation, decodable by its message class", async () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, envelopeBytes([ - entry(AttestationType.SYNDICATE_LIQ, Uint8Array.of(1, 2)), - entry(AttestationType.LIQ_YIELD, liqYieldPayload) + attestationEntry(AttestationType.SYNDICATE_LIQ, Uint8Array.of(1, 2)), + attestationEntry(AttestationType.LIQ_YIELD, liqYieldPayload) ]) ) const payloads = await readEnvelopeAttestations( @@ -325,9 +291,12 @@ describe("oppEnvelopeScan", () => { }) it("ignores other directions and other types", async () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - envelopeBytes([entry(AttestationType.LIQ_YIELD, liqYieldPayload)]) + envelopeBytes([ + attestationEntry(AttestationType.LIQ_YIELD, liqYieldPayload) + ]) ) await expect( readEnvelopeAttestations( @@ -346,14 +315,18 @@ describe("oppEnvelopeScan", () => { }) it("skips an artifact that does not decode rather than throwing", async () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([0xff, 0xff, 0xff, 0xff]), 2 ) - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, - envelopeBytes([entry(AttestationType.LIQ_YIELD, liqYieldPayload)]), + envelopeBytes([ + attestationEntry(AttestationType.LIQ_YIELD, liqYieldPayload) + ]), 3 ) await expect( From 0b575738b5e09f551a831ca2009c47082e7c9efb Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Sat, 10 Oct 2026 15:14:48 -0400 Subject: [PATCH 5/5] Drop the challenge attestation decoders wire-sysio removes CHALLENGE_REQUEST, CHALLENGE_RESPONSE and their ChallengeRequest / ChallengeOperatorHash payloads: no chain produces them, and the depot dropped both on dispatch. The OPP display decoder was their last consumer here. Remove the two decoders. The codec test now pins that both removed wire values (60932, 60945) render raw beside a decoded entry. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ARSeK35wwkiAy4et9mCkVm --- .../src/features/opp/util/AttestationCodec.ts | 4 ---- .../tests/features/opp/util/AttestationCodec.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts index b466f3ae4..4ae040180 100644 --- a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts +++ b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts @@ -1,8 +1,6 @@ import { AttestationType, BatchOperatorGroups, - ChallengeOperatorHash, - ChallengeRequest, NodeOwnerRegistration, OperatorAction, Operators, @@ -33,8 +31,6 @@ export const AttestationDecoders: Partial< Record > = { [AttestationType.OPERATOR_ACTION]: OperatorAction, - [AttestationType.CHALLENGE_RESPONSE]: ChallengeOperatorHash, - [AttestationType.CHALLENGE_REQUEST]: ChallengeRequest, [AttestationType.OPERATORS]: Operators, [AttestationType.BATCH_OPERATOR_GROUPS]: BatchOperatorGroups, // The registration payload (actor, keys, account, tier) — what BAR's diff --git a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts index adadf3666..d58269349 100644 --- a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts +++ b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts @@ -80,8 +80,8 @@ describe("decodeAttestation", () => { }) it.each([ - 60950, 60958, 3001, 3002, 3004, 43520, 60928, 60934, 60951, 60952, 60955, - 60962 + 60950, 60958, 3001, 3002, 3004, 43520, 60928, 60932, 60934, 60945, 60951, + 60952, 60955, 60962 ])( "renders removed wire type %s as raw without blocking active decoding", type => {