Skip to content

Commit

Permalink
feat: add pda finders and contract address parser to the Solana SDK (#…
Browse files Browse the repository at this point in the history
…225)

Add a few helpers to the Solana SDK:
* PDA finders, similar to the ones found in the chainlink-ccip
repository
* A pair of functions to encode and decode contract addresses (from
`string` to `program_id + seed` and vice-versa)

<!-- DON'T DELETE. add your comments above llm generated contents -->
  • Loading branch information
gustavogama-cll authored Jan 8, 2025
1 parent 4adb968 commit 7c9cd3d
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-buses-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": patch
---

Add PDA finders and ContractAddress parser to the Solana SDK
2 changes: 1 addition & 1 deletion e2e/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private_keys = [
]

[solana_config]
chain_id = "1"
chain_id = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG"
port = "8999"
type = "solana"
public_key ="9n1pyVGGo6V4mpiSDMVay5As9NurEkY283wwRk1Kto2C"
Expand Down
75 changes: 47 additions & 28 deletions e2e/tests/solana_inspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,96 @@
package e2e

import (
"context"
"time"

bin "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config"
cselectors "github.com/smartcontractkit/chain-selectors"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/testutils"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
bindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/mcms"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

solanasdk "github.com/smartcontractkit/mcms/sdk/solana"
"github.com/smartcontractkit/mcms/types"
)

type SolanaInspectionTestSuite struct {
suite.Suite
TestSetup

ChainSelector types.ChainSelector
MCMProgramID solana.PublicKey
}

// this key matches the public key in the config.toml so it gets funded by the genesis block
var privateKey = "DmPfeHBC8Brf8s5qQXi25bmJ996v6BHRtaLc6AH51yFGSqQpUMy1oHkbbXobPNBdgGH2F29PAmoq9ZZua4K9vCc"
var (
// this key matches the public key in the config.toml so it gets funded by the genesis block
privateKey = "DmPfeHBC8Brf8s5qQXi25bmJ996v6BHRtaLc6AH51yFGSqQpUMy1oHkbbXobPNBdgGH2F29PAmoq9ZZua4K9vCc"
testPDASeed = [32]byte{'t', 'e', 's', 't', '-', 'm', 'c', 'm'}
)

// SetupSuite runs before the test suite
func (s *SolanaInspectionTestSuite) SetupSuite() {
s.TestSetup = *InitializeSharedTestSetup(s.T())
s.MCMProgramID = solana.MustPublicKeyFromBase58(s.SolanaChain.SolanaPrograms["mcm"])

details, err := cselectors.GetChainDetailsByChainIDAndFamily(s.SolanaChain.ChainID, cselectors.FamilySolana)
s.Require().NoError(err)
s.ChainSelector = types.ChainSelector(details.ChainSelector)
}

func (s *SolanaInspectionTestSuite) TestSetupMCM() {
mcm.SetProgramID(config.McmProgram)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
s.T().Cleanup(cancel)

wallet, err := solana.PrivateKeyFromBase58(privateKey)
require.NoError(s.T(), err)
bindings.SetProgramID(s.MCMProgramID)

ctx := tests.Context(s.T())
wallet, err := solana.PrivateKeyFromBase58(privateKey)
s.Require().NoError(err)

seed := config.TestMsigNamePaddedBuffer
multisigConfigPDA := mcms.McmConfigAddress(seed)
rootMetadataPDA := mcms.RootMetadataAddress(seed)
expiringRootAndOpCountPDA := mcms.ExpiringRootAndOpCountAddress(seed)
configPDA, err := solanasdk.FindConfigPDA(s.MCMProgramID, testPDASeed)
s.Require().NoError(err)
rootMetadataPDA, err := solanasdk.FindRootMetadataPDA(s.MCMProgramID, testPDASeed)
s.Require().NoError(err)
expiringRootAndOpCountPDA, err := solanasdk.FindExpiringRootAndOpCountPDA(s.MCMProgramID, testPDASeed)
s.Require().NoError(err)

// get program data account
data, accErr := s.SolanaClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{
Commitment: config.DefaultCommitment,
data, err := s.SolanaClient.GetAccountInfoWithOpts(ctx, s.MCMProgramID, &rpc.GetAccountInfoOpts{
Commitment: rpc.CommitmentConfirmed,
})
require.NoError(s.T(), accErr)
s.Require().NoError(err)

// decode program data
var programData struct {
DataType uint32
Address solana.PublicKey
}
require.NoError(s.T(), bin.UnmarshalBorsh(&programData, data.Bytes()))
err = bin.UnmarshalBorsh(&programData, data.Bytes())
s.Require().NoError(err)

ix, err := mcm.NewInitializeInstruction(
config.TestChainID,
seed,
multisigConfigPDA,
ix, err := bindings.NewInitializeInstruction(
uint64(s.ChainSelector),
testPDASeed,
configPDA,
wallet.PublicKey(),
solana.SystemProgramID,
config.McmProgram,
s.MCMProgramID,
programData.Address,
rootMetadataPDA,
expiringRootAndOpCountPDA,
).ValidateAndBuild()
require.NoError(s.T(), err)
testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, []solana.Instruction{ix}, wallet, config.DefaultCommitment)

testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, []solana.Instruction{ix}, wallet, rpc.CommitmentConfirmed)

// get config and validate
var configAccount mcm.MultisigConfig
err = common.GetAccountDataBorshInto(ctx, s.SolanaClient, multisigConfigPDA, config.DefaultCommitment, &configAccount)
var configAccount bindings.MultisigConfig
err = common.GetAccountDataBorshInto(ctx, s.SolanaClient, configPDA, rpc.CommitmentConfirmed, &configAccount)
require.NoError(s.T(), err, "failed to get account data")

require.Equal(s.T(), config.TestChainID, configAccount.ChainId)
require.Equal(s.T(), uint64(s.ChainSelector), configAccount.ChainId)
require.Equal(s.T(), wallet.PublicKey(), configAccount.Owner)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ require (
github.com/gagliardetto/binary v0.8.0
github.com/gagliardetto/solana-go v1.12.0
github.com/go-playground/validator/v10 v10.23.0
github.com/google/go-cmp v0.6.0
github.com/joho/godotenv v1.5.1
github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52
github.com/smartcontractkit/chain-selectors v1.0.35
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20241220212237-609f7b0c9734
github.com/smartcontractkit/chainlink-common v0.3.0
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.1
github.com/spf13/cast v1.7.1
github.com/stretchr/testify v1.10.0
Expand Down Expand Up @@ -87,7 +87,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/rpc v1.2.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
Expand Down Expand Up @@ -145,6 +144,7 @@ require (
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartcontractkit/chainlink-common v0.3.0 // indirect
github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 // indirect
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
github.com/stretchr/objx v0.5.2 // indirect
Expand Down
65 changes: 65 additions & 0 deletions sdk/solana/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package solana

import (
"encoding/binary"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go"
)

func FindSignerPDA(programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("multisig_signer"), pdaSeed[:]}
return findPDA(programID, seeds)
}

func FindConfigPDA(programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("multisig_config"), pdaSeed[:]}
return findPDA(programID, seeds)
}

func FindConfigSignersPDA(programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("multisig_config_signers"), pdaSeed[:]}
return findPDA(programID, seeds)
}

func FindRootMetadataPDA(programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("root_metadata"), pdaSeed[:]}
return findPDA(programID, seeds)
}

func FindExpiringRootAndOpCountPDA(programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("expiring_root_and_op_count"), pdaSeed[:]}
return findPDA(programID, seeds)
}

func FindRootSignaturesPDA(
programID solana.PublicKey, pdaSeed PDASeed, root common.Hash, validUntil uint32,
) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("root_signatures"), pdaSeed[:], root[:], validUntilBytes(validUntil)}
return findPDA(programID, seeds)
}

func FindSeenSignedHashesPDA(
programID solana.PublicKey, pdaSeed PDASeed, root common.Hash, validUntil uint32,
) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("seen_signed_hashes"), pdaSeed[:], root[:], validUntilBytes(validUntil)}
return findPDA(programID, seeds)
}

func findPDA(programID solana.PublicKey, seeds [][]byte) (solana.PublicKey, error) {
pda, _, err := solana.FindProgramAddress(seeds, programID)
if err != nil {
return solana.PublicKey{}, fmt.Errorf("unable to find %s pda: %w", string(seeds[0]), err)
}

return pda, nil
}

func validUntilBytes(validUntil uint32) []byte {
const uint32Size = 4
vuBytes := make([]byte, uint32Size)
binary.LittleEndian.PutUint32(vuBytes, validUntil)

return vuBytes
}
65 changes: 65 additions & 0 deletions sdk/solana/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package solana

import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

var (
testProgramID = solana.MustPublicKeyFromBase58("6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX")
testPDASeed = PDASeed{'t', 'e', 's', 't', '-', 'm', 'c', 'm'}
testRoot = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")
)

func Test_FindSignerPDA(t *testing.T) {
t.Parallel()
pda, err := FindSignerPDA(testProgramID, testPDASeed)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("62gDM6BRLf2w1yXfmpePUTsuvbeBbu4QqdjV32wcc4UG")))
}

func Test_FindConfigPDA(t *testing.T) {
t.Parallel()
pda, err := FindConfigPDA(testProgramID, testPDASeed)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("CiPYshUKNDV9i4p4MLaqXSRqYWtnMtW6b1aYjh4Lw9nP")))
}

func Test_getConfigSignersPDA(t *testing.T) {
t.Parallel()
pda, err := FindConfigSignersPDA(testProgramID, testPDASeed)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("EZJdMB7TCRcSTP6KMp1HPnzwNtW6wvqKXHLAZh1Jn81w")))
}

func Test_FindRootMetadataPDA(t *testing.T) {
t.Parallel()
pda, err := FindRootMetadataPDA(testProgramID, testPDASeed)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("H45XH8Z1zpcLUHLLQzUwEgB1s3mZQcRvCYfcHriRcMxN")))
}

func Test_FindExpiringRootAndOpCountPDA(t *testing.T) {
t.Parallel()
pda, err := FindExpiringRootAndOpCountPDA(testProgramID, testPDASeed)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("7nh2qGybwNRxL3zKpiSUzk2yc9CjCb5MhrB61B98hYZu")))
}

func Test_FindRootSignaturesPDA(t *testing.T) {
t.Parallel()
pda, err := FindRootSignaturesPDA(testProgramID, testPDASeed, testRoot, 1735689600)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("528jBx5Mn1EPt4vG47CRkr1zhj8QVfSMvfvBZksZdrHr")))
}

func Test_FindSeenSignedHashesPDA(t *testing.T) {
t.Parallel()
pda, err := FindSeenSignedHashesPDA(testProgramID, testPDASeed, testRoot, 1735689600)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("FxPYSHG9tm35T43zpAuVDdNY8uMPQfaaVBftxVrLyXVq")))
}
40 changes: 40 additions & 0 deletions sdk/solana/contract_address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package solana

import (
"bytes"
"fmt"
"strings"

"github.com/gagliardetto/solana-go"
)

type PDASeed [32]byte

// ContractAddress returns a string representation of a solana contract id
// which is a combination of the program id and the seed <PROGRAM_ID>.<SEED>
func ContractAddress(programID solana.PublicKey, pdaSeed PDASeed) string {
return fmt.Sprintf("%s.%s", programID.String(), bytes.Trim(pdaSeed[:], "\x00"))
}

func ParseContractAddress(address string) (solana.PublicKey, PDASeed, error) {
const numParts = 2
parts := strings.SplitN(address, ".", numParts)
if len(parts) != numParts {
return solana.PublicKey{}, PDASeed{}, fmt.Errorf("invalid solana contract address format: %q", address)
}

programID, err := solana.PublicKeyFromBase58(parts[0])
if err != nil {
return solana.PublicKey{}, PDASeed{}, fmt.Errorf("unable to parse solana program id: %w", err)
}

allSeedBytes := []byte(parts[1])
if len(allSeedBytes) > len(PDASeed{}) {
return solana.PublicKey{}, PDASeed{}, fmt.Errorf("pda seed is too long (max %d bytes)", len(PDASeed{}))
}

var pdaSeed PDASeed
copy(pdaSeed[:], []byte(parts[1])[:])

return programID, pdaSeed, nil
}
Loading

0 comments on commit 7c9cd3d

Please sign in to comment.