Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

split upgrade command #16421

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 0 additions & 2 deletions deployment/ccip/changeset/solana/cs_deploy_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import (
commontypes "github.com/smartcontractkit/chainlink/deployment/common/types"
)

// This test will not work locally on Mac because the arm64 validator we use does not support upgrades
// https://solana.stackexchange.com/questions/17478/solana-localnet-error-while-upgrading-a-program-loaded-at-genesis-using-solan
func TestDeployChainContractsChangesetSolana(t *testing.T) {
t.Parallel()
lggr := logger.TestLogger(t)
Expand Down
164 changes: 146 additions & 18 deletions deployment/solana_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,29 +146,56 @@
}
tempFile.Close() // Close before passing to external command

cmd := exec.Command("solana", "program", "deploy", programFile,
"--program-id", programID.String(),
"--upgrade-authority", tempFile.Name(),
"--keypair", tempFile.Name(),
"--fee-payer", tempFile.Name(),
"--url", c.URL)
// Step 1: Get the current program account size
currentSize, err := getProgramAccountSize(programID.String())
if err != nil {
return "", fmt.Errorf("error getting current program account size: %w", err)
}
logger.Debugw("Current program account size", "size", currentSize)

// Capture the command output
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
// Step 2: Get the size of the new program binary
newSize, err := getFileSize(programFile)
if err != nil {
return "", fmt.Errorf("error getting new program binary size: %w", err)
}
logger.Debugw("New program binary size", "size", newSize)

// Run the command
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("error running command: %s: %s: %s", cmd.String(), err.Error(), stderr.String())
// Step 3: Check if additional space is needed
if newSize > currentSize {
additionalSpace := newSize - currentSize
logger.Debugw("Additional space required", "size", additionalSpace)

// Step 4: Extend the program account
err = extendProgramAccount(programID.String(), additionalSpace, tempFile.Name())
if err != nil {
return "", fmt.Errorf("error extending program account: %w", err)
}
logger.Debug("Program account extended successfully")
} else {
logger.Debug("No additional space needed for program account")
}

// Parse and return the program ID
output := stdout.String()
// Step 5: Write the new program binary to a buffer account
bufferAddress, err := writeBuffer(programFile, tempFile.Name())
if err != nil {
return "", fmt.Errorf("error writing buffer: %w", err)
}
logger.Debugw("Buffer account created", "address", bufferAddress)

// TODO: obviously need to do this better
time.Sleep(5 * time.Second)
return parseProgramID(output)
// Step 6: Upgrade the program using the buffer
newProgramID, err := upgradeProgram(programID.String(), bufferAddress, tempFile.Name())
if err != nil {
return "", fmt.Errorf("error upgrading program: %w", err)
}
logger.Debug("Program upgraded successfully")

// Step 7: Close the buffer account to reclaim SOL
err = closeBuffer(bufferAddress, tempFile.Name())
if err != nil {
return "", fmt.Errorf("error closing buffer account: %w", err)
}
logger.Debug("Buffer account closed successfully")
return newProgramID, nil
}

func (c SolChain) GetAccountDataBorshInto(ctx context.Context, pubkey solana.PublicKey, accountState interface{}) error {
Expand All @@ -195,3 +222,104 @@
}
return output[startIdx : startIdx+endIdx], nil
}

// getProgramAccountSize retrieves the current size of the program account
func getProgramAccountSize(programID string) (int, error) {
cmd := exec.Command("solana", "program", "show", programID)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return 0, err
}

// Parse the output to find the data length
output := out.String()
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "Data Length:") {
parts := strings.Split(line, ":")
if len(parts) < 2 {
return 0, fmt.Errorf("invalid data length line: %s", line)
}
sizeStr := strings.TrimSpace(parts[1])
size, err := strconv.Atoi(sizeStr)
if err != nil {
return 0, fmt.Errorf("failed to parse data length: %v", err)

Check failure on line 248 in deployment/solana_chain.go

View workflow job for this annotation

GitHub Actions / GolangCI Lint (deployment)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
return size, nil
}
}
return 0, fmt.Errorf("data length not found in program account info")

Check failure on line 253 in deployment/solana_chain.go

View workflow job for this annotation

GitHub Actions / GolangCI Lint (deployment)

fmt.Errorf can be replaced with errors.New (perfsprint)
}

// getFileSize returns the size of a file in bytes
func getFileSize(filePath string) (int, error) {
cmd := exec.Command("stat", "-c%s", filePath)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return 0, err
}
sizeStr := strings.TrimSpace(out.String())
size, err := strconv.Atoi(sizeStr)
if err != nil {
return 0, fmt.Errorf("failed to parse file size: %v", err)

Check failure on line 268 in deployment/solana_chain.go

View workflow job for this annotation

GitHub Actions / GolangCI Lint (deployment)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
return size, nil
}

// extendProgramAccount extends the program account by the specified additional space
func extendProgramAccount(programID string, additionalSpace int, upgradeAuthorityKey string) error {
cmd := exec.Command("solana", "program", "extend", "--keypair", upgradeAuthorityKey, programID, strconv.Itoa(additionalSpace))

Check failure on line 275 in deployment/solana_chain.go

View workflow job for this annotation

GitHub Actions / GolangCI Lint (deployment)

G204: Subprocess launched with a potential tainted input or cmd arguments (gosec)
return cmd.Run()
}

// writeBuffer writes the new program binary to a buffer account and returns the buffer address
func writeBuffer(newProgramBinary, upgradeAuthorityKey string) (string, error) {
cmd := exec.Command("solana", "program", "write-buffer", newProgramBinary, "--keypair", upgradeAuthorityKey)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}

// Parse the output to extract the buffer address
output := out.String()
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "Buffer:") {
parts := strings.Split(line, ":")
if len(parts) < 2 {
return "", fmt.Errorf("invalid buffer address line: %s", line)
}
bufferAddress := strings.TrimSpace(parts[1])
return bufferAddress, nil
}
}
return "", fmt.Errorf("buffer address not found in output")

Check failure on line 302 in deployment/solana_chain.go

View workflow job for this annotation

GitHub Actions / GolangCI Lint (deployment)

fmt.Errorf can be replaced with errors.New (perfsprint)
}

// upgradeProgram upgrades the program using the buffer account
func upgradeProgram(programID, bufferAddress, upgradeAuthorityKey string) (string, error) {
cmd := exec.Command("solana", "program", "upgrade", "--keypair", upgradeAuthorityKey, programID, bufferAddress)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}

// Parse the output to find the new program ID
output := out.String()
fmt.Println(output)
return parseProgramID(output)
}

// closeBuffer closes the buffer account and reclaims the SOL
func closeBuffer(bufferAddress, upgradeAuthorityKey string) error {
cmd := exec.Command("solana", "program", "close", "--keypair", upgradeAuthorityKey, "--buffers", bufferAddress)
return cmd.Run()
}
Loading