Skip to content

Commit

Permalink
feat(solana): simulate executor operation mcms
Browse files Browse the repository at this point in the history
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
  • Loading branch information
graham-chainlink committed Feb 3, 2025
1 parent 28d52c3 commit f8bcdd8
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-gifts-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

feat(solana): simulate executor operation mcms
37 changes: 37 additions & 0 deletions e2e/tests/solana/simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
26 changes: 20 additions & 6 deletions sdk/solana/simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package solana

import (
"context"
"encoding/json"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -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)
}

Expand Down
98 changes: 98 additions & 0 deletions sdk/solana/simulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit f8bcdd8

Please sign in to comment.