Skip to content

Commit

Permalink
feat: add solana timelock execute to the SDK [DPA-1467] (#248)
Browse files Browse the repository at this point in the history
Adds the timelock  `ExecuteBatchInstruction` to the solana SDK

---------

Co-authored-by: Graham Goh <[email protected]>
  • Loading branch information
ecPablo and graham-chainlink authored Jan 23, 2025
1 parent 7a5944e commit e153c75
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 124 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-carrots-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

Timelock execute batch on solana SDK.
44 changes: 19 additions & 25 deletions e2e/tests/solana/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ type SolanaTestSuite struct {

ChainSelector types.ChainSelector
MCMProgramID solana.PublicKey
TimelockWorkerProgramID solana.PublicKey
TimelockProgramID solana.PublicKey
AccessControllerProgramID solana.PublicKey
Roles timelockutils.RoleMap
}
Expand Down Expand Up @@ -153,11 +153,11 @@ func (s *SolanaTestSuite) SetupMCM(pdaSeed [32]byte) {
s.Require().Equal(wallet.PublicKey(), configAccount.Owner)
}

func (s *SolanaTestSuite) SetupTimelockWorker(pdaSeed [32]byte, minDelay time.Duration) {
func (s *SolanaTestSuite) SetupTimelock(pdaSeed [32]byte, minDelay time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
s.T().Cleanup(cancel)

timelock.SetProgramID(s.TimelockWorkerProgramID)
timelock.SetProgramID(s.TimelockProgramID)
access_controller.SetProgramID(s.AccessControllerProgramID)

admin, err := solana.PrivateKeyFromBase58(privateKey)
Expand All @@ -181,7 +181,7 @@ func (s *SolanaTestSuite) SetupTimelockWorker(pdaSeed [32]byte, minDelay time.Du
})

s.Run("init timelock", func() {
data, accErr := s.SolanaClient.GetAccountInfoWithOpts(ctx, s.TimelockWorkerProgramID, &rpc.GetAccountInfoOpts{
data, accErr := s.SolanaClient.GetAccountInfoWithOpts(ctx, s.TimelockProgramID, &rpc.GetAccountInfoOpts{
Commitment: rpc.CommitmentConfirmed,
})
s.Require().NoError(accErr)
Expand All @@ -192,7 +192,7 @@ func (s *SolanaTestSuite) SetupTimelockWorker(pdaSeed [32]byte, minDelay time.Du
}
s.Require().NoError(bin.UnmarshalBorsh(&programData, data.Bytes()))

pda, err2 := solanasdk.FindTimelockConfigPDA(s.TimelockWorkerProgramID, pdaSeed)
pda, err2 := solanasdk.FindTimelockConfigPDA(s.TimelockProgramID, pdaSeed)
s.Require().NoError(err2)

initTimelockIx, err3 := timelock.NewInitializeInstruction(
Expand All @@ -201,7 +201,7 @@ func (s *SolanaTestSuite) SetupTimelockWorker(pdaSeed [32]byte, minDelay time.Du
pda,
admin.PublicKey(),
solana.SystemProgramID,
s.TimelockWorkerProgramID,
s.TimelockProgramID,
programData.Address,
s.AccessControllerProgramID,
roleMap[timelock.Proposer_Role].AccessController.PublicKey(),
Expand Down Expand Up @@ -255,7 +255,7 @@ func (s *SolanaTestSuite) SetupTimelockWorker(pdaSeed [32]byte, minDelay time.Du
func (s *SolanaTestSuite) SetupSuite() {
s.TestSetup = *e2e.InitializeSharedTestSetup(s.T())
s.MCMProgramID = solana.MustPublicKeyFromBase58(s.SolanaChain.SolanaPrograms["mcm"])
s.TimelockWorkerProgramID = solana.MustPublicKeyFromBase58(s.SolanaChain.SolanaPrograms["timelock"])
s.TimelockProgramID = solana.MustPublicKeyFromBase58(s.SolanaChain.SolanaPrograms["timelock"])
s.AccessControllerProgramID = solana.MustPublicKeyFromBase58(s.SolanaChain.SolanaPrograms["access_controller"])

details, err := cselectors.GetChainDetailsByChainIDAndFamily(s.SolanaChain.ChainID, cselectors.FamilySolana)
Expand Down Expand Up @@ -314,7 +314,7 @@ func (s *SolanaTestSuite) getBatchAddAccessIxs(ctx context.Context, timelockID [
end = len(addresses)
}
chunk := addresses[i:end]
pda, err := solanasdk.FindTimelockConfigPDA(s.TimelockWorkerProgramID, timelockID)
pda, err := solanasdk.FindTimelockConfigPDA(s.TimelockProgramID, timelockID)
s.Require().NoError(err)

ix := timelock.NewBatchAddAccessInstruction(
Expand All @@ -337,22 +337,17 @@ func (s *SolanaTestSuite) getBatchAddAccessIxs(ctx context.Context, timelockID [
return ixs
}

func (s *SolanaTestSuite) initOperation(ctx context.Context, op timelockutils.Operation, timelockID [32]byte) {
admin, err := solana.PrivateKeyFromBase58(privateKey)
s.Require().NoError(err)

ixs := s.getInitOperationIxs(timelockID, op, admin.PublicKey())
tx := testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, ixs, admin, rpc.CommitmentConfirmed)
func (s *SolanaTestSuite) initOperation(ctx context.Context, op timelockutils.Operation, timelockID [32]byte, auth solana.PrivateKey) {
ixs := s.getInitOperationIxs(timelockID, op, auth.PublicKey())
tx := testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, ixs, auth, rpc.CommitmentConfirmed)
s.Require().NotNil(tx)
}

func (s *SolanaTestSuite) getInitOperationIxs(timelockID [32]byte, op timelockutils.Operation, authority solana.PublicKey) []solana.Instruction {
configPDA, err := solanasdk.FindTimelockConfigPDA(s.TimelockWorkerProgramID, timelockID)
configPDA, err := solanasdk.FindTimelockConfigPDA(s.TimelockProgramID, timelockID)
s.Require().NoError(err)

operationPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockWorkerProgramID, timelockID, op.OperationID())
operationPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockProgramID, timelockID, op.OperationID())
s.Require().NoError(err)

ixs := []solana.Instruction{}
initOpIx, ioErr := timelock.NewInitializeOperationInstruction(
timelockID,
Expand Down Expand Up @@ -383,7 +378,6 @@ func (s *SolanaTestSuite) getInitOperationIxs(timelockID [32]byte, op timelockut

ixs = append(ixs, appendIxsIx)
}

finOpIx, foErr := timelock.NewFinalizeOperationInstruction(
timelockID,
op.OperationID(),
Expand All @@ -399,10 +393,10 @@ func (s *SolanaTestSuite) getInitOperationIxs(timelockID [32]byte, op timelockut
}

func (s *SolanaTestSuite) scheduleOperation(ctx context.Context, timelockID [32]byte, delay time.Duration, opID [32]byte) {
operationPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockWorkerProgramID, timelockID, opID)
operationPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockProgramID, timelockID, opID)
s.Require().NoError(err)

configPDA, err := solanasdk.FindTimelockConfigPDA(s.TimelockWorkerProgramID, timelockID)
configPDA, err := solanasdk.FindTimelockConfigPDA(s.TimelockProgramID, timelockID)
s.Require().NoError(err)

admin, err := solana.PrivateKeyFromBase58(privateKey)
Expand All @@ -424,13 +418,13 @@ func (s *SolanaTestSuite) scheduleOperation(ctx context.Context, timelockID [32]
}

func (s *SolanaTestSuite) executeOperation(ctx context.Context, timelockID [32]byte, opID [32]byte) {
operationPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockWorkerProgramID, timelockID, opID)
operationPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockProgramID, timelockID, opID)
s.Require().NoError(err)

configPDA, err := solanasdk.FindTimelockConfigPDA(s.TimelockWorkerProgramID, timelockID)
configPDA, err := solanasdk.FindTimelockConfigPDA(s.TimelockProgramID, timelockID)
s.Require().NoError(err)

signerPDA, err := solanasdk.FindTimelockSignerPDA(s.TimelockWorkerProgramID, timelockID)
signerPDA, err := solanasdk.FindTimelockSignerPDA(s.TimelockProgramID, timelockID)
s.Require().NoError(err)

admin, err := solana.PrivateKeyFromBase58(privateKey)
Expand Down Expand Up @@ -459,7 +453,7 @@ func (s *SolanaTestSuite) waitForOperationToBeReady(ctx context.Context, timeloc
const pollInterval = 500 * time.Millisecond
const timeBuffer = 2 * time.Second

opPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockWorkerProgramID, timelockID, opID)
opPDA, err := solanasdk.FindTimelockOperationPDA(s.TimelockProgramID, timelockID, opID)
s.Require().NoError(err)

var opAccount timelock.Operation
Expand Down
241 changes: 241 additions & 0 deletions e2e/tests/solana/timelock_execution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//go:build e2e
// +build e2e

package solanae2e

import (
"context"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/token"
"github.com/gagliardetto/solana-go/rpc"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/testutils"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/access_controller"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock"
timelockutils "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/timelock"

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

var testTimelockExecuteID = [32]byte{'t', 'e', 's', 't', '-', 'e', 'x', 'e', 'c', 't', 'i', 'm', 'e', 'l', 'o', 'c', 'k'}

const BatchAddAccessChunkSize = 24

// Test_Solana_TimelockExecute tests the timelock Execute functionality by scheduling a mint tokens transaction and
// executing it via the timelock ExecuteBatch
func (s *SolanaTestSuite) Test_Solana_TimelockExecute() {
s.SetupTimelock(testTimelockExecuteID, 1)
// Get required programs and accounts
ctx := context.Background()
timelock.SetProgramID(s.TimelockProgramID)
access_controller.SetProgramID(s.AccessControllerProgramID)

// Fund the auth private key
auth, err := solana.PrivateKeyFromBase58(privateKey)
s.Require().NoError(err)

// Setup SPL token for testing a mint via timelock
mintKeypair, err := solana.NewRandomPrivateKey()
s.Require().NoError(err)
mint := mintKeypair.PublicKey()
// set up the token program
signerPDA, err := mcmsSolana.FindTimelockSignerPDA(s.TimelockProgramID, testTimelockExecuteID)
s.Require().NoError(err)
receiverATA := s.setupTokenProgram(ctx, auth, signerPDA, mintKeypair)

// Get receiverATA initial balance
initialBalance, err := s.SolanaClient.GetTokenAccountBalance(
context.Background(),
receiverATA, // The associated token account address
rpc.CommitmentProcessed,
)
s.Require().NoError(err)

// Set propose roles
proposerAndExecutorKey := s.setProposerAndExecutor(ctx, auth, s.Roles)
s.Require().NotNil(proposerAndExecutorKey)

// Schedule the mint tx
var predecessor [32]byte
salt := [32]byte{123}
mintIx, operationID := s.scheduleMintTx(ctx,
mint,
receiverATA,
s.Roles[timelock.Proposer_Role].AccessController.PublicKey(),
signerPDA,
*proposerAndExecutorKey,
predecessor,
salt)

// --- act: call Timelock Execute ---
executor := mcmsSolana.NewTimelockExecutor(s.SolanaClient, *proposerAndExecutorKey)
contractID := mcmsSolana.ContractAddress(s.TimelockProgramID, testTimelockExecuteID)
ixData, err := mintIx.Data()
s.Require().NoError(err)
accounts := mintIx.Accounts()
accounts = append(accounts, &solana.AccountMeta{PublicKey: solana.Token2022ProgramID, IsSigner: false, IsWritable: false})
solanaTx, err := mcmsSolana.NewTransaction(solana.Token2022ProgramID.String(), ixData, accounts, "Token", []string{})
s.Require().NoError(err)
batchOp := types.BatchOperation{
Transactions: []types.Transaction{solanaTx},
ChainSelector: s.ChainSelector,
}
// --- Wait for the operation to be ready ---
s.waitForOperationToBeReady(ctx, testTimelockExecuteID, operationID)
signature, err := executor.Execute(ctx, batchOp, contractID, predecessor, salt)
s.Require().NoError(err)
s.Require().NotEqual(signature, "")

// --- assert balances
finalBalance, err := s.SolanaClient.GetTokenAccountBalance(
ctx,
receiverATA,
rpc.CommitmentProcessed,
)
s.Require().NoError(err)

// final balance should be 1000000000000 more units
s.Require().Equal(initialBalance.Value.Amount, "0")
s.Require().Equal(finalBalance.Value.Amount, "1000000000000")
}

// setProposerAndExecutor sets the proposer for the timelock
func (s *SolanaTestSuite) setProposerAndExecutor(ctx context.Context, auth solana.PrivateKey, roleMap timelockutils.RoleMap) *solana.PrivateKey {
proposerAndExecutorKey := solana.NewWallet()
testutils.FundAccounts(ctx, []solana.PrivateKey{proposerAndExecutorKey.PrivateKey}, s.SolanaClient, s.T())

// Add proposers to the timelock program
batchAddAccessIxs, err := timelockutils.GetBatchAddAccessIxs(
ctx,
testTimelockExecuteID,
roleMap[timelock.Proposer_Role].AccessController.PublicKey(),
timelock.Proposer_Role,
[]solana.PublicKey{proposerAndExecutorKey.PublicKey()},
auth,
BatchAddAccessChunkSize,
s.SolanaClient)
s.Require().NoError(err)
for _, ix := range batchAddAccessIxs {
testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, []solana.Instruction{ix}, auth, rpc.CommitmentConfirmed)
}

// Add executor to the timelock program
batchAddAccessIxs, err = timelockutils.GetBatchAddAccessIxs(
ctx,
testTimelockExecuteID,
roleMap[timelock.Executor_Role].AccessController.PublicKey(),
timelock.Executor_Role,
[]solana.PublicKey{proposerAndExecutorKey.PublicKey()},
auth,
BatchAddAccessChunkSize,
s.SolanaClient)
s.Require().NoError(err)
for _, ix := range batchAddAccessIxs {
testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, []solana.Instruction{ix}, auth, rpc.CommitmentConfirmed)
}

return &proposerAndExecutorKey.PrivateKey
}

// scheduleMintTx schedules a MintTx on the timelock
func (s *SolanaTestSuite) scheduleMintTx(
ctx context.Context,
mint,
receiverATA, // The account that will receive the mint funds.
roleAccessController solana.PublicKey, // Roles checker PDA account for checking roles

// The account that will sign the transaction during timelock execution.
// Note that this is a different account to the auth set for the timelock schedule ix,
// this is because the timelock PDA signer account will sign the transaction during execution
// and not the deployer account.
authPublicKey solana.PublicKey,
auth solana.PrivateKey, // The account to sign the init, append schedule instructions.
predecessor, salt [32]byte) (instruction *token.Instruction, operationID [32]byte) {
amount := 1000 * solana.LAMPORTS_PER_SOL
mintIx, err := token.NewMintToInstruction(amount, mint, receiverATA, authPublicKey, nil).ValidateAndBuild()
s.Require().NoError(err)
for _, acc := range mintIx.Accounts() {
if acc.PublicKey == authPublicKey {
acc.IsSigner = false
}
}
s.Require().NoError(err)
// Get the operation ID
ixData, err := mintIx.Data()
s.Require().NoError(err)
accounts := make([]timelock.InstructionAccount, 0, len(mintIx.Accounts())+1)
for _, account := range mintIx.Accounts() {
accounts = append(accounts, timelock.InstructionAccount{
Pubkey: account.PublicKey,
IsSigner: account.IsSigner,
IsWritable: account.IsWritable,
})
}
accounts = append(accounts, timelock.InstructionAccount{
Pubkey: solana.Token2022ProgramID,
IsSigner: false,
IsWritable: false,
})
opInstructions := []timelock.InstructionData{{Data: ixData, ProgramId: solana.Token2022ProgramID, Accounts: accounts}}
operationID = mcmsSolana.HashOperation(opInstructions, predecessor, salt)
operationPDA, err := mcmsSolana.FindTimelockOperationPDA(s.TimelockProgramID, testTimelockExecuteID, operationID)
s.Require().NoError(err)
configPDA, err := mcmsSolana.FindTimelockConfigPDA(s.TimelockProgramID, testTimelockExecuteID)
s.Require().NoError(err)
// Preload and Init Operation
ixs := []solana.Instruction{}
initOpIx, err := timelock.NewInitializeOperationInstruction(
testTimelockExecuteID,
operationID,
predecessor,
salt,
uint32(len(opInstructions)),
operationPDA,
configPDA,
auth.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
s.Require().NoError(err)
ixs = append(ixs, initOpIx)
// Append the ix

for _, ix := range opInstructions {
appendIx, errAppend := timelock.NewAppendInstructionsInstruction(
testTimelockExecuteID,
operationID,
[]timelock.InstructionData{ix}, // this should be a slice of instruction within 1232 bytes
operationPDA,
configPDA,
auth.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
s.Require().NoError(errAppend)
ixs = append(ixs, appendIx)
}
// Finalize Operation
finOpIx, err := timelock.NewFinalizeOperationInstruction(
testTimelockExecuteID,
operationID,
operationPDA,
configPDA,
auth.PublicKey(),
).ValidateAndBuild()
s.Require().NoError(err)
ixs = append(ixs, finOpIx)
testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, ixs, auth, rpc.CommitmentConfirmed)
// Schedule the operation
scheduleIx, err := timelock.NewScheduleBatchInstruction(
testTimelockExecuteID,
operationID,
1,
operationPDA,
configPDA,
roleAccessController,
auth.PublicKey(),
).ValidateAndBuild()
s.Require().NoError(err)
testutils.SendAndConfirm(ctx, s.T(), s.SolanaClient, []solana.Instruction{scheduleIx}, auth, rpc.CommitmentConfirmed)

return mintIx, operationID
}
Loading

0 comments on commit e153c75

Please sign in to comment.