Skip to content

Commit 4a51a72

Browse files
committed
WIP: aggregator integration test
Now fails on: logger.go:146: 2025-07-02T18:17:45.388+0100 ERROR WorkflowEngine internal/retry.go:32 error: workflowID 997b1b3076fe0eb5d99a95a227363ad1288712fe34118d9d6bc2d4c925c15d40: failed to resolve workflow capabilities: workflowID 997b1b3076fe0eb5d99a95a227363ad1288712fe34118d9d6bc2d4c925c15d40: stepID offchain_reporting@1.0.0: stepRef secure-mint-reattest: failed to initialize capability for step: workflowID 997b1b3076fe0eb5d99a95a227363ad1288712fe34118d9d6bc2d4c925c15d40: stepID offchain_reporting@1.0.0: failed to register capability to workflow ({Metadata:{WorkflowID:997b1b3076fe0eb5d99a95a227363ad1288712fe34118d9d6bc2d4c925c15d40 WorkflowOwner:0100000000000000000000000000000000000001 ReferenceID:secure-mint-reattest} Config:0x14003f3d8b0}): error validating value map[aggregation_config:map[Underlying:map[chain_id:map[Underlying:1]]] aggregation_method:secure_mint encoder:EVM encoder_config:map[Underlying:map[abi:map[Underlying:(bytes32 FeedID, uint224 Price, uint32 Timestamp)[] Reports]]] key_id:evm report_id:0003 request_timeout_ms:0] jsonschema: '/aggregation_method' does not validate with https://github.com/smartcontractkit/chainlink/capabilities/offchain_reporting@1.0.0/config#/properties/aggregation_method/enum: value must be one of "data_feeds", "llo_streams", "identical", "reduce", retrying in 5s {"version": "unset@unset", "workflowID": "997b1b3076fe0eb5d99a95a227363ad1288712fe34118d9d6bc2d4c925c15d40"} github.com/smartcontractkit/chainlink/v2/core/services/workflows/internal.RunWithRetries /Users/ggerritsen/dev/cll/chainlink/core/services/workflows/internal/retry.go:32 github.com/smartcontractkit/chainlink/v2/core/services/workflows.(*Engine).init /Users/ggerritsen/dev/cll/chainlink/core/services/workflows/engine.go:335
1 parent fa06d07 commit 4a51a72

2 files changed

Lines changed: 227 additions & 0 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package keystone
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"github.com/shopspring/decimal"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities"
13+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
14+
feeds_consumer "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/feeds_consumer_1_0_0"
15+
16+
"github.com/smartcontractkit/chainlink/v2/core/capabilities/integration_tests/framework"
17+
)
18+
19+
func Test_runSecureMintWorkflow(t *testing.T) {
20+
ctx := t.Context()
21+
lggr := logger.Test(t)
22+
chainID := "1"
23+
24+
// setup the trigger sink that will receive the trigger event in the securemint-specific format
25+
triggerSink := framework.NewTriggerSink(t, "securemint-trigger", "1.0.0")
26+
27+
// setup the dons, the size is not important for this test
28+
workflowDonConfiguration, err := framework.NewDonConfiguration(framework.NewDonConfigurationParams{Name: "Workflow", NumNodes: 4, F: 1, AcceptsWorkflows: true})
29+
require.NoError(t, err)
30+
triggerDonConfiguration, err := framework.NewDonConfiguration(framework.NewDonConfigurationParams{Name: "Trigger", NumNodes: 4, F: 1})
31+
require.NoError(t, err)
32+
targetDonConfiguration, err := framework.NewDonConfiguration(framework.NewDonConfigurationParams{Name: "Target", NumNodes: 4, F: 1})
33+
require.NoError(t, err)
34+
35+
workflowDon, consumer := setupKeystoneDons(ctx, t, lggr, workflowDonConfiguration, triggerDonConfiguration,
36+
targetDonConfiguration, triggerSink)
37+
38+
// generate a wf job
39+
job := createSecureMintWorkflowJob(t, workflowOwnerID, chainID, consumer.Address())
40+
err = workflowDon.AddJob(ctx, &job)
41+
require.NoError(t, err)
42+
43+
// create the test trigger event in the format expected by the secure mint transmitter
44+
triggerEvent := createSecureMintTriggerEvent(t)
45+
triggerOutput, err := triggerEvent.ToMap()
46+
require.NoError(t, err)
47+
48+
// send the trigger event to the trigger sink and wait for the consumer to receive the feeds
49+
triggerSink.SendOutput(triggerOutput, "securemint-trigger")
50+
h := newSecureMintHandler([]secureMintUpdate{}, uint32(time.Now().Unix()))
51+
waitForConsumerReports(t, consumer, h)
52+
}
53+
54+
type secureMintUpdate struct {
55+
feedID string
56+
price decimal.Decimal
57+
}
58+
59+
func createSecureMintTriggerEvent(t *testing.T) *commoncap.OCRTriggerEvent {
60+
// Create mock signatures (in a real scenario, these would be actual OCR signatures)
61+
sigs := []commoncap.OCRAttributedOnchainSignature{
62+
{
63+
Signer: 0,
64+
Signature: []byte("mock-signature-1"),
65+
},
66+
{
67+
Signer: 1,
68+
Signature: []byte("mock-signature-2"),
69+
},
70+
}
71+
72+
// Create the OCR trigger event
73+
return &commoncap.OCRTriggerEvent{
74+
ConfigDigest: []byte{0: 1, 31: 2},
75+
SeqNr: 0,
76+
Report: []byte("mock-report-data"), // In a real scenario, this would be a marshaled OCRTriggerReport
77+
Sigs: sigs,
78+
}
79+
}
80+
81+
// secureMintHandler is a handler for the received feeds
82+
// produced by a workflow using the secure mint trigger and aggregator
83+
type secureMintHandler struct {
84+
expected []secureMintUpdate
85+
ts uint32 // unix timestamp in seconds
86+
found map[string]struct{}
87+
}
88+
89+
func newSecureMintHandler(expected []secureMintUpdate, ts uint32) *secureMintHandler {
90+
found := make(map[string]struct{})
91+
for _, update := range expected {
92+
found[update.feedID] = struct{}{}
93+
}
94+
return &secureMintHandler{
95+
expected: expected,
96+
ts: ts,
97+
found: found,
98+
}
99+
}
100+
101+
// Implement the feedReceivedHandler interface
102+
// to handle the received feeds
103+
func (h *secureMintHandler) handleFeedReceived(t *testing.T, feed *feeds_consumer.KeystoneFeedsConsumerFeedReceived) (done bool) {
104+
t.Logf("handling event feedID %x", feed.FeedId[:])
105+
106+
// Convert feed ID to string for comparison
107+
feedIDStr := fmt.Sprintf("0x%x", feed.FeedId[:])
108+
109+
// Find the expected update for this feed ID
110+
var expectedUpdate *secureMintUpdate
111+
for _, update := range h.expected {
112+
if update.feedID == feedIDStr {
113+
expectedUpdate = &update
114+
break
115+
}
116+
}
117+
118+
// TODO(gg): update the assertions to properly verify the decimal value
119+
120+
require.NotNil(t, expectedUpdate, "feedID %s not found in expected updates", feedIDStr)
121+
122+
// Verify the price (assuming 18 decimal places like in the original test)
123+
assert.Equal(t, expectedUpdate.price.Shift(18).BigInt(), feed.Price)
124+
assert.Equal(t, h.ts, feed.Timestamp)
125+
126+
// Mark this feed as found
127+
delete(h.found, expectedUpdate.feedID)
128+
129+
// Return true if all expected feeds have been found
130+
return len(h.found) == 0
131+
}
132+
133+
func (h *secureMintHandler) handleDone(t *testing.T) {
134+
t.Logf("found %d of %d expected feeds", len(h.expected)-len(h.found), len(h.expected))
135+
require.Empty(t, h.found, "not all expected feeds were received")
136+
}

core/capabilities/integration_tests/keystone/workflow.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,94 @@ func createLLOStreamWorkflowJob(t *testing.T,
134134
consumerAddr.String()))
135135
return workflowJobSpec.Job()
136136
}
137+
138+
/* Keith's example from the doc:
139+
name: "secure-mint-ex"
140+
owner: "0xFFFsomeaddr"
141+
142+
triggers:
143+
- id: "secure_mint@1.0.0" # NEW TRIGGER
144+
config:
145+
maxFrequencyMs: 5000
146+
feedIds:
147+
- "1020001001"
148+
- "1020000101"
149+
- "1020000102"
150+
151+
consensus:
152+
- id: "offchain_reporting@1.0.0"
153+
ref: "secure-mint-reattest"
154+
inputs:
155+
observations:
156+
- $(trigger.outputs)
157+
config:
158+
report_id: "0003"
159+
key_id: "evm"
160+
aggregation_method: "secure_mint" #NEW AGGREGRATION METHOD
161+
aggregation_config:
162+
chain_id:
163+
<CHAIN_ID_FOR_WRITE_TARGET> # NEW Param, to match write target
164+
165+
encoder: "EVM"
166+
encoder_config:
167+
abi: (bytes32 FeedID, uint224 Price, uint32 Timestamp)[] Reports # Existing feed abi
168+
169+
targets:
170+
- id: "write_ethereum-testnet-sepolia-linea-1@1.0.0"
171+
inputs:
172+
signed_report: $(secure-mint-reattest.outputs)
173+
config:
174+
address: "0x3524AbD1923402484852E6De6d656965aB37767A"
175+
deltaStage: "45s"
176+
schedule: "oneAtATime"
177+
*/
178+
179+
const secureMintWorkflow = `
180+
name: "sm_wf"
181+
owner: "0x%s"
182+
triggers:
183+
- id: "securemint-trigger@1.0.0"
184+
config:
185+
maxFrequencyMs: 5000
186+
feedIds:
187+
- "1020001001"
188+
189+
consensus:
190+
- id: "offchain_reporting@1.0.0"
191+
ref: "secure-mint-reattest"
192+
inputs:
193+
observations:
194+
- "$(trigger.outputs)"
195+
config:
196+
report_id: "0003"
197+
key_id: "evm"
198+
aggregation_method: "secure_mint" #NEW AGGREGRATION METHOD
199+
aggregation_config:
200+
chain_id:
201+
"%s" # CHAIN_ID_FOR_WRITE_TARGET: NEW Param, to match write target
202+
encoder: "EVM"
203+
encoder_config:
204+
abi: (bytes32 FeedID, uint224 Price, uint32 Timestamp)[] Reports # Existing feed abi
205+
206+
targets:
207+
- id: "write_geth-testnet@1.0.0"
208+
inputs:
209+
signed_report: $(secure-mint-reattest.outputs)
210+
config:
211+
address: "%s"
212+
deltaStage: "45s"
213+
schedule: "oneAtATime"
214+
`
215+
216+
// TODO(gg): needed in targets config?:
217+
// params: ["$(report)"]
218+
// abi: "receive(report bytes)"
219+
220+
func createSecureMintWorkflowJob(t *testing.T,
221+
workflowOwner string,
222+
chainID string,
223+
consumerAddr common.Address) job.Job {
224+
spec := fmt.Sprintf(secureMintWorkflow, workflowOwner, chainID, consumerAddr.String())
225+
workflowJobSpec := testspecs.GenerateWorkflowJobSpec(t, spec)
226+
return workflowJobSpec.Job()
227+
}

0 commit comments

Comments
 (0)