diff --git a/Cargo.lock b/Cargo.lock index 7ff8e9c4..1f04e576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,9 +1461,11 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "app-core", + "ethereum_ssz", "k256", "rollups-types", "sequencer-core", + "tempfile", "trolley", ] @@ -3854,11 +3856,13 @@ dependencies = [ name = "rollups-e2e" version = "0.1.0" dependencies = [ + "alloy", "alloy-primitives", "alloy-sol-types", "app-core", "c-app-engine", "c-wallet-engine", + "cartesi-rollups-contracts", "ethereum_ssz", "futures", "libtest-mimic", @@ -4291,6 +4295,7 @@ dependencies = [ "tokio-util", "toml", "tower-http", + "tower-service", "tracing", "url", ] diff --git a/README.md b/README.md index d60fff13..52e71c87 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,19 @@ When batches risk becoming stale on L1, the sequencer stops serving and startup determines the required repair. A lost or untrustworthy local state instead requires an operator rebuild. Both recovery modes are described below. +The current signing/batch format uses domain version 2 and requires +`valid_until_block`. It requires a new canonical image and fresh setup; old +signatures, pending batches, and baseline databases are incompatible. + ## Trust Model -The sequencer is a **centralized, single-writer** system. It cannot steal funds or forge invalid state — the rollup validates everything independently, and the proof system later enforces it. But the sequencer can: +The sequencer is a **centralized, single-writer** system. The rollup independently validates canonical execution, but fund availability after foreclosure requires an [application-specific exit](docs/protocol/application-contract.md#8-foreclosure-and-emergency-exits), demonstrated by the reference wallet and required of each production app. The sequencer can: - **Censor** — refuse to include a user's operations. - **Go offline** — stop providing soft confirmations. - **Diverge** — if batches fail to land on L1 in time, soft confirmations that were issued become invalid. -**Direct inputs** (L1 → L2 messages, used for deposits) bypass the sequencer entirely. They are posted directly to L1 and are **uncensorable** by the sequencer — the scheduler drains them at every `safe_block` boundary. A censoring sequencer can delay when a direct input is executed (up to `MAX_WAIT_BLOCKS`, ~4h), but cannot prevent it. +**Direct inputs** (L1 → L2 messages, used for deposits) bypass the sequencer entirely. They are posted directly to L1 and are **uncensorable** by the sequencer — the scheduler drains them at every `safe_block` boundary. A censoring sequencer can delay when a direct input is executed (up to `MAX_WAIT_BLOCKS`, ~4h), but cannot prevent it while later L1 inputs continue to drive the scheduler. During normal operation the sequencer advances logical frame time after five newly-safe blocks have accumulated. That clock tick drains every covered direct @@ -155,12 +159,17 @@ environment: CARTESI_SEQUENCER_ALLOW_INSECURE_RPC: "true" ``` +Startup omits the RPC URL, plaintext opt-in warnings show only its origin, and +request transport errors strip reqwest's attached URL before retry wrappers +format it. RPC response bodies and dependency debug/trace logging can still +contain server-supplied secrets; keep diagnostic logs private. + Process exit codes follow the orchestrator exit-code contract: `0` clean shutdown, `10` restart (expect a recovery boot), `20` transient refusal (retry with backoff), `30` terminal command refusal (operator required — e.g. setup not complete, identity mismatch, canonical divergence, persistent storage/application invariant failure), `40` a previous instance left work past the checkpoint (wipe the data directory and run `setup --recovery`), and `1` for an unclassified operational failure. Diagnosed terminal runtime faults, including worker panics, immediately call `abort()` (SIGABRT, status 134), without worker drain or database settlement. Supervisors must treat SIGABRT as terminal class. Ordinary shutdown, expected recovery, and transient errors still drain gracefully. Startup panics caught by the command harness are projected to `30`; panics after runtime scope creation abort; `101` remains possible before the command harness starts. The constants live in `sequencer/src/commands/error.rs`; supervisor recipes are in [`docs/watchdog/operator-deployment.md`](docs/watchdog/operator-deployment.md). Fixed protocol identity (EIP-712): - domain name: `CartesiAppSequencer` -- domain version: `1` +- domain version: `2` - `chain_id` and `verifying_contract` come from `CARTESI_SEQUENCER_BLOCKCHAIN_ID` and `CARTESI_SEQUENCER_APP_ADDRESS` Most queue sizes, polling intervals, and safety limits are now internal runtime constants instead of public launch-time configuration. @@ -195,7 +204,8 @@ Request shape: { "message": { "nonce": 0, - "max_fee": 1, + "max_fee": 1418, + "valid_until_block": 1120, "data": "0x..." }, "signature": "0x...", @@ -226,10 +236,10 @@ Success response after inclusion: ### `GET /fee` -Fee quote for setting signed user-op `max_fee` before `POST /tx`. All three fields are log-space exponents (base 129/128), the same encoding as `max_fee`. Inclusion rejects any op with `max_fee` below the open-frame `fee`. +Fee quote for setting signed user-op `max_fee` before `POST /tx`. The three fee fields are log-space exponents (base 129/128), the same encoding as `max_fee`. Inclusion rejects any op with `max_fee` below the open-frame `fee`. ```json -{ "fee": 1356, "recommended_fee": 1356, "suggested_max_fee": 1409 } +{ "fee": 1365, "recommended_fee": 1365, "suggested_max_fee": 1418, "safe_block": 1000 } ``` Notes: @@ -240,6 +250,13 @@ Notes: - `200` while an open frame exists (the admitted runtime always has one). Responses carry `Cache-Control: no-store`. - `503` with code `UNAVAILABLE` during shutdown, or if no open frame exists. +Choose `UserOp.valid_until_block` from `safe_block` plus an explicit signing +lifetime (for example 120 blocks). The inclusive deadline uses the frame clock, +not wall time or L1 inclusion time. Expiry consumes neither nonce nor fee. A +delayed batch retains its frame clock, while scheduler staleness independently +bounds its inclusion delay. `u64::MAX` explicitly chooses no practical expiry; +it is not a default. Price/slippage constraints belong in app method payloads. + ### `GET /nonce?sender=
` The nonce to sign into `sender`'s next user op. @@ -253,6 +270,7 @@ Notes: - `sender` is parsed like the `POST /tx` field: `0x` plus 40 hex digits in any case, echoed in EIP-55 casing. A missing or malformed parameter is `400` with code `BAD_REQUEST`. - `next_nonce` is one past the sender's latest included op, or 0 if it has none. It counts soft-confirmed ops: an op acknowledged with `200` before this request is counted. (The `nonce` in a `POST /tx` response or WS message is instead the nonce that op consumed.) - Keep one op in flight per sender: sign `next_nonce`, submit, and increment on `200`. Concurrent submits from one sender are unsupported; they can reach the sequencer out of order and be rejected. After a submit times out, a `next_nonce` above the op's nonce means it was included; an unchanged one is inconclusive, since the op may still be queued, so resubmit the same signed op rather than signing a new one at that nonce. +- Recovery can rewind the nonce. A revoked confirmation does not revoke its signature; see [signature validity after recovery](docs/recovery/README.md#revoked-confirmations-and-signed-operations) before replacing an operation. - `next_nonce` of 4294967295 (`u32::MAX`) means the account is exhausted: that nonce has no successor, so `POST /tx` can include no further op from it. - The value is a hint, not a reservation; the application is authoritative. It can go down, because a restart that runs automatic recovery may invalidate soft-confirmed ops. After a `422` bad-nonce rejection, query again rather than incrementing. - After an operator rebuild from a checkpoint (`setup --recovery`), a sender with no surviving op since the rebuild reads 0 even when its nonce in the rebuilt baseline is higher; this includes a sender whose post-rebuild ops a later recovery invalidated. A `422` bad-nonce rejection names the expected nonce (`bad nonce: expected N, got M`). @@ -263,7 +281,7 @@ Notes: The EIP-712 domain `POST /tx` verifies signatures against, keyed as `eth_signTypedData_v4` expects: ```json -{ "name": "CartesiAppSequencer", "version": "1", "chainId": 31337, "verifyingContract": "0x..." } +{ "name": "CartesiAppSequencer", "version": "2", "chainId": 31337, "verifyingContract": "0x..." } ``` Clients pin their own domain and assert that it matches this one, the way a wallet checks `eth_chainId`. `chainId` is a JSON number, exact in JavaScript below 2^53. That covers every chain browser wallets accept (MetaMask refuses IDs above 4503599627370476); a larger ID fails closed, because a signature over a rounded `chainId` recovers a different sender at `POST /tx`. Do not sign with a served domain unchecked: `chainId` and `verifyingContract` are what keep a signature from replaying on another deployment, and the name and version are the same everywhere. diff --git a/docs/cartesi-machine.md b/docs/cartesi-machine.md index 288dbebf..44e51b66 100644 --- a/docs/cartesi-machine.md +++ b/docs/cartesi-machine.md @@ -15,7 +15,8 @@ identifiers; loading rejects any mismatch. A CM bump therefore invalidates every stored image, release tarball, and watchdog checkpoint: rebuild the canonical images and re-`init` watchdog state directories. The kernel (`machine-linux-image`) and guest tools (`machine-guest-tools`) are paired with -the emulator release and move with it. The development shell pins the same +the emulator release and move with it. The kernel download must match the +SHA-512 pinned in `toolchain-pins.env`; the release manifest records that pin. The development shell pins the same emulator through the parent flake. **A deployed application is pinned to its emulator version.** Its on-chain @@ -52,6 +53,13 @@ Any other host, including the watchdog, must match it exactly: A fixed point is permanent: once the canonical machine reaches one, no later input is ever processed for that application. +## Merkle proof geometry + +In v0.21, `get_proof` returns `sibling_hashes` from leaf to root, matching the +rollups-contracts proof arrays. Account paths stop at the accounts-drive root; +drive paths continue to the machine's 64-bit root. The machine uses 32-byte +leaves. Keep the proof exporter pinned to these rules when changing versions. + ## Storage, sharing, and snapshots - **Loading** defaults to `SHARING_NONE`: every backing file is mapped diff --git a/docs/protocol/application-contract.md b/docs/protocol/application-contract.md index 7776b9e3..f61431bd 100644 --- a/docs/protocol/application-contract.md +++ b/docs/protocol/application-contract.md @@ -25,7 +25,7 @@ a reference wallet integration. | `progress() -> ApplicationProgress` | Return the count/clock pair from the engine by value. | Execution callers use `validate_and_execute_user_op`, `execute_valid_user_op`, -and `execute_direct_input`. The first enforces `max_fee >= current_fee` before +and `execute_direct_input`. The first enforces the inclusive signed block deadline and `max_fee >= current_fee` before app validation and rejects an accepted op at the exhausted nonce `u32::MAX` after it. App validation checks the nonce and fee balance; a rejection is not persisted. Trusted replay @@ -269,3 +269,43 @@ Canonical extraction belongs to application-specific recovery tooling. The runtime `Application` trait stays independent of machine image layouts and host export tooling; requiring a method to compile would establish its availability, not the correctness or operational readiness of the recovery path. + +### 8. Foreclosure and emergency exits + +Canonical state integrity does not establish a withdrawal path after foreclosure. +Each custody application supplies a canonical accounts drive and withdrawal +builder compatible with its deployed contracts, covering credited balances and +finalized but unexecuted deposits. Contract refunds cover only unfinalized inputs; +the scheduler's pending-direct queue is custody even before the app credits it. +Stopping inputs also stops the input-driven age backstop. Recovery checkpoints +and comparison files do not by themselves constitute an exit proof. + +The reference wallet publishes 64-byte records (32-byte ABI address followed by +a full 256-bit big-endian balance), sorted by address, in the `accounts` raw flash +drive before each accepted guest yield. Its entitlement is the current balance +plus valid supported-token deposits still queued by the scheduler. Draining moves +a deposit between those terms, preserving the sum; normal withdrawals debit the +balance before producing their independently executable voucher. + +The example drive starts at `0x90000000000000`, has length 64 MiB, and holds up to +2^20 nonzero accounts. Unused records are zero, and publication clears vacated +slots and flushes guest writes. This is a bounded reference integration, subject +to the wallet's guest-memory/compute capacity; it is not a capacity promise for a +production DeFi engine. A production integration must size and test its own state +layout and supported workload before custody. + +Deploy with the matching image and the [wallet withdrawal builder](../../examples/app-core/contracts/WalletWithdrawalOutputBuilder.sol): +`log2LeavesPerAccount=1`, `log2MaxNumOfAccounts=20`, and +`accountsDriveStartIndex=0x90000000000000 >> 26`. Pin the builder's token to the +same token as the guest. The standard USD builder has a 96-bit amount format and +cannot decode these records. A nonzero guardian is required to enable foreclosure. + +From an independently trusted last-finalized machine checkpoint, run +`lua5.4 scripts/wallet-exit-proof.lua SNAPSHOT OWNER` at the repository root. +The exporter verifies the hash tree and emits the drive root/path and account +bytes/path. After foreclosure, submit `proveAccountsDriveMerkleRoot` once, then +`withdraw(account, {accountIndex, accountRootSiblings})` for each entitlement. +Match the exported machine root to the deployed consensus's last-finalized root; +soft-confirmed or merely safe local snapshots are insufficient. Anyone may submit +a proof, but the builder pays the proved owner, and the contract prevents a second +withdrawal at that account index. diff --git a/docs/protocol/scheduler-semantics.md b/docs/protocol/scheduler-semantics.md index b76a43d6..293a0a1f 100644 --- a/docs/protocol/scheduler-semantics.md +++ b/docs/protocol/scheduler-semantics.md @@ -56,7 +56,9 @@ Before *anything* else, drain every fridge direct that has gone **overdue**: ([`force_execute_overdue`](../../sequencer-core/src/scheduler/mod.rs)). This is the censorship-resistance guarantee: a direct cannot be held in the fridge forever by a sequencer that freezes `safe_block` or stops submitting batches — -once it ages out, it executes regardless of any frame. It runs on every input +once it ages out, it executes on the next input regardless of any frame. The +backstop requires a later L1 input; wall-clock time alone does not execute it. +It runs on every input tick (even a malformed batch advances `current_block` and can trip the backstop). Drained directs execute in FIFO (ascending `inclusion_block`) order. @@ -71,7 +73,7 @@ case) **consumes the nonce**. | a | SSZ-decode the payload as `Batch` | `BatchRejected(DecodeFailed)` | not consumed | | b | `batch.nonce == next_expected_batch_nonce` | `BatchRejected(WrongNonce)` | not consumed | | c | *empty frames?* → accept as a no-op | `BatchExecuted` | **consumed** | -| d | structural frame check (below) | `BatchRejected(SafeBlockAboveInclusionBlock \| NonMonotonicSafeBlocks)` | not consumed | +| d | structural frame check (below) | `BatchRejected(SafeBlockAboveInclusionBlock \| NonMonotonicSafeBlocks \| FeePriceOutOfRange)` | not consumed | | e | staleness: `inclusion_block − frames[0].safe_block ≥ MAX_WAIT_BLOCKS` | `BatchSkippedStale` | **not consumed** | | f | execute every frame (below) | `BatchExecuted` | **consumed** | @@ -82,7 +84,14 @@ case) **consumes the nonce**. accounted for L1 state it was included before) — else `SafeBlockAboveInclusionBlock`; - frame `safe_block`s are **non-decreasing** across the batch — else - `NonMonotonicSafeBlocks`. + `NonMonotonicSafeBlocks`; +- every frame's `fee_price ≤ MAX_EXPONENT` from the [fee conversion](../../sequencer-core/src/fee.rs) — else `FeePriceOutOfRange`. + +All frames pass these checks before any frame drain or user-op execution. The +overdue-direct backstop still runs before batch validation. +The fee bound is a canonical guest rule: rebuild and deploy the matching machine +image to enforce it on chain; updating the host alone does not change an +existing application's template. **Gate e — staleness** is measured against the **first** frame only (`has_elapsed_since`, mirrored off-chain by @@ -106,7 +115,7 @@ For each frame, in order ([`process_batch_payload`](../../sequencer-core/src/sch with `(frame.fee_price, frame.safe_block)`. A user op is silently skipped (no state change, no output) when its signature is unrecoverable, when app `validate_user_op` rejects it, or when a protocol guard fails - (`max_fee ≥ fee_price`, or a nonce of `u32::MAX`, which has no + (`safe_block ≤ valid_until_block`, `max_fee ≥ fee_price`, or a nonce of `u32::MAX`, which has no successor) — the fold is a pure deterministic function and emits no diagnostics at the library seam. diff --git a/docs/recovery/README.md b/docs/recovery/README.md index 1a45fa52..51946b65 100644 --- a/docs/recovery/README.md +++ b/docs/recovery/README.md @@ -43,6 +43,23 @@ or the scheduler's next nonce after cockroach recovery. Production needs no accepted ancestor or submitted sentinel to repair a fully invalidated branch ([I16](../invariants.md#i16-the-batch-tree-has-exactly-one-valid-parentless-root-carrying-the-deployments-anchor-nonce)). +## Revoked confirmations and signed operations + +Recovery revokes the predicted inclusion, not the user's signature. A rolled-back +operation can be submitted by anyone who obtained its bytes, including from a +pending L1 batch, and can execute again when its nonce is current and validation +passes, including the signed `valid_until_block` deadline at the replacement +frame clock. Reusing the same signed operation remains a valid retry only +within that deadline. + +To supersede a sequence of revoked operations, the sender must get a replacement +included at every affected nonce; replacing only the first leaves later signed +operations executable. The reference wallet has no dedicated cancellation method. +Applications with price-sensitive operations must define deterministic signed +expiry and price/slippage constraints in their payload and enforce them during +canonical execution. A sequencer-local signature blacklist cannot revoke a +signature in the canonical scheduler. + ## Lifecycle and startup dispatch The recovery cycle crosses a process boundary: @@ -56,7 +73,8 @@ The recovery cycle crosses a process boundary: deliberate restart. Process ownership and shutdown belong to the [authority-boundary ADR](../plans/2026-08-authority-boundary-adr.md). 3. **Recover under the process lock, with no workers.** Check local terminal - facts before any provider call, attempt initial L1 sync, select at most one + facts before any provider call, require a fresh open-application status, + attempt initial L1 sync, select at most one repair, then inspect again after a repair. 4. **Prepare, admit, launch.** Prepare resources without starting tasks. A final current inspection must still authorize serving. It creates a single-use @@ -75,8 +93,19 @@ checkpoint. After initial sync, one consistent `RecoveryInspection` selects: | `EstimatedBatchInDanger(N)` | Retry; an estimate alone cannot authorize invalidation. | | `CanonicalDivergence(N)` or missing recovery checkpoint | Refuse; automatic recovery cannot repair this trust failure. | -Only an initial-sync **provider failure** may fall back to a still-usable -persisted view. Other failures keep their typed retry/refuse classification. +Before repair, startup checks the pinned chain and `isForeclosed()` at Latest. +Foreclosure refuses startup terminally; an unavailable status read retries +without repair, even with a fresh persisted input view. During operation the +submitter repeats this check on every poll, including idle polls, and decodes +`ApplicationForeclosed(address)` from gas estimation as terminal. Runtime +observation follows the existing terminal worker policy and stops the process; +health probes then become unavailable. Detection waits for the submitter's +current RPC work and poll cadence. It does not make admission atomic with an +L1 contract transition. A Latest reorg can cause a conservative stop; a deliberate +restart reads status again. + +After the open-application check, only an initial-sync **provider failure** may +fall back to a still-usable persisted view. Other failures keep their typed retry/refuse classification. Post-flush sync has no such fallback: it must establish the view required by the cascade. @@ -331,8 +360,8 @@ facts; the flush observation and admission witness do not survive. Its invariants cover terminal dominance, repair preconditions, a caught-up post-flush view, and admission soundness. It abstracts successful repair as producing a Tip; concrete transactions and rollback are checked by Rust tests. -Neither model covers external era/generation metadata, the application-input -projection, or snapshot artifact/lease/GC durability. Those obligations remain +Neither model covers the external foreclosure-status gate, external +era/generation metadata, the application-input projection, or snapshot artifact/lease/GC durability. Those obligations remain in storage constraints, tests, and the [snapshot lifecycle](../snapshots/lifecycle.md). Run the configured checks with: diff --git a/docs/review/register.md b/docs/review/register.md index 9d83bc5f..12b3c29f 100644 --- a/docs/review/register.md +++ b/docs/review/register.md @@ -122,16 +122,15 @@ and `classify_input_reader` in [startup recovery](../../sequencer/src/recovery/m Next: classify failures by the setup phase and pin the external exit code; preserve genuinely transient provider failures. -### Startup logs the full RPC URL - -The `sequencer startup` event includes `eth_rpc_url` verbatim. Operator URLs can -carry credentials in userinfo, paths, or query parameters; private-key -redaction does not cover this field. - -Evidence: [`commands/run/mod.rs`](../../sequencer/src/commands/run/mod.rs). -Next: omit the field or define a safe endpoint representation, and check -diagnostic/help paths with a synthetic credential-bearing URL. No credential -exposure in an actual deployment was established by this review. +### Production custody exit conformance + +The reference wallet's [exit integration](../protocol/application-contract.md#8-foreclosure-and-emergency-exits) +is an example, not verification of the production DeFi engine. Before production +custody, pin that engine's drive/builder layout and independently demonstrate +credited-balance and finalized-pending-deposit exits against its deployed +contracts, including its supported capacity. The reference scenario uses real +canonical execution and Application proofs with a finalization fixture; it does +not validate Dave's dispute/epoch finalization pipeline. ## Bounded investigations and cleanup diff --git a/docs/threat-model/README.md b/docs/threat-model/README.md index 1641ee5f..773e7572 100644 --- a/docs/threat-model/README.md +++ b/docs/threat-model/README.md @@ -10,7 +10,7 @@ What we are protecting: - **Rollup state integrity.** The canonical on-chain state must reflect a deterministic replay of user operations and direct inputs. Any divergence between the sequencer's off-chain view and the scheduler's on-chain execution is a state-integrity failure. - **Soft-confirmation honesty.** Every soft confirmation issued by the sequencer must land on L1 as promised, or be explicitly revoked via recovery. -- **User funds.** No user operation, replay, or protocol break can cause users to lose funds. +- **User funds.** Canonical execution enforces application authorization and accounting; fund availability after foreclosure depends on the application's [exit integration](../protocol/application-contract.md#8-foreclosure-and-emergency-exits). - **Batch-submitter key.** Held in operator infra; not hijackable by network attackers. ## Actors and trust diff --git a/docs/watchdog/README.md b/docs/watchdog/README.md index aa92aea9..e3043fdc 100644 --- a/docs/watchdog/README.md +++ b/docs/watchdog/README.md @@ -49,6 +49,12 @@ retry: a failed tick's retry is the next scheduled one. ### Canonical execution +External starting checkpoints used by `init` and `replay` pass full hash-tree +verification before trust checks. Reading the cached root alone can miss a +modified backing file whose hash sidecars are unchanged. This verifies storage +integrity; a non-genesis checkpoint still requires an independently trusted +canonical provenance. + The watchdog drives the machine in-process through the `cartesi` Lua module, with the reference rollup host semantics the `cartesi-machine` CLI and Dave implement ([details](../cartesi-machine.md#rollup-host-semantics)). It clones diff --git a/examples/app-core/contracts/WalletWithdrawalOutputBuilder.sol b/examples/app-core/contracts/WalletWithdrawalOutputBuilder.sol new file mode 100644 index 00000000..71379c36 --- /dev/null +++ b/examples/app-core/contracts/WalletWithdrawalOutputBuilder.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.28; + +/// @notice Reference wallet exits preserve the full uint256 token amount. +contract WalletWithdrawalOutputBuilder { + address public immutable token; + + constructor(address token_) { + token = token_; + } + + function buildWithdrawalOutput(address, bytes calldata account) + external view returns (bytes memory) + { + require(account.length == 64, "invalid wallet account length"); + (address owner, uint256 balance) = abi.decode(account, (address, uint256)); + return abi.encodeWithSignature( + "Voucher(address,uint256,bytes)", token, uint256(0), + abi.encodeWithSignature("transfer(address,uint256)", owner, balance) + ); + } +} diff --git a/examples/app-core/src/application/wallet.rs b/examples/app-core/src/application/wallet.rs index 31387377..58a629fd 100644 --- a/examples/app-core/src/application/wallet.rs +++ b/examples/app-core/src/application/wallet.rs @@ -1,7 +1,7 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::io::Write; use std::path::{Path, PathBuf}; @@ -124,6 +124,37 @@ impl WalletApp { }) } + /// Exit entitlement includes deposits finalized before their scheduled execution. + pub fn exit_accounts<'a>( + &self, + pending: impl Iterator