From 98b90543972d37e4c00196f3f00bcf5f380ea04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deividas=20Kar=C5=BEinauskas?= Date: Fri, 9 Aug 2024 12:17:12 +0300 Subject: [PATCH 01/16] Write Chain Target: Validate that signed report metadata matches request metadata (#14066) * Hardcoded value for call with exact gas * Record gasProvided in route function * Add a getter for transmission gas limit * Update snapshot * Changeset * Remove unused import * Rename to gas limit * Update gethwrappers * Uncomment test code * Remove copy/pasta comment * Slight rename * Allow retrying transmissions with more gas * Only allow retrying failed transmissions * Update snapshot * Fix state for InvalidReceiver check * Check for initial state * Actually store gas limit provided to receiver * Update gethwrappers * Remove unused struct * Correctly mark invalid receiver when receiver interface unsupported * Create TransmissionInfo struct * Update gethwrappers * Bump gas limit * Bump gas even more * Update KeystoneFeedsConsumer.sol to implement IERC165 * Use getTransmissionInfo * Use TransmissionState to determine if transmission should be created * Fix test * Fix trailing line * Update a mock to the GetLatestValue("getTransmissionInfo") call in a test * Remove TODO + replace panic with err * Remove redundant empty lines * Typo * Fix nil pointer dereference in write target implementation * Remove unused constant * Name mapping values * Add changeset * Validate that report metadata matches request metadata * Derive report metadata length from the struct * Bytes() => Encode() * Simplify decoding * Undo redundant change * More simplifications * More cleanup * Remove redundant comment * Changeset * Update tests --------- Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com> --- .changeset/tall-poems-swim.md | 5 + core/capabilities/targets/write_target.go | 145 +++++++++++++----- .../capabilities/targets/write_target_test.go | 88 +++++------ core/services/relay/evm/write_target_test.go | 137 ++++++----------- 4 files changed, 198 insertions(+), 177 deletions(-) create mode 100644 .changeset/tall-poems-swim.md diff --git a/.changeset/tall-poems-swim.md b/.changeset/tall-poems-swim.md new file mode 100644 index 00000000000..0408383bd03 --- /dev/null +++ b/.changeset/tall-poems-swim.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal diff --git a/core/capabilities/targets/write_target.go b/core/capabilities/targets/write_target.go index 4524c4fd449..282a4741a6a 100644 --- a/core/capabilities/targets/write_target.go +++ b/core/capabilities/targets/write_target.go @@ -1,7 +1,9 @@ package targets import ( + "bytes" "context" + "encoding/binary" "encoding/hex" "fmt" "math/big" @@ -13,7 +15,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -21,9 +22,6 @@ var ( _ capabilities.ActionCapability = &WriteTarget{} ) -// required field of target's config in the workflow spec -const signedReportField = "signed_report" - type WriteTarget struct { cr commontypes.ContractReader cw commontypes.ChainWriter @@ -71,22 +69,105 @@ func NewWriteTarget(lggr logger.Logger, id string, cr commontypes.ContractReader } } -type EvmConfig struct { +// Note: This should be a shared type that the OCR3 package validates as well +type ReportV1Metadata struct { + Version uint8 + WorkflowExecutionID [32]byte + Timestamp uint32 + DonID uint32 + DonConfigVersion uint32 + WorkflowCID [32]byte + WorkflowName [10]byte + WorkflowOwner [20]byte + ReportID [2]byte +} + +func (rm ReportV1Metadata) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + err := binary.Write(buf, binary.BigEndian, rm) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (rm ReportV1Metadata) Length() int { + bytes, err := rm.Encode() + if err != nil { + return 0 + } + return len(bytes) +} + +func decodeReportMetadata(data []byte) (metadata ReportV1Metadata, err error) { + if len(data) < metadata.Length() { + return metadata, fmt.Errorf("data too short: %d bytes", len(data)) + } + return metadata, binary.Read(bytes.NewReader(data[:metadata.Length()]), binary.BigEndian, &metadata) +} + +type Config struct { + // Address of the contract that will get the forwarded report Address string } -func parseConfig(rawConfig *values.Map) (config EvmConfig, err error) { - if rawConfig == nil { - return config, fmt.Errorf("missing config field") +type Inputs struct { + SignedReport types.SignedReport +} + +type Request struct { + Metadata capabilities.RequestMetadata + Config Config + Inputs Inputs +} + +func evaluate(rawRequest capabilities.CapabilityRequest) (r Request, err error) { + r.Metadata = rawRequest.Metadata + + if rawRequest.Config == nil { + return r, fmt.Errorf("missing config field") + } + + if err = rawRequest.Config.UnwrapTo(&r.Config); err != nil { + return r, err + } + + if !common.IsHexAddress(r.Config.Address) { + return r, fmt.Errorf("'%v' is not a valid address", r.Config.Address) } - if err := rawConfig.UnwrapTo(&config); err != nil { - return config, err + if rawRequest.Inputs == nil { + return r, fmt.Errorf("missing inputs field") } - if !common.IsHexAddress(config.Address) { - return config, fmt.Errorf("'%v' is not a valid address", config.Address) + + // required field of target's config in the workflow spec + const signedReportField = "signed_report" + signedReport, ok := rawRequest.Inputs.Underlying[signedReportField] + if !ok { + return r, fmt.Errorf("missing required field %s", signedReportField) + } + + if err = signedReport.UnwrapTo(&r.Inputs.SignedReport); err != nil { + return r, err + } + + reportMetadata, err := decodeReportMetadata(r.Inputs.SignedReport.Report) + if err != nil { + return r, err + } + + if reportMetadata.Version != 1 { + return r, fmt.Errorf("unsupported report version: %d", reportMetadata.Version) } - return config, nil + + if hex.EncodeToString(reportMetadata.WorkflowExecutionID[:]) != rawRequest.Metadata.WorkflowExecutionID || + hex.EncodeToString(reportMetadata.WorkflowOwner[:]) != rawRequest.Metadata.WorkflowOwner || + hex.EncodeToString(reportMetadata.WorkflowName[:]) != rawRequest.Metadata.WorkflowName || + hex.EncodeToString(reportMetadata.WorkflowCID[:]) != rawRequest.Metadata.WorkflowID { + return r, fmt.Errorf("report metadata does not match request metadata. reportMetadata: %+v, requestMetadata: %+v", reportMetadata, rawRequest.Metadata) + } + + return r, nil } func success() <-chan capabilities.CapabilityResponse { @@ -98,7 +179,7 @@ func success() <-chan capabilities.CapabilityResponse { return callback } -func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { +func (cap *WriteTarget) Execute(ctx context.Context, rawRequest capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { // Bind to the contract address on the write path. // Bind() requires a connection to the node's RPCs and // cannot be run during initialization. @@ -114,47 +195,27 @@ func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.Capabi cap.bound = true } - cap.lggr.Debugw("Execute", "request", request) + cap.lggr.Debugw("Execute", "rawRequest", rawRequest) - reqConfig, err := parseConfig(request.Config) + request, err := evaluate(rawRequest) if err != nil { return nil, err } - if request.Inputs == nil { - return nil, fmt.Errorf("missing inputs field") - } - - signedReport, ok := request.Inputs.Underlying[signedReportField] - if !ok { - return nil, fmt.Errorf("missing required field %s", signedReportField) - } - - inputs := types.SignedReport{} - if err = signedReport.UnwrapTo(&inputs); err != nil { - return nil, err - } - - if len(inputs.Report) == 0 { - // We received any empty report -- this means we should skip transmission. - cap.lggr.Debugw("Skipping empty report", "request", request) - return success(), nil - } - // TODO: validate encoded report is prefixed with workflowID and executionID that match the request meta - rawExecutionID, err := hex.DecodeString(request.Metadata.WorkflowExecutionID) if err != nil { return nil, err } + // Check whether value was already transmitted on chain queryInputs := struct { Receiver string WorkflowExecutionID []byte ReportId []byte }{ - Receiver: reqConfig.Address, + Receiver: request.Config.Address, WorkflowExecutionID: rawExecutionID, - ReportId: inputs.ID, + ReportId: request.Inputs.SignedReport.ID, } var transmissionInfo TransmissionInfo if err = cap.cr.GetLatestValue(ctx, "forwarder", "getTransmissionInfo", primitives.Unconfirmed, queryInputs, &transmissionInfo); err != nil { @@ -163,7 +224,7 @@ func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.Capabi switch { case transmissionInfo.State == 0: // NOT_ATTEMPTED - cap.lggr.Infow("non-empty report - tranasmission not attempted - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures), "executionID", request.Metadata.WorkflowExecutionID) + cap.lggr.Infow("non-empty report - tranasmission not attempted - attempting to push to txmgr", "request", request, "reportLen", len(request.Inputs.SignedReport.Report), "reportContextLen", len(request.Inputs.SignedReport.Context), "nSignatures", len(request.Inputs.SignedReport.Signatures), "executionID", request.Metadata.WorkflowExecutionID) case transmissionInfo.State == 1: // SUCCEEDED cap.lggr.Infow("returning without a tranmission attempt - report already onchain ", "executionID", request.Metadata.WorkflowExecutionID) return success(), nil @@ -175,7 +236,7 @@ func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.Capabi cap.lggr.Infow("returning without a tranmission attempt - transmission already attempted and failed, sufficient gas was provided", "executionID", request.Metadata.WorkflowExecutionID, "receiverGasMinimum", cap.receiverGasMinimum, "transmissionGasLimit", transmissionInfo.GasLimit) return success(), nil } else { - cap.lggr.Infow("non-empty report - retrying a failed transmission - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures), "executionID", request.Metadata.WorkflowExecutionID, "receiverGasMinimum", cap.receiverGasMinimum, "transmissionGasLimit", transmissionInfo.GasLimit) + cap.lggr.Infow("non-empty report - retrying a failed transmission - attempting to push to txmgr", "request", request, "reportLen", len(request.Inputs.SignedReport.Report), "reportContextLen", len(request.Inputs.SignedReport.Context), "nSignatures", len(request.Inputs.SignedReport.Signatures), "executionID", request.Metadata.WorkflowExecutionID, "receiverGasMinimum", cap.receiverGasMinimum, "transmissionGasLimit", transmissionInfo.GasLimit) } default: return nil, fmt.Errorf("unexpected transmission state: %v", transmissionInfo.State) @@ -194,7 +255,7 @@ func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.Capabi RawReport []byte ReportContext []byte Signatures [][]byte - }{reqConfig.Address, inputs.Report, inputs.Context, inputs.Signatures} + }{request.Config.Address, request.Inputs.SignedReport.Report, request.Inputs.SignedReport.Context, request.Inputs.SignedReport.Signatures} if req.RawReport == nil { req.RawReport = make([]byte, 0) diff --git a/core/capabilities/targets/write_target_test.go b/core/capabilities/targets/write_target_test.go index 0fa750911dd..522fee32513 100644 --- a/core/capabilities/targets/write_target_test.go +++ b/core/capabilities/targets/write_target_test.go @@ -2,6 +2,7 @@ package targets_test import ( "context" + "encoding/hex" "errors" "math/big" "testing" @@ -38,14 +39,36 @@ func TestWriteTarget(t *testing.T) { }) require.NoError(t, err) + reportMetadata := targets.ReportV1Metadata{ + Version: 1, + WorkflowExecutionID: [32]byte{}, + Timestamp: 0, + DonID: 0, + DonConfigVersion: 0, + WorkflowCID: [32]byte{}, + WorkflowName: [10]byte{}, + WorkflowOwner: [20]byte{}, + ReportID: [2]byte{}, + } + + reportMetadataBytes, err := reportMetadata.Encode() + require.NoError(t, err) + validInputs, err := values.NewMap(map[string]any{ "signed_report": map[string]any{ - "report": []byte{1, 2, 3}, + "report": reportMetadataBytes, "signatures": [][]byte{}, }, }) require.NoError(t, err) + validMetadata := capabilities.RequestMetadata{ + WorkflowID: hex.EncodeToString(reportMetadata.WorkflowCID[:]), + WorkflowOwner: hex.EncodeToString(reportMetadata.WorkflowOwner[:]), + WorkflowName: hex.EncodeToString(reportMetadata.WorkflowName[:]), + WorkflowExecutionID: hex.EncodeToString(reportMetadata.WorkflowExecutionID[:]), + } + cr.On("Bind", mock.Anything, []types.BoundContract{ { Address: forwarderAddr, @@ -69,11 +92,9 @@ func TestWriteTarget(t *testing.T) { t.Run("succeeds with valid report", func(t *testing.T) { req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: validInputs, + Metadata: validMetadata, + Config: config, + Inputs: validInputs, } ch, err2 := writeTarget.Execute(ctx, req) @@ -82,36 +103,11 @@ func TestWriteTarget(t *testing.T) { require.NotNil(t, response) }) - t.Run("succeeds with empty report", func(t *testing.T) { - emptyInputs, err2 := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": []byte{}, - }, - "signatures": [][]byte{}, - }) - - require.NoError(t, err2) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowExecutionID: "test-id", - }, - Config: config, - Inputs: emptyInputs, - } - - ch, err2 := writeTarget.Execute(ctx, req) - require.NoError(t, err2) - response := <-ch - require.Nil(t, response.Value) - }) - t.Run("fails when ChainReader's GetLatestValue returns error", func(t *testing.T) { req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: validInputs, + Metadata: validMetadata, + Config: config, + Inputs: validInputs, } cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmissionInfo", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("reader error")) @@ -121,11 +117,9 @@ func TestWriteTarget(t *testing.T) { t.Run("fails when ChainWriter's SubmitTransaction returns error", func(t *testing.T) { req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: validInputs, + Metadata: validMetadata, + Config: config, + Inputs: validInputs, } cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(errors.New("writer error")) @@ -152,11 +146,9 @@ func TestWriteTarget(t *testing.T) { t.Run("fails with nil config", func(t *testing.T) { req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: nil, - Inputs: validInputs, + Metadata: validMetadata, + Config: nil, + Inputs: validInputs, } _, err2 := writeTarget.Execute(ctx, req) require.Error(t, err2) @@ -164,11 +156,9 @@ func TestWriteTarget(t *testing.T) { t.Run("fails with nil inputs", func(t *testing.T) { req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: nil, + Metadata: validMetadata, + Config: config, + Inputs: nil, } _, err2 := writeTarget.Execute(ctx, req) require.Error(t, err2) diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go index 57a0f80f8d3..54e36714226 100644 --- a/core/services/relay/evm/write_target_test.go +++ b/core/services/relay/evm/write_target_test.go @@ -2,6 +2,7 @@ package evm_test import ( "bytes" + "encoding/hex" "errors" "fmt" "math/big" @@ -146,14 +147,53 @@ func TestEvmWrite(t *testing.T) { }) require.NoError(t, err) + reportMetadata := targets.ReportV1Metadata{ + Version: 1, + WorkflowExecutionID: [32]byte{}, + Timestamp: 0, + DonID: 0, + DonConfigVersion: 0, + WorkflowCID: [32]byte{}, + WorkflowName: [10]byte{}, + WorkflowOwner: [20]byte{}, + ReportID: [2]byte{}, + } + + reportMetadataBytes, err := reportMetadata.Encode() + require.NoError(t, err) + + signatures := [][]byte{} + + validInputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": reportMetadataBytes, + "signatures": signatures, + "context": []byte{4, 5}, + "id": []byte{9, 9}, + }, + }) + require.NoError(t, err) + + validMetadata := capabilities.RequestMetadata{ + WorkflowID: hex.EncodeToString(reportMetadata.WorkflowCID[:]), + WorkflowOwner: hex.EncodeToString(reportMetadata.WorkflowOwner[:]), + WorkflowName: hex.EncodeToString(reportMetadata.WorkflowName[:]), + WorkflowExecutionID: hex.EncodeToString(reportMetadata.WorkflowExecutionID[:]), + } + + validConfig, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), + }) + require.NoError(t, err) + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { req := args.Get(1).(txmgr.TxRequest) payload := make(map[string]any) method := forwardABI.Methods["report"] err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) require.NoError(t, err) - require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) - require.Equal(t, [][]byte{}, payload["signatures"]) + require.Equal(t, reportMetadataBytes, payload["rawReport"]) + require.Equal(t, signatures, payload["signatures"]) }).Once() t.Run("succeeds with valid report", func(t *testing.T) { @@ -161,59 +201,10 @@ func TestEvmWrite(t *testing.T) { capability, err := evm.NewWriteTarget(ctx, relayer, chain, lggr) require.NoError(t, err) - config, err := values.NewMap(map[string]any{ - "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), - }) - require.NoError(t, err) - - inputs, err := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": []byte{1, 2, 3}, - "signatures": [][]byte{}, - "context": []byte{4, 5}, - "id": []byte{9, 9}, - }, - }) - require.NoError(t, err) - - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: inputs, - } - - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) - - response := <-ch - require.Nil(t, response.Err) - }) - - t.Run("succeeds with empty report", func(t *testing.T) { - ctx := testutils.Context(t) - capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) - require.NoError(t, err) - - config, err := values.NewMap(map[string]any{ - "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), - }) - require.NoError(t, err) - - inputs, err := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": nil, - }, - }) - require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: inputs, + Metadata: validMetadata, + Config: validConfig, + Inputs: validInputs, } ch, err := capability.Execute(ctx, req) @@ -233,19 +224,10 @@ func TestEvmWrite(t *testing.T) { }) require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": nil, - }, - }) - require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: invalidConfig, - Inputs: inputs, + Metadata: validMetadata, + Config: invalidConfig, + Inputs: validInputs, } _, err = capability.Execute(ctx, req) @@ -257,27 +239,10 @@ func TestEvmWrite(t *testing.T) { capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) require.NoError(t, err) - config, err := values.NewMap(map[string]any{ - "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), - }) - require.NoError(t, err) - - inputs, err := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": []byte{1, 2, 3}, - "signatures": [][]byte{}, - "context": []byte{4, 5}, - "id": []byte{9, 9}, - }, - }) - require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "test-id", - }, - Config: config, - Inputs: inputs, + Metadata: validMetadata, + Config: validConfig, + Inputs: validInputs, } txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, errors.New("TXM error")) From 4b9169131cd44d6cb4f00dae2b33e49626af5f7d Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 9 Aug 2024 12:48:22 +0200 Subject: [PATCH 02/16] [TT-1326] Update Solidty Foundry pipeline with Slither (#13986) * More univeral lcov prunning * update Shared code cov * exclude deleted files from Slither * use single source of truth for all Solidity Foundry jobs * fix json * compact output with jq * fix condition for fmt * try to scope tests to changes * move matrix check to step level * fix outputs path * trigger * test with Automation change * try with shared * run fmt also if any sol files were modified * fix job name in collect metrics * trigger pipeline only for localised change + update changes info * add changeset * remove test change * do not run forge fmt if shared contracts have changed * try artifact pipeline by hijacking hardhat * # This is a combination of 2 commits. # This is the 1st commit message: CR changes + test them # This is the commit message #2: use shell array * CR changes + test them use shell array init array CR changes + test them use shell array init array * remove test files * do not run Slither for test files * do not run fmt if test files were modified * remove unused config file * restore old Hardhat pipeline * add missing transmission setup * fix basic info condition, join 2 steps into 1, define higher-level coverage exclusions * define actions for installing Slither and solc-select * run all tests also if package.json changes; run them on all non_src changes * add action for validating whether all Slither reports and UML diagrams were generated * fetch origin in validation action * compare with HEAD in validate action * compare with origin in validation action * handle both csv and shell arrays in the validation action * update artifact pipeline with new actions * fix workflow after tests * fix how validation actions works with commits * treat shared as any other product * small fixes * apply CR changes * remove special handling for deleted files * remove apt-get update * use only dorny/paths * remove unused input * CR changes: use dorny/paths with quantifier, move scope validation to an action, remove whitespaces * fix workflow * fail bash scripts on erors * add set -euo pipefail to bash scripts * define action to detect foundry version * fix select solc version script, better slither report output * checkout repo * add id --- .../action.yml | 26 ++ .github/actions/setup-slither/action.yaml | 10 + .github/actions/setup-solc-select/action.yaml | 30 ++ .../validate-artifact-scope/action.yaml | 103 +++++ .../validate-solidity-artifacts/action.yaml | 115 ++++++ .../workflows/solidity-foundry-artifacts.yml | 371 ++++++++++++++++++ .github/workflows/solidity-foundry.yml | 304 +++++++++++--- .github/workflows/solidity-hardhat.yml | 2 +- contracts/.changeset/itchy-deers-deny.md | 5 + .../slither/.slither.config-artifacts.json | 3 + .../slither/.slither.config-ccip-pr.json | 4 + .../slither/.slither.config-default-pr.json | 4 + contracts/scripts/ccip_lcov_prune | 29 -- .../scripts/ci/generate_slither_report.sh | 87 ++++ contracts/scripts/ci/generate_uml.sh | 121 ++++++ contracts/scripts/ci/modify_remappings.sh | 30 ++ contracts/scripts/ci/select_solc_version.sh | 118 ++++++ contracts/scripts/lcov_prune | 77 ++++ 18 files changed, 1354 insertions(+), 85 deletions(-) create mode 100644 .github/actions/detect-solidity-foundry-version/action.yml create mode 100644 .github/actions/setup-slither/action.yaml create mode 100644 .github/actions/setup-solc-select/action.yaml create mode 100644 .github/actions/validate-artifact-scope/action.yaml create mode 100644 .github/actions/validate-solidity-artifacts/action.yaml create mode 100644 .github/workflows/solidity-foundry-artifacts.yml create mode 100644 contracts/.changeset/itchy-deers-deny.md create mode 100644 contracts/configs/slither/.slither.config-artifacts.json create mode 100644 contracts/configs/slither/.slither.config-ccip-pr.json create mode 100644 contracts/configs/slither/.slither.config-default-pr.json delete mode 100755 contracts/scripts/ccip_lcov_prune create mode 100755 contracts/scripts/ci/generate_slither_report.sh create mode 100755 contracts/scripts/ci/generate_uml.sh create mode 100755 contracts/scripts/ci/modify_remappings.sh create mode 100755 contracts/scripts/ci/select_solc_version.sh create mode 100755 contracts/scripts/lcov_prune diff --git a/.github/actions/detect-solidity-foundry-version/action.yml b/.github/actions/detect-solidity-foundry-version/action.yml new file mode 100644 index 00000000000..b37f1e25094 --- /dev/null +++ b/.github/actions/detect-solidity-foundry-version/action.yml @@ -0,0 +1,26 @@ +name: 'Detect Foundry version in GNUmakefile' +description: 'Detects Foundry version in GNUmakefile' +inputs: + working-directory: + description: 'The GNUmakefile directory' + required: false + default: 'contracts' +outputs: + foundry-version: + description: 'Foundry version found in GNUmakefile' + value: ${{ steps.extract-foundry-version.outputs.foundry-version }} +runs: + using: 'composite' + steps: + - name: Extract Foundry version + id: extract-foundry-version + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + foundry_version=$(grep -Eo "foundryup --version [^ ]+" GNUmakefile | awk '{print $3}') + if [ -z "$foundry_version" ]; then + echo "::error::Foundry version not found in GNUmakefile" + exit 1 + fi + echo "Foundry version found: $foundry_version" + echo "foundry-version=$foundry_version" >> $GITHUB_OUTPUT diff --git a/.github/actions/setup-slither/action.yaml b/.github/actions/setup-slither/action.yaml new file mode 100644 index 00000000000..b8bef38575d --- /dev/null +++ b/.github/actions/setup-slither/action.yaml @@ -0,0 +1,10 @@ +name: Setup Slither +description: Installs Slither 0.10.3 for contract analysis. Requires Python 3.6 or higher. +runs: + using: composite + steps: + - name: Install Slither + shell: bash + run: | + python -m pip install --upgrade pip + pip install slither-analyzer==0.10.3 diff --git a/.github/actions/setup-solc-select/action.yaml b/.github/actions/setup-solc-select/action.yaml new file mode 100644 index 00000000000..b74ffae018d --- /dev/null +++ b/.github/actions/setup-solc-select/action.yaml @@ -0,0 +1,30 @@ +name: Setup Solc Select +description: Installs Solc Select, required versions and selects the version to use. Requires Python 3.6 or higher. +inputs: + to_install: + description: Comma-separated list of solc versions to install + required: true + to_use: + description: Solc version to use + required: true + +runs: + using: composite + steps: + - name: Install solc-select and solc + shell: bash + run: | + pip3 install solc-select + sudo ln -s /usr/local/bin/solc-select /usr/bin/solc-select + + IFS=',' read -ra versions <<< "${{ inputs.to_install }}" + for version in "${versions[@]}"; do + solc-select install $version + if [ $? -ne 0 ]; then + echo "Failed to install Solc $version" + exit 1 + fi + done + + solc-select install ${{ inputs.to_use }} + solc-select use ${{ inputs.to_use }} diff --git a/.github/actions/validate-artifact-scope/action.yaml b/.github/actions/validate-artifact-scope/action.yaml new file mode 100644 index 00000000000..7440efc63a3 --- /dev/null +++ b/.github/actions/validate-artifact-scope/action.yaml @@ -0,0 +1,103 @@ +name: Validate Artifact Scope +description: Checks there are any modified Solidity files outside of the specified scope. If so, it prints a warning message, but does not fail the workflow. +inputs: + product: + description: The product for which the artifacts are being generated + required: true + sol_files: + description: Comma-separated (CSV) or space-separated (shell) list of Solidity files to check + required: true + +runs: + using: composite + steps: + - name: Transform input array + id: transform_input_array + shell: bash + run: | + is_csv_format() { + local input="$1" + if [[ "$input" =~ "," ]]; then + return 0 + else + return 1 + fi + } + + is_space_separated_string() { + local input="$1" + if [[ "$input" =~ ^[^[:space:]]+([[:space:]][^[:space:]]+)*$ ]]; then + return 0 + else + return 1 + fi + } + + array="${{ inputs.sol_files }}" + + if is_csv_format "$array"; then + echo "::debug::CSV format detected, nothing to do" + echo "sol_files=$array" >> $GITHUB_OUTPUT + exit 0 + fi + + if is_space_separated_string "$array"; then + echo "::debug::Space-separated format detected, converting to CSV" + csv_array="${array// /,}" + echo "sol_files=$csv_array" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "::error::Invalid input format for sol_files. Please provide a comma-separated (CSV) or space-separated (shell) list of Solidity files" + exit 1 + + - name: Check for changes outside of artifact scope + shell: bash + run: | + echo "::debug::All modified contracts:" + echo "${{ steps.transform_input_array.outputs.sol_files }}" | tr ',' '\n' + if [ "${{ inputs.product }}" = "shared" ]; then + excluded_paths_pattern="!/^contracts\/src\/v0\.8\/interfaces/ && !/^contracts\/src\/v0\.8\/${{ inputs.product }}/ && !/^contracts\/src\/v0\.8\/[^\/]+\.sol$/" + else + excluded_paths_pattern="!/^contracts\/src\/v0\.8\/${{ inputs.product }}/" + fi + echo "::debug::Excluded paths: $excluded_paths_pattern" + unexpected_files=$(echo "${{ steps.transform_input_array.outputs.sol_files }}" | tr ',' '\n' | awk "$excluded_paths_pattern") + missing_files="" + set -e + set -o pipefail + if [[ -n "$unexpected_files" ]]; then + products=() + productsStr="" + IFS=$'\n' read -r -d '' -a files <<< "$unexpected_files" || true + echo "Files: ${files[@]}" + + for file in "${files[@]}"; do + missing_files+="$file," + + product=$(echo "$file" | awk -F'src/v0.8/' '{if ($2 ~ /\//) print substr($2, 1, index($2, "/")-1); else print "shared"}') + if [[ ! " ${products[@]} " =~ " ${product} " ]]; then + products+=("$product") + productsStr+="$product, " + fi + done + productsStr=${productsStr%, } + + set +e + set +o pipefail + + missing_files=$(echo $missing_files | tr ',' '\n') + + echo "Error: Found modified contracts outside of the expected scope: ${{ inputs.product }}" + echo "Files:" + echo "$missing_files" + echo "Action required: If you want to generate artifacts for other products ($productsStr) run this workflow again with updated configuration" + + echo "# Warning!" >> $GITHUB_STEP_SUMMARY + echo "## Reason: Found modified contracts outside of the expected scope: ${{ inputs.product }}" >> $GITHUB_STEP_SUMMARY + echo "### Files:" >> $GITHUB_STEP_SUMMARY + echo "$missing_files" >> $GITHUB_STEP_SUMMARY + echo "## Action required: If you want to generate artifacts for other products ($productsStr) run this workflow again with updated configuration" >> $GITHUB_STEP_SUMMARY + else + echo "No unexpected files found." + fi diff --git a/.github/actions/validate-solidity-artifacts/action.yaml b/.github/actions/validate-solidity-artifacts/action.yaml new file mode 100644 index 00000000000..5357a87f96b --- /dev/null +++ b/.github/actions/validate-solidity-artifacts/action.yaml @@ -0,0 +1,115 @@ +name: Validate Solidity Artifacts +description: Checks whether Slither reports and UML diagrams were generated for all necessary files. If not, a warning is printed in job summary, but the job is not marked as failed. +inputs: + slither_reports_path: + description: Path to the Slither reports directory (without trailing slash) + required: true + uml_diagrams_path: + description: Path to the UML diagrams directory (without trailing slash) + required: true + validate_slither_reports: + description: Whether Slither reports should be validated + required: true + validate_uml_diagrams: + description: Whether UML diagrams should be validated + required: true + sol_files: + description: Comma-separated (CSV) or space-separated (shell) list of Solidity files to check + required: true + +runs: + using: composite + steps: + - name: Transform input array + id: transform_input_array + shell: bash + run: | + is_csv_format() { + local input="$1" + if [[ "$input" =~ "," ]]; then + return 0 + else + return 1 + fi + } + + is_space_separated_string() { + local input="$1" + if [[ "$input" =~ ^[^[:space:]]+([[:space:]][^[:space:]]+)*$ ]]; then + return 0 + else + return 1 + fi + } + + array="${{ inputs.sol_files }}" + + if is_csv_format "$array"; then + echo "::debug::CSV format detected, nothing to do" + echo "sol_files=$array" >> $GITHUB_OUTPUT + exit 0 + fi + + if is_space_separated_string "$array"; then + echo "::debug::Space-separated format detected, converting to CSV" + csv_array="${array// /,}" + echo "sol_files=$csv_array" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "::error::Invalid input format for sol_files. Please provide a comma-separated (CSV) or space-separated (shell) list of Solidity files" + exit 1 + + - name: Validate UML diagrams + if: ${{ inputs.validate_uml_diagrams == 'true' }} + shell: bash + run: | + echo "Validating UML diagrams" + IFS=',' read -r -a modified_files <<< "${{ steps.transform_input_array.outputs.sol_files }}" + missing_svgs=() + for file in "${modified_files[@]}"; do + svg_file="$(basename "${file%.sol}").svg" + if [ ! -f "${{ inputs.uml_diagrams_path }}/$svg_file" ]; then + echo "Error: UML diagram for $file not found" + missing_svgs+=("$file") + fi + done + + if [ ${#missing_svgs[@]} -gt 0 ]; then + echo "Error: Missing UML diagrams for files: ${missing_svgs[@]}" + echo "# Warning!" >> $GITHUB_STEP_SUMMARY + echo "## Reason: Missing UML diagrams for files:" >> $GITHUB_STEP_SUMMARY + for file in "${missing_svgs[@]}"; do + echo " $file" >> $GITHUB_STEP_SUMMARY + done + echo "## Action required: Please try to generate artifacts for them locally or using a different tool" >> $GITHUB_STEP_SUMMARY + else + echo "All UML diagrams generated successfully" + fi + + - name: Validate Slither reports + if: ${{ inputs.validate_slither_reports == 'true' }} + shell: bash + run: | + echo "Validating Slither reports" + IFS=',' read -r -a modified_files <<< "${{ steps.transform_input_array.outputs.sol_files }}" + missing_reports=() + for file in "${modified_files[@]}"; do + report_file="$(basename "${file%.sol}")-slither-report.md" + if [ ! -f "${{ inputs.slither_reports_path }}/$report_file" ]; then + echo "Error: Slither report for $file not found" + missing_reports+=("$file") + fi + done + + if [ ${#missing_reports[@]} -gt 0 ]; then + echo "Error: Missing Slither reports for files: ${missing_reports[@]}" + echo "# Warning!" >> $GITHUB_STEP_SUMMARY + echo "## Reason: Missing Slither reports for files:" >> $GITHUB_STEP_SUMMARY + for file in "${missing_reports[@]}"; do + echo " $file" >> $GITHUB_STEP_SUMMARY + done + echo "## Action required: Please try to generate artifacts for them locally" >> $GITHUB_STEP_SUMMARY + else + echo "All Slither reports generated successfully" + fi diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml new file mode 100644 index 00000000000..e489033d675 --- /dev/null +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -0,0 +1,371 @@ +name: Solidity Foundry Artifact Generation +on: + workflow_dispatch: + inputs: + product: + type: choice + description: 'product for which to generate artifacts; should be the same as Foundry profile' + required: true + options: + - "automation" + - "ccip" + - "functions" + - "keystone" + - "l2ep" + - "liquiditymanager" + - "llo-feeds" + - "operatorforwarder" + - "shared" + - "transmission" + - "vrf" + base_ref: + description: 'commit or tag to be used as base reference, when looking for modified Solidity files' + required: true + +env: + FOUNDRY_PROFILE: ci + +jobs: + changes: + name: Detect changes + runs-on: ubuntu-latest + outputs: + changes: ${{ steps.changes.outputs.sol }} + product_changes: ${{ steps.changes-transform.outputs.product_changes }} + product_files: ${{ steps.changes-transform.outputs.product_files }} + changeset_changes: ${{ steps.changes-dorny.outputs.changeset }} + changeset_files: ${{ steps.changes-dorny.outputs.changeset_files }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Find modified contracts + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + with: + list-files: 'csv' + base: ${{ inputs.base_ref }} + predicate-quantifier: every + filters: | + ignored: &ignored + - '!contracts/src/v0.8/**/test/**' + - '!contracts/src/v0.8/**/tests/**' + - '!contracts/src/v0.8/**/mock/**' + - '!contracts/src/v0.8/**/mocks/**' + - '!contracts/src/v0.8/**/*.t.sol' + - '!contracts/src/v0.8/*.t.sol' + - '!contracts/src/v0.8/**/testhelpers/**' + - '!contracts/src/v0.8/testhelpers/**' + - '!contracts/src/v0.8/vendor/**' + other_shared: + - modified|added: 'contracts/src/v0.8/(interfaces/**/*.sol|*.sol)' + - *ignored + sol: + - modified|added: 'contracts/src/v0.8/**/*.sol' + - *ignored + product: &product + - modified|added: 'contracts/src/v0.8/${{ inputs.product }}/**/*.sol' + - *ignored + changeset: + - modified|added: 'contracts/.changeset/!(README)*.md' + + # Manual transformation needed, because shared contracts have a different folder structure + - name: Transform modified files + id: changes-transform + shell: bash + run: | + if [ "${{ inputs.product }}" = "shared" ]; then + echo "::debug:: Product is shared, transforming changes" + if [[ "${{ steps.changes.outputs.product }}" == "true" && "${{ steps.changes.outputs.other_shared }}" == "true" ]]; then + echo "::debug:: Changes were found in 'shared' folder and in 'interfaces' and root folders" + echo "product_changes=true" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.product_files }},${{ steps.changes.outputs.other_shared_files }}" >> $GITHUB_OUTPUT + elif [[ "${{ steps.changes.outputs.product }}" == "false" && "${{ steps.changes.outputs.other_shared }}" == "true" ]]; then + echo "::debug:: Only contracts in' interfaces' and root folders were modified" + echo "product_changes=true" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.other_shared_files }}" >> $GITHUB_OUTPUT + elif [[ "${{ steps.changes.outputs.product }}" == "true" && "${{ steps.changes.outputs.other_shared }}" == "false" ]]; then + echo "::debug:: Only contracts in 'shared' folder were modified" + echo "product_changes=true" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.product_files }}" >> $GITHUB_OUTPUT + else + echo "::debug:: No contracts were modified" + echo "product_changes=false" >> $GITHUB_OUTPUT + echo "product_files=" >> $GITHUB_OUTPUT + fi + else + echo "product_changes=${{ steps.changes.outputs.product }}" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.product_files }}" >> $GITHUB_OUTPUT + fi + + - name: Check for changes outside of artifact scope + uses: ./.github/actions/validate-artifact-scope + if: ${{ steps.changes.outputs.sol == 'true' }} + with: + sol_files: ${{ steps.changes.outputs.sol_files }} + product: ${{ inputs.product }} + + gather-basic-info: + name: Gather basic info + if: ${{ needs.changes.outputs.product_changes == 'true' }} + runs-on: ubuntu-22.04 + needs: [ changes ] + outputs: + foundry_version: ${{ steps.extract-foundry-version.outputs.foundry-version }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + fetch-depth: 0 + + - name: Extract Foundry version + uses: ./.github/actions/detect-solidity-foundry-version + with: + working-directory: contracts + + - name: Copy modified changesets + if: ${{ needs.changes.outputs.changeset_changes == 'true' }} + run: | + mkdir -p contracts/changesets + files="${{ needs.changes.outputs.changeset_files }}" + IFS=",' + for changeset in $files; do + echo "::debug:: Copying $changeset" + cp $changeset contracts/changesets + done + + - name: Generate basic info and modified contracts list + shell: bash + run: | + echo "Commit SHA used to generate artifacts: ${{ github.sha }}" > contracts/commit_sha_base_ref.txt + echo "Base reference SHA used to find modified contracts: ${{ inputs.base_ref }}" >> contracts/commit_sha_base_ref.txt + + IFS=',' read -r -a modified_files <<< "${{ needs.changes.outputs.product_files }}" + echo "# Modified contracts:" > contracts/modified_contracts.md + for file in "${modified_files[@]}"; do + echo " - [$file](${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/$file)" >> contracts/modified_contracts.md + echo "$file" >> contracts/modified_contracts.txt + done + + - name: Upload basic info and modified contracts list + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 2 + continue-on-error: true + with: + name: tmp-basic-info + path: | + contracts/modified_contracts.md + contracts/modified_contracts.txt + contracts/commit_sha_base_ref.txt + contracts/changesets + retention-days: 7 + + # some of the artifacts can only be generated on product level, and we cannot scope them to single contracts + # some product-level modifications might also require shared contracts changes, so if these happened we need to generate artifacts for shared contracts as well + coverage-and-book: + if: ${{ needs.changes.outputs.product_changes == 'true' }} + name: Generate Docs and Code Coverage reports + runs-on: ubuntu-22.04 + needs: [changes, gather-basic-info] + steps: + - name: Prepare exclusion list + id: prepare-exclusion-list + run: | + cat < coverage_exclusions.json + ["automation", "functions", "vrf"] + EOF + coverage_exclusions=$(cat coverage_exclusions.json | jq -c .) + echo "coverage_exclusions=$coverage_exclusions" >> $GITHUB_OUTPUT + + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + + - name: Create directories + shell: bash + run: | + mkdir -p contracts/code-coverage + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + with: + version: ${{ needs.gather-basic-info.outputs.foundry_version }} + + # required for code coverage report generation + - name: Setup LCOV + uses: hrishikesh-kadam/setup-lcov@f5da1b26b0dcf5d893077a3c4f29cf78079c841d # v1.0.0 + + - name: Run Forge build for product contracts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + run: | + forge --version + forge build + working-directory: contracts + env: + FOUNDRY_PROFILE: ${{ inputs.product }} + + - name: Run coverage for product contracts + if: ${{ !contains(fromJson(steps.prepare-exclusion-list.outputs.coverage_exclusions), inputs.product) && needs.changes.outputs.product_changes == 'true' }} + working-directory: contracts + run: forge coverage --report lcov --report-file code-coverage/lcov.info + env: + FOUNDRY_PROFILE: ${{ inputs.product }} + + - name: Generate Code Coverage HTML report for product contracts + if: ${{ !contains(fromJson(steps.prepare-exclusion-list.outputs.coverage_exclusions), inputs.product) && needs.changes.outputs.product_changes == 'true' }} + shell: bash + working-directory: contracts + run: genhtml code-coverage/lcov.info --branch-coverage --output-directory code-coverage + + - name: Run Forge doc for product contracts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + run: forge doc --build -o docs + working-directory: contracts + env: + FOUNDRY_PROFILE: ${{ inputs.product }} + + - name: Upload Artifacts for product contracts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 2 + continue-on-error: true + with: + name: tmp-${{ inputs.product }}-artifacts + path: | + contracts/docs + contracts/code-coverage/lcov-.info + contracts/code-coverage + retention-days: 7 + + # Generates UML diagrams and Slither reports for modified contracts + uml-static-analysis: + if: ${{ needs.changes.outputs.product_changes == 'true' }} + name: Generate UML and Slither reports for modified contracts + runs-on: ubuntu-22.04 + needs: [changes, gather-basic-info] + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + fetch-depth: 0 + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + with: + version: ${{ needs.gather-basic-info.outputs.foundry_version }} + + - name: Install Sol2uml + run: | + npm link sol2uml --only=production + + - name: Set up Python + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 + with: + python-version: '3.8' + + - name: Install solc-select and solc + uses: ./.github/actions/setup-solc-select + with: + to_install: '0.8.19' + to_use: '0.8.19' + + - name: Install Slither + uses: ./.github/actions/setup-slither + + - name: Generate UML + shell: bash + run: | + contract_list="${{ needs.changes.outputs.product_files }}" + + # modify remappings so that solc can find dependencies + ./contracts/scripts/ci/modify_remappings.sh contracts contracts/remappings.txt + mv remappings_modified.txt remappings.txt + + ./contracts/scripts/ci/generate_uml.sh "./" "contracts/uml-diagrams" "$contract_list" + + - name: Generate Slither Markdown reports + run: | + contract_list="${{ needs.changes.outputs.product_files }}" + + echo "::debug::Processing contracts: $contract_list" + ./contracts/scripts/ci/generate_slither_report.sh "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/" contracts/configs/slither/.slither.config-artifacts.json "." "$contract_list" "contracts/slither-reports" "--solc-remaps @=contracts/node_modules/@" + + - name: Upload UMLs and Slither reports + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 10 + continue-on-error: true + with: + name: tmp-contracts-artifacts + path: | + contracts/uml-diagrams + contracts/slither-reports + retention-days: 7 + + - name: Validate if all Slither run for all contracts + uses: ./.github/actions/validate-solidity-artifacts + with: + validate_slither_reports: 'true' + validate_uml_diagrams: 'true' + slither_reports_path: 'contracts/slither-reports' + uml_diagrams_path: 'contracts/uml-diagrams' + sol_files: ${{ needs.changes.outputs.product_files }} + + gather-all-artifacts: + name: Gather all artifacts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + runs-on: ubuntu-latest + needs: [coverage-and-book, uml-static-analysis, gather-basic-info, changes] + steps: + - name: Download all artifacts + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + path: review_artifacts + merge-multiple: true + + - name: Upload all artifacts as single package + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + with: + name: review-artifacts-${{ inputs.product }}-${{ github.sha }} + path: review_artifacts + retention-days: 60 + + - name: Remove temporary artifacts + uses: geekyeggo/delete-artifact@24928e75e6e6590170563b8ddae9fac674508aa1 # v5.0 + with: + name: tmp-* + + - name: Print Artifact URL in job summary + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ARTIFACTS=$(gh api -X GET repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts) + ARTIFACT_ID=$(echo "$ARTIFACTS" | jq '.artifacts[] | select(.name=="review-artifacts-${{ inputs.product }}-${{ github.sha }}") | .id') + echo "Artifact ID: $ARTIFACT_ID" + + echo "# Solidity Review Artifact Generated" >> $GITHUB_STEP_SUMMARY + echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit SHA used: **${{ github.sha }}**" >> $GITHUB_STEP_SUMMARY + echo "[Artifact URL](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID)" >> $GITHUB_STEP_SUMMARY + + notify-no-changes: + if: ${{ needs.changes.outputs.product_changes == 'false' }} + needs: [changes] + runs-on: ubuntu-latest + steps: + - name: Print warning in job summary + shell: bash + run: | + echo "# Solidity Review Artifact NOT Generated" >> $GITHUB_STEP_SUMMARY + echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit SHA used: **${{ github.sha }}**" >> $GITHUB_STEP_SUMMARY + echo "## Reason: No modified Solidity files found for ${{ inputs.product }}" >> $GITHUB_STEP_SUMMARY + echo "* no modified Solidity files found between ${{ inputs.base_ref }} and ${{ github.sha }} commits" >> $GITHUB_STEP_SUMMARY + echo "* or they are located outside of ./contracts/src/v0.8 folder" >> $GITHUB_STEP_SUMMARY + echo "* or they were limited to test files" >> $GITHUB_STEP_SUMMARY + exit 1 diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index 4ec9e424471..906cb76ffe5 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -3,42 +3,122 @@ on: [pull_request] env: FOUNDRY_PROFILE: ci - # Has to match the `make foundry` version in `contracts/GNUmakefile` - FOUNDRY_VERSION: nightly-de33b6af53005037b463318d2628b5cfcaf39916 + +# Making changes: +# * use the top-level matrix to decide, which checks should run for each product. +# * when enabling code coverage, remember to adjust the minimum code coverage as it's set to 98.5% by default. jobs: + define-matrix: + name: Define test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.define-matrix.outputs.matrix }} + foundry-version: ${{ steps.extract-foundry-version.outputs.foundry-version }} + steps: + - name: Define test matrix + id: define-matrix + shell: bash + run: | + cat < matrix.json + [ + { "name": "automation", "setup": { "run-coverage": false, "min-coverage": 98.5, "run-gas-snapshot": false, "run-forge-fmt": false }}, + { "name": "ccip", "setup": { "run-coverage": true, "min-coverage": 98.5, "run-gas-snapshot": true, "run-forge-fmt": true }}, + { "name": "functions", "setup": { "run-coverage": false, "min-coverage": 98.5, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "keystone", "setup": { "run-coverage": true, "min-coverage": 74.1, "run-gas-snapshot": false, "run-forge-fmt": false }}, + { "name": "l2ep", "setup": { "run-coverage": true, "min-coverage": 65.6, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "liquiditymanager", "setup": { "run-coverage": true, "min-coverage": 46.3, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "llo-feeds", "setup": { "run-coverage": true, "min-coverage": 49.3, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "operatorforwarder", "setup": { "run-coverage": true, "min-coverage": 55.7, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "shared", "setup": { "run-coverage": true, "min-coverage": 32.6, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "transmission", "setup": { "run-coverage": true, "min-coverage": 65.6, "run-gas-snapshot": true, "run-forge-fmt": false }}, + { "name": "vrf", "setup": { "run-coverage": false, "min-coverage": 98.5, "run-gas-snapshot": false, "run-forge-fmt": false }} + ] + EOF + + matrix=$(cat matrix.json | jq -c .) + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Extract Foundry version + id: extract-foundry-version + uses: ./.github/actions/detect-solidity-foundry-version + with: + working-directory: contracts + changes: name: Detect changes runs-on: ubuntu-latest outputs: - changes: ${{ steps.changes.outputs.src }} + non_src_changes: ${{ steps.changes.outputs.non_src }} + sol_modified: ${{ steps.changes.outputs.sol }} + sol_modified_files: ${{ steps.changes.outputs.sol_files }} + not_test_sol_modified: ${{ steps.changes.outputs.not_test_sol }} + not_test_sol_modified_files: ${{ steps.changes.outputs.not_test_sol_files }} + all_changes: ${{ steps.changes.outputs.changes }} steps: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: changes with: + list-files: 'shell' filters: | - src: - - 'contracts/src/v0.8/**/*' + non_src: - '.github/workflows/solidity-foundry.yml' - 'contracts/foundry.toml' - 'contracts/gas-snapshots/*.gas-snapshot' + - 'contracts/package.json' + sol: + - modified|added: 'contracts/src/v0.8/**/*.sol' + not_test_sol: + - modified|added: 'contracts/src/v0.8/**/!(*.t).sol' + automation: + - 'contracts/src/v0.8/automation/**/*.sol' + ccip: + - 'contracts/src/v0.8/ccip/**/*.sol' + functions: + - 'contracts/src/v0.8/functions/**/*.sol' + keystone: + - 'contracts/src/v0.8/keystone/**/*.sol' + l2ep: + - 'contracts/src/v0.8/l2ep/**/*.sol' + liquiditymanager: + - 'contracts/src/v0.8/liquiditymanager/**/*.sol' + llo-feeds: + - 'contracts/src/v0.8/llo-feeds/**/*.sol' + operatorforwarder: + - 'contracts/src/v0.8/operatorforwarder/**/*.sol' + vrf: + - 'contracts/src/v0.8/vrf/**/*.sol' + shared: + - 'contracts/src/v0.8/shared/**/*.sol' + - 'contracts/src/v0.8/*.sol' + - 'contracts/src/v0.8/mocks/**/*.sol' + - 'contracts/src/v0.8/tests/**/*.sol' + - 'contracts/src/v0.8/vendor/**/*.sol' + transmission: + - 'contracts/src/v0.8/transmission/**/*.sol' tests: + if: ${{ needs.changes.outputs.non_src_changes == 'true' || needs.changes.outputs.sol_modified == 'true' }} strategy: fail-fast: false matrix: - product: [automation, ccip, functions, keystone, l2ep, liquiditymanager, llo-feeds, operatorforwarder, shared, vrf] - needs: [changes] - name: Foundry Tests ${{ matrix.product }} - # See https://github.com/foundry-rs/foundry/issues/3827 + product: ${{fromJson(needs.define-matrix.outputs.matrix)}} + needs: [define-matrix, changes] + name: Foundry Tests ${{ matrix.product.name }} runs-on: ubuntu-22.04 # The if statements for steps after checkout repo is workaround for # passing required check for PRs that don't have filtered changes. steps: - name: Checkout the repo + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true' }} uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: submodules: recursive @@ -47,127 +127,241 @@ jobs: # and not native Foundry. This is to make sure the dependencies # stay in sync. - name: Setup NodeJS - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true' }} uses: ./.github/actions/setup-nodejs - name: Install Foundry - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true' }} uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 with: - version: ${{ env.FOUNDRY_VERSION }} + version: ${{ needs.define-matrix.outputs.foundry-version }} - name: Run Forge build - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true' }} run: | forge --version forge build id: build working-directory: contracts env: - FOUNDRY_PROFILE: ${{ matrix.product }} + FOUNDRY_PROFILE: ${{ matrix.product.name }} - name: Run Forge tests - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true' }} run: | forge test -vvv id: test working-directory: contracts env: - FOUNDRY_PROFILE: ${{ matrix.product }} + FOUNDRY_PROFILE: ${{ matrix.product.name }} - name: Run Forge snapshot - if: ${{ !contains(fromJson('["vrf"]'), matrix.product) && !contains(fromJson('["automation"]'), matrix.product) && !contains(fromJson('["keystone"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + if: ${{ (contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true') + && matrix.product.setup.run-gas-snapshot }} run: | - forge snapshot --nmt "test_?Fuzz_\w{1,}?" --check gas-snapshots/${{ matrix.product }}.gas-snapshot + forge snapshot --nmt "test_?Fuzz_\w{1,}?" --check gas-snapshots/${{ matrix.product.name }}.gas-snapshot id: snapshot working-directory: contracts env: - FOUNDRY_PROFILE: ${{ matrix.product }} + FOUNDRY_PROFILE: ${{ matrix.product.name }} - - name: Run coverage - if: ${{ contains(fromJson('["ccip"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + # required for code coverage report generation + - name: Setup LCOV + if: ${{ (contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true') + && matrix.product.setup.run-coverage }} + uses: hrishikesh-kadam/setup-lcov@f5da1b26b0dcf5d893077a3c4f29cf78079c841d # v1.0.0 + + - name: Run coverage for ${{ matrix.product.name }} + if: ${{ (contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true') + && matrix.product.setup.run-coverage }} working-directory: contracts run: forge coverage --report lcov env: - FOUNDRY_PROFILE: ${{ matrix.product }} + FOUNDRY_PROFILE: ${{ matrix.product.name }} - - name: Prune report - if: ${{ contains(fromJson('["ccip"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + - name: Prune lcov report + if: ${{ (contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true') + && matrix.product.setup.run-coverage }} run: | sudo apt-get install lcov - ./contracts/scripts/ccip_lcov_prune ./contracts/lcov.info ./lcov.info.pruned + ./contracts/scripts/lcov_prune ${{ matrix.product.name }} ./contracts/lcov.info ./contracts/lcov.info.pruned - - name: Report code coverage - if: ${{ contains(fromJson('["ccip"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + - name: Report code coverage for ${{ matrix.product.name }} + if: ${{ (contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true') + && matrix.product.setup.run-coverage }} uses: zgosalvez/github-actions-report-lcov@a546f89a65a0cdcd82a92ae8d65e74d450ff3fbc # v4.1.4 with: - update-comment: true - coverage-files: lcov.info.pruned - minimum-coverage: 98.5 - artifact-name: code-coverage-report + update-comment: false + coverage-files: ./contracts/lcov.info.pruned + minimum-coverage: ${{ matrix.product.min-coverage }} + artifact-name: code-coverage-report-${{ matrix.product.name }} working-directory: ./contracts - github-token: ${{ secrets.GITHUB_TOKEN }} - name: Collect Metrics - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) + || contains(fromJson(needs.changes.outputs.all_changes), 'shared') + || needs.changes.outputs.non_src_changes == 'true' }} id: collect-gha-metrics uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1 with: - id: solidity-foundry + id: ${{ matrix.product.name }}-solidity-foundry + org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} + basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} + hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} + this-job-name: Foundry Tests ${{ matrix.product.name }} + continue-on-error: true + + analyze: + needs: [ changes, define-matrix ] + name: Run static analysis + if: needs.changes.outputs.not_test_sol_modified == 'true' + runs-on: ubuntu-22.04 + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + submodules: recursive + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + with: + version: ${{ needs.define-matrix.outputs.foundry-version }} + + - name: Set up Python + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 + with: + python-version: '3.8' + + - name: Install solc-select and solc + uses: ./.github/actions/setup-solc-select + with: + to_install: '0.8.19' + to_use: '0.8.19' + + - name: Install Slither + uses: ./.github/actions/setup-slither + + - name: Run Slither + shell: bash + run: | + # modify remappings so that solc can find dependencies + ./contracts/scripts/ci/modify_remappings.sh contracts contracts/remappings.txt + mv remappings_modified.txt remappings.txt + + FILES="${{ needs.changes.outputs.not_test_sol_modified_files }}" + + for FILE in $FILES; do + PRODUCT=$(echo "$FILE" | awk -F'src/[^/]*/' '{print $2}' | cut -d'/' -f1) + echo "::debug::Running Slither for $FILE in $PRODUCT" + SLITHER_CONFIG="contracts/configs/slither/.slither.config-$PRODUCT-pr.json" + if [ ! -f $SLITHER_CONFIG ]; then + echo "::debug::No Slither config found for $PRODUCT, using default" + SLITHER_CONFIG="contracts/configs/slither/.slither.config-default-pr.json" + fi + ./contracts/scripts/ci/generate_slither_report.sh "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/" "$SLITHER_CONFIG" "." "$FILE" "contracts/slither-reports" "--solc-remaps @=contracts/node_modules/@" + done + + - name: Print Slither summary + shell: bash + run: | + echo "# Static analysis results " >> $GITHUB_STEP_SUMMARY + for file in "contracts/slither-reports"/*.md; do + if [ -e "$file" ]; then + cat "$file" >> $GITHUB_STEP_SUMMARY + fi + done + + - name: Validate if all Slither run for all contracts + uses: ./.github/actions/validate-solidity-artifacts + with: + validate_slither_reports: 'true' + slither_reports_path: 'contracts/slither-reports' + sol_files: ${{ needs.changes.outputs.not_test_sol_modified_files }} + + - name: Upload Slither report + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 10 + continue-on-error: true + with: + name: slither-reports-${{ github.sha }} + path: | + contracts/slither-reports + retention-days: 7 + + - name: Collect Metrics + id: collect-gha-metrics + uses: smartcontractkit/push-gha-metrics-action@dea9b546553cb4ca936607c2267a09c004e4ab3f # v3.0.0 + with: + id: solidity-foundry-slither org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} - this-job-name: Foundry Tests ${{ matrix.product }} + this-job-name: Run static analysis continue-on-error: true solidity-forge-fmt: + name: Forge fmt ${{ matrix.product.name }} + if: ${{ needs.changes.outputs.non_src_changes == 'true' || needs.changes.outputs.not_test_sol_modified == 'true' }} + needs: [define-matrix, changes] strategy: fail-fast: false matrix: - product: [ ccip ] - needs: [ changes ] - name: Forge fmt ${{ matrix.product }} - # See https://github.com/foundry-rs/foundry/issues/3827 + product: ${{fromJson(needs.define-matrix.outputs.matrix)}} runs-on: ubuntu-22.04 - - # The if statements for steps after checkout repo is workaround for - # passing required check for PRs that don't have filtered changes. steps: - name: Checkout the repo + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) && matrix.product.setup.run-forge-fmt }} uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: submodules: recursive - # Only needed because we use the NPM versions of packages - # and not native Foundry. This is to make sure the dependencies - # stay in sync. - name: Setup NodeJS - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) && matrix.product.setup.run-forge-fmt }} uses: ./.github/actions/setup-nodejs - name: Install Foundry - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) && matrix.product.setup.run-forge-fmt }} uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 with: - version: ${{ env.FOUNDRY_VERSION }} + version: ${{ needs.define-matrix.outputs.foundry-version }} - name: Run Forge fmt - if: needs.changes.outputs.changes == 'true' - run: | - forge fmt --check + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) && matrix.product.setup.run-forge-fmt }} + run: forge fmt --check id: fmt working-directory: contracts env: - FOUNDRY_PROFILE: ${{ matrix.product }} + FOUNDRY_PROFILE: ${{ matrix.product.name }} - name: Collect Metrics - if: needs.changes.outputs.changes == 'true' + if: ${{ contains(fromJson(needs.changes.outputs.all_changes), matrix.product.name) && matrix.product.setup.run-forge-fmt }} id: collect-gha-metrics uses: smartcontractkit/push-gha-metrics-action@dea9b546553cb4ca936607c2267a09c004e4ab3f # v3.0.0 with: - id: solidity-forge-fmt + id: solidity-forge-fmt-${{ matrix.product.name }} org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} - this-job-name: Foundry Tests ${{ matrix.product }} + this-job-name: Forge fmt ${{ matrix.product.name }} continue-on-error: true diff --git a/.github/workflows/solidity-hardhat.yml b/.github/workflows/solidity-hardhat.yml index f28cf499072..705fad3be60 100644 --- a/.github/workflows/solidity-hardhat.yml +++ b/.github/workflows/solidity-hardhat.yml @@ -84,4 +84,4 @@ jobs: basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} this-job-name: Solidity - continue-on-error: true \ No newline at end of file + continue-on-error: true diff --git a/contracts/.changeset/itchy-deers-deny.md b/contracts/.changeset/itchy-deers-deny.md new file mode 100644 index 00000000000..888d58ce311 --- /dev/null +++ b/contracts/.changeset/itchy-deers-deny.md @@ -0,0 +1,5 @@ +--- +"@chainlink/contracts": patch +--- + +More comprehensive & product-scoped Solidity Foundry pipeline diff --git a/contracts/configs/slither/.slither.config-artifacts.json b/contracts/configs/slither/.slither.config-artifacts.json new file mode 100644 index 00000000000..75071341f51 --- /dev/null +++ b/contracts/configs/slither/.slither.config-artifacts.json @@ -0,0 +1,3 @@ +{ + "filter_paths": "(openzeppelin|mocks/|test/|tests/|testhelpers)" +} diff --git a/contracts/configs/slither/.slither.config-ccip-pr.json b/contracts/configs/slither/.slither.config-ccip-pr.json new file mode 100644 index 00000000000..84d231ea07b --- /dev/null +++ b/contracts/configs/slither/.slither.config-ccip-pr.json @@ -0,0 +1,4 @@ +{ + "filter_paths": "(openzeppelin|mocks/|test/|tests/|testhelpers)", + "detectors_to_exclude": "pragma,solc-version,naming-convention,assembly,reentrancy-events,timestamp,calls-loop,unused-return" +} diff --git a/contracts/configs/slither/.slither.config-default-pr.json b/contracts/configs/slither/.slither.config-default-pr.json new file mode 100644 index 00000000000..1ef145a7954 --- /dev/null +++ b/contracts/configs/slither/.slither.config-default-pr.json @@ -0,0 +1,4 @@ +{ + "filter_paths": "(openzeppelin|mocks/|test/|tests/|testhelpers)", + "detectors_to_exclude": "pragma" +} diff --git a/contracts/scripts/ccip_lcov_prune b/contracts/scripts/ccip_lcov_prune deleted file mode 100755 index 002e5a3f133..00000000000 --- a/contracts/scripts/ccip_lcov_prune +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# src/v0.8/ccip/libraries/Internal.sol -# src/v0.8/ccip/libraries/RateLimiter.sol -# src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol -# src/v0.8/ccip/libraries/MerkleMultiProof.sol -# src/v0.8/ccip/libraries/Pool.sol -# excluded because Foundry doesn't support coverage on library files - -# BurnWithFromMintTokenPool is excluded because Forge doesn't seem to -# register coverage, even though it is 100% covered. - -lcov --remove $1 -o $2 \ - '*/ccip/test/*' \ - '*/vendor/*' \ - '*/shared/*' \ - 'src/v0.8/ccip/ocr/OCR2Abstract.sol' \ - 'src/v0.8/ccip/libraries/Internal.sol' \ - 'src/v0.8/ccip/libraries/RateLimiter.sol' \ - 'src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol' \ - 'src/v0.8/ccip/libraries/MerkleMultiProof.sol' \ - 'src/v0.8/ccip/libraries/Pool.sol' \ - 'src/v0.8/ConfirmedOwnerWithProposal.sol' \ - 'src/v0.8/tests/MockV3Aggregator.sol' \ - 'src/v0.8/ccip/applications/CCIPClientExample.sol' \ - 'src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol' \ - --rc lcov_branch_coverage=1 diff --git a/contracts/scripts/ci/generate_slither_report.sh b/contracts/scripts/ci/generate_slither_report.sh new file mode 100755 index 00000000000..7fe31d40efc --- /dev/null +++ b/contracts/scripts/ci/generate_slither_report.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function check_chainlink_dir() { + local param_dir="chainlink" + current_dir=$(pwd) + + current_base=$(basename "$current_dir") + + if [[ "$current_base" != "$param_dir" ]]; then + >&2 echo "The script must be run from the root of $param_dir directory" + exit 1 + fi +} + +check_chainlink_dir + +if [ "$#" -lt 5 ]; then + >&2 echo "Generates Markdown Slither reports and saves them to a target directory." + >&2 echo "Usage: $0 [slither extra params]" + exit 1 +fi + +REPO_URL=$1 +CONFIG_FILE=$2 +SOURCE_DIR=$3 +FILES=${4// /} # Remove any spaces from the list of files +TARGET_DIR=$5 +SLITHER_EXTRA_PARAMS=$6 + +run_slither() { + local FILE=$1 + local TARGET_DIR=$2 + + if [[ ! -f "$FILE" ]]; then + >&2 echo "::error:File not found: $FILE" + return 1 + fi + + set +e + source ./contracts/scripts/ci/select_solc_version.sh "$FILE" + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to select Solc version for $FILE" + return 1 + fi + set -e + + SLITHER_OUTPUT_FILE="$TARGET_DIR/$(basename "${FILE%.sol}")-slither-report.md" + + output=$(slither --config-file "$CONFIG_FILE" "$FILE" --checklist --markdown-root "$REPO_URL" --fail-none $SLITHER_EXTRA_PARAMS) + if [ $? -ne 0 ]; then + >&2 echo "::error::Slither failed for $FILE" + exit 1 + fi + output=$(echo "$output" | sed '/\*\*THIS CHECKLIST IS NOT COMPLETE\*\*. Use `--show-ignored-findings` to show all the results./d' | sed '/Summary/d') + + echo "# Summary for $FILE" > "$SLITHER_OUTPUT_FILE" + echo "$output" >> "$SLITHER_OUTPUT_FILE" + + if [[ -z "$output" ]]; then + echo "No issues found." >> "$SLITHER_OUTPUT_FILE" + fi +} + +process_files() { + local SOURCE_DIR=$1 + local TARGET_DIR=$2 + local FILES=(${3//,/ }) # Split the comma-separated list into an array + + mkdir -p "$TARGET_DIR" + + for FILE in "${FILES[@]}"; do + FILE=${FILE//\"/} + run_slither "$SOURCE_DIR/$FILE" "$TARGET_DIR" + done +} + +set +e +process_files "$SOURCE_DIR" "$TARGET_DIR" "${FILES[@]}" + +if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to generate Slither reports" + exit 1 +fi + +echo "Slither reports saved in $TARGET_DIR folder" diff --git a/contracts/scripts/ci/generate_uml.sh b/contracts/scripts/ci/generate_uml.sh new file mode 100755 index 00000000000..65745c93bbe --- /dev/null +++ b/contracts/scripts/ci/generate_uml.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function check_chainlink_dir() { + local param_dir="chainlink" + current_dir=$(pwd) + + current_base=$(basename "$current_dir") + + if [[ "$current_base" != "$param_dir" ]]; then + >&2 echo "The script must be run from the root of $param_dir directory" + exit 1 + fi +} + +check_chainlink_dir + +if [ "$#" -lt 2 ]; then + >&2 echo "Generates UML diagrams for all contracts in a directory after flattening them to avoid call stack overflows." + >&2 echo "Usage: $0 [comma-separated list of files]" + exit 1 +fi + +SOURCE_DIR="$1" +TARGET_DIR="$2" +FILES=${3// /} # Remove any spaces from the list of files +FAILED_FILES=() + +flatten_and_generate_uml() { + local FILE=$1 + local TARGET_DIR=$2 + + set +e + FLATTENED_FILE="$TARGET_DIR/flattened_$(basename "$FILE")" + echo "::debug::Flattening $FILE to $FLATTENED_FILE" + forge flatten "$FILE" -o "$FLATTENED_FILE" --root contracts + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to flatten $FILE" + FAILED_FILES+=("$FILE") + return + fi + + OUTPUT_FILE=${FLATTENED_FILE//"flattened_"/""} + OUTPUT_FILE_SVG="${OUTPUT_FILE%.sol}.svg" + echo "::debug::Generating SVG UML for $FLATTENED_FILE to $OUTPUT_FILE_SVG" + sol2uml "$FLATTENED_FILE" -o "$OUTPUT_FILE_SVG" + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to generate UML diagram in SVG format for $FILE" + FAILED_FILES+=("$FILE") + rm "$FLATTENED_FILE" + return + fi + OUTPUT_FILE_DOT="${OUTPUT_FILE%.sol}.dot" + echo "::debug::Generating DOT UML for $FLATTENED_FILE to $OUTPUT_FILE_DOT" + sol2uml "$FLATTENED_FILE" -o "$OUTPUT_FILE_DOT" -f dot + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to generate UML diagram in DOT format for $FILE" + FAILED_FILES+=("$FILE") + rm "$FLATTENED_FILE" + return + fi + + rm "$FLATTENED_FILE" + set -e +} + +process_all_files_in_directory() { + local SOURCE_DIR=$1 + local TARGET_DIR=$2 + + mkdir -p "$TARGET_DIR" + + find "$SOURCE_DIR" -type f -name '*.sol' | while read -r ITEM; do + flatten_and_generate_uml "$ITEM" "$TARGET_DIR" + done +} + +process_selected_files() { + local SOURCE_DIR=$1 + local TARGET_DIR=$2 + local FILES=(${3//,/ }) # Split the comma-separated list into an array + + mkdir -p "$TARGET_DIR" + + for FILE in "${FILES[@]}"; do + FILE=${FILE//\"/} + MATCHES=($(find "$SOURCE_DIR" -type f -path "*/$FILE")) + + if [[ ${#MATCHES[@]} -gt 1 ]]; then + >&2 echo "Error: Multiple matches found for $FILE:" + for MATCH in "${MATCHES[@]}"; do + >&2 echo " $MATCH" + done + exit 1 + elif [[ ${#MATCHES[@]} -eq 1 ]]; then + >&2 echo "::debug::File found: ${MATCHES[0]}" + flatten_and_generate_uml "${MATCHES[0]}" "$TARGET_DIR" + else + >&2 echo "::error::File $FILE does not exist within the source directory $SOURCE_DIR." + exit 1 + fi + done +} + +# if FILES is empty, process all files in the directory, otherwise process only the selected files +if [[ -z "$FILES" ]]; then + process_all_files_in_directory "$SOURCE_DIR" "$TARGET_DIR" +else + process_selected_files "$SOURCE_DIR" "$TARGET_DIR" "$FILES" +fi + +if [[ "${#FAILED_FILES[@]}" -gt 0 ]]; then + >&2 echo ":error::Failed to generate UML diagrams for ${#FAILED_FILES[@]} files:" + for FILE in "${FAILED_FILES[@]}"; do + >&2 echo " $FILE" + echo "$FILE" >> "$TARGET_DIR/uml_generation_failures.txt" + done +fi + +echo "UML diagrams saved in $TARGET_DIR folder" diff --git a/contracts/scripts/ci/modify_remappings.sh b/contracts/scripts/ci/modify_remappings.sh new file mode 100755 index 00000000000..e64ca369b0c --- /dev/null +++ b/contracts/scripts/ci/modify_remappings.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ "$#" -ne 2 ]; then + >&2 echo "Usage: $0 " + exit 1 +fi + +DIR_PREFIX=$1 +REMAPPINGS_FILE=$2 + +if [ ! -f "$REMAPPINGS_FILE" ]; then + >&2 echo "::error:: Remappings file '$REMAPPINGS_FILE' not found." + exit 1 +fi + +OUTPUT_FILE="remappings_modified.txt" + +while IFS= read -r line; do + if [[ "$line" =~ ^[^=]+= ]]; then + REMAPPED_PATH="${line#*=}" + MODIFIED_LINE="${line%=*}=${DIR_PREFIX}/${REMAPPED_PATH}" + echo "$MODIFIED_LINE" >> "$OUTPUT_FILE" + else + echo "$line" >> "$OUTPUT_FILE" + fi +done < "$REMAPPINGS_FILE" + +echo "Modified remappings have been saved to: $OUTPUT_FILE" diff --git a/contracts/scripts/ci/select_solc_version.sh b/contracts/scripts/ci/select_solc_version.sh new file mode 100755 index 00000000000..3f7d7864ab7 --- /dev/null +++ b/contracts/scripts/ci/select_solc_version.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function check_chainlink_dir() { + local param_dir="chainlink" + current_dir=$(pwd) + + current_base=$(basename "$current_dir") + + if [[ "$current_base" != "$param_dir" ]]; then + >&2 echo "::error::The script must be run from the root of $param_dir directory" + exit 1 + fi +} + +check_chainlink_dir + +FILE="$1" + +if [[ "$#" -lt 1 ]]; then + echo "Detects the Solidity version of a file and selects the appropriate Solc version." + echo "If the version is not installed, it will be installed and selected." + echo "Will prefer to use the version from Foundry profile if it satisfies the version in the file." + echo "Usage: $0 " + exit 1 +fi + +if [[ -z "$FILE" ]]; then + >&2 echo "::error:: File not provided." + exit 1 +fi + +extract_product() { + local path=$1 + + echo "$path" | awk -F'src/[^/]*/' '{print $2}' | cut -d'/' -f1 +} + +extract_pragma() { + local FILE=$1 + + if [[ -f "$FILE" ]]; then + SOLCVER="$(grep --no-filename '^pragma solidity' "$FILE" | cut -d' ' -f3)" + else + >&2 echo ":error::$FILE is not a file or it could not be found. Exiting." + return 1 + fi + SOLCVER="$(echo "$SOLCVER" | sed 's/[^0-9\.^]//g')" + >&2 echo "::debug::Detected Solidity version in pragma: $SOLCVER" + echo "$SOLCVER" +} + +echo "Detecting Solc version for $FILE" + +# Set FOUNDRY_PROFILE to the product name only if it is set; otherwise either already set value will be used or it will be empty +PRODUCT=$(extract_product "$FILE") +if [ -n "$PRODUCT" ]; then + FOUNDRY_PROFILE="$PRODUCT" +fi +SOLC_IN_PROFILE=$(forge config --json --root contracts | jq ".solc") +SOLC_IN_PROFILE=$(echo "$SOLC_IN_PROFILE" | tr -d "'\"") +echo "::debug::Detected Solidity version in profile: $SOLC_IN_PROFILE" + +set +e +SOLCVER=$(extract_pragma "$FILE") + +if [[ $? -ne 0 ]]; then + echo "Error: Failed to extract the Solidity version from $FILE." + return 1 +fi + +set -e + +SOLCVER=$(echo "$SOLCVER" | tr -d "'\"") + +if [[ "$SOLC_IN_PROFILE" != "null" && -n "$SOLCVER" ]]; then + set +e + COMPAT_SOLC_VERSION=$(npx semver "$SOLC_IN_PROFILE" -r "$SOLCVER") + exit_code=$? + set -e + if [[ $exit_code -eq 0 && -n "$COMPAT_SOLC_VERSION" ]]; then + echo "::debug::Version $SOLC_IN_PROFILE satisfies the constraint $SOLCVER" + SOLC_TO_USE="$SOLC_IN_PROFILE" + else + echo "::debug::Version $SOLC_IN_PROFILE does not satisfy the constraint $SOLCVER" + SOLC_TO_USE="$SOLCVER" + fi + elif [[ "$SOLC_IN_PROFILE" != "null" && -z "$SOLCVER" ]]; then + >&2 echo "::error::No version found in the Solidity file. Exiting" + return 1 + elif [[ "$SOLC_IN_PROFILE" == "null" && -n "$SOLCVER" ]]; then + echo "::debug::Using the version from the file: $SOLCVER" + SOLC_TO_USE="$SOLCVER" + else + >&2 echo "::error::No version found in the profile or the Solidity file." + return 1 +fi + +echo "Will use $SOLC_TO_USE" +SOLC_TO_USE=$(echo "$SOLC_TO_USE" | tr -d "'\"") +SOLC_TO_USE="$(echo "$SOLC_TO_USE" | sed 's/[^0-9\.]//g')" + +INSTALLED_VERSIONS=$(solc-select versions) + +if echo "$INSTALLED_VERSIONS" | grep -q "$SOLC_TO_USE"; then + echo "::debug::Version $SOLCVER is already installed." + if echo "$INSTALLED_VERSIONS" | grep "$SOLC_TO_USE" | grep -q "current"; then + echo "::debug::Version $SOLCVER is already selected." + else + echo "::debug::Selecting $SOLC_TO_USE" + solc-select use "$SOLC_TO_USE" + fi +else + echo "::debug::Version $SOLC_TO_USE is not installed." + solc-select install "$SOLC_TO_USE" + solc-select use "$SOLC_TO_USE" +fi diff --git a/contracts/scripts/lcov_prune b/contracts/scripts/lcov_prune new file mode 100755 index 00000000000..0f16715cb2e --- /dev/null +++ b/contracts/scripts/lcov_prune @@ -0,0 +1,77 @@ +#!/bin/bash + +if [ "$#" -ne 3 ]; then + >&2 echo "Usage: $0 " + exit 1 +fi + +set -e + +product_name=$1 +input_coverage_file=$2 +output_coverage_file=$3 + +# src/v0.8/ccip/libraries/Internal.sol +# src/v0.8/ccip/libraries/RateLimiter.sol +# src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol +# src/v0.8/ccip/libraries/MerkleMultiProof.sol +# src/v0.8/ccip/libraries/Pool.sol +# excluded because Foundry doesn't support coverage on library files + +# BurnWithFromMintTokenPool is excluded because Forge doesn't seem to +# register coverage, even though it is 100% covered. +exclusion_list_ccip=( + "src/v0.8/ccip/ocr/OCR2Abstract.sol" + "src/v0.8/ccip/libraries/Internal.sol" + "src/v0.8/ccip/libraries/RateLimiter.sol" + "src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol" + "src/v0.8/ccip/libraries/MerkleMultiProof.sol" + "src/v0.8/ccip/libraries/Pool.sol" + "src/v0.8/ConfirmedOwnerWithProposal.sol" + "src/v0.8/tests/MockV3Aggregator.sol" + "src/v0.8/ccip/applications/CCIPClientExample.sol" + "src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol" +) + +exclusion_list_shared=( + "*/shared/*" +) + +exclusion_list_common=( + "*/$product_name/test/*" + "*/vendor/*" +) + +all_exclusions=() + +case "$product_name" in + "ccip") + all_exclusions+=("${exclusion_list_ccip[@]}") + ;; + "shared") + # No product-specific exclusions for shared + ;; + *) + ;; +esac + +all_exclusions+=("${exclusion_list_common[@]}") + +if [ "$product_name" != "shared" ]; then + all_exclusions+=("${exclusion_list_shared[@]}") +fi + +echo "Excluding the following files for product $product_name:" +for exclusion in "${all_exclusions[@]}"; do + echo "$exclusion" +done + +lcov_command="lcov --remove $input_coverage_file -o $output_coverage_file" + +for exclusion in "${all_exclusions[@]}"; do + lcov_command+=" \"$exclusion\"" +done + +lcov_command+=" --rc lcov_branch_coverage=1" + +eval $lcov_command From 14dabac529833900c8db70ae25536e578822df93 Mon Sep 17 00:00:00 2001 From: Adam Hamrick Date: Fri, 9 Aug 2024 07:49:06 -0400 Subject: [PATCH 03/16] [TT-1421] Adds Dot Graphs to CI Upload (#13946) * Adds dot graphs to CI upload * Fix load imports * Verified graphs properly uploaded * Default save DOT traces * use Seth v1.1.2, render DOT graphs for failed transactions in job summary * try newer action * use newer action * run with debug * use newer action * try one more thing with GIthub_workspace * newer action version * last try with image generation * do not try to render dot graphs anymore * fix imports * remove on-demand failure * trace only reverted * use new method in all smoke tests --------- Co-authored-by: Bartek Tofel --- .github/workflows/integration-tests.yml | 24 +++++++++++------- .gitignore | 3 ++- .../actions/vrf/common/actions.go | 7 +++--- .../docker/test_env/test_env_builder.go | 11 +++++--- integration-tests/go.mod | 2 +- integration-tests/go.sum | 4 +-- integration-tests/load/go.mod | 2 +- integration-tests/load/go.sum | 4 +-- integration-tests/smoke/automation_test.go | 4 +-- integration-tests/smoke/flux_test.go | 5 ++-- integration-tests/smoke/forwarder_ocr_test.go | 4 +-- .../smoke/forwarders_ocr2_test.go | 4 +-- integration-tests/smoke/keeper_test.go | 4 +-- integration-tests/smoke/ocr2_test.go | 4 +-- integration-tests/smoke/ocr_test.go | 5 ++-- integration-tests/smoke/runlog_test.go | 4 +-- integration-tests/smoke/vrf_test.go | 5 ++-- integration-tests/testconfig/default.toml | 6 ++--- .../universal/log_poller/helpers.go | 5 ++-- integration-tests/utils/seth.go | 25 +++++++++++++++++++ 20 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 integration-tests/utils/seth.go diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 76f397f046a..fd5784df8c0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -356,10 +356,11 @@ jobs: cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ inputs.evm-ref || github.sha }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - artifacts_name: ${{ matrix.product.name }}-test-logs + artifacts_name: ${{ matrix.product.name }}-test-artifacts artifacts_location: | ./integration-tests/smoke/logs/ ./integration-tests/smoke/db_dumps/ + ./integration-tests/smoke/seth_artifacts/ /tmp/gotest.log publish_check_name: ${{ matrix.product.name }} token: ${{ secrets.GITHUB_TOKEN }} @@ -395,7 +396,7 @@ jobs: - name: Print failed test summary if: always() - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/show-test-summary@75a9005952a9e905649cfb5a6971fd9429436acd # v2.3.25 + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/show-test-summary@70ccaef155381025e411cf7cd1fa5ef8f668ed75 # v2.3.25 eth-smoke-tests-matrix-log-poller: if: ${{ !(contains(join(github.event.pull_request.labels.*.name, ' '), 'skip-smoke-tests') || github.event_name == 'workflow_dispatch') || inputs.distinct_run_name != '' }} @@ -470,10 +471,11 @@ jobs: cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ inputs.evm-ref || github.sha }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - artifacts_name: ${{ matrix.product.name }}-test-logs + artifacts_name: ${{ matrix.product.name }}-test-artifacts artifacts_location: | ./integration-tests/smoke/logs/ ./integration-tests/smoke/db_dumps/ + ./integration-tests/smoke/seth_artifacts/ /tmp/gotest.log publish_check_name: ${{ matrix.product.name }} token: ${{ secrets.GITHUB_TOKEN }} @@ -509,8 +511,7 @@ jobs: - name: Print failed test summary if: always() - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/show-test-summary@75a9005952a9e905649cfb5a6971fd9429436acd # v2.3.25 - + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/show-test-summary@70ccaef155381025e411cf7cd1fa5ef8f668ed75 # v2.3.25 eth-smoke-tests-matrix: if: ${{ !contains(join(github.event.pull_request.labels.*.name, ' '), 'skip-smoke-tests') }} @@ -708,10 +709,11 @@ jobs: cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ inputs.evm-ref || github.sha }}${{ matrix.product.tag_suffix }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - artifacts_name: ${{ matrix.product.name }}${{ matrix.product.tag_suffix }}-test-logs + artifacts_name: ${{ matrix.product.name }}${{ matrix.product.tag_suffix }}-test-artifacts artifacts_location: | ./integration-tests/smoke/logs/ ./integration-tests/smoke/db_dumps/ + ./integration-tests/smoke/seth_artifacts/ /tmp/gotest.log publish_check_name: ${{ matrix.product.name }} token: ${{ secrets.GITHUB_TOKEN }} @@ -778,7 +780,7 @@ jobs: - name: Print failed test summary if: always() - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/show-test-summary@75a9005952a9e905649cfb5a6971fd9429436acd # v2.3.25 + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/show-test-summary@70ccaef155381025e411cf7cd1fa5ef8f668ed75 # v2.3.25 with: test_directories: ./integration-tests/smoke/ @@ -958,9 +960,11 @@ jobs: cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ steps.get_latest_version.outputs.latest_version }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - artifacts_name: node-migration-test-logs + artifacts_name: node-migration-test-artifacts artifacts_location: | ./integration-tests/migration/logs + ./integration-tests/migration/db_dumps + ./integration-tests/migration/seth_artifacts /tmp/gotest.log publish_check_name: Node Migration Test Results token: ${{ secrets.GITHUB_TOKEN }} @@ -1283,9 +1287,11 @@ jobs: cache_key_id: core-solana-e2e-${{ env.MOD_CACHE_VERSION }} token: ${{ secrets.GITHUB_TOKEN }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - artifacts_name: solana-test-logs + artifacts_name: solana-test-artifacts artifacts_location: | ./integration-tests/smoke/logs + ./integration-tests/smoke/db_dumps + ./integration-tests/smoke/seth_artifacts /tmp/gotest.log QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} diff --git a/.gitignore b/.gitignore index 10636f88d81..07dc8baa13a 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ MacOSX* *report.xml *report.json *.out +dot_graphs/ contracts/yarn.lock @@ -101,7 +102,7 @@ tools/flakeytests/coverage.txt # Runtime test configuration that might contain secrets override*.toml -# Pythin venv +# Python venv .venv/ ocr_soak_report.csv diff --git a/integration-tests/actions/vrf/common/actions.go b/integration-tests/actions/vrf/common/actions.go index 5697a261766..e1bda549e71 100644 --- a/integration-tests/actions/vrf/common/actions.go +++ b/integration-tests/actions/vrf/common/actions.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/smartcontractkit/chainlink/integration-tests/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/go-resty/resty/v2" @@ -19,7 +21,6 @@ import ( ctf_test_env "github.com/smartcontractkit/chainlink-testing-framework/docker/test_env" "github.com/smartcontractkit/chainlink-testing-framework/utils/conversions" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" "github.com/smartcontractkit/chainlink/integration-tests/actions" @@ -367,7 +368,7 @@ func BuildNewCLEnvForVRF(l zerolog.Logger, t *testing.T, envConfig VRFEnvConfig, if err != nil { return nil, nil, fmt.Errorf("%s, err: %w", "error getting first evm network", err) } - sethClient, err := seth_utils.GetChainClient(envConfig.TestConfig, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, envConfig.TestConfig, evmNetwork) if err != nil { return nil, nil, fmt.Errorf("%s, err: %w", "error getting seth client", err) } @@ -403,7 +404,7 @@ func LoadExistingCLEnvForVRF( if err != nil { return nil, nil, err } - sethClient, err := seth_utils.GetChainClient(envConfig.TestConfig, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, envConfig.TestConfig, evmNetwork) if err != nil { return nil, nil, err } diff --git a/integration-tests/docker/test_env/test_env_builder.go b/integration-tests/docker/test_env/test_env_builder.go index fbd4a7e8705..1ab577bf54e 100644 --- a/integration-tests/docker/test_env/test_env_builder.go +++ b/integration-tests/docker/test_env/test_env_builder.go @@ -11,9 +11,10 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "go.uber.org/zap/zapcore" + "github.com/smartcontractkit/seth" + "github.com/smartcontractkit/chainlink-testing-framework/blockchain" ctf_config "github.com/smartcontractkit/chainlink-testing-framework/config" "github.com/smartcontractkit/chainlink-testing-framework/docker/test_env" @@ -21,10 +22,10 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/logstream" "github.com/smartcontractkit/chainlink-testing-framework/networks" "github.com/smartcontractkit/chainlink-testing-framework/testreporters" + "github.com/smartcontractkit/chainlink-testing-framework/testsummary" "github.com/smartcontractkit/chainlink-testing-framework/utils/osutil" - "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" - "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" ) type CleanUpType string @@ -347,6 +348,10 @@ func (b *CLTestEnvBuilder) Build() (*CLClusterTestEnv, error) { } b.l.Info().Msg("Finished dumping state of all Postgres DBs used by Chainlink Nodes") } + + if b.testConfig.GetSethConfig() != nil && ((b.t.Failed() && slices.Contains(b.testConfig.GetSethConfig().TraceOutputs, seth.TraceOutput_DOT) && b.testConfig.GetSethConfig().TracingLevel != seth.TracingLevel_None) || (!b.t.Failed() && slices.Contains(b.testConfig.GetSethConfig().TraceOutputs, seth.TraceOutput_DOT) && b.testConfig.GetSethConfig().TracingLevel == seth.TracingLevel_All)) { + _ = testsummary.AddEntry(b.t.Name(), "dot_graphs", "true") + } }) } else { b.l.Warn().Msg("LogStream won't be cleaned up, because either test instance is not set or cleanup type is set to none") diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 1ebda7f521a..a7783f7daa8 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 github.com/smartcontractkit/havoc/k8schaos v0.0.0-20240409145249-e78d20847e37 github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 - github.com/smartcontractkit/seth v1.1.1 + github.com/smartcontractkit/seth v1.1.2 github.com/smartcontractkit/wasp v0.4.5 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index a0642a0b922..411b3ddd46b 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1514,8 +1514,8 @@ github.com/smartcontractkit/havoc/k8schaos v0.0.0-20240409145249-e78d20847e37 h1 github.com/smartcontractkit/havoc/k8schaos v0.0.0-20240409145249-e78d20847e37/go.mod h1:/kFr0D7SI/vueXl1N03uzOun4nViGPFRyA5X6eL3jXw= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 h1:e38V5FYE7DA1JfKXeD5Buo/7lczALuVXlJ8YNTAUxcw= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7/go.mod h1:fb1ZDVXACvu4frX3APHZaEBp0xi1DIm34DcA0CwTsZM= -github.com/smartcontractkit/seth v1.1.1 h1:6hvexjJD7ek8ht/CLlEwQcH21K2E/WEYwbSRdKInZmM= -github.com/smartcontractkit/seth v1.1.1/go.mod h1:cDfKHi/hJLpO9sRpVbrflrHCOV+MJPAMJHloExJnIXk= +github.com/smartcontractkit/seth v1.1.2 h1:98v9VUFUpNhU7UofeF/bGyUIVv9jnt+JlIE+I8mhX2c= +github.com/smartcontractkit/seth v1.1.2/go.mod h1:cDfKHi/hJLpO9sRpVbrflrHCOV+MJPAMJHloExJnIXk= github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 h1:yiKnypAqP8l0OX0P3klzZ7SCcBUxy5KqTAKZmQOvSQE= github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1/go.mod h1:q6f4fe39oZPdsh1i57WznEZgxd8siidMaSFq3wdPmVg= github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 h1:Dai1bn+Q5cpeGMQwRdjOdVjG8mmFFROVkSKuUgBErRQ= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index bd47a97f48a..4a15b97abfd 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -21,7 +21,7 @@ require ( github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240214231432-4ad5eb95178c github.com/smartcontractkit/chainlink/v2 v2.9.0-beta0.0.20240216210048-da02459ddad8 github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 - github.com/smartcontractkit/seth v1.1.1 + github.com/smartcontractkit/seth v1.1.2 github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 github.com/smartcontractkit/wasp v0.4.7 github.com/stretchr/testify v1.9.0 diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 7789b949449..625da73ba03 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1496,8 +1496,8 @@ github.com/smartcontractkit/havoc/k8schaos v0.0.0-20240409145249-e78d20847e37 h1 github.com/smartcontractkit/havoc/k8schaos v0.0.0-20240409145249-e78d20847e37/go.mod h1:/kFr0D7SI/vueXl1N03uzOun4nViGPFRyA5X6eL3jXw= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 h1:e38V5FYE7DA1JfKXeD5Buo/7lczALuVXlJ8YNTAUxcw= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7/go.mod h1:fb1ZDVXACvu4frX3APHZaEBp0xi1DIm34DcA0CwTsZM= -github.com/smartcontractkit/seth v1.1.1 h1:6hvexjJD7ek8ht/CLlEwQcH21K2E/WEYwbSRdKInZmM= -github.com/smartcontractkit/seth v1.1.1/go.mod h1:cDfKHi/hJLpO9sRpVbrflrHCOV+MJPAMJHloExJnIXk= +github.com/smartcontractkit/seth v1.1.2 h1:98v9VUFUpNhU7UofeF/bGyUIVv9jnt+JlIE+I8mhX2c= +github.com/smartcontractkit/seth v1.1.2/go.mod h1:cDfKHi/hJLpO9sRpVbrflrHCOV+MJPAMJHloExJnIXk= github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 h1:yiKnypAqP8l0OX0P3klzZ7SCcBUxy5KqTAKZmQOvSQE= github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1/go.mod h1:q6f4fe39oZPdsh1i57WznEZgxd8siidMaSFq3wdPmVg= github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 h1:Dai1bn+Q5cpeGMQwRdjOdVjG8mmFFROVkSKuUgBErRQ= diff --git a/integration-tests/smoke/automation_test.go b/integration-tests/smoke/automation_test.go index 92b25dfd522..39a9f754920 100644 --- a/integration-tests/smoke/automation_test.go +++ b/integration-tests/smoke/automation_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + "github.com/smartcontractkit/chainlink/integration-tests/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -1427,7 +1427,7 @@ func setupAutomationTestDocker( evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(automationTestConfig, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, automationTestConfig, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, sethClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(*automationTestConfig.GetCommonConfig().ChainlinkNodeFunding)) diff --git a/integration-tests/smoke/flux_test.go b/integration-tests/smoke/flux_test.go index d8773690b23..d66cdbd2849 100644 --- a/integration-tests/smoke/flux_test.go +++ b/integration-tests/smoke/flux_test.go @@ -8,12 +8,13 @@ import ( "testing" "time" + "github.com/smartcontractkit/chainlink/integration-tests/utils" + "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/logging" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" "github.com/smartcontractkit/chainlink/integration-tests/actions" @@ -49,7 +50,7 @@ func TestFluxBasic(t *testing.T) { evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") adapterUUID := uuid.NewString() diff --git a/integration-tests/smoke/forwarder_ocr_test.go b/integration-tests/smoke/forwarder_ocr_test.go index a249775dc6a..1eff96cb7a2 100644 --- a/integration-tests/smoke/forwarder_ocr_test.go +++ b/integration-tests/smoke/forwarder_ocr_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + "github.com/smartcontractkit/chainlink/integration-tests/utils" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -49,7 +49,7 @@ func TestForwarderOCRBasic(t *testing.T) { evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, sethClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(*config.Common.ChainlinkNodeFunding)) diff --git a/integration-tests/smoke/forwarders_ocr2_test.go b/integration-tests/smoke/forwarders_ocr2_test.go index 863b36e4ede..e3cced94fd7 100644 --- a/integration-tests/smoke/forwarders_ocr2_test.go +++ b/integration-tests/smoke/forwarders_ocr2_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + "github.com/smartcontractkit/chainlink/integration-tests/utils" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -50,7 +50,7 @@ func TestForwarderOCR2Basic(t *testing.T) { evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, sethClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(*config.Common.ChainlinkNodeFunding)) diff --git a/integration-tests/smoke/keeper_test.go b/integration-tests/smoke/keeper_test.go index 4ff1c90bd1e..b6118025a19 100644 --- a/integration-tests/smoke/keeper_test.go +++ b/integration-tests/smoke/keeper_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + "github.com/smartcontractkit/chainlink/integration-tests/utils" "github.com/ethereum/go-ethereum/common" "github.com/onsi/gomega" @@ -1243,7 +1243,7 @@ func setupKeeperTest(l zerolog.Logger, t *testing.T, config *tc.TestConfig) ( evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, sethClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(*config.Common.ChainlinkNodeFunding)) diff --git a/integration-tests/smoke/ocr2_test.go b/integration-tests/smoke/ocr2_test.go index 56a95c50bda..90afff94cf3 100644 --- a/integration-tests/smoke/ocr2_test.go +++ b/integration-tests/smoke/ocr2_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + "github.com/smartcontractkit/chainlink/integration-tests/utils" "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" @@ -163,7 +163,7 @@ func prepareORCv2SmokeTestEnv(t *testing.T, testData ocr2test, l zerolog.Logger, evmNetwork, err := testEnv.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") nodeClients := testEnv.ClCluster.NodeAPIs() diff --git a/integration-tests/smoke/ocr_test.go b/integration-tests/smoke/ocr_test.go index 0b4ac3de30b..8d17a020714 100644 --- a/integration-tests/smoke/ocr_test.go +++ b/integration-tests/smoke/ocr_test.go @@ -5,8 +5,6 @@ import ( "testing" "time" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/smartcontractkit/seth" @@ -19,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/integration-tests/contracts" "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" tc "github.com/smartcontractkit/chainlink/integration-tests/testconfig" + "github.com/smartcontractkit/chainlink/integration-tests/utils" ) const ( @@ -99,7 +98,7 @@ func prepareORCv1SmokeTestEnv(t *testing.T, l zerolog.Logger, firstRoundResult i evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") nodeClients := env.ClCluster.NodeAPIs() diff --git a/integration-tests/smoke/runlog_test.go b/integration-tests/smoke/runlog_test.go index 515d9dea33c..1558b447327 100644 --- a/integration-tests/smoke/runlog_test.go +++ b/integration-tests/smoke/runlog_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + "github.com/smartcontractkit/chainlink/integration-tests/utils" "github.com/google/uuid" "github.com/onsi/gomega" @@ -47,7 +47,7 @@ func TestRunLogBasic(t *testing.T) { evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, sethClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(*config.Common.ChainlinkNodeFunding)) diff --git a/integration-tests/smoke/vrf_test.go b/integration-tests/smoke/vrf_test.go index 04e760796db..53e74ac7ff1 100644 --- a/integration-tests/smoke/vrf_test.go +++ b/integration-tests/smoke/vrf_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/smartcontractkit/chainlink/integration-tests/utils" + "github.com/google/uuid" "github.com/onsi/gomega" "github.com/rs/zerolog" @@ -13,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/logging" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" "github.com/smartcontractkit/chainlink/integration-tests/actions" @@ -204,7 +205,7 @@ func prepareVRFtestEnv(t *testing.T, l zerolog.Logger) (*test_env.CLClusterTestE evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - sethClient, err := seth_utils.GetChainClient(config, *evmNetwork) + sethClient, err := utils.TestAwareSethClient(t, config, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, sethClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(*config.Common.ChainlinkNodeFunding)) diff --git a/integration-tests/testconfig/default.toml b/integration-tests/testconfig/default.toml index 0d0bb14da95..9609c6175d3 100644 --- a/integration-tests/testconfig/default.toml +++ b/integration-tests/testconfig/default.toml @@ -134,13 +134,13 @@ FeeCapDefault = '200 gwei' [Seth] # controls which transactions are decoded/traced. Possbile values are: none, all, reverted (default). # if transaction level doesn't match, then calling Decode() does nothing. It's advised to keep it set -# to 'reverted' to limit noise. If you combine it with 'trace_to_json' it will save all possible data -# in JSON files for reverted transactions. +# to 'reverted' to limit noise. tracing_level = "reverted" -# saves each decoding/tracing results to JSON files; what exactly is saved depends on what we +# saves each decoding/tracing results to DOT files; what exactly is saved depends on what we # were able te decode, we try to save maximum information possible. It can either be: # just tx hash, decoded transaction or call trace. Which transactions traces are saved depends # on 'tracing_level'. +trace_outputs = ["dot", "console"] # number of addresses to be generated and runtime, if set to 0, no addresses will be generated # each generated address will receive a proportion of native tokens from root private key's balance diff --git a/integration-tests/universal/log_poller/helpers.go b/integration-tests/universal/log_poller/helpers.go index daa4784ec16..bacb5db6ed4 100644 --- a/integration-tests/universal/log_poller/helpers.go +++ b/integration-tests/universal/log_poller/helpers.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/smartcontractkit/chainlink/integration-tests/utils" + "github.com/jmoiron/sqlx" "github.com/smartcontractkit/seth" "github.com/smartcontractkit/wasp" @@ -30,7 +32,6 @@ import ( ctf_test_env "github.com/smartcontractkit/chainlink-testing-framework/docker/test_env" "github.com/smartcontractkit/chainlink-testing-framework/logging" "github.com/smartcontractkit/chainlink-testing-framework/networks" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" "github.com/smartcontractkit/chainlink/integration-tests/actions" "github.com/smartcontractkit/chainlink/integration-tests/client" "github.com/smartcontractkit/chainlink/integration-tests/contracts" @@ -1059,7 +1060,7 @@ func SetupLogPollerTestDocker( evmNetwork, err := env.GetFirstEvmNetwork() require.NoError(t, err, "Error getting first evm network") - chainClient, err := seth_utils.GetChainClient(testConfig, *evmNetwork) + chainClient, err := utils.TestAwareSethClient(t, testConfig, evmNetwork) require.NoError(t, err, "Error getting seth client") err = actions.FundChainlinkNodesFromRootAddress(l, chainClient, contracts.ChainlinkClientToChainlinkNodeWithKeysAndAddress(env.ClCluster.NodeAPIs()), big.NewFloat(chainlinkNodeFunding)) diff --git a/integration-tests/utils/seth.go b/integration-tests/utils/seth.go new file mode 100644 index 00000000000..237be1a508d --- /dev/null +++ b/integration-tests/utils/seth.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + "testing" + + pkg_seth "github.com/smartcontractkit/seth" + + "github.com/smartcontractkit/chainlink-testing-framework/blockchain" + ctf_config "github.com/smartcontractkit/chainlink-testing-framework/config" + seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" +) + +// DynamicArtifactDirConfigFn returns a function that sets Seth's artifacts directory to a unique directory for the test +func DynamicArtifactDirConfigFn(t *testing.T) func(*pkg_seth.Config) error { + return func(cfg *pkg_seth.Config) error { + cfg.ArtifactsDir = fmt.Sprintf("seth_artifacts/%s", t.Name()) + return nil + } +} + +// TestAwareSethClient returns a Seth client with the artifacts directory set to a unique directory for the test +func TestAwareSethClient(t *testing.T, sethConfig ctf_config.SethConfig, evmNetwork *blockchain.EVMNetwork) (*pkg_seth.Client, error) { + return seth_utils.GetChainClientWithConfigFunction(sethConfig, *evmNetwork, DynamicArtifactDirConfigFn(t)) +} From 7a561304a4650ee60ee96cf0b601266a1461488d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 9 Aug 2024 21:27:41 +0900 Subject: [PATCH 04/16] core/scripts/keystone improvements (#14075) * scripts: keystone: Use Hostname() over Host Host can contain host:port which will then result in invalid bootstrap addresses (host:port:port) * scripts: keystone: Add command to deploy workflow spec * scripts: keystone: oracle spec used the wrong provider type * scripts: keystone: Add utility to delete a workflow spec * Simplify the changes to scripts a bit * scripts: keystone: Allow overriding PublicKeys.json/NodeList.txt location --- core/scripts/keystone/.gitignore | 1 + core/scripts/keystone/main.go | 2 + .../keystone/src/01_deploy_contracts_cmd.go | 49 ++++++++---- .../keystone/src/02_deploy_jobspecs_cmd.go | 29 ++++++-- .../src/03_gen_crib_cluster_overrides_cmd.go | 29 ++++++-- .../03_gen_crib_cluster_overrides_cmd_test.go | 2 +- .../keystone/src/04_delete_ocr3_jobs_cmd.go | 30 +++++++- .../keystone/src/06_deploy_workflows_cmd.go | 71 ++++++++++++++++++ .../keystone/src/07_delete_workflows_cmd.go | 74 +++++++++++++++++++ core/scripts/keystone/src/88_gen_jobspecs.go | 4 +- .../keystone/src/88_gen_ocr3_config.go | 4 +- .../keystone/src/88_gen_ocr3_config_test.go | 2 +- core/scripts/keystone/src/99_fetch_keys.go | 11 +-- core/scripts/keystone/src/99_files.go | 4 +- .../__snapshots__/88_gen_jobspecs_test.snap | 8 +- core/scripts/keystone/templates/oracle.toml | 2 +- 16 files changed, 269 insertions(+), 53 deletions(-) create mode 100644 core/scripts/keystone/src/06_deploy_workflows_cmd.go create mode 100644 core/scripts/keystone/src/07_delete_workflows_cmd.go diff --git a/core/scripts/keystone/.gitignore b/core/scripts/keystone/.gitignore index 4af4a42a015..92bf9aabc58 100644 --- a/core/scripts/keystone/.gitignore +++ b/core/scripts/keystone/.gitignore @@ -3,3 +3,4 @@ !*-sample.sh keystone .cache/ +artefacts/ diff --git a/core/scripts/keystone/main.go b/core/scripts/keystone/main.go index 571623578ac..3486830ca32 100644 --- a/core/scripts/keystone/main.go +++ b/core/scripts/keystone/main.go @@ -20,6 +20,8 @@ func main() { src.NewGenerateCribClusterOverridesCommand(), src.NewDeleteJobsCommand(), src.NewDeployAndInitializeCapabilitiesRegistryCommand(), + src.NewDeployWorkflowsCommand(), + src.NewDeleteWorkflowsCommand(), } commandsList := func(commands []command) string { diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index 2ca60bdfaf2..b3049737956 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -52,8 +52,12 @@ func (g *deployContracts) Run(args []string) { skipFunding := fs.Bool("skipfunding", false, "skip funding the transmitters") onlySetConfig := fs.Bool("onlysetconfig", false, "set the config on the OCR3 contract without deploying the contracts or funding transmitters") dryRun := fs.Bool("dryrun", false, "dry run, don't actually deploy the contracts and do not fund transmitters") + publicKeys := fs.String("publickeys", "", "Custom public keys json location") + nodeList := fs.String("nodes", "", "Custom node list location") + artefactsDir := fs.String("artefacts", "", "Custom artefacts directory location") err := fs.Parse(args) + if err != nil || *ocrConfigFile == "" || ocrConfigFile == nil || *ethUrl == "" || ethUrl == nil || @@ -63,11 +67,21 @@ func (g *deployContracts) Run(args []string) { os.Exit(1) } + if *artefactsDir == "" { + *artefactsDir = defaultArtefactsDir + } + if *publicKeys == "" { + *publicKeys = defaultPublicKeys + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + os.Setenv("ETH_URL", *ethUrl) os.Setenv("ETH_CHAIN_ID", fmt.Sprintf("%d", *chainID)) os.Setenv("ACCOUNT_KEY", *accountKey) - deploy(*ocrConfigFile, *skipFunding, *dryRun, *onlySetConfig) + deploy(*nodeList, *publicKeys, *ocrConfigFile, *skipFunding, *dryRun, *onlySetConfig, *artefactsDir) } // deploy does the following: @@ -77,16 +91,20 @@ func (g *deployContracts) Run(args []string) { // 4. Writes the deployed contract addresses to a file // 5. Funds the transmitters func deploy( + nodeList string, + publicKeys string, configFile string, skipFunding bool, dryRun bool, onlySetConfig bool, + artefacts string, ) { env := helpers.SetupEnv(false) ocrConfig := generateOCR3Config( + nodeList, configFile, env.ChainID, - ".cache/PublicKeys.json", + publicKeys, ) if dryRun { @@ -96,11 +114,11 @@ func deploy( if onlySetConfig { fmt.Println("Skipping deployment of contracts and skipping funding transmitters, only setting config") - setOCR3Config(env, ocrConfig) + setOCR3Config(env, ocrConfig, artefacts) return } - if ContractsAlreadyDeployed() { + if ContractsAlreadyDeployed(artefacts) { fmt.Println("Contracts already deployed") return } @@ -118,10 +136,10 @@ func deploy( jsonBytes, err := json.Marshal(contracts) PanicErr(err) - err = os.WriteFile(DeployedContractsFilePath(), jsonBytes, 0600) + err = os.WriteFile(DeployedContractsFilePath(artefacts), jsonBytes, 0600) PanicErr(err) - setOCR3Config(env, ocrConfig) + setOCR3Config(env, ocrConfig, artefacts) if skipFunding { fmt.Println("Skipping funding transmitters") @@ -139,8 +157,9 @@ func deploy( func setOCR3Config( env helpers.Environment, ocrConfig orc2drOracleConfig, + artefacts string, ) { - loadedContracts, err := LoadDeployedContracts() + loadedContracts, err := LoadDeployedContracts(artefacts) PanicErr(err) ocrContract, err := ocr3_capability.NewOCR3Capability(loadedContracts.OCRContract, env.Ec) @@ -161,16 +180,16 @@ func setOCR3Config( loadedContracts.SetConfigTxBlock = receipt.BlockNumber.Uint64() jsonBytes, err := json.Marshal(loadedContracts) PanicErr(err) - err = os.WriteFile(DeployedContractsFilePath(), jsonBytes, 0600) + err = os.WriteFile(DeployedContractsFilePath(artefacts), jsonBytes, 0600) PanicErr(err) } -func LoadDeployedContracts() (deployedContracts, error) { - if !ContractsAlreadyDeployed() { +func LoadDeployedContracts(artefacts string) (deployedContracts, error) { + if !ContractsAlreadyDeployed(artefacts) { return deployedContracts{}, fmt.Errorf("no deployed contracts found, run deploy first") } - jsonBytes, err := os.ReadFile(DeployedContractsFilePath()) + jsonBytes, err := os.ReadFile(DeployedContractsFilePath(artefacts)) if err != nil { return deployedContracts{}, err } @@ -180,13 +199,13 @@ func LoadDeployedContracts() (deployedContracts, error) { return contracts, err } -func ContractsAlreadyDeployed() bool { - _, err := os.Stat(DeployedContractsFilePath()) +func ContractsAlreadyDeployed(artefacts string) bool { + _, err := os.Stat(DeployedContractsFilePath(artefacts)) return err == nil } -func DeployedContractsFilePath() string { - return filepath.Join(artefactsDir, deployedContractsJSON) +func DeployedContractsFilePath(artefacts string) string { + return filepath.Join(artefacts, deployedContractsJSON) } func DeployForwarder(e helpers.Environment) *forwarder.KeystoneForwarder { diff --git a/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go b/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go index 5918650cf88..275943d6388 100644 --- a/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go +++ b/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go @@ -16,8 +16,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/cmd" ) -type deployJobSpecs struct { -} +type deployJobSpecs struct{} func NewDeployJobSpecsCommand() *deployJobSpecs { return &deployJobSpecs{} @@ -32,6 +31,11 @@ func (g *deployJobSpecs) Run(args []string) { chainID := fs.Int64("chainid", 11155111, "chain id") p2pPort := fs.Int64("p2pport", 6690, "p2p port") onlyReplay := fs.Bool("onlyreplay", false, "only replay the block from the OCR3 contract setConfig transaction") + templatesLocation := fs.String("templates", "", "Custom templates location") + nodeList := fs.String("nodes", "", "Custom node list location") + publicKeys := fs.String("publickeys", "", "Custom public keys json location") + artefactsDir := fs.String("artefacts", "", "Custom artefacts directory location") + err := fs.Parse(args) if err != nil || chainID == nil || *chainID == 0 || p2pPort == nil || *p2pPort == 0 || onlyReplay == nil { fs.Usage() @@ -43,12 +47,27 @@ func (g *deployJobSpecs) Run(args []string) { fmt.Println("Deploying OCR3 job specs") } - nodes := downloadNodeAPICredentialsDefault() - deployedContracts, err := LoadDeployedContracts() + if *artefactsDir == "" { + *artefactsDir = defaultArtefactsDir + } + if *publicKeys == "" { + *publicKeys = defaultPublicKeys + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + if *templatesLocation == "" { + *templatesLocation = "templates" + } + + nodes := downloadNodeAPICredentials(*nodeList) + deployedContracts, err := LoadDeployedContracts(*artefactsDir) PanicErr(err) jobspecs := genSpecs( - ".cache/PublicKeys.json", ".cache/NodeList.txt", "templates", + *publicKeys, + *nodeList, + *templatesLocation, *chainID, *p2pPort, deployedContracts.OCRContract.Hex(), ) flattenedSpecs := []hostSpec{jobspecs.bootstrap} diff --git a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go index cb3acf903b3..6b98951459e 100644 --- a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go +++ b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go @@ -9,8 +9,7 @@ import ( helpers "github.com/smartcontractkit/chainlink/core/scripts/common" ) -type generateCribClusterOverrides struct { -} +type generateCribClusterOverrides struct{} func NewGenerateCribClusterOverridesCommand() *generateCribClusterOverrides { return &generateCribClusterOverrides{} @@ -24,25 +23,39 @@ func (g *generateCribClusterOverrides) Run(args []string) { fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) chainID := fs.Int64("chainid", 11155111, "chain id") outputPath := fs.String("outpath", "../crib", "the path to output the generated overrides") + publicKeys := fs.String("publickeys", "", "Custom public keys json location") + nodeList := fs.String("nodes", "", "Custom node list location") + artefactsDir := fs.String("artefacts", "", "Custom artefacts directory location") - deployedContracts, err := LoadDeployedContracts() - helpers.PanicErr(err) templatesDir := "templates" - err = fs.Parse(args) + err := fs.Parse(args) if err != nil || outputPath == nil || *outputPath == "" || chainID == nil || *chainID == 0 { fs.Usage() os.Exit(1) } - lines := generateCribConfig(".cache/PublicKeys.json", chainID, templatesDir, deployedContracts.ForwarderContract.Hex()) + if *artefactsDir == "" { + *artefactsDir = defaultArtefactsDir + } + if *publicKeys == "" { + *publicKeys = defaultPublicKeys + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + + deployedContracts, err := LoadDeployedContracts(*artefactsDir) + helpers.PanicErr(err) + + lines := generateCribConfig(*nodeList, *publicKeys, chainID, templatesDir, deployedContracts.ForwarderContract.Hex()) cribOverridesStr := strings.Join(lines, "\n") err = os.WriteFile(filepath.Join(*outputPath, "crib-cluster-overrides.yaml"), []byte(cribOverridesStr), 0600) helpers.PanicErr(err) } -func generateCribConfig(pubKeysPath string, chainID *int64, templatesDir string, forwarderAddress string) []string { - nca := downloadNodePubKeys(*chainID, pubKeysPath) +func generateCribConfig(nodeList string, pubKeysPath string, chainID *int64, templatesDir string, forwarderAddress string) []string { + nca := downloadNodePubKeys(nodeList, *chainID, pubKeysPath) nodeAddresses := []string{} for _, node := range nca[1:] { diff --git a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go index 722b01e91c3..53d43c2342f 100644 --- a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go +++ b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go @@ -13,7 +13,7 @@ func TestGenerateCribConfig(t *testing.T) { forwarderAddress := "0x1234567890abcdef" publicKeysPath := "./testdata/PublicKeys.json" - lines := generateCribConfig(publicKeysPath, &chainID, templatesDir, forwarderAddress) + lines := generateCribConfig(defaultNodeList, publicKeysPath, &chainID, templatesDir, forwarderAddress) snaps.MatchSnapshot(t, strings.Join(lines, "\n")) } diff --git a/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go b/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go index 2ebed000ed4..136691962dd 100644 --- a/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go +++ b/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go @@ -5,14 +5,14 @@ import ( "encoding/json" "flag" "fmt" + "os" "github.com/urfave/cli" helpers "github.com/smartcontractkit/chainlink/core/scripts/common" ) -type deleteJobs struct { -} +type deleteJobs struct{} type OCRSpec struct { ContractID string @@ -22,11 +22,16 @@ type BootSpec struct { ContractID string } +type WorkflowSpec struct { + WorkflowID string +} + type JobSpec struct { Id string Name string BootstrapSpec BootSpec OffChainReporting2OracleSpec OCRSpec + WorkflowSpec WorkflowSpec } func NewDeleteJobsCommand() *deleteJobs { @@ -38,9 +43,26 @@ func (g *deleteJobs) Name() string { } func (g *deleteJobs) Run(args []string) { - deployedContracts, err := LoadDeployedContracts() + fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) + nodeList := fs.String("nodes", "", "Custom node list location") + artefactsDir := fs.String("artefacts", "", "Custom artefacts directory location") + + err := fs.Parse(args) + if err != nil { + fs.Usage() + os.Exit(1) + } + + if *artefactsDir == "" { + *artefactsDir = defaultArtefactsDir + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + + deployedContracts, err := LoadDeployedContracts(*artefactsDir) helpers.PanicErr(err) - nodes := downloadNodeAPICredentialsDefault() + nodes := downloadNodeAPICredentials(*nodeList) for _, node := range nodes { output := &bytes.Buffer{} diff --git a/core/scripts/keystone/src/06_deploy_workflows_cmd.go b/core/scripts/keystone/src/06_deploy_workflows_cmd.go new file mode 100644 index 00000000000..0ca8e5d4a7b --- /dev/null +++ b/core/scripts/keystone/src/06_deploy_workflows_cmd.go @@ -0,0 +1,71 @@ +package src + +import ( + "bytes" + "errors" + "flag" + "fmt" + "os" + + "github.com/urfave/cli" + + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" +) + +type deployWorkflows struct{} + +func NewDeployWorkflowsCommand() *deployWorkflows { + return &deployWorkflows{} +} + +func (g *deployWorkflows) Name() string { + return "deploy-workflows" +} + +func (g *deployWorkflows) Run(args []string) { + fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) + workflowFile := fs.String("workflow", "workflow.yml", "path to workflow file") + nodeList := fs.String("nodes", "", "Custom node list location") + err := fs.Parse(args) + if err != nil || workflowFile == nil || *workflowFile == "" { + fs.Usage() + os.Exit(1) + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + fmt.Println("Deploying workflows") + + // use a separate list + nodes := downloadNodeAPICredentials(*nodeList) + + if _, err = os.Stat(*workflowFile); err != nil { + PanicErr(errors.New("toml file does not exist")) + } + + for i, n := range nodes { + if i == 0 { + continue // skip bootstrap node + } + output := &bytes.Buffer{} + client, app := newApp(n, output) + fmt.Println("Logging in:", n.url) + loginFs := flag.NewFlagSet("test", flag.ContinueOnError) + loginFs.Bool("bypass-version-check", true, "") + loginCtx := cli.NewContext(app, loginFs, nil) + err := client.RemoteLogin(loginCtx) + helpers.PanicErr(err) + output.Reset() + + fmt.Printf("Deploying workflow\n... \n") + fs := flag.NewFlagSet("test", flag.ExitOnError) + err = fs.Parse([]string{*workflowFile}) + + helpers.PanicErr(err) + err = client.CreateJob(cli.NewContext(app, fs, nil)) + if err != nil { + fmt.Println("Failed to deploy workflow:", "Error:", err) + } + output.Reset() + } +} diff --git a/core/scripts/keystone/src/07_delete_workflows_cmd.go b/core/scripts/keystone/src/07_delete_workflows_cmd.go new file mode 100644 index 00000000000..cccedaf9e70 --- /dev/null +++ b/core/scripts/keystone/src/07_delete_workflows_cmd.go @@ -0,0 +1,74 @@ +package src + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/urfave/cli" + + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" +) + +type deleteWorkflows struct{} + +func NewDeleteWorkflowsCommand() *deleteWorkflows { + return &deleteWorkflows{} +} + +func (g *deleteWorkflows) Name() string { + return "delete-workflows" +} + +func (g *deleteWorkflows) Run(args []string) { + fs := flag.NewFlagSet(g.Name(), flag.ExitOnError) + nodeList := fs.String("nodes", "", "Custom node list location") + + err := fs.Parse(args) + if err != nil { + fs.Usage() + os.Exit(1) + } + + if *nodeList == "" { + *nodeList = defaultNodeList + } + + nodes := downloadNodeAPICredentials(*nodeList) + + for _, node := range nodes { + output := &bytes.Buffer{} + client, app := newApp(node, output) + + fmt.Println("Logging in:", node.url) + loginFs := flag.NewFlagSet("test", flag.ContinueOnError) + loginFs.Bool("bypass-version-check", true, "") + loginCtx := cli.NewContext(app, loginFs, nil) + err := client.RemoteLogin(loginCtx) + helpers.PanicErr(err) + output.Reset() + + fileFs := flag.NewFlagSet("test", flag.ExitOnError) + err = client.ListJobs(cli.NewContext(app, fileFs, nil)) + helpers.PanicErr(err) + + var parsed []JobSpec + err = json.Unmarshal(output.Bytes(), &parsed) + helpers.PanicErr(err) + + for _, jobSpec := range parsed { + if jobSpec.WorkflowSpec.WorkflowID != "" { + fmt.Println("Deleting workflow job ID:", jobSpec.Id, "name:", jobSpec.Name) + set := flag.NewFlagSet("test", flag.ExitOnError) + err = set.Parse([]string{jobSpec.Id}) + helpers.PanicErr(err) + err = client.DeleteJob(cli.NewContext(app, set, nil)) + helpers.PanicErr(err) + } + } + + output.Reset() + } +} diff --git a/core/scripts/keystone/src/88_gen_jobspecs.go b/core/scripts/keystone/src/88_gen_jobspecs.go index 6a9c911a5f5..5f0b9097d2f 100644 --- a/core/scripts/keystone/src/88_gen_jobspecs.go +++ b/core/scripts/keystone/src/88_gen_jobspecs.go @@ -34,12 +34,12 @@ func genSpecs( ocrConfigContractAddress string, ) donHostSpec { nodes := downloadNodeAPICredentials(nodeListPath) - nca := downloadNodePubKeys(chainID, pubkeysPath) + nca := downloadNodePubKeys(nodeListPath, chainID, pubkeysPath) bootstrapNode := nca[0] bootstrapSpecLines, err := readLines(filepath.Join(templatesDir, bootstrapSpecTemplate)) helpers.PanicErr(err) - bootHost := nodes[0].url.Host + bootHost := nodes[0].url.Hostname() bootstrapSpecLines = replacePlaceholders( bootstrapSpecLines, chainID, p2pPort, diff --git a/core/scripts/keystone/src/88_gen_ocr3_config.go b/core/scripts/keystone/src/88_gen_ocr3_config.go index fe9241a2bde..1107df57ca1 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config.go @@ -96,10 +96,10 @@ func mustReadConfig(fileName string) (output TopLevelConfigSource) { return mustParseJSON[TopLevelConfigSource](fileName) } -func generateOCR3Config(configFile string, chainID int64, pubKeysPath string) orc2drOracleConfig { +func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKeysPath string) orc2drOracleConfig { topLevelCfg := mustReadConfig(configFile) cfg := topLevelCfg.OracleConfig - nca := downloadNodePubKeys(chainID, pubKeysPath) + nca := downloadNodePubKeys(nodeList, chainID, pubKeysPath) onchainPubKeys := []common.Address{} for _, n := range nca { diff --git a/core/scripts/keystone/src/88_gen_ocr3_config_test.go b/core/scripts/keystone/src/88_gen_ocr3_config_test.go index 185354ec2fe..10cdc07b204 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config_test.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config_test.go @@ -10,7 +10,7 @@ import ( func TestGenerateOCR3Config(t *testing.T) { // Generate OCR3 config - config := generateOCR3Config("./testdata/SampleConfig.json", 11155111, "./testdata/PublicKeys.json") + config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 11155111, "./testdata/PublicKeys.json") matchOffchainConfig := match.Custom("OffchainConfig", func(s any) (any, error) { // coerce the value to a string diff --git a/core/scripts/keystone/src/99_fetch_keys.go b/core/scripts/keystone/src/99_fetch_keys.go index 4fcb6f138a7..b115a7bb94d 100644 --- a/core/scripts/keystone/src/99_fetch_keys.go +++ b/core/scripts/keystone/src/99_fetch_keys.go @@ -17,14 +17,14 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/web/presenters" ) -func downloadNodePubKeys(chainID int64, pubKeysPath string) []NodeKeys { +func downloadNodePubKeys(nodeList string, chainID int64, pubKeysPath string) []NodeKeys { // Check if file exists already, and if so, return the keys if _, err := os.Stat(pubKeysPath); err == nil { fmt.Println("Loading existing public keys at:", pubKeysPath) return mustParseJSON[[]NodeKeys](pubKeysPath) } - nodes := downloadNodeAPICredentialsDefault() + nodes := downloadNodeAPICredentials(nodeList) nodesKeys := mustFetchNodesKeys(chainID, nodes) marshalledNodeKeys, err := json.MarshalIndent(nodesKeys, "", " ") @@ -40,13 +40,6 @@ func downloadNodePubKeys(chainID int64, pubKeysPath string) []NodeKeys { return nodesKeys } -// downloadNodeAPICredentialsDefault downloads the node API credentials, or loads them from disk if they already exist -// -// The nodes are sorted by URL. In the case of crib, the bootstrap node is the first node in the list. -func downloadNodeAPICredentialsDefault() []*node { - return downloadNodeAPICredentials(".cache/NodeList.txt") -} - // downloadNodeAPICredentials downloads the node API credentials, or loads them from disk if they already exist // // The nodes are sorted by URL. In the case of crib, the bootstrap node is the first node in the list. diff --git a/core/scripts/keystone/src/99_files.go b/core/scripts/keystone/src/99_files.go index d334b0fd56c..08ba12e4194 100644 --- a/core/scripts/keystone/src/99_files.go +++ b/core/scripts/keystone/src/99_files.go @@ -11,7 +11,9 @@ import ( ) const ( - artefactsDir = "artefacts" + defaultArtefactsDir = "artefacts" + defaultPublicKeys = ".cache/PublicKeys.json" + defaultNodeList = ".cache/NodeList.txt" deployedContractsJSON = "deployed_contracts.json" bootstrapSpecTemplate = "bootstrap.toml" cribOverrideTemplate = "crib-overrides.yaml" diff --git a/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap b/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap index 1ee7f678945..21e28f3801c 100755 --- a/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap +++ b/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap @@ -33,7 +33,7 @@ chainID = "11155111" command = "chainlink-ocr3-capability" ocrVersion = 3 pluginName = "ocr-capability" -providerType = "plugin" +providerType = "ocr3-capability" telemetryType = "plugin" [onchainSigningStrategy] @@ -63,7 +63,7 @@ chainID = "11155111" command = "chainlink-ocr3-capability" ocrVersion = 3 pluginName = "ocr-capability" -providerType = "plugin" +providerType = "ocr3-capability" telemetryType = "plugin" [onchainSigningStrategy] @@ -93,7 +93,7 @@ chainID = "11155111" command = "chainlink-ocr3-capability" ocrVersion = 3 pluginName = "ocr-capability" -providerType = "plugin" +providerType = "ocr3-capability" telemetryType = "plugin" [onchainSigningStrategy] @@ -123,7 +123,7 @@ chainID = "11155111" command = "chainlink-ocr3-capability" ocrVersion = 3 pluginName = "ocr-capability" -providerType = "plugin" +providerType = "ocr3-capability" telemetryType = "plugin" [onchainSigningStrategy] diff --git a/core/scripts/keystone/templates/oracle.toml b/core/scripts/keystone/templates/oracle.toml index f2ff87de923..f5d539873fe 100644 --- a/core/scripts/keystone/templates/oracle.toml +++ b/core/scripts/keystone/templates/oracle.toml @@ -17,7 +17,7 @@ chainID = "{{ chain_id }}" command = "chainlink-ocr3-capability" ocrVersion = 3 pluginName = "ocr-capability" -providerType = "plugin" +providerType = "ocr3-capability" telemetryType = "plugin" [onchainSigningStrategy] From cefbb09797249309ac18e4ef81147e30f7c24360 Mon Sep 17 00:00:00 2001 From: Christopher Dimitri Sastropranoto Date: Fri, 9 Aug 2024 19:33:37 +0700 Subject: [PATCH 05/16] KS-391: Capabilities registry reentrancy fix (#13970) * prevent malicious a node operator from taking over another node belonging to another node operator * prevent malicious node operator from becoming the admin for another node operator * prevent reentrancy when setting DON config * update wrappers and add changeset * fix solhint --- .changeset/weak-rabbits-sell.md | 5 ++ contracts/.changeset/tender-comics-check.md | 5 ++ .../v0.8/keystone/CapabilitiesRegistry.sol | 9 ++- .../CapabilitiesRegistry_AddDONTest.t.sol | 73 +++++++++++++++++++ .../mocks/MaliciousConfigurationContract.sol | 47 ++++++++++++ .../capabilities_registry.go | 2 +- ...rapper-dependency-versions-do-not-edit.txt | 2 +- 7 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 .changeset/weak-rabbits-sell.md create mode 100644 contracts/.changeset/tender-comics-check.md create mode 100644 contracts/src/v0.8/keystone/test/mocks/MaliciousConfigurationContract.sol diff --git a/.changeset/weak-rabbits-sell.md b/.changeset/weak-rabbits-sell.md new file mode 100644 index 00000000000..3f0785d3d5e --- /dev/null +++ b/.changeset/weak-rabbits-sell.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal prevent reentrancy when configuring DON in Capabilities Registry diff --git a/contracts/.changeset/tender-comics-check.md b/contracts/.changeset/tender-comics-check.md new file mode 100644 index 00000000000..6ea48d92e4e --- /dev/null +++ b/contracts/.changeset/tender-comics-check.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +#internal prevent reentrancy when configuring DON in capabilities registry diff --git a/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol b/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol index ba61584d0ab..ad6f26e8dc9 100644 --- a/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol +++ b/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol @@ -961,6 +961,11 @@ contract CapabilitiesRegistry is OwnerIsCreator, TypeAndVersionInterface { donCapabilityConfig.capabilityIds.push(configuration.capabilityId); donCapabilityConfig.capabilityConfigs[configuration.capabilityId] = configuration.config; + s_dons[donParams.id].isPublic = donParams.isPublic; + s_dons[donParams.id].acceptsWorkflows = donParams.acceptsWorkflows; + s_dons[donParams.id].f = donParams.f; + s_dons[donParams.id].configCount = donParams.configCount; + _setDONCapabilityConfig( donParams.id, donParams.configCount, @@ -969,10 +974,6 @@ contract CapabilitiesRegistry is OwnerIsCreator, TypeAndVersionInterface { configuration.config ); } - s_dons[donParams.id].isPublic = donParams.isPublic; - s_dons[donParams.id].acceptsWorkflows = donParams.acceptsWorkflows; - s_dons[donParams.id].f = donParams.f; - s_dons[donParams.id].configCount = donParams.configCount; emit ConfigSet(donParams.id, donParams.configCount); } diff --git a/contracts/src/v0.8/keystone/test/CapabilitiesRegistry_AddDONTest.t.sol b/contracts/src/v0.8/keystone/test/CapabilitiesRegistry_AddDONTest.t.sol index 65c85e4f74c..dc0b85bfa3f 100644 --- a/contracts/src/v0.8/keystone/test/CapabilitiesRegistry_AddDONTest.t.sol +++ b/contracts/src/v0.8/keystone/test/CapabilitiesRegistry_AddDONTest.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import {BaseTest} from "./BaseTest.t.sol"; import {ICapabilityConfiguration} from "../interfaces/ICapabilityConfiguration.sol"; import {CapabilitiesRegistry} from "../CapabilitiesRegistry.sol"; +import {MaliciousConfigurationContract} from "./mocks/MaliciousConfigurationContract.sol"; contract CapabilitiesRegistry_AddDONTest is BaseTest { function setUp() public override { @@ -245,3 +246,75 @@ contract CapabilitiesRegistry_AddDONTest is BaseTest { assertEq(donInfo.nodeP2PIds[1], P2P_ID_THREE); } } + +contract CapabilitiesRegistry_AddDONTest_WhenMaliciousCapabilityConfigurationConfigured is BaseTest { + function setUp() public override { + BaseTest.setUp(); + CapabilitiesRegistry.Capability[] memory capabilities = new CapabilitiesRegistry.Capability[](2); + + address maliciousConfigContractAddr = address( + new MaliciousConfigurationContract(s_capabilityWithConfigurationContractId) + ); + s_basicCapability.configurationContract = maliciousConfigContractAddr; + capabilities[0] = s_basicCapability; + capabilities[1] = s_capabilityWithConfigurationContract; + + CapabilitiesRegistry.NodeOperator[] memory nodeOperators = _getNodeOperators(); + nodeOperators[0].admin = maliciousConfigContractAddr; + nodeOperators[1].admin = maliciousConfigContractAddr; + nodeOperators[2].admin = maliciousConfigContractAddr; + + s_CapabilitiesRegistry.addNodeOperators(nodeOperators); + s_CapabilitiesRegistry.addCapabilities(capabilities); + + CapabilitiesRegistry.NodeParams[] memory nodes = new CapabilitiesRegistry.NodeParams[](3); + bytes32[] memory capabilityIds = new bytes32[](1); + capabilityIds[0] = s_basicHashedCapabilityId; + + nodes[0] = CapabilitiesRegistry.NodeParams({ + nodeOperatorId: TEST_NODE_OPERATOR_ONE_ID, + p2pId: P2P_ID, + signer: NODE_OPERATOR_ONE_SIGNER_ADDRESS, + hashedCapabilityIds: capabilityIds + }); + + bytes32[] memory nodeTwoCapabilityIds = new bytes32[](1); + nodeTwoCapabilityIds[0] = s_basicHashedCapabilityId; + + nodes[1] = CapabilitiesRegistry.NodeParams({ + nodeOperatorId: TEST_NODE_OPERATOR_TWO_ID, + p2pId: P2P_ID_TWO, + signer: NODE_OPERATOR_TWO_SIGNER_ADDRESS, + hashedCapabilityIds: nodeTwoCapabilityIds + }); + + nodes[2] = CapabilitiesRegistry.NodeParams({ + nodeOperatorId: TEST_NODE_OPERATOR_THREE_ID, + p2pId: P2P_ID_THREE, + signer: NODE_OPERATOR_THREE_SIGNER_ADDRESS, + hashedCapabilityIds: capabilityIds + }); + + s_CapabilitiesRegistry.addNodes(nodes); + + changePrank(ADMIN); + } + + function test_RevertWhen_MaliciousCapabilitiesConfigContractTriesToRemoveCapabilitiesFromDONNodes() public { + bytes32[] memory nodes = new bytes32[](2); + nodes[0] = P2P_ID; + nodes[1] = P2P_ID_THREE; + + CapabilitiesRegistry.CapabilityConfiguration[] + memory capabilityConfigs = new CapabilitiesRegistry.CapabilityConfiguration[](1); + capabilityConfigs[0] = CapabilitiesRegistry.CapabilityConfiguration({ + capabilityId: s_basicHashedCapabilityId, + config: BASIC_CAPABILITY_CONFIG + }); + + vm.expectRevert( + abi.encodeWithSelector(CapabilitiesRegistry.CapabilityRequiredByDON.selector, s_basicHashedCapabilityId, DON_ID) + ); + s_CapabilitiesRegistry.addDON(nodes, capabilityConfigs, true, true, F_VALUE); + } +} diff --git a/contracts/src/v0.8/keystone/test/mocks/MaliciousConfigurationContract.sol b/contracts/src/v0.8/keystone/test/mocks/MaliciousConfigurationContract.sol new file mode 100644 index 00000000000..72c2e23efeb --- /dev/null +++ b/contracts/src/v0.8/keystone/test/mocks/MaliciousConfigurationContract.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ICapabilityConfiguration} from "../../interfaces/ICapabilityConfiguration.sol"; +import {CapabilitiesRegistry} from "../../CapabilitiesRegistry.sol"; +import {ERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165.sol"; +import {Constants} from "../Constants.t.sol"; + +contract MaliciousConfigurationContract is ICapabilityConfiguration, ERC165, Constants { + bytes32 internal s_capabilityWithConfigurationContractId; + + constructor(bytes32 capabilityWithConfigContractId) { + s_capabilityWithConfigurationContractId = capabilityWithConfigContractId; + } + + function getCapabilityConfiguration(uint32) external view returns (bytes memory configuration) { + return bytes(""); + } + + function beforeCapabilityConfigSet(bytes32[] calldata, bytes calldata, uint64, uint32) external { + CapabilitiesRegistry.NodeParams[] memory nodes = new CapabilitiesRegistry.NodeParams[](2); + bytes32[] memory hashedCapabilityIds = new bytes32[](1); + + hashedCapabilityIds[0] = s_capabilityWithConfigurationContractId; + + // Set node one's signer to another address + nodes[0] = CapabilitiesRegistry.NodeParams({ + nodeOperatorId: TEST_NODE_OPERATOR_ONE_ID, + p2pId: P2P_ID, + signer: NODE_OPERATOR_ONE_SIGNER_ADDRESS, + hashedCapabilityIds: hashedCapabilityIds + }); + + nodes[1] = CapabilitiesRegistry.NodeParams({ + nodeOperatorId: TEST_NODE_OPERATOR_ONE_ID, + p2pId: P2P_ID_THREE, + signer: NODE_OPERATOR_THREE_SIGNER_ADDRESS, + hashedCapabilityIds: hashedCapabilityIds + }); + + CapabilitiesRegistry(msg.sender).updateNodes(nodes); + } + + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == this.getCapabilityConfiguration.selector ^ this.beforeCapabilityConfigSet.selector; + } +} diff --git a/core/gethwrappers/keystone/generated/capabilities_registry/capabilities_registry.go b/core/gethwrappers/keystone/generated/capabilities_registry/capabilities_registry.go index c345a86569f..2cfbe120649 100644 --- a/core/gethwrappers/keystone/generated/capabilities_registry/capabilities_registry.go +++ b/core/gethwrappers/keystone/generated/capabilities_registry/capabilities_registry.go @@ -87,7 +87,7 @@ type CapabilitiesRegistryNodeParams struct { var CapabilitiesRegistryMetaData = &bind.MetaData{ ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"AccessForbidden\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"}],\"name\":\"CapabilityAlreadyExists\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"}],\"name\":\"CapabilityDoesNotExist\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"}],\"name\":\"CapabilityIsDeprecated\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"}],\"name\":\"CapabilityRequiredByDON\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"}],\"name\":\"DONDoesNotExist\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"}],\"name\":\"DuplicateDONCapability\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"nodeP2PId\",\"type\":\"bytes32\"}],\"name\":\"DuplicateDONNode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"proposedConfigurationContract\",\"type\":\"address\"}],\"name\":\"InvalidCapabilityConfigurationContractInterface\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"nodeCount\",\"type\":\"uint256\"}],\"name\":\"InvalidFaultTolerance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32[]\",\"name\":\"hashedCapabilityIds\",\"type\":\"bytes32[]\"}],\"name\":\"InvalidNodeCapabilities\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidNodeOperatorAdmin\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"}],\"name\":\"InvalidNodeP2PId\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidNodeSigner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"lengthOne\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lengthTwo\",\"type\":\"uint256\"}],\"name\":\"LengthMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"nodeP2PId\",\"type\":\"bytes32\"}],\"name\":\"NodeAlreadyExists\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"nodeP2PId\",\"type\":\"bytes32\"}],\"name\":\"NodeDoesNotExist\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"nodeP2PId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"}],\"name\":\"NodeDoesNotSupportCapability\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"}],\"name\":\"NodeOperatorDoesNotExist\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"nodeP2PId\",\"type\":\"bytes32\"}],\"name\":\"NodePartOfCapabilitiesDON\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"nodeP2PId\",\"type\":\"bytes32\"}],\"name\":\"NodePartOfWorkflowDON\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"}],\"name\":\"CapabilityConfigured\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"}],\"name\":\"CapabilityDeprecated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"configCount\",\"type\":\"uint32\"}],\"name\":\"ConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"signer\",\"type\":\"bytes32\"}],\"name\":\"NodeAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"}],\"name\":\"NodeOperatorAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"}],\"name\":\"NodeOperatorRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"}],\"name\":\"NodeOperatorUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"}],\"name\":\"NodeRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"signer\",\"type\":\"bytes32\"}],\"name\":\"NodeUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"labelledName\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"},{\"internalType\":\"enumCapabilitiesRegistry.CapabilityType\",\"name\":\"capabilityType\",\"type\":\"uint8\"},{\"internalType\":\"enumCapabilitiesRegistry.CapabilityResponseType\",\"name\":\"responseType\",\"type\":\"uint8\"},{\"internalType\":\"address\",\"name\":\"configurationContract\",\"type\":\"address\"}],\"internalType\":\"structCapabilitiesRegistry.Capability[]\",\"name\":\"capabilities\",\"type\":\"tuple[]\"}],\"name\":\"addCapabilities\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32[]\",\"name\":\"nodes\",\"type\":\"bytes32[]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"config\",\"type\":\"bytes\"}],\"internalType\":\"structCapabilitiesRegistry.CapabilityConfiguration[]\",\"name\":\"capabilityConfigurations\",\"type\":\"tuple[]\"},{\"internalType\":\"bool\",\"name\":\"isPublic\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"acceptsWorkflows\",\"type\":\"bool\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"}],\"name\":\"addDON\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"}],\"internalType\":\"structCapabilitiesRegistry.NodeOperator[]\",\"name\":\"nodeOperators\",\"type\":\"tuple[]\"}],\"name\":\"addNodeOperators\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"signer\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"hashedCapabilityIds\",\"type\":\"bytes32[]\"}],\"internalType\":\"structCapabilitiesRegistry.NodeParams[]\",\"name\":\"nodes\",\"type\":\"tuple[]\"}],\"name\":\"addNodes\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32[]\",\"name\":\"hashedCapabilityIds\",\"type\":\"bytes32[]\"}],\"name\":\"deprecateCapabilities\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCapabilities\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"hashedId\",\"type\":\"bytes32\"},{\"internalType\":\"string\",\"name\":\"labelledName\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"},{\"internalType\":\"enumCapabilitiesRegistry.CapabilityType\",\"name\":\"capabilityType\",\"type\":\"uint8\"},{\"internalType\":\"enumCapabilitiesRegistry.CapabilityResponseType\",\"name\":\"responseType\",\"type\":\"uint8\"},{\"internalType\":\"address\",\"name\":\"configurationContract\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isDeprecated\",\"type\":\"bool\"}],\"internalType\":\"structCapabilitiesRegistry.CapabilityInfo[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"hashedId\",\"type\":\"bytes32\"}],\"name\":\"getCapability\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"hashedId\",\"type\":\"bytes32\"},{\"internalType\":\"string\",\"name\":\"labelledName\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"},{\"internalType\":\"enumCapabilitiesRegistry.CapabilityType\",\"name\":\"capabilityType\",\"type\":\"uint8\"},{\"internalType\":\"enumCapabilitiesRegistry.CapabilityResponseType\",\"name\":\"responseType\",\"type\":\"uint8\"},{\"internalType\":\"address\",\"name\":\"configurationContract\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isDeprecated\",\"type\":\"bool\"}],\"internalType\":\"structCapabilitiesRegistry.CapabilityInfo\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"}],\"name\":\"getCapabilityConfigs\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"}],\"name\":\"getDON\",\"outputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configCount\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"bool\",\"name\":\"isPublic\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"acceptsWorkflows\",\"type\":\"bool\"},{\"internalType\":\"bytes32[]\",\"name\":\"nodeP2PIds\",\"type\":\"bytes32[]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"config\",\"type\":\"bytes\"}],\"internalType\":\"structCapabilitiesRegistry.CapabilityConfiguration[]\",\"name\":\"capabilityConfigurations\",\"type\":\"tuple[]\"}],\"internalType\":\"structCapabilitiesRegistry.DONInfo\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getDONs\",\"outputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configCount\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"bool\",\"name\":\"isPublic\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"acceptsWorkflows\",\"type\":\"bool\"},{\"internalType\":\"bytes32[]\",\"name\":\"nodeP2PIds\",\"type\":\"bytes32[]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"config\",\"type\":\"bytes\"}],\"internalType\":\"structCapabilitiesRegistry.CapabilityConfiguration[]\",\"name\":\"capabilityConfigurations\",\"type\":\"tuple[]\"}],\"internalType\":\"structCapabilitiesRegistry.DONInfo[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"labelledName\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"}],\"name\":\"getHashedCapabilityId\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"}],\"name\":\"getNode\",\"outputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configCount\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"workflowDONId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"signer\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"hashedCapabilityIds\",\"type\":\"bytes32[]\"},{\"internalType\":\"uint256[]\",\"name\":\"capabilitiesDONIds\",\"type\":\"uint256[]\"}],\"internalType\":\"structCapabilitiesRegistry.NodeInfo\",\"name\":\"nodeInfo\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"}],\"name\":\"getNodeOperator\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"}],\"internalType\":\"structCapabilitiesRegistry.NodeOperator\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getNodeOperators\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"}],\"internalType\":\"structCapabilitiesRegistry.NodeOperator[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getNodes\",\"outputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"configCount\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"workflowDONId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"signer\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"hashedCapabilityIds\",\"type\":\"bytes32[]\"},{\"internalType\":\"uint256[]\",\"name\":\"capabilitiesDONIds\",\"type\":\"uint256[]\"}],\"internalType\":\"structCapabilitiesRegistry.NodeInfo[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"hashedCapabilityId\",\"type\":\"bytes32\"}],\"name\":\"isCapabilityDeprecated\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32[]\",\"name\":\"donIds\",\"type\":\"uint32[]\"}],\"name\":\"removeDONs\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32[]\",\"name\":\"nodeOperatorIds\",\"type\":\"uint32[]\"}],\"name\":\"removeNodeOperators\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32[]\",\"name\":\"removedNodeP2PIds\",\"type\":\"bytes32[]\"}],\"name\":\"removeNodes\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32[]\",\"name\":\"nodes\",\"type\":\"bytes32[]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"capabilityId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"config\",\"type\":\"bytes\"}],\"internalType\":\"structCapabilitiesRegistry.CapabilityConfiguration[]\",\"name\":\"capabilityConfigurations\",\"type\":\"tuple[]\"},{\"internalType\":\"bool\",\"name\":\"isPublic\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"acceptsWorkflows\",\"type\":\"bool\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"}],\"name\":\"updateDON\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32[]\",\"name\":\"nodeOperatorIds\",\"type\":\"uint32[]\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"}],\"internalType\":\"structCapabilitiesRegistry.NodeOperator[]\",\"name\":\"nodeOperators\",\"type\":\"tuple[]\"}],\"name\":\"updateNodeOperators\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"nodeOperatorId\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"signer\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"p2pId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"hashedCapabilityIds\",\"type\":\"bytes32[]\"}],\"internalType\":\"structCapabilitiesRegistry.NodeParams[]\",\"name\":\"nodes\",\"type\":\"tuple[]\"}],\"name\":\"updateNodes\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", - Bin: "0x6080604052600e80546001600160401b0319166401000000011790553480156200002857600080fd5b503380600081620000805760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615620000b357620000b381620000bc565b50505062000167565b336001600160a01b03821603620001165760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640162000077565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6150f680620001776000396000f3fe608060405234801561001057600080fd5b50600436106101ae5760003560e01c80635e65e309116100ee5780638da5cb5b11610097578063d8bc7b6811610071578063d8bc7b68146103f6578063ddbe4f8214610409578063e29581aa1461041e578063f2fde38b1461043357600080fd5b80638da5cb5b1461039b5780639cb7c5f4146103c3578063d59a79f6146103e357600080fd5b806373ac22b4116100c857806373ac22b41461036d57806379ba50971461038057806386fa42461461038857600080fd5b80635e65e3091461033257806366acaa3314610345578063715f52951461035a57600080fd5b8063235374051161015b578063398f377311610135578063398f3773146102cb5780633f2a13c9146102de57806350c946fe146102ff5780635d83d9671461031f57600080fd5b80632353740514610285578063275459f2146102a55780632c01a1e8146102b857600080fd5b80631d05394c1161018c5780631d05394c1461023b578063214502431461025057806322bdbcbc1461026557600080fd5b80630fe5800a146101b357806312570011146101d9578063181f5a77146101fc575b600080fd5b6101c66101c1366004613e8b565b610446565b6040519081526020015b60405180910390f35b6101ec6101e7366004613eef565b61047a565b60405190151581526020016101d0565b604080518082018252601a81527f4361706162696c6974696573526567697374727920312e302e30000000000000602082015290516101d09190613f76565b61024e610249366004613fce565b610487565b005b61025861069c565b6040516101d09190614150565b6102786102733660046141eb565b6107f9565b6040516101d09190614243565b6102986102933660046141eb565b6108e6565b6040516101d09190614256565b61024e6102b3366004613fce565b61092a565b61024e6102c6366004613fce565b610a01565b61024e6102d9366004613fce565b610c9d565b6102f16102ec366004614269565b610e5c565b6040516101d0929190614293565b61031261030d366004613eef565b611048565b6040516101d09190614358565b61024e61032d366004613fce565b611122565b61024e610340366004613fce565b611217565b61034d61193f565b6040516101d0919061436b565b61024e610368366004613fce565b611b22565b61024e61037b366004613fce565b611bd4565b61024e6120a2565b61024e6103963660046143e0565b61219f565b60005460405173ffffffffffffffffffffffffffffffffffffffff90911681526020016101d0565b6103d66103d1366004613eef565b6124df565b6040516101d0919061452f565b61024e6103f1366004614561565b61271a565b61024e610404366004614616565b6127e3565b6104116128ad565b6040516101d091906146bb565b6104266129a1565b6040516101d09190614730565b61024e6104413660046147c9565b612aaa565b6000828260405160200161045b929190614293565b6040516020818303038152906040528051906020012090505b92915050565b6000610474600583612abe565b61048f612ad9565b60005b818110156106975760008383838181106104ae576104ae6147e4565b90506020020160208101906104c391906141eb565b63ffffffff8181166000908152600d60209081526040808320805464010000000081049095168085526001820190935290832094955093909290916a010000000000000000000090910460ff16905b61051b83612b5c565b8110156105bb57811561057157600c60006105368584612b66565b8152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffff1690556105b3565b6105b18663ffffffff16600c60006105928588612b6690919063ffffffff16565b8152602001908152602001600020600401612b7290919063ffffffff16565b505b600101610512565b508354640100000000900463ffffffff16600003610612576040517f2b62be9b00000000000000000000000000000000000000000000000000000000815263ffffffff861660048201526024015b60405180910390fd5b63ffffffff85166000818152600d6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffff00000000000000000000001690558051938452908301919091527ff264aae70bf6a9d90e68e0f9b393f4e7fbea67b063b0f336e0b36c1581703651910160405180910390a15050505050806001019050610492565b505050565b600e54606090640100000000900463ffffffff1660006106bd600183614842565b63ffffffff1667ffffffffffffffff8111156106db576106db613d25565b60405190808252806020026020018201604052801561076257816020015b6040805160e081018252600080825260208083018290529282018190526060808301829052608083019190915260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9092019101816106f95790505b509050600060015b8363ffffffff168163ffffffff1610156107d65763ffffffff8082166000908152600d602052604090205416156107ce576107a481612b7e565b8383815181106107b6576107b66147e4565b6020026020010181905250816107cb9061485f565b91505b60010161076a565b506107e2600184614842565b63ffffffff1681146107f2578082525b5092915050565b60408051808201909152600081526060602082015263ffffffff82166000908152600b60209081526040918290208251808401909352805473ffffffffffffffffffffffffffffffffffffffff168352600181018054919284019161085d90614897565b80601f016020809104026020016040519081016040528092919081815260200182805461088990614897565b80156108d65780601f106108ab576101008083540402835291602001916108d6565b820191906000526020600020905b8154815290600101906020018083116108b957829003601f168201915b5050505050815250509050919050565b6040805160e0810182526000808252602082018190529181018290526060808201839052608082019290925260a0810182905260c081019190915261047482612b7e565b610932612ad9565b60005b63ffffffff811682111561069757600083838363ffffffff1681811061095d5761095d6147e4565b905060200201602081019061097291906141eb565b63ffffffff81166000908152600b6020526040812080547fffffffffffffffffffffffff00000000000000000000000000000000000000001681559192506109bd6001830182613cb8565b505060405163ffffffff8216907fa59268ca81d40429e65ccea5385b59cf2d3fc6519371dee92f8eb1dae5107a7a90600090a2506109fa816148ea565b9050610935565b6000805473ffffffffffffffffffffffffffffffffffffffff163314905b82811015610c97576000848483818110610a3b57610a3b6147e4565b602090810292909201356000818152600c90935260409092206001810154929350919050610a98576040517fd82f6adb00000000000000000000000000000000000000000000000000000000815260048101839052602401610609565b6000610aa682600401612b5c565b1115610afb57610ab96004820184612b66565b6040517f60a6d89800000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015260248101839052604401610609565b805468010000000000000000900463ffffffff1615610b635780546040517f60b9df730000000000000000000000000000000000000000000000000000000081526801000000000000000090910463ffffffff16600482015260248101839052604401610609565b83158015610b9d5750805463ffffffff166000908152600b602052604090205473ffffffffffffffffffffffffffffffffffffffff163314155b15610bd6576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b6001810154610be790600790612b72565b506002810154610bf990600990612b72565b506000828152600c6020526040812080547fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016815560018101829055600281018290559060048201818181610c4e8282613cf2565b5050505050507f5254e609a97bab37b7cc79fe128f85c097bd6015c6e1624ae0ba392eb975320582604051610c8591815260200190565b60405180910390a15050600101610a1f565b50505050565b610ca5612ad9565b60005b81811015610697576000838383818110610cc457610cc46147e4565b9050602002810190610cd6919061490d565b610cdf9061494b565b805190915073ffffffffffffffffffffffffffffffffffffffff16610d30576040517feeacd93900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600e54604080518082018252835173ffffffffffffffffffffffffffffffffffffffff908116825260208086015181840190815263ffffffff9095166000818152600b909252939020825181547fffffffffffffffffffffffff00000000000000000000000000000000000000001692169190911781559251919290916001820190610dbc9082614a05565b5050600e8054909150600090610dd79063ffffffff166148ea565b91906101000a81548163ffffffff021916908363ffffffff160217905550816000015173ffffffffffffffffffffffffffffffffffffffff168163ffffffff167f78e94ca80be2c30abc061b99e7eb8583b1254781734b1e3ce339abb57da2fe8e8460200151604051610e4a9190613f76565b60405180910390a35050600101610ca8565b63ffffffff8083166000908152600d60209081526040808320805464010000000090049094168084526001909401825280832085845260030190915281208054606093849390929091610eae90614897565b80601f0160208091040260200160405190810160405280929190818152602001828054610eda90614897565b8015610f275780601f10610efc57610100808354040283529160200191610f27565b820191906000526020600020905b815481529060010190602001808311610f0a57829003601f168201915b5050506000888152600260208190526040909120015492935060609262010000900473ffffffffffffffffffffffffffffffffffffffff1615915061103a905057600086815260026020819052604091829020015490517f8318ed5d00000000000000000000000000000000000000000000000000000000815263ffffffff891660048201526201000090910473ffffffffffffffffffffffffffffffffffffffff1690638318ed5d90602401600060405180830381865afa158015610ff1573d6000803e3d6000fd5b505050506040513d6000823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682016040526110379190810190614b1f565b90505b9093509150505b9250929050565b6040805160e0810182526000808252602082018190529181018290526060808201839052608082019290925260a0810182905260c08101919091526040805160e0810182526000848152600c6020908152838220805463ffffffff8082168652640100000000820481168487018190526801000000000000000090920416858701526001820154606086015260028201546080860152835260030190529190912060a08201906110f790612e49565b815260200161111a600c6000868152602001908152602001600020600401612e49565b905292915050565b61112a612ad9565b60005b81811015610697576000838383818110611149576111496147e4565b905060200201359050611166816003612abe90919063ffffffff16565b61119f576040517fe181733f00000000000000000000000000000000000000000000000000000000815260048101829052602401610609565b6111aa600582612e56565b6111e3576040517ff7d7a29400000000000000000000000000000000000000000000000000000000815260048101829052602401610609565b60405181907fdcea1b78b6ddc31592a94607d537543fcaafda6cc52d6d5cc7bbfca1422baf2190600090a25060010161112d565b6000805473ffffffffffffffffffffffffffffffffffffffff163314905b82811015610c97576000848483818110611251576112516147e4565b90506020028101906112639190614b8d565b61126c90614bc1565b6040808201516000908152600c6020908152828220805463ffffffff168352600b82528383208451808601909552805473ffffffffffffffffffffffffffffffffffffffff16855260018101805496975091959394939092840191906112d190614897565b80601f01602080910402602001604051908101604052809291908181526020018280546112fd90614897565b801561134a5780601f1061131f5761010080835404028352916020019161134a565b820191906000526020600020905b81548152906001019060200180831161132d57829003601f168201915b50505091909252505050600183015490915061139a5782604001516040517fd82f6adb00000000000000000000000000000000000000000000000000000000815260040161060991815260200190565b841580156113bf5750805173ffffffffffffffffffffffffffffffffffffffff163314155b156113f8576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b6020830151611433576040517f8377314600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001820154602084015181146114b457602084015161145490600790612abe565b1561148b576040517f8377314600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b602084015160018401556114a0600782612b72565b5060208401516114b290600790612e56565b505b606084015180516000036114f657806040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c94565b8354600090859060049061151790640100000000900463ffffffff166148ea565b91906101000a81548163ffffffff021916908363ffffffff1602179055905060005b82518110156115fc5761156f838281518110611557576115576147e4565b60200260200101516003612abe90919063ffffffff16565b6115a757826040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c94565b6115f38382815181106115bc576115bc6147e4565b60200260200101518760030160008563ffffffff1663ffffffff168152602001908152602001600020612e5690919063ffffffff16565b50600101611539565b50845468010000000000000000900463ffffffff16801561175d5763ffffffff8082166000908152600d60209081526040808320805464010000000090049094168352600190930181528282206002018054845181840281018401909552808552929392909183018282801561169157602002820191906000526020600020905b81548152602001906001019080831161167d575b5050505050905060005b815181101561175a576116f08282815181106116b9576116b96147e4565b60200260200101518960030160008763ffffffff1663ffffffff168152602001908152602001600020612abe90919063ffffffff16565b61175257818181518110611706576117066147e4565b6020026020010151836040517f03dcd86200000000000000000000000000000000000000000000000000000000815260040161060992919091825263ffffffff16602082015260400190565b60010161169b565b50505b600061176b87600401612e49565b905060005b81518163ffffffff1610156118b1576000828263ffffffff1681518110611799576117996147e4565b60209081029190910181015163ffffffff8082166000908152600d8452604080822080546401000000009004909316825260019092018452818120600201805483518187028101870190945280845293955090939192909183018282801561182057602002820191906000526020600020905b81548152602001906001019080831161180c575b5050505050905060005b815181101561189d5761187f828281518110611848576118486147e4565b60200260200101518c60030160008a63ffffffff1663ffffffff168152602001908152602001600020612abe90919063ffffffff16565b61189557818181518110611706576117066147e4565b60010161182a565b505050806118aa906148ea565b9050611770565b50875187547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000001663ffffffff90911690811788556040808a015160028a018190556020808c01518351928352908201527f4b5b465e22eea0c3d40c30e936643245b80d19b2dcf75788c0699fe8d8db645b910160405180910390a25050505050505050806001019050611235565b600e5460609063ffffffff166000611958600183614842565b63ffffffff1667ffffffffffffffff81111561197657611976613d25565b6040519080825280602002602001820160405280156119bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816119945790505b509050600060015b8363ffffffff168163ffffffff161015611b0c5763ffffffff81166000908152600b602052604090205473ffffffffffffffffffffffffffffffffffffffff1615611b045763ffffffff81166000908152600b60209081526040918290208251808401909352805473ffffffffffffffffffffffffffffffffffffffff1683526001810180549192840191611a5890614897565b80601f0160208091040260200160405190810160405280929190818152602001828054611a8490614897565b8015611ad15780601f10611aa657610100808354040283529160200191611ad1565b820191906000526020600020905b815481529060010190602001808311611ab457829003601f168201915b505050505081525050838381518110611aec57611aec6147e4565b602002602001018190525081611b019061485f565b91505b6001016119c4565b50600e546107e29060019063ffffffff16614842565b611b2a612ad9565b60005b81811015610697576000838383818110611b4957611b496147e4565b9050602002810190611b5b9190614cd8565b611b6490614d1b565b90506000611b7a82600001518360200151610446565b9050611b87600382612e56565b611bc0576040517febf5255100000000000000000000000000000000000000000000000000000000815260048101829052602401610609565b611bca8183612e62565b5050600101611b2d565b6000805473ffffffffffffffffffffffffffffffffffffffff163314905b82811015610c97576000848483818110611c0e57611c0e6147e4565b9050602002810190611c209190614b8d565b611c2990614bc1565b805163ffffffff166000908152600b602090815260408083208151808301909252805473ffffffffffffffffffffffffffffffffffffffff168252600181018054959650939491939092840191611c7f90614897565b80601f0160208091040260200160405190810160405280929190818152602001828054611cab90614897565b8015611cf85780601f10611ccd57610100808354040283529160200191611cf8565b820191906000526020600020905b815481529060010190602001808311611cdb57829003601f168201915b50505091909252505081519192505073ffffffffffffffffffffffffffffffffffffffff16611d5e5781516040517fadd9ae1e00000000000000000000000000000000000000000000000000000000815263ffffffff9091166004820152602401610609565b83158015611d835750805173ffffffffffffffffffffffffffffffffffffffff163314155b15611dbc576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b6040808301516000908152600c60205220600181015415611e115782604001516040517f5461848300000000000000000000000000000000000000000000000000000000815260040161060991815260200190565b6040830151611e545782604001516040517f64e2ee9200000000000000000000000000000000000000000000000000000000815260040161060991815260200190565b60208301511580611e7157506020830151611e7190600790612abe565b15611ea8576040517f8377314600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60608301518051600003611eea57806040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c94565b81548290600490611f0890640100000000900463ffffffff166148ea565b82546101009290920a63ffffffff818102199093169183160217909155825464010000000090041660005b8251811015611fde57611f51838281518110611557576115576147e4565b611f8957826040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c94565b611fd5838281518110611f9e57611f9e6147e4565b60200260200101518560030160008563ffffffff1663ffffffff168152602001908152602001600020612e5690919063ffffffff16565b50600101611f33565b50845183547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000001663ffffffff918216178455604086015160028501556020860151600185018190556120349160079190612e5616565b50604085015161204690600990612e56565b50845160408087015160208089015183519283529082015263ffffffff909216917f74becb12a5e8fd0e98077d02dfba8f647c9670c9df177e42c2418cf17a636f05910160405180910390a25050505050806001019050611bf2565b60015473ffffffffffffffffffffffffffffffffffffffff163314612123576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e6572000000000000000000006044820152606401610609565b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b8281146121e2576040517fab8b67c60000000000000000000000000000000000000000000000000000000081526004810184905260248101829052604401610609565b6000805473ffffffffffffffffffffffffffffffffffffffff16905b848110156124d757600086868381811061221a5761221a6147e4565b905060200201602081019061222f91906141eb565b63ffffffff81166000908152600b6020526040902080549192509073ffffffffffffffffffffffffffffffffffffffff1661229e576040517fadd9ae1e00000000000000000000000000000000000000000000000000000000815263ffffffff83166004820152602401610609565b60008686858181106122b2576122b26147e4565b90506020028101906122c4919061490d565b6122cd9061494b565b805190915073ffffffffffffffffffffffffffffffffffffffff1661231e576040517feeacd93900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b815473ffffffffffffffffffffffffffffffffffffffff16331480159061235b57503373ffffffffffffffffffffffffffffffffffffffff861614155b15612394576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b8051825473ffffffffffffffffffffffffffffffffffffffff908116911614158061241057506020808201516040516123cd9201613f76565b60405160208183030381529060405280519060200120826001016040516020016123f79190614dc1565b6040516020818303038152906040528051906020012014155b156124c957805182547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9091161782556020810151600183019061246a9082614a05565b50806000015173ffffffffffffffffffffffffffffffffffffffff168363ffffffff167f86f41145bde5dd7f523305452e4aad3685508c181432ec733d5f345009358a2883602001516040516124c09190613f76565b60405180910390a35b5050508060010190506121fe565b505050505050565b6125206040805160e0810182526000808252606060208301819052928201839052909182019081526020016000815260006020820181905260409091015290565b6040805160e0810182528381526000848152600260209081529290208054919283019161254c90614897565b80601f016020809104026020016040519081016040528092919081815260200182805461257890614897565b80156125c55780601f1061259a576101008083540402835291602001916125c5565b820191906000526020600020905b8154815290600101906020018083116125a857829003601f168201915b505050505081526020016002600085815260200190815260200160002060010180546125f090614897565b80601f016020809104026020016040519081016040528092919081815260200182805461261c90614897565b80156126695780601f1061263e57610100808354040283529160200191612669565b820191906000526020600020905b81548152906001019060200180831161264c57829003601f168201915b50505091835250506000848152600260208181526040909220015491019060ff16600381111561269b5761269b61444c565b815260008481526002602081815260409092200154910190610100900460ff1660018111156126cc576126cc61444c565b81526000848152600260208181526040928390209091015462010000900473ffffffffffffffffffffffffffffffffffffffff169083015201612710600585612abe565b1515905292915050565b612722612ad9565b63ffffffff8089166000908152600d6020526040812054640100000000900490911690819003612786576040517f2b62be9b00000000000000000000000000000000000000000000000000000000815263ffffffff8a166004820152602401610609565b6127d8888888886040518060a001604052808f63ffffffff168152602001876127ae906148ea565b97508763ffffffff1681526020018a1515815260200189151581526020018860ff168152506130f6565b505050505050505050565b6127eb612ad9565b600e805460009164010000000090910463ffffffff1690600461280d836148ea565b82546101009290920a63ffffffff81810219909316918316021790915581166000818152600d602090815260409182902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000001684179055815160a08101835292835260019083015286151590820152841515606082015260ff841660808201529091506128a39089908990899089906130f6565b5050505050505050565b606060006128bb6003612e49565b90506000815167ffffffffffffffff8111156128d9576128d9613d25565b60405190808252806020026020018201604052801561294b57816020015b6129386040805160e0810182526000808252606060208301819052928201839052909182019081526020016000815260006020820181905260409091015290565b8152602001906001900390816128f75790505b50905060005b82518110156107f25761297c83828151811061296f5761296f6147e4565b60200260200101516124df565b82828151811061298e5761298e6147e4565b6020908102919091010152600101612951565b606060006129af6009612e49565b90506000815167ffffffffffffffff8111156129cd576129cd613d25565b604051908082528060200260200182016040528015612a5457816020015b6040805160e081018252600080825260208083018290529282018190526060808301829052608083019190915260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9092019101816129eb5790505b50905060005b82518110156107f257612a85838281518110612a7857612a786147e4565b6020026020010151611048565b828281518110612a9757612a976147e4565b6020908102919091010152600101612a5a565b612ab2612ad9565b612abb8161391a565b50565b600081815260018301602052604081205415155b9392505050565b60005473ffffffffffffffffffffffffffffffffffffffff163314612b5a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610609565b565b6000610474825490565b6000612ad28383613a0f565b6000612ad28383613a39565b6040805160e0810182526000808252602080830182905282840182905260608084018390526080840183905260a0840181905260c084015263ffffffff8581168352600d8252848320805464010000000090049091168084526001909101825284832060028101805487518186028101860190985280885295969295919493909190830182828015612c2f57602002820191906000526020600020905b815481526020019060010190808311612c1b575b505050505090506000815167ffffffffffffffff811115612c5257612c52613d25565b604051908082528060200260200182016040528015612c9857816020015b604080518082019091526000815260606020820152815260200190600190039081612c705790505b50905060005b8151811015612db0576040518060400160405280848381518110612cc457612cc46147e4565b60200260200101518152602001856003016000868581518110612ce957612ce96147e4565b602002602001015181526020019081526020016000208054612d0a90614897565b80601f0160208091040260200160405190810160405280929190818152602001828054612d3690614897565b8015612d835780601f10612d5857610100808354040283529160200191612d83565b820191906000526020600020905b815481529060010190602001808311612d6657829003601f168201915b5050505050815250828281518110612d9d57612d9d6147e4565b6020908102919091010152600101612c9e565b506040805160e08101825263ffffffff8089166000818152600d6020818152868320548086168752948b168187015260ff680100000000000000008604811697870197909752690100000000000000000085048716151560608701529290915290526a010000000000000000000090049091161515608082015260a08101612e3785612e49565b81526020019190915295945050505050565b60606000612ad283613b2c565b6000612ad28383613b88565b608081015173ffffffffffffffffffffffffffffffffffffffff1615612fb057608081015173ffffffffffffffffffffffffffffffffffffffff163b1580612f5b575060808101516040517f01ffc9a70000000000000000000000000000000000000000000000000000000081527f78bea72100000000000000000000000000000000000000000000000000000000600482015273ffffffffffffffffffffffffffffffffffffffff909116906301ffc9a790602401602060405180830381865afa158015612f35573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612f599190614e6f565b155b15612fb05760808101516040517fabb5e3fd00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091166004820152602401610609565b600082815260026020526040902081518291908190612fcf9082614a05565b5060208201516001820190612fe49082614a05565b5060408201516002820180547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660018360038111156130265761302661444c565b021790555060608201516002820180547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff1661010083600181111561306d5761306d61444c565b0217905550608091909101516002909101805473ffffffffffffffffffffffffffffffffffffffff90921662010000027fffffffffffffffffffff0000000000000000000000000000000000000000ffff90921691909117905560405182907f04f0a9bcf3f3a3b42a4d7ca081119755f82ebe43e0d30c8f7292c4fe0dc4a2ae90600090a25050565b805163ffffffff9081166000908152600d602090815260408083208286015190941683526001909301905220608082015160ff161580613148575060808201518590613143906001614e8c565b60ff16115b156131915760808201516040517f25b4d61800000000000000000000000000000000000000000000000000000000815260ff909116600482015260248101869052604401610609565b6001826020015163ffffffff16111561327957815163ffffffff166000908152600d6020908152604082209084015160019182019183916131d29190614842565b63ffffffff1663ffffffff168152602001908152602001600020905060005b6131fa82612b5c565b81101561327657613229846000015163ffffffff16600c60006105928587600001612b6690919063ffffffff16565b50600c60006132388484612b66565b8152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffff1690556001016131f1565b50505b60005b858110156134b3576132a9878783818110613299576132996147e4565b8592602090910201359050612e56565b61330a5782518787838181106132c1576132c16147e4565b6040517f636e405700000000000000000000000000000000000000000000000000000000815263ffffffff90941660048501526020029190910135602483015250604401610609565b82606001511561346157825163ffffffff16600c6000898985818110613332576133326147e4565b602090810292909201358352508101919091526040016000205468010000000000000000900463ffffffff16148015906133ac5750600c600088888481811061337d5761337d6147e4565b602090810292909201358352508101919091526040016000205468010000000000000000900463ffffffff1615155b1561340e5782518787838181106133c5576133c56147e4565b6040517f60b9df7300000000000000000000000000000000000000000000000000000000815263ffffffff90941660048501526020029190910135602483015250604401610609565b8251600c6000898985818110613426576134266147e4565b90506020020135815260200190815260200160002060000160086101000a81548163ffffffff021916908363ffffffff1602179055506134ab565b82516134a99063ffffffff16600c60008a8a86818110613483576134836147e4565b905060200201358152602001908152602001600020600401612e5690919063ffffffff16565b505b60010161327c565b5060005b8381101561378f57368585838181106134d2576134d26147e4565b90506020028101906134e4919061490d565b90506134f260038235612abe565b61352b576040517fe181733f00000000000000000000000000000000000000000000000000000000815281356004820152602401610609565b61353760058235612abe565b15613571576040517ff7d7a29400000000000000000000000000000000000000000000000000000000815281356004820152602401610609565b803560009081526003840160205260408120805461358e90614897565b905011156135da5783516040517f3927d08000000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015281356024820152604401610609565b60005b878110156136e4576136818235600c60008c8c86818110613600576136006147e4565b9050602002013581526020019081526020016000206003016000600c60008e8e88818110613630576136306147e4565b90506020020135815260200190815260200160002060000160049054906101000a900463ffffffff1663ffffffff1663ffffffff168152602001908152602001600020612abe90919063ffffffff16565b6136dc57888882818110613697576136976147e4565b6040517fa7e792500000000000000000000000000000000000000000000000000000000081526020909102929092013560048301525082356024820152604401610609565b6001016135dd565b506002830180546001810182556000918252602091829020833591015561370d90820182614ea5565b8235600090815260038601602052604090209161372b919083614f0a565b50835160208086015161378692918435908c908c9061374c90880188614ea5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613bd792505050565b506001016134b7565b50604080830151835163ffffffff9081166000908152600d602090815284822080549415156901000000000000000000027fffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffff90951694909417909355606086015186518316825284822080549115156a0100000000000000000000027fffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffff9092169190911790556080860151865183168252848220805460ff9290921668010000000000000000027fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff909216919091179055918501805186518316845292849020805493909216640100000000027fffffffffffffffffffffffffffffffffffffffffffffffff00000000ffffffff9093169290921790558351905191517ff264aae70bf6a9d90e68e0f9b393f4e7fbea67b063b0f336e0b36c15817036519261390a929163ffffffff92831681529116602082015260400190565b60405180910390a1505050505050565b3373ffffffffffffffffffffffffffffffffffffffff821603613999576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610609565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6000826000018281548110613a2657613a266147e4565b9060005260206000200154905092915050565b60008181526001830160205260408120548015613b22576000613a5d600183615025565b8554909150600090613a7190600190615025565b9050818114613ad6576000866000018281548110613a9157613a916147e4565b9060005260206000200154905080876000018481548110613ab457613ab46147e4565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080613ae757613ae7615038565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050610474565b6000915050610474565b606081600001805480602002602001604051908101604052809291908181526020018280548015613b7c57602002820191906000526020600020905b815481526020019060010190808311613b68575b50505050509050919050565b6000818152600183016020526040812054613bcf57508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155610474565b506000610474565b6000848152600260208190526040909120015462010000900473ffffffffffffffffffffffffffffffffffffffff16156124d757600084815260026020819052604091829020015490517ffba64a7c0000000000000000000000000000000000000000000000000000000081526201000090910473ffffffffffffffffffffffffffffffffffffffff169063fba64a7c90613c7e908690869086908b908d90600401615067565b600060405180830381600087803b158015613c9857600080fd5b505af1158015613cac573d6000803e3d6000fd5b50505050505050505050565b508054613cc490614897565b6000825580601f10613cd4575050565b601f016020900490600052602060002090810190612abb9190613d0c565b5080546000825590600052602060002090810190612abb91905b5b80821115613d215760008155600101613d0d565b5090565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040516080810167ffffffffffffffff81118282101715613d7757613d77613d25565b60405290565b60405160a0810167ffffffffffffffff81118282101715613d7757613d77613d25565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715613de757613de7613d25565b604052919050565b600067ffffffffffffffff821115613e0957613e09613d25565b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b600082601f830112613e4657600080fd5b8135613e59613e5482613def565b613da0565b818152846020838601011115613e6e57600080fd5b816020850160208301376000918101602001919091529392505050565b60008060408385031215613e9e57600080fd5b823567ffffffffffffffff80821115613eb657600080fd5b613ec286838701613e35565b93506020850135915080821115613ed857600080fd5b50613ee585828601613e35565b9150509250929050565b600060208284031215613f0157600080fd5b5035919050565b60005b83811015613f23578181015183820152602001613f0b565b50506000910152565b60008151808452613f44816020860160208601613f08565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b602081526000612ad26020830184613f2c565b60008083601f840112613f9b57600080fd5b50813567ffffffffffffffff811115613fb357600080fd5b6020830191508360208260051b850101111561104157600080fd5b60008060208385031215613fe157600080fd5b823567ffffffffffffffff811115613ff857600080fd5b61400485828601613f89565b90969095509350505050565b60008151808452602080850194506020840160005b8381101561404157815187529582019590820190600101614025565b509495945050505050565b600082825180855260208086019550808260051b84010181860160005b848110156140c9578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051845284015160408585018190526140b581860183613f2c565b9a86019a9450505090830190600101614069565b5090979650505050505050565b600063ffffffff8083511684528060208401511660208501525060ff604083015116604084015260608201511515606084015260808201511515608084015260a082015160e060a085015261412e60e0850182614010565b905060c083015184820360c0860152614147828261404c565b95945050505050565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c5577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526141b38583516140d6565b94509285019290850190600101614179565b5092979650505050505050565b803563ffffffff811681146141e657600080fd5b919050565b6000602082840312156141fd57600080fd5b612ad2826141d2565b73ffffffffffffffffffffffffffffffffffffffff8151168252600060208201516040602085015261423b6040850182613f2c565b949350505050565b602081526000612ad26020830184614206565b602081526000612ad260208301846140d6565b6000806040838503121561427c57600080fd5b614285836141d2565b946020939093013593505050565b6040815260006142a66040830185613f2c565b82810360208401526141478185613f2c565b600063ffffffff808351168452602081818501511681860152816040850151166040860152606084015160608601526080840151608086015260a0840151915060e060a086015261430c60e0860183614010565b60c08581015187830391880191909152805180835290830193506000918301905b8083101561434d578451825293830193600192909201919083019061432d565b509695505050505050565b602081526000612ad260208301846142b8565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c5577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526143ce858351614206565b94509285019290850190600101614394565b600080600080604085870312156143f657600080fd5b843567ffffffffffffffff8082111561440e57600080fd5b61441a88838901613f89565b9096509450602087013591508082111561443357600080fd5b5061444087828801613f89565b95989497509550505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b805182526000602082015160e0602085015261449a60e0850182613f2c565b9050604083015184820360408601526144b38282613f2c565b9150506060830151600481106144cb576144cb61444c565b60608501526080830151600281106144e5576144e561444c565b8060808601525060a083015161451360a086018273ffffffffffffffffffffffffffffffffffffffff169052565b5060c083015161452760c086018215159052565b509392505050565b602081526000612ad2602083018461447b565b8015158114612abb57600080fd5b803560ff811681146141e657600080fd5b60008060008060008060008060c0898b03121561457d57600080fd5b614586896141d2565b9750602089013567ffffffffffffffff808211156145a357600080fd5b6145af8c838d01613f89565b909950975060408b01359150808211156145c857600080fd5b506145d58b828c01613f89565b90965094505060608901356145e981614542565b925060808901356145f981614542565b915061460760a08a01614550565b90509295985092959890939650565b600080600080600080600060a0888a03121561463157600080fd5b873567ffffffffffffffff8082111561464957600080fd5b6146558b838c01613f89565b909950975060208a013591508082111561466e57600080fd5b5061467b8a828b01613f89565b909650945050604088013561468f81614542565b9250606088013561469f81614542565b91506146ad60808901614550565b905092959891949750929550565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c5577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc088860301845261471e85835161447b565b945092850192908501906001016146e4565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c5577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526147938583516142b8565b94509285019290850190600101614759565b803573ffffffffffffffffffffffffffffffffffffffff811681146141e657600080fd5b6000602082840312156147db57600080fd5b612ad2826147a5565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b63ffffffff8281168282160390808211156107f2576107f2614813565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361489057614890614813565b5060010190565b600181811c908216806148ab57607f821691505b6020821081036148e4577f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b50919050565b600063ffffffff80831681810361490357614903614813565b6001019392505050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc183360301811261494157600080fd5b9190910192915050565b60006040823603121561495d57600080fd5b6040516040810167ffffffffffffffff828210818311171561498157614981613d25565b8160405261498e856147a5565b835260208501359150808211156149a457600080fd5b506149b136828601613e35565b60208301525092915050565b601f821115610697576000816000526020600020601f850160051c810160208610156149e65750805b601f850160051c820191505b818110156124d7578281556001016149f2565b815167ffffffffffffffff811115614a1f57614a1f613d25565b614a3381614a2d8454614897565b846149bd565b602080601f831160018114614a865760008415614a505750858301515b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600386901b1c1916600185901b1785556124d7565b6000858152602081207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08616915b82811015614ad357888601518255948401946001909101908401614ab4565b5085821015614b0f57878501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600388901b60f8161c191681555b5050505050600190811b01905550565b600060208284031215614b3157600080fd5b815167ffffffffffffffff811115614b4857600080fd5b8201601f81018413614b5957600080fd5b8051614b67613e5482613def565b818152856020838501011115614b7c57600080fd5b614147826020830160208601613f08565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8183360301811261494157600080fd5b600060808236031215614bd357600080fd5b614bdb613d54565b614be4836141d2565b81526020808401358183015260408401356040830152606084013567ffffffffffffffff80821115614c1557600080fd5b9085019036601f830112614c2857600080fd5b813581811115614c3a57614c3a613d25565b8060051b9150614c4b848301613da0565b8181529183018401918481019036841115614c6557600080fd5b938501935b83851015614c8357843582529385019390850190614c6a565b606087015250939695505050505050565b6020808252825182820181905260009190848201906040850190845b81811015614ccc57835183529284019291840191600101614cb0565b50909695505050505050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6183360301811261494157600080fd5b8035600281106141e657600080fd5b600060a08236031215614d2d57600080fd5b614d35613d7d565b823567ffffffffffffffff80821115614d4d57600080fd5b614d5936838701613e35565b83526020850135915080821115614d6f57600080fd5b50614d7c36828601613e35565b602083015250604083013560048110614d9457600080fd5b6040820152614da560608401614d0c565b6060820152614db6608084016147a5565b608082015292915050565b6000602080835260008454614dd581614897565b8060208701526040600180841660008114614df75760018114614e3157614e61565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00851660408a0152604084151560051b8a01019550614e61565b89600052602060002060005b85811015614e585781548b8201860152908301908801614e3d565b8a016040019650505b509398975050505050505050565b600060208284031215614e8157600080fd5b8151612ad281614542565b60ff818116838216019081111561047457610474614813565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112614eda57600080fd5b83018035915067ffffffffffffffff821115614ef557600080fd5b60200191503681900382131561104157600080fd5b67ffffffffffffffff831115614f2257614f22613d25565b614f3683614f308354614897565b836149bd565b6000601f841160018114614f885760008515614f525750838201355b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600387901b1c1916600186901b17835561501e565b6000838152602090207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0861690835b82811015614fd75786850135825560209485019460019092019101614fb7565b5086821015615012577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88860031b161c19848701351681555b505060018560011b0183555b5050505050565b8181038181111561047457610474614813565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6080815284608082015260007f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8611156150a057600080fd5b8560051b808860a0850137820182810360a090810160208501526150c690820187613f2c565b91505063ffffffff8085166040840152808416606084015250969550505050505056fea164736f6c6343000818000a", + Bin: "0x6080604052600e80546001600160401b0319166401000000011790553480156200002857600080fd5b503380600081620000805760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615620000b357620000b381620000bc565b50505062000167565b336001600160a01b03821603620001165760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640162000077565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6150f780620001776000396000f3fe608060405234801561001057600080fd5b50600436106101ae5760003560e01c80635e65e309116100ee5780638da5cb5b11610097578063d8bc7b6811610071578063d8bc7b68146103f6578063ddbe4f8214610409578063e29581aa1461041e578063f2fde38b1461043357600080fd5b80638da5cb5b1461039b5780639cb7c5f4146103c3578063d59a79f6146103e357600080fd5b806373ac22b4116100c857806373ac22b41461036d57806379ba50971461038057806386fa42461461038857600080fd5b80635e65e3091461033257806366acaa3314610345578063715f52951461035a57600080fd5b8063235374051161015b578063398f377311610135578063398f3773146102cb5780633f2a13c9146102de57806350c946fe146102ff5780635d83d9671461031f57600080fd5b80632353740514610285578063275459f2146102a55780632c01a1e8146102b857600080fd5b80631d05394c1161018c5780631d05394c1461023b578063214502431461025057806322bdbcbc1461026557600080fd5b80630fe5800a146101b357806312570011146101d9578063181f5a77146101fc575b600080fd5b6101c66101c1366004613e8c565b610446565b6040519081526020015b60405180910390f35b6101ec6101e7366004613ef0565b61047a565b60405190151581526020016101d0565b604080518082018252601a81527f4361706162696c6974696573526567697374727920312e302e30000000000000602082015290516101d09190613f77565b61024e610249366004613fcf565b610487565b005b61025861069c565b6040516101d09190614151565b6102786102733660046141ec565b6107f9565b6040516101d09190614244565b6102986102933660046141ec565b6108e6565b6040516101d09190614257565b61024e6102b3366004613fcf565b61092a565b61024e6102c6366004613fcf565b610a01565b61024e6102d9366004613fcf565b610c9d565b6102f16102ec36600461426a565b610e5c565b6040516101d0929190614294565b61031261030d366004613ef0565b611048565b6040516101d09190614359565b61024e61032d366004613fcf565b611122565b61024e610340366004613fcf565b611217565b61034d61193f565b6040516101d0919061436c565b61024e610368366004613fcf565b611b22565b61024e61037b366004613fcf565b611bd4565b61024e6120a2565b61024e6103963660046143e1565b61219f565b60005460405173ffffffffffffffffffffffffffffffffffffffff90911681526020016101d0565b6103d66103d1366004613ef0565b6124df565b6040516101d09190614530565b61024e6103f1366004614562565b61271a565b61024e610404366004614617565b6127e3565b6104116128ad565b6040516101d091906146bc565b6104266129a1565b6040516101d09190614731565b61024e6104413660046147ca565b612aaa565b6000828260405160200161045b929190614294565b6040516020818303038152906040528051906020012090505b92915050565b6000610474600583612abe565b61048f612ad9565b60005b818110156106975760008383838181106104ae576104ae6147e5565b90506020020160208101906104c391906141ec565b63ffffffff8181166000908152600d60209081526040808320805464010000000081049095168085526001820190935290832094955093909290916a010000000000000000000090910460ff16905b61051b83612b5c565b8110156105bb57811561057157600c60006105368584612b66565b8152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffff1690556105b3565b6105b18663ffffffff16600c60006105928588612b6690919063ffffffff16565b8152602001908152602001600020600401612b7290919063ffffffff16565b505b600101610512565b508354640100000000900463ffffffff16600003610612576040517f2b62be9b00000000000000000000000000000000000000000000000000000000815263ffffffff861660048201526024015b60405180910390fd5b63ffffffff85166000818152600d6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffff00000000000000000000001690558051938452908301919091527ff264aae70bf6a9d90e68e0f9b393f4e7fbea67b063b0f336e0b36c1581703651910160405180910390a15050505050806001019050610492565b505050565b600e54606090640100000000900463ffffffff1660006106bd600183614843565b63ffffffff1667ffffffffffffffff8111156106db576106db613d26565b60405190808252806020026020018201604052801561076257816020015b6040805160e081018252600080825260208083018290529282018190526060808301829052608083019190915260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9092019101816106f95790505b509050600060015b8363ffffffff168163ffffffff1610156107d65763ffffffff8082166000908152600d602052604090205416156107ce576107a481612b7e565b8383815181106107b6576107b66147e5565b6020026020010181905250816107cb90614860565b91505b60010161076a565b506107e2600184614843565b63ffffffff1681146107f2578082525b5092915050565b60408051808201909152600081526060602082015263ffffffff82166000908152600b60209081526040918290208251808401909352805473ffffffffffffffffffffffffffffffffffffffff168352600181018054919284019161085d90614898565b80601f016020809104026020016040519081016040528092919081815260200182805461088990614898565b80156108d65780601f106108ab576101008083540402835291602001916108d6565b820191906000526020600020905b8154815290600101906020018083116108b957829003601f168201915b5050505050815250509050919050565b6040805160e0810182526000808252602082018190529181018290526060808201839052608082019290925260a0810182905260c081019190915261047482612b7e565b610932612ad9565b60005b63ffffffff811682111561069757600083838363ffffffff1681811061095d5761095d6147e5565b905060200201602081019061097291906141ec565b63ffffffff81166000908152600b6020526040812080547fffffffffffffffffffffffff00000000000000000000000000000000000000001681559192506109bd6001830182613cb9565b505060405163ffffffff8216907fa59268ca81d40429e65ccea5385b59cf2d3fc6519371dee92f8eb1dae5107a7a90600090a2506109fa816148eb565b9050610935565b6000805473ffffffffffffffffffffffffffffffffffffffff163314905b82811015610c97576000848483818110610a3b57610a3b6147e5565b602090810292909201356000818152600c90935260409092206001810154929350919050610a98576040517fd82f6adb00000000000000000000000000000000000000000000000000000000815260048101839052602401610609565b6000610aa682600401612b5c565b1115610afb57610ab96004820184612b66565b6040517f60a6d89800000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015260248101839052604401610609565b805468010000000000000000900463ffffffff1615610b635780546040517f60b9df730000000000000000000000000000000000000000000000000000000081526801000000000000000090910463ffffffff16600482015260248101839052604401610609565b83158015610b9d5750805463ffffffff166000908152600b602052604090205473ffffffffffffffffffffffffffffffffffffffff163314155b15610bd6576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b6001810154610be790600790612b72565b506002810154610bf990600990612b72565b506000828152600c6020526040812080547fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016815560018101829055600281018290559060048201818181610c4e8282613cf3565b5050505050507f5254e609a97bab37b7cc79fe128f85c097bd6015c6e1624ae0ba392eb975320582604051610c8591815260200190565b60405180910390a15050600101610a1f565b50505050565b610ca5612ad9565b60005b81811015610697576000838383818110610cc457610cc46147e5565b9050602002810190610cd6919061490e565b610cdf9061494c565b805190915073ffffffffffffffffffffffffffffffffffffffff16610d30576040517feeacd93900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600e54604080518082018252835173ffffffffffffffffffffffffffffffffffffffff908116825260208086015181840190815263ffffffff9095166000818152600b909252939020825181547fffffffffffffffffffffffff00000000000000000000000000000000000000001692169190911781559251919290916001820190610dbc9082614a06565b5050600e8054909150600090610dd79063ffffffff166148eb565b91906101000a81548163ffffffff021916908363ffffffff160217905550816000015173ffffffffffffffffffffffffffffffffffffffff168163ffffffff167f78e94ca80be2c30abc061b99e7eb8583b1254781734b1e3ce339abb57da2fe8e8460200151604051610e4a9190613f77565b60405180910390a35050600101610ca8565b63ffffffff8083166000908152600d60209081526040808320805464010000000090049094168084526001909401825280832085845260030190915281208054606093849390929091610eae90614898565b80601f0160208091040260200160405190810160405280929190818152602001828054610eda90614898565b8015610f275780601f10610efc57610100808354040283529160200191610f27565b820191906000526020600020905b815481529060010190602001808311610f0a57829003601f168201915b5050506000888152600260208190526040909120015492935060609262010000900473ffffffffffffffffffffffffffffffffffffffff1615915061103a905057600086815260026020819052604091829020015490517f8318ed5d00000000000000000000000000000000000000000000000000000000815263ffffffff891660048201526201000090910473ffffffffffffffffffffffffffffffffffffffff1690638318ed5d90602401600060405180830381865afa158015610ff1573d6000803e3d6000fd5b505050506040513d6000823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682016040526110379190810190614b20565b90505b9093509150505b9250929050565b6040805160e0810182526000808252602082018190529181018290526060808201839052608082019290925260a0810182905260c08101919091526040805160e0810182526000848152600c6020908152838220805463ffffffff8082168652640100000000820481168487018190526801000000000000000090920416858701526001820154606086015260028201546080860152835260030190529190912060a08201906110f790612e49565b815260200161111a600c6000868152602001908152602001600020600401612e49565b905292915050565b61112a612ad9565b60005b81811015610697576000838383818110611149576111496147e5565b905060200201359050611166816003612abe90919063ffffffff16565b61119f576040517fe181733f00000000000000000000000000000000000000000000000000000000815260048101829052602401610609565b6111aa600582612e56565b6111e3576040517ff7d7a29400000000000000000000000000000000000000000000000000000000815260048101829052602401610609565b60405181907fdcea1b78b6ddc31592a94607d537543fcaafda6cc52d6d5cc7bbfca1422baf2190600090a25060010161112d565b6000805473ffffffffffffffffffffffffffffffffffffffff163314905b82811015610c97576000848483818110611251576112516147e5565b90506020028101906112639190614b8e565b61126c90614bc2565b6040808201516000908152600c6020908152828220805463ffffffff168352600b82528383208451808601909552805473ffffffffffffffffffffffffffffffffffffffff16855260018101805496975091959394939092840191906112d190614898565b80601f01602080910402602001604051908101604052809291908181526020018280546112fd90614898565b801561134a5780601f1061131f5761010080835404028352916020019161134a565b820191906000526020600020905b81548152906001019060200180831161132d57829003601f168201915b50505091909252505050600183015490915061139a5782604001516040517fd82f6adb00000000000000000000000000000000000000000000000000000000815260040161060991815260200190565b841580156113bf5750805173ffffffffffffffffffffffffffffffffffffffff163314155b156113f8576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b6020830151611433576040517f8377314600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001820154602084015181146114b457602084015161145490600790612abe565b1561148b576040517f8377314600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b602084015160018401556114a0600782612b72565b5060208401516114b290600790612e56565b505b606084015180516000036114f657806040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c95565b8354600090859060049061151790640100000000900463ffffffff166148eb565b91906101000a81548163ffffffff021916908363ffffffff1602179055905060005b82518110156115fc5761156f838281518110611557576115576147e5565b60200260200101516003612abe90919063ffffffff16565b6115a757826040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c95565b6115f38382815181106115bc576115bc6147e5565b60200260200101518760030160008563ffffffff1663ffffffff168152602001908152602001600020612e5690919063ffffffff16565b50600101611539565b50845468010000000000000000900463ffffffff16801561175d5763ffffffff8082166000908152600d60209081526040808320805464010000000090049094168352600190930181528282206002018054845181840281018401909552808552929392909183018282801561169157602002820191906000526020600020905b81548152602001906001019080831161167d575b5050505050905060005b815181101561175a576116f08282815181106116b9576116b96147e5565b60200260200101518960030160008763ffffffff1663ffffffff168152602001908152602001600020612abe90919063ffffffff16565b61175257818181518110611706576117066147e5565b6020026020010151836040517f03dcd86200000000000000000000000000000000000000000000000000000000815260040161060992919091825263ffffffff16602082015260400190565b60010161169b565b50505b600061176b87600401612e49565b905060005b81518163ffffffff1610156118b1576000828263ffffffff1681518110611799576117996147e5565b60209081029190910181015163ffffffff8082166000908152600d8452604080822080546401000000009004909316825260019092018452818120600201805483518187028101870190945280845293955090939192909183018282801561182057602002820191906000526020600020905b81548152602001906001019080831161180c575b5050505050905060005b815181101561189d5761187f828281518110611848576118486147e5565b60200260200101518c60030160008a63ffffffff1663ffffffff168152602001908152602001600020612abe90919063ffffffff16565b61189557818181518110611706576117066147e5565b60010161182a565b505050806118aa906148eb565b9050611770565b50875187547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000001663ffffffff90911690811788556040808a015160028a018190556020808c01518351928352908201527f4b5b465e22eea0c3d40c30e936643245b80d19b2dcf75788c0699fe8d8db645b910160405180910390a25050505050505050806001019050611235565b600e5460609063ffffffff166000611958600183614843565b63ffffffff1667ffffffffffffffff81111561197657611976613d26565b6040519080825280602002602001820160405280156119bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816119945790505b509050600060015b8363ffffffff168163ffffffff161015611b0c5763ffffffff81166000908152600b602052604090205473ffffffffffffffffffffffffffffffffffffffff1615611b045763ffffffff81166000908152600b60209081526040918290208251808401909352805473ffffffffffffffffffffffffffffffffffffffff1683526001810180549192840191611a5890614898565b80601f0160208091040260200160405190810160405280929190818152602001828054611a8490614898565b8015611ad15780601f10611aa657610100808354040283529160200191611ad1565b820191906000526020600020905b815481529060010190602001808311611ab457829003601f168201915b505050505081525050838381518110611aec57611aec6147e5565b602002602001018190525081611b0190614860565b91505b6001016119c4565b50600e546107e29060019063ffffffff16614843565b611b2a612ad9565b60005b81811015610697576000838383818110611b4957611b496147e5565b9050602002810190611b5b9190614cd9565b611b6490614d1c565b90506000611b7a82600001518360200151610446565b9050611b87600382612e56565b611bc0576040517febf5255100000000000000000000000000000000000000000000000000000000815260048101829052602401610609565b611bca8183612e62565b5050600101611b2d565b6000805473ffffffffffffffffffffffffffffffffffffffff163314905b82811015610c97576000848483818110611c0e57611c0e6147e5565b9050602002810190611c209190614b8e565b611c2990614bc2565b805163ffffffff166000908152600b602090815260408083208151808301909252805473ffffffffffffffffffffffffffffffffffffffff168252600181018054959650939491939092840191611c7f90614898565b80601f0160208091040260200160405190810160405280929190818152602001828054611cab90614898565b8015611cf85780601f10611ccd57610100808354040283529160200191611cf8565b820191906000526020600020905b815481529060010190602001808311611cdb57829003601f168201915b50505091909252505081519192505073ffffffffffffffffffffffffffffffffffffffff16611d5e5781516040517fadd9ae1e00000000000000000000000000000000000000000000000000000000815263ffffffff9091166004820152602401610609565b83158015611d835750805173ffffffffffffffffffffffffffffffffffffffff163314155b15611dbc576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b6040808301516000908152600c60205220600181015415611e115782604001516040517f5461848300000000000000000000000000000000000000000000000000000000815260040161060991815260200190565b6040830151611e545782604001516040517f64e2ee9200000000000000000000000000000000000000000000000000000000815260040161060991815260200190565b60208301511580611e7157506020830151611e7190600790612abe565b15611ea8576040517f8377314600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60608301518051600003611eea57806040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c95565b81548290600490611f0890640100000000900463ffffffff166148eb565b82546101009290920a63ffffffff818102199093169183160217909155825464010000000090041660005b8251811015611fde57611f51838281518110611557576115576147e5565b611f8957826040517f3748d4c60000000000000000000000000000000000000000000000000000000081526004016106099190614c95565b611fd5838281518110611f9e57611f9e6147e5565b60200260200101518560030160008563ffffffff1663ffffffff168152602001908152602001600020612e5690919063ffffffff16565b50600101611f33565b50845183547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000001663ffffffff918216178455604086015160028501556020860151600185018190556120349160079190612e5616565b50604085015161204690600990612e56565b50845160408087015160208089015183519283529082015263ffffffff909216917f74becb12a5e8fd0e98077d02dfba8f647c9670c9df177e42c2418cf17a636f05910160405180910390a25050505050806001019050611bf2565b60015473ffffffffffffffffffffffffffffffffffffffff163314612123576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e6572000000000000000000006044820152606401610609565b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b8281146121e2576040517fab8b67c60000000000000000000000000000000000000000000000000000000081526004810184905260248101829052604401610609565b6000805473ffffffffffffffffffffffffffffffffffffffff16905b848110156124d757600086868381811061221a5761221a6147e5565b905060200201602081019061222f91906141ec565b63ffffffff81166000908152600b6020526040902080549192509073ffffffffffffffffffffffffffffffffffffffff1661229e576040517fadd9ae1e00000000000000000000000000000000000000000000000000000000815263ffffffff83166004820152602401610609565b60008686858181106122b2576122b26147e5565b90506020028101906122c4919061490e565b6122cd9061494c565b805190915073ffffffffffffffffffffffffffffffffffffffff1661231e576040517feeacd93900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b815473ffffffffffffffffffffffffffffffffffffffff16331480159061235b57503373ffffffffffffffffffffffffffffffffffffffff861614155b15612394576040517f9473075d000000000000000000000000000000000000000000000000000000008152336004820152602401610609565b8051825473ffffffffffffffffffffffffffffffffffffffff908116911614158061241057506020808201516040516123cd9201613f77565b60405160208183030381529060405280519060200120826001016040516020016123f79190614dc2565b6040516020818303038152906040528051906020012014155b156124c957805182547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9091161782556020810151600183019061246a9082614a06565b50806000015173ffffffffffffffffffffffffffffffffffffffff168363ffffffff167f86f41145bde5dd7f523305452e4aad3685508c181432ec733d5f345009358a2883602001516040516124c09190613f77565b60405180910390a35b5050508060010190506121fe565b505050505050565b6125206040805160e0810182526000808252606060208301819052928201839052909182019081526020016000815260006020820181905260409091015290565b6040805160e0810182528381526000848152600260209081529290208054919283019161254c90614898565b80601f016020809104026020016040519081016040528092919081815260200182805461257890614898565b80156125c55780601f1061259a576101008083540402835291602001916125c5565b820191906000526020600020905b8154815290600101906020018083116125a857829003601f168201915b505050505081526020016002600085815260200190815260200160002060010180546125f090614898565b80601f016020809104026020016040519081016040528092919081815260200182805461261c90614898565b80156126695780601f1061263e57610100808354040283529160200191612669565b820191906000526020600020905b81548152906001019060200180831161264c57829003601f168201915b50505091835250506000848152600260208181526040909220015491019060ff16600381111561269b5761269b61444d565b815260008481526002602081815260409092200154910190610100900460ff1660018111156126cc576126cc61444d565b81526000848152600260208181526040928390209091015462010000900473ffffffffffffffffffffffffffffffffffffffff169083015201612710600585612abe565b1515905292915050565b612722612ad9565b63ffffffff8089166000908152600d6020526040812054640100000000900490911690819003612786576040517f2b62be9b00000000000000000000000000000000000000000000000000000000815263ffffffff8a166004820152602401610609565b6127d8888888886040518060a001604052808f63ffffffff168152602001876127ae906148eb565b97508763ffffffff1681526020018a1515815260200189151581526020018860ff168152506130f6565b505050505050505050565b6127eb612ad9565b600e805460009164010000000090910463ffffffff1690600461280d836148eb565b82546101009290920a63ffffffff81810219909316918316021790915581166000818152600d602090815260409182902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000001684179055815160a08101835292835260019083015286151590820152841515606082015260ff841660808201529091506128a39089908990899089906130f6565b5050505050505050565b606060006128bb6003612e49565b90506000815167ffffffffffffffff8111156128d9576128d9613d26565b60405190808252806020026020018201604052801561294b57816020015b6129386040805160e0810182526000808252606060208301819052928201839052909182019081526020016000815260006020820181905260409091015290565b8152602001906001900390816128f75790505b50905060005b82518110156107f25761297c83828151811061296f5761296f6147e5565b60200260200101516124df565b82828151811061298e5761298e6147e5565b6020908102919091010152600101612951565b606060006129af6009612e49565b90506000815167ffffffffffffffff8111156129cd576129cd613d26565b604051908082528060200260200182016040528015612a5457816020015b6040805160e081018252600080825260208083018290529282018190526060808301829052608083019190915260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9092019101816129eb5790505b50905060005b82518110156107f257612a85838281518110612a7857612a786147e5565b6020026020010151611048565b828281518110612a9757612a976147e5565b6020908102919091010152600101612a5a565b612ab2612ad9565b612abb8161391b565b50565b600081815260018301602052604081205415155b9392505050565b60005473ffffffffffffffffffffffffffffffffffffffff163314612b5a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610609565b565b6000610474825490565b6000612ad28383613a10565b6000612ad28383613a3a565b6040805160e0810182526000808252602080830182905282840182905260608084018390526080840183905260a0840181905260c084015263ffffffff8581168352600d8252848320805464010000000090049091168084526001909101825284832060028101805487518186028101860190985280885295969295919493909190830182828015612c2f57602002820191906000526020600020905b815481526020019060010190808311612c1b575b505050505090506000815167ffffffffffffffff811115612c5257612c52613d26565b604051908082528060200260200182016040528015612c9857816020015b604080518082019091526000815260606020820152815260200190600190039081612c705790505b50905060005b8151811015612db0576040518060400160405280848381518110612cc457612cc46147e5565b60200260200101518152602001856003016000868581518110612ce957612ce96147e5565b602002602001015181526020019081526020016000208054612d0a90614898565b80601f0160208091040260200160405190810160405280929190818152602001828054612d3690614898565b8015612d835780601f10612d5857610100808354040283529160200191612d83565b820191906000526020600020905b815481529060010190602001808311612d6657829003601f168201915b5050505050815250828281518110612d9d57612d9d6147e5565b6020908102919091010152600101612c9e565b506040805160e08101825263ffffffff8089166000818152600d6020818152868320548086168752948b168187015260ff680100000000000000008604811697870197909752690100000000000000000085048716151560608701529290915290526a010000000000000000000090049091161515608082015260a08101612e3785612e49565b81526020019190915295945050505050565b60606000612ad283613b2d565b6000612ad28383613b89565b608081015173ffffffffffffffffffffffffffffffffffffffff1615612fb057608081015173ffffffffffffffffffffffffffffffffffffffff163b1580612f5b575060808101516040517f01ffc9a70000000000000000000000000000000000000000000000000000000081527f78bea72100000000000000000000000000000000000000000000000000000000600482015273ffffffffffffffffffffffffffffffffffffffff909116906301ffc9a790602401602060405180830381865afa158015612f35573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612f599190614e70565b155b15612fb05760808101516040517fabb5e3fd00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091166004820152602401610609565b600082815260026020526040902081518291908190612fcf9082614a06565b5060208201516001820190612fe49082614a06565b5060408201516002820180547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660018360038111156130265761302661444d565b021790555060608201516002820180547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff1661010083600181111561306d5761306d61444d565b0217905550608091909101516002909101805473ffffffffffffffffffffffffffffffffffffffff90921662010000027fffffffffffffffffffff0000000000000000000000000000000000000000ffff90921691909117905560405182907f04f0a9bcf3f3a3b42a4d7ca081119755f82ebe43e0d30c8f7292c4fe0dc4a2ae90600090a25050565b805163ffffffff9081166000908152600d602090815260408083208286015190941683526001909301905220608082015160ff161580613148575060808201518590613143906001614e8d565b60ff16115b156131915760808201516040517f25b4d61800000000000000000000000000000000000000000000000000000000815260ff909116600482015260248101869052604401610609565b6001826020015163ffffffff16111561327957815163ffffffff166000908152600d6020908152604082209084015160019182019183916131d29190614843565b63ffffffff1663ffffffff168152602001908152602001600020905060005b6131fa82612b5c565b81101561327657613229846000015163ffffffff16600c60006105928587600001612b6690919063ffffffff16565b50600c60006132388484612b66565b8152602081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffff1690556001016131f1565b50505b60005b858110156134b3576132a9878783818110613299576132996147e5565b8592602090910201359050612e56565b61330a5782518787838181106132c1576132c16147e5565b6040517f636e405700000000000000000000000000000000000000000000000000000000815263ffffffff90941660048501526020029190910135602483015250604401610609565b82606001511561346157825163ffffffff16600c6000898985818110613332576133326147e5565b602090810292909201358352508101919091526040016000205468010000000000000000900463ffffffff16148015906133ac5750600c600088888481811061337d5761337d6147e5565b602090810292909201358352508101919091526040016000205468010000000000000000900463ffffffff1615155b1561340e5782518787838181106133c5576133c56147e5565b6040517f60b9df7300000000000000000000000000000000000000000000000000000000815263ffffffff90941660048501526020029190910135602483015250604401610609565b8251600c6000898985818110613426576134266147e5565b90506020020135815260200190815260200160002060000160086101000a81548163ffffffff021916908363ffffffff1602179055506134ab565b82516134a99063ffffffff16600c60008a8a86818110613483576134836147e5565b905060200201358152602001908152602001600020600401612e5690919063ffffffff16565b505b60010161327c565b5060005b838110156138c157368585838181106134d2576134d26147e5565b90506020028101906134e4919061490e565b90506134f260038235612abe565b61352b576040517fe181733f00000000000000000000000000000000000000000000000000000000815281356004820152602401610609565b61353760058235612abe565b15613571576040517ff7d7a29400000000000000000000000000000000000000000000000000000000815281356004820152602401610609565b803560009081526003840160205260408120805461358e90614898565b905011156135da5783516040517f3927d08000000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015281356024820152604401610609565b60005b878110156136e4576136818235600c60008c8c86818110613600576136006147e5565b9050602002013581526020019081526020016000206003016000600c60008e8e88818110613630576136306147e5565b90506020020135815260200190815260200160002060000160049054906101000a900463ffffffff1663ffffffff1663ffffffff168152602001908152602001600020612abe90919063ffffffff16565b6136dc57888882818110613697576136976147e5565b6040517fa7e792500000000000000000000000000000000000000000000000000000000081526020909102929092013560048301525082356024820152604401610609565b6001016135dd565b506002830180546001810182556000918252602091829020833591015561370d90820182614ea6565b8235600090815260038601602052604090209161372b919083614f0b565b50604080850151855163ffffffff9081166000908152600d602090815284822080549415156901000000000000000000027fffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffff90951694909417909355606088015188518316825284822080549115156a0100000000000000000000027fffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffff9092169190911790556080880151885183168252848220805460ff9290921668010000000000000000027fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff909216919091179055828801805189518416835294909120805494909216640100000000027fffffffffffffffffffffffffffffffffffffffffffffffff00000000ffffffff909416939093179055855191516138b892918435908c908c9061387e90880188614ea6565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613bd892505050565b506001016134b7565b50815160208301516040517ff264aae70bf6a9d90e68e0f9b393f4e7fbea67b063b0f336e0b36c15817036519261390b92909163ffffffff92831681529116602082015260400190565b60405180910390a1505050505050565b3373ffffffffffffffffffffffffffffffffffffffff82160361399a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610609565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6000826000018281548110613a2757613a276147e5565b9060005260206000200154905092915050565b60008181526001830160205260408120548015613b23576000613a5e600183615026565b8554909150600090613a7290600190615026565b9050818114613ad7576000866000018281548110613a9257613a926147e5565b9060005260206000200154905080876000018481548110613ab557613ab56147e5565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080613ae857613ae8615039565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050610474565b6000915050610474565b606081600001805480602002602001604051908101604052809291908181526020018280548015613b7d57602002820191906000526020600020905b815481526020019060010190808311613b69575b50505050509050919050565b6000818152600183016020526040812054613bd057508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155610474565b506000610474565b6000848152600260208190526040909120015462010000900473ffffffffffffffffffffffffffffffffffffffff16156124d757600084815260026020819052604091829020015490517ffba64a7c0000000000000000000000000000000000000000000000000000000081526201000090910473ffffffffffffffffffffffffffffffffffffffff169063fba64a7c90613c7f908690869086908b908d90600401615068565b600060405180830381600087803b158015613c9957600080fd5b505af1158015613cad573d6000803e3d6000fd5b50505050505050505050565b508054613cc590614898565b6000825580601f10613cd5575050565b601f016020900490600052602060002090810190612abb9190613d0d565b5080546000825590600052602060002090810190612abb91905b5b80821115613d225760008155600101613d0e565b5090565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040516080810167ffffffffffffffff81118282101715613d7857613d78613d26565b60405290565b60405160a0810167ffffffffffffffff81118282101715613d7857613d78613d26565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715613de857613de8613d26565b604052919050565b600067ffffffffffffffff821115613e0a57613e0a613d26565b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b600082601f830112613e4757600080fd5b8135613e5a613e5582613df0565b613da1565b818152846020838601011115613e6f57600080fd5b816020850160208301376000918101602001919091529392505050565b60008060408385031215613e9f57600080fd5b823567ffffffffffffffff80821115613eb757600080fd5b613ec386838701613e36565b93506020850135915080821115613ed957600080fd5b50613ee685828601613e36565b9150509250929050565b600060208284031215613f0257600080fd5b5035919050565b60005b83811015613f24578181015183820152602001613f0c565b50506000910152565b60008151808452613f45816020860160208601613f09565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b602081526000612ad26020830184613f2d565b60008083601f840112613f9c57600080fd5b50813567ffffffffffffffff811115613fb457600080fd5b6020830191508360208260051b850101111561104157600080fd5b60008060208385031215613fe257600080fd5b823567ffffffffffffffff811115613ff957600080fd5b61400585828601613f8a565b90969095509350505050565b60008151808452602080850194506020840160005b8381101561404257815187529582019590820190600101614026565b509495945050505050565b600082825180855260208086019550808260051b84010181860160005b848110156140ca578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051845284015160408585018190526140b681860183613f2d565b9a86019a945050509083019060010161406a565b5090979650505050505050565b600063ffffffff8083511684528060208401511660208501525060ff604083015116604084015260608201511515606084015260808201511515608084015260a082015160e060a085015261412f60e0850182614011565b905060c083015184820360c0860152614148828261404d565b95945050505050565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c6577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526141b48583516140d7565b9450928501929085019060010161417a565b5092979650505050505050565b803563ffffffff811681146141e757600080fd5b919050565b6000602082840312156141fe57600080fd5b612ad2826141d3565b73ffffffffffffffffffffffffffffffffffffffff8151168252600060208201516040602085015261423c6040850182613f2d565b949350505050565b602081526000612ad26020830184614207565b602081526000612ad260208301846140d7565b6000806040838503121561427d57600080fd5b614286836141d3565b946020939093013593505050565b6040815260006142a76040830185613f2d565b82810360208401526141488185613f2d565b600063ffffffff808351168452602081818501511681860152816040850151166040860152606084015160608601526080840151608086015260a0840151915060e060a086015261430d60e0860183614011565b60c08581015187830391880191909152805180835290830193506000918301905b8083101561434e578451825293830193600192909201919083019061432e565b509695505050505050565b602081526000612ad260208301846142b9565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c6577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526143cf858351614207565b94509285019290850190600101614395565b600080600080604085870312156143f757600080fd5b843567ffffffffffffffff8082111561440f57600080fd5b61441b88838901613f8a565b9096509450602087013591508082111561443457600080fd5b5061444187828801613f8a565b95989497509550505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b805182526000602082015160e0602085015261449b60e0850182613f2d565b9050604083015184820360408601526144b48282613f2d565b9150506060830151600481106144cc576144cc61444d565b60608501526080830151600281106144e6576144e661444d565b8060808601525060a083015161451460a086018273ffffffffffffffffffffffffffffffffffffffff169052565b5060c083015161452860c086018215159052565b509392505050565b602081526000612ad2602083018461447c565b8015158114612abb57600080fd5b803560ff811681146141e757600080fd5b60008060008060008060008060c0898b03121561457e57600080fd5b614587896141d3565b9750602089013567ffffffffffffffff808211156145a457600080fd5b6145b08c838d01613f8a565b909950975060408b01359150808211156145c957600080fd5b506145d68b828c01613f8a565b90965094505060608901356145ea81614543565b925060808901356145fa81614543565b915061460860a08a01614551565b90509295985092959890939650565b600080600080600080600060a0888a03121561463257600080fd5b873567ffffffffffffffff8082111561464a57600080fd5b6146568b838c01613f8a565b909950975060208a013591508082111561466f57600080fd5b5061467c8a828b01613f8a565b909650945050604088013561469081614543565b925060608801356146a081614543565b91506146ae60808901614551565b905092959891949750929550565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c6577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc088860301845261471f85835161447c565b945092850192908501906001016146e5565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b828110156141c6577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526147948583516142b9565b9450928501929085019060010161475a565b803573ffffffffffffffffffffffffffffffffffffffff811681146141e757600080fd5b6000602082840312156147dc57600080fd5b612ad2826147a6565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b63ffffffff8281168282160390808211156107f2576107f2614814565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361489157614891614814565b5060010190565b600181811c908216806148ac57607f821691505b6020821081036148e5577f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b50919050565b600063ffffffff80831681810361490457614904614814565b6001019392505050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc183360301811261494257600080fd5b9190910192915050565b60006040823603121561495e57600080fd5b6040516040810167ffffffffffffffff828210818311171561498257614982613d26565b8160405261498f856147a6565b835260208501359150808211156149a557600080fd5b506149b236828601613e36565b60208301525092915050565b601f821115610697576000816000526020600020601f850160051c810160208610156149e75750805b601f850160051c820191505b818110156124d7578281556001016149f3565b815167ffffffffffffffff811115614a2057614a20613d26565b614a3481614a2e8454614898565b846149be565b602080601f831160018114614a875760008415614a515750858301515b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600386901b1c1916600185901b1785556124d7565b6000858152602081207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08616915b82811015614ad457888601518255948401946001909101908401614ab5565b5085821015614b1057878501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600388901b60f8161c191681555b5050505050600190811b01905550565b600060208284031215614b3257600080fd5b815167ffffffffffffffff811115614b4957600080fd5b8201601f81018413614b5a57600080fd5b8051614b68613e5582613df0565b818152856020838501011115614b7d57600080fd5b614148826020830160208601613f09565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8183360301811261494257600080fd5b600060808236031215614bd457600080fd5b614bdc613d55565b614be5836141d3565b81526020808401358183015260408401356040830152606084013567ffffffffffffffff80821115614c1657600080fd5b9085019036601f830112614c2957600080fd5b813581811115614c3b57614c3b613d26565b8060051b9150614c4c848301613da1565b8181529183018401918481019036841115614c6657600080fd5b938501935b83851015614c8457843582529385019390850190614c6b565b606087015250939695505050505050565b6020808252825182820181905260009190848201906040850190845b81811015614ccd57835183529284019291840191600101614cb1565b50909695505050505050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6183360301811261494257600080fd5b8035600281106141e757600080fd5b600060a08236031215614d2e57600080fd5b614d36613d7e565b823567ffffffffffffffff80821115614d4e57600080fd5b614d5a36838701613e36565b83526020850135915080821115614d7057600080fd5b50614d7d36828601613e36565b602083015250604083013560048110614d9557600080fd5b6040820152614da660608401614d0d565b6060820152614db7608084016147a6565b608082015292915050565b6000602080835260008454614dd681614898565b8060208701526040600180841660008114614df85760018114614e3257614e62565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00851660408a0152604084151560051b8a01019550614e62565b89600052602060002060005b85811015614e595781548b8201860152908301908801614e3e565b8a016040019650505b509398975050505050505050565b600060208284031215614e8257600080fd5b8151612ad281614543565b60ff818116838216019081111561047457610474614814565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112614edb57600080fd5b83018035915067ffffffffffffffff821115614ef657600080fd5b60200191503681900382131561104157600080fd5b67ffffffffffffffff831115614f2357614f23613d26565b614f3783614f318354614898565b836149be565b6000601f841160018114614f895760008515614f535750838201355b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600387901b1c1916600186901b17835561501f565b6000838152602090207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0861690835b82811015614fd85786850135825560209485019460019092019101614fb8565b5086821015615013577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88860031b161c19848701351681555b505060018560011b0183555b5050505050565b8181038181111561047457610474614814565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6080815284608082015260007f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8611156150a157600080fd5b8560051b808860a0850137820182810360a090810160208501526150c790820187613f2d565b91505063ffffffff8085166040840152808416606084015250969550505050505056fea164736f6c6343000818000a", } var CapabilitiesRegistryABI = CapabilitiesRegistryMetaData.ABI diff --git a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt index cae02cda285..5b2288e4faa 100644 --- a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt +++ b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -1,5 +1,5 @@ GETH_VERSION: 1.13.8 -capabilities_registry: ../../../contracts/solc/v0.8.24/CapabilitiesRegistry/CapabilitiesRegistry.abi ../../../contracts/solc/v0.8.24/CapabilitiesRegistry/CapabilitiesRegistry.bin 6d2e3aa3a6f3aed2cf24b613743bb9ae4b9558f48a6864dc03b8b0ebb37235e3 +capabilities_registry: ../../../contracts/solc/v0.8.24/CapabilitiesRegistry/CapabilitiesRegistry.abi ../../../contracts/solc/v0.8.24/CapabilitiesRegistry/CapabilitiesRegistry.bin bb794cc0042784b060d1d63090e2086670b88ba3685067cd436305f36054c82b feeds_consumer: ../../../contracts/solc/v0.8.24/KeystoneFeedsConsumer/KeystoneFeedsConsumer.abi ../../../contracts/solc/v0.8.24/KeystoneFeedsConsumer/KeystoneFeedsConsumer.bin 8c3a2b18a80be41e7c40d2bc3a4c8d1b5e18d55c1fd20ad5af68cebb66109fc5 forwarder: ../../../contracts/solc/v0.8.24/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.24/KeystoneForwarder/KeystoneForwarder.bin 45d9b866c64b41c1349a90b6764aee42a6d078b454d38f369b5fe02b23b9d16e ocr3_capability: ../../../contracts/solc/v0.8.24/OCR3Capability/OCR3Capability.abi ../../../contracts/solc/v0.8.24/OCR3Capability/OCR3Capability.bin 8bf0f53f222efce7143dea6134552eb26ea1eef845407b4475a0d79b7d7ba9f8 From 288257a4f0a0ab714630f266c32953a2b0619c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 9 Aug 2024 22:07:17 +0900 Subject: [PATCH 06/16] multi-chain fixes (#14077) * ocr2: Shouldn't require "publicKey" under the multi-chain codepath * ocr2: Use parent ocrKeyBundleID for publicKey when using multi-chain --- .../__snapshots__/88_gen_jobspecs_test.snap | 4 -- core/scripts/keystone/templates/oracle.toml | 1 - core/services/ocr2/delegate.go | 2 +- core/services/ocr2/validate/validate.go | 19 ------ core/services/ocr2/validate/validate_test.go | 60 ------------------- core/services/ocrcommon/adapters.go | 5 +- core/services/ocrcommon/adapters_test.go | 4 +- 7 files changed, 5 insertions(+), 90 deletions(-) diff --git a/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap b/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap index 21e28f3801c..a4b4e6e3021 100755 --- a/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap +++ b/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap @@ -39,7 +39,6 @@ telemetryType = "plugin" [onchainSigningStrategy] strategyName = 'single-chain' [onchainSigningStrategy.config] -publicKey = '8fa807463ad73f9ee855cfd60ba406dcf98a2855b3dd8af613107b0f6890a707' -------------------------------- Oracle 1: @@ -69,7 +68,6 @@ telemetryType = "plugin" [onchainSigningStrategy] strategyName = 'single-chain' [onchainSigningStrategy.config] -publicKey = '8fa807463ad73f9ee855cfd60ba406dcf98a2855b3dd8af613107b0f6890a707' -------------------------------- Oracle 2: @@ -99,7 +97,6 @@ telemetryType = "plugin" [onchainSigningStrategy] strategyName = 'single-chain' [onchainSigningStrategy.config] -publicKey = '8fa807463ad73f9ee855cfd60ba406dcf98a2855b3dd8af613107b0f6890a707' -------------------------------- Oracle 3: @@ -129,7 +126,6 @@ telemetryType = "plugin" [onchainSigningStrategy] strategyName = 'single-chain' [onchainSigningStrategy.config] -publicKey = '8fa807463ad73f9ee855cfd60ba406dcf98a2855b3dd8af613107b0f6890a707' --- diff --git a/core/scripts/keystone/templates/oracle.toml b/core/scripts/keystone/templates/oracle.toml index f5d539873fe..6049ad925d4 100644 --- a/core/scripts/keystone/templates/oracle.toml +++ b/core/scripts/keystone/templates/oracle.toml @@ -23,4 +23,3 @@ telemetryType = "plugin" [onchainSigningStrategy] strategyName = 'single-chain' [onchainSigningStrategy.config] -publicKey = '8fa807463ad73f9ee855cfd60ba406dcf98a2855b3dd8af613107b0f6890a707' diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 5c44825ca2a..f53ceaefa14 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -772,7 +772,7 @@ func (d *Delegate) newServicesGenericPlugin( } keyBundles[name] = os } - onchainKeyringAdapter, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, lggr) + onchainKeyringAdapter, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, kb.PublicKey(), lggr) if err != nil { return nil, err } diff --git a/core/services/ocr2/validate/validate.go b/core/services/ocr2/validate/validate.go index 8d98a282674..a224249e1e8 100644 --- a/core/services/ocr2/validate/validate.go +++ b/core/services/ocr2/validate/validate.go @@ -197,18 +197,6 @@ func (o *OCR2OnchainSigningStrategy) IsMultiChain() bool { return o.StrategyName == "multi-chain" } -func (o *OCR2OnchainSigningStrategy) PublicKey() (string, error) { - pk, ok := o.Config["publicKey"] - if !ok { - return "", nil - } - pkString, ok := pk.(string) - if !ok { - return "", fmt.Errorf("expected string publicKey value, but got: %T", pk) - } - return pkString, nil -} - func (o *OCR2OnchainSigningStrategy) ConfigCopy() job.JSONConfig { copiedConfig := make(job.JSONConfig) for k, v := range o.Config { @@ -251,13 +239,6 @@ func validateGenericPluginSpec(ctx context.Context, spec *job.OCR2OracleSpec, rc if err != nil { return err } - pk, ossErr := onchainSigningStrategy.PublicKey() - if ossErr != nil { - return ossErr - } - if pk == "" { - return errors.New("generic config invalid: must provide public key for the onchain signing strategy") - } } plugEnv := env.NewPlugin(p.PluginName) diff --git a/core/services/ocr2/validate/validate_test.go b/core/services/ocr2/validate/validate_test.go index b92752c647d..1356e0db628 100644 --- a/core/services/ocr2/validate/validate_test.go +++ b/core/services/ocr2/validate/validate_test.go @@ -49,7 +49,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] juelsPerFeeCoinSource = """ ds1 [type=bridge name=voter_turnout]; @@ -105,7 +104,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] juelsPerFeeCoinSource = """ ds1 [type=bridge name=voter_turnout]; @@ -150,7 +148,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -174,7 +171,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -200,7 +196,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -226,7 +221,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -253,7 +247,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -279,7 +272,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -303,7 +295,6 @@ chainID = 1337 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, assertion: func(t *testing.T, os job.Job, err error) { @@ -344,7 +335,6 @@ answer1 [type=median index=0]; strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] juelsPerFeeCoinSource = """ ds1 [type=bridge name=voter_turnout]; @@ -383,7 +373,6 @@ answer1 [type=median index=0]; strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] juelsPerFeeCoinSource = """ -> @@ -415,7 +404,6 @@ answer1 [type=median index=0]; strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] juelsPerFeeCoinSource = """ ds1 [type=bridge name=voter_turnout]; @@ -429,46 +417,6 @@ chainID = 1337 require.Contains(t, err.Error(), "no such relay blerg supported") }, }, - { - name: "Generic public onchain signing strategy with no public key", - toml: ` -type = "offchainreporting2" -pluginType = "plugin" -schemaVersion = 1 -relay = "evm" -contractID = "0x613a38AC1659769640aaE063C651F48E0250454C" -p2pPeerID = "12D3KooWHfYFQ8hGttAYbMCevQVESEQhzJAqFZokMVtom8bNxwGq" -p2pv2Bootstrappers = [ -"12D3KooWHfYFQ8hGttAYbMCevQVESEQhzJAqFZokMVtom8bNxwGq@127.0.0.1:5001", -] -ocrKeyBundleID = "73e8966a78ca09bb912e9565cfb79fbe8a6048fab1f0cf49b18047c3895e0447" -monitoringEndpoint = "chain.link:4321" -transmitterID = "0xF67D0290337bca0847005C7ffD1BC75BA9AAE6e4" -observationTimeout = "10s" -observationSource = """ -ds1 [type=bridge name=voter_turnout]; -ds1_parse [type=jsonparse path="one,two"]; -ds1_multiply [type=multiply times=1.23]; -ds1 -> ds1_parse -> ds1_multiply -> answer1; -answer1 [type=median index=0]; -""" -[relayConfig] -chainID = 1337 -[onchainSigningStrategy] -strategyName = "single-chain" -[onchainSigningStrategy.config] -evm = "" -publicKey = "" -[pluginConfig] -pluginName = "median" -telemetryType = "median" -OCRVersion=2 -`, - assertion: func(t *testing.T, os job.Job, err error) { - require.Error(t, err) - require.Contains(t, err.Error(), "must provide public key for the onchain signing strategy") - }, - }, { name: "Generic plugin config validation - nothing provided", toml: ` @@ -493,7 +441,6 @@ chainID = 4 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] `, @@ -525,7 +472,6 @@ chainID = 4 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] PluginName="some random name" @@ -559,7 +505,6 @@ chainID = 4 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] PluginName="some random name" @@ -594,7 +539,6 @@ chainID = 4 strategyName = "single-chain" [onchainSigningStrategy.config] evm = "" -publicKey = "0x1234567890123456789012345678901234567890" [pluginConfig] PluginName="some random name" @@ -712,7 +656,6 @@ func TestOCR2OnchainSigningStrategy_Unmarshal(t *testing.T) { strategyName = "single-chain" [onchainSigningStrategy.config] evm = "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17" -publicKey = "0x1234567890123456789012345678901234567890" ` oss := &envelope2{} tree, err := toml.Load(payload) @@ -725,12 +668,9 @@ publicKey = "0x1234567890123456789012345678901234567890" err = json.Unmarshal(b, oss) require.NoError(t, err) - pk, err := oss.OnchainSigningStrategy.PublicKey() - require.NoError(t, err) kbID, err := oss.OnchainSigningStrategy.KeyBundleID("evm") require.NoError(t, err) assert.False(t, oss.OnchainSigningStrategy.IsMultiChain()) - assert.Equal(t, "0x1234567890123456789012345678901234567890", pk) assert.Equal(t, "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17", kbID) } diff --git a/core/services/ocrcommon/adapters.go b/core/services/ocrcommon/adapters.go index 372d9e37f15..53e62be9a07 100644 --- a/core/services/ocrcommon/adapters.go +++ b/core/services/ocrcommon/adapters.go @@ -87,12 +87,11 @@ type OCR3OnchainKeyringMultiChainAdapter struct { lggr logger.Logger } -func NewOCR3OnchainKeyringMultiChainAdapter(ost map[string]ocr2key.KeyBundle, lggr logger.Logger) (*OCR3OnchainKeyringMultiChainAdapter, error) { +func NewOCR3OnchainKeyringMultiChainAdapter(ost map[string]ocr2key.KeyBundle, publicKey ocrtypes.OnchainPublicKey, lggr logger.Logger) (*OCR3OnchainKeyringMultiChainAdapter, error) { if len(ost) == 0 { return nil, errors.New("no key bundles provided") } - // We don't need to check for the existence of `publicKey` in the keyBundles map because it is required on validation on `validate/validate.go` - return &OCR3OnchainKeyringMultiChainAdapter{ost, ost["publicKey"].PublicKey(), lggr}, nil + return &OCR3OnchainKeyringMultiChainAdapter{ost, publicKey, lggr}, nil } func (a *OCR3OnchainKeyringMultiChainAdapter) PublicKey() ocrtypes.OnchainPublicKey { diff --git a/core/services/ocrcommon/adapters_test.go b/core/services/ocrcommon/adapters_test.go index fed854b0b32..e7d45627299 100644 --- a/core/services/ocrcommon/adapters_test.go +++ b/core/services/ocrcommon/adapters_test.go @@ -162,9 +162,9 @@ publicKey = "pub-key" keyBundles[name] = os } - adapter, err := ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, logger.TestLogger(t)) + adapter, err := ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, pk, logger.TestLogger(t)) require.NoError(t, err) - _, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(map[string]ocr2key.KeyBundle{}, logger.TestLogger(t)) + _, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(map[string]ocr2key.KeyBundle{}, pk, logger.TestLogger(t)) require.Error(t, err, "no key bundles provided") sig, err := adapter.Sign(configDigest, seqNr, reportInfo) From 349778bbc6048bbea7e6ac0ae711126922bfe1f5 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 9 Aug 2024 16:16:24 +0200 Subject: [PATCH 07/16] add missing step id to action (#14088) * add missing step id * do not fail if Slither fails * try with not failing generation --- .github/workflows/solidity-foundry-artifacts.yml | 1 + contracts/scripts/ci/generate_slither_report.sh | 14 ++++++-------- contracts/scripts/ci/generate_uml.sh | 2 +- contracts/scripts/ci/select_solc_version.sh | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index e489033d675..50a77e2846b 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -118,6 +118,7 @@ jobs: fetch-depth: 0 - name: Extract Foundry version + id: extract-foundry-version uses: ./.github/actions/detect-solidity-foundry-version with: working-directory: contracts diff --git a/contracts/scripts/ci/generate_slither_report.sh b/contracts/scripts/ci/generate_slither_report.sh index 7fe31d40efc..bc876ae1182 100755 --- a/contracts/scripts/ci/generate_slither_report.sh +++ b/contracts/scripts/ci/generate_slither_report.sh @@ -44,15 +44,13 @@ run_slither() { >&2 echo "::error::Failed to select Solc version for $FILE" return 1 fi - set -e SLITHER_OUTPUT_FILE="$TARGET_DIR/$(basename "${FILE%.sol}")-slither-report.md" - - output=$(slither --config-file "$CONFIG_FILE" "$FILE" --checklist --markdown-root "$REPO_URL" --fail-none $SLITHER_EXTRA_PARAMS) - if [ $? -ne 0 ]; then - >&2 echo "::error::Slither failed for $FILE" - exit 1 + if ! output=$(slither --config-file "$CONFIG_FILE" "$FILE" --checklist --markdown-root "$REPO_URL" --fail-none $SLITHER_EXTRA_PARAMS); then + >&2 echo "::warning::Slither failed for $FILE" + return 0 fi + set -e output=$(echo "$output" | sed '/\*\*THIS CHECKLIST IS NOT COMPLETE\*\*. Use `--show-ignored-findings` to show all the results./d' | sed '/Summary/d') echo "# Summary for $FILE" > "$SLITHER_OUTPUT_FILE" @@ -80,8 +78,8 @@ set +e process_files "$SOURCE_DIR" "$TARGET_DIR" "${FILES[@]}" if [[ $? -ne 0 ]]; then - >&2 echo "::error::Failed to generate Slither reports" - exit 1 + >&2 echo "::warning::Failed to generate some Slither reports" + exit 0 fi echo "Slither reports saved in $TARGET_DIR folder" diff --git a/contracts/scripts/ci/generate_uml.sh b/contracts/scripts/ci/generate_uml.sh index 65745c93bbe..c71d0a1ac7d 100755 --- a/contracts/scripts/ci/generate_uml.sh +++ b/contracts/scripts/ci/generate_uml.sh @@ -88,7 +88,7 @@ process_selected_files() { MATCHES=($(find "$SOURCE_DIR" -type f -path "*/$FILE")) if [[ ${#MATCHES[@]} -gt 1 ]]; then - >&2 echo "Error: Multiple matches found for $FILE:" + >&2 echo "::error:: Multiple matches found for $FILE:" for MATCH in "${MATCHES[@]}"; do >&2 echo " $MATCH" done diff --git a/contracts/scripts/ci/select_solc_version.sh b/contracts/scripts/ci/select_solc_version.sh index 3f7d7864ab7..cfa13de0f60 100755 --- a/contracts/scripts/ci/select_solc_version.sh +++ b/contracts/scripts/ci/select_solc_version.sh @@ -66,7 +66,7 @@ set +e SOLCVER=$(extract_pragma "$FILE") if [[ $? -ne 0 ]]; then - echo "Error: Failed to extract the Solidity version from $FILE." + >&2 echo "::error:: Failed to extract the Solidity version from $FILE." return 1 fi From 95cb692500218a337f5ba05771d240abd9c01c62 Mon Sep 17 00:00:00 2001 From: Bolek <1416262+bolekk@users.noreply.github.com> Date: Fri, 9 Aug 2024 07:54:01 -0700 Subject: [PATCH 08/16] [KS-421] Improve logging from remote capabilities (#14058) Add validations and sanitizing --- core/capabilities/remote/dispatcher.go | 2 +- core/capabilities/remote/target/client.go | 11 +++-- .../capabilities/remote/target/client_test.go | 14 ++++-- .../remote/target/endtoend_test.go | 4 +- .../remote/target/request/client_request.go | 2 +- .../target/request/client_request_test.go | 9 +++- .../remote/target/request/server_request.go | 2 +- core/capabilities/remote/target/server.go | 25 ++++++++--- .../capabilities/remote/target/server_test.go | 16 +++---- core/capabilities/remote/trigger_publisher.go | 6 ++- .../capabilities/remote/trigger_subscriber.go | 4 +- .../remote/trigger_subscriber_test.go | 5 +-- core/capabilities/remote/utils.go | 43 +++++++++++++++++++ core/capabilities/remote/utils_test.go | 23 ++++++++++ 14 files changed, 132 insertions(+), 34 deletions(-) diff --git a/core/capabilities/remote/dispatcher.go b/core/capabilities/remote/dispatcher.go index c1ee5db2944..dab4f6c98bf 100644 --- a/core/capabilities/remote/dispatcher.go +++ b/core/capabilities/remote/dispatcher.go @@ -180,7 +180,7 @@ func (d *dispatcher) receive() { receiver, ok := d.receivers[k] d.mu.RUnlock() if !ok { - d.lggr.Debugw("received message for unregistered capability", "capabilityId", k.capId, "donId", k.donId) + d.lggr.Debugw("received message for unregistered capability", "capabilityId", SanitizeLogString(k.capId), "donId", k.donId) d.tryRespondWithError(msg.Sender, body, types.Error_CAPABILITY_NOT_FOUND) continue } diff --git a/core/capabilities/remote/target/client.go b/core/capabilities/remote/target/client.go index 5b65bf63e44..4273169d23e 100644 --- a/core/capabilities/remote/target/client.go +++ b/core/capabilities/remote/target/client.go @@ -9,6 +9,7 @@ import ( commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target/request" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -151,7 +152,11 @@ func (c *client) Receive(ctx context.Context, msg *types.MessageBody) { c.mutex.Lock() defer c.mutex.Unlock() - messageID := GetMessageID(msg) + messageID, err := GetMessageID(msg) + if err != nil { + c.lggr.Errorw("invalid message ID", "err", err, "id", remote.SanitizeLogString(string(msg.MessageId))) + return + } c.lggr.Debugw("Remote client target receiving message", "messageID", messageID) @@ -167,8 +172,8 @@ func (c *client) Receive(ctx context.Context, msg *types.MessageBody) { } func GetMessageIDForRequest(req commoncap.CapabilityRequest) (string, error) { - if req.Metadata.WorkflowID == "" || req.Metadata.WorkflowExecutionID == "" { - return "", errors.New("workflow ID and workflow execution ID must be set in request metadata") + if !remote.IsValidWorkflowOrExecutionID(req.Metadata.WorkflowID) || !remote.IsValidWorkflowOrExecutionID(req.Metadata.WorkflowExecutionID) { + return "", errors.New("workflow ID and workflow execution ID in request metadata are invalid") } return req.Metadata.WorkflowID + req.Metadata.WorkflowExecutionID, nil diff --git a/core/capabilities/remote/target/client_test.go b/core/capabilities/remote/target/client_test.go index 6d26b51b8ae..2198636a7a2 100644 --- a/core/capabilities/remote/target/client_test.go +++ b/core/capabilities/remote/target/client_test.go @@ -21,6 +21,11 @@ import ( p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" ) +const ( + workflowID1 = "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0" + workflowExecutionID1 = "95ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0abbadeed" +) + func Test_Client_DonTopologies(t *testing.T) { ctx := testutils.Context(t) @@ -192,8 +197,8 @@ func testClient(ctx context.Context, t *testing.T, numWorkflowPeers int, workflo responseCh, err := caller.Execute(ctx, commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, Config: transmissionSchedule, Inputs: executeInputs, @@ -234,7 +239,10 @@ func (t *clientTestServer) Receive(_ context.Context, msg *remotetypes.MessageBo defer t.mux.Unlock() sender := toPeerID(msg.Sender) - messageID := target.GetMessageID(msg) + messageID, err := target.GetMessageID(msg) + if err != nil { + panic(err) + } if t.messageIDToSenders[messageID] == nil { t.messageIDToSenders[messageID] = make(map[p2ptypes.PeerID]bool) diff --git a/core/capabilities/remote/target/endtoend_test.go b/core/capabilities/remote/target/endtoend_test.go index cfab50f0fe7..31bdc83e266 100644 --- a/core/capabilities/remote/target/endtoend_test.go +++ b/core/capabilities/remote/target/endtoend_test.go @@ -261,8 +261,8 @@ func testRemoteTarget(ctx context.Context, t *testing.T, underlying commoncap.Ta responseCh, err := caller.Execute(ctx, commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, Config: transmissionSchedule, Inputs: executeInputs, diff --git a/core/capabilities/remote/target/request/client_request.go b/core/capabilities/remote/target/request/client_request.go index 50a742c2188..0370fd229cf 100644 --- a/core/capabilities/remote/target/request/client_request.go +++ b/core/capabilities/remote/target/request/client_request.go @@ -170,7 +170,7 @@ func (c *ClientRequest) OnMessage(_ context.Context, msg *types.MessageBody) err } } } else { - c.lggr.Warnw("received error response", "error", msg.ErrorMsg) + c.lggr.Warnw("received error response", "error", remote.SanitizeLogString(msg.ErrorMsg)) c.errorCount[msg.ErrorMsg]++ if c.errorCount[msg.ErrorMsg] == c.requiredIdenticalResponses { c.sendResponse(commoncap.CapabilityResponse{Err: errors.New(msg.ErrorMsg)}) diff --git a/core/capabilities/remote/target/request/client_request_test.go b/core/capabilities/remote/target/request/client_request_test.go index 07f43dbc71f..7edb2f5e534 100644 --- a/core/capabilities/remote/target/request/client_request_test.go +++ b/core/capabilities/remote/target/request/client_request_test.go @@ -20,6 +20,11 @@ import ( p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" ) +const ( + workflowID1 = "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0" + workflowExecutionID1 = "95ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0abbadeed" +) + func Test_ClientRequest_MessageValidation(t *testing.T) { lggr := logger.TestLogger(t) @@ -68,8 +73,8 @@ func Test_ClientRequest_MessageValidation(t *testing.T) { capabilityRequest := commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, Inputs: executeInputs, Config: transmissionSchedule, diff --git a/core/capabilities/remote/target/request/server_request.go b/core/capabilities/remote/target/request/server_request.go index b8ae05bc316..16e90a034bc 100644 --- a/core/capabilities/remote/target/request/server_request.go +++ b/core/capabilities/remote/target/request/server_request.go @@ -134,7 +134,7 @@ func (e *ServerRequest) executeRequest(ctx context.Context, payload []byte) erro return fmt.Errorf("failed to marshal capability response: %w", err) } - e.lggr.Debugw("received execution results", "metadata", capabilityRequest.Metadata, "error", capResponse.Err) + e.lggr.Debugw("received execution results", "workflowExecutionID", capabilityRequest.Metadata.WorkflowExecutionID, "error", capResponse.Err) e.setResult(responsePayload) return nil } diff --git a/core/capabilities/remote/target/server.go b/core/capabilities/remote/target/server.go index 39023ffb3fa..56cad3739b6 100644 --- a/core/capabilities/remote/target/server.go +++ b/core/capabilities/remote/target/server.go @@ -11,6 +11,7 @@ import ( commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target/request" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" @@ -129,9 +130,14 @@ func (r *server) Receive(ctx context.Context, msg *types.MessageBody) { r.receiveLock.Lock() defer r.receiveLock.Unlock() - r.lggr.Debugw("received request for msg", "msgId", msg.MessageId) if msg.Method != types.MethodExecute { - r.lggr.Errorw("received request for unsupported method type", "method", msg.Method) + r.lggr.Errorw("received request for unsupported method type", "method", remote.SanitizeLogString(msg.Method)) + return + } + + messageId, err := GetMessageID(msg) + if err != nil { + r.lggr.Errorw("invalid message id", "err", err, "id", remote.SanitizeLogString(string(msg.MessageId))) return } @@ -143,9 +149,10 @@ func (r *server) Receive(ctx context.Context, msg *types.MessageBody) { // A request is uniquely identified by the message id and the hash of the payload to prevent a malicious // actor from sending a different payload with the same message id - messageId := GetMessageID(msg) requestID := messageId + hex.EncodeToString(msgHash[:]) + r.lggr.Debugw("received request", "msgId", msg.MessageId, "requestID", requestID) + if requestIDs, ok := r.messageIDToRequestIDsCount[messageId]; ok { requestIDs[requestID] = requestIDs[requestID] + 1 } else { @@ -156,7 +163,7 @@ func (r *server) Receive(ctx context.Context, msg *types.MessageBody) { if len(requestIDs) > 1 { // This is a potential attack vector as well as a situation that will occur if the client is sending non-deterministic payloads // so a warning is logged - r.lggr.Warnw("received messages with the same id and different payloads", "messageID", messageId, "requestIDToCount", requestIDs) + r.lggr.Warnw("received messages with the same id and different payloads", "messageID", messageId, "lenRequestIDs", len(requestIDs)) } if _, ok := r.requestIDToRequest[requestID]; !ok { @@ -177,7 +184,7 @@ func (r *server) Receive(ctx context.Context, msg *types.MessageBody) { err = reqAndMsgID.request.OnMessage(ctx, msg) if err != nil { - r.lggr.Errorw("request failed to OnMessage new message", "request", reqAndMsgID, "err", err) + r.lggr.Errorw("request failed to OnMessage new message", "messageID", reqAndMsgID.messageID, "err", err) } } @@ -201,8 +208,12 @@ func (r *server) getMessageHash(msg *types.MessageBody) ([32]byte, error) { return hash, nil } -func GetMessageID(msg *types.MessageBody) string { - return string(msg.MessageId) +func GetMessageID(msg *types.MessageBody) (string, error) { + idStr := string(msg.MessageId) + if !remote.IsValidID(idStr) { + return "", fmt.Errorf("invalid message id") + } + return idStr, nil } func (r *server) Ready() error { diff --git a/core/capabilities/remote/target/server_test.go b/core/capabilities/remote/target/server_test.go index 2460a2dd0f7..505a2dcce5d 100644 --- a/core/capabilities/remote/target/server_test.go +++ b/core/capabilities/remote/target/server_test.go @@ -39,8 +39,8 @@ func Test_Server_ExcludesNonDeterministicInputAttributes(t *testing.T) { _, err = caller.Execute(context.Background(), commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, Inputs: inputs, }) @@ -67,8 +67,8 @@ func Test_Server_RespondsAfterSufficientRequests(t *testing.T) { _, err := caller.Execute(context.Background(), commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, }) require.NoError(t, err) @@ -94,8 +94,8 @@ func Test_Server_InsufficientCallers(t *testing.T) { _, err := caller.Execute(context.Background(), commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, }) require.NoError(t, err) @@ -121,8 +121,8 @@ func Test_Server_CapabilityError(t *testing.T) { _, err := caller.Execute(context.Background(), commoncap.CapabilityRequest{ Metadata: commoncap.RequestMetadata{ - WorkflowID: "workflowID", - WorkflowExecutionID: "workflowExecutionID", + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, }, }) require.NoError(t, err) diff --git a/core/capabilities/remote/trigger_publisher.go b/core/capabilities/remote/trigger_publisher.go index 146b8789689..b4d749754d4 100644 --- a/core/capabilities/remote/trigger_publisher.go +++ b/core/capabilities/remote/trigger_publisher.go @@ -102,6 +102,10 @@ func (p *triggerPublisher) Receive(_ context.Context, msg *types.MessageBody) { p.lggr.Errorw("sender not a member of its workflow DON", "capabilityId", p.capInfo.ID, "callerDonId", msg.CallerDonId, "sender", sender) return } + if !IsValidWorkflowOrExecutionID(req.Metadata.WorkflowID) { + p.lggr.Errorw("received trigger request with invalid workflow ID", "capabilityId", p.capInfo.ID, "workflowId", SanitizeLogString(req.Metadata.WorkflowID)) + return + } p.lggr.Debugw("received trigger registration", "capabilityId", p.capInfo.ID, "workflowId", req.Metadata.WorkflowID, "sender", sender) key := registrationKey{msg.CallerDonId, req.Metadata.WorkflowID} nowMs := time.Now().UnixMilli() @@ -145,7 +149,7 @@ func (p *triggerPublisher) Receive(_ context.Context, msg *types.MessageBody) { p.lggr.Errorw("failed to register trigger", "capabilityId", p.capInfo.ID, "workflowId", req.Metadata.WorkflowID, "err", err) } } else { - p.lggr.Errorw("received trigger request with unknown method", "method", msg.Method, "sender", sender) + p.lggr.Errorw("received trigger request with unknown method", "method", SanitizeLogString(msg.Method), "sender", sender) } } diff --git a/core/capabilities/remote/trigger_subscriber.go b/core/capabilities/remote/trigger_subscriber.go index 2d038e45c08..d957614886a 100644 --- a/core/capabilities/remote/trigger_subscriber.go +++ b/core/capabilities/remote/trigger_subscriber.go @@ -189,7 +189,7 @@ func (s *triggerSubscriber) Receive(_ context.Context, msg *types.MessageBody) { registration, found := s.registeredWorkflows[workflowId] s.mu.RUnlock() if !found { - s.lggr.Errorw("received message for unregistered workflow", "capabilityId", s.capInfo.ID, "workflowID", workflowId, "sender", sender) + s.lggr.Errorw("received message for unregistered workflow", "capabilityId", s.capInfo.ID, "workflowID", SanitizeLogString(workflowId), "sender", sender) continue } key := triggerEventKey{ @@ -217,7 +217,7 @@ func (s *triggerSubscriber) Receive(_ context.Context, msg *types.MessageBody) { } } } else { - s.lggr.Errorw("received trigger event with unknown method", "method", msg.Method, "sender", sender) + s.lggr.Errorw("received trigger event with unknown method", "method", SanitizeLogString(msg.Method), "sender", sender) } } diff --git a/core/capabilities/remote/trigger_subscriber_test.go b/core/capabilities/remote/trigger_subscriber_test.go index 2e34b03ec5c..c834a271d56 100644 --- a/core/capabilities/remote/trigger_subscriber_test.go +++ b/core/capabilities/remote/trigger_subscriber_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/values" @@ -22,7 +21,7 @@ import ( const ( peerID1 = "12D3KooWF3dVeJ6YoT5HFnYhmwQWWMoEwVFzJQ5kKCMX3ZityxMC" peerID2 = "12D3KooWQsmok6aD8PZqt3RnJhQRrNzKHLficq7zYFRp7kZ1hHP8" - workflowID1 = "workflowID1" + workflowID1 = "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0" ) var ( @@ -63,7 +62,7 @@ func TestTriggerSubscriber_RegisterAndReceive(t *testing.T) { }) // register trigger - config := &capabilities.RemoteTriggerConfig{ + config := &commoncap.RemoteTriggerConfig{ RegistrationRefresh: 100 * time.Millisecond, RegistrationExpiry: 100 * time.Second, MinResponsesToAggregate: 1, diff --git a/core/capabilities/remote/utils.go b/core/capabilities/remote/utils.go index dba24b843cc..10e4e3082c9 100644 --- a/core/capabilities/remote/utils.go +++ b/core/capabilities/remote/utils.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "unicode" "google.golang.org/protobuf/proto" @@ -16,6 +17,12 @@ import ( p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" ) +const ( + maxLoggedStringLen = 256 + validWorkflowIDLen = 64 + maxIDLen = 128 +) + func ValidateMessage(msg p2ptypes.Message, expectedReceiver p2ptypes.PeerID) (*remotetypes.MessageBody, error) { var topLevelMessage remotetypes.Message err := proto.Unmarshal(msg.Payload, &topLevelMessage) @@ -93,3 +100,39 @@ func AggregateModeRaw(elemList [][]byte, minIdenticalResponses uint32) ([]byte, } return found, nil } + +func SanitizeLogString(s string) string { + tooLongSuffix := "" + if len(s) > maxLoggedStringLen { + s = s[:maxLoggedStringLen] + tooLongSuffix = " [TRUNCATED]" + } + for i := 0; i < len(s); i++ { + if !unicode.IsPrint(rune(s[i])) { + return "[UNPRINTABLE] " + hex.EncodeToString([]byte(s)) + tooLongSuffix + } + } + return s + tooLongSuffix +} + +// Workflow IDs and Execution IDs are 32-byte hex-encoded strings +func IsValidWorkflowOrExecutionID(id string) bool { + if len(id) != validWorkflowIDLen { + return false + } + _, err := hex.DecodeString(id) + return err == nil +} + +// Trigger event IDs and message IDs can only contain printable characters and must be non-empty +func IsValidID(id string) bool { + if len(id) == 0 || len(id) > maxIDLen { + return false + } + for i := 0; i < len(id); i++ { + if !unicode.IsPrint(rune(id[i])) { + return false + } + } + return true +} diff --git a/core/capabilities/remote/utils_test.go b/core/capabilities/remote/utils_test.go index 8bebf71fb66..177ab5a7d14 100644 --- a/core/capabilities/remote/utils_test.go +++ b/core/capabilities/remote/utils_test.go @@ -118,3 +118,26 @@ func TestDefaultModeAggregator_Aggregate(t *testing.T) { require.NoError(t, err) require.Equal(t, res, capResponse1) } + +func TestSanitizeLogString(t *testing.T) { + require.Equal(t, "hello", remote.SanitizeLogString("hello")) + require.Equal(t, "[UNPRINTABLE] 0a", remote.SanitizeLogString("\n")) + + longString := "" + for i := 0; i < 100; i++ { + longString += "aa-aa-aa-" + } + require.Equal(t, longString[:256]+" [TRUNCATED]", remote.SanitizeLogString(longString)) +} + +func TestIsValidWorkflowID(t *testing.T) { + require.False(t, remote.IsValidWorkflowOrExecutionID("too_short")) + require.False(t, remote.IsValidWorkflowOrExecutionID("nothex--95ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0")) + require.True(t, remote.IsValidWorkflowOrExecutionID("15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0")) +} + +func TestIsValidTriggerEventID(t *testing.T) { + require.False(t, remote.IsValidID("")) + require.False(t, remote.IsValidID("\n\n")) + require.True(t, remote.IsValidID("id_id_2")) +} From bd648bd73df2a1de91a463a988f4c5b61e74b240 Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko <34754799+dhaidashenko@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:05:10 +0200 Subject: [PATCH 09/16] Custom Astar finality (#14021) * Custom Astar finality * fix merge artifact * fix lint issue * simplify isRequestingFinalizedBlock * avoid iterating through the whole batch again * fix errors wrapping --- .changeset/warm-houses-build.md | 5 + core/chains/evm/client/chain_client.go | 4 + core/chains/evm/client/chain_client_test.go | 4 +- core/chains/evm/client/evm_client.go | 4 +- core/chains/evm/client/helpers_test.go | 4 +- core/chains/evm/client/rpc_client.go | 170 +++++++++++++++--- core/chains/evm/client/rpc_client_test.go | 115 +++++++++++- core/chains/evm/config/chaintype/chaintype.go | 6 +- core/chains/evm/testutils/client.go | 8 +- core/services/chainlink/config_test.go | 4 +- core/services/ocr/contract_tracker.go | 2 +- 11 files changed, 277 insertions(+), 49 deletions(-) create mode 100644 .changeset/warm-houses-build.md diff --git a/.changeset/warm-houses-build.md b/.changeset/warm-houses-build.md new file mode 100644 index 00000000000..6ce6215a88c --- /dev/null +++ b/.changeset/warm-houses-build.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Added custom finality calculation for Astar #internal diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index c39214471ce..c27d294ebfd 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -160,9 +160,13 @@ func (c *chainClient) BalanceAt(ctx context.Context, account common.Address, blo return c.multiNode.BalanceAt(ctx, account, blockNumber) } +// BatchCallContext - sends all given requests as a single batch. // Request specific errors for batch calls are returned to the individual BatchElem. // Ensure the same BatchElem slice provided by the caller is passed through the call stack // to ensure the caller has access to the errors. +// Note: some chains (e.g Astar) have custom finality requests, so even when FinalityTagEnabled=true, finality tag +// might not be properly handled and returned results might have weaker finality guarantees. It's highly recommended +// to use HeadTracker to identify latest finalized block. func (c *chainClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { return c.multiNode.BatchCallContext(ctx, b) } diff --git a/core/chains/evm/client/chain_client_test.go b/core/chains/evm/client/chain_client_test.go index a0b89cabbc0..47041e40e91 100644 --- a/core/chains/evm/client/chain_client_test.go +++ b/core/chains/evm/client/chain_client_test.go @@ -328,7 +328,7 @@ func TestEthClient_HeaderByNumber(t *testing.T) { `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, {"happy parity", expectedBlockNum, expectedBlockNum.Int64(), nil, `{"author":"0xd1aeb42885a43b72b518182ef893125814811048","difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa00f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","0x880ece08ea8c49dfd9"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, - {"missing header", expectedBlockNum, 0, fmt.Errorf("no live nodes available for chain %s", testutils.FixtureChainID.String()), + {"missing header", expectedBlockNum, 0, fmt.Errorf("RPCClient returned error (eth-primary-rpc-0): not found"), `null`}, } @@ -366,7 +366,7 @@ func TestEthClient_HeaderByNumber(t *testing.T) { ctx, cancel := context.WithTimeout(tests.Context(t), 5*time.Second) result, err := ethClient.HeadByNumber(ctx, expectedBlockNum) if test.error != nil { - require.Error(t, err, test.error) + require.EqualError(t, err, test.error.Error()) } else { require.NoError(t, err) require.Equal(t, expectedBlockHash, result.Hash.Hex()) diff --git a/core/chains/evm/client/evm_client.go b/core/chains/evm/client/evm_client.go index 36768086833..1fd533d6aab 100644 --- a/core/chains/evm/client/evm_client.go +++ b/core/chains/evm/client/evm_client.go @@ -22,13 +22,13 @@ func NewEvmClient(cfg evmconfig.NodePool, chainCfg commonclient.ChainConfig, cli for i, node := range nodes { if node.SendOnly != nil && *node.SendOnly { rpc := NewRPCClient(lggr, empty, (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, - commonclient.Secondary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout) + commonclient.Secondary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) sendonly := commonclient.NewSendOnlyNode(lggr, (url.URL)(*node.HTTPURL), *node.Name, chainID, rpc) sendonlys = append(sendonlys, sendonly) } else { rpc := NewRPCClient(lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), - chainID, commonclient.Primary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout) + chainID, commonclient.Primary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) primaryNode := commonclient.NewNode(cfg, chainCfg, lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, *node.Order, rpc, "EVM") diff --git a/core/chains/evm/client/helpers_test.go b/core/chains/evm/client/helpers_test.go index 8caacb4190a..e996ccc5e4f 100644 --- a/core/chains/evm/client/helpers_test.go +++ b/core/chains/evm/client/helpers_test.go @@ -140,7 +140,7 @@ func NewChainClientWithTestNode( } lggr := logger.Test(t) - rpc := NewRPCClient(lggr, *parsed, rpcHTTPURL, "eth-primary-rpc-0", id, chainID, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := NewRPCClient(lggr, *parsed, rpcHTTPURL, "eth-primary-rpc-0", id, chainID, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") n := commonclient.NewNode[*big.Int, *evmtypes.Head, RPCClient]( nodeCfg, clientMocks.ChainConfig{NoNewHeadsThresholdVal: noNewHeadsThreshold}, lggr, *parsed, rpcHTTPURL, "eth-primary-node-0", id, chainID, 1, rpc, "EVM") @@ -152,7 +152,7 @@ func NewChainClientWithTestNode( return nil, pkgerrors.Errorf("sendonly ethereum rpc url scheme must be http(s): %s", u.String()) } var empty url.URL - rpc := NewRPCClient(lggr, empty, &sendonlyRPCURLs[i], fmt.Sprintf("eth-sendonly-rpc-%d", i), id, chainID, commonclient.Secondary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := NewRPCClient(lggr, empty, &sendonlyRPCURLs[i], fmt.Sprintf("eth-sendonly-rpc-%d", i), id, chainID, commonclient.Secondary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") s := commonclient.NewSendOnlyNode[*big.Int, RPCClient]( lggr, u, fmt.Sprintf("eth-sendonly-%d", i), chainID, rpc) sendonlys = append(sendonlys, s) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 200703dd42f..07aa86fc450 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -2,6 +2,7 @@ package client import ( "context" + "encoding/json" "errors" "fmt" "math/big" @@ -28,6 +29,7 @@ import ( commonclient "github.com/smartcontractkit/chainlink/v2/common/client" commontypes "github.com/smartcontractkit/chainlink/v2/common/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" @@ -120,6 +122,7 @@ type rpcClient struct { largePayloadRpcTimeout time.Duration rpcTimeout time.Duration finalizedBlockPollInterval time.Duration + chainType chaintype.ChainType ws rawclient http *rawclient @@ -156,10 +159,12 @@ func NewRPCClient( finalizedBlockPollInterval time.Duration, largePayloadRpcTimeout time.Duration, rpcTimeout time.Duration, + chainType chaintype.ChainType, ) RPCClient { r := &rpcClient{ largePayloadRpcTimeout: largePayloadRpcTimeout, rpcTimeout: rpcTimeout, + chainType: chainType, } r.name = name r.id = id @@ -396,8 +401,28 @@ func (r *rpcClient) CallContext(ctx context.Context, result interface{}, method return err } -func (r *rpcClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.largePayloadRpcTimeout) +func (r *rpcClient) BatchCallContext(rootCtx context.Context, b []rpc.BatchElem) error { + // Astar's finality tags provide weaker finality guarantees than we require. + // Fetch latest finalized block using Astar's custom requests and populate it after batch request completes + var astarRawLatestFinalizedBlock json.RawMessage + var requestedFinalizedBlock bool + if r.chainType == chaintype.ChainAstar { + for _, el := range b { + if !isRequestingFinalizedBlock(el) { + continue + } + + requestedFinalizedBlock = true + err := r.astarLatestFinalizedBlock(rootCtx, &astarRawLatestFinalizedBlock) + if err != nil { + return fmt.Errorf("failed to get astar latest finalized block: %w", err) + } + + break + } + } + + ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(rootCtx, r.largePayloadRpcTimeout) defer cancel() lggr := r.newRqLggr().With("nBatchElems", len(b), "batchElems", b) @@ -412,8 +437,46 @@ func (r *rpcClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) err duration := time.Since(start) r.logResult(lggr, err, duration, r.getRPCDomain(), "BatchCallContext") + if err != nil { + return err + } - return err + if r.chainType == chaintype.ChainAstar && requestedFinalizedBlock { + // populate requested finalized block with correct value + for _, el := range b { + if !isRequestingFinalizedBlock(el) { + continue + } + + el.Error = nil + err = json.Unmarshal(astarRawLatestFinalizedBlock, el.Result) + if err != nil { + el.Error = fmt.Errorf("failed to unmarshal astar finalized block into provided struct: %w", err) + } + } + } + + return nil +} + +func isRequestingFinalizedBlock(el rpc.BatchElem) bool { + isGetBlock := el.Method == "eth_getBlockByNumber" && len(el.Args) > 0 + if !isGetBlock { + return false + } + + if el.Args[0] == rpc.FinalizedBlockNumber { + return true + } + + switch arg := el.Args[0].(type) { + case string: + return arg == rpc.FinalizedBlockNumber.String() + case fmt.Stringer: + return arg.String() == rpc.FinalizedBlockNumber.String() + default: + return false + } } // TODO: Full transition from SubscribeNewHead to SubscribeToHeads is done in BCI-2875 @@ -601,17 +664,84 @@ func (r *rpcClient) HeaderByHash(ctx context.Context, hash common.Hash) (header return } -func (r *rpcClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, error) { - return r.blockByNumber(ctx, rpc.FinalizedBlockNumber.String()) +func (r *rpcClient) LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) { + // capture chStopInFlight to ensure we are not updating chainInfo with observations related to previous life cycle + ctx, cancel, chStopInFlight, _, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) + defer cancel() + if r.chainType == chaintype.ChainAstar { + // astar's finality tags provide weaker guarantee. Use their custom request to request latest finalized block + err = r.astarLatestFinalizedBlock(ctx, &head) + } else { + err = r.ethGetBlockByNumber(ctx, rpc.FinalizedBlockNumber.String(), &head) + } + + if err != nil { + return + } + + if head == nil { + err = r.wrapRPCClientError(ethereum.NotFound) + return + } + + head.EVMChainID = ubig.New(r.chainID) + + r.onNewFinalizedHead(ctx, chStopInFlight, head) + return +} + +func (r *rpcClient) astarLatestFinalizedBlock(ctx context.Context, result interface{}) (err error) { + var hashResult string + err = r.CallContext(ctx, &hashResult, "chain_getFinalizedHead") + if err != nil { + return fmt.Errorf("failed to get astar latest finalized hash: %w", err) + } + + var astarHead struct { + Number *hexutil.Big `json:"number"` + } + err = r.CallContext(ctx, &astarHead, "chain_getHeader", hashResult, false) + if err != nil { + return fmt.Errorf("failed to get astar head by hash: %w", err) + } + + if astarHead.Number == nil { + return r.wrapRPCClientError(fmt.Errorf("expected non empty head number of finalized block")) + } + + err = r.ethGetBlockByNumber(ctx, astarHead.Number.String(), result) + if err != nil { + return fmt.Errorf("failed to get astar finalized block: %w", err) + } + + return nil } func (r *rpcClient) BlockByNumber(ctx context.Context, number *big.Int) (head *evmtypes.Head, err error) { - hex := ToBlockNumArg(number) - return r.blockByNumber(ctx, hex) + ctx, cancel, chStopInFlight, _, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) + defer cancel() + hexNumber := ToBlockNumArg(number) + err = r.ethGetBlockByNumber(ctx, hexNumber, &head) + if err != nil { + return + } + + if head == nil { + err = r.wrapRPCClientError(ethereum.NotFound) + return + } + + head.EVMChainID = ubig.New(r.chainID) + + if hexNumber == rpc.LatestBlockNumber.String() { + r.onNewHead(ctx, chStopInFlight, head) + } + + return } -func (r *rpcClient) blockByNumber(ctx context.Context, number string) (head *evmtypes.Head, err error) { - ctx, cancel, chStopInFlight, ws, http := r.acquireQueryCtx(ctx, r.rpcTimeout) +func (r *rpcClient) ethGetBlockByNumber(ctx context.Context, number string, result interface{}) (err error) { + ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) defer cancel() const method = "eth_getBlockByNumber" args := []interface{}{number, false} @@ -623,30 +753,14 @@ func (r *rpcClient) blockByNumber(ctx context.Context, number string) (head *evm lggr.Debug("RPC call: evmclient.Client#CallContext") start := time.Now() if http != nil { - err = r.wrapHTTP(http.rpc.CallContext(ctx, &head, method, args...)) + err = r.wrapHTTP(http.rpc.CallContext(ctx, result, method, args...)) } else { - err = r.wrapWS(ws.rpc.CallContext(ctx, &head, method, args...)) + err = r.wrapWS(ws.rpc.CallContext(ctx, result, method, args...)) } duration := time.Since(start) r.logResult(lggr, err, duration, r.getRPCDomain(), "CallContext") - if err != nil { - return nil, err - } - if head == nil { - err = r.wrapRPCClientError(ethereum.NotFound) - return - } - head.EVMChainID = ubig.New(r.chainID) - - switch number { - case rpc.FinalizedBlockNumber.String(): - r.onNewFinalizedHead(ctx, chStopInFlight, head) - case rpc.LatestBlockNumber.String(): - r.onNewHead(ctx, chStopInFlight, head) - } - - return + return err } func (r *rpcClient) BlockByHash(ctx context.Context, hash common.Hash) (head *evmtypes.Head, err error) { diff --git a/core/chains/evm/client/rpc_client_test.go b/core/chains/evm/client/rpc_client_test.go index d6a11e0d013..12821880996 100644 --- a/core/chains/evm/client/rpc_client_test.go +++ b/core/chains/evm/client/rpc_client_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -23,6 +24,7 @@ import ( commonclient "github.com/smartcontractkit/chainlink/v2/common/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) @@ -58,7 +60,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) // set to default values @@ -108,7 +110,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) ch := make(chan *evmtypes.Head) @@ -131,7 +133,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { t.Run("Block's chain ID matched configured", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) ch := make(chan *evmtypes.Head) @@ -148,7 +150,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { }) wsURL := server.WSURL() observedLggr, observed := logger.TestObserved(t, zap.DebugLevel) - rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) server.Close() _, err := rpc.SubscribeNewHead(ctx, make(chan *evmtypes.Head)) @@ -158,7 +160,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { t.Run("Subscription error is properly wrapper", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) sub, err := rpc.SubscribeNewHead(ctx, make(chan *evmtypes.Head)) @@ -186,7 +188,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { }) wsURL := server.WSURL() observedLggr, observed := logger.TestObserved(t, zap.DebugLevel) - rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) server.Close() _, err := rpc.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) @@ -203,7 +205,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { return resp }) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) sub, err := rpc.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) @@ -252,7 +254,7 @@ func TestRPCClient_LatestFinalizedBlock(t *testing.T) { } server := createRPCServer() - rpc := client.NewRPCClient(lggr, *server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout) + rpc := client.NewRPCClient(lggr, *server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) defer rpc.Close() server.Head = &evmtypes.Head{Number: 128} @@ -362,7 +364,7 @@ func TestRpcClientLargePayloadTimeout(t *testing.T) { // use something unreasonably large for RPC timeout to ensure that we use largePayloadRPCTimeout const rpcTimeout = time.Hour const largePayloadRPCTimeout = tests.TestInterval - rpc := client.NewRPCClient(logger.Test(t), *rpcURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, largePayloadRPCTimeout, rpcTimeout) + rpc := client.NewRPCClient(logger.Test(t), *rpcURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, largePayloadRPCTimeout, rpcTimeout, "") require.NoError(t, rpc.Dial(ctx)) defer rpc.Close() err := testCase.Fn(ctx, rpc) @@ -370,3 +372,98 @@ func TestRpcClientLargePayloadTimeout(t *testing.T) { }) } } + +func TestAstarCustomFinality(t *testing.T) { + t.Parallel() + + chainId := big.NewInt(123456) + // create new server that returns 4 block for Astar custom finality and 8 block for finality tag. + wsURL := testutils.NewWSServer(t, chainId, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "chain_getFinalizedHead": + resp.Result = `"0xf14c499253fd7bbcba142e5dd77dad8b5ad598c1dc414a66bacdd8dae14a6759"` + case "chain_getHeader": + if assert.True(t, params.IsArray()) && assert.Equal(t, "0xf14c499253fd7bbcba142e5dd77dad8b5ad598c1dc414a66bacdd8dae14a6759", params.Array()[0].String()) { + resp.Result = `{"parentHash":"0x1311773bc6b4efc8f438ed1f094524b2a1233baf8a35396f641fcc42a378fc62","number":"0x4","stateRoot":"0x0e4920dc5516b587e1f74a0b65963134523a12cc11478bb314e52895758fbfa2","extrinsicsRoot":"0x5b02446dcab0659eb07d4a38f28f181c1b78a71b2aba207bb0ea1f0f3468e6bd","digest":{"logs":["0x066175726120ad678e0800000000","0x04525053529023158dc8e8fd0180bf26d88233a3d94eed2f4e43480395f0809f28791965e4d34e9b3905","0x0466726f6e88017441e97acf83f555e0deefef86db636bc8a37eb84747603412884e4df4d2280400","0x056175726101018a0a57edf70cc5474323114a47ee1e7f645b8beea5a1560a996416458e89f42bdf4955e24d32b5da54e1bf628aaa7ce4b8c0fa2b95c175a139d88786af12a88c"]}}` + } + case "eth_getBlockByNumber": + assert.True(t, params.IsArray()) + switch params.Array()[0].String() { + case "0x4": + resp.Result = `{"author":"0x5accb3bf9194a5f81b2087d4bd6ac47c62775d49","baseFeePerGas":"0xb576270823","difficulty":"0x0","extraData":"0x","gasLimit":"0xe4e1c0","gasUsed":"0x0","hash":"0x7441e97acf83f555e0deefef86db636bc8a37eb84747603412884e4df4d22804","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x5accb3bf9194a5f81b2087d4bd6ac47c62775d49","nonce":"0x0000000000000000","number":"0x4","parentHash":"0x6ba069c318b692bf2cc0bd7ea070a9382a20c2f52413c10554b57c2e381bf2bb","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x201","stateRoot":"0x17c46d359b9af773312c747f1d20032c67658d9a2923799f00533b73789cf49b","timestamp":"0x66acdc22","totalDifficulty":"0x0","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}` + case "finalized": + resp.Result = `{"author":"0x1687736326c9fea17e25fc5287613693c912909c","baseFeePerGas":"0x3b9aca00","difficulty":"0x0","extraData":"0x","gasLimit":"0xe4e1c0","gasUsed":"0x0","hash":"0x62f03413681948b06882e7d9f91c4949bc39ded98d36336ab03faea038ec8e3d","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x1687736326c9fea17e25fc5287613693c912909c","nonce":"0x0000000000000000","number":"0x8","parentHash":"0x43f504afdc639cbb8daf5fd5328a37762164b73f9c70ed54e1928c1fca6d8f23","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x200","stateRoot":"0x0cb938d51ad83bdf401e3f5f7f989e60df64fdea620d394af41a3e72629f7495","timestamp":"0x61bd8d1a","totalDifficulty":"0x0","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}` + default: + assert.Fail(t, fmt.Sprintf("unexpected eth_getBlockByNumber param: %v", params.Array())) + } + default: + assert.Fail(t, fmt.Sprintf("unexpected method: %s", method)) + } + return + }).WSURL() + + const expectedFinalizedBlockNumber = int64(4) + const expectedFinalizedBlockHash = "0x7441e97acf83f555e0deefef86db636bc8a37eb84747603412884e4df4d22804" + rpcClient := client.NewRPCClient(logger.Test(t), *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, chaintype.ChainAstar) + defer rpcClient.Close() + err := rpcClient.Dial(tests.Context(t)) + require.NoError(t, err) + + testCases := []struct { + Name string + GetLatestFinalized func(ctx context.Context) (*evmtypes.Head, error) + }{ + { + Name: "Direct LatestFinalized call", + GetLatestFinalized: func(ctx context.Context) (*evmtypes.Head, error) { + return rpcClient.LatestFinalizedBlock(ctx) + }, + }, + { + Name: "BatchCallContext with Finalized tag as string", + GetLatestFinalized: func(ctx context.Context) (*evmtypes.Head, error) { + result := &evmtypes.Head{} + req := rpc.BatchElem{ + Method: "eth_getBlockByNumber", + Args: []interface{}{rpc.FinalizedBlockNumber.String(), false}, + Result: result, + } + err := rpcClient.BatchCallContext(ctx, []rpc.BatchElem{ + req, + }) + if err != nil { + return nil, err + } + + return result, req.Error + }, + }, + { + Name: "BatchCallContext with Finalized tag as BlockNumber", + GetLatestFinalized: func(ctx context.Context) (*evmtypes.Head, error) { + result := &evmtypes.Head{} + req := rpc.BatchElem{ + Method: "eth_getBlockByNumber", + Args: []interface{}{rpc.FinalizedBlockNumber, false}, + Result: result, + } + err := rpcClient.BatchCallContext(ctx, []rpc.BatchElem{req}) + if err != nil { + return nil, err + } + + return result, req.Error + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + lf, err := testCase.GetLatestFinalized(tests.Context(t)) + require.NoError(t, err) + require.NotNil(t, lf) + assert.Equal(t, expectedFinalizedBlockHash, lf.Hash.String()) + assert.Equal(t, expectedFinalizedBlockNumber, lf.Number) + }) + } +} diff --git a/core/chains/evm/config/chaintype/chaintype.go b/core/chains/evm/config/chaintype/chaintype.go index 623a80f54f2..07ea6206241 100644 --- a/core/chains/evm/config/chaintype/chaintype.go +++ b/core/chains/evm/config/chaintype/chaintype.go @@ -9,6 +9,7 @@ type ChainType string const ( ChainArbitrum ChainType = "arbitrum" + ChainAstar ChainType = "astar" ChainCelo ChainType = "celo" ChainGnosis ChainType = "gnosis" ChainHedera ChainType = "hedera" @@ -36,7 +37,7 @@ func (c ChainType) IsL2() bool { func (c ChainType) IsValid() bool { switch c { - case "", ChainArbitrum, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync: + case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync: return true } return false @@ -46,6 +47,8 @@ func ChainTypeFromSlug(slug string) ChainType { switch slug { case "arbitrum": return ChainArbitrum + case "astar": + return ChainAstar case "celo": return ChainCelo case "gnosis": @@ -121,6 +124,7 @@ func (c *ChainTypeConfig) String() string { var ErrInvalidChainType = fmt.Errorf("must be one of %s or omitted", strings.Join([]string{ string(ChainArbitrum), + string(ChainAstar), string(ChainCelo), string(ChainGnosis), string(ChainHedera), diff --git a/core/chains/evm/testutils/client.go b/core/chains/evm/testutils/client.go index 89c97b01e6d..1e5523fbff9 100644 --- a/core/chains/evm/testutils/client.go +++ b/core/chains/evm/testutils/client.go @@ -148,8 +148,12 @@ func (ts *testWSServer) newWSHandler(chainID *big.Int, callback JSONRPCHandler) ts.t.Log("Received message", string(data)) req := gjson.ParseBytes(data) if !req.IsObject() { - ts.t.Logf("Request must be object: %v", req.Type) - return + if isSingleObjectArray := req.IsArray() && len(req.Array()) == 1; !isSingleObjectArray { + ts.t.Logf("Request must be object: %v", req.Type) + return + } + + req = req.Array()[0] } if e := req.Get("error"); e.Exists() { ts.t.Logf("Received jsonrpc error: %v", e) diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 0038be8a979..f5a9d335928 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -1304,7 +1304,7 @@ func TestConfig_Validate(t *testing.T) { - 1: 10 errors: - ChainType: invalid value (Foo): must not be set with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Foo): must be one of arbitrum, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted + - ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted - HeadTracker.HistoryDepth: invalid value (30): must be greater than or equal to FinalizedBlockOffset - GasEstimator.BumpThreshold: invalid value (0): cannot be 0 if auto-purge feature is enabled for Foo - Transactions.AutoPurge.Threshold: missing: needs to be set if auto-purge feature is enabled for Foo @@ -1317,7 +1317,7 @@ func TestConfig_Validate(t *testing.T) { - 2: 5 errors: - ChainType: invalid value (Arbitrum): only "optimismBedrock" can be used with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Arbitrum): must be one of arbitrum, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted + - ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted - FinalityDepth: invalid value (0): must be greater than or equal to 1 - MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1 - 3.Nodes: 5 errors: diff --git a/core/services/ocr/contract_tracker.go b/core/services/ocr/contract_tracker.go index 6651e4b65d9..d7199874a9f 100644 --- a/core/services/ocr/contract_tracker.go +++ b/core/services/ocr/contract_tracker.go @@ -399,7 +399,7 @@ func (t *OCRContractTracker) LatestBlockHeight(ctx context.Context) (blockheight // care about the block height; we have no way of getting the L1 block // height anyway return 0, nil - case "", chaintype.ChainArbitrum, chaintype.ChainCelo, chaintype.ChainGnosis, chaintype.ChainHedera, chaintype.ChainKroma, chaintype.ChainOptimismBedrock, chaintype.ChainScroll, chaintype.ChainWeMix, chaintype.ChainXLayer, chaintype.ChainZkEvm, chaintype.ChainZkSync: + case "", chaintype.ChainArbitrum, chaintype.ChainAstar, chaintype.ChainCelo, chaintype.ChainGnosis, chaintype.ChainHedera, chaintype.ChainKroma, chaintype.ChainOptimismBedrock, chaintype.ChainScroll, chaintype.ChainWeMix, chaintype.ChainXLayer, chaintype.ChainZkEvm, chaintype.ChainZkSync: // continue } latestBlockHeight := t.getLatestBlockHeight() From c330defde2211aa4a0d8392f867400a829220b2f Mon Sep 17 00:00:00 2001 From: Juan Farber Date: Fri, 9 Aug 2024 14:47:24 -0300 Subject: [PATCH 10/16] [BCI-3573] - Remove dependence on FinalityDepth in EVM TXM code (#13794) * removing finality within common/txmgr * remove finality from common/txmgr comment * latestFinalizedBlockNum support within evm/txmgr txstore * refactor tests after evm/txmgr txstore changes * mocks for both common and core/evm * remove comments that are still referencing to finalityDepth within evm/txmgr * add changeset * error if no LatestFinalizedHead was found * fix mocks version * fix mocks version * mock versioning * mock version * mock version * mock version * mock version * inject head tracker trimmed API * fix tests * mocks * remove unnecesary tests as finalized head is guaranteed * ht in shell local * remove evm dep from confirmer and use generics instead * make markOldTxesMissingReceiptAsErrored condition inclusive * use already initialized head tracker within shell local * fix mocking, fix unit test * fix a potential bug * fix bug * refactor * address comments * fix lint * rename * have log back * update comments * remove nil check * minor * fix test * grammar * rephrase --------- Co-authored-by: Joe Huang --- .changeset/chatty-spiders-double.md | 5 + common/txmgr/confirmer.go | 66 ++++++--- common/txmgr/types/mocks/tx_store.go | 22 +-- common/txmgr/types/tx_store.go | 2 +- core/chains/evm/txmgr/builder.go | 13 +- core/chains/evm/txmgr/confirmer_test.go | 144 ++++++++++++-------- core/chains/evm/txmgr/evm_tx_store.go | 27 ++-- core/chains/evm/txmgr/evm_tx_store_test.go | 7 +- core/chains/evm/txmgr/mocks/evm_tx_store.go | 22 +-- core/chains/evm/txmgr/txmgr_test.go | 1 + core/cmd/shell_local.go | 2 +- core/cmd/shell_local_test.go | 3 +- core/internal/cltest/mocks.go | 1 + 13 files changed, 191 insertions(+), 124 deletions(-) create mode 100644 .changeset/chatty-spiders-double.md diff --git a/.changeset/chatty-spiders-double.md b/.changeset/chatty-spiders-double.md new file mode 100644 index 00000000000..750a11628fe --- /dev/null +++ b/.changeset/chatty-spiders-double.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +remove dependency on FinalityDepth in EVM TXM code. #internal diff --git a/common/txmgr/confirmer.go b/common/txmgr/confirmer.go index 1e3922fdbfb..3b421191782 100644 --- a/common/txmgr/confirmer.go +++ b/common/txmgr/confirmer.go @@ -102,6 +102,10 @@ var ( }, []string{"chainID"}) ) +type confirmerHeadTracker[HEAD types.Head[BLOCK_HASH], BLOCK_HASH types.Hashable] interface { + LatestAndFinalizedBlock(ctx context.Context) (latest, finalized HEAD, err error) +} + // Confirmer is a broad service which performs four different tasks in sequence on every new longest chain // Step 1: Mark that all currently pending transaction attempts were broadcast before this block // Step 2: Check pending transactions for receipts @@ -133,14 +137,15 @@ type Confirmer[ ks txmgrtypes.KeyStore[ADDR, CHAIN_ID, SEQ] enabledAddresses []ADDR - mb *mailbox.Mailbox[HEAD] - stopCh services.StopChan - wg sync.WaitGroup - initSync sync.Mutex - isStarted bool - + mb *mailbox.Mailbox[HEAD] + stopCh services.StopChan + wg sync.WaitGroup + initSync sync.Mutex + isStarted bool nConsecutiveBlocksChainTooShort int isReceiptNil func(R) bool + + headTracker confirmerHeadTracker[HEAD, BLOCK_HASH] } func NewConfirmer[ @@ -164,6 +169,7 @@ func NewConfirmer[ lggr logger.Logger, isReceiptNil func(R) bool, stuckTxDetector txmgrtypes.StuckTxDetector[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE], + headTracker confirmerHeadTracker[HEAD, BLOCK_HASH], ) *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { lggr = logger.Named(lggr, "Confirmer") return &Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]{ @@ -181,6 +187,7 @@ func NewConfirmer[ mb: mailbox.NewSingle[HEAD](), isReceiptNil: isReceiptNil, stuckTxDetector: stuckTxDetector, + headTracker: headTracker, } } @@ -297,7 +304,20 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) pro return fmt.Errorf("CheckConfirmedMissingReceipt failed: %w", err) } - if err := ec.CheckForReceipts(ctx, head.BlockNumber()); err != nil { + _, latestFinalizedHead, err := ec.headTracker.LatestAndFinalizedBlock(ctx) + if err != nil { + return fmt.Errorf("failed to retrieve latest finalized head: %w", err) + } + + if !latestFinalizedHead.IsValid() { + return fmt.Errorf("latest finalized head is not valid") + } + + if latestFinalizedHead.BlockNumber() > head.BlockNumber() { + ec.lggr.Debugw("processHead received old block", "latestFinalizedHead", latestFinalizedHead.BlockNumber(), "headNum", head.BlockNumber(), "time", time.Since(mark), "id", "confirmer") + } + + if err := ec.CheckForReceipts(ctx, head.BlockNumber(), latestFinalizedHead.BlockNumber()); err != nil { return fmt.Errorf("CheckForReceipts failed: %w", err) } @@ -318,7 +338,7 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) pro ec.lggr.Debugw("Finished RebroadcastWhereNecessary", "headNum", head.BlockNumber(), "time", time.Since(mark), "id", "confirmer") mark = time.Now() - if err := ec.EnsureConfirmedTransactionsInLongestChain(ctx, head); err != nil { + if err := ec.EnsureConfirmedTransactionsInLongestChain(ctx, head, latestFinalizedHead.BlockNumber()); err != nil { return fmt.Errorf("EnsureConfirmedTransactionsInLongestChain failed: %w", err) } @@ -395,8 +415,8 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) Che return } -// CheckForReceipts finds attempts that are still pending and checks to see if a receipt is present for the given block number -func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) CheckForReceipts(ctx context.Context, blockNum int64) error { +// CheckForReceipts finds attempts that are still pending and checks to see if a receipt is present for the given block number. +func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) CheckForReceipts(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64) error { attempts, err := ec.txStore.FindTxAttemptsRequiringReceiptFetch(ctx, ec.chainID) if err != nil { return fmt.Errorf("FindTxAttemptsRequiringReceiptFetch failed: %w", err) @@ -443,7 +463,7 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) Che return fmt.Errorf("unable to mark txes as 'confirmed_missing_receipt': %w", err) } - if err := ec.txStore.MarkOldTxesMissingReceiptAsErrored(ctx, blockNum, ec.chainConfig.FinalityDepth(), ec.chainID); err != nil { + if err := ec.txStore.MarkOldTxesMissingReceiptAsErrored(ctx, blockNum, latestFinalizedBlockNum, ec.chainID); err != nil { return fmt.Errorf("unable to confirm buried unconfirmed txes': %w", err) } return nil @@ -1004,22 +1024,30 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) han } } -// EnsureConfirmedTransactionsInLongestChain finds all confirmed txes up to the depth +// EnsureConfirmedTransactionsInLongestChain finds all confirmed txes up to the earliest head // of the given chain and ensures that every one has a receipt with a block hash that is // in the given chain. // // If any of the confirmed transactions does not have a receipt in the chain, it has been // re-org'd out and will be rebroadcast. -func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) EnsureConfirmedTransactionsInLongestChain(ctx context.Context, head types.Head[BLOCK_HASH]) error { - if head.ChainLength() < ec.chainConfig.FinalityDepth() { - logArgs := []interface{}{ - "chainLength", head.ChainLength(), "finalityDepth", ec.chainConfig.FinalityDepth(), - } +func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) EnsureConfirmedTransactionsInLongestChain(ctx context.Context, head types.Head[BLOCK_HASH], latestFinalizedHeadNumber int64) error { + logArgs := []interface{}{ + "chainLength", head.ChainLength(), "latestFinalizedHead number", latestFinalizedHeadNumber, + } + + if head.BlockNumber() < latestFinalizedHeadNumber { + errMsg := "current head is shorter than latest finalized head" + ec.lggr.Errorw(errMsg, append(logArgs, "head block number", head.BlockNumber())...) + return errors.New(errMsg) + } + + calculatedFinalityDepth := uint32(head.BlockNumber() - latestFinalizedHeadNumber) + if head.ChainLength() < calculatedFinalityDepth { if ec.nConsecutiveBlocksChainTooShort > logAfterNConsecutiveBlocksChainTooShort { - warnMsg := "Chain length supplied for re-org detection was shorter than FinalityDepth. Re-org protection is not working properly. This could indicate a problem with the remote RPC endpoint, a compatibility issue with a particular blockchain, a bug with this particular blockchain, heads table being truncated too early, remote node out of sync, or something else. If this happens a lot please raise a bug with the Chainlink team including a log output sample and details of the chain and RPC endpoint you are using." + warnMsg := "Chain length supplied for re-org detection was shorter than the depth from the latest head to the finalized head. Re-org protection is not working properly. This could indicate a problem with the remote RPC endpoint, a compatibility issue with a particular blockchain, a bug with this particular blockchain, heads table being truncated too early, remote node out of sync, or something else. If this happens a lot please raise a bug with the Chainlink team including a log output sample and details of the chain and RPC endpoint you are using." ec.lggr.Warnw(warnMsg, append(logArgs, "nConsecutiveBlocksChainTooShort", ec.nConsecutiveBlocksChainTooShort)...) } else { - logMsg := "Chain length supplied for re-org detection was shorter than FinalityDepth" + logMsg := "Chain length supplied for re-org detection was shorter than the depth from the latest head to the finalized head" ec.lggr.Debugw(logMsg, append(logArgs, "nConsecutiveBlocksChainTooShort", ec.nConsecutiveBlocksChainTooShort)...) } ec.nConsecutiveBlocksChainTooShort++ diff --git a/common/txmgr/types/mocks/tx_store.go b/common/txmgr/types/mocks/tx_store.go index 0b9c7110660..4467729e167 100644 --- a/common/txmgr/types/mocks/tx_store.go +++ b/common/txmgr/types/mocks/tx_store.go @@ -1854,17 +1854,17 @@ func (_c *TxStore_MarkAllConfirmedMissingReceipt_Call[ADDR, CHAIN_ID, TX_HASH, B return _c } -// MarkOldTxesMissingReceiptAsErrored provides a mock function with given fields: ctx, blockNum, finalityDepth, chainID -func (_m *TxStore[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, finalityDepth uint32, chainID CHAIN_ID) error { - ret := _m.Called(ctx, blockNum, finalityDepth, chainID) +// MarkOldTxesMissingReceiptAsErrored provides a mock function with given fields: ctx, blockNum, latestFinalizedBlockNum, chainID +func (_m *TxStore[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64, chainID CHAIN_ID) error { + ret := _m.Called(ctx, blockNum, latestFinalizedBlockNum, chainID) if len(ret) == 0 { panic("no return value specified for MarkOldTxesMissingReceiptAsErrored") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64, uint32, CHAIN_ID) error); ok { - r0 = rf(ctx, blockNum, finalityDepth, chainID) + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, CHAIN_ID) error); ok { + r0 = rf(ctx, blockNum, latestFinalizedBlockNum, chainID) } else { r0 = ret.Error(0) } @@ -1880,15 +1880,15 @@ type TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR types.Hashable, CHAIN_ // MarkOldTxesMissingReceiptAsErrored is a helper method to define mock.On call // - ctx context.Context // - blockNum int64 -// - finalityDepth uint32 +// - latestFinalizedBlockNum int64 // - chainID CHAIN_ID -func (_e *TxStore_Expecter[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) MarkOldTxesMissingReceiptAsErrored(ctx interface{}, blockNum interface{}, finalityDepth interface{}, chainID interface{}) *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { - return &TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]{Call: _e.mock.On("MarkOldTxesMissingReceiptAsErrored", ctx, blockNum, finalityDepth, chainID)} +func (_e *TxStore_Expecter[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) MarkOldTxesMissingReceiptAsErrored(ctx interface{}, blockNum interface{}, latestFinalizedBlockNum interface{}, chainID interface{}) *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { + return &TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]{Call: _e.mock.On("MarkOldTxesMissingReceiptAsErrored", ctx, blockNum, latestFinalizedBlockNum, chainID)} } -func (_c *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) Run(run func(ctx context.Context, blockNum int64, finalityDepth uint32, chainID CHAIN_ID)) *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { +func (_c *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) Run(run func(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64, chainID CHAIN_ID)) *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(int64), args[2].(uint32), args[3].(CHAIN_ID)) + run(args[0].(context.Context), args[1].(int64), args[2].(int64), args[3].(CHAIN_ID)) }) return _c } @@ -1898,7 +1898,7 @@ func (_c *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HAS return _c } -func (_c *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) RunAndReturn(run func(context.Context, int64, uint32, CHAIN_ID) error) *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { +func (_c *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) RunAndReturn(run func(context.Context, int64, int64, CHAIN_ID) error) *TxStore_MarkOldTxesMissingReceiptAsErrored_Call[ADDR, CHAIN_ID, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { _c.Call.Return(run) return _c } diff --git a/common/txmgr/types/tx_store.go b/common/txmgr/types/tx_store.go index 63b56dd169a..5489a57e636 100644 --- a/common/txmgr/types/tx_store.go +++ b/common/txmgr/types/tx_store.go @@ -89,7 +89,7 @@ type TransactionStore[ HasInProgressTransaction(ctx context.Context, account ADDR, chainID CHAIN_ID) (exists bool, err error) LoadTxAttempts(ctx context.Context, etx *Tx[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE]) error MarkAllConfirmedMissingReceipt(ctx context.Context, chainID CHAIN_ID) (err error) - MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, finalityDepth uint32, chainID CHAIN_ID) error + MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64, chainID CHAIN_ID) error PreloadTxes(ctx context.Context, attempts []TxAttempt[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE]) error SaveConfirmedMissingReceiptAttempt(ctx context.Context, timeout time.Duration, attempt *TxAttempt[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE], broadcastAt time.Time) error SaveInProgressAttempt(ctx context.Context, attempt *TxAttempt[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE]) error diff --git a/core/chains/evm/txmgr/builder.go b/core/chains/evm/txmgr/builder.go index d85d6acdc8c..cbfb8775cfb 100644 --- a/core/chains/evm/txmgr/builder.go +++ b/core/chains/evm/txmgr/builder.go @@ -1,6 +1,7 @@ package txmgr import ( + "context" "math/big" "time" @@ -13,12 +14,15 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/forwarders" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" - httypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/keystore" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) +type latestAndFinalizedBlockHeadTracker interface { + LatestAndFinalizedBlock(ctx context.Context) (latest, finalized *evmtypes.Head, err error) +} + // NewTxm constructs the necessary dependencies for the EvmTxm (broadcaster, confirmer, etc) and returns a new EvmTxManager func NewTxm( ds sqlutil.DataSource, @@ -33,7 +37,7 @@ func NewTxm( logPoller logpoller.LogPoller, keyStore keystore.Eth, estimator gas.EvmFeeEstimator, - headTracker httypes.HeadTracker, + headTracker latestAndFinalizedBlockHeadTracker, ) (txm TxManager, err error, ) { @@ -55,7 +59,7 @@ func NewTxm( evmBroadcaster := NewEvmBroadcaster(txStore, txmClient, txmCfg, feeCfg, txConfig, listenerConfig, keyStore, txAttemptBuilder, lggr, checker, chainConfig.NonceAutoSync(), chainConfig.ChainType()) evmTracker := NewEvmTracker(txStore, keyStore, chainID, lggr) stuckTxDetector := NewStuckTxDetector(lggr, client.ConfiguredChainID(), chainConfig.ChainType(), fCfg.PriceMax(), txConfig.AutoPurge(), estimator, txStore, client) - evmConfirmer := NewEvmConfirmer(txStore, txmClient, txmCfg, feeCfg, txConfig, dbConfig, keyStore, txAttemptBuilder, lggr, stuckTxDetector) + evmConfirmer := NewEvmConfirmer(txStore, txmClient, txmCfg, feeCfg, txConfig, dbConfig, keyStore, txAttemptBuilder, lggr, stuckTxDetector, headTracker) evmFinalizer := NewEvmFinalizer(lggr, client.ConfiguredChainID(), chainConfig.RPCDefaultBatchSize(), txStore, client, headTracker) var evmResender *Resender if txConfig.ResendAfterThreshold() > 0 { @@ -116,8 +120,9 @@ func NewEvmConfirmer( txAttemptBuilder TxAttemptBuilder, lggr logger.Logger, stuckTxDetector StuckTxDetector, + headTracker latestAndFinalizedBlockHeadTracker, ) *Confirmer { - return txmgr.NewConfirmer(txStore, client, chainConfig, feeConfig, txConfig, dbConfig, keystore, txAttemptBuilder, lggr, func(r *evmtypes.Receipt) bool { return r == nil }, stuckTxDetector) + return txmgr.NewConfirmer(txStore, client, chainConfig, feeConfig, txConfig, dbConfig, keystore, txAttemptBuilder, lggr, func(r *evmtypes.Receipt) bool { return r == nil }, stuckTxDetector, headTracker) } // NewEvmTracker instantiates a new EVM tracker for abandoned transactions diff --git a/core/chains/evm/txmgr/confirmer_test.go b/core/chains/evm/txmgr/confirmer_test.go index 6b107b222a6..cce6dc8fc65 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -34,6 +34,7 @@ import ( evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" gasmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/keystore" ksmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/keystore/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" @@ -131,7 +132,8 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, feeEstimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) - ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector) + ht := headtracker.NewSimulatedHeadTracker(ethClient, true, 0) + ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector, ht) ctx := tests.Context(t) // Can't close unstarted instance @@ -145,19 +147,27 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { // Can't start an already started instance err = ec.Start(ctx) require.Error(t, err) + + latestFinalizedHead := evmtypes.Head{ + Number: 8, + Hash: testutils.NewHash(), + Parent: nil, + IsFinalized: true, // We are guaranteed to receive a latestFinalizedHead. + } + head := evmtypes.Head{ Hash: testutils.NewHash(), Number: 10, Parent: &evmtypes.Head{ Hash: testutils.NewHash(), Number: 9, - Parent: &evmtypes.Head{ - Number: 8, - Hash: testutils.NewHash(), - Parent: nil, - }, + Parent: &latestFinalizedHead, }, } + + ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(&head, nil).Once() + ethClient.On("LatestFinalizedBlock", mock.Anything).Return(&latestFinalizedHead, nil).Once() + err = ec.ProcessHead(ctx, &head) require.NoError(t, err) // Can successfully close once @@ -199,6 +209,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { nonce := int64(0) ctx := tests.Context(t) blockNum := int64(0) + latestFinalizedBlockNum := int64(0) t.Run("only finds eth_txes in unconfirmed state with at least one broadcast attempt", func(t *testing.T) { mustInsertFatalErrorEthTx(t, txStore, fromAddress) @@ -211,7 +222,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { mustCreateUnstartedGeneratedTx(t, txStore, fromAddress, config.EVM().ChainID()) // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) }) etx1 := cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, nonce, fromAddress) @@ -232,7 +243,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) var err error etx1, err = txStore.FindTxWithAttempts(ctx, etx1.ID) @@ -261,7 +272,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // No error because it is merely logged - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) etx, err := txStore.FindTxWithAttempts(ctx, etx1.ID) require.NoError(t, err) @@ -289,7 +300,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // No error because it is merely logged - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) etx, err := txStore.FindTxWithAttempts(ctx, etx1.ID) require.NoError(t, err) @@ -326,7 +337,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // Check that the receipt was saved etx, err := txStore.FindTxWithAttempts(ctx, etx1.ID) @@ -388,7 +399,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // Check that the state was updated etx, err := txStore.FindTxWithAttempts(ctx, etx2.ID) @@ -416,7 +427,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // No receipt, but no error either etx, err := txStore.FindTxWithAttempts(ctx, etx3.ID) @@ -443,7 +454,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // No receipt, but no error either etx, err := txStore.FindTxWithAttempts(ctx, etx3.ID) @@ -472,7 +483,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // Check that the receipt was unchanged etx, err := txStore.FindTxWithAttempts(ctx, etx3.ID) @@ -523,7 +534,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // Check that the state was updated var err error @@ -576,7 +587,7 @@ func TestEthConfirmer_CheckForReceipts(t *testing.T) { }).Once() // Do the thing - require.NoError(t, ec.CheckForReceipts(ctx, blockNum)) + require.NoError(t, ec.CheckForReceipts(ctx, blockNum, latestFinalizedBlockNum)) // Check that the state was updated etx5, err = txStore.FindTxWithAttempts(ctx, etx5.ID) @@ -614,6 +625,7 @@ func TestEthConfirmer_CheckForReceipts_batching(t *testing.T) { etx := cltest.MustInsertUnconfirmedEthTx(t, txStore, 0, fromAddress) var attempts []txmgr.TxAttempt + latestFinalizedBlockNum := int64(0) // Total of 5 attempts should lead to 3 batched fetches (2, 2, 1) for i := 0; i < 5; i++ { @@ -650,7 +662,7 @@ func TestEthConfirmer_CheckForReceipts_batching(t *testing.T) { elems[0].Result = &evmtypes.Receipt{} }).Once() - require.NoError(t, ec.CheckForReceipts(ctx, 42)) + require.NoError(t, ec.CheckForReceipts(ctx, 42, latestFinalizedBlockNum)) } func TestEthConfirmer_CheckForReceipts_HandlesNonFwdTxsWithForwardingEnabled(t *testing.T) { @@ -671,6 +683,8 @@ func TestEthConfirmer_CheckForReceipts_HandlesNonFwdTxsWithForwardingEnabled(t * _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore) ec := newEthConfirmer(t, txStore, ethClient, cfg, evmcfg, ethKeyStore, nil) ctx := tests.Context(t) + latestFinalizedBlockNum := int64(0) + // tx is not forwarded and doesn't have meta set. EthConfirmer should handle nil meta values etx := cltest.MustInsertUnconfirmedEthTx(t, txStore, 0, fromAddress) attempt := newBroadcastLegacyEthTxAttempt(t, etx.ID, 2) @@ -697,7 +711,7 @@ func TestEthConfirmer_CheckForReceipts_HandlesNonFwdTxsWithForwardingEnabled(t * *(elems[0].Result.(*evmtypes.Receipt)) = txmReceipt // confirmed }).Once() - require.NoError(t, ec.CheckForReceipts(ctx, 42)) + require.NoError(t, ec.CheckForReceipts(ctx, 42, latestFinalizedBlockNum)) // Check receipt is inserted correctly. dbtx, err = txStore.FindTxWithAttempts(ctx, etx.ID) @@ -724,6 +738,7 @@ func TestEthConfirmer_CheckForReceipts_only_likely_confirmed(t *testing.T) { ec := newEthConfirmer(t, txStore, ethClient, cfg, evmcfg, ethKeyStore, nil) ctx := tests.Context(t) + latestFinalizedBlockNum := int64(0) var attempts []txmgr.TxAttempt // inserting in DESC nonce order to test DB ASC ordering @@ -755,7 +770,7 @@ func TestEthConfirmer_CheckForReceipts_only_likely_confirmed(t *testing.T) { elems[3].Result = &evmtypes.Receipt{} }).Once() - require.NoError(t, ec.CheckForReceipts(ctx, 42)) + require.NoError(t, ec.CheckForReceipts(ctx, 42, latestFinalizedBlockNum)) cltest.BatchElemMustMatchParams(t, captured[0], attempts[0].Hash, "eth_getTransactionReceipt") cltest.BatchElemMustMatchParams(t, captured[1], attempts[1].Hash, "eth_getTransactionReceipt") @@ -778,6 +793,7 @@ func TestEthConfirmer_CheckForReceipts_should_not_check_for_likely_unconfirmed(t ec := newEthConfirmer(t, txStore, ethClient, gconfig, config, ethKeyStore, nil) ctx := tests.Context(t) + latestFinalizedBlockNum := int64(0) etx := cltest.MustInsertUnconfirmedEthTx(t, txStore, 1, fromAddress) for i := 0; i < 4; i++ { @@ -788,7 +804,7 @@ func TestEthConfirmer_CheckForReceipts_should_not_check_for_likely_unconfirmed(t // latest nonce is lower that all attempts' nonces ethClient.On("SequenceAt", mock.Anything, mock.Anything, mock.Anything).Return(evmtypes.Nonce(0), nil) - require.NoError(t, ec.CheckForReceipts(ctx, 42)) + require.NoError(t, ec.CheckForReceipts(ctx, 42, latestFinalizedBlockNum)) } func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt_scoped_to_key(t *testing.T) { @@ -809,6 +825,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt_scoped_to_key(t ec := newEthConfirmer(t, txStore, ethClient, cfg, evmcfg, ethKeyStore, nil) ctx := tests.Context(t) + latestFinalizedBlockNum := int64(0) // STATE // key 1, tx with nonce 0 is unconfirmed @@ -832,7 +849,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt_scoped_to_key(t *(elems[0].Result.(*evmtypes.Receipt)) = txmReceipt2_9 }).Once() - require.NoError(t, ec.CheckForReceipts(ctx, 10)) + require.NoError(t, ec.CheckForReceipts(ctx, 10, latestFinalizedBlockNum)) mustTxBeInState(t, txStore, etx1_0, txmgrcommon.TxUnconfirmed) mustTxBeInState(t, txStore, etx1_1, txmgrcommon.TxUnconfirmed) @@ -850,7 +867,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt_scoped_to_key(t *(elems[0].Result.(*evmtypes.Receipt)) = txmReceipt1_1 }).Once() - require.NoError(t, ec.CheckForReceipts(ctx, 11)) + require.NoError(t, ec.CheckForReceipts(ctx, 11, latestFinalizedBlockNum)) mustTxBeInState(t, txStore, etx1_0, txmgrcommon.TxConfirmedMissingReceipt) mustTxBeInState(t, txStore, etx1_1, txmgrcommon.TxConfirmed) @@ -861,9 +878,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { t.Parallel() db := pgtest.NewSqlxDB(t) - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].FinalityDepth = ptr[uint32](50) - }) + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) {}) txStore := cltest.NewTestTxStore(t, db) ethKeyStore := cltest.NewKeyStore(t, db).Eth() @@ -876,6 +891,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { ec := newEthConfirmer(t, txStore, ethClient, cfg, evmcfg, ethKeyStore, nil) ctx := tests.Context(t) + latestFinalizedBlockNum := int64(0) // STATE // eth_txes with nonce 0 has two attempts (broadcast before block 21 and 41) the first of which will get a receipt @@ -949,7 +965,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { // PERFORM // Block num of 43 is one higher than the receipt (as would generally be expected) - require.NoError(t, ec.CheckForReceipts(ctx, 43)) + require.NoError(t, ec.CheckForReceipts(ctx, 43, latestFinalizedBlockNum)) // Expected state is that the "top" eth_tx is now confirmed, with the // two below it "confirmed_missing_receipt" and the "bottom" eth_tx also confirmed @@ -1009,7 +1025,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { // PERFORM // Block num of 44 is one higher than the receipt (as would generally be expected) - require.NoError(t, ec.CheckForReceipts(ctx, 44)) + require.NoError(t, ec.CheckForReceipts(ctx, 44, latestFinalizedBlockNum)) // Expected state is that the "top" two eth_txes are now confirmed, with the // one below it still "confirmed_missing_receipt" and the bottom one remains confirmed @@ -1038,7 +1054,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { // eth_txes with nonce 2 is confirmed // eth_txes with nonce 3 is confirmed - t.Run("continues to leave eth_txes with state 'confirmed_missing_receipt' unchanged if at least one attempt is above EVM.FinalityDepth", func(t *testing.T) { + t.Run("continues to leave eth_txes with state 'confirmed_missing_receipt' unchanged if at least one attempt is above LatestFinalizedBlockNum", func(t *testing.T) { ethClient.On("SequenceAt", mock.Anything, mock.Anything, mock.Anything).Return(evmtypes.Nonce(10), nil) ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { return len(b) == 2 && @@ -1051,9 +1067,11 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { elems[1].Result = &evmtypes.Receipt{} }).Once() + latestFinalizedBlockNum = 30 + // PERFORM // Block num of 80 puts the first attempt (21) below threshold but second attempt (41) still above - require.NoError(t, ec.CheckForReceipts(ctx, 80)) + require.NoError(t, ec.CheckForReceipts(ctx, 80, latestFinalizedBlockNum)) // Expected state is that the "top" two eth_txes are now confirmed, with the // one below it still "confirmed_missing_receipt" and the bottom one remains confirmed @@ -1078,7 +1096,7 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { // eth_txes with nonce 2 is confirmed // eth_txes with nonce 3 is confirmed - t.Run("marks eth_Txes with state 'confirmed_missing_receipt' as 'errored' if a receipt fails to show up and all attempts are buried deeper than EVM.FinalityDepth", func(t *testing.T) { + t.Run("marks eth_Txes with state 'confirmed_missing_receipt' as 'errored' if a receipt fails to show up and all attempts are buried deeper than LatestFinalizedBlockNum", func(t *testing.T) { ethClient.On("SequenceAt", mock.Anything, mock.Anything, mock.Anything).Return(evmtypes.Nonce(10), nil) ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { return len(b) == 2 && @@ -1091,9 +1109,11 @@ func TestEthConfirmer_CheckForReceipts_confirmed_missing_receipt(t *testing.T) { elems[1].Result = &evmtypes.Receipt{} }).Once() + latestFinalizedBlockNum = 50 + // PERFORM // Block num of 100 puts the first attempt (21) and second attempt (41) below threshold - require.NoError(t, ec.CheckForReceipts(ctx, 100)) + require.NoError(t, ec.CheckForReceipts(ctx, 100, latestFinalizedBlockNum)) // Expected state is that the "top" two eth_txes are now confirmed, with the // one below it marked as "fatal_error" and the bottom one remains confirmed @@ -1117,9 +1137,7 @@ func TestEthConfirmer_CheckConfirmedMissingReceipt(t *testing.T) { t.Parallel() db := pgtest.NewSqlxDB(t) - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].FinalityDepth = ptr[uint32](50) - }) + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) {}) txStore := cltest.NewTestTxStore(t, db) ethKeyStore := cltest.NewKeyStore(t, db).Eth() @@ -1197,9 +1215,7 @@ func TestEthConfirmer_CheckConfirmedMissingReceipt_batchSendTransactions_fails(t t.Parallel() db := pgtest.NewSqlxDB(t) - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].FinalityDepth = ptr[uint32](50) - }) + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) {}) txStore := cltest.NewTestTxStore(t, db) ethKeyStore := cltest.NewKeyStore(t, db).Eth() @@ -1262,7 +1278,6 @@ func TestEthConfirmer_CheckConfirmedMissingReceipt_smallEvmRPCBatchSize_middleBa db := pgtest.NewSqlxDB(t) cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].FinalityDepth = ptr[uint32](50) c.EVM[0].RPCDefaultBatchSize = ptr[uint32](1) }) txStore := cltest.NewTestTxStore(t, db) @@ -1651,8 +1666,9 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), ccfg.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) + ht := headtracker.NewSimulatedHeadTracker(ethClient, true, 0) // Create confirmer with necessary state - ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), ccfg.EVM(), txmgr.NewEvmTxmFeeConfig(ccfg.EVM().GasEstimator()), ccfg.EVM().Transactions(), cfg.Database(), kst, txBuilder, lggr, stuckTxDetector) + ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), ccfg.EVM(), txmgr.NewEvmTxmFeeConfig(ccfg.EVM().GasEstimator()), ccfg.EVM().Transactions(), cfg.Database(), kst, txBuilder, lggr, stuckTxDetector, ht) servicetest.Run(t, ec) currentHead := int64(30) oldEnough := int64(15) @@ -1700,7 +1716,8 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), ccfg.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) - ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), ccfg.EVM(), txmgr.NewEvmTxmFeeConfig(ccfg.EVM().GasEstimator()), ccfg.EVM().Transactions(), cfg.Database(), kst, txBuilder, lggr, stuckTxDetector) + ht := headtracker.NewSimulatedHeadTracker(ethClient, true, 0) + ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), ccfg.EVM(), txmgr.NewEvmTxmFeeConfig(ccfg.EVM().GasEstimator()), ccfg.EVM().Transactions(), cfg.Database(), kst, txBuilder, lggr, stuckTxDetector, ht) servicetest.Run(t, ec) currentHead := int64(30) oldEnough := int64(15) @@ -2672,6 +2689,13 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { gconfig, config := newTestChainScopedConfig(t) ec := newEthConfirmer(t, txStore, ethClient, gconfig, config, ethKeyStore, nil) + latestFinalizedHead := evmtypes.Head{ + Number: 8, + Hash: testutils.NewHash(), + Parent: nil, + IsFinalized: false, // We are guaranteed to receive a latestFinalizedHead. + } + head := evmtypes.Head{ Hash: testutils.NewHash(), Number: 10, @@ -2685,16 +2709,15 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { }, }, } - t.Run("does nothing if there aren't any transactions", func(t *testing.T) { - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) }) t.Run("does nothing to unconfirmed transactions", func(t *testing.T) { etx := cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 0, fromAddress) // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2706,7 +2729,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { mustInsertEthReceipt(t, txStore, head.Number, head.Hash, etx.TxAttempts[0].Hash) // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2719,7 +2742,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { mustInsertEthReceipt(t, txStore, head.Parent.Parent.Number-1, testutils.NewHash(), etx.TxAttempts[0].Hash) // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2740,7 +2763,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { }), fromAddress).Return(commonclient.Successful, nil).Once() // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2763,7 +2786,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { commonclient.Successful, nil).Once() // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2798,7 +2821,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { }), fromAddress).Return(commonclient.Successful, nil).Once() // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2818,7 +2841,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { // Add receipt that is higher than head mustInsertEthReceipt(t, txStore, head.Number+1, testutils.NewHash(), attempt.Hash) - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head)) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), &head, latestFinalizedHead.BlockNumber())) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -3158,7 +3181,8 @@ func TestEthConfirmer_ProcessStuckTransactions(t *testing.T) { ge := evmcfg.EVM().GasEstimator() txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, feeEstimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), evmcfg.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) - ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(ge), evmcfg.EVM().Transactions(), cfg.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector) + ht := headtracker.NewSimulatedHeadTracker(ethClient, true, 0) + ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(ge), evmcfg.EVM().Transactions(), cfg.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector, ht) servicetest.Run(t, ec) ctx := tests.Context(t) @@ -3172,9 +3196,13 @@ func TestEthConfirmer_ProcessStuckTransactions(t *testing.T) { tx := mustInsertUnconfirmedTxWithBroadcastAttempts(t, txStore, nonce, fromAddress, autoPurgeMinAttempts, blockNum-int64(autoPurgeThreshold), marketGasPrice.Add(oneGwei)) head := evmtypes.Head{ - Hash: testutils.NewHash(), - Number: blockNum, + Hash: testutils.NewHash(), + Number: blockNum, + IsFinalized: true, } + + ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(&head, nil).Once() + ethClient.On("LatestFinalizedBlock", mock.Anything).Return(&head, nil).Once() ethClient.On("SequenceAt", mock.Anything, mock.Anything, mock.Anything).Return(evmtypes.Nonce(0), nil).Once() ethClient.On("BatchCallContext", mock.Anything, mock.Anything).Return(nil).Once() @@ -3196,9 +3224,12 @@ func TestEthConfirmer_ProcessStuckTransactions(t *testing.T) { require.Equal(t, bumpedFee.Legacy, latestAttempt.TxFee.Legacy) head = evmtypes.Head{ - Hash: testutils.NewHash(), - Number: blockNum + 1, + Hash: testutils.NewHash(), + Number: blockNum + 1, + IsFinalized: true, } + ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(&head, nil).Once() + ethClient.On("LatestFinalizedBlock", mock.Anything).Return(&head, nil).Once() ethClient.On("SequenceAt", mock.Anything, mock.Anything, mock.Anything).Return(evmtypes.Nonce(1), nil) ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { return len(b) == 4 && cltest.BatchElemMatchesParams(b[0], latestAttempt.Hash, "eth_getTransactionReceipt") @@ -3237,7 +3268,8 @@ func newEthConfirmer(t testing.TB, txStore txmgr.EvmTxStore, ethClient client.Cl }, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ks, estimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), estimator, txStore, ethClient) - ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ks, txBuilder, lggr, stuckTxDetector) + ht := headtracker.NewSimulatedHeadTracker(ethClient, true, 0) + ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ks, txBuilder, lggr, stuckTxDetector, ht) ec.SetResumeCallback(fn) servicetest.Run(t, ec) return ec diff --git a/core/chains/evm/txmgr/evm_tx_store.go b/core/chains/evm/txmgr/evm_tx_store.go index 45de437e443..4bdf191376b 100644 --- a/core/chains/evm/txmgr/evm_tx_store.go +++ b/core/chains/evm/txmgr/evm_tx_store.go @@ -34,8 +34,8 @@ import ( var ( ErrKeyNotUpdated = errors.New("evmTxStore: Key not updated") - // ErrCouldNotGetReceipt is the error string we save if we reach our finality depth for a confirmed transaction without ever getting a receipt - // This most likely happened because an external wallet used the account for this nonce + // ErrCouldNotGetReceipt is the error string we save if we reach our LatestFinalizedBlockNum for a confirmed transaction + // without ever getting a receipt. This most likely happened because an external wallet used the account for this nonce ErrCouldNotGetReceipt = "could not get receipt" ) @@ -959,11 +959,11 @@ func (o *evmTxStore) SaveFetchedReceipts(ctx context.Context, r []*evmtypes.Rece // NOTE: We continue to attempt to resend evm.txes in this state on // every head to guard against the extremely rare scenario of nonce gap due to // reorg that excludes the transaction (from another wallet) that had this -// nonce (until finality depth is reached, after which we make the explicit +// nonce (until LatestFinalizedBlockNum is reached, after which we make the explicit // decision to give up). This is done in the EthResender. // // We will continue to try to fetch a receipt for these attempts until all -// attempts are below the finality depth from current head. +// attempts are equal to or below the LatestFinalizedBlockNum from current head. func (o *evmTxStore) MarkAllConfirmedMissingReceipt(ctx context.Context, chainID *big.Int) (err error) { var cancel context.CancelFunc ctx, cancel = o.stopCh.Ctx(ctx) @@ -1430,23 +1430,18 @@ ORDER BY nonce ASC // markOldTxesMissingReceiptAsErrored // -// Once eth_tx has all of its attempts broadcast before some cutoff threshold +// Once eth_tx has all of its attempts broadcast equal to or before latestFinalizedBlockNum // without receiving any receipts, we mark it as fatally errored (never sent). // // The job run will also be marked as errored in this case since we never got a // receipt and thus cannot pass on any transaction hash -func (o *evmTxStore) MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, finalityDepth uint32, chainID *big.Int) error { +func (o *evmTxStore) MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64, chainID *big.Int) error { var cancel context.CancelFunc ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() - // cutoffBlockNum is a block height - // Any 'confirmed_missing_receipt' eth_tx with all attempts older than this block height will be marked as errored - // We will not try to query for receipts for this transaction any more - cutoff := blockNum - int64(finalityDepth) - if cutoff <= 0 { - return nil - } - if cutoff <= 0 { + // Any 'confirmed_missing_receipt' eth_tx with all attempts equal to or older than latestFinalizedBlockNum will be marked as errored + // We will not try to query for receipts for this transaction anymore + if latestFinalizedBlockNum <= 0 { return nil } // note: if QOpt passes in a sql.Tx this will reuse it @@ -1466,12 +1461,12 @@ FROM ( WHERE e2.state = 'confirmed_missing_receipt' AND e2.evm_chain_id = $3 GROUP BY e2.id - HAVING max(evm.tx_attempts.broadcast_before_block_num) < $2 + HAVING max(evm.tx_attempts.broadcast_before_block_num) <= $2 ) FOR UPDATE OF e1 ) e0 WHERE e0.id = evm.txes.id -RETURNING e0.id, e0.nonce`, ErrCouldNotGetReceipt, cutoff, chainID.String()) +RETURNING e0.id, e0.nonce`, ErrCouldNotGetReceipt, latestFinalizedBlockNum, chainID.String()) if err != nil { return pkgerrors.Wrap(err, "markOldTxesMissingReceiptAsErrored failed to query") diff --git a/core/chains/evm/txmgr/evm_tx_store_test.go b/core/chains/evm/txmgr/evm_tx_store_test.go index 191a0a5fed2..992bd1f434c 100644 --- a/core/chains/evm/txmgr/evm_tx_store_test.go +++ b/core/chains/evm/txmgr/evm_tx_store_test.go @@ -1117,13 +1117,14 @@ func TestORM_MarkOldTxesMissingReceiptAsErrored(t *testing.T) { ethKeyStore := cltest.NewKeyStore(t, db).Eth() ethClient := evmtest.NewEthClientMockWithDefaultChain(t) _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore) + latestFinalizedBlockNum := int64(8) // tx state should be confirmed missing receipt - // attempt should be broadcast before cutoff time + // attempt should be before latestFinalizedBlockNum t.Run("successfully mark errored transactions", func(t *testing.T) { etx := mustInsertConfirmedMissingReceiptEthTxWithLegacyAttempt(t, txStore, 1, 7, time.Now(), fromAddress) - err := txStore.MarkOldTxesMissingReceiptAsErrored(tests.Context(t), 10, 2, ethClient.ConfiguredChainID()) + err := txStore.MarkOldTxesMissingReceiptAsErrored(tests.Context(t), 10, latestFinalizedBlockNum, ethClient.ConfiguredChainID()) require.NoError(t, err) etx, err = txStore.FindTxWithAttempts(ctx, etx.ID) @@ -1133,7 +1134,7 @@ func TestORM_MarkOldTxesMissingReceiptAsErrored(t *testing.T) { t.Run("successfully mark errored transactions w/ qopt passing in sql.Tx", func(t *testing.T) { etx := mustInsertConfirmedMissingReceiptEthTxWithLegacyAttempt(t, txStore, 1, 7, time.Now(), fromAddress) - err := txStore.MarkOldTxesMissingReceiptAsErrored(tests.Context(t), 10, 2, ethClient.ConfiguredChainID()) + err := txStore.MarkOldTxesMissingReceiptAsErrored(tests.Context(t), 10, latestFinalizedBlockNum, ethClient.ConfiguredChainID()) require.NoError(t, err) // must run other query outside of postgres transaction so changes are committed diff --git a/core/chains/evm/txmgr/mocks/evm_tx_store.go b/core/chains/evm/txmgr/mocks/evm_tx_store.go index b40c0ca8376..a9a175e3d94 100644 --- a/core/chains/evm/txmgr/mocks/evm_tx_store.go +++ b/core/chains/evm/txmgr/mocks/evm_tx_store.go @@ -2214,17 +2214,17 @@ func (_c *EvmTxStore_MarkAllConfirmedMissingReceipt_Call) RunAndReturn(run func( return _c } -// MarkOldTxesMissingReceiptAsErrored provides a mock function with given fields: ctx, blockNum, finalityDepth, chainID -func (_m *EvmTxStore) MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, finalityDepth uint32, chainID *big.Int) error { - ret := _m.Called(ctx, blockNum, finalityDepth, chainID) +// MarkOldTxesMissingReceiptAsErrored provides a mock function with given fields: ctx, blockNum, latestFinalizedBlockNum, chainID +func (_m *EvmTxStore) MarkOldTxesMissingReceiptAsErrored(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64, chainID *big.Int) error { + ret := _m.Called(ctx, blockNum, latestFinalizedBlockNum, chainID) if len(ret) == 0 { panic("no return value specified for MarkOldTxesMissingReceiptAsErrored") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64, uint32, *big.Int) error); ok { - r0 = rf(ctx, blockNum, finalityDepth, chainID) + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, *big.Int) error); ok { + r0 = rf(ctx, blockNum, latestFinalizedBlockNum, chainID) } else { r0 = ret.Error(0) } @@ -2240,15 +2240,15 @@ type EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call struct { // MarkOldTxesMissingReceiptAsErrored is a helper method to define mock.On call // - ctx context.Context // - blockNum int64 -// - finalityDepth uint32 +// - latestFinalizedBlockNum int64 // - chainID *big.Int -func (_e *EvmTxStore_Expecter) MarkOldTxesMissingReceiptAsErrored(ctx interface{}, blockNum interface{}, finalityDepth interface{}, chainID interface{}) *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call { - return &EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call{Call: _e.mock.On("MarkOldTxesMissingReceiptAsErrored", ctx, blockNum, finalityDepth, chainID)} +func (_e *EvmTxStore_Expecter) MarkOldTxesMissingReceiptAsErrored(ctx interface{}, blockNum interface{}, latestFinalizedBlockNum interface{}, chainID interface{}) *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call { + return &EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call{Call: _e.mock.On("MarkOldTxesMissingReceiptAsErrored", ctx, blockNum, latestFinalizedBlockNum, chainID)} } -func (_c *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call) Run(run func(ctx context.Context, blockNum int64, finalityDepth uint32, chainID *big.Int)) *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call { +func (_c *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call) Run(run func(ctx context.Context, blockNum int64, latestFinalizedBlockNum int64, chainID *big.Int)) *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(int64), args[2].(uint32), args[3].(*big.Int)) + run(args[0].(context.Context), args[1].(int64), args[2].(int64), args[3].(*big.Int)) }) return _c } @@ -2258,7 +2258,7 @@ func (_c *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call) Return(_a0 error) return _c } -func (_c *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call) RunAndReturn(run func(context.Context, int64, uint32, *big.Int) error) *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call { +func (_c *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call) RunAndReturn(run func(context.Context, int64, int64, *big.Int) error) *EvmTxStore_MarkOldTxesMissingReceiptAsErrored_Call { _c.Call.Return(run) return _c } diff --git a/core/chains/evm/txmgr/txmgr_test.go b/core/chains/evm/txmgr/txmgr_test.go index 5f932db8720..3d52e6eb4f0 100644 --- a/core/chains/evm/txmgr/txmgr_test.go +++ b/core/chains/evm/txmgr/txmgr_test.go @@ -627,6 +627,7 @@ func TestTxm_GetTransactionStatus(t *testing.T) { ethClient.On("PendingNonceAt", mock.Anything, mock.Anything).Return(uint64(0), nil).Maybe() ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(head, nil).Once() ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(head.Parent, nil).Once() + ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(head, nil) feeEstimator := gasmocks.NewEvmFeeEstimator(t) feeEstimator.On("Start", mock.Anything).Return(nil).Once() feeEstimator.On("Close", mock.Anything).Return(nil).Once() diff --git a/core/cmd/shell_local.go b/core/cmd/shell_local.go index e19cc485d8b..ed8b653c05b 100644 --- a/core/cmd/shell_local.go +++ b/core/cmd/shell_local.go @@ -669,7 +669,7 @@ func (s *Shell) RebroadcastTransactions(c *cli.Context) (err error) { feeCfg := txmgr.NewEvmTxmFeeConfig(chain.Config().EVM().GasEstimator()) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, ethClient.ConfiguredChainID(), "", assets.NewWei(assets.NewEth(100).ToInt()), chain.Config().EVM().Transactions().AutoPurge(), nil, orm, ethClient) ec := txmgr.NewEvmConfirmer(orm, txmgr.NewEvmTxmClient(ethClient, chain.Config().EVM().NodePool().Errors()), - cfg, feeCfg, chain.Config().EVM().Transactions(), app.GetConfig().Database(), keyStore.Eth(), txBuilder, chain.Logger(), stuckTxDetector) + cfg, feeCfg, chain.Config().EVM().Transactions(), app.GetConfig().Database(), keyStore.Eth(), txBuilder, chain.Logger(), stuckTxDetector, chain.HeadTracker()) totalNonces := endingNonce - beginningNonce + 1 nonces := make([]evmtypes.Nonce, totalNonces) for i := int64(0); i < totalNonces; i++ { diff --git a/core/cmd/shell_local_test.go b/core/cmd/shell_local_test.go index 60545269e29..8ed48dcaa20 100644 --- a/core/cmd/shell_local_test.go +++ b/core/cmd/shell_local_test.go @@ -11,9 +11,8 @@ import ( commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" - "github.com/smartcontractkit/chainlink/v2/core/capabilities" - "github.com/smartcontractkit/chainlink/v2/common/client" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" "github.com/smartcontractkit/chainlink/v2/core/cmd" cmdMocks "github.com/smartcontractkit/chainlink/v2/core/cmd/mocks" diff --git a/core/internal/cltest/mocks.go b/core/internal/cltest/mocks.go index 9e0ee2f3f20..fd01f72c131 100644 --- a/core/internal/cltest/mocks.go +++ b/core/internal/cltest/mocks.go @@ -392,6 +392,7 @@ func NewLegacyChainsWithMockChain(t testing.TB, ethClient evmclient.Client, cfg scopedCfg := evmtest.NewChainScopedConfig(t, cfg) ch.On("ID").Return(scopedCfg.EVM().ChainID()) ch.On("Config").Return(scopedCfg) + ch.On("HeadTracker").Return(nil) return NewLegacyChainsWithChain(ch, cfg) } From 1fcfd80af03e12828b18a4fded88a2cca0f77e65 Mon Sep 17 00:00:00 2001 From: Bolek <1416262+bolekk@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:25:22 -0700 Subject: [PATCH 11/16] [Keystone] Minor fixes (#14091) A bundle of minor fixes in several components: 1. Launcher: adjust defaultStreamConfig values to a resonable max capacity (250 MB per channel) 2. Trigger shims: change some incorrect read-locks to rw-locks 3. Trigger shims: enforce max possible number of bundled worklflow IDs in an event 4. Mode Aggregator: avoid unnecessary early exit 5. Message Cache: avoid unnecessary early exit while collecting ready messages --- core/capabilities/launcher.go | 12 +++++----- core/capabilities/remote/message_cache.go | 8 +++---- core/capabilities/remote/trigger_publisher.go | 4 ++-- .../capabilities/remote/trigger_subscriber.go | 24 ++++++++++++------- core/capabilities/remote/utils.go | 3 ++- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/core/capabilities/launcher.go b/core/capabilities/launcher.go index 3fc321087b8..3182b192b74 100644 --- a/core/capabilities/launcher.go +++ b/core/capabilities/launcher.go @@ -28,16 +28,16 @@ import ( ) var defaultStreamConfig = p2ptypes.StreamConfig{ - IncomingMessageBufferSize: 1000000, - OutgoingMessageBufferSize: 1000000, - MaxMessageLenBytes: 100000, + IncomingMessageBufferSize: 500, + OutgoingMessageBufferSize: 500, + MaxMessageLenBytes: 500000, // 500 KB; max capacity = 500 * 500000 = 250 MB MessageRateLimiter: ragep2p.TokenBucketParams{ Rate: 100.0, - Capacity: 1000, + Capacity: 500, }, BytesRateLimiter: ragep2p.TokenBucketParams{ - Rate: 100000.0, - Capacity: 1000000, + Rate: 5000000.0, // 5 MB/s + Capacity: 10000000, // 10 MB }, } diff --git a/core/capabilities/remote/message_cache.go b/core/capabilities/remote/message_cache.go index 27f909c5165..f3a3a79b2c6 100644 --- a/core/capabilities/remote/message_cache.go +++ b/core/capabilities/remote/message_cache.go @@ -60,12 +60,12 @@ func (c *messageCache[EventID, PeerID]) Ready(eventID EventID, minCount uint32, if msg.timestamp >= minTimestamp { countAboveMinTimestamp++ accPayloads = append(accPayloads, msg.payload) - if countAboveMinTimestamp >= minCount { - ev.wasReady = true - return true, accPayloads - } } } + if countAboveMinTimestamp >= minCount { + ev.wasReady = true + return true, accPayloads + } return false, nil } diff --git a/core/capabilities/remote/trigger_publisher.go b/core/capabilities/remote/trigger_publisher.go index b4d749754d4..23b778f6018 100644 --- a/core/capabilities/remote/trigger_publisher.go +++ b/core/capabilities/remote/trigger_publisher.go @@ -163,7 +163,7 @@ func (p *triggerPublisher) registrationCleanupLoop() { return case <-ticker.C: now := time.Now().UnixMilli() - p.mu.RLock() + p.mu.Lock() for key, req := range p.registrations { callerDon := p.workflowDONs[key.callerDonId] ready, _ := p.messageCache.Ready(key, uint32(2*callerDon.F+1), now-p.config.RegistrationExpiry.Milliseconds(), false) @@ -178,7 +178,7 @@ func (p *triggerPublisher) registrationCleanupLoop() { p.messageCache.Delete(key) } } - p.mu.RUnlock() + p.mu.Unlock() } } } diff --git a/core/capabilities/remote/trigger_subscriber.go b/core/capabilities/remote/trigger_subscriber.go index d957614886a..c932dc68e37 100644 --- a/core/capabilities/remote/trigger_subscriber.go +++ b/core/capabilities/remote/trigger_subscriber.go @@ -6,7 +6,6 @@ import ( sync "sync" "time" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/services" @@ -23,11 +22,11 @@ import ( // // TriggerSubscriber communicates with corresponding TriggerReceivers on remote nodes. type triggerSubscriber struct { - config *capabilities.RemoteTriggerConfig + config *commoncap.RemoteTriggerConfig capInfo commoncap.CapabilityInfo - capDonInfo capabilities.DON + capDonInfo commoncap.DON capDonMembers map[p2ptypes.PeerID]struct{} - localDonInfo capabilities.DON + localDonInfo commoncap.DON dispatcher types.Dispatcher aggregator types.Aggregator messageCache *messageCache[triggerEventKey, p2ptypes.PeerID] @@ -53,16 +52,19 @@ var _ types.Receiver = &triggerSubscriber{} var _ services.Service = &triggerSubscriber{} // TODO makes this configurable with a default -const defaultSendChannelBufferSize = 1000 +const ( + defaultSendChannelBufferSize = 1000 + maxBatchedWorkflowIDs = 1000 +) -func NewTriggerSubscriber(config *capabilities.RemoteTriggerConfig, capInfo commoncap.CapabilityInfo, capDonInfo capabilities.DON, localDonInfo capabilities.DON, dispatcher types.Dispatcher, aggregator types.Aggregator, lggr logger.Logger) *triggerSubscriber { +func NewTriggerSubscriber(config *commoncap.RemoteTriggerConfig, capInfo commoncap.CapabilityInfo, capDonInfo commoncap.DON, localDonInfo commoncap.DON, dispatcher types.Dispatcher, aggregator types.Aggregator, lggr logger.Logger) *triggerSubscriber { if aggregator == nil { lggr.Warnw("no aggregator provided, using default MODE aggregator", "capabilityId", capInfo.ID) aggregator = NewDefaultModeAggregator(uint32(capDonInfo.F + 1)) } if config == nil { lggr.Info("no config provided, using default values") - config = &capabilities.RemoteTriggerConfig{} + config = &commoncap.RemoteTriggerConfig{} } config.ApplyDefaults() capDonMembers := make(map[p2ptypes.PeerID]struct{}) @@ -184,6 +186,10 @@ func (s *triggerSubscriber) Receive(_ context.Context, msg *types.MessageBody) { s.lggr.Errorw("received message with invalid trigger metadata", "capabilityId", s.capInfo.ID, "sender", sender) return } + if len(meta.WorkflowIds) > maxBatchedWorkflowIDs { + s.lggr.Errorw("received message with too many workflow IDs - truncating", "capabilityId", s.capInfo.ID, "nWorkflows", len(meta.WorkflowIds), "sender", sender) + meta.WorkflowIds = meta.WorkflowIds[:maxBatchedWorkflowIDs] + } for _, workflowId := range meta.WorkflowIds { s.mu.RLock() registration, found := s.registeredWorkflows[workflowId] @@ -197,10 +203,10 @@ func (s *triggerSubscriber) Receive(_ context.Context, msg *types.MessageBody) { workflowId: workflowId, } nowMs := time.Now().UnixMilli() - s.mu.RLock() + s.mu.Lock() creationTs := s.messageCache.Insert(key, sender, nowMs, msg.Payload) ready, payloads := s.messageCache.Ready(key, s.config.MinResponsesToAggregate, nowMs-s.config.MessageExpiry.Milliseconds(), true) - s.mu.RUnlock() + s.mu.Unlock() if nowMs-creationTs > s.config.RegistrationExpiry.Milliseconds() { s.lggr.Warnw("received trigger event for an expired ID", "triggerEventID", meta.TriggerEventId, "capabilityId", s.capInfo.ID, "workflowId", workflowId, "sender", sender) continue diff --git a/core/capabilities/remote/utils.go b/core/capabilities/remote/utils.go index 10e4e3082c9..7e303eefc8f 100644 --- a/core/capabilities/remote/utils.go +++ b/core/capabilities/remote/utils.go @@ -92,7 +92,8 @@ func AggregateModeRaw(elemList [][]byte, minIdenticalResponses uint32) ([]byte, hashToCount[sha]++ if hashToCount[sha] >= minIdenticalResponses { found = elem - break + // update in case we find another elem with an even higher count + minIdenticalResponses = hashToCount[sha] } } if found == nil { From 518cc281b14727c26cd7fcb9db882b45837d443a Mon Sep 17 00:00:00 2001 From: kanth <70688600+defistar@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:26:04 +0530 Subject: [PATCH 12/16] feat: CCIP-2465 enumerableMap for addressToBytes Mapping (#14012) * feat: CCIP-2465 enumerableMap for addressToBytes Mapping * fix: CCIP-2465 prettier write for new library contracts * fix: CCIP-2465 gas-snapshot updated and changeset added for new library * fix: CCIP-2465 gassnapshot corrections for EnumerableAddresses --- contracts/.changeset/nice-planets-share.md | 5 + contracts/gas-snapshots/shared.gas-snapshot | 28 ++-- .../enumerable/EnumerableMapAddresses.sol | 89 +++++++++++- .../enumerable/EnumerableMapBytes32.sol | 136 ++++++++++++++++++ .../enumerable/EnumerableMapAddresses.t.sol | 111 +++++++++++--- 5 files changed, 338 insertions(+), 31 deletions(-) create mode 100644 contracts/.changeset/nice-planets-share.md create mode 100644 contracts/src/v0.8/shared/enumerable/EnumerableMapBytes32.sol diff --git a/contracts/.changeset/nice-planets-share.md b/contracts/.changeset/nice-planets-share.md new file mode 100644 index 00000000000..a8af56ac613 --- /dev/null +++ b/contracts/.changeset/nice-planets-share.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': minor +--- + +EnumerableMap Library for an Address to Bytes mapping diff --git a/contracts/gas-snapshots/shared.gas-snapshot b/contracts/gas-snapshots/shared.gas-snapshot index 0419c42a6aa..3cc143ecc0e 100644 --- a/contracts/gas-snapshots/shared.gas-snapshot +++ b/contracts/gas-snapshots/shared.gas-snapshot @@ -51,21 +51,29 @@ CallWithExactGas__callWithExactGasSafeReturnData:test_NoContractReverts() (gas: CallWithExactGas__callWithExactGasSafeReturnData:test_NoGasForCallExactCheckReverts() (gas: 16139) CallWithExactGas__callWithExactGasSafeReturnData:test_NotEnoughGasForCallReverts() (gas: 16547) CallWithExactGas__callWithExactGasSafeReturnData:test_callWithExactGasSafeReturnData_ThrowOOGError_Revert() (gas: 36752) -EnumerableMapAddresses_at:testAtSuccess() (gas: 95001) -EnumerableMapAddresses_at:testBytes32AtSuccess() (gas: 94770) +EnumerableMapAddresses_at:testAtSuccess() (gas: 95086) +EnumerableMapAddresses_at:testBytes32AtSuccess() (gas: 94877) EnumerableMapAddresses_contains:testBytes32ContainsSuccess() (gas: 93518) EnumerableMapAddresses_contains:testContainsSuccess() (gas: 93696) -EnumerableMapAddresses_get:testBytes32GetSuccess() (gas: 94249) -EnumerableMapAddresses_get:testGetSuccess() (gas: 94436) -EnumerableMapAddresses_get_errorMessage:testGetErrorMessageSuccess() (gas: 94477) -EnumerableMapAddresses_length:testBytes32LengthSuccess() (gas: 72404) -EnumerableMapAddresses_length:testLengthSuccess() (gas: 72582) -EnumerableMapAddresses_remove:testBytes32RemoveSuccess() (gas: 73408) +EnumerableMapAddresses_get:testBytes32GetSuccess() (gas: 94278) +EnumerableMapAddresses_get:testGetSuccess() (gas: 94453) +EnumerableMapAddresses_get_errorMessage:testGetErrorMessageSuccess() (gas: 94489) +EnumerableMapAddresses_length:testBytes32LengthSuccess() (gas: 72445) +EnumerableMapAddresses_length:testLengthSuccess() (gas: 72640) +EnumerableMapAddresses_remove:testBytes32RemoveSuccess() (gas: 73462) EnumerableMapAddresses_remove:testRemoveSuccess() (gas: 73686) EnumerableMapAddresses_set:testBytes32SetSuccess() (gas: 94496) EnumerableMapAddresses_set:testSetSuccess() (gas: 94685) -EnumerableMapAddresses_tryGet:testBytes32TryGetSuccess() (gas: 94604) -EnumerableMapAddresses_tryGet:testTryGetSuccess() (gas: 94864) +EnumerableMapAddresses_tryGet:testBytes32TryGetSuccess() (gas: 94622) +EnumerableMapAddresses_tryGet:testTryGetSuccess() (gas: 94893) +EnumerableMapAddresses_at:testBytesAtSuccess() (gas: 96564) +EnumerableMapAddresses_contains:testBytesContainsSuccess() (gas: 94012) +EnumerableMapAddresses_get:testBytesGetSuccess() (gas: 95879) +EnumerableMapAddresses_get_errorMessage:testBytesGetErrorMessageSuccess() (gas: 95878) +EnumerableMapAddresses_length:testBytesLengthSuccess() (gas: 73011) +EnumerableMapAddresses_remove:testBytesRemoveSuccess() (gas: 74249) +EnumerableMapAddresses_set:testBytesSetSuccess() (gas: 95428) +EnumerableMapAddresses_tryGet:testBytesTryGetSuccess() (gas: 96279) OpStackBurnMintERC677_constructor:testConstructorSuccess() (gas: 1743649) OpStackBurnMintERC677_interfaceCompatibility:testBurnCompatibility() (gas: 298649) OpStackBurnMintERC677_interfaceCompatibility:testMintCompatibility() (gas: 137957) diff --git a/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol b/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol index 6fbd37c60d7..c14a03b4443 100644 --- a/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol +++ b/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol @@ -1,12 +1,15 @@ // SPDX-License-Identifier: MIT +/* solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore */ pragma solidity ^0.8.0; import {EnumerableMap} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableMapBytes32} from "./EnumerableMapBytes32.sol"; // TODO: the lib can be replaced with OZ v5.1 post-upgrade, which has AddressToAddressMap and AddressToBytes32Map library EnumerableMapAddresses { using EnumerableMap for EnumerableMap.UintToAddressMap; using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; + using EnumerableMapBytes32 for EnumerableMapBytes32.Bytes32ToBytesMap; struct AddressToAddressMap { EnumerableMap.UintToAddressMap _inner; @@ -57,8 +60,6 @@ library EnumerableMapAddresses { return map._inner.get(uint256(uint160(key)), errorMessage); } - // AddressToBytes32Map - struct AddressToBytes32Map { EnumerableMap.Bytes32ToBytes32Map _inner; } @@ -137,4 +138,88 @@ library EnumerableMapAddresses { function get(AddressToBytes32Map storage map, address key) internal view returns (bytes32) { return map._inner.get(bytes32(uint256(uint160(key)))); } + + struct AddressToBytesMap { + EnumerableMapBytes32.Bytes32ToBytesMap _inner; + } + + /** + * @dev Sets the value for `key` in the map. Returns true if the key was added to the map, that is if it was not already present. + * @param map The map where the value will be set + * @param key The key to set the value for + * @param value The value to set for the key + * @return bool indicating whether the key was added to the map + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function set(AddressToBytesMap storage map, address key, bytes memory value) internal returns (bool) { + return map._inner.set(bytes32(uint256(uint160(key))), value); + } + + /** + * @dev Removes the value for `key` in the map. Returns true if the key was removed from the map, that is if it was present. + * @param map The map where the value will be removed + * @param key The key to remove the value for + * @return bool indicating whether the key was removed from the map + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function remove(AddressToBytesMap storage map, address key) internal returns (bool) { + return map._inner.remove(bytes32(uint256(uint160(key)))); + } + + /** + * @dev Checks if the map contains the `key`. Returns true if the key is in the map. + * @param map The map to check for the presence of the key + * @param key The key to check for presence in the map + * @return bool indicating whether the key is in the map + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function contains(AddressToBytesMap storage map, address key) internal view returns (bool) { + return map._inner.contains(bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns the number of elements in the map. + * @param map The map to check the length of + * @return uint256 indicating the number of elements in the map + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function length(AddressToBytesMap storage map) internal view returns (uint256) { + return map._inner.length(); + } + + /** + * @dev Returns the element stored at position `index` in the map. Note that there are no guarantees on the ordering of values inside the array, and it may change when more values are added or removed. + * @param map The map to retrieve the element from + * @param index The index to retrieve the element at + * @return address The key of the element at the specified index + * @return bytes The value of the element at the specified index + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function at(AddressToBytesMap storage map, uint256 index) internal view returns (address, bytes memory) { + (bytes32 key, bytes memory value) = map._inner.at(index); + return (address(uint160(uint256(key))), value); + } + + /** + * @dev Tries to return the value associated with `key`. Does not revert if `key` is not in the map. + * @param map The map to retrieve the value from + * @param key The key to retrieve the value for + * @return bool indicating whether the key was in the map + * @return bytes The value associated with the key + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function tryGet(AddressToBytesMap storage map, address key) internal view returns (bool, bytes memory) { + return map._inner.tryGet(bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns the value associated with `key`. + * @param map The map to retrieve the value from + * @param key The key to retrieve the value for + * @return bytes The value associated with the key + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function get(AddressToBytesMap storage map, address key) internal view returns (bytes memory) { + return map._inner.get(bytes32(uint256(uint160(key)))); + } } diff --git a/contracts/src/v0.8/shared/enumerable/EnumerableMapBytes32.sol b/contracts/src/v0.8/shared/enumerable/EnumerableMapBytes32.sol new file mode 100644 index 00000000000..2ec9098f855 --- /dev/null +++ b/contracts/src/v0.8/shared/enumerable/EnumerableMapBytes32.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +/* solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore */ +pragma solidity ^0.8.0; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] + * type. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time + * (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableMapBytes32 for EnumerableMapBytes32.Bytes32ToBytesMap; + * + * // Declare a set state variable + * EnumerableMapBytes32.Bytes32ToBytesMap private myMap; + * } + * ``` + * + * The following map types are supported: + * + * - `bytes32 -> bytes` (`Bytes32ToBytes`) + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean up an EnumerableMapBytes32, you should remove all elements one by one. + * ==== + */ +library EnumerableMapBytes32 { + using EnumerableSet for EnumerableSet.Bytes32Set; + + error NonexistentKeyError(); + + struct Bytes32ToBytesMap { + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 => bytes) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function set(Bytes32ToBytesMap storage map, bytes32 key, bytes memory value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function remove(Bytes32ToBytesMap storage map, bytes32 key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function contains(Bytes32ToBytesMap storage map, bytes32 key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function length(Bytes32ToBytesMap storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function at(Bytes32ToBytesMap storage map, uint256 index) internal view returns (bytes32, bytes memory) { + bytes32 key = map._keys.at(index); + return (key, map._values[key]); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function tryGet(Bytes32ToBytesMap storage map, bytes32 key) internal view returns (bool, bytes memory) { + bytes memory value = map._values[key]; + if (value.length == 0) { + return (contains(map, key), bytes("")); + } else { + return (true, value); + } + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function get(Bytes32ToBytesMap storage map, bytes32 key) internal view returns (bytes memory) { + bytes memory value = map._values[key]; + if (value.length == 0 && !contains(map, key)) { + revert NonexistentKeyError(); + } + return value; + } +} diff --git a/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol b/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol index 900c546f66d..097e79e372e 100644 --- a/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol +++ b/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol @@ -7,11 +7,14 @@ import {EnumerableMapAddresses} from "../../enumerable/EnumerableMapAddresses.so contract EnumerableMapAddressesTest is BaseTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; EnumerableMapAddresses.AddressToAddressMap internal s_addressToAddressMap; EnumerableMapAddresses.AddressToBytes32Map internal s_addressToBytes32Map; + EnumerableMapAddresses.AddressToBytesMap internal s_addressToBytesMap; bytes32 internal constant MOCK_BYTES32_VALUE = bytes32(uint256(42)); + bytes internal constant MOCK_BYTES_VALUE = "0x123456789abcdef"; function setUp() public virtual override { BaseTest.setUp(); @@ -21,6 +24,7 @@ contract EnumerableMapAddressesTest is BaseTest { contract EnumerableMapAddresses_set is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testSetSuccess() public { assertTrue(!s_addressToAddressMap.contains(address(this))); @@ -35,11 +39,19 @@ contract EnumerableMapAddresses_set is EnumerableMapAddressesTest { assertTrue(s_addressToBytes32Map.contains(address(this))); assertTrue(!s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); } + + function testBytesSetSuccess() public { + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertTrue(s_addressToBytesMap.contains(address(this))); + assertTrue(!s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + } } contract EnumerableMapAddresses_remove is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testRemoveSuccess() public { assertTrue(!s_addressToAddressMap.contains(address(this))); @@ -58,11 +70,21 @@ contract EnumerableMapAddresses_remove is EnumerableMapAddressesTest { assertTrue(!s_addressToBytes32Map.contains(address(this))); assertTrue(!s_addressToBytes32Map.remove(address(this))); } + + function testBytesRemoveSuccess() public { + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertTrue(s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.remove(address(this))); + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(!s_addressToBytesMap.remove(address(this))); + } } contract EnumerableMapAddresses_contains is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testContainsSuccess() public { assertTrue(!s_addressToAddressMap.contains(address(this))); @@ -75,55 +97,81 @@ contract EnumerableMapAddresses_contains is EnumerableMapAddressesTest { assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); assertTrue(s_addressToBytes32Map.contains(address(this))); } + + function testBytesContainsSuccess() public { + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertTrue(s_addressToBytesMap.contains(address(this))); + } } contract EnumerableMapAddresses_length is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testLengthSuccess() public { - assertTrue(s_addressToAddressMap.length() == 0); + assertEq(s_addressToAddressMap.length(), 0); assertTrue(s_addressToAddressMap.set(address(this), address(this))); - assertTrue(s_addressToAddressMap.length() == 1); + assertEq(s_addressToAddressMap.length(), 1); assertTrue(s_addressToAddressMap.remove(address(this))); - assertTrue(s_addressToAddressMap.length() == 0); + assertEq(s_addressToAddressMap.length(), 0); } function testBytes32LengthSuccess() public { - assertTrue(s_addressToBytes32Map.length() == 0); + assertEq(s_addressToBytes32Map.length(), 0); assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); - assertTrue(s_addressToBytes32Map.length() == 1); + assertEq(s_addressToBytes32Map.length(), 1); assertTrue(s_addressToBytes32Map.remove(address(this))); - assertTrue(s_addressToBytes32Map.length() == 0); + assertEq(s_addressToBytes32Map.length(), 0); + } + + function testBytesLengthSuccess() public { + assertEq(s_addressToBytesMap.length(), 0); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertEq(s_addressToBytesMap.length(), 1); + assertTrue(s_addressToBytesMap.remove(address(this))); + assertEq(s_addressToBytesMap.length(), 0); } } contract EnumerableMapAddresses_at is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testAtSuccess() public { - assertTrue(s_addressToAddressMap.length() == 0); + assertEq(s_addressToAddressMap.length(), 0); assertTrue(s_addressToAddressMap.set(address(this), address(this))); - assertTrue(s_addressToAddressMap.length() == 1); + assertEq(s_addressToAddressMap.length(), 1); (address key, address value) = s_addressToAddressMap.at(0); - assertTrue(key == address(this)); - assertTrue(value == address(this)); + assertEq(key, address(this)); + assertEq(value, address(this)); } function testBytes32AtSuccess() public { - assertTrue(s_addressToBytes32Map.length() == 0); + assertEq(s_addressToBytes32Map.length(), 0); assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); - assertTrue(s_addressToBytes32Map.length() == 1); + assertEq(s_addressToBytes32Map.length(), 1); (address key, bytes32 value) = s_addressToBytes32Map.at(0); - assertTrue(key == address(this)); - assertTrue(value == MOCK_BYTES32_VALUE); + assertEq(key, address(this)); + assertEq(value, MOCK_BYTES32_VALUE); + } + + function testBytesAtSuccess() public { + assertEq(s_addressToBytesMap.length(), 0); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertEq(s_addressToBytesMap.length(), 1); + (address key, bytes memory value) = s_addressToBytesMap.at(0); + assertEq(key, address(this)); + assertEq(value, MOCK_BYTES_VALUE); } } contract EnumerableMapAddresses_tryGet is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testTryGetSuccess() public { assertTrue(!s_addressToAddressMap.contains(address(this))); @@ -131,7 +179,7 @@ contract EnumerableMapAddresses_tryGet is EnumerableMapAddressesTest { assertTrue(s_addressToAddressMap.contains(address(this))); (bool success, address value) = s_addressToAddressMap.tryGet(address(this)); assertTrue(success); - assertTrue(value == address(this)); + assertEq(value, address(this)); } function testBytes32TryGetSuccess() public { @@ -140,37 +188,62 @@ contract EnumerableMapAddresses_tryGet is EnumerableMapAddressesTest { assertTrue(s_addressToBytes32Map.contains(address(this))); (bool success, bytes32 value) = s_addressToBytes32Map.tryGet(address(this)); assertTrue(success); - assertTrue(value == MOCK_BYTES32_VALUE); + assertEq(value, MOCK_BYTES32_VALUE); + } + + function testBytesTryGetSuccess() public { + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertTrue(s_addressToBytesMap.contains(address(this))); + (bool success, bytes memory value) = s_addressToBytesMap.tryGet(address(this)); + assertTrue(success); + assertEq(value, MOCK_BYTES_VALUE); } } contract EnumerableMapAddresses_get is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testGetSuccess() public { assertTrue(!s_addressToAddressMap.contains(address(this))); assertTrue(s_addressToAddressMap.set(address(this), address(this))); assertTrue(s_addressToAddressMap.contains(address(this))); - assertTrue(s_addressToAddressMap.get(address(this)) == address(this)); + assertEq(s_addressToAddressMap.get(address(this)), address(this)); } function testBytes32GetSuccess() public { assertTrue(!s_addressToBytes32Map.contains(address(this))); assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); assertTrue(s_addressToBytes32Map.contains(address(this))); - assertTrue(s_addressToBytes32Map.get(address(this)) == MOCK_BYTES32_VALUE); + assertEq(s_addressToBytes32Map.get(address(this)), MOCK_BYTES32_VALUE); + } + + function testBytesGetSuccess() public { + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertTrue(s_addressToBytesMap.contains(address(this))); + assertEq(s_addressToBytesMap.get(address(this)), MOCK_BYTES_VALUE); } } contract EnumerableMapAddresses_get_errorMessage is EnumerableMapAddressesTest { using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytesMap; function testGetErrorMessageSuccess() public { assertTrue(!s_addressToAddressMap.contains(address(this))); assertTrue(s_addressToAddressMap.set(address(this), address(this))); assertTrue(s_addressToAddressMap.contains(address(this))); - assertTrue(s_addressToAddressMap.get(address(this), "EnumerableMapAddresses: nonexistent key") == address(this)); + assertEq(s_addressToAddressMap.get(address(this), "EnumerableMapAddresses: nonexistent key"), address(this)); + } + + function testBytesGetErrorMessageSuccess() public { + assertTrue(!s_addressToBytesMap.contains(address(this))); + assertTrue(s_addressToBytesMap.set(address(this), MOCK_BYTES_VALUE)); + assertTrue(s_addressToBytesMap.contains(address(this))); + assertEq(s_addressToBytesMap.get(address(this)), MOCK_BYTES_VALUE); } } From 2b86088670596de517b35d652c85a031f0fb6faf Mon Sep 17 00:00:00 2001 From: Anirudh Warrier <12178754+anirudhwarrier@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:54:53 +0400 Subject: [PATCH 13/16] refactor + support v2.3 benchmark test (#14052) * update feeds used in automation tests * comment unused vars * support v2.3 in benchmark test --- .../actions/automation_ocr_helpers.go | 2 +- .../actions/automationv2/actions.go | 67 +++++++++---- integration-tests/actions/keeper_helpers.go | 6 +- integration-tests/benchmark/keeper_test.go | 95 +++++++------------ .../contracts/contract_models.go | 2 +- .../contracts/ethereum_contracts.go | 20 ++-- .../contracts/ethereum_keeper_contracts.go | 1 + .../testconfig/keeper/keeper.toml | 67 ++++++++++++- .../testsetups/keeper_benchmark.go | 28 +++--- 9 files changed, 178 insertions(+), 110 deletions(-) diff --git a/integration-tests/actions/automation_ocr_helpers.go b/integration-tests/actions/automation_ocr_helpers.go index 05c4501fbe6..3e552371f98 100644 --- a/integration-tests/actions/automation_ocr_helpers.go +++ b/integration-tests/actions/automation_ocr_helpers.go @@ -438,7 +438,7 @@ func deployRegistry( wethToken contracts.WETHToken, ethUSDFeed contracts.MockETHUSDFeed, ) contracts.KeeperRegistry { - ef, err := contracts.DeployMockETHLINKFeed(client, big.NewInt(2e18)) + ef, err := contracts.DeployMockLINKETHFeed(client, big.NewInt(2e18)) require.NoError(t, err, "Deploying mock ETH-Link feed shouldn't fail") gf, err := contracts.DeployMockGASFeed(client, big.NewInt(2e11)) require.NoError(t, err, "Deploying mock gas feed shouldn't fail") diff --git a/integration-tests/actions/automationv2/actions.go b/integration-tests/actions/automationv2/actions.go index 40caf15917b..9075b863b65 100644 --- a/integration-tests/actions/automationv2/actions.go +++ b/integration-tests/actions/automationv2/actions.go @@ -62,8 +62,9 @@ type AutomationTest struct { LinkToken contracts.LinkToken Transcoder contracts.UpkeepTranscoder - EthLinkFeed contracts.MockETHLINKFeed - EthUSDFeed contracts.MockETHUSDFeed + LINKETHFeed contracts.MockLINKETHFeed + ETHUSDFeed contracts.MockETHUSDFeed + LINKUSDFeed contracts.MockETHUSDFeed WETHToken contracts.WETHToken GasFeed contracts.MockGasFeed Registry contracts.KeeperRegistry @@ -192,31 +193,30 @@ func (a *AutomationTest) LoadTranscoder(address string) error { return nil } -func (a *AutomationTest) DeployEthLinkFeed() error { - ethLinkFeed, err := contracts.DeployMockETHLINKFeed(a.ChainClient, a.RegistrySettings.FallbackLinkPrice) +func (a *AutomationTest) DeployLinkEthFeed() error { + ethLinkFeed, err := contracts.DeployMockLINKETHFeed(a.ChainClient, a.RegistrySettings.FallbackLinkPrice) if err != nil { return err } - a.EthLinkFeed = ethLinkFeed + a.LINKETHFeed = ethLinkFeed return nil } -func (a *AutomationTest) LoadEthLinkFeed(address string) error { - ethLinkFeed, err := contracts.LoadMockETHLINKFeed(a.ChainClient, common.HexToAddress(address)) +func (a *AutomationTest) LoadLinkEthFeed(address string) error { + ethLinkFeed, err := contracts.LoadMockLINKETHFeed(a.ChainClient, common.HexToAddress(address)) if err != nil { return err } - a.EthLinkFeed = ethLinkFeed + a.LINKETHFeed = ethLinkFeed return nil } func (a *AutomationTest) DeployEthUSDFeed() error { - // FallbackLinkPrice and FallbackETHPrice are the same ethUSDFeed, err := contracts.DeployMockETHUSDFeed(a.ChainClient, a.RegistrySettings.FallbackLinkPrice) if err != nil { return err } - a.EthUSDFeed = ethUSDFeed + a.ETHUSDFeed = ethUSDFeed return nil } @@ -225,7 +225,25 @@ func (a *AutomationTest) LoadEthUSDFeed(address string) error { if err != nil { return err } - a.EthUSDFeed = ethUSDFeed + a.ETHUSDFeed = ethUSDFeed + return nil +} + +func (a *AutomationTest) DeployLinkUSDFeed() error { + linkUSDFeed, err := contracts.DeployMockETHUSDFeed(a.ChainClient, a.RegistrySettings.FallbackLinkPrice) + if err != nil { + return err + } + a.LINKUSDFeed = linkUSDFeed + return nil +} + +func (a *AutomationTest) LoadLinkUSDFeed(address string) error { + linkUSDFeed, err := contracts.LoadMockETHUSDFeed(a.ChainClient, common.HexToAddress(address)) + if err != nil { + return err + } + a.LINKUSDFeed = linkUSDFeed return nil } @@ -269,13 +287,13 @@ func (a *AutomationTest) DeployRegistry() error { registryOpts := &contracts.KeeperRegistryOpts{ RegistryVersion: a.RegistrySettings.RegistryVersion, LinkAddr: a.LinkToken.Address(), - ETHFeedAddr: a.EthLinkFeed.Address(), + ETHFeedAddr: a.LINKETHFeed.Address(), GasFeedAddr: a.GasFeed.Address(), TranscoderAddr: a.Transcoder.Address(), RegistrarAddr: utils.ZeroAddress.Hex(), Settings: a.RegistrySettings, - LinkUSDFeedAddr: a.EthUSDFeed.Address(), - NativeUSDFeedAddr: a.EthUSDFeed.Address(), + LinkUSDFeedAddr: a.ETHUSDFeed.Address(), + NativeUSDFeedAddr: a.LINKUSDFeed.Address(), WrappedNativeAddr: a.WETHToken.Address(), } registry, err := contracts.DeployKeeperRegistry(a.ChainClient, registryOpts) @@ -563,7 +581,7 @@ func (a *AutomationTest) SetConfigOnRegistry() error { { GasFeePPB: 100, FlatFeeMilliCents: big.NewInt(500), - PriceFeed: common.HexToAddress(a.EthUSDFeed.Address()), // ETH/USD feed and LINK/USD feed are the same + PriceFeed: common.HexToAddress(a.ETHUSDFeed.Address()), Decimals: 18, FallbackPrice: big.NewInt(1000), MinSpend: big.NewInt(200), @@ -571,7 +589,7 @@ func (a *AutomationTest) SetConfigOnRegistry() error { { GasFeePPB: 100, FlatFeeMilliCents: big.NewInt(500), - PriceFeed: common.HexToAddress(a.EthUSDFeed.Address()), // ETH/USD feed and LINK/USD feed are the same + PriceFeed: common.HexToAddress(a.LINKUSDFeed.Address()), Decimals: 18, FallbackPrice: big.NewInt(1000), MinSpend: big.NewInt(200), @@ -853,14 +871,17 @@ func (a *AutomationTest) SetupAutomationDeployment(t *testing.T) { err = a.DeployWETH() require.NoError(t, err, "Error deploying weth token contract") - err = a.DeployEthLinkFeed() - require.NoError(t, err, "Error deploying eth link feed contract") + err = a.DeployLinkEthFeed() + require.NoError(t, err, "Error deploying link eth feed contract") err = a.DeployGasFeed() require.NoError(t, err, "Error deploying gas feed contract") err = a.DeployEthUSDFeed() require.NoError(t, err, "Error deploying eth usd feed contract") + err = a.DeployLinkUSDFeed() + require.NoError(t, err, "Error deploying link usd feed contract") + err = a.DeployTranscoder() require.NoError(t, err, "Error deploying transcoder contract") @@ -873,7 +894,7 @@ func (a *AutomationTest) SetupAutomationDeployment(t *testing.T) { } func (a *AutomationTest) LoadAutomationDeployment(t *testing.T, linkTokenAddress, - ethLinkFeedAddress, gasFeedAddress, transcoderAddress, registryAddress, registrarAddress string) { + linkEthFeedAddress, linkUsdFeedAddress, EthUsdFeedAddress, gasFeedAddress, transcoderAddress, registryAddress, registrarAddress string) { l := logging.GetTestLogger(t) err := a.CollectNodeDetails() require.NoError(t, err, "Error collecting node details") @@ -883,10 +904,14 @@ func (a *AutomationTest) LoadAutomationDeployment(t *testing.T, linkTokenAddress err = a.LoadLINK(linkTokenAddress) require.NoError(t, err, "Error loading link token contract") - err = a.LoadEthLinkFeed(ethLinkFeedAddress) - require.NoError(t, err, "Error loading eth link feed contract") + err = a.LoadLinkEthFeed(linkEthFeedAddress) + require.NoError(t, err, "Error loading link eth feed contract") err = a.LoadEthGasFeed(gasFeedAddress) require.NoError(t, err, "Error loading gas feed contract") + err = a.LoadEthUSDFeed(EthUsdFeedAddress) + require.NoError(t, err, "Error loading eth usd feed contract") + err = a.LoadLinkUSDFeed(linkUsdFeedAddress) + require.NoError(t, err, "Error loading link usd feed contract") err = a.LoadTranscoder(transcoderAddress) require.NoError(t, err, "Error loading transcoder contract") err = a.LoadRegistry(registryAddress) diff --git a/integration-tests/actions/keeper_helpers.go b/integration-tests/actions/keeper_helpers.go index ee1662cc180..618ca969334 100644 --- a/integration-tests/actions/keeper_helpers.go +++ b/integration-tests/actions/keeper_helpers.go @@ -81,7 +81,7 @@ func DeployKeeperContracts( client *seth.Client, linkFundsForEachUpkeep *big.Int, ) (contracts.KeeperRegistry, contracts.KeeperRegistrar, []contracts.KeeperConsumer, []*big.Int) { - ef, err := contracts.DeployMockETHLINKFeed(client, big.NewInt(2e18)) + ef, err := contracts.DeployMockLINKETHFeed(client, big.NewInt(2e18)) require.NoError(t, err, "Deploying mock ETH-Link feed shouldn't fail") gf, err := contracts.DeployMockGASFeed(client, big.NewInt(2e11)) require.NoError(t, err, "Deploying mock gas feed shouldn't fail") @@ -136,7 +136,7 @@ func DeployPerformanceKeeperContracts( checkGasToBurn, // How much gas should be burned on checkUpkeep() calls performGasToBurn int64, // How much gas should be burned on performUpkeep() calls ) (contracts.KeeperRegistry, contracts.KeeperRegistrar, []contracts.KeeperConsumerPerformance, []*big.Int) { - ef, err := contracts.DeployMockETHLINKFeed(chainClient, big.NewInt(2e18)) + ef, err := contracts.DeployMockLINKETHFeed(chainClient, big.NewInt(2e18)) require.NoError(t, err, "Deploying mock ETH-Link feed shouldn't fail") gf, err := contracts.DeployMockGASFeed(chainClient, big.NewInt(2e11)) require.NoError(t, err, "Deploying mock gas feed shouldn't fail") @@ -196,7 +196,7 @@ func DeployPerformDataCheckerContracts( linkFundsForEachUpkeep *big.Int, expectedData []byte, ) (contracts.KeeperRegistry, contracts.KeeperRegistrar, []contracts.KeeperPerformDataChecker, []*big.Int) { - ef, err := contracts.DeployMockETHLINKFeed(chainClient, big.NewInt(2e18)) + ef, err := contracts.DeployMockLINKETHFeed(chainClient, big.NewInt(2e18)) require.NoError(t, err, "Deploying mock ETH-Link feed shouldn't fail") gf, err := contracts.DeployMockGASFeed(chainClient, big.NewInt(2e11)) require.NoError(t, err, "Deploying mock gas feed shouldn't fail") diff --git a/integration-tests/benchmark/keeper_test.go b/integration-tests/benchmark/keeper_test.go index 177b3521013..fde550bbdf4 100644 --- a/integration-tests/benchmark/keeper_test.go +++ b/integration-tests/benchmark/keeper_test.go @@ -10,26 +10,26 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/blockchain" - ctf_config "github.com/smartcontractkit/chainlink-testing-framework/config" - env_client "github.com/smartcontractkit/chainlink-testing-framework/k8s/client" + ctfconfig "github.com/smartcontractkit/chainlink-testing-framework/config" + envclient "github.com/smartcontractkit/chainlink-testing-framework/k8s/client" "github.com/smartcontractkit/chainlink-testing-framework/k8s/environment" "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/chainlink" "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/ethereum" "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/reorg" "github.com/smartcontractkit/chainlink-testing-framework/logging" "github.com/smartcontractkit/chainlink-testing-framework/networks" - seth_utils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" + sethutils "github.com/smartcontractkit/chainlink-testing-framework/utils/seth" "github.com/smartcontractkit/chainlink/integration-tests/actions" "github.com/smartcontractkit/chainlink/integration-tests/contracts" - eth_contracts "github.com/smartcontractkit/chainlink/integration-tests/contracts/ethereum" + ethcontracts "github.com/smartcontractkit/chainlink/integration-tests/contracts/ethereum" tc "github.com/smartcontractkit/chainlink/integration-tests/testconfig" "github.com/smartcontractkit/chainlink/integration-tests/testsetups" "github.com/smartcontractkit/chainlink/integration-tests/types" ) var ( - performanceChainlinkResources = map[string]interface{}{ + chainlinkResources = map[string]interface{}{ "resources": map[string]interface{}{ "requests": map[string]interface{}{ "cpu": "1000m", @@ -41,7 +41,7 @@ var ( }, }, } - performanceDbResources = map[string]interface{}{ + dbResources = map[string]interface{}{ "resources": map[string]interface{}{ "requests": map[string]interface{}{ "cpu": "1000m", @@ -55,33 +55,6 @@ var ( "stateful": true, "capacity": "10Gi", } - - soakChainlinkResources = map[string]interface{}{ - "resources": map[string]interface{}{ - "requests": map[string]interface{}{ - "cpu": "350m", - "memory": "1Gi", - }, - "limits": map[string]interface{}{ - "cpu": "350m", - "memory": "1Gi", - }, - }, - } - soakDbResources = map[string]interface{}{ - "resources": map[string]interface{}{ - "requests": map[string]interface{}{ - "cpu": "250m", - "memory": "256Mi", - }, - "limits": map[string]interface{}{ - "cpu": "250m", - "memory": "256Mi", - }, - }, - "stateful": true, - "capacity": "10Gi", - } ) type NetworkConfig struct { @@ -115,9 +88,9 @@ func TestAutomationBenchmark(t *testing.T) { benchmarkTestNetwork := getNetworkConfig(&config) l.Info().Str("Namespace", testEnvironment.Cfg.Namespace).Msg("Connected to Keepers Benchmark Environment") - testNetwork := seth_utils.MustReplaceSimulatedNetworkUrlWithK8(l, benchmarkNetwork, *testEnvironment) + testNetwork := sethutils.MustReplaceSimulatedNetworkUrlWithK8(l, benchmarkNetwork, *testEnvironment) - chainClient, err := seth_utils.GetChainClientWithConfigFunction(&config, testNetwork, seth_utils.OneEphemeralKeysLiveTestnetAutoFixFn) + chainClient, err := sethutils.GetChainClientWithConfigFunction(&config, testNetwork, sethutils.OneEphemeralKeysLiveTestnetAutoFixFn) require.NoError(t, err, "Error getting Seth client") registryVersions := addRegistry(&config) @@ -173,40 +146,42 @@ func TestAutomationBenchmark(t *testing.T) { keeperBenchmarkTest.Run() } -func addRegistry(config *tc.TestConfig) []eth_contracts.KeeperRegistryVersion { +func addRegistry(config *tc.TestConfig) []ethcontracts.KeeperRegistryVersion { switch *config.Keeper.Common.RegistryToTest { case "1_1": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_1_1} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_1_1} case "1_2": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_1_2} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_1_2} case "1_3": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_1_3} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_1_3} case "2_0": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_0} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_0} case "2_1": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_1} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_1} case "2_2": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_2} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_2} + case "2_3": + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_3} case "2_0-1_3": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_0, eth_contracts.RegistryVersion_1_3} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_0, ethcontracts.RegistryVersion_1_3} case "2_1-2_0-1_3": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_1, - eth_contracts.RegistryVersion_2_0, eth_contracts.RegistryVersion_1_3} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_1, + ethcontracts.RegistryVersion_2_0, ethcontracts.RegistryVersion_1_3} case "2_2-2_1": - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_2, eth_contracts.RegistryVersion_2_1} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_2, ethcontracts.RegistryVersion_2_1} case "2_0-Multiple": - return repeatRegistries(eth_contracts.RegistryVersion_2_0, *config.Keeper.Common.NumberOfRegistries) + return repeatRegistries(ethcontracts.RegistryVersion_2_0, *config.Keeper.Common.NumberOfRegistries) case "2_1-Multiple": - return repeatRegistries(eth_contracts.RegistryVersion_2_1, *config.Keeper.Common.NumberOfRegistries) + return repeatRegistries(ethcontracts.RegistryVersion_2_1, *config.Keeper.Common.NumberOfRegistries) case "2_2-Multiple": - return repeatRegistries(eth_contracts.RegistryVersion_2_2, *config.Keeper.Common.NumberOfRegistries) + return repeatRegistries(ethcontracts.RegistryVersion_2_2, *config.Keeper.Common.NumberOfRegistries) default: - return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_0} + return []ethcontracts.KeeperRegistryVersion{ethcontracts.RegistryVersion_2_0} } } -func repeatRegistries(registryVersion eth_contracts.KeeperRegistryVersion, numberOfRegistries int) []eth_contracts.KeeperRegistryVersion { - repeatedRegistries := make([]eth_contracts.KeeperRegistryVersion, 0) +func repeatRegistries(registryVersion ethcontracts.KeeperRegistryVersion, numberOfRegistries int) []ethcontracts.KeeperRegistryVersion { + repeatedRegistries := make([]ethcontracts.KeeperRegistryVersion, 0) for i := 0; i < numberOfRegistries; i++ { repeatedRegistries = append(repeatedRegistries, registryVersion) } @@ -316,12 +291,8 @@ func SetupAutomationBenchmarkEnv(t *testing.T, keeperTestConfig types.KeeperBenc PreventPodEviction: true, }) - dbResources := performanceDbResources - chainlinkResources := performanceChainlinkResources - if strings.Contains(strings.ToLower(strings.Join(keeperTestConfig.GetConfigurationNames(), ",")), "soak") { - chainlinkResources = soakChainlinkResources - dbResources = soakDbResources - } + dbResources := dbResources + chainlinkResources := chainlinkResources // Test can run on simulated, simulated-non-dev, testnets if testNetwork.Name == networks.SimulatedEVMNonDev.Name { @@ -389,10 +360,10 @@ func SetupAutomationBenchmarkEnv(t *testing.T, keeperTestConfig types.KeeperBenc // for simulated-nod-dev each CL node gets its own RPC node if testNetwork.Name == networks.SimulatedEVMNonDev.Name { podName := fmt.Sprintf("%s-ethereum-geth:%d", testNetwork.Name, i) - txNodeInternalWs, err := testEnvironment.Fwd.FindPort(podName, "geth", "ws-rpc").As(env_client.RemoteConnection, env_client.WS) + txNodeInternalWs, err := testEnvironment.Fwd.FindPort(podName, "geth", "ws-rpc").As(envclient.RemoteConnection, envclient.WS) require.NoError(t, err, "Error finding WS ports") internalWsURLs = append(internalWsURLs, txNodeInternalWs) - txNodeInternalHttp, err := testEnvironment.Fwd.FindPort(podName, "geth", "http-rpc").As(env_client.RemoteConnection, env_client.HTTP) + txNodeInternalHttp, err := testEnvironment.Fwd.FindPort(podName, "geth", "http-rpc").As(envclient.RemoteConnection, envclient.HTTP) require.NoError(t, err, "Error finding HTTP ports") internalHttpURLs = append(internalHttpURLs, txNodeInternalHttp) // for testnets with more than 1 RPC nodes @@ -412,8 +383,8 @@ func SetupAutomationBenchmarkEnv(t *testing.T, keeperTestConfig types.KeeperBenc testNetwork.URLs = []string{internalWsURLs[i]} var overrideFn = func(_ interface{}, target interface{}) { - ctf_config.MustConfigOverrideChainlinkVersion(keeperTestConfig.GetChainlinkImageConfig(), target) - ctf_config.MightConfigOverridePyroscopeKey(keeperTestConfig.GetPyroscopeConfig(), target) + ctfconfig.MustConfigOverrideChainlinkVersion(keeperTestConfig.GetChainlinkImageConfig(), target) + ctfconfig.MightConfigOverridePyroscopeKey(keeperTestConfig.GetPyroscopeConfig(), target) } tomlConfig, err := actions.BuildTOMLNodeConfigForK8s(keeperTestConfig, testNetwork) diff --git a/integration-tests/contracts/contract_models.go b/integration-tests/contracts/contract_models.go index 5983f95c9f5..ea63f1aa4db 100644 --- a/integration-tests/contracts/contract_models.go +++ b/integration-tests/contracts/contract_models.go @@ -224,7 +224,7 @@ type JobByInstance struct { Instance string } -type MockETHLINKFeed interface { +type MockLINKETHFeed interface { Address() string LatestRoundData() (*big.Int, error) LatestRoundDataUpdatedAt() (*big.Int, error) diff --git a/integration-tests/contracts/ethereum_contracts.go b/integration-tests/contracts/ethereum_contracts.go index 5b08c9a9fbf..0d493dff45e 100644 --- a/integration-tests/contracts/ethereum_contracts.go +++ b/integration-tests/contracts/ethereum_contracts.go @@ -1195,19 +1195,19 @@ func (v *EthereumMockETHLINKFeed) LatestRoundDataUpdatedAt() (*big.Int, error) { return data.UpdatedAt, nil } -func DeployMockETHLINKFeed(client *seth.Client, answer *big.Int) (MockETHLINKFeed, error) { +func DeployMockLINKETHFeed(client *seth.Client, answer *big.Int) (MockLINKETHFeed, error) { abi, err := mock_ethlink_aggregator_wrapper.MockETHLINKAggregatorMetaData.GetAbi() if err != nil { - return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to get MockETHLINKFeed ABI: %w", err) + return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to get MockLINKETHFeed ABI: %w", err) } - data, err := client.DeployContract(client.NewTXOpts(), "MockETHLINKFeed", *abi, common.FromHex(mock_ethlink_aggregator_wrapper.MockETHLINKAggregatorMetaData.Bin), answer) + data, err := client.DeployContract(client.NewTXOpts(), "MockLINKETHFeed", *abi, common.FromHex(mock_ethlink_aggregator_wrapper.MockETHLINKAggregatorMetaData.Bin), answer) if err != nil { - return &EthereumMockETHLINKFeed{}, fmt.Errorf("MockETHLINKFeed instance deployment have failed: %w", err) + return &EthereumMockETHLINKFeed{}, fmt.Errorf("MockLINKETHFeed instance deployment have failed: %w", err) } instance, err := mock_ethlink_aggregator_wrapper.NewMockETHLINKAggregator(data.Address, wrappers.MustNewWrappedContractBackend(nil, client)) if err != nil { - return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to instantiate MockETHLINKFeed instance: %w", err) + return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to instantiate MockLINKETHFeed instance: %w", err) } return &EthereumMockETHLINKFeed{ @@ -1217,17 +1217,17 @@ func DeployMockETHLINKFeed(client *seth.Client, answer *big.Int) (MockETHLINKFee }, nil } -func LoadMockETHLINKFeed(client *seth.Client, address common.Address) (MockETHLINKFeed, error) { +func LoadMockLINKETHFeed(client *seth.Client, address common.Address) (MockLINKETHFeed, error) { abi, err := mock_ethlink_aggregator_wrapper.MockETHLINKAggregatorMetaData.GetAbi() if err != nil { - return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to get MockETHLINKFeed ABI: %w", err) + return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to get MockLINKETHFeed ABI: %w", err) } - client.ContractStore.AddABI("MockETHLINKFeed", *abi) - client.ContractStore.AddBIN("MockETHLINKFeed", common.FromHex(mock_ethlink_aggregator_wrapper.MockETHLINKAggregatorMetaData.Bin)) + client.ContractStore.AddABI("MockLINKETHFeed", *abi) + client.ContractStore.AddBIN("MockLINKETHFeed", common.FromHex(mock_ethlink_aggregator_wrapper.MockETHLINKAggregatorMetaData.Bin)) instance, err := mock_ethlink_aggregator_wrapper.NewMockETHLINKAggregator(address, wrappers.MustNewWrappedContractBackend(nil, client)) if err != nil { - return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to instantiate MockETHLINKFeed instance: %w", err) + return &EthereumMockETHLINKFeed{}, fmt.Errorf("failed to instantiate MockLINKETHFeed instance: %w", err) } return &EthereumMockETHLINKFeed{ diff --git a/integration-tests/contracts/ethereum_keeper_contracts.go b/integration-tests/contracts/ethereum_keeper_contracts.go index 28fdd15b13e..38aa5c58f0f 100644 --- a/integration-tests/contracts/ethereum_keeper_contracts.go +++ b/integration-tests/contracts/ethereum_keeper_contracts.go @@ -150,6 +150,7 @@ type KeeperRegistrySettings struct { MaxPerformGas uint32 // max gas allowed for an upkeep within perform FallbackGasPrice *big.Int // gas price used if the gas price feed is stale FallbackLinkPrice *big.Int // LINK price used if the LINK price feed is stale + FallbackNativePrice *big.Int // Native price used if the Native price feed is stale MaxCheckDataSize uint32 MaxPerformDataSize uint32 MaxRevertDataSize uint32 diff --git a/integration-tests/testconfig/keeper/keeper.toml b/integration-tests/testconfig/keeper/keeper.toml index b4a6a3b2c0e..39eae1ea53c 100644 --- a/integration-tests/testconfig/keeper/keeper.toml +++ b/integration-tests/testconfig/keeper/keeper.toml @@ -57,7 +57,21 @@ contract_call_interval = "4s" [Seth] # keeper benchmark running on simulated network requires 100k per node -root_key_funds_buffer = 700_000 +root_key_funds_buffer = 1_000_000 + +[Benchmark.Keeper.Common] +registry_to_test = "2_1" +number_of_registries = 1 +number_of_nodes = 6 +number_of_upkeeps = 1000 +upkeep_gas_limit = 1500000 +check_gas_to_burn = 10000 +perform_gas_to_burn = 1000 +max_perform_gas = 5000000 +block_range = 3600 +block_interval = 60 +forces_single_tx_key = false +delete_jobs_on_end = true [Benchmark.NodeConfig] BaseConfigTOML = """ @@ -95,3 +109,54 @@ HistoryDepth = 100 Mode = 'FixedPrice' LimitDefault = 5_000_000 """ + +[Soak.Keeper.Common] +registry_to_test = "2_1" +number_of_registries = 1 +number_of_nodes = 6 +number_of_upkeeps = 50 +upkeep_gas_limit = 1500000 +check_gas_to_burn = 10000 +perform_gas_to_burn = 1000 +max_perform_gas = 5000000 +block_range = 28800 +block_interval = 300 +forces_single_tx_key = false +delete_jobs_on_end = true + +[Soak.NodeConfig] +BaseConfigTOML = """ +[Feature] +LogPoller = true + +[OCR2] +Enabled = true + +[P2P] +[P2P.V2] +Enabled = true +AnnounceAddresses = ["0.0.0.0:6690"] +ListenAddresses = ["0.0.0.0:6690"] +[Keeper] +TurnLookBack = 0 +[WebServer] +HTTPWriteTimeout = '1h' +""" + +CommonChainConfigTOML = """ +""" + +[Soak.NodeConfig.ChainConfigTOMLByChainID] +# applicable for simulated chain +1337 = """ +FinalityDepth = 50 +LogPollInterval = '1s' +MinIncomingConfirmations = 1 + +[HeadTracker] +HistoryDepth = 100 + +[GasEstimator] +Mode = 'FixedPrice' +LimitDefault = 5_000_000 +""" diff --git a/integration-tests/testsetups/keeper_benchmark.go b/integration-tests/testsetups/keeper_benchmark.go index 4803a5249f0..a3d6c426e4b 100644 --- a/integration-tests/testsetups/keeper_benchmark.go +++ b/integration-tests/testsetups/keeper_benchmark.go @@ -62,9 +62,11 @@ type KeeperBenchmarkTest struct { chainClient *seth.Client testConfig tt.KeeperBenchmarkTestConfig - linkToken contracts.LinkToken - ethFeed contracts.MockETHLINKFeed - gasFeed contracts.MockGasFeed + linkToken contracts.LinkToken + linkethFeed contracts.MockLINKETHFeed + gasFeed contracts.MockGasFeed + ethusdFeed contracts.MockETHUSDFeed + wrappedNative contracts.WETHToken } // UpkeepConfig dictates details of how the test's upkeep contracts should be called and configured @@ -163,10 +165,10 @@ func (k *KeeperBenchmarkTest) Setup(env *environment.Environment, config tt.Keep } if common.IsHexAddress(c.EthFeedAddress) { - _, err = contracts.LoadMockETHLINKFeed(k.chainClient, common.HexToAddress(c.EthFeedAddress)) + _, err = contracts.LoadMockLINKETHFeed(k.chainClient, common.HexToAddress(c.EthFeedAddress)) require.NoError(k.t, err, "Loading ETH-Link feed Contract shouldn't fail") } else { - k.ethFeed, err = contracts.DeployMockETHLINKFeed(k.chainClient, big.NewInt(2e18)) + k.linkethFeed, err = contracts.DeployMockLINKETHFeed(k.chainClient, big.NewInt(2e18)) require.NoError(k.t, err, "Deploying mock ETH-Link feed shouldn't fail") } @@ -178,6 +180,11 @@ func (k *KeeperBenchmarkTest) Setup(env *environment.Environment, config tt.Keep require.NoError(k.t, err, "Deploying mock gas feed shouldn't fail") } + k.ethusdFeed, err = contracts.DeployMockETHUSDFeed(k.chainClient, big.NewInt(200000000000)) + require.NoError(k.t, err, "Deploying mock ETH-USD feed shouldn't fail") + k.wrappedNative, err = contracts.DeployWETHTokenContract(k.log, k.chainClient) + require.NoError(k.t, err, "Deploying WETH Token Contract shouldn't fail") + for index := range inputs.RegistryVersions { k.log.Info().Int("Index", index).Msg("Starting Test Setup") k.DeployBenchmarkKeeperContracts(index) @@ -240,15 +247,14 @@ func (k *KeeperBenchmarkTest) Run() { txKeyId = 0 } kr := k.keeperRegistries[rIndex] - // TODO: need to add the LINK, WETH and WETH/USD feed to support v23 ocrConfig, err := actions.BuildAutoOCR2ConfigVarsWithKeyIndex( - k.t, nodesWithoutBootstrap, *inputs.KeeperRegistrySettings, kr.Address(), k.Inputs.DeltaStage, txKeyId, common.Address{}, kr.ChainModuleAddress(), kr.ReorgProtectionEnabled(), nil, nil, nil, + k.t, nodesWithoutBootstrap, *inputs.KeeperRegistrySettings, kr.Address(), k.Inputs.DeltaStage, txKeyId, common.Address{}, kr.ChainModuleAddress(), kr.ReorgProtectionEnabled(), k.linkToken, k.wrappedNative, k.ethusdFeed, ) require.NoError(k.t, err, "Building OCR config shouldn't fail") rv := inputs.RegistryVersions[rIndex] // Send keeper jobs to registry and chainlink nodes - if rv == ethereum.RegistryVersion_2_0 || rv == ethereum.RegistryVersion_2_1 || rv == ethereum.RegistryVersion_2_2 { + if rv >= ethereum.RegistryVersion_2_0 { actions.CreateOCRKeeperJobs(k.t, k.chainlinkNodes, kr.Address(), k.chainClient.ChainID, txKeyId, rv) if rv == ethereum.RegistryVersion_2_0 { err = kr.SetConfig(*inputs.KeeperRegistrySettings, ocrConfig) @@ -708,7 +714,7 @@ func (k *KeeperBenchmarkTest) DeployBenchmarkKeeperContracts(index int) { registry, err = contracts.DeployKeeperRegistry(k.chainClient, &contracts.KeeperRegistryOpts{ RegistryVersion: registryVersion, LinkAddr: k.linkToken.Address(), - ETHFeedAddr: k.ethFeed.Address(), + ETHFeedAddr: k.linkethFeed.Address(), GasFeedAddr: k.gasFeed.Address(), TranscoderAddr: actions.ZeroAddress.Hex(), RegistrarAddr: actions.ZeroAddress.Hex(), @@ -731,13 +737,13 @@ func (k *KeeperBenchmarkTest) DeployBenchmarkKeeperContracts(index int) { require.NoError(k.t, err, "Funding keeper registrar contract shouldn't fail") } else { // OCR automation - v2.X registry, registrar = actions.DeployAutoOCRRegistryAndRegistrar( - k.t, k.chainClient, registryVersion, *k.Inputs.KeeperRegistrySettings, k.linkToken, nil, nil, + k.t, k.chainClient, registryVersion, *k.Inputs.KeeperRegistrySettings, k.linkToken, k.wrappedNative, k.ethusdFeed, ) // Fund the registry with LINK err := k.linkToken.Transfer(registry.Address(), big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(int64(k.Inputs.Upkeeps.NumberOfUpkeeps)))) require.NoError(k.t, err, "Funding keeper registry contract shouldn't fail") - ocrConfig, err := actions.BuildAutoOCR2ConfigVars(k.t, k.chainlinkNodes[1:], *k.Inputs.KeeperRegistrySettings, registrar.Address(), k.Inputs.DeltaStage, registry.ChainModuleAddress(), registry.ReorgProtectionEnabled(), nil, nil, nil) + ocrConfig, err := actions.BuildAutoOCR2ConfigVars(k.t, k.chainlinkNodes[1:], *k.Inputs.KeeperRegistrySettings, registrar.Address(), k.Inputs.DeltaStage, registry.ChainModuleAddress(), registry.ReorgProtectionEnabled(), k.linkToken, k.wrappedNative, k.ethusdFeed) require.NoError(k.t, err, "Building OCR config shouldn't fail") k.log.Debug().Interface("KeeperRegistrySettings", *k.Inputs.KeeperRegistrySettings).Interface("OCRConfig", ocrConfig).Msg("Config") require.NoError(k.t, err, "Error building OCR config vars") From 5d4d996ed275b78eda8d189ad9fac3ddc3055fe2 Mon Sep 17 00:00:00 2001 From: Cedric Date: Mon, 12 Aug 2024 21:10:52 +0100 Subject: [PATCH 14/16] [fix] Don't initialize the remote dispatcher (#14097) --- core/services/chainlink/application.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 6a381b1ffa8..17c217b1c90 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -214,8 +214,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { signer := externalPeer externalPeerWrapper = externalPeer remoteDispatcher := remote.NewDispatcher(externalPeerWrapper, signer, opts.CapabilitiesRegistry, globalLogger) - srvcs = append(srvcs, remoteDispatcher) - dispatcher = remoteDispatcher } else { dispatcher = opts.CapabilitiesDispatcher From 220ca2a88aa8c3666da6d45dbf5d5a30e9f584ce Mon Sep 17 00:00:00 2001 From: Sergey Kudasov Date: Tue, 13 Aug 2024 10:49:15 +0200 Subject: [PATCH 15/16] Turn CRIB persistence test by default (#14101) * try run chaos test for CRIB * less logs * finalize --- .github/workflows/crib-integration-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crib-integration-test.yml b/.github/workflows/crib-integration-test.yml index 248004636bc..a67ac641bf9 100644 --- a/.github/workflows/crib-integration-test.yml +++ b/.github/workflows/crib-integration-test.yml @@ -98,9 +98,9 @@ jobs: CRIB_NETWORK: geth CRIB_NODES: 5 GAP_URL: ${{ secrets.GAP_URL }} -# SETH_LOG_LEVEL: debug + SETH_LOG_LEVEL: info # RESTY_DEBUG: true -# TEST_PERSISTENCE: true + TEST_PERSISTENCE: true run: |- go test -v -run TestCRIB - name: Destroy CRIB Environment From eb31cf7970bef1615b10b5a734c16879b448f30a Mon Sep 17 00:00:00 2001 From: Matthew Pendrey Date: Tue, 13 Aug 2024 15:17:00 +0100 Subject: [PATCH 16/16] speedup keystone e2e tests (#14105) --- .changeset/ninety-ways-run.md | 5 +++++ .../integration_tests/keystone_contracts_setup.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/ninety-ways-run.md diff --git a/.changeset/ninety-ways-run.md b/.changeset/ninety-ways-run.md new file mode 100644 index 00000000000..0b4508bdd24 --- /dev/null +++ b/.changeset/ninety-ways-run.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal speed up keystone e2e tests diff --git a/core/capabilities/integration_tests/keystone_contracts_setup.go b/core/capabilities/integration_tests/keystone_contracts_setup.go index 004a4c32a3a..38925cb0a3c 100644 --- a/core/capabilities/integration_tests/keystone_contracts_setup.go +++ b/core/capabilities/integration_tests/keystone_contracts_setup.go @@ -209,7 +209,7 @@ func setupCapabilitiesRegistryContract(ctx context.Context, t *testing.T, workfl triggerCapabilityConfig := newCapabilityConfig() triggerCapabilityConfig.RemoteConfig = &pb.CapabilityConfig_RemoteTriggerConfig{ RemoteTriggerConfig: &pb.RemoteTriggerConfig{ - RegistrationRefresh: durationpb.New(60000 * time.Millisecond), + RegistrationRefresh: durationpb.New(1000 * time.Millisecond), RegistrationExpiry: durationpb.New(60000 * time.Millisecond), // F + 1 MinResponsesToAggregate: uint32(triggerDon.F) + 1,