feat: async transaction execution - #702
Conversation
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.
| 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) |
There was a problem hiding this comment.
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.
| #[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); |
There was a problem hiding this comment.
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.
No description provided.