Event Verification

This page describes exactly what happens when you call the verification helpers on client.Events from the CRE Connect Go SDK. The algorithm is implemented in events/events.go (crec-sdk). Use this page as a conceptual reference, and see the Verify Event Signatures guide for runnable code.

Entry points, one algorithm

The SDK exposes type-specific verifiers; each handles exactly one event family and uses the same underlying algorithm:

MethodEvent type acceptedReturns when called on the wrong type
client.Events.Verify(event)watcher.eventErrOnlyWatcherEventsSupported
client.Events.VerifyOperationStatus(event)operation.statusErrOnlyOperationStatusSupported
client.Events.VerifyQueryStatus(event)query.statusErrOnlyQueryStatusSupported

Per-call variants (VerifyWithOrgID, VerifyWithWorkflowOwner, VerifyOperationStatusWithOrgID, VerifyOperationStatusWithWorkflowOwner, VerifyQueryStatusWithOrgID, VerifyQueryStatusWithWorkflowOwner) let you override the workflow-owner identity for a single call without rebuilding the client. The watcher.status and wallet.status event families are not covered by these helpers; for those, use the lower-level VerifyOCRSignatures(...) directly (see Lower-level entry point).

Inputs

Both verifiers require three things from the SDK configuration plus the event itself:

  • The event with its headers.proofs populated (exactly one OCRProof).
  • A workflow owner address. The SDK derives this either from a configured OrgID (passed via crec.WithOrgID(...)) or directly from a configured WorkflowOwner (crec.WithWorkflowOwner(...)). There is no default: at least one of the two must be set.
  • A signer set. The SDK uses DefaultValidSigners unless overridden.
  • A signature threshold. The SDK uses DefaultMinRequiredSignatures = 4 unless overridden.

If neither OrgID nor WorkflowOwner is configured, both verifiers return the sentinel ErrOrgIDOrWorkflowOwnerReq on every call.

Algorithm: step by step

1. Type and payload extraction

Each verifier first checks event.Headers.Type matches the type it accepts (and returns the corresponding sentinel above otherwise). It then extracts the typed payload, WatcherEventPayload for Verify and OperationStatusPayload for VerifyOperationStatus, which carries the verifiable_event bytes used in step 3.

2. Off-Chain Reporting (OCR) proof extraction

The proofs array must contain exactly one OCRProof. The SDK enforces this with two sentinels:

  • ErrNoOCRProofs: empty proofs array.
  • ErrMultipleOCRProofs: more than one proof present (other proof types are skipped, but multiple OCR proofs are an error).

parseOCRProofData then hex-decodes both ocr_report and ocr_context and validates that the report is at least 109 + 32 = 141 bytes long (the offset of the embedded payload hash).

3. Local event hash

The SDK computes the local event hash from the payload received in the envelope:

eventHash = keccak256(payload.verifiable_event)

This is the application-visible payload, the bytes you would actually consume, turned into a 32-byte digest for comparison against the on-chain-attested hash.

4. Bind report, workflow owner, and event hash (verifyEventHash)

verifyEventHash enforces two invariants on the OCR report:

BytesMeaningCheck
ocr_report[87:107]The 20-byte address of the workflow owner that produced the report.Must equal the configured workflowOwner (derived from OrgID or set directly).
ocr_report[109:]The 32-byte hash the workflow signed over.Must equal the locally-computed eventHash.

Either check failing aborts verification: the event was either signed by a different workflow or for different bytes than what the SDK observed.

5. OCR signature verification (verifySignatures)

Once the report is bound to the right workflow and payload, the SDK validates the OCR signatures:

reportHash = keccak256(keccak256(ocr_report) ‖ ocr_context)

For each entry in proof.signatures:

  1. Decode the hex signature into a 65-byte (r ‖ s ‖ v) buffer.
  2. Normalize the v byte: Ethereum-style values (27 / 28) are decremented by 27 to match the secp256k1 0 / 1 convention.
  3. Recover the public key with crypto.SigToPub(reportHash, sig).
  4. Convert the public key to an Ethereum address with crypto.PubkeyToAddress.
  5. If that address is in the configured valid signers map and has not already been used in this round, increment the unique signature count.

Verification short-circuits as soon as the count reaches MinRequiredSignatures (default 4).

6. Decision

ResultMeaning
(true, nil)All checks passed: the event is authentic and untampered.
(false, ErrInvalidEventHash)Workflow-owner / event-hash binding (verifyEventHash) failed: bytes 87:107 did not equal the configured workflow owner, or bytes 109+ did not equal keccak256(payload.verifiable_event).
(false, ErrVerifyEvent)Lower-level error wrapping the underlying cause (malformed report, bad signature length, signer recovery failure, etc.).
(false, ErrVerificationNotConfigured)The valid-signers map is empty (configure crec.WithEventVerification).
(false, ErrOrgIDOrWorkflowOwnerReq) / ErrWorkflowOwnerRequiredVerifier identity context not configured.
(false, ErrOnlyWatcherEventsSupported) / ErrOnlyOperationStatusSupportedWrong helper called for the event type.
(false, nil)Signatures parsed cleanly but fewer than MinRequiredSignatures recovered to known signers.

The full sentinel list is documented in Error Handling.

Worked example

Below is the conceptual flow when an application receives a watcher.event and verifies it. Add the events import, import "github.com/smartcontractkit/crec-sdk/events", to use the sentinel errors.

polled, _, err := client.Events.Poll(ctx, channelID, nil)
if err != nil { /* handle */ }

for _, ev := range polled {
    ok, err := client.Events.Verify(&ev)
    switch {
    case errors.Is(err, events.ErrNoOCRProofs):
        // The event arrived before its OCR proof: re-poll later.
        continue
    case err != nil:
        log.Printf("verification error: %v", err)
        continue
    case !ok:
        log.Printf("event %s failed verification", ev.EventId)
        continue
    }
    handle(ev)
}

Verify and VerifyOperationStatus are purely local: they do no network I/O. Poll makes a single HTTP call to the events endpoint and surfaces transport errors to the caller without retry, so wrap it in your own retry logic if you need automatic retries. When verification returns ErrNoOCRProofs for an event, skip that event with errors.Is(err, events.ErrNoOCRProofs) and re-poll on the next cycle.

Lower-level entry point

If you only have an OCR report, an OCR context, and a list of signatures (for example, when bridging events out of one CRE Connect tenant and verifying them in another system), call VerifyOCRSignatures(ocrReport, ocrContext, signatures). This entry point performs steps 5–6 above without the workflow-owner / event-hash binding from step 4.

Get the latest Chainlink content straight to your inbox.