Skip to content

feat: async transaction execution - #702

Open
MicaiahReid wants to merge 3 commits into
mainfrom
feat/async-transaction-execution
Open

feat: async transaction execution#702
MicaiahReid wants to merge 3 commits into
mainfrom
feat/async-transaction-execution

Conversation

@MicaiahReid

Copy link
Copy Markdown
Collaborator

No description provided.

sendTransaction now validates and (unless skipPreflight) simulates the
transaction up front, returning an error on failure. On success it dispatches
the transaction to the execution engine and immediately returns the signature
instead of blocking until execution completes, matching a real Solana node;
clients fetch the result by signature once it has executed.

To keep an accepted transaction from spuriously failing if its blockhash ages
out during the asynchronous delay, blockhash recency is validated once at
admission (ValidatedRecentBlockhashAtAdmission) and trusted at execution, while
durable-nonce transactions still validate at execution (ValidateAtExecution).

The admission-time checks honor the global --skip-signature-verification and
--skip-blockhash-check escape hatches; skip_signature_verification is plumbed
onto SurfnetSvm alongside skip_blockhash_check. The same change is propagated to
the Jito bundle path, the block-production runloop, and the SDK.
getLatestBlockhash now returns the committed slot's blockhash together with a
lastValidBlockHeight computed from that blockhash's actual age in the
recent-blockhash window, instead of assuming a fixed MAX_RECENT_BLOCKHASHES
offset. isBlockhashValid likewise uses the age-aware
is_recent_blockhash_valid_for_processing check so an expired-by-age blockhash is
correctly reported as invalid.
Adapts existing tests to the asynchronous, non-blocking sendTransaction (await
the future, drop the join-handle wait) and adds coverage for: transactions and
loader write batches surviving internal blockhash expiry, rejecting an invalid
blockhash at admission, accepting an invalid blockhash when --skip-blockhash-check
is set, honoring the global --skip-signature-verification flag at admission,
is_blockhash_valid expiry-by-age, and the age-aware getLatestBlockhash response.
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes transaction submission asynchronous and changes blockhash validation around that flow. The main changes are:

  • sendTransaction now performs admission checks before enqueueing.
  • Transaction processing now uses a structured ProcessTransactionRequest.
  • Blockhash validation can now distinguish execution-time checks from admission-time checks.
  • The global skip-signature-verification setting is passed into Surfnet setup.

Confidence Score: 4/5

The durable-nonce admission path needs a fix before merging.

  • V0 durable-nonce transactions can be classified with only static keys.
  • Failed executions can still be counted as successful in transaction metrics.
  • The rest of the request-shape and runloop forwarding changes look consistent.

crates/core/src/surfnet/svm.rs and crates/core/src/rpc/full.rs

Important Files Changed

Filename Overview
crates/core/src/rpc/full.rs Changes sendTransaction to an async admission-and-enqueue flow and records success metrics before execution completes.
crates/core/src/surfnet/svm.rs Adds blockhash validation modes and nonce-detection helpers used by the new admission path.
crates/core/src/surfnet/locker.rs Threads the selected blockhash validation mode through preflight, simulation, and execution.
crates/core/src/runloops/mod.rs Updates the runloop to consume the new process-transaction request struct.
crates/core/src/rpc/jito.rs Keeps Jito bundle execution on execution-time blockhash validation.
crates/types/src/types.rs Adds the transaction request struct and blockhash validation mode enum.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant RPC as sendTransaction RPC
    participant SVM as Admission checks
    participant Runloop
    participant Exec as SVM execution

    Client->>RPC: sendTransaction(tx)
    RPC->>SVM: signature, blockhash, optional preflight
    SVM-->>RPC: admitted
    RPC->>Runloop: ProcessTransactionRequest
    RPC-->>Client: signature
    Runloop->>Exec: execute transaction
    Exec-->>Runloop: success or failure status
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant RPC as sendTransaction RPC
    participant SVM as Admission checks
    participant Runloop
    participant Exec as SVM execution

    Client->>RPC: sendTransaction(tx)
    RPC->>SVM: signature, blockhash, optional preflight
    SVM-->>RPC: admitted
    RPC->>Runloop: ProcessTransactionRequest
    RPC-->>Client: signature
    Runloop->>Exec: execute transaction
    Exec-->>Runloop: success or failure status
Loading

Reviews (1): Last reviewed commit: "test(core): cover async transaction exec..." | Re-trigger Greptile

Comment on lines +1518 to +1530
let program_id = tx
.message
.static_account_keys()
.get(instruction.program_id_index as usize)?;
if !system_program::check_id(program_id)
|| !is_advance_nonce_instruction_data(&instruction.data)
{
return None;
}

tx.message
.static_account_keys()
.get(*instruction.accounts.first()? as usize)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Loaded Nonce Keys Misclassified

For a v0 durable-nonce transaction, the nonce account index can refer to the resolved account list, but this helper only reads static_account_keys(). The RPC can then treat the transaction as non-nonce and use admission-only recent-blockhash validation, so a valid nonce transaction can be rejected with BlockhashNotFound or skip the nonce-account blockhash check.

Comment on lines +1768 to +1771
#[cfg(feature = "prometheus")]
if let Some(m) = crate::telemetry::metrics() {
m.record_transaction(true, rpc_start.elapsed().as_millis() as u64);
m.record_rpc_request("sendTransaction", rpc_start.elapsed().as_millis() as u64);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Enqueued Transactions Count As Executed

After enqueueing, the RPC drops the status receiver and records record_transaction(true, ...) before the runloop executes the transaction. A transaction that later fails execution is still counted as successful, so monitoring can show successful transaction processing while the stored transaction status contains an error.

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