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:
| Method | Event type accepted | Returns when called on the wrong type |
|---|---|---|
client.Events.Verify(event) | watcher.event | ErrOnlyWatcherEventsSupported |
client.Events.VerifyOperationStatus(event) | operation.status | ErrOnlyOperationStatusSupported |
client.Events.VerifyQueryStatus(event) | query.status | ErrOnlyQueryStatusSupported |
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.proofspopulated (exactly oneOCRProof). - A workflow owner address. The SDK derives this either from a configured
OrgID(passed viacrec.WithOrgID(...)) or directly from a configuredWorkflowOwner(crec.WithWorkflowOwner(...)). There is no default: at least one of the two must be set. - A signer set. The SDK uses
DefaultValidSignersunless overridden. - A signature threshold. The SDK uses
DefaultMinRequiredSignatures = 4unless 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:
| Bytes | Meaning | Check |
|---|---|---|
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:
- Decode the hex signature into a 65-byte (
r ‖ s ‖ v) buffer. - Normalize the
vbyte: Ethereum-style values (27/28) are decremented by27to match thesecp256k10/1convention. - Recover the public key with
crypto.SigToPub(reportHash, sig). - Convert the public key to an Ethereum address with
crypto.PubkeyToAddress. - 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
| Result | Meaning |
|---|---|
(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) / ErrWorkflowOwnerRequired | Verifier identity context not configured. |
(false, ErrOnlyWatcherEventsSupported) / ErrOnlyOperationStatusSupported | Wrong 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.
Related
- Verifiable Events: what
Verifyactually proves. - Verify Event Signatures: the runnable SDK recipe.
- SDK Configuration Options:
WithEventVerification,WithOrgID,WithWorkflowOwner. - Error Handling: the verification-related sentinel errors.