// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Blockchain; using Nethermind.Facade.Filters; using Nethermind.Blockchain.Find; using Nethermind.Blockchain.Receipts; using Nethermind.Core; using Nethermind.Core.BlockAccessLists; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Core.Specs; using Nethermind.Evm; using Nethermind.Facade; using Nethermind.Facade.Eth; using Nethermind.Facade.Eth.RpcTransaction; using Nethermind.Facade.Find; using Nethermind.Facade.Proxy.Models.Simulate; using Nethermind.Facade.Simulate; using Nethermind.Int256; using Nethermind.JsonRpc.Data; using Nethermind.JsonRpc.Exceptions; using Nethermind.JsonRpc.Modules.Eth.FeeHistory; using Nethermind.JsonRpc.Modules.Eth.GasPrice; using Nethermind.Logging; using Nethermind.Network; using Nethermind.Network.Contract.P2P; using Nethermind.Serialization.Json; using Nethermind.Serialization.Rlp; using Nethermind.State; using Nethermind.State.Proofs; using Nethermind.Synchronization.ParallelSync; using Nethermind.Trie; using Nethermind.TxPool; using Nethermind.Wallet; using System; using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using Nethermind.Crypto; using Block = Nethermind.Core.Block; using BlockHeader = Nethermind.Core.BlockHeader; using ResultType = Nethermind.Core.ResultType; using Signature = Nethermind.Core.Crypto.Signature; using Transaction = Nethermind.Core.Transaction; namespace Nethermind.JsonRpc.Modules.Eth; public partial class EthRpcModule( IJsonRpcConfig rpcConfig, IBlockchainBridge blockchainBridge, IBlockFinder blockFinder, IBlockTree blockTree, IReceiptFinder receiptFinder, IStateReader stateReader, ITxPool txPool, ITxSender txSender, IWallet wallet, ILogManager logManager, ISpecProvider specProvider, IGasPriceOracle gasPriceOracle, IEthSyncingInfo ethSyncingInfo, IFeeHistoryOracle feeHistoryOracle, IProtocolsManager protocolsManager, IForkInfo forkInfo, ulong? secondsPerSlot, HeadBlockSignal headBlockSignal, IEthCapabilitiesProvider capabilitiesProvider, IBlockForRpcFactory blockForRpcFactory, HashesOnlyBlockReader? hashesOnlyBlockReader = null, IReceiptFinder? storedReceiptFinder = null) : IEthRpcModule { public const int GetProofStorageKeyLimit = 1024; public const int MaxGetStorageSlots = StorageValuesRequest.MaxSlots; private const string UnknownAccount = "unknown account"; protected readonly Encoding _messageEncoding = Encoding.UTF8; 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)); protected readonly ITxSender _txSender = txSender ?? throw new ArgumentNullException(nameof(txSender)); protected readonly IWallet _wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); protected readonly ISpecProvider _specProvider = specProvider ?? throw new ArgumentNullException(nameof(specProvider)); protected readonly IBlockForRpcFactory _blockForRpcFactory = blockForRpcFactory ?? throw new ArgumentNullException(nameof(blockForRpcFactory)); private readonly HashesOnlyBlockReader? _hashesOnlyBlockReader = hashesOnlyBlockReader; // Stored receipts only: a sender lookup must not drive receipt regeneration, which costs a block execution. private readonly IReceiptFinder? _storedReceiptFinder = storedReceiptFinder; protected readonly ILogger _logger = logManager.GetClassLogger(); private static readonly TxDecoder TxRlpDecoder = TxDecoder.Instance; protected readonly IGasPriceOracle _gasPriceOracle = gasPriceOracle ?? throw new ArgumentNullException(nameof(gasPriceOracle)); private readonly IEthCapabilitiesProvider _capabilitiesProvider = capabilitiesProvider ?? throw new ArgumentNullException(nameof(capabilitiesProvider)); protected readonly IEthSyncingInfo _ethSyncingInfo = ethSyncingInfo ?? throw new ArgumentNullException(nameof(ethSyncingInfo)); protected readonly IFeeHistoryOracle _feeHistoryOracle = feeHistoryOracle ?? throw new ArgumentNullException(nameof(feeHistoryOracle)); protected readonly IProtocolsManager _protocolsManager = protocolsManager ?? throw new ArgumentNullException(nameof(protocolsManager)); protected readonly ulong _secondsPerSlot = secondsPerSlot ?? throw new ArgumentNullException(nameof(secondsPerSlot)); private readonly HeadBlockSignal _headBlockSignal = headBlockSignal ?? throw new ArgumentNullException(nameof(headBlockSignal)); /// In-flight eth_sendRawTransactionSync calls on this instance. /// /// Counts node-wide only because the method is sharable: the module pool runs every such call on its single /// shared instance. /// private int _syncRequests; private ResultWrapper? _chainIdResponse; readonly JsonSerializerOptions UnchangedDictionaryKeyOptions = new(EthereumJsonSerializer.JsonOptionsIndented) { DictionaryKeyPolicy = null }; public ResultWrapper eth_protocolVersion() { int highestVersion = _protocolsManager.GetHighestProtocolVersion(Protocol.Eth); return ResultWrapper.Success(highestVersion.ToHexString()); } public ResultWrapper eth_syncing() => ResultWrapper.Success(_ethSyncingInfo.GetFullInfo()); public ResultWrapper eth_snapshot() => ResultWrapper.Fail("eth_snapshot not supported"); public ResultWrapper
eth_coinbase() => ResultWrapper
.Success(Address.Zero); public async Task> eth_gasPrice() => ResultWrapper.Success(await _gasPriceOracle.GetGasPriceEstimate()); public ResultWrapper eth_blobBaseFee() { BlockHeader? head = _blockFinder.Head?.Header; if (head?.ExcessBlobGas is null) { return ResultWrapper.Success(null); } IReleaseSpec spec = _specProvider.GetSpec(head); if (!BlobGasCalculator.TryCalculateFeePerBlobGas(head.ExcessBlobGas.Value, spec.BlobBaseFeeUpdateFraction, out UInt256 feePerBlobGas)) { return ResultWrapper.Fail("Unable to calculate the current blob base fee"); } return ResultWrapper.Success(feePerBlobGas); } public ResultWrapper eth_baseFee() { BlockHeader? head = _blockFinder.Head?.Header; if (head is null) { return ResultWrapper.Success(null); } IEip1559Spec specFor1559 = _specProvider.GetSpecFor1559(head.Number + 1); if (!specFor1559.IsEip1559Enabled) { return ResultWrapper.Success(null); } return ResultWrapper.Success(BaseFeeCalculator.Calculate(head, specFor1559)); } public ResultWrapper eth_maxPriorityFeePerGas() { UInt256 maxPriorityFeePerGas = _gasPriceOracle.GetMaxPriorityGasFeeEstimate(); return ResultWrapper.Success(maxPriorityFeePerGas); } public ResultWrapper eth_feeHistory(ulong blockCount, BlockParameter newestBlock, double[] rewardPercentiles) => _feeHistoryOracle.GetFeeHistory(blockCount, newestBlock, rewardPercentiles); public ResultWrapper> eth_accounts() { try { return ResultWrapper>.Success(_wallet.GetAccounts()); } catch (Exception) { return ResultWrapper>.Fail("Error while getting key addresses from wallet."); } } public ResultWrapper eth_blockNumber() { ulong number = _blockchainBridge.HeadBlock?.Number ?? 0; return ResultWrapper.Success(number); } public ResultWrapper eth_getBalance(Address address, BlockParameter? blockParameter = null) { SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader header = searchResult.Object; if (!_blockchainBridge.HasStateForBlock(header!)) { return GetStateFailureResult(header); } _stateReader.TryGetAccount(header!, address, out AccountStruct account); return ResultWrapper.Success(account.Balance); } public ResultWrapper eth_getStorageAt(Address address, StorageIndex positionIndex, BlockParameter? blockParameter = null) { SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader? header = searchResult.Object; if (!_blockchainBridge.HasStateForBlock(header!)) { return GetStateFailureResult(header!); } try { _stateReader.GetStorage(header!, address, positionIndex, out UInt256 storage); return ResultWrapper.Success(storage.IsZero ? Bytes32.Zero.Unwrap() : storage.ToBigEndian()); } catch (MissingTrieNodeException e) when (e.InnerException is not StateNotRetainedException) { Hash256 hash = e.Hash; return ResultWrapper.Fail($"missing trie node {hash} (path ) state {hash} is not available", ErrorCodes.ResourceNotFound); } } public ResultWrapper eth_getStorageValues( StorageValuesRequest requests, BlockParameter? blockParameter = null) { if (requests.TooManySlots) return TooManySlotsError(); if (requests.Entries.Count == 0 || requests.TotalSlots == 0) return ResultWrapper.Fail("empty request", ErrorCodes.InvalidParams); SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); BlockHeader header = searchResult.Object!; if (!_blockchainBridge.HasStateForBlock(header)) return GetStateFailureResult(header); byte[] buffer = ArrayPool.Shared.Rent(requests.TotalSlots * 32); int bufferOffset = 0; using ArrayPoolSpan storageValues = new(requests.TotalSlots); Dictionary[]> slots = new(requests.Entries.Count); foreach (KeyValuePair entry in requests.Entries) { UInt256[] slotKeys = entry.Value; Span storage = storageValues.Slice(0, slotKeys.Length); _stateReader.GetStorage(header, entry.Key, slotKeys, storage); Memory[] values = new Memory[slotKeys.Length]; for (int i = 0; i < slotKeys.Length; i++) { Memory slot = buffer.AsMemory(bufferOffset, 32); storage[i].ToBigEndian(slot.Span); values[i] = slot; bufferOffset += 32; } slots[entry.Key] = values; } return ResultWrapper.Success(new StorageValuesResult(buffer, slots)); } private static ResultWrapper TooManySlotsError() => ResultWrapper.Fail($"too many slots (max {MaxGetStorageSlots})", ErrorCodes.InvalidParams); public virtual ResultWrapper eth_getTransactionCount(Address address, BlockParameter? blockParameter) { if (blockParameter == BlockParameter.Pending) { UInt256 pendingNonce = _txPool.GetLatestPendingNonce(address); return ResultWrapper.Success(pendingNonce); } SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader header = searchResult.Object; if (!_blockchainBridge.HasStateForBlock(header!)) { return GetStateFailureResult(header); } _stateReader.TryGetAccount(header!, address, out AccountStruct account); return ResultWrapper.Success(account.Nonce); } public ResultWrapper eth_getBlockTransactionCountByHash(Hash256 blockHash) => eth_getBlockTransactionCountByNumber(new BlockParameter(blockHash)); public ResultWrapper eth_getBlockTransactionCountByNumber(BlockParameter blockParameter) { if (FindHashesOnlyBlock(blockParameter) is { } hashesOnly) { return ResultWrapper.Success((UInt256)hashesOnly.TransactionHashes.Length); } SearchResult searchResult = _blockFinder.SearchForBlock(blockParameter); return searchResult.IsError ? ResultWrapper.Success(null) : ResultWrapper.Success((UInt256)searchResult.Object!.Transactions.Length); } public ResultWrapper eth_getUncleCountByBlockHash(Hash256 blockHash) => eth_getUncleCountByBlockNumber(new BlockParameter(blockHash)); public ResultWrapper eth_getUncleCountByBlockNumber(BlockParameter? blockParameter) { if (FindHashesOnlyBlock(blockParameter) is { } hashesOnly) { return ResultWrapper.Success((UInt256)hashesOnly.Block.Uncles.Length); } SearchResult searchResult = _blockFinder.SearchForBlock(blockParameter); return searchResult.IsError ? ResultWrapper.Success(null) : ResultWrapper.Success((UInt256)searchResult.Object!.Uncles.Length); } private HashesOnlyBlock? FindHashesOnlyBlock(BlockParameter? blockParameter) => blockParameter is null ? null : _hashesOnlyBlockReader?.Find(_blockFinder, blockParameter); public ResultWrapper eth_getCode(Address address, BlockParameter? blockParameter = null) { SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader header = searchResult.Object; return !_blockchainBridge.HasStateForBlock(header!) ? GetStateFailureResult(header) : ResultWrapper.Success( _stateReader.TryGetAccount(header!, address, out AccountStruct account) ? _stateReader.GetCode(account.CodeHash) : []); } public ResultWrapper eth_sign(Address addressData, byte[] message) { if (!_wallet.TrySignMessage(message, addressData, out Signature sig)) return ResultWrapper.Fail("authentication needed: password or unlock", ErrorCodes.AccountLocked); if (_logger.IsTrace) _logger.Trace($"eth_sign request {addressData}, {message}, result: {sig}"); return ResultWrapper.Success(sig); } public virtual async Task> eth_sendTransaction(SignableTransactionForRpc rpcTx) => ReportUnknownAccountFirst(rpcTx, await SignAndSendTransaction(rpcTx)); /// /// A request from an account this node holds no key for fails as an unknown account whatever else is wrong /// with it, so the wallet is asked only once has failed. /// protected ResultWrapper ReportUnknownAccountFirst(SignableTransactionForRpc rpcTx, ResultWrapper result) => result.Result.ResultType == ResultType.Failure && !HoldsAccount((rpcTx as LegacyTransactionForRpc)?.From ?? Address.Zero) ? ResultWrapper.Fail(UnknownAccount, ErrorCodes.InvalidInput) : result; /// /// Checks the fees of as a transaction to send and fills the fees and gas it leaves out. /// /// The error that stops the send, or null when the check and the fill pass. /// /// A blob transaction is sent without its sidecar and fails anyway, so it is not checked or filled: /// the fill and its KZG work would be lost. /// protected async Task?> CheckAndFillSendTransaction(SignableTransactionForRpc rpcTx) { if (rpcTx is BlobTransactionForRpc || _blockFinder.Head?.Header is not { } head) return null; IReleaseSpec spec = _specProvider.GetSpec(head); if (FeeDefaultRules.Error(rpcTx, spec.IsEip1559Enabled) is { } feeError) return ResultWrapper.Fail(feeError, ErrorCodes.InvalidInput); return await FillFeesAndGas(rpcTx, head, spec) is { } fillError ? ResultWrapper.Fail(fillError.Error, fillError.ErrorCode) : null; } private async Task> SignAndSendTransaction(SignableTransactionForRpc rpcTx) { if (await CheckAndFillSendTransaction(rpcTx) is { } sendError) return sendError; // A frame transaction priced per gas buys max_gas at those prices, which only a fork can price. IReleaseSpec? frameSpec = rpcTx is FrameTransactionForRpc && _blockFinder.Head?.Header is { } head ? _specProvider.GetSpec(head) : null; Result txResult = rpcTx.ToValidatedTransaction(spec: frameSpec); if (!txResult.Success(out Transaction tx, out string error)) { return ResultWrapper.Fail(error, ErrorCodes.InvalidInput); } tx.ChainId = _blockchainBridge.GetChainId(); UInt256? nonce = rpcTx is LegacyTransactionForRpc legacy ? legacy.Nonce : null; TxHandlingOptions options = nonce is null ? TxHandlingOptions.ManagedNonce : TxHandlingOptions.None; return await SendTx(tx, options); } public virtual async Task> eth_sendRawTransaction(byte[] transaction) { try { Transaction tx = TxRlpDecoder.DecodeCompleteNotNull(transaction, RlpBehaviors.AllowUnsigned | RlpBehaviors.SkipTypedWrapping | RlpBehaviors.InMempoolForm); return await SendTx(tx); } catch (RlpException) { return ResultWrapper.Fail("Invalid RLP.", ErrorCodes.TransactionRejected); } } public virtual ResultWrapper eth_signTransaction(SignableTransactionForRpc rpcTx) { if (!_rpcConfig.EnableEthSignTransaction) return ResultWrapper.Fail("eth_signTransaction is disabled", ErrorCodes.MethodNotFound); // With the signing fields present, fees are checked before the rest of the request; a request carrying // blobs has its sidecar checked first. if (rpcTx.MissingSigningField() is null && rpcTx is not BlobTransactionForRpc { Blobs: not null } && _blockFinder.Head?.Header is { } signingHead && FeeDefaultRules.Error(rpcTx, _specProvider.GetSpec(signingHead).IsEip1559Enabled) is { } feeError) { return ResultWrapper.Fail(feeError, ErrorCodes.InvalidInput); } IReleaseSpec? frameSpec = rpcTx is FrameTransactionForRpc && _blockFinder.Head?.Header is { } frameHead ? _specProvider.GetSpec(frameHead) : null; Result txResult = rpcTx.ToSignableTransaction(frameSpec); if (!txResult.Success(out Transaction tx, out string error)) return ResultWrapper.Fail(error, ErrorCodes.InvalidInput); ulong chainId = _blockchainBridge.GetChainId(); tx.ChainId = chainId; ResultWrapper? feeCapError = CheckTxFeeCap(tx); if (feeCapError is not null) return feeCapError; // Sidecar must be attached before encode; signing only sets tx.Signature so the wrapper survives. if (rpcTx is BlobTransactionForRpc blobTx) { ProofVersion version = _blockFinder.Head?.Header is { } head ? _specProvider.GetSpec(head).BlobProofVersion : ProofVersion.V0; if (blobTx.TryAttachSidecar(tx, version) is { } attachError) return ResultWrapper.Fail(attachError, ErrorCodes.InvalidInput); } // The account is looked up only once the request itself is valid. if (!_wallet.TrySignTransaction(tx, chainId)) { string signingError = HoldsAccount(tx.SenderAddress) ? "authentication needed: password or unlock" : UnknownAccount; return ResultWrapper.Fail(signingError, ErrorCodes.InvalidInput); } tx.Hash = tx.CalculateHash(); if (_logger.IsInfo) _logger.Info($"eth_signTransaction signed tx {tx.Hash} from {tx.SenderAddress}"); return BuildSignedResult(tx); } private bool HoldsAccount(Address? address) => address is not null && _wallet.HasKey(address); private static ResultWrapper BuildSignedResult(Transaction tx) { const RlpBehaviors encodeBehaviors = RlpBehaviors.SkipTypedWrapping | RlpBehaviors.InMempoolForm; ArrayPoolList buffer = TxDecoder.Instance.EncodeToArrayPoolList(tx, encodeBehaviors); try { return ResultWrapper.Success(new SignTransactionResult { Raw = buffer, Tx = TransactionForRpc.FromTransaction(tx) }); } catch { buffer.Dispose(); throw; } } private ResultWrapper? CheckTxFeeCap(Transaction tx) { ulong cap = _rpcConfig.RpcTxFeeCap; if (cap == 0) return null; // Cap covers execution gas only (maxFeePerGas * gasLimit). Blob gas (4844) is intentionally // excluded — keeps parity with the reference implementation. UInt256 perGas = tx.Type >= TxType.EIP1559 ? tx.MaxFeePerGas : tx.GasPrice; UInt256 capWei = cap; // Reject overflow as cap-exceeded: a wraparound multiplication would otherwise let huge // fee values silently slip through. bool overflow = UInt256.MultiplyOverflow(perGas, (UInt256)tx.GasLimit, out UInt256 totalFee); if (!overflow && totalFee <= capWei) return null; decimal capEth = capWei.ToDecimal(null) / (decimal)Unit.Ether; string message = overflow ? string.Create(CultureInfo.InvariantCulture, $"tx fee (overflow ether) exceeds the configured cap ({capEth:F2} ether)") : string.Create(CultureInfo.InvariantCulture, $"tx fee ({totalFee.ToDecimal(null) / (decimal)Unit.Ether:F2} ether) exceeds the configured cap ({capEth:F2} ether)"); return ResultWrapper.Fail(message, ErrorCodes.InvalidInput); } public virtual async Task> eth_fillTransaction(SignableTransactionForRpc rpcTx) { BlockHeader? head = _blockFinder.Head?.Header; if (head is null) return ResultWrapper.Fail("No head block available", ErrorCodes.ResourceUnavailable); IReleaseSpec spec = _specProvider.GetSpec(head); ulong chainId = _blockchainBridge.GetChainId(); LegacyTransactionForRpc legacyTx = (LegacyTransactionForRpc)rpcTx; // A missing sender is the zero address, as in geth and as the spec leaves from optional. Address from = legacyTx.From ?? Address.Zero; // Fees are checked before the chain id, as they are when a transaction to send is filled; a request carrying // blobs has its sidecar checked first. if (rpcTx is not BlobTransactionForRpc { Blobs: not null } && FeeDefaultRules.Error(rpcTx, spec.IsEip1559Enabled) is { } feeError) return ResultWrapper.Fail(feeError, ErrorCodes.InvalidInput); if (legacyTx.ChainId is { } requestedChainId && requestedChainId != chainId) return ResultWrapper.Fail(RpcTransactionErrors.InvalidChainId(chainId, requestedChainId), ErrorCodes.InvalidInput); legacyTx.Nonce ??= _txPool.GetLatestPendingNonce(from); if (await FillFeesAndGas(rpcTx, head, spec) is { } fillError) return ResultWrapper.Fail(fillError.Error, fillError.ErrorCode); legacyTx.ChainId ??= chainId; Result txResult = rpcTx.ToValidatedTransaction(gasCap: _rpcConfig.GasCap, spec: spec); if (!txResult.Success(out Transaction tx, out string error)) return ResultWrapper.Fail(error, ErrorCodes.InvalidInput); tx.ChainId = chainId; if (rpcTx is BlobTransactionForRpc { Blobs: not null } withSidecar && withSidecar.TryAttachSidecar(tx, spec.BlobProofVersion) is { } attachError) { return ResultWrapper.Fail(attachError, ErrorCodes.InvalidInput); } return ResultWrapper.Success(new FillTransactionResult { Tx = TransactionForRpc.FromTransaction(tx) }); } /// Fills the fees and gas leaves out, as geth does for a transaction to build. /// The error that stops the fill, or null when the fill succeeds. private async Task<(string Error, int ErrorCode)?> FillFeesAndGas(SignableTransactionForRpc rpcTx, BlockHeader head, IReleaseSpec spec) { LegacyTransactionForRpc legacyTx = (LegacyTransactionForRpc)rpcTx; UInt256? blobBaseFee = head.ExcessBlobGas is { } excessBlobGas && BlobGasCalculator.TryCalculateFeePerBlobGas(excessBlobGas, spec.BlobBaseFeeUpdateFraction, out UInt256 feePerBlobGas) ? feePerBlobGas : null; // Only the defaults FillDefaults will read are estimated: an estimate can scan recent blocks or call L1. TxFillContext fillContext = new() { GasPrice = rpcTx is not EIP1559TransactionForRpc && legacyTx.GasPrice is null ? await _gasPriceOracle.GetGasPriceEstimate() : default, MaxPriorityFeePerGas = rpcTx is EIP1559TransactionForRpc { GasPrice: null, MaxPriorityFeePerGas: null } ? _gasPriceOracle.GetMaxPriorityGasFeeEstimate() : default, BaseFee = BaseFeeCalculator.Calculate(head, _specProvider.GetSpecFor1559(head.Number + 1)), BlobBaseFee = blobBaseFee, Spec = spec, }; Result fillResult = rpcTx.FillDefaults(fillContext); if (!fillResult) return (fillResult.Error!, ErrorCodes.InvalidInput); // A fee cap left in place next to a filled priority fee must still cover it. if (rpcTx is EIP1559TransactionForRpc { MaxFeePerGas: { } filledFeeCap, MaxPriorityFeePerGas: { } filledPriorityFee } && FeeDefaultRules.OrderError(filledFeeCap, filledPriorityFee) is { } orderError) { return (orderError, ErrorCodes.InvalidInput); } if (rpcTx is FrameTransactionForRpc frameTx && NeedsFrameGas(frameTx)) { using RentedTimeout timeout = _rpcConfig.RentTimeout(); Result frameGasResult = FillFrameGas(frameTx, head, timeout.Token, out int errorCode, checksFork: false); if (!frameGasResult) return (frameGasResult.Error!, errorCode); frameTx.Frames = frameGasResult.Data; } if (rpcTx.Gas is null) { // The transaction is built for a later block, so its estimate is outside the fork rule as well. ResultWrapper gasEstimate = ExecuteWithFrameGas( new EstimateGasTxExecutor(_blockchainBridge, _blockFinder, _rpcConfig, _specProvider) { ChecksFork = false }, rpcTx, BlockParameter.Latest, stateOverride: null); if (gasEstimate.Result.ResultType != ResultType.Success) return (gasEstimate.Result.Error ?? "gas estimation failed", gasEstimate.ErrorCode); rpcTx.Gas = (ulong)gasEstimate.Data!.Value; } return null; } public async Task> eth_sendRawTransactionSync(byte[] transaction, ulong? timeoutMs = null) { int maxConcurrent = _rpcConfig.RpcTxSyncMaxConcurrentRequests; if (Interlocked.Increment(ref _syncRequests) > maxConcurrent && maxConcurrent > 0) { Interlocked.Decrement(ref _syncRequests); throw new LimitExceededException("Too many concurrent eth_sendRawTransactionSync requests."); } try { return await SendRawTransactionAndWaitForReceipt(transaction, timeoutMs); } finally { Interlocked.Decrement(ref _syncRequests); } } private async Task> SendRawTransactionAndWaitForReceipt(byte[] transaction, ulong? timeoutMs) { int waitMs = ResolveSyncTimeoutMs(timeoutMs); using CancellationTokenSource cts = new(waitMs); // Submit via the virtual eth_sendRawTransaction so subclass overrides // propagate without needing a separate sync override. ResultWrapper sendResult = await eth_sendRawTransaction(transaction); if (sendResult.Result.ResultType != ResultType.Success) { return ResultWrapper.Fail(sendResult.Result.Error ?? "Send failed", sendResult.ErrorCode); } Hash256 hash = sendResult.Data; while (true) { // Snapshot the next-head Task BEFORE the receipt check: if a head arrives between // the check and the await, the snapshot is already completed and the loop re-checks // immediately. Snapshotting after the check would miss that signal. Task nextHead = _headBlockSignal.NextHeadTask; ResultWrapper receiptResult = eth_getTransactionReceipt(hash); if (receiptResult.Data is not null) { return receiptResult; } try { await nextHead.WaitAsync(cts.Token); } catch (OperationCanceledException) { return ResultWrapper.Fail( $"Transaction {hash} was added to the pool but not included within {waitMs}ms.", ErrorCodes.TxSyncTimeout, hash); } } } 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 async Task> SendTx(Transaction tx, TxHandlingOptions txHandlingOptions = TxHandlingOptions.None) { try { (Hash256 txHash, AcceptTxResult? acceptTxResult) = await _txSender.SendTransaction(tx, txHandlingOptions | TxHandlingOptions.PersistentBroadcast); if (acceptTxResult.Equals(AcceptTxResult.Accepted)) return ResultWrapper.Success(txHash); return acceptTxResult.Equals(AcceptTxResult.SignFailed) ? ResultWrapper.Fail("authentication needed: password or unlock", ErrorCodes.AccountLocked) : ResultWrapper.Fail(acceptTxResult?.ToString() ?? string.Empty, ErrorCodes.TransactionRejected); } catch (Exception e) { if (_logger.IsError) _logger.Error("Failed to send transaction.", e); return ResultWrapper.Fail(e.Message, ErrorCodes.TransactionRejected); } } public virtual ResultWrapper eth_call(SignableTransactionForRpc transactionCall, BlockParameter? blockParameter = null, Dictionary? stateOverride = null, BlockOverride? blockOverride = null) => ExecuteWithFrameGas(new CallTxExecutor(_blockchainBridge, _blockFinder, _rpcConfig, _specProvider), transactionCall, blockParameter, stateOverride, blockOverride); public ResultWrapper>> eth_simulateV1(SimulatePayload payload, BlockParameter? blockParameter = null) => new SimulateTxExecutor(_blockchainBridge, _blockFinder, _rpcConfig, _specProvider, new SimulateBlockMutatorTracerFactory(), secondsPerSlot: _secondsPerSlot) .Execute(payload, blockParameter); public virtual ResultWrapper eth_estimateGas(SignableTransactionForRpc transactionCall, BlockParameter? blockParameter, Dictionary? stateOverride = null, BlockOverride? blockOverride = null) => ExecuteWithFrameGas(new EstimateGasTxExecutor(_blockchainBridge, _blockFinder, _rpcConfig, _specProvider), transactionCall, blockParameter, stateOverride, blockOverride); public virtual ResultWrapper eth_createAccessList(SignableTransactionForRpc transactionCall, BlockParameter? blockParameter = null, Dictionary? stateOverride = null, bool optimize = true) { // The fees are filled once, against the header the call then runs on, before anything prices the request. SearchResult search = _blockFinder.SearchForHeader(blockParameter); CreateAccessListTxExecutor.FeeDefaults feeDefaults = search.IsError ? default : CreateAccessListTxExecutor.FillFeeDefaults(transactionCall, search.Object!, _specProvider.GetSpec(search.Object!).IsEip1559Enabled, _gasPriceOracle); CreateAccessListTxExecutor executor = new(_blockchainBridge, _blockFinder, _rpcConfig, _specProvider, feeDefaults, optimize); // A request whose fees are rejected gets that error as it would with explicit frame limits. return feeDefaults.Error is null ? ExecuteWithFrameGas(executor, transactionCall, blockParameter, stateOverride, searchResult: search) : executor.ExecuteTx(transactionCall, blockParameter, stateOverride, searchResult: search); } public ResultWrapper eth_getBlockByHash(Hash256 blockHash, bool returnFullTransactionObjects) => GetBlock(new BlockParameter(blockHash), returnFullTransactionObjects); public ResultWrapper eth_getBlockByNumber(BlockParameter blockParameter, bool returnFullTransactionObjects) => GetBlock(blockParameter, returnFullTransactionObjects); protected virtual ResultWrapper GetBlock(BlockParameter blockParameter, bool returnFullTransactionObjects) { if (!returnFullTransactionObjects && _hashesOnlyBlockReader?.Find(_blockFinder, blockParameter) is { } hashesOnly) { BlockForRpc? hashesOnlyForRpc = _blockForRpcFactory.Create(hashesOnly.Block, includeFullTransactionData: false, _specProvider, skipTxs: true); hashesOnlyForRpc?.Transactions = BlockTransactions.FromHashes(hashesOnly.TransactionHashes); return ResultWrapper.Success(hashesOnlyForRpc); } SearchResult searchResult = _blockFinder.SearchForBlock(blockParameter, true); if (searchResult.IsError) { return ResultWrapper.Success(null); } Block block = searchResult.Object!; if (returnFullTransactionObjects && block is not null) { _blockchainBridge.RecoverTxSenders(block); } if (block is null) { return ResultWrapper.Success(null); } BlockForRpc? blockForRpc = _blockForRpcFactory.Create(block, returnFullTransactionObjects, _specProvider); if (blockForRpc is null) { return ResultWrapper.Success(null); } if (blockParameter.Type == BlockParameterType.Pending) { blockForRpc.Hash = null; blockForRpc.Nonce = null; blockForRpc.Miner = null; } return ResultWrapper.Success(blockForRpc); } public ResultWrapper eth_getHeaderByHash(Hash256 blockHash) => GetHeader(new BlockParameter(blockHash)); public ResultWrapper eth_getHeaderByNumber(BlockParameter blockParameter) => GetHeader(blockParameter); private ResultWrapper GetHeader(BlockParameter blockParameter) { // The pending tag returns null as proposed in ethereum/execution-apis#877; the block methods // keep their partially-nulled pending object, which that spec does not cover. if (blockParameter.Type == BlockParameterType.Pending) { return ResultWrapper.Success(null); } // SearchForHeader avoids loading the block body — header endpoints don't need transactions/uncles. SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return ResultWrapper.Success(null); } return ResultWrapper.Success(_blockForRpcFactory.CreateHeader(searchResult.Object!, _specProvider)); } public virtual ResultWrapper eth_getTransactionByHash(Hash256 transactionHash) { if (!_blockchainBridge.TryGetTransaction(transactionHash, out TransactionLookupResult? transactionResult, checkTxnPool: true)) { return ResultWrapper.Success(null); } RecoverTxSenderIfNeeded(transactionResult.Value.Transaction); TransactionForRpcContext extraData = transactionResult.Value.ExtraData; TransactionForRpc transactionModel = TransactionForRpc.FromTransaction( transaction: transactionResult.Value.Transaction, extraData: extraData); if (_logger.IsTrace) _logger.Trace($"eth_getTransactionByHash request {transactionHash}, result: {transactionModel.Hash}"); return ResultWrapper.Success(transactionModel); } public ResultWrapper?> eth_getRawTransactionByHash(Hash256 transactionHash) { if (!_blockchainBridge.TryGetTransaction(transactionHash, out TransactionLookupResult? transactionResult, checkTxnPool: true)) { return ResultWrapper?>.Success(null); } Transaction transaction = transactionResult.Value.Transaction; RlpBehaviors encodingSettings = RlpBehaviors.SkipTypedWrapping | (transaction.IsInMempoolForm() ? RlpBehaviors.InMempoolForm : RlpBehaviors.None); return ResultWrapper?>.Success(TxRlpDecoder.EncodeToArrayPoolList(transaction, encodingSettings)); } public virtual ResultWrapper eth_pendingTransactions() { Transaction[] transactions = _txPool.GetPendingTransactions(); TransactionForRpc[] transactionsModels = new TransactionForRpc[transactions.Length]; for (int i = 0; i < transactions.Length; i++) { Transaction transaction = transactions[i]; RecoverTxSenderIfNeeded(transaction); transactionsModels[i] = TransactionForRpc.FromTransaction(transaction, new(_specProvider.ChainId)); transactionsModels[i].BlockHash = Keccak.Zero; } if (_logger.IsTrace) _logger.Trace($"eth_pendingTransactions request, result: {transactionsModels.Length}"); return ResultWrapper.Success(transactionsModels); } public ResultWrapper eth_getTransactionByBlockHashAndIndex(Hash256 blockHash, UInt256 positionIndex) { ResultWrapper result = GetTransactionByBlockAndIndex(new BlockParameter(blockHash), positionIndex); if (_logger.IsTrace && result.Result.ResultType == ResultType.Success) _logger.Trace($"eth_getTransactionByBlockHashAndIndex request {blockHash}, index: {positionIndex}, result: {result.Data?.Hash}"); return result; } public ResultWrapper eth_getTransactionByBlockNumberAndIndex(BlockParameter blockParameter, UInt256 positionIndex) { ResultWrapper result = GetTransactionByBlockAndIndex(blockParameter, positionIndex); if (_logger.IsTrace && result.Result.ResultType == ResultType.Success) _logger.Trace($"eth_getTransactionByBlockNumberAndIndex request {blockParameter}, index: {positionIndex}, result: {result.Data?.Hash}"); return result; } public ResultWrapper?> eth_getRawTransactionByBlockHashAndIndex(Hash256 blockHash, UInt256 positionIndex) { ResultWrapper?> 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?.Count ?? 0}"); return result; } public ResultWrapper?> eth_getRawTransactionByBlockNumberAndIndex(BlockParameter blockParameter, UInt256 positionIndex) { ResultWrapper?> result = GetRawTransactionByBlockAndIndex(blockParameter, positionIndex); if (_logger.IsTrace && result.Result.ResultType == ResultType.Success) _logger.Trace($"eth_getRawTransactionByBlockNumberAndIndex request {blockParameter}, index: {positionIndex}, result length: {result.Data?.Count ?? 0}"); return result; } private ResultWrapper?> GetRawTransactionByBlockAndIndex(BlockParameter blockParameter, UInt256 positionIndex) { if (TryGetStoredTransaction(blockParameter, positionIndex, out _, out Transaction? stored, out _)) { return ResultWrapper?>.Success(stored is null ? null : TxRlpDecoder.EncodeToArrayPoolList(stored, RlpBehaviors.SkipTypedWrapping)); } SearchResult searchResult = _blockFinder.SearchForBlock(blockParameter); if (searchResult.IsError) { return ResultWrapper?>.Success(null); } Block block = searchResult.Object!; if (positionIndex >= block.Transactions.Length) { return ResultWrapper?>.Success(null); } Transaction transaction = block.Transactions[(int)positionIndex]; // Block-stored txs never carry a sidecar (blob commitments live separately), so consensus form only. return ResultWrapper?>.Success(TxRlpDecoder.EncodeToArrayPoolList(transaction, RlpBehaviors.SkipTypedWrapping)); } protected virtual ResultWrapper GetTransactionByBlockAndIndex(BlockParameter blockParameter, UInt256 positionIndex) { BlockHeader header; Transaction transaction; if (TryGetStoredTransaction(blockParameter, positionIndex, out BlockHeader? storedHeader, out Transaction? stored, out int transactionCount)) { if (stored is null) return ResultWrapper.Success(null); header = storedHeader!; transaction = stored; } else { SearchResult searchResult = _blockFinder.SearchForBlock(blockParameter); if (searchResult.IsError) { return ResultWrapper.Success(null); } Block block = searchResult.Object!; if (positionIndex >= block.Transactions.Length) { return ResultWrapper.Success(null); } header = block.Header; transaction = block.Transactions[(int)positionIndex]; transactionCount = block.Transactions.Length; } transaction.SenderAddress ??= FindStoredTxSender(header, transactionCount, (int)positionIndex); RecoverTxSenderIfNeeded(transaction); TransactionForRpcContext extraData = new( chainId: _specProvider.ChainId, blockHash: header.Hash, blockNumber: header.Number, txIndex: (int)positionIndex, blockTimestamp: header.Timestamp, baseFee: header.BaseFeePerGas, receipt: null); TransactionForRpc transactionModel = TransactionForRpc.FromTransaction(transaction, extraData); return ResultWrapper.Success(transactionModel); } public ResultWrapper eth_getUncleByBlockHashAndIndex(Hash256 blockHash, UInt256 positionIndex) => GetUncle(new BlockParameter(blockHash), positionIndex); public ResultWrapper eth_getUncleByBlockNumberAndIndex(BlockParameter blockParameter, UInt256 positionIndex) => GetUncle(blockParameter, positionIndex); private ResultWrapper GetUncle(BlockParameter blockParameter, UInt256 positionIndex) { SearchResult searchResult = _blockFinder.SearchForBlock(blockParameter); if (searchResult.IsError) { return ResultWrapper.Success(null); } Block block = searchResult.Object!; if (positionIndex >= block.Uncles.Length) { return ResultWrapper.Success(null); } BlockHeader uncleHeader = block.Uncles[(int)positionIndex]; return ResultWrapper.Success(_blockForRpcFactory.Create(new Block(uncleHeader), false, _specProvider)); } public ResultWrapper eth_newFilter(Filter filter) { BlockParameter fromBlock = filter.FromBlock; BlockParameter toBlock = filter.ToBlock; int filterId = _blockchainBridge.NewFilter(fromBlock, toBlock, filter.Address, filter.Topics); return ResultWrapper.Success((UInt256)filterId); } public ResultWrapper eth_newBlockFilter() { int filterId = _blockchainBridge.NewBlockFilter(); return ResultWrapper.Success((UInt256)filterId); } public ResultWrapper eth_newPendingTransactionFilter() { int filterId = _blockchainBridge.NewPendingTransactionFilter(); return ResultWrapper.Success((UInt256)filterId); } public ResultWrapper eth_uninstallFilter(UInt256 filterId) { // Nethermind uses int for filter IDs, which is okay, but other clients use UInt256. // If filterId is greater than int.MaxValue, it could not have been created by Nethermind. // In that case, return false instead of throwing an internal cast error. if (filterId > int.MaxValue) return ResultWrapper.Success(false); return ResultWrapper.Success(_blockchainBridge.UninstallFilter((int)filterId)); } public ResultWrapper> eth_getFilterChanges(UInt256 filterId) { int id = filterId <= int.MaxValue ? (int)filterId : -1; FilterType filterType = _blockchainBridge.GetFilterType(id); switch (filterType) { case FilterType.BlockFilter: { return _blockchainBridge.FilterExists(id) ? ResultWrapper>.Success(_blockchainBridge.GetBlockFilterChanges(id)) : ResultWrapper>.Fail(ErrorMessages.FilterNotFound, ErrorCodes.InvalidInput); } case FilterType.PendingTransactionFilter: { return _blockchainBridge.FilterExists(id) ? ResultWrapper>.Success(_blockchainBridge.GetPendingTransactionFilterChanges(id)) : ResultWrapper>.Fail(ErrorMessages.FilterNotFound, ErrorCodes.InvalidInput); } case FilterType.LogFilter: { return _blockchainBridge.FilterExists(id) ? ResultWrapper>.Success(_blockchainBridge.GetLogFilterChanges(id)) : ResultWrapper>.Fail(ErrorMessages.FilterNotFound, ErrorCodes.InvalidInput); } default: { return ResultWrapper>.Fail($"Filter type {filterType} is not supported.", ErrorCodes.InvalidInput); } } } public ResultWrapper> eth_getFilterLogs(UInt256 filterId) { LogFilter? logFilter = filterId <= int.MaxValue ? _blockchainBridge.GetLogFilter((int)filterId) : null; return logFilter is null ? ResultWrapper>.Fail(ErrorMessages.FilterNotFound, ErrorCodes.InvalidInput) : GetLogs(logFilter, logFilter.FromBlock, logFilter.ToBlock); } public ResultWrapper> eth_getLogs(Filter filter) { BlockParameter fromBlock = filter.FromBlock!; BlockParameter toBlock = filter.ToBlock!; LogFilter logFilter = _blockchainBridge.GetFilter(fromBlock, toBlock, filter.Address, filter.Topics); // ReSharper disable once ConditionIsAlwaysTrueOrFalse - can be null in tests if (logFilter is not null && filter is not null) logFilter.UseIndex = filter.UseIndex; return GetLogs(logFilter, fromBlock, toBlock); } private ResultWrapper> GetLogs(LogFilter logFilter, BlockParameter fromBlock, BlockParameter toBlock) { ResultWrapper> FailWithNoHeadersSyncedYet(SearchResult blockResult) => GetFailureResult, BlockHeader>(blockResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); // because of lazy evaluation of enumerable, we need to do the validation here first CancellationTokenSource timeout = BuildTimeoutCancellationTokenSource(); bool timeoutTransferred = false; try { CancellationToken cancellationToken = timeout.Token; if (_blockFinder.IsRangeInFuture(fromBlock, toBlock)) { return ResultWrapper>.Fail(BlockFinderExtensions.BlockRangeInFuture, ErrorCodes.InvalidParams); } if (fromBlock.BlockNumber > toBlock.BlockNumber) { return ResultWrapper>.Fail("invalid block range params", ErrorCodes.InvalidParams); } SearchResult fromResult = blockFinder.SearchForHeader(fromBlock); if (fromResult.IsError) { return FailWithNoHeadersSyncedYet(fromResult); } cancellationToken.ThrowIfCancellationRequested(); SearchResult toResult; if (fromBlock == toBlock) { toResult = fromResult; } else { toResult = blockFinder.SearchForHeader(toBlock); if (toResult.IsError) { return FailWithNoHeadersSyncedYet(toResult); } } cancellationToken.ThrowIfCancellationRequested(); BlockHeader fromBlockHeader = fromResult.Object!; BlockHeader toBlockHeader = toResult.Object!; IEnumerable filterLogs = _blockchainBridge.GetLogs(logFilter, fromBlockHeader, toBlockHeader, cancellationToken); return GetLogsResponse(filterLogs, timeout, out timeoutTransferred); } catch (ResourceNotFoundException e) { return GetLogsFailure(e); } finally { if (!timeoutTransferred) { JsonRpcConfigExtension.ReturnTimeoutCancellationToken(timeout); } } } // While receipts sync, the processed head can trail the synced headers, so a missing block is reported as not synced yet. private ResultWrapper> GetLogsFailure(ResourceNotFoundException e) => e is BlockNotProcessedException && !_ethSyncingInfo.SyncMode.HaveNotSyncedReceiptsYet() ? ResultWrapper>.Fail(e.Message, ErrorCodes.ResourceNotFound) : ResultWrapper>.Fail(ErrorMessages.PrunedHistoryUnavailable, ErrorCodes.PrunedHistoryUnavailable, _ethSyncingInfo.SyncMode.HaveNotSyncedReceiptsYet()); // https://github.com/ethereum/EIPs/issues/1186 public ResultWrapper eth_getProof(Address accountAddress, StorageKeys storageKeys, BlockParameter? blockParameter) { if (storageKeys.Count > GetProofStorageKeyLimit) { return ResultWrapper.Fail( $"storageKeys: {storageKeys.Count} is over the query limit {GetProofStorageKeyLimit}.", ErrorCodes.InvalidParams); } SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader header = searchResult.Object; if (!_blockchainBridge.HasStateForBlock(header!)) { return _ethSyncingInfo.SyncMode.HaveNotSyncedStateYet() || !WasStatePruned(header) ? GetStateFailureResult(header) : ResultWrapper.Fail(ErrorMessages.PrunedHistoryUnavailable, ErrorCodes.PrunedHistoryUnavailable); } using RentedTimeout timeout = _rpcConfig.RentTimeout(); AccountProofCollector accountProofCollector = new(accountAddress, storageKeys, timeout.Token); _blockchainBridge.RunTreeVisitor(accountProofCollector, header!); return ResultWrapper.Success(EchoStorageKeys(accountProofCollector.BuildResult(), storageKeys)); } public ResultWrapper eth_chainId() { ResultWrapper? cachedResponse = Volatile.Read(ref _chainIdResponse); if (cachedResponse is not null) { return cachedResponse; } try { ulong chainId = _blockchainBridge.GetChainId(); CachedChainIdResponse response = new(chainId); return Interlocked.CompareExchange(ref _chainIdResponse, response, null) ?? response; } catch (Exception ex) { return ResultWrapper.Fail(ex.Message, ErrorCodes.InternalError, 0ul); } } private sealed class CachedChainIdResponse : ResultWrapper, IJsonRpcRawResponse { private readonly byte[] _rawResult; public CachedChainIdResponse(ulong chainId) { Data = chainId; string hexChainId = chainId.ToHexString(skipLeadingZeros: true); byte[] rawResult = GC.AllocateUninitializedArray(hexChainId.Length + 2); rawResult[0] = (byte)'"'; Encoding.ASCII.GetBytes(hexChainId, rawResult.AsSpan(1)); rawResult[^1] = (byte)'"'; _rawResult = rawResult; } void IJsonRpcRawResponse.WriteRaw(IBufferWriter writer) => JsonRpcResponseWriter.WriteRawSuccess(writer, _rawResult, in IdRef); } protected void RecoverTxSenderIfNeeded(Transaction transaction) => transaction.SenderAddress ??= _blockchainBridge.RecoverTxSender(transaction); /// Gets the transaction at from the stored block, decoding no other /// transaction. False for a pending or earliest block, or a block whose body is not stored, which the caller then /// searches as before, so the results and errors stay the same. private bool TryGetStoredTransaction(BlockParameter blockParameter, UInt256 positionIndex, out BlockHeader? header, out Transaction? transaction, out int transactionCount) { // Pending has no stored block, and the earliest header lookup throws where the block search returns null. header = blockParameter.Type is BlockParameterType.Pending or BlockParameterType.Earliest ? null : _blockFinder.FindHeader(blockParameter); if (header is null || positionIndex > int.MaxValue || !_blockchainBridge.TryGetStoredTransaction(header, (int)positionIndex, out transaction, out transactionCount)) { transaction = null; transactionCount = 0; return false; } return true; } /// Returns the sender stored with the receipt of the block's transaction at , if any. private Address? FindStoredTxSender(BlockHeader header, int transactionCount, int index) { if (_storedReceiptFinder?.TryGetReceiptsIterator(header.Number, header.Hash!, out ReceiptsIterator iterator) is not true) return null; using ReceiptsIterator receipts = iterator; if (receipts.CountRemaining() != transactionCount) return null; receipts.Skip(index); // An absent sender decodes to the shared zero address, which a stored one never aliases. return receipts.TryGetNext(out TxReceiptStructRef receipt) && receipt.Sender.Bytes != Address.Zero.Bytes ? receipt.Sender.ToAddress() : null; } private ResultWrapper> GetLogsResponse( IEnumerable filterLogs, CancellationTokenSource timeout, out bool timeoutTransferred) { timeoutTransferred = false; bool enforceLogsLimits = JsonRpcContext.Current?.IsAuthenticated != true; bool enforceMaxLogs = enforceLogsLimits && _rpcConfig.MaxLogsPerResponse != 0; if (_rpcConfig.EnableLogsStreamMode) { long? maxLogsResponseBodySize = enforceLogsLimits ? _rpcConfig.MaxLogsResponseBodySize : null; long? maxBatchResponseBodySize = enforceLogsLimits ? _rpcConfig.MaxBatchResponseBodySize : null; ResultWrapper> response = ResultWrapper>.Success( new LogsStreamableResult(filterLogs, _rpcConfig.MaxLogsPerResponse, enforceMaxLogs, maxLogsResponseBodySize, maxBatchResponseBodySize, timeout, _logger)); timeoutTransferred = true; return response; } SerializedLogsResult logs = new(filterLogs); try { foreach (SerializedLogs blockLogs in FoundLogs.Serialize(filterLogs)) { logs.Add(blockLogs); if (enforceMaxLogs && logs.Count > _rpcConfig.MaxLogsPerResponse) { logs.Dispose(); return ResultWrapper>.Fail($"Too many logs requested. Max logs per response is {_rpcConfig.MaxLogsPerResponse}.", ErrorCodes.LimitExceeded); } } } catch { logs.Dispose(); throw; } return ResultWrapper>.Success(logs); } public ResultWrapper eth_getAccount(Address accountAddress, BlockParameter? blockParameter) { SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader header = searchResult.Object!; if (!_blockchainBridge.HasStateForBlock(header)) return GetStateFailureResult(header); return ResultWrapper.Success( _stateReader.TryGetAccount(header, accountAddress, out AccountStruct account) ? new AccountForRpc(account) : null); } public ResultWrapper eth_getAccountInfo(Address accountAddress, BlockParameter? blockParameter) { SearchResult searchResult = _blockFinder.SearchForHeader(blockParameter); if (searchResult.IsError) { return GetFailureResult(searchResult, _ethSyncingInfo.SyncMode.HaveNotSyncedHeadersYet()); } BlockHeader header = searchResult.Object!; if (!_blockchainBridge.HasStateForBlock(header)) return GetStateFailureResult(header); return ResultWrapper.Success( _stateReader.TryGetAccount(header, accountAddress, out AccountStruct account) ? new AccountInfoForRpc { Balance = account.Balance, Nonce = account.Nonce, Code = _stateReader.GetCode(account.CodeHash) ?? [] } : AccountInfoForRpc.Empty); } protected static ResultWrapper GetFailureResult(SearchResult searchResult, bool isTemporary) where TSearch : class => ResultWrapper.Fail(searchResult, isTemporary && searchResult.ErrorCode == ErrorCodes.ResourceNotFound); /// Trims each proof key that was not sent full width, so it is echoed as a quantity. internal static AccountProof EchoStorageKeys(AccountProof proof, StorageKeys storageKeys) { StorageProof[] storageProofs = proof.StorageProofs; for (int i = 0; i < storageProofs.Length; i++) { if (!storageKeys.IsFullWidth(i)) { storageProofs[i].Key = storageProofs[i].Key!.WithoutLeadingZeros().ToArray(); } } return proof; } /// /// Blocks ahead of the processed head and side-chain blocks may simply never have been processed. /// private bool WasStatePruned(BlockHeader header) => _blockFinder.Head?.Number >= header.Number && _blockFinder.IsMainChain(header); private ResultWrapper GetStateFailureResult(BlockHeader header) => ResultWrapper.Fail($"No state available for block {header.ToString(BlockHeader.Format.FullHashAndNumber)}", ErrorCodes.ResourceUnavailable, _ethSyncingInfo.SyncMode.HaveNotSyncedStateYet()); public virtual ResultWrapper eth_getTransactionReceipt(Hash256 txHash) { (TxReceipt? receipt, ulong blockTimestamp, TxGasInfo? gasInfo, int logIndexStart) = _blockchainBridge.GetTxReceiptInfo(txHash); if (receipt is null || gasInfo is null) { return ResultWrapper.Success(null); } if (_logger.IsTrace) _logger.Trace($"eth_getTransactionReceipt request {txHash}, result: {txHash}"); return ResultWrapper.Success(new(txHash, receipt, blockTimestamp, gasInfo.Value, logIndexStart)); } public virtual ResultWrapper?> eth_getBlockReceipts(BlockParameter blockParameter) { SearchResult searchResult = blockFinder.SearchForBlock(blockParameter); return searchResult switch { { IsError: true } => ResultWrapper?>.Success(null), _ => _receiptFinder.GetBlockReceipts(searchResult.Object, _specProvider) }; } public ResultWrapper eth_config() { ForkActivationsSummary forks = forkInfo.GetForkActivationsSummary(_blockFinder.Head?.Header); return ResultWrapper.Success(JsonSerializer.SerializeToNode(new ForkConfigSummary { Current = ForkConfigFactory.Create(forks.Current, _specProvider), Next = ForkConfigFactory.Create(forks.Next, _specProvider), Last = ForkConfigFactory.Create(forks.Last, _specProvider) }, TypeInfoJsonSerializer.GetTypeInfo(UnchangedDictionaryKeyOptions))!); } public ResultWrapper eth_getBlockAccessList(BlockParameter blockParameter) { // A pending block has no committed access list yet. if (blockParameter.Type == BlockParameterType.Pending) { return ResultWrapper.Success(null); } SearchResult searchResult = _blockFinder.SearchForHeaderWithBody(blockParameter); if (searchResult.IsError) { // Unknown blocks yield null per execution-apis; pruned/non-canonical failures keep their error. return searchResult.Error == BlockFinderExtensions.HeaderNotFound ? ResultWrapper.Success(null) : ResultWrapper.Fail(searchResult); } BlockHeader header = searchResult.Object!; if (header.BlockAccessListHash is null) { // Pre-EIP-7928 block: the resource does not exist. return ResultWrapper.Fail("Resource not found", ErrorCodes.BlockAccessListResourceNotFound); } ReadOnlyBlockAccessList? bal = blockchainBridge.GetBlockAccessList(header.Number, header.Hash!); return bal is null ? ResultWrapper.Fail(ErrorMessages.PrunedHistoryUnavailable, ErrorCodes.PrunedHistoryUnavailable) : ResultWrapper.Success(AccountAccessForRpc.FromBlockAccessList(bal)); } public ResultWrapper eth_capabilities() => ResultWrapper.Success(_capabilitiesProvider.GetCapabilities()); private CancellationTokenSource BuildTimeoutCancellationTokenSource() => _rpcConfig.BuildTimeoutCancellationToken(); }