Skip to content

Commit

Permalink
[DPA-1418]: feat(solana): simulate executor operation mcms (#277)
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 authored Feb 4, 2025
1 parent 3f510f7 commit cb6ea80
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 27 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)
}
12 changes: 4 additions & 8 deletions sdk/solana/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -57,14 +56,11 @@ func (e *Encoder) HashOperation(
return common.Hash{}, err
}

toProgramID, _, err := ParseContractAddress(op.Transaction.To)
if errors.Is(err, ErrInvalidContractAddressFormat) {
var pkerr error
toProgramID, pkerr = solana.PublicKeyFromBase58(op.Transaction.To)
if pkerr != nil {
return common.Hash{}, fmt.Errorf("unable to get hash from base58 To address: %w", err)
}
toProgramID, err := ParseProgramID(op.Transaction.To)
if err != nil {
return common.Hash{}, fmt.Errorf("unable to prase program id from To field: %w", err)
}

// Parse Additional fields to get the ix accounts
var additionalFields AdditionalFields
if op.Transaction.AdditionalFields != nil {
Expand Down
2 changes: 1 addition & 1 deletion sdk/solana/encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func TestEncoder_HashOperation(t *testing.T) {
ChainSelector: testChainSelector,
Transaction: types.Transaction{To: "invalid"},
},
wantErr: "unable to get hash from base58 To address: invalid solana contract address format: \"invalid\"",
wantErr: "unable to prase program id from To field: unable to parse base58 solana program id: decode: invalid base58 digit ('l')",
},
}
for _, tt := range tests {
Expand Down
11 changes: 3 additions & 8 deletions sdk/solana/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package solana
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"

Expand Down Expand Up @@ -87,13 +86,9 @@ func (e *Executor) ExecuteOperation(
if err = json.Unmarshal(op.Transaction.AdditionalFields, &additionalFields); err != nil {
return types.TransactionResult{}, fmt.Errorf("unable to unmarshal additional fields: %w", err)
}
toProgramID, _, err := ParseContractAddress(op.Transaction.To)
if errors.Is(err, ErrInvalidContractAddressFormat) {
var pkerr error
toProgramID, pkerr = solana.PublicKeyFromBase58(op.Transaction.To)
if pkerr != nil {
return types.TransactionResult{}, fmt.Errorf("unable to get hash from base58 To address: %w", err)
}
toProgramID, err := ParseProgramID(op.Transaction.To)
if err != nil {
return types.TransactionResult{}, fmt.Errorf("unable to prase program id from To field: %w", err)
}

ix := mcm.NewExecuteInstruction(
Expand Down
19 changes: 14 additions & 5 deletions sdk/solana/simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package solana

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

Expand Down Expand Up @@ -45,14 +46,22 @@ 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)
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 := ParseProgramID(operation.Transaction.To)
if err != nil {
return err
return fmt.Errorf("unable to prase program id from To field: %w", err)
}

s.instructions = append(s.instructions, solana.NewInstruction(
toProgramID,
additionalFields.Accounts,
operation.Transaction.Data,
))

return s.simulate(ctx)
}

Expand Down
100 changes: 100 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,100 @@ 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, err := solana.NewRandomPrivateKey()
require.NoError(t, err)

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)
}
})
}
}
10 changes: 5 additions & 5 deletions sdk/solana/timelock_converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func Test_TimelockConverter_ConvertBatchToChainOperations(t *testing.T) {
AdditionalFields: toJSON(t, AdditionalFields{Accounts: []*solana.AccountMeta{
{PublicKey: solana.MPK("3x12f1G4bt9j7rsBfLE7rZQ5hXoHuHdjtUr2UKW8gjQp"), IsWritable: true},
{PublicKey: solana.MPK("GYWcPzXkdzY9DJLcbFs67phqyYzmJxeEKSTtqEoo8oKz")},
{PublicKey: solana.MPK(proposerAC.PublicKey().String())},
{PublicKey: proposerAC.PublicKey()},
{PublicKey: solana.MPK("62gDM6BRLf2w1yXfmpePUTsuvbeBbu4QqdjV32wcc4UG"), IsWritable: true},
{PublicKey: solana.MPK("11111111111111111111111111111111")},
}}),
Expand All @@ -135,7 +135,7 @@ func Test_TimelockConverter_ConvertBatchToChainOperations(t *testing.T) {
AdditionalFields: toJSON(t, AdditionalFields{Accounts: []*solana.AccountMeta{
{PublicKey: solana.MPK("3x12f1G4bt9j7rsBfLE7rZQ5hXoHuHdjtUr2UKW8gjQp"), IsWritable: true},
{PublicKey: solana.MPK("GYWcPzXkdzY9DJLcbFs67phqyYzmJxeEKSTtqEoo8oKz")},
{PublicKey: solana.MPK(proposerAC.PublicKey().String())},
{PublicKey: proposerAC.PublicKey()},
{PublicKey: solana.MPK("62gDM6BRLf2w1yXfmpePUTsuvbeBbu4QqdjV32wcc4UG"), IsWritable: true},
{PublicKey: solana.MPK("11111111111111111111111111111111")},
}}),
Expand All @@ -154,7 +154,7 @@ func Test_TimelockConverter_ConvertBatchToChainOperations(t *testing.T) {
AdditionalFields: toJSON(t, AdditionalFields{Accounts: []*solana.AccountMeta{
{PublicKey: solana.MPK("3x12f1G4bt9j7rsBfLE7rZQ5hXoHuHdjtUr2UKW8gjQp"), IsWritable: true},
{PublicKey: solana.MPK("GYWcPzXkdzY9DJLcbFs67phqyYzmJxeEKSTtqEoo8oKz")},
{PublicKey: solana.MPK(proposerAC.PublicKey().String())},
{PublicKey: proposerAC.PublicKey()},
{PublicKey: solana.MPK("62gDM6BRLf2w1yXfmpePUTsuvbeBbu4QqdjV32wcc4UG"), IsWritable: true},
{PublicKey: solana.MPK("11111111111111111111111111111111")},
}}),
Expand All @@ -173,7 +173,7 @@ func Test_TimelockConverter_ConvertBatchToChainOperations(t *testing.T) {
AdditionalFields: toJSON(t, AdditionalFields{Accounts: []*solana.AccountMeta{
{PublicKey: solana.MPK("3x12f1G4bt9j7rsBfLE7rZQ5hXoHuHdjtUr2UKW8gjQp"), IsWritable: true},
{PublicKey: solana.MPK("GYWcPzXkdzY9DJLcbFs67phqyYzmJxeEKSTtqEoo8oKz")},
{PublicKey: solana.MPK(proposerAC.PublicKey().String())},
{PublicKey: proposerAC.PublicKey()},
{PublicKey: solana.MPK("62gDM6BRLf2w1yXfmpePUTsuvbeBbu4QqdjV32wcc4UG"), IsWritable: true},
}}),
OperationMetadata: types.OperationMetadata{
Expand All @@ -191,7 +191,7 @@ func Test_TimelockConverter_ConvertBatchToChainOperations(t *testing.T) {
AdditionalFields: toJSON(t, AdditionalFields{Accounts: []*solana.AccountMeta{
{PublicKey: solana.MPK("3x12f1G4bt9j7rsBfLE7rZQ5hXoHuHdjtUr2UKW8gjQp"), IsWritable: true},
{PublicKey: solana.MPK("GYWcPzXkdzY9DJLcbFs67phqyYzmJxeEKSTtqEoo8oKz")},
{PublicKey: solana.MPK(proposerAC.PublicKey().String())},
{PublicKey: proposerAC.PublicKey()},
{PublicKey: solana.MPK("62gDM6BRLf2w1yXfmpePUTsuvbeBbu4QqdjV32wcc4UG"), IsWritable: true},
}}),
OperationMetadata: types.OperationMetadata{
Expand Down

0 comments on commit cb6ea80

Please sign in to comment.