One command brings up the whole private-chain design from MetaLeX's critical review of that design on one machine, onboards two issuers through the real company factory, and checks the result. The review and the experiments behind it are kept with the MetaLeX webapp, which is a private repository.
./stack up./stack status./stack smoke./stack down --allA clean up took 41 to 52 seconds here once the prerequisites were built (see
docs/PREREQUISITES.md). It is a demonstration environment: batch proofs
are fake, every key is a published development key, and nothing is hosted.
| Step | Result |
|---|---|
| 1. L1 | Anvil 1.5.1 loaded with the upstream ZKsync OS v31 snapshot, 1-second blocks, history kept |
| 2. L1 configuration | Validium mode (setPubdataPricingMode, setDAValidatorPair); PrividiumTransactionFilterer set with deposits enabled; ValidatorTimelock upgraded to MultisigCommitter with a custom signing set, the recovery node's signer and threshold 1; a two-party Safe and a recovery timelock (upstream Governance) that owns the chain admin, filterer, committer, ecosystem governance and ecosystem admin |
| 3. Nodes | Main node with batch verification on; recovery node as an external node that replays and co-signs every batch |
| 4. Cybercorps release | The unmodified pinned contracts: CyberCorpFactory, IssuanceManagerFactory, CyberCorpSingleFactory, DealManagerFactory, RoundManagerFactory, CyberAgreementRegistry, CertificateUriBuilder with its image builder, HolderCapCondition behind a proxy, and a test ERC20 as the payment asset (contracts/StackRelease.s.sol). Also Multicall3, the era-contracts copy (contracts/Multicall3.sol) |
| 5. Prividium Core | Organizations off, policy listener on, its own PostgreSQL database for the run |
| 6. Policy on | Main node restarted so every transaction and simulation passes Core's policy service. Core then gets its roles, users and the app server's machine application (below) |
| 7. Two issuers | Each officer calls CyberCorpFactory.deployCyberCorp through Core; the script then registers the company's manager, corp, deal manager, round manager and beacons in Core and adds the issuer's rows. Each officer creates a printer, issues one certificate and deploys scrip |
| 8. Smoke test | 12 checks, listed below |
| What | Address | Notes |
|---|---|---|
| Core RPC | http://localhost:18000/rpc |
The only entry point for wallets and the app. Needs Authorization: Bearer <session token>; sign in with SIWE at /api/siwe-messages and /api/auth/login/crypto-native |
| Core RPC, as the app server | http://localhost:18000/rpc |
Send the key from ~/.cache/metalex-x0/stack-runs/current/app-server.key in an X-API-Key header. Loopback only, valid 7 days. It reads all state and submits signed transactions for policy to judge |
| Core API | http://localhost:18000/api |
Admin wallet is CORE_ADMIN_PK in config/stack.env |
| L1 RPC | http://127.0.0.1:18545 |
Chain ID 31337 |
| Main node RPC | http://127.0.0.1:13050 |
Unauthenticated. It answers eth_getStorageAt for anyone. Never expose it |
| Addresses | ~/.cache/metalex-x0/stack-runs/current/state.json |
L1 contracts, authority contracts, release contracts (including multicall and holderCapCondition), Core role and user IDs, both companies |
Private chain ID is 506. Core binds to all network interfaces on ports 18000, 18010 and 19091, and its admin key is a published one. Run the stack only on a trusted network, and stop it when done.
- Holder A scripifies 100 units through Core and reads the balance.
- Holder A recertifies 20 units.
- Holder A reads its own certificate count.
- Holder A is refused six direct reads: issuer B's printer, certificate, scrip, manager and corp, and a certificate of its own issuer by raw ID.
- Holder B cannot use issuer A's manager.
- Officer A cannot call a setter that has no Core row (
setRestrictionHook). - The raw node port answers a storage read without credentials (a reminder, expected to pass).
- The app server's machine key reads both issuers' records, storage and code through Core.
- Multicall3 batches reads of both companies and
HolderCapConditionin one call. - The app server relays a holder's transaction that was signed with no chain access, and the policy service still refuses another holder's.
- Batches settle on L1.
- Every commit went through
commitBatchesMultisigwith the recovery node's signature. - The certificate text and the holder's address are absent from the settlement transactions.
- The chain admin, filterer and committer are owned by the recovery timelock.
- The old chain owner key can no longer remove the L1 filterer.
Result on 10 November 2026: 15 of 15 on a clean run (docs/evidence/up-run-20261110114452.txt).
The first 12 checks passed on two earlier clean runs (docs/evidence/up-run-20261110103248.txt).
- The release needs one setting on a new chain.
CyberCorpFactory.initializepointslexchexAuthat a fixed address that exists only on the public chains, and company creation calls it. The stack clears it with the owner settersetLexchexAuth(0). LeXcheX is not deployed, so public rounds that mint a LeXcheX badge will not work here. - Company creation works through Core under one umbrella row.
deployCyberCorpcreates five contracts in one transaction (about 2.43 million gas) and none of them needs to be registered first. Deploying the release itself took about 111 million gas on the private chain. - A bridge lock works through Core (
docs/evidence/bridge-lock-through-core.txt). It needs a row for the scrip'sapproveand an umbrella onL2AssetRouter.withdraw(bytes32,bytes). Nobody can call the vault'sregisterTokenthrough Core, because Core's built-in description of that contract omits the function; the router registers the token itself when the holder passes the asset ID computed offline (keccak256(abi.encode(chainId, vault, token))). The holder can read the receipt and the message proof through Core. An umbrella onwithdrawlets a holder lock any token it holds, so the refuse-the-vault hook stays the only per-token control. - The app server needs a Core machine application with two permissions.
full_read_accessalone gives logs, blocks and receipts but refuseseth_call, storage, code and nonce reads;full_sequencer_rpc_accessadds those and lets the server submit other wallets' signed transactions, which the policy service still judges by signer. A machine application answers nothing until its IP allowlist has an entry. - The webapp's send-path guard needs two contracts a new chain lacks. It batches reads through
Multicall3, and it reads
HolderCapCondition, which on the public chains sits at one fixed address that a new chain cannot reproduce. The stack deploys both in the release step. With them, the unmodified guard from the webapp'sdevelopbranch passed for a healthy company and refused a changed manager, using this stack's machine key. The webapp still needs that address per chain. - Deploy before policy is on. Afterwards only the zone admin can deploy, through Core, and the policy service refused a proxy whose constructor calls its implementation.
- Core refuses some published test keys as user wallets (Anvil account 8, for example).
- Two shell variable names break tools. Any variable starting with
FOUNDRY_makescastabort, and Core readsMETRICS_PORTand similar names from the environment before its.env. The stack avoids the first and starts Core with a clean environment.
- No real proofs, no v32 chain, no public testnet, no hosting.
- The webapp is not part of
up. Its server code for the private chain has been run against the stack from scripts, and one screen (create a company) has been run in a browser with a Privy embedded wallet. No injected wallet has been tried against Core. - No indexer runs here, and the webapp's read screens depend on one. The webapp's indexer was run once against the recovery node from a scratch copy, which is not in this repository.
- Deals, rounds, LeXcheX, extensions and conditions are deployed or deployable but have no Core rows and were not exercised.
- The L1 finalize, public transfer and return half of the bridge is not part of
up; it was run without Core in experiment X2. - Prerequisites are not fetched or built; see docs/PREREQUISITES.md.
| Path | Content |
|---|---|
stack |
Entry script: up, smoke, status, down |
config/stack.env |
Paths, ports, keys and the part each test account plays |
lib/stacklib.py |
L1 signing set and authority arrangement, Core configuration, issuer onboarding, smoke test |
contracts/StackRelease.s.sol |
Deployment of the Cybercorps release for a chain the repository's own script does not know |
contracts/Multicall3.sol |
Unmodified copy from era-contracts (MIT), deployed with the release |
docs/ |
Prerequisites and evidence from the runs |
s1-privy-harness/ |
A Vite test page: a Privy embedded wallet signs a transaction that the local server prepared, and the server relays it. provision.sh registers the wallet in Core |
- The scratch copy of the webapp's indexer that one experiment used. It is private application code.
- The result file of the wallet signing test, which holds a wallet address and a login identifier.
- Every upstream build.
docs/PREREQUISITES.mdnames the pinned versions and where each is expected on disk.
Paths in the scripts are those of the machine the stack was built on. config/stack.env sets most of them.