diff --git a/core/capabilities/ccip/ccipsolana/commitcodec.go b/core/capabilities/ccip/ccipsolana/commitcodec.go index 06f8b7d7845..506cdae3665 100644 --- a/core/capabilities/ccip/ccipsolana/commitcodec.go +++ b/core/capabilities/ccip/ccipsolana/commitcodec.go @@ -28,17 +28,23 @@ func (c *CommitPluginCodecV1) Encode(ctx context.Context, report cciptypes.Commi encoder := agbinary.NewBorshEncoder(&buf) combinedRoots := report.BlessedMerkleRoots combinedRoots = append(combinedRoots, report.UnblessedMerkleRoots...) - if len(combinedRoots) != 1 { - return nil, fmt.Errorf("unexpected merkle root length in report: %d", len(combinedRoots)) - } + var mr *ccip_offramp.MerkleRoot + switch len(combinedRoots) { + case 0: + // price updates only, zero the root + case 1: + // valid + merkleRoot := combinedRoots[0] + mr = &ccip_offramp.MerkleRoot{ + SourceChainSelector: uint64(merkleRoot.ChainSel), + OnRampAddress: merkleRoot.OnRampAddress, + MinSeqNr: uint64(merkleRoot.SeqNumsRange.Start()), + MaxSeqNr: uint64(merkleRoot.SeqNumsRange.End()), + MerkleRoot: merkleRoot.MerkleRoot, + } - merkleRoot := combinedRoots[0] - mr := &ccip_offramp.MerkleRoot{ - SourceChainSelector: uint64(merkleRoot.ChainSel), - OnRampAddress: merkleRoot.OnRampAddress, - MinSeqNr: uint64(merkleRoot.SeqNumsRange.Start()), - MaxSeqNr: uint64(merkleRoot.SeqNumsRange.End()), - MerkleRoot: merkleRoot.MerkleRoot, + default: + return nil, fmt.Errorf("unexpected merkle root length in report: %d", len(combinedRoots)) } tpu := make([]ccip_offramp.TokenPriceUpdate, 0, len(report.PriceUpdates.TokenPriceUpdates)) @@ -110,8 +116,10 @@ func (c *CommitPluginCodecV1) Decode(ctx context.Context, bytes []byte) (cciptyp return cciptypes.CommitPluginReport{}, err } - merkleRoots := []cciptypes.MerkleRootChain{ - { + merkleRoots := make([]cciptypes.MerkleRootChain, 0, 1) + // if the merkle root is nil, ignore it + if commitReport.MerkleRoot != nil { + merkleRoots = append(merkleRoots, cciptypes.MerkleRootChain{ ChainSel: cciptypes.ChainSelector(commitReport.MerkleRoot.SourceChainSelector), OnRampAddress: commitReport.MerkleRoot.OnRampAddress, SeqNumsRange: cciptypes.NewSeqNumRange( @@ -119,7 +127,7 @@ func (c *CommitPluginCodecV1) Decode(ctx context.Context, bytes []byte) (cciptyp cciptypes.SeqNum(commitReport.MerkleRoot.MaxSeqNr), ), MerkleRoot: commitReport.MerkleRoot.MerkleRoot, - }, + }) } tokenPriceUpdates := make([]cciptypes.TokenPrice, 0, len(commitReport.PriceUpdates.TokenPriceUpdates)) diff --git a/core/capabilities/ccip/ccipsolana/executecodec.go b/core/capabilities/ccip/ccipsolana/executecodec.go index ad6efebf04f..fb13a4eaa3d 100644 --- a/core/capabilities/ccip/ccipsolana/executecodec.go +++ b/core/capabilities/ccip/ccipsolana/executecodec.go @@ -30,6 +30,13 @@ func NewExecutePluginCodecV1(extraDataCodec common.ExtraDataCodec) *ExecutePlugi } func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) { + if len(report.ChainReports) == 0 { + // OCR3 runs in a constant loop and will produce empty reports, so we need to handle this case + // return an empty report, CCIP will discard it on ShouldAcceptAttestedReport/ShouldTransmitAcceptedReport + // via validateReport before attempting to decode + return nil, nil + } + if len(report.ChainReports) != 1 { return nil, fmt.Errorf("unexpected chain report length: %d", len(report.ChainReports)) } @@ -88,6 +95,8 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec return nil, fmt.Errorf("invalid receiver address: %v", msg.Receiver) } + fmt.Printf("ONRAMP %+v\n", msg.Header.OnRamp) + message = ccip_offramp.Any2SVMRampMessage{ Header: ccip_offramp.RampMessageHeader{ MessageId: msg.Header.MessageID, @@ -101,6 +110,8 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec TokenReceiver: solana.PublicKeyFromBytes(msg.Receiver), TokenAmounts: tokenAmounts, ExtraArgs: extraArgs, + // TODO: is this available? + OnRampAddress: msg.Header.OnRamp, } // should only have an offchain token data if there are tokens as part of the message diff --git a/core/capabilities/ccip/ccipsolana/gas_helpers.go b/core/capabilities/ccip/ccipsolana/gas_helpers.go index 59ab4ff9dcb..2ac8a000d64 100644 --- a/core/capabilities/ccip/ccipsolana/gas_helpers.go +++ b/core/capabilities/ccip/ccipsolana/gas_helpers.go @@ -13,10 +13,10 @@ type EstimateProvider struct { // CalculateMerkleTreeGas is not implemented func (gp EstimateProvider) CalculateMerkleTreeGas(numRequests int) uint64 { - return 0 + return 1 } // CalculateMessageMaxGas is not implemented. func (gp EstimateProvider) CalculateMessageMaxGas(msg cciptypes.Message) uint64 { - return 0 + return 1 } diff --git a/core/capabilities/ccip/configs/solana/chain_writer.go b/core/capabilities/ccip/configs/solana/chain_writer.go index e8deee0ede7..29844e2baa4 100644 --- a/core/capabilities/ccip/configs/solana/chain_writer.go +++ b/core/capabilities/ccip/configs/solana/chain_writer.go @@ -1,7 +1,6 @@ package solana import ( - "encoding/binary" "encoding/json" "errors" "fmt" @@ -28,8 +27,7 @@ const ( merkleRoot = "Info.MerkleRoots.MerkleRoot" ) -func getCommitMethodConfig(fromAddress string, offrampProgramAddress string, destChainSelector uint64, priceOnly bool) chainwriter.MethodConfig { - destChainSelectorBytes := binary.LittleEndian.AppendUint64([]byte{}, destChainSelector) +func getCommitMethodConfig(fromAddress string, offrampProgramAddress string, priceOnly bool) chainwriter.MethodConfig { chainSpecificName := "commit" if priceOnly { chainSpecificName = "commitPriceOnly" @@ -45,17 +43,18 @@ func getCommitMethodConfig(fromAddress string, offrampProgramAddress string, des }, }, ChainSpecificName: chainSpecificName, + ArgsTransform: "CCIPCommit", LookupTables: chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ getCommonAddressLookupTableConfig(offrampProgramAddress), }, }, - Accounts: buildCommitAccountsList(fromAddress, offrampProgramAddress, destChainSelectorBytes, priceOnly), + Accounts: buildCommitAccountsList(fromAddress, offrampProgramAddress, priceOnly), DebugIDLocation: "", } } -func buildCommitAccountsList(fromAddress, offrampProgramAddress string, destChainSelectorBytes []byte, priceOnly bool) []chainwriter.Lookup { +func buildCommitAccountsList(fromAddress, offrampProgramAddress string, priceOnly bool) []chainwriter.Lookup { accounts := []chainwriter.Lookup{} accounts = append(accounts, getOfframpAccountConfig(offrampProgramAddress), @@ -100,7 +99,7 @@ func buildCommitAccountsList(fromAddress, offrampProgramAddress string, destChai getFeeQuoterConfigLookup(offrampProgramAddress), getGlobalStateConfig(offrampProgramAddress), getBillingTokenConfig(offrampProgramAddress), - getChainConfigGasPriceConfig(offrampProgramAddress, destChainSelectorBytes), + getChainConfigGasPriceConfig(offrampProgramAddress), ) return accounts } @@ -117,7 +116,7 @@ func getExecuteMethodConfig(fromAddress string, offrampProgramAddress string) ch }, }, ChainSpecificName: "execute", - ArgsTransform: "CCIP", + ArgsTransform: "CCIPExecute", LookupTables: chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -140,6 +139,7 @@ func getExecuteMethodConfig(fromAddress string, offrampProgramAddress string) ch }, }, }, + Optional: true, // Lookup table is optional if DestTokenAddress is not present in report }, getCommonAddressLookupTableConfig(offrampProgramAddress), }, @@ -155,6 +155,7 @@ func getExecuteMethodConfig(fromAddress string, offrampProgramAddress string) ch }, }, MintAddress: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Location: destTokenAddress}}, + Optional: true, // ATA lookup is optional if DestTokenAddress is not present in report }, }, Accounts: []chainwriter.Lookup{ @@ -307,7 +308,7 @@ func getExecuteMethodConfig(fromAddress string, offrampProgramAddress string) ch } } -func GetSolanaChainWriterConfig(offrampProgramAddress string, fromAddress string, destChainSelector uint64) (chainwriter.ChainWriterConfig, error) { +func GetSolanaChainWriterConfig(offrampProgramAddress string, fromAddress string) (chainwriter.ChainWriterConfig, error) { // check fromAddress pk, err := solana.PublicKeyFromBase58(fromAddress) if err != nil { @@ -333,8 +334,8 @@ func GetSolanaChainWriterConfig(offrampProgramAddress string, fromAddress string ccipconsts.ContractNameOffRamp: { Methods: map[string]chainwriter.MethodConfig{ ccipconsts.MethodExecute: getExecuteMethodConfig(fromAddress, offrampProgramAddress), - ccipconsts.MethodCommit: getCommitMethodConfig(fromAddress, offrampProgramAddress, destChainSelector, false), - ccipconsts.MethodCommitPriceOnly: getCommitMethodConfig(fromAddress, offrampProgramAddress, destChainSelector, true), + ccipconsts.MethodCommit: getCommitMethodConfig(fromAddress, offrampProgramAddress, false), + ccipconsts.MethodCommitPriceOnly: getCommitMethodConfig(fromAddress, offrampProgramAddress, true), }, IDL: ccipOfframpIDL, }, @@ -484,7 +485,7 @@ func getGlobalStateConfig(offrampProgramAddress string) chainwriter.Lookup { {Static: []byte("state")}, }, IsSigner: false, - IsWritable: false, + IsWritable: true, }, Optional: true, } @@ -500,23 +501,23 @@ func getBillingTokenConfig(offrampProgramAddress string) chainwriter.Lookup { {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Location: "Info.TokenPrices.TokenID"}}}, }, IsSigner: false, - IsWritable: false, + IsWritable: true, }, Optional: true, } } -func getChainConfigGasPriceConfig(offrampProgramAddress string, destChainSelector []byte) chainwriter.Lookup { +func getChainConfigGasPriceConfig(offrampProgramAddress string) chainwriter.Lookup { return chainwriter.Lookup{ PDALookups: &chainwriter.PDALookups{ Name: "ChainConfigGasPrice", PublicKey: getFeeQuoterProgramAccount(offrampProgramAddress), Seeds: []chainwriter.Seed{ {Static: []byte("dest_chain")}, - {Static: destChainSelector}, + {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Location: "Info.GasPrices.ChainSel"}}}, }, IsSigner: false, - IsWritable: false, + IsWritable: true, }, Optional: true, } diff --git a/core/capabilities/ccip/configs/solana/chain_writer_test.go b/core/capabilities/ccip/configs/solana/chain_writer_test.go index 34b53f9b186..1ad55ef82fa 100644 --- a/core/capabilities/ccip/configs/solana/chain_writer_test.go +++ b/core/capabilities/ccip/configs/solana/chain_writer_test.go @@ -30,7 +30,7 @@ func TestChainWriterConfigRaw(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := GetSolanaChainWriterConfig("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H", tt.fromAddress, 0) + config, err := GetSolanaChainWriterConfig("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H", tt.fromAddress) if tt.expectedError != "" { assert.EqualError(t, err, tt.expectedError) } else { diff --git a/core/capabilities/ccip/configs/solana/contract_reader.go b/core/capabilities/ccip/configs/solana/contract_reader.go index 3896b2ae9fa..4507d9d11c7 100644 --- a/core/capabilities/ccip/configs/solana/contract_reader.go +++ b/core/capabilities/ccip/configs/solana/contract_reader.go @@ -41,6 +41,15 @@ func DestContractReaderConfig() (config.ContractReader, error) { }, }) + // Prepend custom type so it takes priority over the IDL + offRampIDL.Types = append([]solanacodec.IdlTypeDef{{ + Name: "OnRampAddress", + Type: solanacodec.IdlTypeDefTy{ + Kind: solanacodec.IdlTypeDefTyKindCustom, + Codec: "onramp_address", + }, + }}, offRampIDL.Types...) + var routerIDL solanacodec.IDL if err := json.Unmarshal([]byte(ccipRouterIDL), &routerIDL); err != nil { return config.ContractReader{}, fmt.Errorf("unexpected error: invalid CCIP Router IDL, error: %w", err) @@ -53,18 +62,48 @@ func DestContractReaderConfig() (config.ContractReader, error) { consts.ContractNameOffRamp: { IDL: offRampIDL, Reads: map[string]config.ReadDefinition{ + consts.EventNameExecutionStateChanged: { + ChainSpecificName: consts.EventNameExecutionStateChanged, + ReadType: config.Event, + EventDefinitions: &config.EventDefinitions{ + PollingFilter: &config.PollingFilter{}, + IndexedField0: &config.IndexedField{ + OffChainPath: consts.EventAttributeSourceChain, + OnChainPath: "SourceChainSelector", + }, + IndexedField1: &config.IndexedField{ + OffChainPath: consts.EventAttributeSequenceNumber, + OnChainPath: consts.EventAttributeSequenceNumber, + }, + IndexedField2: &config.IndexedField{ + OffChainPath: consts.EventAttributeState, + OnChainPath: consts.EventAttributeState, + }, + }, + }, + consts.EventNameCommitReportAccepted: { + ChainSpecificName: "CommitReportAccepted", + ReadType: config.Event, + EventDefinitions: &config.EventDefinitions{ + PollingFilter: &config.PollingFilter{}, + }, + OutputModifications: codec.ModifiersConfig{ + &codec.RenameModifierConfig{Fields: map[string]string{"MerkleRoot": "UnblessedMerkleRoots"}}, + &codec.ElementExtractorModifierConfig{Extractions: map[string]*codec.ElementExtractorLocation{"UnblessedMerkleRoots": &locationFirst}}, + }, + }, consts.MethodNameOffRampLatestConfigDetails: { - ChainSpecificName: "Config", - ReadType: config.Account, - PDADefinition: solanacodec.PDATypeDef{Prefix: []byte("config")}, + ChainSpecificName: "Config", + ReadType: config.Account, + PDADefinition: solanacodec.PDATypeDef{Prefix: []byte("config")}, OutputModifications: codec.ModifiersConfig{ // TODO why does Solana have two of these in an array, but EVM has one - &codec.WrapperModifierConfig{ - Fields: map[string]string{"Ocr3": "OcrConfig"}, - }, - &codec.PropertyExtractorConfig{FieldName: "Ocr3"}, - &codec.ElementExtractorFromOnchainModifierConfig{Extractions: map[string]*codec.ElementExtractorLocation{"OcrConfig": &locationFirst}}, - &codec.ByteToBooleanModifierConfig{Fields: []string{"OcrConfig.ConfigInfo.IsSignatureVerificationEnabled"}}, + // &codec.WrapperModifierConfig{ + // Fields: map[string]string{"Ocr3": "OcrConfig"}, + // }, + // &codec.PropertyExtractorConfig{FieldName: "Ocr3"}, + // &codec.ElementExtractorFromOnchainModifierConfig{Extractions: map[string]*codec.ElementExtractorLocation{"OcrConfig": &locationFirst}}, + // &codec.ByteToBooleanModifierConfig{Fields: []string{"OcrConfig.ConfigInfo.IsSignatureVerificationEnabled"}}, }, }, consts.MethodNameGetLatestPriceSequenceNumber: { @@ -72,9 +111,10 @@ func DestContractReaderConfig() (config.ContractReader, error) { ReadType: config.Account, PDADefinition: solanacodec.PDATypeDef{Prefix: []byte("state")}, OutputModifications: codec.ModifiersConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{"LatestPriceSequenceNumber": "LatestSeqNr"}, - }}, + &codec.PropertyExtractorConfig{ + FieldName: "LatestPriceSequenceNumber", + }, + }, }, consts.MethodNameOffRampGetStaticConfig: { ChainSpecificName: "Config", @@ -89,6 +129,21 @@ func DestContractReaderConfig() (config.ContractReader, error) { }, }, }, + MultiReader: &config.MultiReader{ + Reads: []config.ReadDefinition{ + // CCIP expects a NonceManager address, in our case that's the Router + { + ChainSpecificName: "ReferenceAddresses", + ReadType: config.Account, + PDADefinition: solanacodec.PDATypeDef{ + Prefix: []byte("reference_addresses"), + }, + OutputModifications: codec.ModifiersConfig{ + &codec.RenameModifierConfig{Fields: map[string]string{"Router": "NonceManager"}}, + }, + }, + }, + }, }, consts.MethodNameOffRampGetDynamicConfig: { ChainSpecificName: "Config", @@ -100,6 +155,8 @@ func DestContractReaderConfig() (config.ContractReader, error) { &codec.RenameModifierConfig{ Fields: map[string]string{"EnableManualExecutionAfter": "PermissionLessExecutionThresholdSeconds"}, }, + // TODO: figure out how this will be properly configured, if it has to be added to SVM state + &codec.HardCodeModifierConfig{OffChainValues: map[string]any{"IsRMNVerificationDisabled": true}}, }, MultiReader: &config.MultiReader{ Reads: []config.ReadDefinition{ @@ -127,7 +184,9 @@ func DestContractReaderConfig() (config.ContractReader, error) { // // OnRamp addresses supported from the source chain, each of them has a 64 byte address. So this can hold 2 addresses. // // If only one address is configured, then the space for the second address must be zeroed. // // Each address must be right padded with zeros if it is less than 64 bytes. - &codec.ElementExtractorModifierConfig{Extractions: map[string]*codec.ElementExtractorLocation{"OnRamp": &locationFirst}}, + &codec.ElementExtractorFromOnchainModifierConfig{Extractions: map[string]*codec.ElementExtractorLocation{"OnRamp": &locationFirst}}, + // TODO: figure out how this will be properly configured, if it has to be added to SVM state + &codec.HardCodeModifierConfig{OffChainValues: map[string]any{"IsRMNVerificationDisabled": true}}, }, MultiReader: &config.MultiReader{ ReuseParams: true, @@ -139,6 +198,19 @@ func DestContractReaderConfig() (config.ContractReader, error) { Prefix: []byte("reference_addresses"), }, }, + { + // this seems like a hack to extract both State and Config fields? + ChainSpecificName: "SourceChain", + ReadType: config.Account, + PDADefinition: solanacodec.PDATypeDef{ + Prefix: []byte("source_chain_state"), + Seeds: []solanacodec.PDASeed{{Name: "NewChainSelector", Type: solanacodec.IdlType{AsString: solanacodec.IdlTypeU64}}}, + }, + InputModifications: codec.ModifiersConfig{&codec.RenameModifierConfig{Fields: map[string]string{"NewChainSelector": "SourceChainSelector"}}}, + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "State"}, + }, + }, }, }, }, @@ -188,13 +260,12 @@ func DestContractReaderConfig() (config.ContractReader, error) { PDADefinition: solanacodec.PDATypeDef{ Prefix: []byte("fee_billing_token_config"), Seeds: []solanacodec.PDASeed{{ - Name: "Tokens", - Type: solanacodec.IdlType{ - AsIdlTypeVec: &solanacodec.IdlTypeVec{ - Vec: solanacodec.IdlType{AsString: solanacodec.IdlTypePublicKey}, - }, - }, + Name: "Token", + Type: solanacodec.IdlType{AsString: solanacodec.IdlTypePublicKey}, }}}, + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "Config.UsdPerToken"}, + }, }, consts.MethodNameGetFeePriceUpdate: { ChainSpecificName: "DestChain", @@ -221,6 +292,23 @@ func DestContractReaderConfig() (config.ContractReader, error) { }, }, }, + MultiReader: &config.MultiReader{ + ReuseParams: true, + Reads: []config.ReadDefinition{ + { + // this seems like a hack to extract both State and Config fields? + ChainSpecificName: "DestChain", + PDADefinition: solanacodec.PDATypeDef{ + Prefix: []byte("dest_chain"), + Seeds: []solanacodec.PDASeed{{Name: "DestinationChainSelector", Type: solanacodec.IdlType{AsString: solanacodec.IdlTypeU64}}}, + }, + InputModifications: codec.ModifiersConfig{&codec.RenameModifierConfig{Fields: map[string]string{"DestinationChainSelector": "DestChainSelector"}}}, + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "State"}, + }, + }, + }, + }, }, }, }, @@ -233,11 +321,7 @@ func DestContractReaderConfig() (config.ContractReader, error) { Prefix: []byte("config"), }, OutputModifications: codec.ModifiersConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{ - "LinkTokenMint": "LinkToken", - }, - }, + &codec.PropertyExtractorConfig{FieldName: "LinkTokenMint"}, }, }, }, @@ -335,6 +419,7 @@ func SourceContractReaderConfig() (config.ContractReader, error) { MultiReader: &config.MultiReader{ ReuseParams: true, Reads: []config.ReadDefinition{ + // this seems like a hack to extract both State and Config fields? { ChainSpecificName: "DestChain", ReadType: config.Account, @@ -403,13 +488,12 @@ func SourceContractReaderConfig() (config.ContractReader, error) { PDADefinition: solanacodec.PDATypeDef{ Prefix: []byte("fee_billing_token_config"), Seeds: []solanacodec.PDASeed{{ - Name: "Tokens", - Type: solanacodec.IdlType{ - AsIdlTypeVec: &solanacodec.IdlTypeVec{ - Vec: solanacodec.IdlType{AsString: solanacodec.IdlTypePublicKey}, - }, - }, + Name: "Token", + Type: solanacodec.IdlType{AsString: solanacodec.IdlTypePublicKey}, }}}, + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "Config.UsdPerToken"}, + }, }, consts.MethodNameGetFeePriceUpdate: { ChainSpecificName: "DestChain", @@ -448,11 +532,7 @@ func SourceContractReaderConfig() (config.ContractReader, error) { Prefix: []byte("config"), }, OutputModifications: codec.ModifiersConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{ - "LinkTokenMint": "LinkToken", - }, - }, + &codec.PropertyExtractorConfig{FieldName: "LinkTokenMint"}, }, }, }, diff --git a/core/capabilities/ccip/ocrimpls/config_tracker.go b/core/capabilities/ccip/ocrimpls/config_tracker.go index c70aa50030a..c5bf991e7c3 100644 --- a/core/capabilities/ccip/ocrimpls/config_tracker.go +++ b/core/capabilities/ccip/ocrimpls/config_tracker.go @@ -5,7 +5,6 @@ import ( cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" - gethcommon "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ) @@ -75,8 +74,8 @@ func toOnchainPublicKeys(signers [][]byte) []types.OnchainPublicKey { func toOCRAccounts(transmitters [][]byte) []types.Account { accounts := make([]types.Account, len(transmitters)) for i, transmitter := range transmitters { - // TODO: string-encode the transmitter appropriately to the dest chain family. - accounts[i] = types.Account(gethcommon.BytesToAddress(transmitter).Hex()) + // transmitters have chain family specific encoding so we keep them as strings + accounts[i] = types.Account(transmitter) } return accounts } diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter.go b/core/capabilities/ccip/ocrimpls/contract_transmitter.go index 1876ac60b2f..aaf17dae240 100644 --- a/core/capabilities/ccip/ocrimpls/contract_transmitter.go +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter.go @@ -23,46 +23,57 @@ type ToCalldataFunc func( report ocr3types.ReportWithInfo[[]byte], rs, ss [][32]byte, vs [32]byte, -) (any, error) +) (string, string, any, error) + +func NewToCommitCalldata(defaultMethod, priceOnlyMethod string) ToCalldataFunc { + return func( + rawReportCtx [2][32]byte, + report ocr3types.ReportWithInfo[[]byte], + rs, ss [][32]byte, + vs [32]byte, + ) (string, string, any, error) { + // Note that the name of the struct field is very important, since the encoder used + // by the chainwriter uses mapstructure, which will use the struct field name to map + // to the argument name in the function call. + // If, for whatever reason, we want to change the field name, make sure to add a `mapstructure:""` tag + // for that field. + var info ccipocr3.CommitReportInfo + if len(report.Info) != 0 { + var err error + info, err = ccipocr3.DecodeCommitReportInfo(report.Info) + if err != nil { + return "", "", nil, err + } + } -func ToCommitCalldata( - rawReportCtx [2][32]byte, - report ocr3types.ReportWithInfo[[]byte], - rs, ss [][32]byte, - vs [32]byte, -) (any, error) { - // Note that the name of the struct field is very important, since the encoder used - // by the chainwriter uses mapstructure, which will use the struct field name to map - // to the argument name in the function call. - // If, for whatever reason, we want to change the field name, make sure to add a `mapstructure:""` tag - // for that field. - var info ccipocr3.CommitReportInfo - if len(report.Info) != 0 { - var err error - info, err = ccipocr3.DecodeCommitReportInfo(report.Info) - if err != nil { - return nil, err + // fmt.Printf("transmitter: decoded report info %+v\n", info) + + method := defaultMethod + if len(priceOnlyMethod) > 0 && len(info.MerkleRoots) == 0 && len(info.TokenPrices) > 0 { + method = priceOnlyMethod } - } - // WARNING: Be careful if you change the data types. - // Using a different type e.g. `type Foo [32]byte` instead of `[32]byte` - // will trigger undefined chainWriter behavior, e.g. transactions submitted with wrong arguments. - return struct { - ReportContext [2][32]byte - Report []byte - Rs [][32]byte - Ss [][32]byte - RawVs [32]byte - Info ccipocr3.CommitReportInfo - }{ - ReportContext: rawReportCtx, - Report: report.Report, - Rs: rs, - Ss: ss, - RawVs: vs, - Info: info, - }, nil + // WARNING: Be careful if you change the data types. + // Using a different type e.g. `type Foo [32]byte` instead of `[32]byte` + // will trigger undefined chainWriter behavior, e.g. transactions submitted with wrong arguments. + return consts.ContractNameOffRamp, + method, + struct { + ReportContext [2][32]byte + Report []byte + Rs [][32]byte + Ss [][32]byte + RawVs [32]byte + Info ccipocr3.CommitReportInfo + }{ + ReportContext: rawReportCtx, + Report: report.Report, + Rs: rs, + Ss: ss, + RawVs: vs, + Info: info, + }, nil + } } func ToExecCalldata( @@ -70,7 +81,7 @@ func ToExecCalldata( report ocr3types.ReportWithInfo[[]byte], _, _ [][32]byte, _ [32]byte, -) (any, error) { +) (string, string, any, error) { // Note that the name of the struct field is very important, since the encoder used // by the chainwriter uses mapstructure, which will use the struct field name to map // to the argument name in the function call. @@ -85,19 +96,23 @@ func ToExecCalldata( var err error info, err = ccipocr3.DecodeExecuteReportInfo(report.Info) if err != nil { - return nil, err + return "", "", nil, err } } - return struct { - ReportContext [2][32]byte - Report []byte - Info ccipocr3.ExecuteReportInfo - }{ - ReportContext: rawReportCtx, - Report: report.Report, - Info: info, - }, nil + fmt.Printf("transmitter: decoded exec report info %+v\n", info) + + return consts.ContractNameOffRamp, + consts.MethodExecute, + struct { + ReportContext [2][32]byte + Report []byte + Info ccipocr3.ExecuteReportInfo + }{ + ReportContext: rawReportCtx, + Report: report.Report, + Info: info, + }, nil } var _ ocr3types.ContractTransmitter[[]byte] = &commitTransmitter{} @@ -105,8 +120,6 @@ var _ ocr3types.ContractTransmitter[[]byte] = &commitTransmitter{} type commitTransmitter struct { cw commontypes.ContractWriter fromAccount ocrtypes.Account - contractName string - method string offrampAddress string toCalldataFn ToCalldataFunc } @@ -119,13 +132,18 @@ func XXXNewContractTransmitterTestsOnly( offrampAddress string, toCalldataFn ToCalldataFunc, ) ocr3types.ContractTransmitter[[]byte] { + wrappedToCalldataFunc := func(rawReportCtx [2][32]byte, + report ocr3types.ReportWithInfo[[]byte], + rs, ss [][32]byte, + vs [32]byte) (string, string, any, error) { + _, _, args, err := toCalldataFn(rawReportCtx, report, rs, ss, vs) + return contractName, method, args, err + } return &commitTransmitter{ cw: cw, fromAccount: fromAccount, - contractName: contractName, - method: method, offrampAddress: offrampAddress, - toCalldataFn: toCalldataFn, + toCalldataFn: wrappedToCalldataFunc, } } @@ -133,14 +151,13 @@ func NewCommitContractTransmitter( cw commontypes.ContractWriter, fromAccount ocrtypes.Account, offrampAddress string, + defaultMethod, priceOnlyMethod string, ) ocr3types.ContractTransmitter[[]byte] { return &commitTransmitter{ cw: cw, fromAccount: fromAccount, - contractName: consts.ContractNameOffRamp, - method: consts.MethodCommit, offrampAddress: offrampAddress, - toCalldataFn: ToCommitCalldata, + toCalldataFn: NewToCommitCalldata(defaultMethod, priceOnlyMethod), } } @@ -152,8 +169,6 @@ func NewExecContractTransmitter( return &commitTransmitter{ cw: cw, fromAccount: fromAccount, - contractName: consts.ContractNameOffRamp, - method: consts.MethodExecute, offrampAddress: offrampAddress, toCalldataFn: ToExecCalldata, } @@ -198,7 +213,7 @@ func (c *commitTransmitter) Transmit( } // chain writer takes in the raw calldata and packs it on its own. - args, err := c.toCalldataFn(rawReportCtx, reportWithInfo, rs, ss, vs) + contract, method, args, err := c.toCalldataFn(rawReportCtx, reportWithInfo, rs, ss, vs) if err != nil { return fmt.Errorf("failed to generate call data: %w", err) } @@ -211,7 +226,7 @@ func (c *commitTransmitter) Transmit( return fmt.Errorf("failed to generate UUID: %w", err) } zero := big.NewInt(0) - if err := c.cw.SubmitTransaction(ctx, c.contractName, c.method, args, fmt.Sprintf("%s-%s-%s", c.contractName, c.offrampAddress, txID.String()), c.offrampAddress, &meta, zero); err != nil { + if err := c.cw.SubmitTransaction(ctx, contract, method, args, fmt.Sprintf("%s-%s-%s", contract, c.offrampAddress, txID.String()), c.offrampAddress, &meta, zero); err != nil { return fmt.Errorf("failed to submit transaction thru chainwriter: %w", err) } diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go index 53042e475e4..d09b4aa28cc 100644 --- a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -21,14 +21,15 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/chainlink-integrations/evm/heads" + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" "github.com/smartcontractkit/chainlink-integrations/evm/assets" "github.com/smartcontractkit/chainlink-integrations/evm/client" evmconfig "github.com/smartcontractkit/chainlink-integrations/evm/config" "github.com/smartcontractkit/chainlink-integrations/evm/config/chaintype" "github.com/smartcontractkit/chainlink-integrations/evm/config/toml" "github.com/smartcontractkit/chainlink-integrations/evm/gas" + "github.com/smartcontractkit/chainlink-integrations/evm/heads" "github.com/smartcontractkit/chainlink-integrations/evm/keystore" "github.com/smartcontractkit/chainlink-integrations/evm/logpoller" evmtestutils "github.com/smartcontractkit/chainlink-integrations/evm/testutils" @@ -316,7 +317,7 @@ func newTestUniverse(t *testing.T, ks *keyringsAndSigners[[]byte]) *testUniverse contractName, methodTransmitWithSignatures, ocr3HelperAddr.Hex(), - ocrimpls.ToCommitCalldata, + ocrimpls.NewToCommitCalldata(consts.MethodCommit, ""), ) transmitterWithoutSigs := ocrimpls.XXXNewContractTransmitterTestsOnly( chainWriter, diff --git a/core/capabilities/ccip/oraclecreator/plugin.go b/core/capabilities/ccip/oraclecreator/plugin.go index e11d913ed52..05696895627 100644 --- a/core/capabilities/ccip/oraclecreator/plugin.go +++ b/core/capabilities/ccip/oraclecreator/plugin.go @@ -79,6 +79,7 @@ var plugins = map[string]plugin{ TokenDataEncoder: ccipsolana.NewSolanaTokenDataEncoder(), GasEstimateProvider: ccipsolana.NewGasEstimateProvider(), RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return nil }, + PriceOnlyCommitFn: consts.MethodCommitPriceOnly, }, } @@ -94,6 +95,8 @@ type plugin struct { TokenDataEncoder cciptypes.TokenDataEncoder GasEstimateProvider cciptypes.EstimateProvider RMNCrypto func(lggr logger.Logger) cciptypes.RMNCrypto + // PriceOnlyCommitFn optional method override for price only commit reports. + PriceOnlyCommitFn string } // pluginOracleCreator creates oracles that reference plugins running @@ -354,6 +357,8 @@ func (i *pluginOracleCreator) createFactoryAndTransmitter( transmitter = ocrimpls.NewCommitContractTransmitter(destChainWriter, ocrtypes.Account(destFromAccounts[0]), offrampAddrStr, + consts.MethodCommit, + plugins[chainFamily].PriceOnlyCommitFn, ) } else if config.Config.PluginType == uint8(cctypes.PluginTypeCCIPExec) { factory = execocr3.NewExecutePluginFactory( @@ -427,7 +432,12 @@ func (i *pluginOracleCreator) createReadersAndWriters( return nil, nil, fmt.Errorf("failed to get chain selector from chain ID %s: %w", chainID, err1) } - chainReaderConfig, err1 := getChainReaderConfig(i.lggr, chainID, destChainID, homeChainID, ofc, chainSelector, destChainFamily) + if _, exists := plugins[relayChainFamily]; !exists { + i.lggr.Debugw("createReadersAndWriters: skipping unsupported relayer", "chainID", chainID, "family", relayChainFamily) + continue + } + + chainReaderConfig, err1 := getChainReaderConfig(i.lggr, chainID, destChainID, homeChainID, ofc, chainSelector, relayChainFamily) if err1 != nil { return nil, nil, fmt.Errorf("failed to get chain reader config: %w", err1) } @@ -462,7 +472,6 @@ func (i *pluginOracleCreator) createReadersAndWriters( execBatchGasLimit, relayChainFamily, config.Config.OfframpAddress, - chainDetails.ChainSelector, ) if err1 != nil { return nil, nil, err1 @@ -589,8 +598,7 @@ func createChainWriter( transmitters map[types.RelayID][]string, execBatchGasLimit uint64, chainFamily string, - offrampProgramAddress []byte, - destChainSelector uint64, + offrampAddress []byte, ) (types.ContractWriter, error) { var err error var chainWriterConfig []byte @@ -599,8 +607,12 @@ func createChainWriter( switch chainFamily { case relay.NetworkSolana: var solConfig chainwriter.ChainWriterConfig - offrampAddress := solana.PublicKeyFromBytes(offrampProgramAddress) - if solConfig, err = solanaconfig.GetSolanaChainWriterConfig(offrampAddress.String(), transmitter[0], destChainSelector); err == nil { + var offrampProgramAddress solana.PublicKey + // TODO: this function can still be called with EVM inputs, and PublicKeyFromBytes will panic on addresses with len=20 + if len(offrampAddress) == solana.PublicKeyLength { + offrampProgramAddress = solana.PublicKeyFromBytes(offrampAddress) + } + if solConfig, err = solanaconfig.GetSolanaChainWriterConfig(offrampProgramAddress.String(), transmitter[0]); err != nil { return nil, fmt.Errorf("failed to get Solana chain writer config: %w", err) } if chainWriterConfig, err = json.Marshal(solConfig); err != nil { diff --git a/core/services/keystore/keys/ocr2key/solana_keyring.go b/core/services/keystore/keys/ocr2key/solana_keyring.go index e9a017c3ce5..175bd52fabf 100644 --- a/core/services/keystore/keys/ocr2key/solana_keyring.go +++ b/core/services/keystore/keys/ocr2key/solana_keyring.go @@ -4,12 +4,14 @@ import ( "bytes" "crypto/ecdsa" "crypto/sha256" + "encoding/binary" "io" "github.com/ethereum/go-ethereum/crypto" "github.com/smartcontractkit/libocr/offchainreporting2/types" "github.com/smartcontractkit/libocr/offchainreporting2plus/chains/evmutil" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "golang.org/x/crypto/sha3" ) var _ ocrtypes.OnchainKeyring = &solanaKeyring{} @@ -53,10 +55,13 @@ func (skr *solanaKeyring) Sign3(digest types.ConfigDigest, seqNr uint64, r ocrty func (skr *solanaKeyring) reportToSigData3(digest types.ConfigDigest, seqNr uint64, r ocrtypes.Report) []byte { rawReportContext := RawReportContext3(digest, seqNr) - sigData := crypto.Keccak256(r) - sigData = append(sigData, rawReportContext[0][:]...) - sigData = append(sigData, rawReportContext[1][:]...) - return crypto.Keccak256(sigData) + h := sha3.NewLegacyKeccak256() + reportLen := uint16(len(r)) //nolint:gosec // max U16 larger than solana transaction size + binary.Write(h, binary.LittleEndian, reportLen) + h.Write(r) + h.Write(rawReportContext[0][:]) + h.Write(rawReportContext[1][:]) + return h.Sum(nil) } func (skr *solanaKeyring) signBlob(b []byte) (sig []byte, err error) { diff --git a/deployment/ccip/changeset/internal/deploy_home_chain.go b/deployment/ccip/changeset/internal/deploy_home_chain.go index ed693c9c693..9b2968553b0 100644 --- a/deployment/ccip/changeset/internal/deploy_home_chain.go +++ b/deployment/ccip/changeset/internal/deploy_home_chain.go @@ -408,11 +408,7 @@ func BuildOCR3ConfigForCCIPHome( transmittersBytes := make([][]byte, len(transmitters)) for i, transmitter := range transmitters { - parsed, err2 := common.ParseHexOrString(string(transmitter)) - if err2 != nil { - return nil, err2 - } - transmittersBytes[i] = parsed + transmittersBytes[i] = []byte(transmitter) } // validate ocr3 params correctness _, err := ocr3confighelper.PublicConfigFromContractConfig(false, ocrtypes.ContractConfig{ diff --git a/deployment/ccip/changeset/solana/cs_add_remote_chain.go b/deployment/ccip/changeset/solana/cs_add_remote_chain.go index 93278ac0560..d2895b4b3e0 100644 --- a/deployment/ccip/changeset/solana/cs_add_remote_chain.go +++ b/deployment/ccip/changeset/solana/cs_add_remote_chain.go @@ -6,7 +6,6 @@ import ( "fmt" "strconv" - "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" "github.com/smartcontractkit/mcms" @@ -162,11 +161,12 @@ func doAddRemoteChainToSolana( } for remoteChainSel, update := range updates { - var onRampBytes [64]byte + var onRampAddress solOffRamp.OnRampAddress + // var onRampBytes [64]byte // already verified, skipping errcheck addressBytes, _ := s.GetOnRampAddressBytes(remoteChainSel) - addressBytes = common.LeftPadBytes(addressBytes, 64) - copy(onRampBytes[:], addressBytes) + copy(onRampAddress.Bytes[:], addressBytes) + onRampAddress.Len = uint32(len(addressBytes)) // verified while loading state fqRemoteChainPDA, _, _ := solState.FindFqDestChainPDA(remoteChainSel, feeQuoterID) @@ -286,7 +286,7 @@ func doAddRemoteChainToSolana( solOffRamp.SetProgramID(offRampID) validSourceChainConfig := solOffRamp.SourceChainConfig{ - OnRamp: [2][64]byte{onRampBytes, [64]byte{}}, + OnRamp: [2]solOffRamp.OnRampAddress{onRampAddress, {}}, IsEnabled: update.EnabledAsSource, } if offRampUsingMCMS { diff --git a/deployment/ccip/changeset/testhelpers/messagingtest/helpers.go b/deployment/ccip/changeset/testhelpers/messagingtest/helpers.go index 2f2fc32ae3f..d193b6bf59a 100644 --- a/deployment/ccip/changeset/testhelpers/messagingtest/helpers.go +++ b/deployment/ccip/changeset/testhelpers/messagingtest/helpers.go @@ -1,14 +1,21 @@ package messagingtest import ( + "context" "testing" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" chain_selectors "github.com/smartcontractkit/chain-selectors" "github.com/stretchr/testify/require" + solconfig "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + solcommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" + solstate "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink/deployment" @@ -82,7 +89,7 @@ type TestCase struct { TestSetup Replayed bool Nonce uint64 - Receiver common.Address + Receiver []byte MsgData []byte ExtraArgs []byte ExpectedExecutionState int @@ -95,12 +102,17 @@ type TestCaseOutput struct { MsgSentEvent *onramp.OnRampCCIPMessageSent } -func sleepAndReplay(t *testing.T, e testhelpers.DeployedEnv, sourceChain, destChain uint64) { +func sleepAndReplay(t *testing.T, e testhelpers.DeployedEnv, chainSelectors ...uint64) { time.Sleep(30 * time.Second) replayBlocks := make(map[uint64]uint64) - replayBlocks[sourceChain] = 1 - replayBlocks[destChain] = 1 - + for _, selector := range chainSelectors { + family, err := chain_selectors.GetSelectorFamily(selector) + require.NoError(t, err) + // log replay is only available on EVM + if family == chain_selectors.FamilyEVM { + replayBlocks[selector] = 1 + } + } testhelpers.ReplayLogs(t, e.Env.Offchain, replayBlocks) } @@ -116,10 +128,14 @@ func getLatestNonce(tc TestCase) uint64 { }, tc.SourceChain, tc.Sender) require.NoError(tc.T, err) case chain_selectors.FamilySolana: - // var nonceCounterAccount ccip_router.Nonce - // err = common.GetAccountDataBorshInto(ctx, solanaGoClient, nonceEvmPDA, config.DefaultCommitment, &nonceCounterAccount) - // require.NoError(t, err, "failed to get account info") - // require.Equal(t, uint64(1), nonceCounterAccount.Counter) + ctx := context.Background() + client := tc.Env.SolChains[tc.DestChain].Client + noncePDA, err := solstate.FindNoncePDA(tc.SourceChain, solana.PublicKeyFromBytes(tc.Sender), tc.OnchainState.SolChains[tc.DestChain].Router) + require.NoError(tc.T, err) + var nonceCounterAccount ccip_router.Nonce + err = solcommon.GetAccountDataBorshInto(ctx, client, noncePDA, solconfig.DefaultCommitment, &nonceCounterAccount) + // require.NoError(tc.T, err, "failed to get nonce account info") TODO: this account could be missing before first call? + latestNonce = nonceCounterAccount.Counter } return latestNonce } @@ -141,23 +157,21 @@ func Run(tc TestCase) (out TestCaseOutput) { tc.DestChain, tc.TestRouter, router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(tc.Receiver.Bytes(), 32), + Receiver: common.LeftPadBytes(tc.Receiver, 32), Data: tc.MsgData, TokenAmounts: nil, FeeToken: common.HexToAddress("0x0"), ExtraArgs: tc.ExtraArgs, }) + sourceDest := testhelpers.SourceDestPair{ + SourceChainSelector: tc.SourceChain, + DestChainSelector: tc.DestChain, + } expectedSeqNum := map[testhelpers.SourceDestPair]uint64{ - { - SourceChainSelector: tc.SourceChain, - DestChainSelector: tc.DestChain, - }: msgSentEvent.SequenceNumber, + sourceDest: msgSentEvent.SequenceNumber, } expectedSeqNumExec := map[testhelpers.SourceDestPair][]uint64{ - { - SourceChainSelector: tc.SourceChain, - DestChainSelector: tc.DestChain, - }: {msgSentEvent.SequenceNumber}, + sourceDest: {msgSentEvent.SequenceNumber}, } out.MsgSentEvent = msgSentEvent @@ -179,19 +193,16 @@ func Run(tc TestCase) (out TestCaseOutput) { require.Equalf( tc.T, tc.ExpectedExecutionState, - execStates[testhelpers.SourceDestPair{ - SourceChainSelector: tc.SourceChain, - DestChainSelector: tc.DestChain, - }][msgSentEvent.SequenceNumber], + execStates[sourceDest][msgSentEvent.SequenceNumber], "wrong execution state for seq nr %d, expected %d, got %d", msgSentEvent.SequenceNumber, tc.ExpectedExecutionState, - execStates[testhelpers.SourceDestPair{ - SourceChainSelector: tc.SourceChain, - DestChainSelector: tc.DestChain, - }][msgSentEvent.SequenceNumber], + execStates[sourceDest][msgSentEvent.SequenceNumber], ) + // TODO: check this again, nonce not incremented + // is this based on source chain? + // check the sender latestNonce on the dest, should be incremented latestNonce := getLatestNonce(tc) require.Equal(tc.T, tc.Nonce+1, latestNonce) diff --git a/deployment/ccip/changeset/testhelpers/test_assertions.go b/deployment/ccip/changeset/testhelpers/test_assertions.go index ff026962ce6..abe1f949b57 100644 --- a/deployment/ccip/changeset/testhelpers/test_assertions.go +++ b/deployment/ccip/changeset/testhelpers/test_assertions.go @@ -5,16 +5,25 @@ import ( "errors" "fmt" "math/big" + "slices" + "strings" "sync" "testing" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" + solrpc "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" + solconfig "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" + solccip "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip" + solcommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" + + chainsel "github.com/smartcontractkit/chain-selectors" commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -186,18 +195,44 @@ func ConfirmCommitForAllWithExpectedSeqNums( startBlock = startBlocks[dstChain] } - return commonutils.JustError(ConfirmCommitWithExpectedSeqNumRange( - t, - srcChain, - e.Chains[dstChain], - state.Chains[dstChain].OffRamp, - startBlock, - ccipocr3.SeqNumRange{ - ccipocr3.SeqNum(expectedSeqNum), - ccipocr3.SeqNum(expectedSeqNum), - }, - true, - )) + family, err := chainsel.GetSelectorFamily(dstChain) + if err != nil { + return err + } + switch family { + case chainsel.FamilyEVM: + return commonutils.JustError(ConfirmCommitWithExpectedSeqNumRange( + t, + srcChain, + e.Chains[dstChain], + state.Chains[dstChain].OffRamp, + startBlock, + ccipocr3.SeqNumRange{ + ccipocr3.SeqNum(expectedSeqNum), + ccipocr3.SeqNum(expectedSeqNum), + }, + true, + )) + case chainsel.FamilySolana: + var startSlot uint64 + if startBlock != nil { + startSlot = *startBlock + } + return commonutils.JustError(ConfirmCommitWithExpectedSeqNumRangeSol( + t, + srcChain, + e.SolChains[dstChain], + state.SolChains[dstChain].OffRamp, + startSlot, + ccipocr3.SeqNumRange{ + ccipocr3.SeqNum(expectedSeqNum), + ccipocr3.SeqNum(expectedSeqNum), + }, + true, + )) + default: + return fmt.Errorf("unsupported chain family; %v", family) + } }) } @@ -258,8 +293,8 @@ func (c *CommitReportTracker) allCommited(sourceChainSelector uint64) bool { // Waiting is done in parallel per every sourceChain/destChain (lane) passed as argument. func ConfirmMultipleCommits( t *testing.T, - chains map[uint64]deployment.Chain, - state map[uint64]changeset.CCIPChainState, + env deployment.Environment, + state changeset.CCIPOnChainState, startBlocks map[uint64]*uint64, enforceSingleCommit bool, expectedSeqNums map[SourceDestPair]ccipocr3.SeqNumRange, @@ -272,16 +307,40 @@ func ConfirmMultipleCommits( destChain := sourceDest.DestChainSelector errGrp.Go(func() error { - _, err := ConfirmCommitWithExpectedSeqNumRange( - t, - srcChain, - chains[destChain], - state[destChain].OffRamp, - startBlocks[destChain], - seqRange, - enforceSingleCommit, - ) - return err + family, err := chainsel.GetSelectorFamily(destChain) + if err != nil { + return err + } + switch family { + case chainsel.FamilyEVM: + _, err := ConfirmCommitWithExpectedSeqNumRange( + t, + srcChain, + env.Chains[destChain], + state.Chains[destChain].OffRamp, + startBlocks[destChain], + seqRange, + enforceSingleCommit, + ) + return err + case chainsel.FamilySolana: + var startSlot uint64 + if startBlocks[destChain] != nil { + startSlot = *startBlocks[destChain] + } + _, err := ConfirmCommitWithExpectedSeqNumRangeSol( + t, + srcChain, + env.SolChains[destChain], + state.SolChains[destChain].OffRamp, + startSlot, + seqRange, + enforceSingleCommit, + ) + return err + default: + return fmt.Errorf("unsupported chain family; %v", family) + } }) } @@ -345,16 +404,8 @@ func ConfirmCommitWithExpectedSeqNumRange( } defer subscription.Unsubscribe() - var duration time.Duration - deadline, ok := t.Deadline() - if ok { - // make this timer end a minute before so that we don't hit the deadline - duration = deadline.Sub(time.Now().Add(-1 * time.Minute)) - } else { - duration = 5 * time.Minute - } - timer := time.NewTimer(duration) - defer timer.Stop() + timeout := time.NewTimer(tests.WaitTimeout(t)) + defer timeout.Stop() ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for { @@ -377,9 +428,9 @@ func ConfirmCommitWithExpectedSeqNumRange( } case subErr := <-subscription.Err(): return nil, fmt.Errorf("subscription error: %w", subErr) - case <-timer.C: - return nil, fmt.Errorf("timed out after waiting %s duration for commit report on chain selector %d from source selector %d expected seq nr range %s", - duration.String(), dest.Selector, srcSelector, expectedSeqNumRange.String()) + case <-timeout.C: + return nil, fmt.Errorf("timed out after waiting for commit report on chain selector %d from source selector %d expected seq nr range %s", + dest.Selector, srcSelector, expectedSeqNumRange.String()) case report := <-sink: verified := verifyCommitReport(report) if verified { @@ -389,6 +440,135 @@ func ConfirmCommitWithExpectedSeqNumRange( } } +// Scan for events referencing address +func SolEventEmitter[T any]( + t *testing.T, + client *solrpc.Client, + address solana.PublicKey, + eventType string, + startSlot uint64, + done chan any, +) <-chan T { + ch := make(chan T) + go func() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + var until solana.Signature + for { + select { + case <-done: + return + case <-ticker.C: + // Scan for transactions referencing the address + ctx := context.Background() + txSigs, err := client.GetSignaturesForAddressWithOpts( + ctx, + address, + &solrpc.GetSignaturesForAddressOpts{ + Commitment: solrpc.CommitmentConfirmed, + Until: until, + }, + ) + require.NoError(t, err) + + if len(txSigs) == 0 { + continue + } + + // values are returned ordered newest to oldest, so we replay them backwards + for _, txSig := range slices.Backward(txSigs) { + if txSig.Err != nil { + // We're not interested in failed transactions. + continue + } + if txSig.Slot < startSlot { + // Skip any signatures that are before the starting slot + continue + } + v := uint64(0) // v0 = latest, supports address table lookups + tx, err := client.GetTransaction( + ctx, + txSig.Signature, + &solrpc.GetTransactionOpts{ + Commitment: solrpc.CommitmentConfirmed, + Encoding: solana.EncodingBase64, + MaxSupportedTransactionVersion: &v, + }, + ) + require.NoError(t, err) + require.NotNil(t, tx) + + var event T + err = solcommon.ParseEvent(tx.Meta.LogMessages, eventType, &event, solconfig.PrintEvents) + if err != nil && strings.Contains(err.Error(), "event not found") { + continue + } + require.NoError(t, err) + + select { + case ch <- event: + case <-done: + return + } + } + // next scan should stop at the newest signature we've received + until = txSigs[0].Signature + } + } + }() + + return ch +} + +func ConfirmCommitWithExpectedSeqNumRangeSol( + t *testing.T, + srcSelector uint64, + dest deployment.SolChain, + offrampAddress solana.PublicKey, + startSlot uint64, + expectedSeqNumRange ccipocr3.SeqNumRange, + enforceSingleCommit bool, +) (bool, error) { + seenMessages := NewCommitReportTracker(srcSelector, expectedSeqNumRange) + + done := make(chan any) + defer close(done) + sink := SolEventEmitter[solccip.EventCommitReportAccepted](t, dest.Client, offrampAddress, "CommitReportAccepted", startSlot, done) + + timeout := time.NewTimer(tests.WaitTimeout(t)) + defer timeout.Stop() + + for { + select { + case commitEvent := <-sink: + // if merkle root is zero, it only contains price updates + if commitEvent.Report == nil { + t.Logf("Skipping CommitReportAccepted with only price updates") + continue + } + require.Equal(t, srcSelector, commitEvent.Report.SourceChainSelector) + + // TODO: this logic is duplicated with verifyCommitReport, share + mr := commitEvent.Report + seenMessages.visitCommitReport(mr.SourceChainSelector, mr.MinSeqNr, mr.MaxSeqNr) + if mr.SourceChainSelector == srcSelector && + uint64(expectedSeqNumRange.Start()) >= mr.MinSeqNr && + uint64(expectedSeqNumRange.End()) <= mr.MaxSeqNr { + t.Logf("All sequence numbers committed in a single report [%d, %d]", expectedSeqNumRange.Start(), expectedSeqNumRange.End()) + return true, nil + } + + if !enforceSingleCommit && seenMessages.allCommited(srcSelector) { + t.Logf("All sequence numbers already committed from range [%d, %d]", expectedSeqNumRange.Start(), expectedSeqNumRange.End()) + return true, nil + } + case <-timeout.C: + return false, fmt.Errorf("timed out after waiting for commit report on chain selector %d from source selector %d expected seq nr range %s", + dest.Selector, srcSelector, expectedSeqNumRange.String()) + } + } +} + // ConfirmExecWithSeqNrsForAll waits for all chains in the environment to execute the given expectedSeqNums. // If successful, it returns a map that maps the SourceDestPair to the expected sequence number // to its execution state. @@ -418,18 +598,45 @@ func ConfirmExecWithSeqNrsForAll( } wg.Go(func() error { - innerExecutionStates, err := ConfirmExecWithSeqNrs( - t, - srcChain, - e.Chains[dstChain], - state.Chains[dstChain].OffRamp, - startBlock, - seqRange, - ) + family, err := chainsel.GetSelectorFamily(dstChain) if err != nil { return err } + var innerExecutionStates map[uint64]int + switch family { + case chainsel.FamilyEVM: + innerExecutionStates, err = ConfirmExecWithSeqNrs( + t, + srcChain, + e.Chains[dstChain], + state.Chains[dstChain].OffRamp, + startBlock, + seqRange, + ) + if err != nil { + return err + } + case chainsel.FamilySolana: + var startSlot uint64 + if startBlock != nil { + startSlot = *startBlock + } + innerExecutionStates, err = ConfirmExecWithSeqNrsSol( + t, + srcChain, + e.SolChains[dstChain], + state.SolChains[dstChain].OffRamp, + startSlot, + seqRange, + ) + if err != nil { + return err + } + default: + return fmt.Errorf("unsupported chain family; %v", family) + } + mx.Lock() executionStates[sourceDest] = innerExecutionStates mx.Unlock() @@ -458,8 +665,8 @@ func ConfirmExecWithSeqNrs( return nil, errors.New("no expected sequence numbers provided") } - timer := time.NewTimer(tests.WaitTimeout(t)) - defer timer.Stop() + timeout := time.NewTimer(tests.WaitTimeout(t)) + defer timeout.Stop() tick := time.NewTicker(3 * time.Second) defer tick.Stop() sink := make(chan *offramp.OffRampExecutionStateChanged) @@ -512,7 +719,7 @@ func ConfirmExecWithSeqNrs( return executionStates, nil } } - case <-timer.C: + case <-timeout.C: return nil, fmt.Errorf("timed out waiting for ExecutionStateChanged on chain %d (offramp %s) from chain %d with expected sequence numbers %+v", dest.Selector, offRamp.Address().String(), sourceSelector, expectedSeqNrs) case subErr := <-subscription.Err(): @@ -521,6 +728,51 @@ func ConfirmExecWithSeqNrs( } } +func ConfirmExecWithSeqNrsSol( + t *testing.T, + srcSelector uint64, + dest deployment.SolChain, + offrampAddress solana.PublicKey, + startSlot uint64, + expectedSeqNrs []uint64, +) (executionStates map[uint64]int, err error) { + // TODO: share with EVM + // some state to efficiently track the execution states + // of all the expected sequence numbers. + executionStates = make(map[uint64]int) + seqNrsToWatch := make(map[uint64]struct{}) + for _, seqNr := range expectedSeqNrs { + seqNrsToWatch[seqNr] = struct{}{} + } + + done := make(chan any) + defer close(done) + sink := SolEventEmitter[solccip.EventExecutionStateChanged](t, dest.Client, offrampAddress, "ExecutionStateChanged", startSlot, done) + + timeout := time.NewTimer(tests.WaitTimeout(t)) + defer timeout.Stop() + + for { + select { + case execEvent := <-sink: + // TODO: share with EVM + _, found := seqNrsToWatch[execEvent.SequenceNumber] + if found && execEvent.SourceChainSelector == srcSelector { + t.Logf("Received ExecutionStateChanged (state %s) on chain %d (offramp %s) from chain %d with expected sequence number %d", + execEvent.State.String(), dest.Selector, offrampAddress.String(), srcSelector, execEvent.SequenceNumber) + executionStates[execEvent.SequenceNumber] = int(execEvent.State) + delete(seqNrsToWatch, execEvent.SequenceNumber) + if len(seqNrsToWatch) == 0 { + return executionStates, nil + } + } + case <-timeout.C: + return nil, fmt.Errorf("timed out waiting for ExecutionStateChanged on chain %d (offramp %s) from chain %d with expected sequence numbers %+v", + dest.Selector, offrampAddress.String(), srcSelector, expectedSeqNrs) + } + } +} + func ConfirmNoExecConsistentlyWithSeqNr( t *testing.T, sourceSelector uint64, diff --git a/deployment/ccip/changeset/testhelpers/test_environment.go b/deployment/ccip/changeset/testhelpers/test_environment.go index cd8f009060c..2fcaea9200e 100644 --- a/deployment/ccip/changeset/testhelpers/test_environment.go +++ b/deployment/ccip/changeset/testhelpers/test_environment.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + solanago "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" @@ -22,9 +23,9 @@ import ( commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" solBinary "github.com/gagliardetto/binary" + solFeeQuoter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" "github.com/smartcontractkit/chainlink-ccip/chainconfig" - solFeeQuoter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-ccip/pluginconfig" @@ -702,10 +703,15 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn } for _, chain := range solChains { + // TODO: this is a workaround for tokenConfig.GetTokenInfo + tokenInfo := map[cciptypes.UnknownEncodedAddress]pluginconfig.TokenInfo{} + tokenInfo[cciptypes.UnknownEncodedAddress(state.SolChains[chain].LinkToken.String())] = tokenConfig.TokenSymbolToInfo[changeset.LinkSymbol] + // TODO: point this to proper SOL feed, apparently 0 signified SOL + tokenInfo[cciptypes.UnknownEncodedAddress(solanago.SolMint.String())] = tokenConfig.TokenSymbolToInfo[changeset.WethSymbol] + ocrOverride := tc.OCRConfigOverride ocrParams := v1_6.DeriveCCIPOCRParams( - // TODO: tokenInfo is nil for solana - v1_6.WithDefaultCommitOffChainConfig(e.FeedChainSel, nil), + v1_6.WithDefaultCommitOffChainConfig(e.FeedChainSel, tokenInfo), v1_6.WithDefaultExecuteOffChainConfig(tokenDataProviders), v1_6.WithOCRParamOverride(ocrOverride), ) diff --git a/deployment/ccip/changeset/testhelpers/test_helpers.go b/deployment/ccip/changeset/testhelpers/test_helpers.go index 45e3a2a8b88..cab57b32828 100644 --- a/deployment/ccip/changeset/testhelpers/test_helpers.go +++ b/deployment/ccip/changeset/testhelpers/test_helpers.go @@ -7,6 +7,7 @@ import ( "math/big" "net/http" "net/http/httptest" + "slices" "sort" "strings" "testing" @@ -151,7 +152,29 @@ func DeployTestContracts(t *testing.T, } } +func LatestBlock(ctx context.Context, env deployment.Environment, chainSelector uint64) (uint64, error) { + family, err := chainsel.GetSelectorFamily(chainSelector) + if err != nil { + return 0, err + } + + switch family { + case chainsel.FamilyEVM: + latesthdr, err := env.Chains[chainSelector].Client.HeaderByNumber(ctx, nil) + if err != nil { + return 0, errors.Wrapf(err, "failed to get latest header for chain %d", chainSelector) + } + block := latesthdr.Number.Uint64() + return block, nil + case chainsel.FamilySolana: + return env.SolChains[chainSelector].Client.GetSlot(ctx, solTestConfig.DefaultCommitment) + default: + return 0, errors.New("unsupported chain family") + } +} + func LatestBlocksByChain(ctx context.Context, chains map[uint64]deployment.Chain) (map[uint64]uint64, error) { + // TODO: use LatestBlock and include solchains latestBlocks := make(map[uint64]uint64) for _, chain := range chains { latesthdr, err := chain.Client.HeaderByNumber(ctx, nil) @@ -207,13 +230,7 @@ func CCIPSendRequest( state changeset.CCIPOnChainState, cfg *CCIPSendReqConfig, ) (*types.Transaction, uint64, error) { - msg := router.ClientEVM2AnyMessage{ - Receiver: cfg.Evm2AnyMessage.Receiver, - Data: cfg.Evm2AnyMessage.Data, - TokenAmounts: cfg.Evm2AnyMessage.TokenAmounts, - FeeToken: cfg.Evm2AnyMessage.FeeToken, - ExtraArgs: cfg.Evm2AnyMessage.ExtraArgs, - } + msg := cfg.Evm2AnyMessage r := state.Chains[cfg.SourceChain].Router if cfg.IsTestRouter { r = state.Chains[cfg.SourceChain].TestRouter @@ -286,20 +303,25 @@ func CCIPSendCalldata( return calldata, nil } +// testhelpers.SendRequest(t, e, state, src, dest, msg, opts...) +// opts being testRouter, sender +// always return error +// note: there's also DoSendRequest vs SendRequest duplication, v1.6 vs v1.5 + func TestSendRequest( t *testing.T, e deployment.Environment, state changeset.CCIPOnChainState, src, dest uint64, testRouter bool, - evm2AnyMessage router.ClientEVM2AnyMessage, + msg any, ) (msgSentEvent *onramp.OnRampCCIPMessageSent) { - msgSentEvent, err := DoSendRequest(t, e, state, + msgSentEvent, err := SendRequest(t, e, state, WithSender(e.Chains[src].DeployerKey), WithSourceChain(src), WithDestChain(dest), WithTestRouter(testRouter), - WithEvm2AnyMessage(evm2AnyMessage)) + WithEvm2AnyMessage(msg.(router.ClientEVM2AnyMessage))) require.NoError(t, err) return msgSentEvent } @@ -344,8 +366,8 @@ func WithDestChain(destChain uint64) SendReqOpts { } } -// DoSendRequest similar to TestSendRequest but returns an error. -func DoSendRequest( +// SendRequest similar to TestSendRequest but returns an error. +func SendRequest( t *testing.T, e deployment.Environment, state changeset.CCIPOnChainState, @@ -742,9 +764,8 @@ func deploySingleFeed( } func ConfirmRequestOnSourceAndDest(t *testing.T, env deployment.Environment, state changeset.CCIPOnChainState, sourceCS, destCS, expectedSeqNr uint64) error { - latesthdr, err := env.Chains[destCS].Client.HeaderByNumber(testcontext.Get(t), nil) + startBlock, err := LatestBlock(testcontext.Get(t), env, destCS) require.NoError(t, err) - startBlock := latesthdr.Number.Uint64() fmt.Printf("startblock %d", startBlock) msgSentEvent := TestSendRequest(t, env, state, sourceCS, destCS, false, router.ClientEVM2AnyMessage{ Receiver: common.LeftPadBytes(state.Chains[destCS].Receiver.Address().Bytes(), 32), @@ -1304,18 +1325,17 @@ func Transfer( state changeset.CCIPOnChainState, sourceChain, destChain uint64, tokens []router.ClientEVMTokenAmount, - receiver common.Address, + receiver []byte, data, extraArgs []byte, ) (*onramp.OnRampCCIPMessageSent, map[uint64]*uint64) { startBlocks := make(map[uint64]*uint64) - latesthdr, err := env.Chains[destChain].Client.HeaderByNumber(ctx, nil) + block, err := LatestBlock(ctx, env, destChain) require.NoError(t, err) - block := latesthdr.Number.Uint64() startBlocks[destChain] = &block msgSentEvent := TestSendRequest(t, env, state, sourceChain, destChain, false, router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(receiver.Bytes(), 32), + Receiver: common.LeftPadBytes(receiver, 32), Data: data, TokenAmounts: tokens, FeeToken: common.HexToAddress("0x0"), @@ -1327,13 +1347,13 @@ func Transfer( type TestTransferRequest struct { Name string SourceChain, DestChain uint64 - Receiver common.Address + Receiver []byte ExpectedStatus int // optional Tokens []router.ClientEVMTokenAmount Data []byte ExtraArgs []byte - ExpectedTokenBalances map[common.Address]*big.Int + ExpectedTokenBalances []ExpectedBalance } // TransferMultiple sends multiple CCIPMessages (represented as TestTransferRequest) sequentially. @@ -1353,7 +1373,7 @@ func TransferMultiple( map[uint64]*uint64, map[SourceDestPair]cciptypes.SeqNumRange, map[SourceDestPair]map[uint64]int, - map[uint64]map[TokenReceiverIdentifier]*big.Int, + map[uint64][]ExpectedTokenBalance, ) { startBlocks := make(map[uint64]*uint64) expectedSeqNums := make(map[SourceDestPair]cciptypes.SeqNumRange) @@ -1396,67 +1416,49 @@ func TransferMultiple( return startBlocks, expectedSeqNums, expectedExecutionStates, expectedTokenBalances } -// TransferAndWaitForSuccess sends a message from sourceChain to destChain and waits for it to be executed -func TransferAndWaitForSuccess( - ctx context.Context, - t *testing.T, - env deployment.Environment, - state changeset.CCIPOnChainState, - sourceChain, destChain uint64, - tokens []router.ClientEVMTokenAmount, - receiver common.Address, - data []byte, - expectedStatus int, - extraArgs []byte, -) { - identifier := SourceDestPair{ - SourceChainSelector: sourceChain, - DestChainSelector: destChain, - } - - expectedSeqNum := make(map[SourceDestPair]uint64) - expectedSeqNumExec := make(map[SourceDestPair][]uint64) - - msgSentEvent, startBlocks := Transfer(ctx, t, env, state, sourceChain, destChain, tokens, receiver, data, extraArgs) - expectedSeqNum[identifier] = msgSentEvent.SequenceNumber - expectedSeqNumExec[identifier] = []uint64{msgSentEvent.SequenceNumber} - - // Wait for all commit reports to land. - ConfirmCommitForAllWithExpectedSeqNums(t, env, state, expectedSeqNum, startBlocks) - - // Wait for all exec reports to land - states := ConfirmExecWithSeqNrsForAll(t, env, state, expectedSeqNumExec, startBlocks) - require.Equal(t, expectedStatus, states[identifier][msgSentEvent.SequenceNumber]) -} - // TokenBalanceAccumulator is a convenient accumulator to aggregate expected balances of different tokens // used across the tests. You can iterate over your test cases and build the final "expected" balances for tokens (per chain, per sender) // For instance, if your test runs multiple transfers for the same token, and you want to verify the balance of tokens at // the end of the execution, you can simply use that struct for aggregating expected tokens // Please also see WaitForTokenBalances to better understand how you can assert token balances -type TokenBalanceAccumulator map[uint64]map[TokenReceiverIdentifier]*big.Int +type TokenBalanceAccumulator map[uint64][]ExpectedTokenBalance func (t TokenBalanceAccumulator) add( destChain uint64, - receiver common.Address, - expectedBalance map[common.Address]*big.Int) { - for token, balance := range expectedBalance { + receiver []byte, + expectedBalances []ExpectedBalance) { + for _, expected := range expectedBalances { + token := expected.Token + balance := expected.Amount tkIdentifier := TokenReceiverIdentifier{token, receiver} - if _, ok := t[destChain]; !ok { - t[destChain] = make(map[TokenReceiverIdentifier]*big.Int) - } - actual, ok := t[destChain][tkIdentifier] - if !ok { - actual = big.NewInt(0) + idx := slices.IndexFunc(t[destChain], func(b ExpectedTokenBalance) bool { + return slices.Equal(b.Receiver.receiver, tkIdentifier.receiver) && slices.Equal(b.Receiver.token, tkIdentifier.token) + }) + + if idx < 0 { + t[destChain] = append(t[destChain], ExpectedTokenBalance{ + Receiver: tkIdentifier, + Amount: balance, + }) + } else { + t[destChain][idx].Amount = new(big.Int).Add(t[destChain][idx].Amount, balance) } - t[destChain][tkIdentifier] = new(big.Int).Add(actual, balance) } } +type ExpectedBalance struct { + Token []byte + Amount *big.Int +} + +type ExpectedTokenBalance struct { + Receiver TokenReceiverIdentifier + Amount *big.Int +} type TokenReceiverIdentifier struct { - token common.Address - receiver common.Address + token []byte + receiver []byte } // WaitForTokenBalances waits for multiple ERC20 tokens to reach a particular balance @@ -1466,16 +1468,33 @@ type TokenReceiverIdentifier struct { func WaitForTokenBalances( ctx context.Context, t *testing.T, - chains map[uint64]deployment.Chain, - expectedBalances map[uint64]map[TokenReceiverIdentifier]*big.Int, + env deployment.Environment, + expectedBalances map[uint64][]ExpectedTokenBalance, ) { errGrp := &errgroup.Group{} - for chainID, tokens := range expectedBalances { - for id, balance := range tokens { - id := id - balance := balance + for chainSelector, tokens := range expectedBalances { + for _, expected := range tokens { + id := expected.Receiver + balance := expected.Amount errGrp.Go(func() error { - WaitForTheTokenBalance(ctx, t, id.token, id.receiver, chains[chainID], balance) + family, err := chainsel.GetSelectorFamily(chainSelector) + if err != nil { + return err + } + + switch family { + case chainsel.FamilyEVM: + token := common.BytesToAddress(id.token) + receiver := common.BytesToAddress(id.receiver) + WaitForTheTokenBalance(ctx, t, token, receiver, env.Chains[chainSelector], balance) + case chainsel.FamilySolana: + expectedBalance := balance.Uint64() + // TODO: need to pass env rather than chains + token := solana.PublicKeyFromBytes(id.token) + receiver := solana.PublicKeyFromBytes(id.receiver) + WaitForTheTokenBalanceSol(ctx, t, token, receiver, env.SolChains[chainSelector], expectedBalance) + default: + } return nil }) } @@ -1509,26 +1528,27 @@ func WaitForTheTokenBalance( }, tests.WaitTimeout(t), 100*time.Millisecond) } -func GetTokenBalance( +func WaitForTheTokenBalanceSol( ctx context.Context, t *testing.T, - token common.Address, - receiver common.Address, - chain deployment.Chain, -) *big.Int { - tokenContract, err := burn_mint_erc677.NewBurnMintERC677(token, chain.Client) - require.NoError(t, err) - - balance, err := tokenContract.BalanceOf(&bind.CallOpts{Context: ctx}, receiver) - require.NoError(t, err) - - t.Log("Getting token balance", - "actual", balance, - "token", token, - "receiver", receiver, - ) + token solana.PublicKey, + receiver solana.PublicKey, + chain deployment.SolChain, + expected uint64, +) { + require.Eventually(t, func() bool { + _, balance, berr := solTokenUtil.TokenBalance(ctx, chain.Client, receiver, solTestConfig.DefaultCommitment) + require.NoError(t, berr) + // TODO: validate receiver's token mint == token - return balance + t.Log("Waiting for the token balance", + "expected", expected, + "actual", balance, + "token", token, + "receiver", receiver, + ) + return uint64(balance) == expected + }, tests.WaitTimeout(t), 100*time.Millisecond) } func DefaultRouterMessage(receiverAddress common.Address) router.ClientEVM2AnyMessage { diff --git a/deployment/ccip/changeset/v1_6/cs_active_candidate_test.go b/deployment/ccip/changeset/v1_6/cs_active_candidate_test.go index 55326fd7111..875a0e7bc00 100644 --- a/deployment/ccip/changeset/v1_6/cs_active_candidate_test.go +++ b/deployment/ccip/changeset/v1_6/cs_active_candidate_test.go @@ -138,9 +138,8 @@ func Test_ActiveCandidate(t *testing.T) { testhelpers.AssertTimelockOwnership(t, tenv, allChains, state) sendMsg := func() { - latesthdr, err := tenv.Env.Chains[dest].Client.HeaderByNumber(testcontext.Get(t), nil) + block, err := testhelpers.LatestBlock(testcontext.Get(t), tenv.Env, dest) require.NoError(t, err) - block := latesthdr.Number.Uint64() msgSentEvent := testhelpers.TestSendRequest(t, tenv.Env, state, source, dest, false, router.ClientEVM2AnyMessage{ Receiver: common.LeftPadBytes(state.Chains[dest].Receiver.Address().Bytes(), 32), Data: []byte("hello world"), diff --git a/deployment/ccip/changeset/v1_6/cs_add_lane_test.go b/deployment/ccip/changeset/v1_6/cs_add_lane_test.go index 175a16e9c31..7f7b239b8d0 100644 --- a/deployment/ccip/changeset/v1_6/cs_add_lane_test.go +++ b/deployment/ccip/changeset/v1_6/cs_add_lane_test.go @@ -27,9 +27,8 @@ func TestAddLanesWithTestRouter(t *testing.T) { startBlocks := make(map[uint64]*uint64) // Send a message from each chain to every other chain. expectedSeqNumExec := make(map[testhelpers.SourceDestPair][]uint64) - latesthdr, err := e.Env.Chains[chain2].Client.HeaderByNumber(testcontext.Get(t), nil) + block, err := testhelpers.LatestBlock(testcontext.Get(t), e.Env, chain2) require.NoError(t, err) - block := latesthdr.Number.Uint64() startBlocks[chain2] = &block msgSentEvent := testhelpers.TestSendRequest(t, e.Env, state, chain1, chain2, true, router.ClientEVM2AnyMessage{ Receiver: common.LeftPadBytes(state.Chains[chain2].Receiver.Address().Bytes(), 32), diff --git a/deployment/ccip/changeset/v1_6/cs_ccip_home.go b/deployment/ccip/changeset/v1_6/cs_ccip_home.go index ca8fe58e13b..96c476c5e2e 100644 --- a/deployment/ccip/changeset/v1_6/cs_ccip_home.go +++ b/deployment/ccip/changeset/v1_6/cs_ccip_home.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + chain_selectors "github.com/smartcontractkit/chain-selectors" "golang.org/x/exp/maps" mcmslib "github.com/smartcontractkit/mcms" @@ -96,6 +97,16 @@ func validateCommitOffchainConfig(c *pluginconfig.CommitOffchainConfig, selector if err := c.Validate(); err != nil { return fmt.Errorf("invalid commit off-chain config: %w", err) } + + family, err := chain_selectors.GetSelectorFamily(selector) + if err != nil { + return err + } + if family != chain_selectors.FamilyEVM { + // TODO: implement more proper validation + return nil + } + for tokenAddr, tokenConfig := range c.TokenInfo { tokenUnknownAddr, err := ccipocr3.NewUnknownAddressFromHex(string(tokenAddr)) if err != nil { diff --git a/deployment/environment/memory/chain.go b/deployment/environment/memory/chain.go index 2c91c86ab8a..1c5105f2978 100644 --- a/deployment/environment/memory/chain.go +++ b/deployment/environment/memory/chain.go @@ -8,6 +8,7 @@ import ( "math/big" "os" "path/filepath" + "runtime" "strconv" "sync" "testing" @@ -253,7 +254,13 @@ func solChain(t *testing.T, chainID uint64, adminKey *solana.PrivateKey) (string for i := 0; i < maxRetries; i++ { port := freeport.GetOne(t) + image := "" + if runtime.GOOS == "linux" { + image = "solanalabs/solana:v1.18.26" // TODO: workaround on linux + } + bcInput := &blockchain.Input{ + Image: image, Type: "solana", ChainID: strconv.FormatUint(chainID, 10), PublicKey: adminKey.PublicKey().String(), diff --git a/deployment/environment/memory/node.go b/deployment/environment/memory/node.go index d4a950f431f..951ff2d25a3 100644 --- a/deployment/environment/memory/node.go +++ b/deployment/environment/memory/node.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gagliardetto/solana-go" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" @@ -27,6 +28,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" mnCfg "github.com/smartcontractkit/chainlink-framework/multinode/config" + solrpc "github.com/gagliardetto/solana-go/rpc" solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink/deployment" @@ -479,10 +481,11 @@ func CreateKeys(t *testing.T, // need to look more into it, but it seems like with sim chains nodes are sending txs with 0x from address fundAddress(t, chain.DeployerKey, common.Address{}, assets.Ether(1000).ToInt(), backend) case chainsel.FamilyAptos: - err = app.GetKeyStore().Aptos().EnsureKey(ctx) + keystore := app.GetKeyStore().Aptos() + err = keystore.EnsureKey(ctx) require.NoError(t, err, "failed to create key for aptos") - keys, err := app.GetKeyStore().Aptos().GetAll() + keys, err := keystore.GetAll() require.NoError(t, err) require.Len(t, keys, 1) @@ -491,23 +494,22 @@ func CreateKeys(t *testing.T, // TODO: funding case chainsel.FamilyStarknet: - err = app.GetKeyStore().StarkNet().EnsureKey(ctx) + keystore := app.GetKeyStore().StarkNet() + err = keystore.EnsureKey(ctx) require.NoError(t, err, "failed to create key for starknet") - keys, err := app.GetKeyStore().StarkNet().GetAll() + keys, err := keystore.GetAll() require.NoError(t, err) require.Len(t, keys, 1) transmitter := keys[0] transmitters[chain.Selector] = transmitter.ID() - - // TODO: funding default: // TODO: other transmission keys unsupported for now } } - for chain := range solchains { + for chainSelector, chain := range solchains { ctype := chaintype.Solana err = app.GetKeyStore().OCR2().EnsureKeys(ctx, ctype) require.NoError(t, err) @@ -526,9 +528,9 @@ func CreateKeys(t *testing.T, require.Len(t, solkeys, 1) transmitter := solkeys[0] - transmitters[chain] = transmitter.ID() + transmitters[chainSelector] = transmitter.ID() - // TODO: funding + FundSolAccounts(ctx, []solana.PublicKey{transmitter.PublicKey()}, chain.Client, t) } return Keys{ @@ -539,6 +541,16 @@ func CreateKeys(t *testing.T, } } +func FundSolAccounts(ctx context.Context, accounts []solana.PublicKey, solanaGoClient *solrpc.Client, t *testing.T) { + sigs := []solana.Signature{} + for _, v := range accounts { + sig, err := solanaGoClient.RequestAirdrop(ctx, v, 1000*solana.LAMPORTS_PER_SOL, solrpc.CommitmentConfirmed) + require.NoError(t, err) + sigs = append(sigs, sig) + } + // we don't wait for confirmation so we don't block the tests, it'll take a while before nodes start transmitting +} + func createConfigV2Chain(chainID uint64) *v2toml.EVMConfig { chainIDBig := evmutils.NewI(int64(chainID)) chain := v2toml.Defaults(chainIDBig) @@ -558,6 +570,12 @@ func createSolanaChainConfig(chainID string, chain deployment.SolChain) *solcfg. chainConfig := solcfg.Chain{} chainConfig.SetDefaults() + // CCIP requires a non-zero execution fee estimate + computeUnitPriceDefault := uint64(100) + txRetentionTimeout := config.MustNewDuration(10 * time.Minute) + chainConfig.ComputeUnitPriceDefault = &computeUnitPriceDefault + chainConfig.TxRetentionTimeout = txRetentionTimeout + url, err := config.ParseURL(chain.URL) if err != nil { panic(err) diff --git a/integration-tests/smoke/ccip/ccip_disable_lane_test.go b/integration-tests/smoke/ccip/ccip_disable_lane_test.go index e6381100d3a..b19e05a5477 100644 --- a/integration-tests/smoke/ccip/ccip_disable_lane_test.go +++ b/integration-tests/smoke/ccip/ccip_disable_lane_test.go @@ -46,7 +46,7 @@ func TestDisableLane(t *testing.T) { wethPrice = deployment.E18Mult(4000) noOfRequests = 3 sendmessage = func(src, dest uint64, deployer *bind.TransactOpts) (*onramp.OnRampCCIPMessageSent, error) { - return testhelpers.DoSendRequest( + return testhelpers.SendRequest( t, e, state, diff --git a/integration-tests/smoke/ccip/ccip_message_limitations_test.go b/integration-tests/smoke/ccip/ccip_message_limitations_test.go index 11682f8003c..c357555adb4 100644 --- a/integration-tests/smoke/ccip/ccip_message_limitations_test.go +++ b/integration-tests/smoke/ccip/ccip_message_limitations_test.go @@ -152,7 +152,7 @@ func Test_CCIPMessageLimitations(t *testing.T) { t.Logf("Sending msg: %s", msg.name) require.NotEqual(t, msg.fromChain, msg.toChain, "fromChain and toChain cannot be the same") startBlocks[msg.toChain] = nil - msgSentEvent, err := testhelpers.DoSendRequest( + msgSentEvent, err := testhelpers.SendRequest( t, testEnv.Env, onChainState, testhelpers.WithSourceChain(msg.fromChain), testhelpers.WithDestChain(msg.toChain), diff --git a/integration-tests/smoke/ccip/ccip_messaging_test.go b/integration-tests/smoke/ccip/ccip_messaging_test.go index 33133bd958e..71060e09412 100644 --- a/integration-tests/smoke/ccip/ccip_messaging_test.go +++ b/integration-tests/smoke/ccip/ccip_messaging_test.go @@ -2,6 +2,7 @@ package ccip import ( "context" + "fmt" "sync" "sync/atomic" "testing" @@ -9,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" "golang.org/x/exp/maps" @@ -20,6 +22,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment/ccip/manualexechelpers" "github.com/smartcontractkit/chainlink/deployment/environment/memory" testsetups "github.com/smartcontractkit/chainlink/integration-tests/testsetups/ccip" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/v1_6_0/message_hasher" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/v1_6_0/offramp" ) @@ -93,7 +96,7 @@ func Test_CCIPMessaging(t *testing.T) { TestSetup: setup, Replayed: replayed, Nonce: nonce, - Receiver: common.HexToAddress("0xdead"), + Receiver: common.HexToAddress("0xdead").Bytes(), MsgData: []byte("hello eoa"), ExtraArgs: nil, // default extraArgs ExpectedExecutionState: testhelpers.EXECUTION_STATE_SUCCESS, // success because offRamp won't call an EOA @@ -112,7 +115,7 @@ func Test_CCIPMessaging(t *testing.T) { TestSetup: setup, Replayed: out.Replayed, Nonce: out.Nonce, - Receiver: state.Chains[destChain].FeeQuoter.Address(), + Receiver: state.Chains[destChain].FeeQuoter.Address().Bytes(), MsgData: []byte("hello FeeQuoter"), ExtraArgs: nil, // default extraArgs ExpectedExecutionState: testhelpers.EXECUTION_STATE_SUCCESS, // success because offRamp won't call a contract not implementing CCIPReceiver @@ -121,14 +124,14 @@ func Test_CCIPMessaging(t *testing.T) { }) t.Run("message to contract implementing CCIPReceiver", func(t *testing.T) { - latestHead, err := e.Env.Chains[destChain].Client.HeaderByNumber(ctx, nil) + latestHead, err := testhelpers.LatestBlock(ctx, e.Env, destChain) require.NoError(t, err) out = mt.Run( mt.TestCase{ TestSetup: setup, Replayed: out.Replayed, Nonce: out.Nonce, - Receiver: state.Chains[destChain].Receiver.Address(), + Receiver: state.Chains[destChain].Receiver.Address().Bytes(), MsgData: []byte("hello CCIPReceiver"), ExtraArgs: nil, // default extraArgs ExpectedExecutionState: testhelpers.EXECUTION_STATE_SUCCESS, @@ -136,7 +139,7 @@ func Test_CCIPMessaging(t *testing.T) { func(t *testing.T) { iter, err := state.Chains[destChain].Receiver.FilterMessageReceived(&bind.FilterOpts{ Context: ctx, - Start: latestHead.Number.Uint64(), + Start: latestHead, }) require.NoError(t, err) require.True(t, iter.Next()) @@ -153,7 +156,7 @@ func Test_CCIPMessaging(t *testing.T) { TestSetup: setup, Replayed: out.Replayed, Nonce: out.Nonce, - Receiver: state.Chains[destChain].Receiver.Address(), + Receiver: state.Chains[destChain].Receiver.Address().Bytes(), MsgData: []byte("hello CCIPReceiver with low exec gas"), ExtraArgs: testhelpers.MakeEVMExtraArgsV2(1, false), // 1 gas is too low. ExpectedExecutionState: testhelpers.EXECUTION_STATE_FAILURE, // state would be failed onchain due to low gas @@ -185,6 +188,101 @@ func Test_CCIPMessaging(t *testing.T) { require.Equal(t, int32(0), ms.reExecutionsObserved.Load()) } +// NOTE: this is EVM specific (EVM->SVM) +const SVMExtraArgsV1Tag = "0x1f3b3aba" + +func SerializeSVMExtraArgs(data message_hasher.ClientSVMExtraArgsV1) ([]byte, error) { + tagBytes := hexutil.MustDecode(SVMExtraArgsV1Tag) + abi, err := message_hasher.MessageHasherMetaData.GetAbi() + if err != nil { + return nil, err + } + v, err := abi.Methods["encodeSVMExtraArgsV1"].Inputs.Pack(data) + return append(tagBytes, v...), err +} + +func Test_CCIPMessaging_Solana(t *testing.T) { + // Setup 2 chains (EVM and Solana) and a single lane. + ctx := testhelpers.Context(t) + e, _, _ := testsetups.NewIntegrationEnvironment(t, testhelpers.WithSolChains(1)) + + state, err := changeset.LoadOnchainState(e.Env) + require.NoError(t, err) + + allChainSelectors := maps.Keys(e.Env.Chains) + allSolChainSelectors := maps.Keys(e.Env.SolChains) + sourceChain := allChainSelectors[0] + destChain := allSolChainSelectors[0] + t.Log("All chain selectors:", allChainSelectors, + ", sol chain selectors:", allSolChainSelectors, + ", home chain selector:", e.HomeChainSel, + ", feed chain selector:", e.FeedChainSel, + ", source chain selector:", sourceChain, + ", dest chain selector:", destChain, + ) + // connect a single lane, source to dest + testhelpers.AddLaneWithDefaultPricesAndFeeQuoterConfig(t, &e, state, sourceChain, destChain, false) + + var ( + replayed bool + nonce uint64 + sender = common.LeftPadBytes(e.Env.Chains[sourceChain].DeployerKey.From.Bytes(), 32) + out mt.TestCaseOutput + setup = mt.NewTestSetupWithDeployedEnv( + t, + e, + state, + sourceChain, + destChain, + sender, + false, // testRouter + true, // validateResp + ) + ) + + // message := ccip_router.SVM2AnyMessage{ + // Receiver: validReceiverAddress[:], + // FeeToken: wsol.mint, + // TokenAmounts: []ccip_router.SVMTokenAmount{{Token: token0.Mint.PublicKey(), Amount: 1}}, + // ExtraArgs: emptyEVMExtraArgsV2, + // } + + t.Run("message to contract implementing CCIPReceiver", func(t *testing.T) { + latestSlot, err := testhelpers.LatestBlock(ctx, e.Env, destChain) + require.NoError(t, err) + receiver := state.SolChains[destChain].Receiver.Bytes() + extraArgs, err := SerializeSVMExtraArgs(message_hasher.ClientSVMExtraArgsV1{}) // SVM doesn't allow an empty extraArgs + require.NoError(t, err) + out = mt.Run( + mt.TestCase{ + TestSetup: setup, + Replayed: replayed, + Nonce: nonce, + Receiver: receiver, + MsgData: []byte("hello CCIPReceiver"), + ExtraArgs: extraArgs, + ExpectedExecutionState: testhelpers.EXECUTION_STATE_SUCCESS, + ExtraAssertions: []func(t *testing.T){ + func(t *testing.T) { + // TODO: lookup event state, assert counter incremented + // state.SolChains[destChain].Receiver + // TODO: fix up, use the same code event filter does + iter, err := state.Chains[destChain].Receiver.FilterMessageReceived(&bind.FilterOpts{ + Context: ctx, + Start: latestSlot, + }) + require.NoError(t, err) + require.True(t, iter.Next()) + // MessageReceived doesn't emit the data unfortunately, so can't check that. + }, + }, + }, + ) + }) + + fmt.Printf("out: %v\n", out) +} + type monitorState struct { reExecutionsObserved atomic.Int32 } diff --git a/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go b/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go index ec689da2a13..b3a2a2f0542 100644 --- a/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go +++ b/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go @@ -251,7 +251,7 @@ func TestMigrateFromV1_5ToV1_6(t *testing.T) { startBlocks[dest] = &block expectedSeqNumExec := make(map[testhelpers.SourceDestPair][]uint64) expectedSeqNums := make(map[testhelpers.SourceDestPair]uint64) - msgSentEvent, err := testhelpers.DoSendRequest( + msgSentEvent, err := testhelpers.SendRequest( t, e.Env, state, testhelpers.WithSourceChain(src1), testhelpers.WithDestChain(dest), diff --git a/integration-tests/smoke/ccip/ccip_ooo_execution_test.go b/integration-tests/smoke/ccip/ccip_ooo_execution_test.go index aea97dcaf64..010aa49ca45 100644 --- a/integration-tests/smoke/ccip/ccip_ooo_execution_test.go +++ b/integration-tests/smoke/ccip/ccip_ooo_execution_test.go @@ -126,7 +126,7 @@ func Test_OutOfOrderExecution(t *testing.T) { sourceChain, destChain, tokenTransfer, - firstReceiver, + firstReceiver.Bytes(), nil, testhelpers.MakeEVMExtraArgsV2(0, true), ) @@ -145,7 +145,7 @@ func Test_OutOfOrderExecution(t *testing.T) { sourceChain, destChain, usdcTransfer, - secondReceiver, + secondReceiver.Bytes(), nil, nil, ) @@ -163,7 +163,7 @@ func Test_OutOfOrderExecution(t *testing.T) { sourceChain, destChain, tokenTransfer, - thirdReceiver, + thirdReceiver.Bytes(), nil, testhelpers.MakeEVMExtraArgsV2(0, false), ) @@ -181,7 +181,7 @@ func Test_OutOfOrderExecution(t *testing.T) { sourceChain, destChain, tokenTransfer, - fourthReceiver, + fourthReceiver.Bytes(), []byte("this message has enough gas to execute"), testhelpers.MakeEVMExtraArgsV2(300_000, true), ) @@ -192,7 +192,7 @@ func Test_OutOfOrderExecution(t *testing.T) { // Ordered token transfer, but using different sender, should be executed fifthReceiver := utils.RandomAddress() - fifthMessage, err := testhelpers.DoSendRequest(t, e, state, + fifthMessage, err := testhelpers.SendRequest(t, e, state, testhelpers.WithSender(anotherSender), testhelpers.WithSourceChain(sourceChain), testhelpers.WithDestChain(destChain), diff --git a/integration-tests/smoke/ccip/ccip_token_transfer_test.go b/integration-tests/smoke/ccip/ccip_token_transfer_test.go index 827047e193a..53d61e6af05 100644 --- a/integration-tests/smoke/ccip/ccip_token_transfer_test.go +++ b/integration-tests/smoke/ccip/ccip_token_transfer_test.go @@ -6,7 +6,7 @@ import ( "golang.org/x/exp/maps" - "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -16,7 +16,9 @@ import ( "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" testsetups "github.com/smartcontractkit/chainlink/integration-tests/testsetups/ccip" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/v1_2_0/router" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -100,9 +102,9 @@ func TestTokenTransfer(t *testing.T) { Amount: oneE18, }, }, - Receiver: utils.RandomAddress(), - ExpectedTokenBalances: map[common.Address]*big.Int{ - destToken.Address(): oneE18, + Receiver: utils.RandomAddress().Bytes(), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {destToken.Address().Bytes(), oneE18}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, @@ -116,9 +118,9 @@ func TestTokenTransfer(t *testing.T) { Amount: oneE18, }, }, - Receiver: state.Chains[destChain].Receiver.Address(), - ExpectedTokenBalances: map[common.Address]*big.Int{ - destToken.Address(): oneE18, + Receiver: state.Chains[destChain].Receiver.Address().Bytes(), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {destToken.Address().Bytes(), oneE18}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, @@ -140,11 +142,11 @@ func TestTokenTransfer(t *testing.T) { Amount: oneE18, }, }, - Receiver: state.Chains[sourceChain].Receiver.Address(), + Receiver: state.Chains[sourceChain].Receiver.Address().Bytes(), ExtraArgs: testhelpers.MakeEVMExtraArgsV2(300_000, false), - ExpectedTokenBalances: map[common.Address]*big.Int{ - selfServeSrcToken.Address(): new(big.Int).Add(oneE18, oneE18), - srcToken.Address(): oneE18, + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {selfServeSrcToken.Address().Bytes(), new(big.Int).Add(oneE18, oneE18)}, + {srcToken.Address().Bytes(), oneE18}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, @@ -162,11 +164,11 @@ func TestTokenTransfer(t *testing.T) { Amount: new(big.Int).Add(oneE18, oneE18), }, }, - Receiver: utils.RandomAddress(), + Receiver: utils.RandomAddress().Bytes(), ExtraArgs: testhelpers.MakeEVMExtraArgsV2(1, false), - ExpectedTokenBalances: map[common.Address]*big.Int{ - selfServeSrcToken.Address(): oneE18, - srcToken.Address(): new(big.Int).Add(oneE18, oneE18), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {selfServeSrcToken.Address().Bytes(), oneE18}, + {srcToken.Address().Bytes(), new(big.Int).Add(oneE18, oneE18)}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, @@ -184,12 +186,12 @@ func TestTokenTransfer(t *testing.T) { Amount: oneE18, }, }, - Receiver: state.Chains[sourceChain].Receiver.Address(), + Receiver: state.Chains[sourceChain].Receiver.Address().Bytes(), Data: []byte("this should be reverted because gasLimit is too low, no tokens are transferred as well"), ExtraArgs: testhelpers.MakeEVMExtraArgsV2(1, false), - ExpectedTokenBalances: map[common.Address]*big.Int{ - selfServeSrcToken.Address(): big.NewInt(0), - srcToken.Address(): big.NewInt(0), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {selfServeSrcToken.Address().Bytes(), big.NewInt(0)}, + {srcToken.Address().Bytes(), big.NewInt(0)}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_FAILURE, }, @@ -200,8 +202,155 @@ func TestTokenTransfer(t *testing.T) { err = testhelpers.ConfirmMultipleCommits( t, - e.Chains, - state.Chains, + e, + state, + startBlocks, + false, + expectedSeqNums, + ) + require.NoError(t, err) + + execStates := testhelpers.ConfirmExecWithSeqNrsForAll( + t, + e, + state, + testhelpers.SeqNumberRangeToSlice(expectedSeqNums), + startBlocks, + ) + require.Equal(t, expectedExecutionStates, execStates) + + testhelpers.WaitForTokenBalances(ctx, t, e, expectedTokenBalances) +} + +func TestTokenTransfer_Solana(t *testing.T) { + // lggr := logger.TestLogger(t) + ctx := tests.Context(t) + + tenv, _, _ := testsetups.NewIntegrationEnvironment(t, + testhelpers.WithNumOfUsersPerChain(3), + testhelpers.WithSolChains(1)) + + e := tenv.Env + state, err := changeset.LoadOnchainState(e) + require.NoError(t, err) + require.GreaterOrEqual(t, len(e.Chains), 2) + + allChainSelectors := maps.Keys(e.Chains) + allSolChainSelectors := maps.Keys(e.SolChains) + sourceChain, destChain := allChainSelectors[0], allSolChainSelectors[0] + ownerSourceChain := e.Chains[sourceChain].DeployerKey + // ownerDestChain := e.SolChains[destChain].DeployerKey + + require.GreaterOrEqual(t, len(tenv.Users[sourceChain]), 2) + // require.GreaterOrEqual(t, len(tenv.Users[destChain]), 2) TODO: ??? + // selfServeSrcTokenPoolDeployer := tenv.Users[sourceChain][1] + // selfServeDestTokenPoolDeployer := tenv.Users[destChain][1] + + oneE18 := new(big.Int).SetUint64(1e18) + + // Deploy tokens and pool by CCIP Owner + var srcToken *burn_mint_erc677.BurnMintERC677 + var destToken solana.PublicKey + // TODO: + // srcToken, _, destToken, err := testhelpers.DeployTransferableTokenSolana( + // t, + // lggr, + // e, + // sourceChain, + // destChain, + // ownerSourceChain, + // e.ExistingAddresses, + // "OWNER_TOKEN", + // ) + // require.NoError(t, err) + + // TODO: we need to initialize ATA for receiver? + + // Deploy Self Serve tokens and pool + // selfServeSrcToken, _, selfServeDestToken, _, err := testhelpers.DeployTransferableToken( + // lggr, + // tenv.Env.Chains, + // sourceChain, + // destChain, + // selfServeSrcTokenPoolDeployer, + // selfServeDestTokenPoolDeployer, + // state, + // e.ExistingAddresses, + // "SELF_SERVE_TOKEN", + // ) + // require.NoError(t, err) + // + testhelpers.AddLanesForAll(t, &tenv, state) + + testhelpers.MintAndAllow( + t, + e, + state, + map[uint64][]testhelpers.MintTokenInfo{ + sourceChain: { + // testhelpers.NewMintTokenInfo(selfServeSrcTokenPoolDeployer, selfServeSrcToken), + testhelpers.NewMintTokenInfo(ownerSourceChain, srcToken), + }, + // destChain: { + // // testhelpers.NewMintTokenInfo(selfServeDestTokenPoolDeployer, selfServeDestToken), + // testhelpers.NewMintTokenInfo(ownerDestChain, destToken), + // }, + }, + ) + // TODO: how to do MintAndAllow on Solana? + + tcs := []testhelpers.TestTransferRequest{ + { + Name: "Send token to contract", + SourceChain: sourceChain, + DestChain: destChain, + Tokens: []router.ClientEVMTokenAmount{ + { + Token: srcToken.Address(), + Amount: oneE18, + }, + }, + Receiver: state.Chains[destChain].Receiver.Address().Bytes(), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {destToken.Bytes(), oneE18}, + }, + ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, + }, + // { + // Name: "Send N tokens to contract", + // SourceChain: destChain, + // DestChain: sourceChain, + // Tokens: []router.ClientEVMTokenAmount{ + // { + // Token: selfServeDestToken.Address(), + // Amount: oneE18, + // }, + // { + // Token: destToken.Address(), + // Amount: oneE18, + // }, + // { + // Token: selfServeDestToken.Address(), + // Amount: oneE18, + // }, + // }, + // Receiver: state.Chains[sourceChain].Receiver.Address().Bytes(), + // ExtraArgs: testhelpers.MakeEVMExtraArgsV2(300_000, false), + // ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + // {selfServeSrcToken.Address().Bytes(), new(big.Int).Add(oneE18, oneE18)}, + // {srcToken.Address().Bytes(), oneE18}, + // }, + // ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, + // }, + } + + startBlocks, expectedSeqNums, expectedExecutionStates, expectedTokenBalances := + testhelpers.TransferMultiple(ctx, t, e, state, tcs) + + err = testhelpers.ConfirmMultipleCommits( + t, + e, + state, startBlocks, false, expectedSeqNums, @@ -217,5 +366,5 @@ func TestTokenTransfer(t *testing.T) { ) require.Equal(t, expectedExecutionStates, execStates) - testhelpers.WaitForTokenBalances(ctx, t, e.Chains, expectedTokenBalances) + testhelpers.WaitForTokenBalances(ctx, t, e, expectedTokenBalances) } diff --git a/integration-tests/smoke/ccip/ccip_usdc_test.go b/integration-tests/smoke/ccip/ccip_usdc_test.go index e7af88ceedc..ea21ad1bb93 100644 --- a/integration-tests/smoke/ccip/ccip_usdc_test.go +++ b/integration-tests/smoke/ccip/ccip_usdc_test.go @@ -4,7 +4,6 @@ import ( "math/big" "testing" - "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "golang.org/x/exp/maps" @@ -102,7 +101,7 @@ func TestUSDCTokenTransfer(t *testing.T) { tcs := []testhelpers.TestTransferRequest{ { Name: "single USDC token transfer to EOA", - Receiver: utils.RandomAddress(), + Receiver: utils.RandomAddress().Bytes(), SourceChain: chainC, DestChain: chainA, Tokens: []router.ClientEVMTokenAmount{ @@ -110,14 +109,14 @@ func TestUSDCTokenTransfer(t *testing.T) { Token: cChainUSDC.Address(), Amount: tinyOneCoin, }}, - ExpectedTokenBalances: map[common.Address]*big.Int{ - aChainUSDC.Address(): tinyOneCoin, + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {aChainUSDC.Address().Bytes(), tinyOneCoin}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, { Name: "multiple USDC tokens within the same message", - Receiver: utils.RandomAddress(), + Receiver: utils.RandomAddress().Bytes(), SourceChain: chainC, DestChain: chainA, Tokens: []router.ClientEVMTokenAmount{ @@ -130,15 +129,15 @@ func TestUSDCTokenTransfer(t *testing.T) { Amount: tinyOneCoin, }, }, - ExpectedTokenBalances: map[common.Address]*big.Int{ + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ // 2 coins because of the same Receiver - aChainUSDC.Address(): new(big.Int).Add(tinyOneCoin, tinyOneCoin), + {aChainUSDC.Address().Bytes(), new(big.Int).Add(tinyOneCoin, tinyOneCoin)}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, { Name: "USDC token together with another token transferred to EOA", - Receiver: utils.RandomAddress(), + Receiver: utils.RandomAddress().Bytes(), SourceChain: chainA, DestChain: chainC, Tokens: []router.ClientEVMTokenAmount{ @@ -151,15 +150,15 @@ func TestUSDCTokenTransfer(t *testing.T) { Amount: new(big.Int).Mul(tinyOneCoin, big.NewInt(10)), }, }, - ExpectedTokenBalances: map[common.Address]*big.Int{ - cChainUSDC.Address(): tinyOneCoin, - cChainToken.Address(): new(big.Int).Mul(tinyOneCoin, big.NewInt(10)), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {cChainUSDC.Address().Bytes(), tinyOneCoin}, + {cChainToken.Address().Bytes(), new(big.Int).Mul(tinyOneCoin, big.NewInt(10))}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, { Name: "USDC programmable token transfer to valid contract receiver", - Receiver: state.Chains[chainC].Receiver.Address(), + Receiver: state.Chains[chainC].Receiver.Address().Bytes(), SourceChain: chainA, DestChain: chainC, Tokens: []router.ClientEVMTokenAmount{ @@ -169,14 +168,14 @@ func TestUSDCTokenTransfer(t *testing.T) { }, }, Data: []byte("hello world"), - ExpectedTokenBalances: map[common.Address]*big.Int{ - cChainUSDC.Address(): tinyOneCoin, + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {cChainUSDC.Address().Bytes(), tinyOneCoin}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, { Name: "USDC programmable token transfer with too little gas", - Receiver: state.Chains[chainB].Receiver.Address(), + Receiver: state.Chains[chainB].Receiver.Address().Bytes(), SourceChain: chainC, DestChain: chainB, Tokens: []router.ClientEVMTokenAmount{ @@ -186,15 +185,15 @@ func TestUSDCTokenTransfer(t *testing.T) { }, }, Data: []byte("gimme more gas to execute that!"), - ExpectedTokenBalances: map[common.Address]*big.Int{ - bChainUSDC.Address(): new(big.Int).SetUint64(0), + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {bChainUSDC.Address().Bytes(), new(big.Int).SetUint64(0)}, }, ExtraArgs: testhelpers.MakeEVMExtraArgsV2(1, false), ExpectedStatus: testhelpers.EXECUTION_STATE_FAILURE, }, { Name: "USDC token transfer from a different source chain", - Receiver: utils.RandomAddress(), + Receiver: utils.RandomAddress().Bytes(), SourceChain: chainB, DestChain: chainC, Tokens: []router.ClientEVMTokenAmount{ @@ -204,8 +203,8 @@ func TestUSDCTokenTransfer(t *testing.T) { }, }, Data: nil, - ExpectedTokenBalances: map[common.Address]*big.Int{ - cChainUSDC.Address(): tinyOneCoin, + ExpectedTokenBalances: []testhelpers.ExpectedBalance{ + {cChainUSDC.Address().Bytes(), tinyOneCoin}, }, ExpectedStatus: testhelpers.EXECUTION_STATE_SUCCESS, }, @@ -216,8 +215,8 @@ func TestUSDCTokenTransfer(t *testing.T) { err = testhelpers.ConfirmMultipleCommits( t, - e.Chains, - state.Chains, + e, + state, startBlocks, false, expectedSeqNums, @@ -233,7 +232,7 @@ func TestUSDCTokenTransfer(t *testing.T) { ) require.Equal(t, expectedExecutionStates, execStates) - testhelpers.WaitForTokenBalances(ctx, t, e.Chains, expectedTokenBalances) + testhelpers.WaitForTokenBalances(ctx, t, e, expectedTokenBalances) } func updateFeeQuoters(