Verify Event Signatures

Every event the SDK consumes carries an Off-Chain Reporting (OCR) proof from the Chainlink DON, and the SDK refuses to trust an event that does not check out cryptographically. This guide covers the three verification helpers, their per-call variants, and the sentinel errors they return. The deeper algorithm is documented in Concepts: Event Verification.

The verification methods

Three event types can be verified: watcher.event, operation.status, and query.status. Each accepts the workflow owner in three ways:

Use caseWatcher eventsoperation.status eventsquery.status events
Use the client's default (most apps)Verify(event)VerifyOperationStatus(event)VerifyQueryStatus(event)
Multi-org service deriving owner from org IDVerifyWithOrgID(event, orgID)VerifyOperationStatusWithOrgID(event, orgID)VerifyQueryStatusWithOrgID(event, orgID)
Caller already has the workflow owner addressVerifyWithWorkflowOwner(event, addr)VerifyOperationStatusWithWorkflowOwner(event, addr)VerifyQueryStatusWithWorkflowOwner(event, addr)

Verify chooses based on which option you set on the client:

  1. If OrgID is set → VerifyWithOrgID.
  2. Else if WorkflowOwner is set → VerifyWithWorkflowOwner.
  3. Else → returns events.ErrOrgIDOrWorkflowOwnerReq.

Configure the client

Verification is enabled by default with the production DON keys (DefaultMinRequiredSignatures = 4, DefaultValidSigners = the production DON signing addresses).

client, err := crec.NewClient(
    "https://cre-connect.api.chain.link/v1",
    os.Getenv("CREC_API_KEY"),
    crec.WithEventVerification(4, []string{
        "0xff9b062fccb2f042311343048b9518068370f837",
        // ... remaining production DON signers ...
    }),
    crec.WithOrgID("your-org-id"), // OR WithWorkflowOwner("0x...")
)

Use crec.WithoutEventVerification() only in tests against an in-process mock server.

Verifying a watcher event

events, _, err := client.Events.Poll(ctx, channelID, nil)
if err != nil {
    return err
}

for _, ev := range events {
    ok, err := client.Events.Verify(&ev)
    if err != nil {
        log.Printf("event %s verification error: %v", ev.EventId, err)
        continue
    }
    if !ok {
        log.Printf("event %s did not meet signature threshold", ev.EventId)
        continue
    }

    // Safe to process from here.
}

Verify returns false (no error) when:

  • The OCR proof has fewer than MinRequiredSignatures valid signatures from ValidSigners.
  • A signature is structurally valid but recovers to an address not in ValidSigners.

It returns an error when the event is malformed (no proof, bad hex, payload type mismatch, etc.); see the Sentinel errors table below.

Multi-org verification

If a single client receives events from multiple organizations, derive the workflow owner per-event:

ok, err := client.Events.VerifyWithOrgID(&ev, "their-org-id")

Internally this calls WorkflowOwnerFromOrgID(orgID), which uses the CRE canonical CREATE2-style derivation with the configured CRETenantID (default "1"). You can also pre-compute the address yourself and call VerifyWithWorkflowOwner to avoid the derivation per event.

Verifying an operation.status event

ok, err := client.Events.VerifyOperationStatus(&ev)

VerifyOperationStatus is structurally identical to Verify but expects event.Headers.Type == apiClient.EventTypeOperationStatus. The hash recipe differs (it hashes the base64-decoded VerifiableEvent directly), but the signature-checking step is the same.

Lower-level: verifying raw OCR signatures

If you have an OCR report, OCR context, and a list of signatures and want to check them in isolation (for example because you persisted only the proof bytes), use VerifyOCRSignatures:

ok, err := client.Events.VerifyOCRSignatures(report, ctxStr, signatures)

This validates only that enough signatures recover to addresses in ValidSigners. It does not check the event hash or workflow owner; use it only for forensic / replay scenarios.

Sentinel errors

ErrorMeaning
events.ErrVerificationNotConfiguredNo ValidSigners. The client was constructed with WithoutEventVerification().
events.ErrOrgIDOrWorkflowOwnerReqDefault Verify was called but neither OrgID nor WorkflowOwner was configured.
events.ErrOnlyWatcherEventsSupportedVerify was called with a non-watcher.event envelope. Use VerifyOperationStatus for status events.
events.ErrOnlyOperationStatusSupportedSymmetric: VerifyOperationStatus was called with a non-status event.
events.ErrInvalidEventHashThe locally-computed event hash does not match the report. Possible tampering or wrong workflow owner.
events.ErrNoOCRProofsNo OCR proof is attached to the event yet. This is usually a transient race: the backend surfaced the record before the DON had attached the proof. Re-poll on the next cycle; the same event will reappear with the proof attached.
events.ErrMultipleOCRProofsMore than one OCR proof on the same event: exactly one is expected. Indicates a backend bug; report it.
events.ErrOCRReportTooShortThe report is shorter than the minimum needed to extract the payload.
events.ErrParseOCRReport / ErrParseOCRContext / ErrParseSignatureHex parsing failure.
events.ErrRecoverPubKeyFromSignatureA signature was malformed (wrong length / non-recoverable).
events.ErrDeriveWorkflowOwnerWorkflowOwnerFromOrgID failed (typically a malformed OrgID).

Tuning MinRequiredSignatures

DefaultMinRequiredSignatures = 4. For a higher security bar, raise the threshold; for a more permissive setup, lower it. The constraint is MinRequiredSignatures > 0 whenever ValidSigners is non-empty; otherwise crec.NewClient returns crec.ErrInvalidEventVerificationConfig.

Next steps

Get the latest Chainlink content straight to your inbox.