Skip to content

feat(syncing): detect sequencer double-signs and halt with persisted …#3310

Open
CaelRowley wants to merge 3 commits intoevstack:mainfrom
CaelRowley:cael/feat/double-sign-detection
Open

feat(syncing): detect sequencer double-signs and halt with persisted …#3310
CaelRowley wants to merge 3 commits intoevstack:mainfrom
CaelRowley:cael/feat/double-sign-detection

Conversation

@CaelRowley
Copy link
Copy Markdown

@CaelRowley CaelRowley commented May 5, 2026

Overview

Closes #1672

This change introduces detection of double-signing (equivocation) at the same block height during synchronization from DA and P2P sources.

Previously, synchronization processed the first observed block per height and silently ignored subsequent conflicting headers. This PR adds detection and handling for conflicting headers to surface potential sequencer equivocation.

Behavior

For each incoming SignedHeader from DA or P2P, the syncer enforces equivocation detection at that block height.

  • Headers with identical hashes at a given height are treated as benign duplicates.
  • When multiple distinct headers are observed at the same height and are both validly signed by the sequencer, a double-sign condition is detected. On detection:
    • Equivocation evidence is recorded under ds/<height>/<alt-hash>.
    • Error and metrics are emitted for observability.
    • The node halts via a critical error (ErrDoubleSign) to allow external resolution via human consensus, as described in the issue.

Changes

  • Added double-sign detection by comparing incoming headers against the first observed header per height. The lookup order is cache (in-flight) → store (persisted).
  • Introduced in-memory tracking of first-seen headers prior to persistence to allow for early detection.
  • Unified DA and P2P ingestion paths under a single detection flow.
  • Ensured equivocation is recorded once per (height, alternative hash) to avoid duplicate reporting.
  • Enforce ValidateBasic prior to detection to prevent invalid data from being recorded as evidence.
  • Introduced DoubleSignEvidence (proto + Go type) representing a detected equivocation at a given height, including both conflicting headers. Binary encoding support is provided for persistence and transport.
  • Added double_signs_detected_total Prometheus counter metric.
  • Ensured detection is not bypassed for already-applied heights in P2P processing.
  • Equivocation handling halts execution via ErrDoubleSign propagated through the syncer critical error channel.

Summary by CodeRabbit

  • New Features

    • Added double-sign detection and reporting with persisted evidence and a new evidence data type and wire format.
    • Introduced pending signed-header handling to track first-observed headers across sources.
  • Chores

    • Added a counter metric for detected double-sign events.
    • Added a canonical storage key for persisted evidence.
  • Tests

    • Large suite of unit and concurrency tests covering detection, persistence, reporting, and cache interactions.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: detecting sequencer double-signs and halting with persisted evidence, which aligns with the core objective.
Description check ✅ Passed The PR description comprehensively covers the overview, behavior, and changes, directly addressing the requirements from issue #1672.
Linked Issues check ✅ Passed The PR fully implements double-sign detection during synchronization [#1672], with detection from both DA and P2P sources, equivocation evidence recording, and node halting on detection.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing double-sign detection and handling as specified in #1672; no unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
types/double_sign_evidence.go (2)

32-47: ⚡ Quick win

Consider strengthening ValidateBasic defensively.

ValidateBasic is reachable from persistEvidence and (transitively) UnmarshalBinary, but it currently doesn't:

  • Call FirstHeader.ValidateBasic() / AlternateHeader.ValidateBasic() on the embedded headers.
  • Verify the consistency check that buildEvidenceFromPair enforces (ProposerAddress equality).
  • Reject empty FirstSource / AlternateSource.

Tightening this prevents persisting malformed proto-decoded evidence and keeps the validation invariants in one place rather than relying on every caller.

♻️ Proposed change
 func (e *DoubleSignEvidence) ValidateBasic() error {
 	if e == nil {
 		return errors.New("evidence is nil")
 	}
 	if e.FirstHeader == nil || e.AlternateHeader == nil {
 		return errors.New("evidence requires both first and alternate headers")
 	}
 	if e.FirstHeader.Height() != e.Height || e.AlternateHeader.Height() != e.Height {
 		return fmt.Errorf("evidence height %d does not match both headers (%d, %d)",
 			e.Height, e.FirstHeader.Height(), e.AlternateHeader.Height())
 	}
 	if bytes.Equal(e.FirstHeader.Hash(), e.AlternateHeader.Hash()) {
 		return errors.New("evidence headers have identical hash — no equivocation")
 	}
+	if !bytes.Equal(e.FirstHeader.ProposerAddress, e.AlternateHeader.ProposerAddress) {
+		return errors.New("evidence headers have different proposers — not an equivocation")
+	}
+	if e.FirstSource == "" || e.AlternateSource == "" {
+		return errors.New("evidence requires both source labels")
+	}
 	return nil
 }

As per coding guidelines: "Validate all external inputs at type boundaries to prevent invalid state".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types/double_sign_evidence.go` around lines 32 - 47,
DoubleSignEvidence.ValidateBasic currently omits checks on embedded headers and
some invariants; update ValidateBasic to call FirstHeader.ValidateBasic() and
AlternateHeader.ValidateBasic(), verify that FirstHeader.ProposerAddress equals
AlternateHeader.ProposerAddress (the same invariant enforced by
buildEvidenceFromPair), and reject empty FirstSource and AlternateSource (return
errors if zero-length). Preserve the existing height/hash checks and error
messages, and ensure all new failures return clear errors from ValidateBasic.

73-92: ⚡ Quick win

Add nil checks for required protobuf sub-messages in FromProto.

p.FirstHeader and p.AlternateHeader are required message fields that should be validated before use, consistent with how SignedHeader.FromProto checks other.Header == nil and how ValidateBasic enforces both headers are present. The codebase pattern is to validate required nested messages upfront.

♻️ Proposed change
 func (e *DoubleSignEvidence) FromProto(p *pb.DoubleSignEvidence) error {
 	if p == nil {
 		return errors.New("proto evidence is nil")
 	}
+	if p.FirstHeader == nil || p.AlternateHeader == nil {
+		return errors.New("proto evidence missing first or alternate header")
+	}
 	first := new(SignedHeader)
 	if err := first.FromProto(p.FirstHeader); err != nil {
 		return fmt.Errorf("unmarshal first header: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types/double_sign_evidence.go` around lines 73 - 92,
DoubleSignEvidence.FromProto must validate required nested proto messages before
using them: check that p.FirstHeader and p.AlternateHeader are not nil at the
top of DoubleSignEvidence.FromProto and return a descriptive error if either is
nil, then proceed to call SignedHeader.FromProto on each; this mirrors
SignedHeader.FromProto's own nil checks and the expectations in ValidateBasic
and ensures you don't pass nil into first.FromProto or alt.FromProto.
block/internal/syncing/doublesign.go (1)

47-57: 💤 Low value

Use a typed constant for the "stored" source.

types.EvidenceSourceP2P and types.EvidenceSourceDA already exist; the third source ("stored") is referenced as a magic string here and almost certainly in tests. Adding EvidenceSourceStored keeps the canonical set of ingestion-source labels in one place and avoids drift if the value ever changes.

♻️ Proposed change

In types/double_sign_evidence.go:

 const (
 	EvidenceSourceP2P = "p2p"
 	EvidenceSourceDA  = "da"
+	// EvidenceSourceStored is used when the conflicting header was loaded
+	// from the local store rather than observed live on a network path.
+	EvidenceSourceStored = "stored"
 )

In this file:

-	return buildEvidenceFromPair(storedHeader, incoming, "stored", incomingSource), nil
+	return buildEvidenceFromPair(storedHeader, incoming, types.EvidenceSourceStored, incomingSource), nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/doublesign.go` around lines 47 - 57, The code uses the
magic string "stored" as an evidence source; add a new typed constant (e.g.,
EvidenceSourceStored) to the existing enum/constants in types (where
EvidenceSourceP2P and EvidenceSourceDA are defined, likely in
double_sign_evidence.go) and replace the literal "stored" in
buildEvidenceFromPair calls (in function that references storedHeader, incoming,
"stored", incomingSource) with types.EvidenceSourceStored; update any tests that
assert the string value to use the new constant so all ingestion-source labels
are centralized and consistent.
block/internal/syncing/syncer.go (1)

1078-1080: ⚡ Quick win

Don't silently swallow reportDoubleSign's error.

reportDoubleSign already invokes sendCriticalError for the halt, so the returned error here is most likely an evidence-persistence failure. Discarding it means an operator sees the node halt without any signal that the evidence record at ds/<height>/<alt-hash> failed to write — exactly the artifact human consensus is supposed to inspect per the PR's resolution model.

📝 Suggested log on error
 func (s *Syncer) handleDoubleSign(ctx context.Context, ev *types.DoubleSignEvidence) {
-	_ = reportDoubleSign(ctx, s.store, s.metrics, s.logger, s.doubleSignSeen, s.sendCriticalError, ev)
+	if err := reportDoubleSign(ctx, s.store, s.metrics, s.logger, s.doubleSignSeen, s.sendCriticalError, ev); err != nil {
+		s.logger.Error().Err(err).Uint64("height", ev.Height()).Msg("failed to report double-sign evidence")
+	}
 }

Adjust the height accessor (ev.Height() / ev.Conflicting[0].Height()) to whatever types.DoubleSignEvidence exposes.

As per coding guidelines: "Use structured logging with appropriate log levels" and "Wrap errors with context using fmt.Errorf in Go code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/syncer.go` around lines 1078 - 1080, handleDoubleSign
is currently ignoring the error returned by reportDoubleSign; instead capture
its error return and log it (don't swallow it) using the structured logger
(e.g., s.logger.Error) with wrapped context (fmt.Errorf) and include identifying
evidence fields such as ev.Height() and the conflicting block hash/alt-hash
(e.g., ev.Conflicting[0].Hash or whatever the DoubleSignEvidence exposes) so
operators see that persistence of the evidence record (ds/<height>/<alt-hash>)
failed; note that reportDoubleSign already triggers sendCriticalError for the
halt, so only log/wrap the returned error here with clear context.
block/internal/syncing/p2p_handler_doublesign_test.go (1)

85-90: 💤 Low value

Optional: simplify the forged-header construction by skipping the no-op proto round-trip.

var pbDecoded = pbHdr copies the pointer — both names refer to the same proto object. The proto.Unmarshal(bin, pbDecoded) call re-decodes bin (marshaled from pbHdr) back into the very same message, leaving it unchanged. The scoped block and the intermediate variable add noise without changing the outcome.

♻️ Proposed simplification
 forged := new(types.SignedHeader)
-{
-    var pbDecoded = pbHdr
-    require.NoError(t, proto.Unmarshal(bin, pbDecoded))
-    require.NoError(t, forged.FromProto(pbDecoded))
-}
+require.NoError(t, forged.FromProto(pbHdr))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/p2p_handler_doublesign_test.go` around lines 85 - 90,
The test currently does a no-op proto round-trip by setting var pbDecoded =
pbHdr and calling proto.Unmarshal(bin, pbDecoded) before forged.FromProto;
remove the unnecessary pbDecoded and proto.Unmarshal call and call
forged.FromProto directly with pbHdr (or use a proper proto.Clone(pbHdr) if you
need an independent copy) so the forged header is constructed without the
redundant unmarshal step; update the block that references forged, pbHdr,
proto.Unmarshal, and forged.FromProto accordingly.
block/internal/syncing/doublesign_test.go (2)

386-393: 💤 Low value

Optional: same no-op proto round-trip as in p2p_handler_doublesign_test.go.

bin is already a proto-marshal of pbHdr (with tampered signature). r.tryDecodeHeader(bin, 100) receives those raw bytes to exercise the legacy decode path, which is the correct intent. The intermediate proto.Unmarshal(bin, pbHdr) call (same pointer) adds nothing: it decodes bin back into the same message it was marshaled from.

♻️ Proposed simplification
 good := env.signHeaderAtHeight(5, 0x01)
 pbHdr, err := good.ToProto()
 require.NoError(t, err)
 pbHdr.Signature = append([]byte(nil), good.Signature...)
 pbHdr.Signature[0] ^= 0xff
 bin, err := proto.Marshal(pbHdr)
 require.NoError(t, err)

-mockClient := testmocks.NewMockClient(t)
+// bin is a proto-encoded header with a tampered signature; pass it directly.
+mockClient := testmocks.NewMockClient(t)

(Drop the intermediate proto.Unmarshal step entirely — bin is already correct.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/doublesign_test.go` around lines 386 - 393, The test
performs a no-op proto round-trip that is unnecessary: after creating and
tampering pbHdr (via env.signHeaderAtHeight and good.ToProto) you already
marshal it into bin, so the intermediate proto.Unmarshal(bin, pbHdr) is
redundant; remove that proto.Unmarshal call and pass bin directly to
r.tryDecodeHeader (the bytes are already the intended legacy-encoded input),
keeping the tampering of pbHdr.Signature and the subsequent proto.Marshal(pbHdr)
unchanged.

386-393: 💤 Low value

Optional: same redundant proto round-trip pattern as in TestP2PHandler_InvalidSigRejectedBeforeDetector.

The intermediate proto.Marshalproto.Unmarshal cycle is a no-op here: bin is marshaled from pbHdr, then decoded back into the same pointer, restoring identical state. tryDecodeHeader receives bin which represents the proto-encoded format needed for the legacy path — that part is correct. The forged header itself can be derived directly.

♻️ Proposed simplification
 good := env.signHeaderAtHeight(5, 0x01)
 pbHdr, err := good.ToProto()
 require.NoError(t, err)
 pbHdr.Signature = append([]byte(nil), good.Signature...)
 pbHdr.Signature[0] ^= 0xff
 bin, err := proto.Marshal(pbHdr)
 require.NoError(t, err)

(The rest of the test uses bin directly — no forged type.SignedHeader is needed for tryDecodeHeader.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/doublesign_test.go` around lines 386 - 393, The test
performs an unnecessary proto marshal/unmarshal round-trip to create `bin` after
mutating `pbHdr.Signature`; simplify by mutating the proto header produced by
`env.signHeaderAtHeight(...).ToProto()` (the `pbHdr` variable) and then directly
proto.Marshal that `pbHdr` into `bin` to pass to `tryDecodeHeader`, removing the
redundant unmarshal/restore sequence and any unused `forged` SignedHeader
creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@block/internal/syncing/doublesign_test.go`:
- Around line 618-625: Replace the bare string literal "stored" used for
FirstSource in the DoubleSignEvidence test with a typed evidence-source value to
match AlternateSource usage: either add a typed constant (e.g.,
EvidenceSourceStored types.EvidenceSource = "stored") in the types package and
use types.EvidenceSourceStored for FirstSource in the two test instances (p2pEv
and daEv), or define a test-scoped constant in this file (e.g., stored :=
types.EvidenceSource("stored")) and use that for FirstSource so both FirstSource
and AlternateSource are typed as types.EvidenceSource.
- Around line 618-625: Add a typed constant for the "stored" evidence-source
alongside the existing EvidenceSourceP2P and EvidenceSourceDA (e.g., const
EvidenceSourceStored types.EvidenceSource = "stored") in the same file where
EvidenceSourceP2P/DA are defined (the types/double_sign_evidence.go symbol
block). Replace all string-literal uses of "stored" in production code (e.g., in
the DoubleSign handling in doublesign.go where the sentinel is currently
hardcoded) and tests (symbols: the DoubleSignEvidence struct usages in
doublesign_test.go and doublesign_branches_test.go) with the new typed constant
types.EvidenceSourceStored so the sentinel is consistent everywhere.

In `@types/double_sign_evidence.go`:
- Around line 22-29: The DoubleSignEvidence struct lacks JSON tags and a
String() method; update the type DoubleSignEvidence by adding JSON tags to each
field (Height, FirstHeader, AlternateHeader, DetectedAt, FirstSource,
AlternateSource) so they marshal/unmarshal with conventional names (e.g.,
`height`, `first_header`, etc.), and implement a String() method on
DoubleSignEvidence that returns a human-readable representation (include Height,
DetectedAt, FirstSource, AlternateSource and concise summaries of
FirstHeader/AlternateHeader) to satisfy the codebase's inspection/debugging
requirements.

---

Nitpick comments:
In `@block/internal/syncing/doublesign_test.go`:
- Around line 386-393: The test performs a no-op proto round-trip that is
unnecessary: after creating and tampering pbHdr (via env.signHeaderAtHeight and
good.ToProto) you already marshal it into bin, so the intermediate
proto.Unmarshal(bin, pbHdr) is redundant; remove that proto.Unmarshal call and
pass bin directly to r.tryDecodeHeader (the bytes are already the intended
legacy-encoded input), keeping the tampering of pbHdr.Signature and the
subsequent proto.Marshal(pbHdr) unchanged.
- Around line 386-393: The test performs an unnecessary proto marshal/unmarshal
round-trip to create `bin` after mutating `pbHdr.Signature`; simplify by
mutating the proto header produced by `env.signHeaderAtHeight(...).ToProto()`
(the `pbHdr` variable) and then directly proto.Marshal that `pbHdr` into `bin`
to pass to `tryDecodeHeader`, removing the redundant unmarshal/restore sequence
and any unused `forged` SignedHeader creation.

In `@block/internal/syncing/doublesign.go`:
- Around line 47-57: The code uses the magic string "stored" as an evidence
source; add a new typed constant (e.g., EvidenceSourceStored) to the existing
enum/constants in types (where EvidenceSourceP2P and EvidenceSourceDA are
defined, likely in double_sign_evidence.go) and replace the literal "stored" in
buildEvidenceFromPair calls (in function that references storedHeader, incoming,
"stored", incomingSource) with types.EvidenceSourceStored; update any tests that
assert the string value to use the new constant so all ingestion-source labels
are centralized and consistent.

In `@block/internal/syncing/p2p_handler_doublesign_test.go`:
- Around line 85-90: The test currently does a no-op proto round-trip by setting
var pbDecoded = pbHdr and calling proto.Unmarshal(bin, pbDecoded) before
forged.FromProto; remove the unnecessary pbDecoded and proto.Unmarshal call and
call forged.FromProto directly with pbHdr (or use a proper proto.Clone(pbHdr) if
you need an independent copy) so the forged header is constructed without the
redundant unmarshal step; update the block that references forged, pbHdr,
proto.Unmarshal, and forged.FromProto accordingly.

In `@block/internal/syncing/syncer.go`:
- Around line 1078-1080: handleDoubleSign is currently ignoring the error
returned by reportDoubleSign; instead capture its error return and log it (don't
swallow it) using the structured logger (e.g., s.logger.Error) with wrapped
context (fmt.Errorf) and include identifying evidence fields such as ev.Height()
and the conflicting block hash/alt-hash (e.g., ev.Conflicting[0].Hash or
whatever the DoubleSignEvidence exposes) so operators see that persistence of
the evidence record (ds/<height>/<alt-hash>) failed; note that reportDoubleSign
already triggers sendCriticalError for the halt, so only log/wrap the returned
error here with clear context.

In `@types/double_sign_evidence.go`:
- Around line 32-47: DoubleSignEvidence.ValidateBasic currently omits checks on
embedded headers and some invariants; update ValidateBasic to call
FirstHeader.ValidateBasic() and AlternateHeader.ValidateBasic(), verify that
FirstHeader.ProposerAddress equals AlternateHeader.ProposerAddress (the same
invariant enforced by buildEvidenceFromPair), and reject empty FirstSource and
AlternateSource (return errors if zero-length). Preserve the existing
height/hash checks and error messages, and ensure all new failures return clear
errors from ValidateBasic.
- Around line 73-92: DoubleSignEvidence.FromProto must validate required nested
proto messages before using them: check that p.FirstHeader and p.AlternateHeader
are not nil at the top of DoubleSignEvidence.FromProto and return a descriptive
error if either is nil, then proceed to call SignedHeader.FromProto on each;
this mirrors SignedHeader.FromProto's own nil checks and the expectations in
ValidateBasic and ensures you don't pass nil into first.FromProto or
alt.FromProto.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0e8f031-b97f-4dad-94c6-82f3ba156f19

📥 Commits

Reviewing files that changed from the base of the PR and between f36fbd2 and 821a0ef.

⛔ Files ignored due to path filters (1)
  • types/pb/evnode/v1/evnode.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (19)
  • block/internal/cache/generic_cache.go
  • block/internal/cache/generic_cache_test.go
  • block/internal/cache/manager.go
  • block/internal/cache/manager_test.go
  • block/internal/common/metrics.go
  • block/internal/syncing/da_retriever.go
  • block/internal/syncing/da_retriever_test.go
  • block/internal/syncing/doublesign.go
  • block/internal/syncing/doublesign_branches_test.go
  • block/internal/syncing/doublesign_test.go
  • block/internal/syncing/p2p_handler.go
  • block/internal/syncing/p2p_handler_doublesign_test.go
  • block/internal/syncing/p2p_handler_test.go
  • block/internal/syncing/syncer.go
  • block/internal/syncing/syncer_forced_inclusion_test.go
  • block/internal/syncing/syncer_test.go
  • pkg/store/keys.go
  • proto/evnode/v1/evnode.proto
  • types/double_sign_evidence.go

Comment thread block/internal/syncing/doublesign_test.go
Comment on lines +22 to +29
type DoubleSignEvidence struct {
Height uint64
FirstHeader *SignedHeader
AlternateHeader *SignedHeader
DetectedAt time.Time
FirstSource string
AlternateSource string
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add JSON tags and a String() method to DoubleSignEvidence.

The repository coding guidelines for types/**/*.go require JSON tags on struct fields and a String() method on core types. Both help when inspecting persisted/decoded evidence in logs and debug tooling.

♻️ Proposed change
 type DoubleSignEvidence struct {
-	Height          uint64
-	FirstHeader     *SignedHeader
-	AlternateHeader *SignedHeader
-	DetectedAt      time.Time
-	FirstSource     string
-	AlternateSource string
+	Height          uint64        `json:"height"`
+	FirstHeader     *SignedHeader `json:"first_header"`
+	AlternateHeader *SignedHeader `json:"alternate_header"`
+	DetectedAt      time.Time     `json:"detected_at"`
+	FirstSource     string        `json:"first_source"`
+	AlternateSource string        `json:"alternate_source"`
 }
+
+// String returns a concise human-readable summary of the evidence.
+func (e *DoubleSignEvidence) String() string {
+	if e == nil {
+		return "DoubleSignEvidence(nil)"
+	}
+	var firstHash, altHash string
+	if e.FirstHeader != nil {
+		firstHash = e.FirstHeader.Hash().String()
+	}
+	if e.AlternateHeader != nil {
+		altHash = e.AlternateHeader.Hash().String()
+	}
+	return fmt.Sprintf("DoubleSignEvidence{height=%d first=%s(%s) alt=%s(%s) detected_at=%s}",
+		e.Height, firstHash, e.FirstSource, altHash, e.AlternateSource, e.DetectedAt.Format(time.RFC3339Nano))
+}

As per coding guidelines: "Add JSON tags to type struct fields for inspection and marshaling" and "Implement String() methods for all core types to aid in debugging and inspection".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type DoubleSignEvidence struct {
Height uint64
FirstHeader *SignedHeader
AlternateHeader *SignedHeader
DetectedAt time.Time
FirstSource string
AlternateSource string
}
type DoubleSignEvidence struct {
Height uint64 `json:"height"`
FirstHeader *SignedHeader `json:"first_header"`
AlternateHeader *SignedHeader `json:"alternate_header"`
DetectedAt time.Time `json:"detected_at"`
FirstSource string `json:"first_source"`
AlternateSource string `json:"alternate_source"`
}
// String returns a concise human-readable summary of the evidence.
func (e *DoubleSignEvidence) String() string {
if e == nil {
return "DoubleSignEvidence(nil)"
}
var firstHash, altHash string
if e.FirstHeader != nil {
firstHash = e.FirstHeader.Hash().String()
}
if e.AlternateHeader != nil {
altHash = e.AlternateHeader.Hash().String()
}
return fmt.Sprintf("DoubleSignEvidence{height=%d first=%s(%s) alt=%s(%s) detected_at=%s}",
e.Height, firstHash, e.FirstSource, altHash, e.AlternateSource, e.DetectedAt.Format(time.RFC3339Nano))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types/double_sign_evidence.go` around lines 22 - 29, The DoubleSignEvidence
struct lacks JSON tags and a String() method; update the type DoubleSignEvidence
by adding JSON tags to each field (Height, FirstHeader, AlternateHeader,
DetectedAt, FirstSource, AlternateSource) so they marshal/unmarshal with
conventional names (e.g., `height`, `first_header`, etc.), and implement a
String() method on DoubleSignEvidence that returns a human-readable
representation (include Height, DetectedAt, FirstSource, AlternateSource and
concise summaries of FirstHeader/AlternateHeader) to satisfy the codebase's
inspection/debugging requirements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
types/double_sign_evidence.go (1)

111-117: ⚡ Quick win

Wrap the proto.Unmarshal error in UnmarshalBinary.

Line 114 returns the raw error from proto.Unmarshal. Per the coding guidelines ("Wrap errors with context using fmt.Errorf"), this should be wrapped so callers get actionable context.

♻️ Proposed fix
 func (e *DoubleSignEvidence) UnmarshalBinary(data []byte) error {
 	p := new(pb.DoubleSignEvidence)
 	if err := proto.Unmarshal(data, p); err != nil {
-		return err
+		return fmt.Errorf("proto unmarshal double sign evidence: %w", err)
 	}
 	return e.FromProto(p)
 }

As per coding guidelines: "Wrap errors with context using fmt.Errorf in Go code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types/double_sign_evidence.go` around lines 111 - 117, In
DoubleSignEvidence.UnmarshalBinary, wrap the error returned by proto.Unmarshal
with contextual information using fmt.Errorf (e.g., include "failed to unmarshal
DoubleSignEvidence" or similar) so callers receive actionable context; update
the error return at the proto.Unmarshal call in UnmarshalBinary to return the
wrapped error and ensure fmt is imported if not already.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@types/double_sign_evidence.go`:
- Around line 54-74: DoubleSignEvidence.ToProto currently only checks e == nil
but calls e.FirstHeader.ToProto() and e.AlternateHeader.ToProto() which will
panic if those fields are nil; update DoubleSignEvidence.ToProto to validate
that e.FirstHeader and e.AlternateHeader are non-nil before calling ToProto
(e.g., if e.FirstHeader == nil return an error like fmt.Errorf("marshal first
header: header is nil") and similarly for AlternateHeader), then proceed to call
FirstHeader.ToProto() and AlternateHeader.ToProto() and return the assembled
pb.DoubleSignEvidence.

---

Nitpick comments:
In `@types/double_sign_evidence.go`:
- Around line 111-117: In DoubleSignEvidence.UnmarshalBinary, wrap the error
returned by proto.Unmarshal with contextual information using fmt.Errorf (e.g.,
include "failed to unmarshal DoubleSignEvidence" or similar) so callers receive
actionable context; update the error return at the proto.Unmarshal call in
UnmarshalBinary to return the wrapped error and ensure fmt is imported if not
already.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b38bf58-b4ab-4b27-8362-0e93a90b1d81

📥 Commits

Reviewing files that changed from the base of the PR and between 821a0ef and efcd4d9.

📒 Files selected for processing (4)
  • block/internal/syncing/doublesign.go
  • block/internal/syncing/doublesign_branches_test.go
  • block/internal/syncing/doublesign_test.go
  • types/double_sign_evidence.go
✅ Files skipped from review due to trivial changes (2)
  • block/internal/syncing/doublesign_test.go
  • block/internal/syncing/doublesign.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • block/internal/syncing/doublesign_branches_test.go

Comment on lines +54 to +74
func (e *DoubleSignEvidence) ToProto() (*pb.DoubleSignEvidence, error) {
if e == nil {
return nil, errors.New("evidence is nil")
}
first, err := e.FirstHeader.ToProto()
if err != nil {
return nil, fmt.Errorf("marshal first header: %w", err)
}
alt, err := e.AlternateHeader.ToProto()
if err != nil {
return nil, fmt.Errorf("marshal alternate header: %w", err)
}
return &pb.DoubleSignEvidence{
Height: e.Height,
FirstHeader: first,
AlternateHeader: alt,
DetectedAt: e.DetectedAt.UnixNano(),
FirstSource: e.FirstSource,
AlternateSource: e.AlternateSource,
}, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ToProto will panic if FirstHeader or AlternateHeader is nil.

The nil guard at Line 55 only checks e == nil. If either header field is nil (e.g., when ToProto or MarshalBinary is called without prior ValidateBasic), e.FirstHeader.ToProto() at Line 58 (and e.AlternateHeader.ToProto() at Line 62) will panic at runtime.

🛡️ Proposed fix
 func (e *DoubleSignEvidence) ToProto() (*pb.DoubleSignEvidence, error) {
 	if e == nil {
 		return nil, errors.New("evidence is nil")
 	}
+	if e.FirstHeader == nil || e.AlternateHeader == nil {
+		return nil, errors.New("evidence requires both first and alternate headers")
+	}
 	first, err := e.FirstHeader.ToProto()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (e *DoubleSignEvidence) ToProto() (*pb.DoubleSignEvidence, error) {
if e == nil {
return nil, errors.New("evidence is nil")
}
first, err := e.FirstHeader.ToProto()
if err != nil {
return nil, fmt.Errorf("marshal first header: %w", err)
}
alt, err := e.AlternateHeader.ToProto()
if err != nil {
return nil, fmt.Errorf("marshal alternate header: %w", err)
}
return &pb.DoubleSignEvidence{
Height: e.Height,
FirstHeader: first,
AlternateHeader: alt,
DetectedAt: e.DetectedAt.UnixNano(),
FirstSource: e.FirstSource,
AlternateSource: e.AlternateSource,
}, nil
}
func (e *DoubleSignEvidence) ToProto() (*pb.DoubleSignEvidence, error) {
if e == nil {
return nil, errors.New("evidence is nil")
}
if e.FirstHeader == nil || e.AlternateHeader == nil {
return nil, errors.New("evidence requires both first and alternate headers")
}
first, err := e.FirstHeader.ToProto()
if err != nil {
return nil, fmt.Errorf("marshal first header: %w", err)
}
alt, err := e.AlternateHeader.ToProto()
if err != nil {
return nil, fmt.Errorf("marshal alternate header: %w", err)
}
return &pb.DoubleSignEvidence{
Height: e.Height,
FirstHeader: first,
AlternateHeader: alt,
DetectedAt: e.DetectedAt.UnixNano(),
FirstSource: e.FirstSource,
AlternateSource: e.AlternateSource,
}, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types/double_sign_evidence.go` around lines 54 - 74,
DoubleSignEvidence.ToProto currently only checks e == nil but calls
e.FirstHeader.ToProto() and e.AlternateHeader.ToProto() which will panic if
those fields are nil; update DoubleSignEvidence.ToProto to validate that
e.FirstHeader and e.AlternateHeader are non-nil before calling ToProto (e.g., if
e.FirstHeader == nil return an error like fmt.Errorf("marshal first header:
header is nil") and similarly for AlternateHeader), then proceed to call
FirstHeader.ToProto() and AlternateHeader.ToProto() and return the assembled
pb.DoubleSignEvidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: double-sign detection

1 participant