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
26 changes: 0 additions & 26 deletions legacy/pkg/family/solana/testutils/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,8 @@ package soltestutils
import (
"os"
"path/filepath"
"sync"
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/solutils"
)

var (
onceCCIP = &sync.Once{}
)

type downloadFunc func(t *testing.T) string

// downloadChainlinkCCIPProgramArtifacts downloads CCIP Solana artifacts (includes MCMS programs).
func downloadChainlinkCCIPProgramArtifacts(t *testing.T) string {
t.Helper()

cachePath := programsCacheDir()

onceCCIP.Do(func() {
err := solutils.DownloadChainlinkCCIPProgramArtifacts(t.Context(), cachePath, "", nil)
require.NoError(t, err)
})

return cachePath
}

// programsCacheDir returns where to store downloaded .so files. Leaf dir is solana_programs
// (under UserCacheDir/TempDir, so "cache" is implied; avoids read-only pkg/mod paths).
func programsCacheDir() string {
Expand Down
113 changes: 59 additions & 54 deletions legacy/pkg/family/solana/testutils/preload.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package soltestutils

import (
"io"
"os"
"maps"
"path/filepath"
"sync"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -13,71 +13,76 @@ import (
"github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/solutils"
)

// LoadMCMSPrograms loads the MCMS program artifacts into the given directory.
//
// Returns the path to the temporary test directory and a map of program names to IDs.
func LoadMCMSPrograms(t *testing.T, dir string) (string, map[string]string) {
t.Helper()
// solTestExclusive serializes top-level Solana integration tests that mutate global
// gobinding program IDs via SetProgramID. solTestDepth allows nested subtests in
// the same test to re-enter without deadlocking.
var (
solTestExclusive sync.Mutex
solTestCountMu sync.Mutex
solTestDepth int
)

progIDs := loadProgramArtifacts(t,
solutils.MCMSProgramNames, downloadChainlinkCCIPProgramArtifacts, dir,
)
func acquireSolanaTestIsolation(t *testing.T) {

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.

I confess I don't get how this lock works...

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.

here's how I'm reading it: it allows the same t instance to call PreloadMCMS multiple times, without locking. Any other calls will wait. Is that the idea? How often do we call PreloadMCMS this way?

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.

It's more a process wide lock the idea is to mainly serialize the calls to SetProgramID which is what bites us when running these tests with t.Parallel()

Should work something like this:

  1. First, PreloadMCMS() call while solTestDepth == 0 acquires solTestExclusive.
  2. Then Every PreloadMCMS call increments depth and registers a t.Cleanup on that test’s t.
  3. Nested calls see depth > 0, skip re-locking, and only bump depth to deadlock.
  4. Unlock happens only when the outermost cleanup runs (depth back to 0), i.e. when the whole nested stack of tests that called PreloadMCMS has finished.

t.Helper()

return dir, progIDs
solTestCountMu.Lock()
if solTestDepth == 0 {
solTestExclusive.Lock()
}
solTestDepth++
solTestCountMu.Unlock()

t.Cleanup(func() {
solTestCountMu.Lock()
solTestDepth--
release := solTestDepth == 0
solTestCountMu.Unlock()
if release {
solTestExclusive.Unlock()
}
})
}

// PreloadMCMS provides a convenience function to preload the MCMS program artifacts and address
// book for a given selector.
func PreloadMCMS(t *testing.T, selector uint64) (string, map[string]string, *cldf.AddressBookMap) {
var (
mcmsProgramsOnce sync.Once
mcmsProgramsPath string
mcmsProgramIDs map[string]string
)

// sharedMCMSPrograms downloads MCMS Solana program artifacts once per test process
// and returns the shared cache directory plus program IDs.
func sharedMCMSPrograms(t *testing.T) (string, map[string]string) {
t.Helper()

dir := t.TempDir()
mcmsProgramsOnce.Do(func() {
mcmsProgramsPath = programsCacheDir()
err := solutils.DownloadChainlinkCCIPProgramArtifacts(t.Context(), mcmsProgramsPath, "", nil)
require.NoError(t, err)

_, programIDs := LoadMCMSPrograms(t, dir)
mcmsProgramIDs = make(map[string]string, len(solutils.MCMSProgramNames))
for _, name := range solutils.MCMSProgramNames {
id := solutils.GetProgramID(name)
require.NotEmpty(t, id, "program id not found for program name: %s", name)
require.FileExists(t, filepath.Join(mcmsProgramsPath, name+".so"))
mcmsProgramIDs[name] = id
}
})

ab := PreloadAddressBookWithMCMSPrograms(t, selector)
programIDs := make(map[string]string, len(mcmsProgramIDs))
maps.Copy(programIDs, mcmsProgramIDs)

return dir, programIDs, ab
return mcmsProgramsPath, programIDs
}

// loadProgramArtifacts is a helper function that loads program artifacts into a temporary test directory.
// It downloads artifacts using the provided download function and copies the specified programs.
//
// Returns the map of program names to IDs.
func loadProgramArtifacts(t *testing.T, programNames []string, downloadFn downloadFunc, targetDir string) map[string]string {
// PreloadMCMS provides a convenience function to preload the MCMS program artifacts and address
// book for a given selector.
func PreloadMCMS(t *testing.T, selector uint64) (string, map[string]string, *cldf.AddressBookMap) {
t.Helper()

// Download the program artifacts using the provided download function
cachePath := downloadFn(t)

progIDs := make(map[string]string, len(programNames))

// Copy the specific artifacts to the target directory and add the program ID to the map
for _, name := range programNames {
id := solutils.GetProgramID(name)
require.NotEmpty(t, id, "program id not found for program name: %s", name)

src := filepath.Join(cachePath, name+".so")
dst := filepath.Join(targetDir, name+".so")
acquireSolanaTestIsolation(t)

func() {
srcFile, err := os.Open(src)
require.NoError(t, err)
defer srcFile.Close()

dstFile, err := os.Create(dst)
require.NoError(t, err)
defer dstFile.Close()

_, err = io.Copy(dstFile, srcFile)
require.NoError(t, err)
}()

// Add the program ID to the map
progIDs[name] = id
t.Logf("copied solana program %s to %s", name, dst)
}
programsPath, programIDs := sharedMCMSPrograms(t)
ab := PreloadAddressBookWithMCMSPrograms(t, selector)

// Return the path to the cached artifacts and the map of program IDs
return progIDs
return programsPath, programIDs, ab
}
7 changes: 4 additions & 3 deletions mcms/changesets/set-config/changeset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,11 @@ func TestChangeset_VerifyPreconditions(t *testing.T) {
}
}

//nolint:paralleltest // global mcm.SetProgramID state and shared Solana CTF container setup
func TestChangeset_VerifyPreconditions_Solana(t *testing.T) {
t.Parallel()

selector := chain_selectors.TEST_22222222222222222222222222222222222222222222.Selector
rt := newSolanaRuntimeWithDeploy(t, selector)
env := rt.Environment()
env := newSolanaVerifyPreconditionsEnv(t, selector)

validCfg := cldftesthelpers.SingleGroupMCMS(t)
validTargets := mcmsTargets(selector, validCfg, validCfg, validCfg)
Expand All @@ -200,6 +200,7 @@ func TestChangeset_VerifyPreconditions_Solana(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.NoError(t, cs.VerifyPreconditions(env, tt.input))
})
}
Expand Down
40 changes: 40 additions & 0 deletions mcms/changesets/set-config/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package setconfig_test

import (
"context"
"crypto/ecdsa"
"testing"
"time"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
cldfsol "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
Expand Down Expand Up @@ -146,6 +150,42 @@ func evmMCMSChainState(t *testing.T, rt *runtime.Runtime, selector uint64) (*evm
return mcmsState, chain
}

// newSolanaVerifyPreconditionsEnv builds a mock Solana environment for VerifyPreconditions
// only — no CTF container or on-chain deploy.
func newSolanaVerifyPreconditionsEnv(t *testing.T, selector uint64) cldf.Environment {
t.Helper()

ds := cldfdatastore.NewMemoryDataStore()
version := semver.MustParse("1.0.0")
for _, ref := range []struct {
contractType cldf.ContractType
address string
}{
{mcmscontracts.RBACTimelock, "timelock-address"},
{mcmscontracts.ProposerManyChainMultisig, "proposer-address"},
{mcmscontracts.CancellerManyChainMultisig, "canceller-address"},
{mcmscontracts.BypasserManyChainMultisig, "bypasser-address"},
} {
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
Address: ref.address,
ChainSelector: selector,
Type: cldfdatastore.ContractType(ref.contractType),
Version: version,
}))
}

return cldf.Environment{
Logger: logger.Test(t),
DataStore: ds.Seal(),
GetContext: func() context.Context {
return t.Context()
},
BlockChains: cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
selector: cldfsol.Chain{Selector: selector},
}),
}
}

func newSolanaRuntimeWithDeploy(t *testing.T, selector uint64) *runtime.Runtime {
t.Helper()

Expand Down
18 changes: 10 additions & 8 deletions mcms/solana/changesets/fund-mcm-pdas/changeset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestChangeset(t *testing.T) {
selector2 := chainselectors.TEST_33333333333333333333333333333333333333333333.Selector

rt1 := testRuntime(t, selector1)
env1 := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000), true)
env1 := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000))
cs := Changeset{}

t.Run("VerifyPreconditions", func(t *testing.T) {
Expand Down Expand Up @@ -92,7 +92,7 @@ func TestChangeset(t *testing.T) {
},
{
name: "insufficient deployer balance",
env: configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1), true),
env: configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1)),
config: Config{
FundingPerChain: map[uint64]FundingConfig{selector1: {
ProposeMCM: 100,
Expand All @@ -106,7 +106,7 @@ func TestChangeset(t *testing.T) {
{
name: "missing deployer key",
env: func() cldf.Environment {
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000), true)
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000))
chain := env.BlockChains.SolanaChains()[selector1]
chain.DeployerKey = nil
env.BlockChains = cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{selector1: chain})
Expand Down Expand Up @@ -138,8 +138,11 @@ func TestChangeset(t *testing.T) {
}

t.Run("mcms contracts not deployed", func(t *testing.T) {
rt2 := testRuntime(t, selector2)
env := configureFundMCMSignersEnv(t, rt2.Environment(), selector2, rpcWithBalance(t, 1_000), false)
chain := rt1.Environment().BlockChains.SolanaChains()[selector1]
chain.Client = rpcWithBalance(t, 1_000)
env := cldf.Environment{DataStore: newMCMSDataStore(t, selector2, false)}
env.BlockChains = cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{selector2: chain})

err := cs.VerifyPreconditions(env, Config{
FundingPerChain: map[uint64]FundingConfig{selector2: {
ProposeMCM: 100,
Expand All @@ -155,7 +158,7 @@ func TestChangeset(t *testing.T) {
t.Run("Apply", func(t *testing.T) {
var confirmed [][]solana.Instruction

env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, nil, true)
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, nil)
chain := env.BlockChains.SolanaChains()[selector1]
require.NotNil(t, chain.DeployerKey)
deployerKey := *chain.DeployerKey
Expand Down Expand Up @@ -246,12 +249,11 @@ func configureFundMCMSignersEnv(
base cldf.Environment,
selector uint64,
client *rpc.Client,
completeState bool,
) cldf.Environment {
t.Helper()

env := base
env.DataStore = newMCMSDataStore(t, selector, completeState)
env.DataStore = newMCMSDataStore(t, selector, true)

chain := env.BlockChains.SolanaChains()[selector]
if client != nil {
Expand Down
Loading
Loading