diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c474169ec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,34 @@ +# MagicBlock Ephemeral Validator + +Solana-based ephemeral validator with delegation and commit/undelegation lifecycle. + +## Build & Test + +- **Build**: `cargo build` (root Cargo.toml) +- **Lint**: `make lint` +- **Format**: `make fmt` +- **Unit tests**: `cargo test -p ` from repo root +- **Integration tests**: separate workspace at `test-integration/` with its own Cargo.toml, Cargo.lock, and Makefile + - Build: `cd test-integration && cargo check` + - Run: `cd test-integration && make test` +- **Formatting**: `max_width = 80`, edition 2021 (see `rustfmt.toml`) + +## Project Structure + +- `magicblock-validator/` — main validator binary +- `magicblock-committor-service/` — commit/finalize/undelegate task orchestration +- `magicblock-committor-program/` — on-chain program for buffer management (chunks, PDAs) +- `programs/magicblock/` — on-chain MagicBlock program (scheduling, intent processing) +- `magicblock-accounts*/` — account storage and cloning +- `magicblock-rpc-client/` — RPC client utilities +- `magicblock-metrics/` — metrics and instrumentation (`LabelValue` trait) +- `test-integration/` — integration test workspace (separate Cargo workspace, separate Makefile) +- `tools/` — CLI utilities (genx, keypair-base58, ledger-stats, tui-client) + +## Code Style + +- Keep match arms clean: prefer matching enum variants directly over guard clauses +- Move domain logic into the type's own `impl` block, keep delegation enums thin +- Avoid over-abstracting: three similar lines are better than a premature helper +- Use `From` impls for enum wrapping conversions +- Return iterators (`impl Iterator`) over collected `Vec` where practical diff --git a/magicblock-api/src/fund_account.rs b/magicblock-api/src/fund_account.rs index 12583887b..3e9e4e2f0 100644 --- a/magicblock-api/src/fund_account.rs +++ b/magicblock-api/src/fund_account.rs @@ -28,13 +28,10 @@ pub(crate) fn fund_account_with_data( lamports: u64, size: usize, ) { - let account = if let Some(mut acc) = accountsdb.get_account(pubkey) { - acc.set_lamports(lamports); - acc.set_data(vec![0; size]); - acc - } else { - AccountSharedData::new(lamports, size, &Default::default()) - }; + if accountsdb.get_account(pubkey).is_some() { + return; + } + let account = AccountSharedData::new(lamports, size, &Default::default()); let _ = accountsdb.insert_account(pubkey, &account); } @@ -70,16 +67,20 @@ pub(crate) fn funded_faucet( } pub(crate) fn fund_magic_context(accountsdb: &AccountsDb) { + const CONTEXT_LAMPORTS: u64 = u64::MAX; + fund_account_with_data( accountsdb, &magic_program::MAGIC_CONTEXT_PUBKEY, - u64::MAX, + CONTEXT_LAMPORTS, MagicContext::SIZE, ); let mut magic_context = accountsdb .get_account(&magic_program::MAGIC_CONTEXT_PUBKEY) - .unwrap(); + .expect("magic context should have been created"); magic_context.set_delegated(true); + magic_context.set_owner(magic_program::ID); + let _ = accountsdb .insert_account(&magic_program::MAGIC_CONTEXT_PUBKEY, &magic_context); } diff --git a/magicblock-committor-service/CLAUDE.md b/magicblock-committor-service/CLAUDE.md new file mode 100644 index 000000000..8012fd1bf --- /dev/null +++ b/magicblock-committor-service/CLAUDE.md @@ -0,0 +1,123 @@ +# Committor Service + +Orchestrates commit/finalize/undelegate lifecycle for delegated accounts on Base layer. + +## Architecture + +### Task System (`src/tasks/`) + +`BaseTaskImpl` is the central enum representing all task types: +``` +BaseTaskImpl +├── Commit(CommitTask) — commit account state/diff to base layer +├── Finalize(FinalizeTask) — finalize a committed account +├── Undelegate(UndelegateTask) — undelegate an account +└── BaseAction(BaseActionTask) — call handler actions (V1/V2) +``` + +All variants implement `BaseTask` trait via delegation in `BaseTaskImpl`. +Domain logic lives in each type's own `impl` block, `BaseTaskImpl` just delegates. + +### CommitTask (`src/tasks/commit_task.rs`) + +Delivery strategy is an enum: +``` +CommitDelivery +├── StateInArgs — full account data in instruction args +├── StateInBuffer { stage } — data in on-chain buffer +├── DiffInArgs { base_account } — diff against base in args +└── DiffInBuffer { stage, base_account } — diff in buffer +``` + +`CommitBufferStage` tracks buffer lifecycle: `Preparation` -> `Cleanup`. + +Key methods: +- `state_preparation_stage()` / `diff_preparation_stage()` — construct buffer preparation data +- `reset_commit_id()` — CommitTask-specific, not part of BaseTask trait +- `try_optimize_tx_size()` — switches from Args to Buffer delivery (deprecated, moving to strategist) + +### BaseActionTask (`src/tasks/mod.rs`) + +Enum with V1 (legacy call_handler) and V2 (call_handler_v2 with source_program). +`From` impls exist for `BaseActionTaskV1 -> BaseActionTask` and `BaseActionTaskV2 -> BaseActionTask`. + +Each `BaseAction` may carry an optional `BaseActionCallback`. Callbacks are fired on L1 +action failure or timeout — never delayed to intent completion. `BaseActionTask::extract_callback` +uses `take()`, so callbacks are consumed exactly once. + +### Task Builder (`src/tasks/task_builder.rs`) + +`TaskBuilderImpl` creates tasks from `ScheduledIntentBundle`: +- `commit_tasks()` — creates commit stage related tasks - fetches commit IDs and base accounts, creates CommitTask/BaseActionTask +- `finalize_tasks()` — creates finalize stage related tasks - creates Finalize/Undelegate/BaseAction tasks +- `create_action_tasks()` — dispatches V1 vs V2 based on `action.source_program` + +### Task Strategist (`src/tasks/task_strategist.rs`) + +Optimizes task layout for transaction size constraints. Uses `try_optimize_tx_size` on CommitTask +to switch from Args to Buffer strategy when data doesn't fit in instruction args. + +`TransactionStrategy` also owns callback helpers: +- `has_actions_callbacks()` — whether any action task carries a callback +- `extract_action_callbacks()` — takes all callbacks out of action tasks (uses `take()`, safe to call twice) + +### Transaction Flow + +1. `TaskBuilderImpl` creates tasks from intent bundle +2. `TaskStrategist` optimizes task layout into `TransactionStrategy` +3. `DeliveryPreparator` prepares buffers (init, realloc, write chunks) and lookup tables +4. `TransactionPreparator` assembles final transaction from prepared tasks +5. `IntentExecutor` executes intent - (single/two stage) sends transactions to base layer, retries on defined failures by patching, propagates if error is not retriable. Manages action callback lifecycle on failure/timeout. + +### Buffer Preparation (`PreparationTask`) + +On-chain buffer lifecycle for large commits: +1. `init_instruction` — create buffer + chunks accounts +2. `realloc_instructions` — resize buffer if needed +3. `write_instructions` — write data chunks to buffer +4. After commit: `CleanupTask.instruction` closes buffer accounts + +### Intent Executor (`src/intent_executor/`) + +`IntentExecutorImpl` is generic over `TransactionPreparator`, `TaskInfoFetcher`, and +`ActionsCallbackExecutor`. The `A` parameter is the callback executor — abstracted so +`IntentExecutorImpl` only knows *when* to fire callbacks, not how (could be channel, RPC, etc.). + +Callback/timeout policy lives in `single_stage_execution_flow` and `two_stage_execution_flow`, +not inside the sub-executors. The sub-executors (`SingleStageExecutor`, `TwoStageExecutor`) +handle the mechanics of stripping actions and firing callbacks when an error occurs within +their execution loops, but the *timeout wrapping* is the responsibility of the flow methods. + +**Action error / timeout rules:** +- Actions failed (stripped) → fire callbacks, continue without actions (if committed accounts exist) +- Timeout with callbacks → fire all callbacks, strip actions, continue +- Timeout without callbacks → continue unchanged +- Standalone actions (no committed accounts) fail → fire callbacks, intent fails + +**`handle_actions_error` (free fn in `two_stage_executor.rs`, method in `SingleStageExecutor`)**: +Strips action tasks from a strategy via `remove_actions`, extracts + fires callbacks, returns +the stripped tasks as a cleanup strategy for junk. Safe to call multiple times (callbacks are `take()`n). + +**`execute_callbacks()`** on each executor: convenience wrapper — calls `handle_actions_error` +and pushes the resulting cleanup strategy to `inner.junk`. + +**Junk**: `IntentExecutorImpl.junk` collects `TransactionStrategy` values that need on-chain +cleanup (primarily buffer accounts for `CommitTask`). Always push the remaining strategy to +junk on both success AND error paths to ensure buffer cleanup. + +**State machine (`TwoStageExecutor`)**: typestate pattern with `Initialized → Committed → Finalized`. +Transitions via `done(signature)`. `commit()` and `finalize()` return `IntentExecutorResult` +and always `mem::take` their strategy into junk before returning. + +### Key Types + +- `TaskStrategy` — `Args | Buffer` +- `TransactionStrategy` — optimized tasks + lookup table keys +- `ActionsCallbackExecutor` — trait for firing `BaseActionCallback` values; impl determines mechanism + +### Integration Tests (`test-integration/test-committor-service/`) + +- `common.rs` — `TestFixture`, `create_commit_task`, `create_buffer_commit_task` helpers +- `test_delivery_preparator.rs` — buffer preparation, lookup tables, re-prepare scenarios +- `test_transaction_preparator.rs` — transaction assembly with various task combinations +- `test_intent_executor.rs` — end-to-end intent execution