Skip to content

PLT-1022: fund the contract deployer - #59

Open
bdchatham wants to merge 5 commits into
mainfrom
brandon2/plt-1022-fund-the-contract-deployer
Open

PLT-1022: fund the contract deployer#59
bdchatham wants to merge 5 commits into
mainfrom
brandon2/plt-1022-fund-the-contract-deployer

Conversation

@bdchatham

@bdchatham bdchatham commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The contract deployer is now a funded identity the generator receives, not one it mints. Unblocks StorageRW and every multi-contract workload on a real chain.

Defect

NewGenerator deployed contract scenarios from a key minted inside deployAll — before funding ran, and outside gen.Accounts(), so funding could never reach it in any order. Failures panicked, and the deploy nonce was the instance index.

No profile caught it: arctic-1.json is the only funded one and runs EVMTransfer, which never deploys.

Decision

funder.Deployer(cfg) names the deployer; main hands it to NewGenerator. Funding configured → the funding root, which already deploys Disperse. Otherwise a fresh key, as today. Under mock deploy the key file is never read, so --dry-run works with no secret mounted.

Deploys leave auth.Nonce unset and await each receipt, so scenario deploys and the funder's batches form one ordered stream on one key. DeployScenario returns an error instead of panicking.

Tested

Mock JSON-RPC chain tracking per-sender nonces. Funding + contract scenarios — the case no profile exercises — occupies nonces 7–11 gapless. Status-0 deploy and missing-key both error. Each test fails against the defect.

Review fixes

  • A deploy that never mined exited 0 — its 30s timeout aliased main's run-duration filter. Now requires the run context to have expired.
  • The funder's wait was unbounded while the deploy half was bounded; measured blocked at 45s.
  • Deploy priced at 20 gwei against the funder's 100, heading the shared nonce stream at its weakest price.
  • FundAccounts re-read the key file rather than taking the resolved root.
  • MockDeploy now gates funding alongside DryRun.

Verifier

gofmt, go vet, golangci-lint 0 issues, go test -race ./... all pass. make verify fails only at check-bindings, which needs a linux-static solc absent on macOS; no .sol is in the diff and CI passes that gate.

For review

The treasury root now signs scenario contract creations and owns them. No new capability — it already deploys Disperse — and no load path reads that ownership, though Disperse.owner gates three setters. Flagging because it is the highest-value key in the system.

An inherited Sei assumption now covers a different transaction. The ante handler auto-associates a sender on its first EVM transaction; that is now a scenario deploy. A sei-chain reviewer should confirm. If it does not hold, the deploy fails at admission before any spend.

NewGenerator deployed every contract-backed scenario from a key it minted
itself. That key held no balance and sat in no account pool, so funding never
reached it and any chain that charges for gas rejected the deployment. The
deploy nonce came from the scenario's index, correct only for a key that has
never sent a transaction, and every failure path in DeployScenario panicked.

The generator now receives its deployer. funder.Deployer resolves it: the
funding root when funding is configured, otherwise a fresh key, which only a
chain that credits unknown senders can pay for. Mock deploy never reads the
root key, so a dry run still works with no key mounted.

Each deployment leaves its nonce unset, so go-ethereum reads the deployer's
pending nonce from the chain, and deployAll waits for the receipt before it
sends the next one. The scenario deployments and the funder's own Disperse
deployment and disperseEther batches therefore stay one ordered nonce stream on
one key. Deployment failures return errors and fail the run.

Tests cover the funding + contract-scenario intersection that no committed
profile exercises: a deployer starting at a non-zero nonce, the single nonce
stream shared with the funder, and a reverted deployment surfacing as an error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bdchatham
bdchatham marked this pull request as ready for review August 20, 2026 03:22
@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes which key signs scenario contract creations (the funding root), how root secrets are resolved, and EVM nonce ordering on that key—security- and funds-sensitive startup paths.

Overview
Contract deployer is no longer minted inside the generator. main resolves funder.Deployer(cfg) first and passes that account into NewGenerator. With funding (and not mock deploy), that is the same root key that deploys Disperse and funds the pool; without funding or under mock deploy, it is a fresh random key and the root file is not read.

Deployments use live chain nonces (instance index removed): sequential deploys wait for receipts, and FundAccounts takes the resolved root instead of re-reading the key. Scenario deploys return errors instead of panicking; deploy and funder receipt waits get 30s caps, with deploy timeouts formatted so they are not mistaken for run shutdown. Deploy gas fee cap is raised to 100 gwei to match funding on the shared nonce stream. Funding is skipped when MockDeploy is set (alongside dry-run).

Package docs and tests (mock JSON-RPC chain, funded multi-contract nonce ordering) document the single-nonce-stream behavior on the root key.

Reviewed by Cursor Bugbot for commit 9e645a7. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread generator/scenarios/base.go
…s one root

Four fixes from review, each measured rather than argued.

A deploy that never mined exited 0. Converting the deploy path from panic to
error routed the new 30s deployTimeout into main's shutdown filter, which nulled
any context.DeadlineExceeded so the run-duration timeout could exit cleanly. The
filter now requires the run context itself to have expired, so a deadline raised
anywhere else surfaces. endedOnRunContext carries the rule and its truth table is
tested; the test fails against the sentinel-only form.

The funder's WaitMined was unbounded while the deploy half was bounded, and the
deploy comment claimed nothing else bounds startup. Against a chain that accepts
a transaction and never mines it, funding was still blocked at 45s — and a
profile need not set a run duration, so the pod stays alive and silent with no
load offered. Both halves are now bounded the same way.

A contract creation was priced at the load-transaction cap of 20 gwei while the
funder used 100. With one shared key the deploy is the head of the root's nonce
stream, so its weakest price blocked every later root transaction once the base
fee rose past it. Both now use the same cap.

FundAccounts takes the resolved root instead of re-reading the key file, so the
deploy and funding phases cannot disagree about which identity they are — the
same two-identities-where-one-is-assumed bug this change exists to fix.

MockDeploy now gates funding alongside DryRun. They expressed one intent through
two predicates, so a profile setting mockDeploy with funding and no --dry-run
funded a pool that then transacted against code-less addresses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread main.go
The previous commit made main's shutdown filter require the run context to have
expired, so a deploy timeout would surface rather than exit 0. That broke the
case the filter exists for: cobra runs on an uncancelled context and the signal
handler reads off a channel, so a signalled run leaves ctx untouched and the
context error arrives from a background task scope cancelled on the way out.
Every normal SIGTERM would have reported failure.

Fixed at the source instead. The filter goes back to matching the sentinels,
which is what a signalled run actually produces, and DeployScenario no longer
lets its own budget escape as one: an expiry of the deploy timeout is reported
without a context sentinel in the chain, while a sentinel from the caller's
context passes through because that really is a shutdown.

The first attempt wrapped only the WaitMined call. A test against a blackhole
endpoint showed the budget also escapes through the send path — every %w in the
function inherits it — so the conversion moved to the function boundary.

Covered by TestEndedOnRunContext, which pins a background-task cancellation with
a live outer context as a clean exit, and TestDeployTimeoutIsNotAContextSentinel,
which asserts the deploy error carries no sentinel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9e645a7. Configure here.

Comment thread main.go
…deploy one

The previous commit stopped the deploy budget escaping as a context sentinel,
because main reads those as a clean exit so a signalled or duration-bounded run
reports success. It left the identical alias in the funder's own wait, which the
same commit had only just given a timeout: a Disperse deploy or disperseEther
batch that never mined would have exited 0 as a run that funded nothing.

Two sites needing one contract, so the contract is now a helper.
utils.WithinBudget runs work under a deadline of its own and reports an expiry of
that deadline without a sentinel in the chain, while passing a sentinel from the
parent context through untouched. Both the deploy and the funding wait route
through it, so a third caller inherits the rule rather than rediscovering it.

Tested at the helper: a budget expiry carries no sentinel and names the budget, a
parent cancellation still matches context.Canceled, and any other error keeps its
chain. Verified the first fails when the stripping is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One conflict, in generator/scenarios/StorageRW_test.go, where both sides added to
the same file: PLT-465 brought the contention sweep, size-bucket, op-mix, gas-floor
and draw-order tests plus their helpers, and this branch added the deploy-budget
sentinel test. Neither touches the other, so the resolution is the union of both
plus the context import the sentinel assertion needs.

main.go auto-merged cleanly and endedOnRunContext survived intact.

All ten scenario tests pass, including PLT-465's gas-floor sweep against the
EIP-7623 floor and this branch's assertion that a deploy budget carries no
context sentinel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant