From 0e4b4adcd491d2c391ac5d4b3b513c0a72305a4c Mon Sep 17 00:00:00 2001 From: Benjamin Samuels <1222451+bsamuels453@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:23:28 -0700 Subject: [PATCH] squash --- .github/workflows/lint.yml | 31 + .gitignore | 12 + .golangci.yml | 20 + .pre-commit-config.yaml | 5 + CODEOWNERS | 5 + LICENSE | 661 ++++++++++++++++++ README.md | 112 +++ cmd/attacknet/main.go | 113 +++ docs/DOCUMENTATION.md | 283 ++++++++ docs/attacknet.svg | 4 + go.mod | 149 ++++ go.sum | 510 ++++++++++++++ network-configs/default.yaml | 43 ++ network-configs/devnet-12.yaml | 71 ++ .../plan/network-latency-reth.yaml | 197 ++++++ .../plan/restart-resillience-reth.yaml | 213 ++++++ network-configs/reth.yaml | 129 ++++ pkg/chaos-mesh/client.go | 102 +++ pkg/chaos-mesh/pods.go | 45 ++ pkg/chaos-mesh/session.go | 339 +++++++++ pkg/exploration/explore.go | 278 ++++++++ pkg/grafana.go | 59 ++ pkg/health/checker.go | 91 +++ pkg/health/ethereum/beacon_rpc.go | 200 ++++++ pkg/health/ethereum/consensus.go | 118 ++++ pkg/health/ethereum/execution_rpc.go | 150 ++++ pkg/health/ethereum/network_checker.go | 134 ++++ pkg/health/ethereum/util.go | 39 ++ pkg/health/types/types.go | 28 + pkg/kubernetes/kubernetes.go | 79 +++ pkg/kubernetes/port_forward.go | 148 ++++ pkg/kubernetes/types.go | 31 + pkg/kubernetes/utils.go | 16 + pkg/kurtosis/kurtosis.go | 181 +++++ pkg/plan/config.go | 67 ++ pkg/plan/file.go | 75 ++ pkg/plan/network/clients.go | 25 + pkg/plan/network/consensus.go | 75 ++ pkg/plan/network/execution.go | 45 ++ pkg/plan/network/network_builder.go | 172 +++++ pkg/plan/network/types.go | 63 ++ pkg/plan/plan.go | 76 ++ pkg/plan/serialization.go | 124 ++++ pkg/plan/suite/faults.go | 329 +++++++++ pkg/plan/suite/step_builder.go | 143 ++++ pkg/plan/suite/suite_builder.go | 229 ++++++ pkg/plan/suite/targeting.go | 238 +++++++ pkg/plan/suite/targeting_test.go | 116 +++ pkg/plan/suite/test_builder.go | 123 ++++ pkg/plan/suite/types.go | 85 +++ pkg/plan/types.go | 68 ++ pkg/project/config.go | 75 ++ pkg/project/project.go | 52 ++ pkg/runtime.go | 91 +++ pkg/runtime/runtime.go | 70 ++ pkg/test_executor/executor.go | 213 ++++++ pkg/test_executor/types.go | 17 + pkg/types/config.go | 68 ++ planner-configs/clock-skew-nethermind.yaml | 69 ++ planner-configs/clock-skew-reth.yaml | 70 ++ planner-configs/clock-skew.yaml | 63 ++ planner-configs/io-latency-reth.yaml | 53 ++ planner-configs/network-latency-reth.yaml | 56 ++ planner-configs/network-packet-drop-geth.yaml | 54 ++ planner-configs/restart-resillience-reth.yaml | 52 ++ terraform/chaos-mesh.tf | 64 ++ terraform/digitalocean.tf | 47 ++ terraform/main.tf | 15 + terraform/provider.tf | 31 + terraform/terraform.tf | 46 ++ test-suites/clock-skew.yaml | 36 + test-suites/cpu-stress.yaml | 44 ++ test-suites/io-fault.yaml | 45 ++ test-suites/io-latency.yaml | 50 ++ test-suites/io-mistake.yaml | 53 ++ test-suites/kernel-fault.yaml | 43 ++ test-suites/lighthouse-stale-safe-issue.yaml | 41 ++ test-suites/memory-stress.yaml | 46 ++ test-suites/network-bandwidth.yaml | 43 ++ test-suites/network-latency.yaml | 39 ++ test-suites/network-split.yaml | 40 ++ test-suites/packet-corruption.yaml | 43 ++ test-suites/packet-drop.yaml | 41 ++ test-suites/plan/network-latency-reth.yaml | 288 ++++++++ .../plan/restart-resillience-reth.yaml | 169 +++++ test-suites/pod-kill.yaml | 38 + test-suites/pod-restart.yaml | 37 + test-suites/suite.yaml | 35 + 88 files changed, 8986 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CODEOWNERS create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/attacknet/main.go create mode 100644 docs/DOCUMENTATION.md create mode 100644 docs/attacknet.svg create mode 100644 go.mod create mode 100644 go.sum create mode 100644 network-configs/default.yaml create mode 100644 network-configs/devnet-12.yaml create mode 100644 network-configs/plan/network-latency-reth.yaml create mode 100644 network-configs/plan/restart-resillience-reth.yaml create mode 100644 network-configs/reth.yaml create mode 100644 pkg/chaos-mesh/client.go create mode 100644 pkg/chaos-mesh/pods.go create mode 100644 pkg/chaos-mesh/session.go create mode 100644 pkg/exploration/explore.go create mode 100644 pkg/grafana.go create mode 100644 pkg/health/checker.go create mode 100644 pkg/health/ethereum/beacon_rpc.go create mode 100644 pkg/health/ethereum/consensus.go create mode 100644 pkg/health/ethereum/execution_rpc.go create mode 100644 pkg/health/ethereum/network_checker.go create mode 100644 pkg/health/ethereum/util.go create mode 100644 pkg/health/types/types.go create mode 100644 pkg/kubernetes/kubernetes.go create mode 100644 pkg/kubernetes/port_forward.go create mode 100644 pkg/kubernetes/types.go create mode 100644 pkg/kubernetes/utils.go create mode 100644 pkg/kurtosis/kurtosis.go create mode 100644 pkg/plan/config.go create mode 100644 pkg/plan/file.go create mode 100644 pkg/plan/network/clients.go create mode 100644 pkg/plan/network/consensus.go create mode 100644 pkg/plan/network/execution.go create mode 100644 pkg/plan/network/network_builder.go create mode 100644 pkg/plan/network/types.go create mode 100644 pkg/plan/plan.go create mode 100644 pkg/plan/serialization.go create mode 100644 pkg/plan/suite/faults.go create mode 100644 pkg/plan/suite/step_builder.go create mode 100644 pkg/plan/suite/suite_builder.go create mode 100644 pkg/plan/suite/targeting.go create mode 100644 pkg/plan/suite/targeting_test.go create mode 100644 pkg/plan/suite/test_builder.go create mode 100644 pkg/plan/suite/types.go create mode 100644 pkg/plan/types.go create mode 100644 pkg/project/config.go create mode 100644 pkg/project/project.go create mode 100644 pkg/runtime.go create mode 100644 pkg/runtime/runtime.go create mode 100644 pkg/test_executor/executor.go create mode 100644 pkg/test_executor/types.go create mode 100644 pkg/types/config.go create mode 100644 planner-configs/clock-skew-nethermind.yaml create mode 100644 planner-configs/clock-skew-reth.yaml create mode 100644 planner-configs/clock-skew.yaml create mode 100644 planner-configs/io-latency-reth.yaml create mode 100644 planner-configs/network-latency-reth.yaml create mode 100644 planner-configs/network-packet-drop-geth.yaml create mode 100644 planner-configs/restart-resillience-reth.yaml create mode 100644 terraform/chaos-mesh.tf create mode 100644 terraform/digitalocean.tf create mode 100644 terraform/main.tf create mode 100644 terraform/provider.tf create mode 100644 terraform/terraform.tf create mode 100644 test-suites/clock-skew.yaml create mode 100644 test-suites/cpu-stress.yaml create mode 100644 test-suites/io-fault.yaml create mode 100644 test-suites/io-latency.yaml create mode 100644 test-suites/io-mistake.yaml create mode 100644 test-suites/kernel-fault.yaml create mode 100644 test-suites/lighthouse-stale-safe-issue.yaml create mode 100644 test-suites/memory-stress.yaml create mode 100644 test-suites/network-bandwidth.yaml create mode 100644 test-suites/network-latency.yaml create mode 100644 test-suites/network-split.yaml create mode 100644 test-suites/packet-corruption.yaml create mode 100644 test-suites/packet-drop.yaml create mode 100644 test-suites/plan/network-latency-reth.yaml create mode 100644 test-suites/plan/restart-resillience-reth.yaml create mode 100644 test-suites/pod-kill.yaml create mode 100644 test-suites/pod-restart.yaml create mode 100644 test-suites/suite.yaml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..431cc4d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: Go + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + golangci: + name: lint + runs-on: ubuntu-latest + steps: + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: go build + run: go build ./... + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.54.2 + skip-go-installation: true + args: --timeout 3m --verbose \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bad17d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +prysm/consensus/genesis.ssz +geth/execution/geth +.test/ +deposit_manifest.json +/attacknet +.DS_Store +.idea +.vscode +artifacts +webhook +attacknetruns +metrics-server.yaml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..907d9b4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + +linters: + enable: + - errcheck + - govet + - staticcheck + - ineffassign + - gosec + +issues: + exclude-rules: + - path: pkg/kubernetes/port_forward.go + text: Binds to all network interfaces + - path: pkg/plan/ + text: is unused + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ac8f649 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/golangci/golangci-lint + rev: v1.55.2 + hooks: + - id: golangci-lint-full \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..4932555 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +* @bsamuels453 +terraform/ @barnabasbusa +planner-configs/ @parithosh @barnabasbusa +network-configs/ @parithosh @barnabasbusa +test-suites/ @parithosh @barnabasbusa \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bae94e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2728851 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# Attacknet + +Blockchain networks in the wild are subject to a lot of real life variances that have historically been difficult to capture +in local or controlled tests. Chaos testing is a disciplined approach to testing a system by proactively simulating and +identifying failures. Attacknet is a tool that allows you to simulate these real life variances in a controlled environment. +Examples would include adding network latency between nodes, killing nodes at random, or filesystem latency. + +The overall architecture of Attacknet relies on Kubernetes to run the workloads, [Kurtosis](https://github.com/kurtosis-tech/kurtosis) to orchestrate a blockchain network and +[Chaos Mesh](https://chaos-mesh.org/) to inject faults into nodes. Attacknet can then be configured to run healthchecks and +reports back the state of the network at the end of a test. + +![docs/attacknet.svg](docs/attacknet.svg) + +### Capabilities + +The faults supported by Attacknet include: +- Time based: Clock skew +- Network based: Split networks, Packet loss, corruption, latency, bandwidth throttling +- Container based: Restarting containers, killing containers +- Filesystem based: I/O latency, I/O errors +- Stress based: CPU stress, Memory stress +- (WIP) Kernel based: Kernel faults + +Attacknet can be used in the following ways: +- Manually creating specific faults that target nodes matching a criteria +- Genesis devnets of specific topologies using [Kurtosis](https://www.kurtosis.com/), then run faults against them. +- Use the planner to define a matrix of faults and targets, automatically generating the network topology and fault configuration. +- (WIP) Exploratory testing. Dynamically generate various faults/targeting criterion and run faults continuously. + +See [DOCUMENTATION.md](docs/DOCUMENTATION.md) for specific usage examples. + +## Getting started +### Installation/Building + +1. Install Go 1.21 or newer +2. In the project root, run `go build ./cmd/attacknet` +3. Copy the "attacknet" binary path to your PATH variable or directly invoke it + +### Setting up the other bits + +1. Set up a containerd k8s cluster. (1.27 or older), ideally without auto-scaling (as high provisioning time leads to timeouts on kurtosis) +2. Authenticate to the cluster for kubectl +3. Install chaos-mesh + 1. `kubectl create ns chaos-mesh` + 2. `helm repo add chaos-mesh https://charts.chaos-mesh.org` + 3. `helm install chaos-mesh chaos-mesh/chaos-mesh -n=chaos-mesh --version 2.6.1 --set chaosDaemon.runtime=containerd --set chaosDaemon.socketPath=/run/containerd/containerd.sock --set dashboard.securityMode=false --set bpfki.create=true` + 4. To access chaos dashboard, use `kubectl --namespace chaos-mesh port-forward svc/chaos-dashboard 2333` +4. Install [kurtosis locally](https://docs.kurtosis.com/install) +5. Run `kurtosis cluster set cloud`, more information [here](https://docs.kurtosis.com/k8s) +6. If running in digitalocean, edit the kurtosis-config.yml file from `kurtosis config path` and add the following setting under kubernetes-cluster-name: `storage-class: "do-block-storage"` +7. In a separate terminal, run `kurtosis engine start` +8. In a separate terminal, run `kurtosis gateway`. This process needs to stay alive during all attacknet testing and cannot be started via SDK. + +## Usage/Configuration + +See [DOCUMENTATION.md](docs/DOCUMENTATION.md) + +## Contributing +This tool was developed as a collaboration between [Trail of Bits](https://www.trailofbits.com/) and the [Ethereum Foundation](https://github.com/ethereum/). +Thank you for considering helping out with the source code! We welcome contributions from anyone on the internet, and are grateful for even the smallest of fixes! + +If you use this tool for finding bugs, please do ensure that the bug is reported to the relevant project maintainers or to the +[Ethereum foundation Bug bounty program](https://ethereum.org/en/bug-bounty/). Please feel free to reach out to the tool +maintainers on Discord, Email or Twitter for any feature requests. + +If you want to contribute to Attacknet, we recommend running pre-commit before making changes: + +1. Install pre-commit +2. Run `pre-commit install` + +When making pull requests, **please target the `develop` branch, not main.** + +## Changelog + +**March 18, 2024 version v0.4** + +First public release! + +**New** +- Added two new configuration options in the test planner: + - target_node_multiplier, which duplicates the number of nodes on the network containing the client under test + - targets_as_percent_of_network, which adds more non-test nodes to the network to improve client diversity testing +- Added new fault options to the test planner: + - Network latency faults + - Network packet loss faults +- Beacon chain clients are now included in health checking. + +**Fixed** +- Fixed an issue where the test planner's resultant network topology was non-deterministic +- Fixed an issue where a dropped port-forwarding connection to a pod may result in a panic +- Fixed an issue where Chaos Mesh would fail to find targets in networks with more than 10 nodes +- Updated for Kurtosis SDK v0.88.5 + +**Jan 30, 2024 version v0.3 (internal)** +- Fixed the demo example suite +- Fixed issues with the test planner and pod-restart faults. +- Added bootnode configuration for the test planner. +- Attack sizes in the test planner now refer to size in the context of the entire network. + - A supermajority-sized attack will try to target 66%+ nodes in the entire network, not just 66% of the nodes that match the test target criteria. +- Peer scoring is now disabled for all planner-generated network configurations. +- Bootnodes are no longer targetable by planner-generated test suites. + +**Jan 11, 2024 version v0.2 (internal)** +- Updated to kurtosis v0.86.1 +- Updated to Go 1.21 +- Grafana port-forwarding has been temporarily disabled +- Introduces multi-step tests. This allows multiple faults and other actions to be composed into a single test. +- Introduces the suite planner. The suite planner allows the user to define a set of testing criteria/dimensions, which the planner turns into a suite containing multiple tests. +- Successful & failed test suites now emit test artifacts summarizing the results of the test. + +**Dec 15, 2023 version v0.1 (internal)** +- Initial internal release diff --git a/cmd/attacknet/main.go b/cmd/attacknet/main.go new file mode 100644 index 0000000..7aa83c0 --- /dev/null +++ b/cmd/attacknet/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "attacknet/cmd/pkg" + "attacknet/cmd/pkg/plan" + "attacknet/cmd/pkg/project" + "context" + "github.com/alecthomas/kong" + log "github.com/sirupsen/logrus" + "os" +) + +var CLI struct { + Init struct { + Force bool `arg:"force" optional:"" default:"false" name:"force" help:"Overwrite existing project"` + Path string `arg:"" optional:"" type:"existingdir" name:"path" help:"Path to initialize project on. Defaults to current working directory."` + } `cmd:"" help:"Initialize an attacknet project"` + Start struct { + Suite string `arg:"" name:"suite name" help:"The test suite to run. These are located in ./test-suites"` + } `cmd:"" help:"Run a specified test suite"` + Plan struct { + Name string `arg:"" optional:"" name:"name" help:"The name of the test suite to be generated"` + Path string `arg:"" optional:"" type:"existingfile" name:"path" help:"Location of the planner configuration."` + } `cmd:"" help:"Construct an attacknet suite for a client"` + // Explore struct{} `cmd:"" help:"Run in exploration mode"` +} + +func main() { + // todo: use flag for arg parse + + c := kong.Parse(&CLI) + + b := c.Command() + switch b { + case "init": + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + err = project.InitializeProject(dir, CLI.Init.Force) + if err != nil { + log.Fatal(err) + } + case "init ": + err := project.InitializeProject(CLI.Init.Path, CLI.Init.Force) + if err != nil { + log.Fatal(err) + } + case "start ": + ctx, cancelCtxFunc := context.WithCancel(context.Background()) + defer cancelCtxFunc() + cfg, err := project.LoadSuiteConfigFromName(CLI.Start.Suite) + if err != nil { + log.Fatal(err) + } + err = pkg.StartTestSuite(ctx, cfg) + if err != nil { + log.Fatal(err) + os.Exit(1) + } + case "plan ": + config, err := plan.LoadPlannerConfigFromPath(CLI.Plan.Path) + if err != nil { + log.Fatal(err) + os.Exit(1) + } + err = plan.BuildPlan(CLI.Plan.Name, config) + if err != nil { + log.Fatal(err) + os.Exit(1) + } + /* + case "explore": + topo, err := plan.LoadPlannerConfigFromPath("planner-configs/network-latency-reth.yaml") + if err != nil { + log.Fatal(err) + } + suiteCfg, err := project.LoadSuiteConfigFromName("plan/network-latency-reth") + if err != nil { + log.Fatal(err) + } + + f, err := os.ReadFile("webhook") + if err != nil { + log.Fatal(err) + } + w := string(f) + client, err := webhook.NewWithURL(w) + if err != nil { + log.Fatal(err) + } + + err = exploration.StartExploration(topo, suiteCfg) + if err != nil { + message, err := client.CreateContent(fmt.Sprintf("attacknet run completed with error %s", err.Error())) + if err != nil { + log.Fatal(err) + } + _ = message + log.Fatal(err) + } + + message, err := client.CreateContent("attacknet run completed with error ") + if err != nil { + log.Fatal(err) + } + _ = message + os.Exit(1) + */ + default: + log.Fatal("unrecognized arguments") + } +} diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md new file mode 100644 index 0000000..7abe9ba --- /dev/null +++ b/docs/DOCUMENTATION.md @@ -0,0 +1,283 @@ +# Documentation + +## Overview + +Fundamentally, Attacknet is an orchestration tool for performing Chaos Testing. It can be used for multiple different testing workflows ranging from simply injecting faults into a Kubernetes pod to orchestrating a barrage of tests against a dynamically defined network topology. + +To understand its capabilities and how to use it, we've documented a set of "testing workflows", ranging from simple to more complicated, along with accompanying docs on how to use the workflow and what kind of testing it should be used for. + +### Workflow #1, Inject fault into an existing pod, then quit + +For this workflow, we want Attacknet to inject a fault into a pre-existing pod in a pre-existing namespace, then quit. +We need to specify a [Test Suite configuration](#test-suites) file that can provide Attacknet with the information needed to run the workflow. + +Note that if there isn't a Kurtosis enclave running in the `pre-existing-namespace` namespace below, Attacknet will try to genesis a network in the namespace. + +If the config file below is located at `test-suites/suite.yaml`, you would run it using Attacknet by invoking `attacknet start suite` +```yaml +attacknetConfig: + grafanaPodName: grafana + grafanaPodPort: "3000" + allowPostFaultInspection: true + waitBeforeInjectionSeconds: 0 + reuseDevnetBetweenRuns: true + existingDevnetNamespace: pre-existing-namespace + +harnessConfig: # even though we're not creating a network, these still need to be defined. just a quirk that will be fixed eventually + networkType: ethereum + networkPackage: github.com/kurtosis-tech/ethereum-package + networkConfig: plan/default.yaml +testConfig: + tests: + - testName: Restart geth/lighthouse node + planSteps: + - stepType: injectFault + description: 'Restart geth/lighthouse node' + chaosFaultSpec: + apiVersion: chaos-mesh.org/v1alpha1 + kind: PodChaos + spec: + action: pod-failure + duration: 5s + mode: all + selector: + expressionSelectors: + - key: kurtosistech.com/id + operator: In + values: + - el-1-geth-lighthouse + health: + enableChecks: false +``` + +### Workflow #2, Genesis a network, inject fault into a node, ensure the node recovers, then quit + +This workflow requires two config files,a [test suite configuration](#test-suites) and a [network configuration](#network-configs). The test suite config can be found below, and the network config that it points to in [../network-configs/default.yaml](../network-configs/default.yaml) + +We've several changes to the test suite from workflow 1, each is annotated with a comment. +```yaml +attacknetConfig: + grafanaPodName: grafana + grafanaPodPort: "3000" + allowPostFaultInspection: true + waitBeforeInjectionSeconds: 600 # we want to wait until all the nodes are synced and emitting attestations + reuseDevnetBetweenRuns: true + existingDevnetNamespace: pre-existing-namespace + +harnessConfig: + networkType: ethereum + networkPackage: github.com/kurtosis-tech/ethereum-package + networkConfig: plan/default.yaml +testConfig: + tests: + - testName: Restart geth/lighthouse node + planSteps: + - stepType: injectFault + description: 'Restart geth/lighthouse node' + chaosFaultSpec: + apiVersion: chaos-mesh.org/v1alpha1 + kind: PodChaos + spec: + action: pod-failure + duration: 5s + mode: all + selector: + expressionSelectors: + - key: kurtosistech.com/id + operator: In + values: + - el-1-geth-lighthouse + health: + enableChecks: true # we want attacknet to run health checks against EL/CL clients + gracePeriod: 5m0s # How long Attacknet should wait for the network to stabilize before considering the test a failure. +``` + +Since health checks are enabled now, Attacknet will emit a health check artifact once the test concludes (successful or not). These health artifacts can be found in the `./artifacts` directory. + +Note: when Attacknet is run using `start suite`, it's going to check whether a network is already running in the `existingDevnetNamespace` namespace. If no network is running, it will genesis a network using the specified network config. + +### Workflow #3, use the planner to build a test suite for exhaustively testing a single client, then run the test suite + +This workflow is useful for exhaustively testing a specific EL or CL client against a specific fault with various intensities/client combinations. This workflow consumes a [planner config file](#planner-configs) and emits a network config and test suite config that can be run by Attacknet. + +Using the example planner config in the [planner configs docs](#planner-configs), we can use `attacknet plan ` to generate a test suite/network config. The suite plan will be written to `./test-suites/plan/.yaml`, and the network config will be written to `./network-configs/plan/.yaml`. + +The test suite can then be run using `attacknet run plan/`. + +It should be noted that the number of tests generated will be equal to `len(fault_config_dimensions) * len(fault_targeting_dimensions) * len(fault_attack_size_dimensions)`, so budget your testing dimensions accordingly. + +Note that not all faults are supported in the test planner at this time, see the planner config docs for more info. + +## Configuration Files +### Test Suites +Test suites are configuration files that tell Attacknet: +1. Which namespace faults/tests should be injected into in Kubernetes +2. Whether a network should be genesis'ed using Kurtosis, and if so, what that network topology should be. +3. The lifecycle rules for the devnet & whether it should be terminated after the suite concludes. +4. The actual tests to run against the devnet. + +These config files are stored as yaml and are found under `./test-suites`. You can manually create new test suites, or use the planner to generate test suites. + +Test suite configuration is broken into 3 sections: +- Attacknet configuration. +- Harness configuration. This is used to configure the Kurtosis package that will be used to genesis the network. +- Test configuration. This is used to determine which tests should be run against the devnet and how those tests + should be configured. + +Here is an annotated test suite configuration that explains what each bit is for: +```yaml +attacknetConfig: + grafanaPodName: grafana # the name of the pod that grafana will be deployed to. + grafanaPodPort: 3000 # the port grafana is listening to in the pod + waitBeforeInjectionSeconds: 10 + # the number of seconds to wait between the genesis of the network and the injection of faults. To wait for finality, use 25 mins (1500 secs) + reuseDevnetBetweenRuns: true # Whether attacknet should skip enclave deletion after the fault concludes. Defaults to true. + existingDevnetNamespace: kt-ethereum # If you want to reuse a running network, you can specify an existing namespace that contains a Kurtosis enclave and run tests against it. If this field is defined and no Kurtosis enclave is present, the network defined in the harness configuration will be deployed to it. + allowPostFaultInspection: true # When set to true, Attacknet will maintain the port-forward connection to Grafana once the fault has concluded to allow the operator to inspect metrics. Default: true + +harnessConfig: + networkPackage: github.com/kurtosis/ethereum-package # The Kurtosis package to deploy to instrument the devnet. + networkConfig: default.yaml # The configuration to use for the Kurtosis package. These live in ./network-configs and are referenced by their filename. + networkType: ethereum # no touchy + +# The list of tests to be run before termination +testConfig: + tests: + - testName: packetdrop-1 # Name of the test. Used for logging/artifacts. + health: + enableChecks: true # whether health checks should be run after the test concludes + gracePeriod: 2m0s # how long the health checks will attempt to pass before marking the test a failure + planSteps: # the list of steps to facilitate the test, executed in order + - stepType: injectFault # this step injects a fault, the continues to the next step without waiting for the fault to terminate + description: "inject fault" + chaosFaultSpec: # The chaosFaultSpec is basically a pass-thru object for Chaos Mesh fault resources. This means we can support every possible fault out-of-the-box. To determine the schema for each fault type, check the Chaos Mesh docs: https://chaos-mesh.org/docs/simulate-network-chaos-on-kubernetes/. One issue with this method is that Attacknet can't verify whether your faultSpec is valid until it tries to create the resource in Kubernetes, and that comes after genesis which takes a long time on its own. If you run into schema validation issues, try creating these objects directly in Kubernetes to hasten the debug cycle. + kind: NetworkChaos + apiVersion: chaos-mesh.org/v1alpha1 + spec: + selector: + labelSelectors: + kurtosistech.com/id: cl-1-lighthouse-geth-validator + mode: all + action: loss + duration: 1m + loss: + loss: '10' + correlation: '0' + direction: to + - stepType: waitForFaultCompletion # this step waits for all previous running faults to complete before continuing + description: wait for faults to terminate +``` + +#### Plan Steps + +In the above example, we use two planSteps, `injectFault` and `waitForFaultCompletion`. + +The `injectFault` planStep provides a pass-through to Chaos Mesh, where the manifest under `chaosFaultSpec` is directly written to Kubernetes as a manifest. When Attacknet runs an `injectFault` planStep, it waits until Chaos Mesh has confirmed the fault to be injected into the target pod, then proceeds to the next step. Information on how to configure different kinds of faults can be found in the [Chaos Mesh documentation](https://chaos-mesh.org/docs/simulate-pod-chaos-on-kubernetes/). Some examples can be found in the `test-suites/` directory as well. + +The `waitForFaultCompletion` planStep does exactly what it says. Attacknet determines when currently running faults are expected to terminate by checking their manifest's `duration` field, then holds up the test suite execution for the longest expected `duration`. Once the `duration` has elapsed, it checks all outstanding fault manifests and verifies Chaos Mesh was able to turn off the fault properly. + +The `waitForDuration` planStep isn't in the above suite, but it exists. See [pkg/test_executor/types.go](../pkg/test_executor/types.go) for how to configure it. + +### Network Configs +These files define the network topology and configuration of a network to be deployed by Kurtosis. You can create them manually or using the planner tool. + +They are stored under the `network-configs` directory, and are directly passed through to the Kurtosis package when deploying a devnet. +When referencing a network config in a test suite, you don't have to include `network-configs` in the path. + +Since these files are entirely passthrough to the Kurtosis package, see the [Ethereum Kurtosis package](https://github.com/kurtosis-tech/ethereum-package) for further documentation. + +### Planner Configs +These files are used by the test planner feature to generate network configs and test suites. They are found in the `planner-configs/` directory. + +Here's an annotated test plan: + +```yaml +execution: # list of execution clients that will be used in the network topology + - name: geth + image: ethereum/client-go:latest + - name: reth + image: ghcr.io/paradigmxyz/reth:latest +consensus: # list of consensus clients that will be used in the network topology + - name: lighthouse + image: sigp/lighthouse:latest + has_sidecar: true + - name: prysm + image: prysmaticlabs/prysm-beacon-chain:latest + has_sidecar: true +network_params: + num_validator_keys_per_node: 32 # required. +kurtosis_package: "github.com/kurtosis-tech/ethereum-package" +kubernetes_namespace: kt-ethereum +topology: + bootnode_el: geth # self explanatory + bootnode_cl: prysm + targets_as_percent_of_network: 0.25 # [optional] defines what percentage of the network contains the target client. 0.25 means only 25% of nodes will contain the client defined in the fault spec. Warning: low percentages may lead to massive networks. + target_node_multiplier: 2 # optional, default:1. Adds duplicate el/cl combinations based on the multiplier. Useful for testing weird edge cases in consensus +fault_config: + fault_type: ClockSkew # which fault to use. A list of faults currently supported by the planner can be found in pkg/plan/suite/types.go in FaultTypeEnum + target_client: reth # which client to test. this can be an exec client or a consensus client. must show up in the client definitions above. + wait_before_first_test: 300s # how long to wait before running the first test. Set this to 25 minutes to test against a finalized network. + fault_config_dimensions: # the different fault configurations to use when creating tests. At least one config dimension is required. + - skew: -2m # these configs differ for each fault + duration: 1m + grace_period: 1800s # how long to wait for health checks to pass before marking the test as failed + - skew: 2m + duration: 1m + grace_period: 1800s + fault_targeting_dimensions: # Defines how we want to impact the targets. We can inject faults into the client and only the client, or we can inject faults into the node (injects into cl, el, validator) + - MatchingNode + - MatchingClient + fault_attack_size_dimensions: # Defines how many of the matching targets we actually want to attack. + - AttackOneMatching # attacks only one matching target + - AttackMinorityMatching # attacks <33% + - AttackSuperminorityMatching # attacks >33% but <50% + - AttackMajorityMatching # attacks >50% but <66% + - AttackSupermajorityMatching # attacks >66% + - AttackAllMatching # attacks all +``` + +#### Faults supported by planner + +##### ClockSkew +Config: +```yaml + - skew: -2m # how far to skew the clock. can be positive or negative + duration: 1m # how long to skew the clock for + grace_period: 1800s # how long to wait for health checks to pass before marking the test as failed +``` + +##### RestartContainers +Config: +```yaml + - grace_period: 1800s # how long to wait for health checks to pass before marking the test as failed +``` + +##### IOLatency +Config: +```yaml + - grace_period: 1800s # how long to wait for health checks to pass before marking the test as failed + delay: 1000ms # how long the i/o delay should be + duration: 1m # how long the fault should last + percent: 50 # the percentage of i/o requests impacted. +``` + +##### Network Latency +Config: +```yaml + - grace_period: 1800s # how long to wait for health checks to pass before marking the test as failed + delay: 500ms # how long the latency delay should be, on average + jitter: 50ms # the amount of jitter + duration: 5m # how long the fault should last + correlation: 50 # 0 - 100 +``` + +##### Packet Loss +Config: +```yaml + - grace_period: 1800s # how long to wait for health checks to pass before marking the test as failed + loss_percent: 75% # the pct of packets to drop + direction: to # may be to, from, or both + duration: 5m # how long the fault should last +``` + + diff --git a/docs/attacknet.svg b/docs/attacknet.svg new file mode 100644 index 0000000..5b278c8 --- /dev/null +++ b/docs/attacknet.svg @@ -0,0 +1,4 @@ + + + +AttacknetAttacknetKurtosisKurtosisKubernetesKubernetesChaos Mesh ControllerChaos Mesh ControllerNetwork Under TestNetwork Under TestDeployDeployNode 1Node 1EL ClientEL ClientCL ClientCL ClientNode 2Node 2EL ClientEL ClientCL ClientCL ClientNode NNode NEL ClientEL ClientCL ClientCL ClientHealth ChecksHealth ChecksFault ManifestsFault ManifestsDesired Network TopologyDesired Network TopologyFault Types & IntensitiesFault Types & IntensitiesTargeting Criteria(e.g. target Reth nodes)Targeting Criteria...Genesis ConfigGenesis ConfigClient ImagesClient Images \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..84d7255 --- /dev/null +++ b/go.mod @@ -0,0 +1,149 @@ +module attacknet/cmd + +go 1.21 + +toolchain go1.21.3 + +require ( + github.com/alecthomas/kong v0.8.1 + github.com/attestantio/go-eth2-client v0.19.10 + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240104130649-f55576898805 + github.com/ethereum/go-ethereum v1.13.8 + github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc + github.com/kurtosis-tech/kurtosis/api/golang v0.88.5 + github.com/kurtosis-tech/stacktrace v0.0.0-20211028211901-1c67a77b5409 + github.com/rs/zerolog v1.29.1 + github.com/sirupsen/logrus v1.9.3 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 + sigs.k8s.io/controller-runtime v0.16.3 +) + +require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/adrg/xdg v0.4.0 // indirect + github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/emicklei/go-restful/v3 v3.11.1 // indirect + github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/ferranbt/fastssz v0.1.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.7 // indirect + github.com/go-yaml/yaml v2.1.0+incompatible // indirect + github.com/goccy/go-yaml v1.9.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/gosimple/slug v1.13.1 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/holiman/uint256 v1.2.4 // indirect + github.com/huandu/go-clone v1.6.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/kurtosis-tech/kurtosis-portal/api/golang v0.0.0-20231031173452-349f1ec9a443 // indirect + github.com/kurtosis-tech/kurtosis/contexts-config-store v0.0.0-20240109102239-4ba41ce90af0 // indirect + github.com/kurtosis-tech/kurtosis/grpc-file-transfer/golang v0.0.0-20240109102239-4ba41ce90af0 // indirect + github.com/kurtosis-tech/kurtosis/path-compression v0.0.0-20240307154559-64d2929cd265 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mholt/archiver v3.1.1+incompatible // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/nwaples/rardecode v1.1.3 // indirect + github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.1 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.1 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect + google.golang.org/grpc v1.60.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 // indirect + k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect + rsc.io/tmplfunc v0.0.3 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c83bcaf --- /dev/null +++ b/go.sum @@ -0,0 +1,510 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= +github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= +github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= +github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= +github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/attestantio/go-eth2-client v0.19.10 h1:NLs9mcBvZpBTZ3du7Ey2NHQoj8d3UePY7pFBXX6C6qs= +github.com/attestantio/go-eth2-client v0.19.10/go.mod h1:TTz7YF6w4z6ahvxKiHuGPn6DbQn7gH6HPuWm/DEQeGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= +github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240104130649-f55576898805 h1:QjGJQDB+rcJkUEPjn4z3Ur6DgJHBbGobwdOX2i4WQ14= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240104130649-f55576898805/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= +github.com/chromedp/cdproto v0.0.0-20210526005521-9e51b9051fd0/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= +github.com/chromedp/cdproto v0.0.0-20210706234513-2bc298e8be7f h1:lg5k1KAxmknil6Z19LaaeiEs5Pje7hPzRfyWSSnWLP0= +github.com/chromedp/cdproto v0.0.0-20210706234513-2bc298e8be7f/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= +github.com/chromedp/chromedp v0.7.3 h1:FvgJICfjvXtDX+miuMUY0NHuY8zQvjS/TcEQEG6Ldzs= +github.com/chromedp/chromedp v0.7.3/go.mod h1:9gC521Yzgrk078Ulv6KIgG7hJ2x9aWrxMBBobTFk30A= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= +github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 h1:aPEJyR4rPBvDmeyi+l/FS/VtA00IWvjeFvjen1m1l1A= +github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593/go.mod h1:6hk1eMY/u5t+Cf18q5lFMUA1Rc+Sm5I6Ra1QuPyxXCo= +github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw= +github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= +github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/emicklei/go-restful/v3 v3.11.1 h1:S+9bSbua1z3FgCnV0KKOSSZ3mDthb5NyEPL5gEpCvyk= +github.com/emicklei/go-restful/v3 v3.11.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= +github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.13.8 h1:1od+thJel3tM52ZUNQwvpYOeRHlbkVFZ5S8fhi0Lgsg= +github.com/ethereum/go-ethereum v1.13.8/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo= +github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= +github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0-rc.5 h1:QOAag7FoBaBYYHRqzqkhhd8fq5RTubvI4v3Ft/gDVVQ= +github.com/gobwas/ws v1.1.0-rc.5/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w= +github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gosimple/slug v1.1.1/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0= +github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= +github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc h1:PXZQA2WCxe85Tnn+WEvr8fDpfwibmEPgfgFEaC87G24= +github.com/grafana-tools/sdk v0.0.0-20220919052116-6562121319fc/go.mod h1:AHHlOEv1+GGQ3ktHMlhuTUwo3zljV3QJbC0+8o2kn+4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= +github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.6.0 h1:HMo5uvg4wgfiy5FoGOqlFLQED/VGRm2D9Pi8g1FXPGc= +github.com/huandu/go-clone v1.6.0/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= +github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kurtosis-tech/kurtosis-portal/api/golang v0.0.0-20231031173452-349f1ec9a443 h1:jqFVT4FcZU+wG+y5FV+Xz9+IwNqSyredbcFKHfK2Kh8= +github.com/kurtosis-tech/kurtosis-portal/api/golang v0.0.0-20231031173452-349f1ec9a443/go.mod h1:bWSMQK3WHVTGHX9CjxPAb/LtzcmfOxID2wdzakSWQxo= +github.com/kurtosis-tech/kurtosis/api/golang v0.88.5 h1:Bp0YLBb6RyvDx1B5XCR73HHLDj+Ez7h4boRV0wC8IkU= +github.com/kurtosis-tech/kurtosis/api/golang v0.88.5/go.mod h1:9T22P7Vv3j5g6sbm78DxHQ4s9C4Cj3s9JjFQ7DFyYpM= +github.com/kurtosis-tech/kurtosis/contexts-config-store v0.0.0-20240109102239-4ba41ce90af0 h1:/anRCavLbkzCdRMRovtZAx6Dg6di5CgqLciWiQUdNLY= +github.com/kurtosis-tech/kurtosis/contexts-config-store v0.0.0-20240109102239-4ba41ce90af0/go.mod h1:bm+jMBhirwvfuXG99TDZFuvmSwDfeziVJPobRvHgafc= +github.com/kurtosis-tech/kurtosis/grpc-file-transfer/golang v0.0.0-20240109102239-4ba41ce90af0 h1:Er0K/PnwBWI+PgKf5KWRjaHccveJwIW4wrM1ebvWMg8= +github.com/kurtosis-tech/kurtosis/grpc-file-transfer/golang v0.0.0-20240109102239-4ba41ce90af0/go.mod h1:XWnZw30gs8O8ySajNTQubyOOmwkKxxwCzDKuJ7YzZYk= +github.com/kurtosis-tech/kurtosis/path-compression v0.0.0-20240307154559-64d2929cd265 h1:uSDftcGStwuAjHv8fV2TleNCKSWPvUKe7EaplFG3yBI= +github.com/kurtosis-tech/kurtosis/path-compression v0.0.0-20240307154559-64d2929cd265/go.mod h1:aDMrPeS7Gii8W6SDKSKyrBNgEQAUYidriyeKGf+Ml3I= +github.com/kurtosis-tech/stacktrace v0.0.0-20211028211901-1c67a77b5409 h1:YQTATifMUwZEtZYb0LVA7DK2pj8s71iY8rzweuUQ5+g= +github.com/kurtosis-tech/stacktrace v0.0.0-20211028211901-1c67a77b5409/go.mod h1:y5weVs5d9wXXHcDA1awRxkIhhHC1xxYJN8a7aXnE6S8= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= +github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= +github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 h1:0tVE4tdWQK9ZpYygoV7+vS6QkDvQVySboMVEIxBJmXw= +github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7/go.mod h1:wmuf/mdK4VMD+jA9ThwcUKjg3a2XWM9cVfFYjDyY4j4= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/umbracle/gohashtree v0.0.2-alpha.0.20230207094856-5b775a815c10 h1:CQh33pStIp/E30b7TxDlXfM0145bn2e8boI30IxAhTg= +github.com/umbracle/gohashtree v0.0.2-alpha.0.20230207094856-5b775a815c10/go.mod h1:x/Pa0FF5Te9kdrlZKJK82YmAkvL8+f989USgz6Jiw7M= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= +google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo= +google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= +k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 h1:1Rp/XEKP5uxPs6QrsngEHAxBjaAR78iJRiJq5Fi7LSU= +k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/network-configs/default.yaml b/network-configs/default.yaml new file mode 100644 index 0000000..8c85145 --- /dev/null +++ b/network-configs/default.yaml @@ -0,0 +1,43 @@ +participants: + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + +network_params: + deneb_fork_epoch: 1 + num_validator_keys_per_node: 32 +additional_services: + - prometheus_grafana + - dora +persistent: false diff --git a/network-configs/devnet-12.yaml b/network-configs/devnet-12.yaml new file mode 100644 index 0000000..18eacb7 --- /dev/null +++ b/network-configs/devnet-12.yaml @@ -0,0 +1,71 @@ +participants: + - el_type: geth + el_image: ethereum/client-go:v1.13.13 + el_extra_labels: {"ethereum-package.partition": "partA"} + cl_type: lighthouse + cl_image: sigp/lighthouse:v5.0.0 + beacon_extra_labels: {"ethereum-package.partition": "partA"} + validator_extra_labels: {"ethereum-package.partition": "partA"} + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: nethermind + el_image: nethermind/nethermind:1.25.4 + el_extra_labels: {"ethereum-package.partition": "partA"} + cl_type: teku + cl_image: consensys/teku:24.2.0-amd64 + beacon_extra_labels: {"ethereum-package.partition": "partA"} + validator_extra_labels: {"ethereum-package.partition": "partA"} + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.19 + el_extra_labels: {"ethereum-package.partition": "partB"} + cl_type: prysm + cl_image: gcr.io/prysmaticlabs/prysm/beacon-chain:v5.0.0,gcr.io/prysmaticlabs/prysm/validator:v5.0.0 + beacon_extra_labels: {"ethereum-package.partition": "partB"} + validator_extra_labels: {"ethereum-package.partition": "partB"} + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 +network_params: + deneb_fork_epoch: 1 + genesis_delay: 120 +additional_services: + - dora + - goomy_blob + - tx_spammer + - blob_spammer +persistent: true +global_client_log_level: info \ No newline at end of file diff --git a/network-configs/plan/network-latency-reth.yaml b/network-configs/plan/network-latency-reth.yaml new file mode 100644 index 0000000..f7b891e --- /dev/null +++ b/network-configs/plan/network-latency-reth.yaml @@ -0,0 +1,197 @@ +participants: + - el_type: geth + el_image: ethereum/client-go:v1.13.11 + cl_type: prysm + cl_image: gcr.io/prysmaticlabs/prysm/beacon-chain:v4.2.1 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: reth + el_image: parithoshj/reth:main-1a8440a-debug + cl_type: lighthouse + cl_image: sigp/lighthouse:v4.6.0 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: reth + el_image: parithoshj/reth:main-1a8440a-debug + cl_type: prysm + cl_image: gcr.io/prysmaticlabs/prysm/beacon-chain:v4.2.1 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: reth + el_image: parithoshj/reth:main-1a8440a-debug + cl_type: teku + cl_image: consensys/teku:24.1.1-amd64 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + count: 1 + - el_type: reth + el_image: parithoshj/reth:main-1a8440a-debug + cl_type: lodestar + cl_image: chainsafe/lodestar:v1.15.0 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: reth + el_image: parithoshj/reth:main-1a8440a-debug + cl_type: nimbus + cl_image: ethpandaops/nimbus:unstable + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + count: 1 + - el_type: geth + el_image: ethereum/client-go:v1.13.11 + cl_type: lighthouse + cl_image: sigp/lighthouse:v4.6.0 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: geth + el_image: ethereum/client-go:v1.13.11 + cl_type: prysm + cl_image: gcr.io/prysmaticlabs/prysm/beacon-chain:v4.2.1 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: geth + el_image: ethereum/client-go:v1.13.11 + cl_type: teku + cl_image: consensys/teku:24.1.1-amd64 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + count: 1 + - el_type: geth + el_image: ethereum/client-go:v1.13.11 + cl_type: lodestar + cl_image: chainsafe/lodestar:v1.15.0 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 + - el_type: geth + el_image: ethereum/client-go:v1.13.11 + cl_type: nimbus + cl_image: ethpandaops/nimbus:unstable + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + count: 1 + - el_type: erigon + el_image: thorax/erigon:v2.57.3 + cl_type: lighthouse + cl_image: sigp/lighthouse:v4.6.0 + el_min_cpu: 768 + el_max_cpu: 768 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 1536 + cl_max_mem: 1536 + vc_min_cpu: 500 + vc_max_cpu: 500 + vc_min_mem: 512 + vc_max_mem: 512 + count: 1 +network_params: + num_validator_keys_per_node: 32 +additional_services: + - prometheus_grafana + - dora +parallel_keystore_generation: false +persistent: false +disable_peer_scoring: true diff --git a/network-configs/plan/restart-resillience-reth.yaml b/network-configs/plan/restart-resillience-reth.yaml new file mode 100644 index 0000000..860fb7f --- /dev/null +++ b/network-configs/plan/restart-resillience-reth.yaml @@ -0,0 +1,213 @@ +participants: + - el_type: nethermind + el_image: nethermind/nethermind:1.25.1 + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.13 + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.13 + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.13 + cl_type: lodestar + cl_image: chainsafe/lodestar:v1.12.1 + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: erigon + el_image: thorax/erigon:v2.53.4 + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: nethermind + el_image: nethermind/nethermind:1.25.1 + cl_type: lodestar + cl_image: chainsafe/lodestar:v1.12.1 + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: erigon + el_image: thorax/erigon:v2.53.4 + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: nethermind + el_image: nethermind/nethermind:1.25.1 + cl_type: lodestar + cl_image: chainsafe/lodestar:v1.12.1 + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 + - el_type: erigon + el_image: thorax/erigon:v2.53.4 + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 1000 + cl_max_cpu: 1000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1024 + vc_max_mem: 1024 + count: 1 +network_params: + num_validator_keys_per_node: 32 +additional_services: + - prometheus_grafana + - dora +parallel_keystore_generation: false +persistent: false +disable_peer_scoring: true diff --git a/network-configs/reth.yaml b/network-configs/reth.yaml new file mode 100644 index 0000000..f45915c --- /dev/null +++ b/network-configs/reth.yaml @@ -0,0 +1,129 @@ +participants: + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: geth + el_image: ethereum/client-go:latest + cl_type: teku + cl_image: consensys/teku:24.2.0 + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.19 + cl_type: prysm + cl_image: prysmaticlabs/prysm-beacon-chain:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.19 + cl_type: lighthouse + cl_image: sigp/lighthouse:latest + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.19 + cl_type: teku + cl_image: consensys/teku:24.2.0 + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + - el_type: reth + el_image: ghcr.io/paradigmxyz/reth:v0.1.0-alpha.19 + cl_type: lodestar + cl_image: chainsafe/lodestar:v1.16.0 + el_min_cpu: 1000 + el_max_cpu: 1000 + el_min_mem: 1024 + el_max_mem: 1024 + cl_min_cpu: 2000 + cl_max_cpu: 3000 + cl_min_mem: 2048 + cl_max_mem: 2048 + vc_min_cpu: 1000 + vc_max_cpu: 1000 + vc_min_mem: 1028 + vc_max_mem: 1028 + count: 1 + +network_params: + num_validator_keys_per_node: 32 +additional_services: + - el_forkmon + - beacon_metrics_gazer + - dora + - prometheus_grafana +parallel_keystore_generation: false diff --git a/pkg/chaos-mesh/client.go b/pkg/chaos-mesh/client.go new file mode 100644 index 0000000..ec0a0ae --- /dev/null +++ b/pkg/chaos-mesh/client.go @@ -0,0 +1,102 @@ +package chaos_mesh + +import ( + "attacknet/cmd/pkg/kubernetes" + "context" + "encoding/json" + "errors" + "fmt" + api "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/kurtosis-tech/stacktrace" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "reflect" + pkgclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "time" +) + +type ChaosClient struct { + kubeApiClient pkgclient.Client + chaosNamespace string +} + +func CreateClient(namespace string, kubeClient *kubernetes.KubeClient) (*ChaosClient, error) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + chaosScheme := runtime.NewScheme() + err := api.AddToScheme(chaosScheme) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to add chaos-mesh v1alpha1 to scheme") + } + + err = corev1.AddToScheme(chaosScheme) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to add kubernetes core to scheme") + } + + client, err := kubeClient.CreateDerivedClientWithSchema(chaosScheme) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to create a kubernetes API client") + } + + // todo: validate chaos-mesh is installed + + return &ChaosClient{client, namespace}, nil +} + +func (c *ChaosClient) StartFault(ctx context.Context, faultSpec map[string]interface{}) (*FaultSession, error) { + kindObj, exists := faultSpec["kind"] + if !exists { + return nil, stacktrace.NewError("unable to find 'kind' within fault spec") + } + + kind, ok := kindObj.(string) + if !ok { + return nil, stacktrace.NewError("unable to cast faultSpec.Kind to string") + } + + if chaosKind, ok := api.AllKinds()[kind]; ok { + chaos := chaosKind.SpawnObject() + + faultName := fmt.Sprintf("fault-%d", time.Now().UnixMicro()) + faultMeta := metav1.ObjectMeta{Name: faultName, Namespace: c.chaosNamespace} + + reflect.ValueOf(chaos).Elem().FieldByName("ObjectMeta").Set(reflect.ValueOf(faultMeta)) + marshalled, err := json.Marshal(faultSpec) + if err != nil { + return nil, stacktrace.Propagate(err, "could not marshal faultspec") + } + + err = json.Unmarshal(marshalled, &chaos) + if err != nil { + return nil, stacktrace.Propagate(err, "could not unmarshal faultspec") + } + + err = c.kubeApiClient.Create(ctx, chaos) + if err != nil { + return nil, stacktrace.Propagate(err, "could not create custom resource") + } + + return NewFaultSession(ctx, c, chaosKind, faultSpec, faultName) + } else { + return nil, stacktrace.Propagate(errors.New("invalid fault kind"), "invalid fault kind: %s", kind) + } +} + +func (c *ChaosClient) GetPodLabels(ctx context.Context, podName string) (map[string]string, error) { + key := pkgclient.ObjectKey{ + Namespace: c.chaosNamespace, + Name: podName, + } + pod := &corev1.Pod{} + + err := c.kubeApiClient.Get(ctx, key, pod) + if err != nil { + return nil, err + } + labels := pod.GetLabels() + + return labels, nil +} diff --git a/pkg/chaos-mesh/pods.go b/pkg/chaos-mesh/pods.go new file mode 100644 index 0000000..3f5523e --- /dev/null +++ b/pkg/chaos-mesh/pods.go @@ -0,0 +1,45 @@ +package chaos_mesh + +import "attacknet/cmd/pkg/kubernetes" + +type PodUnderTest struct { + Name string + Labels map[string]string + ExpectDeath bool + TouchedByFault bool +} + +func (p *PodUnderTest) GetName() string { + return p.Name +} + +func (p *PodUnderTest) GetLabels() map[string]string { + return p.Labels +} + +func (p *PodUnderTest) MatchesLabel(key, value string) bool { + v, exists := p.Labels[key] + if !exists { + return false + } else { + return v == value + } +} + +type PodUnderTestList []*PodUnderTest + +func (pods PodUnderTestList) ToKubePods() []kubernetes.KubePod { + kubePods := make([]kubernetes.KubePod, len(pods)) + for i, p := range pods { + kubePods[i] = p + } + return kubePods +} + +func GetPodsExpectedToBeDead(pods []*PodUnderTest) map[string]bool { + expectation := make(map[string]bool) + for _, pod := range pods { + expectation[pod.Name] = pod.ExpectDeath + } + return expectation +} diff --git a/pkg/chaos-mesh/session.go b/pkg/chaos-mesh/session.go new file mode 100644 index 0000000..9cce46a --- /dev/null +++ b/pkg/chaos-mesh/session.go @@ -0,0 +1,339 @@ +package chaos_mesh + +import ( + "context" + "fmt" + api "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" + "time" +) + +type FaultPhase string + +type FaultStatus string + +const ( + Starting FaultStatus = "Starting" + InProgress FaultStatus = "In Progress" + Stopping FaultStatus = "Stopping" + Completed FaultStatus = "Completed" + Error FaultStatus = "Error" +) + +var FaultHasNoDurationErr = fmt.Errorf("this fault has no expected duration") + +type FaultSession struct { + client *ChaosClient + faultKind *api.ChaosKind + faultType string + faultAction string + faultSpec map[string]interface{} + Name string + podsFailingRecovery map[string]*api.Record + checkedForMissingPods bool + podsExpectedMissing int + TestStartTime time.Time + TestDuration *time.Duration + TestEndTime *time.Time + TargetSelectionCompleted bool + PodsUnderTest []*PodUnderTest +} + +func NewFaultSession(ctx context.Context, client *ChaosClient, faultKind *api.ChaosKind, faultSpec map[string]interface{}, name string) (*FaultSession, error) { + now := time.Now() + + faultKindStr, ok := faultSpec["kind"].(string) + if !ok { + return nil, stacktrace.NewError("failed to decode faultSpec.kind to string: %s", faultSpec["kind"]) + } + + spec, ok := faultSpec["spec"].(map[string]interface{}) + if !ok { + return nil, stacktrace.NewError("failed to decode faultSpec.spec to map[string]interface{}") + } + + faultAction, ok := spec["action"].(string) + if !ok { + faultAction = "default" + } + + partial := &FaultSession{ + client: client, + faultKind: faultKind, + faultType: faultKindStr, + faultSpec: spec, + faultAction: faultAction, + Name: name, + podsFailingRecovery: map[string]*api.Record{}, + TestStartTime: now, + podsExpectedMissing: 0, + checkedForMissingPods: false, + TargetSelectionCompleted: false, + PodsUnderTest: nil, + } + duration, err := partial.getDuration(ctx) + if err != nil { + if err == FaultHasNoDurationErr { + partial.TestDuration = nil + partial.TestEndTime = nil + } else { + return nil, err + } + } else { + partial.TestDuration = duration + endTime := now.Add(*duration) + partial.TestEndTime = &endTime + } + + return partial, nil +} + +func (f *FaultSession) getKubeFaultResource(ctx context.Context) (client.Object, error) { + key := client.ObjectKey{ + Namespace: f.client.chaosNamespace, + Name: f.Name, + } + + resource := f.faultKind.SpawnObject() + err := f.client.kubeApiClient.Get(ctx, key, resource) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to query for resource %s in namespace %s", key.Name, key.Namespace) + } + return resource, nil +} + +// returns True if TargetSelectionCompleted becomes true +func (f *FaultSession) checkTargetSelectionCompleted(resource client.Object) (bool, error) { + if f.TargetSelectionCompleted { + return false, nil + } + conditionsVal := reflect.ValueOf(resource).Elem().FieldByName("Status").FieldByName("ChaosStatus").FieldByName("Conditions") + conditions, ok := conditionsVal.Interface().([]api.ChaosCondition) + if !ok || conditions == nil { + return false, stacktrace.NewError("Unable to decode status.chaosstatus.conditions") + } + for _, condition := range conditions { + if condition.Type != api.ConditionSelected { + continue + } + if condition.Status == v1.ConditionTrue { + f.TargetSelectionCompleted = true + } + + return true, nil + } + return false, nil +} + +func (f *FaultSession) getFaultRecords(ctx context.Context) ([]*api.Record, error) { + resource, err := f.getKubeFaultResource(ctx) + if err != nil { + return nil, err + } + + // note: we may be able to move this somewhere else once we get a better idea of fault lifecycle management. + targetsSelected, err := f.checkTargetSelectionCompleted(resource) + if err != nil { + return nil, err + } + + // Feel free to figure out a better way to do this. These fields are part of every Chaos status struct we support, + // but since they don't implement a common interface containing the status fields, there's no clean or simple way + // to extract the values in Go. One alternate option may be to serialize to json, then deserialize into an object + // that will ignore the fault-specific fields. + // This section also can't handle errors. The code will panic if the resource isn't compliant, which isn't great. + recordsVal := reflect.ValueOf(resource).Elem().FieldByName("Status").FieldByName("ChaosStatus").FieldByName("Experiment").FieldByName("Records") + records, ok := recordsVal.Interface().([]*api.Record) + if !ok { + return nil, stacktrace.NewError("unable to cast chaos experiment status") + } + + if targetsSelected && records != nil { + err = f.populatePodsUnderTest(ctx, records) + if err != nil { + return nil, err + } + } + + return records, nil +} + +// todo: check which pods are actually expected to die instead of the number of pods. +func (f *FaultSession) checkForFailedRecovery(record *api.Record) (bool, []string) { + messages := map[string]string{} + for i := len(record.Events) - 1; i >= 0; i-- { + if record.Events[i].Type == api.TypeFailed { + msg := record.Events[i].Message + if record.Events[i].Operation == api.Recover { + if _, exists := f.podsFailingRecovery[msg]; !exists { + messages[msg] = msg + f.podsFailingRecovery[msg] = record + } + } else { + log.Errorf("Did not expect operation to be apply for record with message %s", msg) + } + } + } + if len(messages) == 0 { + return false, make([]string, 0) + } + + distinctMessages := make([]string, 0, len(messages)) + for key := range messages { + distinctMessages = append(distinctMessages, key) + } + return true, distinctMessages +} + +/* +This must be run after the fault manifest has been applied and the handler webhook has run. +*/ +func (f *FaultSession) populatePodsUnderTest(ctx context.Context, records []*api.Record) error { + if !f.checkedForMissingPods { + f.checkedForMissingPods = true + // we expect missing pods when the fault is pod kill. + + podsInjected, err := filterInjectedPods(records) + if err != nil { + return err + } + log.Infof("Chaos-mesh has identified %d pods matching the targeting criteria", len(podsInjected)) + if f.faultType == "PodChaos" && f.faultAction == "pod-kill" { + f.podsExpectedMissing = len(podsInjected) + log.Infof("We're expecting %d pods to be terminated from the selected fault", f.podsExpectedMissing) + } + + // populate f.PodsUnderTest + var podsUnderTest []*PodUnderTest + if f.podsExpectedMissing > 0 { + podsUnderTest, err = buildPodsUnderTestSlice(ctx, f.client, podsInjected, true) + } else { + podsUnderTest, err = buildPodsUnderTestSlice(ctx, f.client, podsInjected, false) + } + if err != nil { + return err + } + f.PodsUnderTest = podsUnderTest + + } else { + // suspect unreachable + log.Error("this code is supposed to be unreachable") + } + return nil +} + +// todo: we need a better way of monitoring fault injection status. There's a ton of statefulness represented in +// chaos-mesh that we're glancing over. Situations such as a pod crashing during a fault may produce unexpected behavior +// in this code as it currently stands. +func (f *FaultSession) GetStatus(ctx context.Context) (FaultStatus, error) { + records, err := f.getFaultRecords(ctx) + if err != nil { + return Error, err + } + + if records == nil { + return Starting, nil + } + + podsInjectedAndRecovered := 0 + podsInjectedNotRecovered := 0 + podsNotInjected := 0 + + for _, podRecord := range records { + if podRecord.InjectedCount == 0 { + podsNotInjected += 1 + } else if podRecord.InjectedCount == podRecord.RecoveredCount { + podsInjectedAndRecovered += 1 + } else { + podsInjectedNotRecovered += 1 + + failing, messages := f.checkForFailedRecovery(podRecord) + if failing { + log.Warn("One or more pods failed to recover from the fault and may have crashed:") + for _, msg := range messages { + log.Warnf("Error message: %s", msg) + } + } + } + } + + if podsNotInjected > 0 { + return Starting, nil + } + if podsInjectedNotRecovered-f.podsExpectedMissing > 0 && podsInjectedAndRecovered == 0 { + return InProgress, nil + } + if podsInjectedAndRecovered+len(f.podsFailingRecovery)+f.podsExpectedMissing == len(records) { + return Completed, nil + } + if podsInjectedNotRecovered > 0 && podsInjectedAndRecovered > 0 { + return Stopping, nil + } + // should be impossible to get here + msg := fmt.Sprintf("invalid state, podsInjectedNotRecovered: %d, podsInjectedAndRecovered: %d", podsInjectedNotRecovered, podsInjectedAndRecovered) + panic(msg) +} + +func (f *FaultSession) getDuration(ctx context.Context) (*time.Duration, error) { + resource, err := f.getKubeFaultResource(ctx) + if err != nil { + return nil, err + } + + durationVal := reflect.ValueOf(resource).Elem().FieldByName("Spec").FieldByName("Duration") + durationStr, ok := durationVal.Interface().(*string) + if !ok { + return nil, stacktrace.NewError("unable to cast durationVal to string") + } + if durationStr == nil { + return nil, FaultHasNoDurationErr + } + + duration, err := time.ParseDuration(*durationStr) + if err != nil { + return nil, err + } + return &duration, err +} + +func buildPodsUnderTestSlice(ctx context.Context, client *ChaosClient, podNames []string, expectDeath bool) ([]*PodUnderTest, error) { + podsUnderTest := make([]*PodUnderTest, len(podNames)) + + for i, podName := range podNames { + labels, err := client.GetPodLabels(ctx, podName) + if err != nil { + return nil, err + } + if labels == nil { + return nil, stacktrace.NewError("pod %s had no labels", podName) + } + + podsUnderTest[i] = &PodUnderTest{ + Name: podName, + Labels: labels, + ExpectDeath: expectDeath, + } + } + return podsUnderTest, nil +} + +// filterInjectedPods takes a list of chaos mesh records and returns a list of pod names that are currently in +// the injected phase. +func filterInjectedPods(records []*api.Record) ([]string, error) { + var injectedPods []string + for _, record := range records { + if record.Phase == "Injected" { + parts := strings.Split(record.Id, "/") + if len(parts) <= 2 { + return nil, stacktrace.NewError("fault record id was split into less than two parts") + } + injectedPods = append(injectedPods, parts[1]) + } + } + return injectedPods, nil +} diff --git a/pkg/exploration/explore.go b/pkg/exploration/explore.go new file mode 100644 index 0000000..2a46412 --- /dev/null +++ b/pkg/exploration/explore.go @@ -0,0 +1,278 @@ +package exploration + +const WaitBetweenTestsSecs = 60 +const Seed = 666 + +/* +func getRandomAttackSize() suite.AttackSize { + //return suite.AttackOne + sizes := []suite.AttackSize{ + suite.AttackOne, + suite.AttackAll, + suite.AttackMajority, + suite.AttackMinority, + suite.AttackSupermajority, + suite.AttackSuperminority, + } + i := len(sizes) + return sizes[rand.Intn(i)] +} + +func getTargetSpec() suite.TargetingSpec { + targetSpecs := []suite.TargetingSpec{ + suite.TargetMatchingClient, + suite.TargetMatchingNode, + } + _ = targetSpecs + //return suite.TargetMatchingClient + return targetSpecs[rand.Intn(2)] +} + +func buildRandomLatencyTest(testIdx int, targetDescription string, targetSelectors []*suite.ChaosTargetSelector) (*types.SuiteTest, error) { + minDelayMilliSeconds := 10 + maxDelayMilliSeconds := 1000 + minDurationSeconds := 10 + maxDurationSeconds := 300 + minJitterMilliseconds := 10 + maxJitterMilliseconds := 1000 + minCorrelation := 0 + maxCorrelation := 100 + + grace := time.Second * 600 + duration := time.Second * time.Duration(rand.Intn(maxDurationSeconds-minDurationSeconds)+minDurationSeconds) + delay := time.Millisecond * time.Duration(rand.Intn(maxDelayMilliSeconds-minDelayMilliSeconds)+minDelayMilliSeconds) + jitter := time.Millisecond * time.Duration(rand.Intn(maxJitterMilliseconds-minJitterMilliseconds)+minJitterMilliseconds) + correlation := rand.Intn(maxCorrelation-minCorrelation) + minCorrelation + loc := time.FixedZone("GMT", 0) + timefmt := time.Now().In(loc).Format(http.TimeFormat) + description := fmt.Sprintf("Apply %s network latency for %s. Jitter: %s, correlation: %d against %d targets. %s. TestIdx: %d, TestTime: %d, %s", delay, duration, jitter, correlation, len(targetSelectors), targetDescription, testIdx, time.Now().Unix(), timefmt) + log.Info(description) + return suite.ComposeNetworkLatencyTest( + description, + targetSelectors, + &delay, + &jitter, + &duration, + &grace, + correlation, + ) +} + +func buildRandomClockSkewTest(testIdx int, targetDescription string, targetSelectors []*suite.ChaosTargetSelector) (*types.SuiteTest, error) { + minDelaySeconds := -900 + maxDelaySeconds := 900 + minDurationSeconds := 10 + maxDurationSeconds := 600 + + grace := time.Second * 600 + delay := fmt.Sprintf("%ds", rand.Intn(maxDelaySeconds-minDelaySeconds)+minDelaySeconds) + duration := fmt.Sprintf("%ds", rand.Intn(maxDurationSeconds-minDurationSeconds)+minDurationSeconds) + + loc := time.FixedZone("GMT", 0) + timefmt := time.Now().In(loc).Format(http.TimeFormat) + description := fmt.Sprintf("Apply %s clock skew for %s against %d targets. %s. TestIdx: %d, TestTime: %d, %s", delay, duration, len(targetSelectors), targetDescription, testIdx, time.Now().Unix(), timefmt) + log.Info(description) + return suite.ComposeNodeClockSkewTest( + description, + targetSelectors, + delay, + duration, + &grace, + ) +} + +func buildRandomTest(testIdx int, targetDescription string, targetSelectors []*suite.ChaosTargetSelector) (*types.SuiteTest, error) { + testId := rand.Intn(2) + if testId == 0 { + return buildRandomLatencyTest(testIdx, targetDescription, targetSelectors) + } + if testId == 1 { + return buildRandomClockSkewTest(testIdx, targetDescription, targetSelectors) + } + return nil, stacktrace.NewError("unknown test id") +} + +func pickRandomClient(config *plan.PlannerConfig) (string, bool) { + //return "reth", true + isExec := rand.Intn(2) + if isExec == 1 { + numExecClients := len(config.ExecutionClients) + idx := rand.Intn(numExecClients) + return config.ExecutionClients[idx].Name, true + } else { + numBeaconClients := len(config.ConsensusClients) + idx := rand.Intn(numBeaconClients) + return config.ConsensusClients[idx].Name, false + } +} + +func StartExploration(config *plan.PlannerConfig, suitecfg *types.ConfigParsed) error { + // todo: big refactor + ctx, cancelCtxFunc := context.WithCancel(context.Background()) + defer cancelCtxFunc() + + enclave, err := runtime.SetupEnclave(ctx, suitecfg) + if err != nil { + return err + } + _ = enclave + + nodes, err := network.ComposeNetworkTopology( + config.Topology, + config.FaultConfig.TargetClient, + config.ExecutionClients, + config.ConsensusClients, + ) + if err != nil { + return err + } + testableNodes := nodes[1:] + + for _, n := range nodes { + log.Infof("%s", suite.ConvertToNodeIdTag(len(nodes), n, "execution")) + log.Infof("%s", suite.ConvertToNodeIdTag(len(nodes), n, "consensus")) + } + + // dedupe from runtime? + kubeClient, err := kubernetes.CreateKubeClient(config.KubernetesNamespace) + if err != nil { + return err + } + rand.Seed(Seed) + // create chaos-mesh client + log.Infof("Creating a chaos-mesh client") + chaosClient, err := chaos_mesh.CreateClient(config.KubernetesNamespace, kubeClient) + if err != nil { + return err + } + + var testArtifacts []*artifacts.TestArtifact + var done = make(chan bool, 2) + sigs := make(chan os.Signal, 2) + signal.Notify(sigs, syscall.SIGINT) + go func() { + sig := <-sigs + fmt.Println() + fmt.Println(sig, "Signal received. Ending after next test is completed.") + done <- true // Signal that we're done + + }() + killall := false + testIdx := 1 + skipUntilTest := 29 + for { + loc := time.FixedZone("GMT", 0) + log.Infof("Start loop. GMT time: %s", time.Now().In(loc).Format(http.TimeFormat)) + select { + case <-done: + fmt.Println("Writing test artifacts") + return cleanup(testArtifacts) + default: + if killall { + fmt.Println("Writing test artifacts") + return cleanup(testArtifacts) + } + clientUnderTest, isExec := pickRandomClient(config) + targetSpec := getTargetSpec() + attackSize := getRandomAttackSize() + + targetFilter, err := suite.TargetSpecEnumToLambda(targetSpec, isExec) + if err != nil { + return err + } + nodeFilter := suite.BuildNodeFilteringLambda(clientUnderTest, isExec) + targetSelectors, err := suite.BuildChaosMeshTargetSelectors(len(nodes), testableNodes, attackSize, nodeFilter, targetFilter) + if err != nil { + log.Warn("unable to satisfy targeting constraint. skipping") + continue + } + + for _, selector := range targetSelectors { + for _, s := range selector.Selector { + msg := "Hitting " + for _, target := range s.Values { + msg = fmt.Sprintf("%s %s,", msg, target) + } + log.Info(msg) + } + } + log.Infof("time: %d", time.Now().Unix()) + + var targetingDescription string + if targetSpec == suite.TargetMatchingNode { + targetingDescription = fmt.Sprintf("Impacting the full node of targeted %s clients. Injecting into %s of the matching targets.", clientUnderTest, attackSize) + } else { + targetingDescription = fmt.Sprintf("Impacting the client of targeted %s clients. Injecting into %s of the matching targets.", clientUnderTest, attackSize) + } + + test, err := buildRandomTest( + testIdx, + targetingDescription, + targetSelectors, + ) + + if err != nil { + return err + } + + if skipUntilTest != -1 { + if testIdx < skipUntilTest { + testIdx += 1 + continue + } + } + + testIdx += 1 + log.Info("Running test") + executor := test_executor.CreateTestExecutor(chaosClient, *test) + + err = executor.RunTestPlan(ctx) + if err != nil { + log.Errorf("Error while running test") + fmt.Println("Writing test artifacts") + return cleanup(testArtifacts) + } else { + log.Infof("Test steps completed.") + } + + log.Infof("Starting health checks at %s", time.Now().In(loc).Format(http.TimeFormat)) + podsUnderTest, err := executor.GetPodsUnderTest() + if err != nil { + return err + } + + hc, err := health.BuildHealthChecker(kubeClient, podsUnderTest, test.HealthConfig) + if err != nil { + return err + } + results, err := hc.RunChecks(ctx) + if err != nil { + + fmt.Println("Writing test artifacts") + err := cleanup(testArtifacts) + return err + } + testArtifact := artifacts.BuildTestArtifact(results, podsUnderTest, *test) + testArtifacts = append(testArtifacts, testArtifact) + if !testArtifact.TestPassed { + log.Warn("Some health checks failed. Stopping test suite.") + return cleanup(testArtifacts) + } + + //time.Sleep(WaitBetweenTestsSecs * time.Second) + } + } + + //cleanup(testArtifacts) + //return nil +} + +func cleanup(testArtifacts []*artifacts.TestArtifact) error { + err := artifacts.SerializeTestArtifacts(testArtifacts) + if err != nil { + return err + } + return nil +} + +*/ diff --git a/pkg/grafana.go b/pkg/grafana.go new file mode 100644 index 0000000..193bdcc --- /dev/null +++ b/pkg/grafana.go @@ -0,0 +1,59 @@ +package pkg + +import ( + "attacknet/cmd/pkg/kubernetes" + "attacknet/cmd/pkg/types" + "context" + "fmt" + grafanaSdk "github.com/grafana-tools/sdk" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" +) + +// note: we may move the grafana logic to the health module if we move towards grafana-based health alerts +type GrafanaTunnel struct { + Client *grafanaSdk.Client + portForwardStopCh chan struct{} + allowPostFaultInspection bool + cleanedUp bool +} + +func CreateGrafanaClient(ctx context.Context, kubeClient *kubernetes.KubeClient, config types.AttacknetConfig) (*GrafanaTunnel, error) { + podName := config.GrafanaPodName + exists, err := kubeClient.PodExists(ctx, podName) + if err != nil { + return nil, stacktrace.Propagate(err, "error while determining whether pod exists %s", podName) + } + if !exists { + return nil, stacktrace.NewError("unable to locate grafana pod %s", podName) + } + + var port uint16 + _, err = fmt.Sscan(config.GrafanaPodPort, &port) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to decode port number %s", config.GrafanaPodPort) + } + + stopCh, err := kubeClient.StartPortForwarding(podName, int(port), int(port), true) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to start port forwarder") + } + + client, err := grafanaSdk.NewClient("http://localhost:3000", "", grafanaSdk.DefaultHTTPClient) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to create Grafana client") + } + + return &GrafanaTunnel{client, stopCh, config.AllowPostFaultInspection, false}, nil +} + +func (t *GrafanaTunnel) Cleanup(skipInspection bool) { + if !t.cleanedUp { + if t.allowPostFaultInspection && !skipInspection { + log.Info("Press enter to terminate the port-forward connection.") + _, _ = fmt.Scanln() + } + close(t.portForwardStopCh) + t.cleanedUp = true + } +} diff --git a/pkg/health/checker.go b/pkg/health/checker.go new file mode 100644 index 0000000..1c2cabb --- /dev/null +++ b/pkg/health/checker.go @@ -0,0 +1,91 @@ +package health + +import ( + chaos_mesh "attacknet/cmd/pkg/chaos-mesh" + "attacknet/cmd/pkg/health/ethereum" + "attacknet/cmd/pkg/health/types" + "attacknet/cmd/pkg/kubernetes" + confTypes "attacknet/cmd/pkg/types" + "context" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + "time" +) + +type CheckOrchestrator struct { + checkerImpl types.GenericNetworkChecker + gracePeriod *time.Duration +} + +func BuildHealthChecker(kubeClient *kubernetes.KubeClient, podsUnderTest []*chaos_mesh.PodUnderTest, healthCheckConfig confTypes.HealthCheckConfig) (*CheckOrchestrator, error) { + networkType := "ethereum" + var checkerImpl types.GenericNetworkChecker + + switch networkType { + case "ethereum": + a := ethereum.CreateEthNetworkChecker(kubeClient, podsUnderTest) + checkerImpl = a + default: + log.Errorf("unknown network type: %s", networkType) + return nil, stacktrace.NewError("unknown network type: %s", networkType) + } + return &CheckOrchestrator{checkerImpl: checkerImpl, gracePeriod: healthCheckConfig.GracePeriod}, nil +} + +func (hc *CheckOrchestrator) RunChecks(ctx context.Context) (*types.HealthCheckResult, error) { + start := time.Now() + latestAllowable := start.Add(*hc.gracePeriod) + log.Infof("Allowing up to %.0f seconds for health checks to pass on all nodes", hc.gracePeriod.Seconds()) + + lastHealthCheckResult := &types.HealthCheckResult{} + for { + results, err := hc.checkerImpl.RunAllChecks(ctx, lastHealthCheckResult) + if err != nil { + return nil, err + } + lastHealthCheckResult = results + if AllChecksPassed(results) { + timeToPass := time.Since(start).Seconds() + pctGraceUsed := timeToPass / hc.gracePeriod.Seconds() * 100 + log.Infof("Checks passed in %.0f seconds. Consumed %.1f pct of the %.0f second grace period", timeToPass, pctGraceUsed, hc.gracePeriod.Seconds()) + return results, nil + } + + if time.Now().After(latestAllowable) { + log.Warnf("Grace period elapsed and a health check is still failing. Time: %d", time.Now().Unix()) + return results, nil + } else { + log.Warn("Health checks failed but still in grace period") + time.Sleep(1 * time.Second) + } + } +} + +func AllChecksPassed(checks *types.HealthCheckResult) bool { + if len(checks.LatestElBlockResult.FailingClientsReportedBlock) > 0 { + return false + } + if len(checks.LatestElBlockResult.FailingClientsReportedHash) > 0 { + return false + } + if len(checks.FinalizedElBlockResult.FailingClientsReportedBlock) > 0 { + return false + } + if len(checks.FinalizedElBlockResult.FailingClientsReportedHash) > 0 { + return false + } + if len(checks.LatestClBlockResult.FailingClientsReportedBlock) > 0 { + return false + } + if len(checks.LatestClBlockResult.FailingClientsReportedHash) > 0 { + return false + } + if len(checks.FinalizedClBlockResult.FailingClientsReportedBlock) > 0 { + return false + } + if len(checks.FinalizedClBlockResult.FailingClientsReportedHash) > 0 { + return false + } + + return true +} diff --git a/pkg/health/ethereum/beacon_rpc.go b/pkg/health/ethereum/beacon_rpc.go new file mode 100644 index 0000000..87d30b8 --- /dev/null +++ b/pkg/health/ethereum/beacon_rpc.go @@ -0,0 +1,200 @@ +package ethereum + +import ( + "attacknet/cmd/pkg/health/types" + "attacknet/cmd/pkg/kubernetes" + "context" + "encoding/hex" + "errors" + "fmt" + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/kurtosis-tech/stacktrace" + "github.com/rs/zerolog" + log "github.com/sirupsen/logrus" + "strings" + "time" +) + +type BeaconClientRpc struct { + session *kubernetes.PortForwardsSession + client eth2client.BeaconBlockHeadersProvider +} + +func (e *EthNetworkChecker) getBeaconClientConsensus(ctx context.Context, clients []*BeaconClientRpc, blockType string, maxAttempts int) (*types.BlockConsensusTestResult, error) { + forkChoice, err := getBeaconNetworkConsensus(ctx, clients, blockType) + if err != nil { + return nil, err + } + // determine whether the nodes are in consensus + consensusBlockNum, wrongBlockNum, consensusBlockHash, wrongBlockHash := determineForkConsensus(forkChoice) + if len(wrongBlockNum) > 0 { + if maxAttempts > 0 { + log.Debugf("Nodes not at consensus for %s block. Waiting and re-trying in case we're on block propagation boundary. Attempts left: %d", blockType, maxAttempts-1) + time.Sleep(1 * time.Second) + return e.getBeaconClientConsensus(ctx, clients, blockType, maxAttempts-1) + } else { + reportConsensusDataToLogger(blockType, consensusBlockNum, wrongBlockNum, consensusBlockHash, wrongBlockHash) + } + } + + blockNumWrong := make(map[string]uint64) + for _, node := range wrongBlockNum { + blockNumWrong[node.Pod.GetName()] = node.BlockNumber + } + + blockHashWrong := make(map[string]string) + + for _, node := range wrongBlockHash { + blockHashWrong[node.Pod.GetName()] = node.BlockHash + } + reportConsensusDataToLogger(blockType, consensusBlockNum, wrongBlockNum, consensusBlockHash, wrongBlockHash) + return &types.BlockConsensusTestResult{ + ConsensusBlock: (consensusBlockNum)[0].BlockNumber, + ConsensusHash: consensusBlockHash[0].BlockHash, + FailingClientsReportedBlock: blockNumWrong, + FailingClientsReportedHash: blockHashWrong, + }, nil +} + +func (e *EthNetworkChecker) dialToBeaconClients(ctx context.Context) ([]*BeaconClientRpc, error) { + labelKey := "kurtosistech.com.custom/ethereum-package.client-type" + labelValue := "beacon" + podsToHealthCheck, err := getPodsToHealthCheck( + ctx, + e.kubeClient, + e.podsUnderTest, + e.podsUnderTestLookup, + labelKey, + labelValue) + if err != nil { + return nil, err + } + + // todo: fix this when kurtosis pkg supports setting the port + var port4000Batch []kubernetes.KubePod + var port3500Batch []kubernetes.KubePod + + for _, pod := range podsToHealthCheck { + if strings.Contains(pod.GetName(), "prysm") { + port3500Batch = append(port3500Batch, pod) + } else { + port4000Batch = append(port4000Batch, pod) + } + } + + log.Debugf("Starting port forward sessions to %d pods", len(podsToHealthCheck)) + + portForwardSessions3500, err := e.kubeClient.StartMultiPortForwardToLabeledPods( + port3500Batch, + labelKey, + labelValue, + 3500) + if err != nil { + return nil, err + } + + portForwardSessions4000, err := e.kubeClient.StartMultiPortForwardToLabeledPods( + port4000Batch, + labelKey, + labelValue, + 4000) + if err != nil { + return nil, err + } + + portForwardSessions := append(portForwardSessions3500, portForwardSessions4000...) + + // dial out to clients + rpcClients := make([]*BeaconClientRpc, len(portForwardSessions)) + for i, s := range portForwardSessions { + client, err := dialBeaconRpcClient(ctx, s) + if err != nil { + return nil, err + } + rpcClients[i] = client + } + return rpcClients, nil +} + +func dialBeaconRpcClient(ctx context.Context, session *kubernetes.PortForwardsSession) (*BeaconClientRpc, error) { + // 3 attempts + retryCount := 8 + for i := 0; i <= retryCount; i++ { + httpClient, err := http.New(ctx, + http.WithAddress(fmt.Sprintf("http://localhost:%d", session.LocalPort)), + http.WithLogLevel(zerolog.WarnLevel), + ) + if err != nil { + if i == retryCount { + return nil, stacktrace.Propagate(err, "err while dialing RPC for %s", session.Pod.GetName()) + } else { + time.Sleep(1 * time.Second) + continue + } + + } + provider, isProvider := httpClient.(eth2client.BeaconBlockHeadersProvider) + if !isProvider { + return nil, stacktrace.NewError("unable to cast http client to beacon rpc provider for %s", session.Pod.GetName()) + } + return &BeaconClientRpc{ + session: session, + client: provider, + }, nil + } + return nil, stacktrace.NewError("unreachable beacon rpc") +} + +func (c *BeaconClientRpc) Close() { + c.session.Close() +} + +func (c *BeaconClientRpc) GetLatestBlockBy(ctx context.Context, blockType string) (*ClientForkChoice, error) { + // todo: handle pods that died and we didn't expect it + result, err := c.client.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{Block: blockType}) + if err != nil { + var apiErr *api.Error + if errors.As(err, &apiErr) { + switch apiErr.StatusCode { + case 404: + if blockType == "finalized" { + choice := &ClientForkChoice{ + Pod: c.session.Pod, + BlockNumber: 0, + BlockHash: "None", + } + return choice, nil + } + } + } + // chock it up to a failure we need to retry + // note: at this time this retry logic isn't actually hooked up. I havent seen any failures to hit this RPC + // endpoint yet, so setting up a retry mechanism may just be over-engineering. + choice := &ClientForkChoice{ + Pod: c.session.Pod, + BlockNumber: 0, + BlockHash: "N/A", + } + return choice, nil + //return nil, stacktrace.Propagate(err, "Unable to query for blockType %s with client for %s", blockType, c.session.Pod.GetName()) + } + + slot := uint64(result.Data.Header.Message.Slot) + bodyHash := hex.EncodeToString(result.Data.Header.Message.BodyRoot[:]) + + if slot == 0 && blockType == "finalized" { + return &ClientForkChoice{ + Pod: c.session.Pod, + BlockNumber: slot, + BlockHash: "None", + }, nil + } else { + return &ClientForkChoice{ + Pod: c.session.Pod, + BlockNumber: slot, + BlockHash: bodyHash, + }, nil + } +} diff --git a/pkg/health/ethereum/consensus.go b/pkg/health/ethereum/consensus.go new file mode 100644 index 0000000..9479757 --- /dev/null +++ b/pkg/health/ethereum/consensus.go @@ -0,0 +1,118 @@ +package ethereum + +import ( + "attacknet/cmd/pkg/kubernetes" + "context" + log "github.com/sirupsen/logrus" + "time" +) + +// var UnableToReachLatestConsensusError = fmt.Errorf("there are nodes that disagree on the latest block") +// var UnableToReachSafeConsensusError = fmt.Errorf("there are nodes that disagree on the safe block") +// var UnableToReachFinalConsensusError = fmt.Errorf("there are nodes that disagree on the finalized block") + +type ClientForkChoice struct { + Pod kubernetes.KubePod + BlockNumber uint64 + BlockHash string +} + +func getExecNetworkConsensus(ctx context.Context, nodeClients []*ExecClientRPC, blockType string) ([]*ClientForkChoice, error) { + clientForkVotes := make([]*ClientForkChoice, len(nodeClients)) + for i, client := range nodeClients { + choice, err := client.GetLatestBlockBy(ctx, blockType) + if err != nil { + return nil, err + } + + clientForkVotes[i] = choice + } + return clientForkVotes, nil +} + +func getBeaconNetworkConsensus(ctx context.Context, nodeClients []*BeaconClientRpc, blockType string) ([]*ClientForkChoice, error) { + clientForkVotes := make([]*ClientForkChoice, len(nodeClients)) + for i, client := range nodeClients { + choice, err := client.GetLatestBlockBy(ctx, blockType) + if err != nil { + return nil, err + } + + clientForkVotes[i] = choice + } + return clientForkVotes, nil +} + +func determineForkConsensus(nodes []*ClientForkChoice) ( + consensusBlockNum []*ClientForkChoice, + wrongBlockNum []*ClientForkChoice, + consensusBlockHash []*ClientForkChoice, + wrongBlockHash []*ClientForkChoice) { + + // convert node votes to map + blockVotes := make(map[uint64][]*ClientForkChoice) + for _, vote := range nodes { + blockVotes[vote.BlockNumber] = append(blockVotes[vote.BlockNumber], vote) + } + + //var consensusBlock uint64 + var consensusBlockVotes int + + // determine consensus block height + for _, v := range blockVotes { + if len(v) > consensusBlockVotes { + if consensusBlockVotes != 0 { + wrongBlockNum = append(wrongBlockNum, consensusBlockNum...) + } + //consensusBlock = k + consensusBlockVotes = len(v) + consensusBlockNum = v + } else { + wrongBlockNum = append(wrongBlockNum, v...) + } + } + + // for the consensus block height, determine the consensus hash + var hashVotes = make(map[string][]*ClientForkChoice) + for _, vote := range consensusBlockNum { + hashVotes[vote.BlockHash] = append(hashVotes[vote.BlockHash], vote) + } + //var consensusHash string + var consensusHashVotes int + for _, v := range hashVotes { + if len(v) > consensusHashVotes { + if consensusBlockVotes != 0 { + wrongBlockHash = append(wrongBlockHash, consensusBlockHash...) + } + //consensusHash = k + consensusHashVotes = len(v) + consensusBlockHash = v + } else { + wrongBlockHash = append(wrongBlockHash, v...) + } + } + return +} + +func reportConsensusDataToLogger(consensusType string, + consensusBlockNum []*ClientForkChoice, + wrongBlockNum []*ClientForkChoice, + consensusBlockHash []*ClientForkChoice, + wrongBlockHash []*ClientForkChoice) { + + log.Infof("Consensus %s block height: %d", consensusType, consensusBlockNum[0].BlockNumber) + if len(wrongBlockNum) > 0 { + log.Warnf("Some nodes are out of consensus for block type '%s'. Time: %d", consensusType, time.Now().Unix()) + for _, n := range wrongBlockNum { + log.Warnf("---> Node: %s %s BlockHeight: %d BlockHash: %s", n.Pod.GetName(), consensusType, n.BlockNumber, n.BlockHash) + } + } + + log.Infof("Consensus %s block hash: %s", consensusType, consensusBlockHash[0].BlockHash) + if len(wrongBlockHash) > 0 { + log.Warnf("Some nodes are at the correct height, but with the wrong '%s' block hash", consensusType) + for _, n := range wrongBlockHash { + log.Warnf("---> Node: %s %s BlockHeight: %d BlockHash: %s", n.Pod.GetName(), consensusType, n.BlockNumber, n.BlockHash) + } + } +} diff --git a/pkg/health/ethereum/execution_rpc.go b/pkg/health/ethereum/execution_rpc.go new file mode 100644 index 0000000..10d863a --- /dev/null +++ b/pkg/health/ethereum/execution_rpc.go @@ -0,0 +1,150 @@ +package ethereum + +import ( + "attacknet/cmd/pkg/health/types" + "attacknet/cmd/pkg/kubernetes" + "context" + "fmt" + geth "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + "time" +) + +type ExecClientRPC struct { + session *kubernetes.PortForwardsSession + client *ethclient.Client +} + +func (e *EthNetworkChecker) getExecBlockConsensus(ctx context.Context, clients []*ExecClientRPC, blockType string, maxAttempts int) (*types.BlockConsensusTestResult, error) { + forkChoice, err := getExecNetworkConsensus(ctx, clients, blockType) + if err != nil { + return nil, err + } + // determine whether the nodes are in consensus + consensusBlockNum, wrongBlockNum, consensusBlockHash, wrongBlockHash := determineForkConsensus(forkChoice) + if len(wrongBlockNum) > 0 { + if maxAttempts > 0 { + log.Debugf("Nodes not at consensus for %s block. Waiting and re-trying in case we're on block propagation boundary. Attempts left: %d", blockType, maxAttempts-1) + time.Sleep(1 * time.Second) + return e.getExecBlockConsensus(ctx, clients, blockType, maxAttempts-1) + } else { + reportConsensusDataToLogger(blockType, consensusBlockNum, wrongBlockNum, consensusBlockHash, wrongBlockHash) + } + } + + blockNumWrong := make(map[string]uint64) + for _, node := range wrongBlockNum { + blockNumWrong[node.Pod.GetName()] = node.BlockNumber + } + + blockHashWrong := make(map[string]string) + + for _, node := range wrongBlockHash { + blockHashWrong[node.Pod.GetName()] = node.BlockHash + } + reportConsensusDataToLogger(blockType, consensusBlockNum, wrongBlockNum, consensusBlockHash, wrongBlockHash) + return &types.BlockConsensusTestResult{ + ConsensusBlock: (consensusBlockNum)[0].BlockNumber, + ConsensusHash: consensusBlockHash[0].BlockHash, + FailingClientsReportedBlock: blockNumWrong, + FailingClientsReportedHash: blockHashWrong, + }, nil +} + +func (e *EthNetworkChecker) dialToExecutionClients(ctx context.Context) ([]*ExecClientRPC, error) { + labelKey := "kurtosistech.com.custom/ethereum-package.client-type" + labelValue := "execution" + podsToHealthCheck, err := getPodsToHealthCheck( + ctx, + e.kubeClient, + e.podsUnderTest, + e.podsUnderTestLookup, + labelKey, + labelValue) + if err != nil { + return nil, err + } + + log.Debugf("Starting port forward sessions to %d pods", len(podsToHealthCheck)) + portForwardSessions, err := e.kubeClient.StartMultiPortForwardToLabeledPods( + podsToHealthCheck, + labelKey, + labelValue, + 8545) + if err != nil { + return nil, err + } + + // dial out to clients + rpcClients := make([]*ExecClientRPC, len(portForwardSessions)) + for i, s := range portForwardSessions { + client, err := dialExecRpcClient(s) + if err != nil { + return nil, err + } + rpcClients[i] = client + } + return rpcClients, nil +} + +func dialExecRpcClient(session *kubernetes.PortForwardsSession) (*ExecClientRPC, error) { + c, err := ethclient.Dial(fmt.Sprintf("http://localhost:%d", session.LocalPort)) + if err != nil { + return nil, stacktrace.Propagate(err, "err while dialing RPC for %s", session.Pod.GetName()) + } + return &ExecClientRPC{session: session, client: c}, nil +} + +func (c *ExecClientRPC) Close() { + c.client.Close() + c.session.Close() +} + +func (c *ExecClientRPC) GetLatestBlockBy(ctx context.Context, blockType string) (*ClientForkChoice, error) { + // todo: handle pods that died and we didn't expect it + var head *geth.Header + var choice *ClientForkChoice + err := c.client.Client().CallContext(ctx, &head, "eth_getBlockByNumber", blockType, false) + if err != nil { + notFinalizingErrors := []string{ + "safe block not found", //geth + "finalized block not found", //geth + "Unknown block", //erigon + "Unknown block error", //nethermind + "unknown block", //reth + } + + noFinalBlockFound := false + for _, msg := range notFinalizingErrors { + if err.Error() == msg { + noFinalBlockFound = true + break + } + } + + if noFinalBlockFound { + choice = &ClientForkChoice{ + Pod: c.session.Pod, + BlockNumber: 0, + BlockHash: "None", + } + } else { + return nil, stacktrace.Propagate(err, "error while calling RPC for client %s", c.session.Pod.GetName()) + } + } else { + blockNum := head.Number.Uint64() + hash := head.Hash().String() + if blockNum == 0 { + // use none for hash + hash = "None" + } + choice = &ClientForkChoice{ + BlockNumber: blockNum, + BlockHash: hash, + Pod: c.session.Pod, + } + } + return choice, nil +} diff --git a/pkg/health/ethereum/network_checker.go b/pkg/health/ethereum/network_checker.go new file mode 100644 index 0000000..27c4ae0 --- /dev/null +++ b/pkg/health/ethereum/network_checker.go @@ -0,0 +1,134 @@ +package ethereum + +import ( + chaos_mesh "attacknet/cmd/pkg/chaos-mesh" + "attacknet/cmd/pkg/kubernetes" + "context" + log "github.com/sirupsen/logrus" + "time" +) +import "attacknet/cmd/pkg/health/types" + +type EthNetworkChecker struct { + kubeClient *kubernetes.KubeClient + podsUnderTest []*chaos_mesh.PodUnderTest + podsUnderTestLookup map[string]*chaos_mesh.PodUnderTest + healthCheckStartTime time.Time +} + +func CreateEthNetworkChecker(kubeClient *kubernetes.KubeClient, podsUnderTest []*chaos_mesh.PodUnderTest) *EthNetworkChecker { + // convert podsUnderTest to a lookup + podsUnderTestMap := make(map[string]*chaos_mesh.PodUnderTest) + + for _, pod := range podsUnderTest { + podsUnderTestMap[pod.Name] = pod + } + + return &EthNetworkChecker{ + podsUnderTest: podsUnderTest, + podsUnderTestLookup: podsUnderTestMap, + kubeClient: kubeClient, + healthCheckStartTime: time.Now(), + } +} + +func (e *EthNetworkChecker) RunAllChecks(ctx context.Context, prevHealthCheckResult *types.HealthCheckResult) (*types.HealthCheckResult, error) { + execRpcClients, err := e.dialToExecutionClients(ctx) + if err != nil { + return nil, err + } + beaconRpcClients, err := e.dialToBeaconClients(ctx) + if err != nil { + return nil, err + } + + log.Debug("Ready to query for health checks") + latestElResult, err := e.getExecBlockConsensus(ctx, execRpcClients, "latest", 15) + if err != nil { + return nil, err + } + latestElArtifact := e.convertResultToArtifact(prevHealthCheckResult.LatestElBlockResult, latestElResult) + + finalElResult, err := e.getExecBlockConsensus(ctx, execRpcClients, "finalized", 3) + if err != nil { + return nil, err + } + finalElArtifact := e.convertResultToArtifact(prevHealthCheckResult.FinalizedElBlockResult, finalElResult) + + log.Debugf("Finalization -> latest lag: %d", latestElResult.ConsensusBlock-finalElResult.ConsensusBlock) + + latestClResult, err := e.getBeaconClientConsensus(ctx, beaconRpcClients, "head", 15) + if err != nil { + return nil, err + } + latestClArtifact := e.convertResultToArtifact(prevHealthCheckResult.LatestClBlockResult, latestClResult) + + finalClResult, err := e.getBeaconClientConsensus(ctx, beaconRpcClients, "finalized", 3) + if err != nil { + return nil, err + } + finalClArtifact := e.convertResultToArtifact(prevHealthCheckResult.FinalizedClBlockResult, finalClResult) + + results := &types.HealthCheckResult{ + LatestElBlockResult: latestElArtifact, + FinalizedElBlockResult: finalElArtifact, + LatestClBlockResult: latestClArtifact, + FinalizedClBlockResult: finalClArtifact, + } + + return results, nil +} + +func (e *EthNetworkChecker) convertResultToArtifact( + prevArtifact *types.BlockConsensusArtifact, + result *types.BlockConsensusTestResult) *types.BlockConsensusArtifact { + + timeSinceChecksStarted := time.Since(e.healthCheckStartTime) + recoveredClients := make(map[string]int) + + if prevArtifact != nil { + // we only mark clients as recovered if at some point they were failing health checks. + for client := range prevArtifact.FailingClientsReportedHash { + if _, stillFailing := result.FailingClientsReportedHash[client]; !stillFailing { + recoveredClients[client] = int(timeSinceChecksStarted.Seconds()) + } + } + + for client := range prevArtifact.FailingClientsReportedBlock { + if _, stillFailing := result.FailingClientsReportedBlock[client]; !stillFailing { + recoveredClients[client] = int(timeSinceChecksStarted.Seconds()) + } + } + + // merge previously recovered clients with the new + for k, v := range prevArtifact.NodeRecoveryTimeSeconds { + recoveredClients[k] = v + } + } + + didUnfaultedNodesNeedToRecover := false + for client := range recoveredClients { + if _, wasUnderTest := e.podsUnderTestLookup[client]; !wasUnderTest { + didUnfaultedNodesNeedToRecover = true + } + } + + didUnfaultedNodesFail := false + for client := range result.FailingClientsReportedBlock { + if _, wasUnderTest := e.podsUnderTestLookup[client]; !wasUnderTest { + didUnfaultedNodesFail = true + } + } + for client := range result.FailingClientsReportedHash { + if _, wasUnderTest := e.podsUnderTestLookup[client]; !wasUnderTest { + didUnfaultedNodesFail = true + } + } + + return &types.BlockConsensusArtifact{ + BlockConsensusTestResult: result, + DidUnfaultedNodesFail: didUnfaultedNodesFail, + DidUnfaultedNodesNeedToRecover: didUnfaultedNodesNeedToRecover, + NodeRecoveryTimeSeconds: recoveredClients, + } +} diff --git a/pkg/health/ethereum/util.go b/pkg/health/ethereum/util.go new file mode 100644 index 0000000..1a8787b --- /dev/null +++ b/pkg/health/ethereum/util.go @@ -0,0 +1,39 @@ +package ethereum + +import ( + chaos_mesh "attacknet/cmd/pkg/chaos-mesh" + "attacknet/cmd/pkg/kubernetes" + "context" +) + +func getPodsToHealthCheck( + ctx context.Context, + kubeClient *kubernetes.KubeClient, + podsUnderTest []*chaos_mesh.PodUnderTest, + podsUnderTestLookup map[string]*chaos_mesh.PodUnderTest, + labelKey, labelValue string, +) ([]kubernetes.KubePod, error) { + + var podsToHealthCheck []kubernetes.KubePod + // add pods under test that match the label criteria _and_ aren't expected to die + // todo: depending on whether we're testing network recovery or node recovery, we may want to health check nodes we're expecting to die + for _, pod := range podsUnderTest { + if pod.MatchesLabel(labelKey, labelValue) && !pod.ExpectDeath { + podsToHealthCheck = append(podsToHealthCheck, pod) + } + } + + // add pods that were not targeted by a fault + bystanders, err := kubeClient.PodsMatchingLabel(ctx, labelKey, labelValue) + if err != nil { + return nil, err + } + for _, pod := range bystanders { + _, match := podsUnderTestLookup[pod.GetName()] + // don't add pods we've already added + if !match { + podsToHealthCheck = append(podsToHealthCheck, pod) + } + } + return podsToHealthCheck, nil +} diff --git a/pkg/health/types/types.go b/pkg/health/types/types.go new file mode 100644 index 0000000..95932a0 --- /dev/null +++ b/pkg/health/types/types.go @@ -0,0 +1,28 @@ +package types + +import "context" + +type GenericNetworkChecker interface { + RunAllChecks(context.Context, *HealthCheckResult) (*HealthCheckResult, error) +} + +type BlockConsensusTestResult struct { + ConsensusBlock uint64 `yaml:"consensus_block"` + ConsensusHash string `yaml:"consensus_hash"` + FailingClientsReportedBlock map[string]uint64 `yaml:"failing_clients_reported_block"` + FailingClientsReportedHash map[string]string `yaml:"failing_clients_reported_hash"` +} + +type BlockConsensusArtifact struct { + *BlockConsensusTestResult `yaml:",inline"` + DidUnfaultedNodesFail bool `yaml:"did_unfaulted_nodes_fail"` + DidUnfaultedNodesNeedToRecover bool `yaml:"did_unfaulted_nodes_need_to_recover"` + NodeRecoveryTimeSeconds map[string]int `yaml:"node_recovery_time_seconds"` +} + +type HealthCheckResult struct { + LatestElBlockResult *BlockConsensusArtifact `yaml:"latest_el_block_health_result"` + FinalizedElBlockResult *BlockConsensusArtifact `yaml:"finalized_el_block_health_result"` + LatestClBlockResult *BlockConsensusArtifact `yaml:"latest_cl_block_health_result"` + FinalizedClBlockResult *BlockConsensusArtifact `yaml:"finalized_cl_block_health_result"` +} diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go new file mode 100644 index 0000000..a9e64e2 --- /dev/null +++ b/pkg/kubernetes/kubernetes.go @@ -0,0 +1,79 @@ +package kubernetes + +import ( + "context" + "fmt" + "github.com/kurtosis-tech/stacktrace" + //api "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + //corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "os" + "path/filepath" + pkgclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type KubeClient struct { + clientInternal *rest.Config + clientset *kubernetes.Clientset + namespace string +} + +func CreateKubeClient(namespace string) (*KubeClient, error) { + kubeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config") + + kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to load the default kubeconfig file") + } + + kubeClient, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to build a kubernetes client for the default types") + } + + if err != nil { + return nil, stacktrace.Propagate(err, "unable to create a kubernetes API client") + } + + c := &KubeClient{ + clientInternal: kubeConfig, + clientset: kubeClient, + namespace: namespace, + } + + return c, nil +} + +func (c *KubeClient) CreateDerivedClientWithSchema(scheme *runtime.Scheme) (pkgclient.Client, error) { + return pkgclient.New(c.clientInternal, pkgclient.Options{Scheme: scheme}) +} + +// todo: figure out the actual error conditions/pod doesnt exist error +func (c *KubeClient) PodExists(ctx context.Context, name string) (bool, error) { + _, err := c.clientset.CoreV1().Pods(c.namespace).Get(ctx, name, metav1.GetOptions{}) + if err == nil { + return true, nil + } else { + return false, nil + } +} + +func (c *KubeClient) PodsMatchingLabel(ctx context.Context, labelKey, labelValue string) ([]KubePod, error) { + selector := fmt.Sprintf("%s=%s", labelKey, labelValue) + pods, err := c.clientset.CoreV1().Pods(c.namespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) + if err != nil { + return nil, err + } + + var matchingPods []KubePod + for _, pod := range pods.Items { + labels := pod.GetLabels() + matchingPods = append(matchingPods, &Pod{Name: pod.Name, Labels: labels}) + } + + return matchingPods, nil +} diff --git a/pkg/kubernetes/port_forward.go b/pkg/kubernetes/port_forward.go new file mode 100644 index 0000000..ecb1f99 --- /dev/null +++ b/pkg/kubernetes/port_forward.go @@ -0,0 +1,148 @@ +package kubernetes + +import ( + "errors" + "fmt" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + "io" + "k8s.io/apimachinery/pkg/util/httpstream" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + "net" + "net/http" + "net/url" + "time" +) + +type PortForwardsSession struct { + stopCh chan struct{} + Pod KubePod + TargetPort int + LocalPort int +} + +func (session *PortForwardsSession) Close() { + close(session.stopCh) +} + +func (c *KubeClient) StartMultiPortForwardToLabeledPods( + pods []KubePod, + labelKey, labelValue string, + targetPort int) ([]*PortForwardsSession, error) { + var podsToForward []KubePod + + for _, pod := range pods { + if pod.MatchesLabel(labelKey, labelValue) { + podsToForward = append(podsToForward, pod) + } + } + + portForwardSessions, err := c.StartMultiPortForwards(podsToForward, targetPort) + return portForwardSessions, err +} + +func (c *KubeClient) StartMultiPortForwards(pods []KubePod, targetPort int) ([]*PortForwardsSession, error) { + sessions := make([]*PortForwardsSession, len(pods)) + + for i, pod := range pods { + localPort, err := getFreeEphemeralPort() + if err != nil { + return nil, err + } + stopCh, err := c.StartPortForwarding(pod.GetName(), localPort, targetPort, false) + if err != nil { + return nil, err + } + + sessions[i] = &PortForwardsSession{ + stopCh: stopCh, + Pod: pod, + TargetPort: targetPort, + LocalPort: localPort, + } + } + return sessions, nil +} + +// getFreeEphemeralPort note: you should use this port immediately otherwise another resource may claim it. +func getFreeEphemeralPort() (int, error) { + + listener, err := net.Listen("tcp", ":0") + if err != nil { + return 0, stacktrace.Propagate(err, "Error while finding new ephemeral port") + } + port := listener.Addr().(*net.TCPAddr).Port + err = listener.Close() + if err != nil { + return 0, stacktrace.Propagate(err, "Error while closing listener") + } + return port, nil +} + +func openPortForward(target string, dialer httpstream.Dialer, printToStdout bool, retriesRemaining int) (chan struct{}, error) { + readyCh := make(chan struct{}, 1) + stopCh := make(chan struct{}, 1) + errLogger := io.Discard + stdLogger := io.Discard + if printToStdout { + logger := log.New() + errLogger = CreatePrefixWriter("[port-forward] ", logger.WriterLevel(log.ErrorLevel)) + stdLogger = CreatePrefixWriter("[port-forward] ", logger.WriterLevel(log.InfoLevel)) + } + + portForward, err := portforward.New(dialer, []string{target}, stopCh, readyCh, stdLogger, errLogger) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to create port forward dialer") + } + + portForwardIssueCh := make(chan error, 1) + retryCh := make(chan bool, 1) + defer close(portForwardIssueCh) + defer close(retryCh) + + go func() { + err = portForward.ForwardPorts() + if err != nil { + if retriesRemaining == 0 { + portForwardIssueCh <- stacktrace.Propagate(err, "unable to start port forward session") + } else { + retryCh <- true + } + } + }() + + select { + case <-readyCh: + return stopCh, nil + case err = <-portForwardIssueCh: + return nil, err + case <-retryCh: + time.Sleep(200 * time.Millisecond) + return openPortForward(target, dialer, printToStdout, retriesRemaining-1) + case <-time.After(time.Minute): + return nil, errors.New("timed out after waiting to establish port forward") + } +} + +func (c *KubeClient) StartPortForwarding(pod string, localPort, remotePort int, printToStdout bool) (stopCh chan struct{}, err error) { + roundTripper, upgrader, err := spdy.RoundTripperFor(c.clientInternal) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to create roundtripper") + } + + path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", c.namespace, pod) + serverURL, err := url.Parse(c.clientInternal.Host) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to decode kubeconfig.Host: %s", c.clientInternal.Host) + } + serverURL.Path = path + + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL) + target := fmt.Sprintf("%d:%d", localPort, remotePort) + + stopCh, err = openPortForward(target, dialer, printToStdout, 5) + + log.Debugf("Port-forward established to pod/%s:%d", pod, remotePort) + return stopCh, err +} diff --git a/pkg/kubernetes/types.go b/pkg/kubernetes/types.go new file mode 100644 index 0000000..631c5dd --- /dev/null +++ b/pkg/kubernetes/types.go @@ -0,0 +1,31 @@ +package kubernetes + +type KubePod interface { + GetName() string + GetLabels() map[string]string + MatchesLabel(string, string) bool +} + +type KubePodList []KubePod + +type Pod struct { + Name string + Labels map[string]string +} + +func (p *Pod) GetName() string { + return p.Name +} + +func (p *Pod) GetLabels() map[string]string { + return p.Labels +} + +func (p *Pod) MatchesLabel(key, value string) bool { + v, exists := p.Labels[key] + if !exists { + return false + } else { + return v == value + } +} diff --git a/pkg/kubernetes/utils.go b/pkg/kubernetes/utils.go new file mode 100644 index 0000000..25fbb42 --- /dev/null +++ b/pkg/kubernetes/utils.go @@ -0,0 +1,16 @@ +package kubernetes + +import "io" + +type LogPrefixWriter struct { + prefix string + writer io.Writer +} + +func CreatePrefixWriter(prefix string, writer io.Writer) *LogPrefixWriter { + return &LogPrefixWriter{prefix, writer} +} + +func (pw *LogPrefixWriter) Write(p []byte) (n int, err error) { + return pw.writer.Write(append([]byte(pw.prefix), p...)) +} diff --git a/pkg/kurtosis/kurtosis.go b/pkg/kurtosis/kurtosis.go new file mode 100644 index 0000000..54b7672 --- /dev/null +++ b/pkg/kurtosis/kurtosis.go @@ -0,0 +1,181 @@ +package kurtosis + +import ( + "attacknet/cmd/pkg/types" + "context" + "errors" + "fmt" + "github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings" + "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/enclaves" + "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/starlark_run_config" + "github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + "os" + "strings" + "time" +) + +type EnclaveContextWrapper struct { + Namespace string + kurtosisCtx *kurtosis_context.KurtosisContext + enclaveCtxInner *enclaves.EnclaveContext + reuseDevnetBetweenRuns bool +} + +func (e *EnclaveContextWrapper) Destroy(ctx context.Context) { + if e.reuseDevnetBetweenRuns { + log.Infof("Skipping enclave deletion, enclave in namespace %s was flagged to be skip deletion", e.Namespace) + } else { + log.Infof("Destroying enclave") + err := e.kurtosisCtx.DestroyEnclave(ctx, e.enclaveCtxInner.GetEnclaveName()) + if err != nil { + log.Fatal(err) + } + } +} + +// pass-thru func. Figure out how to remove eventually. +func (e *EnclaveContextWrapper) RunStarlarkRemotePackageBlocking( + ctx context.Context, + packageId string, + cfg *starlark_run_config.StarlarkRunConfig, +) (*enclaves.StarlarkRunResult, error) { + return e.enclaveCtxInner.RunStarlarkRemotePackageBlocking(ctx, packageId, cfg) +} + +// pass-thru func. Figure out how to remove eventually. +func (e *EnclaveContextWrapper) RunStarlarkRemotePackage( + ctx context.Context, + packageRootPath string, + runConfig *starlark_run_config.StarlarkRunConfig, +) (chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine, context.CancelFunc, error) { + return e.enclaveCtxInner.RunStarlarkRemotePackage(ctx, packageRootPath, runConfig) +} + +func GetKurtosisContext() (*kurtosis_context.KurtosisContext, error) { + kurtosisCtx, err := kurtosis_context.NewKurtosisContextFromLocalEngine() + if err != nil { + if strings.Contains(err.Error(), "connect: connection refused") { + return nil, fmt.Errorf("could not connect to the Kurtosis engine. Be sure the engine is running using `kurtosis engine status` or `kurtosis engine start`. You might also need to start the gateway using `kurtosis gateway` - %w", err) + } else { + return nil, err + } + } + return kurtosisCtx, nil +} + +func getEnclaveName(namespace string) string { + var enclaveName string + if namespace != "" { + enclaveName = namespace[3:] + } else { + enclaveName = fmt.Sprintf("attacknet-%d", time.Now().Unix()) + } + return enclaveName +} + +func isErrorNoEnclaveFound(err error) bool { + rootCause := stacktrace.RootCause(err) + if strings.Contains(rootCause.Error(), "Couldn't find an enclave for identifier") { + return true + } else { + return false + } +} + +func CreateOrImportContext(ctx context.Context, kurtosisCtx *kurtosis_context.KurtosisContext, cfg *types.ConfigParsed) (*EnclaveContextWrapper, bool, error) { + enclaveName := getEnclaveName(cfg.AttacknetConfig.ExistingDevnetNamespace) + + // first check for existing enclave + enclaveCtx, err := kurtosisCtx.GetEnclaveContext(ctx, enclaveName) + if err == nil { + if !cfg.AttacknetConfig.ReuseDevnetBetweenRuns { + log.Errorf("An existing enclave was found with the name %s, but ReuseDevnetBetweenRuns is set to false. Todo: add tear-down logic here.", enclaveName) + os.Exit(1) + } + enclaveCtxWrapper := &EnclaveContextWrapper{ + Namespace: fmt.Sprintf("kt-%s", enclaveCtx.GetEnclaveName()), + kurtosisCtx: kurtosisCtx, + enclaveCtxInner: enclaveCtx, + reuseDevnetBetweenRuns: true, + } + return enclaveCtxWrapper, false, nil + } else { + // check if no enclave found + if !isErrorNoEnclaveFound(err) { + return nil, false, err + } + + log.Infof("No existing kurtosis enclave by the name of %s was found. Creating a new one.", enclaveName) + enclaveCtxNew, err := kurtosisCtx.CreateProductionEnclave(ctx, enclaveName) + if err != nil { + return nil, false, err + } + enclaveCtxWrapper := &EnclaveContextWrapper{ + Namespace: fmt.Sprintf("kt-%s", enclaveCtxNew.GetEnclaveName()), + kurtosisCtx: kurtosisCtx, + enclaveCtxInner: enclaveCtxNew, + reuseDevnetBetweenRuns: cfg.AttacknetConfig.ReuseDevnetBetweenRuns, + } + return enclaveCtxWrapper, true, nil + } +} + +func StartNetwork(ctx context.Context, enclaveCtx *EnclaveContextWrapper, harnessConfig types.HarnessConfigParsed) error { + log.Infof("------------ EXECUTING PACKAGE ---------------") + cfg := &starlark_run_config.StarlarkRunConfig{ + SerializedParams: string(harnessConfig.NetworkConfig), + } + a, _, err := enclaveCtx.RunStarlarkRemotePackage(ctx, harnessConfig.NetworkPackage, cfg) + if err != nil { + return stacktrace.Propagate(err, "error running Starklark script") + } + + // todo: clean this up when we decide to add log filtering + progressIndex := 0 + for { + t := <-a + progress := t.GetProgressInfo() + if progress != nil { + progressMsgs := progress.CurrentStepInfo + for i := progressIndex; i < len(progressMsgs); i++ { + log.Infof("[Kurtosis] %s", progressMsgs[i]) + } + progressIndex = len(progressMsgs) + } + + info := t.GetInfo() + if info != nil { + log.Infof("[Kurtosis] %s", info.InfoMessage) + } + + warn := t.GetWarning() + if warn != nil { + log.Warnf("[Kurtosis] %s", warn.WarningMessage) + } + + e := t.GetError() + if e != nil { + log.Errorf("[Kurtosis] %s", e.String()) + return stacktrace.Propagate(errors.New("kurtosis deployment failed during execution"), "%s", e.String()) + } + + insRes := t.GetInstructionResult() + if insRes != nil { + log.Infof("[Kurtosis] %s", insRes.SerializedInstructionResult) + } + + finishRes := t.GetRunFinishedEvent() + if finishRes != nil { + log.Infof("[Kurtosis] %s", finishRes.GetSerializedOutput()) + if finishRes.IsRunSuccessful { + log.Info("[Kurtosis] Devnet genesis successful. Passing back to Attacknet") + return nil + } else { + log.Error("[Kurtosis] There was an error during genesis.") + return stacktrace.Propagate(errors.New("kurtosis deployment failed"), "%s", finishRes.GetSerializedOutput()) + } + } + } +} diff --git a/pkg/plan/config.go b/pkg/plan/config.go new file mode 100644 index 0000000..b466971 --- /dev/null +++ b/pkg/plan/config.go @@ -0,0 +1,67 @@ +package plan + +import ( + "attacknet/cmd/pkg/plan/suite" + "github.com/kurtosis-tech/stacktrace" + "gopkg.in/yaml.v3" + "os" +) + +func validatePlannerFaultConfiguration(c PlannerConfig) error { + // fault type + _, ok := suite.FaultTypes[c.FaultConfig.FaultType] + if !ok { + return stacktrace.NewError("the fault type '%s' is not supported. Supported faults: %v", c.FaultConfig.FaultType, suite.FaultTypesList) + } + + // target client + // todo + + // intensity domains + // todo + + // targeting dimensions + for _, spec := range c.FaultConfig.TargetingDimensions { + _, ok := suite.TargetingSpecs[spec] + if !ok { + return stacktrace.NewError("the fault targeting dimension %s is not supported. Supported dimensions: %v", spec, suite.TargetingSpecList) + } + } + + // attack size dimensions + for _, attackSize := range c.FaultConfig.AttackSizeDimensions { + _, ok := suite.AttackSizes[attackSize] + if !ok { + return stacktrace.NewError("the attack size dimension %s is not supported. Supported dimensions: %v", attackSize, suite.AttackSizesList) + } + } + + // target client + if c.FaultConfig.TargetClient != "all" { + if !c.IsTargetExecutionClient() && !c.IsTargetConsensusClient() { + return stacktrace.NewError("target_client %s is not defined in the execution/consensus client configuration", c.FaultConfig.TargetClient) + } + } + + return nil +} + +func LoadPlannerConfigFromPath(path string) (*PlannerConfig, error) { + bs, err := os.ReadFile(path) + if err != nil { + return nil, stacktrace.Propagate(err, "could not planner config on path %s", path) + } + + var config PlannerConfig + err = yaml.Unmarshal(bs, &config) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to unmarshal planner config from %s", path) + } + + err = validatePlannerFaultConfiguration(config) + if err != nil { + return nil, err + } + + return &config, nil +} diff --git a/pkg/plan/file.go b/pkg/plan/file.go new file mode 100644 index 0000000..7004f39 --- /dev/null +++ b/pkg/plan/file.go @@ -0,0 +1,75 @@ +package plan + +import ( + "fmt" + "github.com/kurtosis-tech/stacktrace" + "os" + "path/filepath" +) + +func preparePaths(testName string) (netRefPath, netConfigPath, planConfigPath string, err error) { + dir, err := os.Getwd() + // initialize to empty string for error cases + netConfigPath = "" + planConfigPath = "" + if err != nil { + return + } + + netRefPath = fmt.Sprintf("plan/%s.yaml", testName) + networkConfigName := fmt.Sprintf("network-configs/%s", netRefPath) + netConfigPath = filepath.Join(dir, networkConfigName) + if _, err = os.Stat(netConfigPath); err == nil { + // delete file + err = os.Remove(netConfigPath) + if err != nil { + err = stacktrace.Propagate(err, "unable to remove file") + return + } + } + + suiteName := fmt.Sprintf("test-suites/plan/%s.yaml", testName) + planConfigPath = filepath.Join(dir, suiteName) + if _, err = os.Stat(planConfigPath); err == nil { + // delete file + err = os.Remove(planConfigPath) + if err != nil { + err = stacktrace.Propagate(err, "unable to remove file") + return + } + } + err = nil + return +} + +func writePlans(netConfigPath, suiteConfigPath string, netConfig, suiteConfig []byte) error { + f, err := os.Create(netConfigPath) + if err != nil { + return stacktrace.Propagate(err, "cannot open network types path %s", netConfigPath) + } + _, err = f.Write(netConfig) + if err != nil { + return stacktrace.Propagate(err, "could not write network types to file") + } + + err = f.Close() + if err != nil { + return stacktrace.Propagate(err, "could not close network types file") + } + + f, err = os.Create(suiteConfigPath) + if err != nil { + return stacktrace.Propagate(err, "cannot open suite types path %s", suiteConfigPath) + } + _, err = f.Write(suiteConfig) + if err != nil { + return stacktrace.Propagate(err, "could not write suite types to file") + } + + err = f.Close() + if err != nil { + return stacktrace.Propagate(err, "could not close suite types file") + } + + return nil +} diff --git a/pkg/plan/network/clients.go b/pkg/plan/network/clients.go new file mode 100644 index 0000000..8415fbe --- /dev/null +++ b/pkg/plan/network/clients.go @@ -0,0 +1,25 @@ +package network + +import ( + "github.com/kurtosis-tech/stacktrace" +) + +func buildNode(index int, execConf, consensusConf ClientVersion) *Node { + return &Node{ + Index: index, + Execution: composeExecutionClient(execConf), + Consensus: composeConsensusClient(consensusConf), + } +} + +func composeBootnode(bootEl, bootCl string, execClients, consensusClients map[string]ClientVersion) (*Node, error) { + execConf, ok := execClients[bootEl] + if !ok { + return nil, stacktrace.NewError("unable to load configuration for exec client %s", bootEl) + } + consConf, ok := consensusClients[bootCl] + if !ok { + return nil, stacktrace.NewError("unable to load configuration for exec client %s", bootCl) + } + return buildNode(1, execConf, consConf), nil +} diff --git a/pkg/plan/network/consensus.go b/pkg/plan/network/consensus.go new file mode 100644 index 0000000..2bd46f5 --- /dev/null +++ b/pkg/plan/network/consensus.go @@ -0,0 +1,75 @@ +package network + +import ( + "github.com/kurtosis-tech/stacktrace" + "strings" +) + +const defaultClCpu = 1000 +const defaultValCpu = 500 + +const defaultClMem = 1536 +const defaultValMem = 512 + +func composeConsensusTesterNetwork(nodeMultiplier int, consensusClient string, execClientList []ClientVersion, consClientMap map[string]ClientVersion) ([]*Node, error) { + // make sure consensusClient actually exists + clientUnderTest, ok := consClientMap[consensusClient] + if !ok { + return nil, stacktrace.NewError("unknown consensus client %s", consensusClient) + } + + // start from 2 because bootnode is index 1 + index := 2 + nodes, err := composeNodesForClTesting(nodeMultiplier, index, clientUnderTest, execClientList) + return nodes, err +} + +func composeNodesForClTesting(nodeMultiplier, index int, consensusClient ClientVersion, execClients []ClientVersion) ([]*Node, error) { + var nodes []*Node + + for _, execClient := range execClients { + for i := 0; i < nodeMultiplier; i++ { + node := buildNode(index, execClient, consensusClient) + nodes = append(nodes, node) + + index += 1 + } + } + return nodes, nil +} + +func composeConsensusClient(config ClientVersion) *ConsensusClient { + image := config.Image + validatorImage := "" + + if strings.Contains(config.Image, ",") { + images := strings.Split(config.Image, ",") + image = images[0] + validatorImage = images[1] + } + if config.HasSidecar { + return &ConsensusClient{ + Type: config.Name, + Image: image, + HasValidatorSidecar: true, + ValidatorImage: validatorImage, + ExtraLabels: make(map[string]string), + CpuRequired: defaultClCpu, + MemoryRequired: defaultClMem, + SidecarCpuRequired: defaultValCpu, + SidecarMemoryRequired: defaultValMem, + } + } else { + return &ConsensusClient{ + Type: config.Name, + Image: image, + HasValidatorSidecar: false, + ValidatorImage: validatorImage, + ExtraLabels: make(map[string]string), + CpuRequired: defaultClCpu, + MemoryRequired: defaultClMem, + SidecarCpuRequired: 0, + SidecarMemoryRequired: 0, + } + } +} diff --git a/pkg/plan/network/execution.go b/pkg/plan/network/execution.go new file mode 100644 index 0000000..51c5eb5 --- /dev/null +++ b/pkg/plan/network/execution.go @@ -0,0 +1,45 @@ +package network + +import ( + "github.com/kurtosis-tech/stacktrace" +) + +const defaultElCpu = 768 +const defaultElMem = 1024 + +func composeExecTesterNetwork(nodeMultiplier int, execClient string, consClientList []ClientVersion, execClientMap map[string]ClientVersion) ([]*Node, error) { + // make sure execClient actually exists + clientUnderTest, ok := execClientMap[execClient] + if !ok { + return nil, stacktrace.NewError("unknown execution client %s", execClient) + } + + // start from 2 because bootnode is index 1 + index := 2 + nodes, err := composeNodesForElTesting(nodeMultiplier, index, clientUnderTest, consClientList) + return nodes, err +} + +func composeNodesForElTesting(nodeMultiplier, index int, execClient ClientVersion, consClientList []ClientVersion) ([]*Node, error) { + var nodes []*Node + + for _, consensusClient := range consClientList { + for i := 0; i < nodeMultiplier; i++ { + node := buildNode(index, execClient, consensusClient) + nodes = append(nodes, node) + + index += 1 + } + } + return nodes, nil +} + +func composeExecutionClient(config ClientVersion) *ExecutionClient { + return &ExecutionClient{ + Type: config.Name, + Image: config.Image, + ExtraLabels: make(map[string]string), + CpuRequired: defaultElCpu, + MemoryRequired: defaultElMem, + } +} diff --git a/pkg/plan/network/network_builder.go b/pkg/plan/network/network_builder.go new file mode 100644 index 0000000..4cd7739 --- /dev/null +++ b/pkg/plan/network/network_builder.go @@ -0,0 +1,172 @@ +package network + +import ( + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" +) + +func clientListsToMaps(execClients, consClients []ClientVersion) (execClientMap, consClientMap map[string]ClientVersion, err error) { + populateClientMap := func(li []ClientVersion) (map[string]ClientVersion, error) { + clients := make(map[string]ClientVersion) + for _, client := range li { + _, exists := clients[client.Name] + if exists { + return nil, stacktrace.NewError("duplicate configuration for client %s", client.Name) + } + clients[client.Name] = client + } + return clients, nil + } + + execClientMap, err = populateClientMap(execClients) + if err != nil { + return nil, nil, err + } + + consClientMap, err = populateClientMap(consClients) + if err != nil { + return nil, nil, err + } + + return execClientMap, consClientMap, nil +} + +func ComposeNetworkTopology(topology Topology, clientUnderTest string, execClients, consClients []ClientVersion) ([]*Node, error) { + if clientUnderTest == "all" { + return nil, stacktrace.NewError("target clientUnderTest 'all' not supported yet") + } + + isExecutionClient := false + for _, execClient := range execClients { + if execClient.Name == clientUnderTest { + isExecutionClient = true + break + } + } + + execClientMap, consClientMap, err := clientListsToMaps(execClients, consClients) + if err != nil { + return nil, err + } + + var nodes []*Node + bootnode, err := composeBootnode(topology.BootnodeEL, topology.BootnodeCl, execClientMap, consClientMap) + if err != nil { + return nil, err + } + nodes = append(nodes, bootnode) + + // determine whether a node multiplier is applied. + var nodeMultiplier int = 1 + if topology.TargetNodeMultiplier != 0 { + nodeMultiplier = int(topology.TargetNodeMultiplier) + } + + // assume already checked clientUnderTest is a member of consClients or execClients + var nodesToTest []*Node + if isExecutionClient { + nodesToTest, err = composeExecTesterNetwork(nodeMultiplier, clientUnderTest, consClients, execClientMap) + } else { + nodesToTest, err = composeConsensusTesterNetwork(nodeMultiplier, clientUnderTest, execClients, consClientMap) + } + if err != nil { + return nil, err + } + nodes = append(nodes, nodesToTest...) + + // add more nodes to the network to satisfy target percent threshold + extraNodes, err := composeNodesToSatisfyTargetPercent( + topology.TargetsAsPercentOfNetwork, + len(nodes)-1, + nodes[len(nodes)-1].Index+1, + clientUnderTest, + execClients, + consClients, + ) + if err != nil { + return nil, err + } + nodes = append(nodes, extraNodes...) + return nodes, nil +} + +func composeNodesToSatisfyTargetPercent(percentTarget float32, targetedNodeCount int, startIndex int, clientUnderTest string, execClients, consClients []ClientVersion) ([]*Node, error) { + // percent target is unconfigured + if percentTarget == 0 { + return []*Node{}, nil + } + + nodesToAdd, err := calcNodesNeededToSatisfyTarget(percentTarget, targetedNodeCount) + if err != nil { + return nil, err + } + + nodes, err := pickExtraNodeClients(startIndex, nodesToAdd, clientUnderTest, execClients, consClients) + return nodes, err +} + +func pickExtraNodeClients(startNodeIndex, nodeCount int, clientUnderTest string, execClients, consClients []ClientVersion) ([]*Node, error) { + var nodes []*Node + //execIndex := 0 + //consIndex := 0 +Exit: + for { + for execIndex := 0; execIndex < len(execClients); execIndex++ { + if execClients[execIndex].Name == clientUnderTest { + continue + } + + for consIndex := 0; consIndex < len(consClients); consIndex++ { + if consClients[consIndex].Name == clientUnderTest { + continue + } + nodes = append(nodes, buildNode(startNodeIndex, execClients[execIndex], consClients[consIndex])) + startNodeIndex += 1 + if len(nodes) == nodeCount { + break Exit + } + } + } + } + return nodes, nil +} + +func pickClient(startIndex int, clientUnderTest string, clients []ClientVersion) (ClientVersion, int, bool, error) { + looped := false + for i := 0; i < len(clients); i++ { + c := clients[startIndex] + + startIndex += 1 + if startIndex >= len(clients) { + looped = true + startIndex = 0 + } + + if c.Name != clientUnderTest { + return c, startIndex, looped, nil + } + } + return ClientVersion{}, 0, looped, stacktrace.NewError("Unable to find any clients defined other than %s. Cannot add more nodes.", clientUnderTest) +} + +func calcNodesNeededToSatisfyTarget(percentTarget float32, targetedNodeCount int) (int, error) { + if percentTarget > 1.0 || percentTarget < 0 { + return 0, stacktrace.NewError("invalid value for targets_as_percent_of_network, must be >=0 and < 1") + } + //if percentTarget > 0.9 + + networkSize := float32(targetedNodeCount) / percentTarget + if networkSize-float32(targetedNodeCount) < 1 { + return 0, stacktrace.NewError("unable to compose a network where targeted nodes are %.2f of the network. The presence of the bootnode prevents this value from exceeding %.2f", percentTarget, float32(targetedNodeCount)/(float32(targetedNodeCount)+1)) + } + + if percentTarget <= 0.30 { + log.Warnf("The currently configured targets_as_percent_of_network of %.2f will create a network of %d nodes", percentTarget, int(networkSize)) + } else { + log.Infof("The currently configured targets_as_percent_of_network of %.2f will create a network of %d nodes", percentTarget, int(networkSize)) + + } + + nodesToAdd := int(networkSize) - targetedNodeCount - 1 + return nodesToAdd, nil +} diff --git a/pkg/plan/network/types.go b/pkg/plan/network/types.go new file mode 100644 index 0000000..d296196 --- /dev/null +++ b/pkg/plan/network/types.go @@ -0,0 +1,63 @@ +package network + +import "fmt" + +type GenesisConfig struct { + PreregisteredValidatorKeysMnemonic *string `yaml:"preregistered_validator_keys_mnemonic,omitempty"` + PreregisteredValidatorCount *int `yaml:"preregistered_validator_count,omitempty"` + NetworkId *int `yaml:"network_id,omitempty"` + DepositContractAddress *string `yaml:"deposit_contract_address,omitempty"` + SecondsPerSlot *int `yaml:"seconds_per_slot,omitempty"` + GenesisDelay *int `yaml:"genesis_delay,omitempty"` + MaxChurn *uint64 `yaml:"max_churn,omitempty"` + EjectionBalance *uint64 `yaml:"ejection_balance,omitempty"` + Eth1FollowDistance *int `yaml:"eth1_follow_distance,omitempty"` + CapellaForkEpoch *int `yaml:"capella_fork_epoch,omitempty"` + DenebForkEpoch *int `yaml:"deneb_fork_epoch,omitempty"` + ElectraForkEpoch *int `yaml:"electra_fork_epoch,omitempty"` + NumValKeysPerNode int `yaml:"num_validator_keys_per_node"` +} + +type Topology struct { + BootnodeEL string `yaml:"bootnode_el"` + BootnodeCl string `yaml:"bootnode_cl"` + TargetsAsPercentOfNetwork float32 `yaml:"targets_as_percent_of_network"` + TargetNodeMultiplier uint `yaml:"target_node_multiplier"` +} + +type ClientVersion struct { + Name string `yaml:"name"` + Image string `yaml:"image"` + HasSidecar bool `yaml:"has_sidecar,omitempty"` +} + +type ExecutionClient struct { + Type string + Image string + ExtraLabels map[string]string + CpuRequired int + MemoryRequired int +} + +type ConsensusClient struct { + Type string + Image string + HasValidatorSidecar bool + ValidatorImage string + ExtraLabels map[string]string + CpuRequired int + MemoryRequired int + SidecarCpuRequired int + SidecarMemoryRequired int +} + +type Node struct { + Index int + Execution *ExecutionClient + Consensus *ConsensusClient + ConsensusVotes int +} + +func (n *Node) ToString() string { + return fmt.Sprintf("#%d %s/%s", n.Index, n.Execution.Type, n.Consensus.Type) +} diff --git a/pkg/plan/plan.go b/pkg/plan/plan.go new file mode 100644 index 0000000..ef1eab1 --- /dev/null +++ b/pkg/plan/plan.go @@ -0,0 +1,76 @@ +package plan + +import ( + "attacknet/cmd/pkg/plan/network" + "attacknet/cmd/pkg/plan/suite" + types "attacknet/cmd/pkg/types" + "gopkg.in/yaml.v3" +) + +func BuildPlan(planName string, config *PlannerConfig) error { + + netRefPath, netConfigPath, suiteConfigPath, err := preparePaths(planName) + if err != nil { + return err + } + + nodes, err := network.ComposeNetworkTopology( + config.Topology, + config.FaultConfig.TargetClient, + config.ExecutionClients, + config.ConsensusClients, + ) + if err != nil { + return err + } + + isExecTarget := config.IsTargetExecutionClient() + // exclude the bootnode from test targeting + potentialNodesUnderTest := nodes[1:] + tests, err := suite.ComposeTestSuite(config.FaultConfig, isExecTarget, potentialNodesUnderTest) + if err != nil { + return err + } + + var attacknetConfig types.AttacknetConfig + if config.KubernetesNamespace == "" { + attacknetConfig = types.AttacknetConfig{ + GrafanaPodName: "grafana", + GrafanaPodPort: "3000", + WaitBeforeInjectionSeconds: uint32(config.FaultConfig.WaitBeforeFirstTest.Seconds()), + ReuseDevnetBetweenRuns: true, + AllowPostFaultInspection: false, + } + } else { + attacknetConfig = types.AttacknetConfig{ + GrafanaPodName: "grafana", + GrafanaPodPort: "3000", + WaitBeforeInjectionSeconds: uint32(config.FaultConfig.WaitBeforeFirstTest.Seconds()), + ReuseDevnetBetweenRuns: true, + ExistingDevnetNamespace: config.KubernetesNamespace, + AllowPostFaultInspection: false, + } + } + + c := types.Config{ + AttacknetConfig: attacknetConfig, + HarnessConfig: types.HarnessConfig{ + NetworkPackage: config.KurtosisPackage, + NetworkConfigPath: netRefPath, + NetworkType: "ethereum", + }, + TestConfig: types.SuiteTestConfigs{Tests: tests}, + } + + suiteConfig, err := yaml.Marshal(c) + if err != nil { + return err + } + + networkConfig, err := SerializeNetworkTopology(nodes, &config.GenesisParams) + if err != nil { + return err + } + + return writePlans(netConfigPath, suiteConfigPath, networkConfig, suiteConfig) +} diff --git a/pkg/plan/serialization.go b/pkg/plan/serialization.go new file mode 100644 index 0000000..c25d567 --- /dev/null +++ b/pkg/plan/serialization.go @@ -0,0 +1,124 @@ +package plan + +import ( + "attacknet/cmd/pkg/plan/network" + "github.com/kurtosis-tech/stacktrace" + "gopkg.in/yaml.v3" + "strings" +) + +func SerializeNetworkTopology(nodes []*network.Node, config *network.GenesisConfig) ([]byte, error) { + serializableNodes := serializeNodes(nodes) + + netConfig := &EthKurtosisConfig{ + Participants: serializableNodes, + NetParams: *config, + AdditionalServices: []string{ + "prometheus_grafana", + "dora", + }, + ParallelKeystoreGen: false, + Persistent: false, + DisablePeerScoring: true, + } + + bs, err := yaml.Marshal(netConfig) + if err != nil { + return nil, stacktrace.Propagate(err, "intermediate yaml marshalling failed") + } + + return bs, nil +} + +func DeserializeNetworkTopology(conf []byte) ([]*network.Node, error) { + parsedConf := EthKurtosisConfig{} + err := yaml.Unmarshal(conf, &parsedConf) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to parse eth network types") + } + + var nodes []*network.Node + + for i, participant := range parsedConf.Participants { + hasSidecar := false + consensusImage := participant.ClClientImage + validatorImage := "" + if participant.ValMaxCpu != 0 { + hasSidecar = true + // todo: remove this + if strings.Contains(consensusImage, ",") { + images := strings.Split(consensusImage, ",") + consensusImage = images[0] + validatorImage = images[1] + } + } + + votesPerNode := parsedConf.NetParams.NumValKeysPerNode + + node := &network.Node{ + Index: i + 1, + ConsensusVotes: votesPerNode, + Consensus: &network.ConsensusClient{ + Type: participant.ClClientType, + Image: consensusImage, + ValidatorImage: validatorImage, + HasValidatorSidecar: hasSidecar, + ExtraLabels: map[string]string{}, + CpuRequired: participant.ClMinCpu, + MemoryRequired: participant.ClMinMemory, + SidecarCpuRequired: participant.ValMinCpu, + SidecarMemoryRequired: participant.ValMinMemory, + }, + Execution: &network.ExecutionClient{ + Type: participant.ElClientType, + Image: participant.ElClientImage, + ExtraLabels: map[string]string{}, + CpuRequired: participant.ElMinCpu, + MemoryRequired: participant.ElMinMemory, + }, + } + + nodes = append(nodes, node) + + } + return nodes, nil +} + +func serializeNodes(nodes []*network.Node) []*Participant { + participants := make([]*Participant, len(nodes)) + for i, node := range nodes { + consensusImage := node.Consensus.Image + + // prysm contingency + //if node.Consensus.HasValidatorSidecar && node.Consensus.ValidatorImage != "" { + // consensusImage = consensusImage + fmt.Sprintf(",%s", node.Consensus.ValidatorImage) + //} + + p := &Participant{ + ElClientType: node.Execution.Type, + ElClientImage: node.Execution.Image, + + ClClientType: node.Consensus.Type, + ClClientImage: consensusImage, + + ElMinCpu: node.Execution.CpuRequired, + ElMaxCpu: node.Execution.CpuRequired, + ElMinMemory: node.Execution.MemoryRequired, + ElMaxMemory: node.Execution.MemoryRequired, + + ClMinCpu: node.Consensus.CpuRequired, + ClMaxCpu: node.Consensus.CpuRequired, + ClMinMemory: node.Consensus.MemoryRequired, + ClMaxMemory: node.Consensus.MemoryRequired, + + ValMinCpu: node.Consensus.SidecarCpuRequired, + ValMaxCpu: node.Consensus.SidecarCpuRequired, + ValMinMemory: node.Consensus.SidecarMemoryRequired, + ValMaxMemory: node.Consensus.SidecarMemoryRequired, + Count: 1, + } + participants[i] = p + } + + return participants +} diff --git a/pkg/plan/suite/faults.go b/pkg/plan/suite/faults.go new file mode 100644 index 0000000..c8f51aa --- /dev/null +++ b/pkg/plan/suite/faults.go @@ -0,0 +1,329 @@ +package suite + +import ( + "attacknet/cmd/pkg/types" + "fmt" + "github.com/kurtosis-tech/stacktrace" + yaml "gopkg.in/yaml.v3" + "strconv" + "strings" + "time" +) + +// We can't use chaos mesh's types because type-inlining is not supported in yaml.v3, making it so you can't serialize +// and deserialize to the same struct. Instead, we create pared down copies of the structs with no inlining. +// Completely scuffed. + +type ChaosExpressionSelector struct { + Key string `yaml:"key"` + Operator string `yaml:"operator"` + Values []string `yaml:"values"` +} + +type Selector struct { + LabelSelectors map[string]string `yaml:"labelSelectors,omitempty"` + ExpressionSelectors []ChaosExpressionSelector `yaml:"expressionSelectors,omitempty"` +} + +type TimeChaosSpec struct { + Selector `yaml:"selector"` + Mode string `yaml:"mode"` + Action string `yaml:"action"` + TimeOffset string `yaml:"timeOffset"` + Duration string `yaml:"duration"` +} + +type TimeChaosFault struct { + Spec TimeChaosSpec `yaml:"spec"` + Kind string `yaml:"kind"` + ApiVersion string `yaml:"apiVersion"` +} + +type TimeChaosWrapper struct { + TimeChaosFault `yaml:"chaosFaultSpec"` +} + +type PodChaosSpec struct { + Selector `yaml:"selector"` + Mode string `yaml:"mode"` + Duration string `yaml:"duration"` + Action string `yaml:"action"` +} + +type PodChaosFault struct { + Spec PodChaosSpec `yaml:"spec"` + Kind string `yaml:"kind"` + ApiVersion string `yaml:"apiVersion"` +} + +type PodChaosWrapper struct { + PodChaosFault `yaml:"chaosFaultSpec"` +} + +type IOChaosSpec struct { + Selector `yaml:"selector"` + Mode string `yaml:"mode"` + + Action string `yaml:"action"` + VolumePath string `yaml:"volumePath"` + Delay *time.Duration `yaml:"delay"` + Percent int `yaml:"percent"` + Duration *time.Duration `yaml:"duration"` +} + +type IOChaosFault struct { + Spec IOChaosSpec `yaml:"spec"` + Kind string `yaml:"kind"` + ApiVersion string `yaml:"apiVersion"` +} + +type IOChaosWrapper struct { + IOChaosFault `yaml:"chaosFaultSpec"` +} + +type NetworkDelaySpec struct { + Latency *time.Duration `yaml:"latency"` + Correlation string `yaml:"correlation,omitempty"` + Jitter *time.Duration `yaml:"jitter,omitempty"` +} +type NetworkLossSpec struct { + Loss string `yaml:"loss"` + Correlation string `yaml:"correlation,omitempty"` +} + +type NetworkDuplicateSpec struct { + Duplicate float32 `yaml:"duplicate"` + Correlation string `yaml:"correlation,omitempty"` +} + +type NetworkCorruptSpec struct { + Corrupt float32 `yaml:"duplicate"` + Correlation string `yaml:"correlation,omitempty"` +} + +type NetworkBandwidthSpec struct { + Rate string `yaml:"rate"` + Limit uint32 `yaml:"limit"` + Buffer uint32 `yaml:"buffer"` + PeakRate *uint64 `yaml:"peak_rate,omitempty"` +} + +type NetworkDropSpec struct { + Loss uint32 `yaml:"loss"` +} + +type NetworkChaosSpec struct { + Selector `yaml:"selector"` + Mode string `yaml:"mode"` + Action string `yaml:"action"` + Duration *time.Duration `yaml:"duration"` + Delay *NetworkDelaySpec `yaml:"delay,omitempty"` + Loss *NetworkLossSpec `yaml:"loss,omitempty"` + Duplicate *NetworkDuplicateSpec `yaml:"duplicate,omitempty"` + Corrupt *NetworkCorruptSpec `yaml:"corrupt,omitempty"` + Bandwidth *NetworkBandwidthSpec `yaml:"bandwidth,omitempty"` + Direction string `yaml:"direction,omitempty"` +} + +type NetworkChaosFault struct { + Spec NetworkChaosSpec `yaml:"spec"` + Kind string `yaml:"kind"` + ApiVersion string `yaml:"apiVersion"` +} + +type NetworkChaosWrapper struct { + NetworkChaosFault `yaml:"chaosFaultSpec"` +} + +func convertFaultSpecToMap[T any](s T) (map[string]interface{}, error) { + // convert to map[string]interface{} using yaml intermediate. seriously. + bs, err := yaml.Marshal(s) + if err != nil { + return nil, stacktrace.Propagate(err, "intermediate yaml marshalling failed") + } + + var faultSpec map[string]interface{} + err = yaml.Unmarshal(bs, &faultSpec) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to deserialize intermediate yaml") + } + return faultSpec, nil +} + +func convertFaultSpecToMapSpecial(s NetworkChaosWrapper) (map[string]interface{}, error) { + // convert to map[string]interface{} using yaml intermediate. seriously. + bs, err := yaml.Marshal(s) + if err != nil { + return nil, stacktrace.Propagate(err, "intermediate yaml marshalling failed") + } + + var faultSpec map[string]interface{} + err = yaml.Unmarshal(bs, &faultSpec) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to deserialize intermediate yaml") + } + return faultSpec, nil +} + +func convertFaultSpecToInjectStep(description string, s interface{}) (*types.PlanStep, error) { + faultSpecMap, err := convertFaultSpecToMap(s) + if err != nil { + return nil, err + } + + return &types.PlanStep{ + StepType: types.InjectFault, + StepDescription: description, + Spec: faultSpecMap, + }, nil +} + +func convertFaultSpecToInjectStepSpecial(description string, s NetworkChaosWrapper) (*types.PlanStep, error) { + faultSpecMap, err := convertFaultSpecToMapSpecial(s) + if err != nil { + return nil, err + } + + return &types.PlanStep{ + StepType: types.InjectFault, + StepDescription: description, + Spec: faultSpecMap, + }, nil +} + +func buildClockSkewFault(description, timeOffset, duration string, expressionSelectors []ChaosExpressionSelector) (*types.PlanStep, error) { + t := TimeChaosWrapper{ + TimeChaosFault: TimeChaosFault{ + Kind: "TimeChaos", + ApiVersion: "chaos-mesh.org/v1alpha1", + Spec: TimeChaosSpec{ + Duration: duration, + TimeOffset: timeOffset, + Mode: "all", + Action: "delay", + Selector: Selector{ + ExpressionSelectors: expressionSelectors, + }, + }, + }, + } + return convertFaultSpecToInjectStep(description, t) +} + +func buildPodRestartFault(description string, expressionSelectors []ChaosExpressionSelector) (*types.PlanStep, error) { + t := PodChaosWrapper{ + PodChaosFault: PodChaosFault{ + Kind: "PodChaos", + ApiVersion: "chaos-mesh.org/v1alpha1", + Spec: PodChaosSpec{ + Duration: "5s", + Mode: "all", + Action: "pod-failure", + Selector: Selector{ + ExpressionSelectors: expressionSelectors, + }, + }, + }, + } + + return convertFaultSpecToInjectStep(description, t) +} + +func getVolumePathForIOFault(podName string) (string, error) { + var nodeType string + parts := strings.Split(podName, "-") + if parts[0] == "el" { + nodeType = "execution" + } else { + nodeType = "consensus" + } + if parts[len(parts)-1] == "validator" { + return "", stacktrace.NewError("cannot create an i/o latency fault on a validator sidecar pod. Try to target matching clients only: %s", podName) + } + clientName := parts[2] + volumeTarget := fmt.Sprintf("/data/%s/%s-data", clientName, nodeType) + return volumeTarget, nil +} + +func buildIOLatencyFault(description string, expressionSelector ChaosExpressionSelector, delay *time.Duration, percent int, duration *time.Duration) ([]types.PlanStep, error) { + var steps []types.PlanStep + + for _, podName := range expressionSelector.Values { + volumePath, err := getVolumePathForIOFault(podName) + if err != nil { + return nil, err + } + + t := IOChaosWrapper{ + IOChaosFault: IOChaosFault{ + Kind: "IOChaos", + ApiVersion: "chaos-mesh.org/v1alpha1", + Spec: IOChaosSpec{ + Duration: duration, + Mode: "all", + Selector: Selector{ + ExpressionSelectors: []ChaosExpressionSelector{expressionSelector}, + }, + Action: "latency", + VolumePath: volumePath, + Delay: delay, + Percent: percent, + }, + }, + } + + step, err := convertFaultSpecToInjectStep(description, t) + if err != nil { + return nil, err + } + steps = append(steps, *step) + } + + return steps, nil +} + +func buildNetworkLatencyFault(description string, expressionSelectors []ChaosExpressionSelector, delay, jitter, duration *time.Duration, correlation int) (*types.PlanStep, error) { + t := NetworkChaosWrapper{ + NetworkChaosFault: NetworkChaosFault{ + Kind: "NetworkChaos", + ApiVersion: "chaos-mesh.org/v1alpha1", + Spec: NetworkChaosSpec{ + Duration: duration, + Mode: "all", + Action: "delay", + Selector: Selector{ + ExpressionSelectors: expressionSelectors, + }, + Delay: &NetworkDelaySpec{ + Latency: delay, + Correlation: fmt.Sprintf("%d", correlation), + Jitter: jitter, + }, + }, + }, + } + + return convertFaultSpecToInjectStepSpecial(description, t) +} + +func buildPacketDropFault(description string, expressionSelectors []ChaosExpressionSelector, percent int, direction string, duration *time.Duration) (*types.PlanStep, error) { + t := NetworkChaosWrapper{ + NetworkChaosFault: NetworkChaosFault{ + Kind: "NetworkChaos", + ApiVersion: "chaos-mesh.org/v1alpha1", + Spec: NetworkChaosSpec{ + Duration: duration, + Mode: "all", + Action: "loss", + Selector: Selector{ + ExpressionSelectors: expressionSelectors, + }, + Direction: direction, + Loss: &NetworkLossSpec{ + Loss: strconv.Itoa(percent), + }, + }, + }, + } + return convertFaultSpecToInjectStepSpecial(description, t) +} diff --git a/pkg/plan/suite/step_builder.go b/pkg/plan/suite/step_builder.go new file mode 100644 index 0000000..c9f4bca --- /dev/null +++ b/pkg/plan/suite/step_builder.go @@ -0,0 +1,143 @@ +package suite + +import ( + "attacknet/cmd/pkg/plan/network" + "attacknet/cmd/pkg/types" + "fmt" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + "time" +) + +type clientType string + +const ( + Execution clientType = "execution" + Consensus clientType = "consensus" + Validator clientType = "validator" +) + +func ConvertToNodeIdTag(networkNodeCount int, node *network.Node, client clientType) string { + nodeNumStr := "" + + if networkNodeCount < 10 { + nodeNumStr = fmt.Sprintf("%d", node.Index) + } else if networkNodeCount < 100 { + nodeNumStr = fmt.Sprintf("%02d", node.Index) + } else { + nodeNumStr = fmt.Sprintf("%03d", node.Index) + } + + switch client { + case Execution: + return fmt.Sprintf("el-%s-%s-%s", nodeNumStr, node.Execution.Type, node.Consensus.Type) + case Consensus: + return fmt.Sprintf("cl-%s-%s-%s", nodeNumStr, node.Consensus.Type, node.Execution.Type) + case Validator: + return fmt.Sprintf("val-%s-%s-%s", nodeNumStr, node.Consensus.Type, node.Execution.Type) + default: + log.Errorf("Unrecognized node type %s", client) + return "" + } +} + +func composeWaitForFaultCompletionStep() *types.PlanStep { + return &types.PlanStep{StepType: types.WaitForFaultCompletion, StepDescription: "wait for faults to terminate"} +} + +func composeNodeClockSkewPlanSteps(targetsSelected []*ChaosTargetSelector, skew, duration string) ([]types.PlanStep, error) { + var steps []types.PlanStep + for _, target := range targetsSelected { + description := fmt.Sprintf("Inject clock skew on target %s", target.Description) + + skewStep, err := buildClockSkewFault(description, skew, duration, target.Selector) + if err != nil { + return nil, err + } + steps = append(steps, *skewStep) + } + + return steps, nil +} + +func composeNodeRestartSteps(targetsSelected []*ChaosTargetSelector) ([]types.PlanStep, error) { + var steps []types.PlanStep + + for _, target := range targetsSelected { + description := fmt.Sprintf("Restart target %s", target.Description) + restartStep, err := buildPodRestartFault(description, target.Selector) + + if err != nil { + return nil, err + } + steps = append(steps, *restartStep) + } + + return steps, nil +} + +func areExprSelectorsMatchingIdIn(expressionSelectors []ChaosExpressionSelector) error { + for _, selector := range expressionSelectors { + if selector.Key != "kurtosistech.com/id" { + return stacktrace.NewError("i/o latency faults can only be target using pod id: %s", selector.Key) + } + if selector.Operator != "In" { + return stacktrace.NewError("i/o latency faults can only be target using the 'In' operator: %s", selector.Operator) + } + } + return nil +} + +func composeIOLatencySteps(targetsSelected []*ChaosTargetSelector, delay *time.Duration, percent int, duration *time.Duration) ([]types.PlanStep, error) { + var steps []types.PlanStep + + for _, target := range targetsSelected { + description := fmt.Sprintf("Inject i/o latency on target %s", target.Description) + err := areExprSelectorsMatchingIdIn(target.Selector) + if err != nil { + return nil, err + } + + // for i/o faults, we need to create a plan step for each individual pod because the fault spec has to say the data path. + for _, selector := range target.Selector { + ioLatencySteps, err := buildIOLatencyFault(description, selector, delay, percent, duration) + if err != nil { + return nil, err + } + steps = append(steps, ioLatencySteps...) + } + } + + return steps, nil + +} + +func composeNetworkLatencySteps(targetsSelected []*ChaosTargetSelector, delay, jitter, duration *time.Duration, correlation int) ([]types.PlanStep, error) { + var steps []types.PlanStep + for _, target := range targetsSelected { + description := fmt.Sprintf("Inject network latency on target %s", target.Description) + + skewStep, err := buildNetworkLatencyFault(description, target.Selector, delay, jitter, duration, correlation) + if err != nil { + return nil, err + } + steps = append(steps, *skewStep) + } + + return steps, nil +} + +func composePacketDropSteps(targetsSelected []*ChaosTargetSelector, percent int, direction string, duration *time.Duration) ([]types.PlanStep, error) { + var steps []types.PlanStep + for _, target := range targetsSelected { + description := fmt.Sprintf("Inject network latency on target %s", target.Description) + + skewStep, err := buildPacketDropFault(description, target.Selector, percent, direction, duration) + if err != nil { + return nil, err + } + steps = append(steps, *skewStep) + } + + return steps, nil +} diff --git a/pkg/plan/suite/suite_builder.go b/pkg/plan/suite/suite_builder.go new file mode 100644 index 0000000..dbb0541 --- /dev/null +++ b/pkg/plan/suite/suite_builder.go @@ -0,0 +1,229 @@ +package suite + +import ( + "attacknet/cmd/pkg/plan/network" + "attacknet/cmd/pkg/types" + "fmt" + "github.com/kurtosis-tech/stacktrace" + log "github.com/sirupsen/logrus" + "strconv" + "time" +) + +func ComposeTestSuite( + config PlannerFaultConfiguration, + isExecClient bool, + nodes []*network.Node) ([]types.SuiteTest, error) { + + var tests []types.SuiteTest + runtimeEstimate := 0 + + nodeFilter := BuildNodeFilteringLambda(config.TargetClient, isExecClient) + + for _, targetDimension := range config.TargetingDimensions { + targetFilter, err := TargetSpecEnumToLambda(targetDimension, isExecClient) + if err != nil { + return nil, err + } + nodeCountsTested := make(map[int]bool) + for _, attackSize := range config.AttackSizeDimensions { + targetSelectors, err := BuildChaosMeshTargetSelectors(len(nodes)+1, nodes, attackSize, nodeFilter, targetFilter) + if err != nil { + cannotMeet, ok := err.(CannotMeetConstraintError) + if !ok { + return nil, err + } + log.Infof("Attack size %s for %d nodes cannot be satisfied. Use more clients if this attack size needs to be tested.", cannotMeet.AttackSize, cannotMeet.TargetableCount) + continue + } + // deduplicate attack sizes that produce the same scope + _, alreadyTested := nodeCountsTested[len(targetSelectors)] + if alreadyTested { + continue + } else { + nodeCountsTested[len(targetSelectors)] = true + } + + for _, faultConfig := range config.FaultConfigDimensions { + // update runtime estimate. find better way + duration, ok := faultConfig["duration"] + if ok { + d, err := time.ParseDuration(duration) + if err == nil { + runtimeEstimate += int(d.Seconds()) + } + } + var targetingDescription string + if targetDimension == TargetMatchingNode { + targetingDescription = fmt.Sprintf("Impacting the full node of targeted %s clients. Injecting into %s of the matching targets.", config.TargetClient, attackSize) + } else { + targetingDescription = fmt.Sprintf("Impacting the client of targeted %s clients. Injecting into %s of the matching targets.", config.TargetClient, attackSize) + } + + test, err := composeTestForFaultType( + config.FaultType, + faultConfig, + targetSelectors, + targetingDescription, + ) + if err != nil { + return nil, err + } + tests = append(tests, *test) + } + } + } + log.Infof("Tests generated: %d", len(tests)) + log.Infof("ESTIMATE: Running this test suite will take, at minimum, %d minutes based on fault durations.", runtimeEstimate/60) + + return tests, nil +} + +func getDurationValue(key string, m map[string]string) (*time.Duration, error) { + + valueStr, ok := m[key] + if !ok { + return nil, stacktrace.NewError("missing %s field", key) + } + duration, err := time.ParseDuration(valueStr) + if err != nil { + return nil, stacktrace.NewError("unable to convert %s field to a time duration", key) + } + return &duration, nil +} + +func getUintValue(key string, m map[string]string) (uint32, error) { + valueStr, ok := m[key] + if !ok { + return 0, stacktrace.NewError("missing %s field", key) + } + uintValue, err := strconv.ParseUint(valueStr, 10, 32) + if err != nil { + return 0, stacktrace.NewError("unable to convert %s field to a uint32", key) + } + return uint32(uintValue), nil +} + +func getFloat32Value(key string, m map[string]string) (float32, error) { + valueStr, ok := m[key] + if !ok { + return 0, stacktrace.NewError("missing %s field", key) + } + floatValue, err := strconv.ParseFloat(valueStr, 32) + if err != nil { + return 0, stacktrace.NewError("unable to convert %s field to a uint32", key) + } + return float32(floatValue), nil +} + +func getStringValue(key string, m map[string]string) (string, error) { + valueStr, ok := m[key] + if !ok { + return "", stacktrace.NewError("missing %s field", key) + } + return valueStr, nil +} + +func composeTestForFaultType( + faultType FaultTypeEnum, + config map[string]string, + targetSelectors []*ChaosTargetSelector, + targetingDescription string, +) (*types.SuiteTest, error) { + + switch faultType { + case FaultClockSkew: + skew, ok := config["skew"] + if !ok { + return nil, stacktrace.NewError("missing skew field for clock skew fault") + } + duration, ok := config["duration"] + if !ok { + return nil, stacktrace.NewError("missing duration field for clock skew fault") + } + graceDuration, err := getDurationValue("grace_period", config) + if err != nil { + return nil, err + } + + description := fmt.Sprintf("Apply %s clock skew for %s against %d targets. %s", skew, duration, len(targetSelectors), targetingDescription) + return ComposeNodeClockSkewTest(description, targetSelectors, skew, duration, graceDuration) + case FaultContainerRestart: + + graceDuration, err := getDurationValue("grace_period", config) + if err != nil { + return nil, err + } + description := fmt.Sprintf("Restarting %d targets. %s", len(targetSelectors), targetingDescription) + return composeNodeRestartTest(description, targetSelectors, graceDuration) + case FaultIOLatency: + grace, err := getDurationValue("grace_period", config) + if err != nil { + return nil, err + } + delay, err := getDurationValue("delay", config) + if err != nil { + return nil, err + } + faultDuration, err := getDurationValue("duration", config) + if err != nil { + return nil, err + } + + percent, ok := config["percent"] + if !ok { + return nil, stacktrace.NewError("missing percent field in io latency fault") + } + percentInt, err := strconv.Atoi(percent) + if err != nil { + return nil, stacktrace.Propagate(err, "unable to parse io latency fault percent field") + } + description := fmt.Sprintf("Apply %s i/o latency for %s. Impacting %d pct of i/o calls. against %d targets. %s", delay, faultDuration, percentInt, len(targetSelectors), targetingDescription) + + return composeIOLatencyTest(description, targetSelectors, delay, percentInt, faultDuration, grace) + case FaultNetworkLatency: + grace, err := getDurationValue("grace_period", config) + if err != nil { + return nil, err + } + delay, err := getDurationValue("delay", config) + if err != nil { + return nil, err + } + jitter, err := getDurationValue("jitter", config) + if err != nil { + return nil, err + } + duration, err := getDurationValue("duration", config) + if err != nil { + return nil, err + } + correlation, err := getUintValue("correlation", config) + if err != nil { + return nil, err + } + description := fmt.Sprintf("Apply %s network latency for %s. Jitter: %s, correlation: %d against %d targets. %s", delay, duration, jitter, correlation, len(targetSelectors), targetingDescription) + return ComposeNetworkLatencyTest(description, targetSelectors, delay, jitter, duration, grace, int(correlation)) + case FaultPacketLoss: + grace, err := getDurationValue("grace_period", config) + if err != nil { + return nil, err + } + duration, err := getDurationValue("duration", config) + if err != nil { + return nil, err + } + lossPercent, err := getUintValue("loss_percent", config) + if err != nil { + return nil, err + } + direction, err := getStringValue("direction", config) + if err != nil { + return nil, err + } + description := fmt.Sprintf("Apply %d packet drop for %s, direction: %s against %d targets. %s", lossPercent, duration, direction, len(targetSelectors), targetingDescription) + return ComposePacketDropTest(description, targetSelectors, int(lossPercent), direction, duration, grace) + } + + return nil, nil +} diff --git a/pkg/plan/suite/targeting.go b/pkg/plan/suite/targeting.go new file mode 100644 index 0000000..931aab4 --- /dev/null +++ b/pkg/plan/suite/targeting.go @@ -0,0 +1,238 @@ +package suite + +import ( + "attacknet/cmd/pkg/plan/network" + "fmt" + "github.com/kurtosis-tech/stacktrace" +) + +type ChaosTargetSelector struct { + Selector []ChaosExpressionSelector + Description string +} + +type CannotMeetConstraintError struct { + AttackSize + TargetableCount int +} + +func (e CannotMeetConstraintError) Error() string { + return fmt.Sprintf("Cannot target '%s' for %d nodes", e.AttackSize, e.TargetableCount) +} + +type NodeFilterCriteria func(n *network.Node) bool +type TargetCriteriaFilter func(AttackSize, int, []*network.Node) ([]*network.Node, error) +type NodeImpactSelector func(networkNodeCount int, node *network.Node) *ChaosTargetSelector + +func BuildNodeFilteringLambda(clientType string, isExecClient bool) TargetCriteriaFilter { + if isExecClient { + return filterNodesByExecClient(clientType) + } else { + return filterNodesByConsensusClient(clientType) + } +} + +func filterNodes(nodes []*network.Node, criteria NodeFilterCriteria) []*network.Node { + var result []*network.Node + for _, n := range nodes { + if criteria(n) { + result = append(result, n) + } + } + return result +} + +func chooseTargetsUsingAttackSize(size AttackSize, networkSize int, targetable []*network.Node) ([]*network.Node, error) { + networkSizeFloat := float32(networkSize) + var nodesToTarget int + switch size { + case AttackOne: + nodesToTarget = 1 + case AttackAll: + nodesToTarget = len(targetable) + case AttackMinority: + nodesToTarget = int(networkSizeFloat * 0.32) + if nodesToTarget > len(targetable) { + return nil, CannotMeetConstraintError{ + AttackSize: size, + TargetableCount: networkSize, + } + } + case AttackSuperminority: + nodesToTarget = int(networkSizeFloat * 0.34) + if float32(nodesToTarget)/networkSizeFloat < 0.333333333 { + nodesToTarget += 1 + } + if float32(nodesToTarget)/networkSizeFloat >= 0.50 || nodesToTarget > len(targetable) { + // not enough nodes to use this attack size + return nil, CannotMeetConstraintError{ + AttackSize: size, + TargetableCount: networkSize, + } + } + case AttackMajority: + nodesToTarget = int(networkSizeFloat * 0.51) + if float32(nodesToTarget)/networkSizeFloat <= 0.50 { + nodesToTarget += 1 + } + if float32(nodesToTarget)/networkSizeFloat >= 0.66 || nodesToTarget > len(targetable) { + // not enough nodes to use this attack size + return nil, CannotMeetConstraintError{ + AttackSize: size, + TargetableCount: networkSize, + } + } + case AttackSupermajority: + nodesToTarget = int(networkSizeFloat * 0.67) + if float32(nodesToTarget)/networkSizeFloat <= 0.666666666 { + nodesToTarget += 1 + } + if float32(nodesToTarget)/networkSizeFloat > 1 || nodesToTarget > len(targetable) { + // not enough nodes to use this attack size + return nil, CannotMeetConstraintError{ + AttackSize: size, + TargetableCount: networkSize, + } + } + } + + if nodesToTarget == 0 { + return nil, CannotMeetConstraintError{ + AttackSize: size, + TargetableCount: len(targetable), + } + } + + var targets []*network.Node + for i := 0; i < nodesToTarget; i++ { + targets = append(targets, targetable[i]) + } + return targets, nil +} + +func createTargetSelectorForNode(networkNodeCount int, node *network.Node) *ChaosTargetSelector { + var targets []string + + elId := ConvertToNodeIdTag(networkNodeCount, node, Execution) + targets = append(targets, elId) + + clId := ConvertToNodeIdTag(networkNodeCount, node, Consensus) + targets = append(targets, clId) + + if node.Consensus.HasValidatorSidecar { + valId := ConvertToNodeIdTag(networkNodeCount, node, Validator) + targets = append(targets, valId) + } + + selector := ChaosExpressionSelector{ + Key: "kurtosistech.com/id", + Operator: "In", + Values: targets, + } + + description := fmt.Sprintf("%s/%s Node (Node #%d)", node.Execution.Type, node.Consensus.Type, node.Index) + return &ChaosTargetSelector{ + Selector: []ChaosExpressionSelector{selector}, + Description: description, + } +} + +func createTargetSelectorForExecClient(networkNodeCount int, node *network.Node) *ChaosTargetSelector { + elId := ConvertToNodeIdTag(networkNodeCount, node, Execution) + selector := ChaosExpressionSelector{ + Key: "kurtosistech.com/id", + Operator: "In", + Values: []string{elId}, + } + + description := fmt.Sprintf("%s client of %s/%s Node (Node #%d)", node.Execution.Type, node.Execution.Type, node.Consensus.Type, node.Index) + return &ChaosTargetSelector{ + Selector: []ChaosExpressionSelector{selector}, + Description: description, + } +} + +func createTargetSelectorForConsensusClient(networkNodeCount int, node *network.Node) *ChaosTargetSelector { + var targets []string + clId := ConvertToNodeIdTag(networkNodeCount, node, Consensus) + targets = append(targets, clId) + + if node.Consensus.HasValidatorSidecar { + valId := ConvertToNodeIdTag(networkNodeCount, node, Validator) + targets = append(targets, valId) + } + + selector := ChaosExpressionSelector{ + Key: "kurtosistech.com/id", + Operator: "In", + Values: targets, + } + + description := fmt.Sprintf("%s client of %s/%s Node (Node #%d)", node.Consensus.Type, node.Execution.Type, node.Consensus.Type, node.Index) + return &ChaosTargetSelector{ + Selector: []ChaosExpressionSelector{selector}, + Description: description, + } +} + +func TargetSpecEnumToLambda(targetSelector TargetingSpec, isExecClient bool) (func(networkNodeCount int, node *network.Node) *ChaosTargetSelector, error) { + if targetSelector == TargetMatchingNode { + return createTargetSelectorForNode, nil + } + if targetSelector == TargetMatchingClient { + if isExecClient { + return createTargetSelectorForExecClient, nil + } else { + return createTargetSelectorForConsensusClient, nil + } + } + return nil, stacktrace.NewError("target selector %s not supported", targetSelector) +} + +func filterNodesByExecClient(elClientType string) TargetCriteriaFilter { + return func(size AttackSize, targetableSetSize int, nodes []*network.Node) ([]*network.Node, error) { + criteria := func(n *network.Node) bool { + return n.Execution.Type == elClientType + } + targetableNodes := filterNodes(nodes, criteria) + if targetableNodes == nil { + return nil, stacktrace.NewError("unable to satisfy targeting constraint") + } + return chooseTargetsUsingAttackSize(size, targetableSetSize, targetableNodes) + } +} + +func filterNodesByConsensusClient(clClientType string) TargetCriteriaFilter { + return func(size AttackSize, targetableSetSize int, nodes []*network.Node) ([]*network.Node, error) { + criteria := func(n *network.Node) bool { + return n.Consensus.Type == clClientType + } + targetableNodes := filterNodes(nodes, criteria) + + return chooseTargetsUsingAttackSize(size, targetableSetSize, targetableNodes) + } +} + +func filterNodesByClientCombo(elClientType, clClientType string) TargetCriteriaFilter { + return func(size AttackSize, targetableSetSize int, nodes []*network.Node) ([]*network.Node, error) { + criteria := func(n *network.Node) bool { + return n.Consensus.Type == clClientType && n.Execution.Type == elClientType + } + targetableNodes := filterNodes(nodes, criteria) + + return chooseTargetsUsingAttackSize(size, targetableSetSize, targetableNodes) + } +} + +func BuildChaosMeshTargetSelectors(networkNodeCount int, nodes []*network.Node, size AttackSize, targetCriteria TargetCriteriaFilter, impactSelector NodeImpactSelector) ([]*ChaosTargetSelector, error) { + targets, err := targetCriteria(size, len(nodes)+1, nodes) + if err != nil { + return nil, err + } + + var targetSelectors []*ChaosTargetSelector + for _, node := range targets { + targetSelectors = append(targetSelectors, impactSelector(networkNodeCount, node)) + } + return targetSelectors, nil +} diff --git a/pkg/plan/suite/targeting_test.go b/pkg/plan/suite/targeting_test.go new file mode 100644 index 0000000..a48b477 --- /dev/null +++ b/pkg/plan/suite/targeting_test.go @@ -0,0 +1,116 @@ +package suite + +import ( + "attacknet/cmd/pkg/plan/network" + "testing" +) + +func NewMockNetworkUnconfigured(nodeCount int) []*network.Node { + nodes := make([]*network.Node, nodeCount) + for i := range nodes { + nodes[i] = &network.Node{} + } + return nodes +} + +func TestChooseTargetsUsingAttackSize(t *testing.T) { + type testCase struct { + NetworkSize int + TargetableCount int + AttackSize AttackSize + ExpectedResultSize int + ExpectConstraintError bool + } + + testCases := []testCase{ + // test baseline cases where network size == targetable + { + NetworkSize: 10, + TargetableCount: 10, + AttackSize: AttackAll, + ExpectedResultSize: 10, + }, + { + NetworkSize: 10, + TargetableCount: 10, + AttackSize: AttackOne, + ExpectedResultSize: 1, + }, + { + NetworkSize: 10, + TargetableCount: 10, + AttackSize: AttackMinority, + ExpectedResultSize: 3, + }, + { + NetworkSize: 10, + TargetableCount: 10, + AttackSize: AttackSuperminority, + ExpectedResultSize: 4, + }, + { + NetworkSize: 10, + TargetableCount: 10, + AttackSize: AttackMajority, + ExpectedResultSize: 6, + }, + { + NetworkSize: 10, + TargetableCount: 10, + AttackSize: AttackSupermajority, + ExpectedResultSize: 7, + }, + // test cases where network size != targetable + { + NetworkSize: 10, + TargetableCount: 5, + AttackSize: AttackAll, // special case where not hitting 100% is ok + ExpectedResultSize: 5, + }, + { + NetworkSize: 10, + TargetableCount: 5, + AttackSize: AttackSuperminority, + ExpectedResultSize: 4, + }, + { + NetworkSize: 10, + TargetableCount: 5, + AttackSize: AttackMinority, + ExpectedResultSize: 3, + }, + + // test a few cases that should produce constraint errors + { + NetworkSize: 10, // can only target up to 50% + TargetableCount: 5, + AttackSize: AttackSupermajority, + ExpectedResultSize: -1, + ExpectConstraintError: true, + }, + { + NetworkSize: 4, + TargetableCount: 4, //can only target 50% or 75% + AttackSize: AttackMajority, + ExpectedResultSize: -1, + ExpectConstraintError: true, + }, + } + + for i, test := range testCases { + nodes := NewMockNetworkUnconfigured(test.TargetableCount) + targets, err := chooseTargetsUsingAttackSize(test.AttackSize, test.NetworkSize, nodes) + + if err != nil { + if !test.ExpectConstraintError { + t.Fatalf("Fail case %d, expected err==nil, case %v", i, test) + } else { + continue + } + } + if len(targets) != test.ExpectedResultSize { + t.Fatalf("Fail case %d, expected %d targets selected, received %d. case %v", i, test.ExpectedResultSize, len(targets), test) + } + + } +} diff --git a/pkg/plan/suite/test_builder.go b/pkg/plan/suite/test_builder.go new file mode 100644 index 0000000..712f069 --- /dev/null +++ b/pkg/plan/suite/test_builder.go @@ -0,0 +1,123 @@ +package suite + +import ( + "attacknet/cmd/pkg/types" + "time" +) + +func ComposeNodeClockSkewTest(description string, targets []*ChaosTargetSelector, skew, duration string, graceDuration *time.Duration) (*types.SuiteTest, error) { + var steps []types.PlanStep + s, err := composeNodeClockSkewPlanSteps(targets, skew, duration) + if err != nil { + return nil, err + } + steps = append(steps, s...) + + waitStep := composeWaitForFaultCompletionStep() + steps = append(steps, *waitStep) + + test := &types.SuiteTest{ + TestName: description, + PlanSteps: steps, + HealthConfig: types.HealthCheckConfig{ + EnableChecks: true, + GracePeriod: graceDuration, + }, + } + + return test, nil +} + +func composeNodeRestartTest(description string, targets []*ChaosTargetSelector, graceDuration *time.Duration) (*types.SuiteTest, error) { + var steps []types.PlanStep + + s, err := composeNodeRestartSteps(targets) + if err != nil { + return nil, err + } + steps = append(steps, s...) + + waitStep := composeWaitForFaultCompletionStep() + steps = append(steps, *waitStep) + + test := &types.SuiteTest{ + TestName: description, + PlanSteps: steps, + HealthConfig: types.HealthCheckConfig{ + EnableChecks: true, + GracePeriod: graceDuration, + }, + } + + return test, nil +} + +func composeIOLatencyTest(description string, targets []*ChaosTargetSelector, delay *time.Duration, percent int, duration *time.Duration, graceDuration *time.Duration) (*types.SuiteTest, error) { + var steps []types.PlanStep + + s, err := composeIOLatencySteps(targets, delay, percent, duration) + if err != nil { + return nil, err + } + steps = append(steps, s...) + + waitStep := composeWaitForFaultCompletionStep() + steps = append(steps, *waitStep) + + test := &types.SuiteTest{ + TestName: description, + PlanSteps: steps, + HealthConfig: types.HealthCheckConfig{ + EnableChecks: true, + GracePeriod: graceDuration, + }, + } + + return test, nil +} + +func ComposeNetworkLatencyTest(description string, targets []*ChaosTargetSelector, delay, jitter, duration, grace *time.Duration, correlation int) (*types.SuiteTest, error) { + var steps []types.PlanStep + s, err := composeNetworkLatencySteps(targets, delay, jitter, duration, correlation) + if err != nil { + return nil, err + } + steps = append(steps, s...) + + waitStep := composeWaitForFaultCompletionStep() + steps = append(steps, *waitStep) + + test := &types.SuiteTest{ + TestName: description, + PlanSteps: steps, + HealthConfig: types.HealthCheckConfig{ + EnableChecks: true, + GracePeriod: grace, + }, + } + + return test, nil +} + +func ComposePacketDropTest(description string, targets []*ChaosTargetSelector, percent int, direction string, duration, grace *time.Duration) (*types.SuiteTest, error) { + var steps []types.PlanStep + s, err := composePacketDropSteps(targets, percent, direction, duration) + if err != nil { + return nil, err + } + steps = append(steps, s...) + + waitStep := composeWaitForFaultCompletionStep() + steps = append(steps, *waitStep) + + test := &types.SuiteTest{ + TestName: description, + PlanSteps: steps, + HealthConfig: types.HealthCheckConfig{ + EnableChecks: true, + GracePeriod: grace, + }, + } + + return test, nil +} diff --git a/pkg/plan/suite/types.go b/pkg/plan/suite/types.go new file mode 100644 index 0000000..906fc24 --- /dev/null +++ b/pkg/plan/suite/types.go @@ -0,0 +1,85 @@ +package suite + +import "time" + +type TargetingSpec string + +const ( + TargetMatchingNode TargetingSpec = "MatchingNode" + TargetMatchingClient TargetingSpec = "MatchingClient" +) + +var TargetingSpecs = map[TargetingSpec]bool{ + TargetMatchingNode: true, + TargetMatchingClient: true, +} + +var TargetingSpecList = []TargetingSpec{ + TargetMatchingNode, + TargetMatchingClient, +} + +type AttackSize string + +const ( + AttackOne AttackSize = "AttackOneMatching" + AttackAll AttackSize = "AttackAllMatching" + AttackMinority AttackSize = "AttackMinorityMatching" // scope will be 0