Initial commit
This commit is contained in:
parent
53344bd46f
commit
f99706cd09
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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")
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 }
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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
|
||||
}
|
Loading…
Reference in New Issue