Skip to content

feat: Add rollups cli withdraw / refund / foreclose - #545

Merged
brunomenezes merged 25 commits into
prerelease/v2-alphafrom
feat/add-rollups-cli-withdraw-refund
Oct 7, 2026
Merged

brunomenezes merged 25 commits into
prerelease/v2-alphafrom
feat/add-rollups-cli-withdraw-refund

Conversation

@brunomenezes

@brunomenezes brunomenezes commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds cartesi foreclose, cartesi refund and cartesi withdraw to recover funds from a foreclosed application in the local environment. They run rollups-node's cartesi-rollups-cli and cartesi-rollups-machine-tool inside the rollups_node container, so no new images are needed.

How it works

cartesi foreclose
  → resolve-signer            (node signer, or CARTESI_AUTH_MNEMONIC / CARTESI_AUTH_PRIVATE_KEY in the shell)
  → find-guardian-index       (first 20 accounts of the mnemonic, or --account-index)
  → cartesi-rollups-cli foreclose

cartesi refund <input-index>
  → get-input-data            (from the node, or --input-file)
  → write-temp-file           (inside the node container)
  → cartesi-rollups-cli refund
  → remove-temp-file

cartesi withdraw --account <address>
  → read-foreclosure-state    (isForeclosed, drive root proven?, last finalized machine root)
  → wait-for-finalized-epoch  (until the node has the epoch with the chain's root)
  → replay-last-finalized-epoch   ♻ cached per application and epoch
  → generate-account-proofs
  → prove-drive-root              ♻ once per application
  → cartesi-rollups-cli withdraw

cartesi withdraw --proof-file <file>
  → check drive root proven → copy proof into the container → cartesi-rollups-cli withdraw

The replay is the slow step. It's cached in the node container's /tmp, and the drive root is proven only once, so every withdrawal after the first one is just a proof and one transaction.

Warning

The replay re-runs every accepted input of the application, so a long-running application with many inputs takes longer to replay. The cached snapshot is about the size of the machine RAM and lives in the node container's /tmp, so it's limited by the container's memory and disk (see cartesi run --memory). It's lost when the environment is recreated, and the next withdrawal replays again.

Features

  • cartesi foreclose signs as the guardian of [withdrawal.config]. A guardian outside the node's signer is set for that one command with CARTESI_AUTH_MNEMONIC or CARTESI_AUTH_PRIVATE_KEY, and CARTESI_AUTH_KIND when both are set. The values are forwarded by name, never in argv. A private key's address is checked against the guardian before anything is sent.
  • cartesi refund <input-index> refunds a deposit that wasn't finalized before the foreclosure.
  • cartesi withdraw withdraws an account's finalized balance with --account, or with a proof generated elsewhere with --proof-file.
  • Shared options: --application, --project-name, --account-index, --yes, --json, --no-wait and --wait-timeout. The application is resolved from the running environment when --application isn't given.
  • Replay safety.
    • The target root is read from the chain, not from the node.
    • A replay is checked against that root before it's cached.
    • A proof whose root doesn't match removes the cached snapshot, so the next run replays it.

Refactoring

  • exec/rollups.ts is split into exec/node-container.ts and exec/cartesi-rollups-machine-tool.ts. The existing node calls (getDeployments, deployAuthority, deployApplication, removeApplication, getAnvilNodeInfo) now go through the same execNodeCommand helper, with no behavior change.
  • The node state (the application's withdrawal config, input bytes, the last accepted epoch) is read with cartesi-rollups-cli app list and read inside the container, so the CLI never queries the node's JSON-RPC from the host and adds no new dependency.
  • The command argument parsers are grouped in base.ts. DEVNET_MNEMONIC is exported from compose/node.ts, and findMnemonicAccountIndex is added to wallet.ts.

CI and tooling

  • The cli workflow sets up Node 24 (actions/setup-node v7.0.0), because the integration tests run the CLI with node.
  • rollups-explorer is bumped to 2.0.0-alpha.4.
  • turbo.json sets agentGuidance: false, which stops turbo from writing AGENTS.md.

Verification

Full suite against the released 0.12.0-alpha.44 images, on Node 24: 325 pass / 1 skip / 0 fail. 90 of the tests are new, and the skip is pre-existing. tsc and biome ci are clean.

recovery.test.ts runs the whole flow on the devnet with an ERC-20 withdrawal application:

  • Foreclosure: done by a guardian from a separate mnemonic.
  • Refunds: two, one with the input read from the node and one with --input-file.
  • Withdrawals:
    • the first replays the epoch and proves the drive root (2 transactions);
    • the second reuses the cached replay (1 transaction);
    • the third uses --proof-file.
  • Balances: each account's balance is checked as 0 before and as the full amount after.
  • Error paths: the expected errors before the foreclosure, and on an account that isn't in the drive.

@brunomenezes brunomenezes self-assigned this Oct 6, 2026
@brunomenezes brunomenezes added cli github_actions Pull requests that update GitHub Actions code labels Oct 6, 2026
@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3cc85bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cartesi/cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 90.65% (🎯 0%) 11868 / 13092
🔵 Statements 90.65% 11868 / 13092
🔵 Functions 79.34% 242 / 305
🔵 Branches 0% 0 / 0
📁 File Coverage (20 files)
File Lines Statements Functions Branches Uncovered Lines
apps/cli/src/base.ts 🔴 39.33% 🔴 39.33% 🟡 64.71% 🔴 0% 49, 70-71, 79-83, 88, 102, ...
apps/cli/src/builder/directory.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/builder/docker.ts 🟢 86.72% 🟢 86.72% 🟡 66.67% 🔴 0% 75-77, 79, 109-111, 169-178
apps/cli/src/builder/empty.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/builder/none.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/builder/nvram.ts 🟢 96.88% 🟢 96.88% 🟢 100% 🔴 0% 27
apps/cli/src/builder/tar.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/commands/foreclose.ts 🔴 49.09% 🔴 49.09% 🔴 50% 🔴 0% 69, 91-145
apps/cli/src/commands/refund.ts 🔴 38.2% 🔴 38.2% 🟡 66.67% 🔴 0% 55-109
apps/cli/src/commands/withdraw.ts 🔴 46.58% 🔴 46.58% 🟡 73.33% 🔴 0% 95-123, 277-278, 301-356, 3...
apps/cli/src/compose/anvil.ts 🟡 79.25% 🟡 79.25% 🟢 100% 🔴 0% 19-29
apps/cli/src/compose/builder.ts 🟢 99.79% 🟢 99.79% 🟢 100% 🔴 0% 228
apps/cli/src/compose/bundler.ts 🔴 4.82% 🔴 4.82% 🔴 0% 🔴 0% 8-40, 44-75, 79-92
apps/cli/src/compose/common.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/database.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/explorer.ts 🔴 6.67% 🔴 6.67% 🔴 0% 🔴 0% 10-38, 43-55, 59-72
apps/cli/src/compose/node.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/passkey.ts 🔴 8.33% 🔴 8.33% 🔴 0% 🔴 0% 9-18, 23-42, 46-59
apps/cli/src/compose/paymaster.ts 🔴 7.69% 🔴 7.69% 🔴 0% 🔴 0% 8-21, 25-44, 48-61
apps/cli/src/compose/proxy.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -

@tuler

tuler commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

First of all I appreciate the effort to bring the EW features to the Cartesi CLI, because IMHO the EW feature is very complex for the application developers to implement (and for the operator to operate, which is not a concern here).

I posted a proposal some time ago on #516 for how emergency withdrawal should work in the Cartesi CLI, focused on the application developer: #516 (comment)

In short:

  • the accounts drive declared in cartesi.toml becomes the entry point;
  • the CLI derives the rest of the withdrawal config from the built machine, or uses devnet defaults;
  • [withdrawal] only holds optional overrides.

I think it also affects the scope of this PR. I thought the rollups-node team was still planning to make changes to the EW feature, by adding an API for it, and potentially a simplification. And I don't think we have an urge right now for these commands. Providing a good developer experience is IMO more important than having the commands.

In any case, about the implementation I was expecting that the foreclose, withdraw and refund commands would be thinner wrappers, targeted specifically for the devnet environment, which could bring more simplification. Things like deciding who signs foreclose for example could be much simpler than the current 550 lines (code+test) if we had devnet assumptions.

There are also some design issues that I was trying to avoid, like for example using the node JSON-RPC from the host, mixing that with node CLI calls in the container.

So my current suggestions are:

  • hold these commands until the node's EW feature improve, then add very thin wrappers that run the node commands inside the container, the same way we do for deploy, with devnet defaults and minimum customization
  • if developers need this before then, a generic passthrough (e.g. cartesi rollups-cli <args...>, which runs cartesi-rollups-cli in the rollups_node container) plus a docs recipe would cover it with very little code, as you suggested before.

Please let me know if there are other motivations that I'm not aware of to add this now.

@brunomenezes

Copy link
Copy Markdown
Member Author

Please let me know if there are other motivations that I'm not aware of to add this now.

Thanks @tuler, I've being following the issue and read the proposal. In a few places the PR is being read as doing more than it does, so here's a clarification point by point.

These are already thin wrappers around the node's tools. The CLI doesn't replay or generate proofs itself. Every step runs cartesi-rollups-machine-tool or cartesi-rollups-cli inside the rollups_node container, the same way deploy does; this PR also moves deploy and the other existing calls onto that same helper. The flags (--yes, --json, --no-wait, --wait-timeout, --proof-file, --account-index) are passed through to cartesi-rollups-cli. All the CLI adds is the sequencing:

cartesi withdraw --account <address>
  → machine-tool replay                (last finalized epoch, kept for the next account)
  → machine-tool prove accounts-drive
  → rollups-cli prove-drive-root       (once per application)
  → rollups-cli withdraw

cartesi refund <input-index>
  → read the input bytes → rollups-cli refund

the sequencing is the DX. I agree that a good developer experience matters more than having the commands, and here the automation of the sequencing is that experience. With alpha.13, which the CLI ships today, cartesi run already deploys an application with [withdrawal.config]. Exercising it, though, means running the replay, the account proofs, prove-drive-root and withdraw by hand inside the container, and carrying the epoch index, store paths and proof files from one step to the next. As an example the rollups-explorer can't withdraw today. I'm a developer on the receiving end of this as well: building tools like rollups-explorer, I need to run foreclose → refund → withdraw again and again, and one command per action is what makes that repeatable. It's also phase 3 of your proposal, with the same command names. When the node ships its EW API improvement, I assume only the internals change, not the commands or flags unless these have some sort of overhaul (which I don't expect to happen). However, I am working with what is available today.

#516 is orthogonal to this PR. The commands read the withdrawal config from the deployed application, not from cartesi.toml. They work the same whether the config is written by hand (today) or derived from the accounts drive. I agree with the derivation direction, and I'm experimenting on it in a separate branch, based on the machine config available after cartesi build.

Foreclose signer. The default path is the devnet one: it signs with the node signer and finds the guardian's index in the node's mnemonic, so a devnet guardian needs no setup. A guardian outside the node signer is a real development case too. I don't use the devnet accounts myself, but my own dev accounts that I fund during development. Keeping the guardian separate from the node signer also tests the roles as they are in production, which is the point of your open question 4 on #516. Setting CARTESI_AUTH_MNEMONIC or CARTESI_AUTH_PRIVATE_KEY for that one command covers it, and nothing changes for developers who don't need it.

Node JSON-RPC from the host. I agree, done in 3cc85bd. The three reads (application, input, epoch) now go through cartesi-rollups-cli app list and read inside the container, which keeps the versions matched and drops @cartesi/client. The only reads left on the host are chain reads through anvil, the same way send and deposit work.

Passthrough plus a docs recipe. A cartesi rollups-cli <args...> passthrough is useful on its own, but the recipe would still be those four manual steps.

@brunomenezes
brunomenezes merged commit 3ad1ba3 into prerelease/v2-alpha Oct 7, 2026
4 checks passed
@brunomenezes
brunomenezes deleted the feat/add-rollups-cli-withdraw-refund branch October 7, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli github_actions Pull requests that update GitHub Actions code

Projects

Status: 📦 Done

Development

Successfully merging this pull request may close these issues.

2 participants