Skip to main content

Nethereum.AppChain.Anchoring

NuGet: Nethereum.AppChain.Anchoring | Source: src/Nethereum.AppChain.Anchoring/

Nethereum.AppChain.Anchoring

PREVIEW — APIs may change between releases.

Anchors AppChain state to any EVM chain (L1 or L2). Commits state roots, block hashes, and optionally compressed block data or ZK proofs to an on-chain anchor contract.

System Architecture​

┌─────────────────────────────────────────────────────────────────────────┐
│ YOUR APPCHAIN │
│ DevChain / CoreChain produces blocks with state roots │
└────────────┬────────────────────────────────────────────────────────────┘
│ eth_getBlockByNumber (standard RPC)
▼
┌─────────────────────────┐ ┌──────────────────────────┐
│ IChainAnchorable │ │ IAnchorSubmissionStrategy │
│ (RpcChainAnchorable) │ │ 7 strategies (DA × Proof) │
│ reads blocks via RPC │ │ builds proof bytes │
└────────────┬────────────┘ └────────────┬─────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ AnchorWorker (IHostedService) │
│ Timer-based: every AnchorCadence blocks, fetches block, calls │
│ strategy.BuildPayload(), submits via IAnchorService │
└────────────┬────────────────────────────────────────────────────────────┘
│ submitAnchor(AggregatedAnchor, bytes proof)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ AppChainAnchor.sol (on anchor chain — L1 or L2) │
│ Validates: chain registered, proof system tier, block continuity │
│ Stores: endBlock, endBlockHash, postStateRoot, blockHashesRoot │
│ Emits: AnchorSubmitted event │
└────────────┬────────────────────────────────────────────────────────────┘
│ events
▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Anchor Indexer │ │ Explorer UI │
│ indexes events │───▶│ Nethereum.Explorer. │
│ to Postgres │ │ Anchoring (Razor CL) │
└──────────────────────┘ └──────────────────────────┘

Two Orthogonal Dimensions​

Anchoring is configured via two independent enums:

Data Availability — what block data is published​

public enum AnchoringDataAvailability : byte
{
None = 0, // anchor struct only, no block data in proof bytes
Calldata = 1, // brotli-compressed block RLP in proof bytes
BlobReference = 2 // blob versioned hash in proof bytes (data in EIP-4844 blobs)
}

Proof Mode — how execution is verified​

public enum AnchoringProofMode : byte
{
None = 0, // trust operator (commitment only)
StarkHash = 1, // STARK proof hash on-chain, proof in blobs, off-chain verification
SnarkOnChain = 2 // SNARK proof on-chain, IVerifier.verify() called by contract
}

On-Chain Proof System — maps to Solidity enum​

// C# (mirrors Solidity ProofSystem enum exactly)
public enum AnchoringOnChainProofSystem : byte
{
NoProof = 0, // commitment only
StarkHashOffChain = 1, // STARK hash, off-chain verification
SnarkOnChain = 2 // SNARK, on-chain verification
}
// Solidity (source of truth)
enum ProofSystem {
NoProof, // 0
StarkHashOffChain, // 1
SnarkOnChain // 2
}

Same names, same values. The C# enum byte IS the contract's proofSystem field.

7 Anchoring Strategies​

Each strategy = DA mode × Proof mode. One class per combination, names tell you everything:

Strategy classDAProofWhat it does
AnchoringStrategy_NoDA_NoProof_CommitmentOnlyNoneNoneJust the anchor struct. Cheapest.
AnchoringStrategy_Calldata_NoProof_SyncOnlyCalldataNoneCompressed blocks for node sync. No proof.
AnchoringStrategy_NoDA_StarkHash_OffChainVerifiableNoneStarkHash32-byte STARK hash. Proof in blobs.
AnchoringStrategy_Calldata_StarkHash_SyncAndOffChainVerifiableCalldataStarkHashSync data + STARK hash combined.
AnchoringStrategy_NoDA_SnarkOnChain_TrustlessVerificationNoneSnarkOnChainSNARK proof verified on-chain. Trustless.
AnchoringStrategy_Calldata_SnarkOnChain_SyncAndTrustlessVerificationCalldataSnarkOnChainSync data + SNARK proof.
AnchoringStrategy_BlobRef_SnarkOnChain_TrustlessVerificationWithBlobDABlobRefSnarkOnChainBlob DA + SNARK proof.

Factory​

var strategy = AnchoringStrategyFactory.Create(
AnchoringDataAvailability.Calldata,
AnchoringProofMode.StarkHash);
// Returns: AnchoringStrategy_Calldata_StarkHash_SyncAndOffChainVerifiable

Invalid combinations are rejected at creation time:

  • BlobReference + None → error (blob DA without proof is pointless)
  • BlobReference + StarkHash → error (STARK proof is already in blobs)

Quick Start​

Minimal — commitment only​

var config = new AnchorConfig
{
Enabled = true,
ChainId = 420420,
TargetChainId = 10, // Optimism
TargetRpcUrl = "https://mainnet.optimism.io",
AnchorContractAddress = "0xYourAnchorContract",
SequencerPrivateKey = operatorKey,
AnchorCadence = 100,
AnchorIntervalMs = 60000,
DataAvailability = AnchoringDataAvailability.None,
ProofMode = AnchoringProofMode.None
};

var anchorService = new AppChainAnchorBatchService(config, web3, appChainId, genesisHash);
var chainAnchorable = new RpcChainAnchorable(appchainWeb3);
var worker = new AnchorWorker(chainAnchorable, anchorService, config);
await worker.StartAsync(cancellationToken);

With compressed calldata for sync​

config.DataAvailability = AnchoringDataAvailability.Calldata;
// Strategy auto-selected: AnchoringStrategy_Calldata_NoProof_SyncOnly

With STARK proof hash​

config.DataAvailability = AnchoringDataAvailability.None;
config.ProofMode = AnchoringProofMode.StarkHash;
// Strategy auto-selected: AnchoringStrategy_NoDA_StarkHash_OffChainVerifiable

Configuration​

PropertyTypeDefaultDescription
EnabledbooltrueEnable/disable the anchor worker
ChainIdBigInteger1337AppChain's chain ID
TargetChainIdBigInteger—Chain ID of the anchor target
TargetRpcUrlstring—RPC endpoint of the anchor chain
AnchorContractAddressstring—Deployed AppChainAnchor contract
SequencerPrivateKeystring—Operator key for signing
AnchorCadenceint100Anchor every N blocks
AnchorIntervalMsint60000Timer poll interval (ms)
MaxRetriesint3Retry attempts per anchor
RetryDelayMsint5000Base retry delay (exponential backoff)
DataAvailabilityenumNoneDA mode (None, Calldata, BlobReference)
ProofModeenumNoneProof mode (None, StarkHash, SnarkOnChain)

On-Chain Contract​

AggregatedAnchor struct​

struct AggregatedAnchor {
uint64 chainId;
bytes32 genesisHash;
uint64 startBlock;
uint64 endBlock;
uint8 anchorVersion;
uint8 proofSystem; // ProofSystem enum (0, 1, or 2)
bytes32 endBlockHash;
bytes32 previousAnchorHash; // chain continuity link
bytes32 blockHashesRoot; // Merkle root of block hashes
bytes32 postStateRoot;
bytes32 manifestHash;
}

Contract validation (every anchor must pass all of these)​

proof.length <= 4096 bytes
chain is registered
authority.canSubmitAnchor(chainId, msg.sender)
genesisHash matches registration
proofSystem >= chain's minimumProofSystem
anchorVersion >= chain's minimumAnchorVersion
schema exists for anchorVersion
startBlock == previousAnchor.endBlock + 1 (no gaps)
previousAnchorHash == previousAnchor.endBlockHash (chain continuity)
endBlock >= startBlock (non-empty range)
if requiresProof: IVerifier.verify(proof, publicInputs)

Verifier contracts (one per proof system)​

ProofSystemVerifierrequiresProofValidates
0 (NoProof)address(0)falseNothing
1 (StarkHashOffChain)StarkBlobCommitmentVerifierfalseHash format
2 (SnarkOnChain)a deployment-supplied IVerifier (e.g. ZiskVerifierAdapter)trueCryptographic proof

ZiskVerifierAdapter accepts a proof only for exactly 11 public inputs and 64 Zisk public values (8 bytes each). The first eight Zisk public values, 32-bit words each held in a 64-bit public, must hold keccak256(abi.encodePacked(publicInputs)) (a proof without that commitment is refused), and the inputs must name the block the proof executed (block number, pre-state root, state root, parent hash and block hash). A wrong input count, a wrong public-values length or an input that is not the executed block reverts. ZiskVerifier (Zisk 1.3.1 PLONK, Nethereum.AppChain.Anchoring.ZiskVerifier.ZiskVerifierService) takes bytes32 verification keys in verifySnarkProof; the adapter constructor still takes uint64[4] keys.

Additional format verifiers for DA: CalldataFormatVerifier, PipelinePayloadVerifier, CalldataStarkVerifier.

Compression​

Block data uses brotli compression via CompressedEnvelope:

var envelope = CompressedEnvelope.Wrap(blockRlp, CompressionAlgo.Brotli);
var original = CompressedEnvelope.Unwrap(envelope);

Envelope format: [version:1][compression_algo:1][compressed_data]

Restart Recovery​

AppChainAnchorBatchService.InitializeAsync() reads getLatestAnchor(chainId) from the contract on startup. Recovers _lastEndBlock and _lastEndBlockHash so the first anchor after restart has correct previousAnchorHash. The contract enforces previousAnchorHash == prev.endBlockHash — without recovery, restart would fail.

Component Projects​

ProjectPurpose
Nethereum.AppChain.AnchoringCore: strategies, worker, batch service, config
Nethereum.AppChain.Anchoring.PostgresIndexer: EF entities, repositories, event processing
Nethereum.Explorer.AnchoringUI: Razor Class Library with Blazor pages (pluggable)
Nethereum.AppChain.ContractsSolidity: AppChainAnchor, verifiers, foundry tests

Pluggable Explorer UI​

Nethereum.Explorer.Anchoring is a Razor Class Library. The explorer references it via AddAdditionalAssemblies:

app.MapRazorComponents<App>()
.AddAdditionalAssemblies(
typeof(IAnchorExplorerService).Assembly);

Same pattern for future modules: Messaging, Proving, Admin.

Aspire Integration​

// AppHost — anchor service with strategy configured via environment
var anchoring = builder.AddProject("anchoring")
.WithReference(mainchain)
.WithReference(appchain)
.WithEnvironment("Anchoring__DataAvailability", "Calldata")
.WithEnvironment("Anchoring__ProofMode", "None");

Status endpoint GET /:

{
"service": "Nethereum.AppChain.Anchoring",
"anchorContract": "0xe7f1...",
"cadence": 10,
"intervalMs": 30000,
"dataAvailability": "Calldata",
"proofMode": "None"
}

Cost Guide​

ScenarioAnchor targetDAProofMonthly cost (1 block/s)
Development—NoneNone$0
Private consortiumL2NoneNone~$0.03
Enterprise syncL2CalldataNone~$0.10
Enterprise verifiableL1NoneStarkHash~$10 + $0.15/proof
Financial (trustless)L2NoneSnarkOnChain~$52
Full DA + proofL2CalldataStarkHash~$0.15

Ad-Hoc Proofs​

Proofs can also be submitted per-block independently of anchoring, via AppChainProofManager:

  • submitBlockProof(chainId, blockNumber, proof, proofSystem) — operator or authorized prover
  • requestBlockProof{value: bond}(chainId, blockNumber) — anyone posts bond
  • fulfillBlockProof(chainId, blockNumber, proof) — prover earns bond

These are separate from anchor strategies — an AppChain can anchor with CommitmentOnly and still accept ad-hoc proofs for individual blocks.

  • Nethereum.CoreChain — Block headers, state management, compression, DA pipeline
  • Nethereum.Web3 — RPC interaction with anchor chain
  • Nethereum.Contracts — Smart contract encoding for AppChainAnchor
  • Nethereum.Explorer — Base explorer framework