Conversation
bf97958 to
3c8de86
Compare
|
Force-pushed as |
3c8de86 to
a5ae018
Compare
b5b7c45 to
14de67e
Compare
|
e2e: three runs, all green, and they cover this branch. #104 stacks on
Overrides on all three: The 17 are master's 15 plus
|
…e liqsol surface The Solana outpost bootstrap only ever loaded `liqsol_core`, so the liqsol staking + syndication surface it hosts had no accounts behind it: the liqSOL mint, its Token-2022 transfer hook, the distribution / stake / withdraw state, the leaderboard, the wire `GlobalState` and the reserve pool simply did not exist on a flow cluster. Anything that drives a REAL `synd` or `report_liq_yield` was therefore unreachable. Load all four programs at genesis and run wire-solana's own `init-*` scripts: - `SolanaOutpostProgramTool` generalizes over the program's CRATE name (`.keys/<name>-keypair.json`, `target/deploy/<name>.so`, `target/idl/<name>.json`) and gains `assertIdlProgramId`, so the id a script resolves from the IDL can be compared to the id the validator loaded. `liqsol_core` stays the default, so every existing caller is unchanged. - `SolanaValidatorProcessSteps.resolvePrograms` returns all four programs (`GenesisAnchorPrograms`), each upgradeable under the ONE per-cluster deployer — the same identity `initialize_global_config` proves against `ProgramData`. - `SolanaAnchorScriptTool` runs ONE `Anchor.toml` `[scripts]` entry per Step (`anchor run <script> --provider.cluster … --provider.wallet …`), the Solana analogue of the hardhat shell-out in `EthereumOutpostBootstrapper`. It prepends wire-solana's own `node_modules/.bin` to the subprocess `PATH`: every `[scripts]` entry is a bare `ts-node`, which anchor-cli hands to `bash -c` WITHOUT adding the workspace bin dir, so on a machine with no global `ts-node` — a CI runner — every script would die `command not found`. Prepending it also means the interpreter is the one that repo's lockfile pins rather than whatever a global shim happens to be. - `SolanaLiqsolSurfaceSteps.planLiqsolSurface` composes the phase: assert the toolchain and the program ids, airdrop the deployer, run the sixteen init scripts in `bash-scripts/reset-local-cluster.sh`'s order — with harness Steps spliced in at the three scripts that need them — then top the rent treasury up to its floor. `init-distro` and `init-controller` are NOT among the scripts. Both write unconditionally and both end in `main().catch(console.error)`, so they exit 0 on a failed write, and `runScript` sees only the exit code: their Report rows could not go red, and a failed init would surface at whichever later step first read `distribution_state` or `controllerState`, under the wrong name. The three writes they carry are three harness Steps instead (`initialize`, `initialize_stake_controller_state`, `initialize_vault`), each reading its target account first, at the position the scripts ran — which is not one position: `init-distro` runs immediately BEFORE `init-wire-config`, whose instruction reads the `distribution_state` it creates, and `init-controller` immediately before `init-global-config`. The Steps are keyed to those script NAMES rather than to an index, so inserting an init script cannot move them. `init-pretoken-purchase-history` gets a gate and a read-back, and this one is not hygiene either. The instruction stores `starting_epoch = current_epoch - 1`, and `PretokenPurchaseHistory::is_initialized()` IS `starting_epoch != 0`. In Solana epoch 0 that underflows and the script exits 1 — every local-mode bootstrap dies. In epoch 1 it SUCCEEDS and stores 0, the program's own "never initialized" sentinel, after which every pre-launch syndication, pretoken and refund path on that cluster fails `InvalidWireState` for the rest of its life. Measured on this branch's own runs the step landed 1.4 s into epoch 1 — the sentinel, every time, invisible only because PostLaunch `synd` never reads the history. So the phase waits for epoch `MinimumPretokenHistoryEpoch` (2, wire-solana's own `MIN_EPOCH` in `bash-scripts/wait-for-validators.sh`, for exactly this reason) immediately before the script, and reads the account back immediately after so a regression in the gate cannot pass silently. The wait's budget comes from `getEpochInfo()` — the slot time actually REMAINING (`slotsInEpoch - slotIndex`, plus whole epochs after it) times a slack factor — not from a whole number of epochs counted at the step. By the time this runs the scripts ahead of it have already spent most of the wait, so a step-relative budget would be mostly slack in the normal case and still too tight for a slow validator in the bad one. The read-back polls for a few slots before decoding, because the script confirms its `.rpc()` at `processed` while the harness reads at `confirmed`. This is also what makes the 100-slot epoch schedule load-bearing rather than cosmetic: at agave's default the wait would be days. The toolchain assertion is not hygiene. Since anchor-cli 0.30, `Anchor.toml`'s `[toolchain]` is applied on EVERY `anchor` invocation, so a `solana` or `anchor` that differs from the pins makes each script Step run `agave-install init <pinned>` first — a network download that repoints `~/.local/share/solana/install/active_release` while THIS cluster's `solana-test-validator` is running out of it. Refusing up front costs one `--version` call per binary; discovering it the other way is a validator that misbehaves mid-bootstrap, blamed on whichever step was unlucky. `reset-local-cluster.sh` itself is NOT used — its `anchor deploy` would fight the genesis load with a different upgrade authority. The treasury is NOT one of the scripts: `fund-treasury` transfers a fixed amount every time it runs, so on a re-run it would keep paying. A script Step is only sound when the script is idempotent, so the treasury gets a harness Step instead — `planFundTreasury` reads the PDA's balance and transfers only the shortfall, the shape `planKeypairAirdrop` already uses. `init-tranche-state` is left out for a harder reason: outside `--features development`, `initialize_tranche_state` pins its `chainlink_feed` and `chainlink_program` accounts to the real MAINNET Chainlink SOL/USD feed and program by address, and a test validator hosts neither — so running it would make a development build a prerequisite of the bootstrap. Nothing on the syndication path reads the tranche state, and neither does any later script here, so the surface is complete without it. - `SolanaAnchorScriptTool` records each script's stdout/stderr TAILS into the step's Report `extra`. A script Step's evidence is its output, and the tails are logged at `debug` (twenty scripts x 4 000 chars would flood the aggregate log the heartbeat greps); without the Report entry a PASSING step would leave nothing behind but the argv that launched it, so `init-wire-config` standing up the wire `GlobalState` and the liqSOL pool's accounting would be unverifiable after the run. - `SolanaFundingTool` gains handle-addressed per-cluster keypairs (`sol-<name>-keypair.json`, the deployer being one of them) and `planKeypairAirdrop`, the operator-free counterpart of `planAirdrop`: the deployer and flow-owned wallets have no `ctx.keyStore` operator identity. - `SolanaValidatorProcess` now passes `--slots-per-epoch`, resolved from the PERSISTED `ClusterConfig` so `create`, `run` and `start.sh` cannot drift from one another. The field is REQUIRED and carries no schema default: `ClusterConfigProvider.resolve` always writes it (from `--solana-slots-per-epoch`, defaulting to 100), so a config without it is malformed rather than old. agave's default keeps a fresh validator at Solana epoch 0 for ~2 days of slot time, and `initialize_pretoken_purchase_history_handler` seeds its history from `current_epoch.checked_sub(1)` — which underflows (`Underflow`, 7418) for as long as the epoch is 0, so the liqsol surface could not be initialized at all. This is the same reason wire-solana's own `run-wire-postlaunch-local.sh` passes the flag. No harness or flow code reads the Solana epoch (the OPP consensus boundary is the DEPOT's epoch), so shortening it changes no behavior under test. The phase is sequenced BEFORE the OPP outpost deploy because that is where `init-global-config` creates the `global_config` every OPP admin op is gated on; `SolanaOutpostBootstrapper.ensureGlobalConfig` then finds it already there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
…ol_core paths
`SolanaLiqSyndicationTool` is the harness's Step palette for the `liqsol_core`
instructions a user, an admin and a permissionless cranker actually call —
`sol_to_liqsol`, `set_wire_state`, `set_token_address`, `synd`,
`inject_bonus_synd_yield` and `report_liq_yield`. Every one is its own Step, so
the Report records each write; `SYNDICATE_LIQ` and `LIQ_YIELD` are queued by the
program itself, never by the harness.
- PDA derivation is a pure value helper (`deriveBasePdas` / `deriveUserPdas`)
over `LiqsolPdaSeed` — the seed registry the bootstrap phase that CREATES
these accounts already derives from, aliased here as `PdaSeed` so every call
site reads the same. The seeds are cross-checked against
`liqsol-core`, `liqsol-token` (the mint + its authority) and `transfer-hook`
(the `ExtraAccountMetaList`). The mint's token accounts are Token-2022 ATAs.
- `readLiqYieldState` and `readWireState` decode `GlobalState` through the
program's OWN Anchor coder, reached via a connection-only
`SolanaOutpostProgramTool.loadReadOnlyProgram` — a read needs the IDL's
layouts and an RPC, never a wallet or a fabricated keypair. It has to be a
`Program` and not a bare `anchor.BorshCoder`: `Program` is what camelCases the
IDL, so its coder keys `globalState` and `liqYieldReported` where a raw-IDL
coder keys `GlobalState` and `liq_yield_reported`, and Anchor exports no
converter to bridge them. A committed `GlobalState` fixture pins that, because
the wrong spelling costs a four-minute cluster to discover. The crank's
reported amount IS the delta between its watermark before and after.
- `set_token_address` is exposed as a Step because it is a hard precondition,
not a convenience: `synd` and `report_liq_yield` resolve their attestation's
depot token code via `OutpostConfig::token_code_for_mint` and refuse with
`LiqTokenNotMapped` when the liqSOL mint is unmapped.
- `SolanaAnchorEnumTool` gains the `WireState` identity enum, its variant tag
and the program's OWN transition table, alongside the existing proto-enum
variant helpers. `runSetWireState` READS `GlobalState.wireState` and refuses
before it submits — `set_wire_state` is a one-way ratchet in `liqsol_core`,
so a re-run or a wrong target would otherwise spend a transaction to earn an
opaque on-chain revert.
- Every instruction's account map is an exported pure builder, and one test per
instruction feeds the runner's OWN map to `program.methods.<ix>().accounts()`
against a trimmed, committed copy of the real `liqsol_core` IDL, asserting
the resolved keys, their order and their writable/signer flags. wire-solana
sets `resolution = false`, so Anchor fills in NOTHING: a renamed key would
otherwise surface only as a run-time failure deep inside a flow, and an extra
key would be silently dropped. The same comparison runs at BOOTSTRAP, against
the IDL actually on disk (`verify-instruction-accounts`), because the
committed fixture can only prove the maps were right when it was generated —
and it names the instruction and the drifted keys rather than reverting later.
The deposit Step stands off Solana's epoch-rewards window, and retries once if
it lands in it anyway.
The runtime distributes an epoch's staking rewards over the first blocks of the
next one (SIMD-118) and refuses every stake instruction while it does;
`liqsol_core` reads the same `EpochRewards` sysvar and fails `deposit_to_reserve`
with `EpochRewardsActive`. At the harness's 100-slot epochs that window recurs
every ~40 seconds, so `runDepositForLiqsol` polls the sysvar and waits it out —
otherwise the flow is a coin flip, which is exactly how it was observed failing.
Polling narrows the window to the gap between the last read and the transaction
landing; it cannot close it. So the submission runs through
`submitWithEpochRewardsRetry`, which on that one error waits the window out and
re-sends EXACTLY once — safe in both shapes it arrives in, because a preflight
refusal sent nothing and an on-chain failure is atomic. All three renderings are
matched, derived from the single `EpochRewardsActiveErrorCode`: the AnchorError
name, `custom program error: 0x1dc5` from a refused preflight, and
`{"InstructionError":[1,{"Custom":7621}]}` from a transaction that PASSED
preflight and executed inside the window — the last being the one that actually
happens to the ~1 % of deposits the poll cannot protect. The retry takes its
`send` as a parameter so the branch is exercised without a validator.
`oppEnvelopeScan.readEnvelopeAttestations` returns the raw attestation payloads
of one type on one edge, so a scenario can assert on an attestation's CONTENT
instead of only its type tag. It delegates to `@wireio/debugging-shared`'s
`readEnvelopeRecordsFromDir` rather than re-walking the directory and re-parsing
the envelopes: that reader already owns the `.data`/`.metadata` pairing, the
filename grammar and the torn-read handling. Decoding the payload stays at the
call site, with the generated message class, so this module re-declares no proto
shape.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
… injectors `flow-liq-syndication` now drives the outpost's own instructions instead of enqueueing attestations behind the program's back. A verify step reads `GlobalState.wireState` and asserts the outpost is still PreLaunch — FIRST, because every step after it writes — then a user is funded and deposits SOL for liqSOL, the admin flips the outpost Launching -> PostLaunch, the user syndicates — and `synd` queues `SYNDICATE_LIQ` itself. A permissionless donation then credits the syndicated pool and a NON-ADMIN crank of `report_liq_yield` queues `LIQ_YIELD`. The circulated `LIQ_YIELD` is DECODED and must carry exactly the delta the crank advanced its watermark by, on exactly the sequence it drew, under the outpost's chain code and the depot's liqSOL token code. Because `set_wire_state` is a one-way ratchet, the scenario is single-shot per cluster — the flow's README says so, and a second run against the same cluster fails at that verify step with the state it found rather than at an opaque on-chain revert several steps later. The step leads the phase so a refused run spends nothing: behind it sits a real 5 SOL `sol_to_liqsol`, which would otherwise land before the refusal. The scenario binds the REAL liqSOL mint to the depot's liqSOL token code as its first step rather than in the shared bootstrap: the bootstrap binds that code to the mock SPL mint the swap flows hold reserves in, and only this flow's cluster should see the change (`wire-outpost-repos-…-belong-in-before-all.md`). `flow-yield-distribution` loses its Solana leg. The SOL staking surface is a separate developer track and `liqsol_core` emits no `STAKING_REWARD`, so the leg could only ever have been an injection; the flow is Ethereum-only until the outpost produces the attestation itself. With both consumers gone, the injection surface goes with them: `SolanaYieldEmitterTool` (and, from the superseded revision of this branch, `SolanaAddAttestationTool` / `SolanaSyndicationTool` and the flow's own emit steps) are deleted. Nothing in the harness writes to a program's outbound message buffer any more. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
…he new flow The root README's outpost summary and `--solana-path` row said the harness bootstraps `opp-outpost`; it now loads all four wire-solana Anchor programs at genesis and runs their `init-*` scripts before the OPP deploy. CLAUDE.md's orchestration map names `SolanaLiqsolSurfaceSteps` and why it is sequenced ahead of the outpost bootstrap, and the flow table rows say what the two touched flows actually assert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
sdk-core and shared to 1.0.92, the wire-libraries-ts release carrying the regenerated SysioContractTypes for sysio.liq and sysio.swap (wire-libraries-ts#84). Produced by scripts/update-wireio-deps.mjs. Change-Id: I2ccd90c14dd35080a9961f615041c70ff86a0e52
482fd8f to
dea084b
Compare
Harness half of the cross-repo
simple_swapfeature (wire-sysio #609 protocol types, wire-solana #523 outpost emitters, both merged). This replaces the earlier injection approach: the flow now drives the realliqsol_corepaths and provesSYNDICATE_LIQandLIQ_YIELDcirculate from the Solana outpost to the depot.Depends on wire-solana #534 (
register_system_pdacreating the pool record on a deployable build). Until it lands, the liqsol surface bootstrap fails atinit-wire-configon any non-development wire-solana build; the flows below were run againstnext+ #534.Commits (4, on master
2d703304)SolanaValidatorProcessStepsloadsliqsol_core,liqsol_token,transfer_hook,validator_leaderboardat genesis (upgradeable, deployer as authority, ids from.keys/). A newSolanaLiqsolSurfacephase, before the OPP outpost bootstrap, runs wire-solana's init scripts throughSolanaAnchorScriptTool(anchor run --provider.cluster --provider.wallet, wire-solana's ownnode_modules/.binon PATH, stdout/stderr tails in the Report) plus read-first harness Steps where a script cannot report failure (initialize,initialize_stake_controller_state,initialize_vault, treasury shortfall). Guards:verify-toolchain(solana/anchor versions equal the[toolchain]pins, so anchor-cli cannot reinstall a toolchain under a running validator),verify-program-ids, an epoch gate beforeinit-pretoken-purchase-history(the instruction underflows in epoch 0 and writes the program's "uninitialized" sentinel in epoch 1; wire-solana's CI gates on epoch 2 for the same reason) and a read-back that fails on the sentinel.init-tranche-stateis deliberately not run: a production build pins the mainnet Chainlink feed, which a test validator does not host.solanaSlotsPerEpochbecomes a persistedClusterConfigfield (--solana-slots-per-epoch, default 100) socreate,runandstart.shcannot drift.SolanaLiqSyndicationTool: one Step per write (sol_to_liqsol,set_wire_state,synd,inject_bonus_synd_yield,report_liq_yield), pure account-map builders checked against the deployed IDL at bootstrap (verify-instruction-accounts) and against a committed fixture in unit tests,set_wire_staterefusing illegal transitions before submitting, the deposit waiting out theEpochRewardswindow and retrying once onEpochRewardsActivein any of its three renderings.SolanaAddAttestationToolandSolanaYieldEmitterToolare deleted.synd,SYNDICATE_LIQobserved in anOUTPOST_SOLANA_DEPOTartifact) → Yield (0.1 SOL × k bonus donation,report_liq_yield,LIQ_YIELDdecoded and its amount checked against the donation) → DepotAcceptsUnknownTypes.flow-yield-distributionloses its Solana leg (the outpost no longer emitsSTAKING_REWARD; it returns when the depot handlesLIQ_YIELD).Review history
Five adversarial rounds (Opus, then Fable) between the original injector approach and this series; round 5 found no blockers. Per-round reports are in the session, summarised in the commit bodies: ts-node resolved only through a host PATH (fixed), two init scripts that could not fail (replaced by harness Steps), the epoch-0/epoch-1 pretoken-history hazard (gated and read back), the toolchain reinstall hazard (asserted), the epoch-rewards deposit race (single retry, tested).
Gate (idle host)
pnpm build,pnpm run lintpnpm testat the tippnpm buildper commitflow-liq-syndication(canonicalrun-flow.mjs+ heartbeat),--solana-path= wire-solananext+ #534, deployable buildstarting_epoch = 1,Registered liqSOL pool user record.captured frominit-wire-configflow-yield-distribution, same buildKnown, out of scope: the bind-registry lock compromise that flakes the full jest suite under load has its own fix on
fix/bind-registry-lock-compromise.🤖 Generated with Claude Code
https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa