From ec705a182e954f444cb3e20133eaf61c47f31cc6 Mon Sep 17 00:00:00 2001 From: Graham Goh Date: Mon, 3 Feb 2025 14:22:17 +1100 Subject: [PATCH] feat(solana): simulate executor operation mcms Simulate the operation of executor ExecuteOperation. Per discussion [here](https://chainlink-core.slack.com/archives/C07NVBK16KS/p1738296756500949?thread_ts=1738215917.353599&cid=C07NVBK16KS), we want to simulate the actual wrapped operation instead of the mcms execute operation. - Added e2e - Added unit tests JIRA: https://smartcontract-it.atlassian.net/browse/DPA-1418 --- e2e/tests/solana/simulator.go | 37 +++++++++++++ sdk/solana/simulator.go | 26 +++++++--- sdk/solana/simulator_test.go | 98 +++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/e2e/tests/solana/simulator.go b/e2e/tests/solana/simulator.go index 8ba132bd..4c10f653 100644 --- a/e2e/tests/solana/simulator.go +++ b/e2e/tests/solana/simulator.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" "github.com/smartcontractkit/mcms" "github.com/smartcontractkit/mcms/sdk" @@ -109,3 +110,39 @@ func (s *SolanaTestSuite) TestSimulator_SimulateSetRoot() { } s.Require().NoError(err) } + +func (s *SolanaTestSuite) TestSimulator_SimulateOperation() { + ctx := context.Background() + + recipientAddress, err := solana.NewRandomPrivateKey() + s.Require().NoError(err) + + auth, err := solana.PrivateKeyFromBase58(privateKey) + s.Require().NoError(err) + + encoder := solanasdk.NewEncoder(s.ChainSelector, 1, false) + executor := solanasdk.NewExecutor(encoder, s.SolanaClient, auth) + simulator := solanasdk.NewSimulator(executor) + + ix, err := system.NewTransferInstruction( + 1*solana.LAMPORTS_PER_SOL, + auth.PublicKey(), + recipientAddress.PublicKey()).ValidateAndBuild() + s.Require().NoError(err) + + ixData, err := ix.Data() + s.Require().NoError(err) + + tx, err := solanasdk.NewTransaction(solana.SystemProgramID.String(), ixData, nil, ix.Accounts(), "System", []string{}) + s.Require().NoError(err) + + op := types.Operation{ + Transaction: tx, + ChainSelector: s.ChainSelector, + } + metadata := types.ChainMetadata{ + MCMAddress: s.MCMProgramID.String(), + } + err = simulator.SimulateOperation(ctx, metadata, op) + s.Require().NoError(err) +} diff --git a/sdk/solana/simulator.go b/sdk/solana/simulator.go index bae37e66..6dd19a04 100644 --- a/sdk/solana/simulator.go +++ b/sdk/solana/simulator.go @@ -2,6 +2,8 @@ package solana import ( "context" + "encoding/json" + "errors" "fmt" "time" @@ -45,14 +47,26 @@ func (s *Simulator) SimulateSetRoot( func (s *Simulator) SimulateOperation( ctx context.Context, metadata types.ChainMetadata, operation types.Operation, ) error { - s.instructions = []solana.Instruction{} - nonce := uint32(0) - proof := []common.Hash{} - _, err := s.executor.ExecuteOperation(ctx, metadata, nonce, proof, operation) - if err != nil { - return err + var additionalFields AdditionalFields + if err := json.Unmarshal(operation.Transaction.AdditionalFields, &additionalFields); err != nil { + return fmt.Errorf("unable to unmarshal additional fields: %w", err) } + toProgramID, _, err := ParseContractAddress(operation.Transaction.To) + if errors.Is(err, ErrInvalidContractAddressFormat) { + var pkerr error + toProgramID, pkerr = solana.PublicKeyFromBase58(operation.Transaction.To) + if pkerr != nil { + return fmt.Errorf("unable to parse the 'To' address: %w", err) + } + } + + s.instructions = append(s.instructions, solana.NewInstruction( + toProgramID, + additionalFields.Accounts, + operation.Transaction.Data, + )) + return s.simulate(ctx) } diff --git a/sdk/solana/simulator_test.go b/sdk/solana/simulator_test.go index 8e019176..d83ef46c 100644 --- a/sdk/solana/simulator_test.go +++ b/sdk/solana/simulator_test.go @@ -3,10 +3,13 @@ package solana import ( "context" "errors" + "fmt" "testing" "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + cselectors "github.com/smartcontractkit/chain-selectors" "github.com/stretchr/testify/require" "github.com/smartcontractkit/mcms/sdk/solana/mocks" @@ -78,3 +81,98 @@ func TestSimulator_SimulateSetRoot(t *testing.T) { }) } } + +func TestSimulator_SimulateOperation(t *testing.T) { + t.Parallel() + + auth, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + selector := cselectors.SOLANA_DEVNET.Selector + + testWallet := solana.NewWallet() + ix, err := system.NewTransferInstruction(20*solana.LAMPORTS_PER_SOL, auth.PublicKey(), testWallet.PublicKey()).ValidateAndBuild() + require.NoError(t, err) + + data, err := ix.Data() + require.NoError(t, err) + + tx, err := NewTransaction(solana.SystemProgramID.String(), data, nil, ix.Accounts(), "solana-testing", []string{}) + require.NoError(t, err) + + tests := []struct { + name string + givenOP types.Operation + setupMocks func(t *testing.T, client *mocks.JSONRPCClient) + expectedError string + }{ + { + name: "success: SimulateOperation", + givenOP: types.Operation{ + Transaction: tx, + ChainSelector: types.ChainSelector(selector), + }, + setupMocks: func(t *testing.T, m *mocks.JSONRPCClient) { + t.Helper() + mockSolanaSimulateTransaction(t, m, 50, nil, nil) + }, + }, + { + name: "error: invalid additional fields", + givenOP: types.Operation{ + Transaction: types.Transaction{ + AdditionalFields: []byte("invalid"), + }, + ChainSelector: types.ChainSelector(selector), + }, + setupMocks: func(t *testing.T, m *mocks.JSONRPCClient) { + t.Helper() + }, + expectedError: "unable to unmarshal additional fields: invalid character 'i' looking for beginning of value", + }, + { + name: "error: block hash fetch failed", + givenOP: types.Operation{ + Transaction: tx, + ChainSelector: types.ChainSelector(selector), + }, + setupMocks: func(t *testing.T, m *mocks.JSONRPCClient) { + t.Helper() + mockSolanaSimulateTransaction(t, m, 50, errors.New("block hash error"), nil) + }, + expectedError: "unable to simulate instruction: block hash error", + }, + { + name: "failure: SimulateTransaction error", + givenOP: types.Operation{ + Transaction: tx, + ChainSelector: types.ChainSelector(selector), + }, + setupMocks: func(t *testing.T, client *mocks.JSONRPCClient) { + t.Helper() + mockSolanaSimulateTransaction(t, client, 50, nil, errors.New("SimulateTransaction error")) + }, + expectedError: "SimulateTransaction error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + executor, client := newTestExecutor(t, auth, testChainSelector) + simulator := NewSimulator(executor) + + tt.setupMocks(t, client) + + err = simulator.SimulateOperation(context.Background(), types.ChainMetadata{ + MCMAddress: fmt.Sprintf("%s.%s", testMCMProgramID.String(), testPDASeed), + }, tt.givenOP) + + if tt.expectedError != "" { + require.ErrorContains(t, err, tt.expectedError) + } else { + require.NoError(t, err) + } + }) + } +}