Skip to content

Commit

Permalink
Merge branch 'develop' into NONEVM-916-logpoller-process-decode
Browse files Browse the repository at this point in the history
  • Loading branch information
reductionista authored Jan 17, 2025
2 parents 3094b3c + afa9636 commit ec368e7
Show file tree
Hide file tree
Showing 19 changed files with 590 additions and 54 deletions.
145 changes: 138 additions & 7 deletions integration-tests/relayinterface/lookups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ import (
"github.com/smartcontractkit/chainlink-solana/pkg/solana/utils"
)

type InnerAccountArgs struct {
Accounts []*solana.AccountMeta
Bitmap uint64
}

type TestAccountArgs struct {
Inner InnerAccountArgs
}

func TestAccountContant(t *testing.T) {
t.Run("AccountConstant resolves valid address", func(t *testing.T) {
expectedAddr := chainwriter.GetRandomPubKey(t)
Expand Down Expand Up @@ -62,8 +71,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)
Expand Down Expand Up @@ -96,8 +105,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)
Expand All @@ -117,12 +126,134 @@ 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 bitmap lookups", func(t *testing.T) {
accounts := [3]*solana.AccountMeta{}

for i := 0; i < 3; i++ {
accounts[i] = &solana.AccountMeta{
PublicKey: chainwriter.GetRandomPubKey(t),
IsSigner: (i)%2 == 0,
IsWritable: (i)%2 == 0,
}
}

lookupConfig := chainwriter.AccountLookup{
Name: "InvalidAccount",
Location: "Inner.Accounts.PublicKey",
IsSigner: chainwriter.MetaBool{BitmapLocation: "Inner.Bitmap"},
IsWritable: chainwriter.MetaBool{BitmapLocation: "Inner.Bitmap"},
}

args := TestAccountArgs{
Inner: InnerAccountArgs{
Accounts: accounts[:],
// should be 101... so {true, false, true}
Bitmap: 5,
},
}

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 due to an invalid number of bitmaps", func(t *testing.T) {
type TestAccountArgsExtended struct {
Inner InnerAccountArgs
Bitmaps []uint64
}

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{BitmapLocation: "Bitmaps"},
IsWritable: chainwriter.MetaBool{BitmapLocation: "Bitmaps"},
}

args := TestAccountArgsExtended{
Inner: InnerAccountArgs{
Accounts: accounts[:],
},
Bitmaps: []uint64{5, 3},
}

_, err := lookupConfig.Resolve(ctx, args, nil, nil)
require.Contains(t, err.Error(), "bitmap value is not a single value")
})

t.Run("AccountLookup fails with MetaBool with an Invalid BitmapLocation", 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{BitmapLocation: "Invalid.Bitmap"},
IsWritable: chainwriter.MetaBool{BitmapLocation: "Invalid.Bitmap"},
}

args := TestAccountArgs{
Inner: InnerAccountArgs{
Accounts: accounts[:],
},
}

_, err := lookupConfig.Resolve(ctx, args, nil, nil)
require.Contains(t, err.Error(), "error reading bitmap from location")
})

t.Run("AccountLookup fails when MetaBool Bitmap is an invalid type", 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{BitmapLocation: "Inner"},
IsWritable: chainwriter.MetaBool{BitmapLocation: "Inner"},
}

args := TestAccountArgs{
Inner: InnerAccountArgs{
Accounts: accounts[:],
},
}

_, err := lookupConfig.Resolve(ctx, args, nil, nil)
require.Contains(t, err.Error(), "invalid value format at path")
})
}

func TestPDALookups(t *testing.T) {
Expand Down Expand Up @@ -435,7 +566,7 @@ func TestLookupTables(t *testing.T) {
Accounts: chainwriter.AccountLookup{
Name: "TestLookupTable",
Location: "Inner.Address",
IsSigner: true,
IsSigner: chainwriter.MetaBool{Value: true},
},
},
},
Expand Down
8 changes: 7 additions & 1 deletion pkg/solana/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,12 @@ func (v *verifiedCachedClient) GetAccountInfoWithOpts(ctx context.Context, addr
}

func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.Logger, ds sqlutil.DataSource) (*chain, error) {
lggr = logger.Named(lggr, "Chain")
lggr = logger.With(lggr, "chainID", id, "chain", "solana")
var ch = chain{
id: id,
cfg: cfg,
lggr: logger.Named(lggr, "Chain"),
lggr: lggr,
clientCache: map[string]*verifiedCachedClient{},
}

Expand Down Expand Up @@ -547,6 +548,11 @@ func (c *chain) Ready() error {
func (c *chain) HealthReport() map[string]error {
report := map[string]error{c.Name(): c.Healthy()}
services.CopyHealth(report, c.txm.HealthReport())
services.CopyHealth(report, c.balanceMonitor.HealthReport())
if c.cfg.MultiNode.Enabled() {
report[c.multiNode.Name()] = c.multiNode.Healthy()
report[c.txSender.Name()] = c.txSender.Healthy()
}
return report
}

Expand Down
60 changes: 57 additions & 3 deletions pkg/solana/chainreader/account_read_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chainreader

import (
"context"
"fmt"

"github.com/gagliardetto/solana-go"

Expand All @@ -15,12 +16,16 @@ type accountReadBinding struct {
namespace, genericName string
codec types.RemoteCodec
key solana.PublicKey
isPda bool // flag to signify whether or not the account read is for a PDA
prefix string // only used for PDA public key calculation
}

func newAccountReadBinding(namespace, genericName string) *accountReadBinding {
func newAccountReadBinding(namespace, genericName, prefix string, isPda bool) *accountReadBinding {
return &accountReadBinding{
namespace: namespace,
genericName: genericName,
prefix: prefix,
isPda: isPda,
}
}

Expand All @@ -34,8 +39,21 @@ func (b *accountReadBinding) SetAddress(key solana.PublicKey) {
b.key = key
}

func (b *accountReadBinding) GetAddress() solana.PublicKey {
return b.key
func (b *accountReadBinding) GetAddress(ctx context.Context, params any) (solana.PublicKey, error) {
// Return the bound key if normal account read
if !b.isPda {
return b.key, nil
}
// Calculate the public key if PDA account read
seedBytes, err := b.buildSeedsSlice(ctx, params)
if err != nil {
return solana.PublicKey{}, fmt.Errorf("failed build seeds list for PDA calculation: %w", err)
}
key, _, err := solana.FindProgramAddress(seedBytes, b.key)
if err != nil {
return solana.PublicKey{}, fmt.Errorf("failed find program address for PDA: %w", err)
}
return key, nil
}

func (b *accountReadBinding) CreateType(forEncoding bool) (any, error) {
Expand All @@ -45,3 +63,39 @@ func (b *accountReadBinding) CreateType(forEncoding bool) (any, error) {
func (b *accountReadBinding) Decode(ctx context.Context, bts []byte, outVal any) error {
return b.codec.Decode(ctx, bts, outVal, codec.WrapItemType(false, b.namespace, b.genericName, codec.ChainConfigTypeAccountDef))
}

// buildSeedsSlice encodes and builds the seedslist to calculate the PDA public key
func (b *accountReadBinding) buildSeedsSlice(ctx context.Context, params any) ([][]byte, error) {
flattenedSeeds := make([]byte, 0, solana.MaxSeeds*solana.MaxSeedLength)
// Append the static prefix string first
flattenedSeeds = append(flattenedSeeds, []byte(b.prefix)...)
// Encode the seeds provided in the params
encodedParamSeeds, err := b.codec.Encode(ctx, params, codec.WrapItemType(true, b.namespace, b.genericName, ""))
if err != nil {
return nil, fmt.Errorf("failed to encode params into bytes for PDA seeds: %w", err)
}
// Append the encoded seeds
flattenedSeeds = append(flattenedSeeds, encodedParamSeeds...)

if len(flattenedSeeds) > solana.MaxSeeds*solana.MaxSeedLength {
return nil, fmt.Errorf("seeds exceed the maximum allowed length")
}

// Splitting the seeds since they are expected to be provided separately to FindProgramAddress
// Arbitrarily separating the seeds at max seed length would still yield the same PDA since
// FindProgramAddress appends the seed bytes together under the hood
numSeeds := len(flattenedSeeds) / solana.MaxSeedLength
if len(flattenedSeeds)%solana.MaxSeedLength != 0 {
numSeeds++
}
seedByteArray := make([][]byte, 0, numSeeds)
for i := 0; i < numSeeds; i++ {
startIdx := i * solana.MaxSeedLength
endIdx := startIdx + solana.MaxSeedLength
if endIdx > len(flattenedSeeds) {
endIdx = len(flattenedSeeds)
}
seedByteArray = append(seedByteArray, flattenedSeeds[startIdx:endIdx])
}
return seedByteArray, nil
}
7 changes: 6 additions & 1 deletion pkg/solana/chainreader/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package chainreader
import (
"context"
"errors"
"fmt"

"github.com/gagliardetto/solana-go"

Expand Down Expand Up @@ -38,7 +39,11 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindin
return nil, err
}

keys[idx] = binding.GetAddress()
key, err := binding.GetAddress(ctx, call.Params)
if err != nil {
return nil, fmt.Errorf("failed to get address for %s account read: %w", call.ReadName, err)
}
keys[idx] = key
}

// Fetch the account data
Expand Down
2 changes: 1 addition & 1 deletion pkg/solana/chainreader/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

type readBinding interface {
SetAddress(solana.PublicKey)
GetAddress() solana.PublicKey
GetAddress(context.Context, any) (solana.PublicKey, error)
SetCodec(types.RemoteCodec)
CreateType(bool) (any, error)
Decode(context.Context, []byte, any) error
Expand Down
4 changes: 2 additions & 2 deletions pkg/solana/chainreader/bindings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ func (_m *mockBinding) SetCodec(_ types.RemoteCodec) {}

func (_m *mockBinding) SetAddress(_ solana.PublicKey) {}

func (_m *mockBinding) GetAddress() solana.PublicKey {
return solana.PublicKey{}
func (_m *mockBinding) GetAddress(_ context.Context, _ any) (solana.PublicKey, error) {
return solana.PublicKey{}, nil
}

func (_m *mockBinding) CreateType(b bool) (any, error) {
Expand Down
27 changes: 14 additions & 13 deletions pkg/solana/chainreader/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,26 @@ func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainContra
}

func (s *SolanaChainReaderService) addAccountRead(namespace string, genericName string, idl codec.IDL, idlType codec.IdlTypeDef, readDefinition config.ReadDefinition) error {
inputAccountIDLDef := codec.NilIdlTypeDefTy
// TODO:
// if hasPDA{
// inputAccountIDLDef = pdaType
// }
if err := s.addCodecDef(true, namespace, genericName, codec.ChainConfigTypeAccountDef, idl, inputAccountIDLDef, readDefinition.InputModifications); err != nil {
return err
}

if err := s.addCodecDef(false, namespace, genericName, codec.ChainConfigTypeAccountDef, idl, idlType, readDefinition.OutputModifications); err != nil {
return err
}

s.lookup.addReadNameForContract(namespace, genericName)

s.bindings.AddReadBinding(namespace, genericName, newAccountReadBinding(
namespace,
genericName,
))
var reader readBinding
var inputAccountIDLDef interface{}
// Create PDA read binding if PDA prefix or seeds configs are populated
if len(readDefinition.PDADefiniton.Prefix) > 0 || len(readDefinition.PDADefiniton.Seeds) > 0 {
inputAccountIDLDef = readDefinition.PDADefiniton
reader = newAccountReadBinding(namespace, genericName, readDefinition.PDADefiniton.Prefix, true)
} else {
inputAccountIDLDef = codec.NilIdlTypeDefTy
reader = newAccountReadBinding(namespace, genericName, "", false)
}
if err := s.addCodecDef(true, namespace, genericName, codec.ChainConfigTypeAccountDef, idl, inputAccountIDLDef, readDefinition.InputModifications); err != nil {
return err
}
s.bindings.AddReadBinding(namespace, genericName, reader)

return nil
}
Expand Down
Loading

0 comments on commit ec368e7

Please sign in to comment.