Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Facade/BlockchainBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ public Hash256[] GetPendingTransactionFilterChanges(int filterId) =>

public void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock) where TCtx : struct, INodeContext<TCtx> => stateReader.RunTreeVisitor(treeVisitor, baseBlock);

public ProofDiagnostics RunTreeVisitorMetered<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock) where TCtx : struct, INodeContext<TCtx> => stateReader.RunTreeVisitorMetered(treeVisitor, baseBlock);

public bool HasStateForBlock(BlockHeader? baseBlock) => stateReader.HasStateForBlock(baseBlock);

public IEnumerable<FilterLog> FindLogs(LogFilter filter, BlockHeader fromBlock, BlockHeader toBlock, CancellationToken cancellationToken = default) => logFinder.FindLogs(filter, fromBlock, toBlock, cancellationToken);
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Facade/IBlockchainBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public interface IBlockchainBridge : ILogFinder

bool TryGetLogs(int filterId, out IEnumerable<FilterLog> filterLogs, CancellationToken cancellationToken = default);
void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock) where TCtx : struct, INodeContext<TCtx>;
ProofDiagnostics RunTreeVisitorMetered<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock) where TCtx : struct, INodeContext<TCtx>;
Comment thread
AnkushinDaniil marked this conversation as resolved.
Outdated
bool HasStateForBlock(BlockHeader? baseBlock);

Witness GenerateExecutionWitness(BlockHeader parent, Block block);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Collections.Generic;
using System.Threading.Tasks;
using Autofac;
using FluentAssertions;
using Nethermind.Blockchain;
using Nethermind.Blockchain.Find;
using Nethermind.Blockchain.Headers;
using Nethermind.Blockchain.Receipts;
using Nethermind.Config;
using Nethermind.Consensus.Processing;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using Nethermind.Core.Test;
using Nethermind.Core.Test.Builders;
using Nethermind.Core.Test.Db;
using Nethermind.Core.Test.Modules;
using Nethermind.Crypto;
using Nethermind.Db;
using Nethermind.Evm.State;
using Nethermind.Int256;
using Nethermind.JsonRpc.Modules;
using Nethermind.JsonRpc.Modules.Proof;
using Nethermind.Logging;
using Nethermind.Specs;
using Nethermind.Specs.Forks;
using Nethermind.State;
using NUnit.Framework;

namespace Nethermind.JsonRpc.Test.Modules.Proof;

[Parallelizable(ParallelScope.None)]
public class ProofRpcModuleMetaTests
{
private IProofRpcModule _proofRpcModule = null!;
private IBlockTree _blockTree = null!;
private IDbProvider _dbProvider = null!;
private TestSpecProvider _specProvider = null!;
private WorldStateManager _worldStateManager = null!;
private IContainer _container = null!;

[SetUp]
public async Task Setup()
{
_dbProvider = await TestMemDbProvider.InitAsync();
_worldStateManager = TestWorldStateFactory.CreateWorldStateManagerForTest(_dbProvider, LimboLogs.Instance);

Hash256 stateRoot;
IWorldState worldState = new WorldState(_worldStateManager.GlobalWorldState, LimboLogs.Instance);
using (System.IDisposable _ = worldState.BeginScope(IWorldState.PreGenesis))
{
worldState.CreateAccount(TestItem.AddressA, 100_000);
worldState.CreateAccount(TestItem.AddressB, 200_000);
worldState.CreateAccount(TestItem.AddressC, 300_000);
worldState.Commit(London.Instance);
worldState.CommitTree(0);
stateRoot = worldState.StateRoot;
}

InMemoryReceiptStorage receiptStorage = new();
_specProvider = new TestSpecProvider(London.Instance);
BlockTreeBuilder blockTreeBuilder = Build.A.BlockTree(new Block(Build.A.BlockHeader.WithStateRoot(stateRoot).TestObject, new BlockBody()), _specProvider)
.WithTransactions(receiptStorage)
.OfChainLength(5);
_blockTree = blockTreeBuilder.TestObject;

_container = new ContainerBuilder()
.AddModule(new TestNethermindModule(new ConfigProvider()))
.AddSingleton<ISpecProvider>(_specProvider)
.AddSingleton<IBlockPreprocessorStep>(new CompositeBlockPreprocessorStep(new RecoverSignatures(new EthereumEcdsa(TestBlockchainIds.ChainId), _specProvider, LimboLogs.Instance)))
.AddSingleton<IBlockTree>(_blockTree)
.AddSingleton<IDbProvider>(_dbProvider)
.AddSingleton<IHeaderFinder>(blockTreeBuilder.HeaderStore)
.AddSingleton<IReceiptStorage>(receiptStorage)
.AddSingleton<IWorldStateManager>(_worldStateManager)
.Build();
_proofRpcModule = _container.Resolve<IRpcModuleFactory<IProofRpcModule>>().Create();
}

[TearDown]
public void TearDown() => _container.Dispose();

[Test]
public void Returns_proof_payload_alongside_meta()
{
ResultWrapper<AccountProofWithMeta> result = _proofRpcModule.proof_getProofWithMeta(
TestItem.AddressA, new HashSet<UInt256>(), BlockParameter.Earliest);

result.Result.ResultType.Should().Be(ResultType.Success);
AccountProofWithMeta payload = result.Data;

payload.Should().NotBeNull();
payload.Proof.Should().NotBeNull();
payload.Proof.Address.Should().Be(TestItem.AddressA);
payload.Proof.Balance.Should().Be(100_000);
payload.Proof.Proof.Should().NotBeNull();
payload.Proof.Proof.Length.Should().BeGreaterThan(0);
}

[Test]
public void Meta_counters_have_sane_values()
{
AccountProofWithMeta payload = _proofRpcModule.proof_getProofWithMeta(
TestItem.AddressA, new HashSet<UInt256>(), BlockParameter.Earliest).Data;

payload.Meta.Should().NotBeNull();
payload.Meta.NodeLookups.Should().BeGreaterThan(0,
"every proof generation must perform at least one node lookup");
payload.Meta.CacheHits.Should().BeGreaterThanOrEqualTo(0);
payload.Meta.CacheHits.Should().BeLessThanOrEqualTo(payload.Meta.NodeLookups,
"cache hits cannot exceed total lookups");
payload.Meta.MaxDepth.Should().BeGreaterThanOrEqualTo(0);
}

[Test]
public void Repeated_calls_increase_cache_hits_on_warm_path()
{
AccountProofWithMeta first = _proofRpcModule.proof_getProofWithMeta(
TestItem.AddressA, new HashSet<UInt256>(), BlockParameter.Earliest).Data;
AccountProofWithMeta second = _proofRpcModule.proof_getProofWithMeta(
TestItem.AddressA, new HashSet<UInt256>(), BlockParameter.Earliest).Data;

second.Meta.CacheHits.Should().BeGreaterThanOrEqualTo(first.Meta.CacheHits,
"warming the cache should not reduce hit count on identical query");
}

[Test]
public void Rejects_too_many_storage_keys()
{
HashSet<UInt256> storageKeys = new();
for (int i = 0; i < 1001; i++)
{
storageKeys.Add((UInt256)i);
}

ResultWrapper<AccountProofWithMeta> result = _proofRpcModule.proof_getProofWithMeta(
TestItem.AddressA, storageKeys, BlockParameter.Earliest);

result.Result.ResultType.Should().Be(ResultType.Failure);
result.ErrorCode.Should().Be(ErrorCodes.InvalidParams);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.State.Proofs;

namespace Nethermind.JsonRpc.Modules.Proof
{
/// <summary>
/// Response payload of <c>proof_getProofWithMeta</c> — the EIP-1186 account proof plus
/// per-call diagnostics from <see cref="ProofMeta"/>.
/// </summary>
public class AccountProofWithMeta
{
/// <summary>EIP-1186 account proof, identical in shape to <c>eth_getProof</c>'s result.</summary>
public AccountProof Proof { get; set; } = null!;

/// <summary>Per-call diagnostics captured during proof construction.</summary>
public ProofMeta Meta { get; set; } = null!;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Collections.Generic;
using Nethermind.Blockchain.Find;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Facade.Eth.RpcTransaction;
using Nethermind.Int256;

namespace Nethermind.JsonRpc.Modules.Proof
{
Expand All @@ -28,5 +31,14 @@ public interface IProofRpcModule : IRpcModule
ExampleResponse = "{\"receipt\":{\"transactionHash\":\"0xfff473e0d10e9dcc18bb4585fb2ba17f682949996f5dfda41c20c425a53b4e71\",\"transactionIndex\":\"0x0\",\"blockHash\":\"0x539822db4041dac07f02819b1337f5f9d7291a996f80d9c05ada334c7a97264c\",\"blockNumber\":\"0x1\",\"cumulativeGasUsed\":\"0x0\",\"gasUsed\":\"0x0\",\"to\":null,\"contractAddress\":null,\"logs\":[],\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"status\":\"0x0\",\"type\":\"0x0\"},\"txProof\":[\"0xf851a073ff16e6f3a3ca20ba99ad5bacc973e800ba7ec7092266fcd2520703613e3d9580808080808080a0a70de17dcf5a91c1b986463b4e8419665333b2a66e66f7127baae3d4d58d052d8080808080808080\",\"0xf86530b862f86080018252089400000000000000000000000000000000000000000181801ca0b4e030f395ed357d206b58d9a0ded408589a9e26f1a5b41010772cd0d84b8d16a04d9797a972bc308ea635f22455881c41c7c9fb946c93db6f99d2bd529675af13\"],\"receiptProof\":[\"0xf851a08e4cd3def722e9727e505d3798454165d832e1aabd5c56e5d0e4e9f0796a783280808080808080a05380738598f169c9e407a0f61558e53ea59a4c5e643aabc57679c7c0a3b761428080808080808080\",\"0xf9012f30b9012bf90128a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421825208b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0\"],\"blockHeader\":\"0xf901f9a0b3157bcccab04639f6393042690a6c9862deebe88c781f911e8dfd265531e9ffa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0541c8844bd420f79a5f7f8db723e2106160d350043de7cf76d78ea13ed0ff6c9a0e1b1585a222beceb3887dc6701802facccf186c2d0f6aa69e26ae0c431fc2b5db9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830f424001833d090080830f424183010203a02ba5557a4c62a513c7e56d1bf13373e0da6bec016755483e91589fe1c6d212e28800000000000003e8\"}")]

ResultWrapper<ReceiptWithProof> proof_getTransactionReceipt([JsonRpcParameter(ExampleValue = "[\"0xfff473e0d10e9dcc18bb4585fb2ba17f682949996f5dfda41c20c425a53b4e71\", \"true\"]")] Hash256 txHash, bool includeHeader);

[JsonRpcMethod(IsImplemented = true,
Description = "Returns the same payload as `eth_getProof` plus per-call diagnostics: `nodeLookups` (total trie-node fetches), `cacheHits` (of those, served from the in-process trie store cache), and `maxDepth` (deepest level reached in the account or any storage trie, in nibbles). Useful as a client-agnostic proxy for the work an EL does to serve a proof. The `proof` field has the same shape and content as `eth_getProof`'s result; duplicate `storageKeys` are deduplicated, matching the existing `eth_getProof` behaviour.",
IsSharable = false,
ExampleResponse = "{\"proof\":{\"address\":\"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\",\"balance\":\"0x18232098799834c9a9e2b\",\"codeHash\":\"0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23\",\"nonce\":\"0x1\",\"storageHash\":\"0x...\",\"accountProof\":[\"0x...\"],\"storageProof\":[]},\"meta\":{\"nodeLookups\":\"0x6e\",\"cacheHits\":\"0x6e\",\"maxDepth\":8}}")]
ResultWrapper<AccountProofWithMeta> proof_getProofWithMeta(
[JsonRpcParameter(ExampleValue = "\"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\"")] Address accountAddress,
[JsonRpcParameter(ExampleValue = "[]", Description = "Storage keys to include in the proof; duplicates are deduplicated.")] HashSet<UInt256> storageKeys,
[JsonRpcParameter(ExampleValue = "\"latest\"")] BlockParameter? blockParameter);
}
}
26 changes: 26 additions & 0 deletions src/Nethermind/Nethermind.JsonRpc/Modules/Proof/ProofMeta.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

namespace Nethermind.JsonRpc.Modules.Proof
{
/// <summary>
/// Per-call diagnostics returned by <c>proof_getProofWithMeta</c> alongside the EIP-1186 proof.
/// </summary>
public class ProofMeta
{
/// <summary>
/// Total trie-node fetches the proof construction triggered (account + any storage tries).
/// </summary>
public long NodeLookups { get; set; }

/// <summary>
/// Subset of <see cref="NodeLookups"/> served from the in-process trie store cache.
/// </summary>
public long CacheHits { get; set; }

/// <summary>
/// Deepest level the visitor reached in the account or any storage trie, in nibbles.
/// </summary>
public int MaxDepth { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using Nethermind.Crypto;
using Nethermind.Facade;
using Nethermind.Facade.Eth;
using Nethermind.Evm;
using Nethermind.Blockchain.Tracing;
using Nethermind.Blockchain.Tracing.Proofs;
using Nethermind.Facade.Eth.RpcTransaction;
using Nethermind.Int256;
using Nethermind.JsonRpc.Data;
using Nethermind.Serialization.Rlp;
using Nethermind.State.OverridableEnv;
using Nethermind.State.Proofs;
using Nethermind.Trie;

namespace Nethermind.JsonRpc.Modules.Proof
{
Expand All @@ -31,9 +34,11 @@ public class ProofRpcModule(
IBlockFinder blockFinder,
IReceiptFinder receiptFinder,
ISpecProvider specProvider,
IJsonRpcConfig jsonRpcConfig)
IJsonRpcConfig jsonRpcConfig,
IBlockchainBridge blockchainBridge)
: IProofRpcModule
{
private const int GetProofWithMetaStorageKeyLimit = 1000;
private readonly HeaderDecoder _headerDecoder = new();
private static readonly IRlpStreamEncoder<TxReceipt> _receiptEncoder = Rlp.GetStreamEncoder<TxReceipt>();

Expand Down Expand Up @@ -175,6 +180,45 @@ public ResultWrapper<ReceiptWithProof> proof_getTransactionReceipt(Hash256 txHas
return ResultWrapper<ReceiptWithProof>.Success(receiptWithProof);
}

public ResultWrapper<AccountProofWithMeta> proof_getProofWithMeta(Address accountAddress, HashSet<UInt256> storageKeys, BlockParameter? blockParameter)
{
if (storageKeys.Count > GetProofWithMetaStorageKeyLimit)
{
return ResultWrapper<AccountProofWithMeta>.Fail(
$"storageKeys: {storageKeys.Count} is over the query limit {GetProofWithMetaStorageKeyLimit}.",
ErrorCodes.InvalidParams);
}

SearchResult<BlockHeader> searchResult = blockFinder.SearchForHeader(blockParameter);
if (searchResult.IsError)
{
return ResultWrapper<AccountProofWithMeta>.Fail(searchResult);
}

BlockHeader header = searchResult.Object;

if (!blockchainBridge.HasStateForBlock(header!))
{
return ResultWrapper<AccountProofWithMeta>.Fail(
$"No state available for block {header!.ToString(BlockHeader.Format.Short)}",
ErrorCodes.ResourceUnavailable);
}

AccountProofCollector accountProofCollector = new(accountAddress, storageKeys);
ProofDiagnostics diagnostics = blockchainBridge.RunTreeVisitorMetered(accountProofCollector, header!);

return ResultWrapper<AccountProofWithMeta>.Success(new AccountProofWithMeta
{
Proof = accountProofCollector.BuildResult(),
Meta = new ProofMeta
{
NodeLookups = diagnostics.NodeLookups,
CacheHits = diagnostics.CacheHits,
MaxDepth = diagnostics.MaxDepth,
},
});
}

private AccountProof[] CollectAccountProofs(ITracer tracer, BlockHeader? baseBlock, ProofTxTracer proofTxTracer)
{
List<AccountProof> accountProofs = new();
Expand Down
15 changes: 15 additions & 0 deletions src/Nethermind/Nethermind.State.Flat/FlatStateReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,20 @@ public void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? ba
patriciaTree.Accept(treeVisitor, stateId.StateRoot.ToCommitment(), visitingOptions);
}

public ProofDiagnostics RunTreeVisitorMetered<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock, VisitingOptions? visitingOptions = null) where TCtx : struct, INodeContext<TCtx>
{
StateId stateId = new(baseBlock);

using ReadOnlySnapshotBundle reader = flatDbManager.GatherReadOnlySnapshotBundle(stateId)
?? throw new InvalidOperationException($"State at {baseBlock} not found");

ReadOnlyStateTrieStoreAdapter trieStoreAdapter = new(reader);

PatriciaTree patriciaTree = new(trieStoreAdapter, logManager);
ProofDiagnostics diagnostics = new();
patriciaTree.AcceptMetered(treeVisitor, stateId.StateRoot.ToCommitment(), diagnostics, visitingOptions);
return diagnostics;
}

public bool HasStateForBlock(BlockHeader? baseBlock) => flatDbManager.HasStateForBlock(new StateId(baseBlock));
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ public void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? ba
patriciaTree.Accept(treeVisitor, stateId.StateRoot.ToCommitment(), visitingOptions);
}

public ProofDiagnostics RunTreeVisitorMetered<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock, VisitingOptions? visitingOptions = null) where TCtx : struct, INodeContext<TCtx>
{
StateId stateId = new(baseBlock);
using SnapshotBundle snapshotBundle = overridableWorldScope.GatherSnapshotBundle(baseBlock);

ConcurrencyController concurrency = new(1);
StateTrieStoreAdapter trieStoreAdapter = new(snapshotBundle, concurrency);

PatriciaTree patriciaTree = new(trieStoreAdapter, LimboLogs.Instance);
ProofDiagnostics diagnostics = new();
patriciaTree.AcceptMetered(treeVisitor, stateId.StateRoot.ToCommitment(), diagnostics, visitingOptions);
return diagnostics;
}

public bool HasStateForBlock(BlockHeader? baseBlock) => overridableWorldScope.HasStateForBlock(baseBlock);
}
}
Expand Down
Loading
Loading