Skip to content

committee-controlled contract upgrades (WIP — review the architecture, scenarios still need mechanical updates) - #121

Draft
zjma wants to merge 1 commit into
mainfrom
committee-controlled-upgrades
Draft

committee-controlled contract upgrades (WIP — review the architecture, scenarios still need mechanical updates)#121
zjma wants to merge 1 commit into
mainfrom
committee-controlled-upgrades

Conversation

@zjma

@zjma zjma commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • @ace becomes a resource account whose SignerCapability lives inside network::SignerStore. Admin's private key has no signing power for @ace after bootstrap.
  • New sealed bootstrap flow: 0x1::resource_account::create_resource_account (admin pubkey as auth_key) → aptos move publish --sender-account X × 11 → network::start_initial_epoch(x, admin_addr, ...) which atomically burns admin's signing path and installs the cap.
  • New upgrade-voting flow: network::new_upgrade_proposal (committee members + admin EOA can submit; admin is non-voting) → existing voting:: framework → network::touch() executes code::publish_package_txn via the SignerCapability once threshold is reached.

Design choices worth your eyes

  1. State representationUpgradeBlob (bytecode + metadata) lives at sticky-object addresses, not inside State. State.upgrade_proposals: vector<Option<UpgradeProposalRef>> only stores pointers. This avoids bloating the per-node state_view_*_bcs fetch with 1–2 MB of bytecode.
  2. n+1 slot model — parallel to existing epoch-config proposals. Each node + admin gets one upgrade slot. Multiple competing upgrades can be in flight; on the first approved one, touch() clears every slot.
  3. StateViewV0 unchanged — the v0 view fn still returns exactly the original BCS layout, so the TS SDK + Rust worker mirrors keep working without changes. Added a new state_view_v1_bcs for future consumers who want upgrade visibility (no consumer in this PR).
  4. current_epoch view — added a tiny #[view] public fun current_epoch(): u64 so the CLI doesn't have to parse full state just to fill in target_epoch.
  5. Sealed-bootstrap window analysis — I previously worried about the window between publish docker build workflow #11 and start_initial_epoch. You called this out as misplaced threat modeling (correct): during bootstrap, admin's key is root of trust by necessity. The "window" doesn't matter; what matters is that after bootstrap the key has zero power. So no atomic script-tx merge.

Known incomplete in this PR

~16 individual CI scenario files still call deployContracts / start_initial_epoch with the OLD signatures. They typecheck (because submitTxn.args is any[]) but will runtime-fail in CI until each is updated with the mechanical pattern:

- const aceContract = adminAccount.accountAddress.toStringLong();
+ const aceContract = await deployContracts(adminAccount, [...packages]);

and

  (await submitTxn({
      signer: adminAccount,
+     sender: aceContract,
      entryFunction: `${aceContract}::network::start_initial_epoch`,
-     args: [<nodes>, threshold, dur],
+     args: [adminAccount.accountAddress.toStringLong(), <nodes>, threshold, dur],
  }))

Affected files: test-solana-example.ts, large-committee.ts, test-fault-tolerance.ts, test-custom-flow-aptos.ts, full-happy-path.ts, test-network-protocol-shortpk.ts, test-auto-epoch-change.ts, test-network-protocol.ts, test-custom-flow-solana.ts, cli-testbed.ts, plus the dkg/dkr/vss protocol tests if they drive a full network.

Already updated (and sufficient for local end-to-end testing):

  • scenarios/common/helpers.ts — sealed bootstrap helpers
  • scenarios/common/ace-network.ts — driver for access-failure scenarios
  • scenarios/run-local-network-forever.ts — the canonical local-test scenario
  • CLI: deploy-contracts.ts, commands/deployment-new.ts, commands/update-contracts.ts, config.ts

Test plan

  • Local: pnpm run-local-network-forever brings up a committee, epoch 0 starts, secrets get generated. Verifies the resource-account bootstrap end-to-end.
  • Local: ace deployment new wizard creates a resource-account deployment on localnet/devnet, seals admin, prints the operator blob with aceAddradminAddr.
  • Local: after deployment, ace deployment update-contracts --version X.Y.Z submits 11 new_upgrade_proposal txns; ace proposal ls shows the open voting sessions; committee members can vote with ace vote <session_addr>.
  • Move unit tests: aptos move test in contracts/{group,vss,...} — pass (verified).
  • Contract compiles: aptos move compile for contracts/network — pass (verified).
  • CLI typecheck: npx tsc --noEmit in cli/ — pass (verified).
  • Scenarios typecheck: npx tsc --noEmit in scenarios/ — pass (verified, but per the "Known incomplete" note above, runtime semantics are wrong for the unmigrated scenarios).
  • Remaining ~16 scenario files need the mechanical pattern update before CI is green.

🤖 Generated with Claude Code

…oting

Contract upgrades are no longer signed by a single admin private key. The @ace
address becomes an Aptos resource account whose SignerCapability lives inside
network::SignerStore; the only path to producing a signer for @ace is through
the network module, gated by the existing voting framework.

## Sealed bootstrap

1. Phase A: admin EOA calls 0x1::resource_account::create_resource_account with
   optional_auth_key = admin's auth_key → resource account materialized at
   X = createResourceAddress(admin, seed), signable by admin's SK.
2. Phase B: admin runs `aptos move publish --sender-account X` 11 times,
   landing all packages under X.
3. Phase C: admin calls network::start_initial_epoch(x, admin_addr, ...). This
   retrieves the SignerCapability via 0x1::resource_account::retrieve_resource_account_cap
   (which atomically burns X's auth_key to zero), stores it in SignerStore,
   and builds the initial State. After this tx, admin's key has no signing
   power for @ace.

## Upgrade flow

- network::new_upgrade_proposal(proposer, package_name, metadata, code, ...)
  is callable by current committee members + admin EOA. Admin retains a
  non-voting proposer slot (parallel to ProposedEpochConfig slot model).
- State.upgrade_proposals (n+1 slots) point at separately-stored UpgradeBlob
  objects holding the compiled bytecode (kept out of the State BCS view to
  avoid bloating the per-node state fetch).
- network::touch() iterates upgrade_proposals, touches their voting sessions,
  and on the first approved proposal calls code::publish_package_txn with a
  signer minted from SignerStore. All upgrade slots are cleared on execute
  so losers can re-propose.

## Components touched

- contracts/network: SignerStore.extend_ref:ExtendRef → signer_cap:SignerCapability;
  start_initial_epoch refactored; UpgradeBlob/UpgradeProposalRef/UpgradeProposalView
  + StateViewV1 + new view fns (current_epoch, state_view_v1_bcs); new_upgrade_proposal
  entry; try_execute_upgrade in touch(). StateViewV0 unchanged → SDK/worker BCS
  mirrors stay compatible with no V1 migration needed.
- cli/deploy-contracts.ts: createAceResourceAccount + deriveAceAddr; publishMovePackage
  threads --sender-account; deployContracts now creates the resource account first
  and returns the resulting aceAddr.
- cli/commands/deployment-new.ts: seed prompt with sensible default
  (ace-${network}-${tag}); start_initial_epoch call updated to sender=aceAddr +
  admin_addr arg; profile persists bootstrapSeed + aceAddr.
- cli/commands/update-contracts.ts: rewritten. No longer publishes; instead
  builds each package via `aptos move build-publish-payload`, parses the
  resulting JSON, and submits new_upgrade_proposal as the admin EOA. Prints
  next-step instructions for committee voting.
- cli/config.ts: TrackedDeployment gains bootstrapSeed: string.
- scenarios/common/helpers.ts: createAceResourceAccount helper; deployContracts
  returns the resource account address; submitTxn gains optional sender override.
- scenarios/common/ace-network.ts: AceNetworkOptions gains aceAddr; sealed
  start_initial_epoch call.
- scenarios/run-local-network-forever.ts: switched to the new flow as the
  canonical local-test entry point.

## Known incomplete (follow-up commits in this PR)

~16 individual CI scenarios still call deployContracts/start_initial_epoch with
the old signatures. They typecheck (submitTxn.args is any[]) but will runtime-fail
in CI until the same mechanical pattern is applied:
  const aceContract = adminAccount.accountAddress.toStringLong();  →
  const aceContract = await deployContracts(adminAccount, [...packages]);
  + add adminAddr as the first arg to start_initial_epoch
  + add sender: aceContract to its submitTxn call.
Local testing via `pnpm run-local-network-forever` or `ace deployment new` works.

Co-Authored-By: Claude Opus 4.7 (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