Cross-Repository Protocol Design & Implementation Audit

Taiko Shasta — Proposal & Block Data: Complete Verification-Chain Audit

A field-by-field trace of every piece of data in a Taiko Proposal and every derived L2 block, showing how each is proved — directly or transitively — against state managed by the protocol contracts on Ethereum L1, and a formal argument that the verification graph is acyclic (no circular dependency).

Scope: taiko-mono · raiko2 · gaiko2 Fork: Shasta (post-Unzen) taiko-mono 9ef35dd raiko2 cb3a5f2 gaiko2 5a7718b Date: 2026-07-22
0
Circular dependencies found
7
L1 trust roots (bedrock)
70+
Distinct data fields traced
3
Independent impls in lock-step
1
Critical bug found (§15)

Headline result. Every piece of proposal-level and block-level data reduces — through a finite, strictly backward-looking chain of cryptographic checks — to one of seven L1 trust roots: the proposal-hash ring buffer, CoreState.lastFinalizedBlockHash, the rest of CoreState, EIP-4844 blob versioned hashes, origin block hashes, the immutable verifier registry (chain id + verifier address + trusted image ids / SGX instances), and the soundness of the proving system itself. The verification graph is a DAG that terminates at these roots; no datum is verified against a value that (directly or indirectly) depends on it.

But the adversarial pass found real bugs. A second, bug-hunting pass (§15) targeted the implementations, not the design. It surfaced a Critical witness-soundness break in the gaiko2 (TEE) prover (D1 — execution over unauthenticated pre-state), a High consensus-split in the Go driver (D2 — a missing fork-time floor), and several Medium/Low issues. These are prover/driver implementation defects, not design cycles: the L1 contracts and the raiko2 ZK guest held up under the same tracing. "Sound design" does not imply "bug-free stack" — see §15.

Contents

  1. Scope & methodology
  2. The seven L1 trust roots
  3. Propose → derive → prove → finalize lifecycle
  4. Data inventory: Proposal & DerivationSource
  5. Data inventory: Manifest (blob-encoded)
  6. Data inventory: derived L2 block & header
  7. Data inventory: anchor transaction
  8. Data inventory: Commitment / Transition / CoreState
  9. Block-derivation rules
  10. The two-layer proof & L1 binding
  11. Verification-dependency graph
  12. Circular-dependency analysis (DAG proof)
  13. Cross-repo consistency matrix
  14. Findings & observations
  15. Adversarial deep-dive (bug hunt)
  16. Key references

1 · Scope & methodology

Three repositories jointly define and verify Taiko's Shasta rollup. This audit reads all three at the commits above and treats them as one system:

RepoRoleLanguageWhat it owns in the trust chain
taiko-mono
/packages/protocol
L1 & L2 smart contracts + the derivation specSolidity Inbox (propose/prove/finalize, ring buffer, CoreState, bonds), verifiers (SGX/RISC0/SP1/compose), Anchor, SignalService, and docs/Derivation.md.
taiko-mono
/packages/taiko-client(-rs)
Node / driver — the reference deriverGo + Rust Converts a Proposed event into L2 blocks (blob→manifest→blocks), builds the anchor tx, inserts via the engine API.
raiko2ZK prover (RISC0 & SP1 guests)Rust Re-derives & re-executes each proposal statelessly inside a zkVM; aggregates into one validity proof matching the L1 Commitment.
gaiko2TEE prover (Intel SGX via EGo)Go Independently re-derives, re-executes, and signs the same commitment inside an SGX enclave; attested key registered on L1.

Method

Two structural facts that govern everything below.

(a) The on-chain Proposal struct is stored only as a hash in a ring buffer; its full contents live off-chain (in the Proposed event + L1 block context + blobs). Whoever reconstructs a proposal must reproduce a struct whose keccak matches the ring-buffer slot — that single equality authenticates all proposal fields at once.

(b) The Proposed event deliberately omits timestamp, originBlockNumber, and originBlockHash. The deriver reconstructs them from the L1 block that emitted the log (timestamp = that block's time; originBlockNumber = emitting block − 1; originBlockHash = that parent's hash). They are re-checked because they are part of the hashed Proposal.

2 · The seven L1 trust roots

These are the only values the whole construction is allowed to assume. Everything else must chain to one of them. All are managed by the protocol contracts on Ethereum (or are a stated cryptographic assumption).

#RootWhere it lives on L1Written by
R1Proposal-hash ring buffer
_proposalHashes[id % ringBufferSize] = keccak256(abi.encode(Proposal))
Inbox.sol:136,286,559propose() at proposal time
R2Last finalized L2 block hash
CoreState.lastFinalizedBlockHash
Inbox.sol:96,378prove() / activate()
R3Rest of CoreStatenextProposalId, lastFinalizedProposalId, lastFinalized/CheckpointTimestamp, lastProposalBlockId Inbox.sol:83-97propose() / prove()
R4EIP-4844 blob versioned hashes — captured via the blobhash() opcode and stored inside the proposal's BlobSlice.blobHashes (→ folded into R1) LibBlobs.sol:44-48propose() / saveForcedInclusion()
R5Origin block hashoriginBlockHash = blockhash(block.number-1), stored in the proposal (→ folded into R1) Inbox.sol:610-618propose()
R6Verifier trust root — two distinct parts: immutable wiring (verifier contract address, taikoChainId, and Inbox._proofVerifier itself) and governance-mutable allowlists (trusted RISC0 imageId via setImageIdTrusted, SP1 vkey via setProgramTrusted, and the SGX instance registry + MRENCLAVE/MRSIGNER policies). Changing an allowlist changes which proofs L1 accepts — see F9. Risc0Verifier.sol, SP1Verifier.sol, SgxVerifier.solImmutable: constructor. Allowlists: owner / DAO controller; SGX via DCAP attestation
R7Soundness of the proving system — the ZK proof system and the SGX TEE/attestation are assumed sound (a cryptographic/hardware assumption, not a checkable value) — (assumption)

R4 and R5 are not separate storage slots — they are values captured by L1 opcodes at propose time and embedded into the Proposal, so they are ultimately anchored by R1. They are called out separately because they are the bridge from "raw off-chain bytes" (blobs, L1 headers) to on-chain commitments.

On R6's mutability (a governance trust assumption). Only the verifier wiring is immutable. The trusted sets R6 depends on — RISC0 image ids, SP1 program keys, and the SGX instance / MRENCLAVE / MRSIGNER allowlists — are on-chain state the verifier owners (the DAO controller, plus DCAP-gated registration for SGX) can change, and a change alters which proofs L1 will finalize. The §12 DAG treats these as L1-managed roots; their integrity is a governance trust assumption, tracked as Observation F9.

3 · Propose → derive → prove → finalize

┌─ L1 (Ethereum) ──────────────────────────────────────────────────────────────┐
 propose(lookahead, data)                              prove(data, proof)
   • dequeue ≤10 forced inclusions (FIFO), append      • decode Commitment
     proposer source last                              • require lastProposalHash == _proposalHashes[lastId]   → R1
   • blobHashes = blobhash(i)         → R4require lastFinalizedBlockHash == firstProposalParentBlockHash → R2
   • originBlockHash = blockhash(n-1) → R5            • saveCheckpoint(endBlockNumber,endStateRoot,lastBlockHash)
   • Proposal = {id,ts,proposer,parent,origin,…,src}   • CoreState.lastFinalized* ← commitment              → R2,R3
   • _proposalHashes[id] = keccak(Proposal)R1      • verifyProof(age, hashCommitment(commitment), proof) → R6,R7
   • emit Proposed(id,proposer,parent,eos,pctg,sources)
└────────────────────────────────────────────────────────────────────────────────┘
        │  Proposed event + blobs                              ▲  Commitment + validity proof
        ▼                                                      │
┌─ Node / driver (taiko-client) ─────────┐   ┌─ Provers (raiko2 ZK / gaiko2 TEE) ──────┐
   reconstruct full Proposal, hash-check      re-derive blocks from (Proposal,blobs) the
   blob → manifest (v0x1|size|zlib|RLP)       same way; re-execute statelessly against
   manifest → L2 blocks (timestamp/gas/       parent state (MPT witness); bind the result
   anchor/coinbase/basefee rules)             into a per-proposal sub-proof, then aggregate
   build anchorV4 tx (golden touch, fixed-k)  into one Commitment whose hash the L1 verifier
   insert via engine API; set head            reproduces.
└─────────────────────────────────────────┘   └─────────────────────────────────────────┘

The driver and the two provers implement the same derivation function D(Proposal, blobs, parentState) → blocks. The driver produces the canonical chain; the provers re-derive and re-execute it and attest that the result is exactly what the rules dictate. L1 then checks the attested commitment against R1/R2 and the proof against R6/R7.

4 · Data inventory — Proposal & DerivationSource

PROPOSAL-LEVEL Struct IInbox.Proposal (IInbox.sol:60-79). The whole struct is authenticated by one equality — hash_proposal(Proposal) == R1 — verified in every prover (raiko2 guest-common/src/lib.rs:852; gaiko2 guestinput_carry.go:112).

FieldTypeOriginHow verified → root
iduint48Proposed.idhash_proposal; aggregation also checks ids are sequential & ∈ uint48 → R1
timestampuint48L1 block time of the emitting log (not in event)hash_proposalR1; also bound into the sub-proof transition
endOfSubmissionWindowTimestampuint48Proposed.eoshash_proposalR1 (used only by preconf handover; not a block field)
proposeraddressProposed.proposer (= msg.sender at propose)hash_proposalR1; also the Transition.proposer checked equal
parentProposalHashbytes32Proposed.parentProposalHash (= getProposalHash(id-1) on L1)hash_proposal; aggregation checks prev.hash == next.parentProposalHash → chains to R1
originBlockNumberuint48emitting L1 block − 1 (not in event)hash_proposal; bounds anchorBlockNumber; matched to the L1 ancestor-header chain → R1,R5
originBlockHashbytes32blockhash(n-1) at propose (not in event)hash_proposal; the L1 ancestor-header chain's tip must equal thisR1,R5
basefeeSharingPctguint8Proposed.basefeeSharingPctg (immutable config)hash_proposal; also re-encoded into block extraData and checked → R1
sources[]DerivationSource[]Proposed.sourceshash_proposalR1 (see below)

SOURCE-LEVEL Struct IInbox.DerivationSource (IInbox.sol:52-57) and LibBlobs.BlobSlice (LibBlobs.sol:21-28). Each source is either a forced inclusion (queued via saveForcedInclusion) or the proposer's own source (appended last).

FieldTypeHow verified → root
isForcedInclusionboolhash_proposalR1; selects the derivation branch (forced sources inherit parent anchor, must have exactly 1 block)
blobSlice.blobHashes[]bytes32[]hash_proposalR1. The raw blob bytes are additionally bound: KZG(blob) → versionedHash == blobHashes[i] (R4 semantics) in raiko2 blob.rs:117 / gaiko2 blob_validate.go:75
blobSlice.offsetuint24hash_proposalR1; the byte offset the manifest decoder reads from
blobSlice.timestampuint48hash_proposalR1; used for the forced-inclusion delay gate

Blob data-availability is fully proved. The versioned hash is a keccak/SHA of the KZG commitment; the guest recomputes it from the actual blob bytes it derives from and requires equality with the on-chain blobHashes — which are themselves inside the ring-buffer proposal hash. A proposer cannot feed the prover blob bytes different from those committed on L1.

5 · Data inventory — Manifest (decoded from blobs)

DERIVED FROM DA The blob bytes decode into a ProposalManifest of DerivationSourceManifests, each holding BlockManifests. Encoding: [32B version=0x1][32B size][zlib(RLP(manifest))] at blobSlice.offset. This is a pure, deterministic function of the blob bytes, which are bound to R4/R1 — so the manifest needs no separate anchor; the prover simply re-runs the decode and rejects/degrades identically to the driver.

Field (BlockManifest)TypeVerification
timestampuint48/u64Decoded from DA, then clamped (§9); out-of-range ⇒ whole source degrades to default. Final value re-checked into the L2 header.
coinbaseaddressDecoded from DA; for forced/default sources overwritten with proposal.proposer. Re-checked as block beneficiary.
anchorBlockNumberuint48/u64Decoded from DA (0 ⇒ inherit parent); validated monotonic & within [origin−MAX_OFFSET, origin] (§9); binds the anchor tx checkpoint number.
gasLimituint48/u64Decoded from DA; validated ±200 ppm of parent, clamped [10M,45M]; +1,000,000 anchor budget added; re-checked as block gasLimit.
transactions[]SignedTx[] (RLP TxEnvelope)Decoded from DA; the guest re-executes them and requires the produced block (tx set, roots) to match the canonical block byte-for-byte.

Invalid-data policy (all three impls identical): version mismatch, bad size/offset, zlib/RLP failure, trailing bytes, forced source with ≠1 block, or block count over the per-source cap (192 pre-Unzen / 768 Unzen) → the source is replaced by the default manifest (one anchor-only block). This is a deterministic function of authenticated bytes, so provers and driver agree. See Observation F7.

6 · Data inventory — derived L2 block & header

BLOCK-LEVEL Each derived block's header. The prover reconstructs the block from (anchor tx + manifest txs + derived env), executes it statelessly against the parent state, and requires generated.header == canonical.header (raiko2 lib.rs:688-719), which forces every field below to be exactly the derivation-rule value.

Header fieldDetermined byHow verified → root
parentHashprevious block hashBlock i>0: equals block i-1's validated hash. Block 0: equals carry.parentBlockHash = Commitment.firstProposalParentBlockHash = R2
numberparent.number + 1re-derived; consensus check ties to parent header (chained to R2)
timestampmanifest, clamped (§9)re-derived from parent-ts & proposal-ts (∈ R1); header must match
gasLimitmanifest gas + 1,000,000re-derived within ±200 ppm of parent; header must match
beneficiary (coinbase)manifest / proposerre-derived; header must match
extraDatabasefeeSharingPctg ‖ proposalId(6B BE) (7 bytes)encode_extra_data(pctg,id); both inputs ∈ R1
mixHash / prevRandaokeccak(abi.encode(parentMixHash, number))re-derived; recursion bottoms out at parent header (chained to R2)
baseFeePerGasEIP-4396 from parent & grandparent times; genesis ⇒ INITIAL_BASE_FEE; clamped [min,1 gwei]re-derived; enforced by consensus validation + header match
difficulty0 pre-Unzen; engine value at Unzenre-derived; header match
stateRootpost-execution outputGuest executes txs against parent state (MPT witness proven vs parent state_root ∈ parent header ∈ R2) and recomputes the root; must equal header. Also bound into Commitment.endStateRoot for the last block.
transactionsRootreceiptsRootlogsBloomgasUsedpost-execution outputsproduced by stateless execution; consensus/post-state validation enforces them (chained to R2 via parent state)
ommersHashwithdrawalsRootempty (fixed)re-derived constants; header match
parentBeaconBlockRootrequestsHashblobGasUsedexcessBlobGasnoncefixed constants (fork-gated)Post-Unzen (the report's scope) each block must have parentBeaconBlockRoot = 0x0…0, requestsHash = EmptyRequestsHash (EIP-7685), blobGasUsed = excessBlobGas = 0, and nonce = 0; pre-Unzen the beacon root / requests hash must be absent. Enforced by the driver's Unzen-aware canonical-block check (common.go:386-407) and by header/consensus validation in both provers — chained to R2 via the header / block-hash equality.
block hashkeccak of the headerrecomputed (hash_slow), used as next block's parent & (last block) Commitment / Transition.blockHashR1,R2 at prove

The execution-integrity linchpin. The parent header supplies the pre-state root used to materialize the MPT witness and to read parent anchor/checkpoint state. A forged state_root would break soundness — so raiko2 recomputes each full ancestor header's hash on deserialize and discards the host-supplied hash (primitives/src/stateless.rs:132-136: let _host_hash = value.hash; Self::from_header(value.header)), then requires that recomputed hash to equal the child's parent_hash. Compact (hash-only) headers are rejected on the proposal path. Result: fixing the parent hash to R2 fixes the parent state_root too — it cannot be forged independently. gaiko2 enforces the same via l2_state.go:32 (binds the parent header to carry.ParentBlockHash) and replay.go:511.

7 · Data inventory — anchor transaction (anchorV4)

SYSTEM TX Every derived block's first transaction is a golden-touch call to Anchor.anchorV4(Checkpoint{blockNumber,blockHash,stateRoot}) (Anchor.sol:124). It is how L1 state is synced into L2. The provers validate its shape and — crucially — its L1 payload.

ElementValueHow verified → root
selectoranchorV4 (0x523e6854)must prefix calldata; ABI-decoded with canonical uint48 padding
sendergolden touch 0x0000…B4Ecrecovered signer must equal it; execution rejects any other sender (Anchor.onlyValidSender)
signaturedeterministic fixed-k (k∈{1,2})gaiko2 re-signs with the same fixed k and requires an identical tx hash (blocks block-hash malleability)
noncegolden-touch nonce at parentmust equal the pre-state account nonce read from the parent state (∈ R2)
gas limitexactly 1,000,000consensus check; matches ANCHOR_GAS_LIMIT
feesmaxFee = baseFee, maxPriority = 0checked against the derived block base fee
checkpoint.blockNumberthe block's anchorBlockNumbermust equal the derived manifest anchor number (§6, §9)
checkpoint.blockHashL1 block hash at that numbermatched against the l1_ancestor_headers chain whose tip = originBlockHashR1,R5
checkpoint.stateRootL1 state root at that numbersame L1 ancestor-header match → R1,R5

Anchor L1-data is proof-enforced, not merely "node-enforced." Derivation.md says the node ensures anchorBlockHash/anchorStateRoot reflect real L1 state. Both provers actually enforce it: they require a contiguous chain of L1 headers (each parent-linked, hashes recomputed) whose final block equals the proposal's originBlockHash/Number, and match every anchored checkpoint against a header in that chain. Forced-inclusion and "stalled anchor" cases are matched instead against the parent L2 CheckpointStore value read by MPT proof. See Observation F6.

8 · Data inventory — Commitment / Transition / CoreState / Checkpoint

PROVE INPUT Struct IInbox.Commitment (IInbox.sol:122-138). The prover does not accept the commitment as trusted input — the guest reconstructs it from the per-proposal carry data and its continuity checks (raiko2 instance.rs:130-158; gaiko2 hash.go:120).

FieldSource (rebuilt by guest)How verified → root
firstProposalIdfirst carry's idL1: firstProposalId ≤ lastFinalizedProposalId+1 (R3); bound in hashCommitment
firstProposalParentBlockHashfirst carry's parentBlockHash = first block's parent hashL1: == lastFinalizedBlockHashR2
lastProposalHashlast carry's proposal hashL1: == getProposalHash(lastId)R1
actualProverconstant across carriesbound in hashCommitment; drives liveness-bond credit; the proof itself fixes it (see §10)
endBlockNumberlast carry checkpoint number= last derived block number; ∈ hashCommitment; saved as checkpoint
endStateRootlast carry checkpoint state root= last block's post-exec state root (chained to R2); ∈ hashCommitment
transitions[]one {proposer,timestamp,blockHash} per proposaleach block hash chains to the next carry's parent (continuity); ∈ hashCommitment

Aggregation continuity (instance.rs:69-127, aggregate_validate.go:104): sequential proposal ids; prev.proposalHash == next.parentProposalHash; prev.checkpoint.blockHash == next.parentBlockHash; constant chainId, verifier, actualProver; all uint48 fields range-checked. This is what lets a single ring-buffer entry (R1, on the last proposal) plus a single R2 value (on the first block's parent) authenticate an entire multi-proposal batch.

L1 OUTPUTS CoreState (R2/R3) and the Checkpoint saved to SignalService are outputs of prove(), computed from the verified commitment. They are consumed by future derivation (the next batch's R2, and the L1→L2 checkpoint the anchor syncs) — never by the proof that produced them. This forward-only flow is the backbone of the acyclicity argument (§12).

9 · Block-derivation rules

The exact rules that turn a manifest + parent context into block metadata. Identical in Derivation.md, the Go/Rust driver, and both provers. Any block whose fields violate these is either rejected (consensus) or the whole source degrades to the default manifest.

FieldRuleOn violation
timestamplower ≤ ts ≤ proposal.timestamp, where lower = max(parent.ts+1, proposal.ts − TIMESTAMP_MAX_OFFSET, SHASTA_FORK_TIME)source → default manifest
anchorBlockNumberparent.anchor ≤ n ≤ origin and n ≥ origin − MAX_ANCHOR_OFFSET; a normal source must strictly advance the anchorsource → default manifest (forced sources may stall)
gasLimitlower ≤ g ≤ upper where upper = min(parent·(1e6+200)/1e6, 45M), lower = min(max(parent·(1e6−200)/1e6, 10M), upper); then +1,000,000source → default manifest
coinbaseforced/default → proposal.proposer; normal → manifest value
timestamp/difficulty/numbernumber=parent+1; difficulty=keccak(abi.encode(parentMixHash,number))
baseFeeEIP-4396 (parent & grandparent block times), clamped to [chainMin, 1 gwei]; genesis → 0.025 gweiconsensus reject

Governing constants (identical across all impls)

ConstantValue (non-mainnet / mainnet)
DERIVATION_SOURCE_MAX_BLOCKS (pre-Unzen / Unzen)192 / 768
MAX_ANCHOR_OFFSET128 / 512
TIMESTAMP_MAX_OFFSET1536 s (12×128) / 6144 s (12×512)
BLOCK_GAS_LIMIT_MAX_CHANGE / denominator200 / 1,000,000 (±0.02%)
MIN / MAX block gas limit10,000,000 / 45,000,000
ANCHOR_GAS_LIMIT (reserved + tx gas)1,000,000
SHASTA_PAYLOAD_VERSION / blob bytes0x1 / 131,072
MAX_FORCED_INCLUSIONS_PER_PROPOSAL10
MAINNET_ANCHOR_CHECK_SKIP_PROPOSAL_OFFSET7 (bootstrap; see F5)

10 · The two-layer proof & its L1 binding

Layer A — per-proposal sub-proof

For one proposal, the guest (prove_shasta_proposal, raiko2 lib.rs:1000) verifies, in order:

Public output = hash_shasta_subproof_input(carry) = keccak(VERIFY_PROOF, chainId, verifier, hash_transition_input(carry)), where the transition-input hash binds all continuity-critical fields (proposal id/hash/parent, parent block hash, prover, transition, end checkpoint).

Layer B — aggregation → L1 commitment

The aggregation guest (aggregate_shasta_zk_with_verifier, lib.rs:1058) verifies each sub-proof (RISC0 receipt / SP1 verify_sp1_proof), requires each sub-proof's journal to equal hash_shasta_subproof_input(carry_i), rebuilds the Commitment from the carries with the continuity checks, and outputs:

shasta_zk_aggregation_output(image_id,
    hash_public_input( hash_commitment(commitment), chainId, verifier, prover ))

The Rust hash_commitment reproduces Solidity LibHashOptimized.hashCommitment word-for-word (libhash/shasta.rs:69-106 vs LibHashOptimized.sol:32-84), and hash_public_input reproduces LibPublicInput.hashPublicInputs. gaiko2's Go encoders (hash.go) do the same.

L1 prove() — the closing checks

// Inbox.prove (Inbox.sol:321-398)
require(state.lastFinalizedBlockHash == expectedParentHash);       // → R2
require(commitment.lastProposalHash == getProposalHash(lastId));   // → R1
_signalService.saveCheckpoint(endBlockNumber,endStateRoot,lastHash);
_coreState.lastFinalized* = commitment.*;                          // → R2,R3 (for next batch)
_proofVerifier.verifyProof(age, hashCommitment(commitment), proof); // → R6,R7

The verifier reproduces the same public input from hashCommitment(commitment), its own address, and taikoChainId (all R6), and checks the ZK proof / SGX signature against it. Because the verifier address and chain id are baked into the public input, a proof made for a different verifier or chain simply fails to reproduce — the binding is self-enforcing.

Defence-in-depth verifier policy. Mainnet finalization runs through a composite verifier requiring two sub-proofs. The post-Unzen ZkRequiredVerifier structurally mandates that the second is always a ZK proof (SGX+ZK or ZK+ZK) — no TEE-only pair can finalize. See Finding F1 for the deprecated MainnetVerifier it replaces.

11 · Verification-dependency graph

Every arrow reads "is verified against". Data flows downward until it rests on the L1 bedrock (green). There is no upward arrow — the graph is a DAG grounded at L1.

Proposal / DA data Derived block / execution Prover check (guest) L1 trust root (bedrock)
Blob bytes (DA) manifest, tx lists Derived L2 blocks headers, state, tx roots anchorV4 checkpoint L1 blockNo/hash/root Proposal fields id, origin, proposer… KZG → versioned hash blob.rs / blob_validate.go re-derive + re-execute stateless, header match L1 ancestor-header chain tip == originBlockHash hash_proposal == ring-buffer slot parent header hash recomputed == child.parentHash (state_root bound) aggregation: rebuild Commitment + continuity seq ids · proposalHash chain · blockHash chain Inbox.prove + verifyProof(hashCommitment) lastProposalHash∈R1 · parent∈R2 · proof∈R6,R7 L1 TRUST ROOTS (Ethereum) — bedrock, no outgoing dependency R1 ring buffer R2 lastFinalizedBlockHash R3 CoreState R4 blob hashes R5 origin hash R6 verifier reg. R7 proving-system soundness (ZK / SGX attestation)
Reduction of proposal & block data to the seven L1 roots. Every path terminates in the green bedrock; no arrow points back up.

12 · Circular-dependency analysis

Let the nodes be the data items and the directed edges be "A is verified against B". A circular dependency exists iff this graph has a cycle. We claim it is a DAG grounded at R1–R7. The argument has two parts.

12.1 · Temporal grounding (the batch chain is backward-only)

A proof for batch N consumes only values that were on L1 before the proof was submitted and that do not depend on the proof's own output:

The proof's outputs — the new lastFinalizedBlockHash, checkpoint, and CoreState — become roots for batch N+1. Ordering nodes by (batch index, propose-before-prove) makes every edge point to a strictly earlier item. A strict order cannot contain a cycle. Base case: activate() seeds R2 with the last Pacaya block hash (LibInboxSetup.activate), independent of any Shasta proof.

12.2 · Each candidate cycle is explicitly broken

Candidate cycleWhy it does not closeEvidence
block hash ↔ state root ("hash needs root, root needs execution, execution needs hash") A block's own state root is an output; execution consumes the parent's state root — a distinct, earlier block. No self-reference. validation.rs:360 post-state check; parent via determine_pre_state_root
parent header ↔ parent state root (forge a header with a fake state_root) Full ancestor header hashes are recomputed and the host hash discarded; the recomputed hash must equal the child parent_hash which chains to R2. Fixing the hash fixes the header bytes, incl. state_root. stateless.rs:132-136 (_host_hash dropped); compute_ancestor_hashes_for_child:510
L1 data in L2 ↔ L1 state (anchor checkpoint verified against L2) Anchored L1 hash/root is matched against an L1 header chain whose tip is the proposal's originBlockHash ∈ R1/R5 — an L1 value, never an L2-derived one. validate_l1_anchor_linkage:133-294
proposal ↔ parent proposal (the hash chain) Finite, strictly-decreasing id chain anchored at the top by the last proposal's ring-buffer entry (R1). Each link is a field already inside an authenticated struct. instance.rs:107-110; L1 Inbox.sol:349
Commitment ↔ proof ("the proof verifies the commitment; the commitment is the proof's input") Not a data cycle: the circuit computes the commitment from R1/R2/R4-anchored inputs and commits its hash as output (R7). L1 independently re-checks lastProposalHash∈R1 and parent∈R2. The proof does not verify itself. Inbox.sol:346-398
parent anchor number ↔ current anchor Parent anchor number is read from L2 Anchor._blockState by MPT proof against the parent state root (∈ R2), plus an optional host hint that must equal the proven value. Current anchor is validated relative to that fixed parent value. verified_parent_anchor_block_number:372-400

Conclusion. No cycle survives. Every proposal-level and block-level datum is verified — directly or through a finite backward chain — against R1–R7, all of which are L1-managed state or a stated cryptographic assumption. The requirement "each piece of data must be verified against the data managed by the protocol contracts on L1" is satisfied, with no circular dependency.

13 · Cross-repo consistency matrix

The three implementations must agree bit-for-bit or a valid proof could fail to finalize (liveness) — or worse, a wrong block could be accepted by one but not caught by another. Verified equal:

ElementL1 (Solidity)raiko2 (Rust ZK)gaiko2 (Go SGX)driver (Go/Rust)
hash_proposalkeccak(abi.encode)✓ shasta.rs:108✓ guestinput_carry.go:557✓ (implicit)
hashCommitment layoutLibHashOptimized:32✓ shasta.rs:69✓ hash.go:146n/a
hashPublicInputsLibPublicInput:18✓ shasta.rs:178✓ hash.go:84n/a
derivation constants (§9)Derivation.md✓ constants.rs✓ manifest_validate.go✓ manifest.go / constants.rs
extraData = pctg‖id(6B)spec✓ payload_helpers.rs:41✓ :1188✓ input.go:166
anchor tx shape (golden touch, fixed-k, 1M gas)Anchor.sol✓ lib.rs:546✓ manifest_validate.go:974✓ anchor.rs:102
invalid-manifest → defaultspec✓ derivation.rs✓ manifest_validate.go:357✓ source_fetcher.go

14 · Findings & observations

The core design is sound and complete (§12). The items below are security context, prover-implementation notes, and edges to watch. Severity reflects impact if the caveat conditions hold; confidence states how much was verified first-hand here.

F0Complete data coverage, no circular dependencyPositive

Confidence: High — linchpin bindings (parent-header hash recompute, ancestor-chain anchoring, ring-buffer & R2 checks) read first-hand in raiko2 + L1 contracts.

Every field enumerated in §4–§8 reduces to R1–R7. The verification graph is an acyclic, L1-grounded DAG. This is the headline result and the answer to the audit's central question.

F1Deprecated MainnetVerifier still live — accepts a zero-ZK (SGX+SGX) pairContext · Being remediated

Confidence: High (both contracts read). Repo: taiko-mono.

The currently-live mainnet verifier (MainnetVerifier, 0x7180…) accepts SGX_GETH + SGX_RETH with no ZK proof — the exact combination behind the June-2026 forged-proof incident. Its contract header marks it DEPRECATED. The replacement ZkRequiredVerifier (0x7284…, Proposal-0019 / Unzen) structurally requires ≥1 ZK proof per batch. Because Inbox._proofVerifier is immutable, cutting over requires an Inbox upgrade (the mechanism Proposal-0019 uses).

Note: This concerns the strength of root R7, not the data-verification graph. Until the Unzen inbox is the active one, finalization safety rests on the SGX registry (R6) alone for TEE-only pairs. Recommend confirming the cutover has executed on the target network before relying on the ZK mandate.

F2gaiko2 native mode ships a published key = golden-touch key; unauthenticated prove endpointsHigh (config-dependent)

Confidence: Medium-High on the structural facts (subagent code read + gaiko2's own in-repo audit docs/audits/); not line-verified first-hand in this pass. Repo: gaiko2.

gaiko2's "native" proving mode uses a hard-coded, publicly-known private key (the same golden-touch key used to sign anchors), with instance id 0xDEADC0DE, and the /prove/shasta* HTTP endpoints are unauthenticated; the aggregate endpoint executes no blocks. If a native/mock instance address is ever registered on-chain in the SgxVerifier, it becomes a proof-forgery oracle for the SGX leg.

Mitigations already in place: mainnet SGX registration is gated by DCAP attestation and an owner allowlist (a mock instance cannot self-register), and — post-Unzen — the ZK mandate (F1) means an SGX forgery alone cannot finalize. Recommend: assert in deploy tooling that no native/mock instance id is ever registered on a production verifier, and require auth on any internet-exposed prove endpoint.

F3gaiko2 block-replay path does not check db.Error() for missing witness nodesConfirmed & elevated → see D1 (Critical)

Confidence: High — this lead was verified first-hand in the deep-dive. Repo: gaiko2.

The medium-confidence lead recorded here has been confirmed and elevated to a Critical finding: GethRunner.Execute computes the state root via IntermediateRoot without ever checking db.Error(), so a withheld witness node reads as empty/0 and can finalize a wrong state root, while the sibling paths (l2_state.go, manifest_tx_filter.go) and raiko2's sparse trie all fail closed. Full exploit chain, scope, and fix are in finding D1 (§15).

F5Mainnet bootstrap: parent anchor read from the parent's anchor calldata (proposals id ≤ 7)Low (bounded)

Confidence: High (constant + driver logic confirmed; raiko2 has the should_bypass_stalled_anchor_linkage analog).

For the first MAINNET_ANCHOR_CHECK_SKIP_PROPOSAL_OFFSET = 7 mainnet proposals, the parent anchor block number is recovered from the parent block's anchorV4/anchorV3 calldata instead of from Anchor._blockState. The calldata is part of an already-authenticated L2 block, so the value stays bound; the exception is bounded to genesis bootstrap. No action required beyond awareness that this special path exists and should not be generalized.

F6Anchor L1-data is stronger than the spec implies — it is proof-enforcedPositive (clarification)

Confidence: High (both provers read).

Derivation.md states the node "enforces" that anchorBlockHash/anchorStateRoot reflect L1 state, which could read as an unproved trust assumption. In fact both provers cryptographically bind these to a contiguous L1 header chain terminating at originBlockHash (∈ R1/R5). Recommend: update the spec wording to say "proved," to avoid a future reader treating it as out-of-protocol trust.

F7"Degrade to default manifest" accepts adversarial bytes then neutralizes themLow (by design)

Confidence: High (all three impls read/consistent).

Malformed blob/manifest content does not fail the proof — the source degrades to a single anchor-only block. This is a deliberate censorship-resistance property (a bad source can't invalidate a good forced inclusion) and is identical across driver and both provers. Soundness rests on the default always being the conservative outcome (empty block, inherited metadata). Recommend: keep the differential tests that assert driver/prover agreement on every degrade trigger; a divergence here is the most likely place a real bug would hide.

F8Proof-age plumbed to verifiers but unused; sub-proof digest has no Inbox/nonce domainLow (observation)

Confidence: High for proof-age (L1 read); Medium for gaiko2 digest (subagent read).

(a) _proposalAge is computed in Inbox.prove and forwarded to every verifier, but no shipped verifier consumes it — it is a forward hook for "prover-killer" handling. (b) The per-proposal signed digest is domain-separated by VERIFY_PROOF + chainId + verifier but not by the Inbox address; cross-deployment replay separation therefore rests on the verifier address being unique per deployment (it is, and it is bound into the public input). Neither is a vulnerability today; both are worth a comment so a future refactor doesn't weaken them.

F9R6 is not uniformly immutable — trusted sets are governance-mutableLow (trust-assumption clarification)

Confidence: High (owner setters read first-hand). Repo: taiko-mono. Raised by: automated PR review, confirmed.

The verifier trust root R6 has two layers. Immutable: the verifier contract addresses, taikoChainId, and Inbox._proofVerifier. Governance-mutable: the trusted RISC0 image ids (Risc0Verifier.setImageIdTrusted, onlyOwner), SP1 program keys (SP1Verifier.setProgramTrusted, onlyOwner), and the SGX instance registry + MRENCLAVE/MRSIGNER allowlists (addInstances / deleteInstances / setMrEnclave / setMrSigner / registerInstance). A change to these sets changes which proofs L1 accepts.

This does not create a cycle — the sets are L1 state, written by governance independently of any single proof — but it is a governance trust assumption: the soundness of R6 rests on the owner / DAO controller (and, for SGX, DCAP attestation) admitting only correct programs and enclaves. Recommend: the report, and any downstream threat model, treat R6's allowlists as governance-controlled state rather than fixed constants.

15 · Adversarial deep-dive — concrete bug hunt

Sections 1–13 prove the design is acyclic and L1-grounded. This section reports a second, adversarial pass whose goal was the opposite: to break it. Eight bug surfaces were each swept by an independent hunter tasked to produce a concrete failing scenario, and — in a parallel workflow — every candidate was put through three independent verifiers (refute / reproduce / cross-impl), keeping only those a majority confirmed against real code. The pass found real implementation bugs. The L1 contracts and the raiko2 ZK guest held up; the defects live in the gaiko2 (TEE) prover and the Go driver, headlined by a gaiko2 witness-soundness break. Per the repository owner's direction, findings are documented in full, including exploit chains.

1
Critical (gaiko2)
1
High (driver)
4
Medium
13
Low / informational

Verification status. The two most severe findings (D1, D2) were re-verified first-hand against the source while writing this. The remaining findings are reported by the single-shot hunters; the parallel 3-vote workflow's independent reconciliation is in progress and may adjust some Medium/Low severities. Each finding below carries an explicit confidence note.

D1gaiko2 executes over unauthenticated pre-state — GethRunner.Execute never checks statedb.Error()Critical (gaiko2)

Repo: gaiko2 · internal/prover/replay.go:51-88 (and the Unzen twin processUnzenReplayBlock, :105-212). Class: soundness. Confidence: High — both sides read first-hand.

The bug. The authoritative state-root path builds a state.StateDB over the witness hash-DB, executes, then returns db.IntermediateRoot(...) (line 82) — with no db.Error() or db.Commit() anywhere. go-ethereum swallows a missing trie node into a deferred error and returns a default value (account treated as non-existent; storage slot reads 0); IntermediateRoot never consults that deferred error — only Commit does, and it is never called. ValidateState(block, db, res, true) returns early in stateless mode before its own root/error check.

Exploit. A malicious prover supplies a witness that omits node B (holding victim account X or a gating storage slot) while keeping its parent branch A, which still references hash(B). During execution the descent to X hits the missing B → the read returns empty/0 with the error set-and-ignored. Because X's subtree is only read, A's hash(B) reference is unchanged, so IntermediateRoot recomputes a root that is self-consistent with a crafted header — while execution used fabricated pre-state. Any path where "reads as 0/empty" benefits the attacker (a bypassed replay-protection / nonce / allowance / processed[id] slot, a zeroed oracle/config, a balance-gated payout) mints or steals; the attacker's own mutations are materialized in the root. Prove only checks result.StateRoot == block.Root() against the attacker's own header — nothing catches it.

Why it's a forgotten check, not by design. The two sibling witness-read paths defend against exactly this: l2_state.go:65-69 calls db.Error() right after db.GetState with a comment — "Surface that error explicitly so an incomplete or corrupt witness cannot masquerade as a legitimately empty storage slot" — and manifest_tx_filter.go re-checks it after every transaction. raiko2's sparse trie fails closed (TrieWitnessError) on any unresolved node.

Scope & severity. A rollup soundness break in gaiko2's state-root producer — a release-blocker for gaiko2 as a standalone / SGX-only / SGX+SGX prover. It is mitigated on a composite verifier that mandates a fail-closed ZK co-prover (post-Unzen ZkRequiredVerifier): the ComposeVerifier requires both sub-proofs over the same commitment hash, and raiko2 (fail-closed) will not produce a ZK proof of the forged commitment — so the forgery cannot finalize there. It can finalize wherever a gaiko2 proof stands without such a co-prover.

Fix. Call db.Error() after execution (or compute the root via db.Commit, which checks it) in both GethRunner.Execute and processUnzenReplayBlock, matching manifestWitnessStateError.

Remediation status: already fixed — gaiko2 PR #45 (merged) landed the statedb.Error() replay guard; the dedicated #46 was closed as a duplicate, and #48 refines the zk-gas / witness-error precedence on top. This audit did not open a duplicate.

D2Go driver omits SHASTA_FORK_TIME from the block-timestamp lower boundHigh (consensus split at fork activation)

Repo: taiko-client (Go driver) · driver/chain_syncer/event/derivation/source_fetcher.go:317-332. Class: liveness / consensus. Confidence: High — verified first-hand.

The bug. ComputeTimestampLowerBound computes max(parent.ts + 1, proposal.ts − TIMESTAMP_MAX_OFFSET) — it has no SHASTA_FORK_TIME floor (its own doc comment claims "the maximum of three constraints" but implements two). The spec (Derivation.md:223) and the other three implementations include it: Rust driver validation.rs:141, raiko2 guest derivation.rs:435, gaiko2 guest manifest_validate.go:456.

Scenario. Shasta activates at a non-genesis timestamp T (the normal rollout). Parent (last pre-fork block) ts = T−2; the first Shasta proposal lands at ts = T+20 (Hoodi offset 1536). Go lower bound = max(T−1, T+20−1536) = T−1; the others = max(…, T) = T. A proposer sets the first block's timestamp to T−1: the Go driver accepts and builds it, while the Rust driver and both provers reject it (ts < lowerBound) and collapse the source to the default manifest. Result — Go-driver nodes build a block that Rust-driver nodes reject and that neither prover can prove: a driver/driver partition and an unprovable chain, precisely at fork activation.

Latent, not live. Genesis-activated chains are unaffected (parent.ts + 1 > fork_ts makes the clamp a no-op), which is why it hasn't fired — it triggers the first time Shasta is scheduled at a future timestamp.

Fix. Add the fork-time floor to ComputeTimestampLowerBound and pass it at both call sites.

Remediation status: a fix adding the fork-time floor to the Go driver is being opened as a separate pull request citing this report (the Rust driver and both provers already have it).

D3gaiko2 skips the anchor excessive-lag window for forced-inclusion sourcesMedium

Repo: gaiko2 · internal/protocol/manifest_validate.go:521-528. Class: liveness / prover split. Confidence: Medium (hunter-reported; workflow reconciliation pending).

The forced-inclusion anchor branch only checks anchor == parentAnchor and returns true; it never applies the origin-window check (anchor < origin − MAX_ANCHOR_OFFSET) that the drivers and raiko2 apply to every block. Scenario: a catch-up proposal [forced (1 block, user txs), normal (advances anchor)] with a stale parent anchor A where origin − A > MAX_ANCHOR_OFFSET. Drivers + raiko2 default the forced source (drop the user txs → empty block); gaiko2 keeps them (non-empty block), then rejects the canonical empty block on the tx-root check → the proposal is unprovable by gaiko2 while raiko2 proves it (prover/prover and prover/driver split). The spec (Derivation.md:236) only exempts forced inclusions from the strict-advance penalty and is silent on the window — that ambiguity produced the split. Fix: apply the excessive-lag window to forced anchors too (and pin the spec so all four impls agree).

D4Mainnet bootstrap (proposal id ≤ 7): parent-anchor source diverges between driver and raiko2Medium

Repos: taiko-client + raiko2 · driver syncer.go:302-308; guest guest-common/src/lib.rs:372-400. Class: liveness (one-time). Confidence: Medium (hunter-reported).

Both drivers recover the parent anchor from the parent block's anchorV4/anchorV3 calldata for mainnet proposals id ≤ MAINNET_ANCHOR_CHECK_SKIP_PROPOSAL_OFFSET (7), because Anchor._blockState.anchorBlockNumber is not yet reliable during bootstrap. raiko2's guest reads the parent anchor only from that storage slot (plus a host cross-check); gaiko2 trusts the host value. So for mainnet id ≤ 7 raiko2's storage-derived anchor can differ from the driver's tx-derived one → the host == verified cross-check fails or yields a wrong anchor → those proposals unprovable by raiko2. A one-time mainnet-launch determinism gap worth confirming against the migration state. Fix: give the guest the same id ≤ 7 tx-based recovery, or guarantee the bootstrap storage matches.

D5L1 forced-inclusion censorship guarantee is unenforced on-chain; escape-hatch config is dead codeMedium

Repo: taiko-mono · Inbox.sol:602-607; unused permissionlessInclusionMultiplier/permissionlessProvingDelay (:121,99); LibForcedInclusion.isOldestForcedInclusionDue (never called). Class: censorship-resistance. Confidence: Medium (hunter-reported).

Forced inclusions are the system's censorship-resistance primitive, but their only consumer — propose — is fully gated by the permissioned _proposerChecker.checkProposer ("Permissionless proposing is temporarily disabled"). The config carries all the scaffolding for a "proposing/proving becomes permissionless once an inclusion is too old" escape hatch (permissionlessInclusionMultiplier, mainnet 160 ≈ 25.6 h), and isOldestForcedInclusionDue exists — but none of it is wired into propose/prove, and checkProposer is never given the inclusion age. If the permissioned set colludes or is unavailable, a due forced inclusion is never processed and its paid fee is locked (no cancel/refund path in saveForcedInclusion) — the guarantee the mechanism exists to provide silently does not hold; it rests on the social expectation of the current whitelist phase. Fix: wire the permissionless escape hatch and add a refund path.

D6Unbounded zlib output — manifest decompression-bomb DoS of the zkVM guestMedium

Repos: all impls · raiko2 manifest.rs:179, gaiko2 manifest_validate.go:387, Go utils/compress.go:69. Class: DoS / liveness. Confidence: Medium (hunter-reported).

Every implementation decompresses the manifest to completion (read_to_end / io.ReadAll) before the block-count cap (192/768) is applied; only the compressed slice is bounded (≤ one blob). A source whose payload is DEFLATE of a long zero-run (~1032:1 ceiling) expands ~130 KB → ~130 MB (single blob) or ~800 MB (multi-blob) in a single allocation, OOM-ing the memory-constrained guest. It is consistent across impls (a DoS, not a state divergence) and gated by L1 blob cost — hence Medium. Fix: a bounded reader capped at max_blocks × max_block_rlp_size.

Lower-severity & informational

IDFindingSevWhereNote
D7gaiko2 BLOCKHASH fails-open for withheld ancestor headersLowgaiko2 replay.go:669-722Same fail-open class as D1, bounded to the ≤256 BLOCKHASH window; raiko2 fails closed.
D8Reverted anchor not rejected on gaiko2's raw replay pathLowgaiko2 replay.goParity gap; caught in the guest-validated flow and on both raiko2 paths.
D9Non-saturating origin − first in stalled-anchor bypassLowgaiko2 manifest_validate.go:1445Underflow needs an L1 reorg regressing the origin; raiko2 saturates. Near-unreachable.
D10raiko2 hashes sub-proof proposal_id as full u64 (no uint48 guard)Lowraiko2 libhash/shasta.rs:43Benign — aggregation guards uint48 in both impls, so an out-of-range id can never finalize.
D11Dead-code derivation.rs with real ABI bugsLowraiko2 libhash/derivation.rsNo caller / no Solidity counterpart; wrong empty-array and bool encodings if ever wired. Recommend delete.
D12Driver beacon path doesn't KZG-verify blob bytes vs commitmentLowtaiko-client blob_datasource.go:163Only commitment→versioned-hash is checked; a bad beacon yields an unprovable chain (local liveness). Add VerifyBlobProof.
D13Empty-blob-hash source: strictness asymmetry (raiko2 rejects inline, gaiko2 continues)Lowraiko2 blob.rs:58 / gaiko2 blob_validate.go:49raiko2's inline-decode path is currently unreachable; latent divergence if ordering changes.
D14CEI order in _dequeueAndProcessForcedInclusions; saveForcedInclusion not nonReentrantLowtaiko-mono Inbox.sol:710,431Not profitable — a reentrant caller only overpays its own fee; no protocol fund loss. Defense-in-depth.
D15init2 finalizes owner-supplied (id, blockHash) with no consistency checkLowtaiko-mono Inbox.sol:217-244Grants nothing beyond existing UUPS upgrade authority (onlyOwner + reinitializer(2)). Centralization note.
D16init3 permanently locks voided forced-inclusion feesLowtaiko-mono Inbox.sol:253-258Owner-gated incident recovery; real user-fund lock with no recovery route.
D17proposalAge overstated vs the liveness "provable-since" basisLowtaiko-mono Inbox.sol:336Nil impact today (all verifiers ignore age); latent if age-based tiering is ever enabled.
D18activate() can reset core state within the 2 h windowLowtaiko-mono Inbox.sol:188-201Owner-only, bounded to 2 h from genesis; orphaned slots cleanly overwritten. Note only.
D19SignalService.saveCheckpoint has no monotonicity / write-once guardLowtaiko-mono SignalService.sol:174-184Safe today (both callers guard order); a future syncer / re-prove path could swap a fresh root for a stale one. Enforce in-function.
I1Manifest version word not fully validated (high 32 bits ignored)Infoall implsIdentical across all three — spec-looseness, not a divergence.
I2SignalService pause freezes all inbound cross-chain verificationInfotaiko-mono SignalService.sol:121,201Owner or immutable pauser can halt bridge consumption. Standard centralization trade-off.
I3Anchor.l1ChainId stored/validated but never read; gaiko2 witness.accounts pinned but unusedInfoAnchor.sol:50 / gaiko2 guestinput.go:127Vestigial fields — confirm no off-chain consumer, else remove to avoid confusion.

Verified clean under adversarial tracing

16 · Key references

TopicFile : line
Proposal / Commitment / Transition structstaiko-mono · packages/protocol/contracts/layer1/core/iface/IInbox.sol:52-156
propose / prove / ring buffer / CoreState…/layer1/core/impl/Inbox.sol:270-400,559-628
Proposal & Commitment hashing (L1)…/layer1/core/libs/LibHashOptimized.sol:24-84
Blob slice / versioned hash capture…/layer1/core/libs/LibBlobs.sol:21-55
Verifier public input…/layer1/verifiers/LibPublicInput.sol:18-52
SGX / RISC0 / SP1 / compose verifiers…/layer1/verifiers/{SgxVerifier,Risc0Verifier,SP1Verifier,compose/*}.sol
Anchor (anchorV4) & checkpoint store…/layer2/core/Anchor.sol:124-186; …/shared/signal/SignalService.sol:174
Derivation specification…/packages/protocol/docs/Derivation.md
ZK guest: derive + execute + bindraiko2 · crates/guest-common/src/lib.rs:427-1034
Commitment reconstruction + continuityraiko2 · crates/primitives-shasta/src/instance.rs:69-187
Parent-header hash recompute (linchpin)raiko2 · crates/primitives/src/stateless.rs:132-136
Blob KZG verificationraiko2 · crates/primitives-shasta/src/blob.rs:30-128
Hashing parity (Rust ↔ Solidity)raiko2 · crates/protocol-shasta/src/libhash/shasta.rs:69-192
TEE prove / aggregate / anchor validategaiko2 · internal/prover/{replay,aggregate}.go; internal/protocol/manifest_validate.go:79-1160
gaiko2 hashing parity + attestationgaiko2 · internal/protocol/hash.go; ego/*
Driver derivation (reference)taiko-mono · packages/taiko-client(-rs)/… source_fetcher / payload / anchor

How to read a claim's strength. Items about the L1 contracts and raiko2's guest were read directly at the commits above and are High confidence. Items about gaiko2 internals (F2, F3, parts of F8) combine a code-mapping pass with gaiko2's own in-repo audit and are flagged Medium; they should be confirmed line-by-line before being actioned or dismissed. Nothing in this report changes protocol code.

Cross-repository protocol design & implementation audit of Taiko Shasta — proposal & block data verification completeness and circular-dependency analysis. Generated 2026-07-22 against taiko-mono 9ef35dd, raiko2 cb3a5f2, gaiko2 5a7718b. This document is an engineering analysis, not a guarantee; the listed cryptographic and hardware assumptions (R7) are out of its scope.