Skip to main content

Nethereum.DevChain

NuGet: Nethereum.DevChain | Source: src/Nethereum.DevChain/

Nethereum.DevChain

Development blockchain with full EVM execution through the Amsterdam hardfork (the newest fork the bundled registry runs; the base DevChainConfig default is Prague, and the DevChain server defaults to Amsterdam), automatic mining, SQLite storage, and extended RPC support. A local Ethereum-compatible chain for testing and development.

Overview​

Nethereum.DevChain provides a complete local blockchain environment:

  • Instant Mining - Transactions mined immediately or on a configurable interval
  • Full EVM Execution - EVM opcode support through Amsterdam (newest registry fork) via Nethereum.EVM
  • SQLite Storage - Default lightweight storage with auto-cleanup (no native dependencies)
  • State Management - Patricia trie-based state with snapshot/revert
  • Extended RPC - Development, debug, and Anvil-compatible APIs
  • Transaction Tracing - debug_traceTransaction and debug_traceCall
  • Forking - Fork from live Ethereum networks

Installation​

dotnet add package Nethereum.DevChain

Dependencies​

  • Nethereum.CoreChain - Core blockchain infrastructure
  • Nethereum.Merkle.Binary - Binary Merkle trie and ZK-friendly hashing
  • Nethereum.RPC.Extensions - Extended RPC utilities
  • Nethereum.JsonRpc.RpcClient - JSON-RPC client
  • Nethereum.Web3 - Web3 and Accounts
  • Nethereum.ChainNode.Hosting - Shared node-composition and hosting layer
  • Nethereum.EngineApi.Hosting - JWT-authenticated Engine API host
  • Microsoft.Data.Sqlite - SQLite storage provider
  • Microsoft.Extensions.Hosting.Abstractions - IHostedService support
  • Microsoft.AspNetCore.App (FrameworkReference) - ASP.NET Core web extensions

Quick Start​

Default (SQLite with auto-cleanup)​

using Nethereum.DevChain;

var node = new DevChainNode();
await node.StartAsync(new[] { "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" });

// Use the node...

node.Dispose(); // SQLite DB deleted automatically

With Custom Config​

var config = new DevChainConfig
{
ChainId = 1337,
BlockGasLimit = 30_000_000,
AutoMine = true,
InitialBalance = BigInteger.Parse("10000000000000000000000") // 10000 ETH
};

var node = new DevChainNode(config);
await node.StartAsync(fundedAddresses);

Fully In-Memory​

var node = DevChainNode.CreateInMemory();
await node.StartAsync(fundedAddresses);

Persistent SQLite​

var node = new DevChainNode(DevChainConfig.Default, "./mychain/chain.db", persistDb: true);
await node.StartAsync(fundedAddresses);
// DB survives restart

Custom ChainId​

var node = new DevChainNode(new DevChainConfig { ChainId = 31337 });
await node.StartAsync(new[] { "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" });

Storage Architecture​

DevChain uses a hybrid storage strategy by default:

DataStoreReason
BlocksSQLiteHistorical, grows unbounded
TransactionsSQLiteHistorical, grows unbounded
ReceiptsSQLiteHistorical, grows unbounded
LogsSQLiteHistorical, grows unbounded
StateSQLite (HistoricalStateStore over SqliteStateStore; snapshot/revert is diff-based)Persisted with the chain
FiltersIn-MemoryTransient, bounded
Trie NodesSQLitePersisted with the chain

SQLite uses WAL journal mode for concurrent reads during block production. The database is auto-deleted on dispose unless persistence is enabled.

Configuration​

DevChainConfig inherits from ChainConfig. Properties like ChainId, BlockGasLimit, BaseFee, and InitialBalance come from the base class.

public class DevChainConfig : ChainConfig
{
public BigInteger ChainId { get; set; } = 1337; // inherited from ChainConfig

// Overridden in DevChainConfig: a computed BigInteger, not a fixed long.
// When left unset it sizes itself to the largest contract the newest fork this
// chain runs will deploy (~130,000,000 at Amsterdam), not a hard-coded 30M.
public override BigInteger BlockGasLimit
{
get => _blockGasLimit ?? BlockGasLimitLargeEnoughToDeployAt(NewestForkThisChainRuns);
set => _blockGasLimit = value;
}

public bool AutoMine { get; set; } = true;
public bool PredeploySystemContracts { get; set; } = true; // false: genesis keeps only what it declares (no EIP-4788/2935 predeploys)
public long BlockTime { get; set; } = 0; // 0 = instant
public int MaxTransactionsPerBlock { get; set; } = 100;
public BigInteger BaseFee { get; set; } = 1_000_000_000; // 1 gwei
public BigInteger InitialBalance { get; set; }; // Default: 10000 ETH (BigInteger.Parse("10000000000000000000000"))

// State trie (inherited from ChainConfig)
public StateTreeType StateTree { get; set; } = StateTreeType.Patricia;
public IHashProvider StateTreeHashProvider { get; set; }; // null = Blake3 for Binary, Keccak for Patricia

// Forking
public string ForkUrl { get; set; }
public long? ForkBlockNumber { get; set; }

// Auto-mine batching
public int AutoMineBatchSize { get; set; } = 1;
public int AutoMineBatchTimeoutMs { get; set; } = 10;
}

// Built-in presets
var config = DevChainConfig.Default; // ChainId 1337
var config = DevChainConfig.Hardhat; // ChainId 31337
var config = DevChainConfig.Anvil; // ChainId 31337

// Binary trie with Poseidon (ZK-friendly)
var zkConfig = new DevChainConfig
{
StateTree = StateTreeType.Binary,
StateTreeHashProvider = new BN254PoseidonPairHashProvider()
};

// Binary trie with Blake3 (fastest)
var fastConfig = new DevChainConfig
{
StateTree = StateTreeType.Binary,
StateTreeHashProvider = new Blake3HashProvider()
};

Core Features​

EIP-4844 Blob Transactions​

Blob sidecars are stored only when the node has an IBlobStore: DevChainNode.CreateInMemory() supplies an in-memory one; the default SQLite constructor and AddDevChainServer do not, so node.BlobStore is null and sidecars are dropped.

// Send blob transaction with sidecar
var tx = new Transaction4844(..., blobVersionedHashes);
tx.Sidecar = sidecar;
await node.SendTransactionAsync(tx);
await node.MineBlockAsync();

// Fetch stored blobs by block number
var blobs = await node.BlobStore.GetBlobsByBlockNumberAsync(blockNumber);
var originalData = BlobEncoder.DecodeBlobs(blobs.Select(b => b.Blob).ToList());

Transaction Processing​

// Send a signed transaction (returns a TransactionExecutionResult, not a hash)
var result = await node.SendTransactionAsync(signedTransaction);

// Get receipt (the hash is on result.TransactionHash)
var receipt = await node.GetTransactionReceiptAsync(result.TransactionHash);

// Raw byte sending (eth_sendRawTransaction) is available via the RPC layer

Contract Execution​

// Execute without state change (individual parameters)
var result = await node.CallAsync(
to: contractAddress,
data: dataBytes,
from: senderAddress,
value: null,
gasLimit: null
);

// eth_call and eth_estimateGas are also available via the RPC layer with CallInput objects

State Access​

var balance = await node.GetBalanceAsync(address);
var code = await node.GetCodeAsync(contractAddress);
var storage = await node.GetStorageAtAsync(contractAddress, slot);
var nonce = await node.GetNonceAsync(address);

Account Management​

Modify account state for testing:

await node.SetBalanceAsync(address, newBalance);
await node.SetCodeAsync(address, bytecode);
await node.SetStorageAtAsync(address, slot, value);
await node.SetNonceAsync(address, nonce);
await node.SetBlockHashAsync(blockNumber, hash);

SetBlockHashAsync stamps the hash into the block store and mirrors it into the EIP-2935 history contract's storage at 0x0000F90827F1C53a10cb7A02335B175320002935, slot blockNumber % 8191. That mirror keeps the BLOCKHASH opcode consistent with the in-guest state-reader view for state tests and stateless replays where the block-hash history is seeded explicitly rather than built up from actual block production.

Snapshots​

// Take snapshot (returns IStateSnapshot)
var snapshot = await node.TakeSnapshotAsync();

// Execute transactions...

// Revert all changes (takes the snapshot object)
await node.RevertToSnapshotAsync(snapshot);

Block Mining​

// Manual mining
await node.MineBlockAsync();

// Time manipulation
node.DevConfig.NextBlockTimestamp = DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds();
await node.MineBlockAsync();

Debug/Trace APIs​

debug_traceTransaction​

Trace a mined transaction step-by-step:

using Nethereum.CoreChain.Tracing;

var config = new OpcodeTraceConfig
{
EnableMemory = true,
DisableStack = false,
DisableStorage = false,
EnableReturnData = true,
Limit = 1000
};

var trace = await node.TraceTransactionAsync(txHash, config);

foreach (var log in trace.StructLogs)
{
Console.WriteLine($"PC: {log.Pc}, Op: {log.Op}, Gas: {log.Gas}");
}

debug_traceCall​

Trace a call without mining, with optional state overrides:

using Nethereum.CoreChain.Tracing;

var stateOverrides = new Dictionary<string, StateOverride>
{
[address] = new StateOverride
{
Balance = new HexBigInteger("0x1000000000000000000"),
Code = "0x...",
State = new Dictionary<string, string>
{
["0x0"] = "0x..."
}
}
};

var trace = await node.TraceCallAsync(callInput, config, stateOverrides);

RPC Handler Extensions​

Register DevChain-specific RPC handlers:

using Nethereum.DevChain.Rpc;
using Nethereum.CoreChain.Rpc;

var registry = new RpcHandlerRegistry();
registry.AddStandardHandlers(); // Core Ethereum methods
registry.AddDevHandlers(); // Dev/debug methods
registry.AddAnvilAliases(); // Anvil compatibility

Development Methods​

MethodDescription
evm_mineMine a block
evm_snapshotCreate state snapshot
evm_revertRevert to snapshot
evm_increaseTimeIncrease block timestamp
evm_setNextBlockTimestampSet next block timestamp

Account Management Methods​

MethodDescription
hardhat_setBalanceSet account balance
hardhat_setCodeSet contract code
hardhat_setNonceSet account nonce
hardhat_setStorageAtSet storage slot
hardhat_impersonateAccountRecord an address as impersonated (registered only via AddDevChainServer, not AddDevHandlers; no signing support)
hardhat_stopImpersonatingAccountStop impersonating (registered only via AddDevChainServer)

Testing Methods​

MethodDescription
testing_buildBlockV1Build a block on the current head from payload attributes and return it as a getPayload response, without persisting it or moving the head. The given transactions are kept in order and all must apply (else -32000); a null list takes the pool. parentHash must be the current head
testing_commitBlockV1Build a block the same way, insert it, make it the canonical head and return its hash. An unapplicable transaction is an error and the head stays; a null list takes the pool and drains it

Debug Methods​

MethodDescription
debug_traceTransactionTrace mined transaction
debug_traceCallTrace call without mining

Anvil Aliases​

All hardhat_* methods are also available as anvil_* for compatibility:

Anvil MethodMaps To
anvil_setBalancehardhat_setBalance
anvil_setCodehardhat_setCode
anvil_setNoncehardhat_setNonce
anvil_setStorageAthardhat_setStorageAt
anvil_mineevm_mine
anvil_snapshotevm_snapshot
anvil_revertevm_revert
anvil_impersonateAccounthardhat_impersonateAccount
anvil_stopImpersonatingAccounthardhat_stopImpersonatingAccount

ASP.NET Core Hosting Extensions​

DevChain includes reusable extensions for hosting in any ASP.NET Core application:

using Nethereum.DevChain.Configuration;
using Nethereum.DevChain.Hosting;

var builder = WebApplication.CreateBuilder(args);

var config = new DevChainServerConfig { ChainId = 31337, Storage = "sqlite" };
builder.AddDevChainServer(config); // Registers DI services, CORS, and hosted service

var app = builder.Build();
await app.MapDevChainEndpointsAsync(); // Maps JSON-RPC POST /, health GET /, and CORS middleware
app.Run();

AddDevChainServer registers DevChainNode, RpcDispatcher, DevAccountManager, storage providers, CORS, and DevChainHostedService as singletons.

Manual RPC Setup​

For non-web scenarios, use the lower-level DI extension on IServiceCollection:

using Nethereum.DevChain.Hosting;

services.AddDevChainServer(config); // IServiceCollection extension

Forking​

Fork from live Ethereum networks to test against real state:

var config = new DevChainConfig
{
ForkUrl = "https://eth.llamarpc.com",
ForkBlockNumber = 19000000
};

var node = new DevChainNode(config);
await node.StartAsync(fundedAddresses);

// Reads hit fork source on cache miss, writes are local only
var balance = await node.GetBalanceAsync("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");

Historical State​

DevChain supports querying state at any past block when using HistoricalStateStore:

// State diffs are automatically recorded per block
// Query balance at a specific block
var pastBalance = await node.GetBalanceAsync(address, blockNumber: 5);
  • Nethereum.CoreChain - Core blockchain infrastructure and storage interfaces
  • Nethereum.DevChain.Server - HTTP JSON-RPC server (dotnet tool)
  • Nethereum.Aspire.DevChain - Aspire-orchestrated variant for distributed dev environments
  • Nethereum.EVM - EVM simulator
  • Nethereum.RPC - Standard debug tracing DTOs

Additional Resources​