From 1ba201b6f7929d13927d3c9d9b16fcc734185ccb Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 15 Jan 2025 18:52:37 -0500 Subject: [PATCH] Added location parsing for IsWritable and IsSignable on AccountLookups --- .../relayinterface/lookups_test.go | 184 +++++++++++++++++- pkg/solana/chainwriter/ccip_example_config.go | 12 +- pkg/solana/chainwriter/chain_writer_test.go | 8 +- pkg/solana/chainwriter/helpers.go | 8 +- pkg/solana/chainwriter/lookups.go | 76 +++++++- 5 files changed, 261 insertions(+), 27 deletions(-) diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 0154b683a..45f55b7fa 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -21,6 +21,14 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) +type InnerAccountArgs struct { + Accounts []*solana.AccountMeta +} + +type TestAccountArgs struct { + Inner InnerAccountArgs +} + func TestAccountContant(t *testing.T) { t.Run("AccountConstant resolves valid address", func(t *testing.T) { expectedAddr := chainwriter.GetRandomPubKey(t) @@ -62,8 +70,8 @@ func TestAccountLookups(t *testing.T) { lookupConfig := chainwriter.AccountLookup{ Name: "TestAccount", Location: "Inner.Address", - IsSigner: true, - IsWritable: true, + IsSigner: chainwriter.MetaBool{Value: true}, + IsWritable: chainwriter.MetaBool{Value: true}, } result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) @@ -96,8 +104,8 @@ func TestAccountLookups(t *testing.T) { lookupConfig := chainwriter.AccountLookup{ Name: "TestAccount", Location: "Inner.Address", - IsSigner: true, - IsWritable: true, + IsSigner: chainwriter.MetaBool{Value: true}, + IsWritable: chainwriter.MetaBool{Value: true}, } result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) @@ -117,12 +125,174 @@ func TestAccountLookups(t *testing.T) { lookupConfig := chainwriter.AccountLookup{ Name: "InvalidAccount", Location: "Invalid.Directory", - IsSigner: true, - IsWritable: true, + IsSigner: chainwriter.MetaBool{Value: true}, + IsWritable: chainwriter.MetaBool{Value: true}, } _, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.Error(t, err) }) + + t.Run("AccountLookup works with MetaBool lookups", func(t *testing.T) { + accounts := [3]*solana.AccountMeta{} + + for i := 0; i < 3; i++ { + accounts[i] = &solana.AccountMeta{ + PublicKey: chainwriter.GetRandomPubKey(t), + IsSigner: true, + IsWritable: false, + } + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Inner.Accounts.PublicKey", + IsSigner: chainwriter.MetaBool{Location: "Inner.Accounts.IsSigner"}, + IsWritable: chainwriter.MetaBool{Location: "Inner.Accounts.IsWritable"}, + } + + args := TestAccountArgs{ + Inner: InnerAccountArgs{ + Accounts: accounts[:], + }, + } + + result, err := lookupConfig.Resolve(ctx, args, nil, nil) + require.NoError(t, err) + + for i, meta := range result { + require.Equal(t, accounts[i], meta) + } + }) + + t.Run("AccountLookup works with MetaBool lookups when a meta field is missing", func(t *testing.T) { + accounts := [3]*solana.AccountMeta{} + + for i := 0; i < 3; i++ { + accounts[i] = &solana.AccountMeta{ + PublicKey: chainwriter.GetRandomPubKey(t), + IsWritable: true, + } + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Inner.Accounts.PublicKey", + IsSigner: chainwriter.MetaBool{Location: "Inner.Accounts.IsSigner"}, + IsWritable: chainwriter.MetaBool{Location: "Inner.Accounts.IsWritable"}, + } + + args := TestAccountArgs{ + Inner: InnerAccountArgs{ + Accounts: accounts[:], + }, + } + + result, err := lookupConfig.Resolve(ctx, args, nil, nil) + require.NoError(t, err) + + for i, meta := range result { + require.Equal(t, accounts[i], meta) + } + }) + + t.Run("AccountLookup works with MetaBool lookups in a different location", func(t *testing.T) { + type TestAccountArgsExtended struct { + Inner InnerAccountArgs + ExternalBool bool + } + + accounts := [3]*solana.AccountMeta{} + + for i := 0; i < 3; i++ { + accounts[i] = &solana.AccountMeta{ + PublicKey: chainwriter.GetRandomPubKey(t), + IsWritable: true, + IsSigner: true, + } + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Inner.Accounts.PublicKey", + IsSigner: chainwriter.MetaBool{Location: "ExternalBool"}, + IsWritable: chainwriter.MetaBool{Location: "ExternalBool"}, + } + + args := TestAccountArgsExtended{ + Inner: InnerAccountArgs{ + Accounts: accounts[:], + }, + ExternalBool: true, + } + + result, err := lookupConfig.Resolve(ctx, args, nil, nil) + require.NoError(t, err) + + for i, meta := range result { + require.Equal(t, accounts[i], meta) + } + }) + + t.Run("AccountLookup fails with MetaBool an invalid number of Meta lookups", func(t *testing.T) { + type TestAccountArgsExtended struct { + Inner InnerAccountArgs + ExternalBools []bool + } + + accounts := [3]*solana.AccountMeta{} + + for i := 0; i < 3; i++ { + accounts[i] = &solana.AccountMeta{ + PublicKey: chainwriter.GetRandomPubKey(t), + IsWritable: true, + IsSigner: true, + } + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Inner.Accounts.PublicKey", + IsSigner: chainwriter.MetaBool{Location: "ExternalBools"}, + IsWritable: chainwriter.MetaBool{Location: "ExternalBools"}, + } + + args := TestAccountArgsExtended{ + Inner: InnerAccountArgs{ + Accounts: accounts[:], + }, + ExternalBools: []bool{true, true}, + } + + _, err := lookupConfig.Resolve(ctx, args, nil, nil) + require.Error(t, err) + }) + + t.Run("AccountLookup fails with MetaBool with an Invalid Location", func(t *testing.T) { + accounts := [3]*solana.AccountMeta{} + + for i := 0; i < 3; i++ { + accounts[i] = &solana.AccountMeta{ + PublicKey: chainwriter.GetRandomPubKey(t), + IsWritable: true, + } + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Inner.Accounts.PublicKey", + IsSigner: chainwriter.MetaBool{Location: "Invalid.IsSigner"}, + IsWritable: chainwriter.MetaBool{Location: "Invalid.IsWritable"}, + } + + args := TestAccountArgs{ + Inner: InnerAccountArgs{ + Accounts: accounts[:], + }, + } + + _, err := lookupConfig.Resolve(ctx, args, nil, nil) + require.Error(t, err) + }) } func TestPDALookups(t *testing.T) { @@ -435,7 +605,7 @@ func TestLookupTables(t *testing.T) { Accounts: chainwriter.AccountLookup{ Name: "TestLookupTable", Location: "Inner.Address", - IsSigner: true, + IsSigner: chainwriter.MetaBool{Value: true}, }, }, }, diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index fc46794a8..6c612ca16 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -104,8 +104,8 @@ func TestConfig() { AccountLookup{ Name: "TokenAccount", Location: "Message.TokenAmounts.DestTokenAddress", - IsSigner: false, - IsWritable: false, + IsSigner: MetaBool{Value: false}, + IsWritable: MetaBool{Value: false}, }, // PDA Account Lookup - Based on an account lookup and an address lookup PDALookups{ @@ -116,16 +116,16 @@ func TestConfig() { PublicKey: AccountLookup{ Name: "TokenAccount", Location: "Message.TokenAmounts.DestTokenAddress", - IsSigner: false, - IsWritable: false, + IsSigner: MetaBool{Value: false}, + IsWritable: MetaBool{Value: false}, }, // The seed is the receiver address. Seeds: []Seed{ {Dynamic: AccountLookup{ Name: "Receiver", Location: "Message.Receiver", - IsSigner: false, - IsWritable: false, + IsSigner: MetaBool{Value: false}, + IsWritable: MetaBool{Value: false}, }}, }, }, diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 947674a2f..f113f33da 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -128,8 +128,8 @@ func TestChainWriter_GetAddresses(t *testing.T) { chainwriter.AccountLookup{ Name: "LookupTable", Location: "LookupTable", - IsSigner: accountLookupMeta.IsSigner, - IsWritable: accountLookupMeta.IsWritable, + IsSigner: chainwriter.MetaBool{Value: accountLookupMeta.IsSigner}, + IsWritable: chainwriter.MetaBool{Value: accountLookupMeta.IsWritable}, }, chainwriter.PDALookups{ Name: "DataAccountPDA", @@ -463,8 +463,8 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { chainwriter.AccountLookup{ Name: "LookupTable", Location: "LookupTable", - IsSigner: false, - IsWritable: false, + IsSigner: chainwriter.MetaBool{Value: false}, + IsWritable: chainwriter.MetaBool{Value: false}, }, chainwriter.PDALookups{ Name: "DataAccountPDA", diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 7d146a25a..b60b4b76d 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -51,8 +51,14 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) { buf := make([]byte, 8) binary.LittleEndian.PutUint64(buf, num) vals = append(vals, buf) + } else if boolean, ok := value.(bool); ok { + if boolean { + vals = append(vals, []byte{0x01}) + } else { + vals = append(vals, []byte{0x00}) + } } else { - return nil, fmt.Errorf("invalid value format at path: %s", location) + return nil, fmt.Errorf("invalid value format at path: %s, type: %s", location, reflect.TypeOf(value).String()) } } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 349ad74bc..e7a3e9230 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -27,10 +27,16 @@ type AccountConstant struct { // AccountLookup dynamically derives an account address from args using a specified location path. type AccountLookup struct { - Name string - Location string - IsSigner bool - IsWritable bool + Name string + Location string + // IsSigner and IsWritable can either be a constant bool or a location to a bool + IsSigner MetaBool + IsWritable MetaBool +} + +type MetaBool struct { + Value bool + Location string } type Seed struct { @@ -89,23 +95,75 @@ func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[str }, nil } -func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { +func (al AccountLookup) Resolve( + _ context.Context, + args any, + _ map[string]map[string][]*solana.AccountMeta, + _ client.Reader, +) ([]*solana.AccountMeta, error) { derivedValues, err := GetValuesAtLocation(args, al.Location) if err != nil { - return nil, fmt.Errorf("error getting account from lookup: %w", err) + return nil, fmt.Errorf("error getting account from '%s': %w", al.Location, err) } var metas []*solana.AccountMeta - for _, address := range derivedValues { + for i, address := range derivedValues { + // Resolve isSigner for this particular pubkey + isSigner, err := resolveMetaBool(al.IsSigner, args, i, len(derivedValues)) + if err != nil { + return nil, err + } + + // Resolve isWritable + isWritable, err := resolveMetaBool(al.IsWritable, args, i, len(derivedValues)) + if err != nil { + return nil, err + } + metas = append(metas, &solana.AccountMeta{ PublicKey: solana.PublicKeyFromBytes(address), - IsSigner: al.IsSigner, - IsWritable: al.IsWritable, + IsSigner: isSigner, + IsWritable: isWritable, }) } + return metas, nil } +func resolveMetaBool(mb MetaBool, args any, pubkeyIndex, pubkeysCount int) (bool, error) { + if mb.Location == "" { + return mb.Value, nil + } + + boolVals, err := GetValuesAtLocation(args, mb.Location) + if err != nil { + return false, fmt.Errorf("error reading bools from location '%s': %w", mb.Location, err) + } + + if len(boolVals) == 0 { + return false, fmt.Errorf("no boolean found at location '%s'", mb.Location) + } + + // boolVals should always equal the number of pubkeys or 1 + if len(boolVals) != pubkeysCount && len(boolVals) != 1 { + return false, fmt.Errorf( + "boolean array length %d doesn't match pubkey count %d for location '%s'", + len(boolVals), pubkeysCount, mb.Location, + ) + } + + // a single boolean value is valid to apply to all pubkeys + data := boolVals[0] + + if len(boolVals) > 1 { + data = boolVals[pubkeyIndex] + } + if len(data) == 0 { + return false, fmt.Errorf("missing data for boolean at index %d in location '%s'", pubkeyIndex, mb.Location) + } + return data[0] != 0, nil +} + func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { // Fetch the inner map for the specified lookup table name innerMap, ok := derivedTableMap[alt.LookupTableName]