diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e72bfdd
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 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 General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is 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. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ 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.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ 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 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. Use with the GNU Affero General Public License.
+
+ 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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 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 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 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 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ 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 GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
\ No newline at end of file
diff --git a/discover/common.go b/discover/common.go
new file mode 100644
index 0000000..597a031
--- /dev/null
+++ b/discover/common.go
@@ -0,0 +1,83 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "crypto/ecdsa"
+ "net"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/p2p/netutil"
+)
+
+// UDPConn is a network connection on which discovery can operate.
+type UDPConn interface {
+ ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)
+ WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error)
+ Close() error
+ LocalAddr() net.Addr
+}
+
+// Config holds settings for the discovery listener.
+type Config struct {
+ // These settings are required and configure the UDP listener:
+ PrivateKey *ecdsa.PrivateKey
+
+ // These settings are optional:
+ NetRestrict *netutil.Netlist // list of allowed IP networks
+ Bootnodes []*enode.Node // list of bootstrap nodes
+ Unhandled chan<- ReadPacket // unhandled packets are sent on this channel
+ Log log.Logger // if set, log messages go here
+ ValidSchemes enr.IdentityScheme // allowed identity schemes
+ Clock mclock.Clock
+ ValidNodeFn func(enode.Node) bool // function to validate a node before it's added to routing tables
+}
+
+func (cfg Config) withDefaults() Config {
+ if cfg.Log == nil {
+ cfg.Log = log.Root()
+ }
+ if cfg.ValidSchemes == nil {
+ cfg.ValidSchemes = enode.ValidSchemes
+ }
+ if cfg.Clock == nil {
+ cfg.Clock = mclock.System{}
+ }
+ return cfg
+}
+
+// ListenUDP starts listening for discovery packets on the given UDP socket.
+func ListenUDP(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
+ return ListenV4(c, ln, cfg)
+}
+
+// ReadPacket is a packet that couldn't be handled. Those packets are sent to the unhandled
+// channel if configured.
+type ReadPacket struct {
+ Data []byte
+ Addr *net.UDPAddr
+}
+
+func min(x, y int) int {
+ if x > y {
+ return y
+ }
+ return x
+}
diff --git a/discover/lookup.go b/discover/lookup.go
new file mode 100644
index 0000000..9ab4a71
--- /dev/null
+++ b/discover/lookup.go
@@ -0,0 +1,226 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "context"
+ "time"
+
+ "github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+// lookup performs a network search for nodes close to the given target. It approaches the
+// target by querying nodes that are closer to it on each iteration. The given target does
+// not need to be an actual node identifier.
+type lookup struct {
+ tab *Table
+ queryfunc func(*node) ([]*node, error)
+ replyCh chan []*node
+ cancelCh <-chan struct{}
+ asked, seen map[enode.ID]bool
+ result nodesByDistance
+ replyBuffer []*node
+ queries int
+}
+
+type queryFunc func(*node) ([]*node, error)
+
+func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *lookup {
+ it := &lookup{
+ tab: tab,
+ queryfunc: q,
+ asked: make(map[enode.ID]bool),
+ seen: make(map[enode.ID]bool),
+ result: nodesByDistance{target: target},
+ replyCh: make(chan []*node, alpha),
+ cancelCh: ctx.Done(),
+ queries: -1,
+ }
+ // Don't query further if we hit ourself.
+ // Unlikely to happen often in practice.
+ it.asked[tab.self().ID()] = true
+ return it
+}
+
+// run runs the lookup to completion and returns the closest nodes found.
+func (it *lookup) run() []*enode.Node {
+ for it.advance() {
+ }
+ return unwrapNodes(it.result.entries)
+}
+
+// advance advances the lookup until any new nodes have been found.
+// It returns false when the lookup has ended.
+func (it *lookup) advance() bool {
+ for it.startQueries() {
+ select {
+ case nodes := <-it.replyCh:
+ it.replyBuffer = it.replyBuffer[:0]
+ for _, n := range nodes {
+ if n != nil && !it.seen[n.ID()] {
+ it.seen[n.ID()] = true
+ it.result.push(n, bucketSize)
+ it.replyBuffer = append(it.replyBuffer, n)
+ }
+ }
+ it.queries--
+ if len(it.replyBuffer) > 0 {
+ return true
+ }
+ case <-it.cancelCh:
+ it.shutdown()
+ }
+ }
+ return false
+}
+
+func (it *lookup) shutdown() {
+ for it.queries > 0 {
+ <-it.replyCh
+ it.queries--
+ }
+ it.queryfunc = nil
+ it.replyBuffer = nil
+}
+
+func (it *lookup) startQueries() bool {
+ if it.queryfunc == nil {
+ return false
+ }
+
+ // The first query returns nodes from the local table.
+ if it.queries == -1 {
+ closest := it.tab.findnodeByID(it.result.target, bucketSize, false)
+ // Avoid finishing the lookup too quickly if table is empty. It'd be better to wait
+ // for the table to fill in this case, but there is no good mechanism for that
+ // yet.
+ if len(closest.entries) == 0 {
+ it.slowdown()
+ }
+ it.queries = 1
+ it.replyCh <- closest.entries
+ return true
+ }
+
+ // Ask the closest nodes that we haven't asked yet.
+ for i := 0; i < len(it.result.entries) && it.queries < alpha; i++ {
+ n := it.result.entries[i]
+ if !it.asked[n.ID()] {
+ it.asked[n.ID()] = true
+ it.queries++
+ go it.query(n, it.replyCh)
+ }
+ }
+ // The lookup ends when no more nodes can be asked.
+ return it.queries > 0
+}
+
+func (it *lookup) slowdown() {
+ sleep := time.NewTimer(1 * time.Second)
+ defer sleep.Stop()
+ select {
+ case <-sleep.C:
+ case <-it.tab.closeReq:
+ }
+}
+
+func (it *lookup) query(n *node, reply chan<- []*node) {
+ fails := it.tab.db.FindFails(n.ID(), n.IP())
+ r, err := it.queryfunc(n)
+ if err == errClosed {
+ // Avoid recording failures on shutdown.
+ reply <- nil
+ return
+ } else if len(r) == 0 {
+ fails++
+ it.tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
+ // Remove the node from the local table if it fails to return anything useful too
+ // many times, but only if there are enough other nodes in the bucket.
+ dropped := false
+ if fails >= maxFindnodeFailures && it.tab.bucketLen(n.ID()) >= bucketSize/2 {
+ dropped = true
+ it.tab.delete(n)
+ }
+ it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "failcount", fails, "dropped", dropped, "err", err)
+ } else if fails > 0 {
+ // Reset failure counter because it counts _consecutive_ failures.
+ it.tab.db.UpdateFindFails(n.ID(), n.IP(), 0)
+ }
+
+ // Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
+ // just remove those again during revalidation.
+ for _, n := range r {
+ it.tab.addSeenNode(n)
+ }
+ reply <- r
+}
+
+// lookupIterator performs lookup operations and iterates over all seen nodes.
+// When a lookup finishes, a new one is created through nextLookup.
+type lookupIterator struct {
+ buffer []*node
+ nextLookup lookupFunc
+ ctx context.Context
+ cancel func()
+ lookup *lookup
+}
+
+type lookupFunc func(ctx context.Context) *lookup
+
+func newLookupIterator(ctx context.Context, next lookupFunc) *lookupIterator {
+ ctx, cancel := context.WithCancel(ctx)
+ return &lookupIterator{ctx: ctx, cancel: cancel, nextLookup: next}
+}
+
+// Node returns the current node.
+func (it *lookupIterator) Node() *enode.Node {
+ if len(it.buffer) == 0 {
+ return nil
+ }
+ return unwrapNode(it.buffer[0])
+}
+
+// Next moves to the next node.
+func (it *lookupIterator) Next() bool {
+ // Consume next node in buffer.
+ if len(it.buffer) > 0 {
+ it.buffer = it.buffer[1:]
+ }
+ // Advance the lookup to refill the buffer.
+ for len(it.buffer) == 0 {
+ if it.ctx.Err() != nil {
+ it.lookup = nil
+ it.buffer = nil
+ return false
+ }
+ if it.lookup == nil {
+ it.lookup = it.nextLookup(it.ctx)
+ continue
+ }
+ if !it.lookup.advance() {
+ it.lookup = nil
+ continue
+ }
+ it.buffer = it.lookup.replyBuffer
+ }
+ return true
+}
+
+// Close ends the iterator.
+func (it *lookupIterator) Close() {
+ it.cancel()
+}
diff --git a/discover/node.go b/discover/node.go
new file mode 100644
index 0000000..9ffe101
--- /dev/null
+++ b/discover/node.go
@@ -0,0 +1,97 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "errors"
+ "math/big"
+ "net"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+// node represents a host on the network.
+// The fields of Node may not be modified.
+type node struct {
+ enode.Node
+ addedAt time.Time // time when the node was added to the table
+ livenessChecks uint // how often liveness was checked
+}
+
+type encPubkey [64]byte
+
+func encodePubkey(key *ecdsa.PublicKey) encPubkey {
+ var e encPubkey
+ math.ReadBits(key.X, e[:len(e)/2])
+ math.ReadBits(key.Y, e[len(e)/2:])
+ return e
+}
+
+func decodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) {
+ if len(e) != len(encPubkey{}) {
+ return nil, errors.New("wrong size public key data")
+ }
+ p := &ecdsa.PublicKey{Curve: curve, X: new(big.Int), Y: new(big.Int)}
+ half := len(e) / 2
+ p.X.SetBytes(e[:half])
+ p.Y.SetBytes(e[half:])
+ if !p.Curve.IsOnCurve(p.X, p.Y) {
+ return nil, errors.New("invalid curve point")
+ }
+ return p, nil
+}
+
+func (e encPubkey) id() enode.ID {
+ return enode.ID(crypto.Keccak256Hash(e[:]))
+}
+
+func wrapNode(n *enode.Node) *node {
+ return &node{Node: *n}
+}
+
+func wrapNodes(ns []*enode.Node) []*node {
+ result := make([]*node, len(ns))
+ for i, n := range ns {
+ result[i] = wrapNode(n)
+ }
+ return result
+}
+
+func unwrapNode(n *node) *enode.Node {
+ return &n.Node
+}
+
+func unwrapNodes(ns []*node) []*enode.Node {
+ result := make([]*enode.Node, len(ns))
+ for i, n := range ns {
+ result[i] = unwrapNode(n)
+ }
+ return result
+}
+
+func (n *node) addr() *net.UDPAddr {
+ return &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
+}
+
+func (n *node) String() string {
+ return n.Node.String()
+}
diff --git a/discover/ntp.go b/discover/ntp.go
new file mode 100644
index 0000000..1bb5239
--- /dev/null
+++ b/discover/ntp.go
@@ -0,0 +1,119 @@
+// Copyright 2016 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Contains the NTP time drift detection via the SNTP protocol:
+// https://tools.ietf.org/html/rfc4330
+
+package discover
+
+import (
+ "fmt"
+ "net"
+ "sort"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+const (
+ ntpPool = "pool.ntp.org" // ntpPool is the NTP server to query for the current time
+ ntpChecks = 3 // Number of measurements to do against the NTP server
+)
+
+// durationSlice attaches the methods of sort.Interface to []time.Duration,
+// sorting in increasing order.
+type durationSlice []time.Duration
+
+func (s durationSlice) Len() int { return len(s) }
+func (s durationSlice) Less(i, j int) bool { return s[i] < s[j] }
+func (s durationSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+// checkClockDrift queries an NTP server for clock drifts and warns the user if
+// one large enough is detected.
+func checkClockDrift() {
+ drift, err := sntpDrift(ntpChecks)
+ if err != nil {
+ return
+ }
+ if drift < -driftThreshold || drift > driftThreshold {
+ log.Warn(fmt.Sprintf("System clock seems off by %v, which can prevent network connectivity", drift))
+ log.Warn("Please enable network time synchronisation in system settings.")
+ } else {
+ log.Debug("NTP sanity check done", "drift", drift)
+ }
+}
+
+// sntpDrift does a naive time resolution against an NTP server and returns the
+// measured drift. This method uses the simple version of NTP. It's not precise
+// but should be fine for these purposes.
+//
+// Note, it executes two extra measurements compared to the number of requested
+// ones to be able to discard the two extremes as outliers.
+func sntpDrift(measurements int) (time.Duration, error) {
+ // Resolve the address of the NTP server
+ addr, err := net.ResolveUDPAddr("udp", ntpPool+":123")
+ if err != nil {
+ return 0, err
+ }
+ // Construct the time request (empty package with only 2 fields set):
+ // Bits 3-5: Protocol version, 3
+ // Bits 6-8: Mode of operation, client, 3
+ request := make([]byte, 48)
+ request[0] = 3<<3 | 3
+
+ // Execute each of the measurements
+ drifts := []time.Duration{}
+ for i := 0; i < measurements+2; i++ {
+ // Dial the NTP server and send the time retrieval request
+ conn, err := net.DialUDP("udp", nil, addr)
+ if err != nil {
+ return 0, err
+ }
+ defer conn.Close()
+
+ sent := time.Now()
+ if _, err = conn.Write(request); err != nil {
+ return 0, err
+ }
+ // Retrieve the reply and calculate the elapsed time
+ conn.SetDeadline(time.Now().Add(5 * time.Second))
+
+ reply := make([]byte, 48)
+ if _, err = conn.Read(reply); err != nil {
+ return 0, err
+ }
+ elapsed := time.Since(sent)
+
+ // Reconstruct the time from the reply data
+ sec := uint64(reply[43]) | uint64(reply[42])<<8 | uint64(reply[41])<<16 | uint64(reply[40])<<24
+ frac := uint64(reply[47]) | uint64(reply[46])<<8 | uint64(reply[45])<<16 | uint64(reply[44])<<24
+
+ nanosec := sec*1e9 + (frac*1e9)>>32
+
+ t := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Duration(nanosec)).Local()
+
+ // Calculate the drift based on an assumed answer time of RRT/2
+ drifts = append(drifts, sent.Sub(t)+elapsed/2)
+ }
+ // Calculate average drif (drop two extremities to avoid outliers)
+ sort.Sort(durationSlice(drifts))
+
+ drift := time.Duration(0)
+ for i := 1; i < len(drifts)-1; i++ {
+ drift += drifts[i]
+ }
+ return drift / time.Duration(measurements), nil
+}
diff --git a/discover/table.go b/discover/table.go
new file mode 100644
index 0000000..2f61d99
--- /dev/null
+++ b/discover/table.go
@@ -0,0 +1,693 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package discover implements the Node Discovery Protocol.
+//
+// The Node Discovery protocol provides a way to find RLPx nodes that
+// can be connected to. It uses a Kademlia-like protocol to maintain a
+// distributed database of the IDs and endpoints of all listening
+// nodes.
+package discover
+
+import (
+ crand "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ mrand "math/rand"
+ "net"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/netutil"
+)
+
+const (
+ alpha = 3 // Kademlia concurrency factor
+ bucketSize = 16 // Kademlia bucket size
+ maxReplacements = 10 // Size of per-bucket replacement list
+
+ // We keep buckets for the upper 1/15 of distances because
+ // it's very unlikely we'll ever encounter a node that's closer.
+ hashBits = len(common.Hash{}) * 8
+ nBuckets = hashBits / 15 // Number of buckets
+ bucketMinDistance = hashBits - nBuckets // Log distance of closest bucket
+
+ // IP address limits.
+ bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
+ tableIPLimit, tableSubnet = 10, 24
+
+ refreshInterval = 30 * time.Minute
+ revalidateInterval = 10 * time.Second
+ copyNodesInterval = 30 * time.Second
+ seedMinTableTime = 5 * time.Minute
+ seedCount = 30
+ seedMaxAge = 5 * 24 * time.Hour
+)
+
+// Table is the 'node table', a Kademlia-like index of neighbor nodes. The table keeps
+// itself up-to-date by verifying the liveness of neighbors and requesting their node
+// records when announcements of a new record version are received.
+type Table struct {
+ mutex sync.Mutex // protects buckets, bucket content, nursery, rand
+ buckets [nBuckets]*bucket // index of known nodes by distance
+ nursery []*node // bootstrap nodes
+ rand *mrand.Rand // source of randomness, periodically reseeded
+ ips netutil.DistinctNetSet
+
+ log log.Logger
+ db *enode.DB // database of known nodes
+ net transport
+ refreshReq chan chan struct{}
+ initDone chan struct{}
+ closeReq chan struct{}
+ closed chan struct{}
+
+ nodeAddedHook func(*node) // for testing
+}
+
+// transport is implemented by the UDP transports.
+type transport interface {
+ Self() *enode.Node
+ RequestENR(*enode.Node) (*enode.Node, error)
+ lookupRandom() []*enode.Node
+ lookupSelf() []*enode.Node
+ ping(*enode.Node) (seq uint64, err error)
+}
+
+// bucket contains nodes, ordered by their last activity. the entry
+// that was most recently active is the first element in entries.
+type bucket struct {
+ entries []*node // live entries, sorted by time of last contact
+ replacements []*node // recently seen nodes to be used if revalidation fails
+ ips netutil.DistinctNetSet
+}
+
+func newTable(t transport, db *enode.DB, bootnodes []*enode.Node, log log.Logger) (*Table, error) {
+ tab := &Table{
+ net: t,
+ db: db,
+ refreshReq: make(chan chan struct{}),
+ initDone: make(chan struct{}),
+ closeReq: make(chan struct{}),
+ closed: make(chan struct{}),
+ rand: mrand.New(mrand.NewSource(0)),
+ ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
+ log: log,
+ }
+ if err := tab.setFallbackNodes(bootnodes); err != nil {
+ return nil, err
+ }
+ for i := range tab.buckets {
+ tab.buckets[i] = &bucket{
+ ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
+ }
+ }
+ tab.seedRand()
+ tab.loadSeedNodes()
+
+ return tab, nil
+}
+
+func (tab *Table) self() *enode.Node {
+ return tab.net.Self()
+}
+
+func (tab *Table) seedRand() {
+ var b [8]byte
+ crand.Read(b[:])
+
+ tab.mutex.Lock()
+ tab.rand.Seed(int64(binary.BigEndian.Uint64(b[:])))
+ tab.mutex.Unlock()
+}
+
+// ReadRandomNodes fills the given slice with random nodes from the table. The results
+// are guaranteed to be unique for a single invocation, no node will appear twice.
+func (tab *Table) ReadRandomNodes(buf []*enode.Node) (n int) {
+ if !tab.isInitDone() {
+ return 0
+ }
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ var nodes []*enode.Node
+ for _, b := range &tab.buckets {
+ for _, n := range b.entries {
+ nodes = append(nodes, unwrapNode(n))
+ }
+ }
+ // Shuffle.
+ for i := 0; i < len(nodes); i++ {
+ j := tab.rand.Intn(len(nodes))
+ nodes[i], nodes[j] = nodes[j], nodes[i]
+ }
+ return copy(buf, nodes)
+}
+
+// getNode returns the node with the given ID or nil if it isn't in the table.
+func (tab *Table) getNode(id enode.ID) *enode.Node {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ b := tab.bucket(id)
+ for _, e := range b.entries {
+ if e.ID() == id {
+ return unwrapNode(e)
+ }
+ }
+ return nil
+}
+
+// close terminates the network listener and flushes the node database.
+func (tab *Table) close() {
+ close(tab.closeReq)
+ <-tab.closed
+}
+
+// setFallbackNodes sets the initial points of contact. These nodes
+// are used to connect to the network if the table is empty and there
+// are no known nodes in the database.
+func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
+ for _, n := range nodes {
+ if err := n.ValidateComplete(); err != nil {
+ return fmt.Errorf("bad bootstrap node %q: %v", n, err)
+ }
+ }
+ tab.nursery = wrapNodes(nodes)
+ return nil
+}
+
+// isInitDone returns whether the table's initial seeding procedure has completed.
+func (tab *Table) isInitDone() bool {
+ select {
+ case <-tab.initDone:
+ return true
+ default:
+ return false
+ }
+}
+
+func (tab *Table) refresh() <-chan struct{} {
+ done := make(chan struct{})
+ select {
+ case tab.refreshReq <- done:
+ case <-tab.closeReq:
+ close(done)
+ }
+ return done
+}
+
+// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes.
+func (tab *Table) loop() {
+ var (
+ revalidate = time.NewTimer(tab.nextRevalidateTime())
+ refresh = time.NewTicker(refreshInterval)
+ copyNodes = time.NewTicker(copyNodesInterval)
+ refreshDone = make(chan struct{}) // where doRefresh reports completion
+ revalidateDone chan struct{} // where doRevalidate reports completion
+ waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs
+ )
+ defer refresh.Stop()
+ defer revalidate.Stop()
+ defer copyNodes.Stop()
+
+ // Start initial refresh.
+ go tab.doRefresh(refreshDone)
+
+loop:
+ for {
+ select {
+ case <-refresh.C:
+ tab.seedRand()
+ if refreshDone == nil {
+ refreshDone = make(chan struct{})
+ go tab.doRefresh(refreshDone)
+ }
+ case req := <-tab.refreshReq:
+ waiting = append(waiting, req)
+ if refreshDone == nil {
+ refreshDone = make(chan struct{})
+ go tab.doRefresh(refreshDone)
+ }
+ case <-refreshDone:
+ for _, ch := range waiting {
+ close(ch)
+ }
+ waiting, refreshDone = nil, nil
+ case <-revalidate.C:
+ revalidateDone = make(chan struct{})
+ go tab.doRevalidate(revalidateDone)
+ case <-revalidateDone:
+ revalidate.Reset(tab.nextRevalidateTime())
+ revalidateDone = nil
+ case <-copyNodes.C:
+ go tab.copyLiveNodes()
+ case <-tab.closeReq:
+ break loop
+ }
+ }
+
+ if refreshDone != nil {
+ <-refreshDone
+ }
+ for _, ch := range waiting {
+ close(ch)
+ }
+ if revalidateDone != nil {
+ <-revalidateDone
+ }
+ close(tab.closed)
+}
+
+// doRefresh performs a lookup for a random target to keep buckets full. seed nodes are
+// inserted if the table is empty (initial bootstrap or discarded faulty peers).
+func (tab *Table) doRefresh(done chan struct{}) {
+ defer close(done)
+
+ // Load nodes from the database and insert
+ // them. This should yield a few previously seen nodes that are
+ // (hopefully) still alive.
+ tab.loadSeedNodes()
+
+ // Run self lookup to discover new neighbor nodes.
+ tab.net.lookupSelf()
+
+ // The Kademlia paper specifies that the bucket refresh should
+ // perform a lookup in the least recently used bucket. We cannot
+ // adhere to this because the findnode target is a 512bit value
+ // (not hash-sized) and it is not easily possible to generate a
+ // sha3 preimage that falls into a chosen bucket.
+ // We perform a few lookups with a random target instead.
+ for i := 0; i < 3; i++ {
+ tab.net.lookupRandom()
+ }
+}
+
+func (tab *Table) loadSeedNodes() {
+ seeds := wrapNodes(tab.db.QuerySeeds(seedCount, seedMaxAge))
+ seeds = append(seeds, tab.nursery...)
+ for i := range seeds {
+ seed := seeds[i]
+ age := log.Lazy{Fn: func() interface{} { return time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP())) }}
+ tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age)
+ tab.addSeenNode(seed)
+ }
+}
+
+// doRevalidate checks that the last node in a random bucket is still live and replaces or
+// deletes the node if it isn't.
+func (tab *Table) doRevalidate(done chan<- struct{}) {
+ defer func() { done <- struct{}{} }()
+
+ last, bi := tab.nodeToRevalidate()
+ if last == nil {
+ // No non-empty bucket found.
+ return
+ }
+
+ // Ping the selected node and wait for a pong.
+ remoteSeq, err := tab.net.ping(unwrapNode(last))
+
+ // Also fetch record if the node replied and returned a higher sequence number.
+ if last.Seq() < remoteSeq {
+ n, err := tab.net.RequestENR(unwrapNode(last))
+ if err != nil {
+ tab.log.Debug("ENR request failed", "id", last.ID(), "addr", last.addr(), "err", err)
+ } else {
+ last = &node{Node: *n, addedAt: last.addedAt, livenessChecks: last.livenessChecks}
+ }
+ }
+
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+ b := tab.buckets[bi]
+ if err == nil {
+ // The node responded, move it to the front.
+ last.livenessChecks++
+ tab.log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
+ tab.bumpInBucket(b, last)
+ return
+ }
+ // No reply received, pick a replacement or delete the node if there aren't
+ // any replacements.
+ if r := tab.replace(b, last); r != nil {
+ tab.log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
+ } else {
+ tab.log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
+ }
+}
+
+// nodeToRevalidate returns the last node in a random, non-empty bucket.
+func (tab *Table) nodeToRevalidate() (n *node, bi int) {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ for _, bi = range tab.rand.Perm(len(tab.buckets)) {
+ b := tab.buckets[bi]
+ if len(b.entries) > 0 {
+ last := b.entries[len(b.entries)-1]
+ return last, bi
+ }
+ }
+ return nil, 0
+}
+
+func (tab *Table) nextRevalidateTime() time.Duration {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ return time.Duration(tab.rand.Int63n(int64(revalidateInterval)))
+}
+
+// copyLiveNodes adds nodes from the table to the database if they have been in the table
+// longer than seedMinTableTime.
+func (tab *Table) copyLiveNodes() {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ now := time.Now()
+ for _, b := range &tab.buckets {
+ for _, n := range b.entries {
+ if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
+ tab.db.UpdateNode(unwrapNode(n))
+ }
+ }
+ }
+}
+
+// findnodeByID returns the n nodes in the table that are closest to the given id.
+// This is used by the FINDNODE/v4 handler.
+//
+// The preferLive parameter says whether the caller wants liveness-checked results. If
+// preferLive is true and the table contains any verified nodes, the result will not
+// contain unverified nodes. However, if there are no verified nodes at all, the result
+// will contain unverified nodes.
+func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ // Scan all buckets. There might be a better way to do this, but there aren't that many
+ // buckets, so this solution should be fine. The worst-case complexity of this loop
+ // is O(tab.len() * nresults).
+ nodes := &nodesByDistance{target: target}
+ liveNodes := &nodesByDistance{target: target}
+ for _, b := range &tab.buckets {
+ for _, n := range b.entries {
+ nodes.push(n, nresults)
+ if preferLive && n.livenessChecks > 0 {
+ liveNodes.push(n, nresults)
+ }
+ }
+ }
+
+ if preferLive && len(liveNodes.entries) > 0 {
+ return liveNodes
+ }
+ return nodes
+}
+
+// len returns the number of nodes in the table.
+func (tab *Table) len() (n int) {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ for _, b := range &tab.buckets {
+ n += len(b.entries)
+ }
+ return n
+}
+
+// bucketLen returns the number of nodes in the bucket for the given ID.
+func (tab *Table) bucketLen(id enode.ID) int {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ return len(tab.bucket(id).entries)
+}
+
+// bucket returns the bucket for the given node ID hash.
+func (tab *Table) bucket(id enode.ID) *bucket {
+ d := enode.LogDist(tab.self().ID(), id)
+ return tab.bucketAtDistance(d)
+}
+
+func (tab *Table) bucketAtDistance(d int) *bucket {
+ if d <= bucketMinDistance {
+ return tab.buckets[0]
+ }
+ return tab.buckets[d-bucketMinDistance-1]
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+// addSeenNode adds a node which may or may not be live to the end of a bucket. If the
+// bucket has space available, adding the node succeeds immediately. Otherwise, the node is
+// added to the replacements list.
+//
+// The caller must not hold tab.mutex.
+func (tab *Table) addSeenNode(n *node) {
+ if n.ID() == tab.self().ID() {
+ return
+ }
+
+ if tab.nodeIsValidFn != nil && !tab.nodeIsValidFn(n.Node) {
+ return
+ }
+
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+ b := tab.bucket(n.ID())
+ if contains(b.entries, n.ID()) {
+ // Already in bucket, don't add.
+ return
+ }
+ if len(b.entries) >= bucketSize {
+ // Bucket full, maybe add as replacement.
+ tab.addReplacement(b, n)
+ return
+ }
+ if !tab.addIP(b, n.IP()) {
+ // Can't add: IP limit reached.
+ return
+ }
+ // Add to end of bucket:
+ b.entries = append(b.entries, n)
+ b.replacements = deleteNode(b.replacements, n)
+ n.addedAt = time.Now()
+ if tab.nodeAddedHook != nil {
+ tab.nodeAddedHook(n)
+ }
+}
+
+/////////////////////////////////////////////////////////////
+// addVerifiedNode adds a node whose existence has been verified recently to the front of a
+// bucket. If the node is already in the bucket, it is moved to the front. If the bucket
+// has no space, the node is added to the replacements list.
+//
+// There is an additional safety measure: if the table is still initializing the node
+// is not added. This prevents an attack where the table could be filled by just sending
+// ping repeatedly.
+//
+// The caller must not hold tab.mutex.
+func (tab *Table) addVerifiedNode(n *node) {
+ if !tab.isInitDone() {
+ return
+ }
+ if n.ID() == tab.self().ID() {
+ return
+ }
+
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+ b := tab.bucket(n.ID())
+ if tab.bumpInBucket(b, n) {
+ // Already in bucket, moved to front.
+ return
+ }
+ if len(b.entries) >= bucketSize {
+ // Bucket full, maybe add as replacement.
+ tab.addReplacement(b, n)
+ return
+ }
+ if !tab.addIP(b, n.IP()) {
+ // Can't add: IP limit reached.
+ return
+ }
+ // Add to front of bucket.
+ b.entries, _ = pushNode(b.entries, n, bucketSize)
+ b.replacements = deleteNode(b.replacements, n)
+ n.addedAt = time.Now()
+ if tab.nodeAddedHook != nil {
+ tab.nodeAddedHook(n)
+ }
+}
+
+// delete removes an entry from the node table. It is used to evacuate dead nodes.
+func (tab *Table) delete(node *node) {
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+
+ tab.deleteInBucket(tab.bucket(node.ID()), node)
+}
+
+func (tab *Table) addIP(b *bucket, ip net.IP) bool {
+ if len(ip) == 0 {
+ return false // Nodes without IP cannot be added.
+ }
+ if netutil.IsLAN(ip) {
+ return true
+ }
+ if !tab.ips.Add(ip) {
+ tab.log.Debug("IP exceeds table limit", "ip", ip)
+ return false
+ }
+ if !b.ips.Add(ip) {
+ tab.log.Debug("IP exceeds bucket limit", "ip", ip)
+ tab.ips.Remove(ip)
+ return false
+ }
+ return true
+}
+
+func (tab *Table) removeIP(b *bucket, ip net.IP) {
+ if netutil.IsLAN(ip) {
+ return
+ }
+ tab.ips.Remove(ip)
+ b.ips.Remove(ip)
+}
+
+func (tab *Table) addReplacement(b *bucket, n *node) {
+ for _, e := range b.replacements {
+ if e.ID() == n.ID() {
+ return // already in list
+ }
+ }
+ if !tab.addIP(b, n.IP()) {
+ return
+ }
+ var removed *node
+ b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
+ if removed != nil {
+ tab.removeIP(b, removed.IP())
+ }
+}
+
+// replace removes n from the replacement list and replaces 'last' with it if it is the
+// last entry in the bucket. If 'last' isn't the last entry, it has either been replaced
+// with someone else or became active.
+func (tab *Table) replace(b *bucket, last *node) *node {
+ if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID() != last.ID() {
+ // Entry has moved, don't replace it.
+ return nil
+ }
+ // Still the last entry.
+ if len(b.replacements) == 0 {
+ tab.deleteInBucket(b, last)
+ return nil
+ }
+ r := b.replacements[tab.rand.Intn(len(b.replacements))]
+ b.replacements = deleteNode(b.replacements, r)
+ b.entries[len(b.entries)-1] = r
+ tab.removeIP(b, last.IP())
+ return r
+}
+
+// bumpInBucket moves the given node to the front of the bucket entry list
+// if it is contained in that list.
+func (tab *Table) bumpInBucket(b *bucket, n *node) bool {
+ for i := range b.entries {
+ if b.entries[i].ID() == n.ID() {
+ if !n.IP().Equal(b.entries[i].IP()) {
+ // Endpoint has changed, ensure that the new IP fits into table limits.
+ tab.removeIP(b, b.entries[i].IP())
+ if !tab.addIP(b, n.IP()) {
+ // It doesn't, put the previous one back.
+ tab.addIP(b, b.entries[i].IP())
+ return false
+ }
+ }
+ // Move it to the front.
+ copy(b.entries[1:], b.entries[:i])
+ b.entries[0] = n
+ return true
+ }
+ }
+ return false
+}
+
+func (tab *Table) deleteInBucket(b *bucket, n *node) {
+ b.entries = deleteNode(b.entries, n)
+ tab.removeIP(b, n.IP())
+}
+
+func contains(ns []*node, id enode.ID) bool {
+ for _, n := range ns {
+ if n.ID() == id {
+ return true
+ }
+ }
+ return false
+}
+
+// pushNode adds n to the front of list, keeping at most max items.
+func pushNode(list []*node, n *node, max int) ([]*node, *node) {
+ if len(list) < max {
+ list = append(list, nil)
+ }
+ removed := list[len(list)-1]
+ copy(list[1:], list)
+ list[0] = n
+ return list, removed
+}
+
+// deleteNode removes n from list.
+func deleteNode(list []*node, n *node) []*node {
+ for i := range list {
+ if list[i].ID() == n.ID() {
+ return append(list[:i], list[i+1:]...)
+ }
+ }
+ return list
+}
+
+// nodesByDistance is a list of nodes, ordered by distance to target.
+type nodesByDistance struct {
+ entries []*node
+ target enode.ID
+}
+
+// push adds the given node to the list, keeping the total size below maxElems.
+func (h *nodesByDistance) push(n *node, maxElems int) {
+ ix := sort.Search(len(h.entries), func(i int) bool {
+ return enode.DistCmp(h.target, h.entries[i].ID(), n.ID()) > 0
+ })
+ if len(h.entries) < maxElems {
+ h.entries = append(h.entries, n)
+ }
+ if ix == len(h.entries) {
+ // farther away than all nodes we already have.
+ // if there was room for it, the node is now the last element.
+ } else {
+ // slide existing entries down to make room
+ // this will overwrite the entry we just appended.
+ copy(h.entries[ix+1:], h.entries[ix:])
+ h.entries[ix] = n
+ }
+}
diff --git a/discover/table_test.go b/discover/table_test.go
new file mode 100644
index 0000000..5f40c96
--- /dev/null
+++ b/discover/table_test.go
@@ -0,0 +1,428 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "math/rand"
+
+ "net"
+ "reflect"
+ "testing"
+ "testing/quick"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/p2p/netutil"
+)
+
+func TestTable_pingReplace(t *testing.T) {
+ run := func(newNodeResponding, lastInBucketResponding bool) {
+ name := fmt.Sprintf("newNodeResponding=%t/lastInBucketResponding=%t", newNodeResponding, lastInBucketResponding)
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ testPingReplace(t, newNodeResponding, lastInBucketResponding)
+ })
+ }
+
+ run(true, true)
+ run(false, true)
+ run(true, false)
+ run(false, false)
+}
+
+func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport)
+ defer db.Close()
+ defer tab.close()
+
+ <-tab.initDone
+
+ // Fill up the sender's bucket.
+ pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
+ pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
+ last := fillBucket(tab, pingSender)
+
+ // Add the sender as if it just pinged us. Revalidate should replace the last node in
+ // its bucket if it is unresponsive. Revalidate again to ensure that
+ transport.dead[last.ID()] = !lastInBucketIsResponding
+ transport.dead[pingSender.ID()] = !newNodeIsResponding
+ tab.addSeenNode(pingSender)
+ tab.doRevalidate(make(chan struct{}, 1))
+ tab.doRevalidate(make(chan struct{}, 1))
+
+ if !transport.pinged[last.ID()] {
+ // Oldest node in bucket is pinged to see whether it is still alive.
+ t.Error("table did not ping last node in bucket")
+ }
+
+ tab.mutex.Lock()
+ defer tab.mutex.Unlock()
+ wantSize := bucketSize
+ if !lastInBucketIsResponding && !newNodeIsResponding {
+ wantSize--
+ }
+ if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize {
+ t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize)
+ }
+ if found := contains(tab.bucket(pingSender.ID()).entries, last.ID()); found != lastInBucketIsResponding {
+ t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding)
+ }
+ wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
+ if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry {
+ t.Errorf("new entry found: %t, want: %t", found, wantNewEntry)
+ }
+}
+
+func TestBucket_bumpNoDuplicates(t *testing.T) {
+ t.Parallel()
+ cfg := &quick.Config{
+ MaxCount: 1000,
+ Rand: rand.New(rand.NewSource(time.Now().Unix())),
+ Values: func(args []reflect.Value, rand *rand.Rand) {
+ // generate a random list of nodes. this will be the content of the bucket.
+ n := rand.Intn(bucketSize-1) + 1
+ nodes := make([]*node, n)
+ for i := range nodes {
+ nodes[i] = nodeAtDistance(enode.ID{}, 200, intIP(200))
+ }
+ args[0] = reflect.ValueOf(nodes)
+ // generate random bump positions.
+ bumps := make([]int, rand.Intn(100))
+ for i := range bumps {
+ bumps[i] = rand.Intn(len(nodes))
+ }
+ args[1] = reflect.ValueOf(bumps)
+ },
+ }
+
+ prop := func(nodes []*node, bumps []int) (ok bool) {
+ tab, db := newTestTable(newPingRecorder())
+ defer db.Close()
+ defer tab.close()
+
+ b := &bucket{entries: make([]*node, len(nodes))}
+ copy(b.entries, nodes)
+ for i, pos := range bumps {
+ tab.bumpInBucket(b, b.entries[pos])
+ if hasDuplicates(b.entries) {
+ t.Logf("bucket has duplicates after %d/%d bumps:", i+1, len(bumps))
+ for _, n := range b.entries {
+ t.Logf(" %p", n)
+ }
+ return false
+ }
+ }
+ checkIPLimitInvariant(t, tab)
+ return true
+ }
+ if err := quick.Check(prop, cfg); err != nil {
+ t.Error(err)
+ }
+}
+
+// This checks that the table-wide IP limit is applied correctly.
+func TestTable_IPLimit(t *testing.T) {
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport)
+ defer db.Close()
+ defer tab.close()
+
+ for i := 0; i < tableIPLimit+1; i++ {
+ n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
+ tab.addSeenNode(n)
+ }
+ if tab.len() > tableIPLimit {
+ t.Errorf("too many nodes in table")
+ }
+ checkIPLimitInvariant(t, tab)
+}
+
+// This checks that the per-bucket IP limit is applied correctly.
+func TestTable_BucketIPLimit(t *testing.T) {
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport)
+ defer db.Close()
+ defer tab.close()
+
+ d := 3
+ for i := 0; i < bucketIPLimit+1; i++ {
+ n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
+ tab.addSeenNode(n)
+ }
+ if tab.len() > bucketIPLimit {
+ t.Errorf("too many nodes in table")
+ }
+ checkIPLimitInvariant(t, tab)
+}
+
+// checkIPLimitInvariant checks that ip limit sets contain an entry for every
+// node in the table and no extra entries.
+func checkIPLimitInvariant(t *testing.T, tab *Table) {
+ t.Helper()
+
+ tabset := netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit}
+ for _, b := range tab.buckets {
+ for _, n := range b.entries {
+ tabset.Add(n.IP())
+ }
+ }
+ if tabset.String() != tab.ips.String() {
+ t.Errorf("table IP set is incorrect:\nhave: %v\nwant: %v", tab.ips, tabset)
+ }
+}
+
+func TestTable_findnodeByID(t *testing.T) {
+ t.Parallel()
+
+ test := func(test *closeTest) bool {
+ // for any node table, Target and N
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport)
+ defer db.Close()
+ defer tab.close()
+ fillTable(tab, test.All)
+
+ // check that closest(Target, N) returns nodes
+ result := tab.findnodeByID(test.Target, test.N, false).entries
+ if hasDuplicates(result) {
+ t.Errorf("result contains duplicates")
+ return false
+ }
+ if !sortedByDistanceTo(test.Target, result) {
+ t.Errorf("result is not sorted by distance to target")
+ return false
+ }
+
+ // check that the number of results is min(N, tablen)
+ wantN := test.N
+ if tlen := tab.len(); tlen < test.N {
+ wantN = tlen
+ }
+ if len(result) != wantN {
+ t.Errorf("wrong number of nodes: got %d, want %d", len(result), wantN)
+ return false
+ } else if len(result) == 0 {
+ return true // no need to check distance
+ }
+
+ // check that the result nodes have minimum distance to target.
+ for _, b := range tab.buckets {
+ for _, n := range b.entries {
+ if contains(result, n.ID()) {
+ continue // don't run the check below for nodes in result
+ }
+ farthestResult := result[len(result)-1].ID()
+ if enode.DistCmp(test.Target, n.ID(), farthestResult) < 0 {
+ t.Errorf("table contains node that is closer to target but it's not in result")
+ t.Logf(" Target: %v", test.Target)
+ t.Logf(" Farthest Result: %v", farthestResult)
+ t.Logf(" ID: %v", n.ID())
+ return false
+ }
+ }
+ }
+ return true
+ }
+ if err := quick.Check(test, quickcfg()); err != nil {
+ t.Error(err)
+ }
+}
+
+func TestTable_ReadRandomNodesGetAll(t *testing.T) {
+ cfg := &quick.Config{
+ MaxCount: 200,
+ Rand: rand.New(rand.NewSource(time.Now().Unix())),
+ Values: func(args []reflect.Value, rand *rand.Rand) {
+ args[0] = reflect.ValueOf(make([]*enode.Node, rand.Intn(1000)))
+ },
+ }
+ test := func(buf []*enode.Node) bool {
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport)
+ defer db.Close()
+ defer tab.close()
+ <-tab.initDone
+
+ for i := 0; i < len(buf); i++ {
+ ld := cfg.Rand.Intn(len(tab.buckets))
+ fillTable(tab, []*node{nodeAtDistance(tab.self().ID(), ld, intIP(ld))})
+ }
+ gotN := tab.ReadRandomNodes(buf)
+ if gotN != tab.len() {
+ t.Errorf("wrong number of nodes, got %d, want %d", gotN, tab.len())
+ return false
+ }
+ if hasDuplicates(wrapNodes(buf[:gotN])) {
+ t.Errorf("result contains duplicates")
+ return false
+ }
+ return true
+ }
+ if err := quick.Check(test, cfg); err != nil {
+ t.Error(err)
+ }
+}
+
+type closeTest struct {
+ Self enode.ID
+ Target enode.ID
+ All []*node
+ N int
+}
+
+func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
+ t := &closeTest{
+ Self: gen(enode.ID{}, rand).(enode.ID),
+ Target: gen(enode.ID{}, rand).(enode.ID),
+ N: rand.Intn(bucketSize),
+ }
+ for _, id := range gen([]enode.ID{}, rand).([]enode.ID) {
+ r := new(enr.Record)
+ r.Set(enr.IP(genIP(rand)))
+ n := wrapNode(enode.SignNull(r, id))
+ n.livenessChecks = 1
+ t.All = append(t.All, n)
+ }
+ return reflect.ValueOf(t)
+}
+
+func TestTable_addVerifiedNode(t *testing.T) {
+ tab, db := newTestTable(newPingRecorder())
+ <-tab.initDone
+ defer db.Close()
+ defer tab.close()
+
+ // Insert two nodes.
+ n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
+ n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
+ tab.addSeenNode(n1)
+ tab.addSeenNode(n2)
+
+ // Verify bucket content:
+ bcontent := []*node{n1, n2}
+ if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
+ t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
+ }
+
+ // Add a changed version of n2.
+ newrec := n2.Record()
+ newrec.Set(enr.IP{99, 99, 99, 99})
+ newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
+ tab.addVerifiedNode(newn2)
+
+ // Check that bucket is updated correctly.
+ newBcontent := []*node{newn2, n1}
+ if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, newBcontent) {
+ t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
+ }
+ checkIPLimitInvariant(t, tab)
+}
+
+func TestTable_addSeenNode(t *testing.T) {
+ tab, db := newTestTable(newPingRecorder())
+ <-tab.initDone
+ defer db.Close()
+ defer tab.close()
+
+ // Insert two nodes.
+ n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
+ n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
+ tab.addSeenNode(n1)
+ tab.addSeenNode(n2)
+
+ // Verify bucket content:
+ bcontent := []*node{n1, n2}
+ if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
+ t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
+ }
+
+ // Add a changed version of n2.
+ newrec := n2.Record()
+ newrec.Set(enr.IP{99, 99, 99, 99})
+ newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
+ tab.addSeenNode(newn2)
+
+ // Check that bucket content is unchanged.
+ if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
+ t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
+ }
+ checkIPLimitInvariant(t, tab)
+}
+
+// This test checks that ENR updates happen during revalidation. If a node in the table
+// announces a new sequence number, the new record should be pulled.
+func TestTable_revalidateSyncRecord(t *testing.T) {
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport)
+ <-tab.initDone
+ defer db.Close()
+ defer tab.close()
+
+ // Insert a node.
+ var r enr.Record
+ r.Set(enr.IP(net.IP{127, 0, 0, 1}))
+ id := enode.ID{1}
+ n1 := wrapNode(enode.SignNull(&r, id))
+ tab.addSeenNode(n1)
+
+ // Update the node record.
+ r.Set(enr.WithEntry("foo", "bar"))
+ n2 := enode.SignNull(&r, id)
+ transport.updateRecord(n2)
+
+ tab.doRevalidate(make(chan struct{}, 1))
+ intable := tab.getNode(id)
+ if !reflect.DeepEqual(intable, n2) {
+ t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq())
+ }
+}
+
+// gen wraps quick.Value so it's easier to use.
+// it generates a random value of the given value's type.
+func gen(typ interface{}, rand *rand.Rand) interface{} {
+ v, ok := quick.Value(reflect.TypeOf(typ), rand)
+ if !ok {
+ panic(fmt.Sprintf("couldn't generate random value of type %T", typ))
+ }
+ return v.Interface()
+}
+
+func genIP(rand *rand.Rand) net.IP {
+ ip := make(net.IP, 4)
+ rand.Read(ip)
+ return ip
+}
+
+func quickcfg() *quick.Config {
+ return &quick.Config{
+ MaxCount: 5000,
+ Rand: rand.New(rand.NewSource(time.Now().Unix())),
+ }
+}
+
+func newkey() *ecdsa.PrivateKey {
+ key, err := crypto.GenerateKey()
+ if err != nil {
+ panic("couldn't generate key: " + err.Error())
+ }
+ return key
+}
diff --git a/discover/table_util_test.go b/discover/table_util_test.go
new file mode 100644
index 0000000..81d654c
--- /dev/null
+++ b/discover/table_util_test.go
@@ -0,0 +1,254 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "math/rand"
+ "net"
+ "sort"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+)
+
+var nullNode *enode.Node
+
+func init() {
+ var r enr.Record
+ r.Set(enr.IP{0, 0, 0, 0})
+ nullNode = enode.SignNull(&r, enode.ID{})
+}
+
+func newTestTable(t transport) (*Table, *enode.DB) {
+ db, _ := enode.OpenDB("")
+ tab, _ := newTable(t, db, nil, nil, log.Root())
+ go tab.loop()
+ return tab, db
+}
+
+// nodeAtDistance creates a node for which enode.LogDist(base, n.id) == ld.
+func nodeAtDistance(base enode.ID, ld int, ip net.IP) *node {
+ var r enr.Record
+ r.Set(enr.IP(ip))
+ return wrapNode(enode.SignNull(&r, idAtDistance(base, ld)))
+}
+
+// nodesAtDistance creates n nodes for which enode.LogDist(base, node.ID()) == ld.
+func nodesAtDistance(base enode.ID, ld int, n int) []*enode.Node {
+ results := make([]*enode.Node, n)
+ for i := range results {
+ results[i] = unwrapNode(nodeAtDistance(base, ld, intIP(i)))
+ }
+ return results
+}
+
+func nodesToRecords(nodes []*enode.Node) []*enr.Record {
+ records := make([]*enr.Record, len(nodes))
+ for i := range nodes {
+ records[i] = nodes[i].Record()
+ }
+ return records
+}
+
+// idAtDistance returns a random hash such that enode.LogDist(a, b) == n
+func idAtDistance(a enode.ID, n int) (b enode.ID) {
+ if n == 0 {
+ return a
+ }
+ // flip bit at position n, fill the rest with random bits
+ b = a
+ pos := len(a) - n/8 - 1
+ bit := byte(0x01) << (byte(n%8) - 1)
+ if bit == 0 {
+ pos++
+ bit = 0x80
+ }
+ b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits
+ for i := pos + 1; i < len(a); i++ {
+ b[i] = byte(rand.Intn(255))
+ }
+ return b
+}
+
+func intIP(i int) net.IP {
+ return net.IP{byte(i), 0, 2, byte(i)}
+}
+
+// fillBucket inserts nodes into the given bucket until it is full.
+func fillBucket(tab *Table, n *node) (last *node) {
+ ld := enode.LogDist(tab.self().ID(), n.ID())
+ b := tab.bucket(n.ID())
+ for len(b.entries) < bucketSize {
+ b.entries = append(b.entries, nodeAtDistance(tab.self().ID(), ld, intIP(ld)))
+ }
+ return b.entries[bucketSize-1]
+}
+
+// fillTable adds nodes the table to the end of their corresponding bucket
+// if the bucket is not full. The caller must not hold tab.mutex.
+func fillTable(tab *Table, nodes []*node) {
+ for _, n := range nodes {
+ tab.addSeenNode(n)
+ }
+}
+
+type pingRecorder struct {
+ mu sync.Mutex
+ dead, pinged map[enode.ID]bool
+ records map[enode.ID]*enode.Node
+ n *enode.Node
+}
+
+func newPingRecorder() *pingRecorder {
+ var r enr.Record
+ r.Set(enr.IP{0, 0, 0, 0})
+ n := enode.SignNull(&r, enode.ID{})
+
+ return &pingRecorder{
+ dead: make(map[enode.ID]bool),
+ pinged: make(map[enode.ID]bool),
+ records: make(map[enode.ID]*enode.Node),
+ n: n,
+ }
+}
+
+// setRecord updates a node record. Future calls to ping and
+// requestENR will return this record.
+func (t *pingRecorder) updateRecord(n *enode.Node) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ t.records[n.ID()] = n
+}
+
+// Stubs to satisfy the transport interface.
+func (t *pingRecorder) Self() *enode.Node { return nullNode }
+func (t *pingRecorder) lookupSelf() []*enode.Node { return nil }
+func (t *pingRecorder) lookupRandom() []*enode.Node { return nil }
+
+// ping simulates a ping request.
+func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ t.pinged[n.ID()] = true
+ if t.dead[n.ID()] {
+ return 0, errTimeout
+ }
+ if t.records[n.ID()] != nil {
+ seq = t.records[n.ID()].Seq()
+ }
+ return seq, nil
+}
+
+// requestENR simulates an ENR request.
+func (t *pingRecorder) RequestENR(n *enode.Node) (*enode.Node, error) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if t.dead[n.ID()] || t.records[n.ID()] == nil {
+ return nil, errTimeout
+ }
+ return t.records[n.ID()], nil
+}
+
+func hasDuplicates(slice []*node) bool {
+ seen := make(map[enode.ID]bool)
+ for i, e := range slice {
+ if e == nil {
+ panic(fmt.Sprintf("nil *Node at %d", i))
+ }
+ if seen[e.ID()] {
+ return true
+ }
+ seen[e.ID()] = true
+ }
+ return false
+}
+
+// checkNodesEqual checks whether the two given node lists contain the same nodes.
+func checkNodesEqual(got, want []*enode.Node) error {
+ if len(got) == len(want) {
+ for i := range got {
+ if !nodeEqual(got[i], want[i]) {
+ goto NotEqual
+ }
+ }
+ }
+ return nil
+
+NotEqual:
+ output := new(bytes.Buffer)
+ fmt.Fprintf(output, "got %d nodes:\n", len(got))
+ for _, n := range got {
+ fmt.Fprintf(output, " %v %v\n", n.ID(), n)
+ }
+ fmt.Fprintf(output, "want %d:\n", len(want))
+ for _, n := range want {
+ fmt.Fprintf(output, " %v %v\n", n.ID(), n)
+ }
+ return errors.New(output.String())
+}
+
+func nodeEqual(n1 *enode.Node, n2 *enode.Node) bool {
+ return n1.ID() == n2.ID() && n1.IP().Equal(n2.IP())
+}
+
+func sortByID(nodes []*enode.Node) {
+ sort.Slice(nodes, func(i, j int) bool {
+ return string(nodes[i].ID().Bytes()) < string(nodes[j].ID().Bytes())
+ })
+}
+
+func sortedByDistanceTo(distbase enode.ID, slice []*node) bool {
+ return sort.SliceIsSorted(slice, func(i, j int) bool {
+ return enode.DistCmp(distbase, slice[i].ID(), slice[j].ID()) < 0
+ })
+}
+
+// hexEncPrivkey decodes h as a private key.
+func hexEncPrivkey(h string) *ecdsa.PrivateKey {
+ b, err := hex.DecodeString(h)
+ if err != nil {
+ panic(err)
+ }
+ key, err := crypto.ToECDSA(b)
+ if err != nil {
+ panic(err)
+ }
+ return key
+}
+
+// hexEncPubkey decodes h as a public key.
+func hexEncPubkey(h string) (ret encPubkey) {
+ b, err := hex.DecodeString(h)
+ if err != nil {
+ panic(err)
+ }
+ if len(b) != len(ret) {
+ panic("invalid length")
+ }
+ copy(ret[:], b)
+ return ret
+}
diff --git a/discover/v4_lookup_test.go b/discover/v4_lookup_test.go
new file mode 100644
index 0000000..ebbbf61
--- /dev/null
+++ b/discover/v4_lookup_test.go
@@ -0,0 +1,347 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "net"
+ "sort"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/status-im/go-discover/discover/v4wire"
+)
+
+func TestUDPv4_Lookup(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+
+ // Lookup on empty table returns no nodes.
+ targetKey, _ := decodePubkey(crypto.S256(), lookupTestnet.target[:])
+ if results := test.udp.LookupPubkey(targetKey); len(results) > 0 {
+ t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results)
+ }
+
+ // Seed table with initial node.
+ fillTable(test.table, []*node{wrapNode(lookupTestnet.node(256, 0))})
+
+ // Start the lookup.
+ resultC := make(chan []*enode.Node, 1)
+ go func() {
+ resultC <- test.udp.LookupPubkey(targetKey)
+ test.close()
+ }()
+
+ // Answer lookup packets.
+ serveTestnet(test, lookupTestnet)
+
+ // Verify result nodes.
+ results := <-resultC
+ t.Logf("results:")
+ for _, e := range results {
+ t.Logf(" ld=%d, %x", enode.LogDist(lookupTestnet.target.id(), e.ID()), e.ID().Bytes())
+ }
+ if len(results) != bucketSize {
+ t.Errorf("wrong number of results: got %d, want %d", len(results), bucketSize)
+ }
+ checkLookupResults(t, lookupTestnet, results)
+}
+
+func TestUDPv4_LookupIterator(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+ defer test.close()
+
+ // Seed table with initial nodes.
+ bootnodes := make([]*node, len(lookupTestnet.dists[256]))
+ for i := range lookupTestnet.dists[256] {
+ bootnodes[i] = wrapNode(lookupTestnet.node(256, i))
+ }
+ fillTable(test.table, bootnodes)
+ go serveTestnet(test, lookupTestnet)
+
+ // Create the iterator and collect the nodes it yields.
+ iter := test.udp.RandomNodes()
+ seen := make(map[enode.ID]*enode.Node)
+ for limit := lookupTestnet.len(); iter.Next() && len(seen) < limit; {
+ seen[iter.Node().ID()] = iter.Node()
+ }
+ iter.Close()
+
+ // Check that all nodes in lookupTestnet were seen by the iterator.
+ results := make([]*enode.Node, 0, len(seen))
+ for _, n := range seen {
+ results = append(results, n)
+ }
+ sortByID(results)
+ want := lookupTestnet.nodes()
+ if err := checkNodesEqual(results, want); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestUDPv4_LookupIteratorClose checks that lookupIterator ends when its Close
+// method is called.
+func TestUDPv4_LookupIteratorClose(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+ defer test.close()
+
+ // Seed table with initial nodes.
+ bootnodes := make([]*node, len(lookupTestnet.dists[256]))
+ for i := range lookupTestnet.dists[256] {
+ bootnodes[i] = wrapNode(lookupTestnet.node(256, i))
+ }
+ fillTable(test.table, bootnodes)
+ go serveTestnet(test, lookupTestnet)
+
+ it := test.udp.RandomNodes()
+ if ok := it.Next(); !ok || it.Node() == nil {
+ t.Fatalf("iterator didn't return any node")
+ }
+
+ it.Close()
+
+ ncalls := 0
+ for ; ncalls < 100 && it.Next(); ncalls++ {
+ if it.Node() == nil {
+ t.Error("iterator returned Node() == nil node after Next() == true")
+ }
+ }
+ t.Logf("iterator returned %d nodes after close", ncalls)
+ if it.Next() {
+ t.Errorf("Next() == true after close and %d more calls", ncalls)
+ }
+ if n := it.Node(); n != nil {
+ t.Errorf("iterator returned non-nil node after close and %d more calls", ncalls)
+ }
+}
+
+func serveTestnet(test *udpTest, testnet *preminedTestnet) {
+ for done := false; !done; {
+ done = test.waitPacketOut(func(p v4wire.Packet, to *net.UDPAddr, hash []byte) {
+ n, key := testnet.nodeByAddr(to)
+ switch p.(type) {
+ case *v4wire.Ping:
+ test.packetInFrom(nil, key, to, &v4wire.Pong{Expiration: futureExp, ReplyTok: hash})
+ case *v4wire.Findnode:
+ dist := enode.LogDist(n.ID(), testnet.target.id())
+ nodes := testnet.nodesAtDistance(dist - 1)
+ test.packetInFrom(nil, key, to, &v4wire.Neighbors{Expiration: futureExp, Nodes: nodes})
+ }
+ })
+ }
+}
+
+// checkLookupResults verifies that the results of a lookup are the closest nodes to
+// the testnet's target.
+func checkLookupResults(t *testing.T, tn *preminedTestnet, results []*enode.Node) {
+ t.Helper()
+ t.Logf("results:")
+ for _, e := range results {
+ t.Logf(" ld=%d, %x", enode.LogDist(tn.target.id(), e.ID()), e.ID().Bytes())
+ }
+ if hasDuplicates(wrapNodes(results)) {
+ t.Errorf("result set contains duplicate entries")
+ }
+ if !sortedByDistanceTo(tn.target.id(), wrapNodes(results)) {
+ t.Errorf("result set not sorted by distance to target")
+ }
+ wantNodes := tn.closest(len(results))
+ if err := checkNodesEqual(results, wantNodes); err != nil {
+ t.Error(err)
+ }
+}
+
+// This is the test network for the Lookup test.
+// The nodes were obtained by running lookupTestnet.mine with a random NodeID as target.
+var lookupTestnet = &preminedTestnet{
+ target: hexEncPubkey("5d485bdcbe9bc89314a10ae9231e429d33853e3a8fa2af39f5f827370a2e4185e344ace5d16237491dad41f278f1d3785210d29ace76cd627b9147ee340b1125"),
+ dists: [257][]*ecdsa.PrivateKey{
+ 251: {
+ hexEncPrivkey("29738ba0c1a4397d6a65f292eee07f02df8e58d41594ba2be3cf84ce0fc58169"),
+ hexEncPrivkey("511b1686e4e58a917f7f848e9bf5539d206a68f5ad6b54b552c2399fe7d174ae"),
+ hexEncPrivkey("d09e5eaeec0fd596236faed210e55ef45112409a5aa7f3276d26646080dcfaeb"),
+ hexEncPrivkey("c1e20dbbf0d530e50573bd0a260b32ec15eb9190032b4633d44834afc8afe578"),
+ hexEncPrivkey("ed5f38f5702d92d306143e5d9154fb21819777da39af325ea359f453d179e80b"),
+ },
+ 252: {
+ hexEncPrivkey("1c9b1cafbec00848d2c174b858219914b42a7d5c9359b1ca03fd650e8239ae94"),
+ hexEncPrivkey("e0e1e8db4a6f13c1ffdd3e96b72fa7012293ced187c9dcdcb9ba2af37a46fa10"),
+ hexEncPrivkey("3d53823e0a0295cb09f3e11d16c1b44d07dd37cec6f739b8df3a590189fe9fb9"),
+ },
+ 253: {
+ hexEncPrivkey("2d0511ae9bf590166597eeab86b6f27b1ab761761eaea8965487b162f8703847"),
+ hexEncPrivkey("6cfbd7b8503073fc3dbdb746a7c672571648d3bd15197ccf7f7fef3d904f53a2"),
+ hexEncPrivkey("a30599b12827b69120633f15b98a7f6bc9fc2e9a0fd6ae2ebb767c0e64d743ab"),
+ hexEncPrivkey("14a98db9b46a831d67eff29f3b85b1b485bb12ae9796aea98d91be3dc78d8a91"),
+ hexEncPrivkey("2369ff1fc1ff8ca7d20b17e2673adc3365c3674377f21c5d9dafaff21fe12e24"),
+ hexEncPrivkey("9ae91101d6b5048607f41ec0f690ef5d09507928aded2410aabd9237aa2727d7"),
+ hexEncPrivkey("05e3c59090a3fd1ae697c09c574a36fcf9bedd0afa8fe3946f21117319ca4973"),
+ hexEncPrivkey("06f31c5ea632658f718a91a1b1b9ae4b7549d7b3bc61cbc2be5f4a439039f3ad"),
+ },
+ 254: {
+ hexEncPrivkey("dec742079ec00ff4ec1284d7905bc3de2366f67a0769431fd16f80fd68c58a7c"),
+ hexEncPrivkey("ff02c8861fa12fbd129d2a95ea663492ef9c1e51de19dcfbbfe1c59894a28d2b"),
+ hexEncPrivkey("4dded9e4eefcbce4262be4fd9e8a773670ab0b5f448f286ec97dfc8cf681444a"),
+ hexEncPrivkey("750d931e2a8baa2c9268cb46b7cd851f4198018bed22f4dceb09dd334a2395f6"),
+ hexEncPrivkey("ce1435a956a98ffec484cd11489c4f165cf1606819ab6b521cee440f0c677e9e"),
+ hexEncPrivkey("996e7f8d1638be92d7328b4770f47e5420fc4bafecb4324fd33b1f5d9f403a75"),
+ hexEncPrivkey("ebdc44e77a6cc0eb622e58cf3bb903c3da4c91ca75b447b0168505d8fc308b9c"),
+ hexEncPrivkey("46bd1eddcf6431bea66fc19ebc45df191c1c7d6ed552dcdc7392885009c322f0"),
+ },
+ 255: {
+ hexEncPrivkey("da8645f90826e57228d9ea72aff84500060ad111a5d62e4af831ed8e4b5acfb8"),
+ hexEncPrivkey("3c944c5d9af51d4c1d43f5d0f3a1a7ef65d5e82744d669b58b5fed242941a566"),
+ hexEncPrivkey("5ebcde76f1d579eebf6e43b0ffe9157e65ffaa391175d5b9aa988f47df3e33da"),
+ hexEncPrivkey("97f78253a7d1d796e4eaabce721febcc4550dd68fb11cc818378ba807a2cb7de"),
+ hexEncPrivkey("a38cd7dc9b4079d1c0406afd0fdb1165c285f2c44f946eca96fc67772c988c7d"),
+ hexEncPrivkey("d64cbb3ffdf712c372b7a22a176308ef8f91861398d5dbaf326fd89c6eaeef1c"),
+ hexEncPrivkey("d269609743ef29d6446e3355ec647e38d919c82a4eb5837e442efd7f4218944f"),
+ hexEncPrivkey("d8f7bcc4a530efde1d143717007179e0d9ace405ddaaf151c4d863753b7fd64c"),
+ },
+ 256: {
+ hexEncPrivkey("8c5b422155d33ea8e9d46f71d1ad3e7b24cb40051413ffa1a81cff613d243ba9"),
+ hexEncPrivkey("937b1af801def4e8f5a3a8bd225a8bcff1db764e41d3e177f2e9376e8dd87233"),
+ hexEncPrivkey("120260dce739b6f71f171da6f65bc361b5fad51db74cf02d3e973347819a6518"),
+ hexEncPrivkey("1fa56cf25d4b46c2bf94e82355aa631717b63190785ac6bae545a88aadc304a9"),
+ hexEncPrivkey("3c38c503c0376f9b4adcbe935d5f4b890391741c764f61b03cd4d0d42deae002"),
+ hexEncPrivkey("3a54af3e9fa162bc8623cdf3e5d9b70bf30ade1d54cc3abea8659aba6cff471f"),
+ hexEncPrivkey("6799a02ea1999aefdcbcc4d3ff9544478be7365a328d0d0f37c26bd95ade0cda"),
+ hexEncPrivkey("e24a7bc9051058f918646b0f6e3d16884b2a55a15553b89bab910d55ebc36116"),
+ },
+ },
+}
+
+type preminedTestnet struct {
+ target encPubkey
+ dists [hashBits + 1][]*ecdsa.PrivateKey
+}
+
+func (tn *preminedTestnet) len() int {
+ n := 0
+ for _, keys := range tn.dists {
+ n += len(keys)
+ }
+ return n
+}
+
+func (tn *preminedTestnet) nodes() []*enode.Node {
+ result := make([]*enode.Node, 0, tn.len())
+ for dist, keys := range tn.dists {
+ for index := range keys {
+ result = append(result, tn.node(dist, index))
+ }
+ }
+ sortByID(result)
+ return result
+}
+
+func (tn *preminedTestnet) node(dist, index int) *enode.Node {
+ key := tn.dists[dist][index]
+ rec := new(enr.Record)
+ rec.Set(enr.IP{127, byte(dist >> 8), byte(dist), byte(index)})
+ rec.Set(enr.UDP(5000))
+ enode.SignV4(rec, key)
+ n, _ := enode.New(enode.ValidSchemes, rec)
+ return n
+}
+
+func (tn *preminedTestnet) nodeByAddr(addr *net.UDPAddr) (*enode.Node, *ecdsa.PrivateKey) {
+ dist := int(addr.IP[1])<<8 + int(addr.IP[2])
+ index := int(addr.IP[3])
+ key := tn.dists[dist][index]
+ return tn.node(dist, index), key
+}
+
+func (tn *preminedTestnet) nodesAtDistance(dist int) []v4wire.Node {
+ result := make([]v4wire.Node, len(tn.dists[dist]))
+ for i := range result {
+ result[i] = nodeToRPC(wrapNode(tn.node(dist, i)))
+ }
+ return result
+}
+
+func (tn *preminedTestnet) neighborsAtDistances(base *enode.Node, distances []uint, elems int) []*enode.Node {
+ var result []*enode.Node
+ for d := range lookupTestnet.dists {
+ for i := range lookupTestnet.dists[d] {
+ n := lookupTestnet.node(d, i)
+ d := enode.LogDist(base.ID(), n.ID())
+ if containsUint(uint(d), distances) {
+ result = append(result, n)
+ if len(result) >= elems {
+ return result
+ }
+ }
+ }
+ }
+ return result
+}
+
+func (tn *preminedTestnet) closest(n int) (nodes []*enode.Node) {
+ for d := range tn.dists {
+ for i := range tn.dists[d] {
+ nodes = append(nodes, tn.node(d, i))
+ }
+ }
+ sort.Slice(nodes, func(i, j int) bool {
+ return enode.DistCmp(tn.target.id(), nodes[i].ID(), nodes[j].ID()) < 0
+ })
+ return nodes[:n]
+}
+
+var _ = (*preminedTestnet).mine // avoid linter warning about mine being dead code.
+
+// mine generates a testnet struct literal with nodes at
+// various distances to the network's target.
+func (tn *preminedTestnet) mine() {
+ // Clear existing slices first (useful when re-mining).
+ for i := range tn.dists {
+ tn.dists[i] = nil
+ }
+
+ targetSha := tn.target.id()
+ found, need := 0, 40
+ for found < need {
+ k := newkey()
+ ld := enode.LogDist(targetSha, encodePubkey(&k.PublicKey).id())
+ if len(tn.dists[ld]) < 8 {
+ tn.dists[ld] = append(tn.dists[ld], k)
+ found++
+ fmt.Printf("found ID with ld %d (%d/%d)\n", ld, found, need)
+ }
+ }
+ fmt.Printf("&preminedTestnet{\n")
+ fmt.Printf(" target: hexEncPubkey(\"%x\"),\n", tn.target[:])
+ fmt.Printf(" dists: [%d][]*ecdsa.PrivateKey{\n", len(tn.dists))
+ for ld, ns := range tn.dists {
+ if len(ns) == 0 {
+ continue
+ }
+ fmt.Printf(" %d: {\n", ld)
+ for _, key := range ns {
+ fmt.Printf(" hexEncPrivkey(\"%x\"),\n", crypto.FromECDSA(key))
+ }
+ fmt.Printf(" },\n")
+ }
+ fmt.Printf(" },\n")
+ fmt.Printf("}\n")
+}
diff --git a/discover/v4_udp.go b/discover/v4_udp.go
new file mode 100644
index 0000000..238fa09
--- /dev/null
+++ b/discover/v4_udp.go
@@ -0,0 +1,787 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "bytes"
+ "container/list"
+ "context"
+ "crypto/ecdsa"
+ crand "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/netutil"
+ "github.com/status-im/go-discover/discover/v4wire"
+)
+
+// Errors
+var (
+ errExpired = errors.New("expired")
+ errUnsolicitedReply = errors.New("unsolicited reply")
+ errUnknownNode = errors.New("unknown node")
+ errTimeout = errors.New("RPC timeout")
+ errClockWarp = errors.New("reply deadline too far in the future")
+ errClosed = errors.New("socket closed")
+ errLowPort = errors.New("low port")
+)
+
+const (
+ respTimeout = 500 * time.Millisecond
+ expiration = 20 * time.Second
+ bondExpiration = 24 * time.Hour
+
+ maxFindnodeFailures = 5 // nodes exceeding this limit are dropped
+ ntpFailureThreshold = 32 // Continuous timeouts after which to check NTP
+ ntpWarningCooldown = 10 * time.Minute // Minimum amount of time to pass before repeating NTP warning
+ driftThreshold = 10 * time.Second // Allowed clock drift before warning user
+
+ // Discovery packets are defined to be no larger than 1280 bytes.
+ // Packets larger than this size will be cut at the end and treated
+ // as invalid because their hash won't match.
+ maxPacketSize = 1280
+)
+
+// UDPv4 implements the v4 wire protocol.
+type UDPv4 struct {
+ conn UDPConn
+ log log.Logger
+ netrestrict *netutil.Netlist
+ priv *ecdsa.PrivateKey
+ localNode *enode.LocalNode
+ db *enode.DB
+ tab *Table
+ closeOnce sync.Once
+ wg sync.WaitGroup
+
+ addReplyMatcher chan *replyMatcher
+ gotreply chan reply
+ closeCtx context.Context
+ cancelCloseCtx context.CancelFunc
+}
+
+// replyMatcher represents a pending reply.
+//
+// Some implementations of the protocol wish to send more than one
+// reply packet to findnode. In general, any neighbors packet cannot
+// be matched up with a specific findnode packet.
+//
+// Our implementation handles this by storing a callback function for
+// each pending reply. Incoming packets from a node are dispatched
+// to all callback functions for that node.
+type replyMatcher struct {
+ // these fields must match in the reply.
+ from enode.ID
+ ip net.IP
+ ptype byte
+
+ // time when the request must complete
+ deadline time.Time
+
+ // callback is called when a matching reply arrives. If it returns matched == true, the
+ // reply was acceptable. The second return value indicates whether the callback should
+ // be removed from the pending reply queue. If it returns false, the reply is considered
+ // incomplete and the callback will be invoked again for the next matching reply.
+ callback replyMatchFunc
+
+ // errc receives nil when the callback indicates completion or an
+ // error if no further reply is received within the timeout.
+ errc chan error
+
+ // reply contains the most recent reply. This field is safe for reading after errc has
+ // received a value.
+ reply v4wire.Packet
+}
+
+type replyMatchFunc func(v4wire.Packet) (matched bool, requestDone bool)
+
+// reply is a reply packet from a certain node.
+type reply struct {
+ from enode.ID
+ ip net.IP
+ data v4wire.Packet
+ // loop indicates whether there was
+ // a matching request by sending on this channel.
+ matched chan<- bool
+}
+
+func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
+ cfg = cfg.withDefaults()
+ closeCtx, cancel := context.WithCancel(context.Background())
+ t := &UDPv4{
+ conn: c,
+ priv: cfg.PrivateKey,
+ netrestrict: cfg.NetRestrict,
+ localNode: ln,
+ db: ln.Database(),
+ gotreply: make(chan reply),
+ addReplyMatcher: make(chan *replyMatcher),
+ closeCtx: closeCtx,
+ cancelCloseCtx: cancel,
+ log: cfg.Log,
+ }
+
+ tab, err := newTable(t, ln.Database(), cfg.Bootnodes, t.log)
+ if err != nil {
+ return nil, err
+ }
+ t.tab = tab
+ go tab.loop()
+
+ t.wg.Add(2)
+ go t.loop()
+ go t.readLoop(cfg.Unhandled)
+ return t, nil
+}
+
+// Self returns the local node.
+func (t *UDPv4) Self() *enode.Node {
+ return t.localNode.Node()
+}
+
+// Close shuts down the socket and aborts any running queries.
+func (t *UDPv4) Close() {
+ t.closeOnce.Do(func() {
+ t.cancelCloseCtx()
+ t.conn.Close()
+ t.wg.Wait()
+ t.tab.close()
+ })
+}
+
+// Resolve searches for a specific node with the given ID and tries to get the most recent
+// version of the node record for it. It returns n if the node could not be resolved.
+func (t *UDPv4) Resolve(n *enode.Node) *enode.Node {
+ // Try asking directly. This works if the node is still responding on the endpoint we have.
+ if rn, err := t.RequestENR(n); err == nil {
+ return rn
+ }
+ // Check table for the ID, we might have a newer version there.
+ if intable := t.tab.getNode(n.ID()); intable != nil && intable.Seq() > n.Seq() {
+ n = intable
+ if rn, err := t.RequestENR(n); err == nil {
+ return rn
+ }
+ }
+ // Otherwise perform a network lookup.
+ var key enode.Secp256k1
+ if n.Load(&key) != nil {
+ return n // no secp256k1 key
+ }
+ result := t.LookupPubkey((*ecdsa.PublicKey)(&key))
+ for _, rn := range result {
+ if rn.ID() == n.ID() {
+ if rn, err := t.RequestENR(rn); err == nil {
+ return rn
+ }
+ }
+ }
+ return n
+}
+
+func (t *UDPv4) ourEndpoint() v4wire.Endpoint {
+ n := t.Self()
+ a := &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
+ return v4wire.NewEndpoint(a, uint16(n.TCP()))
+}
+
+// Ping sends a ping message to the given node.
+func (t *UDPv4) Ping(n *enode.Node) error {
+ _, err := t.ping(n)
+ return err
+}
+
+// ping sends a ping message to the given node and waits for a reply.
+func (t *UDPv4) ping(n *enode.Node) (seq uint64, err error) {
+ rm := t.sendPing(n.ID(), &net.UDPAddr{IP: n.IP(), Port: n.UDP()}, nil)
+ if err = <-rm.errc; err == nil {
+ seq = rm.reply.(*v4wire.Pong).ENRSeq
+ }
+ return seq, err
+}
+
+// sendPing sends a ping message to the given node and invokes the callback
+// when the reply arrives.
+func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) *replyMatcher {
+ req := t.makePing(toaddr)
+ packet, hash, err := v4wire.Encode(t.priv, req)
+ if err != nil {
+ errc := make(chan error, 1)
+ errc <- err
+ return &replyMatcher{errc: errc}
+ }
+ // Add a matcher for the reply to the pending reply queue. Pongs are matched if they
+ // reference the ping we're about to send.
+ rm := t.pending(toid, toaddr.IP, v4wire.PongPacket, func(p v4wire.Packet) (matched bool, requestDone bool) {
+ matched = bytes.Equal(p.(*v4wire.Pong).ReplyTok, hash)
+ if matched && callback != nil {
+ callback()
+ }
+ return matched, matched
+ })
+ // Send the packet.
+ t.localNode.UDPContact(toaddr)
+ t.write(toaddr, toid, req.Name(), packet)
+ return rm
+}
+
+func (t *UDPv4) makePing(toaddr *net.UDPAddr) *v4wire.Ping {
+ return &v4wire.Ping{
+ Version: 4,
+ From: t.ourEndpoint(),
+ To: v4wire.NewEndpoint(toaddr, 0),
+ Expiration: uint64(time.Now().Add(expiration).Unix()),
+ ENRSeq: t.localNode.Node().Seq(),
+ }
+}
+
+// LookupPubkey finds the closest nodes to the given public key.
+func (t *UDPv4) LookupPubkey(key *ecdsa.PublicKey) []*enode.Node {
+ if t.tab.len() == 0 {
+ // All nodes were dropped, refresh. The very first query will hit this
+ // case and run the bootstrapping logic.
+ <-t.tab.refresh()
+ }
+ return t.newLookup(t.closeCtx, encodePubkey(key)).run()
+}
+
+// RandomNodes is an iterator yielding nodes from a random walk of the DHT.
+func (t *UDPv4) RandomNodes() enode.Iterator {
+ return newLookupIterator(t.closeCtx, t.newRandomLookup)
+}
+
+// lookupRandom implements transport.
+func (t *UDPv4) lookupRandom() []*enode.Node {
+ return t.newRandomLookup(t.closeCtx).run()
+}
+
+// lookupSelf implements transport.
+func (t *UDPv4) lookupSelf() []*enode.Node {
+ return t.newLookup(t.closeCtx, encodePubkey(&t.priv.PublicKey)).run()
+}
+
+func (t *UDPv4) newRandomLookup(ctx context.Context) *lookup {
+ var target encPubkey
+ crand.Read(target[:])
+ return t.newLookup(ctx, target)
+}
+
+func (t *UDPv4) newLookup(ctx context.Context, targetKey encPubkey) *lookup {
+ target := enode.ID(crypto.Keccak256Hash(targetKey[:]))
+ ekey := v4wire.Pubkey(targetKey)
+ it := newLookup(ctx, t.tab, target, func(n *node) ([]*node, error) {
+ return t.findnode(n.ID(), n.addr(), ekey)
+ })
+ return it
+}
+
+// findnode sends a findnode request to the given node and waits until
+// the node has sent up to k neighbors.
+func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target v4wire.Pubkey) ([]*node, error) {
+ t.ensureBond(toid, toaddr)
+
+ // Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is
+ // active until enough nodes have been received.
+ nodes := make([]*node, 0, bucketSize)
+ nreceived := 0
+ rm := t.pending(toid, toaddr.IP, v4wire.NeighborsPacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
+ reply := r.(*v4wire.Neighbors)
+ for _, rn := range reply.Nodes {
+ nreceived++
+ n, err := t.nodeFromRPC(toaddr, rn)
+ if err != nil {
+ t.log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toaddr, "err", err)
+ continue
+ }
+ nodes = append(nodes, n)
+ }
+ return true, nreceived >= bucketSize
+ })
+ t.send(toaddr, toid, &v4wire.Findnode{
+ Target: target,
+ Expiration: uint64(time.Now().Add(expiration).Unix()),
+ })
+ // Ensure that callers don't see a timeout if the node actually responded. Since
+ // findnode can receive more than one neighbors response, the reply matcher will be
+ // active until the remote node sends enough nodes. If the remote end doesn't have
+ // enough nodes the reply matcher will time out waiting for the second reply, but
+ // there's no need for an error in that case.
+ err := <-rm.errc
+ if err == errTimeout && rm.reply != nil {
+ err = nil
+ }
+ return nodes, err
+}
+
+// RequestENR sends enrRequest to the given node and waits for a response.
+func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
+ addr := &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
+ t.ensureBond(n.ID(), addr)
+
+ req := &v4wire.ENRRequest{
+ Expiration: uint64(time.Now().Add(expiration).Unix()),
+ }
+ packet, hash, err := v4wire.Encode(t.priv, req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Add a matcher for the reply to the pending reply queue. Responses are matched if
+ // they reference the request we're about to send.
+ rm := t.pending(n.ID(), addr.IP, v4wire.ENRResponsePacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
+ matched = bytes.Equal(r.(*v4wire.ENRResponse).ReplyTok, hash)
+ return matched, matched
+ })
+ // Send the packet and wait for the reply.
+ t.write(addr, n.ID(), req.Name(), packet)
+ if err := <-rm.errc; err != nil {
+ return nil, err
+ }
+ // Verify the response record.
+ respN, err := enode.New(enode.ValidSchemes, &rm.reply.(*v4wire.ENRResponse).Record)
+ if err != nil {
+ return nil, err
+ }
+ if respN.ID() != n.ID() {
+ return nil, fmt.Errorf("invalid ID in response record")
+ }
+ if respN.Seq() < n.Seq() {
+ return n, nil // response record is older
+ }
+ if err := netutil.CheckRelayIP(addr.IP, respN.IP()); err != nil {
+ return nil, fmt.Errorf("invalid IP in response record: %v", err)
+ }
+ return respN, nil
+}
+
+// pending adds a reply matcher to the pending reply queue.
+// see the documentation of type replyMatcher for a detailed explanation.
+func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) *replyMatcher {
+ ch := make(chan error, 1)
+ p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch}
+ select {
+ case t.addReplyMatcher <- p:
+ // loop will handle it
+ case <-t.closeCtx.Done():
+ ch <- errClosed
+ }
+ return p
+}
+
+// handleReply dispatches a reply packet, invoking reply matchers. It returns
+// whether any matcher considered the packet acceptable.
+func (t *UDPv4) handleReply(from enode.ID, fromIP net.IP, req v4wire.Packet) bool {
+ matched := make(chan bool, 1)
+ select {
+ case t.gotreply <- reply{from, fromIP, req, matched}:
+ // loop will handle it
+ return <-matched
+ case <-t.closeCtx.Done():
+ return false
+ }
+}
+
+// loop runs in its own goroutine. it keeps track of
+// the refresh timer and the pending reply queue.
+func (t *UDPv4) loop() {
+ defer t.wg.Done()
+
+ var (
+ plist = list.New()
+ timeout = time.NewTimer(0)
+ nextTimeout *replyMatcher // head of plist when timeout was last reset
+ contTimeouts = 0 // number of continuous timeouts to do NTP checks
+ ntpWarnTime = time.Unix(0, 0)
+ )
+ <-timeout.C // ignore first timeout
+ defer timeout.Stop()
+
+ resetTimeout := func() {
+ if plist.Front() == nil || nextTimeout == plist.Front().Value {
+ return
+ }
+ // Start the timer so it fires when the next pending reply has expired.
+ now := time.Now()
+ for el := plist.Front(); el != nil; el = el.Next() {
+ nextTimeout = el.Value.(*replyMatcher)
+ if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout {
+ timeout.Reset(dist)
+ return
+ }
+ // Remove pending replies whose deadline is too far in the
+ // future. These can occur if the system clock jumped
+ // backwards after the deadline was assigned.
+ nextTimeout.errc <- errClockWarp
+ plist.Remove(el)
+ }
+ nextTimeout = nil
+ timeout.Stop()
+ }
+
+ for {
+ resetTimeout()
+
+ select {
+ case <-t.closeCtx.Done():
+ for el := plist.Front(); el != nil; el = el.Next() {
+ el.Value.(*replyMatcher).errc <- errClosed
+ }
+ return
+
+ case p := <-t.addReplyMatcher:
+ p.deadline = time.Now().Add(respTimeout)
+ plist.PushBack(p)
+
+ case r := <-t.gotreply:
+ var matched bool // whether any replyMatcher considered the reply acceptable.
+ for el := plist.Front(); el != nil; el = el.Next() {
+ p := el.Value.(*replyMatcher)
+ if p.from == r.from && p.ptype == r.data.Kind() && p.ip.Equal(r.ip) {
+ ok, requestDone := p.callback(r.data)
+ matched = matched || ok
+ p.reply = r.data
+ // Remove the matcher if callback indicates that all replies have been received.
+ if requestDone {
+ p.errc <- nil
+ plist.Remove(el)
+ }
+ // Reset the continuous timeout counter (time drift detection)
+ contTimeouts = 0
+ }
+ }
+ r.matched <- matched
+
+ case now := <-timeout.C:
+ nextTimeout = nil
+
+ // Notify and remove callbacks whose deadline is in the past.
+ for el := plist.Front(); el != nil; el = el.Next() {
+ p := el.Value.(*replyMatcher)
+ if now.After(p.deadline) || now.Equal(p.deadline) {
+ p.errc <- errTimeout
+ plist.Remove(el)
+ contTimeouts++
+ }
+ }
+ // If we've accumulated too many timeouts, do an NTP time sync check
+ if contTimeouts > ntpFailureThreshold {
+ if time.Since(ntpWarnTime) >= ntpWarningCooldown {
+ ntpWarnTime = time.Now()
+ go checkClockDrift()
+ }
+ contTimeouts = 0
+ }
+ }
+ }
+}
+
+func (t *UDPv4) send(toaddr *net.UDPAddr, toid enode.ID, req v4wire.Packet) ([]byte, error) {
+ packet, hash, err := v4wire.Encode(t.priv, req)
+ if err != nil {
+ return hash, err
+ }
+ return hash, t.write(toaddr, toid, req.Name(), packet)
+}
+
+func (t *UDPv4) write(toaddr *net.UDPAddr, toid enode.ID, what string, packet []byte) error {
+ _, err := t.conn.WriteToUDP(packet, toaddr)
+ t.log.Trace(">> "+what, "id", toid, "addr", toaddr, "err", err)
+ return err
+}
+
+// readLoop runs in its own goroutine. it handles incoming UDP packets.
+func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) {
+ defer t.wg.Done()
+ if unhandled != nil {
+ defer close(unhandled)
+ }
+
+ buf := make([]byte, maxPacketSize)
+ for {
+ nbytes, from, err := t.conn.ReadFromUDP(buf)
+ if netutil.IsTemporaryError(err) {
+ // Ignore temporary read errors.
+ t.log.Debug("Temporary UDP read error", "err", err)
+ continue
+ } else if err != nil {
+ // Shut down the loop for permament errors.
+ if err != io.EOF {
+ t.log.Debug("UDP read error", "err", err)
+ }
+ return
+ }
+ if t.handlePacket(from, buf[:nbytes]) != nil && unhandled != nil {
+ select {
+ case unhandled <- ReadPacket{buf[:nbytes], from}:
+ default:
+ }
+ }
+ }
+}
+
+func (t *UDPv4) handlePacket(from *net.UDPAddr, buf []byte) error {
+ rawpacket, fromKey, hash, err := v4wire.Decode(buf)
+ if err != nil {
+ t.log.Debug("Bad discv4 packet", "addr", from, "err", err)
+ return err
+ }
+ packet := t.wrapPacket(rawpacket)
+ fromID := fromKey.ID()
+ if err == nil && packet.preverify != nil {
+ err = packet.preverify(packet, from, fromID, fromKey)
+ }
+ t.log.Trace("<< "+packet.Name(), "id", fromID, "addr", from, "err", err)
+ if err == nil && packet.handle != nil {
+ packet.handle(packet, from, fromID, hash)
+ }
+ return err
+}
+
+// checkBond checks if the given node has a recent enough endpoint proof.
+func (t *UDPv4) checkBond(id enode.ID, ip net.IP) bool {
+ return time.Since(t.db.LastPongReceived(id, ip)) < bondExpiration
+}
+
+// ensureBond solicits a ping from a node if we haven't seen a ping from it for a while.
+// This ensures there is a valid endpoint proof on the remote end.
+func (t *UDPv4) ensureBond(toid enode.ID, toaddr *net.UDPAddr) {
+ tooOld := time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration
+ if tooOld || t.db.FindFails(toid, toaddr.IP) > maxFindnodeFailures {
+ rm := t.sendPing(toid, toaddr, nil)
+ <-rm.errc
+ // Wait for them to ping back and process our pong.
+ time.Sleep(respTimeout)
+ }
+}
+
+func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*node, error) {
+ if rn.UDP <= 1024 {
+ return nil, errLowPort
+ }
+ if err := netutil.CheckRelayIP(sender.IP, rn.IP); err != nil {
+ return nil, err
+ }
+ if t.netrestrict != nil && !t.netrestrict.Contains(rn.IP) {
+ return nil, errors.New("not contained in netrestrict list")
+ }
+ key, err := v4wire.DecodePubkey(crypto.S256(), rn.ID)
+ if err != nil {
+ return nil, err
+ }
+ n := wrapNode(enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP)))
+ err = n.ValidateComplete()
+ return n, err
+}
+
+func nodeToRPC(n *node) v4wire.Node {
+ var key ecdsa.PublicKey
+ var ekey v4wire.Pubkey
+ if err := n.Load((*enode.Secp256k1)(&key)); err == nil {
+ ekey = v4wire.EncodePubkey(&key)
+ }
+ return v4wire.Node{ID: ekey, IP: n.IP(), UDP: uint16(n.UDP()), TCP: uint16(n.TCP())}
+}
+
+// wrapPacket returns the handler functions applicable to a packet.
+func (t *UDPv4) wrapPacket(p v4wire.Packet) *packetHandlerV4 {
+ var h packetHandlerV4
+ h.Packet = p
+ switch p.(type) {
+ case *v4wire.Ping:
+ h.preverify = t.verifyPing
+ h.handle = t.handlePing
+ case *v4wire.Pong:
+ h.preverify = t.verifyPong
+ case *v4wire.Findnode:
+ h.preverify = t.verifyFindnode
+ h.handle = t.handleFindnode
+ case *v4wire.Neighbors:
+ h.preverify = t.verifyNeighbors
+ case *v4wire.ENRRequest:
+ h.preverify = t.verifyENRRequest
+ h.handle = t.handleENRRequest
+ case *v4wire.ENRResponse:
+ h.preverify = t.verifyENRResponse
+ }
+ return &h
+}
+
+// packetHandlerV4 wraps a packet with handler functions.
+type packetHandlerV4 struct {
+ v4wire.Packet
+ senderKey *ecdsa.PublicKey // used for ping
+
+ // preverify checks whether the packet is valid and should be handled at all.
+ preverify func(p *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error
+ // handle handles the packet.
+ handle func(req *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte)
+}
+
+// PING/v4
+
+func (t *UDPv4) verifyPing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
+ req := h.Packet.(*v4wire.Ping)
+
+ senderKey, err := v4wire.DecodePubkey(crypto.S256(), fromKey)
+ if err != nil {
+ return err
+ }
+ if v4wire.Expired(req.Expiration) {
+ return errExpired
+ }
+ h.senderKey = senderKey
+ return nil
+}
+
+func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
+ req := h.Packet.(*v4wire.Ping)
+
+ // Reply.
+ t.send(from, fromID, &v4wire.Pong{
+ To: v4wire.NewEndpoint(from, req.From.TCP),
+ ReplyTok: mac,
+ Expiration: uint64(time.Now().Add(expiration).Unix()),
+ ENRSeq: t.localNode.Node().Seq(),
+ })
+
+ // Ping back if our last pong on file is too far in the past.
+ n := wrapNode(enode.NewV4(h.senderKey, from.IP, int(req.From.TCP), from.Port))
+ if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
+ t.sendPing(fromID, from, func() {
+ t.tab.addVerifiedNode(n)
+ })
+ } else {
+ t.tab.addVerifiedNode(n)
+ }
+
+ // Update node database and endpoint predictor.
+ t.db.UpdateLastPingReceived(n.ID(), from.IP, time.Now())
+ t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
+}
+
+// PONG/v4
+
+func (t *UDPv4) verifyPong(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
+ req := h.Packet.(*v4wire.Pong)
+
+ if v4wire.Expired(req.Expiration) {
+ return errExpired
+ }
+ if !t.handleReply(fromID, from.IP, req) {
+ return errUnsolicitedReply
+ }
+ t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
+ t.db.UpdateLastPongReceived(fromID, from.IP, time.Now())
+ return nil
+}
+
+// FINDNODE/v4
+
+func (t *UDPv4) verifyFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
+ req := h.Packet.(*v4wire.Findnode)
+
+ if v4wire.Expired(req.Expiration) {
+ return errExpired
+ }
+ if !t.checkBond(fromID, from.IP) {
+ // No endpoint proof pong exists, we don't process the packet. This prevents an
+ // attack vector where the discovery protocol could be used to amplify traffic in a
+ // DDOS attack. A malicious actor would send a findnode request with the IP address
+ // and UDP port of the target as the source address. The recipient of the findnode
+ // packet would then send a neighbors packet (which is a much bigger packet than
+ // findnode) to the victim.
+ return errUnknownNode
+ }
+ return nil
+}
+
+func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
+ req := h.Packet.(*v4wire.Findnode)
+
+ // Determine closest nodes.
+ target := enode.ID(crypto.Keccak256Hash(req.Target[:]))
+ closest := t.tab.findnodeByID(target, bucketSize, true).entries
+
+ // Send neighbors in chunks with at most maxNeighbors per packet
+ // to stay below the packet size limit.
+ p := v4wire.Neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())}
+ var sent bool
+ for _, n := range closest {
+ if netutil.CheckRelayIP(from.IP, n.IP()) == nil {
+ p.Nodes = append(p.Nodes, nodeToRPC(n))
+ }
+ if len(p.Nodes) == v4wire.MaxNeighbors {
+ t.send(from, fromID, &p)
+ p.Nodes = p.Nodes[:0]
+ sent = true
+ }
+ }
+ if len(p.Nodes) > 0 || !sent {
+ t.send(from, fromID, &p)
+ }
+}
+
+// NEIGHBORS/v4
+
+func (t *UDPv4) verifyNeighbors(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
+ req := h.Packet.(*v4wire.Neighbors)
+
+ if v4wire.Expired(req.Expiration) {
+ return errExpired
+ }
+ if !t.handleReply(fromID, from.IP, h.Packet) {
+ return errUnsolicitedReply
+ }
+ return nil
+}
+
+// ENRREQUEST/v4
+
+func (t *UDPv4) verifyENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
+ req := h.Packet.(*v4wire.ENRRequest)
+
+ if v4wire.Expired(req.Expiration) {
+ return errExpired
+ }
+ if !t.checkBond(fromID, from.IP) {
+ return errUnknownNode
+ }
+ return nil
+}
+
+func (t *UDPv4) handleENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
+ t.send(from, fromID, &v4wire.ENRResponse{
+ ReplyTok: mac,
+ Record: *t.localNode.Node().Record(),
+ })
+}
+
+// ENRRESPONSE/v4
+
+func (t *UDPv4) verifyENRResponse(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
+ if !t.handleReply(fromID, from.IP, h.Packet) {
+ return errUnsolicitedReply
+ }
+ return nil
+}
diff --git a/discover/v4_udp_test.go b/discover/v4_udp_test.go
new file mode 100644
index 0000000..e480fac
--- /dev/null
+++ b/discover/v4_udp_test.go
@@ -0,0 +1,666 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ crand "crypto/rand"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net"
+ "reflect"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/status-im/go-discover/discover/v4wire"
+ "github.com/status-im/go-discover/internal/testlog"
+)
+
+// shared test variables
+var (
+ futureExp = uint64(time.Now().Add(10 * time.Hour).Unix())
+ testTarget = v4wire.Pubkey{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}
+ testRemote = v4wire.Endpoint{IP: net.ParseIP("1.1.1.1").To4(), UDP: 1, TCP: 2}
+ testLocalAnnounced = v4wire.Endpoint{IP: net.ParseIP("2.2.2.2").To4(), UDP: 3, TCP: 4}
+ testLocal = v4wire.Endpoint{IP: net.ParseIP("3.3.3.3").To4(), UDP: 5, TCP: 6}
+)
+
+type udpTest struct {
+ t *testing.T
+ pipe *dgramPipe
+ table *Table
+ db *enode.DB
+ udp *UDPv4
+ sent [][]byte
+ localkey, remotekey *ecdsa.PrivateKey
+ remoteaddr *net.UDPAddr
+}
+
+func newUDPTest(t *testing.T) *udpTest {
+ test := &udpTest{
+ t: t,
+ pipe: newpipe(),
+ localkey: newkey(),
+ remotekey: newkey(),
+ remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303},
+ }
+
+ test.db, _ = enode.OpenDB("")
+ ln := enode.NewLocalNode(test.db, test.localkey)
+ test.udp, _ = ListenV4(test.pipe, ln, Config{
+ PrivateKey: test.localkey,
+ Log: testlog.Logger(t, log.LvlTrace),
+ })
+ test.table = test.udp.tab
+ // Wait for initial refresh so the table doesn't send unexpected findnode.
+ <-test.table.initDone
+ return test
+}
+
+func (test *udpTest) close() {
+ test.udp.Close()
+ test.db.Close()
+}
+
+// handles a packet as if it had been sent to the transport.
+func (test *udpTest) packetIn(wantError error, data v4wire.Packet) {
+ test.t.Helper()
+
+ test.packetInFrom(wantError, test.remotekey, test.remoteaddr, data)
+}
+
+// handles a packet as if it had been sent to the transport by the key/endpoint.
+func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, data v4wire.Packet) {
+ test.t.Helper()
+
+ enc, _, err := v4wire.Encode(key, data)
+ if err != nil {
+ test.t.Errorf("%s encode error: %v", data.Name(), err)
+ }
+ test.sent = append(test.sent, enc)
+ if err = test.udp.handlePacket(addr, enc); err != wantError {
+ test.t.Errorf("error mismatch: got %q, want %q", err, wantError)
+ }
+}
+
+// waits for a packet to be sent by the transport.
+// validate should have type func(X, *net.UDPAddr, []byte), where X is a packet type.
+func (test *udpTest) waitPacketOut(validate interface{}) (closed bool) {
+ test.t.Helper()
+
+ dgram, err := test.pipe.receive()
+ if err == errClosed {
+ return true
+ } else if err != nil {
+ test.t.Error("packet receive error:", err)
+ return false
+ }
+ p, _, hash, err := v4wire.Decode(dgram.data)
+ if err != nil {
+ test.t.Errorf("sent packet decode error: %v", err)
+ return false
+ }
+ fn := reflect.ValueOf(validate)
+ exptype := fn.Type().In(0)
+ if !reflect.TypeOf(p).AssignableTo(exptype) {
+ test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
+ return false
+ }
+ fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(&dgram.to), reflect.ValueOf(hash)})
+ return false
+}
+
+func TestUDPv4_packetErrors(t *testing.T) {
+ test := newUDPTest(t)
+ defer test.close()
+
+ test.packetIn(errExpired, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4})
+ test.packetIn(errUnsolicitedReply, &v4wire.Pong{ReplyTok: []byte{}, Expiration: futureExp})
+ test.packetIn(errUnknownNode, &v4wire.Findnode{Expiration: futureExp})
+ test.packetIn(errUnsolicitedReply, &v4wire.Neighbors{Expiration: futureExp})
+}
+
+func TestUDPv4_pingTimeout(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+ defer test.close()
+
+ key := newkey()
+ toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
+ node := enode.NewV4(&key.PublicKey, toaddr.IP, 0, toaddr.Port)
+ if _, err := test.udp.ping(node); err != errTimeout {
+ t.Error("expected timeout error, got", err)
+ }
+}
+
+type testPacket byte
+
+func (req testPacket) Kind() byte { return byte(req) }
+func (req testPacket) Name() string { return "" }
+
+func TestUDPv4_responseTimeouts(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+ defer test.close()
+
+ rand.Seed(time.Now().UnixNano())
+ randomDuration := func(max time.Duration) time.Duration {
+ return time.Duration(rand.Int63n(int64(max)))
+ }
+
+ var (
+ nReqs = 200
+ nTimeouts = 0 // number of requests with ptype > 128
+ nilErr = make(chan error, nReqs) // for requests that get a reply
+ timeoutErr = make(chan error, nReqs) // for requests that time out
+ )
+ for i := 0; i < nReqs; i++ {
+ // Create a matcher for a random request in udp.loop. Requests
+ // with ptype <= 128 will not get a reply and should time out.
+ // For all other requests, a reply is scheduled to arrive
+ // within the timeout window.
+ p := &replyMatcher{
+ ptype: byte(rand.Intn(255)),
+ callback: func(v4wire.Packet) (bool, bool) { return true, true },
+ }
+ binary.BigEndian.PutUint64(p.from[:], uint64(i))
+ if p.ptype <= 128 {
+ p.errc = timeoutErr
+ test.udp.addReplyMatcher <- p
+ nTimeouts++
+ } else {
+ p.errc = nilErr
+ test.udp.addReplyMatcher <- p
+ time.AfterFunc(randomDuration(60*time.Millisecond), func() {
+ if !test.udp.handleReply(p.from, p.ip, testPacket(p.ptype)) {
+ t.Logf("not matched: %v", p)
+ }
+ })
+ }
+ time.Sleep(randomDuration(30 * time.Millisecond))
+ }
+
+ // Check that all timeouts were delivered and that the rest got nil errors.
+ // The replies must be delivered.
+ var (
+ recvDeadline = time.After(20 * time.Second)
+ nTimeoutsRecv, nNil = 0, 0
+ )
+ for i := 0; i < nReqs; i++ {
+ select {
+ case err := <-timeoutErr:
+ if err != errTimeout {
+ t.Fatalf("got non-timeout error on timeoutErr %d: %v", i, err)
+ }
+ nTimeoutsRecv++
+ case err := <-nilErr:
+ if err != nil {
+ t.Fatalf("got non-nil error on nilErr %d: %v", i, err)
+ }
+ nNil++
+ case <-recvDeadline:
+ t.Fatalf("exceeded recv deadline")
+ }
+ }
+ if nTimeoutsRecv != nTimeouts {
+ t.Errorf("wrong number of timeout errors received: got %d, want %d", nTimeoutsRecv, nTimeouts)
+ }
+ if nNil != nReqs-nTimeouts {
+ t.Errorf("wrong number of successful replies: got %d, want %d", nNil, nReqs-nTimeouts)
+ }
+}
+
+func TestUDPv4_findnodeTimeout(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+ defer test.close()
+
+ toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
+ toid := enode.ID{1, 2, 3, 4}
+ target := v4wire.Pubkey{4, 5, 6, 7}
+ result, err := test.udp.findnode(toid, toaddr, target)
+ if err != errTimeout {
+ t.Error("expected timeout error, got", err)
+ }
+ if len(result) > 0 {
+ t.Error("expected empty result, got", result)
+ }
+}
+
+func TestUDPv4_findnode(t *testing.T) {
+ test := newUDPTest(t)
+ defer test.close()
+
+ // put a few nodes into the table. their exact
+ // distribution shouldn't matter much, although we need to
+ // take care not to overflow any bucket.
+ nodes := &nodesByDistance{target: testTarget.ID()}
+ live := make(map[enode.ID]bool)
+ numCandidates := 2 * bucketSize
+ for i := 0; i < numCandidates; i++ {
+ key := newkey()
+ ip := net.IP{10, 13, 0, byte(i)}
+ n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
+ // Ensure half of table content isn't verified live yet.
+ if i > numCandidates/2 {
+ n.livenessChecks = 1
+ live[n.ID()] = true
+ }
+ nodes.push(n, numCandidates)
+ }
+ fillTable(test.table, nodes.entries)
+
+ // ensure there's a bond with the test node,
+ // findnode won't be accepted otherwise.
+ remoteID := v4wire.EncodePubkey(&test.remotekey.PublicKey).ID()
+ test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.IP, time.Now())
+
+ // check that closest neighbors are returned.
+ expected := test.table.findnodeByID(testTarget.ID(), bucketSize, true)
+ test.packetIn(nil, &v4wire.Findnode{Target: testTarget, Expiration: futureExp})
+ waitNeighbors := func(want []*node) {
+ test.waitPacketOut(func(p *v4wire.Neighbors, to *net.UDPAddr, hash []byte) {
+ if len(p.Nodes) != len(want) {
+ t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSize)
+ }
+ for i, n := range p.Nodes {
+ if n.ID.ID() != want[i].ID() {
+ t.Errorf("result mismatch at %d:\n got: %v\n want: %v", i, n, expected.entries[i])
+ }
+ if !live[n.ID.ID()] {
+ t.Errorf("result includes dead node %v", n.ID.ID())
+ }
+ }
+ })
+ }
+ // Receive replies.
+ want := expected.entries
+ if len(want) > v4wire.MaxNeighbors {
+ waitNeighbors(want[:v4wire.MaxNeighbors])
+ want = want[v4wire.MaxNeighbors:]
+ }
+ waitNeighbors(want)
+}
+
+func TestUDPv4_findnodeMultiReply(t *testing.T) {
+ test := newUDPTest(t)
+ defer test.close()
+
+ rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey)
+ test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.IP, time.Now())
+
+ // queue a pending findnode request
+ resultc, errc := make(chan []*node), make(chan error)
+ go func() {
+ rid := encodePubkey(&test.remotekey.PublicKey).id()
+ ns, err := test.udp.findnode(rid, test.remoteaddr, testTarget)
+ if err != nil && len(ns) == 0 {
+ errc <- err
+ } else {
+ resultc <- ns
+ }
+ }()
+
+ // wait for the findnode to be sent.
+ // after it is sent, the transport is waiting for a reply
+ test.waitPacketOut(func(p *v4wire.Findnode, to *net.UDPAddr, hash []byte) {
+ if p.Target != testTarget {
+ t.Errorf("wrong target: got %v, want %v", p.Target, testTarget)
+ }
+ })
+
+ // send the reply as two packets.
+ list := []*node{
+ wrapNode(enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
+ wrapNode(enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
+ wrapNode(enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
+ wrapNode(enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
+ }
+ rpclist := make([]v4wire.Node, len(list))
+ for i := range list {
+ rpclist[i] = nodeToRPC(list[i])
+ }
+ test.packetIn(nil, &v4wire.Neighbors{Expiration: futureExp, Nodes: rpclist[:2]})
+ test.packetIn(nil, &v4wire.Neighbors{Expiration: futureExp, Nodes: rpclist[2:]})
+
+ // check that the sent neighbors are all returned by findnode
+ select {
+ case result := <-resultc:
+ want := append(list[:2], list[3:]...)
+ if !reflect.DeepEqual(result, want) {
+ t.Errorf("neighbors mismatch:\n got: %v\n want: %v", result, want)
+ }
+ case err := <-errc:
+ t.Errorf("findnode error: %v", err)
+ case <-time.After(5 * time.Second):
+ t.Error("findnode did not return within 5 seconds")
+ }
+}
+
+// This test checks that reply matching of pong verifies the ping hash.
+func TestUDPv4_pingMatch(t *testing.T) {
+ test := newUDPTest(t)
+ defer test.close()
+
+ randToken := make([]byte, 32)
+ crand.Read(randToken)
+
+ test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
+ test.waitPacketOut(func(*v4wire.Pong, *net.UDPAddr, []byte) {})
+ test.waitPacketOut(func(*v4wire.Ping, *net.UDPAddr, []byte) {})
+ test.packetIn(errUnsolicitedReply, &v4wire.Pong{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp})
+}
+
+// This test checks that reply matching of pong verifies the sender IP address.
+func TestUDPv4_pingMatchIP(t *testing.T) {
+ test := newUDPTest(t)
+ defer test.close()
+
+ test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
+ test.waitPacketOut(func(*v4wire.Pong, *net.UDPAddr, []byte) {})
+
+ test.waitPacketOut(func(p *v4wire.Ping, to *net.UDPAddr, hash []byte) {
+ wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000}
+ test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &v4wire.Pong{
+ ReplyTok: hash,
+ To: testLocalAnnounced,
+ Expiration: futureExp,
+ })
+ })
+}
+
+func TestUDPv4_successfulPing(t *testing.T) {
+ test := newUDPTest(t)
+ added := make(chan *node, 1)
+ test.table.nodeAddedHook = func(n *node) { added <- n }
+ defer test.close()
+
+ // The remote side sends a ping packet to initiate the exchange.
+ go test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
+
+ // The ping is replied to.
+ test.waitPacketOut(func(p *v4wire.Pong, to *net.UDPAddr, hash []byte) {
+ pinghash := test.sent[0][:32]
+ if !bytes.Equal(p.ReplyTok, pinghash) {
+ t.Errorf("got pong.ReplyTok %x, want %x", p.ReplyTok, pinghash)
+ }
+ wantTo := v4wire.Endpoint{
+ // The mirrored UDP address is the UDP packet sender
+ IP: test.remoteaddr.IP, UDP: uint16(test.remoteaddr.Port),
+ // The mirrored TCP port is the one from the ping packet
+ TCP: testRemote.TCP,
+ }
+ if !reflect.DeepEqual(p.To, wantTo) {
+ t.Errorf("got pong.To %v, want %v", p.To, wantTo)
+ }
+ })
+
+ // Remote is unknown, the table pings back.
+ test.waitPacketOut(func(p *v4wire.Ping, to *net.UDPAddr, hash []byte) {
+ if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) {
+ t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint())
+ }
+ wantTo := v4wire.Endpoint{
+ // The mirrored UDP address is the UDP packet sender.
+ IP: test.remoteaddr.IP,
+ UDP: uint16(test.remoteaddr.Port),
+ TCP: 0,
+ }
+ if !reflect.DeepEqual(p.To, wantTo) {
+ t.Errorf("got ping.To %v, want %v", p.To, wantTo)
+ }
+ test.packetIn(nil, &v4wire.Pong{ReplyTok: hash, Expiration: futureExp})
+ })
+
+ // The node should be added to the table shortly after getting the
+ // pong packet.
+ select {
+ case n := <-added:
+ rid := encodePubkey(&test.remotekey.PublicKey).id()
+ if n.ID() != rid {
+ t.Errorf("node has wrong ID: got %v, want %v", n.ID(), rid)
+ }
+ if !n.IP().Equal(test.remoteaddr.IP) {
+ t.Errorf("node has wrong IP: got %v, want: %v", n.IP(), test.remoteaddr.IP)
+ }
+ if n.UDP() != test.remoteaddr.Port {
+ t.Errorf("node has wrong UDP port: got %v, want: %v", n.UDP(), test.remoteaddr.Port)
+ }
+ if n.TCP() != int(testRemote.TCP) {
+ t.Errorf("node has wrong TCP port: got %v, want: %v", n.TCP(), testRemote.TCP)
+ }
+ case <-time.After(2 * time.Second):
+ t.Errorf("node was not added within 2 seconds")
+ }
+}
+
+// This test checks that EIP-868 requests work.
+func TestUDPv4_EIP868(t *testing.T) {
+ test := newUDPTest(t)
+ defer test.close()
+
+ test.udp.localNode.Set(enr.WithEntry("foo", "bar"))
+ wantNode := test.udp.localNode.Node()
+
+ // ENR requests aren't allowed before endpoint proof.
+ test.packetIn(errUnknownNode, &v4wire.ENRRequest{Expiration: futureExp})
+
+ // Perform endpoint proof and check for sequence number in packet tail.
+ test.packetIn(nil, &v4wire.Ping{Expiration: futureExp})
+ test.waitPacketOut(func(p *v4wire.Pong, addr *net.UDPAddr, hash []byte) {
+ if p.ENRSeq != wantNode.Seq() {
+ t.Errorf("wrong sequence number in pong: %d, want %d", p.ENRSeq, wantNode.Seq())
+ }
+ })
+ test.waitPacketOut(func(p *v4wire.Ping, addr *net.UDPAddr, hash []byte) {
+ if p.ENRSeq != wantNode.Seq() {
+ t.Errorf("wrong sequence number in ping: %d, want %d", p.ENRSeq, wantNode.Seq())
+ }
+ test.packetIn(nil, &v4wire.Pong{Expiration: futureExp, ReplyTok: hash})
+ })
+
+ // Request should work now.
+ test.packetIn(nil, &v4wire.ENRRequest{Expiration: futureExp})
+ test.waitPacketOut(func(p *v4wire.ENRResponse, addr *net.UDPAddr, hash []byte) {
+ n, err := enode.New(enode.ValidSchemes, &p.Record)
+ if err != nil {
+ t.Fatalf("invalid record: %v", err)
+ }
+ if !reflect.DeepEqual(n, wantNode) {
+ t.Fatalf("wrong node in enrResponse: %v", n)
+ }
+ })
+}
+
+// This test verifies that a small network of nodes can boot up into a healthy state.
+func TestUDPv4_smallNetConvergence(t *testing.T) {
+ t.Parallel()
+
+ // Start the network.
+ nodes := make([]*UDPv4, 4)
+ for i := range nodes {
+ var cfg Config
+ if i > 0 {
+ bn := nodes[0].Self()
+ cfg.Bootnodes = []*enode.Node{bn}
+ }
+ nodes[i] = startLocalhostV4(t, cfg)
+ defer nodes[i].Close()
+ }
+
+ // Run through the iterator on all nodes until
+ // they have all found each other.
+ status := make(chan error, len(nodes))
+ for i := range nodes {
+ node := nodes[i]
+ go func() {
+ found := make(map[enode.ID]bool, len(nodes))
+ it := node.RandomNodes()
+ for it.Next() {
+ found[it.Node().ID()] = true
+ if len(found) == len(nodes) {
+ status <- nil
+ return
+ }
+ }
+ status <- fmt.Errorf("node %s didn't find all nodes", node.Self().ID().TerminalString())
+ }()
+ }
+
+ // Wait for all status reports.
+ timeout := time.NewTimer(30 * time.Second)
+ defer timeout.Stop()
+ for received := 0; received < len(nodes); {
+ select {
+ case <-timeout.C:
+ for _, node := range nodes {
+ node.Close()
+ }
+ case err := <-status:
+ received++
+ if err != nil {
+ t.Error("ERROR:", err)
+ return
+ }
+ }
+ }
+}
+
+func startLocalhostV4(t *testing.T, cfg Config) *UDPv4 {
+ t.Helper()
+
+ cfg.PrivateKey = newkey()
+ db, _ := enode.OpenDB("")
+ ln := enode.NewLocalNode(db, cfg.PrivateKey)
+
+ // Prefix logs with node ID.
+ lprefix := fmt.Sprintf("(%s)", ln.ID().TerminalString())
+ lfmt := log.TerminalFormat(false)
+ cfg.Log = testlog.Logger(t, log.LvlTrace)
+ cfg.Log.SetHandler(log.FuncHandler(func(r *log.Record) error {
+ t.Logf("%s %s", lprefix, lfmt.Format(r))
+ return nil
+ }))
+
+ // Listen.
+ socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ realaddr := socket.LocalAddr().(*net.UDPAddr)
+ ln.SetStaticIP(realaddr.IP)
+ ln.SetFallbackUDP(realaddr.Port)
+ udp, err := ListenV4(socket, ln, cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return udp
+}
+
+// dgramPipe is a fake UDP socket. It queues all sent datagrams.
+type dgramPipe struct {
+ mu *sync.Mutex
+ cond *sync.Cond
+ closing chan struct{}
+ closed bool
+ queue []dgram
+}
+
+type dgram struct {
+ to net.UDPAddr
+ data []byte
+}
+
+func newpipe() *dgramPipe {
+ mu := new(sync.Mutex)
+ return &dgramPipe{
+ closing: make(chan struct{}),
+ cond: &sync.Cond{L: mu},
+ mu: mu,
+ }
+}
+
+// WriteToUDP queues a datagram.
+func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) {
+ msg := make([]byte, len(b))
+ copy(msg, b)
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.closed {
+ return 0, errors.New("closed")
+ }
+ c.queue = append(c.queue, dgram{*to, b})
+ c.cond.Signal()
+ return len(b), nil
+}
+
+// ReadFromUDP just hangs until the pipe is closed.
+func (c *dgramPipe) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
+ <-c.closing
+ return 0, nil, io.EOF
+}
+
+func (c *dgramPipe) Close() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if !c.closed {
+ close(c.closing)
+ c.closed = true
+ }
+ c.cond.Broadcast()
+ return nil
+}
+
+func (c *dgramPipe) LocalAddr() net.Addr {
+ return &net.UDPAddr{IP: testLocal.IP, Port: int(testLocal.UDP)}
+}
+
+func (c *dgramPipe) receive() (dgram, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ var timedOut bool
+ timer := time.AfterFunc(3*time.Second, func() {
+ c.mu.Lock()
+ timedOut = true
+ c.mu.Unlock()
+ c.cond.Broadcast()
+ })
+ defer timer.Stop()
+
+ for len(c.queue) == 0 && !c.closed && !timedOut {
+ c.cond.Wait()
+ }
+ if c.closed {
+ return dgram{}, errClosed
+ }
+ if timedOut {
+ return dgram{}, errTimeout
+ }
+ p := c.queue[0]
+ copy(c.queue, c.queue[1:])
+ c.queue = c.queue[:len(c.queue)-1]
+ return p, nil
+}
diff --git a/discover/v4wire/v4wire.go b/discover/v4wire/v4wire.go
new file mode 100644
index 0000000..bc537a4
--- /dev/null
+++ b/discover/v4wire/v4wire.go
@@ -0,0 +1,293 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package v4wire implements the Discovery v4 Wire Protocol.
+package v4wire
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "errors"
+ "fmt"
+ "math/big"
+ "net"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// RPC packet types
+const (
+ PingPacket = iota + 1 // zero is 'reserved'
+ PongPacket
+ FindnodePacket
+ NeighborsPacket
+ ENRRequestPacket
+ ENRResponsePacket
+)
+
+// RPC request structures
+type (
+ Ping struct {
+ Version uint
+ From, To Endpoint
+ Expiration uint64
+ ENRSeq uint64 `rlp:"optional"` // Sequence number of local record, added by EIP-868.
+
+ // Ignore additional fields (for forward compatibility).
+ Rest []rlp.RawValue `rlp:"tail"`
+ }
+
+ // Pong is the reply to ping.
+ Pong struct {
+ // This field should mirror the UDP envelope address
+ // of the ping packet, which provides a way to discover the
+ // the external address (after NAT).
+ To Endpoint
+ ReplyTok []byte // This contains the hash of the ping packet.
+ Expiration uint64 // Absolute timestamp at which the packet becomes invalid.
+ ENRSeq uint64 `rlp:"optional"` // Sequence number of local record, added by EIP-868.
+
+ // Ignore additional fields (for forward compatibility).
+ Rest []rlp.RawValue `rlp:"tail"`
+ }
+
+ // Findnode is a query for nodes close to the given target.
+ Findnode struct {
+ Target Pubkey
+ Expiration uint64
+ // Ignore additional fields (for forward compatibility).
+ Rest []rlp.RawValue `rlp:"tail"`
+ }
+
+ // Neighbors is the reply to findnode.
+ Neighbors struct {
+ Nodes []Node
+ Expiration uint64
+ // Ignore additional fields (for forward compatibility).
+ Rest []rlp.RawValue `rlp:"tail"`
+ }
+
+ // enrRequest queries for the remote node's record.
+ ENRRequest struct {
+ Expiration uint64
+ // Ignore additional fields (for forward compatibility).
+ Rest []rlp.RawValue `rlp:"tail"`
+ }
+
+ // enrResponse is the reply to enrRequest.
+ ENRResponse struct {
+ ReplyTok []byte // Hash of the enrRequest packet.
+ Record enr.Record
+ // Ignore additional fields (for forward compatibility).
+ Rest []rlp.RawValue `rlp:"tail"`
+ }
+)
+
+// This number is the maximum number of neighbor nodes in a Neighbors packet.
+const MaxNeighbors = 12
+
+// This code computes the MaxNeighbors constant value.
+
+// func init() {
+// var maxNeighbors int
+// p := Neighbors{Expiration: ^uint64(0)}
+// maxSizeNode := Node{IP: make(net.IP, 16), UDP: ^uint16(0), TCP: ^uint16(0)}
+// for n := 0; ; n++ {
+// p.Nodes = append(p.Nodes, maxSizeNode)
+// size, _, err := rlp.EncodeToReader(p)
+// if err != nil {
+// // If this ever happens, it will be caught by the unit tests.
+// panic("cannot encode: " + err.Error())
+// }
+// if headSize+size+1 >= 1280 {
+// maxNeighbors = n
+// break
+// }
+// }
+// fmt.Println("maxNeighbors", maxNeighbors)
+// }
+
+// Pubkey represents an encoded 64-byte secp256k1 public key.
+type Pubkey [64]byte
+
+// ID returns the node ID corresponding to the public key.
+func (e Pubkey) ID() enode.ID {
+ return enode.ID(crypto.Keccak256Hash(e[:]))
+}
+
+// Node represents information about a node.
+type Node struct {
+ IP net.IP // len 4 for IPv4 or 16 for IPv6
+ UDP uint16 // for discovery protocol
+ TCP uint16 // for RLPx protocol
+ ID Pubkey
+}
+
+// Endpoint represents a network endpoint.
+type Endpoint struct {
+ IP net.IP // len 4 for IPv4 or 16 for IPv6
+ UDP uint16 // for discovery protocol
+ TCP uint16 // for RLPx protocol
+}
+
+// NewEndpoint creates an endpoint.
+func NewEndpoint(addr *net.UDPAddr, tcpPort uint16) Endpoint {
+ ip := net.IP{}
+ if ip4 := addr.IP.To4(); ip4 != nil {
+ ip = ip4
+ } else if ip6 := addr.IP.To16(); ip6 != nil {
+ ip = ip6
+ }
+ return Endpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort}
+}
+
+type Packet interface {
+ // packet name and type for logging purposes.
+ Name() string
+ Kind() byte
+}
+
+func (req *Ping) Name() string { return "PING/v4" }
+func (req *Ping) Kind() byte { return PingPacket }
+
+func (req *Pong) Name() string { return "PONG/v4" }
+func (req *Pong) Kind() byte { return PongPacket }
+
+func (req *Findnode) Name() string { return "FINDNODE/v4" }
+func (req *Findnode) Kind() byte { return FindnodePacket }
+
+func (req *Neighbors) Name() string { return "NEIGHBORS/v4" }
+func (req *Neighbors) Kind() byte { return NeighborsPacket }
+
+func (req *ENRRequest) Name() string { return "ENRREQUEST/v4" }
+func (req *ENRRequest) Kind() byte { return ENRRequestPacket }
+
+func (req *ENRResponse) Name() string { return "ENRRESPONSE/v4" }
+func (req *ENRResponse) Kind() byte { return ENRResponsePacket }
+
+// Expired checks whether the given UNIX time stamp is in the past.
+func Expired(ts uint64) bool {
+ return time.Unix(int64(ts), 0).Before(time.Now())
+}
+
+// Encoder/decoder.
+
+const (
+ macSize = 32
+ sigSize = crypto.SignatureLength
+ headSize = macSize + sigSize // space of packet frame data
+)
+
+var (
+ ErrPacketTooSmall = errors.New("too small")
+ ErrBadHash = errors.New("bad hash")
+ ErrBadPoint = errors.New("invalid curve point")
+)
+
+var headSpace = make([]byte, headSize)
+
+// Decode reads a discovery v4 packet.
+func Decode(input []byte) (Packet, Pubkey, []byte, error) {
+ if len(input) < headSize+1 {
+ return nil, Pubkey{}, nil, ErrPacketTooSmall
+ }
+ hash, sig, sigdata := input[:macSize], input[macSize:headSize], input[headSize:]
+ shouldhash := crypto.Keccak256(input[macSize:])
+ if !bytes.Equal(hash, shouldhash) {
+ return nil, Pubkey{}, nil, ErrBadHash
+ }
+ fromKey, err := recoverNodeKey(crypto.Keccak256(input[headSize:]), sig)
+ if err != nil {
+ return nil, fromKey, hash, err
+ }
+
+ var req Packet
+ switch ptype := sigdata[0]; ptype {
+ case PingPacket:
+ req = new(Ping)
+ case PongPacket:
+ req = new(Pong)
+ case FindnodePacket:
+ req = new(Findnode)
+ case NeighborsPacket:
+ req = new(Neighbors)
+ case ENRRequestPacket:
+ req = new(ENRRequest)
+ case ENRResponsePacket:
+ req = new(ENRResponse)
+ default:
+ return nil, fromKey, hash, fmt.Errorf("unknown type: %d", ptype)
+ }
+ s := rlp.NewStream(bytes.NewReader(sigdata[1:]), 0)
+ err = s.Decode(req)
+ return req, fromKey, hash, err
+}
+
+// Encode encodes a discovery packet.
+func Encode(priv *ecdsa.PrivateKey, req Packet) (packet, hash []byte, err error) {
+ b := new(bytes.Buffer)
+ b.Write(headSpace)
+ b.WriteByte(req.Kind())
+ if err := rlp.Encode(b, req); err != nil {
+ return nil, nil, err
+ }
+ packet = b.Bytes()
+ sig, err := crypto.Sign(crypto.Keccak256(packet[headSize:]), priv)
+ if err != nil {
+ return nil, nil, err
+ }
+ copy(packet[macSize:], sig)
+ // Add the hash to the front. Note: this doesn't protect the packet in any way.
+ hash = crypto.Keccak256(packet[macSize:])
+ copy(packet, hash)
+ return packet, hash, nil
+}
+
+// recoverNodeKey computes the public key used to sign the given hash from the signature.
+func recoverNodeKey(hash, sig []byte) (key Pubkey, err error) {
+ pubkey, err := crypto.Ecrecover(hash, sig)
+ if err != nil {
+ return key, err
+ }
+ copy(key[:], pubkey[1:])
+ return key, nil
+}
+
+// EncodePubkey encodes a secp256k1 public key.
+func EncodePubkey(key *ecdsa.PublicKey) Pubkey {
+ var e Pubkey
+ math.ReadBits(key.X, e[:len(e)/2])
+ math.ReadBits(key.Y, e[len(e)/2:])
+ return e
+}
+
+// DecodePubkey reads an encoded secp256k1 public key.
+func DecodePubkey(curve elliptic.Curve, e Pubkey) (*ecdsa.PublicKey, error) {
+ p := &ecdsa.PublicKey{Curve: curve, X: new(big.Int), Y: new(big.Int)}
+ half := len(e) / 2
+ p.X.SetBytes(e[:half])
+ p.Y.SetBytes(e[half:])
+ if !p.Curve.IsOnCurve(p.X, p.Y) {
+ return nil, ErrBadPoint
+ }
+ return p, nil
+}
diff --git a/discover/v4wire/v4wire_test.go b/discover/v4wire/v4wire_test.go
new file mode 100644
index 0000000..3b41619
--- /dev/null
+++ b/discover/v4wire/v4wire_test.go
@@ -0,0 +1,132 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v4wire
+
+import (
+ "encoding/hex"
+ "net"
+ "reflect"
+ "testing"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// EIP-8 test vectors.
+var testPackets = []struct {
+ input string
+ wantPacket interface{}
+}{
+ {
+ input: "71dbda3a79554728d4f94411e42ee1f8b0d561c10e1e5f5893367948c6a7d70bb87b235fa28a77070271b6c164a2dce8c7e13a5739b53b5e96f2e5acb0e458a02902f5965d55ecbeb2ebb6cabb8b2b232896a36b737666c55265ad0a68412f250001ea04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a355",
+ wantPacket: &Ping{
+ Version: 4,
+ From: Endpoint{net.ParseIP("127.0.0.1").To4(), 3322, 5544},
+ To: Endpoint{net.ParseIP("::1"), 2222, 3333},
+ Expiration: 1136239445,
+ },
+ },
+ {
+ input: "e9614ccfd9fc3e74360018522d30e1419a143407ffcce748de3e22116b7e8dc92ff74788c0b6663aaa3d67d641936511c8f8d6ad8698b820a7cf9e1be7155e9a241f556658c55428ec0563514365799a4be2be5a685a80971ddcfa80cb422cdd0101ec04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a3550102",
+ wantPacket: &Ping{
+ Version: 4,
+ From: Endpoint{net.ParseIP("127.0.0.1").To4(), 3322, 5544},
+ To: Endpoint{net.ParseIP("::1"), 2222, 3333},
+ Expiration: 1136239445,
+ ENRSeq: 1,
+ Rest: []rlp.RawValue{{0x02}},
+ },
+ },
+ {
+ input: "c7c44041b9f7c7e41934417ebac9a8e1a4c6298f74553f2fcfdcae6ed6fe53163eb3d2b52e39fe91831b8a927bf4fc222c3902202027e5e9eb812195f95d20061ef5cd31d502e47ecb61183f74a504fe04c51e73df81f25c4d506b26db4517490103f84eb840ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f8443b9a35582999983999999280dc62cc8255c73471e0a61da0c89acdc0e035e260add7fc0c04ad9ebf3919644c91cb247affc82b69bd2ca235c71eab8e49737c937a2c396",
+ wantPacket: &Findnode{
+ Target: hexPubkey("ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f"),
+ Expiration: 1136239445,
+ Rest: []rlp.RawValue{{0x82, 0x99, 0x99}, {0x83, 0x99, 0x99, 0x99}},
+ },
+ },
+ {
+ input: "c679fc8fe0b8b12f06577f2e802d34f6fa257e6137a995f6f4cbfc9ee50ed3710faf6e66f932c4c8d81d64343f429651328758b47d3dbc02c4042f0fff6946a50f4a49037a72bb550f3a7872363a83e1b9ee6469856c24eb4ef80b7535bcf99c0004f9015bf90150f84d846321163782115c82115db8403155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32f84984010203040101b840312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069dbf8599020010db83c4d001500000000abcdef12820d05820d05b84038643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aacf8599020010db885a308d313198a2e037073488203e78203e8b8408dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df738443b9a355010203b525a138aa34383fec3d2719a0",
+ wantPacket: &Neighbors{
+ Nodes: []Node{
+ {
+ ID: hexPubkey("3155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32"),
+ IP: net.ParseIP("99.33.22.55").To4(),
+ UDP: 4444,
+ TCP: 4445,
+ },
+ {
+ ID: hexPubkey("312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069db"),
+ IP: net.ParseIP("1.2.3.4").To4(),
+ UDP: 1,
+ TCP: 1,
+ },
+ {
+ ID: hexPubkey("38643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aac"),
+ IP: net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
+ UDP: 3333,
+ TCP: 3333,
+ },
+ {
+ ID: hexPubkey("8dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73"),
+ IP: net.ParseIP("2001:db8:85a3:8d3:1319:8a2e:370:7348"),
+ UDP: 999,
+ TCP: 1000,
+ },
+ },
+ Expiration: 1136239445,
+ Rest: []rlp.RawValue{{0x01}, {0x02}, {0x03}},
+ },
+ },
+}
+
+// This test checks that the decoder accepts packets according to EIP-8.
+func TestForwardCompatibility(t *testing.T) {
+ testkey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ wantNodeKey := EncodePubkey(&testkey.PublicKey)
+
+ for _, test := range testPackets {
+ input, err := hex.DecodeString(test.input)
+ if err != nil {
+ t.Fatalf("invalid hex: %s", test.input)
+ }
+ packet, nodekey, _, err := Decode(input)
+ if err != nil {
+ t.Errorf("did not accept packet %s\n%v", test.input, err)
+ continue
+ }
+ if !reflect.DeepEqual(packet, test.wantPacket) {
+ t.Errorf("got %s\nwant %s", spew.Sdump(packet), spew.Sdump(test.wantPacket))
+ }
+ if nodekey != wantNodeKey {
+ t.Errorf("got id %v\nwant id %v", nodekey, wantNodeKey)
+ }
+ }
+}
+
+func hexPubkey(h string) (ret Pubkey) {
+ b, err := hex.DecodeString(h)
+ if err != nil {
+ panic(err)
+ }
+ if len(b) != len(ret) {
+ panic("invalid length")
+ }
+ copy(ret[:], b)
+ return ret
+}
diff --git a/discover/v5_udp.go b/discover/v5_udp.go
new file mode 100644
index 0000000..1e3bf4f
--- /dev/null
+++ b/discover/v5_udp.go
@@ -0,0 +1,858 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdsa"
+ crand "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/p2p/netutil"
+ "github.com/status-im/go-discover/discover/v5wire"
+)
+
+const (
+ lookupRequestLimit = 3 // max requests against a single node during lookup
+ findnodeResultLimit = 16 // applies in FINDNODE handler
+ totalNodesResponseLimit = 5 // applies in waitForNodes
+ nodesResponseItemLimit = 3 // applies in sendNodes
+
+ respTimeoutV5 = 700 * time.Millisecond
+)
+
+// codecV5 is implemented by v5wire.Codec (and testCodec).
+//
+// The UDPv5 transport is split into two objects: the codec object deals with
+// encoding/decoding and with the handshake; the UDPv5 object handles higher-level concerns.
+type codecV5 interface {
+ // Encode encodes a packet.
+ Encode(enode.ID, string, v5wire.Packet, *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error)
+
+ // decode decodes a packet. It returns a *v5wire.Unknown packet if decryption fails.
+ // The *enode.Node return value is non-nil when the input contains a handshake response.
+ Decode([]byte, string) (enode.ID, *enode.Node, v5wire.Packet, error)
+}
+
+// UDPv5 is the implementation of protocol version 5.
+type UDPv5 struct {
+ // static fields
+ conn UDPConn
+ tab *Table
+ netrestrict *netutil.Netlist
+ priv *ecdsa.PrivateKey
+ localNode *enode.LocalNode
+ db *enode.DB
+ log log.Logger
+ clock mclock.Clock
+ validSchemes enr.IdentityScheme
+
+ // talkreq handler registry
+ trlock sync.Mutex
+ trhandlers map[string]TalkRequestHandler
+
+ // channels into dispatch
+ packetInCh chan ReadPacket
+ readNextCh chan struct{}
+ callCh chan *callV5
+ callDoneCh chan *callV5
+ respTimeoutCh chan *callTimeout
+
+ // state of dispatch
+ codec codecV5
+ activeCallByNode map[enode.ID]*callV5
+ activeCallByAuth map[v5wire.Nonce]*callV5
+ callQueue map[enode.ID][]*callV5
+
+ // shutdown stuff
+ closeOnce sync.Once
+ closeCtx context.Context
+ cancelCloseCtx context.CancelFunc
+ wg sync.WaitGroup
+}
+
+// TalkRequestHandler callback processes a talk request and optionally returns a reply
+type TalkRequestHandler func(enode.ID, *net.UDPAddr, []byte) []byte
+
+// callV5 represents a remote procedure call against another node.
+type callV5 struct {
+ node *enode.Node
+ packet v5wire.Packet
+ responseType byte // expected packet type of response
+ reqid []byte
+ ch chan v5wire.Packet // responses sent here
+ err chan error // errors sent here
+
+ // Valid for active calls only:
+ nonce v5wire.Nonce // nonce of request packet
+ handshakeCount int // # times we attempted handshake for this call
+ challenge *v5wire.Whoareyou // last sent handshake challenge
+ timeout mclock.Timer
+}
+
+// callTimeout is the response timeout event of a call.
+type callTimeout struct {
+ c *callV5
+ timer mclock.Timer
+}
+
+// ListenV5 listens on the given connection.
+func ListenV5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
+ t, err := newUDPv5(conn, ln, cfg)
+ if err != nil {
+ return nil, err
+ }
+ go t.tab.loop()
+ t.wg.Add(2)
+ go t.readLoop()
+ go t.dispatch()
+ return t, nil
+}
+
+// newUDPv5 creates a UDPv5 transport, but doesn't start any goroutines.
+func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
+ closeCtx, cancelCloseCtx := context.WithCancel(context.Background())
+ cfg = cfg.withDefaults()
+ t := &UDPv5{
+ // static fields
+ conn: conn,
+ localNode: ln,
+ db: ln.Database(),
+ netrestrict: cfg.NetRestrict,
+ priv: cfg.PrivateKey,
+ log: cfg.Log,
+ validSchemes: cfg.ValidSchemes,
+ clock: cfg.Clock,
+ trhandlers: make(map[string]TalkRequestHandler),
+ // channels into dispatch
+ packetInCh: make(chan ReadPacket, 1),
+ readNextCh: make(chan struct{}, 1),
+ callCh: make(chan *callV5),
+ callDoneCh: make(chan *callV5),
+ respTimeoutCh: make(chan *callTimeout),
+ // state of dispatch
+ codec: v5wire.NewCodec(ln, cfg.PrivateKey, cfg.Clock),
+ activeCallByNode: make(map[enode.ID]*callV5),
+ activeCallByAuth: make(map[v5wire.Nonce]*callV5),
+ callQueue: make(map[enode.ID][]*callV5),
+ // shutdown
+ closeCtx: closeCtx,
+ cancelCloseCtx: cancelCloseCtx,
+ }
+ tab, err := newTable(t, t.db, cfg.Bootnodes, cfg.Log)
+ if err != nil {
+ return nil, err
+ }
+ t.tab = tab
+ return t, nil
+}
+
+// Self returns the local node record.
+func (t *UDPv5) Self() *enode.Node {
+ return t.localNode.Node()
+}
+
+// Close shuts down packet processing.
+func (t *UDPv5) Close() {
+ t.closeOnce.Do(func() {
+ t.cancelCloseCtx()
+ t.conn.Close()
+ t.wg.Wait()
+ t.tab.close()
+ })
+}
+
+// Ping sends a ping message to the given node.
+func (t *UDPv5) Ping(n *enode.Node) error {
+ _, err := t.ping(n)
+ return err
+}
+
+// Resolve searches for a specific node with the given ID and tries to get the most recent
+// version of the node record for it. It returns n if the node could not be resolved.
+func (t *UDPv5) Resolve(n *enode.Node) *enode.Node {
+ if intable := t.tab.getNode(n.ID()); intable != nil && intable.Seq() > n.Seq() {
+ n = intable
+ }
+ // Try asking directly. This works if the node is still responding on the endpoint we have.
+ if resp, err := t.RequestENR(n); err == nil {
+ return resp
+ }
+ // Otherwise do a network lookup.
+ result := t.Lookup(n.ID())
+ for _, rn := range result {
+ if rn.ID() == n.ID() && rn.Seq() > n.Seq() {
+ return rn
+ }
+ }
+ return n
+}
+
+// AllNodes returns all the nodes stored in the local table.
+func (t *UDPv5) AllNodes() []*enode.Node {
+ t.tab.mutex.Lock()
+ defer t.tab.mutex.Unlock()
+ nodes := make([]*enode.Node, 0)
+
+ for _, b := range &t.tab.buckets {
+ for _, n := range b.entries {
+ nodes = append(nodes, unwrapNode(n))
+ }
+ }
+ return nodes
+}
+
+// LocalNode returns the current local node running the
+// protocol.
+func (t *UDPv5) LocalNode() *enode.LocalNode {
+ return t.localNode
+}
+
+// RegisterTalkHandler adds a handler for 'talk requests'. The handler function is called
+// whenever a request for the given protocol is received and should return the response
+// data or nil.
+func (t *UDPv5) RegisterTalkHandler(protocol string, handler TalkRequestHandler) {
+ t.trlock.Lock()
+ defer t.trlock.Unlock()
+ t.trhandlers[protocol] = handler
+}
+
+// TalkRequest sends a talk request to n and waits for a response.
+func (t *UDPv5) TalkRequest(n *enode.Node, protocol string, request []byte) ([]byte, error) {
+ req := &v5wire.TalkRequest{Protocol: protocol, Message: request}
+ resp := t.call(n, v5wire.TalkResponseMsg, req)
+ defer t.callDone(resp)
+ select {
+ case respMsg := <-resp.ch:
+ return respMsg.(*v5wire.TalkResponse).Message, nil
+ case err := <-resp.err:
+ return nil, err
+ }
+}
+
+// RandomNodes returns an iterator that finds random nodes in the DHT.
+func (t *UDPv5) RandomNodes() enode.Iterator {
+ if t.tab.len() == 0 {
+ // All nodes were dropped, refresh. The very first query will hit this
+ // case and run the bootstrapping logic.
+ <-t.tab.refresh()
+ }
+
+ return newLookupIterator(t.closeCtx, t.newRandomLookup)
+}
+
+// Lookup performs a recursive lookup for the given target.
+// It returns the closest nodes to target.
+func (t *UDPv5) Lookup(target enode.ID) []*enode.Node {
+ return t.newLookup(t.closeCtx, target).run()
+}
+
+// lookupRandom looks up a random target.
+// This is needed to satisfy the transport interface.
+func (t *UDPv5) lookupRandom() []*enode.Node {
+ return t.newRandomLookup(t.closeCtx).run()
+}
+
+// lookupSelf looks up our own node ID.
+// This is needed to satisfy the transport interface.
+func (t *UDPv5) lookupSelf() []*enode.Node {
+ return t.newLookup(t.closeCtx, t.Self().ID()).run()
+}
+
+func (t *UDPv5) newRandomLookup(ctx context.Context) *lookup {
+ var target enode.ID
+ crand.Read(target[:])
+ return t.newLookup(ctx, target)
+}
+
+func (t *UDPv5) newLookup(ctx context.Context, target enode.ID) *lookup {
+ return newLookup(ctx, t.tab, target, func(n *node) ([]*node, error) {
+ return t.lookupWorker(n, target)
+ })
+}
+
+// lookupWorker performs FINDNODE calls against a single node during lookup.
+func (t *UDPv5) lookupWorker(destNode *node, target enode.ID) ([]*node, error) {
+ var (
+ dists = lookupDistances(target, destNode.ID())
+ nodes = nodesByDistance{target: target}
+ err error
+ )
+ var r []*enode.Node
+ r, err = t.findnode(unwrapNode(destNode), dists)
+ if err == errClosed {
+ return nil, err
+ }
+ for _, n := range r {
+ if n.ID() != t.Self().ID() {
+ nodes.push(wrapNode(n), findnodeResultLimit)
+ }
+ }
+ return nodes.entries, err
+}
+
+// lookupDistances computes the distance parameter for FINDNODE calls to dest.
+// It chooses distances adjacent to logdist(target, dest), e.g. for a target
+// with logdist(target, dest) = 255 the result is [255, 256, 254].
+func lookupDistances(target, dest enode.ID) (dists []uint) {
+ td := enode.LogDist(target, dest)
+ dists = append(dists, uint(td))
+ for i := 1; len(dists) < lookupRequestLimit; i++ {
+ if td+i < 256 {
+ dists = append(dists, uint(td+i))
+ }
+ if td-i > 0 {
+ dists = append(dists, uint(td-i))
+ }
+ }
+ return dists
+}
+
+// ping calls PING on a node and waits for a PONG response.
+func (t *UDPv5) ping(n *enode.Node) (uint64, error) {
+ req := &v5wire.Ping{ENRSeq: t.localNode.Node().Seq()}
+ resp := t.call(n, v5wire.PongMsg, req)
+ defer t.callDone(resp)
+
+ select {
+ case pong := <-resp.ch:
+ return pong.(*v5wire.Pong).ENRSeq, nil
+ case err := <-resp.err:
+ return 0, err
+ }
+}
+
+// requestENR requests n's record.
+func (t *UDPv5) RequestENR(n *enode.Node) (*enode.Node, error) {
+ nodes, err := t.findnode(n, []uint{0})
+ if err != nil {
+ return nil, err
+ }
+ if len(nodes) != 1 {
+ return nil, fmt.Errorf("%d nodes in response for distance zero", len(nodes))
+ }
+ return nodes[0], nil
+}
+
+// findnode calls FINDNODE on a node and waits for responses.
+func (t *UDPv5) findnode(n *enode.Node, distances []uint) ([]*enode.Node, error) {
+ resp := t.call(n, v5wire.NodesMsg, &v5wire.Findnode{Distances: distances})
+ return t.waitForNodes(resp, distances)
+}
+
+// waitForNodes waits for NODES responses to the given call.
+func (t *UDPv5) waitForNodes(c *callV5, distances []uint) ([]*enode.Node, error) {
+ defer t.callDone(c)
+
+ var (
+ nodes []*enode.Node
+ seen = make(map[enode.ID]struct{})
+ received, total = 0, -1
+ )
+ for {
+ select {
+ case responseP := <-c.ch:
+ response := responseP.(*v5wire.Nodes)
+ for _, record := range response.Nodes {
+ node, err := t.verifyResponseNode(c, record, distances, seen)
+ if err != nil {
+ t.log.Debug("Invalid record in "+response.Name(), "id", c.node.ID(), "err", err)
+ continue
+ }
+ nodes = append(nodes, node)
+ }
+ if total == -1 {
+ total = min(int(response.Total), totalNodesResponseLimit)
+ }
+ if received++; received == total {
+ return nodes, nil
+ }
+ case err := <-c.err:
+ return nodes, err
+ }
+ }
+}
+
+// verifyResponseNode checks validity of a record in a NODES response.
+func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distances []uint, seen map[enode.ID]struct{}) (*enode.Node, error) {
+ node, err := enode.New(t.validSchemes, r)
+ if err != nil {
+ return nil, err
+ }
+ if err := netutil.CheckRelayIP(c.node.IP(), node.IP()); err != nil {
+ return nil, err
+ }
+ if c.node.UDP() <= 1024 {
+ return nil, errLowPort
+ }
+ if distances != nil {
+ nd := enode.LogDist(c.node.ID(), node.ID())
+ if !containsUint(uint(nd), distances) {
+ return nil, errors.New("does not match any requested distance")
+ }
+ }
+ if _, ok := seen[node.ID()]; ok {
+ return nil, fmt.Errorf("duplicate record")
+ }
+ seen[node.ID()] = struct{}{}
+ return node, nil
+}
+
+func containsUint(x uint, xs []uint) bool {
+ for _, v := range xs {
+ if x == v {
+ return true
+ }
+ }
+ return false
+}
+
+// call sends the given call and sets up a handler for response packets (of message type
+// responseType). Responses are dispatched to the call's response channel.
+func (t *UDPv5) call(node *enode.Node, responseType byte, packet v5wire.Packet) *callV5 {
+ c := &callV5{
+ node: node,
+ packet: packet,
+ responseType: responseType,
+ reqid: make([]byte, 8),
+ ch: make(chan v5wire.Packet, 1),
+ err: make(chan error, 1),
+ }
+ // Assign request ID.
+ crand.Read(c.reqid)
+ packet.SetRequestID(c.reqid)
+ // Send call to dispatch.
+ select {
+ case t.callCh <- c:
+ case <-t.closeCtx.Done():
+ c.err <- errClosed
+ }
+ return c
+}
+
+// callDone tells dispatch that the active call is done.
+func (t *UDPv5) callDone(c *callV5) {
+ // This needs a loop because further responses may be incoming until the
+ // send to callDoneCh has completed. Such responses need to be discarded
+ // in order to avoid blocking the dispatch loop.
+ for {
+ select {
+ case <-c.ch:
+ // late response, discard.
+ case <-c.err:
+ // late error, discard.
+ case t.callDoneCh <- c:
+ return
+ case <-t.closeCtx.Done():
+ return
+ }
+ }
+}
+
+// dispatch runs in its own goroutine, handles incoming packets and deals with calls.
+//
+// For any destination node there is at most one 'active call', stored in the t.activeCall*
+// maps. A call is made active when it is sent. The active call can be answered by a
+// matching response, in which case c.ch receives the response; or by timing out, in which case
+// c.err receives the error. When the function that created the call signals the active
+// call is done through callDone, the next call from the call queue is started.
+//
+// Calls may also be answered by a WHOAREYOU packet referencing the call packet's authTag.
+// When that happens the call is simply re-sent to complete the handshake. We allow one
+// handshake attempt per call.
+func (t *UDPv5) dispatch() {
+ defer t.wg.Done()
+
+ // Arm first read.
+ t.readNextCh <- struct{}{}
+
+ for {
+ select {
+ case c := <-t.callCh:
+ id := c.node.ID()
+ t.callQueue[id] = append(t.callQueue[id], c)
+ t.sendNextCall(id)
+
+ case ct := <-t.respTimeoutCh:
+ active := t.activeCallByNode[ct.c.node.ID()]
+ if ct.c == active && ct.timer == active.timeout {
+ ct.c.err <- errTimeout
+ }
+
+ case c := <-t.callDoneCh:
+ id := c.node.ID()
+ active := t.activeCallByNode[id]
+ if active != c {
+ panic("BUG: callDone for inactive call")
+ }
+ c.timeout.Stop()
+ delete(t.activeCallByAuth, c.nonce)
+ delete(t.activeCallByNode, id)
+ t.sendNextCall(id)
+
+ case p := <-t.packetInCh:
+ t.handlePacket(p.Data, p.Addr)
+ // Arm next read.
+ t.readNextCh <- struct{}{}
+
+ case <-t.closeCtx.Done():
+ close(t.readNextCh)
+ for id, queue := range t.callQueue {
+ for _, c := range queue {
+ c.err <- errClosed
+ }
+ delete(t.callQueue, id)
+ }
+ for id, c := range t.activeCallByNode {
+ c.err <- errClosed
+ delete(t.activeCallByNode, id)
+ delete(t.activeCallByAuth, c.nonce)
+ }
+ return
+ }
+ }
+}
+
+// startResponseTimeout sets the response timer for a call.
+func (t *UDPv5) startResponseTimeout(c *callV5) {
+ if c.timeout != nil {
+ c.timeout.Stop()
+ }
+ var (
+ timer mclock.Timer
+ done = make(chan struct{})
+ )
+ timer = t.clock.AfterFunc(respTimeoutV5, func() {
+ <-done
+ select {
+ case t.respTimeoutCh <- &callTimeout{c, timer}:
+ case <-t.closeCtx.Done():
+ }
+ })
+ c.timeout = timer
+ close(done)
+}
+
+// sendNextCall sends the next call in the call queue if there is no active call.
+func (t *UDPv5) sendNextCall(id enode.ID) {
+ queue := t.callQueue[id]
+ if len(queue) == 0 || t.activeCallByNode[id] != nil {
+ return
+ }
+ t.activeCallByNode[id] = queue[0]
+ t.sendCall(t.activeCallByNode[id])
+ if len(queue) == 1 {
+ delete(t.callQueue, id)
+ } else {
+ copy(queue, queue[1:])
+ t.callQueue[id] = queue[:len(queue)-1]
+ }
+}
+
+// sendCall encodes and sends a request packet to the call's recipient node.
+// This performs a handshake if needed.
+func (t *UDPv5) sendCall(c *callV5) {
+ // The call might have a nonce from a previous handshake attempt. Remove the entry for
+ // the old nonce because we're about to generate a new nonce for this call.
+ if c.nonce != (v5wire.Nonce{}) {
+ delete(t.activeCallByAuth, c.nonce)
+ }
+
+ addr := &net.UDPAddr{IP: c.node.IP(), Port: c.node.UDP()}
+ newNonce, _ := t.send(c.node.ID(), addr, c.packet, c.challenge)
+ c.nonce = newNonce
+ t.activeCallByAuth[newNonce] = c
+ t.startResponseTimeout(c)
+}
+
+// sendResponse sends a response packet to the given node.
+// This doesn't trigger a handshake even if no keys are available.
+func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet) error {
+ _, err := t.send(toID, toAddr, packet, nil)
+ return err
+}
+
+// send sends a packet to the given node.
+func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet, c *v5wire.Whoareyou) (v5wire.Nonce, error) {
+ addr := toAddr.String()
+ enc, nonce, err := t.codec.Encode(toID, addr, packet, c)
+ if err != nil {
+ t.log.Warn(">> "+packet.Name(), "id", toID, "addr", addr, "err", err)
+ return nonce, err
+ }
+ _, err = t.conn.WriteToUDP(enc, toAddr)
+ t.log.Trace(">> "+packet.Name(), "id", toID, "addr", addr)
+ return nonce, err
+}
+
+// readLoop runs in its own goroutine and reads packets from the network.
+func (t *UDPv5) readLoop() {
+ defer t.wg.Done()
+
+ buf := make([]byte, maxPacketSize)
+ for range t.readNextCh {
+ nbytes, from, err := t.conn.ReadFromUDP(buf)
+ if netutil.IsTemporaryError(err) {
+ // Ignore temporary read errors.
+ t.log.Debug("Temporary UDP read error", "err", err)
+ continue
+ } else if err != nil {
+ // Shut down the loop for permament errors.
+ if err != io.EOF {
+ t.log.Debug("UDP read error", "err", err)
+ }
+ return
+ }
+ t.dispatchReadPacket(from, buf[:nbytes])
+ }
+}
+
+// dispatchReadPacket sends a packet into the dispatch loop.
+func (t *UDPv5) dispatchReadPacket(from *net.UDPAddr, content []byte) bool {
+ select {
+ case t.packetInCh <- ReadPacket{content, from}:
+ return true
+ case <-t.closeCtx.Done():
+ return false
+ }
+}
+
+// handlePacket decodes and processes an incoming packet from the network.
+func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
+ addr := fromAddr.String()
+ fromID, fromNode, packet, err := t.codec.Decode(rawpacket, addr)
+ if err != nil {
+ t.log.Debug("Bad discv5 packet", "id", fromID, "addr", addr, "err", err)
+ return err
+ }
+ if fromNode != nil {
+ // Handshake succeeded, add to table.
+ t.tab.addSeenNode(wrapNode(fromNode))
+ }
+ if packet.Kind() != v5wire.WhoareyouPacket {
+ // WHOAREYOU logged separately to report errors.
+ t.log.Trace("<< "+packet.Name(), "id", fromID, "addr", addr)
+ }
+ t.handle(packet, fromID, fromAddr)
+ return nil
+}
+
+// handleCallResponse dispatches a response packet to the call waiting for it.
+func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, p v5wire.Packet) bool {
+ ac := t.activeCallByNode[fromID]
+ if ac == nil || !bytes.Equal(p.RequestID(), ac.reqid) {
+ t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.Name()), "id", fromID, "addr", fromAddr)
+ return false
+ }
+ if !fromAddr.IP.Equal(ac.node.IP()) || fromAddr.Port != ac.node.UDP() {
+ t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.Name()), "id", fromID, "addr", fromAddr)
+ return false
+ }
+ if p.Kind() != ac.responseType {
+ t.log.Debug(fmt.Sprintf("Wrong discv5 response type %s", p.Name()), "id", fromID, "addr", fromAddr)
+ return false
+ }
+ t.startResponseTimeout(ac)
+ ac.ch <- p
+ return true
+}
+
+// getNode looks for a node record in table and database.
+func (t *UDPv5) getNode(id enode.ID) *enode.Node {
+ if n := t.tab.getNode(id); n != nil {
+ return n
+ }
+ if n := t.localNode.Database().Node(id); n != nil {
+ return n
+ }
+ return nil
+}
+
+// handle processes incoming packets according to their message type.
+func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) {
+ switch p := p.(type) {
+ case *v5wire.Unknown:
+ t.handleUnknown(p, fromID, fromAddr)
+ case *v5wire.Whoareyou:
+ t.handleWhoareyou(p, fromID, fromAddr)
+ case *v5wire.Ping:
+ t.handlePing(p, fromID, fromAddr)
+ case *v5wire.Pong:
+ if t.handleCallResponse(fromID, fromAddr, p) {
+ t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)})
+ }
+ case *v5wire.Findnode:
+ t.handleFindnode(p, fromID, fromAddr)
+ case *v5wire.Nodes:
+ t.handleCallResponse(fromID, fromAddr, p)
+ case *v5wire.TalkRequest:
+ t.handleTalkRequest(p, fromID, fromAddr)
+ case *v5wire.TalkResponse:
+ t.handleCallResponse(fromID, fromAddr, p)
+ }
+}
+
+// handleUnknown initiates a handshake by responding with WHOAREYOU.
+func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr *net.UDPAddr) {
+ challenge := &v5wire.Whoareyou{Nonce: p.Nonce}
+ crand.Read(challenge.IDNonce[:])
+ if n := t.getNode(fromID); n != nil {
+ challenge.Node = n
+ challenge.RecordSeq = n.Seq()
+ }
+ t.sendResponse(fromID, fromAddr, challenge)
+}
+
+var (
+ errChallengeNoCall = errors.New("no matching call")
+ errChallengeTwice = errors.New("second handshake")
+)
+
+// handleWhoareyou resends the active call as a handshake packet.
+func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) {
+ c, err := t.matchWithCall(fromID, p.Nonce)
+ if err != nil {
+ t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err)
+ return
+ }
+
+ // Resend the call that was answered by WHOAREYOU.
+ t.log.Trace("<< "+p.Name(), "id", c.node.ID(), "addr", fromAddr)
+ c.handshakeCount++
+ c.challenge = p
+ p.Node = c.node
+ t.sendCall(c)
+}
+
+// matchWithCall checks whether a handshake attempt matches the active call.
+func (t *UDPv5) matchWithCall(fromID enode.ID, nonce v5wire.Nonce) (*callV5, error) {
+ c := t.activeCallByAuth[nonce]
+ if c == nil {
+ return nil, errChallengeNoCall
+ }
+ if c.handshakeCount > 0 {
+ return nil, errChallengeTwice
+ }
+ return c, nil
+}
+
+// handlePing sends a PONG response.
+func (t *UDPv5) handlePing(p *v5wire.Ping, fromID enode.ID, fromAddr *net.UDPAddr) {
+ remoteIP := fromAddr.IP
+ // Handle IPv4 mapped IPv6 addresses in the
+ // event the local node is binded to an
+ // ipv6 interface.
+ if remoteIP.To4() != nil {
+ remoteIP = remoteIP.To4()
+ }
+ t.sendResponse(fromID, fromAddr, &v5wire.Pong{
+ ReqID: p.ReqID,
+ ToIP: remoteIP,
+ ToPort: uint16(fromAddr.Port),
+ ENRSeq: t.localNode.Node().Seq(),
+ })
+}
+
+// handleFindnode returns nodes to the requester.
+func (t *UDPv5) handleFindnode(p *v5wire.Findnode, fromID enode.ID, fromAddr *net.UDPAddr) {
+ nodes := t.collectTableNodes(fromAddr.IP, p.Distances, findnodeResultLimit)
+ for _, resp := range packNodes(p.ReqID, nodes) {
+ t.sendResponse(fromID, fromAddr, resp)
+ }
+}
+
+// collectTableNodes creates a FINDNODE result set for the given distances.
+func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*enode.Node {
+ var nodes []*enode.Node
+ var processed = make(map[uint]struct{})
+ for _, dist := range distances {
+ // Reject duplicate / invalid distances.
+ _, seen := processed[dist]
+ if seen || dist > 256 {
+ continue
+ }
+
+ // Get the nodes.
+ var bn []*enode.Node
+ if dist == 0 {
+ bn = []*enode.Node{t.Self()}
+ } else if dist <= 256 {
+ t.tab.mutex.Lock()
+ bn = unwrapNodes(t.tab.bucketAtDistance(int(dist)).entries)
+ t.tab.mutex.Unlock()
+ }
+ processed[dist] = struct{}{}
+
+ // Apply some pre-checks to avoid sending invalid nodes.
+ for _, n := range bn {
+ // TODO livenessChecks > 1
+ if netutil.CheckRelayIP(rip, n.IP()) != nil {
+ continue
+ }
+ nodes = append(nodes, n)
+ if len(nodes) >= limit {
+ return nodes
+ }
+ }
+ }
+ return nodes
+}
+
+// packNodes creates NODES response packets for the given node list.
+func packNodes(reqid []byte, nodes []*enode.Node) []*v5wire.Nodes {
+ if len(nodes) == 0 {
+ return []*v5wire.Nodes{{ReqID: reqid, Total: 1}}
+ }
+
+ total := uint8(math.Ceil(float64(len(nodes)) / 3))
+ var resp []*v5wire.Nodes
+ for len(nodes) > 0 {
+ p := &v5wire.Nodes{ReqID: reqid, Total: total}
+ items := min(nodesResponseItemLimit, len(nodes))
+ for i := 0; i < items; i++ {
+ p.Nodes = append(p.Nodes, nodes[i].Record())
+ }
+ nodes = nodes[items:]
+ resp = append(resp, p)
+ }
+ return resp
+}
+
+// handleTalkRequest runs the talk request handler of the requested protocol.
+func (t *UDPv5) handleTalkRequest(p *v5wire.TalkRequest, fromID enode.ID, fromAddr *net.UDPAddr) {
+ t.trlock.Lock()
+ handler := t.trhandlers[p.Protocol]
+ t.trlock.Unlock()
+
+ var response []byte
+ if handler != nil {
+ response = handler(fromID, fromAddr, p.Message)
+ }
+ resp := &v5wire.TalkResponse{ReqID: p.ReqID, Message: response}
+ t.sendResponse(fromID, fromAddr, resp)
+}
diff --git a/discover/v5_udp_test.go b/discover/v5_udp_test.go
new file mode 100644
index 0000000..02516b2
--- /dev/null
+++ b/discover/v5_udp_test.go
@@ -0,0 +1,809 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package discover
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "encoding/binary"
+ "fmt"
+ "math/rand"
+ "net"
+ "reflect"
+ "sort"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/status-im/go-discover/discover/v5wire"
+ "github.com/status-im/go-discover/internal/testlog"
+)
+
+// Real sockets, real crypto: this test checks end-to-end connectivity for UDPv5.
+func TestUDPv5_lookupE2E(t *testing.T) {
+ t.Parallel()
+
+ const N = 5
+ var nodes []*UDPv5
+ for i := 0; i < N; i++ {
+ var cfg Config
+ if len(nodes) > 0 {
+ bn := nodes[0].Self()
+ cfg.Bootnodes = []*enode.Node{bn}
+ }
+ node := startLocalhostV5(t, cfg)
+ nodes = append(nodes, node)
+ defer node.Close()
+ }
+ last := nodes[N-1]
+ target := nodes[rand.Intn(N-2)].Self()
+
+ // It is expected that all nodes can be found.
+ expectedResult := make([]*enode.Node, len(nodes))
+ for i := range nodes {
+ expectedResult[i] = nodes[i].Self()
+ }
+ sort.Slice(expectedResult, func(i, j int) bool {
+ return enode.DistCmp(target.ID(), expectedResult[i].ID(), expectedResult[j].ID()) < 0
+ })
+
+ // Do the lookup.
+ results := last.Lookup(target.ID())
+ if err := checkNodesEqual(results, expectedResult); err != nil {
+ t.Fatalf("lookup returned wrong results: %v", err)
+ }
+}
+
+func startLocalhostV5(t *testing.T, cfg Config) *UDPv5 {
+ cfg.PrivateKey = newkey()
+ db, _ := enode.OpenDB("")
+ ln := enode.NewLocalNode(db, cfg.PrivateKey)
+
+ // Prefix logs with node ID.
+ lprefix := fmt.Sprintf("(%s)", ln.ID().TerminalString())
+ lfmt := log.TerminalFormat(false)
+ cfg.Log = testlog.Logger(t, log.LvlTrace)
+ cfg.Log.SetHandler(log.FuncHandler(func(r *log.Record) error {
+ t.Logf("%s %s", lprefix, lfmt.Format(r))
+ return nil
+ }))
+
+ // Listen.
+ socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ realaddr := socket.LocalAddr().(*net.UDPAddr)
+ ln.SetStaticIP(realaddr.IP)
+ ln.Set(enr.UDP(realaddr.Port))
+ udp, err := ListenV5(socket, ln, cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return udp
+}
+
+// This test checks that incoming PING calls are handled correctly.
+func TestUDPv5_pingHandling(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ test.packetIn(&v5wire.Ping{ReqID: []byte("foo")})
+ test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if !bytes.Equal(p.ReqID, []byte("foo")) {
+ t.Error("wrong request ID in response:", p.ReqID)
+ }
+ if p.ENRSeq != test.table.self().Seq() {
+ t.Error("wrong ENR sequence number in response:", p.ENRSeq)
+ }
+ })
+}
+
+// This test checks that incoming 'unknown' packets trigger the handshake.
+func TestUDPv5_unknownPacket(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ nonce := v5wire.Nonce{1, 2, 3}
+ check := func(p *v5wire.Whoareyou, wantSeq uint64) {
+ t.Helper()
+ if p.Nonce != nonce {
+ t.Error("wrong nonce in WHOAREYOU:", p.Nonce, nonce)
+ }
+ if p.IDNonce == ([16]byte{}) {
+ t.Error("all zero ID nonce")
+ }
+ if p.RecordSeq != wantSeq {
+ t.Errorf("wrong record seq %d in WHOAREYOU, want %d", p.RecordSeq, wantSeq)
+ }
+ }
+
+ // Unknown packet from unknown node.
+ test.packetIn(&v5wire.Unknown{Nonce: nonce})
+ test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
+ check(p, 0)
+ })
+
+ // Make node known.
+ n := test.getNode(test.remotekey, test.remoteaddr).Node()
+ test.table.addSeenNode(wrapNode(n))
+
+ test.packetIn(&v5wire.Unknown{Nonce: nonce})
+ test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
+ check(p, n.Seq())
+ })
+}
+
+// This test checks that incoming FINDNODE calls are handled correctly.
+func TestUDPv5_findnodeHandling(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ // Create test nodes and insert them into the table.
+ nodes253 := nodesAtDistance(test.table.self().ID(), 253, 10)
+ nodes249 := nodesAtDistance(test.table.self().ID(), 249, 4)
+ nodes248 := nodesAtDistance(test.table.self().ID(), 248, 10)
+ fillTable(test.table, wrapNodes(nodes253))
+ fillTable(test.table, wrapNodes(nodes249))
+ fillTable(test.table, wrapNodes(nodes248))
+
+ // Requesting with distance zero should return the node's own record.
+ test.packetIn(&v5wire.Findnode{ReqID: []byte{0}, Distances: []uint{0}})
+ test.expectNodes([]byte{0}, 1, []*enode.Node{test.udp.Self()})
+
+ // Requesting with distance > 256 shouldn't crash.
+ test.packetIn(&v5wire.Findnode{ReqID: []byte{1}, Distances: []uint{4234098}})
+ test.expectNodes([]byte{1}, 1, nil)
+
+ // Requesting with empty distance list shouldn't crash either.
+ test.packetIn(&v5wire.Findnode{ReqID: []byte{2}, Distances: []uint{}})
+ test.expectNodes([]byte{2}, 1, nil)
+
+ // This request gets no nodes because the corresponding bucket is empty.
+ test.packetIn(&v5wire.Findnode{ReqID: []byte{3}, Distances: []uint{254}})
+ test.expectNodes([]byte{3}, 1, nil)
+
+ // This request gets all the distance-253 nodes.
+ test.packetIn(&v5wire.Findnode{ReqID: []byte{4}, Distances: []uint{253}})
+ test.expectNodes([]byte{4}, 4, nodes253)
+
+ // This request gets all the distance-249 nodes and some more at 248 because
+ // the bucket at 249 is not full.
+ test.packetIn(&v5wire.Findnode{ReqID: []byte{5}, Distances: []uint{249, 248}})
+ var nodes []*enode.Node
+ nodes = append(nodes, nodes249...)
+ nodes = append(nodes, nodes248[:10]...)
+ test.expectNodes([]byte{5}, 5, nodes)
+}
+
+func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes []*enode.Node) {
+ nodeSet := make(map[enode.ID]*enr.Record)
+ for _, n := range wantNodes {
+ nodeSet[n.ID()] = n.Record()
+ }
+
+ for {
+ test.waitPacketOut(func(p *v5wire.Nodes, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if !bytes.Equal(p.ReqID, wantReqID) {
+ test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID)
+ }
+ if len(p.Nodes) > 3 {
+ test.t.Fatalf("too many nodes in response")
+ }
+ if p.Total != wantTotal {
+ test.t.Fatalf("wrong total response count %d, want %d", p.Total, wantTotal)
+ }
+ for _, record := range p.Nodes {
+ n, _ := enode.New(enode.ValidSchemesForTesting, record)
+ want := nodeSet[n.ID()]
+ if want == nil {
+ test.t.Fatalf("unexpected node in response: %v", n)
+ }
+ if !reflect.DeepEqual(record, want) {
+ test.t.Fatalf("wrong record in response: %v", n)
+ }
+ delete(nodeSet, n.ID())
+ }
+ })
+ if len(nodeSet) == 0 {
+ return
+ }
+ }
+}
+
+// This test checks that outgoing PING calls work.
+func TestUDPv5_pingCall(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ remote := test.getNode(test.remotekey, test.remoteaddr).Node()
+ done := make(chan error, 1)
+
+ // This ping times out.
+ go func() {
+ _, err := test.udp.ping(remote)
+ done <- err
+ }()
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {})
+ if err := <-done; err != errTimeout {
+ t.Fatalf("want errTimeout, got %q", err)
+ }
+
+ // This ping works.
+ go func() {
+ _, err := test.udp.ping(remote)
+ done <- err
+ }()
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
+ test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.Pong{ReqID: p.ReqID})
+ })
+ if err := <-done; err != nil {
+ t.Fatal(err)
+ }
+
+ // This ping gets a reply from the wrong endpoint.
+ go func() {
+ _, err := test.udp.ping(remote)
+ done <- err
+ }()
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
+ wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 55, 22}, Port: 10101}
+ test.packetInFrom(test.remotekey, wrongAddr, &v5wire.Pong{ReqID: p.ReqID})
+ })
+ if err := <-done; err != errTimeout {
+ t.Fatalf("want errTimeout for reply from wrong IP, got %q", err)
+ }
+}
+
+// This test checks that outgoing FINDNODE calls work and multiple NODES
+// replies are aggregated.
+func TestUDPv5_findnodeCall(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ // Launch the request:
+ var (
+ distances = []uint{230}
+ remote = test.getNode(test.remotekey, test.remoteaddr).Node()
+ nodes = nodesAtDistance(remote.ID(), int(distances[0]), 8)
+ done = make(chan error, 1)
+ response []*enode.Node
+ )
+ go func() {
+ var err error
+ response, err = test.udp.findnode(remote, distances)
+ done <- err
+ }()
+
+ // Serve the responses:
+ test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if !reflect.DeepEqual(p.Distances, distances) {
+ t.Fatalf("wrong distances in request: %v", p.Distances)
+ }
+ test.packetIn(&v5wire.Nodes{
+ ReqID: p.ReqID,
+ Total: 2,
+ Nodes: nodesToRecords(nodes[:4]),
+ })
+ test.packetIn(&v5wire.Nodes{
+ ReqID: p.ReqID,
+ Total: 2,
+ Nodes: nodesToRecords(nodes[4:]),
+ })
+ })
+
+ // Check results:
+ if err := <-done; err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !reflect.DeepEqual(response, nodes) {
+ t.Fatalf("wrong nodes in response")
+ }
+
+ // TODO: check invalid IPs
+ // TODO: check invalid/unsigned record
+}
+
+// This test checks that pending calls are re-sent when a handshake happens.
+func TestUDPv5_callResend(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ remote := test.getNode(test.remotekey, test.remoteaddr).Node()
+ done := make(chan error, 2)
+ go func() {
+ _, err := test.udp.ping(remote)
+ done <- err
+ }()
+ go func() {
+ _, err := test.udp.ping(remote)
+ done <- err
+ }()
+
+ // Ping answered by WHOAREYOU.
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) {
+ test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
+ })
+ // Ping should be re-sent.
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
+ test.packetIn(&v5wire.Pong{ReqID: p.ReqID})
+ })
+ // Answer the other ping.
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
+ test.packetIn(&v5wire.Pong{ReqID: p.ReqID})
+ })
+ if err := <-done; err != nil {
+ t.Fatalf("unexpected ping error: %v", err)
+ }
+ if err := <-done; err != nil {
+ t.Fatalf("unexpected ping error: %v", err)
+ }
+}
+
+// This test ensures we don't allow multiple rounds of WHOAREYOU for a single call.
+func TestUDPv5_multipleHandshakeRounds(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ remote := test.getNode(test.remotekey, test.remoteaddr).Node()
+ done := make(chan error, 1)
+ go func() {
+ _, err := test.udp.ping(remote)
+ done <- err
+ }()
+
+ // Ping answered by WHOAREYOU.
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) {
+ test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
+ })
+ // Ping answered by WHOAREYOU again.
+ test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) {
+ test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
+ })
+ if err := <-done; err != errTimeout {
+ t.Fatalf("unexpected ping error: %q", err)
+ }
+}
+
+// This test checks that calls with n replies may take up to n * respTimeout.
+func TestUDPv5_callTimeoutReset(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ // Launch the request:
+ var (
+ distance = uint(230)
+ remote = test.getNode(test.remotekey, test.remoteaddr).Node()
+ nodes = nodesAtDistance(remote.ID(), int(distance), 8)
+ done = make(chan error, 1)
+ )
+ go func() {
+ _, err := test.udp.findnode(remote, []uint{distance})
+ done <- err
+ }()
+
+ // Serve two responses, slowly.
+ test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) {
+ time.Sleep(respTimeout - 50*time.Millisecond)
+ test.packetIn(&v5wire.Nodes{
+ ReqID: p.ReqID,
+ Total: 2,
+ Nodes: nodesToRecords(nodes[:4]),
+ })
+
+ time.Sleep(respTimeout - 50*time.Millisecond)
+ test.packetIn(&v5wire.Nodes{
+ ReqID: p.ReqID,
+ Total: 2,
+ Nodes: nodesToRecords(nodes[4:]),
+ })
+ })
+ if err := <-done; err != nil {
+ t.Fatalf("unexpected error: %q", err)
+ }
+}
+
+// This test checks that TALKREQ calls the registered handler function.
+func TestUDPv5_talkHandling(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ var recvMessage []byte
+ test.udp.RegisterTalkHandler("test", func(id enode.ID, addr *net.UDPAddr, message []byte) []byte {
+ recvMessage = message
+ return []byte("test response")
+ })
+
+ // Successful case:
+ test.packetIn(&v5wire.TalkRequest{
+ ReqID: []byte("foo"),
+ Protocol: "test",
+ Message: []byte("test request"),
+ })
+ test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if !bytes.Equal(p.ReqID, []byte("foo")) {
+ t.Error("wrong request ID in response:", p.ReqID)
+ }
+ if string(p.Message) != "test response" {
+ t.Errorf("wrong talk response message: %q", p.Message)
+ }
+ if string(recvMessage) != "test request" {
+ t.Errorf("wrong message received in handler: %q", recvMessage)
+ }
+ })
+
+ // Check that empty response is returned for unregistered protocols.
+ recvMessage = nil
+ test.packetIn(&v5wire.TalkRequest{
+ ReqID: []byte("2"),
+ Protocol: "wrong",
+ Message: []byte("test request"),
+ })
+ test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if !bytes.Equal(p.ReqID, []byte("2")) {
+ t.Error("wrong request ID in response:", p.ReqID)
+ }
+ if string(p.Message) != "" {
+ t.Errorf("wrong talk response message: %q", p.Message)
+ }
+ if recvMessage != nil {
+ t.Errorf("handler was called for wrong protocol: %q", recvMessage)
+ }
+ })
+}
+
+// This test checks that outgoing TALKREQ calls work.
+func TestUDPv5_talkRequest(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ remote := test.getNode(test.remotekey, test.remoteaddr).Node()
+ done := make(chan error, 1)
+
+ // This request times out.
+ go func() {
+ _, err := test.udp.TalkRequest(remote, "test", []byte("test request"))
+ done <- err
+ }()
+ test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {})
+ if err := <-done; err != errTimeout {
+ t.Fatalf("want errTimeout, got %q", err)
+ }
+
+ // This request works.
+ go func() {
+ _, err := test.udp.TalkRequest(remote, "test", []byte("test request"))
+ done <- err
+ }()
+ test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if p.Protocol != "test" {
+ t.Errorf("wrong protocol ID in talk request: %q", p.Protocol)
+ }
+ if string(p.Message) != "test request" {
+ t.Errorf("wrong message talk request: %q", p.Message)
+ }
+ test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.TalkResponse{
+ ReqID: p.ReqID,
+ Message: []byte("test response"),
+ })
+ })
+ if err := <-done; err != nil {
+ t.Fatal(err)
+ }
+}
+
+// This test checks that lookup works.
+func TestUDPv5_lookup(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+
+ // Lookup on empty table returns no nodes.
+ if results := test.udp.Lookup(lookupTestnet.target.id()); len(results) > 0 {
+ t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results)
+ }
+
+ // Ensure the tester knows all nodes in lookupTestnet by IP.
+ for d, nn := range lookupTestnet.dists {
+ for i, key := range nn {
+ n := lookupTestnet.node(d, i)
+ test.getNode(key, &net.UDPAddr{IP: n.IP(), Port: n.UDP()})
+ }
+ }
+
+ // Seed table with initial node.
+ initialNode := lookupTestnet.node(256, 0)
+ fillTable(test.table, []*node{wrapNode(initialNode)})
+
+ // Start the lookup.
+ resultC := make(chan []*enode.Node, 1)
+ go func() {
+ resultC <- test.udp.Lookup(lookupTestnet.target.id())
+ test.close()
+ }()
+
+ // Answer lookup packets.
+ asked := make(map[enode.ID]bool)
+ for done := false; !done; {
+ done = test.waitPacketOut(func(p v5wire.Packet, to *net.UDPAddr, _ v5wire.Nonce) {
+ recipient, key := lookupTestnet.nodeByAddr(to)
+ switch p := p.(type) {
+ case *v5wire.Ping:
+ test.packetInFrom(key, to, &v5wire.Pong{ReqID: p.ReqID})
+ case *v5wire.Findnode:
+ if asked[recipient.ID()] {
+ t.Error("Asked node", recipient.ID(), "twice")
+ }
+ asked[recipient.ID()] = true
+ nodes := lookupTestnet.neighborsAtDistances(recipient, p.Distances, 16)
+ t.Logf("Got FINDNODE for %v, returning %d nodes", p.Distances, len(nodes))
+ for _, resp := range packNodes(p.ReqID, nodes) {
+ test.packetInFrom(key, to, resp)
+ }
+ }
+ })
+ }
+
+ // Verify result nodes.
+ results := <-resultC
+ checkLookupResults(t, lookupTestnet, results)
+}
+
+// This test checks the local node can be utilised to set key-values.
+func TestUDPv5_LocalNode(t *testing.T) {
+ t.Parallel()
+ var cfg Config
+ node := startLocalhostV5(t, cfg)
+ defer node.Close()
+ localNd := node.LocalNode()
+
+ // set value in node's local record
+ testVal := [4]byte{'A', 'B', 'C', 'D'}
+ localNd.Set(enr.WithEntry("testing", &testVal))
+
+ // retrieve the value from self to make sure it matches.
+ outputVal := [4]byte{}
+ if err := node.Self().Load(enr.WithEntry("testing", &outputVal)); err != nil {
+ t.Errorf("Could not load value from record: %v", err)
+ }
+ if testVal != outputVal {
+ t.Errorf("Wanted %#x to be retrieved from the record but instead got %#x", testVal, outputVal)
+ }
+}
+
+func TestUDPv5_PingWithIPV4MappedAddress(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ rawIP := net.IPv4(0xFF, 0x12, 0x33, 0xE5)
+ test.remoteaddr = &net.UDPAddr{
+ IP: rawIP.To16(),
+ Port: 0,
+ }
+ remote := test.getNode(test.remotekey, test.remoteaddr).Node()
+ done := make(chan struct{}, 1)
+
+ // This handler will truncate the ipv4-mapped in ipv6 address.
+ go func() {
+ test.udp.handlePing(&v5wire.Ping{ENRSeq: 1}, remote.ID(), test.remoteaddr)
+ done <- struct{}{}
+ }()
+ test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) {
+ if len(p.ToIP) == net.IPv6len {
+ t.Error("Received untruncated ip address")
+ }
+ if len(p.ToIP) != net.IPv4len {
+ t.Errorf("Received ip address with incorrect length: %d", len(p.ToIP))
+ }
+ if !p.ToIP.Equal(rawIP) {
+ t.Errorf("Received incorrect ip address: wanted %s but received %s", rawIP.String(), p.ToIP.String())
+ }
+ })
+ <-done
+}
+
+// udpV5Test is the framework for all tests above.
+// It runs the UDPv5 transport on a virtual socket and allows testing outgoing packets.
+type udpV5Test struct {
+ t *testing.T
+ pipe *dgramPipe
+ table *Table
+ db *enode.DB
+ udp *UDPv5
+ localkey, remotekey *ecdsa.PrivateKey
+ remoteaddr *net.UDPAddr
+ nodesByID map[enode.ID]*enode.LocalNode
+ nodesByIP map[string]*enode.LocalNode
+}
+
+// testCodec is the packet encoding used by protocol tests. This codec does not perform encryption.
+type testCodec struct {
+ test *udpV5Test
+ id enode.ID
+ ctr uint64
+}
+
+type testCodecFrame struct {
+ NodeID enode.ID
+ AuthTag v5wire.Nonce
+ Ptype byte
+ Packet rlp.RawValue
+}
+
+func (c *testCodec) Encode(toID enode.ID, addr string, p v5wire.Packet, _ *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error) {
+ c.ctr++
+ var authTag v5wire.Nonce
+ binary.BigEndian.PutUint64(authTag[:], c.ctr)
+
+ penc, _ := rlp.EncodeToBytes(p)
+ frame, err := rlp.EncodeToBytes(testCodecFrame{c.id, authTag, p.Kind(), penc})
+ return frame, authTag, err
+}
+
+func (c *testCodec) Decode(input []byte, addr string) (enode.ID, *enode.Node, v5wire.Packet, error) {
+ frame, p, err := c.decodeFrame(input)
+ if err != nil {
+ return enode.ID{}, nil, nil, err
+ }
+ return frame.NodeID, nil, p, nil
+}
+
+func (c *testCodec) decodeFrame(input []byte) (frame testCodecFrame, p v5wire.Packet, err error) {
+ if err = rlp.DecodeBytes(input, &frame); err != nil {
+ return frame, nil, fmt.Errorf("invalid frame: %v", err)
+ }
+ switch frame.Ptype {
+ case v5wire.UnknownPacket:
+ dec := new(v5wire.Unknown)
+ err = rlp.DecodeBytes(frame.Packet, &dec)
+ p = dec
+ case v5wire.WhoareyouPacket:
+ dec := new(v5wire.Whoareyou)
+ err = rlp.DecodeBytes(frame.Packet, &dec)
+ p = dec
+ default:
+ p, err = v5wire.DecodeMessage(frame.Ptype, frame.Packet)
+ }
+ return frame, p, err
+}
+
+func newUDPV5Test(t *testing.T) *udpV5Test {
+ test := &udpV5Test{
+ t: t,
+ pipe: newpipe(),
+ localkey: newkey(),
+ remotekey: newkey(),
+ remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303},
+ nodesByID: make(map[enode.ID]*enode.LocalNode),
+ nodesByIP: make(map[string]*enode.LocalNode),
+ }
+ test.db, _ = enode.OpenDB("")
+ ln := enode.NewLocalNode(test.db, test.localkey)
+ ln.SetStaticIP(net.IP{10, 0, 0, 1})
+ ln.Set(enr.UDP(30303))
+ test.udp, _ = ListenV5(test.pipe, ln, Config{
+ PrivateKey: test.localkey,
+ Log: testlog.Logger(t, log.LvlTrace),
+ ValidSchemes: enode.ValidSchemesForTesting,
+ })
+ test.udp.codec = &testCodec{test: test, id: ln.ID()}
+ test.table = test.udp.tab
+ test.nodesByID[ln.ID()] = ln
+ // Wait for initial refresh so the table doesn't send unexpected findnode.
+ <-test.table.initDone
+ return test
+}
+
+// handles a packet as if it had been sent to the transport.
+func (test *udpV5Test) packetIn(packet v5wire.Packet) {
+ test.t.Helper()
+ test.packetInFrom(test.remotekey, test.remoteaddr, packet)
+}
+
+// handles a packet as if it had been sent to the transport by the key/endpoint.
+func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, packet v5wire.Packet) {
+ test.t.Helper()
+
+ ln := test.getNode(key, addr)
+ codec := &testCodec{test: test, id: ln.ID()}
+ enc, _, err := codec.Encode(test.udp.Self().ID(), addr.String(), packet, nil)
+ if err != nil {
+ test.t.Errorf("%s encode error: %v", packet.Name(), err)
+ }
+ if test.udp.dispatchReadPacket(addr, enc) {
+ <-test.udp.readNextCh // unblock UDPv5.dispatch
+ }
+}
+
+// getNode ensures the test knows about a node at the given endpoint.
+func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *enode.LocalNode {
+ id := encodePubkey(&key.PublicKey).id()
+ ln := test.nodesByID[id]
+ if ln == nil {
+ db, _ := enode.OpenDB("")
+ ln = enode.NewLocalNode(db, key)
+ ln.SetStaticIP(addr.IP)
+ ln.Set(enr.UDP(addr.Port))
+ test.nodesByID[id] = ln
+ }
+ test.nodesByIP[string(addr.IP)] = ln
+ return ln
+}
+
+// waitPacketOut waits for the next output packet and handles it using the given 'validate'
+// function. The function must be of type func (X, *net.UDPAddr, v5wire.Nonce) where X is
+// assignable to packetV5.
+func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) {
+ test.t.Helper()
+
+ fn := reflect.ValueOf(validate)
+ exptype := fn.Type().In(0)
+
+ dgram, err := test.pipe.receive()
+ if err == errClosed {
+ return true
+ }
+ if err == errTimeout {
+ test.t.Fatalf("timed out waiting for %v", exptype)
+ return false
+ }
+ ln := test.nodesByIP[string(dgram.to.IP)]
+ if ln == nil {
+ test.t.Fatalf("attempt to send to non-existing node %v", &dgram.to)
+ return false
+ }
+ codec := &testCodec{test: test, id: ln.ID()}
+ frame, p, err := codec.decodeFrame(dgram.data)
+ if err != nil {
+ test.t.Errorf("sent packet decode error: %v", err)
+ return false
+ }
+ if !reflect.TypeOf(p).AssignableTo(exptype) {
+ test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
+ return false
+ }
+ fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(&dgram.to), reflect.ValueOf(frame.AuthTag)})
+ return false
+}
+
+func (test *udpV5Test) close() {
+ test.t.Helper()
+
+ test.udp.Close()
+ test.db.Close()
+ for id, n := range test.nodesByID {
+ if id != test.udp.Self().ID() {
+ n.Database().Close()
+ }
+ }
+ if len(test.pipe.queue) != 0 {
+ test.t.Fatalf("%d unmatched UDP packets in queue", len(test.pipe.queue))
+ }
+}
diff --git a/discover/v5wire/crypto.go b/discover/v5wire/crypto.go
new file mode 100644
index 0000000..fc0a0ed
--- /dev/null
+++ b/discover/v5wire/crypto.go
@@ -0,0 +1,180 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v5wire
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "errors"
+ "fmt"
+ "hash"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "golang.org/x/crypto/hkdf"
+)
+
+const (
+ // Encryption/authentication parameters.
+ aesKeySize = 16
+ gcmNonceSize = 12
+)
+
+// Nonce represents a nonce used for AES/GCM.
+type Nonce [gcmNonceSize]byte
+
+// EncodePubkey encodes a public key.
+func EncodePubkey(key *ecdsa.PublicKey) []byte {
+ switch key.Curve {
+ case crypto.S256():
+ return crypto.CompressPubkey(key)
+ default:
+ panic("unsupported curve " + key.Curve.Params().Name + " in EncodePubkey")
+ }
+}
+
+// DecodePubkey decodes a public key in compressed format.
+func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) {
+ switch curve {
+ case crypto.S256():
+ if len(e) != 33 {
+ return nil, errors.New("wrong size public key data")
+ }
+ return crypto.DecompressPubkey(e)
+ default:
+ return nil, fmt.Errorf("unsupported curve %s in DecodePubkey", curve.Params().Name)
+ }
+}
+
+// idNonceHash computes the ID signature hash used in the handshake.
+func idNonceHash(h hash.Hash, challenge, ephkey []byte, destID enode.ID) []byte {
+ h.Reset()
+ h.Write([]byte("discovery v5 identity proof"))
+ h.Write(challenge)
+ h.Write(ephkey)
+ h.Write(destID[:])
+ return h.Sum(nil)
+}
+
+// makeIDSignature creates the ID nonce signature.
+func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, challenge, ephkey []byte, destID enode.ID) ([]byte, error) {
+ input := idNonceHash(hash, challenge, ephkey, destID)
+ switch key.Curve {
+ case crypto.S256():
+ idsig, err := crypto.Sign(input, key)
+ if err != nil {
+ return nil, err
+ }
+ return idsig[:len(idsig)-1], nil // remove recovery ID
+ default:
+ return nil, fmt.Errorf("unsupported curve %s", key.Curve.Params().Name)
+ }
+}
+
+// s256raw is an unparsed secp256k1 public key ENR entry.
+type s256raw []byte
+
+func (s256raw) ENRKey() string { return "secp256k1" }
+
+// verifyIDSignature checks that signature over idnonce was made by the given node.
+func verifyIDSignature(hash hash.Hash, sig []byte, n *enode.Node, challenge, ephkey []byte, destID enode.ID) error {
+ switch idscheme := n.Record().IdentityScheme(); idscheme {
+ case "v4":
+ var pubkey s256raw
+ if n.Load(&pubkey) != nil {
+ return errors.New("no secp256k1 public key in record")
+ }
+ input := idNonceHash(hash, challenge, ephkey, destID)
+ if !crypto.VerifySignature(pubkey, input, sig) {
+ return errInvalidNonceSig
+ }
+ return nil
+ default:
+ return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme)
+ }
+}
+
+type hashFn func() hash.Hash
+
+// deriveKeys creates the session keys.
+func deriveKeys(hash hashFn, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, n1, n2 enode.ID, challenge []byte) *session {
+ const text = "discovery v5 key agreement"
+ var info = make([]byte, 0, len(text)+len(n1)+len(n2))
+ info = append(info, text...)
+ info = append(info, n1[:]...)
+ info = append(info, n2[:]...)
+
+ eph := ecdh(priv, pub)
+ if eph == nil {
+ return nil
+ }
+ kdf := hkdf.New(hash, eph, challenge, info)
+ sec := session{writeKey: make([]byte, aesKeySize), readKey: make([]byte, aesKeySize)}
+ kdf.Read(sec.writeKey)
+ kdf.Read(sec.readKey)
+ for i := range eph {
+ eph[i] = 0
+ }
+ return &sec
+}
+
+// ecdh creates a shared secret.
+func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte {
+ secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes())
+ if secX == nil {
+ return nil
+ }
+ sec := make([]byte, 33)
+ sec[0] = 0x02 | byte(secY.Bit(0))
+ math.ReadBits(secX, sec[1:])
+ return sec
+}
+
+// encryptGCM encrypts pt using AES-GCM with the given key and nonce. The ciphertext is
+// appended to dest, which must not overlap with plaintext. The resulting ciphertext is 16
+// bytes longer than plaintext because it contains an authentication tag.
+func encryptGCM(dest, key, nonce, plaintext, authData []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ panic(fmt.Errorf("can't create block cipher: %v", err))
+ }
+ aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
+ if err != nil {
+ panic(fmt.Errorf("can't create GCM: %v", err))
+ }
+ return aesgcm.Seal(dest, nonce, plaintext, authData), nil
+}
+
+// decryptGCM decrypts ct using AES-GCM with the given key and nonce.
+func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, fmt.Errorf("can't create block cipher: %v", err)
+ }
+ if len(nonce) != gcmNonceSize {
+ return nil, fmt.Errorf("invalid GCM nonce size: %d", len(nonce))
+ }
+ aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
+ if err != nil {
+ return nil, fmt.Errorf("can't create GCM: %v", err)
+ }
+ pt := make([]byte, 0, len(ct))
+ return aesgcm.Open(pt, nonce, ct, authData)
+}
diff --git a/discover/v5wire/crypto_test.go b/discover/v5wire/crypto_test.go
new file mode 100644
index 0000000..72169b4
--- /dev/null
+++ b/discover/v5wire/crypto_test.go
@@ -0,0 +1,124 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v5wire
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/sha256"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+func TestVector_ECDH(t *testing.T) {
+ var (
+ staticKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
+ publicKey = hexPubkey(crypto.S256(), "0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231")
+ want = hexutil.MustDecode("0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e")
+ )
+ result := ecdh(staticKey, publicKey)
+ check(t, "shared-secret", result, want)
+}
+
+func TestVector_KDF(t *testing.T) {
+ var (
+ ephKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
+ cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000")
+ net = newHandshakeTest()
+ )
+ defer net.close()
+
+ destKey := &testKeyB.PublicKey
+ s := deriveKeys(sha256.New, ephKey, destKey, net.nodeA.id(), net.nodeB.id(), cdata)
+ t.Logf("ephemeral-key = %#x", ephKey.D)
+ t.Logf("dest-pubkey = %#x", EncodePubkey(destKey))
+ t.Logf("node-id-a = %#x", net.nodeA.id().Bytes())
+ t.Logf("node-id-b = %#x", net.nodeB.id().Bytes())
+ t.Logf("challenge-data = %#x", cdata)
+ check(t, "initiator-key", s.writeKey, hexutil.MustDecode("0xdccc82d81bd610f4f76d3ebe97a40571"))
+ check(t, "recipient-key", s.readKey, hexutil.MustDecode("0xac74bb8773749920b0d3a8881c173ec5"))
+}
+
+func TestVector_IDSignature(t *testing.T) {
+ var (
+ key = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
+ destID = enode.HexID("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9")
+ ephkey = hexutil.MustDecode("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231")
+ cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000")
+ )
+
+ sig, err := makeIDSignature(sha256.New(), key, cdata, ephkey, destID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("static-key = %#x", key.D)
+ t.Logf("challenge-data = %#x", cdata)
+ t.Logf("ephemeral-pubkey = %#x", ephkey)
+ t.Logf("node-id-B = %#x", destID.Bytes())
+ expected := "0x94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6"
+ check(t, "id-signature", sig, hexutil.MustDecode(expected))
+}
+
+func TestDeriveKeys(t *testing.T) {
+ t.Parallel()
+
+ var (
+ n1 = enode.ID{1}
+ n2 = enode.ID{2}
+ cdata = []byte{1, 2, 3, 4}
+ )
+ sec1 := deriveKeys(sha256.New, testKeyA, &testKeyB.PublicKey, n1, n2, cdata)
+ sec2 := deriveKeys(sha256.New, testKeyB, &testKeyA.PublicKey, n1, n2, cdata)
+ if sec1 == nil || sec2 == nil {
+ t.Fatal("key agreement failed")
+ }
+ if !reflect.DeepEqual(sec1, sec2) {
+ t.Fatalf("keys not equal:\n %+v\n %+v", sec1, sec2)
+ }
+}
+
+func check(t *testing.T, what string, x, y []byte) {
+ t.Helper()
+
+ if !bytes.Equal(x, y) {
+ t.Errorf("wrong %s: %#x != %#x", what, x, y)
+ } else {
+ t.Logf("%s = %#x", what, x)
+ }
+}
+
+func hexPrivkey(input string) *ecdsa.PrivateKey {
+ key, err := crypto.HexToECDSA(strings.TrimPrefix(input, "0x"))
+ if err != nil {
+ panic(err)
+ }
+ return key
+}
+
+func hexPubkey(curve elliptic.Curve, input string) *ecdsa.PublicKey {
+ key, err := DecodePubkey(curve, hexutil.MustDecode(input))
+ if err != nil {
+ panic(err)
+ }
+ return key
+}
diff --git a/discover/v5wire/encoding.go b/discover/v5wire/encoding.go
new file mode 100644
index 0000000..f502339
--- /dev/null
+++ b/discover/v5wire/encoding.go
@@ -0,0 +1,648 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v5wire
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/ecdsa"
+ crand "crypto/rand"
+ "crypto/sha256"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "hash"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// TODO concurrent WHOAREYOU tie-breaker
+// TODO rehandshake after X packets
+
+// Header represents a packet header.
+type Header struct {
+ IV [sizeofMaskingIV]byte
+ StaticHeader
+ AuthData []byte
+
+ src enode.ID // used by decoder
+}
+
+// StaticHeader contains the static fields of a packet header.
+type StaticHeader struct {
+ ProtocolID [6]byte
+ Version uint16
+ Flag byte
+ Nonce Nonce
+ AuthSize uint16
+}
+
+// Authdata layouts.
+type (
+ whoareyouAuthData struct {
+ IDNonce [16]byte // ID proof data
+ RecordSeq uint64 // highest known ENR sequence of requester
+ }
+
+ handshakeAuthData struct {
+ h struct {
+ SrcID enode.ID
+ SigSize byte // ignature data
+ PubkeySize byte // offset of
+ }
+ // Trailing variable-size data.
+ signature, pubkey, record []byte
+ }
+
+ messageAuthData struct {
+ SrcID enode.ID
+ }
+)
+
+// Packet header flag values.
+const (
+ flagMessage = iota
+ flagWhoareyou
+ flagHandshake
+)
+
+// Protocol constants.
+const (
+ version = 1
+ minVersion = 1
+ sizeofMaskingIV = 16
+
+ minMessageSize = 48 // this refers to data after static headers
+ randomPacketMsgSize = 20
+)
+
+var protocolID = [6]byte{'d', 'i', 's', 'c', 'v', '5'}
+
+// Errors.
+var (
+ errTooShort = errors.New("packet too short")
+ errInvalidHeader = errors.New("invalid packet header")
+ errInvalidFlag = errors.New("invalid flag value in header")
+ errMinVersion = errors.New("version of packet header below minimum")
+ errMsgTooShort = errors.New("message/handshake packet below minimum size")
+ errAuthSize = errors.New("declared auth size is beyond packet length")
+ errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake")
+ errInvalidAuthKey = errors.New("invalid ephemeral pubkey")
+ errNoRecord = errors.New("expected ENR in handshake but none sent")
+ errInvalidNonceSig = errors.New("invalid ID nonce signature")
+ errMessageTooShort = errors.New("message contains no data")
+ errMessageDecrypt = errors.New("cannot decrypt message")
+)
+
+// Public errors.
+var (
+ ErrInvalidReqID = errors.New("request ID larger than 8 bytes")
+)
+
+// Packet sizes.
+var (
+ sizeofStaticHeader = binary.Size(StaticHeader{})
+ sizeofWhoareyouAuthData = binary.Size(whoareyouAuthData{})
+ sizeofHandshakeAuthData = binary.Size(handshakeAuthData{}.h)
+ sizeofMessageAuthData = binary.Size(messageAuthData{})
+ sizeofStaticPacketData = sizeofMaskingIV + sizeofStaticHeader
+)
+
+// Codec encodes and decodes Discovery v5 packets.
+// This type is not safe for concurrent use.
+type Codec struct {
+ sha256 hash.Hash
+ localnode *enode.LocalNode
+ privkey *ecdsa.PrivateKey
+ sc *SessionCache
+
+ // encoder buffers
+ buf bytes.Buffer // whole packet
+ headbuf bytes.Buffer // packet header
+ msgbuf bytes.Buffer // message RLP plaintext
+ msgctbuf []byte // message data ciphertext
+
+ // decoder buffer
+ reader bytes.Reader
+}
+
+// NewCodec creates a wire codec.
+func NewCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *Codec {
+ c := &Codec{
+ sha256: sha256.New(),
+ localnode: ln,
+ privkey: key,
+ sc: NewSessionCache(1024, clock),
+ }
+ return c
+}
+
+// Encode encodes a packet to a node. 'id' and 'addr' specify the destination node. The
+// 'challenge' parameter should be the most recently received WHOAREYOU packet from that
+// node.
+func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) {
+ // Create the packet header.
+ var (
+ head Header
+ session *session
+ msgData []byte
+ err error
+ )
+ switch {
+ case packet.Kind() == WhoareyouPacket:
+ head, err = c.encodeWhoareyou(id, packet.(*Whoareyou))
+ case challenge != nil:
+ // We have an unanswered challenge, send handshake.
+ head, session, err = c.encodeHandshakeHeader(id, addr, challenge)
+ default:
+ session = c.sc.session(id, addr)
+ if session != nil {
+ // There is a session, use it.
+ head, err = c.encodeMessageHeader(id, session)
+ } else {
+ // No keys, send random data to kick off the handshake.
+ head, msgData, err = c.encodeRandom(id)
+ }
+ }
+ if err != nil {
+ return nil, Nonce{}, err
+ }
+
+ // Generate masking IV.
+ if err := c.sc.maskingIVGen(head.IV[:]); err != nil {
+ return nil, Nonce{}, fmt.Errorf("can't generate masking IV: %v", err)
+ }
+
+ // Encode header data.
+ c.writeHeaders(&head)
+
+ // Store sent WHOAREYOU challenges.
+ if challenge, ok := packet.(*Whoareyou); ok {
+ challenge.ChallengeData = bytesCopy(&c.buf)
+ c.sc.storeSentHandshake(id, addr, challenge)
+ } else if msgData == nil {
+ headerData := c.buf.Bytes()
+ msgData, err = c.encryptMessage(session, packet, &head, headerData)
+ if err != nil {
+ return nil, Nonce{}, err
+ }
+ }
+
+ enc, err := c.EncodeRaw(id, head, msgData)
+ return enc, head.Nonce, err
+}
+
+// EncodeRaw encodes a packet with the given header.
+func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) {
+ c.writeHeaders(&head)
+
+ // Apply masking.
+ masked := c.buf.Bytes()[sizeofMaskingIV:]
+ mask := head.mask(id)
+ mask.XORKeyStream(masked[:], masked[:])
+
+ // Write message data.
+ c.buf.Write(msgdata)
+ return c.buf.Bytes(), nil
+}
+
+func (c *Codec) writeHeaders(head *Header) {
+ c.buf.Reset()
+ c.buf.Write(head.IV[:])
+ binary.Write(&c.buf, binary.BigEndian, &head.StaticHeader)
+ c.buf.Write(head.AuthData)
+}
+
+// makeHeader creates a packet header.
+func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header {
+ var authsize int
+ switch flag {
+ case flagMessage:
+ authsize = sizeofMessageAuthData
+ case flagWhoareyou:
+ authsize = sizeofWhoareyouAuthData
+ case flagHandshake:
+ authsize = sizeofHandshakeAuthData
+ default:
+ panic(fmt.Errorf("BUG: invalid packet header flag %x", flag))
+ }
+ authsize += authsizeExtra
+ if authsize > int(^uint16(0)) {
+ panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize))
+ }
+ return Header{
+ StaticHeader: StaticHeader{
+ ProtocolID: protocolID,
+ Version: version,
+ Flag: flag,
+ AuthSize: uint16(authsize),
+ },
+ }
+}
+
+// encodeRandom encodes a packet with random content.
+func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) {
+ head := c.makeHeader(toID, flagMessage, 0)
+
+ // Encode auth data.
+ auth := messageAuthData{SrcID: c.localnode.ID()}
+ if _, err := crand.Read(head.Nonce[:]); err != nil {
+ return head, nil, fmt.Errorf("can't get random data: %v", err)
+ }
+ c.headbuf.Reset()
+ binary.Write(&c.headbuf, binary.BigEndian, auth)
+ head.AuthData = c.headbuf.Bytes()
+
+ // Fill message ciphertext buffer with random bytes.
+ c.msgctbuf = append(c.msgctbuf[:0], make([]byte, randomPacketMsgSize)...)
+ crand.Read(c.msgctbuf)
+ return head, c.msgctbuf, nil
+}
+
+// encodeWhoareyou encodes a WHOAREYOU packet.
+func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error) {
+ // Sanity check node field to catch misbehaving callers.
+ if packet.RecordSeq > 0 && packet.Node == nil {
+ panic("BUG: missing node in whoareyou with non-zero seq")
+ }
+
+ // Create header.
+ head := c.makeHeader(toID, flagWhoareyou, 0)
+ head.AuthData = bytesCopy(&c.buf)
+ head.Nonce = packet.Nonce
+
+ // Encode auth data.
+ auth := &whoareyouAuthData{
+ IDNonce: packet.IDNonce,
+ RecordSeq: packet.RecordSeq,
+ }
+ c.headbuf.Reset()
+ binary.Write(&c.headbuf, binary.BigEndian, auth)
+ head.AuthData = c.headbuf.Bytes()
+ return head, nil
+}
+
+// encodeHandshakeMessage encodes the handshake message packet header.
+func (c *Codec) encodeHandshakeHeader(toID enode.ID, addr string, challenge *Whoareyou) (Header, *session, error) {
+ // Ensure calling code sets challenge.node.
+ if challenge.Node == nil {
+ panic("BUG: missing challenge.Node in encode")
+ }
+
+ // Generate new secrets.
+ auth, session, err := c.makeHandshakeAuth(toID, addr, challenge)
+ if err != nil {
+ return Header{}, nil, err
+ }
+
+ // Generate nonce for message.
+ nonce, err := c.sc.nextNonce(session)
+ if err != nil {
+ return Header{}, nil, fmt.Errorf("can't generate nonce: %v", err)
+ }
+
+ // TODO: this should happen when the first authenticated message is received
+ c.sc.storeNewSession(toID, addr, session)
+
+ // Encode the auth header.
+ var (
+ authsizeExtra = len(auth.pubkey) + len(auth.signature) + len(auth.record)
+ head = c.makeHeader(toID, flagHandshake, authsizeExtra)
+ )
+ c.headbuf.Reset()
+ binary.Write(&c.headbuf, binary.BigEndian, &auth.h)
+ c.headbuf.Write(auth.signature)
+ c.headbuf.Write(auth.pubkey)
+ c.headbuf.Write(auth.record)
+ head.AuthData = c.headbuf.Bytes()
+ head.Nonce = nonce
+ return head, session, err
+}
+
+// encodeAuthHeader creates the auth header on a request packet following WHOAREYOU.
+func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoareyou) (*handshakeAuthData, *session, error) {
+ auth := new(handshakeAuthData)
+ auth.h.SrcID = c.localnode.ID()
+
+ // Create the ephemeral key. This needs to be first because the
+ // key is part of the ID nonce signature.
+ var remotePubkey = new(ecdsa.PublicKey)
+ if err := challenge.Node.Load((*enode.Secp256k1)(remotePubkey)); err != nil {
+ return nil, nil, fmt.Errorf("can't find secp256k1 key for recipient")
+ }
+ ephkey, err := c.sc.ephemeralKeyGen()
+ if err != nil {
+ return nil, nil, fmt.Errorf("can't generate ephemeral key")
+ }
+ ephpubkey := EncodePubkey(&ephkey.PublicKey)
+ auth.pubkey = ephpubkey[:]
+ auth.h.PubkeySize = byte(len(auth.pubkey))
+
+ // Add ID nonce signature to response.
+ cdata := challenge.ChallengeData
+ idsig, err := makeIDSignature(c.sha256, c.privkey, cdata, ephpubkey[:], toID)
+ if err != nil {
+ return nil, nil, fmt.Errorf("can't sign: %v", err)
+ }
+ auth.signature = idsig
+ auth.h.SigSize = byte(len(auth.signature))
+
+ // Add our record to response if it's newer than what remote side has.
+ ln := c.localnode.Node()
+ if challenge.RecordSeq < ln.Seq() {
+ auth.record, _ = rlp.EncodeToBytes(ln.Record())
+ }
+
+ // Create session keys.
+ sec := deriveKeys(sha256.New, ephkey, remotePubkey, c.localnode.ID(), challenge.Node.ID(), cdata)
+ if sec == nil {
+ return nil, nil, fmt.Errorf("key derivation failed")
+ }
+ return auth, sec, err
+}
+
+// encodeMessage encodes an encrypted message packet.
+func (c *Codec) encodeMessageHeader(toID enode.ID, s *session) (Header, error) {
+ head := c.makeHeader(toID, flagMessage, 0)
+
+ // Create the header.
+ nonce, err := c.sc.nextNonce(s)
+ if err != nil {
+ return Header{}, fmt.Errorf("can't generate nonce: %v", err)
+ }
+ auth := messageAuthData{SrcID: c.localnode.ID()}
+ c.buf.Reset()
+ binary.Write(&c.buf, binary.BigEndian, &auth)
+ head.AuthData = bytesCopy(&c.buf)
+ head.Nonce = nonce
+ return head, err
+}
+
+func (c *Codec) encryptMessage(s *session, p Packet, head *Header, headerData []byte) ([]byte, error) {
+ // Encode message plaintext.
+ c.msgbuf.Reset()
+ c.msgbuf.WriteByte(p.Kind())
+ if err := rlp.Encode(&c.msgbuf, p); err != nil {
+ return nil, err
+ }
+ messagePT := c.msgbuf.Bytes()
+
+ // Encrypt into message ciphertext buffer.
+ messageCT, err := encryptGCM(c.msgctbuf[:0], s.writeKey, head.Nonce[:], messagePT, headerData)
+ if err == nil {
+ c.msgctbuf = messageCT
+ }
+ return messageCT, err
+}
+
+// Decode decodes a discovery packet.
+func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, p Packet, err error) {
+ // Unmask the static header.
+ if len(input) < sizeofStaticPacketData {
+ return enode.ID{}, nil, nil, errTooShort
+ }
+ var head Header
+ copy(head.IV[:], input[:sizeofMaskingIV])
+ mask := head.mask(c.localnode.ID())
+ staticHeader := input[sizeofMaskingIV:sizeofStaticPacketData]
+ mask.XORKeyStream(staticHeader, staticHeader)
+
+ // Decode and verify the static header.
+ c.reader.Reset(staticHeader)
+ binary.Read(&c.reader, binary.BigEndian, &head.StaticHeader)
+ remainingInput := len(input) - sizeofStaticPacketData
+ if err := head.checkValid(remainingInput); err != nil {
+ return enode.ID{}, nil, nil, err
+ }
+
+ // Unmask auth data.
+ authDataEnd := sizeofStaticPacketData + int(head.AuthSize)
+ authData := input[sizeofStaticPacketData:authDataEnd]
+ mask.XORKeyStream(authData, authData)
+ head.AuthData = authData
+
+ // Delete timed-out handshakes. This must happen before decoding to avoid
+ // processing the same handshake twice.
+ c.sc.handshakeGC()
+
+ // Decode auth part and message.
+ headerData := input[:authDataEnd]
+ msgData := input[authDataEnd:]
+ switch head.Flag {
+ case flagWhoareyou:
+ p, err = c.decodeWhoareyou(&head, headerData)
+ case flagHandshake:
+ n, p, err = c.decodeHandshakeMessage(addr, &head, headerData, msgData)
+ case flagMessage:
+ p, err = c.decodeMessage(addr, &head, headerData, msgData)
+ default:
+ err = errInvalidFlag
+ }
+ return head.src, n, p, err
+}
+
+// decodeWhoareyou reads packet data after the header as a WHOAREYOU packet.
+func (c *Codec) decodeWhoareyou(head *Header, headerData []byte) (Packet, error) {
+ if len(head.AuthData) != sizeofWhoareyouAuthData {
+ return nil, fmt.Errorf("invalid auth size %d for WHOAREYOU", len(head.AuthData))
+ }
+ var auth whoareyouAuthData
+ c.reader.Reset(head.AuthData)
+ binary.Read(&c.reader, binary.BigEndian, &auth)
+ p := &Whoareyou{
+ Nonce: head.Nonce,
+ IDNonce: auth.IDNonce,
+ RecordSeq: auth.RecordSeq,
+ ChallengeData: make([]byte, len(headerData)),
+ }
+ copy(p.ChallengeData, headerData)
+ return p, nil
+}
+
+func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData, msgData []byte) (n *enode.Node, p Packet, err error) {
+ node, auth, session, err := c.decodeHandshake(fromAddr, head)
+ if err != nil {
+ c.sc.deleteHandshake(auth.h.SrcID, fromAddr)
+ return nil, nil, err
+ }
+
+ // Decrypt the message using the new session keys.
+ msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, session.readKey)
+ if err != nil {
+ c.sc.deleteHandshake(auth.h.SrcID, fromAddr)
+ return node, msg, err
+ }
+
+ // Handshake OK, drop the challenge and store the new session keys.
+ c.sc.storeNewSession(auth.h.SrcID, fromAddr, session)
+ c.sc.deleteHandshake(auth.h.SrcID, fromAddr)
+ return node, msg, nil
+}
+
+func (c *Codec) decodeHandshake(fromAddr string, head *Header) (n *enode.Node, auth handshakeAuthData, s *session, err error) {
+ if auth, err = c.decodeHandshakeAuthData(head); err != nil {
+ return nil, auth, nil, err
+ }
+
+ // Verify against our last WHOAREYOU.
+ challenge := c.sc.getHandshake(auth.h.SrcID, fromAddr)
+ if challenge == nil {
+ return nil, auth, nil, errUnexpectedHandshake
+ }
+ // Get node record.
+ n, err = c.decodeHandshakeRecord(challenge.Node, auth.h.SrcID, auth.record)
+ if err != nil {
+ return nil, auth, nil, err
+ }
+ // Verify ID nonce signature.
+ sig := auth.signature
+ cdata := challenge.ChallengeData
+ err = verifyIDSignature(c.sha256, sig, n, cdata, auth.pubkey, c.localnode.ID())
+ if err != nil {
+ return nil, auth, nil, err
+ }
+ // Verify ephemeral key is on curve.
+ ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey)
+ if err != nil {
+ return nil, auth, nil, errInvalidAuthKey
+ }
+ // Derive sesssion keys.
+ session := deriveKeys(sha256.New, c.privkey, ephkey, auth.h.SrcID, c.localnode.ID(), cdata)
+ session = session.keysFlipped()
+ return n, auth, session, nil
+}
+
+// decodeHandshakeAuthData reads the authdata section of a handshake packet.
+func (c *Codec) decodeHandshakeAuthData(head *Header) (auth handshakeAuthData, err error) {
+ // Decode fixed size part.
+ if len(head.AuthData) < sizeofHandshakeAuthData {
+ return auth, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize)
+ }
+ c.reader.Reset(head.AuthData)
+ binary.Read(&c.reader, binary.BigEndian, &auth.h)
+ head.src = auth.h.SrcID
+
+ // Decode variable-size part.
+ var (
+ vardata = head.AuthData[sizeofHandshakeAuthData:]
+ sigAndKeySize = int(auth.h.SigSize) + int(auth.h.PubkeySize)
+ keyOffset = int(auth.h.SigSize)
+ recOffset = keyOffset + int(auth.h.PubkeySize)
+ )
+ if len(vardata) < sigAndKeySize {
+ return auth, errTooShort
+ }
+ auth.signature = vardata[:keyOffset]
+ auth.pubkey = vardata[keyOffset:recOffset]
+ auth.record = vardata[recOffset:]
+ return auth, nil
+}
+
+// decodeHandshakeRecord verifies the node record contained in a handshake packet. The
+// remote node should include the record if we don't have one or if ours is older than the
+// latest sequence number.
+func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (*enode.Node, error) {
+ node := local
+ if len(remote) > 0 {
+ var record enr.Record
+ if err := rlp.DecodeBytes(remote, &record); err != nil {
+ return nil, err
+ }
+ if local == nil || local.Seq() < record.Seq() {
+ n, err := enode.New(enode.ValidSchemes, &record)
+ if err != nil {
+ return nil, fmt.Errorf("invalid node record: %v", err)
+ }
+ if n.ID() != wantID {
+ return nil, fmt.Errorf("record in handshake has wrong ID: %v", n.ID())
+ }
+ node = n
+ }
+ }
+ if node == nil {
+ return nil, errNoRecord
+ }
+ return node, nil
+}
+
+// decodeMessage reads packet data following the header as an ordinary message packet.
+func (c *Codec) decodeMessage(fromAddr string, head *Header, headerData, msgData []byte) (Packet, error) {
+ if len(head.AuthData) != sizeofMessageAuthData {
+ return nil, fmt.Errorf("invalid auth size %d for message packet", len(head.AuthData))
+ }
+ var auth messageAuthData
+ c.reader.Reset(head.AuthData)
+ binary.Read(&c.reader, binary.BigEndian, &auth)
+ head.src = auth.SrcID
+
+ // Try decrypting the message.
+ key := c.sc.readKey(auth.SrcID, fromAddr)
+ msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, key)
+ if err == errMessageDecrypt {
+ // It didn't work. Start the handshake since this is an ordinary message packet.
+ return &Unknown{Nonce: head.Nonce}, nil
+ }
+ return msg, err
+}
+
+func (c *Codec) decryptMessage(input, nonce, headerData, readKey []byte) (Packet, error) {
+ msgdata, err := decryptGCM(readKey, nonce, input, headerData)
+ if err != nil {
+ return nil, errMessageDecrypt
+ }
+ if len(msgdata) == 0 {
+ return nil, errMessageTooShort
+ }
+ return DecodeMessage(msgdata[0], msgdata[1:])
+}
+
+// checkValid performs some basic validity checks on the header.
+// The packetLen here is the length remaining after the static header.
+func (h *StaticHeader) checkValid(packetLen int) error {
+ if h.ProtocolID != protocolID {
+ return errInvalidHeader
+ }
+ if h.Version < minVersion {
+ return errMinVersion
+ }
+ if h.Flag != flagWhoareyou && packetLen < minMessageSize {
+ return errMsgTooShort
+ }
+ if int(h.AuthSize) > packetLen {
+ return errAuthSize
+ }
+ return nil
+}
+
+// headerMask returns a cipher for 'masking' / 'unmasking' packet headers.
+func (h *Header) mask(destID enode.ID) cipher.Stream {
+ block, err := aes.NewCipher(destID[:16])
+ if err != nil {
+ panic("can't create cipher")
+ }
+ return cipher.NewCTR(block, h.IV[:])
+}
+
+func bytesCopy(r *bytes.Buffer) []byte {
+ b := make([]byte, r.Len())
+ copy(b, r.Bytes())
+ return b
+}
diff --git a/discover/v5wire/encoding_test.go b/discover/v5wire/encoding_test.go
new file mode 100644
index 0000000..355a8f6
--- /dev/null
+++ b/discover/v5wire/encoding_test.go
@@ -0,0 +1,633 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v5wire
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "encoding/hex"
+ "errors"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+// To regenerate discv5 test vectors, run
+//
+// go test -run TestVectors -write-test-vectors
+//
+var writeTestVectorsFlag = flag.Bool("write-test-vectors", false, "Overwrite discv5 test vectors in testdata/")
+
+var (
+ testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f")
+ testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628")
+ testEphKey, _ = crypto.HexToECDSA("0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6")
+ testIDnonce = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+)
+
+// This test checks that the minPacketSize and randomPacketMsgSize constants are well-defined.
+func TestMinSizes(t *testing.T) {
+ var (
+ gcmTagSize = 16
+ emptyMsg = sizeofMessageAuthData + gcmTagSize
+ )
+ t.Log("static header size", sizeofStaticPacketData)
+ t.Log("whoareyou size", sizeofStaticPacketData+sizeofWhoareyouAuthData)
+ t.Log("empty msg size", sizeofStaticPacketData+emptyMsg)
+ if want := emptyMsg; minMessageSize != want {
+ t.Fatalf("wrong minMessageSize %d, want %d", minMessageSize, want)
+ }
+ if sizeofMessageAuthData+randomPacketMsgSize < minMessageSize {
+ t.Fatalf("randomPacketMsgSize %d too small", randomPacketMsgSize)
+ }
+}
+
+// This test checks the basic handshake flow where A talks to B and A has no secrets.
+func TestHandshake(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ // A -> B RANDOM PACKET
+ packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
+ resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
+
+ // A <- B WHOAREYOU
+ challenge := &Whoareyou{
+ Nonce: resp.(*Unknown).Nonce,
+ IDNonce: testIDnonce,
+ RecordSeq: 0,
+ }
+ whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
+ net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
+
+ // A -> B FINDNODE (handshake packet)
+ findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
+ net.nodeB.expectDecode(t, FindnodeMsg, findnode)
+ if len(net.nodeB.c.sc.handshakes) > 0 {
+ t.Fatalf("node B didn't remove handshake from challenge map")
+ }
+
+ // A <- B NODES
+ nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1})
+ net.nodeA.expectDecode(t, NodesMsg, nodes)
+}
+
+// This test checks that handshake attempts are removed within the timeout.
+func TestHandshake_timeout(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ // A -> B RANDOM PACKET
+ packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
+ resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
+
+ // A <- B WHOAREYOU
+ challenge := &Whoareyou{
+ Nonce: resp.(*Unknown).Nonce,
+ IDNonce: testIDnonce,
+ RecordSeq: 0,
+ }
+ whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
+ net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
+
+ // A -> B FINDNODE (handshake packet) after timeout
+ net.clock.Run(handshakeTimeout + 1)
+ findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
+ net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode)
+}
+
+// This test checks handshake behavior when no record is sent in the auth response.
+func TestHandshake_norecord(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ // A -> B RANDOM PACKET
+ packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
+ resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
+
+ // A <- B WHOAREYOU
+ nodeA := net.nodeA.n()
+ if nodeA.Seq() == 0 {
+ t.Fatal("need non-zero sequence number")
+ }
+ challenge := &Whoareyou{
+ Nonce: resp.(*Unknown).Nonce,
+ IDNonce: testIDnonce,
+ RecordSeq: nodeA.Seq(),
+ Node: nodeA,
+ }
+ whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
+ net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
+
+ // A -> B FINDNODE
+ findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
+ net.nodeB.expectDecode(t, FindnodeMsg, findnode)
+
+ // A <- B NODES
+ nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1})
+ net.nodeA.expectDecode(t, NodesMsg, nodes)
+}
+
+// In this test, A tries to send FINDNODE with existing secrets but B doesn't know
+// anything about A.
+func TestHandshake_rekey(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ session := &session{
+ readKey: []byte("BBBBBBBBBBBBBBBB"),
+ writeKey: []byte("AAAAAAAAAAAAAAAA"),
+ }
+ net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session)
+
+ // A -> B FINDNODE (encrypted with zero keys)
+ findnode, authTag := net.nodeA.encode(t, net.nodeB, &Findnode{})
+ net.nodeB.expectDecode(t, UnknownPacket, findnode)
+
+ // A <- B WHOAREYOU
+ challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce}
+ whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
+ net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
+
+ // Check that new keys haven't been stored yet.
+ sa := net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr())
+ if !bytes.Equal(sa.writeKey, session.writeKey) || !bytes.Equal(sa.readKey, session.readKey) {
+ t.Fatal("node A stored keys too early")
+ }
+ if s := net.nodeB.c.sc.session(net.nodeA.id(), net.nodeA.addr()); s != nil {
+ t.Fatal("node B stored keys too early")
+ }
+
+ // A -> B FINDNODE encrypted with new keys
+ findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
+ net.nodeB.expectDecode(t, FindnodeMsg, findnode)
+
+ // A <- B NODES
+ nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1})
+ net.nodeA.expectDecode(t, NodesMsg, nodes)
+}
+
+// In this test A and B have different keys before the handshake.
+func TestHandshake_rekey2(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ initKeysA := &session{
+ readKey: []byte("BBBBBBBBBBBBBBBB"),
+ writeKey: []byte("AAAAAAAAAAAAAAAA"),
+ }
+ initKeysB := &session{
+ readKey: []byte("CCCCCCCCCCCCCCCC"),
+ writeKey: []byte("DDDDDDDDDDDDDDDD"),
+ }
+ net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeysA)
+ net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB)
+
+ // A -> B FINDNODE encrypted with initKeysA
+ findnode, authTag := net.nodeA.encode(t, net.nodeB, &Findnode{Distances: []uint{3}})
+ net.nodeB.expectDecode(t, UnknownPacket, findnode)
+
+ // A <- B WHOAREYOU
+ challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce}
+ whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
+ net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
+
+ // A -> B FINDNODE (handshake packet)
+ findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
+ net.nodeB.expectDecode(t, FindnodeMsg, findnode)
+
+ // A <- B NODES
+ nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1})
+ net.nodeA.expectDecode(t, NodesMsg, nodes)
+}
+
+func TestHandshake_BadHandshakeAttack(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ // A -> B RANDOM PACKET
+ packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
+ resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
+
+ // A <- B WHOAREYOU
+ challenge := &Whoareyou{
+ Nonce: resp.(*Unknown).Nonce,
+ IDNonce: testIDnonce,
+ RecordSeq: 0,
+ }
+ whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
+ net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
+
+ // A -> B FINDNODE
+ incorrect_challenge := &Whoareyou{
+ IDNonce: [16]byte{5, 6, 7, 8, 9, 6, 11, 12},
+ RecordSeq: challenge.RecordSeq,
+ Node: challenge.Node,
+ sent: challenge.sent,
+ }
+ incorrect_findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, incorrect_challenge, &Findnode{})
+ incorrect_findnode2 := make([]byte, len(incorrect_findnode))
+ copy(incorrect_findnode2, incorrect_findnode)
+
+ net.nodeB.expectDecodeErr(t, errInvalidNonceSig, incorrect_findnode)
+
+ // Reject new findnode as previous handshake is now deleted.
+ net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, incorrect_findnode2)
+
+ // The findnode packet is again rejected even with a valid challenge this time.
+ findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
+ net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode)
+}
+
+// This test checks some malformed packets.
+func TestDecodeErrorsV5(t *testing.T) {
+ t.Parallel()
+ net := newHandshakeTest()
+ defer net.close()
+
+ net.nodeA.expectDecodeErr(t, errTooShort, []byte{})
+ // TODO some more tests would be nice :)
+ // - check invalid authdata sizes
+ // - check invalid handshake data sizes
+}
+
+// This test checks that all test vectors can be decoded.
+func TestTestVectorsV5(t *testing.T) {
+ var (
+ idA = enode.PubkeyToIDV4(&testKeyA.PublicKey)
+ idB = enode.PubkeyToIDV4(&testKeyB.PublicKey)
+ addr = "127.0.0.1"
+ session = &session{
+ writeKey: hexutil.MustDecode("0x00000000000000000000000000000000"),
+ readKey: hexutil.MustDecode("0x01010101010101010101010101010101"),
+ }
+ challenge0A, challenge1A, challenge0B Whoareyou
+ )
+
+ // Create challenge packets.
+ c := Whoareyou{
+ Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+ IDNonce: testIDnonce,
+ }
+ challenge0A, challenge1A, challenge0B = c, c, c
+ challenge1A.RecordSeq = 1
+ net := newHandshakeTest()
+ challenge0A.Node = net.nodeA.n()
+ challenge0B.Node = net.nodeB.n()
+ challenge1A.Node = net.nodeA.n()
+ net.close()
+
+ type testVectorTest struct {
+ name string // test vector name
+ packet Packet // the packet to be encoded
+ challenge *Whoareyou // handshake challenge passed to encoder
+ prep func(*handshakeTest) // called before encode/decode
+ }
+ tests := []testVectorTest{
+ {
+ name: "v5.1-whoareyou",
+ packet: &challenge0B,
+ },
+ {
+ name: "v5.1-ping-message",
+ packet: &Ping{
+ ReqID: []byte{0, 0, 0, 1},
+ ENRSeq: 2,
+ },
+ prep: func(net *handshakeTest) {
+ net.nodeA.c.sc.storeNewSession(idB, addr, session)
+ net.nodeB.c.sc.storeNewSession(idA, addr, session.keysFlipped())
+ },
+ },
+ {
+ name: "v5.1-ping-handshake-enr",
+ packet: &Ping{
+ ReqID: []byte{0, 0, 0, 1},
+ ENRSeq: 1,
+ },
+ challenge: &challenge0A,
+ prep: func(net *handshakeTest) {
+ // Update challenge.Header.AuthData.
+ net.nodeA.c.Encode(idB, "", &challenge0A, nil)
+ net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge0A)
+ },
+ },
+ {
+ name: "v5.1-ping-handshake",
+ packet: &Ping{
+ ReqID: []byte{0, 0, 0, 1},
+ ENRSeq: 1,
+ },
+ challenge: &challenge1A,
+ prep: func(net *handshakeTest) {
+ // Update challenge data.
+ net.nodeA.c.Encode(idB, "", &challenge1A, nil)
+ net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge1A)
+ },
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ net := newHandshakeTest()
+ defer net.close()
+
+ // Override all random inputs.
+ net.nodeA.c.sc.nonceGen = func(counter uint32) (Nonce, error) {
+ return Nonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil
+ }
+ net.nodeA.c.sc.maskingIVGen = func(buf []byte) error {
+ return nil // all zero
+ }
+ net.nodeA.c.sc.ephemeralKeyGen = func() (*ecdsa.PrivateKey, error) {
+ return testEphKey, nil
+ }
+
+ // Prime the codec for encoding/decoding.
+ if test.prep != nil {
+ test.prep(net)
+ }
+
+ file := filepath.Join("testdata", test.name+".txt")
+ if *writeTestVectorsFlag {
+ // Encode the packet.
+ d, nonce := net.nodeA.encodeWithChallenge(t, net.nodeB, test.challenge, test.packet)
+ comment := testVectorComment(net, test.packet, test.challenge, nonce)
+ writeTestVector(file, comment, d)
+ }
+ enc := hexFile(file)
+ net.nodeB.expectDecode(t, test.packet.Kind(), enc)
+ })
+ }
+}
+
+// testVectorComment creates the commentary for discv5 test vector files.
+func testVectorComment(net *handshakeTest, p Packet, challenge *Whoareyou, nonce Nonce) string {
+ o := new(strings.Builder)
+ printWhoareyou := func(p *Whoareyou) {
+ fmt.Fprintf(o, "whoareyou.challenge-data = %#x\n", p.ChallengeData)
+ fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.Nonce[:])
+ fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:])
+ fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq)
+ }
+
+ fmt.Fprintf(o, "src-node-id = %#x\n", net.nodeA.id().Bytes())
+ fmt.Fprintf(o, "dest-node-id = %#x\n", net.nodeB.id().Bytes())
+ switch p := p.(type) {
+ case *Whoareyou:
+ // WHOAREYOU packet.
+ printWhoareyou(p)
+ case *Ping:
+ fmt.Fprintf(o, "nonce = %#x\n", nonce[:])
+ fmt.Fprintf(o, "read-key = %#x\n", net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()).writeKey)
+ fmt.Fprintf(o, "ping.req-id = %#x\n", p.ReqID)
+ fmt.Fprintf(o, "ping.enr-seq = %d\n", p.ENRSeq)
+ if challenge != nil {
+ // Handshake message packet.
+ fmt.Fprint(o, "\nhandshake inputs:\n\n")
+ printWhoareyou(challenge)
+ fmt.Fprintf(o, "ephemeral-key = %#x\n", testEphKey.D.Bytes())
+ fmt.Fprintf(o, "ephemeral-pubkey = %#x\n", crypto.CompressPubkey(&testEphKey.PublicKey))
+ }
+ default:
+ panic(fmt.Errorf("unhandled packet type %T", p))
+ }
+ return o.String()
+}
+
+// This benchmark checks performance of handshake packet decoding.
+func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) {
+ net := newHandshakeTest()
+ defer net.close()
+
+ var (
+ idA = net.nodeA.id()
+ challenge = &Whoareyou{Node: net.nodeB.n()}
+ message = &Ping{ReqID: []byte("reqid")}
+ )
+ enc, _, err := net.nodeA.c.Encode(net.nodeB.id(), "", message, challenge)
+ if err != nil {
+ b.Fatal("can't encode handshake packet")
+ }
+ challenge.Node = nil // force ENR signature verification in decoder
+ b.ResetTimer()
+
+ input := make([]byte, len(enc))
+ for i := 0; i < b.N; i++ {
+ copy(input, enc)
+ net.nodeB.c.sc.storeSentHandshake(idA, "", challenge)
+ _, _, _, err := net.nodeB.c.Decode(input, "")
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// This benchmark checks how long it takes to decode an encrypted ping packet.
+func BenchmarkV5_DecodePing(b *testing.B) {
+ net := newHandshakeTest()
+ defer net.close()
+
+ session := &session{
+ readKey: []byte{233, 203, 93, 195, 86, 47, 177, 186, 227, 43, 2, 141, 244, 230, 120, 17},
+ writeKey: []byte{79, 145, 252, 171, 167, 216, 252, 161, 208, 190, 176, 106, 214, 39, 178, 134},
+ }
+ net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session)
+ net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), session.keysFlipped())
+ addrB := net.nodeA.addr()
+ ping := &Ping{ReqID: []byte("reqid"), ENRSeq: 5}
+ enc, _, err := net.nodeA.c.Encode(net.nodeB.id(), addrB, ping, nil)
+ if err != nil {
+ b.Fatalf("can't encode: %v", err)
+ }
+ b.ResetTimer()
+
+ input := make([]byte, len(enc))
+ for i := 0; i < b.N; i++ {
+ copy(input, enc)
+ _, _, packet, _ := net.nodeB.c.Decode(input, addrB)
+ if _, ok := packet.(*Ping); !ok {
+ b.Fatalf("wrong packet type %T", packet)
+ }
+ }
+}
+
+var pp = spew.NewDefaultConfig()
+
+type handshakeTest struct {
+ nodeA, nodeB handshakeTestNode
+ clock mclock.Simulated
+}
+
+type handshakeTestNode struct {
+ ln *enode.LocalNode
+ c *Codec
+}
+
+func newHandshakeTest() *handshakeTest {
+ t := new(handshakeTest)
+ t.nodeA.init(testKeyA, net.IP{127, 0, 0, 1}, &t.clock)
+ t.nodeB.init(testKeyB, net.IP{127, 0, 0, 1}, &t.clock)
+ return t
+}
+
+func (t *handshakeTest) close() {
+ t.nodeA.ln.Database().Close()
+ t.nodeB.ln.Database().Close()
+}
+
+func (n *handshakeTestNode) init(key *ecdsa.PrivateKey, ip net.IP, clock mclock.Clock) {
+ db, _ := enode.OpenDB("")
+ n.ln = enode.NewLocalNode(db, key)
+ n.ln.SetStaticIP(ip)
+ n.c = NewCodec(n.ln, key, clock)
+}
+
+func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p Packet) ([]byte, Nonce) {
+ t.Helper()
+ return n.encodeWithChallenge(t, to, nil, p)
+}
+
+func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *Whoareyou, p Packet) ([]byte, Nonce) {
+ t.Helper()
+
+ // Copy challenge and add destination node. This avoids sharing 'c' among the two codecs.
+ var challenge *Whoareyou
+ if c != nil {
+ challengeCopy := *c
+ challenge = &challengeCopy
+ challenge.Node = to.n()
+ }
+ // Encode to destination.
+ enc, nonce, err := n.c.Encode(to.id(), to.addr(), p, challenge)
+ if err != nil {
+ t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err))
+ }
+ t.Logf("(%s) -> (%s) %s\n%s", n.ln.ID().TerminalString(), to.id().TerminalString(), p.Name(), hex.Dump(enc))
+ return enc, nonce
+}
+
+func (n *handshakeTestNode) expectDecode(t *testing.T, ptype byte, p []byte) Packet {
+ t.Helper()
+
+ dec, err := n.decode(p)
+ if err != nil {
+ t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err))
+ }
+ t.Logf("(%s) %#v", n.ln.ID().TerminalString(), pp.NewFormatter(dec))
+ if dec.Kind() != ptype {
+ t.Fatalf("expected packet type %d, got %d", ptype, dec.Kind())
+ }
+ return dec
+}
+
+func (n *handshakeTestNode) expectDecodeErr(t *testing.T, wantErr error, p []byte) {
+ t.Helper()
+ if _, err := n.decode(p); !errors.Is(err, wantErr) {
+ t.Fatal(fmt.Errorf("(%s) got err %q, want %q", n.ln.ID().TerminalString(), err, wantErr))
+ }
+}
+
+func (n *handshakeTestNode) decode(input []byte) (Packet, error) {
+ _, _, p, err := n.c.Decode(input, "127.0.0.1")
+ return p, err
+}
+
+func (n *handshakeTestNode) n() *enode.Node {
+ return n.ln.Node()
+}
+
+func (n *handshakeTestNode) addr() string {
+ return n.ln.Node().IP().String()
+}
+
+func (n *handshakeTestNode) id() enode.ID {
+ return n.ln.ID()
+}
+
+// hexFile reads the given file and decodes the hex data contained in it.
+// Whitespace and any lines beginning with the # character are ignored.
+func hexFile(file string) []byte {
+ fileContent, err := ioutil.ReadFile(file)
+ if err != nil {
+ panic(err)
+ }
+
+ // Gather hex data, ignore comments.
+ var text []byte
+ for _, line := range bytes.Split(fileContent, []byte("\n")) {
+ line = bytes.TrimSpace(line)
+ if len(line) > 0 && line[0] == '#' {
+ continue
+ }
+ text = append(text, line...)
+ }
+
+ // Parse the hex.
+ if bytes.HasPrefix(text, []byte("0x")) {
+ text = text[2:]
+ }
+ data := make([]byte, hex.DecodedLen(len(text)))
+ if _, err := hex.Decode(data, text); err != nil {
+ panic("invalid hex in " + file)
+ }
+ return data
+}
+
+// writeTestVector writes a test vector file with the given commentary and binary data.
+func writeTestVector(file, comment string, data []byte) {
+ fd, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ panic(err)
+ }
+ defer fd.Close()
+
+ if len(comment) > 0 {
+ for _, line := range strings.Split(strings.TrimSpace(comment), "\n") {
+ fmt.Fprintf(fd, "# %s\n", line)
+ }
+ fmt.Fprintln(fd)
+ }
+ for len(data) > 0 {
+ var chunk []byte
+ if len(data) < 32 {
+ chunk = data
+ } else {
+ chunk = data[:32]
+ }
+ data = data[len(chunk):]
+ fmt.Fprintf(fd, "%x\n", chunk)
+ }
+}
diff --git a/discover/v5wire/msg.go b/discover/v5wire/msg.go
new file mode 100644
index 0000000..c049668
--- /dev/null
+++ b/discover/v5wire/msg.go
@@ -0,0 +1,249 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v5wire
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// Packet is implemented by all message types.
+type Packet interface {
+ Name() string // Name returns a string corresponding to the message type.
+ Kind() byte // Kind returns the message type.
+ RequestID() []byte // Returns the request ID.
+ SetRequestID([]byte) // Sets the request ID.
+}
+
+// Message types.
+const (
+ PingMsg byte = iota + 1
+ PongMsg
+ FindnodeMsg
+ NodesMsg
+ TalkRequestMsg
+ TalkResponseMsg
+ RequestTicketMsg
+ TicketMsg
+ RegtopicMsg
+ RegconfirmationMsg
+ TopicQueryMsg
+
+ UnknownPacket = byte(255) // any non-decryptable packet
+ WhoareyouPacket = byte(254) // the WHOAREYOU packet
+)
+
+// Protocol messages.
+type (
+ // Unknown represents any packet that can't be decrypted.
+ Unknown struct {
+ Nonce Nonce
+ }
+
+ // WHOAREYOU contains the handshake challenge.
+ Whoareyou struct {
+ ChallengeData []byte // Encoded challenge
+ Nonce Nonce // Nonce of request packet
+ IDNonce [16]byte // Identity proof data
+ RecordSeq uint64 // ENR sequence number of recipient
+
+ // Node is the locally known node record of recipient.
+ // This must be set by the caller of Encode.
+ Node *enode.Node
+
+ sent mclock.AbsTime // for handshake GC.
+ }
+
+ // PING is sent during liveness checks.
+ Ping struct {
+ ReqID []byte
+ ENRSeq uint64
+ }
+
+ // PONG is the reply to PING.
+ Pong struct {
+ ReqID []byte
+ ENRSeq uint64
+ ToIP net.IP // These fields should mirror the UDP envelope address of the ping
+ ToPort uint16 // packet, which provides a way to discover the external address (after NAT).
+ }
+
+ // FINDNODE is a query for nodes in the given bucket.
+ Findnode struct {
+ ReqID []byte
+ Distances []uint
+ }
+
+ // NODES is the reply to FINDNODE and TOPICQUERY.
+ Nodes struct {
+ ReqID []byte
+ Total uint8
+ Nodes []*enr.Record
+ }
+
+ // TALKREQ is an application-level request.
+ TalkRequest struct {
+ ReqID []byte
+ Protocol string
+ Message []byte
+ }
+
+ // TALKRESP is the reply to TALKREQ.
+ TalkResponse struct {
+ ReqID []byte
+ Message []byte
+ }
+
+ // REQUESTTICKET requests a ticket for a topic queue.
+ RequestTicket struct {
+ ReqID []byte
+ Topic []byte
+ }
+
+ // TICKET is the response to REQUESTTICKET.
+ Ticket struct {
+ ReqID []byte
+ Ticket []byte
+ }
+
+ // REGTOPIC registers the sender in a topic queue using a ticket.
+ Regtopic struct {
+ ReqID []byte
+ Ticket []byte
+ ENR *enr.Record
+ }
+
+ // REGCONFIRMATION is the reply to REGTOPIC.
+ Regconfirmation struct {
+ ReqID []byte
+ Registered bool
+ }
+
+ // TOPICQUERY asks for nodes with the given topic.
+ TopicQuery struct {
+ ReqID []byte
+ Topic []byte
+ }
+)
+
+// DecodeMessage decodes the message body of a packet.
+func DecodeMessage(ptype byte, body []byte) (Packet, error) {
+ var dec Packet
+ switch ptype {
+ case PingMsg:
+ dec = new(Ping)
+ case PongMsg:
+ dec = new(Pong)
+ case FindnodeMsg:
+ dec = new(Findnode)
+ case NodesMsg:
+ dec = new(Nodes)
+ case TalkRequestMsg:
+ dec = new(TalkRequest)
+ case TalkResponseMsg:
+ dec = new(TalkResponse)
+ case RequestTicketMsg:
+ dec = new(RequestTicket)
+ case TicketMsg:
+ dec = new(Ticket)
+ case RegtopicMsg:
+ dec = new(Regtopic)
+ case RegconfirmationMsg:
+ dec = new(Regconfirmation)
+ case TopicQueryMsg:
+ dec = new(TopicQuery)
+ default:
+ return nil, fmt.Errorf("unknown packet type %d", ptype)
+ }
+ if err := rlp.DecodeBytes(body, dec); err != nil {
+ return nil, err
+ }
+ if dec.RequestID() != nil && len(dec.RequestID()) > 8 {
+ return nil, ErrInvalidReqID
+ }
+ return dec, nil
+}
+
+func (*Whoareyou) Name() string { return "WHOAREYOU/v5" }
+func (*Whoareyou) Kind() byte { return WhoareyouPacket }
+func (*Whoareyou) RequestID() []byte { return nil }
+func (*Whoareyou) SetRequestID([]byte) {}
+
+func (*Unknown) Name() string { return "UNKNOWN/v5" }
+func (*Unknown) Kind() byte { return UnknownPacket }
+func (*Unknown) RequestID() []byte { return nil }
+func (*Unknown) SetRequestID([]byte) {}
+
+func (*Ping) Name() string { return "PING/v5" }
+func (*Ping) Kind() byte { return PingMsg }
+func (p *Ping) RequestID() []byte { return p.ReqID }
+func (p *Ping) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*Pong) Name() string { return "PONG/v5" }
+func (*Pong) Kind() byte { return PongMsg }
+func (p *Pong) RequestID() []byte { return p.ReqID }
+func (p *Pong) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*Findnode) Name() string { return "FINDNODE/v5" }
+func (*Findnode) Kind() byte { return FindnodeMsg }
+func (p *Findnode) RequestID() []byte { return p.ReqID }
+func (p *Findnode) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*Nodes) Name() string { return "NODES/v5" }
+func (*Nodes) Kind() byte { return NodesMsg }
+func (p *Nodes) RequestID() []byte { return p.ReqID }
+func (p *Nodes) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*TalkRequest) Name() string { return "TALKREQ/v5" }
+func (*TalkRequest) Kind() byte { return TalkRequestMsg }
+func (p *TalkRequest) RequestID() []byte { return p.ReqID }
+func (p *TalkRequest) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*TalkResponse) Name() string { return "TALKRESP/v5" }
+func (*TalkResponse) Kind() byte { return TalkResponseMsg }
+func (p *TalkResponse) RequestID() []byte { return p.ReqID }
+func (p *TalkResponse) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*RequestTicket) Name() string { return "REQTICKET/v5" }
+func (*RequestTicket) Kind() byte { return RequestTicketMsg }
+func (p *RequestTicket) RequestID() []byte { return p.ReqID }
+func (p *RequestTicket) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*Regtopic) Name() string { return "REGTOPIC/v5" }
+func (*Regtopic) Kind() byte { return RegtopicMsg }
+func (p *Regtopic) RequestID() []byte { return p.ReqID }
+func (p *Regtopic) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*Ticket) Name() string { return "TICKET/v5" }
+func (*Ticket) Kind() byte { return TicketMsg }
+func (p *Ticket) RequestID() []byte { return p.ReqID }
+func (p *Ticket) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*Regconfirmation) Name() string { return "REGCONFIRMATION/v5" }
+func (*Regconfirmation) Kind() byte { return RegconfirmationMsg }
+func (p *Regconfirmation) RequestID() []byte { return p.ReqID }
+func (p *Regconfirmation) SetRequestID(id []byte) { p.ReqID = id }
+
+func (*TopicQuery) Name() string { return "TOPICQUERY/v5" }
+func (*TopicQuery) Kind() byte { return TopicQueryMsg }
+func (p *TopicQuery) RequestID() []byte { return p.ReqID }
+func (p *TopicQuery) SetRequestID(id []byte) { p.ReqID = id }
diff --git a/discover/v5wire/session.go b/discover/v5wire/session.go
new file mode 100644
index 0000000..d52b5c1
--- /dev/null
+++ b/discover/v5wire/session.go
@@ -0,0 +1,142 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package v5wire
+
+import (
+ "crypto/ecdsa"
+ crand "crypto/rand"
+ "encoding/binary"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/hashicorp/golang-lru/simplelru"
+)
+
+const handshakeTimeout = time.Second
+
+// The SessionCache keeps negotiated encryption keys and
+// state for in-progress handshakes in the Discovery v5 wire protocol.
+type SessionCache struct {
+ sessions *simplelru.LRU
+ handshakes map[sessionID]*Whoareyou
+ clock mclock.Clock
+
+ // hooks for overriding randomness.
+ nonceGen func(uint32) (Nonce, error)
+ maskingIVGen func([]byte) error
+ ephemeralKeyGen func() (*ecdsa.PrivateKey, error)
+}
+
+// sessionID identifies a session or handshake.
+type sessionID struct {
+ id enode.ID
+ addr string
+}
+
+// session contains session information
+type session struct {
+ writeKey []byte
+ readKey []byte
+ nonceCounter uint32
+}
+
+// keysFlipped returns a copy of s with the read and write keys flipped.
+func (s *session) keysFlipped() *session {
+ return &session{s.readKey, s.writeKey, s.nonceCounter}
+}
+
+func NewSessionCache(maxItems int, clock mclock.Clock) *SessionCache {
+ cache, err := simplelru.NewLRU(maxItems, nil)
+ if err != nil {
+ panic("can't create session cache")
+ }
+ return &SessionCache{
+ sessions: cache,
+ handshakes: make(map[sessionID]*Whoareyou),
+ clock: clock,
+ nonceGen: generateNonce,
+ maskingIVGen: generateMaskingIV,
+ ephemeralKeyGen: crypto.GenerateKey,
+ }
+}
+
+func generateNonce(counter uint32) (n Nonce, err error) {
+ binary.BigEndian.PutUint32(n[:4], counter)
+ _, err = crand.Read(n[4:])
+ return n, err
+}
+
+func generateMaskingIV(buf []byte) error {
+ _, err := crand.Read(buf)
+ return err
+}
+
+// nextNonce creates a nonce for encrypting a message to the given session.
+func (sc *SessionCache) nextNonce(s *session) (Nonce, error) {
+ s.nonceCounter++
+ return sc.nonceGen(s.nonceCounter)
+}
+
+// session returns the current session for the given node, if any.
+func (sc *SessionCache) session(id enode.ID, addr string) *session {
+ item, ok := sc.sessions.Get(sessionID{id, addr})
+ if !ok {
+ return nil
+ }
+ return item.(*session)
+}
+
+// readKey returns the current read key for the given node.
+func (sc *SessionCache) readKey(id enode.ID, addr string) []byte {
+ if s := sc.session(id, addr); s != nil {
+ return s.readKey
+ }
+ return nil
+}
+
+// storeNewSession stores new encryption keys in the cache.
+func (sc *SessionCache) storeNewSession(id enode.ID, addr string, s *session) {
+ sc.sessions.Add(sessionID{id, addr}, s)
+}
+
+// getHandshake gets the handshake challenge we previously sent to the given remote node.
+func (sc *SessionCache) getHandshake(id enode.ID, addr string) *Whoareyou {
+ return sc.handshakes[sessionID{id, addr}]
+}
+
+// storeSentHandshake stores the handshake challenge sent to the given remote node.
+func (sc *SessionCache) storeSentHandshake(id enode.ID, addr string, challenge *Whoareyou) {
+ challenge.sent = sc.clock.Now()
+ sc.handshakes[sessionID{id, addr}] = challenge
+}
+
+// deleteHandshake deletes handshake data for the given node.
+func (sc *SessionCache) deleteHandshake(id enode.ID, addr string) {
+ delete(sc.handshakes, sessionID{id, addr})
+}
+
+// handshakeGC deletes timed-out handshakes.
+func (sc *SessionCache) handshakeGC() {
+ deadline := sc.clock.Now().Add(-handshakeTimeout)
+ for key, challenge := range sc.handshakes {
+ if challenge.sent < deadline {
+ delete(sc.handshakes, key)
+ }
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..608f478
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module github.com/status-im/go-discover
+
+go 1.15
+
+require (
+ github.com/davecgh/go-spew v1.1.1
+ github.com/ethereum/go-ethereum v1.10.13
+ github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
+ golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..aaa1f23
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,588 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
+github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
+github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw=
+github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
+github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo=
+github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y=
+github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4=
+github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0=
+github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM=
+github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
+github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
+github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
+github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
+github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
+github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
+github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
+github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304=
+github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ=
+github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
+github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
+github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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 v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
+github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
+github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
+github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
+github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ethereum/go-ethereum v1.10.13 h1:DEYFP9zk+Gruf3ae1JOJVhNmxK28ee+sMELPLgYTXpA=
+github.com/ethereum/go-ethereum v1.10.13/go.mod h1:W3yfrFyL9C1pHcwY5hmRHVDaorTiQxhYBkKyu5mEDHw=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
+github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
+github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
+github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
+github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
+github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs=
+github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
+github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM=
+github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY=
+github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI=
+github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
+github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk=
+github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE=
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
+github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
+github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8=
+github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE=
+github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0=
+github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po=
+github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU=
+github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
+github.com/karalabe/usb v0.0.0-20211005121534-4c5740d64559/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
+github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
+github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
+github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
+github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
+github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
+github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE=
+github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
+github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
+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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
+github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
+github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
+github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
+github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
+github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
+github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
+github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+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-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
+golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-20200317015054-43a5402ce75a/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200107162124-548cf772de50/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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/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-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU=
+golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
+gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
+gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/internal/testlog/testlog.go b/internal/testlog/testlog.go
new file mode 100644
index 0000000..684339f
--- /dev/null
+++ b/internal/testlog/testlog.go
@@ -0,0 +1,142 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package testlog provides a log handler for unit tests.
+package testlog
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// Handler returns a log handler which logs to the unit test log of t.
+func Handler(t *testing.T, level log.Lvl) log.Handler {
+ return log.LvlFilterHandler(level, &handler{t, log.TerminalFormat(false)})
+}
+
+type handler struct {
+ t *testing.T
+ fmt log.Format
+}
+
+func (h *handler) Log(r *log.Record) error {
+ h.t.Logf("%s", h.fmt.Format(r))
+ return nil
+}
+
+// logger implements log.Logger such that all output goes to the unit test log via
+// t.Logf(). All methods in between logger.Trace, logger.Debug, etc. are marked as test
+// helpers, so the file and line number in unit test output correspond to the call site
+// which emitted the log message.
+type logger struct {
+ t *testing.T
+ l log.Logger
+ mu *sync.Mutex
+ h *bufHandler
+}
+
+type bufHandler struct {
+ buf []*log.Record
+ fmt log.Format
+}
+
+func (h *bufHandler) Log(r *log.Record) error {
+ h.buf = append(h.buf, r)
+ return nil
+}
+
+// Logger returns a logger which logs to the unit test log of t.
+func Logger(t *testing.T, level log.Lvl) log.Logger {
+ l := &logger{
+ t: t,
+ l: log.New(),
+ mu: new(sync.Mutex),
+ h: &bufHandler{fmt: log.TerminalFormat(false)},
+ }
+ l.l.SetHandler(log.LvlFilterHandler(level, l.h))
+ return l
+}
+
+func (l *logger) Trace(msg string, ctx ...interface{}) {
+ l.t.Helper()
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.l.Trace(msg, ctx...)
+ l.flush()
+}
+
+func (l *logger) Debug(msg string, ctx ...interface{}) {
+ l.t.Helper()
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.l.Debug(msg, ctx...)
+ l.flush()
+}
+
+func (l *logger) Info(msg string, ctx ...interface{}) {
+ l.t.Helper()
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.l.Info(msg, ctx...)
+ l.flush()
+}
+
+func (l *logger) Warn(msg string, ctx ...interface{}) {
+ l.t.Helper()
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.l.Warn(msg, ctx...)
+ l.flush()
+}
+
+func (l *logger) Error(msg string, ctx ...interface{}) {
+ l.t.Helper()
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.l.Error(msg, ctx...)
+ l.flush()
+}
+
+func (l *logger) Crit(msg string, ctx ...interface{}) {
+ l.t.Helper()
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.l.Crit(msg, ctx...)
+ l.flush()
+}
+
+func (l *logger) New(ctx ...interface{}) log.Logger {
+ return &logger{l.t, l.l.New(ctx...), l.mu, l.h}
+}
+
+func (l *logger) GetHandler() log.Handler {
+ return l.l.GetHandler()
+}
+
+func (l *logger) SetHandler(h log.Handler) {
+ l.l.SetHandler(h)
+}
+
+// flush writes all buffered messages and clears the buffer.
+func (l *logger) flush() {
+ l.t.Helper()
+ for _, r := range l.h.buf {
+ l.t.Logf("%s", l.h.fmt.Format(r))
+ }
+ l.h.buf = nil
+}