-
Notifications
You must be signed in to change notification settings - Fork 690
RPC: add eth_getRawTransactionByBlockHashAndIndex & eth_getRawTransactionByBlockNumberAndIndex & eth_sendRawTransactionSync
#11521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
cced7aa
c5883ed
e43726b
56830f8
1b900b2
2515bd6
c6c4034
725c152
8c02ff8
e92f6d1
dfef1f7
c4532cd
de5bc70
073543f
1616975
dbfa03f
23b69d3
4b8e10c
c9a20a0
4e39068
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,4 +75,6 @@ public string[] EnabledModules | |
| public int FiltersTimeout { get; set; } = 900000; | ||
| public bool PreloadRpcModules { get; set; } | ||
| public bool StrictHexFormat { get; set; } = true; | ||
| public int RpcTxSyncDefaultTimeoutMs { get; set; } = 20_000; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need separate timeouts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They seem to serve different purposes. Default is the no-arg UX (~1–2 block times so tx-not-included callers get fast feedback), Max is a server-side cap against clients passing huge timeouts and pinning workers, same split Geth uses for the same reason I believe ( |
||
| public int RpcTxSyncMaxTimeoutMs { get; set; } = 60_000; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| using Nethermind.Blockchain; | ||
| using Nethermind.Blockchain.Filters; | ||
| using Nethermind.Blockchain.Find; | ||
| using Nethermind.Blockchain.Receipts; | ||
|
|
@@ -56,6 +57,7 @@ public partial class EthRpcModule( | |
| IJsonRpcConfig rpcConfig, | ||
| IBlockchainBridge blockchainBridge, | ||
| IBlockFinder blockFinder, | ||
| IBlockTree blockTree, | ||
| IReceiptFinder receiptFinder, | ||
| IStateReader stateReader, | ||
| ITxPool txPool, | ||
|
|
@@ -77,6 +79,7 @@ public partial class EthRpcModule( | |
| protected readonly IJsonRpcConfig _rpcConfig = rpcConfig ?? throw new ArgumentNullException(nameof(rpcConfig)); | ||
| protected readonly IBlockchainBridge _blockchainBridge = blockchainBridge ?? throw new ArgumentNullException(nameof(blockchainBridge)); | ||
| protected readonly IBlockFinder _blockFinder = blockFinder ?? throw new ArgumentNullException(nameof(blockFinder)); | ||
| private readonly IBlockTree _blockTree = blockTree ?? throw new ArgumentNullException(nameof(blockTree)); | ||
| protected readonly IReceiptFinder _receiptFinder = receiptFinder ?? throw new ArgumentNullException(nameof(receiptFinder)); | ||
| protected readonly IStateReader _stateReader = stateReader ?? throw new ArgumentNullException(nameof(stateReader)); | ||
| protected readonly ITxPool _txPool = txPool ?? throw new ArgumentNullException(nameof(txPool)); | ||
|
|
@@ -355,6 +358,91 @@ public virtual async Task<ResultWrapper<Hash256>> eth_sendRawTransaction(byte[] | |
| } | ||
| } | ||
|
|
||
| public async Task<ResultWrapper<ReceiptForRpc?>> eth_sendRawTransactionSync(byte[] transaction, ulong? timeoutMs = null) | ||
|
svlachakis marked this conversation as resolved.
|
||
| { | ||
| Transaction tx; | ||
| try | ||
| { | ||
| tx = TxDecoder.Instance.DecodeCompleteNotNull(transaction, | ||
| RlpBehaviors.AllowUnsigned | RlpBehaviors.SkipTypedWrapping | RlpBehaviors.InMempoolForm); | ||
| } | ||
| catch (RlpException) | ||
| { | ||
| return ResultWrapper<ReceiptForRpc?>.Fail("Invalid RLP.", ErrorCodes.TransactionRejected); | ||
| } | ||
|
|
||
| int waitMs = ResolveSyncTimeoutMs(timeoutMs); | ||
| using CancellationTokenSource cts = new(waitMs); | ||
| // Coalescing signal — back-to-back blocks fold into a single Release; the catches handle | ||
| // both the at-capacity case and the in-flight-after-dispose race (event dispatch snapshots | ||
| // the invocation list, so an unsubscribe in finally cannot stop a handler already running). | ||
| using SemaphoreSlim signal = new(0, 1); | ||
| void OnNewHead(object? sender, BlockEventArgs _) | ||
| { | ||
| try { signal.Release(); } | ||
| catch (SemaphoreFullException) { } | ||
| catch (ObjectDisposedException) { } | ||
| } | ||
|
|
||
| // Subscribe before submit to avoid losing a fast inclusion to a race. | ||
| _blockTree.NewHeadBlock += OnNewHead; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N semaphores and N event subscriptions per call. Maybe they should be consolidated to 1?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a single In the case where one semaphore receives two releases, if these two
Microseconds vs occasionally missing a block I think it's better to keep N.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TCS is fine, IMO race is avoidable |
||
|
|
||
| try | ||
| { | ||
| ResultWrapper<Hash256> sendResult = await SendTx(tx); | ||
| if (sendResult.Result.ResultType != ResultType.Success) | ||
| { | ||
| return ResultWrapper<ReceiptForRpc?>.Fail(sendResult.Result.Error ?? "Send failed", sendResult.ErrorCode); | ||
| } | ||
| Hash256 hash = sendResult.Data; | ||
|
|
||
| // First iteration is the fast path — tx may already be mined. | ||
| while (true) | ||
| { | ||
| ReceiptForRpc? receipt = TryGetReceipt(hash); | ||
| if (receipt is not null) | ||
| { | ||
| return ResultWrapper<ReceiptForRpc?>.Success(receipt); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| await signal.WaitAsync(cts.Token); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| return ResultWrapper<ReceiptForRpc?>.Fail( | ||
| $"Transaction {hash} was added to the pool but not included within {waitMs}ms.", | ||
| ErrorCodes.Timeout); | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| _blockTree.NewHeadBlock -= OnNewHead; | ||
| } | ||
| } | ||
|
|
||
| private int ResolveSyncTimeoutMs(ulong? requestedMs) | ||
| { | ||
| // Clamp >0 so a negative max can't wrap to a huge ulong and overflow back to a negative delay. | ||
| int max = Math.Max(1, _rpcConfig.RpcTxSyncMaxTimeoutMs); | ||
| if (requestedMs is { } req && req > 0) | ||
| { | ||
| return (int)Math.Min(req, (ulong)max); | ||
| } | ||
| int @default = _rpcConfig.RpcTxSyncDefaultTimeoutMs; | ||
| return @default > 0 ? Math.Min(@default, max) : max; | ||
| } | ||
|
|
||
| private ReceiptForRpc? TryGetReceipt(Hash256 txHash) | ||
| { | ||
| (TxReceipt? receipt, ulong blockTimestamp, TxGasInfo? gasInfo, int logIndexStart) = _blockchainBridge.GetTxReceiptInfo(txHash); | ||
| return receipt is null || gasInfo is null | ||
| ? null | ||
| : new ReceiptForRpc(txHash, receipt, blockTimestamp, gasInfo.Value, logIndexStart); | ||
| } | ||
|
|
||
| private async Task<ResultWrapper<Hash256>> SendTx(Transaction tx, | ||
| TxHandlingOptions txHandlingOptions = TxHandlingOptions.None) | ||
| { | ||
|
|
@@ -516,6 +604,40 @@ public ResultWrapper<TransactionForRpc> eth_getTransactionByBlockNumberAndIndex( | |
| return result; | ||
| } | ||
|
|
||
| public ResultWrapper<string?> eth_getRawTransactionByBlockHashAndIndex(Hash256 blockHash, UInt256 positionIndex) | ||
| { | ||
| ResultWrapper<string?> result = GetRawTransactionByBlockAndIndex(new BlockParameter(blockHash), positionIndex); | ||
| if (_logger.IsTrace && result.Result.ResultType == ResultType.Success) _logger.Trace($"eth_getRawTransactionByBlockHashAndIndex request {blockHash}, index: {positionIndex}, result length: {result.Data?.Length ?? 0}"); | ||
| return result; | ||
| } | ||
|
|
||
| public ResultWrapper<string?> eth_getRawTransactionByBlockNumberAndIndex(BlockParameter blockParameter, UInt256 positionIndex) | ||
| { | ||
| ResultWrapper<string?> result = GetRawTransactionByBlockAndIndex(blockParameter, positionIndex); | ||
| if (_logger.IsTrace && result.Result.ResultType == ResultType.Success) _logger.Trace($"eth_getRawTransactionByBlockNumberAndIndex request {blockParameter}, index: {positionIndex}, result length: {result.Data?.Length ?? 0}"); | ||
| return result; | ||
| } | ||
|
|
||
| private ResultWrapper<string?> GetRawTransactionByBlockAndIndex(BlockParameter blockParameter, UInt256 positionIndex) | ||
| { | ||
| SearchResult<Block> searchResult = _blockFinder.SearchForBlock(blockParameter); | ||
| if (searchResult.IsError) | ||
| { | ||
| return ResultWrapper<string?>.Success(null); | ||
| } | ||
|
|
||
| Block block = searchResult.Object!; | ||
| if (positionIndex >= block.Transactions.Length) | ||
| { | ||
| return ResultWrapper<string?>.Success(null); | ||
| } | ||
|
|
||
| Transaction transaction = block.Transactions[(int)positionIndex]; | ||
| // Block-stored txs never carry a sidecar (blob commitments live separately), so consensus form only. | ||
| using NettyRlpStream stream = TxDecoder.Instance.EncodeToNewNettyStream(transaction, RlpBehaviors.SkipTypedWrapping); | ||
| return ResultWrapper<string?>.Success(stream.AsSpan().ToHexString(true)); | ||
| } | ||
|
|
||
| protected virtual ResultWrapper<TransactionForRpc?> GetTransactionByBlockAndIndex(BlockParameter blockParameter, UInt256 positionIndex) | ||
| { | ||
| SearchResult<Block> searchResult = _blockFinder.SearchForBlock(blockParameter); | ||
|
|
@@ -525,7 +647,7 @@ public ResultWrapper<TransactionForRpc> eth_getTransactionByBlockNumberAndIndex( | |
| } | ||
|
|
||
| Block block = searchResult.Object!; | ||
| if (positionIndex < 0 || positionIndex > block.Transactions.Length - 1) | ||
| if (positionIndex >= block.Transactions.Length) | ||
| { | ||
| return ResultWrapper<TransactionForRpc?>.Success(null); | ||
| } | ||
|
|
@@ -559,7 +681,7 @@ public ResultWrapper<TransactionForRpc> eth_getTransactionByBlockNumberAndIndex( | |
| } | ||
|
|
||
| Block block = searchResult.Object!; | ||
| if (positionIndex < 0 || positionIndex > block.Uncles.Length - 1) | ||
| if (positionIndex >= block.Uncles.Length) | ||
| { | ||
| return ResultWrapper<BlockForRpc?>.Success(null); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.