Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The OPP message flow spans three chains:
- **WIRE depot** (`nodeop` + `kiod`) — system contracts `sysio.epoch`, `sysio.msgch`,
`sysio.opreg`, `sysio.uwrit`, `sysio.reserv`, `sysio.chalg`, …
- **Ethereum outpost** (`anvil`) — `OPP.sol`, `OPPInbound.sol`, `OperatorRegistry.sol`,
`ReserveManager.sol`, `StakingManager.sol` (+ `liqEth`).
`ReserveManager.sol`, and `liqEth`.
- **Solana outpost** (`solana-test-validator`) — the `opp-outpost` Anchor program (+ `liqsol-*`).

## Where this repo fits in the platform
Expand Down
8 changes: 7 additions & 1 deletion packages/cluster-tool-shared/src/config/ClusterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,13 @@ export const ClusterConfigSchema = z.object({
* pre-existing configs — and every real/external depot — stay reserve-free
* unless a caller (or a flow's scenario defaults) opts in.
*/
enableMockReserves: z.boolean().default(false)
enableMockReserves: z.boolean().default(false),
/**
* Whether the local Ethereum bootstrap deploys the transport-only synthetic
* yield emitter. Schema-defaulted `false` so it is absent from the standard
* local outpost surface.
*/
enableMockYieldEmitter: z.boolean().default(false)
})
/** THE canonical cluster configuration — the schema-inferred shape of {@link ClusterConfigSchema}. */
export type ClusterConfig = z.infer<typeof ClusterConfigSchema>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ describe("ClusterConfig shape", () => {
signatureProvider: { type: SignatureProviderType.KEY, ssm: null },
externalOutposts: null,
debuggingServerEnabled: true,
enableMockReserves: false
enableMockReserves: false,
enableMockYieldEmitter: false
}

it("persists the report/logging enum fields as their wire spellings", () => {
Expand All @@ -108,12 +109,13 @@ describe("ClusterConfig shape", () => {
expect(rehydrated).toEqual(config)
})

it("loads a legacy config (no signatureProvider/externalOutposts/debuggingServerEnabled/enableMockReserves) via schema defaults", () => {
it("loads a config without optional platform features via schema defaults", () => {
const parsed = JSON.parse(ClusterConfigSchemaCodec.serialize(config))
delete parsed.signatureProvider
delete parsed.externalOutposts
delete parsed.debuggingServerEnabled
delete parsed.enableMockReserves
delete parsed.enableMockYieldEmitter
const rehydrated = ClusterConfigSchemaCodec.deserialize(
JSON.stringify(parsed)
)
Expand All @@ -124,6 +126,7 @@ describe("ClusterConfig shape", () => {
expect(rehydrated.externalOutposts).toBeNull()
expect(rehydrated.debuggingServerEnabled).toBe(true)
expect(rehydrated.enableMockReserves).toBe(false)
expect(rehydrated.enableMockYieldEmitter).toBe(false)
})

it("defaults the epoch-group + termination overrides to null for a legacy config", () => {
Expand Down
17 changes: 9 additions & 8 deletions packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ export class OptionLeafSpec {
* walk.
*/
export type OptionShapeNode =
| OptionLeafSpec
| OptionShapeNode[]
| OptionShapeObject
OptionLeafSpec | OptionShapeNode[] | OptionShapeObject

/** A nested object of shape nodes (named — no inline object types). */
export interface OptionShapeObject {
Expand Down Expand Up @@ -382,6 +380,10 @@ export function buildOptionShape(
false,
"seed the 8 mock (chain, token) PRIMARY reserves at bootstrap"
),
enableMockYieldEmitter: leaf(
false,
"deploy the transport-only synthetic Ethereum yield emitter"
),
bind: buildBindShape(nodeCount, batchCount, underwriterCount),
bindConfig: optionalLeaf(
OptionLeafType.string,
Expand Down Expand Up @@ -523,7 +525,9 @@ export function applyClusterBuildOptionsArgs(
environmentPathDefaults(environment),
defaults
)
const withShape = flattenOptionLeaves(buildOptionShape(seededDefaults)).reduce(
const withShape = flattenOptionLeaves(
buildOptionShape(seededDefaults)
).reduce(
(instance, optionLeaf) =>
instance.option(
optionLeaf.flag,
Expand Down Expand Up @@ -569,10 +573,7 @@ function isIndexSegment(segment: string): boolean {
}

/** Read a child by segment (arrays accept numeric-string keys uniformly). */
function childOf(
node: OptionTreeContainer,
segment: string
): OptionTreeValue {
function childOf(node: OptionTreeContainer, segment: string): OptionTreeValue {
return (node as OptionTreeObject)[segment] ?? null
}

Expand Down
5 changes: 5 additions & 0 deletions packages/cluster-tool/src/config/ClusterBuildOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export interface ClusterBuildOptions {
* to the bootstrap window (epoch 0), so this only ever seeds pre-EpochBootstrap.
*/
enableMockReserves?: boolean
/**
* Deploy the transport-only Ethereum STAKING_REWARD emitter. Default false;
* only the yield-distribution flow opts in.
*/
enableMockYieldEmitter?: boolean
// termination tuning
terminateMaxConsecutiveMisses?: number
terminateMaxPercentMisses24h?: number
Expand Down
10 changes: 7 additions & 3 deletions packages/cluster-tool/src/config/ClusterConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ export namespace ClusterConfigProvider {
cooldownEpochs: options.cooldownEpochs ?? 1,
terminateMaxConsecutiveMisses:
options.terminateMaxConsecutiveMisses ?? null,
terminateMaxPercentMisses24h: options.terminateMaxPercentMisses24h ?? null,
terminateMaxPercentMisses24h:
options.terminateMaxPercentMisses24h ?? null,
terminateWindowMs: options.terminateWindowMs ?? null,
ethereumPath: assertOption(options.ethereumPath, "ethereumPath"),
solanaPath: assertOption(options.solanaPath, "solanaPath"),
Expand All @@ -160,7 +161,8 @@ export namespace ClusterConfigProvider {
signatureProvider,
externalOutposts,
debuggingServerEnabled: true,
enableMockReserves: options.enableMockReserves ?? false
enableMockReserves: options.enableMockReserves ?? false,
enableMockYieldEmitter: options.enableMockYieldEmitter ?? false
}
}

Expand Down Expand Up @@ -201,7 +203,9 @@ export namespace ClusterConfigProvider {
* @param options - The caller options (carries `bind`, `bindConfig`, counts).
* @returns The resolved bind config.
*/
async function resolveBind(options: ClusterBuildOptions): Promise<BindConfig> {
async function resolveBind(
options: ClusterBuildOptions
): Promise<BindConfig> {
const { bind: cliBind = {} } = options,
topology: ClusterTopologyOptions = {
producerCount: options.nodeCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface EthereumOutpostBootstrapperOptions {
* sharing `<wire-ethereum>/.local/deployments/` wiped each other mid-run).
*/
deploymentsPath: string
/** Deploy the transport-only synthetic yield emitter into the local outpost. */
enableMockYieldEmitter?: boolean
/**
* Number of deterministic accounts to generate — MUST match the run anvil's
* `--accounts` (default: {@link AnvilProcess.AccountCount}) so every generated
Expand Down Expand Up @@ -179,7 +181,8 @@ export class EthereumOutpostBootstrapper {
key: deployerPrivateKey,
addressFile: Path.join(localDir, "outpost-addrs.json"),
gasLimitFile: Path.join(localDir, "outpost-gas-limits.json"),
useMockAggregator: true
useMockAggregator: true,
enableMockYieldEmitter: this.config.enableMockYieldEmitter
}
Fs.writeFileSync(
Path.join(localDir, "liqeth.json"),
Expand Down Expand Up @@ -425,7 +428,8 @@ export namespace EthereumOutpostBootstrapper {
* anvil's `--accounts` so every generated account is pre-funded. */
export function createDefaultOptions(): Partial<EthereumOutpostBootstrapperOptions> {
return {
accountCount: AnvilProcess.AccountCount
accountCount: AnvilProcess.AccountCount,
enableMockYieldEmitter: false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export namespace EthereumOutpostSteps {
ctx.config.bind.anvil.port,
toDialAddress(ctx.config.bind.anvil.address)
),
deploymentsPath: ClusterConfigProvider.ethereumDeploymentsPath(ctx.config)
deploymentsPath: ClusterConfigProvider.ethereumDeploymentsPath(
ctx.config
),
enableMockYieldEmitter: ctx.config.enableMockYieldEmitter
}).bootstrap()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
* 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
* exercising transport and depot accounting without representing a
* production staking surface. 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"
import {
loadOutpostContract,
resolveLatestNonce
} from "../../utils/ethereumUtils.js"

/**
* Minimal `ethers` surface of `MockYieldEmitter.sol`. Typed structurally
Expand Down Expand Up @@ -139,12 +142,15 @@ export async function emitYieldBatch(
rewardEpochIndex: number
): Promise<ethers.TransactionReceipt> {
Assert.ok(entries.length > 0, "EthYieldEmitterTool: empty entries")
Assert.ok(externalEpochRef > 0n, "EthYieldEmitterTool: externalEpochRef must be positive")
Assert.ok(
externalEpochRef > 0n,
"EthYieldEmitterTool: externalEpochRef must be positive"
)

const stakers = entries.map(e => e.staker)
const wireAccounts = entries.map(e => e.wireAccount)
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 shareBpses = entries.map(e => e.shareBps)

const nonce = await resolveLatestNonce(contract)
const tx = await contract.emitYield(
Expand Down
25 changes: 21 additions & 4 deletions packages/cluster-tool/tests/cli/ClusterBuildOptionsArgs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ describe("WIRE_* environment seeding (the run-flow.mjs / e2e-gate contract)", ()
})

it("the environment (per-invocation operator intent) beats scenario defaults", () => {
const options = register({ clusterPath: "/tmp/scenario-cluster" }, environment)
const options = register(
{ clusterPath: "/tmp/scenario-cluster" },
environment
)
expect(options.get("cluster-path")?.default).toBe("/tmp/env-cluster")
})

Expand Down Expand Up @@ -305,7 +308,9 @@ describe("toClusterBuildOptions reverse parse", () => {
{ "epoch-duration-sec": 60 },
{ requiredBatchOperatorCollateral }
)
expect(options.requiredBatchOperatorCollateral).toEqual(requiredBatchOperatorCollateral)
expect(options.requiredBatchOperatorCollateral).toEqual(
requiredBatchOperatorCollateral
)
// absent defaults stay absent — flags never set these leaves
expect(options.requiredUnderwriterCollateral).toBeUndefined()
})
Expand All @@ -329,7 +334,16 @@ describe("toClusterBuildOptions reverse parse", () => {
toClusterBuildOptions({ "enable-mock-reserves": true }).enableMockReserves
).toBe(true)
expect(
toClusterBuildOptions({ "enable-mock-reserves": false }).enableMockReserves
toClusterBuildOptions({ "enable-mock-reserves": false })
.enableMockReserves
).toBe(false)
expect(
toClusterBuildOptions({ "enable-mock-yield-emitter": true })
.enableMockYieldEmitter
).toBe(true)
expect(
toClusterBuildOptions({ "enable-mock-yield-emitter": false })
.enableMockYieldEmitter
).toBe(false)
})

Expand Down Expand Up @@ -358,6 +372,7 @@ describe("register → parse round-trip", () => {
expect(options.bindAll).toBe(false)
// no opt-in ⇒ the default-false mock-reserves flag survives as false
expect(options.enableMockReserves).toBe(false)
expect(options.enableMockYieldEmitter).toBe(false)
// unseeded (null-default) bind ports never materialize
expect(options.bind?.kiod?.port).toBeUndefined()
})
Expand All @@ -376,7 +391,8 @@ describe("register → parse round-trip", () => {
terminateMaxConsecutiveMisses: 5,
terminateMaxPercentMisses24h: 99,
terminateWindowMs: 3_600_000,
enableMockReserves: true
enableMockReserves: true,
enableMockYieldEmitter: true
}),
argv: Record<string, unknown> = {}
registered.forEach((config, flag) => {
Expand All @@ -392,6 +408,7 @@ describe("register → parse round-trip", () => {
expect(options.terminateWindowMs).toBe(3_600_000)
// the scenario-defaults opt-in path the 6 reserve-needing flows rely on
expect(options.enableMockReserves).toBe(true)
expect(options.enableMockYieldEmitter).toBe(true)
})
})

Expand Down
3 changes: 2 additions & 1 deletion packages/cluster-tool/tests/config/clusterConfigFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export const PersistedFixture: ClusterConfig = {
signatureProvider: { type: SignatureProviderType.KEY, ssm: null },
externalOutposts: null,
debuggingServerEnabled: true,
enableMockReserves: false
enableMockReserves: false,
enableMockYieldEmitter: false
}

/** Build a `ClusterConfig` from the fixture (via deserialize — no resolve / env).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ function reserveRow(
reserve_wire_amount: 0,
source_token_precision: 0,
connector_weight_bps: 0,
creator_addr: { kind: SysioReservChainkind.CHAIN_KIND_UNKNOWN, address: "" },
creator_addr: {
kind: SysioReservChainkind.CHAIN_KIND_UNKNOWN,
address: ""
},
requested_wire_amount: 0,
external_token_amount: 0,
registered_at_ms: 0,
Expand Down Expand Up @@ -62,6 +65,7 @@ function uwreqRow(
dst_token_code: { value: 0 },
dst_reserve_code: { value: 0 },
dst_amount: 0,
target_amount: 0,
variance_tolerance_bps: 0,
source_tx_id: "",
depositor: "",
Expand Down Expand Up @@ -103,21 +107,25 @@ interface TableFixtures {

/** A context whose `wire.getSysioContract` serves the fixtures (reads only). */
function newContext(fixtures: TableFixtures): SwapScenarioContext {
const context = new SwapScenarioContext(fixtureConfig(), getLogger("swap-ctx-test"))
const context = new SwapScenarioContext(
fixtureConfig(),
getLogger("swap-ctx-test")
)
const table = <Row>(rows: Row[]) => ({
query: async () => ({ rows, more: false })
})
const clientByName = {
[SysioContractName.reserv]: { tables: { reserves: table(fixtures.reserves) } },
[SysioContractName.reserv]: {
tables: { reserves: table(fixtures.reserves) }
},
[SysioContractName.uwrit]: {
tables: { uwreqs: table(fixtures.uwreqs), locks: table(fixtures.locks) }
}
}
jest
.spyOn(context, "wire", "get")
.mockReturnValue({
getSysioContract: (name: SysioContracts.SysioContractName) => clientByName[name]
} as WireClient)
jest.spyOn(context, "wire", "get").mockReturnValue({
getSysioContract: (name: SysioContracts.SysioContractName) =>
clientByName[name]
} as WireClient)
return context
}

Expand Down Expand Up @@ -147,7 +155,11 @@ describe("SwapScenarioContext", () => {
})
],
locks: [
lockRow({ lock_id: 1, uwreq_id: 7, chain_code: { value: EthereumChain } }),
lockRow({
lock_id: 1,
uwreq_id: 7,
chain_code: { value: EthereumChain }
}),
lockRow({ lock_id: 2, uwreq_id: 7, chain_code: { value: SolanaChain } }),
lockRow({ lock_id: 3, uwreq_id: 9 })
]
Expand All @@ -171,7 +183,10 @@ describe("SwapScenarioContext", () => {

describe("uwreq", () => {
it("finds the request by its (source, destination) chain pair", async () => {
const request = await newContext(fixtures).uwreq(EthereumChain, SolanaChain)
const request = await newContext(fixtures).uwreq(
EthereumChain,
SolanaChain
)
expect(request?.id).toBe(7)
})
it("is empty when the depot has not created the request", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ export function makeFixtureCluster(): FixtureCluster {
signatureProvider: { type: SignatureProviderType.KEY, ssm: null },
externalOutposts: null,
debuggingServerEnabled: true,
enableMockReserves: false
enableMockReserves: false,
enableMockYieldEmitter: false
}

Fs.writeFileSync(
Expand Down
Loading