Replies: 3 comments 19 replies
-
|
Overall, I like the idea, especially since this gets rid of committor_service.sqlite. A few questions:
|
Beta Was this translation helpful? Give feedback.
-
Does it need to wait for confirmation of the ER update before sending? Shouldn't scheduling directly create the PDA? It removes the need for the context and But it does put the burden of checking commit ID on users to derive the account
Shouldn't the accountsdb index of Magic program accounts be fairly small and lag-free? Both approaches are O(accepted intents), but the additional parsing to find next list elements should add latency compared to fetching all and then deserializing all. |
Beta Was this translation helpful? Give feedback.
-
|
This design should work. However, it looks quite complex to me unless there is a strong technical reason for that complexity. I was thinking about a much simpler alternative: Instead of creating a PDA for each intent and maintaining a doubly-linked list across those PDAs, why don’t we store the accepted intents in If we need extra metadata for restart recovery, we can add it to the intent entry itself, or globally in the Is there a strong reason this would not work? Size shouldn't be a problem because "one single account/context containing the list of intents + metadata" vs "10000 PDAs, their creation and closing logics, complexity of handling doubly-linked-list, additional instructions". If support for If for some reason, |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Intent Execution Durability
Motivation
When a user schedules a commit, they are delegating an account to the ER with the expectation that its state will be persisted to the base chain. The validator is obligated to fulfill this — an unexecuted commit is a protocol-level breach of trust.
Today, that obligation is not enforced past the accept boundary. Once an Intent is accepted it leaves
AccountsDBand lives exclusively in process memory. A validator crash anywhere between acceptance and L1 confirmation silently drops the Intent. There is no recovery, no retry, no report — the commitment is gone.This proposal closes that gap by anchoring accepted Intents back in
AccountsDBfor their entire execution lifecycle.Problem
Acceptedintents leaveAccountsDBand exist only in the in-memoryTransactionSchedulerglobal and theCommittorServicempsc channel. Neither survives a process restart.Current flow
The window between step 2 and step 4 is unprotected. Any crash drops all in-flight intents.
Proposed Solution
This proposal is similar to the outbox pattern, where
AccountsDBitself serves as the durable outbox. Rather than handing an Intent directly to theCommittorServiceand hoping for the best, the validator first writes the Intent intoAccountsDBas a dedicated ephemeral account. TheCommittorServicethen processes it from there and only closes the account once L1 execution is confirmed. A crash at any point leaves the account inAccountsDB; on restart the validator callsgetProgramAccountswith a discriminator filter, reads each account's status, and resumes accordingly.Additionally, just before sending a transaction to L1, the validator records the transaction signature back into the
MagicIntentAccountviaSetIntentExecutionStage. This lets the restart logic distinguish "prepared but not yet sent" from "sent — check the signature on L1."No stage is executed twice. Because
SetIntentExecutionStageis committed toAccountsDBbefore the L1 transaction is submitted, any restart that findsstatus = Executing(...)will always query L1 for the stored signature first. A confirmed & successful tx signature advances or closes the intent. The write-before-send ordering is the invariant that makes this hold.New execution flow
Crash 1→2
Intent is still in
MagicContext. The nextAcceptScheduledCommitscall picks it up and retries. No state is lost.Crash 2→3
MagicIntentAccountexists withstatus = Accepted. On restart: discovered viagetProgramAccounts, rescheduled for full execution.Crash 3→4
MagicIntentAccountexists withstatus = Accepted— indistinguishable from crash 2→3. The L1 transaction was never sent, so the Intent can safely be re-prepared and re-executed.Crash 4→5
MagicIntentAccountexists withstatus = Executing(signature). On restart: checksignatureon L1.Crash 5→6
Same as crash 4→5:
status = Executing(signature). Check signature, close or reschedule accordingly.Account Design
MagicIntentAccountephemeral accountSeeds:
["magic-intent", intent_id.to_le_bytes()]Owner: MagicBlock builtin program
Instruction Changes
Modified:
AcceptScheduledCommitsRemoves up to N intents from the front of
MagicContext.scheduled_base_intents. For each intent aMagicIntentAccountephemeral account is created withstatus = Accepted. N is bounded by the transaction account limit: every new account must be passed as writable in the same transaction.Accounts:
Logic (after existing pop from MagicContext): for each accepted intent, create a
MagicIntentAccountephemeral account withstatus = Accepted.New:
SetIntentExecutionStage { intent_id, stage: ExecutionStage }Sets or advances the execution stage of an intent. Handles the initial transition from
Accepted, signature replacement on retry, and advancement fromCommittingtoFinalizing— all through a single instruction with a unified set of transition rules.Accounts:
[validator_auth (signer), intent_pda (writable)]Transitions:
AcceptedExecuting(SingleStage(sig))AcceptedExecuting(TwoStage(Committing(sig)))Executing(SingleStage(_))Executing(SingleStage(new_sig))Executing(TwoStage(Committing(_)))Executing(TwoStage(Committing(new_sig)))Executing(TwoStage(Committing(commit)))Executing(TwoStage(Finalizing { commit, finalize: sig }))Executing(TwoStage(Finalizing { commit, _ }))Executing(TwoStage(Finalizing { commit, finalize: new_sig }))commitmust match stored valueChecks:
Accepted: rejectsTwoStage(Finalizing { .. })— cannot skip the commit phase.Executing(_): the execution type (SingleStagevsTwoStage) cannot change.Executing(TwoStage(Finalizing { .. })): rejectsTwoStage(Committing(_))— downgrade not permitted. ReachingFinalizingmeans the commit transaction was confirmed on L1; going back toCommittingwould re-commit an already-committed account.New:
CloseMagicIntentAccount { intent_id }Deallocates the
MagicIntentAccountephemeral account. Called after L1 execution is confirmed.Accounts:
Logic:
closing_pdaRestart Recovery
Discovery
Cost: one RPC call returning the set of intents that were accepted and not yet closed. No scanning of closed IDs, no range guessing, no dependency on total historical intent count.
Classification and recovery
Intent Discovery:
getProgramAccountsThe MagicBlock builtin program owns
MagicContext(one singleton) and oneMagicIntentAccountper open intent. AgetProgramAccountscall filtered by theMagicIntentAccountdiscriminator returns all intent accounts that were accepted and have not yet been closed. After deserialization, accounts are sorted bybundle.idto restore processing order.Each account's
statusfield determines how recovery proceeds — an account withstatus = Acceptedmeans the intent has not yet been sent to L1 and needs full execution; an account withstatus = Executing(...)requires checking the stored signature(s) on L1 before deciding whether to close or reschedule.Considered Alternatives
Option A: Linked list (
MagicIntentList+prev_id/next_id)Maintain a
MagicIntentListsingleton (head + tail) and embedprev_id/next_idpointers in eachMagicIntentAccount. On restart, traverse from head to tail.Problem — unnecessary complexity:
getProgramAccountsalready returns the same set of open intents in one call. The linked list duplicates this with extra fields per account, a shared singleton written on every accept and every close, and more complex close logic (relinking neighbors).Verdict: Superseded by
getProgramAccounts.Option B: Watermark + range scan
Store
accepted_intent_watermark: u64inMagicContext. On restart, scan intent PDAs for IDs in the range[watermark, intent_id).Mechanism:
getMultipleAccountsfor the rangeProblem — stuck watermark: If a single intent fails persistently (e.g., an integrator bug causes repeated execution failure), the watermark is stuck at that intent's ID. Every subsequent restart must scan from that ID through the entire current range. Other intents in the range are discoverable, but the scan grows unboundedly until the stuck intent is resolved.
Furthermore, the watermark advancement logic requires careful coordination: advancing past gaps means scanning forward for the next live PDA, which requires either additional instructions or off-chain bookkeeping.
Verdict: Functionally correct but operationally fragile. A single stuck intent degrades restart performance for all subsequent restarts.
Option C: Bitmap pages
Group intent IDs into pages of fixed size (e.g., 256). Each page is a PDA storing a bitmap of which intent IDs within its range are still active.
On restart: scan page PDAs from
min_active_pagetocurrent_page, extract set bits, fetch the corresponding intent PDAs.Problem — hot shared account: Every
CloseMagicIntentAccountfor any intent in page range [0, 255] writes to the same page 0 PDA. EveryAcceptScheduledCommitsthat allocates into page 0 also writes to page 0. All operations in the same page range serialize on a single shared account.Problem — page watermark inherits the stuck-intent problem:
min_active_pagecannot advance past page N until all 256 intents in page N close. One stuck intent holds up the entire page's retirement. The restart scan cost is O(N/256) page fetches — better than a plain watermark, but the underlying problem remains.Problem — two-level indirection on restart: Pages tell you which IDs were accepted; you still need a second round of PDA fetches to get the actual intent data.
Verdict: Better than a plain watermark for restart scan efficiency (coarser granularity), but introduces a shared hot-account bottleneck and does not fully eliminate the stuck-intent scan problem.
Comparison
getProgramAccountsMagicIntentListsingletongetProgramAccountsis chosen for its minimal account structure, no shared hot accounts, simplest close logic, and correct stuck-intent behavior.Implementation Scope
magicblock-magic-program-api/src/pda.rsintent_account_pda(id)magicblock-magic-program-api/src/instruction.rsSetIntentExecutionStage,CloseMagicIntentAccountvariantsprograms/magicblock/src/MagicIntentAccount,MagicIntentStatustypesprograms/magicblock/src/schedule_transactions/process_accept_scheduled_commits.rsprograms/magicblock/src/schedule_transactions/(new files)process_set_intent_execution_stage.rs,process_close_intent_account.rsprograms/magicblock/src/magicblock_processor.rsmagicblock-committor-service/src/(new file)restart_loader.rs—getProgramAccountsfetch, sort by id, classification, recovery dispatchmagicblock-committor-service/src/intent_executor/SetIntentExecutionStagebefore L1 submissionmagicblock-accounts/src/scheduled_commits_processor.rsregister_scheduled_commit_sentwithCloseMagicIntentAccountcallBeta Was this translation helpful? Give feedback.
All reactions