Skip to content

Commit 53c76a2

Browse files
Account abstraction integration tests including devchain embedded, change TransactionExecutor to have CallMode instead of using the Evm directly for calls
1 parent 1aaae6f commit 53c76a2

22 files changed

Lines changed: 3658 additions & 170 deletions

src/Nethereum.CoreChain/ChainNodeBase.cs

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public abstract class ChainNodeBase : IChainNode
2525
protected readonly INodeDataService _nodeDataService;
2626
protected readonly TransactionProcessor _transactionProcessor;
2727
protected readonly ITransactionVerificationAndRecovery _txVerifier;
28+
protected readonly TransactionExecutor _executor;
2829

2930
protected ChainNodeBase(
3031
IBlockStore blockStore,
@@ -36,7 +37,8 @@ protected ChainNodeBase(
3637
TransactionProcessor transactionProcessor,
3738
ITransactionVerificationAndRecovery txVerifier,
3839
INodeDataService nodeDataService = null,
39-
ITrieNodeStore trieNodeStore = null)
40+
ITrieNodeStore trieNodeStore = null,
41+
HardforkConfig hardforkConfig = null)
4042
{
4143
_blockStore = blockStore ?? throw new ArgumentNullException(nameof(blockStore));
4244
_transactionStore = transactionStore ?? throw new ArgumentNullException(nameof(transactionStore));
@@ -48,6 +50,7 @@ protected ChainNodeBase(
4850
_txVerifier = txVerifier ?? throw new ArgumentNullException(nameof(txVerifier));
4951
_nodeDataService = nodeDataService ?? new StateStoreNodeDataService(_stateStore, _blockStore);
5052
_trieNodeStore = trieNodeStore;
53+
_executor = new TransactionExecutor(hardforkConfig ?? HardforkConfig.Default);
5154
}
5255

5356
public abstract ChainConfig Config { get; }
@@ -129,49 +132,44 @@ public virtual async Task<CallResult> CallAsync(string to, byte[] data, string f
129132
var blockContext = await GetBlockContextForCallAsync();
130133
var executionStateService = new ExecutionStateService(_nodeDataService);
131134

132-
var code = await _nodeDataService.GetCodeAsync(to);
133-
if (code == null || code.Length == 0)
134-
{
135-
return new CallResult { Success = true, ReturnData = Array.Empty<byte>(), GasUsed = 0 };
136-
}
135+
var isContractCreation = string.IsNullOrEmpty(to);
137136

138137
var callerBalance = await _nodeDataService.GetBalanceAsync(from);
139138
executionStateService.SetInitialChainBalance(from, callerBalance);
140139

141-
var callInput = new CallInput
140+
var ctx = new TransactionExecutionContext
142141
{
143-
From = from,
144-
To = to,
145-
Value = new Nethereum.Hex.HexTypes.HexBigInteger(callValue),
146-
Data = data?.ToHex(true) ?? "0x",
147-
Gas = new Nethereum.Hex.HexTypes.HexBigInteger(callGasLimit),
148-
GasPrice = new Nethereum.Hex.HexTypes.HexBigInteger(0),
149-
ChainId = new Nethereum.Hex.HexTypes.HexBigInteger(Config.ChainId)
142+
Mode = ExecutionMode.Call,
143+
Sender = from,
144+
To = isContractCreation ? null : to,
145+
Data = data,
146+
Value = callValue,
147+
GasLimit = callGasLimit,
148+
GasPrice = 0,
149+
MaxFeePerGas = 0,
150+
MaxPriorityFeePerGas = 0,
151+
Nonce = 0,
152+
IsEip1559 = false,
153+
IsContractCreation = isContractCreation,
154+
BlockNumber = (long)blockContext.BlockNumber,
155+
Timestamp = blockContext.Timestamp,
156+
Coinbase = blockContext.Coinbase,
157+
BaseFee = blockContext.BaseFee,
158+
Difficulty = blockContext.Difficulty,
159+
BlockGasLimit = blockContext.GasLimit,
160+
ChainId = blockContext.ChainId,
161+
ExecutionState = executionStateService,
162+
TraceEnabled = false
150163
};
151164

152-
var programContext = new ProgramContext(
153-
callInput,
154-
executionStateService,
155-
from,
156-
to,
157-
(long)blockContext.BlockNumber,
158-
blockContext.Timestamp,
159-
blockContext.Coinbase,
160-
(long)blockContext.BaseFee);
161-
162-
programContext.GasLimit = blockContext.GasLimit;
163-
programContext.Difficulty = blockContext.Difficulty;
164-
165-
var program = new Program(code, programContext);
166-
var simulator = new EVMSimulator();
167-
await simulator.ExecuteWithCallStackAsync(program, traceEnabled: false);
165+
var result = await _executor.ExecuteAsync(ctx);
168166

169167
return new CallResult
170168
{
171-
Success = !program.ProgramResult.IsRevert,
172-
ReturnData = program.ProgramResult.Result ?? Array.Empty<byte>(),
173-
RevertReason = program.ProgramResult.GetRevertMessage(),
174-
GasUsed = program.TotalGasUsed
169+
Success = result.Success,
170+
ReturnData = result.ReturnData ?? Array.Empty<byte>(),
171+
RevertReason = result.RevertReason ?? result.Error,
172+
GasUsed = result.GasUsed
175173
};
176174
}
177175

src/Nethereum.CoreChain/Rpc/Handlers/Standard/EthSendRawTransactionHandler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ public override async Task<RpcResponseMessage> HandleAsync(RpcRequestMessage req
2828

2929
var result = await context.Node.SendTransactionAsync(signedTx);
3030

31-
if (!result.Success)
31+
if (result.Success || result.Receipt != null)
3232
{
33-
return Error(request.Id, -32000, result.RevertReason ?? "Transaction execution failed");
33+
return Success(request.Id, result.TransactionHash.ToHex(true));
3434
}
3535

36-
return Success(request.Id, result.TransactionHash.ToHex(true));
36+
return Error(request.Id, -32000, result.RevertReason ?? "Transaction rejected");
3737
}
3838
}
3939
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using Nethereum.CoreChain.Rpc;
4+
using Nethereum.JsonRpc.Client;
5+
using Nethereum.JsonRpc.Client.RpcMessages;
6+
7+
namespace Nethereum.DevChain
8+
{
9+
/// <summary>
10+
/// RPC client adapter that wraps a DevChain RpcDispatcher as an IClient.
11+
/// This allows using DevChainNode with standard Web3 and contract services.
12+
///
13+
/// Usage:
14+
/// <code>
15+
/// var node = new DevChainNode(config);
16+
/// await node.StartAsync(prefundedAddresses, initialBalance);
17+
///
18+
/// var registry = new RpcHandlerRegistry();
19+
/// registry.AddStandardHandlers();
20+
///
21+
/// var context = new RpcContext(node, chainId, serviceProvider);
22+
/// var dispatcher = new RpcDispatcher(registry, context);
23+
///
24+
/// var rpcClient = new DevChainRpcClient(dispatcher);
25+
/// var web3 = new Web3(account, rpcClient);
26+
/// </code>
27+
/// </summary>
28+
public class DevChainRpcClient : ClientBase
29+
{
30+
private readonly RpcDispatcher _dispatcher;
31+
32+
public DevChainRpcClient(RpcDispatcher dispatcher)
33+
{
34+
_dispatcher = dispatcher;
35+
}
36+
37+
public override async Task<RpcResponseMessage> SendAsync(RpcRequestMessage rpcRequestMessage, string route = null)
38+
{
39+
return await _dispatcher.DispatchAsync(rpcRequestMessage);
40+
}
41+
42+
protected override async Task<RpcResponseMessage[]> SendAsync(RpcRequestMessage[] requests)
43+
{
44+
var responses = await _dispatcher.DispatchBatchAsync(requests);
45+
return responses.ToArray();
46+
}
47+
}
48+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System;
2+
using System.Numerics;
3+
using Nethereum.CoreChain.Rpc;
4+
using Nethereum.Web3;
5+
using Nethereum.Web3.Accounts;
6+
7+
namespace Nethereum.DevChain
8+
{
9+
/// <summary>
10+
/// Extension methods for creating Web3 instances connected to DevChainNode.
11+
/// </summary>
12+
public static class DevChainWeb3Extensions
13+
{
14+
/// <summary>
15+
/// Creates a Web3 instance connected to this DevChainNode.
16+
/// </summary>
17+
/// <param name="node">The DevChainNode to connect to.</param>
18+
/// <param name="account">The account to use for signing transactions.</param>
19+
/// <param name="serviceProvider">Optional service provider for RPC handlers.</param>
20+
/// <returns>A Web3 instance connected to the DevChain.</returns>
21+
public static IWeb3 CreateWeb3(this DevChainNode node, Account account, IServiceProvider serviceProvider = null)
22+
{
23+
var dispatcher = CreateDispatcher(node, serviceProvider);
24+
var rpcClient = new DevChainRpcClient(dispatcher);
25+
return new Web3.Web3(account, rpcClient);
26+
}
27+
28+
/// <summary>
29+
/// Creates a Web3 instance connected to this DevChainNode using a private key.
30+
/// </summary>
31+
/// <param name="node">The DevChainNode to connect to.</param>
32+
/// <param name="privateKey">The private key to use for signing.</param>
33+
/// <param name="serviceProvider">Optional service provider for RPC handlers.</param>
34+
/// <returns>A Web3 instance connected to the DevChain.</returns>
35+
public static IWeb3 CreateWeb3(this DevChainNode node, string privateKey, IServiceProvider serviceProvider = null)
36+
{
37+
var chainId = (int)node.Config.ChainId;
38+
var account = new Account(privateKey, chainId);
39+
return CreateWeb3(node, account, serviceProvider);
40+
}
41+
42+
/// <summary>
43+
/// Creates a read-only Web3 instance connected to this DevChainNode (no signing capability).
44+
/// </summary>
45+
/// <param name="node">The DevChainNode to connect to.</param>
46+
/// <param name="serviceProvider">Optional service provider for RPC handlers.</param>
47+
/// <returns>A read-only Web3 instance connected to the DevChain.</returns>
48+
public static IWeb3 CreateWeb3(this DevChainNode node, IServiceProvider serviceProvider = null)
49+
{
50+
var dispatcher = CreateDispatcher(node, serviceProvider);
51+
var rpcClient = new DevChainRpcClient(dispatcher);
52+
return new Web3.Web3(rpcClient);
53+
}
54+
55+
/// <summary>
56+
/// Creates an RpcDispatcher for this DevChainNode with standard handlers registered.
57+
/// </summary>
58+
public static RpcDispatcher CreateDispatcher(this DevChainNode node, IServiceProvider serviceProvider = null)
59+
{
60+
var registry = new RpcHandlerRegistry();
61+
registry.AddStandardHandlers();
62+
63+
var context = new RpcContext(node, node.Config.ChainId, serviceProvider ?? new EmptyServiceProvider());
64+
return new RpcDispatcher(registry, context);
65+
}
66+
67+
private class EmptyServiceProvider : IServiceProvider
68+
{
69+
public object GetService(Type serviceType) => null;
70+
}
71+
}
72+
}

src/Nethereum.EVM/TransactionExecutionContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66

77
namespace Nethereum.EVM
88
{
9+
public enum ExecutionMode
10+
{
11+
Transaction,
12+
Call
13+
}
14+
915
public class TransactionExecutionContext
1016
{
17+
public ExecutionMode Mode { get; set; } = ExecutionMode.Transaction;
18+
public bool IsCallMode => Mode == ExecutionMode.Call;
1119
public string Sender { get; set; }
1220
public string To { get; set; }
1321
public byte[] Data { get; set; }

src/Nethereum.EVM/TransactionExecutor.cs

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ public async Task<TransactionExecutionResult> ExecuteAsync(TransactionExecutionC
8282

8383
private void ValidateTransaction(TransactionExecutionContext ctx, TransactionExecutionResult result)
8484
{
85-
// EIP-1559 validation (always enabled - post-London)
86-
if (ctx.IsEip1559)
85+
// Skip gas price validation in Call mode (eth_call doesn't pay for gas)
86+
if (ctx.IsCallMode)
87+
{
88+
ctx.EffectiveGasPrice = 0;
89+
}
90+
else if (ctx.IsEip1559)
8791
{
8892
if (ctx.MaxFeePerGas < ctx.BaseFee)
8993
throw new TransactionValidationException("INSUFFICIENT_MAX_FEE_PER_GAS");
@@ -179,11 +183,15 @@ private async Task SetupStateAsync(TransactionExecutionContext ctx, TransactionE
179183

180184
// EIP-3607: Sender must be EOA (always enabled - post-London)
181185
// EIP-7702: Delegated EOAs are allowed (they have 0xef0100 + address code)
182-
var senderCode = await ctx.ExecutionState.GetCodeAsync(ctx.Sender);
183-
if (senderCode != null && senderCode.Length > 0)
186+
// Skip for Call mode - allow calls from any address
187+
if (!ctx.IsCallMode)
184188
{
185-
if (!IsDelegatedCode(senderCode))
186-
throw new TransactionValidationException("SENDER_NOT_EOA");
189+
var senderCode = await ctx.ExecutionState.GetCodeAsync(ctx.Sender);
190+
if (senderCode != null && senderCode.Length > 0)
191+
{
192+
if (!IsDelegatedCode(senderCode))
193+
throw new TransactionValidationException("SENDER_NOT_EOA");
194+
}
187195
}
188196

189197
// EIP-7702: Process authorization list before execution
@@ -206,26 +214,34 @@ private async Task SetupStateAsync(TransactionExecutionContext ctx, TransactionE
206214
ctx.BlobGasCost = IntrinsicGasCalculator.CalculateBlobGasCost(blobCount, ctx.BlobBaseFee);
207215
}
208216

209-
var maxCost = ctx.GasLimit * (ctx.IsEip1559 ? ctx.MaxFeePerGas : ctx.GasPrice) + ctx.Value + ctx.BlobGasCost;
210-
211-
if (senderBalance < maxCost)
217+
// Skip balance check for gas in Call mode - calls don't require gas payment
218+
if (!ctx.IsCallMode)
212219
{
213-
result.IsValidationError = true;
214-
result.Error = $"Insufficient balance: {senderBalance} < {maxCost}";
215-
return;
220+
var maxCost = ctx.GasLimit * (ctx.IsEip1559 ? ctx.MaxFeePerGas : ctx.GasPrice) + ctx.Value + ctx.BlobGasCost;
221+
222+
if (senderBalance < maxCost)
223+
{
224+
result.IsValidationError = true;
225+
result.Error = $"Insufficient balance: {senderBalance} < {maxCost}";
226+
return;
227+
}
216228
}
217229

218230
ctx.SenderNonceBeforeIncrement = ctx.SenderAccount.Nonce ?? BigInteger.Zero;
219231

220-
// EIP-2681: Nonce overflow (always enabled)
221-
if (ctx.SenderNonceBeforeIncrement >= BigInteger.Parse("18446744073709551615"))
222-
throw new TransactionValidationException("NONCE_IS_MAX");
232+
// Skip nonce validation and increment in Call mode
233+
if (!ctx.IsCallMode)
234+
{
235+
// EIP-2681: Nonce overflow (always enabled)
236+
if (ctx.SenderNonceBeforeIncrement >= BigInteger.Parse("18446744073709551615"))
237+
throw new TransactionValidationException("NONCE_IS_MAX");
223238

224-
ctx.SenderAccount.Nonce = ctx.SenderNonceBeforeIncrement + 1;
225-
ctx.SenderAccount.Balance.UpdateExecutionBalance(-(ctx.GasLimit * ctx.EffectiveGasPrice));
239+
ctx.SenderAccount.Nonce = ctx.SenderNonceBeforeIncrement + 1;
240+
ctx.SenderAccount.Balance.UpdateExecutionBalance(-(ctx.GasLimit * ctx.EffectiveGasPrice));
226241

227-
if (ctx.BlobGasCost > 0)
228-
ctx.SenderAccount.Balance.UpdateExecutionBalance(-ctx.BlobGasCost);
242+
if (ctx.BlobGasCost > 0)
243+
ctx.SenderAccount.Balance.UpdateExecutionBalance(-ctx.BlobGasCost);
244+
}
229245

230246
ctx.TransactionSnapshotId = ctx.ExecutionState.TakeSnapshot();
231247

@@ -442,7 +458,7 @@ private async Task ExecuteCode(TransactionExecutionContext ctx, TransactionExecu
442458
Value = new Hex.HexTypes.HexBigInteger(ctx.Value),
443459
Nonce = new Hex.HexTypes.HexBigInteger(ctx.Nonce),
444460
GasPrice = new Hex.HexTypes.HexBigInteger(ctx.EffectiveGasPrice),
445-
ChainId = new Hex.HexTypes.HexBigInteger(1)
461+
ChainId = new Hex.HexTypes.HexBigInteger(ctx.ChainId)
446462
};
447463

448464
var programContext = new ProgramContext(
@@ -670,6 +686,10 @@ private void FinalizeTransaction(TransactionExecutionContext ctx, TransactionExe
670686

671687
result.EffectiveGasUsed = result.GasUsed;
672688

689+
// Skip gas refund and coinbase payment in Call mode
690+
if (ctx.IsCallMode)
691+
return;
692+
673693
var gasRefundAmount = (ctx.GasLimit - result.GasUsed) * ctx.EffectiveGasPrice;
674694
ctx.SenderAccount.Balance.UpdateExecutionBalance(gasRefundAmount);
675695

0 commit comments

Comments
 (0)