From f6302aca7441b9dae732b25ca19e6f14c82705d7 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 20 Dec 2024 11:42:21 -0500 Subject: [PATCH] updated comments and slight tweaks --- .../relayinterface/lookups_test.go | 6 +-- pkg/solana/chainwriter/ccip_example_config.go | 12 ++--- pkg/solana/chainwriter/chain_writer.go | 44 +++++++++++++++---- pkg/solana/chainwriter/chain_writer_test.go | 22 +++++----- pkg/solana/chainwriter/helpers.go | 3 ++ pkg/solana/chainwriter/lookups.go | 32 +++++--------- 6 files changed, 72 insertions(+), 47 deletions(-) diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 1b91dc8df..7333b2e8d 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -126,7 +126,7 @@ func TestAccountLookups(t *testing.T) { } func TestPDALookups(t *testing.T) { - programID := solana.SystemProgramID + programID := chainwriter.GetRandomPubKey(t) t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { seed := chainwriter.GetRandomPubKey(t) @@ -281,7 +281,7 @@ func TestLookupTables(t *testing.T) { table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, - StaticLookupTables: []string{table.String()}, + StaticLookupTables: []solana.PublicKey{table}, } _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, resolveErr) @@ -342,7 +342,7 @@ func TestLookupTables(t *testing.T) { lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, - StaticLookupTables: []string{invalidTable.String()}, + StaticLookupTables: []solana.PublicKey{invalidTable}, } _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index acdaf3d35..f277935d9 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -1,7 +1,7 @@ package chainwriter import ( - "fmt" + "github.com/gagliardetto/solana-go" ) func TestConfig() { @@ -13,8 +13,8 @@ func TestConfig() { systemProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6E" computeBudgetProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6F" sysvarProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6G" - commonAddressesLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H" - routerLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I" + commonAddressesLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H") + routerLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I") userAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6J" executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` @@ -58,7 +58,7 @@ func TestConfig() { // Static lookup tables are the traditional use case (point 2 above) of Lookup tables. These are lookup // tables which contain commonly used addresses in all CCIP execute transactions. The ChainWriter reads // these lookup tables and appends them to the transaction to reduce the size of the transaction. - StaticLookupTables: []string{ + StaticLookupTables: []solana.PublicKey{ commonAddressesLookupTable, routerLookupTable, }, @@ -255,7 +255,7 @@ func TestConfig() { InputModifications: nil, ChainSpecificName: "commit", LookupTables: LookupTables{ - StaticLookupTables: []string{ + StaticLookupTables: []solana.PublicKey{ commonAddressesLookupTable, routerLookupTable, }, @@ -329,5 +329,5 @@ func TestConfig() { }, }, } - fmt.Println(chainWriterConfig) + _ = chainWriterConfig } diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 8616b9f62..e16a55e60 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -135,8 +135,6 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program ### Error Handling: - Errors are wrapped with the `debugID` for easier tracing. */ -// GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list -// for Solana transactions. func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { @@ -149,6 +147,10 @@ func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTable return addresses, nil } +// FilterLookupTableAddresses takes a list of accounts and two lookup table maps +// (one for derived tables, one for static tables) and filters out any addresses that are +// not used by the accounts. It returns a map of only those lookup table +// addresses that match entries in `accounts`. func (s *SolanaChainWriterService) FilterLookupTableAddresses( accounts []*solana.AccountMeta, derivedTableMap map[string]map[string][]*solana.AccountMeta, @@ -203,7 +205,30 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( return filteredLookupTables } -func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { +// SubmitTransaction builds, encodes, and enqueues a transaction using the provided program +// configuration and method details. It relies on the configured IDL, account lookups, and +// lookup tables to gather the necessary accounts and data. The function retrieves the latest +// blockhash and assigns it to the transaction, so callers do not need to provide one. +// +// Submissions and retries are handled by the underlying transaction manager. If a “debug ID” +// location is configured, SubmitTransaction extracts it from the provided `args` and attaches +// it to errors for easier troubleshooting. Only the first debug ID it encounters will be used. +// +// Parameters: +// - ctx: The context for cancellation and timeouts. +// - contractName: Identifies which Solana program config to use from `s.config.Programs`. +// - method: Specifies which method config to invoke within the chosen program config. +// - args: Arbitrary arguments that are encoded into the transaction payload and/or used for dynamic address lookups. +// - transactionID: A unique identifier for the transaction, used for tracking within the transaction manager. +// - toAddress: The on-chain address (program ID) to which the transaction is directed. +// - meta: Currently unused; included for interface compatibility. +// - value: Currently unused; included for interface compatibility. +// +// Returns: +// +// An error if any stage of the transaction preparation or enqueueing fails. A nil return +// indicates that the transaction was successfully submitted to the transaction manager. +func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, _ *types.TxMeta, _ *big.Int) error { programConfig, exists := s.config.Programs[contractName] if !exists { return fmt.Errorf("failed to find program config for contract name: %s", contractName) @@ -241,6 +266,14 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } + feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) + if err != nil { + return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) + } + + accounts = append([]*solana.AccountMeta{solana.Meta(feePayer).SIGNER().WRITE()}, accounts...) + accounts = append(accounts, solana.Meta(solana.SystemProgramID)) + // Filter the lookup table addresses based on which accounts are actually used filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) @@ -256,11 +289,6 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing program ID: %w", err), debugID) } - feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) - if err != nil { - return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) - } - tx, err := solana.NewTransaction( []solana.Instruction{ solana.NewInstruction(programID, accounts, encodedPayload), diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index ef7b399af..9798bdb4c 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -273,7 +273,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, Seeds: []chainwriter.Lookup{ - // extract seed2 for PDA lookup + // extract seed1 for PDA lookup chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, }, IsSigner: true, @@ -302,7 +302,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { }, }, }, - StaticLookupTables: []string{staticLookupTablePubkey1.String(), staticLookupTablePubkey2.String()}, + StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey1, staticLookupTablePubkey2}, } args := map[string]interface{}{ @@ -441,7 +441,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }, }, }, - StaticLookupTables: []string{staticLookupTablePubkey.String()}, + StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey}, }, Accounts: []chainwriter.Lookup{ chainwriter.AccountConstant{ @@ -528,16 +528,18 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() txID := uuid.NewString() - txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { + txm.On("Enqueue", mock.Anything, admin.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { // match transaction fields to ensure it was built as expected require.Equal(t, recentBlockHash, tx.Message.RecentBlockhash) require.Len(t, tx.Message.Instructions, 1) - require.Len(t, tx.Message.AccountKeys, 5) // fee payer + derived accounts - require.Equal(t, admin, tx.Message.AccountKeys[0]) // fee payer - require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant - require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup - require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup - require.Equal(t, programID, tx.Message.AccountKeys[4]) // instruction program ID + require.Len(t, tx.Message.AccountKeys, 6) // fee payer + derived accounts + require.Equal(t, admin, tx.Message.AccountKeys[0]) // fee payer + require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant + require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup + require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup + require.Equal(t, solana.SystemProgramID, tx.Message.AccountKeys[4]) // system program ID + require.Equal(t, programID, tx.Message.AccountKeys[5]) // instruction program ID + // instruction program ID require.Len(t, tx.Message.AddressTableLookups, 1) // address table look contains entry require.Equal(t, derivedLookupTablePubkey, tx.Message.AddressTableLookups[0].AccountKey) // address table return true diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 8b7276276..6f78c7a63 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -61,6 +61,9 @@ func GetDebugIDAtLocation(args any, location string) (string, error) { return "", err } + if len(debugIDList) == 0 { + return "", errors.New("no debug ID found at location: " + location) + } // there should only be one debug ID, others will be ignored. debugID := string(debugIDList[0]) diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 9f1071c46..b9d3ca7cd 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -52,14 +52,10 @@ type InternalField struct { Location string } -type ValueLookup struct { - Location string -} - // LookupTables represents a list of lookup tables that are used to derive addresses for a program. type LookupTables struct { DerivedLookupTables []DerivedLookupTable - StaticLookupTables []string + StaticLookupTables []solana.PublicKey } // DerivedLookupTable represents a lookup table that is used to derive addresses for a program. @@ -212,19 +208,18 @@ func decodeBorshIntoType(data []byte, typ reflect.Type) (interface{}, error) { // It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from input args). func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([][]byte, error) { var seedBytes [][]byte - maxSeedLength := 32 for _, seed := range lookup.Seeds { if lookupSeed, ok := seed.(AccountLookup); ok { - // Get value from a location (This doens't have to be an address, it can be any value) + // Get value from a location (This doesn't have to be an address, it can be any value) bytes, err := GetValuesAtLocation(args, lookupSeed.Location) if err != nil { return nil, fmt.Errorf("error getting address seed: %w", err) } // validate seed length for _, b := range bytes { - if len(b) > maxSeedLength { - return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", maxSeedLength, len(b)) + if len(b) > solana.MaxSeedLength { + return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", solana.MaxSeedLength, len(b)) } seedBytes = append(seedBytes, b) } @@ -247,7 +242,7 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups) ([]*solana.AccountMeta, error) { - if len(seeds) > 16 { + if len(seeds) > solana.MaxSeeds { return nil, fmt.Errorf("seed maximum exceeded: %d", len(seeds)) } var addresses []*solana.AccountMeta @@ -271,6 +266,8 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Read derived lookup tables for _, derivedLookup := range lookupTables.DerivedLookupTables { + // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resovles to more + // than one address lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) if err != nil { return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) @@ -289,17 +286,11 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Read static lookup tables for _, staticTable := range lookupTables.StaticLookupTables { - // Parse the static table address - tableAddress, err := solana.PublicKeyFromBase58(staticTable) - if err != nil { - return nil, nil, fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err) - } - - addressses, err := getLookupTableAddresses(ctx, s.reader, tableAddress) + addressses, err := getLookupTableAddresses(ctx, s.reader, staticTable) if err != nil { return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) } - staticTableMap[tableAddress] = addressses + staticTableMap[staticTable] = addressses } return derivedTableMap, staticTableMap, nil @@ -312,15 +303,16 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt return nil, nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) } + // Nested map in case the lookup table resolves to multiple addresses resultMap := make(map[string]map[string][]*solana.AccountMeta) var lookupTableMetas []*solana.AccountMeta // Iterate over each address of the lookup table for _, addressMeta := range lookupTableAddresses { - // Fetch account info + // Read the full list of addresses from the lookup table addresses, err := getLookupTableAddresses(ctx, reader, addressMeta.PublicKey) if err != nil { - return nil, nil, fmt.Errorf("error fetching lookup table address: %w", err) + return nil, nil, fmt.Errorf("error fetching lookup table address: %s, error: %w", addressMeta.PublicKey, err) } // Create the inner map for this lookup table