Skip to content

Commit de216bb

Browse files
Implements changeset to propose an OCR Bootstrap Job Spec (#19312)
* Implements changeset to propose an OCR Bootstrap Job Spec * Fixes lint * Updates job spec * Fixes test * Fixes preconditions validation * Fixes input parsing * Fixes input parsing * Fixes test * Implements contract_qualifier as input
1 parent 774a7d6 commit de216bb

17 files changed

Lines changed: 502 additions & 53 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package operations
2+
3+
import (
4+
"github.com/Masterminds/semver/v3"
5+
6+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
7+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
8+
"github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node"
9+
"github.com/smartcontractkit/chainlink-protos/job-distributor/v1/shared/ptypes"
10+
11+
"github.com/smartcontractkit/chainlink/deployment/cre/jobs/pkg"
12+
)
13+
14+
const (
15+
BootstrapNodeTypeKey = "bootstrap"
16+
PluginNodeType = "plugin"
17+
)
18+
19+
type ProposeJobSpecDeps struct {
20+
Env cldf.Environment
21+
}
22+
23+
type ProposeJobSpecInput struct {
24+
Domain string
25+
DONName string
26+
27+
Spec string
28+
29+
DONFilters []TargetDONFilter
30+
JobLabels map[string]string
31+
32+
IsBootstrap bool
33+
}
34+
35+
type ProposeJobSpecOutput struct {
36+
Specs map[string][]string
37+
}
38+
39+
var ProposeJobSpec = operations.NewOperation[ProposeJobSpecInput, ProposeJobSpecOutput, ProposeJobSpecDeps](
40+
"propose-job-spec-op",
41+
semver.MustParse("1.0.0"),
42+
"Propose Job Spec",
43+
func(b operations.Bundle, deps ProposeJobSpecDeps, input ProposeJobSpecInput) (ProposeJobSpecOutput, error) {
44+
b.Logger.Debugw("Proposing job", "DON", input.DONName, "domain", input.Domain, "environment", deps.Env.Name)
45+
req := pkg.ProposeJobRequest{
46+
Spec: input.Spec,
47+
DONName: input.DONName,
48+
Env: deps.Env.Name,
49+
JobLabels: input.JobLabels,
50+
DONFilter: &node.ListNodesRequest_Filter{},
51+
}
52+
53+
nodeType := PluginNodeType
54+
if input.IsBootstrap {
55+
nodeType = BootstrapNodeTypeKey
56+
}
57+
filter := &node.ListNodesRequest_Filter{
58+
Selectors: []*ptypes.Selector{
59+
{
60+
Key: "type",
61+
Op: ptypes.SelectorOp_EQ,
62+
Value: &nodeType,
63+
},
64+
},
65+
}
66+
for _, f := range input.DONFilters {
67+
// DON name is a key, so we just check for its existence instead of equality
68+
if f.Key == FilterKeyDONName {
69+
filter.Selectors = append(filter.Selectors, &ptypes.Selector{
70+
Op: ptypes.SelectorOp_EXIST,
71+
Key: f.Value,
72+
})
73+
} else {
74+
filter.Selectors = append(filter.Selectors, &ptypes.Selector{
75+
Op: ptypes.SelectorOp_EQ,
76+
Key: f.Key,
77+
Value: &f.Value,
78+
})
79+
}
80+
}
81+
82+
req.DONFilter = filter
83+
84+
specs, err := pkg.ProposeJob(b.GetContext(), deps.Env, req)
85+
if err != nil {
86+
return ProposeJobSpecOutput{}, err
87+
}
88+
89+
return ProposeJobSpecOutput{
90+
Specs: specs,
91+
}, nil
92+
},
93+
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package operations
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Masterminds/semver/v3"
7+
8+
chainsel "github.com/smartcontractkit/chain-selectors"
9+
10+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
11+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
12+
13+
"github.com/smartcontractkit/chainlink/deployment/cre/jobs/pkg"
14+
)
15+
16+
const (
17+
DefaultBootstrapJobName = "OCR3 MultiChain Capability Bootstrap"
18+
)
19+
20+
type ProposeOCR3BootstrapJobDeps struct {
21+
Env cldf.Environment
22+
}
23+
24+
type ProposeOCR3BootstrapJobInput struct {
25+
DONName string
26+
Domain string
27+
ContractID string
28+
EnvironmentLabel string
29+
ChainSelectorEVM uint64
30+
31+
JobName string // Optional job name, if not provided, the default will be used.
32+
33+
DONFilters []TargetDONFilter
34+
ExtraLabels map[string]string
35+
}
36+
37+
type ProposeOCR3BootstrapJobOutput struct {
38+
Specs map[string][]string
39+
}
40+
41+
var ProposeOCR3BootstrapJob = operations.NewOperation[ProposeOCR3BootstrapJobInput, ProposeOCR3BootstrapJobOutput, ProposeOCR3BootstrapJobDeps](
42+
"propose-ocr3-bootstrap-job-op",
43+
semver.MustParse("1.0.0"),
44+
"Propose OCR3 Bootstrap Job",
45+
func(b operations.Bundle, deps ProposeOCR3BootstrapJobDeps, input ProposeOCR3BootstrapJobInput) (ProposeOCR3BootstrapJobOutput, error) {
46+
extJobID, err := pkg.BootstrapExternalJobID(input.DONName, input.ChainSelectorEVM)
47+
if err != nil {
48+
return ProposeOCR3BootstrapJobOutput{}, fmt.Errorf("failed to generate external job ID: %w", err)
49+
}
50+
51+
chainID, err := chainsel.GetChainIDFromSelector(input.ChainSelectorEVM)
52+
if err != nil {
53+
return ProposeOCR3BootstrapJobOutput{}, fmt.Errorf("failed to get chain ID from selector: %w", err)
54+
}
55+
56+
jobName := DefaultBootstrapJobName
57+
if input.JobName != "" {
58+
jobName = input.JobName
59+
}
60+
61+
cfg := pkg.BootstrapCfg{
62+
JobName: jobName,
63+
ExternalJobID: extJobID,
64+
ContractID: input.ContractID,
65+
ChainID: chainID,
66+
}
67+
if err = cfg.Validate(); err != nil {
68+
return ProposeOCR3BootstrapJobOutput{}, fmt.Errorf("invalid bootstrap config: %w", err)
69+
}
70+
71+
spec, err := cfg.ResolveSpec()
72+
if err != nil {
73+
return ProposeOCR3BootstrapJobOutput{}, fmt.Errorf("failed to resolve bootstrap job spec: %w", err)
74+
}
75+
76+
report, err := operations.ExecuteOperation(b, ProposeJobSpec, ProposeJobSpecDeps(deps), ProposeJobSpecInput{
77+
Domain: input.Domain,
78+
DONName: input.DONName,
79+
Spec: spec,
80+
JobLabels: input.ExtraLabels,
81+
DONFilters: input.DONFilters,
82+
IsBootstrap: true,
83+
})
84+
if err != nil {
85+
return ProposeOCR3BootstrapJobOutput{}, fmt.Errorf("failed to propose bootstrap job: %w", err)
86+
}
87+
88+
return ProposeOCR3BootstrapJobOutput{
89+
Specs: report.Output.Specs,
90+
}, nil
91+
},
92+
)

deployment/cre/jobs/operations/propose_std_cap.go

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,8 @@ import (
77

88
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
99
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
10-
"github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node"
11-
"github.com/smartcontractkit/chainlink-protos/job-distributor/v1/shared/ptypes"
12-
1310
"github.com/smartcontractkit/chainlink/deployment/cre/jobs/pkg"
1411
"github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain"
15-
"github.com/smartcontractkit/chainlink/deployment/helpers/pointer"
1612
)
1713

1814
const FilterKeyDONName = "don_name"
@@ -22,6 +18,7 @@ type ProposeStandardCapabilityJobDeps struct {
2218
}
2319

2420
type ProposeStandardCapabilityJobInput struct {
21+
Domain string
2522
DONName string
2623
Job pkg.StandardCapabilityJob
2724
DONFilters []TargetDONFilter
@@ -58,44 +55,19 @@ var ProposeStandardCapabilityJob = operations.NewOperation[ProposeStandardCapabi
5855
jobLabels[k] = v
5956
}
6057

61-
filter := &node.ListNodesRequest_Filter{
62-
Selectors: []*ptypes.Selector{
63-
{
64-
Key: "type",
65-
Op: ptypes.SelectorOp_EQ,
66-
Value: pointer.To("plugin"),
67-
},
68-
},
69-
}
70-
for _, f := range input.DONFilters {
71-
// DON name is a key, so we just check for its existence instead of equality
72-
if f.Key == FilterKeyDONName {
73-
filter.Selectors = append(filter.Selectors, &ptypes.Selector{
74-
Op: ptypes.SelectorOp_EXIST,
75-
Key: f.Value,
76-
})
77-
} else {
78-
filter.Selectors = append(filter.Selectors, &ptypes.Selector{
79-
Op: ptypes.SelectorOp_EQ,
80-
Key: f.Key,
81-
Value: &f.Value,
82-
})
83-
}
84-
}
85-
86-
specs, err := pkg.ProposeJob(b.GetContext(), deps.Env, pkg.ProposeJobRequest{
87-
Spec: spec,
88-
DONName: input.DONName,
89-
Env: deps.Env.Name,
90-
JobLabels: jobLabels,
91-
DONFilter: filter,
58+
report, err := operations.ExecuteOperation(b, ProposeJobSpec, ProposeJobSpecDeps(deps), ProposeJobSpecInput{
59+
Domain: input.Domain,
60+
DONName: input.DONName,
61+
Spec: spec,
62+
JobLabels: jobLabels,
63+
DONFilters: input.DONFilters,
9264
})
9365
if err != nil {
9466
return ProposeStandardCapabilityJobOutput{}, fmt.Errorf("failed to propose job: %w", err)
9567
}
9668

9769
return ProposeStandardCapabilityJobOutput{
98-
Specs: specs,
70+
Specs: report.Output.Specs,
9971
}, nil
10072
},
10173
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package pkg
2+
3+
import (
4+
"github.com/Masterminds/semver/v3"
5+
6+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
7+
)
8+
9+
func GetOCR3CapabilityV2AddressRefKey(chainSel uint64, qualifier string) datastore.AddressRefKey {
10+
return datastore.NewAddressRefKey(
11+
chainSel,
12+
"OCR3Capability",
13+
semver.MustParse("2.0.0"),
14+
qualifier,
15+
)
16+
}

deployment/cre/jobs/pkg/fs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package pkg
2+
3+
import "embed"
4+
5+
//go:embed *tmpl
6+
var tmplFS embed.FS

deployment/cre/jobs/pkg/jobs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func ProposeJob(ctx context.Context, e cldf.Environment, req ProposeJobRequest)
3838
JobLabels: req.JobLabels,
3939
OffchainClient: e.Offchain,
4040
Lggr: e.Logger,
41+
ExtraSelectors: req.DONFilter.GetSelectors(),
4142
}
4243
err = offchain.ProposeJob(ctx, offchainReq)
4344
if err != nil {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
type = "bootstrap"
2+
schemaVersion = 1
3+
name = "{{ .JobName }}"
4+
externalJobID = "{{ .ExternalJobID }}"
5+
contractID = "{{ .ContractID }}"
6+
contractConfigTrackerPollInterval = "1s"
7+
contractConfigConfirmations = 1
8+
relay = "evm"
9+
10+
[relayConfig]
11+
chainID = {{ .ChainID }}
12+
providerType = "ocr3-capability"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package pkg
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"text/template"
8+
)
9+
10+
const bootstrapPth = "ocr3_bootstrap.tmpl"
11+
12+
type BootstrapJobInput struct {
13+
ContractQualifier string `json:"contract_qualifier" yaml:"contract_qualifier"` // OCR contract address
14+
ChainSelector uint64 `json:"chain_selector" yaml:"chain_selector"`
15+
}
16+
17+
type BootstrapCfg struct {
18+
JobName string
19+
ExternalJobID string // If empty, will be generated
20+
ContractID string // OCR contract address
21+
ChainID string
22+
}
23+
24+
func (cfg BootstrapCfg) Validate() error {
25+
if cfg.JobName == "" {
26+
return errors.New("ocr3 bootstrap job name cannot be empty")
27+
}
28+
29+
if cfg.ContractID == "" {
30+
return errors.New("ocr3 bootstrap contract ID cannot be empty")
31+
}
32+
33+
if cfg.ChainID == "" {
34+
return errors.New("ocr3 bootstrap chain ID cannot be empty")
35+
}
36+
37+
return nil
38+
}
39+
40+
func (cfg BootstrapCfg) ResolveSpec() (string, error) {
41+
t, err := template.New("s").ParseFS(tmplFS, bootstrapPth)
42+
if err != nil {
43+
return "", fmt.Errorf("failed to parse %s: %w", bootstrapPth, err)
44+
}
45+
46+
b := &bytes.Buffer{}
47+
err = t.ExecuteTemplate(b, bootstrapPth, cfg)
48+
if err != nil {
49+
return "", fmt.Errorf("failed to execute template: %w", err)
50+
}
51+
52+
return b.String(), nil
53+
}
54+
55+
func BootstrapExternalJobID(donName string, evmChainSel uint64) (string, error) {
56+
return ExternalJobID(donName+"-bootstrap", evmChainSel)
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package pkg
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/binary"
6+
7+
"github.com/google/uuid"
8+
)
9+
10+
// NOTE: consider adding contract address to the hash
11+
func ExternalJobID(donName string, evmChainSel uint64) (string, error) {
12+
in := []byte(donName + "-ocr3-capability-job-spec")
13+
b := make([]byte, 8)
14+
binary.BigEndian.PutUint64(b, evmChainSel)
15+
in = append(in, b...)
16+
sha256Hash := sha256.New()
17+
sha256Hash.Write(in)
18+
in = sha256Hash.Sum(nil)[:16]
19+
// tag as valid UUID v4 https://github.com/google/uuid/blob/0f11ee6918f41a04c201eceeadf612a377bc7fbc/version4.go#L53-L54
20+
in[6] = (in[6] & 0x0f) | 0x40 // Version 4
21+
in[8] = (in[8] & 0x3f) | 0x80 // Variant is 10
22+
23+
id, err := uuid.FromBytes(in)
24+
if err != nil {
25+
return "", err
26+
}
27+
28+
return id.String(), nil
29+
}

0 commit comments

Comments
 (0)