diff --git a/.changeset/fine-chefs-grin.md b/.changeset/fine-chefs-grin.md new file mode 100644 index 00000000000..904d08b21c8 --- /dev/null +++ b/.changeset/fine-chefs-grin.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal Add consensus negative system tests diff --git a/go.md b/go.md index 002d71e7b46..9e079fe5a1c 100644 --- a/go.md +++ b/go.md @@ -379,11 +379,14 @@ flowchart LR chainlink/system-tests/tests --> chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based chainlink/system-tests/tests --> chainlink/core/scripts/cre/environment/examples/workflows/v2/cron chainlink/system-tests/tests --> chainlink/system-tests/lib + chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/consensus chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/evmread-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/evmread click chainlink/system-tests/tests href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/regression/cre/consensus --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/regression/cre/consensus href "https://github.com/smartcontractkit/chainlink" chainlink/system-tests/tests/regression/cre/evm/evmread-negative --> chainlink-evm/gethwrappers chainlink/system-tests/tests/regression/cre/evm/evmread-negative --> cre-sdk-go/capabilities/blockchain/evm chainlink/system-tests/tests/regression/cre/evm/evmread-negative --> cre-sdk-go/capabilities/scheduler/cron @@ -458,6 +461,7 @@ flowchart LR chainlink/load-tests chainlink/system-tests/lib chainlink/system-tests/tests + chainlink/system-tests/tests/regression/cre/consensus chainlink/system-tests/tests/regression/cre/evm/evmread-negative chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative chainlink/system-tests/tests/regression/cre/http diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index cebc4412c55..ccc397266fa 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -21,6 +21,8 @@ replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examp replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/evmread => ./smoke/cre/evm/evmread +replace github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus => ./regression/cre/consensus + replace github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmread-negative => ./regression/cre/evm/evmread-negative replace github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative => ./regression/cre/evm/evmwrite-negative @@ -58,12 +60,12 @@ require ( github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v2/cron v0.0.0-20250923184312-03c1c70ed66b github.com/smartcontractkit/chainlink/deployment v0.0.0-20250926230623-96c13ca2551d github.com/smartcontractkit/chainlink/system-tests/lib v0.0.0-20250826151008-ae5ec0ee6f2c + github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus v0.0.0-20251003131804-c1403fe95660 github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmread-negative v0.0.0-20250923184312-03c1c70ed66b github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative v0.0.0-20251002142509-751c3caeb073 github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/http v0.0.0-20250929083906-1fde9e41af0e github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/evmread v0.0.0-20250923184312-03c1c70ed66b github.com/smartcontractkit/libocr v0.0.0-20250905115425-2785a5cee79d - github.com/smartcontractkit/quarantine v0.0.0-20250909213106-ece491bef618 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.16.0 diff --git a/system-tests/tests/regression/cre/consensus/config/config.go b/system-tests/tests/regression/cre/consensus/config/config.go new file mode 100644 index 00000000000..5fc56c98b31 --- /dev/null +++ b/system-tests/tests/regression/cre/consensus/config/config.go @@ -0,0 +1,6 @@ +package config + +type Config struct { + FeedID string + CaseToTrigger string +} diff --git a/system-tests/tests/regression/cre/consensus/go.mod b/system-tests/tests/regression/cre/consensus/go.mod new file mode 100644 index 00000000000..0c6c6e214a6 --- /dev/null +++ b/system-tests/tests/regression/cre/consensus/go.mod @@ -0,0 +1,26 @@ +module github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus + +go 1.24.5 + +require ( + github.com/ethereum/go-ethereum v1.16.2 + github.com/smartcontractkit/cre-sdk-go v0.8.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sys v0.34.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/system-tests/tests/regression/cre/consensus/go.sum b/system-tests/tests/regression/cre/consensus/go.sum new file mode 100644 index 00000000000..cd5c3d9a5d4 --- /dev/null +++ b/system-tests/tests/regression/cre/consensus/go.sum @@ -0,0 +1,48 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/ethereum/go-ethereum v1.16.2 h1:VDHqj86DaQiMpnMgc7l0rwZTg0FRmlz74yupSG5SnzI= +github.com/ethereum/go-ethereum v1.16.2/go.mod h1:X5CIOyo8SuK1Q5GnaEizQVLHT/DfsiGWuNeVdQcEMNA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 h1:1/KdO5AbUr3CmpLjMPuJXPo2wHMbfB8mldKLsg7D4M8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/cre-sdk-go v0.8.0 h1:QHYnz6MgBGFRaTOrP9Nx4HSHUpxYWgRzXGdsAucKAiI= +github.com/smartcontractkit/cre-sdk-go v0.8.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0 h1:aO++xdGcQ8TpxAfXrm7EHeIVLDitB8xg7J8/zSxbdBY= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/regression/cre/consensus/main.go b/system-tests/tests/regression/cre/consensus/main.go new file mode 100644 index 00000000000..631f1baaee0 --- /dev/null +++ b/system-tests/tests/regression/cre/consensus/main.go @@ -0,0 +1,302 @@ +//go:build wasip1 + +package main + +import ( + "encoding/hex" + "fmt" + "log/slog" + "math/big" + "math/rand" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + "gopkg.in/yaml.v3" + + "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus/config" +) + +const ( + defaultPrice = int64(100) + defaultTimestamp = uint32(1759491269) + maxRandomPrice = int64(1000000) + randomTimeOffset = int64(7200) // ±1 hour in seconds +) + +type priceOutput struct { + FeedID [32]byte + Timestamp uint32 + Price *big.Int +} + +func main() { + wasm.NewRunner(parseConfig).Run(RunConsensusNegativeWorkflow) +} + +// parseConfig unmarshals the YAML configuration +func parseConfig(b []byte) (config.Config, error) { + var wfCfg config.Config + if err := yaml.Unmarshal(b, &wfCfg); err != nil { + return config.Config{}, fmt.Errorf("error unmarshalling config: %w", err) + } + return wfCfg, nil +} + +func RunConsensusNegativeWorkflow(wfCfg config.Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[config.Config], error) { + return cre.Workflow[config.Config]{ + cre.Handler( + cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), + onConsensusNegativeTrigger, + ), + }, nil +} + +func onConsensusNegativeTrigger(wfCfg config.Config, runtime cre.Runtime, payload *cron.Payload) (_ any, _ error) { + runtime.Logger().Info("onConsensusNegativeTrigger called", "payload", payload) + + switch wfCfg.CaseToTrigger { + case "Consensus - random timestamps": + return runConsensusGenerateReportWithRandomTimestamps(runtime, wfCfg) + case "Consensus - inconsistent feedIDs": + return runConsensusGenerateReportWithInconsistentFeedIDs(runtime, wfCfg) + case "Consensus - inconsistent prices": + return runConsensusGenerateReportWithInconsistentPrices(runtime, wfCfg) + default: + runtime.Logger().Warn("The provided name for function to test in regression Consensus Workflow did not match any known functions", "functionToTest", wfCfg.CaseToTrigger) + return nil, fmt.Errorf("the provided name for function to test in regression Consensus Workflow did not match any known functions: %s", wfCfg.CaseToTrigger) + } +} + +// runConsensusGenerateReportWithRandomTimestamps writes a report with different timestamps +// different timestamps should cause transaction to fail +func runConsensusGenerateReportWithRandomTimestamps(runtime cre.Runtime, wfCfg config.Config) (*cre.Report, error) { + runtime.Logger().Info("Attempting to write report with different timestamps") + + priceOutputWithRandomTimestamp, err := createPriceOutputWithRandomTimestamp(runtime, wfCfg) + if err != nil { + runtime.Logger().Error("failed to create price output with random timestamps", "error", err) + return nil, fmt.Errorf("failed to create price output with random timestamps: %w", err) + } + + reportWithDifferentTimestamps, err := generateReports(runtime, priceOutputWithRandomTimestamp) + if err != nil { + runtime.Logger().Error("got expected error for WriteReport with random timestamps", "error", err) + return nil, fmt.Errorf("expected error for WriteReport with random timestamps: %w", err) + } + + runtime.Logger().Info("this is not expected: WriteReport with different timestamps should return an error", "generated_report", reportWithDifferentTimestamps) + return reportWithDifferentTimestamps, nil +} + +// runConsensusGenerateReportWithInconsistentFeedIDs writes a report with inconsistent feedIDs +// inconsistent feedIDs should cause consensus to fail +func runConsensusGenerateReportWithInconsistentFeedIDs(runtime cre.Runtime, wfCfg config.Config) (*cre.Report, error) { + runtime.Logger().Info("Attempting to generate report with inconsistent feedIDs") + + priceOutputWithRandomFeedID, err := createPriceOutputWithRandomFeedID(runtime, wfCfg) + if err != nil { + runtime.Logger().Error("failed to create price output with random feedID", "error", err) + return nil, fmt.Errorf("failed to create price output with random feedID: %w", err) + } + + reportWithInconsistentFeedIDs, err := generateReports(runtime, priceOutputWithRandomFeedID) + if err != nil { + runtime.Logger().Error("got expected error for GenerateReport with inconsistent feedIDs", "error", err) + return nil, fmt.Errorf("expected error for GenerateReport with inconsistent feedIDs: %w", err) + } + + runtime.Logger().Info("this is not expected: GenerateReport with inconsistent feedIDs should return an error", "generated_report", reportWithInconsistentFeedIDs) + return reportWithInconsistentFeedIDs, nil +} + +// runConsensusGenerateReportWithInconsistentPrices writes a report with inconsistent prices +// inconsistent prices should cause consensus to fail +func runConsensusGenerateReportWithInconsistentPrices(runtime cre.Runtime, wfCfg config.Config) (*cre.Report, error) { + runtime.Logger().Info("Attempting to generate report with inconsistent prices") + + priceOutputWithRandomPrice, err := createPriceOutputWithRandomPrice(runtime, wfCfg) + if err != nil { + runtime.Logger().Error("failed to create price output with random price", "error", err) + return nil, fmt.Errorf("failed to create price output with random price: %w", err) + } + + reportWithInconsistentPrices, err := generateReports(runtime, priceOutputWithRandomPrice) + if err != nil { + runtime.Logger().Error("got expected error for GenerateReport with inconsistent prices", "error", err) + return nil, fmt.Errorf("expected error for GenerateReport with inconsistent prices: %w", err) + } + + runtime.Logger().Info("this is not expected: GenerateReport with inconsistent prices should return an error", "generated_report", reportWithInconsistentPrices) + return reportWithInconsistentPrices, nil +} + +// createPriceOutputWithRandomTimestamp creates multiple price outputs with different random timestamps +func createPriceOutputWithRandomTimestamp(runtime cre.Runtime, wfCfg config.Config) (priceOutput, error) { + runtime.Logger().Info("creating price outputs with different timestamps") + + defaultFeedID, err := convertFeedIDtoBytes(wfCfg.FeedID) + if err != nil { + runtime.Logger().Error("failed to decode feed ID", "error", err) + return priceOutput{}, fmt.Errorf("failed to decode feed ID: %w", err) + } + + // Generate random timestamp variations (±1 hour from base time) + randomTimestamp := newRandomTimestamp(runtime) + runtime.Logger().Info("creating priceOutput") + outputWithRandomTimestamp := priceOutput{ + FeedID: defaultFeedID, + Timestamp: randomTimestamp, + Price: big.NewInt(defaultPrice), + } + runtime.Logger().Info("priceOutput with random timestamp created") + + return outputWithRandomTimestamp, nil +} + +// createPriceOutputWithRandomFeedID creates price output with random feedID +func createPriceOutputWithRandomFeedID(runtime cre.Runtime, wfCfg config.Config) (priceOutput, error) { + runtime.Logger().Info("creating price output with random feedID") + + randomFeedID := newRandomFeedID(runtime) + runtime.Logger().Info("creating priceOutput with random feedID") + outputWithRandomFeedID := priceOutput{ + FeedID: randomFeedID, + Timestamp: defaultTimestamp, + Price: big.NewInt(defaultPrice), + } + runtime.Logger().Info("priceOutput with random feedID created") + + return outputWithRandomFeedID, nil +} + +// createPriceOutputWithRandomPrice creates price output with random price +func createPriceOutputWithRandomPrice(runtime cre.Runtime, wfCfg config.Config) (priceOutput, error) { + runtime.Logger().Info("creating price output with random price") + + feedID, err := convertFeedIDtoBytes(wfCfg.FeedID) + if err != nil { + runtime.Logger().Error("failed to decode feed ID", "error", err) + return priceOutput{}, fmt.Errorf("failed to decode feed ID: %w", err) + } + + randomPrice := newRandomPrice(runtime) + runtime.Logger().Info("creating priceOutput with random price") + outputWithRandomPrice := priceOutput{ + FeedID: feedID, + Timestamp: defaultTimestamp, + Price: randomPrice, + } + runtime.Logger().Info("priceOutput with random price created") + + return outputWithRandomPrice, nil +} + +func newRandomTimestamp(runtime cre.Runtime) uint32 { + baseTime := time.Now().Unix() + randomOffset := rand.Int63n(randomTimeOffset) - (randomTimeOffset / 2) // ±1 hour in seconds + randomTimestamp := uint32(baseTime + randomOffset) + runtime.Logger().Info("new random timestamp created", "random_timestamp", randomTimestamp) + return randomTimestamp +} + +// newRandomFeedID generates a random 32-byte feedID +func newRandomFeedID(runtime cre.Runtime) [32]byte { + var randomFeedID [32]byte + for i := range randomFeedID { + randomFeedID[i] = byte(rand.Intn(256)) + } + runtime.Logger().Info("new random feedID created", "random_feedID", hex.EncodeToString(randomFeedID[:])) + return randomFeedID +} + +// newRandomPrice generates a random price between 1 and maxRandomPrice +func newRandomPrice(runtime cre.Runtime) *big.Int { + randomPrice := big.NewInt(rand.Int63n(maxRandomPrice) + 1) // Random price between 1 and maxRandomPrice + runtime.Logger().Info("new random price created", "random_price", randomPrice.String()) + return randomPrice +} + +// generateReports encodes price outputs and generates a report using consensus +func generateReports(runtime cre.Runtime, output priceOutput) (*cre.Report, error) { + outputs := make([]priceOutput, 1) + outputs[0] = output + runtime.Logger().Info("Encoding priceOutput...") + encodedPrice, err := encodeReports(outputs) + if err != nil { + runtime.Logger().Error("failed to pack price report", "error", err) + return nil, fmt.Errorf("failed to pack price report: %w", err) + } + runtime.Logger().Info("priceOutput encoded") + + runtime.Logger().Info("Generating report") + report, err := runtime.GenerateReport(&cre.ReportRequest{ + EncodedPayload: encodedPrice, + EncoderName: "evm", + SigningAlgo: "ecdsa", + HashingAlgo: "keccak256", + }).Await() + if err != nil { + runtime.Logger().Error("failed to generate report", "error", err) + return nil, fmt.Errorf("failed to generate report: %w", err) + } + runtime.Logger().Info("Report generated successfully") + + return report, nil +} + +func encodeReports(reports []priceOutput) ([]byte, error) { + typ, err := abi.NewType("tuple[]", "", + []abi.ArgumentMarshaling{ + {Name: "FeedID", Type: "bytes32"}, + {Name: "Timestamp", Type: "uint32"}, + {Name: "Price", Type: "uint224"}, + }) + if err != nil { + return nil, fmt.Errorf("failed to create ABI type: %w", err) + } + + args := abi.Arguments{ + { + Name: "Reports", + Type: typ, + }, + } + return args.Pack(reports) +} + +// convertFeedIDtoBytes converts a hex string feed ID to a 32-byte array +func convertFeedIDtoBytes(feedID string) ([32]byte, error) { + if feedID == "" { + return [32]byte{}, fmt.Errorf("feedID string is empty") + } + + // Remove hex prefix if present + hexStr := feedID + hexPrefix := "0x" + if len(feedID) >= 2 && feedID[:2] == hexPrefix { + hexStr = feedID[2:] + } + + if len(hexStr) == 0 { + return [32]byte{}, fmt.Errorf("feedID string contains no hex data: %q", feedID) + } + + b, err := hex.DecodeString(hexStr) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to decode hex string %q: %w", feedID, err) + } + + var result [32]byte + if len(b) > 32 { + // Truncate if too long + copy(result[:], b[:32]) + } else { + // Pad with zeros if too short + copy(result[:], b) + } + + return result, nil +} diff --git a/system-tests/tests/regression/cre/cre_regression_suite_test.go b/system-tests/tests/regression/cre/cre_regression_suite_test.go index b1d0ee80bb9..1ff2b169775 100644 --- a/system-tests/tests/regression/cre/cre_regression_suite_test.go +++ b/system-tests/tests/regression/cre/cre_regression_suite_test.go @@ -5,8 +5,6 @@ import ( "strings" "testing" - "github.com/smartcontractkit/quarantine" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre" t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" @@ -25,11 +23,22 @@ Inside `core/scripts/cre/environment` directory 3. Stop and clear any existing environment: `go run . env stop -a` 4. Run: `CTF_CONFIGS= go run . env start && ./bin/ctf obs up` to start env + observability 5. Optionally run blockscout `./bin/ctf bs up` - 6. Execute the tests in `system-tests/tests/smoke/cre` with CTF_CONFIG set to the corresponding topology file: - `export CTF_CONFIGS=../../../../core/scripts/cre/environment/configs/.toml; go test -timeout 15m -run ^Test_CRE_Suite$`. + 6. Execute the tests in `system-tests/tests/regression/cre`: `go test -timeout 15m -run "^Test_CRE_V2"`. */ +func Test_CRE_V2_Consensus_Regression(t *testing.T) { + // a template for Consensus negative tests names to avoid duplication + const consensusTestNameTemplate = "[v2] Consensus.%s fails with %s" // e.g. "[v2] Consensus. fails with " + + for _, tCase := range consensusNegativeTestsGenerateReport { + testName := fmt.Sprintf(consensusTestNameTemplate, tCase.caseToTrigger, tCase.name) + t.Run(testName, func(t *testing.T) { + testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t), v2RegistriesFlags...) + ConsensusFailsTest(t, testEnv, tCase) + }) + } +} + func Test_CRE_V2_Cron_Regression(t *testing.T) { - quarantine.Flaky(t, "DX-1929") for _, tCase := range cronInvalidSchedulesTests { testName := "[v2] Cron (Beholder) fails when schedule is " + tCase.name t.Run(testName, func(t *testing.T) { @@ -41,8 +50,7 @@ func Test_CRE_V2_Cron_Regression(t *testing.T) { } func Test_CRE_V2_HTTP_Regression(t *testing.T) { - flags := []string{"--with-contracts-version", "v2"} - testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t), flags...) + testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t), v2RegistriesFlags...) for _, tCase := range httpNegativeTests { testName := "[v2] HTTP Trigger fails with " + tCase.name @@ -77,7 +85,6 @@ func runEVMNegativeTestSuite(t *testing.T, testCases []evmNegativeTest) { } func Test_CRE_V2_EVM_BalanceAt_Invalid_Address_Regression(t *testing.T) { - quarantine.Flaky(t, "DX-1938") runEVMNegativeTestSuite(t, evmNegativeTestsBalanceAtInvalidAddress) } @@ -86,12 +93,10 @@ func Test_CRE_V2_EVM_CallContract_Invalid_Addr_To_Read_Regression(t *testing.T) } func Test_CRE_V2_EVM_CallContract_Invalid_Balance_Reader_Contract_Regression(t *testing.T) { - quarantine.Flaky(t, "DX-1926") runEVMNegativeTestSuite(t, evmNegativeTestsCallContractInvalidBalanceReaderContract) } func Test_CRE_V2_EVM_EstimateGas_Invalid_To_Address_Regression(t *testing.T) { - quarantine.Flaky(t, "DX-1927") runEVMNegativeTestSuite(t, evmNegativeTestsEstimateGasInvalidToAddress) } @@ -100,12 +105,10 @@ func Test_CRE_V2_EVM_FilterLogs_Invalid_Addresses_Regression(t *testing.T) { } func Test_CRE_V2_EVM_FilterLogs_Invalid_FromBlock_Regression(t *testing.T) { - quarantine.Flaky(t, "DX-1928") runEVMNegativeTestSuite(t, evmNegativeTestsFilterLogsWithInvalidFromBlock) } func Test_CRE_V2_EVM_FilterLogs_Invalid_ToBlock_Regression(t *testing.T) { - quarantine.Flaky(t, "DX-1921") runEVMNegativeTestSuite(t, evmNegativeTestsFilterLogsWithInvalidToBlock) } diff --git a/system-tests/tests/regression/cre/v2_consensus_regression_test.go b/system-tests/tests/regression/cre/v2_consensus_regression_test.go new file mode 100644 index 00000000000..9aecb61a150 --- /dev/null +++ b/system-tests/tests/regression/cre/v2_consensus_regression_test.go @@ -0,0 +1,60 @@ +package cre + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + consensus_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus/config" + t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" + ttypes "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" +) + +// regression +type consensusNegativeTest struct { + name string + caseToTrigger string + expectedError string +} + +const ( + expectedConsensusError = "could not process consensus request before expiry" +) + +var consensusNegativeTestsGenerateReport = []consensusNegativeTest{ + // Consensus - generate report with random timestamps + {"random timestamps", "Consensus - random timestamps", expectedConsensusError}, + {"inconsistent feedIDs", "Consensus - inconsistent feedIDs", expectedConsensusError}, + {"inconsistent prices", "Consensus - inconsistent prices", expectedConsensusError}, +} + +func ConsensusFailsTest(t *testing.T, testEnv *ttypes.TestEnvironment, consensusNegativeTest consensusNegativeTest) { + testLogger := framework.L + const workflowFileLocation = "./consensus/main.go" + + for _, bcOutput := range testEnv.WrappedBlockchainOutputs { + chainID := bcOutput.BlockchainOutput.ChainID + + listenerCtx, messageChan, kafkaErrChan := t_helpers.StartBeholder(t, testLogger, testEnv) + + testLogger.Info().Msg("Creating Consensus Fail workflow configuration...") + workflowName := fmt.Sprintf("consensus-fail-workflow-%s-%04d", chainID, rand.Intn(10000)) + feedID := "018e16c38e000320000000000000000000000000000000000000000000000000" // 32 hex characters (16 bytes) + workflowConfig := consensus_negative_config.Config{ + CaseToTrigger: consensusNegativeTest.caseToTrigger, + FeedID: feedID, + } + t_helpers.CompileAndDeployWorkflow(t, testEnv, testLogger, workflowName, &workflowConfig, workflowFileLocation) + + timeout := 90 * time.Second + expectedError := consensusNegativeTest.expectedError + err := t_helpers.AssertBeholderMessage(listenerCtx, t, expectedError, testLogger, messageChan, kafkaErrChan, timeout) + require.NoError(t, err, "Consensus Fail test failed") + testLogger.Info().Msg("Consensus Fail test successfully completed") + } +} diff --git a/system-tests/tests/regression/cre/v2_evm_regression_test.go b/system-tests/tests/regression/cre/v2_evm_regression_test.go index 51801ff5ca1..75320f6e35e 100644 --- a/system-tests/tests/regression/cre/v2_evm_regression_test.go +++ b/system-tests/tests/regression/cre/v2_evm_regression_test.go @@ -50,7 +50,7 @@ const ( writeReportCorruptReceiverAddress = "WriteReport - corrupt receiver address" expectedWriteReportCorruptReceiverAddress = "failed to execute capability: received address is not 20 bytes long" writeReportInvalidGas = "WriteReport - invalid gas" - expectedWriteReportInvalidGas = "failed to submit transaction" + expectedWriteReportInvalidGas = "failed to execute capability" writeReportRandomTimestamps = "WriteReport - random timestamps" ) diff --git a/system-tests/tests/smoke/cre/cre_suite_test.go b/system-tests/tests/smoke/cre/cre_suite_test.go index 8bb0d5ebbe3..f2e3ec407c6 100644 --- a/system-tests/tests/smoke/cre/cre_suite_test.go +++ b/system-tests/tests/smoke/cre/cre_suite_test.go @@ -26,8 +26,7 @@ Inside `core/scripts/cre/environment` directory 3. Stop and clear any existing environment: `go run . env stop -a` 4. Run: `CTF_CONFIGS= go run . env start && ./bin/ctf obs up` to start env + observability 5. Optionally run blockscout `./bin/ctf bs up` - 6. Execute the tests in `system-tests/tests/smoke/cre` with CTF_CONFIG set to the corresponding topology file: - `export CTF_CONFIGS=../../../../core/scripts/cre/environment/configs/.toml; go test -timeout 15m -run ^Test_CRE_Suite$`. + 6. Execute the tests in `system-tests/tests/smoke/cre`: `go test -timeout 15m -run "^Test_CRE_V2"`. */ func Test_CRE_V1_Proof_Of_Reserve(t *testing.T) { testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t)) @@ -89,8 +88,7 @@ func Test_CRE_V1_Billing_Cron_Beholder(t *testing.T) { /* To execute tests with v2 contracts start the local CRE first: 1. Inside `core/scripts/cre/environment` directory: `go run . env restart --with-beholder --with-contracts-version v2` - 2. Execute the tests in `system-tests/tests/smoke/cre` with CTF_CONFIG set to the corresponding topology file: - `export CTF_CONFIGS=../../../../core/scripts/cre/environment/configs/.toml; go test -timeout 15m -run ^Test_CRE_Suite$`. + 2. Execute the tests in `system-tests/tests/smoke/cre`: `go test -timeout 15m -run "^Test_CRE_V2"`. */ func Test_CRE_V2_Suite(t *testing.T) { topology := os.Getenv("TOPOLOGY_NAME") diff --git a/system-tests/tests/test-helpers/beholder_provider.go b/system-tests/tests/test-helpers/beholder_provider.go index 0f7633c1cdb..5bf6ba8c524 100644 --- a/system-tests/tests/test-helpers/beholder_provider.go +++ b/system-tests/tests/test-helpers/beholder_provider.go @@ -16,10 +16,9 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/config" ) -// Constants for configuration const ( // Channel buffer sizes - messageChannelBufferSize = 20 + messageChannelBufferSize = 40 errorChannelBufferSize = 1 channelFullRetryTimeout = 100 * time.Millisecond diff --git a/system-tests/tests/test-helpers/t_helpers.go b/system-tests/tests/test-helpers/t_helpers.go index 50b1cb532b6..88e13b263f0 100644 --- a/system-tests/tests/test-helpers/t_helpers.go +++ b/system-tests/tests/test-helpers/t_helpers.go @@ -40,6 +40,7 @@ import ( commonevents "github.com/smartcontractkit/chainlink-protos/workflows/go/common" workflowevents "github.com/smartcontractkit/chainlink-protos/workflows/go/events" + consensus_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus/config" evmread_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmread-negative/config" evmwrite_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative/config" http_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/http/config" @@ -288,6 +289,7 @@ type WorkflowConfig interface { portypes.WorkflowConfig | crontypes.WorkflowConfig | HTTPWorkflowConfig | + consensus_negative_config.Config | evmread_config.Config | evmread_negative_config.Config | evmwrite_negative_config.Config | @@ -381,6 +383,12 @@ func workflowConfigFactory[T WorkflowConfig](t *testing.T, testLogger zerolog.Lo require.NoError(t, configErr, "failed to create Cron workflow config file") testLogger.Info().Msg("Cron workflow config file created.") + case *consensus_negative_config.Config: + workflowCfgFilePath, configErr := CreateWorkflowYamlConfigFile(workflowName, cfg) + workflowConfigFilePath = workflowCfgFilePath + require.NoError(t, configErr, "failed to create consensus workflow config file") + testLogger.Info().Msg("Consensus workflow config file created.") + case *evmread_config.Config: workflowCfgFilePath, configErr := CreateWorkflowYamlConfigFile(workflowName, cfg) workflowConfigFilePath = workflowCfgFilePath