diff --git a/contracts/generated/log_read_test/constants.go b/contracts/generated/log_read_test/constants.go index 82bd54cc9..7cb9ea0ff 100644 --- a/contracts/generated/log_read_test/constants.go +++ b/contracts/generated/log_read_test/constants.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains constants. package log_read_test diff --git a/contracts/generated/log_read_test/discriminators.go b/contracts/generated/log_read_test/discriminators.go index eec9b5d3b..0c2275138 100644 --- a/contracts/generated/log_read_test/discriminators.go +++ b/contracts/generated/log_read_test/discriminators.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains the discriminators for accounts and events defined in the IDL. package log_read_test @@ -14,5 +14,6 @@ var ( // Instruction discriminators var ( Instruction_CreateLog = [8]byte{215, 95, 248, 114, 153, 204, 208, 48} + Instruction_CreateLogCpi = [8]byte{101, 207, 236, 250, 214, 98, 173, 146} Instruction_CreateTruncatedLog = [8]byte{133, 74, 116, 132, 80, 11, 241, 64} ) diff --git a/contracts/generated/log_read_test/doc.go b/contracts/generated/log_read_test/doc.go new file mode 100644 index 000000000..71dc0c602 --- /dev/null +++ b/contracts/generated/log_read_test/doc.go @@ -0,0 +1,7 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains documentation and example usage for the generated code. + +package log_read_test + +// No documentation available from the IDL. +// Please refer to the IDL source or the program documentation for more information. diff --git a/contracts/generated/log_read_test/errors.go b/contracts/generated/log_read_test/errors.go index 022f87126..de8e428d5 100644 --- a/contracts/generated/log_read_test/errors.go +++ b/contracts/generated/log_read_test/errors.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains errors. package log_read_test diff --git a/contracts/generated/log_read_test/events.go b/contracts/generated/log_read_test/events.go index 09499c0cb..c76cbb969 100644 --- a/contracts/generated/log_read_test/events.go +++ b/contracts/generated/log_read_test/events.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains parsers for the events defined in the IDL. package log_read_test diff --git a/contracts/generated/log_read_test/fetchers.go b/contracts/generated/log_read_test/fetchers.go index 918a36e1b..5c2afff0d 100644 --- a/contracts/generated/log_read_test/fetchers.go +++ b/contracts/generated/log_read_test/fetchers.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains fetcher functions. package log_read_test diff --git a/contracts/generated/log_read_test/instructions.go b/contracts/generated/log_read_test/instructions.go index df2540556..8239f6a58 100644 --- a/contracts/generated/log_read_test/instructions.go +++ b/contracts/generated/log_read_test/instructions.go @@ -1,12 +1,12 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. -// This file contains instructions and instruction parsers. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains instructions. package log_read_test import ( "bytes" "fmt" - errors "github.com/Unheilbar/anchor-go/errors" + errors "github.com/gagliardetto/anchor-go/errors" binary "github.com/gagliardetto/binary" solanago "github.com/gagliardetto/solana-go" ) @@ -53,20 +53,22 @@ func NewCreateLogInstruction( ), nil } -// Builds a "create_truncated_log" instruction. -func NewCreateTruncatedLogInstruction( +// Builds a "create_log_cpi" instruction. +func NewCreateLogCpiInstruction( // Params: valueParam uint64, // Accounts: authorityAccount solanago.PublicKey, systemProgramAccount solanago.PublicKey, + eventAuthorityAccount solanago.PublicKey, + programAccount solanago.PublicKey, ) (solanago.Instruction, error) { buf__ := new(bytes.Buffer) enc__ := binary.NewBorshEncoder(buf__) // Encode the instruction discriminator. - err := enc__.WriteBytes(Instruction_CreateTruncatedLog[:], false) + err := enc__.WriteBytes(Instruction_CreateLogCpi[:], false) if err != nil { return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) } @@ -85,6 +87,10 @@ func NewCreateTruncatedLogInstruction( accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) // Account 1 "system_program": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + // Account 2 "event_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(eventAuthorityAccount, false, false)) + // Account 3 "program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) } // Create the instruction. @@ -95,299 +101,44 @@ func NewCreateTruncatedLogInstruction( ), nil } -type CreateLogInstruction struct { - Value uint64 `json:"value"` - - // Accounts: - Authority solanago.PublicKey `json:"authority"` - AuthoritySigner bool `json:"authority_signer"` - SystemProgram solanago.PublicKey `json:"system_program"` -} - -func (obj *CreateLogInstruction) GetDiscriminator() []byte { - return Instruction_CreateLog[:] -} - -// UnmarshalWithDecoder unmarshals the CreateLogInstruction from Borsh-encoded bytes prefixed with its discriminator. -func (obj *CreateLogInstruction) UnmarshalWithDecoder(decoder *binary.Decoder) error { - var err error - // Read the discriminator and check it against the expected value: - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return fmt.Errorf("failed to read instruction discriminator for %s: %w", "CreateLogInstruction", err) - } - if discriminator != Instruction_CreateLog { - return fmt.Errorf("instruction discriminator mismatch for %s: expected %s, got %s", "CreateLogInstruction", Instruction_CreateLog, discriminator) - } - // Deserialize `Value`: - err = decoder.Decode(&obj.Value) - if err != nil { - return err - } - return nil -} - -func (obj *CreateLogInstruction) UnmarshalAccountIndices(buf []byte) ([]uint8, error) { - // UnmarshalAccountIndices decodes account indices from Borsh-encoded bytes - decoder := binary.NewBorshDecoder(buf) - indices := make([]uint8, 0) - index := uint8(0) - var err error - // Decode from authority account index - index = uint8(0) - err = decoder.Decode(&index) - if err != nil { - return nil, fmt.Errorf("failed to decode %s account index: %w", "authority", err) - } - indices = append(indices, index) - // Decode from system_program account index - index = uint8(0) - err = decoder.Decode(&index) - if err != nil { - return nil, fmt.Errorf("failed to decode %s account index: %w", "system_program", err) - } - indices = append(indices, index) - return indices, nil -} - -func (obj *CreateLogInstruction) PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error { - // PopulateFromAccountIndices sets account public keys from indices and account keys array - if len(indices) != 2 { - return fmt.Errorf("mismatch between expected accounts (%d) and provided indices (%d)", 2, len(indices)) - } - indexOffset := 0 - // Set authority account from index - if indices[indexOffset] >= uint8(len(accountKeys)) { - return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "authority", len(accountKeys)-1) - } - obj.Authority = accountKeys[indices[indexOffset]] - indexOffset++ - // Set system_program account from index - if indices[indexOffset] >= uint8(len(accountKeys)) { - return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "system_program", len(accountKeys)-1) - } - obj.SystemProgram = accountKeys[indices[indexOffset]] - indexOffset++ - return nil -} - -func (obj *CreateLogInstruction) GetAccountKeys() []solanago.PublicKey { - keys := make([]solanago.PublicKey, 0) - keys = append(keys, obj.Authority) - keys = append(keys, obj.SystemProgram) - return keys -} - -// Unmarshal unmarshals the CreateLogInstruction from Borsh-encoded bytes prefixed with the discriminator. -func (obj *CreateLogInstruction) Unmarshal(buf []byte) error { - var err error - err = obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) - if err != nil { - return fmt.Errorf("error while unmarshaling CreateLogInstruction: %w", err) - } - return nil -} - -// UnmarshalCreateLogInstruction unmarshals the instruction from Borsh-encoded bytes prefixed with the discriminator. -func UnmarshalCreateLogInstruction(buf []byte) (*CreateLogInstruction, error) { - obj := new(CreateLogInstruction) - var err error - err = obj.Unmarshal(buf) - if err != nil { - return nil, err - } - return obj, nil -} - -type CreateTruncatedLogInstruction struct { - Value uint64 `json:"value"` +// Builds a "create_truncated_log" instruction. +func NewCreateTruncatedLogInstruction( + // Params: + valueParam uint64, // Accounts: - Authority solanago.PublicKey `json:"authority"` - AuthoritySigner bool `json:"authority_signer"` - SystemProgram solanago.PublicKey `json:"system_program"` -} - -func (obj *CreateTruncatedLogInstruction) GetDiscriminator() []byte { - return Instruction_CreateTruncatedLog[:] -} - -// UnmarshalWithDecoder unmarshals the CreateTruncatedLogInstruction from Borsh-encoded bytes prefixed with its discriminator. -func (obj *CreateTruncatedLogInstruction) UnmarshalWithDecoder(decoder *binary.Decoder) error { - var err error - // Read the discriminator and check it against the expected value: - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return fmt.Errorf("failed to read instruction discriminator for %s: %w", "CreateTruncatedLogInstruction", err) - } - if discriminator != Instruction_CreateTruncatedLog { - return fmt.Errorf("instruction discriminator mismatch for %s: expected %s, got %s", "CreateTruncatedLogInstruction", Instruction_CreateTruncatedLog, discriminator) - } - // Deserialize `Value`: - err = decoder.Decode(&obj.Value) - if err != nil { - return err - } - return nil -} - -func (obj *CreateTruncatedLogInstruction) UnmarshalAccountIndices(buf []byte) ([]uint8, error) { - // UnmarshalAccountIndices decodes account indices from Borsh-encoded bytes - decoder := binary.NewBorshDecoder(buf) - indices := make([]uint8, 0) - index := uint8(0) - var err error - // Decode from authority account index - index = uint8(0) - err = decoder.Decode(&index) - if err != nil { - return nil, fmt.Errorf("failed to decode %s account index: %w", "authority", err) - } - indices = append(indices, index) - // Decode from system_program account index - index = uint8(0) - err = decoder.Decode(&index) - if err != nil { - return nil, fmt.Errorf("failed to decode %s account index: %w", "system_program", err) - } - indices = append(indices, index) - return indices, nil -} - -func (obj *CreateTruncatedLogInstruction) PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error { - // PopulateFromAccountIndices sets account public keys from indices and account keys array - if len(indices) != 2 { - return fmt.Errorf("mismatch between expected accounts (%d) and provided indices (%d)", 2, len(indices)) - } - indexOffset := 0 - // Set authority account from index - if indices[indexOffset] >= uint8(len(accountKeys)) { - return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "authority", len(accountKeys)-1) - } - obj.Authority = accountKeys[indices[indexOffset]] - indexOffset++ - // Set system_program account from index - if indices[indexOffset] >= uint8(len(accountKeys)) { - return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "system_program", len(accountKeys)-1) - } - obj.SystemProgram = accountKeys[indices[indexOffset]] - indexOffset++ - return nil -} - -func (obj *CreateTruncatedLogInstruction) GetAccountKeys() []solanago.PublicKey { - keys := make([]solanago.PublicKey, 0) - keys = append(keys, obj.Authority) - keys = append(keys, obj.SystemProgram) - return keys -} - -// Unmarshal unmarshals the CreateTruncatedLogInstruction from Borsh-encoded bytes prefixed with the discriminator. -func (obj *CreateTruncatedLogInstruction) Unmarshal(buf []byte) error { - var err error - err = obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) - if err != nil { - return fmt.Errorf("error while unmarshaling CreateTruncatedLogInstruction: %w", err) - } - return nil -} + authorityAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) -// UnmarshalCreateTruncatedLogInstruction unmarshals the instruction from Borsh-encoded bytes prefixed with the discriminator. -func UnmarshalCreateTruncatedLogInstruction(buf []byte) (*CreateTruncatedLogInstruction, error) { - obj := new(CreateTruncatedLogInstruction) - var err error - err = obj.Unmarshal(buf) + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_CreateTruncatedLog[:], false) if err != nil { - return nil, err - } - return obj, nil -} - -// Instruction interface defines common methods for all instruction types -type Instruction interface { - GetDiscriminator() []byte - - UnmarshalWithDecoder(decoder *binary.Decoder) error - - UnmarshalAccountIndices(buf []byte) ([]uint8, error) - - PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error - - GetAccountKeys() []solanago.PublicKey -} - -// ParseInstruction parses instruction data and optionally populates accounts -// If accountIndicesData is nil or empty, accounts will not be populated -func ParseInstruction(instructionData []byte, accountIndicesData []byte, accountKeys []solanago.PublicKey) (Instruction, error) { - // Validate inputs - if len(instructionData) < 8 { - return nil, fmt.Errorf("instruction data too short: expected at least 8 bytes, got %d", len(instructionData)) + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) } - // Extract discriminator - discriminator := [8]byte{} - copy(discriminator[:], instructionData[0:8]) - // Parse based on discriminator - switch discriminator { - case Instruction_CreateLog: - instruction := new(CreateLogInstruction) - decoder := binary.NewBorshDecoder(instructionData) - err := instruction.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal instruction as CreateLogInstruction: %w", err) - } - if accountIndicesData != nil && len(accountIndicesData) > 0 { - indices, err := instruction.UnmarshalAccountIndices(accountIndicesData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal account indices: %w", err) - } - err = instruction.PopulateFromAccountIndices(indices, accountKeys) - if err != nil { - return nil, fmt.Errorf("failed to populate accounts: %w", err) - } - } - return instruction, nil - case Instruction_CreateTruncatedLog: - instruction := new(CreateTruncatedLogInstruction) - decoder := binary.NewBorshDecoder(instructionData) - err := instruction.UnmarshalWithDecoder(decoder) + { + // Serialize `valueParam`: + err = enc__.Encode(valueParam) if err != nil { - return nil, fmt.Errorf("failed to unmarshal instruction as CreateTruncatedLogInstruction: %w", err) - } - if accountIndicesData != nil && len(accountIndicesData) > 0 { - indices, err := instruction.UnmarshalAccountIndices(accountIndicesData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal account indices: %w", err) - } - err = instruction.PopulateFromAccountIndices(indices, accountKeys) - if err != nil { - return nil, fmt.Errorf("failed to populate accounts: %w", err) - } + return nil, errors.NewField("valueParam", err) } - return instruction, nil - default: - return nil, fmt.Errorf("unknown instruction discriminator: %s", binary.FormatDiscriminator(discriminator)) } -} + accounts__ := solanago.AccountMetaSlice{} -// ParseInstructionTyped parses instruction data and returns a specific instruction type // T must implement the Instruction interface -func ParseInstructionTyped[T Instruction](instructionData []byte, accountIndicesData []byte, accountKeys []solanago.PublicKey) (T, error) { - instruction, err := ParseInstruction(instructionData, accountIndicesData, accountKeys) - if err != nil { - return *new(T), err - } - typed, ok := instruction.(T) - if !ok { - return *new(T), fmt.Errorf("instruction is not of expected type") + // Add the accounts to the instruction. + { + // Account 0 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + // Account 1 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) } - return typed, nil -} - -// ParseInstructionWithoutAccounts parses instruction data without account information -func ParseInstructionWithoutAccounts(instructionData []byte) (Instruction, error) { - return ParseInstruction(instructionData, nil, []solanago.PublicKey{}) -} -// ParseInstructionWithAccounts parses instruction data with account information -func ParseInstructionWithAccounts(instructionData []byte, accountIndicesData []byte, accountKeys []solanago.PublicKey) (Instruction, error) { - return ParseInstruction(instructionData, accountIndicesData, accountKeys) + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil } diff --git a/contracts/generated/log_read_test/program_id.go b/contracts/generated/log_read_test/program-id.go similarity index 73% rename from contracts/generated/log_read_test/program_id.go rename to contracts/generated/log_read_test/program-id.go index fd4b6b67d..62beffb5e 100644 --- a/contracts/generated/log_read_test/program_id.go +++ b/contracts/generated/log_read_test/program-id.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains the program ID. package log_read_test diff --git a/contracts/generated/log_read_test/tests_test.go b/contracts/generated/log_read_test/tests_test.go index 15cd1cea0..115811e52 100644 --- a/contracts/generated/log_read_test/tests_test.go +++ b/contracts/generated/log_read_test/tests_test.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains tests. package log_read_test diff --git a/contracts/generated/log_read_test/types.go b/contracts/generated/log_read_test/types.go index 0cc5a8a47..c161a13e6 100644 --- a/contracts/generated/log_read_test/types.go +++ b/contracts/generated/log_read_test/types.go @@ -1,4 +1,4 @@ -// Code generated by https://github.com/Unheilbar/anchor-go. DO NOT EDIT. +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. // This file contains parsers for the types defined in the IDL. package log_read_test @@ -6,13 +6,13 @@ package log_read_test import ( "bytes" "fmt" - errors "github.com/Unheilbar/anchor-go/errors" + errors "github.com/gagliardetto/anchor-go/errors" binary "github.com/gagliardetto/binary" ) type TestEvent struct { - StrVal string `json:"str_val"` - U64Value uint64 `json:"u64_value"` + StrVal string `json:"strVal"` + U64Value uint64 `json:"u64Value"` } func (obj TestEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { diff --git a/contracts/go.mod b/contracts/go.mod index e791c5152..cc8c617f2 100644 --- a/contracts/go.mod +++ b/contracts/go.mod @@ -6,6 +6,7 @@ require ( github.com/Unheilbar/anchor-go v1.0.3 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/ethereum/go-ethereum v1.17.0 + github.com/gagliardetto/anchor-go v1.0.0 github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/gofuzz v1.2.2 github.com/gagliardetto/solana-go v1.13.0 @@ -123,7 +124,7 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/smartcontractkit/chain-selectors v1.0.97 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 // indirect - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc // indirect + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09 // indirect github.com/smartcontractkit/chainlink-testing-framework/framework v0.14.3 // indirect github.com/smartcontractkit/chainlink-ton v0.0.0-20260219201907-054376f21418 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 // indirect diff --git a/contracts/go.sum b/contracts/go.sum index 0f72e02c7..aadf095c6 100644 --- a/contracts/go.sum +++ b/contracts/go.sum @@ -182,6 +182,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= +github.com/gagliardetto/anchor-go v1.0.0/go.mod h1:X6c9bx9JnmwNiyy8hmV5pAsq1c/zzPvkdzeq9/qmlCg= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= @@ -501,8 +503,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260220192608-a github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260220192608-af6bd538e0ca/go.mod h1:k1aNQP8nPFkSqWJZ4kFQaa1JGlWVIg6Y+XA2++qs6Cg= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 h1:Z4t2ZY+ZyGWxtcXvPr11y4o3CGqhg3frJB5jXkCSvWA= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc h1:LQBkqJbsduX1O8ijoSmr4DLMvUeAH41UZkV0AThhFyQ= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc/go.mod h1:HXgSKzmZ/bhSx8nHU7hHW6dR+BHSXkdcpFv2T8qJcS8= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09 h1:arMdoV8OMwJ/xN8Zn7mxs19hMK547iXK1Ln4JUFH3g4= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09/go.mod h1:AvurM0bnW/WJdxKD1kYF14aNX/+Pf2xZWp7wKeMl+Bo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= github.com/smartcontractkit/chainlink-deployments-framework v0.81.2 h1:rEPEywuLv6YPIN6YhXJpReapVM+bPOOLXlpJwItlTu0= diff --git a/contracts/programs/log-read-test/Cargo.toml b/contracts/programs/log-read-test/Cargo.toml index 13f2106f3..6be59f58a 100644 --- a/contracts/programs/log-read-test/Cargo.toml +++ b/contracts/programs/log-read-test/Cargo.toml @@ -17,4 +17,4 @@ default = [] idl-build = ["anchor-lang/idl-build"] [dependencies] -anchor-lang = "0.31.0" +anchor-lang = { version = "0.31.0", features = ["event-cpi"] } diff --git a/contracts/programs/log-read-test/src/lib.rs b/contracts/programs/log-read-test/src/lib.rs index ba9814492..045f9d0b1 100644 --- a/contracts/programs/log-read-test/src/lib.rs +++ b/contracts/programs/log-read-test/src/lib.rs @@ -29,6 +29,14 @@ pub mod log_read_test { Ok(()) } + + pub fn create_log_cpi(ctx: Context, value: u64) -> Result<()> { + emit_cpi!(event::TestEvent { + str_val: "Hello, CPI!".to_string(), + u64_value: value, + }); + Ok(()) + } } #[derive(Accounts)] @@ -36,3 +44,10 @@ pub struct Initialization<'info> { pub authority: Signer<'info>, pub system_program: Program<'info, System>, } + +#[event_cpi] +#[derive(Accounts)] +pub struct CreateLogCpi<'info> { + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} diff --git a/go.mod b/go.mod index 944c3a218..f803af1bc 100644 --- a/go.mod +++ b/go.mod @@ -22,12 +22,12 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260203202624-5101f4d33736 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09 github.com/smartcontractkit/chainlink-common/keystore v1.0.0 github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340 github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251210101658-1c5c8e4c4f15 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260217043601-5cc966896c4f + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260311180747-73ee31ef1766 github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e github.com/smartcontractkit/libocr v0.0.0-20251212213002-0a5e2f907dda github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index fed8beed2..9cd808535 100644 --- a/go.sum +++ b/go.sum @@ -565,8 +565,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-8 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d/go.mod h1:bgmqE7x9xwmIVr8PqLbC0M5iPm4AV2DBl596lO6S5Sw= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 h1:Z4t2ZY+ZyGWxtcXvPr11y4o3CGqhg3frJB5jXkCSvWA= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc h1:LQBkqJbsduX1O8ijoSmr4DLMvUeAH41UZkV0AThhFyQ= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc/go.mod h1:HXgSKzmZ/bhSx8nHU7hHW6dR+BHSXkdcpFv2T8qJcS8= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09 h1:arMdoV8OMwJ/xN8Zn7mxs19hMK547iXK1Ln4JUFH3g4= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09/go.mod h1:AvurM0bnW/WJdxKD1kYF14aNX/+Pf2xZWp7wKeMl+Bo= github.com/smartcontractkit/chainlink-common/keystore v1.0.0 h1:sVa3j2FWK/5OxXpnlfDkF1deDAkuXEfaLKzYqBTA880= github.com/smartcontractkit/chainlink-common/keystore v1.0.0/go.mod h1:wGRJJlCFUOKIfBlBGEdBFiTf7R787B8HKyobjiymw3Q= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -577,8 +577,8 @@ github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251210101658-1c github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251210101658-1c5c8e4c4f15/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 h1:T/eCDsUI8EJT4n5zSP4w1mz4RHH+ap8qieA17QYfBhk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260217043601-5cc966896c4f h1:MHlgzqiDPyDV397bZkzS9TtWXb3FR9Pb8FR9cP9h0As= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260311180747-73ee31ef1766 h1:6dCPm7XRRhYmYtoHzxbsSBrbgE5c/lk2RZMK88m/o+c= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260311180747-73ee31ef1766/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 32bb4f04b..f856ab9d7 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -19,8 +19,8 @@ require ( github.com/rs/zerolog v1.34.0 github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260220192608-af6bd538e0ca - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260217043601-5cc966896c4f + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260311180747-73ee31ef1766 github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260223222711-2fa6b0e07db0 github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.7 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 0b285592f..de1f63a6c 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1351,8 +1351,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260224105024-807568ff394d h1:eTMWOP3Q91Qp7b+D2nuh9k2WHct57tBxhn+524YFJq0= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260224105024-807568ff394d/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc h1:LQBkqJbsduX1O8ijoSmr4DLMvUeAH41UZkV0AThhFyQ= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260225165959-ca7f453e0dcc/go.mod h1:HXgSKzmZ/bhSx8nHU7hHW6dR+BHSXkdcpFv2T8qJcS8= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09 h1:arMdoV8OMwJ/xN8Zn7mxs19hMK547iXK1Ln4JUFH3g4= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317155144-2b367262ff09/go.mod h1:AvurM0bnW/WJdxKD1kYF14aNX/+Pf2xZWp7wKeMl+Bo= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1389,8 +1389,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260217043601-5cc966896c4f h1:MHlgzqiDPyDV397bZkzS9TtWXb3FR9Pb8FR9cP9h0As= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260311180747-73ee31ef1766 h1:6dCPm7XRRhYmYtoHzxbsSBrbgE5c/lk2RZMK88m/o+c= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260311180747-73ee31ef1766/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 h1:xHPmFDhff7QpeFxKsZfk+24j4AlnQiFjjRh5O87Peu4= github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= diff --git a/pkg/solana/logpoller/cpi_event_extractor.go b/pkg/solana/logpoller/cpi_event_extractor.go index c71b96aec..56ce454b7 100644 --- a/pkg/solana/logpoller/cpi_event_extractor.go +++ b/pkg/solana/logpoller/cpi_event_extractor.go @@ -15,9 +15,10 @@ import ( ) const ( - MethodDiscriminatorLen = 8 - VecLengthPrefixLen = 4 - CPIEventDataOffset = MethodDiscriminatorLen + VecLengthPrefixLen + MethodDiscriminatorLen = 8 + VecLengthPrefixLen = 4 + CPIEventDataOffsetLegacy = MethodDiscriminatorLen + VecLengthPrefixLen + CPIEventDataOffsetCurrent = MethodDiscriminatorLen ) type cpiFilterKey struct { @@ -112,8 +113,9 @@ func (e *CPIEventExtractor) ExtractCPIEvents( continue } + outerProgram := types.PublicKey(allAccountKeys[outerInstruction.ProgramIDIndex]) programAtStackHeight := map[uint16]types.PublicKey{ - 1: types.PublicKey(allAccountKeys[outerInstruction.ProgramIDIndex]), + 1: outerProgram, } for _, ix := range inner.Instructions { @@ -123,43 +125,38 @@ func (e *CPIEventExtractor) ExtractCPIEvents( } destProgram := types.PublicKey(allAccountKeys[ix.ProgramIDIndex]) - programAtStackHeight[ix.StackHeight] = destProgram - if len(ix.Data) < CPIEventDataOffset { - e.lggr.Warnw("data shorter than cpiEventDataOffset", "dataLen", len(ix.Data), "required", CPIEventDataOffset) - continue + if ix.StackHeight > 0 { + programAtStackHeight[ix.StackHeight] = destProgram } - declaredLen := bin.LittleEndian.Uint32(ix.Data[MethodDiscriminatorLen:CPIEventDataOffset]) - if declaredLen == 0 { - e.lggr.Warnw("cpi event vec length is zero", - "sourceProgram", programAtStackHeight[ix.StackHeight-1].ToSolana().String(), - "destProgram", allAccountKeys[ix.ProgramIDIndex].String(), - ) + if len(ix.Data) <= MethodDiscriminatorLen { continue } - remaining := len(ix.Data) - CPIEventDataOffset - if int(declaredLen) > remaining { - e.lggr.Warnw("cpi event vec length exceeds remaining bytes", - "declaredLen", declaredLen, "remaining", remaining, - "sourceProgram", programAtStackHeight[ix.StackHeight-1].ToSolana().String(), - "destProgram", allAccountKeys[ix.ProgramIDIndex].String(), - ) - continue - } + methodSig := types.EventSignature(ix.Data[:MethodDiscriminatorLen]) - if int(declaredLen) != remaining { - e.lggr.Warnw("cpi event vec length does not match remaining bytes", - "declaredLen", declaredLen, "remaining", remaining, - "sourceProgram", programAtStackHeight[ix.StackHeight-1].ToSolana().String(), - "destProgram", allAccountKeys[ix.ProgramIDIndex].String(), - ) + var eventData []byte + var ok bool + if methodSig == types.AnchorCPIEventDiscriminator() { + eventData, ok = extractAnchorCPIEventData(e.lggr, ix.Data) + } else { + eventData, ok = extractVecCPIEventData(e.lggr, ix.Data, allAccountKeys, ix, programAtStackHeight) + } + if !ok || len(eventData) == 0 { continue } - methodSig := types.EventSignature(ix.Data[:MethodDiscriminatorLen]) - - if ix.StackHeight <= 1 { + // Determine the source program: use StackHeight tracking when available, + // fall back to the outer instruction's program when StackHeight is missing (0). + var sourceProgram types.PublicKey + if ix.StackHeight > 1 { + sp, ok := programAtStackHeight[ix.StackHeight-1] + if !ok { + e.lggr.Warnw("could not find caller for instruction", "stackHeight", ix.StackHeight) + continue + } + sourceProgram = sp + } else if ix.StackHeight == 1 { e.lggr.Warnw("unexpected stack height for inner instruction", "ix", ix, "destProgram", destProgram.ToSolana(), @@ -167,12 +164,8 @@ func (e *CPIEventExtractor) ExtractCPIEvents( "innerIndex", inner.Index, ) continue - } - - sourceProgram, ok := programAtStackHeight[ix.StackHeight-1] - if !ok { - e.lggr.Warnw("could not find caller for instruction", "stackHeight", ix.StackHeight) - continue + } else { + sourceProgram = outerProgram } key := cpiFilterKey{ @@ -185,7 +178,6 @@ func (e *CPIEventExtractor) ExtractCPIEvents( continue } - eventData := ix.Data[CPIEventDataOffset : CPIEventDataOffset+int(declaredLen)] encodedData := base64.StdEncoding.EncodeToString(eventData) e.lggr.Infow("Found CPI event", @@ -219,6 +211,63 @@ func (e *CPIEventExtractor) ExtractCPIEvents( return events } +// extractAnchorCPIEventData handles Anchor 0.31+ emit_cpi! format: [method_disc(8)][event_data(N)]. +// Event data directly follows the 8-byte method discriminator with no vec prefix. +func extractAnchorCPIEventData(lggr logger.SugaredLogger, data []byte) ([]byte, bool) { + if len(data) <= CPIEventDataOffsetCurrent { + lggr.Warnw("anchor CPI event data shorter than method discriminator", "dataLen", len(data), "required", CPIEventDataOffsetCurrent+1) + return nil, false + } + return data[CPIEventDataOffsetCurrent:], true +} + +// extractVecCPIEventData handles the Borsh Vec format used by CCIP's cpi_event and +// Anchor <=0.29: [method_disc(8)][vec_len(4)][event_data(N)]. +// Validation is strict: declaredLen must be >0 and must exactly equal the remaining bytes. +// Returns (nil, false) on any mismatch -- no fallback. +func extractVecCPIEventData( + lggr logger.SugaredLogger, + data []byte, + allAccountKeys []solana.PublicKey, + ix rpc.CompiledInstruction, + programAtStackHeight map[uint16]types.PublicKey, +) ([]byte, bool) { + if len(data) < CPIEventDataOffsetLegacy { + lggr.Warnw("data shorter than cpiEventDataOffset", "dataLen", len(data), "required", CPIEventDataOffsetLegacy) + return nil, false + } + + declaredLen := bin.LittleEndian.Uint32(data[MethodDiscriminatorLen:CPIEventDataOffsetLegacy]) + if declaredLen == 0 { + lggr.Warnw("cpi event vec length is zero", + "sourceProgram", programAtStackHeight[ix.StackHeight-1].ToSolana().String(), + "destProgram", allAccountKeys[ix.ProgramIDIndex].String(), + ) + return nil, false + } + + remaining := len(data) - CPIEventDataOffsetLegacy + if int(declaredLen) > remaining { + lggr.Warnw("cpi event vec length exceeds remaining bytes", + "declaredLen", declaredLen, "remaining", remaining, + "sourceProgram", programAtStackHeight[ix.StackHeight-1].ToSolana().String(), + "destProgram", allAccountKeys[ix.ProgramIDIndex].String(), + ) + return nil, false + } + + if int(declaredLen) != remaining { + lggr.Warnw("cpi event vec length does not match remaining bytes", + "declaredLen", declaredLen, "remaining", remaining, + "sourceProgram", programAtStackHeight[ix.StackHeight-1].ToSolana().String(), + "destProgram", allAccountKeys[ix.ProgramIDIndex].String(), + ) + return nil, false + } + + return data[CPIEventDataOffsetLegacy:], true +} + func getAllAccountKeys(tx *solana.Transaction, meta *rpc.TransactionMeta) []solana.PublicKey { if tx == nil { return nil diff --git a/pkg/solana/logpoller/cpi_event_extractor_test.go b/pkg/solana/logpoller/cpi_event_extractor_test.go index 062bb0e15..5af5cbc29 100644 --- a/pkg/solana/logpoller/cpi_event_extractor_test.go +++ b/pkg/solana/logpoller/cpi_event_extractor_test.go @@ -493,4 +493,318 @@ func TestCPIEventExtractor_ExtractCPIEvents(t *testing.T) { require.Len(t, events, 1) require.Equal(t, sourceProgram.ToSolana().String(), events[0].Program) }) + + t.Run("extracts Anchor emit_cpi event using direct format", func(t *testing.T) { + extractor := NewCPIEventExtractor(logger.Sugared(logger.Test(t))) + + sourceProgram := newRandomPublicKey(t) + destProgram := newRandomPublicKey(t) + anchorMethodSig := types.AnchorCPIEventDiscriminator() + eventSig := newRandomEventSignature(t) + + filter := types.Filter{ + ID: 1, + Name: "anchor-cpi-filter", + Address: sourceProgram, + EventSig: eventSig, + ExtraFilterConfig: types.ExtraFilterConfig{ + DestProgram: destProgram, + MethodSignature: anchorMethodSig, + }, + } + extractor.AddFilter(filter) + + eventPayload := []byte{0xDE, 0xAD, 0xBE, 0xEF} + eventData := append(eventSig[:], eventPayload...) + innerInstData := append(anchorMethodSig[:], eventData...) + + tx := &solana.Transaction{ + Message: solana.Message{ + AccountKeys: []solana.PublicKey{ + solana.PublicKey(sourceProgram), + solana.PublicKey(destProgram), + }, + Instructions: []solana.CompiledInstruction{ + {ProgramIDIndex: 0}, + }, + }, + } + + meta := &rpc.TransactionMeta{ + InnerInstructions: []rpc.InnerInstruction{ + { + Index: 0, + Instructions: []rpc.CompiledInstruction{ + { + ProgramIDIndex: 1, + Data: innerInstData, + StackHeight: 2, + }, + }, + }, + }, + } + + detail := eventDetail{slotNumber: 200} + events := extractor.ExtractCPIEvents(tx, meta, detail, 0) + + require.Len(t, events, 1) + require.True(t, events[0].IsCPI) + require.Equal(t, sourceProgram.ToSolana().String(), events[0].Program) + + decodedData, err := base64.StdEncoding.DecodeString(events[0].Data) + require.NoError(t, err) + require.Equal(t, eventData, decodedData) + }) + + t.Run("rejects vec-format CPI event with mismatched declared length", func(t *testing.T) { + extractor := NewCPIEventExtractor(logger.Sugared(logger.Test(t))) + + sourceProgram := newRandomPublicKey(t) + destProgram := newRandomPublicKey(t) + methodSig := newRandomEventSignature(t) + + filter := types.Filter{ + ID: 1, + Name: "ccip-cpi-filter", + Address: sourceProgram, + EventSig: newRandomEventSignature(t), + ExtraFilterConfig: types.ExtraFilterConfig{ + DestProgram: destProgram, + MethodSignature: methodSig, + }, + } + extractor.AddFilter(filter) + + eventData := []byte{0xAA, 0xBB, 0xCC, 0xDD} + wrongLen := make([]byte, 4) + binary.LittleEndian.PutUint32(wrongLen, uint32(len(eventData)+99)) //nolint:gosec + innerInstData := append(methodSig[:], append(wrongLen, eventData...)...) + + tx := &solana.Transaction{ + Message: solana.Message{ + AccountKeys: []solana.PublicKey{ + solana.PublicKey(sourceProgram), + solana.PublicKey(destProgram), + }, + Instructions: []solana.CompiledInstruction{ + {ProgramIDIndex: 0}, + }, + }, + } + + meta := &rpc.TransactionMeta{ + InnerInstructions: []rpc.InnerInstruction{ + { + Index: 0, + Instructions: []rpc.CompiledInstruction{ + { + ProgramIDIndex: 1, + Data: innerInstData, + StackHeight: 2, + }, + }, + }, + }, + } + + detail := eventDetail{slotNumber: 100} + events := extractor.ExtractCPIEvents(tx, meta, detail, 0) + require.Empty(t, events) + }) + + t.Run("rejects vec-format CPI event with zero declared length", func(t *testing.T) { + extractor := NewCPIEventExtractor(logger.Sugared(logger.Test(t))) + + sourceProgram := newRandomPublicKey(t) + destProgram := newRandomPublicKey(t) + methodSig := newRandomEventSignature(t) + + filter := types.Filter{ + ID: 1, + Name: "ccip-cpi-filter", + Address: sourceProgram, + EventSig: newRandomEventSignature(t), + ExtraFilterConfig: types.ExtraFilterConfig{ + DestProgram: destProgram, + MethodSignature: methodSig, + }, + } + extractor.AddFilter(filter) + + eventData := []byte{0xAA, 0xBB, 0xCC, 0xDD} + zeroLen := []byte{0x00, 0x00, 0x00, 0x00} + innerInstData := append(methodSig[:], append(zeroLen, eventData...)...) + + tx := &solana.Transaction{ + Message: solana.Message{ + AccountKeys: []solana.PublicKey{ + solana.PublicKey(sourceProgram), + solana.PublicKey(destProgram), + }, + Instructions: []solana.CompiledInstruction{ + {ProgramIDIndex: 0}, + }, + }, + } + + meta := &rpc.TransactionMeta{ + InnerInstructions: []rpc.InnerInstruction{ + { + Index: 0, + Instructions: []rpc.CompiledInstruction{ + { + ProgramIDIndex: 1, + Data: innerInstData, + StackHeight: 2, + }, + }, + }, + }, + } + + detail := eventDetail{slotNumber: 100} + events := extractor.ExtractCPIEvents(tx, meta, detail, 0) + require.Empty(t, events) + }) + + t.Run("falls back to outer program when StackHeight is zero", func(t *testing.T) { + extractor := NewCPIEventExtractor(logger.Sugared(logger.Test(t))) + + outerProgram := newRandomPublicKey(t) + destProgram := newRandomPublicKey(t) + methodSig := newRandomEventSignature(t) + + filter := types.Filter{ + ID: 1, + Name: "cpi-filter-stackheight-zero", + Address: outerProgram, + EventSig: newRandomEventSignature(t), + ExtraFilterConfig: types.ExtraFilterConfig{ + DestProgram: destProgram, + MethodSignature: methodSig, + }, + } + extractor.AddFilter(filter) + + eventData := []byte{0xCA, 0xFE, 0xBA, 0xBE} + vecLengthPrefix := make([]byte, 4) + binary.LittleEndian.PutUint32(vecLengthPrefix, uint32(len(eventData))) //nolint:gosec + innerInstData := append(methodSig[:], append(vecLengthPrefix, eventData...)...) + + tx := &solana.Transaction{ + Message: solana.Message{ + AccountKeys: []solana.PublicKey{ + solana.PublicKey(outerProgram), + solana.PublicKey(destProgram), + }, + Instructions: []solana.CompiledInstruction{ + {ProgramIDIndex: 0}, + }, + }, + } + + meta := &rpc.TransactionMeta{ + InnerInstructions: []rpc.InnerInstruction{ + { + Index: 0, + Instructions: []rpc.CompiledInstruction{ + { + ProgramIDIndex: 1, + Data: innerInstData, + StackHeight: 0, + }, + }, + }, + }, + } + + detail := eventDetail{slotNumber: 100} + events := extractor.ExtractCPIEvents(tx, meta, detail, 0) + + require.Len(t, events, 1) + require.True(t, events[0].IsCPI) + require.Equal(t, outerProgram.ToSolana().String(), events[0].Program) + }) +} + +func TestExtractAnchorCPIEventData(t *testing.T) { + lggr := logger.Sugared(logger.Test(t)) + + t.Run("returns event data after discriminator", func(t *testing.T) { + disc := types.AnchorCPIEventDiscriminator() + payload := []byte{0x01, 0x02, 0x03, 0x04} + data := append(disc[:], payload...) + + result, ok := extractAnchorCPIEventData(lggr, data) + require.True(t, ok) + require.Equal(t, payload, result) + }) + + t.Run("rejects data too short", func(t *testing.T) { + data := make([]byte, MethodDiscriminatorLen) + result, ok := extractAnchorCPIEventData(lggr, data) + require.False(t, ok) + require.Nil(t, result) + }) +} + +func TestExtractVecCPIEventData(t *testing.T) { + lggr := logger.Sugared(logger.Test(t)) + + sourceProgram := newRandomPublicKey(t) + destProgram := newRandomPublicKey(t) + allAccountKeys := []solana.PublicKey{ + solana.PublicKey(sourceProgram), + solana.PublicKey(destProgram), + } + programAtStackHeight := map[uint16]types.PublicKey{ + 1: sourceProgram, + } + ix := rpc.CompiledInstruction{ + ProgramIDIndex: 1, + StackHeight: 2, + } + + t.Run("returns event data with valid vec prefix", func(t *testing.T) { + disc := make([]byte, MethodDiscriminatorLen) + payload := []byte{0xAA, 0xBB, 0xCC, 0xDD} + vecLen := make([]byte, 4) + binary.LittleEndian.PutUint32(vecLen, uint32(len(payload))) //nolint:gosec + data := append(disc, append(vecLen, payload...)...) + + result, ok := extractVecCPIEventData(lggr, data, allAccountKeys, ix, programAtStackHeight) + require.True(t, ok) + require.Equal(t, payload, result) + }) + + t.Run("rejects mismatched declared length", func(t *testing.T) { + disc := make([]byte, MethodDiscriminatorLen) + payload := []byte{0xAA, 0xBB, 0xCC, 0xDD} + vecLen := make([]byte, 4) + binary.LittleEndian.PutUint32(vecLen, uint32(len(payload)+10)) //nolint:gosec + data := append(disc, append(vecLen, payload...)...) + + result, ok := extractVecCPIEventData(lggr, data, allAccountKeys, ix, programAtStackHeight) + require.False(t, ok) + require.Nil(t, result) + }) + + t.Run("rejects zero declared length", func(t *testing.T) { + disc := make([]byte, MethodDiscriminatorLen) + payload := []byte{0xAA, 0xBB, 0xCC, 0xDD} + vecLen := []byte{0x00, 0x00, 0x00, 0x00} + data := append(disc, append(vecLen, payload...)...) + + result, ok := extractVecCPIEventData(lggr, data, allAccountKeys, ix, programAtStackHeight) + require.False(t, ok) + require.Nil(t, result) + }) + + t.Run("rejects data shorter than legacy offset", func(t *testing.T) { + data := make([]byte, CPIEventDataOffsetLegacy-1) + result, ok := extractVecCPIEventData(lggr, data, allAccountKeys, ix, programAtStackHeight) + require.False(t, ok) + require.Nil(t, result) + }) } diff --git a/pkg/solana/logpoller/types/types.go b/pkg/solana/logpoller/types/types.go index 7e3ab6edf..848675421 100644 --- a/pkg/solana/logpoller/types/types.go +++ b/pkg/solana/logpoller/types/types.go @@ -2,6 +2,7 @@ package types import ( "context" + "crypto/sha256" "database/sql/driver" "encoding/binary" "encoding/json" @@ -18,6 +19,10 @@ import ( codecv1 "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/v1" ) +// AnchorCPIMethodName is the method name used by Anchor's emit_cpi! macro. +// The discriminator is the first 8 bytes of SHA256("anchor:event"). +const AnchorCPIMethodName = "anchor:event" + type PublicKey solana.PublicKey // Scan implements Scanner for database/sql. @@ -129,6 +134,19 @@ func NewMethodSignatureFromName(methodName string) EventSignature { return EventSignature(solcommoncodec.NewMethodDiscriminatorHashPrefix(methodName)) } +// AnchorCPIEventDiscriminator returns the 8-byte instruction discriminator used by +// Anchor's emit_cpi! macro. Anchor computes SHA256("anchor:event"), interprets the +// first 8 bytes as a big-endian u64, and writes it on-chain as little-endian. +// This matches Anchor's EVENT_IX_TAG_LE constant. +func AnchorCPIEventDiscriminator() EventSignature { + sum := sha256.Sum256([]byte(AnchorCPIMethodName)) + var sig EventSignature + for i := range EventSignatureLength { + sig[i] = sum[EventSignatureLength-1-i] + } + return sig +} + // Scan implements Scanner for database/sql. func (s *EventSignature) Scan(src interface{}) error { return scanFixedLengthArray("EventSignature", EventSignatureLength, src, s[:]) diff --git a/pkg/solana/logpoller/types/types_test.go b/pkg/solana/logpoller/types/types_test.go new file mode 100644 index 000000000..d02658bab --- /dev/null +++ b/pkg/solana/logpoller/types/types_test.go @@ -0,0 +1,23 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDiscriminatorStability(t *testing.T) { + t.Run("AnchorCPIEventDiscriminator matches Anchor EVENT_IX_TAG_LE", func(t *testing.T) { + expected := EventSignature{0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d} + require.Equal(t, expected, AnchorCPIEventDiscriminator()) + }) + + t.Run("cpiEvent method signature matches CCIP RMN Remote Instruction_CpiEvent", func(t *testing.T) { + expected := EventSignature{0xbc, 0xd8, 0xa6, 0x6c, 0x1a, 0xa6, 0x8e, 0xb6} + require.Equal(t, expected, NewMethodSignatureFromName("cpiEvent")) + }) + + t.Run("AnchorCPIEventDiscriminator differs from cpiEvent method signature", func(t *testing.T) { + require.NotEqual(t, AnchorCPIEventDiscriminator(), NewMethodSignatureFromName("cpiEvent")) + }) +} diff --git a/pkg/solana/solana_service.go b/pkg/solana/solana_service.go index a0e1347ab..a59abcb4f 100644 --- a/pkg/solana/solana_service.go +++ b/pkg/solana/solana_service.go @@ -648,7 +648,7 @@ func convertSolPubKeysToCommon(keys []solana.PublicKey) []commonsol.PublicKey { } func convertFilter(f commonsol.LPFilterQuery) (logpollertypes.Filter, error) { - return logpollertypes.Filter{ + filter := logpollertypes.Filter{ Name: f.Name, Address: logpollertypes.PublicKey(f.Address), EventName: f.EventName, @@ -659,7 +659,27 @@ func convertFilter(f commonsol.LPFilterQuery) (logpollertypes.Filter, error) { Retention: f.Retention, MaxLogsKept: f.MaxLogsKept, IncludeReverted: f.IncludeReverted, - }, nil + } + + if f.CPIFilterConfig != nil { + if len(f.CPIFilterConfig.DestAddress) != solana.PublicKeyLength { + return logpollertypes.Filter{}, fmt.Errorf("invalid CPI filter dest address length: expected %d, got %d", solana.PublicKeyLength, len(f.CPIFilterConfig.DestAddress)) + } + var destProgram logpollertypes.PublicKey + copy(destProgram[:], f.CPIFilterConfig.DestAddress[:]) + methodSig := logpollertypes.EventSignature{} + if f.CPIFilterConfig.MethodName == logpollertypes.AnchorCPIMethodName { + methodSig = logpollertypes.AnchorCPIEventDiscriminator() + } else { + methodSig = logpollertypes.NewMethodSignatureFromName(f.CPIFilterConfig.MethodName) + } + filter.ExtraFilterConfig = logpollertypes.ExtraFilterConfig{ + DestProgram: destProgram, + MethodSignature: methodSig, + } + } + + return filter, nil } func convertAccounts(accs []*rpc.Account) ([]*commonsol.Account, error) {