Skip to content

Commit 1bd6d2d

Browse files
feat(mcms/analyzer): add Solana native programs to decoder registry (#942)
This pull request enhances the Solana analyzer by adding support for native Solana programs and improving instruction decoding capabilities. It introduces a registry for Solana native programs, ensures their decoders are registered by default, and adds comprehensive support for Anchor IDL instruction decoding. It also adds Go bindings for some native programs that are not available by default in the gagliardetto/solana-go library to the `experimental/analyzer/solana/programs` package. --- OPT-486
1 parent 77f9639 commit 1bd6d2d

43 files changed

Lines changed: 3704 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/moody-ends-bathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
feat(mcms/analyzer): add Solana native programs to registry

experimental/analyzer/registry_solana.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@ package analyzer
22

33
import (
44
"fmt"
5+
"maps"
56

6-
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
7+
"github.com/gagliardetto/solana-go"
8+
9+
computebudget "github.com/gagliardetto/solana-go/programs/compute-budget"
10+
"github.com/gagliardetto/solana-go/programs/memo"
11+
"github.com/gagliardetto/solana-go/programs/stake"
12+
"github.com/gagliardetto/solana-go/programs/system"
13+
"github.com/gagliardetto/solana-go/programs/token"
14+
"github.com/gagliardetto/solana-go/programs/tokenregistry"
15+
"github.com/gagliardetto/solana-go/programs/vote"
716

17+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
818
"github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer/pointer"
19+
"github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer/solana/programs/loaderv3"
20+
verify "github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer/solana/programs/otter_verify"
921
)
1022

1123
// SolanaDecoderRegistry is an interface for retrieving and managing Solana instruction decoders.
@@ -61,8 +73,38 @@ func (reg environmentSolanaRegistry) AddSolanaInstructionDecoder(typeAndVersion
6173
reg.registry[typeAndVersion.String()] = decoder
6274
}
6375

76+
var nativePrograms = map[solana.PublicKey]deployment.TypeAndVersion{
77+
solana.BPFLoaderUpgradeableProgramID: deployment.MustTypeAndVersionFromString("BPFLoaderUpgradeable 1.0.0"),
78+
solana.SystemProgramID: deployment.MustTypeAndVersionFromString("System 1.0.0"),
79+
solana.ComputeBudget: deployment.MustTypeAndVersionFromString("ComputeBudget 1.0.0"),
80+
solana.MemoProgramID: deployment.MustTypeAndVersionFromString("Memo 1.0.0"),
81+
solana.StakeProgramID: deployment.MustTypeAndVersionFromString("Stake 1.0.0"),
82+
solana.TokenProgramID: deployment.MustTypeAndVersionFromString("Token 1.0.0"),
83+
solana.VoteProgramID: deployment.MustTypeAndVersionFromString("Vote 1.0.0"),
84+
verify.ProgramID: deployment.MustTypeAndVersionFromString("OtterVerify 1.0.0"),
85+
tokenregistry.ProgramID(): deployment.MustTypeAndVersionFromString("TokenRegistry 1.0.0"),
86+
}
87+
88+
var nativeDecoderMappings = map[string]DecodeInstructionFn{
89+
nativePrograms[solana.BPFLoaderUpgradeableProgramID].String(): DIFn(loaderv3.DecodeInstruction),
90+
nativePrograms[solana.ComputeBudget].String(): DIFn(computebudget.DecodeInstruction),
91+
nativePrograms[solana.MemoProgramID].String(): DIFn(memo.DecodeInstruction),
92+
nativePrograms[solana.StakeProgramID].String(): DIFn(stake.DecodeInstruction),
93+
nativePrograms[solana.SystemProgramID].String(): DIFn(system.DecodeInstruction),
94+
nativePrograms[solana.TokenProgramID].String(): DIFn(token.DecodeInstruction),
95+
nativePrograms[solana.VoteProgramID].String(): DIFn(vote.DecodeInstruction),
96+
nativePrograms[tokenregistry.ProgramID()].String(): DIFn(tokenregistry.DecodeInstruction),
97+
nativePrograms[verify.ProgramID].String(): DIFn(verify.DecodeInstruction),
98+
}
99+
64100
// NewEnvironmentSolanaRegistry creates a new environmentSolanaRegistry from the provided ABI mappings and domain name.
65101
func NewEnvironmentSolanaRegistry(env deployment.Environment, decoderMappings map[string]DecodeInstructionFn) (*environmentSolanaRegistry, error) {
102+
if decoderMappings == nil {
103+
decoderMappings = map[string]DecodeInstructionFn{}
104+
} else {
105+
decoderMappings = maps.Clone(decoderMappings)
106+
}
107+
66108
addressesByChain, errAddrBook := env.ExistingAddresses.Addresses() //nolint:staticcheck
67109
if errAddrBook != nil {
68110
return nil, errAddrBook
@@ -84,6 +126,15 @@ func NewEnvironmentSolanaRegistry(env deployment.Environment, decoderMappings ma
84126
addressesByChain[address.ChainSelector] = chainAddresses
85127
}
86128

129+
// add native programs and mappings
130+
for chainSelector, addresses := range addressesByChain {
131+
for pk, contractTypeAndVersion := range nativePrograms {
132+
addresses[pk.String()] = contractTypeAndVersion
133+
}
134+
addressesByChain[chainSelector] = addresses
135+
}
136+
maps.Insert(decoderMappings, maps.All(nativeDecoderMappings))
137+
87138
return &environmentSolanaRegistry{
88139
registry: decoderMappings,
89140
env: env,

experimental/analyzer/registry_solana_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package analyzer
22

33
import (
4+
"maps"
5+
"slices"
46
"testing"
57

8+
"github.com/samber/lo"
9+
chainsel "github.com/smartcontractkit/chain-selectors"
610
"github.com/stretchr/testify/require"
711

812
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
@@ -98,7 +102,8 @@ func Test_EnvironmentSolanaRegistry_GetSolanaInstructionDecoderByAddress(t *test
98102
reg, err := NewEnvironmentSolanaRegistry(
99103
deployment.Environment{
100104
ExistingAddresses: deployment.NewMemoryAddressBook(),
101-
DataStore: ds.Seal()},
105+
DataStore: ds.Seal(),
106+
},
102107
tt.registry,
103108
)
104109
require.NoError(t, err)
@@ -162,3 +167,39 @@ func Test_EnvironmentSolanaRegistry_Decoders_ReturnsDefensiveCopy(t *testing.T)
162167
require.NoError(t, getErr)
163168
require.Nil(t, decoder)
164169
}
170+
171+
func Test_EnvironmentSolanaRegistry_NilDecoderMappings(t *testing.T) {
172+
t.Parallel()
173+
174+
reg, err := NewEnvironmentSolanaRegistry(deployment.Environment{
175+
ExistingAddresses: deployment.NewMemoryAddressBookFromMap(map[uint64]map[string]deployment.TypeAndVersion{
176+
chainsel.SOLANA_DEVNET.Selector: {
177+
"TestContract1111111111111111111111111111111": deployment.MustTypeAndVersionFromString("TestContract 1.0.0"),
178+
},
179+
}),
180+
DataStore: datastore.NewMemoryDataStore().Seal(),
181+
}, nil)
182+
require.NoError(t, err)
183+
require.NotNil(t, reg)
184+
185+
require.Equal(t, reg.addressesByChain, deployment.AddressesByChain{ //nolint:testifylint
186+
chainsel.SOLANA_DEVNET.Selector: {
187+
"TestContract1111111111111111111111111111111": deployment.MustTypeAndVersionFromString("TestContract 1.0.0"),
188+
"BPFLoaderUpgradeab1e11111111111111111111111": deployment.MustTypeAndVersionFromString("BPFLoaderUpgradeable 1.0.0"),
189+
"11111111111111111111111111111111": deployment.MustTypeAndVersionFromString("System 1.0.0"),
190+
"ComputeBudget111111111111111111111111111111": deployment.MustTypeAndVersionFromString("ComputeBudget 1.0.0"),
191+
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr": deployment.MustTypeAndVersionFromString("Memo 1.0.0"),
192+
"Stake11111111111111111111111111111111111111": deployment.MustTypeAndVersionFromString("Stake 1.0.0"),
193+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA": deployment.MustTypeAndVersionFromString("Token 1.0.0"),
194+
"Vote111111111111111111111111111111111111111": deployment.MustTypeAndVersionFromString("Vote 1.0.0"),
195+
"verifycLy8mB96wd9wqq3WDXQwM4oU6r42Th37Db9fC": deployment.MustTypeAndVersionFromString("OtterVerify 1.0.0"),
196+
"CmPVzy88JSB4S223yCvFmBxTLobLya27KgEDeNPnqEub": deployment.MustTypeAndVersionFromString("TokenRegistry 1.0.0"),
197+
},
198+
})
199+
require.ElementsMatch(t, slices.Collect(maps.Keys(reg.registry)), []string{
200+
"Memo 1.0.0", "OtterVerify 1.0.0", "BPFLoaderUpgradeable 1.0.0", "ComputeBudget 1.0.0",
201+
"TokenRegistry 1.0.0", "Vote 1.0.0", "Stake 1.0.0", "System 1.0.0", "Token 1.0.0",
202+
})
203+
decodeFns := lo.FilterValues(reg.registry, func(_ string, fn DecodeInstructionFn) bool { return fn != nil })
204+
require.Len(t, decodeFns, len(nativePrograms))
205+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package idl
2+
3+
import (
4+
ag_binary "github.com/gagliardetto/binary"
5+
)
6+
7+
type IDLClose struct{}
8+
9+
func (inst *IDLClose) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error {
10+
return nil
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package idl
2+
3+
import (
4+
ag_binary "github.com/gagliardetto/binary"
5+
)
6+
7+
// IDLCreate instruction
8+
type IDLCreate struct{ DataLen uint64 }
9+
10+
func (inst *IDLCreate) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error {
11+
{
12+
err := decoder.Decode(&inst.DataLen)
13+
if err != nil {
14+
return err
15+
}
16+
}
17+
18+
return nil
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package idl
2+
3+
import (
4+
ag_binary "github.com/gagliardetto/binary"
5+
)
6+
7+
type IDLCreateBuffer struct{}
8+
9+
func (inst *IDLCreateBuffer) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error {
10+
return nil
11+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package idl
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
8+
ag_binary "github.com/gagliardetto/binary"
9+
ag_solanago "github.com/gagliardetto/solana-go"
10+
)
11+
12+
const ProgramName = "AnchorIDL"
13+
14+
// ProgramID is a zero sentinel since IDL instructions are sent to the target program
15+
// rather than a dedicated IDL program address.
16+
var ProgramID ag_solanago.PublicKey
17+
18+
// Discriminator is the fixed 8-byte Anchor discriminator that identifies IDL instructions.
19+
var Discriminator = []byte{64, 244, 188, 120, 167, 233, 105, 10}
20+
21+
const (
22+
InstructionCreate uint8 = iota // One-time initializer for creating the program's IDL account.
23+
InstructionCreateBuffer // Creates a new IDL account buffer. Can be called several times.
24+
InstructionWrite // Appends the given data to the end of the IDL account buffer.
25+
InstructionSetBuffer // Sets a new data buffer for the IdlAccount.
26+
InstructionSetAuthority // Sets a new authority on the IdlAccount.
27+
InstructionClose // Closes the IDL PDA account.
28+
InstructionResize // Increases account size for accounts that need over 10kb.
29+
)
30+
31+
var InstructionImplDef = ag_binary.NewVariantDefinition(
32+
ag_binary.Uint8TypeIDEncoding,
33+
[]ag_binary.VariantType{
34+
{Name: "create", Type: (*IDLCreate)(nil)},
35+
{Name: "create_buffer", Type: (*IDLCreateBuffer)(nil)},
36+
{Name: "write", Type: (*IDLWrite)(nil)},
37+
{Name: "set_buffer", Type: (*IDLSetBuffer)(nil)},
38+
{Name: "set_authority", Type: (*IDLSetAuthority)(nil)},
39+
{Name: "close", Type: (*IDLClose)(nil)},
40+
{Name: "resize", Type: (*IDLResize)(nil)},
41+
},
42+
)
43+
44+
type Instruction struct {
45+
ag_binary.BaseVariant
46+
}
47+
48+
func (inst *Instruction) ProgramID() ag_solanago.PublicKey {
49+
return ProgramID
50+
}
51+
52+
func (inst *Instruction) Accounts() (out []*ag_solanago.AccountMeta) {
53+
if gettable, ok := inst.Impl.(ag_solanago.AccountsGettable); ok {
54+
return gettable.GetAccounts()
55+
}
56+
57+
return nil
58+
}
59+
60+
func (inst *Instruction) Data() ([]byte, error) {
61+
buf := new(bytes.Buffer)
62+
if err := ag_binary.NewBorshEncoder(buf).Encode(inst); err != nil {
63+
return nil, fmt.Errorf("unable to encode IDL instruction: %w", err)
64+
}
65+
66+
return buf.Bytes(), nil
67+
}
68+
69+
func (inst *Instruction) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error {
70+
return inst.UnmarshalBinaryVariant(decoder, InstructionImplDef)
71+
}
72+
73+
func (inst *Instruction) MarshalWithEncoder(encoder *ag_binary.Encoder) error {
74+
if err := encoder.WriteUint8(inst.TypeID.Uint8()); err != nil {
75+
return fmt.Errorf("unable to write variant type: %w", err)
76+
}
77+
78+
return encoder.Encode(inst.Impl)
79+
}
80+
81+
// IsInstruction reports whether data starts with the IDL instruction discriminator.
82+
func IsInstruction(data []byte) bool {
83+
return len(data) >= 9 && bytes.Equal(data[:8], Discriminator)
84+
}
85+
86+
// DecodeInstruction decodes a Solana IDL instruction from the given accounts and data.
87+
// The data must start with the 8-byte IDL discriminator followed by a 1-byte instruction type.
88+
func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instruction, error) {
89+
if !IsInstruction(data) {
90+
return nil, errors.New("not a valid IDL instruction: invalid discriminator or insufficient data length")
91+
}
92+
inst := new(Instruction)
93+
if err := ag_binary.NewBorshDecoder(data[8:]).Decode(inst); err != nil {
94+
return nil, fmt.Errorf("unable to decode IDL instruction: %w", err)
95+
}
96+
if v, ok := inst.Impl.(ag_solanago.AccountsSettable); ok {
97+
if err := v.SetAccounts(accounts); err != nil {
98+
return nil, fmt.Errorf("unable to set accounts for IDL instruction: %w", err)
99+
}
100+
}
101+
102+
return inst, nil
103+
}

0 commit comments

Comments
 (0)