Skip to content

Commit 8d22875

Browse files
committed
CCIP attestation signer registry changesets
Deploy base signer registry changeset Error improvements Add initialize changeset Add changeset for NOP rotation Add green key changeset Add promotion changesets Adjust visibility WIP test Add IDL changeset Add timelock transfer Cleanup Add upgrade authority transfer changeset Add MCMS ownership validation checks MCMS support Update with contract changes from audit feedback
1 parent 06e682a commit 8d22875

19 files changed

+1137
-2320
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package ccip_attestation
2+
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"context"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
13+
"github.com/Masterminds/semver/v3"
14+
"github.com/gagliardetto/solana-go"
15+
signer_registry "github.com/smartcontractkit/ccip-base/chains/solana/go_bindings"
16+
chainsel "github.com/smartcontractkit/chain-selectors"
17+
18+
cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
19+
20+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
21+
"github.com/smartcontractkit/chainlink/deployment"
22+
"github.com/smartcontractkit/chainlink/deployment/ccip/shared"
23+
)
24+
25+
// use this changeset to deploy the base signer registry contract
26+
var _ cldf.ChangeSet[DeployBaseSignerRegistryContractConfig] = DeployBaseSignerRegistryContractChangeset
27+
28+
// use this changeset to initialize the base signer registry contract and set an initial owner
29+
var _ cldf.ChangeSet[InitalizeBaseSignerRegistryContractConfig] = InitializeBaseSignerRegistryContractChangeset
30+
31+
type DeployBaseSignerRegistryContractConfig struct {
32+
ChainSelector uint64
33+
Version semver.Version
34+
WorkflowRun string
35+
ArtifactId string
36+
IsUpgrade bool
37+
}
38+
39+
type InitalizeBaseSignerRegistryContractConfig struct {
40+
ChainSelector uint64
41+
Owner solana.PublicKey
42+
}
43+
44+
func DeployBaseSignerRegistryContractChangeset(e cldf.Environment, c DeployBaseSignerRegistryContractConfig) (cldf.ChangesetOutput, error) {
45+
e.Logger.Infow("Deploying base signer registry", "chain_selector", c.ChainSelector)
46+
c.Validate(e)
47+
chainSel := c.ChainSelector
48+
chain := e.BlockChains.SolanaChains()[chainSel]
49+
50+
newAddresses := cldf.NewMemoryAddressBook()
51+
if err := DownloadReleaseArtifactsFromGithubWorkflowRun(context.Background(), c.WorkflowRun, c.ArtifactId, chain.ProgramsPath); err != nil {
52+
return cldf.ChangesetOutput{}, fmt.Errorf("failed to download release artifacts: %w", err)
53+
}
54+
_, err := deployBaseSignerRegistryContract(e, chain, newAddresses, c)
55+
if err != nil {
56+
return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy base signer registry contract: %w", err)
57+
}
58+
59+
return cldf.ChangesetOutput{
60+
AddressBook: newAddresses,
61+
}, nil
62+
}
63+
64+
func InitializeBaseSignerRegistryContractChangeset(e cldf.Environment, c InitalizeBaseSignerRegistryContractConfig) (cldf.ChangesetOutput, error) {
65+
e.Logger.Infow("Initializing base signer registry", "chain_selector", c.ChainSelector)
66+
c.Validate(e)
67+
chainSel := c.ChainSelector
68+
chain := e.BlockChains.SolanaChains()[chainSel]
69+
authority := chain.DeployerKey.PublicKey()
70+
71+
configPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("config")}, signer_registry.ProgramID)
72+
signersPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("signers")}, signer_registry.ProgramID)
73+
eventAuthorityPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, signer_registry.ProgramID)
74+
ix, err := signer_registry.NewInitializeInstruction(authority, solana.SystemProgramID, configPda, signersPda, eventAuthorityPda, signer_registry.ProgramID)
75+
if err != nil {
76+
return cldf.ChangesetOutput{}, fmt.Errorf("Failed to initialize base signer registry contract: %w", err)
77+
}
78+
79+
if err := chain.Confirm([]solana.Instruction{ix}); err != nil {
80+
return cldf.ChangesetOutput{}, fmt.Errorf("Failed to initialize base signer registry contract: %w", err)
81+
}
82+
83+
return cldf.ChangesetOutput{}, nil
84+
}
85+
86+
func deployBaseSignerRegistryContract(e cldf.Environment, chain cldf_solana.Chain, ab cldf.AddressBook, config DeployBaseSignerRegistryContractConfig,
87+
) (solana.PublicKey, error) {
88+
contractType := shared.BaseSignerRegistry
89+
programName := deployment.BaseSignerRegistryProgramName
90+
91+
programID, err := chain.DeployProgram(e.Logger, cldf_solana.ProgramInfo{
92+
Name: programName,
93+
Bytes: deployment.SolanaProgramBytes[programName],
94+
}, config.IsUpgrade, true)
95+
96+
if err != nil {
97+
return solana.PublicKey{}, fmt.Errorf("failed to deploy program: %w", err)
98+
}
99+
address := solana.MustPublicKeyFromBase58(programID)
100+
101+
e.Logger.Infow("Deployed program", "Program", contractType, "addr", programID, "chain", chain.String())
102+
tv := cldf.NewTypeAndVersion(contractType, config.Version)
103+
err = ab.Save(chain.Selector, programID, tv)
104+
if err != nil {
105+
return solana.PublicKey{}, fmt.Errorf("failed to save address: %w", err)
106+
}
107+
108+
return address, nil
109+
110+
}
111+
112+
func (c DeployBaseSignerRegistryContractConfig) Validate(e cldf.Environment) error {
113+
if err := cldf.IsValidChainSelector(c.ChainSelector); err != nil {
114+
return fmt.Errorf("invalid chain selector: %d - %w", c.ChainSelector, err)
115+
}
116+
family, _ := chainsel.GetSelectorFamily(c.ChainSelector)
117+
if family != chainsel.FamilySolana {
118+
return fmt.Errorf("chain %d is not a solana chain", c.ChainSelector)
119+
}
120+
121+
return nil
122+
}
123+
124+
func (c InitalizeBaseSignerRegistryContractConfig) Validate(e cldf.Environment) error {
125+
if err := cldf.IsValidChainSelector(c.ChainSelector); err != nil {
126+
return fmt.Errorf("invalid chain selector: %d - %w", c.ChainSelector, err)
127+
}
128+
family, _ := chainsel.GetSelectorFamily(c.ChainSelector)
129+
if family != chainsel.FamilySolana {
130+
return fmt.Errorf("chain %d is not a solana chain", c.ChainSelector)
131+
}
132+
133+
return nil
134+
}
135+
136+
func DownloadReleaseArtifactsFromGithubWorkflowRun(
137+
ctx context.Context,
138+
run string,
139+
artifactId string,
140+
targetPath string,
141+
) error {
142+
url := fmt.Sprintf(
143+
"https://github.com/smartcontractkit/ccip-base/actions/runs/%s/artifacts/%s",
144+
run,
145+
artifactId,
146+
)
147+
148+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
149+
if err != nil {
150+
return fmt.Errorf("failed to create request: %w", err)
151+
}
152+
153+
client := &http.Client{}
154+
resp, err := client.Do(req)
155+
if err != nil {
156+
return fmt.Errorf("failed to download release asset: %w", err)
157+
}
158+
defer resp.Body.Close()
159+
160+
if resp.StatusCode != http.StatusOK {
161+
return fmt.Errorf("failed to download release asset: HTTP %d", resp.StatusCode)
162+
}
163+
164+
// Read the entire zip file into memory
165+
body, err := io.ReadAll(resp.Body)
166+
if err != nil {
167+
return fmt.Errorf("failed to read response body: %w", err)
168+
}
169+
170+
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
171+
if err != nil {
172+
return fmt.Errorf("failed to create zip reader: %w", err)
173+
}
174+
175+
// Extract each file from the zip archive
176+
for _, file := range zipReader.File {
177+
targetPath := filepath.Join(targetPath, file.Name)
178+
179+
if file.FileInfo().IsDir() {
180+
if err := os.MkdirAll(targetPath, file.Mode()); err != nil {
181+
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
182+
}
183+
continue
184+
}
185+
186+
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
187+
return fmt.Errorf("failed to create parent directory for %s: %w", targetPath, err)
188+
}
189+
190+
fileReader, err := file.Open()
191+
if err != nil {
192+
return fmt.Errorf("failed to open file in zip %s: %w", file.Name, err)
193+
}
194+
defer fileReader.Close()
195+
196+
targetFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, file.Mode())
197+
if err != nil {
198+
return fmt.Errorf("failed to create file %s: %w", targetPath, err)
199+
}
200+
201+
if _, err := io.Copy(targetFile, fileReader); err != nil {
202+
targetFile.Close()
203+
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
204+
}
205+
206+
}
207+
208+
return nil
209+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package ccip_attestation_test
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/gagliardetto/solana-go"
10+
signerRegistry "github.com/smartcontractkit/ccip-base/chains/solana/go_bindings"
11+
ccip_attestation "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/ccip_attestation"
12+
"github.com/smartcontractkit/chainlink/deployment/environment/memory"
13+
"github.com/smartcontractkit/chainlink/v2/core/logger"
14+
"go.uber.org/zap/zapcore"
15+
16+
chain_selectors "github.com/smartcontractkit/chain-selectors"
17+
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
18+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
19+
commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset"
20+
"github.com/stretchr/testify/require"
21+
)
22+
23+
func TestSignerRegistryInitialization(t *testing.T) {
24+
t.Parallel()
25+
lggr := logger.TestLogger(t)
26+
// We download the artifacts before spinning out the environment so the program can be preloaded.
27+
_, currentFile, _, _ := runtime.Caller(0)
28+
rootDir := filepath.Dir(filepath.Dir(filepath.Dir(currentFile)))
29+
targetPath := filepath.Join(rootDir, "ccip/changeset/internal", "solana_contracts")
30+
// Replace with the desired run to test
31+
ccip_attestation.DownloadReleaseArtifactsFromGithubWorkflowRun(context.Background(), "17459947443", "3925592769", targetPath)
32+
33+
e := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
34+
SolChains: 1,
35+
})
36+
solChain1 := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chain_selectors.FamilySolana))[0]
37+
38+
owner, err := solana.NewRandomPrivateKey()
39+
require.NoError(t, err)
40+
41+
e, err = commonchangeset.Apply(t, e,
42+
commonchangeset.Configure(
43+
// deployer creates token
44+
cldf.CreateLegacyChangeSet(ccip_attestation.InitializeBaseSignerRegistryContractChangeset),
45+
ccip_attestation.InitalizeBaseSignerRegistryContractConfig{
46+
ChainSelector: solChain1,
47+
Owner: owner.PublicKey(),
48+
},
49+
),
50+
)
51+
require.NoError(t, err)
52+
53+
programId := solana.MustPublicKeyFromBase58(memory.SolanaProgramIDs["ccip_signer_registry"])
54+
configPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("config")}, programId)
55+
56+
var configAccount signerRegistry.Config
57+
chain := e.BlockChains.SolanaChains()[solChain1]
58+
err = chain.GetAccountDataBorshInto(context.Background(), configPda, &configAccount)
59+
require.NoError(t, err)
60+
61+
require.Equal(t, owner.PublicKey(), configAccount.Owner, "owner not set correctly in config PDA")
62+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package ccip_attestation
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"path/filepath"
8+
9+
signer_registry "github.com/smartcontractkit/ccip-base/chains/solana/go_bindings"
10+
chainsel "github.com/smartcontractkit/chain-selectors"
11+
12+
cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
13+
14+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
15+
16+
"github.com/smartcontractkit/chainlink/deployment"
17+
cs_solana "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/solana_v0_1_1"
18+
)
19+
20+
// use this changeset to upload the IDL for the attestation program
21+
var _ cldf.ChangeSet[BaseIDLConfig] = BaseUploadIDLChangeset
22+
23+
// use this changeset to set the authority for the IDL of the attestation program (timelock)
24+
var _ cldf.ChangeSet[BaseIDLConfig] = BaseSetAuthorityIDLChangeset
25+
26+
const IdlIxTag uint64 = 0x0a69e9a778bcf440
27+
28+
type BaseIDLConfig struct {
29+
ChainSelector uint64
30+
WorkflowRun string
31+
ArtifactId string
32+
}
33+
34+
// resolve artifacts based on workflow run and write anchor.toml file to simulate anchor workspace
35+
func repoSetup(e cldf.Environment, chain cldf_solana.Chain, run string, artifactId string) error {
36+
e.Logger.Debug("Downloading artifacts from workflow run...")
37+
err := DownloadReleaseArtifactsFromGithubWorkflowRun(context.Background(), run, artifactId, chain.ProgramsPath)
38+
if err != nil {
39+
return fmt.Errorf("error downloading program artifacts: %w", err)
40+
}
41+
42+
// get anchor version
43+
output, err := cs_solana.RunCommand("anchor", []string{"--version"}, ".")
44+
if err != nil {
45+
return errors.New("anchor-cli not installed in path")
46+
}
47+
e.Logger.Debugw("Anchor version command output", "output", output)
48+
anchorVersion, err := cs_solana.ParseAnchorVersion(output)
49+
if err != nil {
50+
return fmt.Errorf("error parsing anchor version: %w", err)
51+
}
52+
// create Anchor.toml
53+
// this creates anchor workspace with cluster and wallet configured
54+
if err := cs_solana.WriteAnchorToml(e, filepath.Join(chain.ProgramsPath, "Anchor.toml"), anchorVersion, chain.URL, chain.KeypairPath); err != nil {
55+
return fmt.Errorf("error writing Anchor.toml: %w", err)
56+
}
57+
58+
return nil
59+
}
60+
61+
func (c BaseIDLConfig) Validate(e cldf.Environment) error {
62+
if err := cldf.IsValidChainSelector(c.ChainSelector); err != nil {
63+
return fmt.Errorf("invalid chain selector: %d - %w", c.ChainSelector, err)
64+
}
65+
family, _ := chainsel.GetSelectorFamily(c.ChainSelector)
66+
if family != chainsel.FamilySolana {
67+
return fmt.Errorf("chain %d is not a solana chain", c.ChainSelector)
68+
}
69+
return nil
70+
}
71+
72+
func BaseUploadIDLChangeset(e cldf.Environment, c BaseIDLConfig) (cldf.ChangesetOutput, error) {
73+
if err := c.Validate(e); err != nil {
74+
return cldf.ChangesetOutput{}, fmt.Errorf("error validating idl config: %w", err)
75+
}
76+
chain := e.BlockChains.SolanaChains()[c.ChainSelector]
77+
if err := repoSetup(e, chain, c.WorkflowRun, c.ArtifactId); err != nil {
78+
return cldf.ChangesetOutput{}, fmt.Errorf("error setting up repo: %w", err)
79+
}
80+
81+
err := cs_solana.IdlInit(e, chain.ProgramsPath, signer_registry.ProgramID.String(), deployment.BaseSignerRegistryProgramName)
82+
if err != nil {
83+
return cldf.ChangesetOutput{}, err
84+
}
85+
return cldf.ChangesetOutput{}, nil
86+
}
87+
88+
func BaseSetAuthorityIDLChangeset(e cldf.Environment, c BaseIDLConfig) (cldf.ChangesetOutput, error) {
89+
if err := c.Validate(e); err != nil {
90+
return cldf.ChangesetOutput{}, fmt.Errorf("error validating idl config: %w", err)
91+
}
92+
chain := e.BlockChains.SolanaChains()[c.ChainSelector]
93+
94+
timelockSignerPDA, err := cs_solana.FetchTimelockSigner(e, c.ChainSelector)
95+
if err != nil {
96+
return cldf.ChangesetOutput{}, fmt.Errorf("error loading timelockSignerPDA: %w", err)
97+
}
98+
99+
err = cs_solana.SetAuthorityIDLByCLI(e, timelockSignerPDA.String(), chain.ProgramsPath, signer_registry.ProgramID.String(), deployment.BaseSignerRegistryProgramName, "")
100+
if err != nil {
101+
return cldf.ChangesetOutput{}, err
102+
}
103+
104+
return cldf.ChangesetOutput{}, nil
105+
}

0 commit comments

Comments
 (0)