Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4471500
Provision Anvil Ethereum transaction policy
huangminghuang Jul 21, 2026
dc3b5bd
Generate unified Ethereum client configuration
huangminghuang Jul 21, 2026
8494a58
Clarify operator provider identifiers
huangminghuang Jul 21, 2026
12a47c1
Merge master and adopt protobuf Ethereum client schema
huangminghuang Aug 4, 2026
3224980
Merge remote-tracking branch 'origin/master' into fix/sec-140
huangminghuang Aug 12, 2026
0c93fdc
Provision finite Anvil Ethereum transaction policy
huangminghuang Aug 12, 2026
6786454
Address SEC-140 review follow-ups
huangminghuang Aug 12, 2026
04d3f01
Raise Anvil transaction policy for full flows
huangminghuang Aug 12, 2026
feea3d5
Cover buffered Anvil transaction policy limits
huangminghuang Aug 12, 2026
2928752
Avoid fixed Anvil port in configuration tests
huangminghuang Aug 12, 2026
b369a54
Merge remote-tracking branch 'origin/master' into fix/sec-140
huangminghuang Aug 12, 2026
d0fe77e
Merge remote-tracking branch 'origin/master' into fix/sec-140
huangminghuang Aug 13, 2026
c161234
SEC-140 emit local Ethereum policy ProtoJSON
huangminghuang Aug 13, 2026
2be3def
fix(cluster-tool): default challenge fixture id
huangminghuang Aug 13, 2026
fb3853f
Merge remote-tracking branch 'origin/master' into fix/sec-140
huangminghuang Aug 13, 2026
a2bc3b1
Merge remote-tracking branch 'origin/master' into fix/sec-140
huangminghuang Aug 18, 2026
ef0e7d9
Merge origin/master into fix/sec-140
huangminghuang Aug 21, 2026
c39840e
fix(ci): pin compatible AWS credential providers
huangminghuang Aug 21, 2026
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
8 changes: 5 additions & 3 deletions docs/setup-external-cluster-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,11 @@ wire-cluster-tool run --cluster-path /path/to/external
```

`run` resumes from the persisted state, dialing the addresses in
`ClusterConfig.bind`. The operator daemons' outpost-client endpoints
(`--outpost-ethereum-client` / `--outpost-solana-client`) are built at run time
from `config.bind.{anvil,solana}` — so they always match the merged bind.
`ClusterConfig.bind`. Artifact preparation regenerates
`data/ethereum-client.json` from `config.bind.anvil` and passes it through
`--outpost-ethereum-client-config-file`; the Solana endpoint remains the inline
`--outpost-solana-client` option built from `config.bind.solana`. Both therefore
match the merged bind on every relaunch.

## Step 5 (optional) — package per-node archives

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@aws-sdk/client-sns": "3.1102.0",
"@aws-sdk/client-ssm": "3.1102.0",
"@aws-sdk/client-sts": "3.1102.0",
"@aws-sdk/credential-provider-env": "3.972.68",
"@aws-sdk/credential-provider-node": "3.972.79",
"@types/proper-lockfile": "4.1.4",
"uuid": "11",
"get-port": "7.2.0",
Expand Down
35 changes: 35 additions & 0 deletions packages/cluster-tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ After `create`:
├── anvil/ # anvil state (local ETH outpost only)
├── solana-ledger/ # validator ledger (local SOL outpost only)
├── eth-abis/ # address-embedded outpost ABIs
├── ethereum-client.json # shared Ethereum client config for operator daemons
├── solana-idls/ # liqsol_core (opp-outpost) IDL
├── ethereum-deployments/ # outpost-addrs.json
└── opp-debugging/ # OPP envelope .data / .metadata pairs
Expand All @@ -332,6 +333,40 @@ In external-outpost mode no local `anvil` / `solana-ledger` state is written
(`cluster-state.json` records them as `null`); the operator-daemon artifacts come
from the `--external-outpost-config` instead.

### Generated Ethereum client configuration

Artifact preparation writes one `data/ethereum-client.json`, and every operator
daemon passes it through `--outpost-ethereum-client-config-file`. The
protobuf-JSON document uses `schema_version: 1`, nests the stable `eth-default`
client and signature-provider ids under `connection`, and records `chain_id` as
a number. Signature-provider ids are process-local, so each daemon can register
its own Ethereum private key as `eth-default` while safely sharing the same
client configuration file. Daemon argument builders remain pure and reuse the
artifact on create, run, restart, and flow-provisioned starts.

Local Anvil clusters embed the following finite `transaction_policy` in every
generated Ethereum client. The values live in
`AnvilEthereumTransactionPolicyConfig`; they are not production recommendations.

| Limit | Anvil value |
|---|---:|
| Maximum priority fee per gas | `2,000,000,000` wei (2 gwei) |
| Maximum fee per gas | `100,000,000,000` wei (100 gwei) |
| Maximum final gas limit | `6,000,000` |
| Maximum total native cost | `700,000,000,000,000,000` wei (0.7 ETH) |

Nodeop applies the final 20% estimate buffer before checking the gas cap. At
the full `6,000,000 × 100 gwei = 0.6 ETH` gas bound, the total-cost cap leaves
`0.1 ETH` for transaction value. These limits cover the measured local
`epochIn` and `commit` workloads, including the 4,392,032-gas remote emissions
high-water estimate after nodeop's 20% buffer, while retaining a finite
configuration-error boundary. External-outpost configuration continues to omit
the policy, because the reviewed limits for its operator-selected endpoint are
outside this tool's scope. Bios and producer-only nodes receive neither an
Ethereum signing client nor an orphaned client-config option.

Production policy selection happens outside `wire-tools-ts`.

---

## Programmatic usage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Assert from "node:assert"
import {
EthereumClientConfigurationConfig,
type EthereumTransactionPolicy
} from "./EthereumClientConfigurationConfig.js"

/** Finite SEC-131 transaction limits for Anvil-backed operator daemons. */
export namespace AnvilEthereumTransactionPolicyConfig {
/** Maximum EIP-1559 priority fee per gas in wei: 2 gwei. */
export const MaximumPriorityFeePerGasWei = "2000000000"
/** Maximum EIP-1559 fee per gas in wei: 100 gwei. */
export const MaximumFeePerGasWei = "100000000000"
/** Maximum final gas limit after nodeop's 20% estimate buffer. */
export const MaximumGasLimit = "6000000"
/** Maximum native cost in wei: 0.7 ETH. */
export const MaximumTotalNativeCostWei = "700000000000000000"

/**
* Create the finite policy embedded in each local Anvil client.
* These limits are for local development and test clusters only; they are not
* production policy recommendations.
*
* @returns A validated policy using SEC-131's protobuf field spelling.
*/
export function create(): EthereumTransactionPolicy {
const policy: EthereumTransactionPolicy = {
max_priority_fee_per_gas_wei: MaximumPriorityFeePerGasWei,
max_fee_per_gas_wei: MaximumFeePerGasWei,
max_gas_limit: MaximumGasLimit,
max_total_native_cost_wei: MaximumTotalNativeCostWei
}
EthereumClientConfigurationConfig.assertTransactionPolicy(policy)
Assert.ok(
BigInt(policy.max_total_native_cost_wei) >=
BigInt(policy.max_gas_limit) * BigInt(policy.max_fee_per_gas_wei),
"Anvil Ethereum transaction policy total-cost cap must cover gas-limit × maximum-fee caps"
)
return policy
}
}
3 changes: 2 additions & 1 deletion packages/cluster-tool/src/config/ClusterConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,14 +604,15 @@ export namespace ClusterConfigProvider {
Path.isAbsolute(ref) ? ref : Path.resolve(baseDir, ref)
return {
ethereum: {
...config.ethereum,
addressFile: resolveRef(config.ethereum.addressFile),
abiFiles: config.ethereum.abiFiles.map(resolveRef),
chainId: config.ethereum.chainId,
...(config.ethereum.liqEthAddressFile != null
? { liqEthAddressFile: resolveRef(config.ethereum.liqEthAddressFile) }
: {})
},
solana: {
...config.solana,
idlFile: resolveRef(config.solana.idlFile),
...(config.solana.mintsFile != null
? { mintsFile: resolveRef(config.solana.mintsFile) }
Expand Down
188 changes: 188 additions & 0 deletions packages/cluster-tool/src/config/EthereumClientConfigurationConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import Assert from "node:assert"

/** Finite limits nested in one host-side Ethereum signing client. */
export interface EthereumTransactionPolicy {
readonly max_priority_fee_per_gas_wei: string
readonly max_fee_per_gas_wei: string
readonly max_gas_limit: string
readonly max_total_native_cost_wei: string
}

/** One signing-capable Ethereum RPC connection. */
export interface EthereumClientConnection {
readonly client_id: string
readonly signature_provider_id: string
readonly rpc_url: string
}

/** One EVM signing client in nodeop's host-side configuration. */
export interface EthereumClientConfiguration {
readonly connection: EthereumClientConnection
readonly chain_id: number
readonly transaction_policy?: EthereumTransactionPolicy
}

/** Versioned ProtoJSON document consumed by `--outpost-ethereum-client-config-file`. */
export interface EthereumClientConfigurationFile {
readonly schema_version: number
readonly clients: readonly EthereumClientConfiguration[]
}

const CanonicalPositiveDecimal = /^[1-9][0-9]*$/,
SafeIdentifier = /^[A-Za-z0-9._-]{1,64}$/,
MaximumUint32 = 2 ** 32 - 1,
MaximumUint256 = (1n << 256n) - 1n

/** Construct and validate the host-only Ethereum client ProtoJSON document. */
export namespace EthereumClientConfigurationConfig {
/** Schema revision defined by `client_config.proto`. */
export const SchemaVersion = 1

/**
* Create one nodeop client configuration.
*
* This is intentionally a host-side ProtoJSON factory, not an OPP protocol
* model. SEC-131 keeps the client configuration outside the shared OPP model
* bundles consumed by Solidity and Solana.
*
* @param clientId - Stable identifier referenced by the Ethereum plugins.
* @param signatureProviderId - Process-local Ethereum signing-provider id.
* @param rpcUrl - HTTP(S) Ethereum JSON-RPC endpoint.
* @param chainId - Positive EVM chain identifier expected from the endpoint.
* @param transactionPolicy - Optional finite local expenditure policy.
* @returns A validated document using the protobuf field spelling accepted by nodeop.
*/
export function create(
clientId: string,
signatureProviderId: string,
rpcUrl: string,
chainId: number,
transactionPolicy?: EthereumTransactionPolicy
): EthereumClientConfigurationFile {
const configuration: EthereumClientConfigurationFile = {
schema_version: SchemaVersion,
clients: [
{
connection: {
client_id: clientId,
signature_provider_id: signatureProviderId,
rpc_url: rpcUrl
},
chain_id: chainId,
...(transactionPolicy == null
? {}
: { transaction_policy: transactionPolicy })
}
]
}
assertValid(configuration)
return configuration
}

/**
* Validate and return the canonical ProtoJSON value written for nodeop.
*
* @param configuration - Host-side configuration to persist.
* @returns The same validated ProtoJSON document.
*/
export function toJson(
configuration: EthereumClientConfigurationFile
): EthereumClientConfigurationFile {
assertValid(configuration)
return configuration
}

/**
* Assert the local factory's document satisfies the SEC-131 schema boundary.
*
* @param configuration - Document to check before persisting it.
* @returns Nothing; invalid documents throw an assertion error.
*/
export function assertValid(
configuration: EthereumClientConfigurationFile
): void {
Assert.equal(
configuration.schema_version,
SchemaVersion,
`Ethereum client configuration schema_version must be ${SchemaVersion}`
)
Assert.equal(
configuration.clients.length,
1,
"Operator daemon Ethereum configuration must contain exactly one client"
)

const [client] = configuration.clients
Assert.ok(client.connection != null, "Ethereum client connection must be present")
Assert.match(
client.connection.client_id,
SafeIdentifier,
"Ethereum client_id must be 1-64 ASCII letters, digits, '.', '_', or '-'"
)
Assert.ok(
client.connection.signature_provider_id.length > 0,
"Ethereum signature_provider_id must not be empty"
)
const rpcUrl = new URL(client.connection.rpc_url)
Assert.ok(
(rpcUrl.protocol === "http:" || rpcUrl.protocol === "https:") &&
rpcUrl.hostname.length > 0 &&
rpcUrl.hash.length === 0,
"Ethereum rpc_url must use http or https with a host and no fragment"
)
Assert.ok(
Number.isInteger(client.chain_id) &&
client.chain_id > 0 &&
client.chain_id <= MaximumUint32,
"Ethereum chain_id must be a positive uint32"
)

if (client.transaction_policy != null) {
assertTransactionPolicy(client.transaction_policy)
}
}

/**
* Assert that a finite policy uses canonical uint256 decimal fields and a
* valid EIP-1559 fee relationship.
*
* @param policy - Transaction policy nested in a client configuration.
* @returns Nothing; invalid policies throw an assertion error.
*/
export function assertTransactionPolicy(
policy: EthereumTransactionPolicy
): void {
const maximumPriorityFeePerGas = positiveUint(
policy.max_priority_fee_per_gas_wei,
"max_priority_fee_per_gas_wei",
MaximumUint256
),
maximumFeePerGas = positiveUint(
policy.max_fee_per_gas_wei,
"max_fee_per_gas_wei",
MaximumUint256
)
positiveUint(policy.max_gas_limit, "max_gas_limit", MaximumUint256)
positiveUint(
policy.max_total_native_cost_wei,
"max_total_native_cost_wei",
MaximumUint256
)
Assert.ok(
maximumPriorityFeePerGas <= maximumFeePerGas,
"Ethereum priority-fee cap must not exceed maximum-fee cap"
)
}
}

/** Parse one canonical positive unsigned decimal bounded by `maximum`. */
function positiveUint(value: string, field: string, maximum: bigint): bigint {
Assert.match(
value,
CanonicalPositiveDecimal,
`Ethereum ${field} must be a canonical positive decimal string`
)
const parsed = BigInt(value)
Assert.ok(parsed <= maximum, `Ethereum ${field} exceeds its supported domain`)
return parsed
}
2 changes: 2 additions & 0 deletions packages/cluster-tool/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export * from "./ApiNodeConfig.js"
export * from "./BatchOperatorSchedule.js"
export * from "./AnvilEthereumTransactionPolicyConfig.js"
export * from "./BindConfigProvider.js"
export * from "./ClusterBuildOptions.js"
export * from "./ClusterConfigProvider.js"
export * from "./DaemonConfig.js"
export * from "./ExternalClusterConfigProvider.js"
export * from "./EthereumClientConfigurationConfig.js"
export * from "./NodeConfig.js"
export * from "./SignatureProviderConfigProvider.js"
export * from "./SSMClientProvider.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ export interface OperatorDaemonArtifacts {
readonly ethereumAbiFiles: string[]
/** Deployed Ethereum outpost addresses (from `outpost-addrs.json`). */
readonly ethereumAddresses: Record<string, string>
/** Generated unified Ethereum client JSON shared by operator daemon processes. */
readonly ethereumClientConfigurationFile: string
/** The OPP outpost program id (base58) — `liqsol_core`'s `declare_id`. */
readonly solanaProgramId: string
/** Cluster-local verbatim copy of the `liqsol_core` (OPP outpost) IDL. */
readonly solanaIdlFile: string
}

/** Typed cross-step handle to the prepared {@link OperatorDaemonArtifacts}. */
export const OperatorDaemonArtifactsKey: OutputKey<OperatorDaemonArtifacts> = outputKey(
"cluster.operatorDaemonArtifacts",
"outpost deploy artifacts for operator daemon command lines (ETH ABIs + addrs, SOL program id + IDL)"
)
export const OperatorDaemonArtifactsKey: OutputKey<OperatorDaemonArtifacts> =
outputKey(
"cluster.operatorDaemonArtifacts",
"outpost artifacts for operator daemon command lines (ETH ABIs + client configs, SOL program id + IDL)"
)
Loading
Loading