Skip to content

Commit c4c73cb

Browse files
committed
Merge branch 'main' into event-structure
2 parents e60be69 + f7fe834 commit c4c73cb

5 files changed

Lines changed: 181 additions & 62 deletions

File tree

config.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import (
1313
// It mirrors the structure produced by our existing config.tmpl files and remains
1414
// compatible with server-side gomplate rendering used during e2e runs.
1515
type Config struct {
16-
Network string `yaml:"network" json:"network"`
17-
ChainID string `yaml:"chainID" json:"chainID"`
18-
CourierURL string `yaml:"courierURL" json:"courierURL"`
19-
Service string `yaml:"service" json:"service"`
20-
ApiKeySecret string `yaml:"apiKeySecret,omitempty" json:"apiKeySecret,omitempty"`
21-
ChainSelector string `yaml:"chainSelector" json:"chainSelector"`
22-
WatcherID string `yaml:"watcherID" json:"watcherID"`
23-
WorkflowName string `yaml:"workflowName" json:"workflowName"`
16+
Network string `yaml:"network" json:"network"`
17+
ChainID string `yaml:"chainID" json:"chainID"`
18+
CourierURL string `yaml:"courierURL" json:"courierURL"`
19+
Service *string `yaml:"service,omitempty" json:"service,omitempty"`
20+
ApiKeySecret string `yaml:"apiKeySecret,omitempty" json:"apiKeySecret,omitempty"`
21+
ChainSelector string `yaml:"chainSelector" json:"chainSelector"`
22+
WatcherID string `yaml:"watcherID" json:"watcherID"`
23+
WorkflowName string `yaml:"workflowName" json:"workflowName"`
2424

2525
DetectEventTriggerConfig DetectEventTriggerConfig `yaml:"detectEventTriggerConfig" json:"detectEventTriggerConfig"`
2626
}

event_processing.go

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import (
66
"encoding/json"
77
"fmt"
88
"log/slog"
9-
"math"
109
"math/big"
1110
"reflect"
12-
"strconv"
1311
"strings"
1412
"time"
1513

@@ -301,7 +299,7 @@ func MustEvent(abiJSON, eventName string) gethAbi.Event {
301299
// BuildAndHashEventEnvelope builds the base64 verifiable-event JSON and computes keccak256(type+"."+b64).
302300
// Note: parameters and metadata are treated as read-only; they are not mutated.
303301
func BuildAndHashEventEnvelope(
304-
service string,
302+
service *string,
305303
eventName string,
306304
contractAddress string,
307305
eventABIJSON string,
@@ -323,14 +321,16 @@ func BuildAndHashEventEnvelope(
323321
}
324322

325323
event := map[string]any{
326-
"service": service,
327324
"name": eventName,
328325
"address": contractAddress,
329326
"topic_hash": eventABI.ID.Hex(),
330327
"log_index": logIndex,
331328
"block_number": blockNumber,
332329
"parameters": parameters,
333330
}
331+
if service != nil {
332+
event["service"] = *service
333+
}
334334

335335
transaction := map[string]any{
336336
"timestamp": timestamp,
@@ -350,7 +350,11 @@ func BuildAndHashEventEnvelope(
350350

351351
marshalledVerifiableEvent, _ := json.Marshal(verifiableEventBody)
352352
base64VerifiableEvent := base64.StdEncoding.EncodeToString(marshalledVerifiableEvent)
353-
typeName := service + "." + eventName
353+
// Build typeName: if service is nil, use just eventName, otherwise use service.eventName
354+
typeName := eventName
355+
if service != nil {
356+
typeName = *service + "." + eventName
357+
}
354358
payloadToSign := typeName + "." + base64VerifiableEvent
355359
eventHash := crypto.Keccak256Hash([]byte(payloadToSign))
356360

@@ -364,12 +368,12 @@ func BuildAndHashEventEnvelope(
364368

365369
// ResolveAPIKey returns the API key to use for Courier requests.
366370
// Only the secret-based approach is supported:
367-
// - cfg.ApiKeySecret MUST be set to the secret ID.
371+
// - apiKeySecret MUST be set to the secret ID.
368372
// - The secret MUST resolve via rt.GetSecret.
369373
//
370374
// If resolution fails, an empty string is returned and callers should error.
371-
func ResolveAPIKey(rt cre.Runtime, cfg *Config) string {
372-
secretID := strings.TrimSpace(cfg.ApiKeySecret)
375+
func ResolveAPIKey(rt cre.Runtime, apiKeySecret string) string {
376+
secretID := strings.TrimSpace(apiKeySecret)
373377
if secretID == "" {
374378
return ""
375379
}
@@ -378,7 +382,7 @@ func ResolveAPIKey(rt cre.Runtime, cfg *Config) string {
378382
s, err := rt.GetSecret(&cre.SecretRequest{Id: secretID}).Await()
379383

380384
if err != nil {
381-
slog.Warn("failed to resolve API key secret", "error", err)
385+
rt.Logger().Warn("ResolveAPIKey failed to get secret", "error", err)
382386
return ""
383387
}
384388

@@ -400,20 +404,12 @@ func PostSignedEvent(cfg *Config, rt cre.Runtime, eventName, address string, pre
400404
}
401405
rpb := report.X_GeneratedCodeOnly_Unwrap()
402406

403-
// Convert ChainSelector to uint64
404-
chainSelector, err := strconv.ParseUint(cfg.ChainSelector, 10, 64)
405-
if err != nil {
406-
return "", fmt.Errorf("invalid chain selector: %w", err)
407-
}
408-
409407
// Compose HTTP body
410408
bodyMap := map[string]any{
411-
"event_id": uuid.New().String(),
412409
"created_at": int64(pre.BlockTimestamp) * 1000, // Convert seconds to milliseconds for server
413410
"watcher_id": cfg.WatcherID,
414-
"domain": cfg.Service,
415411
"name": eventName,
416-
"chain_selector": chainSelector,
412+
"chain_selector": cfg.ChainSelector,
417413
"address": address,
418414
"ocr_report": "0x" + hex.EncodeToString(rpb.RawReport),
419415
"ocr_context": "0x" + hex.EncodeToString(rpb.ReportContext),
@@ -427,42 +423,66 @@ func PostSignedEvent(cfg *Config, rt cre.Runtime, eventName, address string, pre
427423
return out
428424
}(),
429425
}
426+
if cfg.Service != nil {
427+
bodyMap["domain"] = *cfg.Service
428+
}
430429
body, _ := json.Marshal(bodyMap)
431430

432431
// HTTP POST with identical consensus
432+
// We aggregate only the integer StatusCode to ensure compatibility with Identical consensus.
433433
client := &httpcap.Client{}
434-
key := ResolveAPIKey(rt, cfg)
435-
436-
_, err = httpcap.SendRequest(cfg, rt, client, func(_ *Config, _ *slog.Logger, sr *httpcap.SendRequester) (*httpcap.Response, error) {
437-
if key == "" {
438-
return nil, fmt.Errorf("courier API key is required but not configured")
439-
}
440-
headers := map[string]string{
441-
"Content-Type": "application/json",
442-
"Api-Key": key,
443-
}
444-
req := &httpcap.Request{
445-
Url: strings.TrimRight(cfg.CourierURL, "/") + "/system/onchain-watcher-events",
446-
Method: "POST",
447-
Headers: headers,
448-
Body: body,
449-
}
450-
return sr.SendRequest(req).Await()
451-
}, cre.ConsensusIdenticalAggregation[*httpcap.Response]()).Await()
434+
key := ResolveAPIKey(rt, cfg.ApiKeySecret)
435+
436+
_, err = httpcap.SendRequest(
437+
cfg,
438+
rt,
439+
client,
440+
func(_ *Config, _ *slog.Logger, sr *httpcap.SendRequester) (int, error) {
441+
if key == "" {
442+
return 0, fmt.Errorf("courier API key is required but not configured")
443+
}
444+
headers := map[string]string{
445+
"Content-Type": "application/json",
446+
"Api-Key": key,
447+
}
448+
req := &httpcap.Request{
449+
Url: strings.TrimRight(cfg.CourierURL, "/") + "/system/onchain-watcher-events",
450+
Method: "POST",
451+
Headers: headers,
452+
Body: body,
453+
}
454+
resp, err := sr.SendRequest(req).Await()
455+
if err != nil {
456+
return 0, err
457+
}
458+
if resp == nil {
459+
return 0, fmt.Errorf("nil response")
460+
}
461+
// Treat any 4xx/5xx as an error (caller may retry).
462+
if resp.StatusCode >= 400 {
463+
return 0, fmt.Errorf("courier API responded with status %d", resp.StatusCode)
464+
}
465+
return int(resp.StatusCode), nil
466+
},
467+
cre.ConsensusIdenticalAggregation[int](),
468+
).Await()
452469
if err != nil {
453470
return "", err
454471
}
472+
455473
return pre.Base64Event, nil
456474
}
457475

458-
// CheckResponse validates an HTTP response status code and returns it as int.
459-
// Returns an error if the status code exceeds int32 bounds.
460-
func CheckResponse(resp *httpcap.Response) (int, error) {
461-
code := resp.StatusCode
462-
if code > math.MaxInt32 {
463-
return 0, fmt.Errorf("API responded with invalid status code %d", code)
476+
// CheckResponse validates the httpcap response and returns it unchanged if acceptable.
477+
func CheckResponse(resp *httpcap.Response) (*httpcap.Response, error) {
478+
if resp == nil {
479+
return nil, fmt.Errorf("nil response")
480+
}
481+
// Treat any 4xx/5xx as an error (caller may retry).
482+
if resp.StatusCode >= 400 {
483+
return nil, fmt.Errorf("courier API responded with status %d", resp.StatusCode)
464484
}
465-
return int(code), nil
485+
return resp, nil
466486
}
467487

468488
// DecodeEventParams decodes an EVM log's topics/data into a named parameter map, using the provided ABI JSON and event-name.

event_processing_test.go

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"math/big"
99
"testing"
1010

11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/ethereum/go-ethereum/crypto"
1113
httpcap "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
1214
httpmock "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http/mock"
1315
"github.com/smartcontractkit/cre-sdk-go/cre/testutils"
14-
15-
"github.com/ethereum/go-ethereum/common"
1616
"github.com/stretchr/testify/require"
1717
)
1818

@@ -39,8 +39,9 @@ func TestBuildAndSignEventEnvelope_IncludesParametersInEventAndTopLevel(t *testi
3939
}
4040
params := SanitiseJSON(raw).(map[string]any)
4141

42+
testService := "test_service"
4243
res, err := BuildAndHashEventEnvelope(
43-
"test_service",
44+
&testService,
4445
"Sender",
4546
"0xContract",
4647
testABIForCommon,
@@ -85,6 +86,108 @@ func TestBuildAndSignEventEnvelope_IncludesParametersInEventAndTopLevel(t *testi
8586
require.Equal(t, "meta", meta["extra"])
8687
}
8788

89+
func TestBuildAndHashEventEnvelope_WithNilService(t *testing.T) {
90+
// prepare parameters
91+
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
92+
raw := map[string]any{
93+
"Sender": addr.Bytes(),
94+
}
95+
params := SanitiseJSON(raw).(map[string]any)
96+
97+
// Build event envelope with nil service
98+
res, err := BuildAndHashEventEnvelope(
99+
nil,
100+
"Sender",
101+
"0xContract",
102+
testABIForCommon,
103+
"1",
104+
100,
105+
2,
106+
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
107+
1_700_000_000,
108+
params,
109+
map[string]any{"extra": "meta"},
110+
)
111+
require.NoError(t, err)
112+
require.NotEmpty(t, res.Base64Event)
113+
// When service is nil, typeName should be just the event name (no service prefix)
114+
require.Equal(t, "Sender", res.Type)
115+
require.NotEqual(t, common.Hash{}, res.EventHash)
116+
117+
decoded, err := base64.StdEncoding.DecodeString(res.Base64Event)
118+
require.NoError(t, err)
119+
var obj map[string]any
120+
require.NoError(t, json.Unmarshal(decoded, &obj))
121+
122+
ev := obj["event"].(map[string]any)
123+
// service field should not be present when service is nil
124+
_, hasService := ev["service"]
125+
require.False(t, hasService, "service field should not be present when service is nil")
126+
require.Equal(t, "Sender", ev["name"])
127+
require.Equal(t, "0xContract", ev["address"])
128+
}
129+
130+
func TestBuildAndHashEventEnvelope_ServiceHashCompatibility(t *testing.T) {
131+
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
132+
raw := map[string]any{
133+
"Sender": addr.Bytes(),
134+
}
135+
params := SanitiseJSON(raw).(map[string]any)
136+
metadata := map[string]any{"extra": "meta"}
137+
138+
eventName := "Sender"
139+
140+
resNilService, err := BuildAndHashEventEnvelope(
141+
nil,
142+
eventName,
143+
"0xContract",
144+
testABIForCommon,
145+
"1",
146+
100,
147+
2,
148+
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
149+
1_700_000_000,
150+
params,
151+
metadata,
152+
)
153+
require.NoError(t, err)
154+
155+
testService := "operations"
156+
resWithService, err := BuildAndHashEventEnvelope(
157+
&testService,
158+
eventName,
159+
"0xContract",
160+
testABIForCommon,
161+
"1",
162+
100,
163+
2,
164+
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
165+
1_700_000_000,
166+
params,
167+
metadata,
168+
)
169+
require.NoError(t, err)
170+
171+
require.Equal(t, eventName, resNilService.Type, "nil service should produce typeName = eventName")
172+
require.Equal(t, "operations."+eventName, resWithService.Type, "service should produce typeName = service.eventName")
173+
174+
// Note: base64 payloads differ because service is included in the event JSON when present
175+
// This is expected behavior - the service field is part of the verifiable event structure
176+
require.NotEqual(t, resNilService.Base64Event, resWithService.Base64Event,
177+
"base64 verifiable event payloads differ because service is included in JSON when present")
178+
179+
require.NotEqual(t, resNilService.EventHash, resWithService.EventHash,
180+
"event hashes must differ when service is nil vs present (compatibility boundary)")
181+
182+
expectedNilHash := common.BytesToHash(crypto.Keccak256([]byte(eventName + "." + resNilService.Base64Event)))
183+
expectedServiceHash := common.BytesToHash(crypto.Keccak256([]byte("operations." + eventName + "." + resWithService.Base64Event)))
184+
185+
require.Equal(t, expectedNilHash, resNilService.EventHash,
186+
"nil-service hash should match keccak256(eventName + \".\" + base64payload)")
187+
require.Equal(t, expectedServiceHash, resWithService.EventHash,
188+
"service-prefixed hash should match keccak256(service.eventName + \".\" + base64payload)")
189+
}
190+
88191
func TestSanitiseJSON_Conversions(t *testing.T) {
89192
// prepare a structure with varied types
90193
b20 := make([]byte, 20) // 20 bytes -> address-like
@@ -164,11 +267,9 @@ func TestPostSignedEvent_HTTPPayloadStructure(t *testing.T) {
164267
require.NoError(t, json.Unmarshal(req.Body, &body))
165268

166269
// required fields
167-
require.NotEmpty(t, body["event_id"])
168270
require.Equal(t, "test", body["domain"])
169271
require.Equal(t, "Sender", body["name"])
170-
// Courier protocol expects chain_selector as number
171-
require.Equal(t, float64(11155111), body["chain_selector"], "chain_selector should be a number")
272+
require.Equal(t, "11155111", body["chain_selector"], "chain_selector should be a string")
172273
require.Equal(t, "0xABCDEF", body["address"])
173274

174275
// ocr report/context hex encoded
@@ -196,12 +297,13 @@ func TestPostSignedEvent_HTTPPayloadStructure(t *testing.T) {
196297
}
197298

198299
// Workflow config for POST (use secret id, not inline key)
300+
testService := "test"
199301
cfg := &Config{
200302
Network: "evm",
201303
ChainID: "1",
202304
ChainSelector: "11155111", // Provide explicit selector
203305
CourierURL: "http://example.com",
204-
Service: "test",
306+
Service: &testService,
205307
ApiKeySecret: "courier",
206308
DetectEventTriggerConfig: DetectEventTriggerConfig{
207309
ContractName: "TestConsumer",
@@ -213,7 +315,7 @@ func TestPostSignedEvent_HTTPPayloadStructure(t *testing.T) {
213315
"Sender": common.HexToAddress("0x1111111111111111111111111111111111111111").Bytes(),
214316
}).(map[string]any)
215317
pre, err := BuildAndHashEventEnvelope(
216-
"test",
318+
&testService,
217319
"Sender",
218320
"0xABCDEF",
219321
testABIForCommon,

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ go 1.25.3
44

55
require (
66
github.com/ethereum/go-ethereum v1.16.7
7-
github.com/google/uuid v1.6.0
87
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251211142334-5c3421fe2c8d
98
github.com/smartcontractkit/cre-sdk-go v1.1.2
109
github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.10.0

0 commit comments

Comments
 (0)