Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion src/Nethermind/Nethermind.Facade/BlockchainBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,8 @@ public Hash256[] GetPendingTransactionFilterChanges(int filterId) =>

public Address? RecoverTxSender(Transaction tx) => ecdsa.RecoverAddress(tx);

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

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

Expand Down
4 changes: 3 additions & 1 deletion src/Nethermind/Nethermind.Facade/IBlockchainBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public interface IBlockchainBridge : ILogFinder
IEnumerable<FilterLog> GetLogs(BlockParameter fromBlock, BlockParameter toBlock, HashSet<AddressAsKey>? addresses = null, IEnumerable<Hash256[]?>? topics = null, CancellationToken cancellationToken = default);

bool TryGetLogs(int filterId, out IEnumerable<FilterLog> filterLogs, CancellationToken cancellationToken = default);
void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock) where TCtx : struct, INodeContext<TCtx>;
/// <inheritdoc cref="Nethermind.State.IStateReader.RunTreeVisitor{TCtx}"/>
void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? baseBlock, ProofDiagnostics? diagnostics = null) where TCtx : struct, INodeContext<TCtx>;

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,46 @@ 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 = new();
blockchainBridge.RunTreeVisitor(accountProofCollector, header!, diagnostics: diagnostics);

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
4 changes: 2 additions & 2 deletions src/Nethermind/Nethermind.State.Flat/FlatStateReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public ReadOnlySpan<byte> GetStorage(BlockHeader? baseBlock, Address address, in

public byte[]? GetCode(in ValueHash256 codeHash) => codeHash == Keccak.OfAnEmptyString.ValueHash256 ? [] : codeDb[codeHash.Bytes];

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

Expand All @@ -62,7 +62,7 @@ public void RunTreeVisitor<TCtx>(ITreeVisitor<TCtx> treeVisitor, BlockHeader? ba
ReadOnlyStateTrieStoreAdapter trieStoreAdapter = new(reader);

PatriciaTree patriciaTree = new(trieStoreAdapter, logManager);
patriciaTree.Accept(treeVisitor, stateId.StateRoot.ToCommitment(), visitingOptions);
patriciaTree.Accept(treeVisitor, stateId.StateRoot.ToCommitment(), visitingOptions, diagnostics: diagnostics);
}

public bool HasStateForBlock(BlockHeader? baseBlock) => flatDbManager.HasStateForBlock(new StateId(baseBlock));
Expand Down
Loading
Loading