Skip to content

Commit

Permalink
Type fix for ChainSelector and ArgsTransform for commit method in Cha…
Browse files Browse the repository at this point in the history
…inWriter (#1083)

* Type fix for ChainSelector in ChainWriter

* GetValuesAtLocation: fix for Bytes32

* GetValuesAtLocation: fix for ccipocr3.UnknownEncodedAddress

* Added new ArgsTransform to adjust accounts for CCIP commit method

* Tidied go mod

* Added tests for CCIP types and commit method args transform

* Fixed linting

* Updated test names

---------

Co-authored-by: Blaž Hrastnik <[email protected]>
Co-authored-by: amit-momin <[email protected]>
  • Loading branch information
3 people authored Feb 20, 2025
1 parent 3333854 commit 8b7e09b
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 26 deletions.
2 changes: 1 addition & 1 deletion integration-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/lib/pq v1.10.9
github.com/pelletier/go-toml/v2 v2.2.3
github.com/rs/zerolog v1.33.0
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250212131315-e9b53b05b02a
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250206215114-fb6c3c35e8e3
github.com/smartcontractkit/chainlink-common v0.4.2-0.20250205141137-8f50d72601bb
github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250213035259-e727e73f6181
Expand Down Expand Up @@ -327,7 +328,6 @@ require (
github.com/slack-go/slack v0.15.0 // indirect
github.com/smartcontractkit/chain-selectors v1.0.40 // indirect
github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250212131315-e9b53b05b02a // indirect
github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 // indirect
github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250207205350-420ccacab78a // indirect
Expand Down
32 changes: 24 additions & 8 deletions integration-tests/relayinterface/lookups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package relayinterface

import (
"context"
"encoding/binary"
"testing"
"time"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens"
Expand Down Expand Up @@ -310,9 +312,15 @@ func TestPDALookups(t *testing.T) {
})
t.Run("PDALookup resolves valid PDA with non-address lookup seeds", func(t *testing.T) {
seed1 := []byte("test_seed")
seed2 := []byte("another_seed")

pda, _, err := solana.FindProgramAddress([][]byte{seed1, seed2}, programID)
seed2 := uint64(4)
bufSeed2 := make([]byte, 8)
binary.LittleEndian.PutUint64(bufSeed2, seed2)
seed3 := ccipocr3.ChainSelector(4)
bufSeed3 := make([]byte, 8)
binary.LittleEndian.PutUint64(bufSeed3, uint64(seed3))
seed4 := ccipocr3.Bytes32(chainwriter.GetRandomPubKey(t).Bytes())

pda, _, err := solana.FindProgramAddress([][]byte{seed1, bufSeed2, bufSeed3, seed4[:]}, programID)
require.NoError(t, err)

expectedMeta := []*solana.AccountMeta{
Expand All @@ -329,14 +337,18 @@ func TestPDALookups(t *testing.T) {
Seeds: []chainwriter.Seed{
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}},
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}},
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed3", Location: "ccip_chain_selector"}}},
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed4", Location: "ccip_bytes"}}},
},
IsSigner: false,
IsWritable: true,
}}

args := map[string]interface{}{
"test_seed": seed1,
"another_seed": seed2,
"test_seed": seed1,
"another_seed": seed2,
"ccip_chain_selector": seed3,
"ccip_bytes": seed4,
}

result, err := pdaLookup.Resolve(ctx, args, nil, client.MultiClient{})
Expand Down Expand Up @@ -366,8 +378,10 @@ func TestPDALookups(t *testing.T) {
t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) {
seed1 := chainwriter.GetRandomPubKey(t)
seed2 := chainwriter.GetRandomPubKey(t)
addr3 := chainwriter.GetRandomPubKey(t)
seed3 := ccipocr3.UnknownEncodedAddress(addr3.String())

pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes()}, programID)
pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes(), addr3.Bytes()}, programID)
require.NoError(t, err)

expectedMeta := []*solana.AccountMeta{
Expand All @@ -384,14 +398,16 @@ func TestPDALookups(t *testing.T) {
Seeds: []chainwriter.Seed{
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}},
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}},
{Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed3", Location: "unknown_encoded_address"}}},
},
IsSigner: false,
IsWritable: true,
}}

args := map[string]interface{}{
"test_seed": seed1,
"another_seed": seed2,
"test_seed": seed1,
"another_seed": seed2,
"unknown_encoded_address": seed3,
}

result, err := pdaLookup.Resolve(ctx, args, nil, client.MultiClient{})
Expand Down
2 changes: 1 addition & 1 deletion pkg/solana/chainwriter/chain_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra
return errorWithDebugID(fmt.Errorf("error finding transform function: %w", tfErr), debugID)
}
s.lggr.Debugw("Applying args transformation", "contract", contractName, "method", method)
args, err = transformFunc(ctx, s, args, accounts, toAddress)
args, accounts, err = transformFunc(ctx, s, args, accounts, toAddress)
if err != nil {
return errorWithDebugID(fmt.Errorf("error transforming args: %w", err), debugID)
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/solana/chainwriter/chain_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ func TestChainWriter_CCIPOfframp(t *testing.T) {
},
},
ChainSpecificName: "execute",
ArgsTransform: "CCIP",
ArgsTransform: "CCIPExecute",
LookupTables: chainwriter.LookupTables{},
Accounts: []chainwriter.Lookup{
{AccountConstant: &chainwriter.AccountConstant{
Expand Down Expand Up @@ -822,7 +822,7 @@ func TestChainWriter_CCIPOfframp(t *testing.T) {
},
},
ChainSpecificName: "commit",
ArgsTransform: "",
ArgsTransform: "CCIPCommit",
LookupTables: chainwriter.LookupTables{},
Accounts: []chainwriter.Lookup{
{AccountConstant: &chainwriter.AccountConstant{
Expand Down Expand Up @@ -922,7 +922,7 @@ func TestChainWriter_CCIPOfframp(t *testing.T) {
require.NoError(t, submitErr)
})

t.Run("CCIP commit is encoded successfully", func(t *testing.T) {
t.Run("CCIP commit is encoded successfully and ArgsTransform is applied correctly.", func(t *testing.T) {
// mock txm
txm := txmMocks.NewTxManager(t)
// initialize chain writer
Expand Down Expand Up @@ -963,6 +963,8 @@ func TestChainWriter_CCIPOfframp(t *testing.T) {
dec := ag_binary.NewBorshDecoder(payload)
err := dec.Decode(&decoded)
require.NoError(t, err)
// The CCIPCommit ArgsTransform should remove the last account since no price updates were provided in the report
require.Len(t, tx.Message.Instructions[0].Accounts, 2)
return true
}), &txID, mock.Anything).Return(nil).Once()

Expand Down
12 changes: 12 additions & 0 deletions pkg/solana/chainwriter/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,22 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) {
vals = append(vals, value.Bytes())
case ccipocr3.UnknownAddress:
vals = append(vals, value)
case ccipocr3.UnknownEncodedAddress:
decoded, err := solana.PublicKeyFromBase58(string(value))
if err != nil {
return nil, err
}
vals = append(vals, decoded[:])
case uint64:
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, value)
vals = append(vals, buf)
case ccipocr3.ChainSelector:
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, uint64(value))
vals = append(vals, buf)
case ccipocr3.Bytes32:
vals = append(vals, value[:])
case [32]uint8:
vals = append(vals, value[:])
default:
Expand Down
48 changes: 35 additions & 13 deletions pkg/solana/chainwriter/transform_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chainwriter

import (
"context"
"errors"
"fmt"

"github.com/gagliardetto/solana-go"
Expand All @@ -26,22 +27,24 @@ type ReportPostTransform struct {
TokenIndexes []byte
}

func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, any, solana.AccountMetaSlice, string) (any, error), error) {
func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, any, solana.AccountMetaSlice, string) (any, solana.AccountMetaSlice, error), error) {
switch id {
case "CCIP":
return CCIPArgsTransform, nil
case "CCIPExecute":
return CCIPExecuteArgsTransform, nil
case "CCIPCommit":
return CCIPCommitAccountTransform, nil
default:
return nil, fmt.Errorf("transform not found")
}
}

// This Transform function looks up the token pool addresses in the accounts slice and augments the args
// with the indexes of the token pool addresses in the accounts slice.
func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, error) {
func CCIPExecuteArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, solana.AccountMetaSlice, error) {
// Fetch offramp config to use to fetch the router address
offrampProgramConfig, ok := cw.config.Programs[ccipconsts.ContractNameOffRamp]
if !ok {
return nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameOffRamp)
return nil, nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameOffRamp)
}
// PDA lookup to fetch router address
routerAddrLookup := PDALookups{
Expand All @@ -61,16 +64,16 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a
}
accountMetas, err := routerAddrLookup.Resolve(ctx, nil, nil, cw.client)
if err != nil {
return nil, fmt.Errorf("failed to fetch the router program address from the reference addresses account: %w", err)
return nil, nil, fmt.Errorf("failed to fetch the router program address from the reference addresses account: %w", err)
}
if len(accountMetas) != 1 {
return nil, fmt.Errorf("expect 1 address to be returned for router address, received %d: %w", len(accountMetas), err)
return nil, nil, fmt.Errorf("expect 1 address to be returned for router address, received %d: %w", len(accountMetas), err)
}

// Fetch router config to use to fetch TokenAdminRegistry
routerProgramConfig, ok := cw.config.Programs[ccipconsts.ContractNameRouter]
if !ok {
return nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameRouter)
return nil, nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameRouter)
}

routerAddress := accountMetas[0].PublicKey
Expand Down Expand Up @@ -101,7 +104,7 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a

tableMap, _, err := cw.ResolveLookupTables(ctx, args, TokenPoolLookupTable)
if err != nil {
return nil, err
return nil, nil, err
}
registryTables := tableMap["PoolLookupTable"]
tokenPoolAddresses := []solana.PublicKey{}
Expand All @@ -114,20 +117,20 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a
for _, address := range tokenPoolAddresses {
if account.PublicKey == address {
if i > 255 {
return nil, fmt.Errorf("index %d out of range for uint8", i)
return nil, nil, fmt.Errorf("index %d out of range for uint8", i)
}
tokenIndexes = append(tokenIndexes, uint8(i)) //nolint:gosec
}
}
}

if len(tokenIndexes) != len(tokenPoolAddresses) {
return nil, fmt.Errorf("missing token pools in accounts")
return nil, nil, fmt.Errorf("missing token pools in accounts")
}

argsTyped, ok := args.(ReportPreTransform)
if !ok {
return nil, fmt.Errorf("args is not of type ReportPreTransform")
return nil, nil, fmt.Errorf("args is not of type ReportPreTransform")
}

argsTransformed := ReportPostTransform{
Expand All @@ -138,5 +141,24 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a
TokenIndexes: tokenIndexes,
}

return argsTransformed, nil
return argsTransformed, accounts, nil
}

// This Transform function trims off the GlobalState account from commit transactions if there are no token or gas price updates
func CCIPCommitAccountTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, solana.AccountMetaSlice, error) {
var tokenPriceVals, gasPriceVals [][]byte
var err error
tokenPriceVals, err = GetValuesAtLocation(args, "Info.TokenPrices.TokenID")
if err != nil && !errors.Is(err, errFieldNotFound) {
return nil, nil, fmt.Errorf("error getting values at location: %w", err)
}
gasPriceVals, err = GetValuesAtLocation(args, "Info.GasPrices.ChainSel")
if err != nil && !errors.Is(err, errFieldNotFound) {
return nil, nil, fmt.Errorf("error getting values at location: %w", err)
}
transformedAccounts := accounts
if len(tokenPriceVals) == 0 && len(gasPriceVals) == 0 {
transformedAccounts = accounts[:len(accounts)-1]
}
return args, transformedAccounts, nil
}
110 changes: 110 additions & 0 deletions pkg/solana/chainwriter/transform_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package chainwriter_test

import (
"context"
"testing"

"github.com/gagliardetto/solana-go"
ccipconsts "github.com/smartcontractkit/chainlink-ccip/pkg/consts"
"github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/client"
clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks"
)

func Test_CCIPExecuteArgsTransform(t *testing.T) {
ctx := tests.Context(t)
offrampAddress := chainwriter.GetRandomPubKey(t)
routerAddress := chainwriter.GetRandomPubKey(t)

// simplified CCIP Config - only IDLs are required for CCIPExecute ArgsTransform
ccipCWConfig := chainwriter.ChainWriterConfig{
Programs: map[string]chainwriter.ProgramConfig{
ccipconsts.ContractNameOffRamp: {
IDL: ccipOfframpIDL,
},
// Requires only the IDL for the CCIPArgsTransform to fetch the TokenAdminRegistry
ccipconsts.ContractNameRouter: {
IDL: ccipRouterIDL,
},
},
}
// mock client
rw := clientmocks.NewReaderWriter(t)
mc := *client.NewMultiClient(func(context.Context) (client.ReaderWriter, error) {
return rw, nil
})
// initialize chain writer
cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), mc, nil, nil, ccipCWConfig)
require.NoError(t, err)

destTokenAddr := chainwriter.GetRandomPubKey(t)
poolKeys := []solana.PublicKey{destTokenAddr}
poolKeys = append(poolKeys, chainwriter.CreateTestPubKeys(t, 1)...)

args := chainwriter.ReportPreTransform{
Info: ccipocr3.ExecuteReportInfo{
AbstractReports: []ccipocr3.ExecutePluginReportSingleChain{{
Messages: []ccipocr3.Message{{
TokenAmounts: []ccipocr3.RampTokenAmount{{
DestTokenAddress: ccipocr3.UnknownAddress(destTokenAddr.Bytes()),
}},
}},
}},
},
}

accounts := []*solana.AccountMeta{{PublicKey: poolKeys[0]}, {PublicKey: poolKeys[1]}}

t.Run("CCIPExecute ArgsTransform includes token indexes", func(t *testing.T) {
pda, _, err := solana.FindProgramAddress([][]byte{[]byte("token_admin_registry"), destTokenAddr.Bytes()}, routerAddress)
require.NoError(t, err)

lookupTable := mockTokenAdminRegistryLookupTable(t, rw, pda)
mockFetchRouterAddress(t, rw, routerAddress, offrampAddress)
mockFetchLookupTableAddresses(t, rw, lookupTable, poolKeys)
transformedArgs, newAccounts, err := chainwriter.CCIPExecuteArgsTransform(ctx, cw, args, accounts, offrampAddress.String())
require.NoError(t, err)
// Accounts should be unchanged
require.Len(t, newAccounts, 2)
typedArgs, ok := transformedArgs.(chainwriter.ReportPostTransform)
require.True(t, ok)
require.NotNil(t, typedArgs.TokenIndexes)
require.Len(t, typedArgs.TokenIndexes, 1)
})
}

func Test_CCIPCommitAccountTransform(t *testing.T) {
ctx := tests.Context(t)
offrampAddress := chainwriter.GetRandomPubKey(t)
key1 := chainwriter.GetRandomPubKey(t)
key2 := chainwriter.GetRandomPubKey(t)
t.Run("CCIPCommit ArgsTransform does not affect accounts if token prices exist", func(t *testing.T) {
args := struct {
Info ccipocr3.CommitReportInfo
}{
Info: ccipocr3.CommitReportInfo{
TokenPrices: []ccipocr3.TokenPrice{{TokenID: ccipocr3.UnknownEncodedAddress(key1.String())}},
},
}
accounts := []*solana.AccountMeta{{PublicKey: key1}, {PublicKey: key2}}
_, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, nil, args, accounts, offrampAddress.String())
require.NoError(t, err)
require.Len(t, newAccounts, 2)
})
t.Run("CCIPCommit ArgsTransform removes last account if token and gas prices do not exist", func(t *testing.T) {
args := struct {
Info ccipocr3.CommitReportInfo
}{
Info: ccipocr3.CommitReportInfo{},
}
accounts := []*solana.AccountMeta{{PublicKey: key1}, {PublicKey: key2}}
_, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, nil, args, accounts, offrampAddress.String())
require.NoError(t, err)
require.Len(t, newAccounts, 1)
})
}

0 comments on commit 8b7e09b

Please sign in to comment.