Skip to content

feat: deploy spol contracts#545

Open
leovct wants to merge 32 commits intomainfrom
feat/bundle-spol-into-pos-deployer
Open

feat: deploy spol contracts#545
leovct wants to merge 32 commits intomainfrom
feat/bundle-spol-into-pos-deployer

Conversation

@leovct
Copy link
Copy Markdown
Member

@leovct leovct commented Apr 28, 2026

No description provided.

MaximusHaximus and others added 23 commits April 17, 2026 20:04
Adds an optional sPOL/LST contract deployment phase on top of the existing
PoS devnet. Opt in with `dev.deploy_lst_contracts: true` (see
`params-spol-test.yml` for the canonical example). When enabled, after the
base PoS + validator stake are up, a new starlark module runs a
`spol-contract-deployer` container which:

1. Deploys mock PolygonMigration + RootChainManager on L1 (the real
   contracts don't exist on a fresh devnet).
2. Writes a kurtosis-devnet scenario into script/input.json wiring the
   runtime-discovered PoS addresses + mock addresses.
3. Invokes the existing `Deploy.s.sol:run(string)` entrypoint to deploy
   the full sPOL suite to L1 and L2.
4. Runs a kurtosis-specific SetupInitialValidators variant that
   registers the single devnet validator (id 1).

The resulting addresses are exposed via a `lst-contract-addresses`
kurtosis artifact which consumers (e.g. lst-api#93) read by name.

## Deployer image

Follows the existing `pos-contract-deployer` / `pos-el-genesis-builder` /
`pos-validator-config-generator` pattern:

- `docker/spol-contract-deployer.Dockerfile` — clones 0xPolygon/spol-contracts
  at a pinned ref, overlays kurtosis mocks from `docker/spol-mocks/`,
  pre-installs soldeer deps, warms the forge cache.
- `.github/workflows/publish-images.yaml` — gains a
  `publish-spol-contract-deployer` job mirroring the three existing ones,
  pinned to spol-contracts main @ 3c4bdf6c.

No changes required in spol-contracts itself — the runtime scenario-JSON
write reuses upstream's existing `loadConfigFromJson` path.

## Config / tests / docs

- `DEV_ARGS` gains `deploy_lst_contracts` (bool, default false) and
  `lst_deployer_params` (reward_fee / fee_receiver / max_divergence).
- `sanity_check.star` validates both the top-level flag and the
  `lst_deployer_params` sub-keys against a LST_DEPLOYER_PARAMS allowlist.
- Parser and sanity_check tests cover defaults merge, partial override,
  the `deploy_lst_contracts requires should_deploy_l1` invariant, and
  unknown-key rejection.
- `docs/configuration/reference.md` and the README document the new
  options under existing sections.

## Static files layout

`static_files/contracts/` split into `pos/` (existing L1/L2 scripts) and
`lst/deploy-lst-contracts.sh` so each deployer only uploads the files it
needs.

Verified end-to-end against a live kurtosis devnet — L1 Anvil + L2 Bor +
Heimdall, sPOL L1+L2 deploy produces consistent CREATE2 addresses
(accessManagerL1 == accessManagerL2, polBridger symmetric).
The previous implementation did `git clone --depth 1 --branch main`
followed by `git checkout ${SHA}`. The shallow clone only fetches the
tip of main, so the checkout fails with "pathspec did not match any
file(s) known to git" — but its non-zero exit was swallowed by the
`|| true` on the trailing `find`, so every image was silently built
from whatever main currently pointed at, not the pinned SHA.

This was benign up to now because the commits on main since the pin
(3c4bdf6c -> a2a24ea) are docs/license-only, so the compiled Solidity
matches. A Solidity-affecting commit landing on main between pin and
publish would have shipped an image whose tag claimed one SHA but
whose content matched another, with no diagnostic.

Fix by dropping --depth 1 and --branch so the checkout works for any
ref form (short SHA, full SHA, tag, branch). Removes the now-unused
SPOL_CONTRACTS_BRANCH arg from the publish workflow.
Forge's `new X{salt: y}(args)` in solidity compiles to a CALL into the
canonical deterministic deployer at
0x4e59b44847b379578588920cA78FbF26c0B4956C. Bor's genesis here had
`"alloc": {}` — so that address has no code on chain, forge's
simulation still succeeds against its in-memory model (including
`_verifyDeploymentL2` view checks), the deployment artifact gets
written with the predicted CREATE2 addresses, the deployer container
exits zero, but the broadcast-side CREATE2 calls silently no-op and
the predicted addresses have no code on L2.

The usual runtime recovery — fund the EIP-2470 signer and broadcast
its pre-signed legacy deploy tx — cannot work here because
`eip155Block: 0` in this genesis rejects legacy tx without chain-id
replay protection. Pre-alloc'ing the deployer's runtime bytecode at
genesis matches what Anvil does by default and is the only way to
guarantee forge CREATE2 emission materialises contracts on Bor.

Evidence: lst-api#93 e2e runs against `params-spol-test.yml` (anvil L1
+ Bor L2) deploy sPOL successfully as far as forge is concerned
— `All verifications passed for L2!` — but the subsequent
`sPOLChild.paused()` at the reported `0xF70710...` address returns
0x (no code). On the sibling kurtosis-pos PR #534, the same root
cause surfaces earlier and more loudly: jobs whose L1 backend is
`ethereum-package` (geth) fail outright with
`Error: script failed: missing CREATE2 deployer: 0x4e59b44847b379578588920cA78FbF26c0B4956C`,
because forge's pre-flight check runs against the first fork and
geth L1 — unlike anvil L1 — doesn't come with the deployer either.
The anvil-L1 CI job passes only because forge's check runs on L1
(anvil, which has the deployer) and never rechecks on L2 after the
second `vm.createSelectFork`.
The prior commit added the EIP-2470 CREATE2 deployer to the genesis
template at static_files/el/genesis/genesis.json. That was the wrong
spot: builder.sh runs after the template is rendered and, as its final
step, overwrites the template's `alloc` field with the alloc produced
by `generate-genesis.js` (genesis-contracts):

    # Add the alloc field to the temporary EL genesis to create the final EL genesis.
    jq --arg key 'alloc' '. + {($key): input | .[$key]}' \
      "${EL_GENESIS_FILE}" "${EL_GENESIS_ALLOC_FILE}" > tmp.json
    mv tmp.json "${EL_GENESIS_FILE}"

Consequence: the template's alloc is discarded, and the deployer never
appears in the final L2 genesis. Confirmed from the enclave dump on the
lst-api#93 e2e run: l2-el-genesis-tmp/genesis.json had the entry,
l2-el-genesis/genesis.json (the one Bor actually uses) did not.

Move the alloc into builder.sh as a jq step on `EL_GENESIS_ALLOC_FILE`,
mirroring the existing pattern for the EIP-2935 block-hash history
contract. That file is then preserved by the subsequent merge.

Revert the no-op alloc in the template so the source of the rule is
single-sited in builder.sh.
…me as network_params

Mainnet-realistic defaults (128 blocks / 120s buffer) preserve today's
cadence for everyone who doesn't override. Test profiles can compress
checkpoint round-trips to ~15-20s by setting e.g. 8 / 10s — which is
what params-spol-test.yml now does so the sPOL e2e suite can observe
burn → checkpoint → exit inside a single test timeout without waiting
2–3 minutes per checkpoint.

Pattern mirrors cl_checkpoint_poll_interval: a network_params knob with
a centrally-defined default, consumed from the single template boundary
in src/cl/genesis.star, validated via sanity_check.star.

Motivated by the sPOL LST service's end-to-end migration-exit test,
which needs real matic.js proofs round-tripping through kurtosis. The
old 128-block cadence made that path infeasible inside a reasonable
test timeout.
Mirrors the existing WithdrawManager `exitPeriod=1` override. Without it,
a validator unstake scheduled by `StakeManager.unstake(validatorId)` does
not become claimable until `currentEpoch + dynasty` — which at the
mainnet default (`~886k` bor blocks, i.e. days) is well past any devnet
test budget. Setting `dynasty=1` brings the unbond window down to one
checkpoint, which at the fast-cadence overrides (`avg_checkpoint_length=8`,
`checkpoint_buffer_time=10s`) is ~16-25s.

Unblocks the downstream `sPOLController.withdrawPOL(user)` path that
consumes those per-validator nonces — lst-api's happy-path claim test
currently reverts with `NoNoncesReady(user)` for exactly this reason.
…Value

The previous commit called `updateDynasty(uint256)` which does not exist
on StakeManager. The actual function is `updateDynastyValue(uint256)` —
`onlyGovernance`, sets BOTH `dynasty` (default 886 epochs) and
`WITHDRAWAL_DELAY` (default 2^13 epochs) to the new value in one call.

Both fields gate the claim delay, so a single `updateDynastyValue(1)`
call achieves what the previous commit message described.
# Conflicts:
#	src/config/input_parser.star
#	src/config/sanity_check.star
#	static_files/contracts/deploy-l1-contracts.sh
#	static_files/contracts/deploy-l2-contracts.sh
#	static_files/contracts/l1/deploy-plasma-bridge.sh
#	static_files/contracts/l2/deploy-plasma-bridge.sh
#	static_files/contracts/pos/deploy-l1-contracts.sh
#	static_files/contracts/pos/deploy-l2-contracts.sh
… image

Folds the standalone spol-contract-deployer image into pos-contract-deployer
alongside pos-contracts and pos-portal. The bundled image now ships all three
contract sets under /opt/{pos-contracts,pos-portal,spol-contracts}; the LST
deploy step pulls it from setup_images.contract_deployer like every other
deployer, removing the dedicated spol_contract_deployer_image entry.

The Dockerfile gains a third clone+build block (full clone, soldeer install,
forge build) and a runtime COPY block for the ~21MB sPOL surface forge needs
at script time (foundry.toml, lockfiles, remappings, src/, script/,
dependencies/, out/, cache/). test/, broadcast/, audits/ are skipped.

publish-images.yaml drops the standalone spol-contract-deployer job and
extends pos-contract-deployer with SPOL_CONTRACTS_{BRANCH,COMMIT_SHA}
build-args + matching com.polygon.spol-contracts.{branch,commit} OCI labels.
POS_CONTRACT_DEPLOYER_VERSION bumped to 0.0.3 to reflect the new bundled
contents.
…n + RootChainManager

Daryll's branch shipped MockPolygonMigration and MockRootChainManager because
neither contract existed on a fresh kurtosis PoS devnet. After PR #538
(migrate-matic-to-pol + pos-bridge), both are deployed for real:

- PolygonMigration → .root.tokens.PolygonMigration (from migrate-matic-to-pol-token.sh)
- RootChainManagerProxy → .root.posBridge.RootChainManagerProxy (from deploy-pos-bridge.sh L1)

sPOL passes both addresses to constructors only — no method calls during deploy
— so the real contracts are interface-compatible with what the mocks provided.

deploy-lst-contracts.sh now reads both from the accumulated contractAddresses.json
artifact instead of running a separate DeployMocks step. The DeployMocks.s.sol /
MockPolygonMigration.sol / MockRootChainManager.sol files are deleted.

Renames docker/spol-mocks/ → docker/spol-kurtosis/ since "mocks" no longer fits
— only SetupInitialValidatorsKurtosis.s.sol remains, and it's a kurtosis-specific
validator-id-1 bootstrap rather than a mock. Container path updates to match
(script/mock/ → script/kurtosis/).
SetupInitialValidatorsKurtosis hardcoded validator id 1 with 100% share, so
the kurtosis devnet's sPOL deployment only worked when participants count was
exactly 1. Multi-validator devnets (POLYGON_POS_PARTICIPANT.count >= 2) had
N validators staked in StakeManager but only id 1 registered in sPOLController.

The script now reads VALIDATOR_COUNT from env, loops 1..N adding each (pos-contracts
assigns validator ids sequentially starting at 1, so this matches on-chain state),
and distributes shares equally with any integer-division remainder going to
validator 1 so shares always sum to 100. Capped at 100 since shares are uint8.

main.star and lst_deployer.star pass len(validator_accounts) through as
VALIDATOR_COUNT.
…e, move LST scripts to l1/

Two changes:

1. SetupInitialValidatorsKurtosis is now a Mustache template rendered at
   devnet-launch time via plan.render_templates(), with VALIDATOR_COUNT baked
   in as a compile-time constant. The bash wrapper copies the rendered .sol
   into /opt/spol-contracts/script/kurtosis/ at runtime so its relative
   `import "../../src/sPOLController.sol"` resolves. This drops the env-var
   dance, lets solc constant-fold the loop bounds (3422 → 3040 bytes of code),
   and keeps the pattern consistent with how every other deployer ships its
   scripts — through static_files/, not baked into the image.

2. Moves the LST deploy bash + template into static_files/contracts/l1/ since
   the script is L1-anchored: forge runs with --rpc-url L1_RPC_URL, the
   sPOLController canonical state lives on L1, validator setup is L1-only.
   (sPOL.s.Deploy uses vm.createSelectFork to also deploy on L2 within the
   same forge invocation, but that's an internal mechanic — the entrypoint is
   L1.) Drops the now-empty contracts/lst/ subdir and the docker/spol-kurtosis/
   bake-time directory entirely.
…buySPOL rationale

The LST deploy now runs unconditionally whenever should_deploy_matic_contracts
is true (the default). The deploy_lst_contracts opt-in flag and the
lst_deployer_params dict are both removed from the user-facing config — the
sPOLController parameters (reward_fee, fee_receiver, max_divergence) are now
hardcoded constants in lst_deployer.star with the same values that used to be
the documented defaults. Devnet usage doesn't benefit from tuning these.

Knock-on cleanups:
- main.star gates LST on should_deploy_matic_contracts (the same gate that
  guarantees the accumulated contractAddresses.json contains PolygonMigration
  and RootChainManagerProxy with the expected shape).
- Drops DEV_ARGS.deploy_lst_contracts, DEV_ARGS.lst_deployer_params, the
  lst_deployer_params merge logic in input_parser, the LST_DEPLOYER_PARAMS
  allowlist + sub-validator in sanity_check, and the three matching tests.
- Drops the LST Deployer doc section from configuration/reference.md (only
  one row remains in the dev table; should_deploy_matic_contracts now also
  governs LST).
- Removes the params-spol-test.yml example file from the repo root — out of
  place there (every other config sits under .github/configs/) and no longer
  meaningful with LST always-on.
- README's numbered deploy list now mentions plasma bridge, MATIC→POL,
  pos bridge, and sPOL/LST. The LST line in "Optional features" is gone.

Also fixes the rationale in setupInitialValidators.s.sol for skipping
buySPOL. Daryll's original comment blamed delegationDepositPOL; the actual
incompatibility is that sPOLController._buySharesFromValidator calls
validatorContract.restakeAndStakePOL — a fused function not present in the
pinned pos-contracts ValidatorShare (which exposes restakePOL and
buyVoucherPOL separately). Adds a clarifying comment near the forge-script
invocation in deploy-lst-contracts.sh to explain why L2_RPC_URL is read but
not used directly (consumed inside Deploy.s.sol via vm.createSelectFork).
…op redundant gate)

- jq -re for all artifact reads in deploy-lst-contracts.sh so set -e catches
  missing/null keys at the read site instead of letting them propagate as
  "null" strings into forge script and revert deep in the deploy. Carryover
  from a since-dropped review PR. Scoped to deploy-lst-contracts.sh only;
  applying the same to the other deploy scripts is a separate cleanup.
- README links: plasma bridge → 0xPolygon/pos-contracts, MATIC→POL migration
  → 0xPolygon/pol-token (previously unlinked).
- docs: drop the redundant "— gates the entire flow" trailer from the
  should_deploy_matic_contracts row in configuration/reference.md.
- main.star: drop the should_deploy_matic_contracts guard and its long
  comment from the LST call site. The L2 plasma-bridge and pos-bridge
  deploys above it run unconditionally; LST now matches that pattern.
  jq -re makes a missing-key failure mode explicit if a user opts out of
  contract deploy and supplies a partial addresses file.
…int cadence by default

Three small fixes carried over from review:

- el_chain_id was incorrectly resurrected during the merge of
  feat/spol-lst-deployer into main: Daryll's branch had added it to
  DEV_ARGS.network_params and POLYGON_POS_PARAMS, but main had since
  removed it (it lives in constants.EL_CHAIN_ID and shouldn't be
  per-devnet overridable). lst_deployer was the only consumer reading
  network_params.el_chain_id; switched to constants.EL_CHAIN_ID
  directly, removed the field from defaults + sanity check.
- Default checkpoint cadence is now the fast values from the deleted
  params-spol-test.yml (cl_avg_checkpoint_length=8,
  cl_checkpoint_buffer_time=10s) instead of the mainnet-conservative
  128/120s. The slower values are useless for a kurtosis devnet — sPOL
  e2e tests need ~15-20s burn→checkpoint→exit round-trips, and no
  in-tree config relied on the slow defaults. Comment dropped.
- Stripped the pseudo-justification comment from
  REWARD_FEE/FEE_RECEIVER/MAX_DIVERGENCE in lst_deployer.star — the unit
  comments on each line are sufficient.
…CTS_CONFIG_FILE_PATH

Daryll's branch had renamed static_files/contracts/{deploy-l*-contracts.sh}
to static_files/contracts/pos/* and updated this path to match. The merge
of main accepted main's flat layout (l1/ + l2/ subfolders for the
plasma-bridge / pos-bridge / matic-to-pol scripts) and dropped Daryll's
duplicate pos/ files, but the .star file still pointed at the deleted
pos/ path. Surfaced as "/static_files/contracts/pos doesn't exist" on
the first kurtosis run.
…m-package geth

sPOL's Deploy.s.sol uses Solidity's `new X{salt: y}` syntax for deterministic
CREATE2 addresses. solc compiles that to a CALL into the canonical EIP-2470
deployer at 0x4e59b44847b379578588920cA78FbF26c0B4956C. The L2 Bor genesis
already pre-allocates this contract (see static_files/el/genesis/builder.sh),
but the ethereum-package L1 geth genesis does not — so the LST deploy was
failing with `missing CREATE2 deployer` once it forked to L1.

Mirrors the L2 prealloc by extending account_util with a sibling helper
to_ethereum_pkg_preallocated_contract (returns {balance, code} entries) and
adding the CREATE2 deployer to the L1 prefunded_accounts list passed to
ethereum-package. The genesis-generator's el_premine_addrs handler accepts
arbitrary {balance, code} objects, so the entry passes through to the L1
genesis alloc untouched.

The standard EIP-2470 runtime bootstrap (fund one-shot signer, broadcast
pre-signed legacy tx) can't rescue this — eip155Block: 0 on the L1 geth
rejects legacy txs without chain-id replay protection.
…main.star

main.star was orchestrating six contract deploys directly (plasma L1 +
matic-to-pol + pos-bridge L1 + plasma L2 + pos-bridge L2 + LST). Pulls
that orchestration into a single src/contracts/contracts.star module
exposing two primitives:

- deploy_l1_contracts(...) — plasma L1 + matic-to-pol + pos-bridge L1,
  returns (addresses, validator_config)
- deploy_l2_contracts(...) — plasma L2 + pos-bridge L2 + LST, returns
  the final addresses artifact

main.star drops four imports (the individual deployers) for one
(contracts), and the two orchestration blocks shrink from ~30 lines each
to a single call. Net main.star size: 232 → 184 lines.

deploy_l{1,2}_contracts (vs the shorter deploy_l{1,2}) reads better at
the call site — `contracts.deploy_l1` would be ambiguous next to
`l1_launcher.launch`.

Also drops the now-redundant docstring on lst_deployer.deploy_lst_contracts;
peer deployers in this directory have no docstring, so matching that
style keeps the orchestrator and its callees consistent.
Defaults are no longer mainnet-conservative; the fast values that the
comment described as test-only overrides are now baseline (see commit
8580fa4).
…rator

Reusing 'l1_addresses' / 'addresses' across the chained deploy calls hid
the fact that each step actually produces a distinct kurtosis artifact
(plasma-bridge-l1-addresses → plasma-bridge-l1-addresses-with-pol →
pos-bridge-l1-addresses on L1; plasma-bridge-addresses →
pos-bridge-addresses on L2). Naming variables after the artifact each
step produces makes the threading explicit.
The module-level REWARD_FEE/FEE_RECEIVER/MAX_DIVERGENCE constants were each
referenced exactly once. Inlining them into the env_vars dict drops a layer
of indirection while keeping the unit/intent comments next to the values.
…tract-addresses artifact

LST was the only deploy step that wrote a separate kurtosis artifact instead of
accumulating onto the chain. Folds it in: the bash wrapper now reads the
upstream pos-bridge addresses, runs forge as before, then merges
script/deployment.json's sPOL_L1 / sPOL_L2 keys under .root.spol / .child.spol
in a single contractAddresses.json. The orchestrator returns the merged artifact
as the final pos_contract_addresses, so downstream consumers get one file
covering plasma + matic-to-pol + pos-bridge + sPOL/LST instead of fetching two.

Renames the artifact from "lst-contract-addresses" / "spol-addresses" to
"pos-contract-addresses" — describes what's in it (the full PoS contract
suite) rather than the last step that touched it. Per-step artifact names
(plasma-bridge-l1-addresses, plasma-bridge-l1-addresses-with-pol, etc.) keep
their what-was-just-added pattern; the consumer-facing final artifact gets
a what's-inside name.
…bility

Sweep over all kurtosis artifact names produced by the contract deploy
chain so each name describes what it is (or where it ran), without
lineage cruft. Five intermediate renames + doc/CI updates pointing at
the final accumulated artifact:

| Step                | Before                              | After                      |
| ------------------- | ----------------------------------- | -------------------------- |
| matic→pol migration | plasma-bridge-l1-addresses-with-pol | pol-migration-addresses    |
| plasma L2           | plasma-bridge-addresses             | plasma-bridge-l2-addresses |
| pos-bridge L2       | pos-bridge-addresses                | pos-bridge-l2-addresses    |
| LST run service     | lst-contracts-deployer              | lst-deployer               |
| LST template script | lst-setup-validators-script         | lst-validator-setup-script |

Plus, updates docs/, .claude/, .github/ references that previously fetched
the (now-superseded) `pos-bridge-addresses` to use `pos-contract-addresses`
— the merged final artifact that includes plasma + matic-to-pol + pos-bridge
+ sPOL/LST. Renames the on-disk JSON filename in the partial-redeploy flow
to match.
@leovct leovct mentioned this pull request Apr 28, 2026
@leovct leovct deployed to publish-images April 28, 2026 14:35 — with GitHub Actions Active
The pos-e2e companion branch tracks the pos-bridge-addresses →
pos-contract-addresses artifact rename done in this PR. Pin the e2e action
defaults to that branch so CI on this branch picks up the matching e2e
helpers; otherwise the e2e setup.bash would fail looking for the old
artifact name.

Branch-name pin (rather than SHA) is intentional during the PR cycle —
allows additional commits on the e2e branch to flow through without
needing to bump the SHA here. Flip back to a SHA on main once both PRs
have merged.
@leovct
Copy link
Copy Markdown
Member Author

leovct commented Apr 28, 2026

Test run: https://github.com/0xPolygon/kurtosis-pos/actions/runs/25060704919/job/73413595528

Some failures:

leovct added 2 commits April 28, 2026 17:17
Two CI scenarios that bypass kurtosis-pos's L1 launcher were failing:

1. run-with-external-l1 launches ethereum-package directly via
   --args-file external-l1/ethereum.yml, skipping main.star's L1 path
   and our _merge_l1_prefunded_accounts helper. The CREATE2 deployer
   prealloc never ran on L1, so sPOL's `new X{salt: y}` calls failed
   with "missing CREATE2 deployer". Fix: prealloc the same EIP-2470
   bytecode in ethereum.yml's prefunded_accounts directly.

2. run-with-cl-el-genesis is the partial-redeploy flow — it captures
   pos-contract-addresses from a prior run, then re-launches with
   should_deploy_matic_contracts: false to reuse the prior L1's
   contracts. LST is the only L2 deploy step that re-touches L1 (via
   vm.createSelectFork), so re-running it against an L1 that already
   has sPOL deployed at the same salt-derived addresses fails with
   CREATE2 CreateCollision. Fix: gate LST on should_deploy_matic_contracts
   in contracts.deploy_l2_contracts. When the user supplies pre-deployed
   addresses, sPOL was deployed by the prior run too — return the
   pos-bridge artifact as the final result.

   plasma_bridge L2 and pos_bridge L2 keep running unconditionally
   because they use plain CREATE (no salt collisions) and are needed
   to wire the fresh L2 chain to the existing L1.
…rvative

Commit 8580fa4 dropped cl_avg_checkpoint_length 128→8 and
cl_checkpoint_buffer_time 120s→10s, hoping to shorten devnet round-trips.
That broke every plasma withdraw test in pos-e2e: with checkpoints landing
every ~10s, a new checkpoint can land on L1 between polycli's heimdall
lookup and the exit submission, shifting RootChainProxy.currentHeaderBlock
and triggering WITHDRAW_BLOCK_NOT_A_PART_OF_SUBMITTED_HEADER reverts on
WithdrawManager.startExitWithBurntTokens.

Restore 128/120s defaults and document the failure mode in a comment so
nobody repeats the change. Test profiles that explicitly need fast
checkpointing (sPOL e2e, etc.) can keep overriding via params file.
…et-conservative"

This reverts 7433710. The fast cadence (8 / 10s) is fine for devnet
defaults — pos-e2e's wait_for_new_checkpoint will be updated to wait
for one extra checkpoint past the burn-covering one, which lets polycli
build proofs against a reorg-stable L2 view.

See companion change in pos-e2e:
  core/helpers/scripts/pos-bridge.bash adds checkpoint_settle_lookahead=2.
leovct added 2 commits April 29, 2026 10:12
v0.1.111 includes the merkle-proof power-of-2 padding fix from polygon-cli's
fix/pos-exit-proof-non-power-of-2-merkle, which makes 'pos exit-proof' work
against checkpoint ranges that aren't powers of 2 (the case our fast-cadence
devnets hit). Bumps the runtime fetch in run-e2e-tests/action.yml and the
build-arg in publish-images.yaml so both the pos-contract-deployer image
and the e2e test runner ship with the fix.
The branch-name pin was a transient stopgap during PR review. Now that
chore/rename-kurtosis-pos-artifact has merged to pos-e2e main as 036860c,
flip both action defaults to that SHA so CI runs against an immutable
ref instead of a feature branch that's about to be deleted.
@leovct
Copy link
Copy Markdown
Member Author

leovct commented Apr 29, 2026

Waiting for:

@leovct leovct marked this pull request as ready for review April 29, 2026 08:27
@leovct leovct requested review from a team and MaximusHaximus April 29, 2026 08:27
leovct added 2 commits April 29, 2026 11:49
The cl-el-genesis test path runs `kurtosis run` twice: a first deploy that
extracts the L2 EL/CL genesis + contract addresses, then a second deploy
against the same L1 with `should_deploy_matic_contracts=false`.

`contracts.deploy_l2_contracts` runs unconditionally on both deploys, so
`deploy-pos-bridge.sh` re-executed its L1 cross-chain wiring on the redeploy.
That wiring issues 7× `RootChainManager.mapToken`, each of which calls
`StateSender.syncState` and bumps the L1 state-sync counter. With L1 preserved
from the first deploy (counter already > 0) and bor's volumes wiped
(lastStateId reset to 0), the post-redeploy state-sync counter sat ahead of
bor — so when the bats deposit fired event #N, bor saw it as `stateID=N`
against `Last state id was 0` and rejected it forever.

Gate the L1 wiring section on `SKIP_L1_WIRING`, derived from
`should_deploy_matic_contracts`. The L2 child contracts still redeploy at
the same deterministic addresses, and the L1 wiring from the prior deploy
is preserved untouched.

Fixes the `bridge POL from L1 to L2 via plasma bridge` regression on
`run-with-cl-el-genesis` introduced by #538.
After volume wipe + redeploy, bor's chaindata is fresh (lastStateId=0)
but L1's StateSender already counts 7 events from the first deploy's
mapToken wiring. Heimdall returns the next state-sync event with id=N
to bor, which rejects it as non-contiguous: bor expects last+1=1, sees
the embedded id=N, and retries forever — same fingerprint we saw in
the failing run. Pre-fill the StateReceiver precompile's slot 0
(`uint256 public lastStateId`) in the saved EL genesis so bor restarts
already in lockstep with L1.

Combined with the prior commit (skip pos-bridge L1 wiring on redeploy),
the cl-el-genesis bats `bridge POL from L1 to L2 via plasma bridge`
test now passes end-to-end.

Also routes the extracted artifacts via tmp/cl-el-genesis/ rather than
.github/configs/nightly/cl-el-genesis/. Kurtosis 1.18+ strips leading
dots from top-level dirs when packing the package archive (mholt/archives
regression in kurtosis #2947), so read_file(".github/...") fails to
resolve files under hidden dirs locally. CI's pinned 1.15.2 is
unaffected, but this keeps local and CI behaviour identical and unblocks
local reproduction.

Verified locally:
- L1 StateSender.counter: 7 (post first deploy + 7 mapToken events)
- Pre-fill applied: alloc[0x...1001].storage[0x0..0] = 0x...07
- Post redeploy: bor lastStateId() = 7 (read from genesis pre-fill)
- Post bats deposit: L1=8, L2=8 (state-sync flows end-to-end)
- bats: ok 1 bridge POL from L1 to L2 via plasma bridge
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants