diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 7333b2e8d..fd148abff 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -145,8 +145,8 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountConstant{Name: "seed", Address: seed.String()}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountConstant{Name: "seed", Address: seed.String()}}, }, IsSigner: false, IsWritable: true, @@ -175,9 +175,9 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}, - chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}, }, IsSigner: false, IsWritable: true, @@ -198,8 +198,8 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}}, }, IsSigner: false, IsWritable: true, @@ -233,9 +233,9 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}, - chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}, }, IsSigner: false, IsWritable: true, @@ -403,8 +403,8 @@ func TestLookupTables(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index f277935d9..adbd4d324 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -47,8 +47,8 @@ func TestConfig() { IsWritable: false, }, // Seeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, }, IsSigner: false, IsWritable: false, @@ -86,9 +86,9 @@ func TestConfig() { IsWritable: false, }, // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, - AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, }, IsSigner: false, IsWritable: false, @@ -120,13 +120,13 @@ func TestConfig() { IsWritable: false, }, // The seed is the receiver address. - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ Name: "Receiver", Location: "Message.Receiver", IsSigner: false, IsWritable: false, - }, + }}, }, }, // Account constant @@ -146,8 +146,8 @@ func TestConfig() { IsWritable: false, }, // The seed, once again, is the destination token address. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, }, IsSigner: false, IsWritable: false, @@ -175,9 +175,9 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{Location: "Message.Header.DestChainSelector"}, - AccountLookup{Location: "Message.Header.SourceChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, + {Dynamic: AccountLookup{Location: "Message.Header.SourceChainSelector"}}, }, IsSigner: false, IsWritable: false, @@ -191,11 +191,11 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoot", - }, + }}, }, IsSigner: false, IsWritable: false, @@ -211,9 +211,9 @@ func TestConfig() { }, // In this case, the user configured multiple seeds. These will be used in conjunction // with the public key to generate one or multiple PDA accounts. - Seeds: []Lookup{ - AccountLookup{Location: "Message.Receiver"}, - AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.Receiver"}}, + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, }, }, // Account constant @@ -284,11 +284,11 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoots", - }, + }}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index e16a55e60..3ee198941 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -80,22 +80,22 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) { for program, programConfig := range config.Programs { var idl codec.IDL if err := json.Unmarshal([]byte(programConfig.IDL), &idl); err != nil { - return nil, fmt.Errorf("failed to unmarshal IDL: %w", err) + return nil, fmt.Errorf("failed to unmarshal IDL for program: %s, error: %w", program, err) } idlCodec, err := codec.NewIDLInstructionsCodec(idl, binary.LittleEndian()) if err != nil { - return nil, fmt.Errorf("failed to create codec from IDL: %w", err) + return nil, fmt.Errorf("failed to create codec from IDL for program: %s, error: %w", program, err) } for method, methodConfig := range programConfig.Methods { if methodConfig.InputModifications != nil { modConfig, err := methodConfig.InputModifications.ToModifier(codec.DecoderHooks...) if err != nil { - return nil, fmt.Errorf("failed to create input modifications: %w", err) + return nil, fmt.Errorf("failed to create input modifications for method %s.%s, error: %w", program, method, err) } // add mods to codec idlCodec, err = codec.NewNamedModifierCodec(idlCodec, method, modConfig) if err != nil { - return nil, fmt.Errorf("failed to create named codec: %w", err) + return nil, fmt.Errorf("failed to create named codec for method %s.%s, error: %w", program, method, err) } } } @@ -250,6 +250,10 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra codec := s.codecs[contractName] encodedPayload, err := codec.Encode(ctx, args, method) + + discriminator := GetDiscriminator(methodConfig.ChainSpecificName) + encodedPayload = append(discriminator[:], encodedPayload...) + if err != nil { return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID) } @@ -302,7 +306,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Enqueue transaction - if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { + if err = s.txm.Enqueue(ctx, methodConfig.FromAddress, tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 9798bdb4c..ff1d357c2 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -85,9 +85,9 @@ func TestChainWriter_GetAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: derivedTablePdaLookupMeta.IsSigner, IsWritable: derivedTablePdaLookupMeta.IsWritable, @@ -129,9 +129,9 @@ func TestChainWriter_GetAddresses(t *testing.T) { chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: pdaLookupMeta.IsSigner, IsWritable: pdaLookupMeta.IsWritable, @@ -272,9 +272,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: true, IsWritable: true, @@ -289,9 +289,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "MiscPDA", PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: true, IsWritable: true, @@ -428,9 +428,9 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: false, IsWritable: false, @@ -459,9 +459,9 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 6f78c7a63..a4b18e4d5 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -3,6 +3,7 @@ package chainwriter import ( "context" "crypto/sha256" + "encoding/binary" "errors" "fmt" "reflect" @@ -41,12 +42,15 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) { if err != nil { return nil, err } - for _, value := range addressList { if byteArray, ok := value.([]byte); ok { vals = append(vals, byteArray) } else if address, ok := value.(solana.PublicKey); ok { vals = append(vals, address.Bytes()) + } else if num, ok := value.(uint64); ok { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, num) + vals = append(vals, buf) } else { return nil, fmt.Errorf("invalid value format at path: %s", location) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index b9d3ca7cd..c5298f2ce 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -33,6 +33,11 @@ type AccountLookup struct { IsWritable bool } +type Seed struct { + Static []byte // Static seed value + Dynamic Lookup // Dynamic lookup for seed +} + // PDALookups generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. type PDALookups struct { Name string @@ -40,7 +45,7 @@ type PDALookups struct { // there will be multiple PDAs generated by combining each PublicKey with the seeds. PublicKey Lookup // Seeds to be derived from an additional lookup - Seeds []Lookup + Seeds []Seed IsSigner bool IsWritable bool // OPTIONAL: On-chain location and type of desired data from PDA (e.g. a sub-account of the data account) @@ -210,29 +215,35 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable var seedBytes [][]byte for _, seed := range lookup.Seeds { - if lookupSeed, ok := seed.(AccountLookup); ok { - // 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) > solana.MaxSeedLength { - return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", solana.MaxSeedLength, len(b)) + if seed.Static != nil { + seedBytes = append(seedBytes, seed.Static) + } + if seed.Dynamic != nil { + dynamicSeed := seed.Dynamic + if lookupSeed, ok := dynamicSeed.(AccountLookup); ok { + // Get value from a location (This doens'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) > 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) + } + } else { + // Get address seeds from the lookup + seedAddresses, err := GetAddresses(ctx, args, []Lookup{dynamicSeed}, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting address seed: %w", err) } - seedBytes = append(seedBytes, b) - } - } else { - // Get address seeds from the lookup - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, reader) - if err != nil { - return nil, fmt.Errorf("error getting address seed: %w", err) - } - // Add each address seed as bytes - for _, address := range seedAddresses { - seedBytes = append(seedBytes, address.PublicKey.Bytes()) + // Add each address seed as bytes + for _, address := range seedAddresses { + seedBytes = append(seedBytes, address.PublicKey.Bytes()) + } } } } @@ -266,7 +277,7 @@ 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 + // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resolves to more // than one address lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) if err != nil {