Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/some-lamps-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

#internal changeset support for Solana CCIP attestation signer registry contract
1 change: 1 addition & 0 deletions core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ require (
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gagliardetto/anchor-go v0.3.2 // indirect
github.com/gagliardetto/binary v0.8.0 // indirect
github.com/gagliardetto/metaplex-go v0.2.1 // indirect
github.com/gagliardetto/solana-go v1.13.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e h1:5jVSh2l/ho6ajWhSPNN84eHEdq3dp0T7+f6r3Tc6hsk=
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e/go.mod h1:IJgIiGUARc4aOr4bOQ85klmjsShkEEfiRc6q/yBSfo8=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -606,7 +607,10 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gagliardetto/anchor-go v0.3.2 h1:/nlAp3B6s4DBlNABWrQ5bA2zpsUGVDqBfwmbUUg4ChQ=
github.com/gagliardetto/anchor-go v0.3.2/go.mod h1:Jf9/DBNo6GsG6RguE4ZuJz+PZtypKg3iUpB++Oc6ynQ=
github.com/gagliardetto/binary v0.6.1/go.mod h1:aOfYkc20U0deHaHn/LVZXiqlkDbFAX0FpTlDhsXa0S0=
github.com/gagliardetto/binary v0.7.6/go.mod h1:mUuay5LL8wFVnIlecHakSZMvcdqfs+CsotR5n77kyjM=
github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg=
github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c=
github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw=
Expand All @@ -615,6 +619,7 @@ github.com/gagliardetto/hashsearch v0.0.0-20191005111333-09dd671e19f9/go.mod h1:
github.com/gagliardetto/metaplex-go v0.2.1 h1:NMBsgJe3I2avKZ39dfYQvXsGsr2BxUgARkA9LZ6szBg=
github.com/gagliardetto/metaplex-go v0.2.1/go.mod h1:6ZLYBvlWcXktXQ/QcBJYRzKgK7Q3WgiGD7BjE7Zxpw4=
github.com/gagliardetto/solana-go v1.4.0/go.mod h1:NFuoDwHPvw858ZMHUJr6bkhN8qHt4x6e+U3EYHxAwNY=
github.com/gagliardetto/solana-go v1.5.0/go.mod h1:1KFOW7mlR/TSjYFeLCYmfpSptRdNJMtpgChelKy2oU0=
github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig=
github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k=
github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw=
Expand Down Expand Up @@ -1721,6 +1726,7 @@ github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 h1:ba4VRWSkRzgdP5hB5OxexIzBXZbSwgcw8bEu06ivGQI=
github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863/go.mod h1:oPTjPNrRucLv9mU27iNPj6n0CWWcNFhoXFOLVGJwHCA=
github.com/streamingfast/logging v0.0.0-20220405224725-2755dab2ce75/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU=
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo=
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
package ccip_attestation

import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/Masterminds/semver/v3"
"github.com/gagliardetto/solana-go"
chainsel "github.com/smartcontractkit/chain-selectors"

signer_registry "github.com/smartcontractkit/chainlink/deployment/ccip/shared/bindings/signer_registry_solana"

cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"

sol_binary "github.com/gagliardetto/binary"
sol_rpc "github.com/gagliardetto/solana-go/rpc"

cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/ccip/shared"
)

// use this changeset to deploy the base signer registry contract
var _ cldf.ChangeSet[DeployBaseSignerRegistryContractConfig] = DeployBaseSignerRegistryContractChangeset

// use this changeset to initialize the base signer registry contract and set an initial owner
var _ cldf.ChangeSet[InitalizeBaseSignerRegistryContractConfig] = InitializeBaseSignerRegistryContractChangeset

type DeployBaseSignerRegistryContractConfig struct {
ChainSelector uint64
Version semver.Version
WorkflowRun string
ArtifactID string
IsUpgrade bool
}

type InitalizeBaseSignerRegistryContractConfig struct {
ChainSelector uint64
}

func DeployBaseSignerRegistryContractChangeset(e cldf.Environment, c DeployBaseSignerRegistryContractConfig) (cldf.ChangesetOutput, error) {
e.Logger.Infow("Deploying base signer registry", "chain_selector", c.ChainSelector)
err := c.Validate(e)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy base signer registry contract: %w", err)
}
chainSel := c.ChainSelector
chain := e.BlockChains.SolanaChains()[chainSel]

newAddresses := cldf.NewMemoryAddressBook()

programFileName := deployment.BaseSignerRegistryProgramName + ".so"
programFilePath := filepath.Join(chain.ProgramsPath, programFileName)
if _, err := os.Stat(programFilePath); err != nil {
if !os.IsNotExist(err) {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to check existing program artifact: %w", err)
}
if strings.TrimSpace(c.WorkflowRun) == "" || strings.TrimSpace(c.ArtifactID) == "" {
return cldf.ChangesetOutput{}, fmt.Errorf("program artifact %s not found in %s and workflow run/artifact ID not provided", programFileName, chain.ProgramsPath)
}
if err := DownloadReleaseArtifactsFromGithubWorkflowRun(context.Background(), c.WorkflowRun, c.ArtifactID, chain.ProgramsPath); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to download release artifacts: %w", err)
}
}
_, err = deployBaseSignerRegistryContract(e, chain, newAddresses, c)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy base signer registry contract: %w", err)
}

return cldf.ChangesetOutput{
AddressBook: newAddresses,
}, nil
}

func InitializeBaseSignerRegistryContractChangeset(e cldf.Environment, c InitalizeBaseSignerRegistryContractConfig) (cldf.ChangesetOutput, error) {
e.Logger.Infow("Initializing base signer registry", "chain_selector", c.ChainSelector)
err := c.Validate(e)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to initialize base signer registry contract: %w", err)
}
chainSel := c.ChainSelector
chain := e.BlockChains.SolanaChains()[chainSel]
authority := chain.DeployerKey.PublicKey()

configPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("config")}, signer_registry.ProgramID)
signersPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("signers")}, signer_registry.ProgramID)
eventAuthorityPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, signer_registry.ProgramID)
programData, err := getSolProgramData(e, chain, signer_registry.ProgramID)
if err != nil {
return cldf.ChangesetOutput{}, err
}

ix, err := signer_registry.NewInitializeInstruction(
authority,
solana.SystemProgramID,
configPda,
signersPda,
signer_registry.ProgramID,
programData.Address,
eventAuthorityPda,
signer_registry.ProgramID,
)

if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to initialize base signer registry contract: %w", err)
}

if err := chain.Confirm([]solana.Instruction{ix}); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to initialize base signer registry contract: %w", err)
}

return cldf.ChangesetOutput{}, nil
}

func deployBaseSignerRegistryContract(e cldf.Environment, chain cldf_solana.Chain, ab cldf.AddressBook, config DeployBaseSignerRegistryContractConfig,
) (solana.PublicKey, error) {
contractType := shared.SVMSignerRegistry
programName := deployment.BaseSignerRegistryProgramName

programID, err := chain.DeployProgram(e.Logger, cldf_solana.ProgramInfo{
Name: programName,
Bytes: deployment.SolanaProgramBytes[programName],
}, config.IsUpgrade, true)

if err != nil {
return solana.PublicKey{}, fmt.Errorf("failed to deploy program: %w", err)
}
address := solana.MustPublicKeyFromBase58(programID)

e.Logger.Infow("Deployed program", "Program", contractType, "addr", programID, "chain", chain.String())
tv := cldf.NewTypeAndVersion(contractType, config.Version)
err = ab.Save(chain.Selector, programID, tv)
if err != nil {
return solana.PublicKey{}, fmt.Errorf("failed to save address: %w", err)
}

return address, nil
}

func (c DeployBaseSignerRegistryContractConfig) Validate(e cldf.Environment) error {
if err := cldf.IsValidChainSelector(c.ChainSelector); err != nil {
return fmt.Errorf("invalid chain selector: %d - %w", c.ChainSelector, err)
}
family, _ := chainsel.GetSelectorFamily(c.ChainSelector)
if family != chainsel.FamilySolana {
return fmt.Errorf("chain %d is not a solana chain", c.ChainSelector)
}

return nil
}

func (c InitalizeBaseSignerRegistryContractConfig) Validate(e cldf.Environment) error {
if err := cldf.IsValidChainSelector(c.ChainSelector); err != nil {
return fmt.Errorf("invalid chain selector: %d - %w", c.ChainSelector, err)
}
family, _ := chainsel.GetSelectorFamily(c.ChainSelector)
if family != chainsel.FamilySolana {
return fmt.Errorf("chain %d is not a solana chain", c.ChainSelector)
}

return nil
}

func DownloadReleaseArtifactsFromGithubWorkflowRun(
ctx context.Context,
run string,
artifactID string,
targetPath string,
) error {
url := fmt.Sprintf(
"https://github.com/smartcontractkit/ccip-base/actions/runs/%s/artifacts/%s",
run,
artifactID,
)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to download release asset: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download release asset: HTTP %d", resp.StatusCode)
}

// Read the entire zip file into memory
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return fmt.Errorf("failed to create zip reader: %w", err)
}

// Extract each file from the zip archive
for _, file := range zipReader.File {
// Clean the file path to prevent directory traversal
cleanedName := filepath.Clean(file.Name)
// Ensure the file path doesn't escape the target directory
if strings.Contains(cleanedName, "..") {
return fmt.Errorf("invalid file path in archive: %s", file.Name)
}
filePath := filepath.Join(targetPath, cleanedName)

if file.FileInfo().IsDir() {
if err := os.MkdirAll(filePath, file.Mode()); err != nil {
return fmt.Errorf("failed to create directory %s: %w", filePath, err)
}
continue
}

if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("failed to create parent directory for %s: %w", filePath, err)
}

fileReader, err := file.Open()
if err != nil {
return fmt.Errorf("failed to open file in zip %s: %w", file.Name, err)
}

targetFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, file.Mode())
if err != nil {
fileReader.Close()
return fmt.Errorf("failed to create file %s: %w", filePath, err)
}

// Limit the amount of data to copy to prevent decompression bombs
const maxFileSize = 100 * 1024 * 1024 // 100MB limit per file
limitedReader := io.LimitReader(fileReader, maxFileSize)
n, err := io.Copy(targetFile, limitedReader)
fileReader.Close()
targetFile.Close()
if err != nil {
return fmt.Errorf("failed to write file %s: %w", filePath, err)
}
if n == maxFileSize {
return fmt.Errorf("file %s exceeds maximum allowed size of %d bytes", filePath, maxFileSize)
}
}

return nil
}

func getSolProgramData(e cldf.Environment, chain cldf_solana.Chain, programID solana.PublicKey) (struct {
DataType uint32
Address solana.PublicKey
}, error) {
var programData struct {
DataType uint32
Address solana.PublicKey
}
data, err := chain.Client.GetAccountInfoWithOpts(e.GetContext(), programID, &sol_rpc.GetAccountInfoOpts{
Commitment: sol_rpc.CommitmentConfirmed,
})
if err != nil {
return programData, fmt.Errorf("failed to deploy program: %w", err)
}

err = sol_binary.UnmarshalBorsh(&programData, data.Bytes())
if err != nil {
return programData, fmt.Errorf("failed to unmarshal program data: %w", err)
}
return programData, nil
}
Loading
Loading