Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common/batch/batch_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
78 changes: 1 addition & 77 deletions node/derivation/batch_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions node/derivation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions node/derivation/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 7 additions & 7 deletions node/derivation/derivation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -276,21 +276,21 @@ 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 {
if rebuilt[i] != batchInfo.blobHashes[i] {
// 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
}
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}

Expand Down
87 changes: 15 additions & 72 deletions node/derivation/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion node/derivation/reorg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions node/derivation/static_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,27 +118,27 @@ 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)
// would still drift codecs.
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)
}
}
}
2 changes: 1 addition & 1 deletion node/derivation/tag_advance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 1 addition & 6 deletions node/derivation/tag_advance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions node/derivation/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand All @@ -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
Expand Down
Loading
Loading