Skip to main content

Nethereum.ChainNode.Hosting

NuGet: Nethereum.ChainNode.Hosting | Source: src/Nethereum.ChainNode.Hosting/

Nethereum.ChainNode.Hosting

Nethereum.ChainNode.Hosting is the shared node-composition layer used by every Nethereum-hosted chain — the mainnet follower (Nethereum.MainnetChain), the AppChain server (Nethereum.AppChain.Server.Core), and the single-process DevChain (Nethereum.DevChain). It wires storage (RocksDB or in-memory), the DevP2P peer pool and listener, the mempool, and the sync stack into one ChainNode object from a single ChainNodeConfig. A host supplies an IChainDefinition (genesis + chain profile + tip source) and gets back a running node; it does not have to hand-assemble PeerPoolManager, PeerListener, RelayMempool and friends itself. Starting an RPC server and peer discovery (discv4/discv5/DNS) are not part of this library — those stay the host's job. What it does carry for the hosts is the one JSON-RPC HTTP endpoint they all map (RpcHttpEndpoint) and the host, origin and module settings that endpoint reads.

Compose a node​

using Nethereum.ChainNode.Hosting;
using Nethereum.ChainNode.Hosting.Configuration;

// A preset gives you a coherent starting point; presets set Storage + Sync.Mode
// (and, for SnapSyncV2, Sync.Snap.AdvertiseSnap2).
var config = ChainNodeConfigFactory.Create(ChainNodePreset.Pruned);
config.Storage.DataDirectory = "./my-chain-data";
config.Network.ListenPort = 30303;

// definition: your IChainDefinition — supplies genesis, IChainProfile, and the tip source.
var node = await ChainNode.StartAsync(definition, config, loggerFactory);

// node.Bundle -> IChainStoreBundle (blocks/state/receipts/...)
// node.Mempool -> ChainNodeMempool (TxPool, Relay, BroadcastPool)
// node.Listener -> PeerListener (inbound eth/snap serving), null if Network.Serve == false
// node.Sync -> ChainNodeSyncStack (peer pool, scheduler, block source), null if Sync.Mode == None

await node.DisposeAsync();

ChainNode.StartAsync is ComposeAsync + StartServingAsync + StartSyncAsync in one call. Compose and start the stages separately when you need to do something in between (wire tx-submission off the mempool before peers connect, pass serve callbacks, or swap the storage backend):

var node = await ChainNode.ComposeAsync(definition, config, loggerFactory,
storageFactory: (cfg, logger) => ChainNodeStorage.Open(cfg, logger, mySigner));

await node.StartServingAsync(new ChainNodeServeCallbacks
{
TrustedTransactionsReceived = msg => { /* ... */ },
});

await node.StartSyncAsync(minPeerLatestBlockFactory: b => Task.FromResult(b.Metadata.GetLastBlock() + 1));

StartServingAsync no-ops (returns the existing/null Listener) when Network.Serve is false. When it does start a listener it also starts node.BlockRangeAnnouncer, which tells every eth/69+ peer the head (BlockRangeUpdate) as it moves; peers pull the new blocks, and a block-range update received from a peer reaches ChainNodeServeCallbacks.BlockRangeUpdateReceived. StartSyncAsync no-ops when Sync.Mode is SyncMode.None. Both are idempotent — calling them again returns the already-started instance.

ChainNodePreset (InMemory, Pruned, Archive, SnapSync, SnapSyncV2) sets Storage and Sync.Mode (and, for SnapSyncV2, Sync.Snap.AdvertiseSnap2); use AsRole(ChainNodeRole.Signer|Follower|GossipNode) afterwards to set the mempool's retention/relay policy (ChainNodeConfigFactory.AsRole).

Config reference​

Every field below is on ChainNodeConfig (Configuration/ChainNodeConfig.cs), one sub-config per section. "Level" is beginner-safe (defaults work, safe to leave alone) or expert (changes storage format, protocol behaviour, or is a named opt-in seam).

Storage — ChainNodeStorageConfig (Configuration/ChainNodeStorageConfig.cs)​

FieldMeaningDefaultLevel
DataDirectoryRocksDB path; ignored when InMemory is true."./chain-data"beginner
InMemoryUse the in-memory store bundle instead of RocksDB (tests, ephemeral chains).falsebeginner
PathKeyedStatePath-keyed vs. hash-keyed state trie on disk. Set via preset, not by hand.falseexpert
JournalBlocksHistorical-state journal window: -1 disables the journal, 0 = full archive (HistoricalStateOptions.FullArchive), >0 = pruning window of that many blocks.128expert
TrieNodeHistoryBlocksTrie-node history retention window; only meaningful when PathKeyedState is true.-1expert
TrieNodeHistoryIndexBuild the trie-node history index.falseexpert
SplitHistoryStoreSplit hot/cold history storage.falseexpert
HotWindowBlocksSize of the hot window when SplitHistoryStore is on.128expert
PromotionEnabledEnable cold→hot promotion in the split store.falseexpert
UseFreezerHistoryUse the append-only freezer store for history.falseexpert
FreezerHistoryDirectoryFreezer directory; null uses the storage layer's own default.nullexpert
BackgroundFreezeIndexingBuild freezer indexes on a background sweep.trueexpert
FreezerBackgroundDegreeOfParallelismParallelism for the background freeze-index sweep; 0 = default.0expert
BlockCacheSizeRocksDB block cache, bytes.1 GiBexpert
EnableRocksDbStatisticsCollect RocksDB statistics and log block-cache counters.falseexpert
FlushCadenceBlocksFlush every N blocks; 1 = flush every block. Live on every node kind.1expert
PrefetchWorkersDedicated threads that prefetch each block's transactions into the state caches; 0 = off. Only the mainnet follower composes the prefetcher.0expert
StateRootWorkersDedicated threads that update storage tries in parallel while a block's state root is computed; 1 = sequential. Only the mainnet follower passes it; AppChain and DevChain run with the default.4expert
EnableLogIndexBuild the log/filter index.falseexpert

ChainNodeStorage.BuildStorageOptions maps this 1:1 onto RocksDbStorageOptions, except JournalBlocks and FlushCadenceBlocks; BuildJournalOptions implements the JournalBlocks tri-state above.

Network — ChainNodeNetworkConfig (Configuration/ChainNodeNetworkConfig.cs)​

FieldMeaningDefaultLevel
ServeAccept inbound eth/snap connections. StartServingAsync no-ops when false.truebeginner
ListenPortInbound RLPx port.30303beginner
BindAddressInbound bind address.IPAddress.Anybeginner
DialBudgetPerSecondOutbound dial-rate limit.5expert
MaxPeersPerIPv4Subnet / MaxPeersPerIPv6SubnetAnti-eclipse peer caps per subnet.10 / 10expert
NodeKeyFilePath to the persisted node identity key; defaults to <DataDirectory>/nodekey when unset. An in-memory node with neither NodeKeyFile nor NodeKeyHex generates its own ephemeral key, kept on the config so later resolutions return the same identity.nullbeginner
NodeKeyHexNode identity key as hex; wins over NodeKeyFile when set.nullexpert
TrustedPeersEnode URLs dialed (via ResolveDialEnodes) and marked trusted.[]expert
TrustedBootnodesEnode URLs marked as trusted dial targets only — see "Trust and dialing" below.[]expert / seam
TrustedNodeIdsAdditional node-ids trusted for inbound connections (not full enode URLs).[]expert / seam
TargetPeerCountSteady-state peer pool size.16beginner
MaxConcurrentDialsOutbound dial concurrency.10expert
MaxInboundPeersInbound peer cap.25expert
MaxInboundPerIPInbound peer cap per IP.9expert
HandshakeTimeoutMsRLPx/eth handshake timeout.5000expert
IdleTimeoutPeer idle disconnect timeout.30 sexpert
MirrorRemoteStatusServe a status reply that mirrors what the connecting peer sent instead of this node's real status.falseexpert / seam — see note below
ClientIdClient id string advertised in the eth handshake."Nethereum"beginner
DiscoveryChainNodeDiscoveryConfig (Discv4/Discv5 toggles+ports). Live on mainnet (MainnetNodeComposition.cs). On AppChain/DevChain it is explicitly disabled by default and a validator (ChainNodeDiscoveryValidator.RefuseIfRequested) throws InvalidOperationException at startup if you try to turn it on.new instancen/a

MirrorRemoteStatus note: the underlying PeerListenerOptions.MirrorRemoteStatus (Nethereum.DevP2P.Sync/Serving/PeerListenerOptions.cs) defaults to true. Because ChainNodeServeListener.BuildListenerOptions always sets it from config.Network.MirrorRemoteStatus (ChainNodeServeListener.BuildListenerOptions), which defaults to false, a ChainNode-composed listener defaults to serving its own real status (built from genesis/fork thresholds/head — see BuildStatusTemplateAsync), not the mirrored behaviour you'd get from PeerListenerOptions directly. BuildStatusTemplateAsync runs unconditionally on every StartServingAsync call, even though its result is only used when MirrorRemoteStatus is false.

Sync — ChainNodeSyncConfig (Configuration/ChainNodeSyncConfig.cs)​

FieldMeaningDefaultLevel
ModeSyncMode.None | ForwardExecute | Snap. StartSyncAsync no-ops on None.ForwardExecutebeginner
FollowPeerEnodeSingle enode to prepend to the dial list; also gates ChainNodeConfig.FollowsAPeer.nullbeginner
TrustedPeersOnlyTipOnly compute the canonical tip from trusted peers (PeerHeadCanonicalSource).falseexpert
FloorTargetPeerCountByDialPoolOpt-in seam: raise the pool's effective TargetPeerCount to max(TargetPeerCount, dialTargets.Count) so every configured dial target gets a slot. AppChain turns this on.falseexpert / seam
HeaderBatchSize / BodyBatchSizeBatch sizes for DevP2PBlockSource.192 / 64expert
MaxInFlightPerPeerScheduler's max in-flight requests per peer.1expert
MinPeerLatestBlockDon't connect to peers whose advertised head is below this block. Overridable per-call via StartSyncAsync(minPeerLatestBlockFactory:).0expert / seam
BulkSyncPassed straight to RocksDbChainStoreBundle.Open(bulkSync:) — the only field here consumed by ChainNodeStorage, not by the sync stack.falseexpert
StartBlock, Blocks, HeadersFrom, HeadersTo, CheckpointEvery, KeepLatestCheckpoints, ReceiptBackfill, ContinueOnMismatchDeclared; the shared config-to-FollowerOptions translation that wires these into ChainNode.Hosting is not wired yet (tracked as NotYetWired in ChainNodeConfigSurface). AppChain's follower hardcodes its own values today instead of reading them (AppChainDevP2PFollower.cs).see sourcen/a
SnapChainNodeSnapConfig — see below.new instance—
SnapBootstrapConvenience get/set that aliases Mode == SyncMode.Snap. Setting it true selects Snap; setting it false moves Mode from Snap to ForwardExecute and leaves any other mode as it was.computedbeginner

ChainNodeSnapConfig (Configuration/ChainNodeSnapConfig.cs):

FieldMeaningDefaultLevel
AdvertiseSnap2Advertise snap/2 in the handshake worker and to inbound peers. Set by the SnapSyncV2 preset.falseexpert / seam
SoftResponseLimitCap on snap response size served by PatriciaSnapRequestHandler; null uses that handler's own default.nullexpert
BackwardSkeletonPhase1, Phase1Only, Phase1FirstDeclared; same NotYetWired status as the Sync catch-up fields above — AppChain hardcodes UseBackwardSkeleton=true and never reads Phase1Only/Phase1First today.true / false / falsen/a

Mempool — ChainNodeMempoolConfig (Configuration/ChainNodeMempoolConfig.cs)​

FieldMeaningDefaultLevel
MaxPoolSizeMax pooled transactions (default TxPool).5000beginner
MaxTxsPerSenderMax pooled transactions per sender (default TxPool).64beginner
RetentionMempoolRetention.Full | RelayOnly. Set per-role by AsRole.Fullbeginner
RelayMempoolRelay.Ours | All. Set per-role by AsRole.Oursbeginner
EnableTrustedPeerAdmissionOpt-in seam: wires PeerListenerOptions.OnTrustedTransactionsReceived[From] to ChainNodeMempool.TrustedAdmission[From], so transactions pushed by a trusted peer are admitted to the local pool without going through normal gossip admission. The field defaults false, but AppChain turns it on via AppChainServerConfig.DefaultNode(); mainnet and DevChain leave it off.false (field); AppChain sets trueexpert / seam
PoolFactoryOverride how the ITxPool is built; bypasses MaxPoolSize/MaxTxsPerSender if set.nullexpert

ChainNodeConfigFactory.AsRole: Signer → Full/Ours, Follower → RelayOnly/Ours, GossipNode → Full/All.

Rpc — ChainNodeRpcConfig (Configuration/ChainNodeRpcConfig.cs)​

ChainNode.Hosting does not start an RPC server and ChainNode never reads this section itself — it exists so a host can carry its RPC settings on the same ChainNodeConfig object it passes to ChainNode. The hosts turn the section into endpoint options with ChainNodeRpcConfigExtensions.ToHttpOptions and ToModuleSet, described under "RPC HTTP endpoint" below.

FieldMeaningDefaultLevel
HostRPC bind host. Read by AppChain (AppChainServerRunner.cs); DevChain reads it through DevChainServerConfig.Host, which forwards onto this section."127.0.0.1"beginner
PortRPC bind port. Read by AppChain; DevChain reads it through DevChainServerConfig.Port, which forwards onto this section. Validated by AppChainServerConfigValidator.8545beginner
MetricsPortLive on AppChain and DevChain: gates Nethereum.Aspire.ServiceDefaults' AddPrometheusMetrics/MapPrometheusMetrics.0beginner
MaxLogBlockRangeLive on AppChain and DevChain via ChainNodeRpcConfigExtensions.ApplyTo → CoreChain.ChainConfig.RpcMaxLogBlockRange, enforced by LogQueryGuards.EnforceBlockRangeCap.10000expert
MaxLogResultsLive the same way, enforced by LogQueryGuards.EnforceResultCap.10000expert
GasCapLive the same way, read by ChainNodeBase.cs for eth_call/estimateGas/traceCall.50000000expert
ModulesComma-separated JSON-RPC modules served: eth, net and web3 are the default; add txpool or debug (admin is also accepted on AppChain; the mainnet follower registers no admin_* method). An unknown name throws ArgumentException. Live on AppChain and the mainnet follower (MainnetChain:RpcModules); DevChain always serves its full development method set."eth,net,web3"expert
AllowedHostsHost header names accepted besides localhost and the bind host; * = any.""expert
AllowedOriginsBrowser origins allowed to open the WebSocket; * = any, none by default. Live on AppChain; DevChain serves no WebSocket endpoint.""expert
UrlComputed http://{Host}:{Port}.computedbeginner

RPC HTTP endpoint — RpcHttpEndpoint (Rpc/RpcHttpEndpoint.cs)​

AppChain, DevChain and the mainnet follower map the same POST / endpoint:

public static void MapRpcHttpEndpoint(
this WebApplication app,
IRpcDispatcher dispatcher,
JsonSerializerOptions serializerOptions,
RpcHttpOptions options,
ILogger? logger)

It answers 403 invalid host specified when the Host header fails RpcHostPolicy, 413 when the body is larger than RpcHttpOptions.MaxRequestBodySize (10 MiB), and 415 unless the content type is application/json, application/json-rpc or application/jsonrequest. The body is handled by the CoreChain JsonRpcEnvelope, which caps a batch response at RpcHttpOptions.MaxResponseSize (25,000,000 bytes).

RpcHostPolicy accepts a missing Host, any IP literal, localhost, RpcHttpOptions.BindHost, the names in AllowedHosts, and every name when the list holds *. RpcOriginPolicy accepts a request with no Origin header, and otherwise only an origin that matches an AllowedOrigins entry by scheme, host and port (* = any). AppChain and the mainnet follower use the default host policy; DevChain adds * when AllowedHosts is empty.

ChainNodeConfigSurface (Configuration/ChainNodeConfigSurface.cs) is now the single source of truth for whether any ChainNodeConfig field is Live/Disabled/NotYetWired on AppChain and DevChain, driving both hosts' --help-advanced output; a reflection-based coverage test (ChainNodeConfigSurfaceCoverageTests) fails the build if a field is added to ChainNodeConfig without a matching entry there. The per-field notes below predate that descriptor and are being superseded by it field by field — treat ChainNodeConfigSurface.All as authoritative over this table where the two disagree.

Producer — ChainNodeProducerConfig (Configuration/ChainNodeProducerConfig.cs)​

Block-producing nodes (AppChain and DevChain) share one section for the block gas limit and base fee. A mainnet follower takes its gas limit from the headers it imports and has no Producer section of its own.

FieldMeaningDefaultLevel
BlockGasLimitGas limit of produced blocks; must be positive when set (ChainNodeProducerValidator.Validate throws InvalidOperationException naming the section).null (the chain's own default)expert
BaseFeeBase fee of produced blocks, in wei (ulong).null (the chain's own default, 1 gwei)expert
public static void ApplyTo(this ChainNodeProducerConfig producer, ChainConfig target)

ApplyTo copies the values that are set onto the chain config; AppChain and DevChain call it when they build their live chain config. DevChainServerConfig.BlockGasLimit keeps its public setter and forwards onto this section; AppChain reads the section from AppChainServerConfig.Node.Producer.

Maintenance — ChainNodeMaintenanceConfig (Configuration/ChainNodeMaintenanceConfig.cs)​

Live on all three nodes via the shared ChainNodeMaintenanceRunner (ChainNodeMaintenanceRunner.cs): every host calls ChainNodeMaintenanceRunner.AnyRequested/RunAndReportExitCodeAsync against this exact ChainNodeConfig.Maintenance object before composing/serving, and the process exits with the run's result (0=ok, non-zero=failure) rather than falling through to normal startup.

FieldMeaningDefault
VerboseVerbose maintenance logging.false
WipeStateWipe state before boot.false
CompactAllForce a full RocksDB compaction.false
RebuildStateFromFlatRebuild the trie from the flat state store.false
VerifyFlatVerify flat-state consistency.false
VerifyFlatSampleAccountsPerShardSample size for VerifyFlat.0

Trust and dialing model​

Three lists interact, and they are not symmetric — this is the subtlest part of the config surface.

  • Network.TrustedPeers (enode URLs): fed into ChainNodeConfig.ResolveDialEnodes() (ChainNodeConfig.ResolveDialEnodes, prepended by Sync.FollowPeerEnode when set). These enodes are actively dialed — ChainNodeSyncStack.DialTargets includes them, and StartPoolAsync calls pool.EnqueueCandidate(enode) for each one. Their node-ids also flow into ResolveTrustedNodeIds() (below), so they're trusted inbound too.
  • Network.TrustedBootnodes (enode URLs): only ever combined with ResolveDialEnodes() to build trustedDialKeys for the PeerPoolManager constructor (TrustedDialTargets). They mark a peer as trusted if/when it connects, but they are not enqueued as dial candidates — TrustedBootnodes alone will not cause an outbound connection. Also note: TrustedBootnodes is not included in ResolveTrustedNodeIds(), so it has no effect on inbound trust either. Its only effect today is outbound trust-marking in the dial pool.
  • Network.TrustedNodeIds (node-ids, not enode URLs): combined with the node-ids parsed out of ResolveDialEnodes() (i.e. TrustedPeers + FollowPeerEnode, not TrustedBootnodes) to build ResolveTrustedNodeIds(), which becomes PeerListenerOptions.TrustedNodeIds — governing which inbound connections are treated as trusted (e.g. eligible for EnableTrustedPeerAdmission's bypass).
  • Independently, profile.Bootnodes (from IChainProfile, chain-specific — mainnet bootnodes, AppChain seed nodes, etc.) are appended to the dial list in DialTargets but carry no trust.

A malformed enode in Sync.FollowPeerEnode, Network.TrustedPeers or Network.TrustedBootnodes refuses the start instead of being logged and dialled. The hosts call the validator at their configuration boundary, before storage, the node key or genesis is touched:

public static void RefuseMalformed(ChainNodeConfig node, string configPath)

It throws InvalidOperationException naming every offending key, for example DevChain:Node:Network:TrustedPeers:1. The chain profile's bootnodes are checked the same way when the sync stack starts. ChainNodeNetworkConfig.SplitEnodeList turns a comma-separated list into trimmed entries, which is how the mainnet --trusted-peer value and the AppChain peer flags are read.

In short: TrustedPeers = dial + trust (both directions); TrustedBootnodes = trust-if-dialed-elsewhere, outbound only; TrustedNodeIds = inbound trust only, independent of dialing.

How the three hosts use it​

  • AppChain (src/Nethereum.AppChain.Server.Core/AppChainComposition.cs) calls Nethereum.ChainNode.Hosting.ChainNode.StartAsync directly (compose+serve+sync in one call), then layers consensus (Clique or producer-authority), messaging, and MUD deployment on top using chainNode.Bundle, chainNode.Mempool, chainNode.Listener. It's the only host that turns on Sync.FloorTargetPeerCountByDialPool — set in DefaultNode()'s Sync block (AppChainServerConfig.DefaultNode; it also turns on Mempool.EnableTrustedPeerAdmission).
  • Mainnet (src/Nethereum.MainnetChain/Hosting/MainnetNodeComposition.cs) wraps ChainNode in an IHostedService (MainnetNodeRuntime). It calls ChainNode.ComposeAsync with a custom storageFactory (to inject a signer), runs its own boot-recovery/integrity gate (_recoveryGate.EnsureConsistentOrEscalate), then StartSyncAsync with a minPeerLatestBlockFactory, and only afterwards — in its own StartAsync — starts DNS seeding, inbound serving (StartServingAsync), and discv4/discv5 discovery. Starting discv4/discv5 discovery is mainnet's own orchestration responsibility (it runs in MainnetNodeRuntime, not in ChainNode.Hosting), but it reads its discovery settings off the shared ChainNodeConfig.Network.Discovery sub-config: MainnetNodeRuntime consults Network.Discovery.DisableDiscv4/Discv4Port/DisableDiscv5/Discv5Port (MainnetNodeComposition.cs), reading only ListenPort off the flat MainnetChainServerConfig (MainnetNodeComposition.cs). That sub-config is itself seeded from the flat CLI fields by MainnetChainNodeConfigFactory.MapNetwork (Configuration/MainnetChainNodeConfigFactory.cs).
  • DevChain (src/Nethereum.DevChain/Hosting/DevChainComposition.cs) does not call ChainNode.ComposeAsync/StartAsync at all — it already owns its storage bundle (DevChainNode) and instead calls the same lower-level building blocks ChainNode itself calls internally: ChainNodeMempool.Create, ChainNodeServeListener.StartAsync (gated on Network.Serve), and ChainNodeSyncStack.StartAsync (gated on Sync.Mode != None), wiring them together by hand (DevChainComposition.cs).

Confusing/inert surface (read the code, not the field name)​

Superseded by ChainNodeConfigSurface (Configuration/ChainNodeConfigSurface.cs) for AppChain and DevChain: every field below now has one authoritative entry there, marked Live/Disabled/ NotYetWired, with a reflection-checked known consumer for every Live entry (ChainNodeConfigSurfaceCoverageTests) — read that file, not this list, for the current AppChain/ DevChain truth. This list is kept only as history and for mainnet, which the descriptor does not cover yet (its CLI keys are still flat, tracked separately):

  1. Network.Discovery (ChainNodeDiscoveryConfig) — live on mainnet; on AppChain/DevChain now Disabled (a validator throws if you try to enable it), not silently inert.
  2. Maintenance (ChainNodeMaintenanceConfig) — Live on all three nodes via the shared ChainNodeMaintenanceRunner.
  3. Sync.{StartBlock,Blocks,HeadersFrom,HeadersTo,CheckpointEvery,KeepLatestCheckpoints,ReceiptBackfill,ContinueOnMismatch} — still NotYetWired on AppChain/DevChain (the config-to-FollowerOptions translation is not wired yet); mainnet's own backward-walk/checkpoint/receipt-backfill code reads its own flat fields, not this mirror.
  4. Sync.Snap.{BackwardSkeletonPhase1,Phase1Only,Phase1First} — same NotYetWired status as above. Only AdvertiseSnap2 and SoftResponseLimit are Live.
  5. Rpc.{MetricsPort,MaxLogBlockRange,MaxLogResults,GasCap} — now Live on AppChain/DevChain.
  6. Storage.FlushCadenceBlocks — Live on every node kind through FlushCadences.ForBlocks; ChainNodeStorage.BuildStorageOptions does not read it. Storage.PrefetchWorkers and Storage.StateRootWorkers are NotYetWired on AppChain and DevChain: only the mainnet follower composes them.
  7. Network.MirrorRemoteStatus defaults to false through this library, the opposite of the underlying PeerListenerOptions.MirrorRemoteStatus default of true — see the note under Network above.

If you're extending one of these fields, add it to ChainNodeConfigSurface.All — the coverage test fails the build otherwise — rather than assuming setting it changes behaviour.