From 73b913af691302a531fda353b696f9392a53ff04 Mon Sep 17 00:00:00 2001 From: corey Date: Mon, 27 Jul 2026 14:54:31 +0800 Subject: [PATCH] fix(node): make root verification non-blocking for full nodes Keep layer1 validators fail-closed while allowing regular nodes to continue syncing when root verification fails. Co-authored-by: Cursor --- node/derivation/derivation.go | 27 +++++++++++++++++++----- node/derivation/verify_policy_test.go | 30 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 node/derivation/verify_policy_test.go diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index b49ed1264..ed31bf191 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -483,17 +483,26 @@ func (d *Derivation) derivationBlock(ctx context.Context) { if err := d.verifyBatchRoots(batchInfo, lastHeader); err != nil { // stateException only when the verifier produced a real mismatch // verdict (root or withdrawal root). Transient failures (e.g. - // MessageRoot RPC error) just log and retry next poll. + // MessageRoot RPC error) must not be reported as divergence. if errors.Is(err, ErrBatchVerifyDivergence) { d.metrics.SetBatchStatus(stateException) } - d.logger.Error("batch roots verification failed", "batchIndex", batchInfo.batchIndex, "error", err) - return + if enforceBatchRootVerification(d.verifyMode) { + d.logger.Error("batch roots verification failed", "batchIndex", batchInfo.batchIndex, "error", err) + return + } + // Tendermint's logger exposes debug/info/error but no warning + // method, so retain warning semantics as a structured info event. + d.logger.Info("batch roots verification warning; continuing in non-validator mode", + "severity", "warning", "batchIndex", batchInfo.batchIndex, "error", err) + } else { + d.metrics.SetBatchStatus(stateNormal) } - d.metrics.SetBatchStatus(stateNormal) d.metrics.SetL1SyncHeight(lg.BlockNumber) - // SPEC-005 section 4.7.3: a verified batch (layer1 or local verify) advances safe. + // SPEC-005 section 4.7.3: a content-verified batch advances safe. + // Regular nodes treat root verification as an observational check; + // layer1 validator nodes enforce it above. d.tagAdvancer.advanceSafe(d.ctx, batchInfo.batchIndex, lastHeader) } @@ -510,6 +519,14 @@ func (d *Derivation) derivationBlock(ctx context.Context) { d.logger.Info("write latest derivation l1 height success", "l1BlockNumber", end) } +// enforceBatchRootVerification reports whether a root verification failure must +// stop derivation. Layer1 mode is the validator deployment mode; local mode is +// used by regular nodes, which still verify roots for observability but must not +// lose sync when historical state is unavailable. +func enforceBatchRootVerification(verifyMode string) bool { + return verifyMode == VerifyModeLayer1 +} + func (d *Derivation) fetchRollupLog(ctx context.Context, from, to uint64) ([]eth.Log, error) { query := ethereum.FilterQuery{ FromBlock: big.NewInt(0).SetUint64(from), diff --git a/node/derivation/verify_policy_test.go b/node/derivation/verify_policy_test.go new file mode 100644 index 000000000..fa64db281 --- /dev/null +++ b/node/derivation/verify_policy_test.go @@ -0,0 +1,30 @@ +package derivation + +import "testing" + +func TestEnforceBatchRootVerification(t *testing.T) { + tests := []struct { + name string + verifyMode string + want bool + }{ + { + name: "layer1 validator enforces", + verifyMode: VerifyModeLayer1, + want: true, + }, + { + name: "local node observes", + verifyMode: VerifyModeLocal, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := enforceBatchRootVerification(tt.verifyMode); got != tt.want { + t.Fatalf("enforceBatchRootVerification(%q) = %t, want %t", tt.verifyMode, got, tt.want) + } + }) + } +}