Skip to content

feat(bootstrap): adopt Chainlink node keys via [key_import] - #1317

Open
tt-cll wants to merge 5 commits into
mainfrom
split/1-key-import
Open

feat(bootstrap): adopt Chainlink node keys via [key_import]#1317
tt-cll wants to merge 5 commits into
mainfrom
split/1-key-import

Conversation

@tt-cll

@tt-cll tt-cll commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

https://smartcontract-it.atlassian.net/browse/CCIP-12567

[key_import] in the bootstrap config adopts a key exported from a Chainlink node instead of generating a fresh one: the OCR2 bundle's onchain signing key for a verifier, the funded EVM account for an executor. Both have to survive the CL-to-standalone move or the committee signer set needs reconfiguring and the transmitter needs refunding.

  • Two paths and an optional expected_id pin (enforced against the keystore as well as the export) against mounting the wrong node's file.
  • The import target is inferred — each app declares exactly one non-CSA key — and the export format is detected from the file (OCR2 bundle / eth v3 keystore), so there is nothing to mistype.
  • Import runs only when the key is absent: no-op on restart, files can be unmounted after first boot. KMS backend is rejected with a clear reason.

Part of the CL-to-standalone migration series; the operator procedure lands with the cutover PR.

Testing

Unit tests for both decoders (real export shapes), format detection, validation, and EnsureImportedKey including the expected_id enforcement against an existing keystore key.

Checklist

  • Breaking changes documented in changelog (see changelog directory)
  • Cross link related PRs (in this or other repositories)

Stack created with GitHub Stacks CLIGive Feedback 💬

@tt-cll
tt-cll requested review from a team as code owners July 31, 2026 10:52
Copilot AI review requested due to automatic review settings July 31, 2026 10:52
@github-actions

Copy link
Copy Markdown

👋 tt-cll, thanks for creating this pull request!

To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team.

Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a bootstrap-time key adoption flow to support migrating CCV operators from running on a Chainlink node to standalone binaries without changing on-chain signer/transmitter identities.

Changes:

  • Introduces [key_import] bootstrap config and wiring to import a Chainlink-exported OCR2 bundle (EVM onchain key) or ETH v3 keystore on first boot.
  • Adds import format detection/decoders plus EnsureImportedKey logic (no-op when the key already exists).
  • Updates config docs/README and adds a changelog entry describing the migration behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/configdoc/registry/registry.go Populates KeyImport in the configdoc instance for generated docs/examples.
docs/config/bootstrap/config.documented.toml Documents the new [key_import] section in the generated bootstrap config reference.
changelog/2026-07-29_cl_to_standalone_key_migration.md Operator-facing changelog describing the migration and key adoption.
bootstrap/README.md Adds bootstrap documentation for adopting a key from a Chainlink node export.
bootstrap/keys/import.go Implements format detection, OCR2/ETH export decoding, and keystore import envelopeing.
bootstrap/keys/import_test.go Unit tests for decoders, format detection, and EnsureImportedKey behavior (including expected_id).
bootstrap/config.go Adds KeyImport config schema + validation and conversion to keys.Import.
bootstrap/bootstrap.go Wires key import into keystore initialization and resolves which declared key is importable.
Suppressed comments (1)

bootstrap/config.go:194

  • validateKeyImport currently rejects a configured [key_import] unless expected_id is set, but the PR description (and bootstrap/README.md) describe expected_id as optional. The keys.Import implementation already treats ExpectedID as opt-in, so this validation makes the feature stricter than intended and breaks migrations that omit expected_id.
	if strings.TrimSpace(imp.ExpectedID) == "" {
		return []error{fmt.Errorf(
			"invalid 'key_import' section: expected_id is required: it is the check that fails " +
				"the boot when the wrong node's export is mounted")}
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread bootstrap/config.go
Comment thread docs/config/bootstrap/config.documented.toml Outdated
Comment thread bootstrap/keys/import.go Outdated
@tt-cll tt-cll mentioned this pull request Jul 31, 2026
2 tasks
Comment thread bootstrap/keys/import.go Outdated
Comment thread bootstrap/keys/import.go
Comment thread bootstrap/keys/import.go
Comment thread bootstrap/keys/import.go
}
}

blob, err := encodeForImport(keyName, keyType, privateKey)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the reason why we do decodeImport, which decrypts the private key data, and then an encodeForImport, which re-encrypts the data w/ constant password (declared in this file), to import the key into the right name? I'm wondering if we can simplify this a bit if we export the keys from chainlink under a specific keyName instead, would clean up a decent bit of code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if that export trick would work. I don't see a way to specify a format or key name to export to in the request body https://github.com/smartcontractkit/chainlink-common/blob/3207712d718767c65e222c376baeed33dc8ca419/keystore/admin.go#L72. We have to do some massaging to get the keys into our format and that's not supported by the API it looks like.

Comment thread bootstrap/README.md Outdated
Comment thread bootstrap/config.go
Copilot AI review requested due to automatic review settings July 31, 2026 15:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (3)

bootstrap/keys/import.go:106

  • Import.Validate only checks that expected_id is non-empty; it does not validate that it is a 20-byte hex address. If an operator mistypes expected_id (wrong length / non-hex), the failure later can look like “wrong node's export is mounted”, which is misleading. Validate expected_id's shape up front so config errors are reported clearly.
	if strings.TrimSpace(i.ExpectedID) == "" {
		return fmt.Errorf("field 'expected_id' is required: it is the check that fails the boot " +
			"when the wrong node's export is mounted")
	}

bootstrap/keys/import.go:155

  • EnsureImportedKey’s non-secp256k1 error message interpolates spec.Format, but spec.Format is allowed to be empty (normal path: format auto-detected). In that case the message becomes confusing (“but imports carry…”). Populate a fallback label when spec.Format is empty.
	if keyType != keystore.ECDSA_S256 {
		return fmt.Errorf(
			"key %q is declared as %s but %s imports carry a secp256k1 key and can only populate %s",
			keyName, keyType, spec.Format, keystore.ECDSA_S256,
		)

bootstrap/bootstrap.go:863

  • New resolveKeyImport + initializeKeystore branching adds critical startup behavior (KMS backend rejection and inference of which declared key gets imported). There are bootstrap package tests, but none cover the key-import selection/error cases, so regressions here would be easy to miss.
func resolveKeyImport(backend KeystoreBackend, requiredKeys []keyToInit, keyImport *KeyImport) (string, keys.Import, error) {
	if keyImport == nil {
		return "", keys.Import{}, nil
	}
	if backend == KeystoreBackendKMS {

Copilot AI review requested due to automatic review settings July 31, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

bootstrap/keys/import.go:155

  • The key-type mismatch error message includes spec.Format, but spec.Format is often left empty (format auto-detected). Using %s yields a blank in the message ("but imports"), which is confusing when diagnosing misdeclared key types.
	if keyType != keystore.ECDSA_S256 {
		return fmt.Errorf(
			"key %q is declared as %s but %s imports carry a secp256k1 key and can only populate %s",
			keyName, keyType, spec.Format, keystore.ECDSA_S256,
		)

bootstrap/config.go:387

  • key_import is explicitly unsupported with the KMS keystore backend (enforced later in resolveKeyImport), but config validation currently doesn't surface that incompatibility. Adding a validation-time check improves operator feedback by failing fast with a clear config error.
	// The key import populates the keystore, which both modes initialize when [db] and [keystore]
	// are present, so it is validated in every mode rather than only alongside the JD infra bundle.
	errs = append(errs, validateKeyImport(c.KeyImport)...)
	return errors.Join(errs...)

@github-actions

Copy link
Copy Markdown

Code coverage report:

Package main split/1-key-import Diff
github.com/smartcontractkit/chainlink-ccv/aggregator 50.86% 50.85% -0.01%
github.com/smartcontractkit/chainlink-ccv/bootstrap 58.98% 68.81% +9.83%
github.com/smartcontractkit/chainlink-ccv/cli 64.65% 64.65% +0.00%
github.com/smartcontractkit/chainlink-ccv/cmd 31.39% 31.39% +0.00%
github.com/smartcontractkit/chainlink-ccv/common 46.51% 46.51% +0.00%
github.com/smartcontractkit/chainlink-ccv/executor 45.91% 45.91% +0.00%
github.com/smartcontractkit/chainlink-ccv/indexer 35.55% 35.55% +0.00%
github.com/smartcontractkit/chainlink-ccv/integration 56.07% 56.09% +0.02%
github.com/smartcontractkit/chainlink-ccv/internal 0.00% 0.00% +0.00%
github.com/smartcontractkit/chainlink-ccv/pkg 100.00% 100.00% +0.00%
github.com/smartcontractkit/chainlink-ccv/pricer 0.00% 0.00% +0.00%
github.com/smartcontractkit/chainlink-ccv/protocol 63.06% 63.06% +0.00%
github.com/smartcontractkit/chainlink-ccv/tools 43.61% 43.61% +0.00%
github.com/smartcontractkit/chainlink-ccv/verifier 35.89% 35.89% +0.00%
Total 48.30% 48.60% +0.30%

Files added (in split/1-key-import):

  • github.com/smartcontractkit/chainlink-ccv/bootstrap/keys/import.go

@tt-cll
tt-cll enabled auto-merge July 31, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants