diff --git a/common/batch/batch_cache.go b/common/batch/batch_cache.go index 0c9027c19..0c7da1a58 100644 --- a/common/batch/batch_cache.go +++ b/common/batch/batch_cache.go @@ -939,7 +939,7 @@ func (bc *BatchCache) createBatchHeader(dataHash common.Hash, sidecar *ethtypes. // in order; L1 message transactions are excluded from the payload but their // hashes and queue indices are tracked separately. // -// Exported for derivation Path B (SPEC-005), which must rebuild blob bytes from +// Exported for derivation local verify (SPEC-005), which must rebuild blob bytes from // local L2 blocks using the same encoding the sequencer applied at seal time. func ParsingTxs(transactions []*ethtypes.Transaction, totalL1MessagePoppedBefore uint64) ( txsPayload []byte, @@ -1020,7 +1020,7 @@ func (bc *BatchCache) sealEffectiveBlobCount(blockTimestamp uint64, replayCommit // BlockContext blob the batch builder writes for each block. // Format: Number(8) || Timestamp(8) || BaseFee(32) || GasLimit(8) || numTxs(2) || numL1Messages(2) // -// Exported for derivation Path B (SPEC-005); see ParsingTxs. +// Exported for derivation local verify (SPEC-005); see ParsingTxs. func BuildBlockContext(header *ethtypes.Header, txsNum, l1MsgNum int) []byte { blsBytes := make([]byte, 60) diff --git a/node/derivation/batch_info.go b/node/derivation/batch_info.go index 74fcfbf4b..fa8f6bd15 100644 --- a/node/derivation/batch_info.go +++ b/node/derivation/batch_info.go @@ -61,26 +61,9 @@ type BatchInfo struct { parentTotalL1MessagePopped uint64 // blobHashes is the ordered list of EIP-4844 blob versioned hashes - // declared by the L1 commitBatch tx. Path B uses this to compare + // declared by the L1 commitBatch tx. local verify uses this to compare // against locally-rebuilt versioned hashes (SPEC-005 section 4). blobHashes []common.Hash - - // hasCalldataBlockContexts records whether the L1 commitBatch tx - // carried BlockContexts in calldata (legacy ABI) versus relying on - // the blob payload to encode them at the head (new ABI with - // LastBlockNumber + NumL1Messages). This is the only correct - // discriminator for Path B's blob payload format: - // - true -> blob = TxsPayload (V1 encoding, txs only) - // - false -> blob = TxsPayloadV2 (V2 encoding, blockContexts || txs) - // `batch.Version` byte is NOT a valid discriminator because the - // sequencer's createBatchHeader sets it from - // (isBatchUpgraded, isBatchV2Upgraded) while handleBatchSealing - // chooses encoding from (isBatchUpgraded, V2-fits-in-cap), so - // version=1 batches frequently carry V2-encoded blobs in the - // V1->V2 transition window. Path A already keys off - // `batch.BlockContexts != nil` (see ParseBatch); Path B mirrors - // that with this flag. - hasCalldataBlockContexts bool } func (bi *BatchInfo) FirstBlockNumber() uint64 { @@ -99,65 +82,6 @@ func (bi *BatchInfo) TxNum() uint64 { return bi.txNum } -// ParseBatchMetadataOnly populates BatchInfo using only L1 calldata -- -// it does NOT touch the blob sidecar and does NOT decode any transactions. -// -// Used by Path B (SPEC-005), which verifies the batch by rebuilding the -// blob locally rather than downloading and decoding it. Fields populated: -// batchIndex, version, root, withdrawalRoot, parentTotalL1MessagePopped, -// firstBlockNumber, lastBlockNumber. blockContexts / SafeL2Data / blobs -// are intentionally left empty; callers in Path B must not call derive(). -// -// blobHashes is populated separately by the caller from tx.BlobHashes(). -func (bi *BatchInfo) ParseBatchMetadataOnly(batch geth.RPCRollupBatch) error { - parentBatchHeader := commonbatch.BatchHeaderBytes(batch.ParentBatchHeader) - parentBatchIndex, err := parentBatchHeader.BatchIndex() - if err != nil { - return fmt.Errorf("decode batch header index error:%v", err) - } - totalL1MessagePopped, err := parentBatchHeader.TotalL1MessagePopped() - if err != nil { - return fmt.Errorf("decode batch header totalL1MessagePopped error:%v", err) - } - bi.parentTotalL1MessagePopped = totalL1MessagePopped - bi.root = batch.PostStateRoot - bi.batchIndex = parentBatchIndex + 1 - bi.withdrawalRoot = batch.WithdrawRoot - bi.version = uint64(batch.Version) - bi.lastBlockNumber = batch.LastBlockNumber - // New commitBatch ABI (rollupABI / commitBatchWithProof) leaves - // batch.BlockContexts nil; legacy ABIs (beforeMoveBlockCtxABI, - // legacyRollupABI) populate it from calldata. UnPackData reflects this - // directly. See the field doc on BatchInfo for why version byte cannot - // be used here. - bi.hasCalldataBlockContexts = len(batch.BlockContexts) > 0 - - // Derive firstBlockNumber from parent batch's LastBlockNumber + 1. - // V0 -> V1 transition leaves parent LastBlockNumber unset; in that - // case fall back to decoding the first BlockContext from calldata. - parentVersion, err := parentBatchHeader.Version() - if err != nil { - return fmt.Errorf("decode parent batch header version error:%v", err) - } - if parentVersion == 0 { - if len(batch.BlockContexts) < 2+60 { - return fmt.Errorf("calldata block contexts too short for first block context: have %d, need %d", len(batch.BlockContexts), 2+60) - } - var firstBlock BlockContext - if err := firstBlock.Decode(batch.BlockContexts[2 : 2+60]); err != nil { - return fmt.Errorf("decode first block context error:%v", err) - } - bi.firstBlockNumber = firstBlock.Number - } else { - parentLast, err := parentBatchHeader.LastBlockNumber() - if err != nil { - return fmt.Errorf("decode parent batch header lastBlockNumber error:%v", err) - } - bi.firstBlockNumber = parentLast + 1 - } - return nil -} - // ParseBatch This method is externally referenced for parsing Batch func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { if len(batch.Sidecar.Blobs) == 0 { diff --git a/node/derivation/config.go b/node/derivation/config.go index 20458c41e..9433a9e05 100644 --- a/node/derivation/config.go +++ b/node/derivation/config.go @@ -33,9 +33,9 @@ const ( VerifyModeLayer1 = "layer1" VerifyModeLocal = "local" - // DefaultVerifyMode is pathB: rebuild + compare locally on the happy path, - // no beacon blob fetch. Operators who need the legacy "always pull blob" - // behavior can set --derivation.verify-mode=pathA. + // DefaultVerifyMode is "local": rebuild + compare locally on the happy + // path, no beacon blob fetch. Operators who need the legacy "always + // pull blob" behavior can set --derivation.verify-mode=layer1. DefaultVerifyMode = VerifyModeLocal // DefaultReorgCheckDepth is the number of recent L1 blocks to check for diff --git a/node/derivation/config_test.go b/node/derivation/config_test.go index 94f5cc710..e26ce5889 100644 --- a/node/derivation/config_test.go +++ b/node/derivation/config_test.go @@ -9,7 +9,7 @@ import ( // startup; the validation switch in SetCliContext rejects unknown values // fail-fast so a typo never reaches the main loop. -func TestVerifyMode_DefaultIsPathB(t *testing.T) { +func TestVerifyMode_DefaultIsLocal(t *testing.T) { if got := DefaultConfig().VerifyMode; got != VerifyModeLocal { t.Fatalf("DefaultConfig().VerifyMode = %q, want %q", got, VerifyModeLocal) } @@ -38,7 +38,7 @@ func TestVerifyMode_AcceptsExplicitModes(t *testing.T) { func TestVerifyMode_RejectsUnknown(t *testing.T) { // "hybrid" was the old default; ensure post-removal it's rejected so // stale operator configs fail loud rather than silently falling back to - // pathB. + // local. for _, bad := range []string{"pathC", "hybrid"} { err := validateAndDefaultVerifyModeErr(t, bad) if !strings.Contains(err.Error(), bad) { diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index 26ca2cb93..a7b168a1a 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -61,7 +61,7 @@ type Derivation struct { fetchBlockRange uint64 pollInterval time.Duration logProgressInterval time.Duration - verifyMode string // SPEC-005 section 4.2: "pathA" or "pathB" (default); bound at startup, never switches. + verifyMode string // SPEC-005 section 4.2: "layer1" or "local" (default); bound at startup, never switches. reorgCheckDepth uint64 // SPEC-005 section 4.7.6: how far back to scan for L1 hash divergence each poll. tagAdvancer *tagAdvancer @@ -262,7 +262,7 @@ func (d *Derivation) derivationBlock(ctx context.Context) { d.logger.Error("fetch batch info outline failed", "err", err) return } - d.logger.Info("path B fetched batch metadata", + d.logger.Info("local verify fetched batch metadata", "batchIndex", batchInfo.batchIndex, "version", batchInfo.version, "parentTotalL1Popped", batchInfo.parentTotalL1MessagePopped, @@ -276,7 +276,7 @@ func (d *Derivation) derivationBlock(ctx context.Context) { } lastHeader, err = d.fetchLocalLastHeader(ctx, batchInfo) if err != nil { - d.logger.Error("path B local last-header fetch failed", "batchIndex", batchInfo.batchIndex, "error", err) + d.logger.Error("local verify local last-header fetch failed", "batchIndex", batchInfo.batchIndex, "error", err) return } for i := range rebuilt { @@ -284,13 +284,13 @@ func (d *Derivation) derivationBlock(ctx context.Context) { // TODO reorg batchInfoFull, fetchErr := d.fetchRollupDataByTxHash(lg.TxHash, lg.BlockNumber) if fetchErr != nil { - d.logger.Error("path B self-heal: fetch real batch failed", + d.logger.Error("local verify self-heal: fetch real batch failed", "batchIndex", batchInfo.batchIndex, "error", fetchErr) return } lastHeader, err = d.deriveForce(batchInfoFull) if err != nil { - d.logger.Error("path B self-heal: derive failed", + d.logger.Error("local verify self-heal: derive failed", "batchIndex", batchInfo.batchIndex, "error", err) return } @@ -321,7 +321,7 @@ func (d *Derivation) derivationBlock(ctx context.Context) { d.metrics.SetSyncedBatchIndex(batchInfo.batchIndex) default: // Unreachable: validateAndDefaultVerifyMode rejects unknown values - // at startup and normalises empty to DefaultVerifyMode (pathB). + // at startup and normalises empty to DefaultVerifyMode (local). // If we get here it's a programming error -- a new mode added to // the constant set without a switch arm. Fail loud rather than // silently fall through to stale semantics. @@ -345,7 +345,7 @@ func (d *Derivation) derivationBlock(ctx context.Context) { d.metrics.SetBatchStatus(stateNormal) d.metrics.SetL1SyncHeight(lg.BlockNumber) - // SPEC-005 section 4.7.3: a verified batch (Path A or Path B) advances safe. + // SPEC-005 section 4.7.3: a verified batch (layer1 or local verify) advances safe. d.tagAdvancer.advanceSafe(d.ctx, batchInfo.batchIndex, lastHeader) } diff --git a/node/derivation/metrics.go b/node/derivation/metrics.go index a71849ad6..285525157 100644 --- a/node/derivation/metrics.go +++ b/node/derivation/metrics.go @@ -25,39 +25,16 @@ type Metrics struct { LatestBatchIndex metrics.Gauge SyncedBatchIndex metrics.Gauge - // SPEC-005 section 4.6 Path B counters. PathBTriggered increments once per batch - // processed under VerifyModeLocal; PathBFailed is the unlabelled total. - // PathBFailedByKind carries a "kind" label so dashboards / alerts can split - // failures by category (versioned hash mismatch vs local block missing vs - // encoding error vs ...). Increment both via IncPathBFailedKind so the - // total stays in sync with the sum across kinds. - PathBTriggered metrics.Counter - PathBFailed metrics.Counter - PathBFailedByKind metrics.Counter - - // SPEC-005 §4.2 / §4.6 Path B self-heal counters. On a divergence verdict - // (ErrBatchVerifyDivergence; covers versioned_hash_mismatch + - // blob_count_mismatch) the Path B branch in derivation.go pulls the real - // blob from beacon, re-derives the batch via the V2 engine API - // (NewL2BlockV2, which reorgs locally divergent unsafe blocks), and - // re-runs the shared verifyBatchRoots: - // - // - PathBSelfHealTriggered : self-heal attempt started (divergence detected) - // - PathBSelfHealSucceeded : self-heal completed and verifyBatchRoots passed - // - PathBSelfHealFailedByKind : self-heal failed; sub_kind label = - // blob_unavailable / parse_error / derive_error / roots_mismatch - // - // Temporary EL dependency: NewL2BlockV2 lives in go-ethereum PR #325 - // (https://github.com/morph-l2/go-ethereum/pull/325). go.mod currently - // pins to that PR's HEAD commit; once #325 merges and a release is cut, - // the bump is reverted to the released pseudo-version with no caller - // change. morph-reth's matching change is tracked separately. - PathBSelfHealTriggered metrics.Counter - PathBSelfHealSucceeded metrics.Counter - PathBSelfHealFailedByKind metrics.Counter - - // SPEC-005 section 4.7 Tag management metrics. Replace the (previously absent) - // blocktag instrumentation; on-call alerts should now key off these. + // LocalVerifyTriggered increments once per batch processed under + // VerifyModeLocal -- presence/absence on dashboards confirms the local + // verifier is running. Failure tracking is intentionally not split into + // separate counters; failures surface as Error logs and propagate as + // ErrBatchVerifyDivergence to BatchStatus=stateException. + LocalVerifyTriggered metrics.Counter + + // Tag management metrics. SafeL2BlockNumber / FinalizedL2BlockNumber are + // the canonical "where is the chain now" gauges; the counters track + // transitions for rate-based alerts. SafeAdvanceTotal metrics.Counter FinalizedAdvanceTotal metrics.Counter SafeL2BlockNumber metrics.Gauge @@ -108,42 +85,12 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { Name: "synced_batch_index", Help: "", }, labels).With(labelsAndValues...), - PathBTriggered: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + LocalVerifyTriggered: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: namespace, Subsystem: metricsSubsystem, - Name: "path_b_triggered_total", - Help: "Number of batches verified via SPEC-005 Path B (local-rebuild).", + Name: "local_verify_triggered_total", + Help: "Number of batches processed by the local-rebuild verifier.", }, labels).With(labelsAndValues...), - PathBFailed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: metricsSubsystem, - Name: "path_b_failed_total", - Help: "Path B failures: local block missing, encoding error, or versioned hash mismatch.", - }, labels).With(labelsAndValues...), - PathBFailedByKind: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: metricsSubsystem, - Name: "path_b_failed_by_kind_total", - Help: "Path B failures broken down by kind label (versioned_hash_mismatch, local_block_missing, ...).", - }, append(append([]string(nil), labels...), "kind")).With(labelsAndValues...), - PathBSelfHealTriggered: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: metricsSubsystem, - Name: "path_b_self_heal_triggered_total", - Help: "Times Path B detected a divergence verdict and entered the self-heal branch (pull real blob → re-derive via NewL2BlockV2 → shared verifyBatchRoots).", - }, labels).With(labelsAndValues...), - PathBSelfHealSucceeded: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: metricsSubsystem, - Name: "path_b_self_heal_succeeded_total", - Help: "Times Path B self-heal completed and the shared verifyBatchRoots passed.", - }, labels).With(labelsAndValues...), - PathBSelfHealFailedByKind: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: metricsSubsystem, - Name: "path_b_self_heal_failed_total", - Help: "Path B self-heal failures broken down by sub_kind label (blob_unavailable, parse_error, derive_error, roots_mismatch).", - }, append(append([]string(nil), labels...), "sub_kind")).With(labelsAndValues...), SafeAdvanceTotal: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: namespace, Subsystem: metricsSubsystem, @@ -207,12 +154,8 @@ func (m *Metrics) SetSyncedBatchIndex(batchIndex uint64) { m.SyncedBatchIndex.Set(float64(batchIndex)) } -func (m *Metrics) IncPathBTriggered() { - m.PathBTriggered.Add(1) -} - -func (m *Metrics) IncPathBFailed() { - m.PathBFailed.Add(1) +func (m *Metrics) IncLocalVerifyTriggered() { + m.LocalVerifyTriggered.Add(1) } func (m *Metrics) IncSafeAdvance() { diff --git a/node/derivation/reorg.go b/node/derivation/reorg.go index 27f4cbf80..ed457ec0e 100644 --- a/node/derivation/reorg.go +++ b/node/derivation/reorg.go @@ -111,7 +111,7 @@ func (d *Derivation) handleL1Reorg(reorgAtL1Height uint64) error { // Both situations are recovered by the same op-stack-style "reset to a known // good parent and re-derive forward" pattern: the next derivationBlock poll // re-fetches L1 commit batch logs from the rewound cursor, re-runs Path A or -// Path B verification, and re-populates safe via advanceSafe. Persistent +// local verify verification, and re-populates safe via advanceSafe. Persistent // problems surface naturally when verifyBatchRoots fails on re-derivation. // // L2 chain rollback is intentionally NOT performed here -- the same commit diff --git a/node/derivation/static_scan_test.go b/node/derivation/static_scan_test.go index d075fe137..dea87dbb1 100644 --- a/node/derivation/static_scan_test.go +++ b/node/derivation/static_scan_test.go @@ -118,19 +118,19 @@ func TestNoBlocktagReferences(t *testing.T) { } } -// TestPathBUsesCommonBlobPackage guards SPEC-005 section 3.4: Path B must use +// TestLocalVerifyUsesCommonBlobPackage guards SPEC-005 section 3.4: local verify must use // `common/blob` helpers (the same set tx-submitter calls), not the duplicate // implementations under `common/batch/blob.go`. Codec drift between the two // would cause permanent versioned hash mismatches. -func TestPathBUsesCommonBlobPackage(t *testing.T) { - body, err := os.ReadFile("verify_path_b.go") +func TestLocalVerifyUsesCommonBlobPackage(t *testing.T) { + body, err := os.ReadFile("verify_local.go") if err != nil { - t.Fatalf("read verify_path_b.go: %v", err) + t.Fatalf("read verify_local.go: %v", err) } src := string(body) if !strings.Contains(src, `"morph-l2/common/blob"`) { - t.Fatalf("verify_path_b.go must import morph-l2/common/blob") + t.Fatalf("verify_local.go must import morph-l2/common/blob") } // Sanity check the actual call sites -- import is necessary but not // sufficient; mismatched calls (e.g., commonbatch.CompressBatchBytes) @@ -138,7 +138,7 @@ func TestPathBUsesCommonBlobPackage(t *testing.T) { required := []string{"commonblob.CompressBatchBytes", "commonblob.MakeBlobTxSidecar"} for _, sym := range required { if !strings.Contains(src, sym) { - t.Errorf("verify_path_b.go missing required call %q", sym) + t.Errorf("verify_local.go missing required call %q", sym) } } } diff --git a/node/derivation/tag_advance.go b/node/derivation/tag_advance.go index ecb0d1713..4fc2f1a2d 100644 --- a/node/derivation/tag_advance.go +++ b/node/derivation/tag_advance.go @@ -59,7 +59,7 @@ func newTagAdvancer(l2Client tagL2Client, metrics *Metrics, logger tmlog.Logger) } // advanceSafe is called by the derivation main loop after a batch passes both -// content verification (Path A or Path B) and verifyBatchRoots. It records the +// content verification (layer1 or local verify) and verifyBatchRoots. It records the // new safe head and flushes via SetBlockTags. func (t *tagAdvancer) advanceSafe(ctx context.Context, batchIndex uint64, lastHeader *eth.Header) { if lastHeader == nil { diff --git a/node/derivation/tag_advance_test.go b/node/derivation/tag_advance_test.go index 661913b35..9e9f15b2a 100644 --- a/node/derivation/tag_advance_test.go +++ b/node/derivation/tag_advance_test.go @@ -53,12 +53,7 @@ func newDiscardMetrics() *Metrics { BatchStatus: discard.NewGauge(), LatestBatchIndex: discard.NewGauge(), SyncedBatchIndex: discard.NewGauge(), - PathBTriggered: discard.NewCounter(), - PathBFailed: discard.NewCounter(), - PathBFailedByKind: discard.NewCounter(), - PathBSelfHealTriggered: discard.NewCounter(), - PathBSelfHealSucceeded: discard.NewCounter(), - PathBSelfHealFailedByKind: discard.NewCounter(), + LocalVerifyTriggered: discard.NewCounter(), SafeAdvanceTotal: discard.NewCounter(), FinalizedAdvanceTotal: discard.NewCounter(), SafeL2BlockNumber: discard.NewGauge(), diff --git a/node/derivation/verify.go b/node/derivation/verify.go index df22fb20c..2a73d6615 100644 --- a/node/derivation/verify.go +++ b/node/derivation/verify.go @@ -14,7 +14,7 @@ import ( // a true "verifier reached a verdict of inconsistent" — i.e. the local // chain disagrees with what L1 committed. Currently produced by: // - verifyBatchRoots, when local stateRoot or withdrawalRoot ≠ L1 calldata -// - verify_path_b's pathBFail, for kinds versioned_hash_mismatch and +// - verify_local's rebuildBlob, for kinds versioned_hash_mismatch and // blob_count_mismatch // // Call sites in derivation.go gate `metrics.SetBatchStatus(stateException)` @@ -30,7 +30,7 @@ var ErrBatchVerifyDivergence = errors.New("batch verify: divergence verdict") // SPEC-005 section 3.4 invariant: this check is independent of blob data -- both // batchInfo.root (postStateRoot) and batchInfo.withdrawalRoot are extracted // from L1 calldata at parse time, so this function runs identically under -// Path A (online beacon blob) and Path B (local-rebuild) verification modes. +// layer1 (beacon blob) and local-rebuild verification modes. // // Returns nil on match. On mismatch the error wraps ErrBatchVerifyDivergence // so callers can distinguish a real divergence verdict from a transient diff --git a/node/derivation/verify_local.go b/node/derivation/verify_local.go index 4a64cf1cf..798411571 100644 --- a/node/derivation/verify_local.go +++ b/node/derivation/verify_local.go @@ -14,7 +14,7 @@ import ( commonblob "morph-l2/common/blob" ) -// SPEC-005 section 4 Path B: blob-independent batch content verification. +// SPEC-005 section 4 local verify: blob-independent batch content verification. // // In VerifyModeLocal the node does not pull blobs from the beacon chain on // the happy path. Instead it reads the L2 blocks in the batch range from @@ -27,16 +27,14 @@ import ( // // On versioned_hash_mismatch the spec (SPEC-005 §4.3) calls for a // single-batch self-heal: pull the real blob from beacon, decode + derive -// the batch via the existing Path A engine API path (which would replace -// the locally divergent blocks via EL forkchoice), then re-run the shared +// the batch via the layer1 engine API path (which would replace the +// locally divergent blocks via EL forkchoice), then re-run the shared // verifyBatchRoots. That self-heal is **currently TODO** and not wired // up here -- it is blocked on the EL number-continuity check (`params.Number // == latestNumber + 1` in morph-reth `crates/engine-api/src/builder.rs` // and go-ethereum `eth/catalyst/l2_api.go`) being relaxed in a separate // spec. Until then a versioned_hash_mismatch falls through to the legacy -// failure path (log + return + retry next poll) under -// path_b_failed_by_kind_total{kind="versioned_hash_mismatch"} and the -// path_b_self_heal_* counters stay at 0. +// failure path (log + return + retry next poll). // // Mode is selected at startup via --derivation.verify-mode and is not // switchable at runtime. @@ -44,7 +42,14 @@ import ( // fetchBatchInfoOutline pulls the L1 commitBatch tx, decodes its calldata, // and populates a BatchInfo using only the calldata + tx blob hashes -- no // beacon blob fetch. Returned BatchInfo is sufficient for -// verifyBatchContentPathB and verifyBatchRoots. +// verifyBatchContentLocal and verifyBatchRoots. +// +// Only the new commitBatch ABI (rollupABI commitBatch / commitBatchWithProof) +// is supported. lastBlockNumber comes from batch.LastBlockNumber and +// firstBlockNumber from parent header's LastBlockNumber + 1. Legacy-ABI +// batches (calldata BlockContexts + V1 blob encoding) are not handled here +// -- they only exist on historical batches that have long since been +// finalized. func (d *Derivation) fetchBatchInfoOutline(ctx context.Context, txHash common.Hash, blockNumber uint64) (*BatchInfo, error) { tx, pending, err := d.l1Client.TransactionByHash(ctx, txHash) if err != nil { @@ -58,18 +63,39 @@ func (d *Derivation) fetchBatchInfoOutline(ctx context.Context, txHash common.Ha return nil, err } - bi := new(BatchInfo) - if err := bi.ParseBatchMetadataOnly(batch); err != nil { - return nil, fmt.Errorf("parse batch metadata error: %w", err) + parentHeader := commonbatch.BatchHeaderBytes(batch.ParentBatchHeader) + parentBatchIndex, err := parentHeader.BatchIndex() + if err != nil { + return nil, fmt.Errorf("decode batch header index error:%v", err) + } + parentTotalL1Popped, err := parentHeader.TotalL1MessagePopped() + if err != nil { + return nil, fmt.Errorf("decode batch header totalL1MessagePopped error:%v", err) + } + + bi := &BatchInfo{ + batchIndex: parentBatchIndex + 1, + version: uint64(batch.Version), + root: batch.PostStateRoot, + withdrawalRoot: batch.WithdrawRoot, + parentTotalL1MessagePopped: parentTotalL1Popped, + lastBlockNumber: batch.LastBlockNumber, + l1BlockNumber: blockNumber, + txHash: txHash, + nonce: tx.Nonce(), + blobHashes: tx.BlobHashes(), } - bi.l1BlockNumber = blockNumber - bi.txHash = txHash - bi.nonce = tx.Nonce() - bi.blobHashes = tx.BlobHashes() + + parentLast, err := parentHeader.LastBlockNumber() + if err != nil { + return nil, fmt.Errorf("decode parent batch header lastBlockNumber error:%v", err) + } + bi.firstBlockNumber = parentLast + 1 + return bi, nil } -// verifyBatchContentPathB rebuilds blob versioned hashes from local L2 +// verifyBatchContentLocal rebuilds blob versioned hashes from local L2 // blocks in the [batchInfo.firstBlockNumber, batchInfo.lastBlockNumber] // range and compares them against batchInfo.blobHashes (taken from the L1 // commitBatch tx). Returns nil on match. @@ -91,14 +117,13 @@ func (d *Derivation) fetchBatchInfoOutline(ctx context.Context, txHash common.Ha // sentinel) vs "verifier produced a divergence verdict" (wrap // ErrBatchVerifyDivergence) and update the SentinelContract test. func (d *Derivation) rebuildBlob(ctx context.Context, batchInfo *BatchInfo) ([]common.Hash, error) { - d.metrics.IncPathBTriggered() + d.metrics.IncLocalVerifyTriggered() // Standard log fields used by every failure-path Error log. Per-site // kvs are appended at the call site. logBase := []interface{}{ "batchIndex", batchInfo.batchIndex, "version", batchInfo.version, - "hasCalldataBlockContexts", batchInfo.hasCalldataBlockContexts, "firstBlock", batchInfo.firstBlockNumber, "lastBlock", batchInfo.lastBlockNumber, "parentTotalL1Popped", batchInfo.parentTotalL1MessagePopped, @@ -106,15 +131,15 @@ func (d *Derivation) rebuildBlob(ctx context.Context, batchInfo *BatchInfo) ([]c } if batchInfo.firstBlockNumber == 0 || batchInfo.lastBlockNumber < batchInfo.firstBlockNumber { - d.logger.Error("path B verification failed: invalid block range", + d.logger.Error("local verify verification failed: invalid block range", append([]interface{}{"kind", "invalid_block_range"}, logBase...)...) - return nil, fmt.Errorf("path B [invalid_block_range]: invalid block range [%d, %d]", + return nil, fmt.Errorf("local verify [invalid_block_range]: invalid block range [%d, %d]", batchInfo.firstBlockNumber, batchInfo.lastBlockNumber) } if len(batchInfo.blobHashes) == 0 { - d.logger.Error("path B verification failed: no blob hashes recorded", + d.logger.Error("local verify verification failed: no blob hashes recorded", append([]interface{}{"kind", "empty_blob_hashes"}, logBase...)...) - return nil, fmt.Errorf("path B [empty_blob_hashes]: no blob hashes recorded for batch %d", batchInfo.batchIndex) + return nil, fmt.Errorf("local verify [empty_blob_hashes]: no blob hashes recorded for batch %d", batchInfo.batchIndex) } bd := commonbatch.NewBatchData() @@ -123,21 +148,21 @@ func (d *Derivation) rebuildBlob(ctx context.Context, batchInfo *BatchInfo) ([]c for n := batchInfo.firstBlockNumber; n <= batchInfo.lastBlockNumber; n++ { block, err := d.l2Client.BlockByNumber(ctx, big.NewInt(int64(n))) if err != nil { - d.logger.Error("path B verification failed: read local block", + d.logger.Error("local verify verification failed: read local block", append([]interface{}{"kind", "local_block_read_error", "blockNumber", n, "cause", err}, logBase...)...) - return nil, fmt.Errorf("path B [local_block_read_error]: read local block %d failed: %w", n, err) + return nil, fmt.Errorf("local verify [local_block_read_error]: read local block %d failed: %w", n, err) } if block == nil { - d.logger.Error("path B verification failed: local block missing", + d.logger.Error("local verify verification failed: local block missing", append([]interface{}{"kind", "local_block_missing", "blockNumber", n}, logBase...)...) - return nil, fmt.Errorf("path B [local_block_missing]: local block %d missing", n) + return nil, fmt.Errorf("local verify [local_block_missing]: local block %d missing", n) } txsPayload, l1TxHashes, newTotal, l2TxNum, err := commonbatch.ParsingTxs(block.Transactions(), totalL1MessagePopped) if err != nil { - d.logger.Error("path B verification failed: parse local block txs", + d.logger.Error("local verify verification failed: parse local block txs", append([]interface{}{"kind", "parsing_txs_error", "blockNumber", n, "cause", err}, logBase...)...) - return nil, fmt.Errorf("path B [parsing_txs_error]: parsingTxs failed at block %d: %w", n, err) + return nil, fmt.Errorf("local verify [parsing_txs_error]: parsingTxs failed at block %d: %w", n, err) } l1MsgNum := int(newTotal - totalL1MessagePopped) blockCtx := commonbatch.BuildBlockContext(block.Header(), l2TxNum+l1MsgNum, l1MsgNum) @@ -145,41 +170,19 @@ func (d *Derivation) rebuildBlob(ctx context.Context, batchInfo *BatchInfo) ([]c totalL1MessagePopped = newTotal } - // Pick V1 or V2 blob payload format. The discriminator is the L1 - // commitBatch ABI variant — NOT the BatchHeader version byte: - // - // - Legacy ABI (BlockContexts in calldata) -> blob = TxsPayload (V1) - // - New ABI (LastBlockNumber + NumL1Messages, no BlockContexts in - // calldata) -> blob = TxsPayloadV2 (V2; blockContexts || txs at - // blob head) - // - // Sequencer's createBatchHeader sets version byte from - // (isBatchUpgraded, isBatchV2Upgraded) while handleBatchSealing chooses - // encoding from (isBatchUpgraded, V2-fits-in-cap); during the V1->V2 - // transition window a single batch can have version=1 + V2 encoding. - // Path A already keys off `batch.BlockContexts != nil` - // (batch_info.go::ParseBatch); Path B mirrors that here via the - // `hasCalldataBlockContexts` flag set in ParseBatchMetadataOnly. - var ( - payload []byte - chosenEncoding string - ) - if batchInfo.hasCalldataBlockContexts { - payload = bd.TxsPayload() - chosenEncoding = "V1" - } else { - payload = bd.TxsPayloadV2() - chosenEncoding = "V2" - } + // New-ABI only: blob payload is V2-encoded (blockContexts || txs at the + // blob head). Legacy-ABI batches are out of scope for local verify. + payload := bd.TxsPayloadV2() + const chosenEncoding = "V2" compressed, err := commonblob.CompressBatchBytes(payload) if err != nil { - d.logger.Error("path B verification failed: compress", + d.logger.Error("local verify verification failed: compress", append([]interface{}{ "kind", "compress_error", "encoding", chosenEncoding, "payloadLen", len(payload), "cause", err, }, logBase...)...) - return nil, fmt.Errorf("path B [compress_error]: compress failed: %w", err) + return nil, fmt.Errorf("local verify [compress_error]: compress failed: %w", err) } // maxBlobs is only an upper bound for sidecar capacity; the actual @@ -189,17 +192,17 @@ func (d *Derivation) rebuildBlob(ctx context.Context, batchInfo *BatchInfo) ([]c // the wrong blob count and a confusing hash mismatch later. sidecar, err := commonblob.MakeBlobTxSidecar(compressed, len(batchInfo.blobHashes)) if err != nil { - d.logger.Error("path B verification failed: build sidecar", + d.logger.Error("local verify verification failed: build sidecar", append([]interface{}{ "kind", "sidecar_build_error", "encoding", chosenEncoding, "payloadLen", len(payload), "compressedLen", len(compressed), "cause", err, }, logBase...)...) - return nil, fmt.Errorf("path B [sidecar_build_error]: build sidecar failed: %w", err) + return nil, fmt.Errorf("local verify [sidecar_build_error]: build sidecar failed: %w", err) } rebuilt := sidecar.BlobHashes() if len(rebuilt) != len(batchInfo.blobHashes) { - d.logger.Error("path B verification failed: blob count mismatch", + d.logger.Error("local verify verification failed: blob count mismatch", append([]interface{}{ "kind", "blob_count_mismatch", "encoding", chosenEncoding, "payloadLen", len(payload), "compressedLen", len(compressed), @@ -207,7 +210,7 @@ func (d *Derivation) rebuildBlob(ctx context.Context, batchInfo *BatchInfo) ([]c "rebuiltHashes", hashesHexCSV(rebuilt), "expectedHashes", hashesHexCSV(batchInfo.blobHashes), }, logBase...)...) - return nil, fmt.Errorf("path B [blob_count_mismatch]: blob count mismatch (rebuilt=%d, l1=%d): %w", + return nil, fmt.Errorf("local verify [blob_count_mismatch]: blob count mismatch (rebuilt=%d, l1=%d): %w", len(rebuilt), len(batchInfo.blobHashes), ErrBatchVerifyDivergence) } return rebuilt, nil @@ -225,15 +228,15 @@ func hashesHexCSV(hs []common.Hash) string { } // fetchLocalLastHeader returns the local L2 header at -// batchInfo.lastBlockNumber. Used by Path B after content verification +// batchInfo.lastBlockNumber. Used by local verify after content verification // succeeds, to feed verifyBatchRoots. func (d *Derivation) fetchLocalLastHeader(ctx context.Context, batchInfo *BatchInfo) (*eth.Header, error) { header, err := d.l2Client.HeaderByNumber(ctx, big.NewInt(int64(batchInfo.lastBlockNumber))) if err != nil { - return nil, fmt.Errorf("path B: read local header at %d failed: %w", batchInfo.lastBlockNumber, err) + return nil, fmt.Errorf("local verify: read local header at %d failed: %w", batchInfo.lastBlockNumber, err) } if header == nil { - return nil, fmt.Errorf("path B: local header at %d missing", batchInfo.lastBlockNumber) + return nil, fmt.Errorf("local verify: local header at %d missing", batchInfo.lastBlockNumber) } return header, nil } diff --git a/node/flags/flags.go b/node/flags/flags.go index c5de97115..cfe004e73 100644 --- a/node/flags/flags.go +++ b/node/flags/flags.go @@ -241,9 +241,9 @@ var ( DerivationVerifyMode = cli.StringFlag{ Name: "derivation.verify-mode", - Usage: `Batch verification mode (SPEC-005 §4.2). "pathA" pulls beacon blob, decodes, and derives blocks via engine. "pathB" (default) rebuilds blob bytes from local L2 blocks and compares versioned hashes against L1 (no beacon fetch on the happy path); on versioned hash mismatch the verifier is designed to self-heal by pulling the real blob and re-deriving the batch — currently TODO, blocked on EL number-continuity check relaxation in morph-reth/go-ethereum (separate spec). Selected at startup; not switchable at runtime.`, + Usage: `Batch verification mode (SPEC-005 §4.2). "layer1" pulls beacon blob, decodes, and derives blocks via engine. "local" (default) rebuilds blob bytes from local L2 blocks and compares versioned hashes against L1 (no beacon fetch on the happy path); on versioned hash mismatch the verifier is designed to self-heal by pulling the real blob and re-deriving the batch — currently TODO, blocked on EL number-continuity check relaxation in morph-reth/go-ethereum (separate spec). Selected at startup; not switchable at runtime.`, EnvVar: prefixEnvVar("DERIVATION_VERIFY_MODE"), - Value: "pathB", + Value: "local", } DerivationReorgCheckDepth = cli.Uint64Flag{ diff --git a/node/types/retryable_client.go b/node/types/retryable_client.go index 3bf5e93f4..77dd641d6 100644 --- a/node/types/retryable_client.go +++ b/node/types/retryable_client.go @@ -131,7 +131,7 @@ func (rc *RetryableClient) NewSafeL2Block(ctx context.Context, safeL2Data *catal // skips verifyBlock + ValidateState (used for L1-confirmed blocks where // the caller already trusts the block's content). // -// Used by SPEC-005 §4.3 Path B self-heal: when local-rebuild produces a +// Used by SPEC-005 §4.3 local verify self-heal: when local-rebuild produces a // versioned hash that disagrees with L1, we pull the real blob, derive // the batch using the true sequencer payload, and rewrite the locally // divergent unsafe blocks via this API. @@ -278,11 +278,11 @@ func (rc *RetryableClient) SetBlockTags(ctx context.Context, safeBlockHash commo // // Permanent classifications (do NOT retry): // - ethereum.NotFound: target block / header doesn't exist locally. With -// SPEC-005 Path B reading L2 blocks the sequencer hasn't yet sealed +// SPEC-005 local verify reading L2 blocks the sequencer hasn't yet sealed // locally (snapshot too old, sync still catching up), this is a "wait // for sync" condition, not a transient RPC blip; retrying every // backoff tick for 30 minutes wastes the cycle and hides the gap from -// the operator. The caller (e.g. verify_path_b) surfaces the missing +// the operator. The caller (e.g. verify_local) surfaces the missing // block, derivation logs an Error, and the next poll re-evaluates. // - DiscontinuousBlockError: structurally invalid input that no amount // of retry will fix. diff --git a/node/types/retryable_client_test.go b/node/types/retryable_client_test.go index 78c2fee85..96703ea47 100644 --- a/node/types/retryable_client_test.go +++ b/node/types/retryable_client_test.go @@ -9,7 +9,7 @@ import ( ) // retryableError must classify ethereum.NotFound as permanent so that -// SPEC-005 Path B fails fast when a target L2 block has not yet been sealed +// SPEC-005 local verify fails fast when a target L2 block has not yet been sealed // locally (snapshot too old or P2P sync still catching up). Without this // classification the caller blocks for the full 30-minute backoff budget // before the gap is surfaced.