feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent e4778b4576
commit 379cb741ed
9082 changed files with 997824 additions and 997542 deletions
+9
View File
@@ -0,0 +1,9 @@
.git
**/target/
**/*.txt
**/*.md
/docker/
!/target/release-artifacts/**/*
# dotfiles in the repo root
/.*
+29
View File
@@ -0,0 +1,29 @@
root = true
[*]
indent_style=tab
indent_size=tab
tab_width=4
end_of_line=lf
charset=utf-8
trim_trailing_whitespace=true
max_line_length=100
insert_final_newline=true
[*.yml]
indent_style=space
indent_size=2
tab_width=8
end_of_line=lf
[*.sh]
indent_style=space
indent_size=4
tab_width=8
end_of_line=lf
[*.json]
indent_style=space
indent_size=2
tab_width=8
end_of_line=lf
+675
View File
@@ -0,0 +1,675 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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:
{project} Copyright (C) {year} {fullname}
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
<http://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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+214
View File
@@ -0,0 +1,214 @@
# Pezcumulus ☁️
[![Doc](https://img.shields.io/badge/pezcumulus%20docs-master-brightgreen)](https://docs.pezkuwichain.io/pezcumulus/)
This repository contains both the Pezcumulus SDK and also specific chains implemented on top of this SDK.
If you only want to run a **PezkuwiChain Teyrchain Node**, check out our [container section](../docs/contributor/container.md).
## Pezcumulus SDK
A set of tools for writing [Bizinikiwi](https://bizinikiwi.pezkuwichain.app/)-based [PezkuwiChain](https://wiki.network.pezkuwichain.io/en/)
[teyrchains](https://wiki.network.pezkuwichain.io/docs/en/learn-parachains). Refer to the included [overview](docs/overview.md)
for architectural details, and the [Connect to a relay chain how-to
guide](https://docs.pezkuwichain.io/reference/how-to-guides/parachains/connect-to-a-relay-chain/) for a guided walk-through
of using these tools.
It's easy to write blockchains using Bizinikiwi, and the overhead of writing teyrchains' distribution, p2p, database, and
synchronization layers should be just as low. This project aims to make it easy to write teyrchains for PezkuwiChain by
leveraging the power of Bizinikiwi.
Pezcumulus clouds are shaped sort of like dots; together they form a system that is intricate, beautiful and functional.
### Consensus
[`teyrchain-consensus`](https://github.com/pezkuwichain/pezkuwi-sdk/blob/master/pezcumulus/client/consensus/common/src/parachain_consensus.rs)
is a [consensus engine](https://docs.pezkuwichain.io/v3/advanced/consensus) for Bizinikiwi that follows a PezkuwiChain [relay
chain](https://wiki.network.pezkuwichain.io/docs/en/learn-architecture#relay-chain). This will run a PezkuwiChain node internally,
and dictate to the client and synchronization algorithms which chain to follow,
[finalize](https://wiki.network.pezkuwichain.io/docs/en/learn-consensus#probabilistic-vs-provable-finality), and treat as best.
### Collator
A PezkuwiChain [collator](https://wiki.network.pezkuwichain.io/docs/en/learn-collator) for the teyrchain is implemented by the
`pezkuwi-teyrchain` binary (previously called `pezkuwi-collator`).
You may run `pezkuwi-teyrchain` locally after building it or using one of the container option described
[here](../docs/contributor/container.md).
### Relay Chain Interaction
To operate a teyrchain node, a connection to the corresponding relay chain is necessary. This can be achieved in one of
two ways:
1. Run a full relay chain node within the teyrchain node (default)
2. Connect to an external relay chain node via WebSocket RPC
#### In-process Relay Chain Node
If an external relay chain node is not specified (default behavior), then a full relay chain node is spawned within the
same process.
This node has all of the typical components of a regular PezkuwiChain node and will have to fully sync with the relay chain
to work.
##### Example command
```bash
pezkuwi-teyrchain \
--chain teyrchain-chainspec.json \
--tmp \
-- \
--chain relaychain-chainspec.json
```
#### External Relay Chain Node
An external relay chain node is connected via WebSocket RPC by using the `--relay-chain-rpc-urls` command line
argument. This option accepts one or more space-separated WebSocket URLs to a full relay chain node. By default, only
the first URL will be used, with the rest as a backup in case the connection to the first node is lost.
Teyrchain nodes using this feature won't have to fully sync with the relay chain to work, so in general they will use
fewer system resources.
**Note:** At this time, any teyrchain nodes using this feature will still spawn a significantly cut-down relay chain
node in-process. Even though they lack the majority of normal PezkuwiChain subsystems, they will still need to connect
directly to the relay chain network.
##### Example command
```bash
pezkuwi-teyrchain \
--chain teyrchain-chainspec.json \
--tmp \
--relay-chain-rpc-urls \
"ws://relaychain-rpc-endpoint:9944" \
"ws://relaychain-rpc-endpoint-backup:9944" \
-- \
--chain relaychain-chainspec.json
```
## Installation and Setup
Before building Pezcumulus SDK based nodes / runtimes prepare your environment by following Bizinikiwi [installation
instructions](https://docs.pezkuwichain.io/main-docs/install/).
To launch a local network, you can use [zombienet](https://github.com/paritytech/zombienet) for quick setup and
experimentation or follow the [manual setup](#manual-setup).
### Zombienet
We use Zombienet to spin up networks for integration tests and local networks. Follow [these installation
steps](https://github.com/paritytech/zombienet#requirements-by-provider) to set it up on your machine. A simple network
specification with two relay chain nodes and one collator is located at
[zombienet/examples/small_network.toml](zombienet/examples/small_network.toml).
#### Which provider should I use?
Zombienet offers multiple providers to run networks. Choose the one that best fits your needs:
- **Podman:** Choose this if you want to spin up a network quick and easy.
- **Native:** Choose this if you want to develop and deploy your changes. Requires compilation of the binaries.
- **Kubernetes:** Choose this for advanced use-cases or running on cloud-infrastructure.
#### How to run
To run the example network, use the following commands:
```bash
# Podman provider
zombienet --provider podman spawn ./zombienet/examples/small_network.toml
# Native provider, assumes pezkuwi and pezkuwi-teyrchains binary in $PATH
zombienet --provider native spawn ./zombienet/examples/small_network.toml
```
### Manual Setup
#### Launch the Relay Chain
```bash
# Clone
git clone https://github.com/pezkuwichain/pezkuwi-sdk
# Compile PezkuwiChain's required binaries
cargo build --release -p pezkuwi
# Generate a raw chain spec
./target/release/pezkuwi build-spec --chain pezkuwichain-local --disable-default-bootnode --raw > pezkuwichain-local-cfde.json
# Alice
./target/release/pezkuwi --chain pezkuwichain-local-cfde.json --alice --tmp
# Bob (In a separate terminal)
./target/release/pezkuwi --chain pezkuwichain-local-cfde.json --bob --tmp --port 30334
```
#### Launch the Teyrchain
```bash
# Compile
cargo build --release -p pezkuwi-teyrchain-bin
# Export genesis state
./target/release/pezkuwi-teyrchain export-genesis-state > genesis-state
# Export genesis wasm
./target/release/pezkuwi-teyrchain export-genesis-wasm > genesis-wasm
# Collator1
./target/release/pezkuwi-teyrchain --collator --alice --force-authoring \
--tmp --port 40335 --rpc-port 9946 -- --chain pezkuwichain-local-cfde.json --port 30335
# Collator2
./target/release/pezkuwi-teyrchain --collator --bob --force-authoring \
--tmp --port 40336 --rpc-port 9947 -- --chain pezkuwichain-local-cfde.json --port 30336
# Teyrchain Full Node 1
./target/release/pezkuwi-teyrchain --tmp --port 40337 --rpc-port 9948 -- \
--chain pezkuwichain-local-cfde.json --port 30337
```
#### Register the teyrchain
![image](https://user-images.githubusercontent.com/2915325/99548884-1be13580-2987-11eb-9a8b-20be658d34f9.png)
## Asset Hub 🪙
This repository also contains the Asset Hub runtimes. Asset Hub is a system teyrchain providing an asset store for the
PezkuwiChain ecosystem.
### Build & Launch a Node
To run an Asset Hub node, you will need to compile the `pezkuwi-teyrchain` binary:
```bash
cargo build --release --locked --bin pezkuwi-teyrchain
```
Once the executable is built, launch the teyrchain node via:
```bash
CHAIN=asset-hub-zagros # or asset-hub-kusama
./target/release/pezkuwi-teyrchain --chain $CHAIN
```
Refer to the [setup instructions](#manual-setup) to run a local network for development.
## Bridge-hub 📝
See [the `bridge-hubs` readme](teyrchains/runtimes/bridge-hubs/README.md) for details.
## Testnets
### Paseo
[Paseo](https://pezkuwichain.io/?rpc=wss%3A%2F%2Fpaseo.rpc.amforc.com#/explorer) is the newest testnet for PezkuwiChain,
replacing pezkuwichain as a decentralised, community run, stable testnet for Teyrchain teams and dapp developers to build on.
For more information, check the [Paseo repo](https://github.com/paseo-network).
### zagros
[zagros](https://pezkuwichain.io/?rpc=wss%3A%2F%2Fzagros-rpc.polkadot.io#/explorer)
is a long running testnet for PezkuwiChain,
primarily intended to provide a testing environment for Parity to test the latest changes in the SDK.
### Testnet Teyrchains
A few testnet teyrchain instances:
- [Asset Hub zagros](https://pezkuwichain.io/?rpc=wss%3A%2F%2Fzagros-asset-hub-rpc.polkadot.io#/explorer)
- [Frequency Paseo](https://pezkuwichain.io/?rpc=wss%3A%2F%2F0.rpc.testnet.amplica.io#/explorer)
The network uses horizontal message passing (HRMP) to enable communication between teyrchains and the relay chain and,
in turn, between teyrchains. This means that every message is sent to the relay chain, and from the relay chain to its
destination teyrchain.
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "pezcumulus-pov-validator"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
homepage.workspace = true
description = "A tool for validating PoVs locally"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codec = { workspace = true }
pezkuwi-node-primitives = { workspace = true }
pezkuwi-primitives = { workspace = true }
pezkuwi-teyrchain-primitives = { workspace = true }
pezsc-executor = { workspace = true }
pezsp-core = { workspace = true }
pezsp-io = { workspace = true }
pezsp-maybe-compressed-blob = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[features]
runtime-benchmarks = [
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"pezsc-executor/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
]
+151
View File
@@ -0,0 +1,151 @@
// This file is part of Pezcumulus.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// 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/>.
use clap::Parser;
use codec::{Decode, Encode};
use pezkuwi_node_primitives::{BlockData, PoV, POV_BOMB_LIMIT};
use pezkuwi_primitives::PersistedValidationData;
use pezkuwi_teyrchain_primitives::primitives::ValidationParams;
use pezsc_executor::WasmExecutor;
use pezsp_core::traits::{CallContext, CodeExecutor, RuntimeCode, WrappedRuntimeCode};
use std::{fs, path::PathBuf, time::Instant};
use tracing::level_filters::LevelFilter;
// This is now determined by the chain, call `validation_code_bomb_limit` API.
// max_code_size * 10 = 30MB currently. Update constant if needed.
const VALIDATION_CODE_BOMB_LIMIT: usize = 30 * 1024 * 1024;
/// Tool for validating a `PoV` locally.
#[derive(Parser)]
struct Cli {
/// The path to the validation code that should be used to validate the `PoV`.
///
/// The validation code can either be downloaded from the relay chain that the teyrchain is
/// connected to or by building the runtime manually to obtain the WASM binary.
#[arg(long)]
validation_code: PathBuf,
/// The path to the `PoV` to validate.
///
/// The `PoV`'s can be obtained by running `pezkuwi-teyrchains --collator --chain YOUR_CHAIN
/// --export-pov-to-path PATH_TO_EXPORT` and then choose one of the exported `PoV`'s.
#[arg(long)]
pov: PathBuf,
}
fn main() -> anyhow::Result<()> {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(LevelFilter::INFO.into()),
)
.with_writer(std::io::stderr)
.try_init();
let cli = Cli::parse();
let validation_code = fs::read(&cli.validation_code).map_err(|error| {
tracing::error!(%error, path = %cli.validation_code.display(), "Failed to read validation code");
anyhow::anyhow!("Failed to read validation code")
})?;
let validation_code =
pezsp_maybe_compressed_blob::decompress(&validation_code, VALIDATION_CODE_BOMB_LIMIT)
.map_err(|error| {
tracing::error!(%error, "Failed to decompress validation code");
anyhow::anyhow!("Failed to decompress validation code")
})?;
let pov_file = fs::read(&cli.pov).map_err(|error| {
tracing::error!(%error, path = %cli.pov.display(), "Failed to read PoV");
anyhow::anyhow!("Failed to read PoV")
})?;
let executor = WasmExecutor::<pezsp_io::BizinikiwiHostFunctions>::builder()
.with_allow_missing_host_functions(true)
.build();
let runtime_code = RuntimeCode {
code_fetcher: &WrappedRuntimeCode(validation_code.into()),
heap_pages: None,
// The hash is used for caching, which we need here, but we only use one wasm file. So, the
// actual hash is not that important.
hash: vec![1, 2, 3],
};
// We are calling `Core_version` to get the wasm file compiled. We don't care about the result.
let _ = executor
.call(
&mut pezsp_io::TestExternalities::default().ext(),
&runtime_code,
"Core_version",
&[],
CallContext::Offchain,
)
.0;
let pov_file_ptr = &mut &pov_file[..];
let pov = PoV::decode(pov_file_ptr).map_err(|error| {
tracing::error!(%error, "Failed to decode `PoV`");
anyhow::anyhow!("Failed to decode `PoV`")
})?;
let pvd = PersistedValidationData::decode(pov_file_ptr).map_err(|error| {
tracing::error!(%error, "Failed to `PersistedValidationData`");
anyhow::anyhow!("Failed to decode `PersistedValidationData`")
})?;
let pov = pezsp_maybe_compressed_blob::decompress(&pov.block_data.0, POV_BOMB_LIMIT).map_err(
|error| {
tracing::error!(%error, "Failed to decompress `PoV`");
anyhow::anyhow!("Failed to decompress `PoV`")
},
)?;
let validation_params = ValidationParams {
relay_parent_number: pvd.relay_parent_number,
relay_parent_storage_root: pvd.relay_parent_storage_root,
parent_head: pvd.parent_head,
block_data: BlockData(pov.into()),
};
tracing::info!("Starting validation");
let start = Instant::now();
let res = executor
.call(
&mut pezsp_io::TestExternalities::default().ext(),
&runtime_code,
"validate_block",
&validation_params.encode(),
CallContext::Offchain,
)
.0;
let duration = start.elapsed();
match res {
Ok(_) => tracing::info!("Validation was successful"),
Err(error) => tracing::error!(%error, "Validation failed"),
}
tracing::info!("Validation took {}ms", duration.as_millis());
Ok(())
}
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "pezcumulus-client-bootnodes"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
description = "Teyrchain bootnodes registration and discovery."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[lints]
workspace = true
[build-dependencies]
prost-build = { workspace = true }
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
hex = { workspace = true, default-features = true }
ip_network = { workspace = true }
log = { workspace = true, default-features = true }
num-traits = { workspace = true, default-features = true }
prost = { workspace = true }
tokio = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-network = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsp-consensus-babe = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-consensus-babe/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+21
View File
@@ -0,0 +1,21 @@
// This file is part of Pezcumulus.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// 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/>.
fn main() {
prost_build::compile_protos(&["src/schema/response.proto"], &["src/schema"]).unwrap();
}
@@ -0,0 +1,559 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnode advertisement.
use crate::config::MAX_ADDRESSES;
use codec::{Compact, CompactRef, Decode, Encode};
use cumulus_primitives_core::{
relay_chain::{Hash as RelayHash, Header as RelayHeader},
ParaId,
};
use cumulus_relay_chain_interface::{RelayChainInterface, RelayChainResult};
use futures::{future::Fuse, pin_mut, FutureExt, StreamExt};
use ip_network::IpNetwork;
use log::{debug, error, trace, warn};
use prost::Message;
use pezsc_network::{
config::OutgoingResponse,
event::{DhtEvent, Event},
multiaddr::Protocol,
request_responses::IncomingRequest,
service::traits::NetworkService,
KademliaKey, Multiaddr,
};
use pezsp_consensus_babe::{digests::CompatibleDigestItem, Epoch, Randomness};
use pezsp_runtime::traits::Header as _;
use std::{collections::HashSet, pin::Pin, sync::Arc};
use tokio::time::Sleep;
/// Log target for this file.
const LOG_TARGET: &str = "bootnodes::advertisement";
/// Delay before retrying the DHT content provider publish operation.
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(30);
/// Teyrchain bootnode advertisement parameters.
pub struct BootnodeAdvertisementParams {
/// Teyrchain ID.
pub para_id: ParaId,
/// Relay chain interface.
pub relay_chain_interface: Arc<dyn RelayChainInterface>,
/// Relay chain node network service.
pub relay_chain_network: Arc<dyn NetworkService>,
/// Bootnode request-response protocol request receiver.
pub request_receiver: async_channel::Receiver<IncomingRequest>,
/// Teyrchain node network service.
pub teyrchain_network: Arc<dyn NetworkService>,
/// Whether to advertise non-global IPs.
pub advertise_non_global_ips: bool,
/// Teyrchain genesis hash.
pub teyrchain_genesis_hash: Vec<u8>,
/// Teyrchain fork ID.
pub teyrchain_fork_id: Option<String>,
/// Teyrchain side public addresses.
pub public_addresses: Vec<Multiaddr>,
}
/// Teyrchain bootnode advertisement service.
pub struct BootnodeAdvertisement {
para_id: ParaId,
para_id_scale_compact: Vec<u8>,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_network: Arc<dyn NetworkService>,
current_epoch_key: Option<KademliaKey>,
next_epoch_key: Option<KademliaKey>,
current_epoch_publish_retry: Pin<Box<Fuse<Sleep>>>,
next_epoch_publish_retry: Pin<Box<Fuse<Sleep>>>,
request_receiver: async_channel::Receiver<IncomingRequest>,
teyrchain_network: Arc<dyn NetworkService>,
advertise_non_global_ips: bool,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
public_addresses: Vec<Multiaddr>,
}
impl BootnodeAdvertisement {
/// Create a new bootnode advertisement service.
pub fn new(
BootnodeAdvertisementParams {
para_id,
relay_chain_interface,
relay_chain_network,
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
public_addresses,
}: BootnodeAdvertisementParams,
) -> Self {
// Discard `/p2p/<peer_id>` from public addresses on initialization to not generate warnings
// on every request for what is an operator mistake.
let local_peer_id = teyrchain_network.local_peer_id();
let public_addresses = public_addresses
.into_iter()
.filter_map(|mut addr| match addr.iter().last() {
Some(Protocol::P2p(peer_id)) if &peer_id == local_peer_id.as_ref() => {
addr.pop();
Some(addr)
},
Some(Protocol::P2p(_)) => {
warn!(
target: LOG_TARGET,
"Discarding public address containing not our peer ID: {addr}",
);
None
},
_ => Some(addr),
})
.collect();
Self {
para_id,
para_id_scale_compact: CompactRef(&para_id).encode(),
relay_chain_interface,
relay_chain_network,
current_epoch_key: None,
next_epoch_key: None,
current_epoch_publish_retry: Box::pin(Fuse::terminated()),
next_epoch_publish_retry: Box::pin(Fuse::terminated()),
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
public_addresses,
}
}
async fn current_epoch(&self, hash: RelayHash) -> RelayChainResult<Epoch> {
let res = self
.relay_chain_interface
.call_runtime_api("BabeApi_current_epoch", hash, &[])
.await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
async fn next_epoch(&self, hash: RelayHash) -> RelayChainResult<Epoch> {
let res = self
.relay_chain_interface
.call_runtime_api("BabeApi_next_epoch", hash, &[])
.await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
fn epoch_key(&self, randomness: Randomness) -> KademliaKey {
self.para_id_scale_compact
.clone()
.into_iter()
.chain(randomness.into_iter())
.collect::<Vec<_>>()
.into()
}
async fn current_and_next_epoch_keys(
&self,
header: RelayHeader,
) -> (Option<KademliaKey>, Option<KademliaKey>) {
let hash = header.hash();
let number = header.number();
let current_epoch = match self.current_epoch(hash).await {
Ok(epoch) => Some(epoch),
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to query current epoch for #{number} {hash:?}: {e}",
);
None
},
};
let next_epoch = match self.next_epoch(hash).await {
Ok(epoch) => Some(epoch),
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to query next epoch for #{number} {hash:?}: {e}",
);
None
},
};
(
current_epoch.map(|epoch| self.epoch_key(epoch.randomness)),
next_epoch.map(|epoch| self.epoch_key(epoch.randomness)),
)
}
async fn handle_import_notification(&mut self, header: RelayHeader) {
if let Some(ref old_current_epoch_key) = self.current_epoch_key {
// Readvertise on start of new epoch only.
let Some(next_epoch_descriptor) =
header.digest().convert_first(|v| v.as_next_epoch_descriptor())
else {
return;
};
let next_epoch_key = self.epoch_key(next_epoch_descriptor.randomness);
if Some(&next_epoch_key) == self.next_epoch_key.as_ref() {
trace!(
target: LOG_TARGET,
"Next epoch descriptor contains the same randomness as the previous one, \
not considering this as epoch change (switched fork?)",
);
return;
}
// Epoch changed, cancel retry attempts.
self.current_epoch_publish_retry = Box::pin(Fuse::terminated());
self.next_epoch_publish_retry = Box::pin(Fuse::terminated());
debug!(target: LOG_TARGET, "New epoch started, readvertising teyrchain bootnode.");
// Stop advertisement of the obsolete key.
debug!(
target: LOG_TARGET,
"Stopping advertisement of bootnode for old current epoch key {}",
hex::encode(old_current_epoch_key.as_ref()),
);
self.relay_chain_network.stop_providing(old_current_epoch_key.clone());
// Advertise current keys.
self.current_epoch_key = self.next_epoch_key.clone();
self.next_epoch_key = Some(next_epoch_key);
if let Some(ref current_epoch_key) = self.current_epoch_key {
debug!(
target: LOG_TARGET,
"Advertising bootnode for current (old next) epoch key {}",
hex::encode(current_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(current_epoch_key.clone());
}
if let Some(ref next_epoch_key) = self.next_epoch_key {
debug!(
target: LOG_TARGET,
"Advertising bootnode for next epoch key {}",
hex::encode(next_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(next_epoch_key.clone());
}
} else {
// First advertisement on startup.
let (current_epoch_key, next_epoch_key) =
self.current_and_next_epoch_keys(header).await;
self.current_epoch_key = current_epoch_key.clone();
self.next_epoch_key = next_epoch_key.clone();
if let Some(current_epoch_key) = current_epoch_key {
debug!(
target: LOG_TARGET,
"Initial advertisement of bootnode for current epoch key {}",
hex::encode(current_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(current_epoch_key);
} else {
warn!(
target: LOG_TARGET,
"Initial advertisement of bootnode for current epoch failed: no key."
);
}
if let Some(next_epoch_key) = next_epoch_key {
debug!(
target: LOG_TARGET,
"Initial advertisement of bootnode for next epoch key {}",
hex::encode(next_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(next_epoch_key);
} else {
warn!(
target: LOG_TARGET,
"Initial advertisement of bootnode for next epoch failed: no key."
);
}
}
}
/// The list of teyrchain side addresses.
///
/// The addresses are sorted as follows:
/// 1) public addresses provided by the operator
/// 2) global listen addresses
/// 3) discovered external addresses
/// 4) non-global listen addresses
/// 5) loopback listen addresses
fn paranode_addresses(&self) -> Vec<Multiaddr> {
let local_peer_id = self.teyrchain_network.local_peer_id();
// Discard `/p2p/<peer_id>` part. `None` if the address contains foreign peer ID.
let without_p2p = |mut addr: Multiaddr| match addr.iter().last() {
Some(Protocol::P2p(peer_id)) if &peer_id == local_peer_id.as_ref() => {
addr.pop();
Some(addr)
},
Some(Protocol::P2p(_)) => {
warn!(
target: LOG_TARGET,
"Ignoring teyrchain side address containing not our peer ID: {addr}",
);
None
},
_ => Some(addr),
};
// Check if the address is global.
let is_global = |address: &Multiaddr| {
address.iter().all(|protocol| match protocol {
// The `ip_network` library is used because its `is_global()` method is stable,
// while `is_global()` in the standard library currently isn't.
Protocol::Ip4(ip) => IpNetwork::from(ip).is_global(),
Protocol::Ip6(ip) => IpNetwork::from(ip).is_global(),
_ => true,
})
};
// Check if the address is a loopback address.
let is_loopback = |address: &Multiaddr| {
address.iter().any(|protocol| match protocol {
Protocol::Ip4(ip) => IpNetwork::from(ip).is_loopback(),
Protocol::Ip6(ip) => IpNetwork::from(ip).is_loopback(),
_ => false,
})
};
// 1) public addresses provided by the operator
let public_addresses = self.public_addresses.clone().into_iter();
// 2) global listen addresses
let global_listen_addresses =
self.teyrchain_network.listen_addresses().into_iter().filter(is_global);
// 3a) discovered external addresses (global)
let global_external_addresses =
self.teyrchain_network.external_addresses().into_iter().filter(is_global);
// 3b) discovered external addresses (non-global)
let non_global_external_addresses = self
.teyrchain_network
.external_addresses()
.into_iter()
.filter(|addr| !is_global(addr));
// 4) non-global listen addresses
let non_global_listen_addresses = self
.teyrchain_network
.listen_addresses()
.into_iter()
.filter(|addr| !is_global(addr) && !is_loopback(addr));
// 5) loopback listen addresses
let loopback_listen_addresses =
self.teyrchain_network.listen_addresses().into_iter().filter(is_loopback);
let mut seen = HashSet::new();
public_addresses
.chain(global_listen_addresses)
.chain(global_external_addresses)
.chain(
self.advertise_non_global_ips
.then_some(
non_global_external_addresses
.chain(non_global_listen_addresses)
.chain(loopback_listen_addresses),
)
.into_iter()
.flatten(),
)
.filter_map(without_p2p)
// Deduplicate addresses.
.filter(|addr| seen.insert(addr.clone()))
.take(MAX_ADDRESSES)
.collect()
}
fn handle_request(&mut self, req: IncomingRequest) {
if req.payload == self.para_id_scale_compact {
trace!(
target: LOG_TARGET,
"Serving paranode addresses request from {:?} for teyrchain ID {}",
req.peer,
self.para_id,
);
let response = crate::schema::Response {
peer_id: self.teyrchain_network.local_peer_id().to_bytes(),
addrs: self.paranode_addresses().iter().map(|a| a.to_vec()).collect(),
genesis_hash: self.teyrchain_genesis_hash.clone(),
fork_id: self.teyrchain_fork_id.clone(),
};
let _ = req.pending_response.send(OutgoingResponse {
result: Ok(response.encode_to_vec()),
reputation_changes: Vec::new(),
sent_feedback: None,
});
} else {
let payload = req.payload;
match Compact::<ParaId>::decode(&mut &payload[..]) {
Ok(para_id) => {
trace!(
target: LOG_TARGET,
"Ignoring request for teyrchain ID {} != self teyrchain ID {} from {:?}",
para_id.0,
self.para_id,
req.peer,
);
},
Err(e) => {
trace!(
target: LOG_TARGET,
"Cannot decode teyrchain ID in a request from {:?}: {e}",
req.peer,
);
},
}
}
}
fn handle_dht_event(&mut self, event: DhtEvent) {
match event {
DhtEvent::StartedProviding(key) =>
if Some(&key) == self.current_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Successfully published provider for current epoch key {}",
hex::encode(key.as_ref()),
);
} else if Some(&key) == self.next_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Successfully published provider for next epoch key {}",
hex::encode(key.as_ref()),
);
},
DhtEvent::StartProvidingFailed(key) => {
if Some(&key) == self.current_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Failed to publish provider for current epoch key {}. Retrying in {RETRY_DELAY:?}",
hex::encode(key.as_ref()),
);
self.current_epoch_publish_retry =
Box::pin(tokio::time::sleep(RETRY_DELAY).fuse());
} else if Some(&key) == self.next_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Failed to publish provider for next epoch key {}. Retrying in {RETRY_DELAY:?}",
hex::encode(key.as_ref()),
);
self.next_epoch_publish_retry =
Box::pin(tokio::time::sleep(RETRY_DELAY).fuse());
}
},
_ => {},
}
}
fn retry_for_current_epoch(&mut self) {
if let Some(current_epoch_key) = self.current_epoch_key.clone() {
debug!(
target: LOG_TARGET,
"Retrying advertising bootnode for current epoch key {}",
hex::encode(current_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(current_epoch_key);
} else {
error!(
target: LOG_TARGET,
"Retrying advertising bootnode for current epoch failed: no key. This is a bug."
);
}
}
fn retry_for_next_epoch(&mut self) {
if let Some(next_epoch_key) = self.next_epoch_key.clone() {
debug!(
target: LOG_TARGET,
"Retrying advertising bootnode for next epoch key {}",
hex::encode(next_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(next_epoch_key);
} else {
error!(
target: LOG_TARGET,
"Retrying advertising bootnode for next epoch failed: no key. This is a bug."
);
}
}
/// Run the bootnode advertisement service.
pub async fn run(mut self) -> RelayChainResult<()> {
let mut import_notification_stream =
self.relay_chain_interface.import_notification_stream().await?;
let dht_event_stream = self
.relay_chain_network
.event_stream("teyrchain-bootnode-discovery")
.filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
})
.fuse();
pin_mut!(dht_event_stream);
loop {
tokio::select! {
header = import_notification_stream.next() => match header {
Some(header) => self.handle_import_notification(header).await,
None => {
debug!(
target: LOG_TARGET,
"Import notification stream terminated, terminating bootnode advertisement."
);
return Ok(());
}
},
req = self.request_receiver.recv() => match req {
Ok(req) => {
self.handle_request(req);
},
Err(_) => {
debug!(
target: LOG_TARGET,
"Paranode request receiver terminated, terminating bootnode advertisement."
);
return Ok(());
}
},
event = dht_event_stream.select_next_some() => self.handle_dht_event(event),
() = &mut self.current_epoch_publish_retry => self.retry_for_current_epoch(),
() = &mut self.next_epoch_publish_retry => self.retry_for_next_epoch(),
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnode request-response protocol configuration.
use pezsc_network::{
request_responses::IncomingRequest, service::traits::NetworkBackend, ProtocolName,
};
use pezsp_runtime::traits::Block as BlockT;
use std::time::Duration;
/// Maximum number of addresses allowed in the response.
pub const MAX_ADDRESSES: usize = 32;
/// Expected maximum number of simultaneous requests from remote peers.
/// Should be enough for a testnet with a plenty of nodes starting at the same time.
const INBOUND_CHANNEL_SIZE: usize = 1000;
/// Maximum request size. Should be enough to fit SCALE-compact-encoded `para_id`.
const MAX_REQUEST_SIZE: u64 = 128;
/// Maximum response size as per RFC.
const MAX_RESPONSE_SIZE: u64 = 16 * 1024;
/// Request-response protocol timeout.
const TIMEOUT: Duration = Duration::from_secs(20);
/// Bootnode request-response protocol name given a genesis hash and fork id.
pub fn paranode_protocol_name<Hash: AsRef<[u8]>>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> ProtocolName {
let genesis_hash = genesis_hash.as_ref();
if let Some(fork_id) = fork_id {
// This is not stated in RFC-0008, but other pezkuwi protocol names are based on `fork_id`
// if it is present, so we also use it here.
format!("/{}/{}/paranode", array_bytes::bytes2hex("", genesis_hash), fork_id)
} else {
format!("/{}/paranode", array_bytes::bytes2hex("", genesis_hash))
}
.into()
}
/// Bootnode request-response protocol config.
pub fn bootnode_request_response_config<
Hash: AsRef<[u8]>,
B: BlockT,
N: NetworkBackend<B, <B as BlockT>::Hash>,
>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> (N::RequestResponseProtocolConfig, async_channel::Receiver<IncomingRequest>) {
let (inbound_tx, inbound_rx) = async_channel::bounded(INBOUND_CHANNEL_SIZE);
let config = N::request_response_config(
paranode_protocol_name(genesis_hash, fork_id),
Vec::new(),
MAX_REQUEST_SIZE,
MAX_RESPONSE_SIZE,
TIMEOUT,
Some(inbound_tx),
);
(config, inbound_rx)
}
@@ -0,0 +1,468 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnode discovery.
//!
//! The discovery works as follows:
//! 1. We start teyrchain bootnode content provider discovery on the relay chain DHT in
//! [`BootnodeDiscovery::start_discovery`].
//! 2. We handle every provider discovered in [`BootnodeDiscovery::handle_providers`] and try to
//! request the bootnodes from the provider over a `/paranode` request-response protocol.
//! 3. The request result is handled in [`BootnodeDiscovery::handle_response`]. If the request
//! fails this is a sign of the provider addresses not being cached by the remote / dropped by
//! the networking library (the case with libp2p). In this case we perform a `FIND_NODE` query
//! to get the provider addresses first and repeat the request once we know them.
//! 4. When the request over the `/paranode` protocol succeeds, we add the bootnode addresses as
//! known addresses to the teyrchain networking.
//! 5. If the content provider discovery had completed, all `FIND_NODE` queries finished, and all
//! requests over the `/paranode` protocol succeded or failed, but we have not found any
//! bootnode addresses, we repeat the discovery process after a cooldown period.
use crate::{config::MAX_ADDRESSES, schema::Response};
use codec::{CompactRef, Decode, Encode};
use cumulus_primitives_core::{relay_chain::Hash as RelayHash, ParaId};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
use futures::{
channel::oneshot,
future::{BoxFuture, Fuse, FusedFuture},
pin_mut,
stream::FuturesUnordered,
FutureExt, StreamExt,
};
use log::{debug, error, info, trace, warn};
use prost::Message;
use pezsc_network::{
event::{DhtEvent, Event},
request_responses::{IfDisconnected, RequestFailure},
service::traits::NetworkService,
KademliaKey, Multiaddr, PeerId, ProtocolName,
};
use pezsp_consensus_babe::{Epoch, Randomness};
use std::{collections::HashSet, pin::Pin, sync::Arc, time::Duration};
use tokio::time::{sleep, Sleep};
/// Log target for this file.
const LOG_TARGET: &str = "bootnodes::discovery";
/// Delay before retrying discovery in case of failure. Needed to rate-limit the attempts,
/// especially in small testnets where a discovery attempt can be almost instant.
const RETRY_DELAY: Duration = Duration::from_secs(30);
/// Teyrchain bootnode discovery parameters.
pub struct BootnodeDiscoveryParams {
/// Teyrchain ID.
pub para_id: ParaId,
/// Teyrchain node network service.
pub teyrchain_network: Arc<dyn NetworkService>,
/// Teyrchain genesis hash.
pub teyrchain_genesis_hash: Vec<u8>,
/// Teyrchain fork ID.
pub teyrchain_fork_id: Option<String>,
/// Relay chain interface.
pub relay_chain_interface: Arc<dyn RelayChainInterface>,
/// Relay chain network service.
pub relay_chain_network: Arc<dyn NetworkService>,
/// `/paranode` protocol name.
pub paranode_protocol_name: ProtocolName,
}
/// Teyrchain bootnode discovery service.
pub struct BootnodeDiscovery {
para_id_scale_compact: Vec<u8>,
teyrchain_network: Arc<dyn NetworkService>,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_network: Arc<dyn NetworkService>,
latest_relay_chain_hash: Option<RelayHash>,
key_being_discovered: Option<KademliaKey>,
paranode_protocol_name: ProtocolName,
pending_responses: FuturesUnordered<
BoxFuture<
'static,
(PeerId, Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>),
>,
>,
direct_requests: HashSet<PeerId>,
find_node_queries: HashSet<PeerId>,
pending_start_discovery: Pin<Box<Fuse<Sleep>>>,
succeeded: bool,
}
impl BootnodeDiscovery {
/// Create a new bootnode discovery service.
pub fn new(
BootnodeDiscoveryParams {
para_id,
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_network,
paranode_protocol_name,
}: BootnodeDiscoveryParams,
) -> Self {
Self {
para_id_scale_compact: CompactRef(&para_id).encode(),
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_network,
latest_relay_chain_hash: None,
key_being_discovered: None,
paranode_protocol_name,
pending_responses: FuturesUnordered::default(),
direct_requests: HashSet::new(),
find_node_queries: HashSet::new(),
// Trigger the discovery immediately on startup.
pending_start_discovery: Box::pin(sleep(Duration::ZERO).fuse()),
succeeded: false,
}
}
async fn current_epoch(&mut self, hash: RelayHash) -> RelayChainResult<Epoch> {
let res = self
.relay_chain_interface
.call_runtime_api("BabeApi_current_epoch", hash, &[])
.await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
fn epoch_key(&self, randomness: Randomness) -> KademliaKey {
self.para_id_scale_compact
.clone()
.into_iter()
.chain(randomness.into_iter())
.collect::<Vec<_>>()
.into()
}
/// Start bootnode discovery.
async fn start_discovery(&mut self) -> RelayChainResult<()> {
let Some(hash) = self.latest_relay_chain_hash else {
error!(
target: LOG_TARGET,
"Failed to start bootnode discovery: no relay chain hash available. This is a bug.",
);
// This is a graceful panic via the failure of essential task.
return Err(RelayChainError::GenericError("no relay chain hash available".to_string()));
};
let current_epoch = self.current_epoch(hash).await?;
let current_epoch_key = self.epoch_key(current_epoch.randomness);
self.key_being_discovered = Some(current_epoch_key.clone());
self.relay_chain_network.get_providers(current_epoch_key.clone());
debug!(
target: LOG_TARGET,
"Started discovery of teyrchain bootnode providers for current epoch key {}",
hex::encode(current_epoch_key),
);
Ok(())
}
/// Schedule bootnode discovery if needed. Returns `false` if the discovery event loop should be
/// terminated.
fn maybe_retry_discovery(&mut self) -> bool {
let discovery_in_progress = self.key_being_discovered.is_some() ||
!self.pending_responses.is_empty() ||
!self.find_node_queries.is_empty();
let discovery_scheduled = !self.pending_start_discovery.is_terminated();
if discovery_in_progress || discovery_scheduled {
// Discovery is already in progress or scheduled, just continue the event loop.
true
} else {
if self.succeeded {
// No need to start discovery again if the previous attempt succeeded.
info!(
target: LOG_TARGET,
"Teyrchain bootnode discovery on the relay chain DHT succeeded",
);
false
} else {
debug!(
target: LOG_TARGET,
"Retrying teyrchain bootnode discovery on the relay chain DHT in {RETRY_DELAY:?}",
);
self.pending_start_discovery = Box::pin(sleep(RETRY_DELAY).fuse());
true
}
}
}
fn request_bootnode(&mut self, peer_id: PeerId) {
trace!(
target: LOG_TARGET,
"Requesting teyrchain bootnode from the relay chain {peer_id:?}",
);
let (tx, rx) = oneshot::channel();
self.relay_chain_network.start_request(
peer_id,
self.paranode_protocol_name.clone(),
self.para_id_scale_compact.clone(),
None,
tx,
IfDisconnected::TryConnect,
);
self.pending_responses.push(async move { (peer_id, rx.await) }.boxed());
}
fn handle_providers(&mut self, providers: Vec<PeerId>) {
debug!(
target: LOG_TARGET,
"Found teyrchain bootnode providers on the relay chain: {providers:?}",
);
for peer_id in providers {
if peer_id == self.relay_chain_network.local_peer_id() {
continue;
}
// libp2p may yield the same provider multiple times; skip if we alredy queried it.
if self.direct_requests.contains(&peer_id) || self.find_node_queries.contains(&peer_id)
{
continue;
}
// Directly request a bootnode from the peer without performing a `FIND_NODE` query
// first. With litep2p backend this will likely succeed, because cached provider
// addresses are automatically added to the transport manager known addresses list.
//
// With libp2p backend, or if the remote did not return the cached addresses of the
// provider, the request will fail and we will perform a `FIND_NODE` query.
self.direct_requests.insert(peer_id);
self.request_bootnode(peer_id);
}
}
fn handle_response(
&mut self,
peer_id: PeerId,
res: Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>,
) {
let direct_request = self.direct_requests.remove(&peer_id);
let response = match res {
Ok(Ok((payload, _))) => match Response::decode(payload.as_slice()) {
Ok(response) => response,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to decode teyrchain bootnode response from {peer_id:?}: {e}",
);
return;
},
},
Ok(Err(e)) => {
if direct_request {
// It only makes sense to try to find the node on the DHT in case of "address
// not available" error. Unfortunately, libp2p and litep2p backends report such
// errors differently, and also some network library could break the error
// reporting in the future. So, to be on the safe side and avoid subtle bugs,
// we always try to find the node on the DHT in case of the request failure.
debug!(
target: LOG_TARGET,
"Failed to directly query teyrchain bootnode from {peer_id:?}: {e}. \
Starting FIND_NODE query on the DHT",
);
self.find_node_queries.insert(peer_id);
self.relay_chain_network.find_closest_peers(peer_id);
} else {
debug!(
target: LOG_TARGET,
"Failed to query teyrchain bootnode from {peer_id:?} after finding
the node addresses on the DHT: {e}",
);
}
return;
},
Err(_) => {
debug!(
target: LOG_TARGET,
"Teyrchain bootnode request to {peer_id:?} canceled. \
The node is likely terminating.",
);
return;
},
};
match (response.genesis_hash, response.fork_id) {
(genesis_hash, fork_id)
if genesis_hash == self.teyrchain_genesis_hash &&
fork_id == self.teyrchain_fork_id => {},
(genesis_hash, fork_id) => {
warn!(
target: LOG_TARGET,
"Received invalid teyrchain bootnode response from {peer_id:?}: \
genesis hash {}, fork ID {:?} don't match expected genesis hash {}, fork ID {:?}",
hex::encode(genesis_hash),
fork_id,
hex::encode(&self.teyrchain_genesis_hash),
self.teyrchain_fork_id,
);
return;
},
}
let paranode_peer_id = match PeerId::from_bytes(response.peer_id.as_slice()) {
Ok(peer_id) => peer_id,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to decode teyrchain peer ID in response from {peer_id:?}: {e}",
);
return;
},
};
if paranode_peer_id == self.teyrchain_network.local_peer_id() {
warn!(
target: LOG_TARGET,
"Received own teyrchain node peer ID in bootnode response from {peer_id:?}. \
This should not happen as we don't request teyrchain bootnodes from self.",
);
return;
}
let paranode_addresses = response
.addrs
.into_iter()
.map(Multiaddr::try_from)
.take(MAX_ADDRESSES)
.collect::<Result<Vec<_>, _>>();
let paranode_addresses = match paranode_addresses {
Ok(paranode_addresses) => paranode_addresses,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to decode teyrchain node addresses in response from {peer_id:?}: {e}",
);
return;
},
};
debug!(
target: LOG_TARGET,
"Discovered teyrchain bootnode {paranode_peer_id:?} with addresses {paranode_addresses:?}",
);
paranode_addresses.into_iter().for_each(|addr| {
self.teyrchain_network.add_known_address(paranode_peer_id, addr);
self.succeeded = true;
});
}
fn handle_dht_event(&mut self, event: DhtEvent) {
match event {
DhtEvent::ProvidersFound(key, providers)
// libp2p generates empty events, so also check if `providers` are not empty.
if Some(key.clone()) == self.key_being_discovered && !providers.is_empty() =>
self.handle_providers(providers),
DhtEvent::NoMoreProviders(key) if Some(key.clone()) == self.key_being_discovered => {
debug!(
target: LOG_TARGET,
"Teyrchain bootnode providers discovery finished for key {}",
hex::encode(key),
);
self.key_being_discovered = None;
},
DhtEvent::ProvidersNotFound(key) if Some(key.clone()) == self.key_being_discovered => {
debug!(
target: LOG_TARGET,
"Teyrchain bootnode providers not found for key {}",
hex::encode(key),
);
self.key_being_discovered = None;
},
DhtEvent::ClosestPeersFound(peer_id, peers) if self.find_node_queries.remove(&peer_id) => {
if let Some((_, addrs)) = peers
.into_iter()
.find(|(peer, addrs)| peer == &peer_id && !addrs.is_empty())
{
trace!(
target: LOG_TARGET,
"Found addresses on the DHT for teyrchain bootnode provider {peer_id:?}: {addrs:?}",
);
for address in addrs {
self.relay_chain_network.add_known_address(peer_id, address);
}
self.request_bootnode(peer_id);
} else {
debug!(
target: LOG_TARGET,
"Failed to find addresses on the DHT for teyrchain bootnode provider {peer_id:?}",
);
}
},
DhtEvent::ClosestPeersNotFound(peer_id) if self.find_node_queries.remove(&peer_id) => {
debug!(
target: LOG_TARGET,
"Failed to find addresses on the DHT for teyrchain bootnode provider {peer_id:?}",
);
},
_ => {},
}
}
/// Run the bootnode discovery service.
pub async fn run(mut self) -> RelayChainResult<()> {
let mut import_notification_stream =
self.relay_chain_interface.import_notification_stream().await?.fuse();
let dht_event_stream = self
.relay_chain_network
.event_stream("teyrchain-bootnode-discovery")
.filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
})
.fuse();
pin_mut!(dht_event_stream);
// Make sure the relay chain hash is always available before starting the discovery.
let header = import_notification_stream.select_next_some().await;
self.latest_relay_chain_hash = Some(header.hash());
loop {
if !self.maybe_retry_discovery() {
return Ok(());
}
tokio::select! {
_ = &mut self.pending_start_discovery => {
self.start_discovery().await?;
},
header = import_notification_stream.select_next_some() => {
self.latest_relay_chain_hash = Some(header.hash());
},
event = dht_event_stream.select_next_some() => self.handle_dht_event(event),
(peer_id, res) = self.pending_responses.select_next_some(),
if !self.pending_responses.is_empty() =>
self.handle_response(peer_id, res),
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnodes advertisement and discovery service.
#![warn(missing_docs)]
mod advertisement;
mod config;
mod discovery;
mod task;
mod schema {
include!(concat!(env!("OUT_DIR"), "/response.rs"));
}
pub use config::bootnode_request_response_config;
pub use task::{start_bootnode_tasks, StartBootnodeTasksParams};
@@ -0,0 +1,20 @@
syntax = "proto2";
package response;
message Response {
// Peer ID of the node on the parachain side.
required bytes peer_id = 1;
// Multiaddresses of the parachain side of the node. The list and format are the same as for
// the `listenAddrs` field of the `identify` protocol.
repeated bytes addrs = 2;
// Genesis hash of the parachain. Used to determine the name of the networking protocol
// to connect to the parachain. Untrusted.
required bytes genesis_hash = 3;
// So-called "fork ID" of the parachain. Used to determine the name of the networking protocol
// to connect to the parachain. Untrusted.
optional string fork_id = 4;
};
+201
View File
@@ -0,0 +1,201 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnodes advertisement and discovery service.
use crate::{
advertisement::{BootnodeAdvertisement, BootnodeAdvertisementParams},
config::paranode_protocol_name,
discovery::{BootnodeDiscovery, BootnodeDiscoveryParams},
};
use cumulus_primitives_core::{relay_chain::BlockId, ParaId};
use cumulus_relay_chain_interface::RelayChainInterface;
use log::{debug, error};
use num_traits::Zero;
use pezsc_network::{request_responses::IncomingRequest, service::traits::NetworkService, Multiaddr};
use pezsc_service::TaskManager;
use std::sync::Arc;
/// Log target for this crate.
const LOG_TARGET: &str = "bootnodes";
/// Bootnode advertisement task params.
pub struct StartBootnodeTasksParams<'a> {
/// Enable embedded DHT bootnode.
pub embedded_dht_bootnode: bool,
/// Enable DHT bootnode discovery.
pub dht_bootnode_discovery: bool,
/// Teyrchain ID.
pub para_id: ParaId,
/// Task manager.
pub task_manager: &'a mut TaskManager,
/// Relay chain interface.
pub relay_chain_interface: Arc<dyn RelayChainInterface>,
/// Relay chain fork ID.
pub relay_chain_fork_id: Option<String>,
/// Relay chain network service.
pub relay_chain_network: Arc<dyn NetworkService>,
/// `/paranode` protocol request receiver.
pub request_receiver: async_channel::Receiver<IncomingRequest>,
/// Teyrchain node network service.
pub teyrchain_network: Arc<dyn NetworkService>,
/// Whether to advertise non-global IP addresses.
pub advertise_non_global_ips: bool,
/// Teyrchain genesis hash.
pub teyrchain_genesis_hash: Vec<u8>,
/// Teyrchain fork ID.
pub teyrchain_fork_id: Option<String>,
/// Teyrchain public addresses provided by the operator.
pub teyrchain_public_addresses: Vec<Multiaddr>,
}
async fn bootnode_advertisement(
para_id: ParaId,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_network: Arc<dyn NetworkService>,
request_receiver: async_channel::Receiver<IncomingRequest>,
teyrchain_network: Arc<dyn NetworkService>,
advertise_non_global_ips: bool,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
public_addresses: Vec<Multiaddr>,
) {
let bootnode_advertisement = BootnodeAdvertisement::new(BootnodeAdvertisementParams {
para_id,
relay_chain_interface,
relay_chain_network,
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
public_addresses,
});
if let Err(e) = bootnode_advertisement.run().await {
error!(target: LOG_TARGET, "Bootnode advertisement terminated with error: {e}");
}
}
async fn bootnode_discovery(
para_id: ParaId,
teyrchain_network: Arc<dyn NetworkService>,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_fork_id: Option<String>,
relay_chain_network: Arc<dyn NetworkService>,
) {
let relay_chain_genesis_hash =
match relay_chain_interface.header(BlockId::Number(Zero::zero())).await {
Ok(Some(header)) => header.hash().as_bytes().to_vec(),
Ok(None) => {
error!(
target: LOG_TARGET,
"Bootnode discovery: relay chain genesis hash does not exist",
);
// Make essential task fail.
return;
},
Err(e) => {
error!(
target: LOG_TARGET,
"Bootnode discovery: failed to obtain relay chain genesis hash: {e}",
);
// Make essential task fail.
return;
},
};
let paranode_protocol_name =
paranode_protocol_name(relay_chain_genesis_hash, relay_chain_fork_id.as_deref());
let bootnode_discovery = BootnodeDiscovery::new(BootnodeDiscoveryParams {
para_id,
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_network,
paranode_protocol_name,
});
match bootnode_discovery.run().await {
// Do not terminate the essentil task if bootnode discovery succeeded.
Ok(()) => std::future::pending().await,
Err(e) => error!(target: LOG_TARGET, "Bootnode discovery terminated with error: {e}"),
}
}
/// Start teyrchain bootnode advertisement and discovery tasks.
pub fn start_bootnode_tasks(
StartBootnodeTasksParams {
embedded_dht_bootnode,
dht_bootnode_discovery,
para_id,
task_manager,
relay_chain_interface,
relay_chain_fork_id,
relay_chain_network,
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
teyrchain_public_addresses,
}: StartBootnodeTasksParams,
) {
debug!(
target: LOG_TARGET,
"Embedded DHT bootnode enabled: {embedded_dht_bootnode}; \
DHT bootnode discovery enabled: {dht_bootnode_discovery}",
);
if embedded_dht_bootnode {
task_manager.spawn_essential_handle().spawn(
"pezcumulus-dht-bootnode-advertisement",
None,
bootnode_advertisement(
para_id,
relay_chain_interface.clone(),
relay_chain_network.clone(),
request_receiver,
teyrchain_network.clone(),
advertise_non_global_ips,
teyrchain_genesis_hash.clone(),
teyrchain_fork_id.clone(),
teyrchain_public_addresses,
),
);
}
if dht_bootnode_discovery {
task_manager.spawn_essential_handle().spawn(
"pezcumulus-dht-bootnode-discovery",
None,
bootnode_discovery(
para_id,
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_fork_id,
relay_chain_network,
),
);
}
}
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "pezcumulus-client-cli"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Teyrchain node CLI utilities."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
clap = { features = ["derive"], workspace = true }
codec = { workspace = true, default-features = true }
url = { workspace = true }
# Bizinikiwi
pezsc-chain-spec = { workspace = true, default-features = true }
pezsc-cli = { workspace = true, default-features = false }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = false }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezsc-chain-spec/runtime-benchmarks",
"pezsc-cli/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+502
View File
@@ -0,0 +1,502 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Pezcumulus CLI library.
#![warn(missing_docs)]
use std::{
fs,
io::{self, Write},
path::PathBuf,
sync::Arc,
};
use codec::Encode;
use pezsc_chain_spec::ChainSpec;
use pezsc_cli::RpcEndpoint;
use pezsc_client_api::HeaderBackend;
use pezsc_service::{
config::{PrometheusConfig, RpcBatchRequestConfig, TelemetryEndpoints},
BasePath, TransactionPoolOptions,
};
use pezsp_core::hexdisplay::HexDisplay;
use pezsp_runtime::traits::{Block as BlockT, Zero};
use url::Url;
/// The `purge-chain` command used to remove the whole chain: the teyrchain and the relay chain.
#[derive(Debug, clap::Parser)]
#[group(skip)]
pub struct PurgeChainCmd {
/// The base struct of the purge-chain command.
#[command(flatten)]
pub base: pezsc_cli::PurgeChainCmd,
/// Only delete the para chain database
#[arg(long, aliases = &["para"])]
pub teyrchain: bool,
/// Only delete the relay chain database
#[arg(long, aliases = &["relay"])]
pub relaychain: bool,
}
impl PurgeChainCmd {
/// Run the purge command
pub fn run(
&self,
para_config: pezsc_service::Configuration,
relay_config: pezsc_service::Configuration,
) -> pezsc_cli::Result<()> {
let databases = match (self.teyrchain, self.relaychain) {
(true, true) | (false, false) => {
vec![("teyrchain", para_config.database), ("relaychain", relay_config.database)]
},
(true, false) => vec![("teyrchain", para_config.database)],
(false, true) => vec![("relaychain", relay_config.database)],
};
let db_paths = databases
.iter()
.map(|(chain_label, database)| {
database.path().ok_or_else(|| {
pezsc_cli::Error::Input(format!(
"Cannot purge custom database implementation of: {}",
chain_label,
))
})
})
.collect::<pezsc_cli::Result<Vec<_>>>()?;
if !self.base.yes {
for db_path in &db_paths {
println!("{}", db_path.display());
}
print!("Are you sure to remove? [y/N]: ");
io::stdout().flush().expect("failed to flush stdout");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
match input.chars().next() {
Some('y') | Some('Y') => {},
_ => {
println!("Aborted");
return Ok(());
},
}
}
for db_path in &db_paths {
match fs::remove_dir_all(db_path) {
Ok(_) => {
println!("{:?} removed.", &db_path);
},
Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
eprintln!("{:?} did not exist.", &db_path);
},
Err(err) => return Err(err.into()),
}
}
Ok(())
}
}
impl pezsc_cli::CliConfiguration for PurgeChainCmd {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
&self.base.shared_params
}
fn database_params(&self) -> Option<&pezsc_cli::DatabaseParams> {
Some(&self.base.database_params)
}
}
/// Get the SCALE encoded genesis header of the teyrchain.
pub fn get_raw_genesis_header<B, C>(client: Arc<C>) -> pezsc_cli::Result<Vec<u8>>
where
B: BlockT,
C: HeaderBackend<B> + 'static,
{
let genesis_hash =
client
.hash(Zero::zero())?
.ok_or(pezsc_cli::Error::Client(pezsp_blockchain::Error::Backend(
"Failed to lookup genesis block hash when exporting genesis head data.".into(),
)))?;
let genesis_header = client.header(genesis_hash)?.ok_or(pezsc_cli::Error::Client(
pezsp_blockchain::Error::Backend(
"Failed to lookup genesis header by hash when exporting genesis head data.".into(),
),
))?;
Ok(genesis_header.encode())
}
/// Command for exporting the genesis head data of the teyrchain
#[derive(Debug, clap::Parser)]
pub struct ExportGenesisHeadCommand {
/// Output file name or stdout if unspecified.
#[arg()]
pub output: Option<PathBuf>,
/// Write output in binary. Default is to write in hex.
#[arg(short, long)]
pub raw: bool,
#[allow(missing_docs)]
#[command(flatten)]
pub shared_params: pezsc_cli::SharedParams,
}
impl ExportGenesisHeadCommand {
/// Run the export-genesis-head command
pub fn run<B, C>(&self, client: Arc<C>) -> pezsc_cli::Result<()>
where
B: BlockT,
C: HeaderBackend<B> + 'static,
{
let raw_header = get_raw_genesis_header(client)?;
let output_buf = if self.raw {
raw_header
} else {
format!("0x{:?}", HexDisplay::from(&raw_header)).into_bytes()
};
if let Some(output) = &self.output {
fs::write(output, output_buf)?;
} else {
io::stdout().write_all(&output_buf)?;
}
Ok(())
}
}
impl pezsc_cli::CliConfiguration for ExportGenesisHeadCommand {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
&self.shared_params
}
fn base_path(&self) -> pezsc_cli::Result<Option<BasePath>> {
// As we are just exporting the genesis wasm a tmp database is enough.
//
// As otherwise we may "pollute" the global base path.
Ok(Some(BasePath::new_temp_dir()?))
}
}
/// Command for exporting the genesis wasm file.
#[derive(Debug, clap::Parser)]
pub struct ExportGenesisWasmCommand {
/// Output file name or stdout if unspecified.
#[arg()]
pub output: Option<PathBuf>,
/// Write output in binary. Default is to write in hex.
#[arg(short, long)]
pub raw: bool,
#[allow(missing_docs)]
#[command(flatten)]
pub shared_params: pezsc_cli::SharedParams,
}
impl ExportGenesisWasmCommand {
/// Run the export-genesis-wasm command
pub fn run(&self, chain_spec: &dyn ChainSpec) -> pezsc_cli::Result<()> {
let raw_wasm_blob = extract_genesis_wasm(chain_spec)?;
let output_buf = if self.raw {
raw_wasm_blob
} else {
format!("0x{:?}", HexDisplay::from(&raw_wasm_blob)).into_bytes()
};
if let Some(output) = &self.output {
fs::write(output, output_buf)?;
} else {
io::stdout().write_all(&output_buf)?;
}
Ok(())
}
}
/// Extract the genesis code from a given ChainSpec.
pub fn extract_genesis_wasm(chain_spec: &dyn ChainSpec) -> pezsc_cli::Result<Vec<u8>> {
let mut storage = chain_spec.build_storage()?;
storage
.top
.remove(pezsp_core::storage::well_known_keys::CODE)
.ok_or_else(|| "Could not find wasm file in genesis state!".into())
}
impl pezsc_cli::CliConfiguration for ExportGenesisWasmCommand {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
&self.shared_params
}
fn base_path(&self) -> pezsc_cli::Result<Option<BasePath>> {
// As we are just exporting the genesis wasm a tmp database is enough.
//
// As otherwise we may "pollute" the global base path.
Ok(Some(BasePath::new_temp_dir()?))
}
}
fn validate_relay_chain_url(arg: &str) -> Result<Url, String> {
let url = Url::parse(arg).map_err(|e| e.to_string())?;
let scheme = url.scheme();
if scheme == "ws" || scheme == "wss" {
Ok(url)
} else {
Err(format!(
"'{}' URL scheme not supported. Only websocket RPC is currently supported",
url.scheme()
))
}
}
/// The `run` command used to run a node.
#[derive(Debug, clap::Parser)]
#[group(skip)]
pub struct RunCmd {
/// The pezcumulus RunCmd inherents from pezsc_cli's
#[command(flatten)]
pub base: pezsc_cli::RunCmd,
/// Run node as collator.
///
/// Note that this is the same as running with `--validator`.
#[arg(long, conflicts_with = "validator")]
pub collator: bool,
/// Creates a less resource-hungry node that retrieves relay chain data from an RPC endpoint.
///
/// The provided URLs should point to RPC endpoints of the relay chain.
/// This node connects to the remote nodes following the order they were specified in. If the
/// connection fails, it attempts to connect to the next endpoint in the list.
///
/// Note: This option doesn't stop the node from connecting to the relay chain network but
/// reduces bandwidth use.
#[arg(
long,
value_parser = validate_relay_chain_url,
num_args = 0..,
alias = "relay-chain-rpc-url"
)]
pub relay_chain_rpc_urls: Vec<Url>,
/// EXPERIMENTAL: This is meant to be used only if collator is overshooting the PoV size, and
/// building blocks that do not fit in the max_pov_size. It is a percentage of the max_pov_size
/// configuration of the relay-chain.
///
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
#[arg(long)]
pub experimental_max_pov_percentage: Option<u32>,
/// Disable embedded DHT bootnode.
///
/// Do not advertise the node as a teyrchain bootnode on the relay chain DHT.
#[arg(long)]
pub no_dht_bootnode: bool,
/// Disable DHT bootnode discovery.
///
/// Disable discovery of the teyrchain bootnodes via the relay chain DHT.
#[arg(long)]
pub no_dht_bootnode_discovery: bool,
}
impl RunCmd {
/// Create a [`NormalizedRunCmd`] which merges the `collator` cli argument into `validator` to
/// have only one.
pub fn normalize(&self) -> NormalizedRunCmd {
let mut new_base = self.base.clone();
new_base.validator = self.base.validator || self.collator;
NormalizedRunCmd { base: new_base }
}
/// Create [`CollatorOptions`] representing options only relevant to teyrchain collator nodes
pub fn collator_options(&self) -> CollatorOptions {
let relay_chain_mode = if self.relay_chain_rpc_urls.is_empty() {
RelayChainMode::Embedded
} else {
RelayChainMode::ExternalRpc(self.relay_chain_rpc_urls.clone())
};
CollatorOptions {
relay_chain_mode,
embedded_dht_bootnode: !self.no_dht_bootnode,
dht_bootnode_discovery: !self.no_dht_bootnode_discovery,
}
}
}
/// Possible modes for the relay chain to operate in.
#[derive(Clone, Debug)]
pub enum RelayChainMode {
/// Spawn embedded relay chain node
Embedded,
/// Connect to remote relay chain node via websocket RPC
ExternalRpc(Vec<Url>),
}
/// Options only relevant for collator/teyrchain nodes
#[derive(Clone, Debug)]
pub struct CollatorOptions {
/// How this collator retrieves relay chain information
pub relay_chain_mode: RelayChainMode,
/// Enable embedded DHT bootnode.
pub embedded_dht_bootnode: bool,
/// Enable DHT bootnode discovery.
pub dht_bootnode_discovery: bool,
}
/// A non-redundant version of the `RunCmd` that sets the `validator` field when the
/// original `RunCmd` had the `collator` field.
/// This is how we make `--collator` imply `--validator`.
pub struct NormalizedRunCmd {
/// The pezcumulus RunCmd inherents from pezsc_cli's
pub base: pezsc_cli::RunCmd,
}
impl pezsc_cli::CliConfiguration for NormalizedRunCmd {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
self.base.shared_params()
}
fn import_params(&self) -> Option<&pezsc_cli::ImportParams> {
self.base.import_params()
}
fn network_params(&self) -> Option<&pezsc_cli::NetworkParams> {
self.base.network_params()
}
fn keystore_params(&self) -> Option<&pezsc_cli::KeystoreParams> {
self.base.keystore_params()
}
fn offchain_worker_params(&self) -> Option<&pezsc_cli::OffchainWorkerParams> {
self.base.offchain_worker_params()
}
fn node_name(&self) -> pezsc_cli::Result<String> {
self.base.node_name()
}
fn dev_key_seed(&self, is_dev: bool) -> pezsc_cli::Result<Option<String>> {
self.base.dev_key_seed(is_dev)
}
fn telemetry_endpoints(
&self,
chain_spec: &Box<dyn pezsc_cli::ChainSpec>,
) -> pezsc_cli::Result<Option<TelemetryEndpoints>> {
self.base.telemetry_endpoints(chain_spec)
}
fn role(&self, is_dev: bool) -> pezsc_cli::Result<pezsc_cli::Role> {
self.base.role(is_dev)
}
fn force_authoring(&self) -> pezsc_cli::Result<bool> {
self.base.force_authoring()
}
fn prometheus_config(
&self,
default_listen_port: u16,
chain_spec: &Box<dyn pezsc_cli::ChainSpec>,
) -> pezsc_cli::Result<Option<PrometheusConfig>> {
self.base.prometheus_config(default_listen_port, chain_spec)
}
fn disable_grandpa(&self) -> pezsc_cli::Result<bool> {
self.base.disable_grandpa()
}
fn rpc_max_connections(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_connections()
}
fn rpc_cors(&self, is_dev: bool) -> pezsc_cli::Result<Option<Vec<String>>> {
self.base.rpc_cors(is_dev)
}
fn rpc_addr(&self, default_listen_port: u16) -> pezsc_cli::Result<Option<Vec<RpcEndpoint>>> {
self.base.rpc_addr(default_listen_port)
}
fn rpc_methods(&self) -> pezsc_cli::Result<pezsc_service::config::RpcMethods> {
self.base.rpc_methods()
}
fn rpc_rate_limit(&self) -> pezsc_cli::Result<Option<std::num::NonZeroU32>> {
Ok(self.base.rpc_params.rpc_rate_limit)
}
fn rpc_rate_limit_whitelisted_ips(&self) -> pezsc_cli::Result<Vec<pezsc_service::config::IpNetwork>> {
Ok(self.base.rpc_params.rpc_rate_limit_whitelisted_ips.clone())
}
fn rpc_rate_limit_trust_proxy_headers(&self) -> pezsc_cli::Result<bool> {
Ok(self.base.rpc_params.rpc_rate_limit_trust_proxy_headers)
}
fn rpc_max_request_size(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_request_size()
}
fn rpc_max_response_size(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_response_size()
}
fn rpc_max_subscriptions_per_connection(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_subscriptions_per_connection()
}
fn rpc_buffer_capacity_per_connection(&self) -> pezsc_cli::Result<u32> {
Ok(self.base.rpc_params.rpc_message_buffer_capacity_per_connection)
}
fn rpc_batch_config(&self) -> pezsc_cli::Result<RpcBatchRequestConfig> {
self.base.rpc_batch_config()
}
fn transaction_pool(&self, is_dev: bool) -> pezsc_cli::Result<TransactionPoolOptions> {
self.base.transaction_pool(is_dev)
}
fn max_runtime_instances(&self) -> pezsc_cli::Result<Option<usize>> {
self.base.max_runtime_instances()
}
fn runtime_cache_size(&self) -> pezsc_cli::Result<u8> {
self.base.runtime_cache_size()
}
fn base_path(&self) -> pezsc_cli::Result<Option<BasePath>> {
self.base.base_path()
}
}
+72
View File
@@ -0,0 +1,72 @@
[package]
name = "pezcumulus-client-collator"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Common node-side functionality and glue code to collate teyrchain blocks."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-client-network = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
[dev-dependencies]
async-trait = { workspace = true }
# Bizinikiwi
pezsp-maybe-compressed-blob = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-subsystem-test-helpers = { workspace = true }
# Pezcumulus
pezcumulus-test-client = { workspace = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
pezcumulus-test-runtime = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-client-network/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezcumulus-test-runtime/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
+141
View File
@@ -0,0 +1,141 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Pezcumulus Collator implementation for Bizinikiwi.
use pezkuwi_node_primitives::CollationGenerationConfig;
use pezkuwi_node_subsystem::messages::{CollationGenerationMessage, CollatorProtocolMessage};
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
pub mod service;
/// Relay-chain-driven collators are those whose block production is driven purely
/// by new relay chain blocks and the most recently included teyrchain blocks
/// within them.
///
/// This method of driving collators is not suited to anything but the most simple teyrchain
/// consensus mechanisms, and this module may soon be deprecated.
pub mod relay_chain_driven {
use futures::{
channel::{mpsc, oneshot},
prelude::*,
};
use pezkuwi_node_primitives::{CollationGenerationConfig, CollationResult};
use pezkuwi_node_subsystem::messages::{CollationGenerationMessage, CollatorProtocolMessage};
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
use cumulus_primitives_core::{relay_chain::Hash as PHash, PersistedValidationData};
/// A request to author a collation, based on the advancement of the relay chain.
///
/// See the module docs for more info on relay-chain-driven collators.
pub struct CollationRequest {
relay_parent: PHash,
pvd: PersistedValidationData,
sender: oneshot::Sender<Option<CollationResult>>,
}
impl CollationRequest {
/// Get the relay parent of the collation request.
pub fn relay_parent(&self) -> &PHash {
&self.relay_parent
}
/// Get the [`PersistedValidationData`] for the request.
pub fn persisted_validation_data(&self) -> &PersistedValidationData {
&self.pvd
}
/// Complete the request with a collation, if any.
pub fn complete(self, collation: Option<CollationResult>) {
let _ = self.sender.send(collation);
}
}
/// Initialize the collator with Pezkuwi's collation-generation
/// subsystem, returning a stream of collation requests to handle.
pub async fn init(
key: CollatorPair,
para_id: ParaId,
overseer_handle: OverseerHandle,
) -> mpsc::Receiver<CollationRequest> {
let mut overseer_handle = overseer_handle;
let (stream_tx, stream_rx) = mpsc::channel(0);
let config = CollationGenerationConfig {
key,
para_id,
collator: Some(Box::new(move |relay_parent, validation_data| {
// Cloning the channel on each usage effectively makes the channel
// unbounded. The channel is actually bounded by the block production
// and consensus systems of Pezkuwi, which limits the amount of possible
// blocks.
let mut stream_tx = stream_tx.clone();
let validation_data = validation_data.clone();
Box::pin(async move {
let (this_tx, this_rx) = oneshot::channel();
let request =
CollationRequest { relay_parent, pvd: validation_data, sender: this_tx };
if stream_tx.send(request).await.is_err() {
return None;
}
this_rx.await.ok().flatten()
})
})),
};
overseer_handle
.send_msg(CollationGenerationMessage::Initialize(config), "StartCollator")
.await;
overseer_handle
.send_msg(CollatorProtocolMessage::CollateOn(para_id), "StartCollator")
.await;
stream_rx
}
}
/// Initialize the collation-related subsystems on the relay-chain side.
///
/// This must be done prior to collation, and does not set up any callback for collation.
/// For callback-driven collators, use the [`relay_chain_driven`] module.
pub async fn initialize_collator_subsystems(
overseer_handle: &mut OverseerHandle,
key: CollatorPair,
para_id: ParaId,
reinitialize: bool,
) {
let config = CollationGenerationConfig { key, para_id, collator: None };
if reinitialize {
overseer_handle
.send_msg(CollationGenerationMessage::Reinitialize(config), "StartCollator")
.await;
} else {
overseer_handle
.send_msg(CollationGenerationMessage::Initialize(config), "StartCollator")
.await;
}
overseer_handle
.send_msg(CollatorProtocolMessage::CollateOn(para_id), "StartCollator")
.await;
}
+361
View File
@@ -0,0 +1,361 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! The Pezcumulus [`CollatorService`] is a utility struct for performing common
//! operations used in teyrchain consensus/authoring.
use cumulus_client_network::WaitToAnnounce;
use cumulus_primitives_core::{CollationInfo, CollectCollationInfo, TeyrchainBlockData};
use pezsc_client_api::BlockBackend;
use pezsp_api::{ApiExt, ProvideRuntimeApi};
use pezsp_consensus::BlockStatus;
use pezsp_core::traits::SpawnNamed;
use pezsp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT, Zero};
use cumulus_client_consensus_common::TeyrchainCandidate;
use pezkuwi_node_primitives::{
BlockData, Collation, CollationSecondedSignal, MaybeCompressedPoV, PoV,
};
use codec::Encode;
use futures::channel::oneshot;
use parking_lot::Mutex;
use std::sync::Arc;
/// The logging target.
const LOG_TARGET: &str = "pezcumulus-collator";
/// Utility functions generally applicable to writing collators for Pezcumulus.
pub trait ServiceInterface<Block: BlockT> {
/// Checks the status of the given block hash in the Teyrchain.
///
/// Returns `true` if the block could be found and is good to be build on.
fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool;
/// Build a full [`Collation`] from a given [`TeyrchainCandidate`]. This requires
/// that the underlying block has been fully imported into the underlying client,
/// as implementations will fetch underlying runtime API data.
///
/// This also returns the unencoded teyrchain block data, in case that is desired.
fn build_collation(
&self,
parent_header: &Block::Header,
block_hash: Block::Hash,
candidate: TeyrchainCandidate<Block>,
) -> Option<(Collation, TeyrchainBlockData<Block>)>;
/// Inform networking systems that the block should be announced after a signal has
/// been received to indicate the block has been seconded by a relay-chain validator.
///
/// This sets up the barrier and returns the sending side of a channel, for the signal
/// to be passed through.
fn announce_with_barrier(
&self,
block_hash: Block::Hash,
) -> oneshot::Sender<CollationSecondedSignal>;
/// Directly announce a block on the network.
fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>);
}
/// The [`CollatorService`] provides common utilities for teyrchain consensus and authoring.
///
/// This includes logic for checking the block status of arbitrary teyrchain headers
/// gathered from the relay chain state, creating full [`Collation`]s to be shared with validators,
/// and distributing new teyrchain blocks along the network.
pub struct CollatorService<Block: BlockT, BS, RA> {
block_status: Arc<BS>,
wait_to_announce: Arc<Mutex<WaitToAnnounce<Block>>>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
runtime_api: Arc<RA>,
}
impl<Block: BlockT, BS, RA> Clone for CollatorService<Block, BS, RA> {
fn clone(&self) -> Self {
Self {
block_status: self.block_status.clone(),
wait_to_announce: self.wait_to_announce.clone(),
announce_block: self.announce_block.clone(),
runtime_api: self.runtime_api.clone(),
}
}
}
impl<Block, BS, RA> CollatorService<Block, BS, RA>
where
Block: BlockT,
BS: BlockBackend<Block>,
RA: ProvideRuntimeApi<Block>,
RA::Api: CollectCollationInfo<Block>,
{
/// Create a new instance.
pub fn new(
block_status: Arc<BS>,
spawner: Arc<dyn SpawnNamed + Send + Sync>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
runtime_api: Arc<RA>,
) -> Self {
let wait_to_announce =
Arc::new(Mutex::new(WaitToAnnounce::new(spawner, announce_block.clone())));
Self { block_status, wait_to_announce, announce_block, runtime_api }
}
/// Checks the status of the given block hash in the Teyrchain.
///
/// Returns `true` if the block could be found and is good to be build on.
pub fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
match self.block_status.block_status(hash) {
Ok(BlockStatus::Queued) => {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Skipping candidate production, because block is still queued for import.",
);
false
},
Ok(BlockStatus::InChainWithState) => true,
Ok(BlockStatus::InChainPruned) => {
tracing::error!(
target: LOG_TARGET,
"Skipping candidate production, because block `{:?}` is already pruned!",
hash,
);
false
},
Ok(BlockStatus::KnownBad) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
"Block is tagged as known bad and is included in the relay chain! Skipping candidate production!",
);
false
},
Ok(BlockStatus::Unknown) => {
if header.number().is_zero() {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
"Could not find the header of the genesis block in the database!",
);
} else {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Skipping candidate production, because block is unknown.",
);
}
false
},
Err(e) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?e,
"Failed to get block status.",
);
false
},
}
}
/// Fetch the collation info from the runtime.
///
/// Returns `Ok(Some((CollationInfo, ApiVersion)))` on success, `Err(_)` on error or `Ok(None)`
/// if the runtime api isn't implemented by the runtime. `ApiVersion` being the version of the
/// [`CollectCollationInfo`] runtime api.
pub fn fetch_collation_info(
&self,
block_hash: Block::Hash,
header: &Block::Header,
) -> Result<Option<(CollationInfo, u32)>, pezsp_api::ApiError> {
let runtime_api = self.runtime_api.runtime_api();
let api_version =
match runtime_api.api_version::<dyn CollectCollationInfo<Block>>(block_hash)? {
Some(version) => version,
None => {
tracing::error!(
target: LOG_TARGET,
"Could not fetch `CollectCollationInfo` runtime api version."
);
return Ok(None);
},
};
let collation_info = if api_version < 2 {
#[allow(deprecated)]
runtime_api
.collect_collation_info_before_version_2(block_hash)?
.into_latest(header.encode().into())
} else {
runtime_api.collect_collation_info(block_hash, header)?
};
Ok(Some((collation_info, api_version)))
}
/// Build a full [`Collation`] from a given [`TeyrchainCandidate`]. This requires
/// that the underlying block has been fully imported into the underlying client,
/// as it fetches underlying runtime API data.
///
/// This also returns the unencoded teyrchain block data, in case that is desired.
pub fn build_collation(
&self,
parent_header: &Block::Header,
block_hash: Block::Hash,
candidate: TeyrchainCandidate<Block>,
) -> Option<(Collation, TeyrchainBlockData<Block>)> {
let block = candidate.block;
let compact_proof = match candidate
.proof
.into_compact_proof::<HashingFor<Block>>(*parent_header.state_root())
{
Ok(proof) => proof,
Err(e) => {
tracing::error!(target: "pezcumulus-collator", "Failed to compact proof: {:?}", e);
return None;
},
};
// Create the teyrchain block data for the validators.
let (collation_info, _api_version) = self
.fetch_collation_info(block_hash, block.header())
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to collect collation info.",
)
})
.ok()
.flatten()?;
// Workaround for: https://github.com/pezkuwichain/pezkuwi-sdk/issues/98
//
// We are always using the `api_version` of the parent block. The `api_version` can only
// change with a runtime upgrade and this is when we want to observe the old `api_version`.
// Because this old `api_version` is the one used to validate this block. Otherwise we
// already assume the `api_version` is higher than what the relay chain will use and this
// will lead to validation errors.
let api_version = self
.runtime_api
.runtime_api()
.api_version::<dyn CollectCollationInfo<Block>>(parent_header.hash())
.ok()
.flatten()?;
let block_data = TeyrchainBlockData::<Block>::new(vec![block], compact_proof);
let pov = pezkuwi_node_primitives::maybe_compress_pov(PoV {
block_data: BlockData(if api_version >= 3 {
block_data.encode()
} else {
let block_data = block_data.as_v0();
if block_data.is_none() {
tracing::error!(
target: LOG_TARGET,
"Trying to submit a collation with multiple blocks is not supported by the current runtime."
);
}
block_data?.encode()
}),
});
let upward_messages = collation_info
.upward_messages
.try_into()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Number of upward messages should not be greater than `MAX_UPWARD_MESSAGE_NUM`",
)
})
.ok()?;
let horizontal_messages = collation_info
.horizontal_messages
.try_into()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Number of horizontal messages should not be greater than `MAX_HORIZONTAL_MESSAGE_NUM`",
)
})
.ok()?;
let collation = Collation {
upward_messages,
new_validation_code: collation_info.new_validation_code,
processed_downward_messages: collation_info.processed_downward_messages,
horizontal_messages,
hrmp_watermark: collation_info.hrmp_watermark,
head_data: collation_info.head_data,
proof_of_validity: MaybeCompressedPoV::Compressed(pov),
};
Some((collation, block_data))
}
/// Inform the networking systems that the block should be announced after an appropriate
/// signal has been received. This returns the sending half of the signal.
pub fn announce_with_barrier(
&self,
block_hash: Block::Hash,
) -> oneshot::Sender<CollationSecondedSignal> {
let (result_sender, signed_stmt_recv) = oneshot::channel();
self.wait_to_announce.lock().wait_to_announce(block_hash, signed_stmt_recv);
result_sender
}
}
impl<Block, BS, RA> ServiceInterface<Block> for CollatorService<Block, BS, RA>
where
Block: BlockT,
BS: BlockBackend<Block>,
RA: ProvideRuntimeApi<Block>,
RA::Api: CollectCollationInfo<Block>,
{
fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
CollatorService::check_block_status(self, hash, header)
}
fn build_collation(
&self,
parent_header: &Block::Header,
block_hash: Block::Hash,
candidate: TeyrchainCandidate<Block>,
) -> Option<(Collation, TeyrchainBlockData<Block>)> {
CollatorService::build_collation(self, parent_header, block_hash, candidate)
}
fn announce_with_barrier(
&self,
block_hash: Block::Hash,
) -> oneshot::Sender<CollationSecondedSignal> {
CollatorService::announce_with_barrier(self, block_hash)
}
fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>) {
(self.announce_block)(block_hash, data)
}
}
+104
View File
@@ -0,0 +1,104 @@
[package]
name = "pezcumulus-client-consensus-aura"
description = "AURA consensus algorithm for teyrchains"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
parking_lot = { workspace = true }
schnellru = { workspace = true }
tokio = { workspace = true, features = ["macros"] }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-consensus-aura = { workspace = true, default-features = true }
pezsc-consensus-babe = { workspace = true, default-features = true }
pezsc-consensus-slots = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-application-crypto = { workspace = true, default-features = true }
pezsp-block-builder = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-aura = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-timestamp = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-collator = { workspace = true, default-features = true }
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-client-consensus-proposer = { workspace = true, default-features = true }
pezcumulus-client-teyrchain-inherent = { workspace = true, default-features = true }
pezcumulus-primitives-aura = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
[dev-dependencies]
pezcumulus-test-client = { workspace = true }
pezcumulus-test-relay-sproof-builder = { workspace = true }
rstest = { workspace = true }
pezsp-keyring = { workspace = true }
pezsp-tracing = { workspace = true }
pezsp-version = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-collator/runtime-benchmarks",
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-client-consensus-proposer/runtime-benchmarks",
"pezcumulus-client-teyrchain-inherent/runtime-benchmarks",
"pezcumulus-primitives-aura/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus-aura/runtime-benchmarks",
"pezsc-consensus-babe/runtime-benchmarks",
"pezsc-consensus-slots/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-block-builder/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-aura/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-timestamp/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,442 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! The core collator logic for Aura - slot claiming, block proposing, and collation
//! packaging.
//!
//! The [`Collator`] struct exposed here is meant to be a component of higher-level logic
//! which actually manages the control flow of the collator - which slots to claim, how
//! many collations to build, when to work, etc.
//!
//! This module also exposes some standalone functions for common operations when building
//! aura-based collators.
use codec::Codec;
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{
self as consensus_common, TeyrchainBlockImportMarker, TeyrchainCandidate,
};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_client_teyrchain_inherent::{TeyrchainInherentData, TeyrchainInherentDataProvider};
use cumulus_primitives_core::{
relay_chain::Hash as PHash, DigestItem, PersistedValidationData, TeyrchainBlockData,
};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::{Collation, MaybeCompressedPoV};
use pezkuwi_primitives::{Header as PHeader, Id as ParaId};
use crate::collators::RelayParentData;
use futures::prelude::*;
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction};
use pezsc_consensus_aura::standalone as aura_internal;
use pezsc_network_types::PeerId;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_consensus::BlockOrigin;
use pezsp_consensus_aura::{AuraApi, Slot, SlotDuration};
use pezsp_core::crypto::Pair;
use pezsp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider};
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::{
generic::Digest,
traits::{Block as BlockT, HashingFor, Header as HeaderT, Member},
};
use pezsp_state_machine::StorageChanges;
use pezsp_timestamp::Timestamp;
use std::{error::Error, time::Duration};
/// Parameters for instantiating a [`Collator`].
pub struct Params<BI, CIDP, RClient, Proposer, CS> {
/// A builder for inherent data builders.
pub create_inherent_data_providers: CIDP,
/// The block import handle.
pub block_import: BI,
/// An interface to the relay-chain client.
pub relay_client: RClient,
/// The keystore handle used for accessing teyrchain key material.
pub keystore: KeystorePtr,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The identifier of the teyrchain within the relay-chain.
pub para_id: ParaId,
/// The block proposer used for building blocks.
pub proposer: Proposer,
/// The collator service used for bundling proposals into collations and announcing
/// to the network.
pub collator_service: CS,
}
/// A utility struct for writing collation logic that makes use of Aura entirely
/// or in part. See module docs for more details.
pub struct Collator<Block, P, BI, CIDP, RClient, Proposer, CS> {
create_inherent_data_providers: CIDP,
block_import: BI,
relay_client: RClient,
keystore: KeystorePtr,
para_id: ParaId,
proposer: Proposer,
collator_service: CS,
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
}
impl<Block, P, BI, CIDP, RClient, Proposer, CS> Collator<Block, P, BI, CIDP, RClient, Proposer, CS>
where
Block: BlockT,
RClient: RelayChainInterface,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block>,
CS: CollatorServiceInterface<Block>,
P: Pair,
P::Public: AppPublic + Member,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
/// Instantiate a new instance of the `Aura` manager.
pub fn new(params: Params<BI, CIDP, RClient, Proposer, CS>) -> Self {
Collator {
create_inherent_data_providers: params.create_inherent_data_providers,
block_import: params.block_import,
relay_client: params.relay_client,
keystore: params.keystore,
para_id: params.para_id,
proposer: params.proposer,
collator_service: params.collator_service,
_marker: std::marker::PhantomData,
}
}
/// Explicitly creates the inherent data for teyrchain block authoring and overrides
/// the timestamp inherent data with the one provided, if any. Additionally allows to specify
/// relay parent descendants that can be used to prevent authoring at the tip of the relay
/// chain.
pub async fn create_inherent_data_with_rp_offset(
&self,
relay_parent: PHash,
validation_data: &PersistedValidationData,
parent_hash: Block::Hash,
timestamp: impl Into<Option<Timestamp>>,
relay_parent_descendants: Option<RelayParentData>,
collator_peer_id: PeerId,
) -> Result<(TeyrchainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
let paras_inherent_data = TeyrchainInherentDataProvider::create_at(
relay_parent,
&self.relay_client,
validation_data,
self.para_id,
relay_parent_descendants
.map(RelayParentData::into_inherent_descendant_list)
.unwrap_or_default(),
Vec::new(),
collator_peer_id,
)
.await;
let paras_inherent_data = match paras_inherent_data {
Some(p) => p,
None =>
return Err(
format!("Could not create paras inherent data at {:?}", relay_parent).into()
),
};
let mut other_inherent_data = self
.create_inherent_data_providers
.create_inherent_data_providers(parent_hash, ())
.map_err(|e| e as Box<dyn Error + Send + Sync + 'static>)
.await?
.create_inherent_data()
.await
.map_err(Box::new)?;
if let Some(timestamp) = timestamp.into() {
other_inherent_data.replace_data(pezsp_timestamp::INHERENT_IDENTIFIER, &timestamp);
}
Ok((paras_inherent_data, other_inherent_data))
}
/// Explicitly creates the inherent data for teyrchain block authoring and overrides
/// the timestamp inherent data with the one provided, if any.
pub async fn create_inherent_data(
&self,
relay_parent: PHash,
validation_data: &PersistedValidationData,
parent_hash: Block::Hash,
timestamp: impl Into<Option<Timestamp>>,
collator_peer_id: PeerId,
) -> Result<(TeyrchainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
self.create_inherent_data_with_rp_offset(
relay_parent,
validation_data,
parent_hash,
timestamp,
None,
collator_peer_id,
)
.await
}
/// Build and import a teyrchain block on the given parent header, using the given slot claim.
pub async fn build_block_and_import(
&mut self,
parent_header: &Block::Header,
slot_claim: &SlotClaim<P::Public>,
additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
inherent_data: (TeyrchainInherentData, InherentData),
proposal_duration: Duration,
max_pov_size: usize,
) -> Result<Option<TeyrchainCandidate<Block>>, Box<dyn Error + Send + 'static>> {
let mut digest = additional_pre_digest.into().unwrap_or_default();
digest.push(slot_claim.pre_digest.clone());
let maybe_proposal = self
.proposer
.propose(
&parent_header,
&inherent_data.0,
inherent_data.1,
Digest { logs: digest },
proposal_duration,
Some(max_pov_size),
)
.await
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
let proposal = match maybe_proposal {
None => return Ok(None),
Some(p) => p,
};
let sealed_importable = seal::<_, P>(
proposal.block,
proposal.storage_changes,
&slot_claim.author_pub,
&self.keystore,
)
.map_err(|e| e as Box<dyn Error + Send>)?;
let block = Block::new(
sealed_importable.post_header(),
sealed_importable
.body
.as_ref()
.expect("body always created with this `propose` fn; qed")
.clone(),
);
self.block_import
.import_block(sealed_importable)
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)
.await?;
Ok(Some(TeyrchainCandidate { block, proof: proposal.proof }))
}
/// Propose, seal, import a block and packaging it into a collation.
///
/// Provide the slot to build at as well as any other necessary pre-digest logs,
/// the inherent data, and the proposal duration and PoV size limits.
///
/// The Aura pre-digest should not be explicitly provided and is set internally.
///
/// This does not announce the collation to the teyrchain network or the relay chain.
pub async fn collate(
&mut self,
parent_header: &Block::Header,
slot_claim: &SlotClaim<P::Public>,
additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
inherent_data: (TeyrchainInherentData, InherentData),
proposal_duration: Duration,
max_pov_size: usize,
) -> Result<Option<(Collation, TeyrchainBlockData<Block>)>, Box<dyn Error + Send + 'static>> {
let maybe_candidate = self
.build_block_and_import(
parent_header,
slot_claim,
additional_pre_digest,
inherent_data,
proposal_duration,
max_pov_size,
)
.await?;
let Some(candidate) = maybe_candidate else { return Ok(None) };
let hash = candidate.block.header().hash();
if let Some((collation, block_data)) =
self.collator_service.build_collation(parent_header, hash, candidate)
{
block_data.log_size_info();
if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
tracing::info!(
target: crate::LOG_TARGET,
"Compressed PoV size: {}kb",
pov.block_data.0.len() as f64 / 1024f64,
);
}
Ok(Some((collation, block_data)))
} else {
Err(Box::<dyn Error + Send + Sync>::from("Unable to produce collation"))
}
}
/// Get the underlying collator service.
pub fn collator_service(&self) -> &CS {
&self.collator_service
}
}
/// A claim on an Aura slot.
pub struct SlotClaim<Pub> {
author_pub: Pub,
pre_digest: DigestItem,
slot: Slot,
timestamp: Timestamp,
}
impl<Pub> SlotClaim<Pub> {
/// Create a slot-claim from the given author public key, slot, and timestamp.
///
/// This does not check whether the author actually owns the slot or the timestamp
/// falls within the slot.
pub fn unchecked<P>(author_pub: Pub, slot: Slot, timestamp: Timestamp) -> Self
where
P: Pair<Public = Pub>,
P::Public: Codec,
P::Signature: Codec,
{
SlotClaim { author_pub, timestamp, pre_digest: aura_internal::pre_digest::<P>(slot), slot }
}
/// Get the author's public key.
pub fn author_pub(&self) -> &Pub {
&self.author_pub
}
/// Get the Aura pre-digest for this slot.
pub fn pre_digest(&self) -> &DigestItem {
&self.pre_digest
}
/// Get the slot assigned to this claim.
pub fn slot(&self) -> Slot {
self.slot
}
/// Get the timestamp corresponding to the relay-chain slot this claim was
/// generated against.
pub fn timestamp(&self) -> Timestamp {
self.timestamp
}
}
/// Attempt to claim a slot derived from the given relay-parent header's slot.
pub async fn claim_slot<B, C, P>(
client: &C,
parent_hash: B::Hash,
relay_parent_header: &PHeader,
slot_duration: SlotDuration,
relay_chain_slot_duration: Duration,
keystore: &KeystorePtr,
) -> Result<Option<SlotClaim<P::Public>>, Box<dyn Error>>
where
B: BlockT,
C: ProvideRuntimeApi<B> + Send + Sync + 'static,
C::Api: AuraApi<B, P::Public>,
P: Pair,
P::Public: Codec,
P::Signature: Codec,
{
// load authorities
let authorities = client.runtime_api().authorities(parent_hash).map_err(Box::new)?;
// Determine the current slot and timestamp based on the relay-parent's.
let (slot_now, timestamp) = match consensus_common::relay_slot_and_timestamp(
relay_parent_header,
relay_chain_slot_duration,
) {
Some((r_s, t)) => {
let our_slot = Slot::from_timestamp(t, slot_duration);
tracing::debug!(
target: crate::LOG_TARGET,
relay_slot = ?r_s,
para_slot = ?our_slot,
timestamp = ?t,
?slot_duration,
?relay_chain_slot_duration,
"Adjusted relay-chain slot to teyrchain slot"
);
(our_slot, t)
},
None => return Ok(None),
};
// Try to claim the slot locally.
let author_pub = {
let res = aura_internal::claim_slot::<P>(slot_now, &authorities, keystore).await;
match res {
Some(p) => p,
None => return Ok(None),
}
};
Ok(Some(SlotClaim::unchecked::<P>(author_pub, slot_now, timestamp)))
}
/// Seal a block with a signature in the header.
pub fn seal<B: BlockT, P>(
pre_sealed: B,
storage_changes: StorageChanges<HashingFor<B>>,
author_pub: &P::Public,
keystore: &KeystorePtr,
) -> Result<BlockImportParams<B>, Box<dyn Error + Send + Sync + 'static>>
where
P: Pair,
P::Signature: Codec + TryFrom<Vec<u8>>,
P::Public: AppPublic,
{
let (pre_header, body) = pre_sealed.deconstruct();
let pre_hash = pre_header.hash();
let block_number = *pre_header.number();
// seal the block.
let block_import_params = {
let seal_digest =
aura_internal::seal::<_, P>(&pre_hash, &author_pub, keystore).map_err(Box::new)?;
let mut block_import_params = BlockImportParams::new(BlockOrigin::Own, pre_header);
block_import_params.post_digests.push(seal_digest);
block_import_params.body = Some(body);
block_import_params.state_action =
StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(storage_changes));
block_import_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
block_import_params
};
let post_hash = block_import_params.post_hash();
tracing::info!(
target: crate::LOG_TARGET,
"🔖 Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
block_number,
post_hash,
pre_hash,
);
Ok(block_import_params)
}
@@ -0,0 +1,277 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! This provides the option to run a basic relay-chain driven Aura implementation.
//!
//! This collator only builds on top of the most recently included block, limiting the
//! block time to a maximum of two times the relay-chain block time, and requiring the
//! block to be built and distributed to validators between two relay-chain blocks.
//!
//! For more information about AuRa, the Bizinikiwi crate should be checked.
use codec::{Codec, Decode};
use cumulus_client_collator::{
relay_chain_driven::CollationRequest, service::ServiceInterface as CollatorServiceInterface,
};
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_core::{relay_chain::BlockId as RBlockId, CollectCollationInfo};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::CollationResult;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId, ValidationCode};
use futures::{channel::mpsc::Receiver, prelude::*};
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf};
use pezsc_consensus::BlockImport;
use pezsc_network_types::PeerId;
use pezsp_api::{CallApiAt, ProvideRuntimeApi};
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member};
use pezsp_state_machine::Backend as _;
use std::{sync::Arc, time::Duration};
use crate::collator as collator_util;
/// Parameters for [`run`].
pub struct Params<BI, CIDP, Client, RClient, Proposer, CS> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// A handle to the relay-chain client's "Overseer" or task orchestrator.
pub overseer_handle: OverseerHandle,
/// The length of slots in the relay chain.
pub relay_chain_slot_duration: Duration,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Receiver for collation requests. If `None`, Aura consensus will establish a new receiver.
/// Should be used when a chain migrates from a different consensus algorithm and was already
/// processing collation requests before initializing Aura.
pub collation_request_receiver: Option<Receiver<CollationRequest>>,
}
/// Run bare Aura consensus as a relay-chain-driven collator.
pub fn run<Block, P, BI, CIDP, Client, RClient, Proposer, CS>(
params: Params<BI, CIDP, Client, RClient, Proposer, CS>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT + Send,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ CallApiAt<Block>
+ Send
+ Sync
+ 'static,
Client::Api: AuraApi<Block, P::Public> + CollectCollationInfo<Block>,
RClient: RelayChainInterface + Send + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + Send + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
P: Pair,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
async move {
let mut collation_requests = match params.collation_request_receiver {
Some(receiver) => receiver,
None =>
cumulus_client_collator::relay_chain_driven::init(
params.collator_key,
params.para_id,
params.overseer_handle,
)
.await,
};
let mut collator = {
let params = collator_util::Params {
create_inherent_data_providers: params.create_inherent_data_providers,
block_import: params.block_import,
relay_client: params.relay_client.clone(),
keystore: params.keystore.clone(),
collator_peer_id: params.collator_peer_id,
para_id: params.para_id,
proposer: params.proposer,
collator_service: params.collator_service,
};
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
};
let mut last_processed_slot = 0;
let mut last_relay_chain_block = Default::default();
while let Some(request) = collation_requests.next().await {
macro_rules! reject_with_error {
($err:expr) => {{
request.complete(None);
tracing::error!(target: crate::LOG_TARGET, err = ?{ $err });
continue;
}};
}
macro_rules! try_request {
($x:expr) => {{
match $x {
Ok(x) => x,
Err(e) => reject_with_error!(e),
}
}};
}
let validation_data = request.persisted_validation_data();
let parent_header =
try_request!(Block::Header::decode(&mut &validation_data.parent_head.0[..]));
let parent_hash = parent_header.hash();
if !collator.collator_service().check_block_status(parent_hash, &parent_header) {
continue;
}
let Ok(Some(code)) =
params.para_client.state_at(parent_hash).map_err(drop).and_then(|s| {
s.storage(&pezsp_core::storage::well_known_keys::CODE).map_err(drop)
})
else {
continue;
};
super::check_validation_code_or_log(
&ValidationCode::from(code).hash(),
params.para_id,
&params.relay_client,
*request.relay_parent(),
)
.await;
let relay_parent_header =
match params.relay_client.header(RBlockId::hash(*request.relay_parent())).await {
Err(e) => reject_with_error!(e),
Ok(None) => continue, // sanity: would be inconsistent to get `None` here
Ok(Some(h)) => h,
};
let slot_duration = match params.para_client.runtime_api().slot_duration(parent_hash) {
Ok(d) => d,
Err(e) => reject_with_error!(e),
};
let claim = match collator_util::claim_slot::<_, _, P>(
&*params.para_client,
parent_hash,
&relay_parent_header,
slot_duration,
params.relay_chain_slot_duration,
&params.keystore,
)
.await
{
Ok(None) => continue,
Ok(Some(c)) => c,
Err(e) => reject_with_error!(e),
};
// With async backing this function will be called every relay chain block.
//
// Most teyrchains currently run with 12 seconds slots and thus, they would try to
// produce multiple blocks per slot which very likely would fail on chain. Thus, we have
// this "hack" to only produce one block per slot per relay chain fork.
//
// With https://github.com/pezkuwichain/pezkuwi-sdk/issues/127 this implementation will be
// obsolete and also the underlying issue will be fixed.
if last_processed_slot >= *claim.slot() &&
last_relay_chain_block < *relay_parent_header.number()
{
continue;
}
let (teyrchain_inherent_data, other_inherent_data) = try_request!(
collator
.create_inherent_data(
*request.relay_parent(),
&validation_data,
parent_hash,
claim.timestamp(),
params.collator_peer_id,
)
.await
);
let allowed_pov_size = (validation_data.max_pov_size / 2) as usize;
let maybe_collation = try_request!(
collator
.collate(
&parent_header,
&claim,
None,
(teyrchain_inherent_data, other_inherent_data),
params.authoring_duration,
allowed_pov_size,
)
.await
);
if let Some((collation, block_data)) = maybe_collation {
let Some(block_hash) = block_data.blocks().first().map(|b| b.hash()) else {
continue;
};
let result_sender =
Some(collator.collator_service().announce_with_barrier(block_hash));
request.complete(Some(CollationResult { collation, result_sender }));
} else {
request.complete(None);
tracing::debug!(target: crate::LOG_TARGET, "No block proposal");
}
last_processed_slot = *claim.slot();
last_relay_chain_block = *relay_parent_header.number();
}
}
}
@@ -0,0 +1,511 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! A collator for Aura that looks ahead of the most recently included teyrchain block
//! when determining what to build upon.
//!
//! This collator also builds additional blocks when the maximum backlog is not saturated.
//! The size of the backlog is determined by invoking a runtime API. If that runtime API
//! is not supported, this assumes a maximum backlog size of 1.
//!
//! This takes more advantage of asynchronous backing, though not complete advantage.
//! When the backlog is not saturated, this approach lets the backlog temporarily 'catch up'
//! with periods of higher throughput. When the backlog is saturated, we typically
//! fall back to the limited cadence of a single teyrchain block per relay-chain block.
//!
//! Despite this, the fact that there is a backlog at all allows us to spend more time
//! building the block, as there is some buffer before it can get posted to the relay-chain.
//! The main limitation is block propagation time - i.e. the new blocks created by an author
//! must be propagated to the next author before their turn.
use codec::{Codec, Encode};
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_aura::AuraUnincludedSegmentApi;
use cumulus_primitives_core::{CollectCollationInfo, PersistedValidationData};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::SubmitCollationParams;
use pezkuwi_node_subsystem::messages::CollationGenerationMessage;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId, OccupiedCoreAssumption};
use crate::{
collator as collator_util,
collators::{claim_queue_at, BackingGroupConnectionHelper},
export_pov_to_path,
};
use futures::prelude::*;
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf};
use pezsc_consensus::BlockImport;
use pezsc_network_types::PeerId;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::{AuraApi, Slot};
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member};
use pezsp_timestamp::Timestamp;
use std::{path::PathBuf, sync::Arc, time::Duration};
/// Parameters for [`run`].
pub struct Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// The para client's backend, used to access the database.
pub para_backend: Arc<Backend>,
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// A validation code hash provider, used to get the current validation code hash.
pub code_hash_provider: CHP,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// A handle to the relay-chain client's "Overseer" or task orchestrator.
pub overseer_handle: OverseerHandle,
/// The length of slots in the relay chain.
pub relay_chain_slot_duration: Duration,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
pub reinitialize: bool,
/// The maximum percentage of the maximum PoV size that the collator can use.
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
pub max_pov_percentage: Option<u32>,
}
/// Get the current teyrchain slot from a given block hash.
///
/// Returns the teyrchain slot, relay chain slot, and timestamp.
fn get_teyrchain_slot<Block, Client, P>(
para_client: &Client,
block_hash: Block::Hash,
relay_parent_header: &pezkuwi_primitives::Header,
relay_chain_slot_duration: Duration,
) -> Option<(Slot, Slot, Timestamp)>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>,
Client::Api: AuraApi<Block, P>,
P: Codec,
{
let slot_duration =
match pezsc_consensus_aura::standalone::slot_duration_at(para_client, block_hash) {
Ok(sd) => sd,
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to acquire teyrchain slot duration");
return None;
},
};
tracing::debug!(target: crate::LOG_TARGET, ?slot_duration, ?block_hash, "Teyrchain slot duration acquired");
let (relay_slot, timestamp) =
consensus_common::relay_slot_and_timestamp(relay_parent_header, relay_chain_slot_duration)?;
let slot_now = Slot::from_timestamp(timestamp, slot_duration);
tracing::debug!(
target: crate::LOG_TARGET,
?relay_slot,
para_slot = ?slot_now,
?timestamp,
?slot_duration,
?relay_chain_slot_duration,
"Adjusted relay-chain slot to teyrchain slot"
);
Some((slot_now, relay_slot, timestamp))
}
/// Run async-backing-friendly Aura.
pub fn run<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>(
params: Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + CollectCollationInfo<Block> + AuraUnincludedSegmentApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
run_with_export::<_, P, _, _, _, _, _, _, _, _>(ParamsWithExport { params, export_pov: None })
}
/// Parameters for [`run_with_export`].
pub struct ParamsWithExport<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS> {
/// The parameters.
pub params: Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>,
/// When set, the collator will export every produced `POV` to this folder.
pub export_pov: Option<PathBuf>,
}
/// Run async-backing-friendly Aura.
///
/// This is exactly the same as [`run`], but it supports the optional export of each produced `POV`
/// to the file system.
pub fn run_with_export<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>(
ParamsWithExport { mut params, export_pov }: ParamsWithExport<
BI,
CIDP,
Client,
Backend,
RClient,
CHP,
Proposer,
CS,
>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + CollectCollationInfo<Block> + AuraUnincludedSegmentApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
async move {
cumulus_client_collator::initialize_collator_subsystems(
&mut params.overseer_handle,
params.collator_key,
params.para_id,
params.reinitialize,
)
.await;
let mut import_notifications = match params.relay_client.import_notification_stream().await
{
Ok(s) => s,
Err(err) => {
tracing::error!(
target: crate::LOG_TARGET,
?err,
"Failed to initialize consensus: no relay chain import notification stream"
);
return;
},
};
let mut collator = {
let params = collator_util::Params {
create_inherent_data_providers: params.create_inherent_data_providers,
block_import: params.block_import,
relay_client: params.relay_client.clone(),
keystore: params.keystore.clone(),
collator_peer_id: params.collator_peer_id,
para_id: params.para_id,
proposer: params.proposer,
collator_service: params.collator_service,
};
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
};
let mut connection_helper = BackingGroupConnectionHelper::new(
params.keystore.clone(),
params.overseer_handle.clone(),
);
while let Some(relay_parent_header) = import_notifications.next().await {
let relay_parent = relay_parent_header.hash();
let Some(core_index) = claim_queue_at(relay_parent, &mut params.relay_client)
.await
.iter_claims_at_depth_for_para(0, params.para_id)
.next()
else {
tracing::trace!(
target: crate::LOG_TARGET,
?relay_parent,
?params.para_id,
"Para is not scheduled on any core, skipping import notification",
);
continue;
};
let max_pov_size = match params
.relay_client
.persisted_validation_data(
relay_parent,
params.para_id,
OccupiedCoreAssumption::Included,
)
.await
{
Ok(None) => continue,
Ok(Some(pvd)) => pvd.max_pov_size,
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to gather information from relay-client");
continue;
},
};
let (included_block, initial_parent) = match crate::collators::find_parent(
relay_parent,
params.para_id,
&*params.para_backend,
&params.relay_client,
)
.await
{
Some(value) => value,
None => continue,
};
let para_client = &*params.para_client;
let keystore = &params.keystore;
let can_build_upon = |block_hash| {
let (slot_now, relay_slot, timestamp) = get_teyrchain_slot::<_, _, P::Public>(
para_client,
block_hash,
&relay_parent_header,
params.relay_chain_slot_duration,
)?;
Some(super::can_build_upon::<_, _, P>(
slot_now,
relay_slot,
timestamp,
block_hash,
included_block.hash(),
para_client,
&keystore,
))
};
// Build in a loop until not allowed. Note that the authorities can change
// at any block, so we need to re-claim our slot every time.
let mut parent_hash = initial_parent.hash;
let mut parent_header = initial_parent.header;
let overseer_handle = &mut params.overseer_handle;
// Do not try to build upon an unknown, pruned or bad block
if !collator.collator_service().check_block_status(parent_hash, &parent_header) {
continue;
}
// Trigger pre-conect to backing groups if necessary.
if let (Some((slot_now, _relay_slot, _timestamp)), Ok(authorities)) = (
get_teyrchain_slot::<_, _, P::Public>(
para_client,
parent_hash,
&relay_parent_header,
params.relay_chain_slot_duration,
),
para_client.runtime_api().authorities(parent_hash),
) {
connection_helper.update::<P>(slot_now, &authorities).await;
}
// This needs to change to support elastic scaling, but for continuously
// scheduled chains this ensures that the backlog will grow steadily.
for n_built in 0..2 {
let slot_claim = match can_build_upon(parent_hash) {
Some(fut) => match fut.await {
None => break,
Some(c) => c,
},
None => break,
};
tracing::debug!(
target: crate::LOG_TARGET,
?relay_parent,
unincluded_segment_len = initial_parent.depth + n_built,
"Slot claimed. Building"
);
let validation_data = PersistedValidationData {
parent_head: parent_header.encode().into(),
relay_parent_number: *relay_parent_header.number(),
relay_parent_storage_root: *relay_parent_header.state_root(),
max_pov_size,
};
// Build and announce collations recursively until
// `can_build_upon` fails or building a collation fails.
let (teyrchain_inherent_data, other_inherent_data) = match collator
.create_inherent_data(
relay_parent,
&validation_data,
parent_hash,
slot_claim.timestamp(),
params.collator_peer_id,
)
.await
{
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err);
break;
},
Ok(x) => x,
};
let Some(validation_code_hash) =
params.code_hash_provider.code_hash_at(parent_hash)
else {
tracing::error!(target: crate::LOG_TARGET, ?parent_hash, "Could not fetch validation code hash");
break;
};
super::check_validation_code_or_log(
&validation_code_hash,
params.para_id,
&params.relay_client,
relay_parent,
)
.await;
let allowed_pov_size = if let Some(max_pov_percentage) = params.max_pov_percentage {
validation_data.max_pov_size * max_pov_percentage / 100
} else {
// Set the block limit to 85% of the maximum PoV size.
//
// Once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 issue is
// fixed, the reservation should be removed.
validation_data.max_pov_size * 85 / 100
} as usize;
match collator
.collate(
&parent_header,
&slot_claim,
None,
(teyrchain_inherent_data, other_inherent_data),
params.authoring_duration,
allowed_pov_size,
)
.await
{
Ok(Some((collation, block_data))) => {
let Some(new_block_header) =
block_data.blocks().first().map(|b| b.header().clone())
else {
tracing::error!(target: crate::LOG_TARGET, "Produced PoV doesn't contain any blocks");
break;
};
let new_block_hash = new_block_header.hash();
// Here we are assuming that the import logic protects against equivocations
// and provides sybil-resistance, as it should.
collator.collator_service().announce_block(new_block_hash, None);
if let Some(ref export_pov) = export_pov {
export_pov_to_path::<Block>(
export_pov.clone(),
collation.proof_of_validity.clone().into_compressed(),
new_block_hash,
*new_block_header.number(),
parent_header.clone(),
*relay_parent_header.state_root(),
*relay_parent_header.number(),
validation_data.max_pov_size,
);
}
// Send a submit-collation message to the collation generation subsystem,
// which then distributes this to validators.
//
// Here we are assuming that the leaf is imported, as we've gotten an
// import notification.
overseer_handle
.send_msg(
CollationGenerationMessage::SubmitCollation(
SubmitCollationParams {
relay_parent,
collation,
parent_head: parent_header.encode().into(),
validation_code_hash,
result_sender: None,
core_index,
},
),
"SubmitCollation",
)
.await;
parent_hash = new_block_hash;
parent_header = new_block_header;
},
Ok(None) => {
tracing::debug!(target: crate::LOG_TARGET, "No block proposal");
break;
},
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err);
break;
},
}
}
}
}
}
@@ -0,0 +1,709 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Stock, pure Aura collators.
//!
//! This includes the [`basic`] collator, which only builds on top of the most recently
//! included teyrchain block, as well as the [`lookahead`] collator, which prospectively
//! builds on teyrchain blocks which have not yet been included in the relay chain.
use crate::collator::SlotClaim;
use codec::Codec;
use cumulus_client_consensus_common::{self as consensus_common, ParentSearchParams};
use cumulus_primitives_aura::{AuraUnincludedSegmentApi, Slot};
use cumulus_primitives_core::{relay_chain::Header as RelayHeader, BlockT};
use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface};
use pezkuwi_node_subsystem::messages::{CollatorProtocolMessage, RuntimeApiRequest};
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
use pezkuwi_primitives::{
Hash as RelayHash, Id as ParaId, OccupiedCoreAssumption, ValidationCodeHash,
DEFAULT_SCHEDULING_LOOKAHEAD,
};
use pezsc_consensus_aura::{standalone as aura_internal, AuraApi};
use pezsp_api::{ApiExt, ProvideRuntimeApi, RuntimeApiInfo};
use pezsp_core::Pair;
use pezsp_keystore::KeystorePtr;
use pezsp_timestamp::Timestamp;
pub mod basic;
pub mod lookahead;
pub mod slot_based;
// This is an arbitrary value which is guaranteed to exceed the required depth for 500ms blocks
// built with a relay parent offset of 1. It must be larger than the unincluded segment capacity.
//
// The formula we use to compute the capacity of the unincluded segment in the teyrchain runtime
// is:
// UNINCLUDED_SEGMENT_CAPACITY = (2 + RELAY_PARENT_OFFSET) * BLOCK_PROCESSING_VELOCITY + 1.
//
// Since we only search for parent blocks which have already been imported,
// we can guarantee that all imported blocks respect the unincluded segment
// rules specified by the teyrchain's runtime and thus will never be too deep. This is just an extra
// sanity check.
const PARENT_SEARCH_DEPTH: usize = 40;
// Helper to pre-connect to the backing group we got assigned to and keep the connection
// open until backing group changes or own slot ends.
struct BackingGroupConnectionHelper {
keystore: pezsp_keystore::KeystorePtr,
overseer_handle: OverseerHandle,
our_slot: Option<Slot>,
}
impl BackingGroupConnectionHelper {
pub fn new(keystore: pezsp_keystore::KeystorePtr, overseer_handle: OverseerHandle) -> Self {
Self { keystore, overseer_handle, our_slot: None }
}
async fn send_subsystem_message(&mut self, message: CollatorProtocolMessage) {
self.overseer_handle.send_msg(message, "BackingGroupConnectionHelper").await;
}
/// Update the current slot and initiate connections to backing groups if needed.
pub async fn update<P>(&mut self, current_slot: Slot, authorities: &[P::Public])
where
P: pezsp_core::Pair + Send + Sync,
P::Public: Codec,
{
if Some(current_slot) <= self.our_slot {
// Current slot or next slot is ours.
// We already sent pre-connect message, no need to proceed further.
return;
}
let next_slot = current_slot + 1;
let next_slot_is_ours =
aura_internal::claim_slot::<P>(next_slot, authorities, &self.keystore)
.await
.is_some();
if next_slot_is_ours {
// Only send message if we were not connected. This avoids sending duplicate messages
// when running with a single collator.
if self.our_slot.is_none() {
// Next slot is ours, send connect message.
tracing::debug!(target: crate::LOG_TARGET, "Our slot {} is next, connecting to backing groups", next_slot);
self.send_subsystem_message(CollatorProtocolMessage::ConnectToBackingGroups)
.await;
}
self.our_slot = Some(next_slot);
} else if self.our_slot.take().is_some() {
// Next slot is not ours, send disconnect only if we had a slot before.
tracing::debug!(target: crate::LOG_TARGET, "Current slot = {}, disconnecting from backing groups", current_slot);
self.send_subsystem_message(CollatorProtocolMessage::DisconnectFromBackingGroups)
.await;
}
}
}
/// Check the `local_validation_code_hash` against the validation code hash in the relay chain
/// state.
///
/// If the code hashes do not match, it prints a warning.
async fn check_validation_code_or_log(
local_validation_code_hash: &ValidationCodeHash,
para_id: ParaId,
relay_client: &impl RelayChainInterface,
relay_parent: RelayHash,
) {
let state_validation_code_hash = match relay_client
.validation_code_hash(relay_parent, para_id, OccupiedCoreAssumption::Included)
.await
{
Ok(hash) => hash,
Err(error) => {
tracing::debug!(
target: super::LOG_TARGET,
%error,
?relay_parent,
%para_id,
"Failed to fetch validation code hash",
);
return;
},
};
match state_validation_code_hash {
Some(state) =>
if state != *local_validation_code_hash {
tracing::warn!(
target: super::LOG_TARGET,
%para_id,
?relay_parent,
?local_validation_code_hash,
relay_validation_code_hash = ?state,
"Teyrchain code doesn't match validation code stored in the relay chain state.",
);
},
None => {
tracing::warn!(
target: super::LOG_TARGET,
%para_id,
?relay_parent,
"Could not find validation code for teyrchain in the relay chain state.",
);
},
}
}
/// Fetch scheduling lookahead at given relay parent.
async fn scheduling_lookahead(
relay_parent: RelayHash,
relay_client: &impl RelayChainInterface,
) -> Option<u32> {
let runtime_api_version = relay_client
.version(relay_parent)
.await
.map_err(|e| {
tracing::error!(
target: super::LOG_TARGET,
error = ?e,
"Failed to fetch relay chain runtime version.",
)
})
.ok()?;
let teyrchain_host_runtime_api_version = runtime_api_version
.api_version(
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<pezkuwi_primitives::Block>>::ID,
)
.unwrap_or_default();
if teyrchain_host_runtime_api_version <
RuntimeApiRequest::SCHEDULING_LOOKAHEAD_RUNTIME_REQUIREMENT
{
return None;
}
match relay_client.scheduling_lookahead(relay_parent).await {
Ok(scheduling_lookahead) => Some(scheduling_lookahead),
Err(err) => {
tracing::error!(
target: crate::LOG_TARGET,
?err,
?relay_parent,
"Failed to fetch scheduling lookahead from relay chain",
);
None
},
}
}
// Returns the claim queue at the given relay parent.
async fn claim_queue_at(
relay_parent: RelayHash,
relay_client: &impl RelayChainInterface,
) -> ClaimQueueSnapshot {
// Get `ClaimQueue` from runtime
match relay_client.claim_queue(relay_parent).await {
Ok(claim_queue) => claim_queue.into(),
Err(error) => {
tracing::error!(
target: crate::LOG_TARGET,
?error,
?relay_parent,
"Failed to query claim queue runtime API",
);
Default::default()
},
}
}
// Checks if we own the slot at the given block and whether there
// is space in the unincluded segment.
async fn can_build_upon<Block: BlockT, Client, P>(
para_slot: Slot,
relay_slot: Slot,
timestamp: Timestamp,
parent_hash: Block::Hash,
included_block: Block::Hash,
client: &Client,
keystore: &KeystorePtr,
) -> Option<SlotClaim<P::Public>>
where
Client: ProvideRuntimeApi<Block>,
Client::Api: AuraApi<Block, P::Public> + AuraUnincludedSegmentApi<Block> + ApiExt<Block>,
P: Pair,
P::Public: Codec,
P::Signature: Codec,
{
let runtime_api = client.runtime_api();
let authorities = runtime_api.authorities(parent_hash).ok()?;
let author_pub = aura_internal::claim_slot::<P>(para_slot, &authorities, keystore).await?;
// This function is typically called when we want to build block N. At that point, the
// unincluded segment in the runtime is unaware of the hash of block N-1. If the unincluded
// segment in the runtime is full, but block N-1 is the included block, the unincluded segment
// should have length 0 and we can build. Since the hash is not available to the runtime
// however, we need this extra check here.
if parent_hash == included_block {
return Some(SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp));
}
let api_version = runtime_api
.api_version::<dyn AuraUnincludedSegmentApi<Block>>(parent_hash)
.ok()
.flatten()?;
let slot = if api_version > 1 { relay_slot } else { para_slot };
runtime_api
.can_build_upon(parent_hash, included_block, slot)
.ok()?
.then(|| SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp))
}
/// Use [`cumulus_client_consensus_common::find_potential_parents`] to find teyrchain blocks that
/// we can build on. Once a list of potential parents is retrieved, return the last one of the
/// longest chain.
async fn find_parent<Block>(
relay_parent: RelayHash,
para_id: ParaId,
para_backend: &impl pezsc_client_api::Backend<Block>,
relay_client: &impl RelayChainInterface,
) -> Option<(<Block as BlockT>::Header, consensus_common::PotentialParent<Block>)>
where
Block: BlockT,
{
let parent_search_params = ParentSearchParams {
relay_parent,
para_id,
ancestry_lookback: scheduling_lookahead(relay_parent, relay_client)
.await
.unwrap_or(DEFAULT_SCHEDULING_LOOKAHEAD)
.saturating_sub(1) as usize,
max_depth: PARENT_SEARCH_DEPTH,
ignore_alternative_branches: true,
};
let potential_parents = cumulus_client_consensus_common::find_potential_parents::<Block>(
parent_search_params,
para_backend,
relay_client,
)
.await;
let potential_parents = match potential_parents {
Err(e) => {
tracing::error!(
target: crate::LOG_TARGET,
?relay_parent,
err = ?e,
"Could not fetch potential parents to build upon"
);
return None;
},
Ok(x) => x,
};
let included_block = potential_parents.iter().find(|x| x.depth == 0)?.header.clone();
potential_parents
.into_iter()
.max_by_key(|a| a.depth)
.map(|parent| (included_block, parent))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collators::{can_build_upon, BackingGroupConnectionHelper};
use codec::Encode;
use cumulus_primitives_aura::Slot;
use cumulus_primitives_core::BlockT;
use cumulus_relay_chain_interface::PHash;
use cumulus_test_client::{
runtime::{Block, Hash},
Client, DefaultTestClientBuilderExt, InitBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use futures::StreamExt;
use pezkuwi_overseer::{Event, Handle};
use pezkuwi_primitives::HeadData;
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
use pezsp_consensus::BlockOrigin;
use pezsp_keystore::{Keystore, KeystorePtr};
use pezsp_timestamp::Timestamp;
use std::sync::{Arc, Mutex};
async fn import_block<I: BlockImport<Block>>(
importer: &I,
block: Block,
origin: BlockOrigin,
import_as_best: bool,
) {
let (header, body) = block.deconstruct();
let mut block_import_params = BlockImportParams::new(origin, header);
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(import_as_best));
block_import_params.body = Some(body);
importer.import_block(block_import_params).await.unwrap();
}
fn sproof_with_parent_by_hash(client: &Client, hash: PHash) -> RelayStateSproofBuilder {
let header = client.header(hash).ok().flatten().expect("No header for parent block");
let included = HeadData(header.encode());
let mut builder = RelayStateSproofBuilder::default();
builder.para_id = cumulus_test_client::runtime::TEYRCHAIN_ID.into();
builder.included_para_head = Some(included);
builder
}
async fn build_and_import_block(client: &Client, included: Hash) -> Block {
let sproof = sproof_with_parent_by_hash(client, included);
let block_builder = client.init_block_builder(None, sproof).block_builder;
let block = block_builder.build().unwrap().block;
let origin = BlockOrigin::NetworkInitialSync;
import_block(client, block.clone(), origin, true).await;
block
}
fn set_up_components(num_authorities: usize) -> (Arc<Client>, KeystorePtr) {
let keystore = Arc::new(pezsp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
for key in pezsp_keyring::Sr25519Keyring::iter().take(num_authorities) {
Keystore::sr25519_generate_new(
&*keystore,
pezsp_application_crypto::key_types::AURA,
Some(&key.to_seed()),
)
.expect("Can insert key into MemoryKeyStore");
}
(Arc::new(TestClientBuilder::new().build()), keystore)
}
/// This tests a special scenario where the unincluded segment in the runtime
/// is full. We are calling `can_build_upon`, passing the last built block as the
/// included one. In the runtime we will not find the hash of the included block in the
/// unincluded segment. The `can_build_upon` runtime API would therefore return `false`, but
/// we are ensuring on the node side that we are are always able to build on the included block.
#[tokio::test]
async fn test_can_build_upon() {
let (client, keystore) = set_up_components(6);
let genesis_hash = client.chain_info().genesis_hash;
let mut last_hash = genesis_hash;
// Fill up the unincluded segment tracker in the runtime.
while can_build_upon::<_, _, pezsp_consensus_aura::sr25519::AuthorityPair>(
Slot::from(u64::MAX),
Slot::from(u64::MAX),
Timestamp::default(),
last_hash,
genesis_hash,
&*client,
&keystore,
)
.await
.is_some()
{
let block = build_and_import_block(&client, genesis_hash).await;
last_hash = block.header().hash();
}
// Blocks were built with the genesis hash set as included block.
// We call `can_build_upon` with the last built block as the included block.
let result = can_build_upon::<_, _, pezsp_consensus_aura::sr25519::AuthorityPair>(
Slot::from(u64::MAX),
Slot::from(u64::MAX),
Timestamp::default(),
last_hash,
last_hash,
&*client,
&keystore,
)
.await;
assert!(result.is_some());
}
/// Helper to create a mock overseer handle and message recorder
fn create_overseer_handle() -> (OverseerHandle, Arc<Mutex<Vec<CollatorProtocolMessage>>>) {
let messages = Arc::new(Mutex::new(Vec::new()));
let messages_clone = messages.clone();
let (tx, mut rx) = pezkuwi_node_subsystem_util::metered::channel(100);
// Spawn a task to receive and record overseer messages
tokio::spawn(async move {
while let Some(event) = rx.next().await {
if let Event::MsgToSubsystem { msg, .. } = event {
if let pezkuwi_node_subsystem::AllMessages::CollatorProtocol(cp_msg) = msg {
messages_clone.lock().unwrap().push(cp_msg);
}
}
}
});
(Handle::new(tx), messages)
}
#[tokio::test]
async fn preconnect_when_next_slot_is_ours() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update call
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Update with slot 5, next slot (6) should be ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
// Give time for message to be processed
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
assert_eq!(helper.our_slot, Some(Slot::from(6)));
}
#[tokio::test]
async fn preconnect_no_duplicate_connect_message() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update calls
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Update with slot 5, next slot (6) is ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
// Give time for message to be processed
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(messages_recorder.lock().unwrap().len(), 1);
messages_recorder.lock().unwrap().clear();
// Update with slot 5 again - should not send another message
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
// Update with slot 1 (our slot) - should not send another message
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(6), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
}
#[tokio::test]
async fn preconnect_disconnect_when_slot_passes() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update calls
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
// Slot 5 -> Ferdie, Slot 6 -> Alice
// Update with slot 5, next slot (6) is ours -> should connect
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(helper.our_slot, Some(Slot::from(6)));
messages_recorder.lock().unwrap().clear();
// Update with slot 8, next slot (9) is Charlie's -> should disconnect
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(8), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1, "Expected exactly one disconnect message");
assert!(matches!(messages[0], CollatorProtocolMessage::DisconnectFromBackingGroups));
assert_eq!(helper.our_slot, None);
}
messages_recorder.lock().unwrap().clear();
// Update again with slot 8, next slot (9) is Charlie's -> should not send another
// disconnect message
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(8), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 0, "Expected no messages");
assert_eq!(helper.our_slot, None);
}
#[tokio::test]
async fn preconnect_no_disconnect_without_previous_connection() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update call
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
// Slot 5 -> Ferdie
// Update with slot 1 (Bob's slot), next slot (2) is Charlie's
// Since we never connected before (our_slot is None), we should not send disconnect
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(1), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Should not send any message since we never connected
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
assert_eq!(helper.our_slot, None);
}
#[tokio::test]
async fn preconnect_multiple_cycles() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update calls
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
// Slot 5 -> Ferdie, Slot 6 -> Alice, Slot 7 -> Bob, ...
// Cycle 1: Connect at slot 5, next slot (6) is ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
}
assert_eq!(helper.our_slot, Some(Slot::from(6)));
messages_recorder.lock().unwrap().clear();
// Cycle 1: Disconnect at slot 7, next slot (8) is Charlie's
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(7), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::DisconnectFromBackingGroups));
}
assert_eq!(helper.our_slot, None);
messages_recorder.lock().unwrap().clear();
// Cycle 2: Connect again at slot 11, next slot (12) is ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(11), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
}
assert_eq!(helper.our_slot, Some(Slot::from(12)));
}
#[tokio::test]
async fn preconnect_handles_empty_authorities() {
let keystore = Arc::new(pezsp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Pass empty authorities list
let authorities = vec![];
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(0), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Should not send any message if authorities list is empty
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
}
}
/// Holds a relay parent and its descendants.
pub struct RelayParentData {
/// The relay parent block header
relay_parent: RelayHeader,
/// Ordered collection of descendant block headers, from oldest to newest
descendants: Vec<RelayHeader>,
}
impl RelayParentData {
/// Creates a new instance with the given relay parent and no descendants.
pub fn new(relay_parent: RelayHeader) -> Self {
Self { relay_parent, descendants: Default::default() }
}
/// Creates a new instance with the given relay parent and descendants.
pub fn new_with_descendants(relay_parent: RelayHeader, descendants: Vec<RelayHeader>) -> Self {
Self { relay_parent, descendants }
}
/// Returns a reference to the relay parent header.
pub fn relay_parent(&self) -> &RelayHeader {
&self.relay_parent
}
/// Returns the number of descendants.
#[cfg(test)]
pub fn descendants_len(&self) -> usize {
self.descendants.len()
}
/// Consumes the structure and returns a vector containing the relay parent followed by its
/// descendants in chronological order. The resulting list should be provided to the teyrchain
/// inherent data.
pub fn into_inherent_descendant_list(self) -> Vec<RelayHeader> {
let Self { relay_parent, mut descendants } = self;
if descendants.is_empty() {
return Default::default();
}
let mut result = vec![relay_parent];
result.append(&mut descendants);
result
}
}
@@ -0,0 +1,647 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use codec::{Codec, Encode};
use super::CollatorMessage;
use crate::{
collator as collator_util,
collators::{
check_validation_code_or_log,
slot_based::{
relay_chain_data_cache::{RelayChainData, RelayChainDataCache},
slot_timer::{SlotInfo, SlotTimer},
},
BackingGroupConnectionHelper, RelayParentData,
},
LOG_TARGET,
};
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_aura::{AuraUnincludedSegmentApi, Slot};
use cumulus_primitives_core::{
extract_relay_parent, rpsr_digest, ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem,
PersistedValidationData, RelayParentOffsetApi,
};
use cumulus_relay_chain_interface::RelayChainInterface;
use futures::prelude::*;
use pezkuwi_primitives::{
Block as RelayBlock, CoreIndex, Hash as RelayHash, Header as RelayHeader, Id as ParaId,
};
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf, UsageProvider};
use pezsc_consensus::BlockImport;
use pezsc_consensus_aura::SlotDuration;
use pezsc_network_types::PeerId;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member, Zero};
use std::{collections::VecDeque, sync::Arc, time::Duration};
/// Parameters for [`run_block_builder`].
pub struct BuilderTaskParams<
Block: BlockT,
BI,
CIDP,
Client,
Backend,
RelayClient,
CHP,
Proposer,
CS,
> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// The para client's backend, used to access the database.
pub para_backend: Arc<Backend>,
/// A handle to the relay-chain client.
pub relay_client: RelayClient,
/// A validation code hash provider, used to get the current validation code hash.
pub code_hash_provider: CHP,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Channel to send built blocks to the collation task.
pub collator_sender: pezsc_utils::mpsc::TracingUnboundedSender<CollatorMessage<Block>>,
/// Slot duration of the relay chain.
pub relay_chain_slot_duration: Duration,
/// Offset all time operations by this duration.
///
/// This is a time quantity that is subtracted from the actual timestamp when computing
/// the time left to enter a new slot. In practice, this *left-shifts* the clock time with the
/// intent to keep our "clock" slightly behind the relay chain one and thus reducing the
/// likelihood of encountering unfavorable notification arrival timings (i.e. we don't want to
/// wait for relay chain notifications because we woke up too early).
pub slot_offset: Duration,
/// The maximum percentage of the maximum PoV size that the collator can use.
/// It will be removed once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 is fixed.
pub max_pov_percentage: Option<u32>,
}
/// Run block-builder.
pub fn run_block_builder<Block, P, BI, CIDP, Client, Backend, RelayClient, CHP, Proposer, CS>(
params: BuilderTaskParams<Block, BI, CIDP, Client, Backend, RelayClient, CHP, Proposer, CS>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ UsageProvider<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + RelayParentOffsetApi<Block> + AuraUnincludedSegmentApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RelayClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
async move {
tracing::info!(target: LOG_TARGET, "Starting slot-based block-builder task.");
let BuilderTaskParams {
relay_client,
create_inherent_data_providers,
para_client,
keystore,
block_import,
collator_peer_id,
para_id,
proposer,
collator_service,
collator_sender,
code_hash_provider,
authoring_duration,
relay_chain_slot_duration,
para_backend,
slot_offset,
max_pov_percentage,
} = params;
let mut slot_timer = SlotTimer::<_, _, P>::new_with_offset(
para_client.clone(),
slot_offset,
relay_chain_slot_duration,
);
let mut collator = {
let params = collator_util::Params {
create_inherent_data_providers,
block_import,
relay_client: relay_client.clone(),
keystore: keystore.clone(),
collator_peer_id,
para_id,
proposer,
collator_service,
};
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
};
let mut relay_chain_data_cache = RelayChainDataCache::new(relay_client.clone(), para_id);
let mut connection_helper = BackingGroupConnectionHelper::new(
keystore.clone(),
relay_client
.overseer_handle()
// Should never fail. If it fails, then providing collations to relay chain
// doesn't work either. So it is fine to panic here.
.expect("Relay chain interface must provide overseer handle."),
);
loop {
// We wait here until the next slot arrives.
if slot_timer.wait_until_next_slot().await.is_err() {
tracing::error!(target: LOG_TARGET, "Unable to wait for next slot.");
return;
};
let Ok(relay_best_hash) = relay_client.best_block_hash().await else {
tracing::warn!(target: crate::LOG_TARGET, "Unable to fetch latest relay chain block hash.");
continue;
};
let best_hash = para_client.info().best_hash;
let relay_parent_offset =
para_client.runtime_api().relay_parent_offset(best_hash).unwrap_or_default();
let Ok(para_slot_duration) = crate::slot_duration(&*para_client) else {
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
continue;
};
let Ok(Some(rp_data)) = offset_relay_parent_find_descendants(
&mut relay_chain_data_cache,
relay_best_hash,
relay_parent_offset,
)
.await
else {
continue;
};
let Some(para_slot) = adjust_para_to_relay_parent_slot(
rp_data.relay_parent(),
relay_chain_slot_duration,
para_slot_duration,
) else {
continue;
};
let relay_parent = rp_data.relay_parent().hash();
let relay_parent_header = rp_data.relay_parent().clone();
let Some((included_header, parent)) =
crate::collators::find_parent(relay_parent, para_id, &*para_backend, &relay_client)
.await
else {
continue;
};
let parent_hash = parent.hash;
let parent_header = &parent.header;
// Retrieve the core.
let core = match determine_core(
&mut relay_chain_data_cache,
&relay_parent_header,
para_id,
parent_header,
relay_parent_offset,
)
.await
{
Err(()) => {
tracing::debug!(
target: LOG_TARGET,
?relay_parent,
"Failed to determine core"
);
continue;
},
Ok(Some(cores)) => {
tracing::debug!(
target: LOG_TARGET,
?relay_parent,
core_selector = ?cores.selector,
claim_queue_offset = ?cores.claim_queue_offset,
"Going to claim core",
);
cores
},
Ok(None) => {
tracing::debug!(
target: LOG_TARGET,
?relay_parent,
"No core scheduled"
);
continue;
},
};
let Ok(RelayChainData { max_pov_size, last_claimed_core_selector, .. }) =
relay_chain_data_cache.get_mut_relay_chain_data(relay_parent).await
else {
continue;
};
slot_timer.update_scheduling(core.total_cores().into());
// We mainly call this to inform users at genesis if there is a mismatch with the
// on-chain data.
collator.collator_service().check_block_status(parent_hash, parent_header);
let Ok(relay_slot) =
pezsc_consensus_babe::find_pre_digest::<RelayBlock>(&relay_parent_header)
.map(|babe_pre_digest| babe_pre_digest.slot())
else {
tracing::error!(target: crate::LOG_TARGET, "Relay chain does not contain babe slot. This should never happen.");
continue;
};
let included_header_hash = included_header.hash();
if let Ok(authorities) = para_client.runtime_api().authorities(parent_hash) {
connection_helper.update::<P>(para_slot.slot, &authorities).await;
}
let slot_claim = match crate::collators::can_build_upon::<_, _, P>(
para_slot.slot,
relay_slot,
para_slot.timestamp,
parent_hash,
included_header_hash,
&*para_client,
&keystore,
)
.await
{
Some(slot) => slot,
None => {
tracing::debug!(
target: crate::LOG_TARGET,
unincluded_segment_len = parent.depth,
relay_parent = ?relay_parent,
relay_parent_num = %relay_parent_header.number(),
included_hash = ?included_header_hash,
included_num = %included_header.number(),
parent = ?parent_hash,
slot = ?para_slot.slot,
"Not building block."
);
continue;
},
};
tracing::debug!(
target: crate::LOG_TARGET,
unincluded_segment_len = parent.depth,
relay_parent = %relay_parent,
relay_parent_num = %relay_parent_header.number(),
relay_parent_offset,
included_hash = %included_header_hash,
included_num = %included_header.number(),
parent = %parent_hash,
slot = ?para_slot.slot,
"Building block."
);
let validation_data = PersistedValidationData {
parent_head: parent_header.encode().into(),
relay_parent_number: *relay_parent_header.number(),
relay_parent_storage_root: *relay_parent_header.state_root(),
max_pov_size: *max_pov_size,
};
let (teyrchain_inherent_data, other_inherent_data) = match collator
.create_inherent_data_with_rp_offset(
relay_parent,
&validation_data,
parent_hash,
slot_claim.timestamp(),
Some(rp_data),
collator_peer_id,
)
.await
{
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err);
break;
},
Ok(x) => x,
};
let validation_code_hash = match code_hash_provider.code_hash_at(parent_hash) {
None => {
tracing::error!(target: crate::LOG_TARGET, ?parent_hash, "Could not fetch validation code hash");
break;
},
Some(v) => v,
};
check_validation_code_or_log(
&validation_code_hash,
para_id,
&relay_client,
relay_parent,
)
.await;
let allowed_pov_size = if let Some(max_pov_percentage) = max_pov_percentage {
validation_data.max_pov_size * max_pov_percentage / 100
} else {
// Set the block limit to 85% of the maximum PoV size.
//
// Once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 issue is
// fixed, this should be removed.
validation_data.max_pov_size * 85 / 100
} as usize;
let adjusted_authoring_duration =
slot_timer.adjust_authoring_duration(authoring_duration);
tracing::debug!(target: crate::LOG_TARGET, duration = ?adjusted_authoring_duration, "Adjusted proposal duration.");
let Some(adjusted_authoring_duration) = adjusted_authoring_duration else {
tracing::debug!(
target: crate::LOG_TARGET,
unincluded_segment_len = parent.depth,
relay_parent = ?relay_parent,
relay_parent_num = %relay_parent_header.number(),
included_hash = ?included_header_hash,
included_num = %included_header.number(),
parent = ?parent_hash,
slot = ?para_slot.slot,
"Not building block due to insufficient authoring duration."
);
continue;
};
let Ok(Some(candidate)) = collator
.build_block_and_import(
&parent_header,
&slot_claim,
Some(vec![CumulusDigestItem::CoreInfo(core.core_info()).to_digest_item()]),
(teyrchain_inherent_data, other_inherent_data),
adjusted_authoring_duration,
allowed_pov_size,
)
.await
else {
tracing::error!(target: crate::LOG_TARGET, "Unable to build block at slot.");
continue;
};
let new_block_hash = candidate.block.header().hash();
// Announce the newly built block to our peers.
collator.collator_service().announce_block(new_block_hash, None);
*last_claimed_core_selector = Some(core.core_selector());
if let Err(err) = collator_sender.unbounded_send(CollatorMessage {
relay_parent,
parent_header: parent_header.clone(),
teyrchain_candidate: candidate,
validation_code_hash,
core_index: core.core_index(),
max_pov_size: validation_data.max_pov_size,
}) {
tracing::error!(target: crate::LOG_TARGET, ?err, "Unable to send block to collation task.");
return;
}
}
}
}
/// Translate the slot of the relay parent to the slot of the teyrchain.
fn adjust_para_to_relay_parent_slot(
relay_header: &RelayHeader,
relay_chain_slot_duration: Duration,
para_slot_duration: SlotDuration,
) -> Option<SlotInfo> {
let relay_slot = pezsc_consensus_babe::find_pre_digest::<RelayBlock>(&relay_header)
.map(|babe_pre_digest| babe_pre_digest.slot())
.ok()?;
let new_slot = Slot::from_timestamp(
relay_slot
.timestamp(SlotDuration::from_millis(relay_chain_slot_duration.as_millis() as u64))?,
para_slot_duration,
);
let para_slot = SlotInfo { slot: new_slot, timestamp: new_slot.timestamp(para_slot_duration)? };
tracing::debug!(
target: LOG_TARGET,
timestamp = ?para_slot.timestamp,
slot = ?para_slot.slot,
"Teyrchain slot adjusted to relay chain.",
);
Some(para_slot)
}
/// Finds a relay chain parent block at a specified offset from the best block, collecting its
/// descendants.
///
/// # Returns
/// * `Ok(RelayParentData)` - Contains the target relay parent and its ordered list of descendants
/// * `Err(())` - If any relay chain block header cannot be retrieved
///
/// The function traverses backwards from the best block until it finds the block at the specified
/// offset, collecting all blocks in between to maintain the chain of ancestry.
pub(crate) async fn offset_relay_parent_find_descendants<RelayClient>(
relay_chain_data_cache: &mut RelayChainDataCache<RelayClient>,
relay_best_block: RelayHash,
relay_parent_offset: u32,
) -> Result<Option<RelayParentData>, ()>
where
RelayClient: RelayChainInterface + Clone + 'static,
{
let Ok(mut relay_header) = relay_chain_data_cache
.get_mut_relay_chain_data(relay_best_block)
.await
.map(|d| d.relay_parent_header.clone())
else {
tracing::error!(target: LOG_TARGET, ?relay_best_block, "Unable to fetch best relay chain block header.");
return Err(());
};
if relay_parent_offset == 0 {
return Ok(Some(RelayParentData::new(relay_header)));
}
if pezsc_consensus_babe::contains_epoch_change::<RelayBlock>(&relay_header) {
tracing::debug!(target: LOG_TARGET, ?relay_best_block, relay_best_block_number = relay_header.number(), "Relay parent is in previous session.");
return Ok(None);
}
let mut required_ancestors: VecDeque<RelayHeader> = Default::default();
required_ancestors.push_front(relay_header.clone());
while required_ancestors.len() < relay_parent_offset as usize {
let next_header = relay_chain_data_cache
.get_mut_relay_chain_data(*relay_header.parent_hash())
.await?
.relay_parent_header
.clone();
if pezsc_consensus_babe::contains_epoch_change::<RelayBlock>(&next_header) {
tracing::debug!(target: LOG_TARGET, ?relay_best_block, ancestor = %next_header.hash(), ancestor_block_number = next_header.number(), "Ancestor of best block is in previous session.");
return Ok(None);
}
required_ancestors.push_front(next_header.clone());
relay_header = next_header;
}
let relay_parent = relay_chain_data_cache
.get_mut_relay_chain_data(*relay_header.parent_hash())
.await?
.relay_parent_header
.clone();
tracing::debug!(
target: LOG_TARGET,
relay_parent_hash = %relay_parent.hash(),
relay_parent_num = relay_parent.number(),
num_descendants = required_ancestors.len(),
"Relay parent descendants."
);
Ok(Some(RelayParentData::new_with_descendants(relay_parent, required_ancestors.into())))
}
/// Return value of [`determine_core`].
pub(crate) struct Core {
selector: CoreSelector,
claim_queue_offset: ClaimQueueOffset,
core_index: CoreIndex,
number_of_cores: u16,
}
impl Core {
/// Returns the current [`CoreInfo`].
fn core_info(&self) -> CoreInfo {
CoreInfo {
selector: self.selector,
claim_queue_offset: self.claim_queue_offset,
number_of_cores: self.number_of_cores.into(),
}
}
/// Returns the current [`CoreSelector`].
pub(crate) fn core_selector(&self) -> CoreSelector {
self.selector
}
/// Returns the current [`CoreIndex`].
pub(crate) fn core_index(&self) -> CoreIndex {
self.core_index
}
/// Returns the total number of cores.
pub(crate) fn total_cores(&self) -> u16 {
self.number_of_cores
}
}
/// Determine the core for the given `para_id`.
pub(crate) async fn determine_core<H: HeaderT, RI: RelayChainInterface + 'static>(
relay_chain_data_cache: &mut RelayChainDataCache<RI>,
relay_parent: &RelayHeader,
para_id: ParaId,
para_parent: &H,
relay_parent_offset: u32,
) -> Result<Option<Core>, ()> {
let cores_at_offset = &relay_chain_data_cache
.get_mut_relay_chain_data(relay_parent.hash())
.await?
.claim_queue
.iter_claims_at_depth_for_para(relay_parent_offset as usize, para_id)
.collect::<Vec<_>>();
let is_new_relay_parent = if para_parent.number().is_zero() {
true
} else {
match extract_relay_parent(para_parent.digest()) {
Some(last_relay_parent) => last_relay_parent != relay_parent.hash(),
None =>
rpsr_digest::extract_relay_parent_storage_root(para_parent.digest())
.ok_or(())?
.0 != *relay_parent.state_root(),
}
};
let core_info = CumulusDigestItem::find_core_info(para_parent.digest());
// If we are using a new relay parent, we can start over from the start.
let (selector, core_index) = if is_new_relay_parent {
let Some(core_index) = cores_at_offset.get(0) else { return Ok(None) };
(0, *core_index)
} else if let Some(core_info) = core_info {
let selector = core_info.selector.0 as usize + 1;
let Some(core_index) = cores_at_offset.get(selector) else { return Ok(None) };
(selector, *core_index)
} else {
let last_claimed_core_selector = relay_chain_data_cache
.get_mut_relay_chain_data(relay_parent.hash())
.await?
.last_claimed_core_selector;
let selector = last_claimed_core_selector.map_or(0, |cs| cs.0 as usize) + 1;
let Some(core_index) = cores_at_offset.get(selector) else { return Ok(None) };
(selector, *core_index)
};
Ok(Some(Core {
selector: CoreSelector(selector as u8),
core_index,
claim_queue_offset: ClaimQueueOffset(relay_parent_offset as u8),
number_of_cores: cores_at_offset.len() as u16,
}))
}
@@ -0,0 +1,145 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use futures::{stream::FusedStream, StreamExt};
use pezsc_consensus::{BlockImport, StateAction};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use pezsp_api::{ApiExt, CallApiAt, CallContext, Core, ProvideRuntimeApi, StorageProof};
use pezsp_runtime::traits::{Block as BlockT, Header as _};
use pezsp_trie::proof_size_extension::ProofSizeExt;
use std::sync::Arc;
/// Handle for receiving the block and the storage proof from the [`SlotBasedBlockImport`].
///
/// This handle should be passed to [`Params`](super::Params) or can also be dropped if the node is
/// not running as collator.
pub struct SlotBasedBlockImportHandle<Block> {
receiver: TracingUnboundedReceiver<(Block, StorageProof)>,
}
impl<Block> SlotBasedBlockImportHandle<Block> {
/// Returns the next item.
///
/// The future will never return when the internal channel is closed.
pub async fn next(&mut self) -> (Block, StorageProof) {
loop {
if self.receiver.is_terminated() {
futures::pending!()
} else if let Some(res) = self.receiver.next().await {
return res;
}
}
}
}
/// Special block import for the slot based collator.
pub struct SlotBasedBlockImport<Block, BI, Client> {
inner: BI,
client: Arc<Client>,
sender: TracingUnboundedSender<(Block, StorageProof)>,
}
impl<Block, BI, Client> SlotBasedBlockImport<Block, BI, Client> {
/// Create a new instance.
///
/// The returned [`SlotBasedBlockImportHandle`] needs to be passed to the
/// [`Params`](super::Params), so that this block import instance can communicate with the
/// collation task. If the node is not running as a collator, just dropping the handle is fine.
pub fn new(inner: BI, client: Arc<Client>) -> (Self, SlotBasedBlockImportHandle<Block>) {
let (sender, receiver) = tracing_unbounded("SlotBasedBlockImportChannel", 1000);
(Self { sender, client, inner }, SlotBasedBlockImportHandle { receiver })
}
}
impl<Block, BI: Clone, Client> Clone for SlotBasedBlockImport<Block, BI, Client> {
fn clone(&self) -> Self {
Self { inner: self.inner.clone(), client: self.client.clone(), sender: self.sender.clone() }
}
}
#[async_trait::async_trait]
impl<Block, BI, Client> BlockImport<Block> for SlotBasedBlockImport<Block, BI, Client>
where
Block: BlockT,
BI: BlockImport<Block> + Send + Sync,
BI::Error: Into<pezsp_consensus::Error>,
Client: ProvideRuntimeApi<Block> + CallApiAt<Block> + Send + Sync,
Client::StateBackend: Send,
Client::Api: Core<Block>,
{
type Error = pezsp_consensus::Error;
async fn check_block(
&self,
block: pezsc_consensus::BlockCheckParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
self.inner.check_block(block).await.map_err(Into::into)
}
async fn import_block(
&self,
mut params: pezsc_consensus::BlockImportParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
// If the channel exists and it is required to execute the block, we will execute the block
// here. This is done to collect the storage proof and to prevent re-execution, we push
// downwards the state changes. `StateAction::ApplyChanges` is ignored, because it either
// means that the node produced the block itself or the block was imported via state sync.
if !self.sender.is_closed() && !matches!(params.state_action, StateAction::ApplyChanges(_))
{
let mut runtime_api = self.client.runtime_api();
runtime_api.set_call_context(CallContext::Onchain);
runtime_api.record_proof();
let recorder = runtime_api
.proof_recorder()
.expect("Proof recording is enabled in the line above; qed.");
runtime_api.register_extension(ProofSizeExt::new(recorder));
let parent_hash = *params.header.parent_hash();
let block = Block::new(params.header.clone(), params.body.clone().unwrap_or_default());
runtime_api
.execute_block(parent_hash, block.clone().into())
.map_err(|e| Box::new(e) as Box<_>)?;
let storage_proof =
runtime_api.extract_proof().expect("Proof recording was enabled above; qed");
let state = self.client.state_at(parent_hash).map_err(|e| Box::new(e) as Box<_>)?;
let gen_storage_changes = runtime_api
.into_storage_changes(&state, parent_hash)
.map_err(pezsp_consensus::Error::ChainLookup)?;
if params.header.state_root() != &gen_storage_changes.transaction_storage_root {
return Err(pezsp_consensus::Error::Other(Box::new(
pezsp_blockchain::Error::InvalidStateRoot,
)));
}
params.state_action = StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(
gen_storage_changes,
));
let _ = self.sender.unbounded_send((block, storage_proof));
}
self.inner.import_block(params).await.map_err(Into::into)
}
}
@@ -0,0 +1,189 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use codec::Encode;
use std::path::PathBuf;
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::{MaybeCompressedPoV, SubmitCollationParams};
use pezkuwi_node_subsystem::messages::CollationGenerationMessage;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
use cumulus_primitives_core::relay_chain::BlockId;
use futures::prelude::*;
use crate::export_pov_to_path;
use pezsc_utils::mpsc::TracingUnboundedReceiver;
use pezsp_runtime::traits::{Block as BlockT, Header};
use super::CollatorMessage;
const LOG_TARGET: &str = "aura::pezcumulus::collation_task";
/// Parameters for the collation task.
pub struct Params<Block: BlockT, RClient, CS> {
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The para's ID.
pub para_id: ParaId,
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
pub reinitialize: bool,
/// Collator service interface
pub collator_service: CS,
/// Receiver channel for communication with the block builder task.
pub collator_receiver: TracingUnboundedReceiver<CollatorMessage<Block>>,
/// The handle from the special slot based block import.
pub block_import_handle: super::SlotBasedBlockImportHandle<Block>,
/// When set, the collator will export every produced `POV` to this folder.
pub export_pov: Option<PathBuf>,
}
/// Asynchronously executes the collation task for a teyrchain.
///
/// This function initializes the collator subsystems necessary for producing and submitting
/// collations to the relay chain. It listens for new best relay chain block notifications and
/// handles collator messages. If our teyrchain is scheduled on a core and we have a candidate,
/// the task will build a collation and send it to the relay chain.
pub async fn run_collation_task<Block, RClient, CS>(
Params {
relay_client,
collator_key,
para_id,
reinitialize,
collator_service,
mut collator_receiver,
mut block_import_handle,
export_pov,
}: Params<Block, RClient, CS>,
) where
Block: BlockT,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
RClient: RelayChainInterface + Clone + 'static,
{
let Ok(mut overseer_handle) = relay_client.overseer_handle() else {
tracing::error!(target: LOG_TARGET, "Failed to get overseer handle.");
return;
};
cumulus_client_collator::initialize_collator_subsystems(
&mut overseer_handle,
collator_key,
para_id,
reinitialize,
)
.await;
loop {
futures::select! {
collator_message = collator_receiver.next() => {
let Some(message) = collator_message else {
return;
};
handle_collation_message(message, &collator_service, &mut overseer_handle,relay_client.clone(),export_pov.clone()).await;
},
block_import_msg = block_import_handle.next().fuse() => {
// TODO: Implement me.
// Issue: https://github.com/pezkuwichain/pezkuwi-sdk/issues/24
let _ = block_import_msg;
}
}
}
}
/// Handle an incoming collation message from the block builder task.
/// This builds the collation from the [`CollatorMessage`] and submits it to
/// the collation-generation subsystem of the relay chain.
async fn handle_collation_message<Block: BlockT, RClient: RelayChainInterface + Clone + 'static>(
message: CollatorMessage<Block>,
collator_service: &impl CollatorServiceInterface<Block>,
overseer_handle: &mut OverseerHandle,
relay_client: RClient,
export_pov: Option<PathBuf>,
) {
let CollatorMessage {
parent_header,
teyrchain_candidate,
validation_code_hash,
relay_parent,
core_index,
max_pov_size,
} = message;
let hash = teyrchain_candidate.block.header().hash();
let number = *teyrchain_candidate.block.header().number();
let (collation, block_data) =
match collator_service.build_collation(&parent_header, hash, teyrchain_candidate) {
Some(collation) => collation,
None => {
tracing::warn!(target: LOG_TARGET, %hash, ?number, ?core_index, "Unable to build collation.");
return;
},
};
block_data.log_size_info();
if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
if let Some(pov_path) = export_pov {
if let Ok(Some(relay_parent_header)) =
relay_client.header(BlockId::Hash(relay_parent)).await
{
if let Some(header) = block_data.blocks().first().map(|b| b.header()) {
export_pov_to_path::<Block>(
pov_path.clone(),
pov.clone(),
header.hash(),
*header.number(),
parent_header.clone(),
relay_parent_header.state_root,
relay_parent_header.number,
max_pov_size,
);
}
} else {
tracing::error!(target: LOG_TARGET, "Failed to get relay parent header from hash: {relay_parent:?}");
}
}
tracing::info!(
target: LOG_TARGET,
"Compressed PoV size: {}kb",
pov.block_data.0.len() as f64 / 1024f64,
);
}
tracing::debug!(target: LOG_TARGET, ?core_index, ?hash, %number, "Submitting collation for core.");
overseer_handle
.send_msg(
CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
relay_parent,
collation,
parent_head: parent_header.encode().into(),
validation_code_hash,
core_index,
result_sender: None,
}),
"SubmitCollation",
)
.await;
}
@@ -0,0 +1,270 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! # Architecture Overview
//!
//! The block building mechanism operates through two coordinated tasks:
//!
//! 1. **Block Builder Task**: Orchestrates the timing and execution of teyrchain block production
//! 2. **Collator Task**: Processes built blocks into collations for relay chain submission
//!
//! # Block Builder Task Details
//!
//! The block builder task manages block production timing and execution through an iterative
//! process:
//!
//! 1. Awaits the next production signal from the internal timer
//! 2. Retrieves the current best relay chain block and identifies a valid parent block (see
//! [find_potential_parents][cumulus_client_consensus_common::find_potential_parents] for parent
//! selection criteria)
//! 3. Validates that:
//! - The teyrchain has an assigned core on the relay chain
//! - No block has been previously built on the target core
//! 4. Executes block building and import operations
//! 5. Transmits the completed block to the collator task
//!
//! # Block Production Timing
//!
//! When a block is produced is determined by the following parameters:
//!
//! - Teyrchain slot duration
//! - Number of assigned teyrchain cores
//! - Teyrchain runtime configuration
//!
//! ## Timing Examples
//!
//! The following table demonstrates various timing configurations and their effects. The "AURA
//! Slot" column shows which author is responsible for the block.
//!
//! | Slot Duration (ms) | Cores | Production Attempts (ms) | AURA Slot |
//! |-------------------|--------|-------------------------|------------|
//! | 2000 | 3 | 0, 2000, 4000, 6000 | 0, 1, 2, 3 |
//! | 6000 | 1 | 0, 6000, 12000, 18000 | 0, 1, 2, 3 |
//! | 6000 | 3 | 0, 2000, 4000, 6000 | 0, 0, 0, 1 |
//! | 12000 | 1 | 0, 6000, 12000, 18000 | 0, 0, 1, 1 |
//! | 12000 | 3 | 0, 2000, 4000, 6000 | 0, 0, 0, 0 |
//!
//! # Collator Task Details
//!
//! The collator task receives built blocks from the block builder task and performs two primary
//! functions:
//!
//! 1. Block compression
//! 2. Submission to the collation-generation subsystem
use self::{block_builder_task::run_block_builder, collation_task::run_collation_task};
pub use block_import::{SlotBasedBlockImport, SlotBasedBlockImportHandle};
use codec::Codec;
use consensus_common::TeyrchainCandidate;
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_aura::AuraUnincludedSegmentApi;
use cumulus_primitives_core::RelayParentOffsetApi;
use cumulus_relay_chain_interface::RelayChainInterface;
use futures::FutureExt;
use pezkuwi_primitives::{
CollatorPair, CoreIndex, Hash as RelayHash, Id as ParaId, ValidationCodeHash,
};
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf, UsageProvider};
use pezsc_consensus::BlockImport;
use pezsc_network_types::PeerId;
use pezsc_utils::mpsc::tracing_unbounded;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::{crypto::Pair, traits::SpawnEssentialNamed};
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Member};
use std::{path::PathBuf, sync::Arc, time::Duration};
mod block_builder_task;
mod block_import;
mod collation_task;
mod relay_chain_data_cache;
mod slot_timer;
#[cfg(test)]
mod tests;
/// Parameters for [`run`].
pub struct Params<Block, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// The para client's backend, used to access the database.
pub para_backend: Arc<Backend>,
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// A validation code hash provider, used to get the current validation code hash.
pub code_hash_provider: CHP,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
pub reinitialize: bool,
/// Offset slots by a fixed duration. This can be used to create more preferrable authoring
/// timings.
pub slot_offset: Duration,
/// The handle returned by [`SlotBasedBlockImport`].
pub block_import_handle: SlotBasedBlockImportHandle<Block>,
/// Spawner for spawning futures.
pub spawner: Spawner,
/// Slot duration of the relay chain
pub relay_chain_slot_duration: Duration,
/// When set, the collator will export every produced `POV` to this folder.
pub export_pov: Option<PathBuf>,
/// The maximum percentage of the maximum PoV size that the collator can use.
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
pub max_pov_percentage: Option<u32>,
}
/// Run aura-based block building and collation task.
pub fn run<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner>(
params: Params<Block, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner>,
) where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ UsageProvider<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + AuraUnincludedSegmentApi<Block> + RelayParentOffsetApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + Clone + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
Spawner: SpawnEssentialNamed + Clone + 'static,
{
let Params {
create_inherent_data_providers,
block_import,
para_client,
para_backend,
relay_client,
code_hash_provider,
keystore,
collator_key,
collator_peer_id,
para_id,
proposer,
collator_service,
authoring_duration,
reinitialize,
slot_offset,
block_import_handle,
spawner,
export_pov,
relay_chain_slot_duration,
max_pov_percentage,
} = params;
let (tx, rx) = tracing_unbounded("mpsc_builder_to_collator", 100);
let collator_task_params = collation_task::Params {
relay_client: relay_client.clone(),
collator_key,
para_id,
reinitialize,
collator_service: collator_service.clone(),
collator_receiver: rx,
block_import_handle,
export_pov,
};
let collation_task_fut = run_collation_task::<Block, _, _>(collator_task_params);
let block_builder_params = block_builder_task::BuilderTaskParams {
create_inherent_data_providers,
block_import,
para_client,
para_backend,
relay_client,
code_hash_provider,
keystore,
collator_peer_id,
para_id,
proposer,
collator_service,
authoring_duration,
collator_sender: tx,
relay_chain_slot_duration,
slot_offset,
max_pov_percentage,
};
let block_builder_fut =
run_block_builder::<Block, P, _, _, _, _, _, _, _, _>(block_builder_params);
spawner.spawn_essential_blocking(
"slot-based-block-builder",
Some("slot-based-collator"),
block_builder_fut.boxed(),
);
spawner.spawn_essential_blocking(
"slot-based-collation",
Some("slot-based-collator"),
collation_task_fut.boxed(),
);
}
/// Message to be sent from the block builder to the collation task.
///
/// Contains all data necessary to submit a collation to the relay chain.
struct CollatorMessage<Block: BlockT> {
/// The hash of the relay chain block that provides the context for the teyrchain block.
pub relay_parent: RelayHash,
/// The header of the parent block.
pub parent_header: Block::Header,
/// The teyrchain block candidate.
pub teyrchain_candidate: TeyrchainCandidate<Block>,
/// The validation code hash at the parent block.
pub validation_code_hash: ValidationCodeHash,
/// Core index that this block should be submitted on
pub core_index: CoreIndex,
/// Maximum pov size. Currently needed only for exporting PoV.
pub max_pov_size: u32,
}
@@ -0,0 +1,122 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Utility for caching [`RelayChainData`] for different relay blocks.
use crate::collators::claim_queue_at;
use cumulus_primitives_core::CoreSelector;
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
use pezkuwi_primitives::{
Hash as RelayHash, Header as RelayHeader, Id as ParaId, OccupiedCoreAssumption,
};
use pezsp_runtime::generic::BlockId;
/// Contains relay chain data necessary for teyrchain block building.
#[derive(Clone, Debug)]
pub struct RelayChainData {
/// Current relay chain parent header.
pub relay_parent_header: RelayHeader,
/// The claim queue at the relay parent.
pub claim_queue: ClaimQueueSnapshot,
/// Maximum configured PoV size on the relay chain.
pub max_pov_size: u32,
/// The last [`CoreSelector`] we used.
pub last_claimed_core_selector: Option<CoreSelector>,
}
/// Simple helper to fetch relay chain data and cache it based on the current relay chain best block
/// hash.
pub struct RelayChainDataCache<RI> {
relay_client: RI,
para_id: ParaId,
cached_data: schnellru::LruMap<RelayHash, RelayChainData>,
}
impl<RI> RelayChainDataCache<RI>
where
RI: RelayChainInterface + 'static,
{
pub fn new(relay_client: RI, para_id: ParaId) -> Self {
Self {
relay_client,
para_id,
// 50 cached relay chain blocks should be more than enough.
cached_data: schnellru::LruMap::new(schnellru::ByLength::new(50)),
}
}
/// Fetch required [`RelayChainData`] from the relay chain.
/// If this data has been fetched in the past for the incoming hash, it will reuse
/// cached data.
pub async fn get_mut_relay_chain_data(
&mut self,
relay_parent: RelayHash,
) -> Result<&mut RelayChainData, ()> {
let insert_data = if self.cached_data.peek(&relay_parent).is_some() {
tracing::trace!(target: crate::LOG_TARGET, %relay_parent, "Using cached data for relay parent.");
None
} else {
tracing::trace!(target: crate::LOG_TARGET, %relay_parent, "Relay chain best block changed, fetching new data from relay chain.");
Some(self.update_for_relay_parent(relay_parent).await?)
};
Ok(self
.cached_data
.get_or_insert(relay_parent, || {
insert_data.expect("`insert_data` exists if not cached yet; qed")
})
.expect("There is space for at least one element; qed"))
}
/// Fetch fresh data from the relay chain for the given relay parent hash.
async fn update_for_relay_parent(&self, relay_parent: RelayHash) -> Result<RelayChainData, ()> {
let claim_queue = claim_queue_at(relay_parent, &self.relay_client).await;
let Ok(Some(relay_parent_header)) =
self.relay_client.header(BlockId::Hash(relay_parent)).await
else {
tracing::warn!(target: crate::LOG_TARGET, "Unable to fetch latest relay chain block header.");
return Err(());
};
let max_pov_size = match self
.relay_client
.persisted_validation_data(relay_parent, self.para_id, OccupiedCoreAssumption::Included)
.await
{
Ok(None) => return Err(()),
Ok(Some(pvd)) => pvd.max_pov_size,
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to gather information from relay-client");
return Err(());
},
};
Ok(RelayChainData {
relay_parent_header,
claim_queue,
max_pov_size,
last_claimed_core_selector: None,
})
}
#[cfg(test)]
pub(crate) fn insert_test_data(&mut self, relay_parent: RelayHash, data: RelayChainData) {
self.cached_data.insert(relay_parent, data);
}
}
@@ -0,0 +1,647 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use crate::LOG_TARGET;
use codec::Codec;
use cumulus_primitives_aura::Slot;
use cumulus_primitives_core::BlockT;
use pezsc_client_api::UsageProvider;
use pezsc_consensus_aura::SlotDuration;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::Pair;
use pezsp_runtime::traits::Member;
use pezsp_timestamp::Timestamp;
use std::{
cmp::{max, min},
sync::Arc,
time::Duration,
};
/// Lower limits of allowed block production interval.
/// Defensive mechanism, corresponds to 12 cores at 6 second block time.
const BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS: Duration = Duration::from_millis(500);
/// Theoretically, the block production is capped at `BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS`.
/// In practice, there might be slight deviations due to timing inaccuracies and delays.
///
/// This constant is taken into account while adjusting the authoring duration to fit into the slot.
/// Therefore, it will only reduce the authoring duration if we are within the
/// `BLOCK_PRODUCTION_ADJUSTMENT_MS` threshold of the next slot.
///
/// ### 12 cores 500ms blocks
///
/// For example, for 12 cores 500ms blocks: the next slot is scheduled in 490ms due to delays.
/// In that case, we still want to attempt producing the block, as missing the slot would be worse
/// than producing slightly too fast.
const BLOCK_PRODUCTION_THRESHOLD_MS: Duration = Duration::from_millis(100);
/// The amount of time the authoring duration of the last block production attempt
/// should be reduced by to fit into the slot timing.
const BLOCK_PRODUCTION_ADJUSTMENT_MS: Duration = Duration::from_millis(1000);
#[derive(Debug)]
pub(crate) struct SlotInfo {
pub timestamp: Timestamp,
pub slot: Slot,
}
/// Manages block-production timings based on chain parameters and assigned cores.
#[derive(Debug)]
pub(crate) struct SlotTimer<Block, Client, P> {
/// Teyrchain client that is used for runtime calls
client: Arc<Client>,
/// Offset the current time by this duration.
time_offset: Duration,
/// Last reported core count.
last_reported_core_num: Option<u32>,
/// Slot duration of the relay chain. This is used to compute how man block-production
/// attempts we should trigger per relay chain block.
relay_slot_duration: Duration,
/// Stores the latest slot that was reported by [`Self::wait_until_next_slot`].
last_reported_slot: Option<Slot>,
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
}
/// Compute when to try block-authoring next.
/// The exact time point is determined by the slot duration of relay- and teyrchain as
/// well as the last observed core count. If more cores are available, we attempt to author blocks
/// for them.
///
/// Returns a tuple with:
/// - `Duration`: How long to wait until the next slot.
/// - `Slot`: The AURA slot used for authoring
fn compute_next_wake_up_time(
para_slot_duration: SlotDuration,
relay_slot_duration: Duration,
core_count: Option<u32>,
time_now: Duration,
time_offset: Duration,
) -> (Duration, Slot) {
let para_slots_per_relay_block =
(relay_slot_duration.as_millis() / para_slot_duration.as_millis() as u128) as u32;
let assigned_core_num = core_count.unwrap_or(1);
// Trigger at least once per relay block, if we have for example 12 second slot duration,
// we should still produce two blocks if we are scheduled on every relay block.
let mut block_production_interval = min(para_slot_duration.as_duration(), relay_slot_duration);
if assigned_core_num > para_slots_per_relay_block &&
para_slot_duration.as_duration() >= relay_slot_duration
{
block_production_interval =
max(relay_slot_duration / assigned_core_num, BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS);
tracing::debug!(
target: LOG_TARGET,
?block_production_interval,
"Expected to produce for {assigned_core_num} cores but only have {para_slots_per_relay_block} slots. Attempting to produce multiple blocks per slot."
);
}
let (duration, timestamp) =
time_until_next_attempt(time_now, block_production_interval, time_offset);
let aura_slot = Slot::from_timestamp(timestamp, para_slot_duration);
(duration, aura_slot)
}
/// Compute the time until the next slot changes.
///
/// Returns None if the next slot cannot be computed.
fn compute_time_until_next_slot_change(
para_slot_duration: SlotDuration,
time_now: Duration,
time_offset: Duration,
last_reported_slot: Slot,
) -> Option<(Duration, Slot)> {
let now = time_now.saturating_sub(time_offset);
let next_slot = last_reported_slot + Slot::from(1);
let Some(next_slot_timestamp) = next_slot.timestamp(para_slot_duration) else {
return None;
};
let remaining_time = next_slot_timestamp.as_duration().saturating_sub(now);
Some((remaining_time, next_slot))
}
/// Returns current duration since Unix epoch.
fn duration_now() -> Duration {
use std::time::SystemTime;
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_else(|e| {
panic!("Current time {:?} is before Unix epoch. Something is wrong: {:?}", now, e)
})
}
/// Adjust the authoring duration.
fn adjust_authoring_duration(
mut authoring_duration: Duration,
next_block: (Duration, Slot),
next_slot_change: (Duration, Slot),
different_authors: bool,
) -> Option<Duration> {
let (duration, next_block_slot) = next_block;
let (duration_until_next_slot, next_slot) = next_slot_change;
// The authoring of blocks must stop 1 second before the slot ends.
let duration_until_deadline =
duration_until_next_slot.saturating_sub(BLOCK_PRODUCTION_ADJUSTMENT_MS);
tracing::debug!(
target: LOG_TARGET,
?authoring_duration,
?duration,
?next_block_slot,
?duration_until_next_slot,
?next_slot,
?duration_until_deadline,
?different_authors,
"Adjusting authoring duration for slot.",
);
// Ensure no blocks are produced in the last second of the slot,
// regardless of authoring duration.
if duration_until_deadline == Duration::ZERO {
if different_authors {
tracing::warn!(
target: LOG_TARGET,
?duration_until_next_slot,
?next_slot,
"Not enough time left in the slot to adjust authoring duration. Skipping block production for the slot."
);
return None;
}
// If authors are the same, we can still attempt producing the block
// considering the next block duration.
return Some(authoring_duration.min(duration));
}
// Clamp the authoring duration to fit into the slot deadline only if authors are different.
// For most cases, the deadline is farther in the future than the authoring duration.
if different_authors && authoring_duration >= duration_until_deadline {
authoring_duration = duration_until_deadline;
// Ensure we are not going below the minimum interval within a reasonable threshold.
// For 12 cores, we might have a scenario where the last 3 blocks are skipped:
// - Block 10: next slot change in 1.493s:
// - After adjusting the deadline: 1.493s - 1s = 0.493s the block could be produced
// without issues.
// - Block 11: next slot change in 0.993s - skipped by the deadline
// - Block 12: next slot change in 0.493s - skipped by the deadline
if authoring_duration <
BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS.saturating_sub(BLOCK_PRODUCTION_THRESHOLD_MS)
{
tracing::debug!(
target: LOG_TARGET,
?authoring_duration,
?next_slot,
"Authoring duration is below minimum. Skipping block production for the slot."
);
return None;
}
}
// The `duration` intends to slightly adjust when then block production
// attempt happens. This goes slightly below the `BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS`
// threshold.
Some(authoring_duration.min(duration))
}
/// Returns the duration until the next block production should be attempted.
/// Returns:
/// - Duration: The duration until the next attempt.
fn time_until_next_attempt(
now: Duration,
block_production_interval: Duration,
offset: Duration,
) -> (Duration, Timestamp) {
let now = now.as_millis().saturating_sub(offset.as_millis());
let next_slot_time = ((now + block_production_interval.as_millis()) /
block_production_interval.as_millis()) *
block_production_interval.as_millis();
let remaining_millis = next_slot_time - now;
(Duration::from_millis(remaining_millis as u64), Timestamp::from(next_slot_time as u64))
}
impl<Block, Client, P> SlotTimer<Block, Client, P>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + UsageProvider<Block> + Send + Sync + 'static,
Client::Api: AuraApi<Block, P::Public>,
P: Pair,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
/// Create a new slot timer.
pub fn new_with_offset(
client: Arc<Client>,
time_offset: Duration,
relay_slot_duration: Duration,
) -> Self {
Self {
client,
time_offset,
last_reported_core_num: None,
relay_slot_duration,
last_reported_slot: Default::default(),
_marker: Default::default(),
}
}
/// Inform the slot timer about the last seen number of cores.
pub fn update_scheduling(&mut self, num_cores_next_block: u32) {
self.last_reported_core_num = Some(num_cores_next_block);
}
/// Returns the slot and how much time left until the next block production attempt.
pub fn time_until_next_block(&mut self, slot_duration: SlotDuration) -> (Duration, Slot) {
compute_next_wake_up_time(
slot_duration,
self.relay_slot_duration,
self.last_reported_core_num,
duration_now(),
self.time_offset,
)
}
/// Compute the time until the next slot changes.
fn time_until_next_slot_change(
&mut self,
slot_duration: SlotDuration,
) -> Option<(Duration, Slot)> {
compute_time_until_next_slot_change(
slot_duration,
duration_now(),
self.time_offset,
self.last_reported_slot.unwrap_or_default(),
)
}
/// Check if two slots have different authors based on AURA round-robin algorithm.
///
/// Returns true if the authors for the two slots are different.
fn check_different_slot_authors(&self, slot: Slot, next_slot: Slot) -> bool {
let best_hash = self.client.usage_info().chain.best_hash;
let Ok(authorities) = self.client.runtime_api().authorities(best_hash) else {
tracing::warn!(target: LOG_TARGET, "Failed to fetch authorities for slot author comparison");
// Presume they are different, this will adjust the slot authoring duration more
// conservatively.
return true;
};
let authorities_len = authorities.len() as u64;
if authorities_len <= 1 {
return false;
}
let author1_idx = *slot % authorities_len;
let author2_idx = *next_slot % authorities_len;
author1_idx != author2_idx
}
/// Adjust the authoring duration to fit into the slot timing.
///
/// Returns the adjusted authoring duration and the slot that it corresponds to.
pub fn adjust_authoring_duration(&mut self, authoring_duration: Duration) -> Option<Duration> {
let Ok(slot_duration) = crate::slot_duration(&*self.client) else {
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
return None;
};
let next_block = self.time_until_next_block(slot_duration);
let Some(next_slot_change) = self.time_until_next_slot_change(slot_duration) else {
tracing::error!(
target: LOG_TARGET,
"Failed to compute time until next slot change. Using unadjusted authoring duration."
);
return Some(authoring_duration);
};
// Check if authors at current and next slots are different
let current_slot = self.last_reported_slot.unwrap_or(next_block.1);
let different_authors = self.check_different_slot_authors(current_slot, next_slot_change.1);
adjust_authoring_duration(
authoring_duration,
next_block,
next_slot_change,
different_authors,
)
}
/// Returns a future that resolves when the next block production should be attempted.
pub async fn wait_until_next_slot(&mut self) -> Result<(), ()> {
let slot_duration = match crate::slot_duration(&*self.client) {
Ok(d) => d,
Err(error) => {
tracing::error!(target: LOG_TARGET, %error, "Failed to fetch slot duration from runtime.");
return Err(());
},
};
let (time_until_next_attempt, mut next_aura_slot) =
self.time_until_next_block(slot_duration);
tracing::trace!(
target: LOG_TARGET,
?time_until_next_attempt,
aura_slot = ?next_aura_slot,
last_reported = ?self.last_reported_slot,
"Determined next block production opportunity."
);
match self.last_reported_slot {
// If we already reported a slot, we don't want to skip a slot. But we also don't want
// to go through all the slots if a node was halted for some reason.
Some(ls) if ls + 1 < next_aura_slot && next_aura_slot <= ls + 3 => {
next_aura_slot = ls + 1u64;
},
None | Some(_) => {
tracing::trace!(target: LOG_TARGET, ?time_until_next_attempt, "Sleeping until the next slot.");
tokio::time::sleep(time_until_next_attempt).await;
},
}
tracing::debug!(
target: LOG_TARGET,
?slot_duration,
aura_slot = ?next_aura_slot,
"New block production opportunity."
);
self.last_reported_slot = Some(next_aura_slot);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use pezsc_consensus_aura::SlotDuration;
const RELAY_CHAIN_SLOT_DURATION: u64 = 6000;
#[rstest]
// Test that different now timestamps have correct impact
// ||||
#[case(6000, Some(1), 1000, 0, 5000)]
#[case(6000, Some(1), 0, 0, 6000)]
#[case(6000, Some(1), 6000, 0, 6000)]
#[case(6000, Some(0), 6000, 0, 6000)]
// Test that `None` core defaults to 1
// ||||
#[case(6000, None, 1000, 0, 5000)]
#[case(6000, None, 0, 0, 6000)]
#[case(6000, None, 6000, 0, 6000)]
// Test that offset affects the current time correctly
// ||||
#[case(6000, Some(1), 1000, 1000, 6000)]
#[case(6000, Some(1), 12000, 2000, 2000)]
#[case(6000, Some(1), 12000, 6000, 6000)]
#[case(6000, Some(1), 12000, 7000, 1000)]
// Test that number of cores affects the block production interval
// |||||||
#[case(6000, Some(3), 12000, 0, 2000)]
#[case(6000, Some(2), 12000, 0, 3000)]
#[case(6000, Some(3), 11999, 0, 1)]
// High core count
// ||||||||
#[case(6000, Some(12), 0, 0, 500)]
/// Test that the minimum block interval is respected
/// at high core counts.
/// |||||||||
#[case(6000, Some(100), 0, 0, 500)]
// Test that slot_duration works correctly
// ||||
#[case(2000, Some(1), 1000, 0, 1000)]
#[case(2000, Some(1), 3000, 0, 1000)]
#[case(2000, Some(1), 10000, 0, 2000)]
#[case(2000, Some(2), 1000, 0, 1000)]
// Cores are ignored if relay_slot_duration != para_slot_duration
// |||||||
#[case(2000, Some(3), 3000, 0, 1000)]
// For long slot durations, we should still check
// every relay chain block for the slot.
// |||||
#[case(12000, None, 0, 0, 6000)]
#[case(12000, None, 6100, 0, 5900)]
#[case(12000, None, 6000, 2000, 2000)]
#[case(12000, Some(2), 6000, 0, 3000)]
#[case(12000, Some(3), 6000, 0, 2000)]
#[case(12000, Some(3), 8100, 0, 1900)]
fn test_get_next_slot(
#[case] para_slot_millis: u64,
#[case] core_count: Option<u32>,
#[case] time_now: u64,
#[case] offset_millis: u64,
#[case] expected_wait_duration: u128,
) {
let para_slot_duration = SlotDuration::from_millis(para_slot_millis); // 6 second slots
let relay_slot_duration = Duration::from_millis(RELAY_CHAIN_SLOT_DURATION);
let time_now = Duration::from_millis(time_now); // 1 second passed
let offset = Duration::from_millis(offset_millis);
let (wait_duration, _) = compute_next_wake_up_time(
para_slot_duration,
relay_slot_duration,
core_count,
time_now,
offset,
);
assert_eq!(wait_duration.as_millis(), expected_wait_duration, "Wait time mismatch.");
// Should wait 5 seconds
}
#[rstest]
// Basic slot change scenarios
#[case(6000, 0, 0, Slot::from(0), 6000, Slot::from(1))]
#[case(6000, 1000, 0, Slot::from(0), 5000, Slot::from(1))]
#[case(6000, 6000, 0, Slot::from(1), 6000, Slot::from(2))]
#[case(6000, 12000, 0, Slot::from(2), 6000, Slot::from(3))]
// Test with offset
#[case(6000, 1000, 1000, Slot::from(0), 6000, Slot::from(1))]
#[case(6000, 2000, 1000, Slot::from(0), 5000, Slot::from(1))]
#[case(6000, 6000, 3000, Slot::from(0), 3000, Slot::from(1))]
// Different slot durations
#[case(3000, 1000, 0, Slot::from(0), 2000, Slot::from(1))]
#[case(3000, 3000, 0, Slot::from(1), 3000, Slot::from(2))]
#[case(12000, 6000, 0, Slot::from(0), 6000, Slot::from(1))]
#[case(12000, 12000, 0, Slot::from(1), 12000, Slot::from(2))]
// Edge cases - at slot boundary
#[case(6000, 5999, 0, Slot::from(0), 1, Slot::from(1))]
#[case(6000, 11999, 0, Slot::from(1), 1, Slot::from(2))]
fn test_compute_time_until_next_slot_change(
#[case] para_slot_millis: u64,
#[case] time_now: u64,
#[case] offset_millis: u64,
#[case] last_reported_slot: Slot,
#[case] expected_duration: u128,
#[case] expected_next_slot: Slot,
) {
let para_slot_duration = SlotDuration::from_millis(para_slot_millis);
let time_now = Duration::from_millis(time_now);
let offset = Duration::from_millis(offset_millis);
let result = compute_time_until_next_slot_change(
para_slot_duration,
time_now,
offset,
last_reported_slot,
);
assert!(result.is_some(), "Expected result to be Some");
let (duration, next_slot) = result.unwrap();
assert_eq!(duration.as_millis(), expected_duration, "Duration mismatch");
assert_eq!(next_slot, expected_next_slot, "Next slot mismatch");
}
#[rstest]
// Various scenarios for 2s block production adjustment.
#[case::blocks_2s_fits_next_block(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(2000), Slot::from(1)), // Next block
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(2000)), // Expected
)]
#[case::blocks_2s_closer_next_slot(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1950), Slot::from(1)), // Next block
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(1950)), // Expected
)]
#[case::blocks_2s_closer_next_slot_bigger(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1500), Slot::from(1)), // Next block
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(1500)), // Expected
)]
#[case::blocks_2s_reduce_by_1s(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(2000), Slot::from(1)), // Next block
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(1000)), // Expected
)]
#[case::blocks_2s_reduce_by_1s_plus_offset(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1950), Slot::from(1)), // Next block
(Duration::from_millis(1950), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(950)), // Expected
)]
#[case::blocks_2s_reduce_to_minimum(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1400), Slot::from(1)), // Next block
(Duration::from_millis(1400), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(400)), // Expected
)]
#[case::blocks_2s_reduce_below_minimum(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1300), Slot::from(1)), // Next block
(Duration::from_millis(1300), Slot::from(2)), // Next slot change
true, // Different authors
None, // Expected to reduce below minimum
)]
#[case::blocks_2s_same_author(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1400), Slot::from(1)), // Next block
(Duration::from_millis(1400), Slot::from(2)), // Next slot change
false, // Different authors
Some(Duration::from_millis(1400)), // Expected no adjustment for last second.
)]
// Various scenarios for 500ms block production adjustment.
#[case::blocks_500ms_fits_next_block(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(500), Slot::from(1)), // Next block
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(500)), // Expected
)]
#[case::blocks_500ms_closer_next_slot(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(450), Slot::from(1)), // Next block
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(450)), // Expected
)]
#[case::blocks_500ms_closer_next_slot_bigger(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(400), Slot::from(1)), // Next block
(Duration::from_millis(1500), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(400)), // Expected
)]
#[case::blocks_500ms_reduce_by_1s(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(500), Slot::from(1)), // Next block
(Duration::from_millis(1000), Slot::from(2)), // Next slot change
true, // Different authors
None, // Expected
)]
#[case::blocks_500ms_reduce_by_1s_closer(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(500), Slot::from(1)), // Next block
(Duration::from_millis(500), Slot::from(2)), // Next slot change
true, // Different authors
None, // Expected
)]
// If we are producing with 1 collator for 500ms authoring duration,
// we must produce the last two slots and ignore the 1s adjustment.
#[case::blocks_500ms_same_author(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(410), Slot::from(1)), // Next block
(Duration::from_millis(1000), Slot::from(2)), // Next slot change
false, // Different authors
Some(Duration::from_millis(410)), // Expected no adjustment for last second.
)]
#[case::blocks_500ms_same_author_closer(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(400), Slot::from(1)), // Next block
(Duration::from_millis(400), Slot::from(2)), // Next slot change
false, // Different authors
Some(Duration::from_millis(400)), // Expected no adjustment for last second.
)]
fn test_adjust_authoring_duration(
#[case] authoring_duration: Duration,
#[case] next_block: (Duration, Slot),
#[case] next_slot_change: (Duration, Slot),
#[case] different_authors: bool,
#[case] expected: Option<Duration>,
) {
pezsp_tracing::init_for_tests();
let result = adjust_authoring_duration(
authoring_duration,
next_block,
next_slot_change,
different_authors,
);
tracing::debug!("Adjusted authoring duration: {:?}", result);
assert_eq!(result, expected);
}
}
@@ -0,0 +1,730 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use super::{
block_builder_task::{determine_core, offset_relay_parent_find_descendants},
relay_chain_data_cache::{RelayChainData, RelayChainDataCache},
};
use async_trait::async_trait;
use codec::Encode;
use cumulus_primitives_core::{ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem};
use cumulus_relay_chain_interface::*;
use futures::Stream;
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
use pezkuwi_primitives::{
CandidateEvent, CommittedCandidateReceiptV2, CoreIndex, Hash as RelayHash,
Header as RelayHeader, Id as ParaId,
};
use rstest::rstest;
use pezsc_consensus_babe::{
AuthorityId, ConsensusLog as BabeConsensusLog, NextEpochDescriptor, BABE_ENGINE_ID,
};
use pezsp_core::sr25519;
use pezsp_runtime::{generic::BlockId, testing::Header as TestHeader, traits::Header};
use pezsp_version::RuntimeVersion;
use std::{
collections::{BTreeMap, HashMap, VecDeque},
pin::Pin,
};
#[tokio::test]
async fn offset_test_zero_offset() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 0).await;
assert!(result.is_ok());
let data = result.unwrap().unwrap();
assert_eq!(data.descendants_len(), 0);
assert_eq!(data.relay_parent().hash(), best_hash);
assert!(data.into_inherent_descendant_list().is_empty());
}
#[tokio::test]
async fn offset_test_two_offset() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 2).await;
assert!(result.is_ok());
let data = result.unwrap().unwrap();
assert_eq!(data.descendants_len(), 2);
assert_eq!(*data.relay_parent().number(), 98);
let descendant_list = data.into_inherent_descendant_list();
assert_eq!(descendant_list.len(), 3);
assert_eq!(*descendant_list.first().unwrap().number(), 98);
assert_eq!(*descendant_list.last().unwrap().number(), 100);
}
#[tokio::test]
async fn offset_test_five_offset() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 5).await;
assert!(result.is_ok());
let data = result.unwrap().unwrap();
assert_eq!(data.descendants_len(), 5);
assert_eq!(*data.relay_parent().number(), 95);
let descendant_list = data.into_inherent_descendant_list();
assert_eq!(descendant_list.len(), 6);
assert_eq!(*descendant_list.first().unwrap().number(), 95);
assert_eq!(*descendant_list.last().unwrap().number(), 100);
}
#[tokio::test]
async fn offset_test_too_long() {
let (headers, _best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, _best_hash, 200).await;
assert!(result.is_err());
let result = offset_relay_parent_find_descendants(&mut cache, _best_hash, 101).await;
assert!(result.is_err());
}
#[derive(PartialEq)]
enum HasEpochChange {
Yes,
No,
}
#[rstest]
#[case::in_best(
&[HasEpochChange::No, HasEpochChange::No, HasEpochChange::Yes],
)]
#[case::in_first_ancestor(
&[HasEpochChange::No, HasEpochChange::Yes, HasEpochChange::No],
)]
#[case::in_second_ancestor(
&[HasEpochChange::Yes, HasEpochChange::No, HasEpochChange::No],
)]
#[tokio::test]
async fn offset_returns_none_when_epoch_change_encountered(#[case] flags: &[HasEpochChange]) {
let (headers, best_hash) = build_headers_with_epoch_flags(flags);
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 3).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn determine_core_new_relay_parent() {
let (headers, _best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: Default::default(),
number: 100,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a test para parent header at block 0 (genesis)
let para_parent = TestHeader::new_from_number(0);
// Setup claim queue data for the cache
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1)]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
let core = result.unwrap();
let core = core.unwrap();
assert_eq!(core.core_selector(), CoreSelector(0));
assert_eq!(core.core_index(), CoreIndex(0));
assert_eq!(core.total_cores(), 2);
}
#[tokio::test]
async fn determine_core_with_core_info() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header with core info in digest
let core_info = CoreInfo {
selector: CoreSelector(0),
claim_queue_offset: ClaimQueueOffset(0),
number_of_cores: 3.into(),
};
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(CumulusDigestItem::CoreInfo(core_info).to_digest_item());
// Add relay parent storage root to make it a non-new relay parent
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data for the cache
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(core)) => {
assert_eq!(core.core_selector(), CoreSelector(1)); // Should be next selector (0 + 1)
assert_eq!(core.core_index(), CoreIndex(1));
assert_eq!(core.total_cores(), 3);
},
Ok(None) => panic!("Expected Some core, got None"),
Err(()) => panic!("determine_core returned error"),
}
}
#[tokio::test]
async fn determine_core_no_cores_available() {
let (headers, _best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: Default::default(),
number: 100,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a test para parent header at block 0 (genesis)
let para_parent = TestHeader::new_from_number(0);
// Setup empty claim queue
cache.set_test_data(relay_parent.clone(), vec![]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
let core = result.unwrap();
assert!(core.is_none());
}
#[tokio::test]
async fn determine_core_selector_overflow() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
let core_info = CoreInfo {
selector: CoreSelector(1),
claim_queue_offset: ClaimQueueOffset(0),
number_of_cores: 2.into(),
};
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(CumulusDigestItem::CoreInfo(core_info).to_digest_item());
// Add relay parent storage root to make it a non-new relay parent
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue with only 2 cores
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1)]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
let core = result.unwrap();
assert!(core.is_none()); // Should return None when selector overflows
}
#[tokio::test]
async fn determine_core_uses_last_claimed_core_selector() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header without core info in digest (non-genesis)
// Need to add relay parent storage root to digest to make it a non-new relay parent
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data with last_claimed_core_selector set to 1
cache.set_test_data_with_last_selector(
relay_parent.clone(),
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
Some(CoreSelector(1)),
);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(core)) => {
// Should use last_claimed_core_selector (1) + 1 = 2
assert_eq!(core.core_selector(), CoreSelector(2));
assert_eq!(core.core_index(), CoreIndex(2));
assert_eq!(core.total_cores(), 3);
},
Ok(None) => panic!("Expected Some core, got None"),
Err(()) => panic!("determine_core returned error"),
}
}
#[tokio::test]
async fn determine_core_uses_last_claimed_core_selector_wraps_around() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header without core info in digest (non-genesis)
// Need to add relay parent storage root to digest to make it a non-new relay parent
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data with last_claimed_core_selector set to 2 (last index)
// Next selector should wrap around to out of bounds and return None
cache.set_test_data_with_last_selector(
relay_parent.clone(),
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
Some(CoreSelector(2)),
);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(_)) => panic!("Expected None due to selector overflow"),
Ok(None) => {
// This is expected - selector 2 + 1 = 3, but only cores 0,1,2 available
},
Err(()) => panic!("determine_core returned error"),
}
}
#[tokio::test]
async fn determine_core_no_last_claimed_core_selector() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header without core info in digest (non-genesis)
// Need to add relay parent storage root to digest to make it a non-new relay parent
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data with no last_claimed_core_selector (None)
cache.set_test_data_with_last_selector(
relay_parent.clone(),
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
None,
);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(core)) => {
// Should start from selector 0 + 1 = 1 when no last selector
assert_eq!(core.core_selector(), CoreSelector(1));
assert_eq!(core.core_index(), CoreIndex(1));
assert_eq!(core.total_cores(), 3);
},
Ok(None) => panic!("Expected Some core, got None"),
Err(()) => panic!("determine_core returned error"),
}
}
#[derive(Clone)]
struct TestRelayClient {
headers: HashMap<RelayHash, RelayHeader>,
}
impl TestRelayClient {
fn new(headers: HashMap<RelayHash, RelayHeader>) -> Self {
Self { headers }
}
}
#[async_trait]
impl RelayChainInterface for TestRelayClient {
async fn validators(&self, _: RelayHash) -> RelayChainResult<Vec<ValidatorId>> {
unimplemented!("Not needed for test")
}
async fn best_block_hash(&self) -> RelayChainResult<RelayHash> {
unimplemented!("Not needed for test")
}
async fn finalized_block_hash(&self) -> RelayChainResult<RelayHash> {
unimplemented!("Not needed for test")
}
async fn retrieve_dmq_contents(
&self,
_: ParaId,
_: RelayHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
unimplemented!("Not needed for test")
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
_: ParaId,
_: RelayHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
unimplemented!("Not needed for test")
}
async fn persisted_validation_data(
&self,
_: RelayHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
use cumulus_primitives_core::PersistedValidationData;
Ok(Some(PersistedValidationData {
parent_head: Default::default(),
relay_parent_number: 100,
relay_parent_storage_root: Default::default(),
max_pov_size: 1024 * 1024,
}))
}
async fn validation_code_hash(
&self,
_: RelayHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
unimplemented!("Not needed for test")
}
async fn candidate_pending_availability(
&self,
_: RelayHash,
_: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceiptV2>> {
unimplemented!("Not needed for test")
}
async fn candidates_pending_availability(
&self,
_: RelayHash,
_: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceiptV2>> {
unimplemented!("Not needed for test")
}
async fn session_index_for_child(&self, _: RelayHash) -> RelayChainResult<SessionIndex> {
unimplemented!("Not needed for test")
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
unimplemented!("Not needed for test")
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
unimplemented!("Not needed for test")
}
async fn get_storage_by_key(
&self,
_: RelayHash,
_: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
unimplemented!("Not needed for test")
}
async fn prove_read(
&self,
_: RelayHash,
_: &Vec<Vec<u8>>,
) -> RelayChainResult<pezsc_client_api::StorageProof> {
unimplemented!("Not needed for test")
}
async fn wait_for_block(&self, _: RelayHash) -> RelayChainResult<()> {
unimplemented!("Not needed for test")
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn header(
&self,
block_id: BlockId<pezkuwi_primitives::Block>,
) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(_) => unimplemented!("Not needed for test"),
};
let header = self.headers.get(&hash);
Ok(header.cloned())
}
async fn availability_cores(
&self,
_relay_parent: RelayHash,
) -> RelayChainResult<Vec<CoreState<RelayHash, BlockNumber>>> {
unimplemented!("Not needed for test");
}
async fn version(&self, _: RelayHash) -> RelayChainResult<RuntimeVersion> {
unimplemented!("Not needed for test");
}
async fn claim_queue(
&self,
_: RelayHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
// Return empty claim queue for offset tests
Ok(BTreeMap::new())
}
async fn call_runtime_api(
&self,
_method_name: &'static str,
_hash: RelayHash,
_payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
unimplemented!("Not needed for test")
}
async fn scheduling_lookahead(&self, _: RelayHash) -> RelayChainResult<u32> {
unimplemented!("Not needed for test")
}
async fn candidate_events(&self, _: RelayHash) -> RelayChainResult<Vec<CandidateEvent>> {
unimplemented!("Not needed for test")
}
}
/// Build a consecutive set of relay headers whose digest entries optionally carry a BABE
/// epoch-change marker, returning the underlying map and the hash of the last header.
fn build_headers_with_epoch_flags(
flags: &[HasEpochChange],
) -> (HashMap<RelayHash, RelayHeader>, RelayHash) {
let mut headers = HashMap::new();
let mut parent_hash = RelayHash::default();
let mut last_hash = RelayHash::default();
for (index, has_epoch_change) in flags.iter().enumerate() {
let digest = if *has_epoch_change == HasEpochChange::Yes {
babe_epoch_change_digest()
} else {
Default::default()
};
let header = RelayHeader {
parent_hash,
number: (index as u32 + 1),
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
let hash = header.hash();
headers.insert(hash, header);
parent_hash = hash;
last_hash = hash;
}
(headers, last_hash)
}
/// Create a digest containing a single BABE `NextEpochData` item for use in tests.
fn babe_epoch_change_digest() -> pezsp_runtime::generic::Digest {
let mut digest = pezsp_runtime::generic::Digest::default();
let authority_id = AuthorityId::from(sr25519::Public::from_raw([1u8; 32]));
let next_epoch =
NextEpochDescriptor { authorities: vec![(authority_id, 1u64)], randomness: [0u8; 32] };
let log = BabeConsensusLog::NextEpochData(next_epoch);
digest.push(pezsp_runtime::generic::DigestItem::Consensus(BABE_ENGINE_ID, log.encode()));
digest
}
fn create_header_chain() -> (HashMap<RelayHash, RelayHeader>, RelayHash) {
let mut headers = HashMap::new();
let mut current_parent = None;
let mut header_hash = RelayHash::repeat_byte(0x1);
// Create chain from highest to lowest number
for number in 1..=100 {
let mut header = RelayHeader {
parent_hash: Default::default(),
number,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
if let Some(hash) = current_parent {
header.parent_hash = hash;
}
header_hash = header.hash();
// Store header and update parent for next iteration
headers.insert(header_hash, header.clone());
current_parent = Some(header_hash);
}
(headers, header_hash)
}
// Test extension for RelayChainDataCache
impl RelayChainDataCache<TestRelayClient> {
fn set_test_data(&mut self, relay_parent_header: RelayHeader, cores: Vec<CoreIndex>) {
self.set_test_data_with_last_selector(relay_parent_header, cores, None);
}
fn set_test_data_with_last_selector(
&mut self,
relay_parent_header: RelayHeader,
cores: Vec<CoreIndex>,
last_claimed_core_selector: Option<CoreSelector>,
) {
let relay_parent_hash = relay_parent_header.hash();
let mut claim_queue = BTreeMap::new();
for core_index in cores {
claim_queue.insert(core_index, [ParaId::from(1)].into());
}
let claim_queue_snapshot = ClaimQueueSnapshot::from(claim_queue);
let data = RelayChainData {
relay_parent_header,
claim_queue: claim_queue_snapshot,
max_pov_size: 1024 * 1024,
last_claimed_core_selector,
};
self.insert_test_data(relay_parent_hash, data);
}
}
@@ -0,0 +1,392 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
/// An import queue which provides some equivocation resistance with lenient trait bounds.
///
/// Equivocation resistance in general is a hard problem, as different nodes in the network
/// may see equivocations in a different order, and therefore may not agree on which blocks
/// should be thrown out and which ones should be kept.
use codec::Codec;
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
use cumulus_primitives_core::{CumulusDigestItem, RelayBlockIdentifier};
use parking_lot::Mutex;
use pezkuwi_primitives::Hash as RHash;
use pezsc_consensus::{
import_queue::{BasicQueue, Verifier as VerifierT},
BlockImport, BlockImportParams, ForkChoiceStrategy,
};
use pezsc_consensus_aura::{standalone as aura_internal, AuthoritiesTracker};
use pezsc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE};
use schnellru::{ByLength, LruMap};
use pezsp_api::ProvideRuntimeApi;
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
use pezsp_consensus::{error::Error as ConsensusError, BlockOrigin};
use pezsp_consensus_aura::{AuraApi, Slot, SlotDuration};
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use std::{fmt::Debug, sync::Arc};
const LRU_WINDOW: u32 = 512;
const EQUIVOCATION_LIMIT: usize = 16;
struct NaiveEquivocationDefender<N> {
/// We distinguish blocks by `(Slot, BlockNumber, RelayParent)`.
cache: LruMap<(u64, N, RHash), usize>,
}
impl<N: std::hash::Hash + PartialEq> Default for NaiveEquivocationDefender<N> {
fn default() -> Self {
NaiveEquivocationDefender { cache: LruMap::new(ByLength::new(LRU_WINDOW)) }
}
}
impl<N: std::hash::Hash + PartialEq> NaiveEquivocationDefender<N> {
// Returns `true` if equivocation is beyond the limit.
fn insert_and_check(&mut self, slot: Slot, block_number: N, relay_chain_parent: RHash) -> bool {
let val = self
.cache
.get_or_insert((*slot, block_number, relay_chain_parent), || 0)
.expect("insertion with ByLength limiter always succeeds; qed");
if *val == EQUIVOCATION_LIMIT {
true
} else {
*val += 1;
false
}
}
}
/// A teyrchain block import verifier that checks for equivocation limits within each slot.
pub struct Verifier<P: Pair, Client, Block: BlockT, CIDP> {
client: Arc<Client>,
create_inherent_data_providers: CIDP,
defender: Mutex<NaiveEquivocationDefender<NumberFor<Block>>>,
telemetry: Option<TelemetryHandle>,
// Unused for now. Will be plugged in with a later PR.
_authorities_tracker: AuthoritiesTracker<P, Block, Client>,
}
impl<P, Client, Block, CIDP> Verifier<P, Client, Block, CIDP>
where
P: Pair,
P::Signature: Codec,
P::Public: Codec + Debug,
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
CIDP: CreateInherentDataProviders<Block, ()>,
{
/// Creates a new Verifier instance for handling teyrchain block import verification in Aura
/// consensus.
pub fn new(
client: Arc<Client>,
inherent_data_provider: CIDP,
telemetry: Option<TelemetryHandle>,
) -> Self {
Self {
client: client.clone(),
create_inherent_data_providers: inherent_data_provider,
defender: Mutex::new(NaiveEquivocationDefender::default()),
telemetry,
_authorities_tracker: AuthoritiesTracker::new(client),
}
}
}
#[async_trait::async_trait]
impl<P, Client, Block, CIDP> VerifierT<Block> for Verifier<P, Client, Block, CIDP>
where
P: Pair,
P::Signature: Codec,
P::Public: Codec + Debug,
Block: BlockT,
Client: HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ ProvideRuntimeApi<Block>
+ Send
+ Sync,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
CIDP: CreateInherentDataProviders<Block, ()>,
{
async fn verify(
&self,
mut block_params: BlockImportParams<Block>,
) -> Result<BlockImportParams<Block>, String> {
// Skip checks that include execution, if being told so, or when importing only state.
//
// This is done for example when gap syncing and it is expected that the block after the gap
// was checked/chosen properly, e.g. by warp syncing to this block using a finality proof.
if block_params.state_action.skip_execution_checks() || block_params.with_state() {
block_params.fork_choice = Some(ForkChoiceStrategy::Custom(block_params.with_state()));
return Ok(block_params);
}
let post_hash = block_params.header.hash();
let parent_hash = *block_params.header.parent_hash();
// check seal and update pre-hash/post-hash
{
let authorities = aura_internal::fetch_authorities(self.client.as_ref(), parent_hash)
.map_err(|e| {
format!("Could not fetch authorities at {:?}: {}", parent_hash, e)
})?;
let slot_duration = self
.client
.runtime_api()
.slot_duration(parent_hash)
.map_err(|e| e.to_string())?;
let slot_now = slot_now(slot_duration);
let res = aura_internal::check_header_slot_and_seal::<Block, P>(
slot_now,
block_params.header,
&authorities,
);
match res {
Ok((pre_header, slot, seal_digest)) => {
telemetry!(
self.telemetry;
CONSENSUS_TRACE;
"aura.checked_and_importing";
"pre_header" => ?pre_header,
);
// We need some kind of identifier for the relay parent, in the worst case we
// take the all `0` hash.
let relay_parent =
match CumulusDigestItem::find_relay_block_identifier(pre_header.digest()) {
None => Default::default(),
Some(RelayBlockIdentifier::ByHash(h)) |
Some(RelayBlockIdentifier::ByStorageRoot {
storage_root: h, ..
}) => h,
};
block_params.header = pre_header;
block_params.post_digests.push(seal_digest);
block_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
block_params.post_hash = Some(post_hash);
// Check for and reject egregious amounts of equivocations.
//
// If the `origin` is `ConsensusBroadcast`, we ignore the result of the
// equivocation check. This `origin` is for example used by pov-recovery.
if self.defender.lock().insert_and_check(
slot,
*block_params.header.number(),
relay_parent,
) && !matches!(block_params.origin, BlockOrigin::ConsensusBroadcast)
{
return Err(format!(
"Rejecting block {:?} due to excessive equivocations at slot",
post_hash,
));
}
},
Err(aura_internal::SealVerificationError::Deferred(hdr, slot)) => {
telemetry!(
self.telemetry;
CONSENSUS_DEBUG;
"aura.header_too_far_in_future";
"hash" => ?post_hash,
"a" => ?hdr,
"b" => ?slot,
);
return Err(format!(
"Rejecting block ({:?}) from future slot {:?}",
post_hash, slot
));
},
Err(e) =>
return Err(format!(
"Rejecting block ({:?}) with invalid seal ({:?})",
post_hash, e
)),
}
}
// Check inherents.
if let Some(body) = block_params.body.clone() {
let block = Block::new(block_params.header.clone(), body);
let create_inherent_data_providers = self
.create_inherent_data_providers
.create_inherent_data_providers(parent_hash, ())
.await
.map_err(|e| format!("Could not create inherent data {:?}", e))?;
pezsp_block_builder::check_inherents(
self.client.clone(),
parent_hash,
block,
&create_inherent_data_providers,
)
.await
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
}
Ok(block_params)
}
}
fn slot_now(slot_duration: SlotDuration) -> Slot {
let timestamp = pezsp_timestamp::InherentDataProvider::from_system_time().timestamp();
Slot::from_timestamp(timestamp, slot_duration)
}
/// Start an import queue for a Pezcumulus node which checks blocks' seals and inherent data.
///
/// Pass in only inherent data providers which don't include aura or teyrchain consensus inherents,
/// e.g. things like timestamp and custom inherents for the runtime.
///
/// The others are generated explicitly internally.
///
/// This should only be used for runtimes where the runtime does not check all inherents and
/// seals in `execute_block` (see <https://github.com/pezkuwichain/kurdistan-sdk/issues/91>)
pub fn fully_verifying_import_queue<P, Client, Block: BlockT, I, CIDP>(
client: Arc<Client>,
block_import: I,
create_inherent_data_providers: CIDP,
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
registry: Option<&prometheus_endpoint::Registry>,
telemetry: Option<TelemetryHandle>,
) -> BasicQueue<Block>
where
P: Pair + 'static,
P::Signature: Codec,
P::Public: Codec + Debug,
I: BlockImport<Block, Error = ConsensusError>
+ TeyrchainBlockImportMarker
+ Send
+ Sync
+ 'static,
Client: HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ ProvideRuntimeApi<Block>
+ Send
+ Sync
+ 'static,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
{
let verifier = Verifier::<P, _, _, _> {
client: client.clone(),
create_inherent_data_providers,
defender: Mutex::new(NaiveEquivocationDefender::default()),
telemetry,
_authorities_tracker: AuthoritiesTracker::new(client.clone()),
};
BasicQueue::new(verifier, Box::new(block_import), None, spawner, registry)
}
#[cfg(test)]
mod test {
use super::*;
use codec::Encode;
use cumulus_test_client::{
runtime::Block, seal_block, Client, InitBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use futures::FutureExt;
use pezkuwi_primitives::{HeadData, PersistedValidationData};
use pezsc_client_api::HeaderBackend;
use pezsp_consensus_aura::sr25519;
use pezsp_tracing::try_init_simple;
use std::{collections::HashSet, sync::Arc};
#[test]
fn import_equivocated_blocks_from_recovery() {
try_init_simple();
let client = Arc::new(TestClientBuilder::default().build());
let verifier = Verifier::<sr25519::AuthorityPair, Client, Block, _> {
client: client.clone(),
create_inherent_data_providers: |_, _| async move {
Ok(pezsp_timestamp::InherentDataProvider::from_system_time())
},
defender: Mutex::new(NaiveEquivocationDefender::default()),
telemetry: None,
_authorities_tracker: AuthoritiesTracker::new(client.clone()),
};
let genesis = client.info().best_hash;
let mut sproof = RelayStateSproofBuilder::default();
sproof.included_para_head = Some(HeadData(client.header(genesis).unwrap().encode()));
sproof.para_id = cumulus_test_client::runtime::TEYRCHAIN_ID.into();
let validation_data = PersistedValidationData {
relay_parent_number: 1,
parent_head: client.header(genesis).unwrap().encode().into(),
..Default::default()
};
let block_builder = client.init_block_builder(Some(validation_data), sproof);
let block = block_builder.block_builder.build().unwrap();
let mut blocks = Vec::new();
for _ in 0..EQUIVOCATION_LIMIT + 1 {
blocks.push(seal_block(block.block.clone(), &client))
}
// sr25519 should generate a different signature every time you sign something and thus, all
// blocks get a different hash (even if they are the same block).
assert_eq!(blocks.iter().map(|b| b.hash()).collect::<HashSet<_>>().len(), blocks.len());
blocks.iter().take(EQUIVOCATION_LIMIT).for_each(|block| {
let mut params =
BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header().clone());
params.body = Some(block.extrinsics().to_vec());
verifier.verify(params).now_or_never().unwrap().unwrap();
});
// Now let's try some previously verified block and a block we have not verified yet.
//
// Verify should fail, because we are above the limit. However, when we change the origin to
// `ConsensusBroadcast`, it should work.
let extra_blocks =
vec![blocks[EQUIVOCATION_LIMIT / 2].clone(), blocks.last().unwrap().clone()];
extra_blocks.into_iter().for_each(|block| {
let mut params =
BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header().clone());
params.body = Some(block.extrinsics().to_vec());
assert!(verifier
.verify(params)
.now_or_never()
.unwrap()
.map(drop)
.unwrap_err()
.contains("excessive equivocations at slot"));
// When it comes from `pov-recovery`, we will accept it
let mut params =
BlockImportParams::new(BlockOrigin::ConsensusBroadcast, block.header().clone());
params.body = Some(block.extrinsics().to_vec());
assert!(verifier.verify(params).now_or_never().unwrap().is_ok());
});
}
}
@@ -0,0 +1,126 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Teyrchain specific wrapper for the AuRa import queue.
use codec::Codec;
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
use prometheus_endpoint::Registry;
use pezsc_client_api::{backend::AuxStore, BlockOf, UsageProvider};
use pezsc_consensus::{import_queue::DefaultImportQueue, BlockImport};
use pezsc_consensus_aura::{AuraVerifier, CompatibilityMode};
use pezsc_consensus_slots::InherentDataProviderExt;
use pezsc_telemetry::TelemetryHandle;
use pezsp_api::{ApiExt, ProvideRuntimeApi};
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
use pezsp_consensus::Error as ConsensusError;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_runtime::traits::Block as BlockT;
use std::{fmt::Debug, sync::Arc};
/// Parameters for [`import_queue`].
pub struct ImportQueueParams<'a, I, C, CIDP, S> {
/// The block import to use.
pub block_import: I,
/// The client to interact with the chain.
pub client: Arc<C>,
/// The inherent data providers, to create the inherent data.
pub create_inherent_data_providers: CIDP,
/// The spawner to spawn background tasks.
pub spawner: &'a S,
/// The prometheus registry.
pub registry: Option<&'a Registry>,
/// The telemetry handle.
pub telemetry: Option<TelemetryHandle>,
}
/// Start an import queue for the Aura consensus algorithm.
pub fn import_queue<P, Block, I, C, S, CIDP>(
ImportQueueParams {
block_import,
client,
create_inherent_data_providers,
spawner,
registry,
telemetry,
}: ImportQueueParams<'_, I, C, CIDP, S>,
) -> Result<DefaultImportQueue<Block>, pezsp_consensus::Error>
where
Block: BlockT,
C::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public> + ApiExt<Block>,
C: 'static
+ ProvideRuntimeApi<Block>
+ BlockOf
+ Send
+ Sync
+ AuxStore
+ UsageProvider<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
I: BlockImport<Block, Error = ConsensusError>
+ TeyrchainBlockImportMarker
+ Send
+ Sync
+ 'static,
P: Pair + 'static,
P::Public: Debug + Codec,
P::Signature: Codec,
S: pezsp_core::traits::SpawnEssentialNamed,
CIDP: CreateInherentDataProviders<Block, ()> + Sync + Send + 'static,
CIDP::InherentDataProviders: InherentDataProviderExt + Send + Sync,
{
pezsc_consensus_aura::import_queue::<P, _, _, _, _, _>(pezsc_consensus_aura::ImportQueueParams {
block_import,
justification_import: None,
client,
create_inherent_data_providers,
spawner,
registry,
check_for_equivocation: pezsc_consensus_aura::CheckForEquivocation::No,
telemetry,
compatibility_mode: CompatibilityMode::None,
})
}
/// Parameters of [`build_verifier`].
pub struct BuildVerifierParams<C, CIDP> {
/// The client to interact with the chain.
pub client: Arc<C>,
/// The inherent data providers, to create the inherent data.
pub create_inherent_data_providers: CIDP,
/// The telemetry handle.
pub telemetry: Option<TelemetryHandle>,
}
/// Build the [`AuraVerifier`].
pub fn build_verifier<P: Pair, C, CIDP, B: BlockT>(
BuildVerifierParams { client, create_inherent_data_providers, telemetry }: BuildVerifierParams<
C,
CIDP,
>,
) -> AuraVerifier<C, P, CIDP, B> {
pezsc_consensus_aura::build_verifier(pezsc_consensus_aura::BuildVerifierParams {
client,
create_inherent_data_providers,
telemetry,
check_for_equivocation: pezsc_consensus_aura::CheckForEquivocation::No,
compatibility_mode: CompatibilityMode::None,
})
}
@@ -0,0 +1,86 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! The AuRa consensus algorithm for teyrchains.
//!
//! This extends the Bizinikiwi provided AuRa consensus implementation to make it compatible for
//! teyrchains.
//!
//! For more information about AuRa, the Bizinikiwi crate should be checked.
use codec::Encode;
use cumulus_primitives_core::PersistedValidationData;
use cumulus_primitives_core::relay_chain::HeadData;
use pezkuwi_primitives::{BlockNumber as RBlockNumber, Hash as RHash};
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
use std::{fs, fs::File, path::PathBuf};
mod import_queue;
pub use import_queue::{build_verifier, import_queue, BuildVerifierParams, ImportQueueParams};
use pezkuwi_node_primitives::PoV;
pub use pezsc_consensus_aura::{
slot_duration, standalone::slot_duration_at, AuraVerifier, BuildAuraWorkerParams,
SlotProportion,
};
pub use pezsc_consensus_slots::InherentDataProviderExt;
pub mod collator;
pub mod collators;
pub mod equivocation_import_queue;
const LOG_TARGET: &str = "aura::pezcumulus";
/// Export the given `pov` to the file system at `path`.
///
/// The file will be named `block_hash_block_number.pov`.
///
/// The `parent_header`, `relay_parent_storage_root` and `relay_parent_number` will also be
/// stored in the file alongside the `pov`. This enables stateless validation of the `pov`.
pub(crate) fn export_pov_to_path<Block: BlockT>(
path: PathBuf,
pov: PoV,
block_hash: Block::Hash,
block_number: NumberFor<Block>,
parent_header: Block::Header,
relay_parent_storage_root: RHash,
relay_parent_number: RBlockNumber,
max_pov_size: u32,
) {
if let Err(error) = fs::create_dir_all(&path) {
tracing::error!(target: LOG_TARGET, %error, path = %path.display(), "Failed to create PoV export directory");
return;
}
let mut file = match File::create(path.join(format!("{block_hash:?}_{block_number}.pov"))) {
Ok(f) => f,
Err(error) => {
tracing::error!(target: LOG_TARGET, %error, "Failed to export PoV.");
return;
},
};
pov.encode_to(&mut file);
PersistedValidationData {
parent_head: HeadData(parent_header.encode()),
relay_parent_number,
relay_parent_storage_root,
max_pov_size,
}
.encode_to(&mut file);
}
@@ -0,0 +1,78 @@
[package]
name = "pezcumulus-client-consensus-common"
description = "Pezcumulus specific common consensus implementations"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-consensus-babe = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-slots = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-timestamp = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-pov-recovery = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
schnellru = { workspace = true }
[dev-dependencies]
futures-timer = { workspace = true }
tokio = { features = ["macros"], workspace = true }
# Bizinikiwi
pezsp-tracing = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-test-client = { workspace = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-pov-recovery/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-streams/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus-babe/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-slots/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-timestamp/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,77 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! (unstable) Composable utilities for constructing import queues for teyrchains.
//!
//! Unlike standalone chains, teyrchains have the requirement that all consensus logic
//! must be checked within the runtime. This property means that work which is normally
//! done in the import queue per-block, such as checking signatures, quorums, and whether
//! inherent extrinsics were constructed faithfully do not need to be done, per se.
//!
//! It may seem that it would be beneficial for the client to do these checks regardless,
//! but in practice this means that clients would just reject blocks which are _valid_ according
//! to their Teyrchain Validation Function, which is the ultimate source of consensus truth.
//!
//! However, teyrchain runtimes expose two different access points for executing blocks
//! in full nodes versus executing those blocks in the teyrchain validation environment.
//! At the time of writing, the inherent and consensus checks in most Pezcumulus runtimes
//! are only performed during teyrchain validation, not full node block execution.
//!
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/91> for details.
use pezsp_consensus::error::Error as ConsensusError;
use pezsp_runtime::traits::Block as BlockT;
use pezsc_consensus::{
block_import::{BlockImport, BlockImportParams},
import_queue::{BasicQueue, Verifier},
};
use crate::TeyrchainBlockImportMarker;
/// A [`Verifier`] for blocks which verifies absolutely nothing.
///
/// This should only be used when the runtime is responsible for checking block seals and inherents.
pub struct VerifyNothing;
#[async_trait::async_trait]
impl<Block: BlockT> Verifier<Block> for VerifyNothing {
async fn verify(
&self,
params: BlockImportParams<Block>,
) -> Result<BlockImportParams<Block>, String> {
Ok(params)
}
}
/// An import queue which does no verification.
///
/// This should only be used when the runtime is responsible for checking block seals and inherents.
pub fn verify_nothing_import_queue<Block: BlockT, I>(
block_import: I,
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
registry: Option<&prometheus_endpoint::Registry>,
) -> BasicQueue<Block>
where
I: BlockImport<Block, Error = ConsensusError>
+ TeyrchainBlockImportMarker
+ Send
+ Sync
+ 'static,
{
BasicQueue::new(VerifyNothing, Box::new(block_import), None, spawner, registry)
}
@@ -0,0 +1,407 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use pezsc_client_api::{blockchain::Backend as _, Backend, HeaderBackend as _};
use pezsp_blockchain::{HashAndNumber, HeaderMetadata, TreeRoute};
use pezsp_runtime::traits::{Block as BlockT, NumberFor, One, Saturating, UniqueSaturatedInto, Zero};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
const LOG_TARGET: &str = "level-monitor";
/// Value good enough to be used with teyrchains using the current backend implementation
/// that ships with Bizinikiwi. This value may change in the future.
pub const MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT: usize = 32;
// Counter threshold after which we are going to eventually cleanup our internal data.
const CLEANUP_THRESHOLD: u32 = 32;
/// Upper bound to the number of leaves allowed for each level of the blockchain.
///
/// If the limit is set and more leaves are detected on block import, then the older ones are
/// dropped to make space for the fresh blocks.
///
/// In environments where blocks confirmations from the relay chain may be "slow", then
/// setting an upper bound helps keeping the chain health by dropping old (presumably) stale
/// leaves and prevents discarding new blocks because we've reached the backend max value.
pub enum LevelLimit {
/// Limit set to [`MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT`].
Default,
/// No explicit limit, however a limit may be implicitly imposed by the backend implementation.
None,
/// Custom value.
Some(usize),
}
/// Support structure to constrain the number of leaves at each level.
pub struct LevelMonitor<Block: BlockT, BE> {
/// Max number of leaves for each level.
level_limit: usize,
/// Monotonic counter used to keep track of block freshness.
pub(crate) import_counter: NumberFor<Block>,
/// Map between blocks hashes and freshness.
pub(crate) freshness: HashMap<Block::Hash, NumberFor<Block>>,
/// Blockchain levels cache.
pub(crate) levels: HashMap<NumberFor<Block>, HashSet<Block::Hash>>,
/// Lower level number stored by the levels map.
lowest_level: NumberFor<Block>,
/// Backend reference to remove blocks on level saturation.
backend: Arc<BE>,
}
/// Contains information about the target scheduled for removal.
struct TargetInfo<Block: BlockT> {
/// Index of freshest leaf in the leaves array.
freshest_leaf_idx: usize,
/// Route from target to its freshest leaf.
freshest_route: TreeRoute<Block>,
}
impl<Block, BE> LevelMonitor<Block, BE>
where
Block: BlockT,
BE: Backend<Block>,
{
/// Instance a new monitor structure.
pub fn new(level_limit: usize, backend: Arc<BE>) -> Self {
let mut monitor = LevelMonitor {
level_limit,
import_counter: Zero::zero(),
freshness: HashMap::new(),
levels: HashMap::new(),
lowest_level: Zero::zero(),
backend,
};
monitor.restore();
monitor
}
/// Restore the structure using the backend.
///
/// Blocks freshness values are inferred from the height and not from the effective import
/// moment. This is a not accurate but "good-enough" best effort solution.
///
/// Level limits are not enforced during this phase.
fn restore(&mut self) {
let info = self.backend.blockchain().info();
log::debug!(
target: LOG_TARGET,
"Restoring chain level monitor from last finalized block: {} {}",
info.finalized_number,
info.finalized_hash
);
self.lowest_level = info.finalized_number;
self.import_counter = info.finalized_number;
for leaf in self.backend.blockchain().leaves().unwrap_or_default() {
let Ok(mut meta) = self.backend.blockchain().header_metadata(leaf) else {
log::debug!(
target: LOG_TARGET,
"Could not fetch header metadata for leaf: {leaf:?}",
);
continue;
};
self.import_counter = self.import_counter.max(meta.number);
// Populate the monitor until we don't hit an already imported branch
while !self.freshness.contains_key(&meta.hash) {
self.freshness.insert(meta.hash, meta.number);
self.levels.entry(meta.number).or_default().insert(meta.hash);
if meta.number <= self.lowest_level {
break;
}
meta = match self.backend.blockchain().header_metadata(meta.parent) {
Ok(m) => m,
Err(_) => {
// This can happen after we have warp synced a node.
log::debug!(
target: LOG_TARGET,
"Could not fetch header metadata for parent: {:?}",
meta.parent,
);
break;
},
}
}
}
log::debug!(
target: LOG_TARGET,
"Restored chain level monitor up to height {}",
self.import_counter
);
}
/// Check and enforce the limit bound at the given height.
///
/// In practice this will enforce the given height in having a number of blocks less than
/// the limit passed to the constructor.
///
/// If the given level is found to have a number of blocks greater than or equal the limit
/// then the limit is enforced by choosing one (or more) blocks to remove.
///
/// The removal strategy is driven by the block freshness.
///
/// A block freshness is determined by the most recent leaf freshness descending from the block
/// itself. In other words its freshness is equal to its more "fresh" descendant.
///
/// The least "fresh" blocks are eventually removed.
pub fn enforce_limit(&mut self, number: NumberFor<Block>) {
let level_len = self.levels.get(&number).map(|l| l.len()).unwrap_or_default();
if level_len < self.level_limit {
return;
}
// Sort leaves by freshness only once (less fresh first) and keep track of
// leaves that were invalidated on removal.
let mut leaves = self.backend.blockchain().leaves().unwrap_or_default();
leaves.sort_unstable_by(|a, b| self.freshness.get(a).cmp(&self.freshness.get(b)));
let mut invalidated_leaves = HashSet::new();
// This may not be the most efficient way to remove **multiple** entries, but is the easy
// one :-). Should be considered that in "normal" conditions the number of blocks to remove
// is 0 or 1, it is not worth to complicate the code too much. One condition that may
// trigger multiple removals (2+) is if we restart the node using an existing db and a
// smaller limit wrt the one previously used.
let remove_count = level_len - self.level_limit + 1;
log::debug!(
target: LOG_TARGET,
"Detected leaves overflow at height {number}, removing {remove_count} obsolete blocks",
);
(0..remove_count).all(|_| {
self.find_target(number, &leaves, &invalidated_leaves).map_or(false, |target| {
self.remove_target(target, number, &leaves, &mut invalidated_leaves);
true
})
});
}
// Helper function to find the best candidate to be removed.
//
// Given a set of blocks with height equal to `number` (potential candidates)
// 1. For each candidate fetch all the leaves that are descending from it.
// 2. Set the candidate freshness equal to the fresher of its descending leaves.
// 3. The target is set as the candidate that is less fresh.
//
// Input `leaves` are assumed to be already ordered by "freshness" (less fresh first).
//
// Returns the index of the target fresher leaf within `leaves` and the route from target to
// such leaf.
fn find_target(
&self,
number: NumberFor<Block>,
leaves: &[Block::Hash],
invalidated_leaves: &HashSet<usize>,
) -> Option<TargetInfo<Block>> {
let mut target_info: Option<TargetInfo<Block>> = None;
let blockchain = self.backend.blockchain();
let best_hash = blockchain.info().best_hash;
// Leaves that where already assigned to some node and thus can be skipped
// during the search.
let mut assigned_leaves = HashSet::new();
let level = self.levels.get(&number)?;
for blk_hash in level.iter().filter(|hash| **hash != best_hash) {
// Search for the fresher leaf information for this block
let candidate_info = leaves
.iter()
.enumerate()
.filter(|(leaf_idx, _)| {
!assigned_leaves.contains(leaf_idx) && !invalidated_leaves.contains(leaf_idx)
})
.rev()
.find_map(|(leaf_idx, leaf_hash)| {
if blk_hash == leaf_hash {
let entry = HashAndNumber { number, hash: *blk_hash };
TreeRoute::new(vec![entry], 0).ok().map(|freshest_route| TargetInfo {
freshest_leaf_idx: leaf_idx,
freshest_route,
})
} else {
match pezsp_blockchain::tree_route(blockchain, *blk_hash, *leaf_hash) {
Ok(route) if route.retracted().is_empty() => Some(TargetInfo {
freshest_leaf_idx: leaf_idx,
freshest_route: route,
}),
Err(err) => {
log::warn!(
target: LOG_TARGET,
"(Lookup) Unable getting route from {:?} to {:?}: {}",
blk_hash,
leaf_hash,
err,
);
None
},
_ => None,
}
}
});
let candidate_info = match candidate_info {
Some(candidate_info) => {
assigned_leaves.insert(candidate_info.freshest_leaf_idx);
candidate_info
},
None => {
// This should never happen
log::error!(
target: LOG_TARGET,
"Unable getting route to any leaf from {:?} (this is a bug)",
blk_hash,
);
continue;
},
};
// Found fresher leaf for this candidate.
// This candidate is set as the new target if:
// 1. its fresher leaf is less fresh than the previous target fresher leaf AND
// 2. best block is not in its route
let is_less_fresh = || {
target_info
.as_ref()
.map(|ti| candidate_info.freshest_leaf_idx < ti.freshest_leaf_idx)
.unwrap_or(true)
};
let not_contains_best = || {
candidate_info
.freshest_route
.enacted()
.iter()
.all(|entry| entry.hash != best_hash)
};
if is_less_fresh() && not_contains_best() {
let early_stop = candidate_info.freshest_leaf_idx == 0;
target_info = Some(candidate_info);
if early_stop {
// We will never find a candidate with an worst freshest leaf than this.
break;
}
}
}
target_info
}
// Remove the target block and all its descendants.
//
// Leaves should have already been ordered by "freshness" (less fresh first).
fn remove_target(
&mut self,
target: TargetInfo<Block>,
number: NumberFor<Block>,
leaves: &[Block::Hash],
invalidated_leaves: &mut HashSet<usize>,
) {
let mut remove_leaf = |number, hash| {
log::debug!(target: LOG_TARGET, "Removing block (@{}) {:?}", number, hash);
if let Err(err) = self.backend.remove_leaf_block(hash) {
log::debug!(target: LOG_TARGET, "Remove not possible for {}: {}", hash, err);
return false;
}
self.levels.get_mut(&number).map(|level| level.remove(&hash));
self.freshness.remove(&hash);
true
};
invalidated_leaves.insert(target.freshest_leaf_idx);
// Takes care of route removal. Starts from the leaf and stops as soon as an error is
// encountered. In this case an error is interpreted as the block being not a leaf
// and it will be removed while removing another route from the same block but to a
// different leaf.
let mut remove_route = |route: TreeRoute<Block>| {
route.enacted().iter().rev().all(|elem| remove_leaf(elem.number, elem.hash));
};
let target_hash = target.freshest_route.common_block().hash;
debug_assert_eq!(
target.freshest_route.common_block().number,
number,
"This is a bug in LevelMonitor::find_target() or the Backend is corrupted"
);
// Remove freshest (cached) route first.
remove_route(target.freshest_route);
// Don't bother trying with leaves we already found to not be our descendants.
let to_skip = leaves.len() - target.freshest_leaf_idx;
leaves.iter().enumerate().rev().skip(to_skip).for_each(|(leaf_idx, leaf_hash)| {
if invalidated_leaves.contains(&leaf_idx) {
return;
}
match pezsp_blockchain::tree_route(self.backend.blockchain(), target_hash, *leaf_hash) {
Ok(route) if route.retracted().is_empty() => {
invalidated_leaves.insert(leaf_idx);
remove_route(route);
},
Err(err) => {
log::warn!(
target: LOG_TARGET,
"(Removal) unable getting route from {:?} to {:?}: {}",
target_hash,
leaf_hash,
err,
);
},
_ => (),
};
});
remove_leaf(number, target_hash);
}
/// Add a new imported block information to the monitor.
pub fn block_imported(&mut self, number: NumberFor<Block>, hash: Block::Hash) {
let finalized_num = self.backend.blockchain().info().finalized_number;
if number > finalized_num {
// Only blocks above the last finalized block should be added to the monitor
self.import_counter += One::one();
self.freshness.insert(hash, self.import_counter);
self.levels.entry(number).or_default().insert(hash);
}
let delta: u32 = finalized_num.saturating_sub(self.lowest_level).unique_saturated_into();
if delta >= CLEANUP_THRESHOLD {
// Do cleanup once in a while, we are allowed to have some obsolete information.
for i in 0..delta {
let number = self.lowest_level + i.unique_saturated_into();
self.levels.remove(&number).map(|level| {
level.iter().for_each(|hash| {
self.freshness.remove(hash);
})
});
}
self.lowest_level = finalized_num;
}
}
}
@@ -0,0 +1,220 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use codec::Decode;
use pezkuwi_primitives::{Block as PBlock, Hash as PHash, Header as PHeader, ValidationCodeHash};
use cumulus_primitives_core::{relay_chain, AbridgedHostConfiguration};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
use pezsc_client_api::Backend;
use pezsc_consensus::{shared_data::SharedData, BlockImport, ImportResult};
use pezsp_consensus_slots::Slot;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
use pezsp_timestamp::Timestamp;
use std::{sync::Arc, time::Duration};
mod level_monitor;
mod parent_search;
#[cfg(test)]
mod tests;
mod teyrchain_consensus;
pub use parent_search::*;
pub use cumulus_relay_chain_streams::finalized_heads;
pub use teyrchain_consensus::spawn_teyrchain_consensus_tasks;
use level_monitor::LevelMonitor;
pub use level_monitor::{LevelLimit, MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT};
pub mod import_queue;
/// Provides the hash of validation code used for authoring/execution of blocks at a given
/// hash.
pub trait ValidationCodeHashProvider<Hash> {
fn code_hash_at(&self, at: Hash) -> Option<ValidationCodeHash>;
}
impl<F, Hash> ValidationCodeHashProvider<Hash> for F
where
F: Fn(Hash) -> Option<ValidationCodeHash>,
{
fn code_hash_at(&self, at: Hash) -> Option<ValidationCodeHash> {
(self)(at)
}
}
/// The result from building a collation.
pub struct TeyrchainCandidate<B> {
/// The block that was built for this candidate.
pub block: B,
/// The proof that was recorded while building the block.
pub proof: pezsp_trie::StorageProof,
}
/// Teyrchain specific block import.
///
/// Specialized block import for teyrchains. It supports to delay setting the best block until the
/// relay chain has included a candidate in its best block. By default the delayed best block
/// setting is disabled. The block import also monitors the imported blocks and prunes by default if
/// there are too many blocks at the same height. Too many blocks at the same height can for example
/// happen if the relay chain is rejecting the teyrchain blocks in the validation.
pub struct TeyrchainBlockImport<Block: BlockT, BI, BE> {
inner: BI,
monitor: Option<SharedData<LevelMonitor<Block, BE>>>,
delayed_best_block: bool,
}
impl<Block: BlockT, BI, BE: Backend<Block>> TeyrchainBlockImport<Block, BI, BE> {
/// Create a new instance.
///
/// The number of leaves per level limit is set to `LevelLimit::Default`.
pub fn new(inner: BI, backend: Arc<BE>) -> Self {
Self::new_with_limit(inner, backend, LevelLimit::Default)
}
/// Create a new instance with an explicit limit to the number of leaves per level.
///
/// This function alone doesn't enforce the limit on levels for old imported blocks,
/// the limit is eventually enforced only when new blocks are imported.
pub fn new_with_limit(inner: BI, backend: Arc<BE>, level_leaves_max: LevelLimit) -> Self {
let level_limit = match level_leaves_max {
LevelLimit::None => None,
LevelLimit::Some(limit) => Some(limit),
LevelLimit::Default => Some(MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT),
};
let monitor =
level_limit.map(|level_limit| SharedData::new(LevelMonitor::new(level_limit, backend)));
Self { inner, monitor, delayed_best_block: false }
}
/// Create a new instance which delays setting the best block.
///
/// The number of leaves per level limit is set to `LevelLimit::Default`.
pub fn new_with_delayed_best_block(inner: BI, backend: Arc<BE>) -> Self {
Self {
delayed_best_block: true,
..Self::new_with_limit(inner, backend, LevelLimit::Default)
}
}
}
impl<Block: BlockT, I: Clone, BE> Clone for TeyrchainBlockImport<Block, I, BE> {
fn clone(&self) -> Self {
TeyrchainBlockImport {
inner: self.inner.clone(),
monitor: self.monitor.clone(),
delayed_best_block: self.delayed_best_block,
}
}
}
#[async_trait::async_trait]
impl<Block, BI, BE> BlockImport<Block> for TeyrchainBlockImport<Block, BI, BE>
where
Block: BlockT,
BI: BlockImport<Block> + Send + Sync,
BE: Backend<Block>,
{
type Error = BI::Error;
async fn check_block(
&self,
block: pezsc_consensus::BlockCheckParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
self.inner.check_block(block).await
}
async fn import_block(
&self,
mut params: pezsc_consensus::BlockImportParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
// Blocks are stored within the backend by using POST hash.
let hash = params.post_hash();
let number = *params.header.number();
if params.with_state() {
// Force imported state finality.
// Required for warp sync. We assume that preconditions have been
// checked properly and we are importing a finalized block with state.
params.finalized = true;
}
if self.delayed_best_block {
// Best block is determined by the relay chain, or if we are doing the initial sync
// we import all blocks as new best.
params.fork_choice = Some(pezsc_consensus::ForkChoiceStrategy::Custom(
params.origin == pezsp_consensus::BlockOrigin::NetworkInitialSync,
));
}
let maybe_lock = self.monitor.as_ref().map(|monitor_lock| {
let mut monitor = monitor_lock.shared_data_locked();
monitor.enforce_limit(number);
monitor.release_mutex()
});
let res = self.inner.import_block(params).await?;
if let (Some(mut monitor_lock), ImportResult::Imported(_)) = (maybe_lock, &res) {
let mut monitor = monitor_lock.upgrade();
monitor.block_imported(number, hash);
}
Ok(res)
}
}
/// Marker trait denoting a block import type that fits the teyrchain requirements.
pub trait TeyrchainBlockImportMarker {}
impl<B: BlockT, BI, BE> TeyrchainBlockImportMarker for TeyrchainBlockImport<B, BI, BE> {}
/// Get the relay-parent slot and timestamp from a header.
pub fn relay_slot_and_timestamp(
relay_parent_header: &PHeader,
relay_chain_slot_duration: Duration,
) -> Option<(Slot, Timestamp)> {
pezsc_consensus_babe::find_pre_digest::<PBlock>(relay_parent_header)
.map(|babe_pre_digest| {
let slot = babe_pre_digest.slot();
let t = Timestamp::new(relay_chain_slot_duration.as_millis() as u64 * *slot);
(slot, t)
})
.ok()
}
/// Reads abridged host configuration from the relay chain storage at the given relay parent.
pub async fn load_abridged_host_configuration(
relay_parent: PHash,
relay_client: &impl RelayChainInterface,
) -> Result<Option<AbridgedHostConfiguration>, RelayChainError> {
relay_client
.get_storage_by_key(relay_parent, relay_chain::well_known_keys::ACTIVE_CONFIG)
.await?
.map(|bytes| {
AbridgedHostConfiguration::decode(&mut &bytes[..])
.map_err(RelayChainError::DeserializationError)
})
.transpose()
}
@@ -0,0 +1,419 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use codec::Decode;
use pezkuwi_primitives::Hash as RelayHash;
use cumulus_primitives_core::{
relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
ParaId,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
use pezsc_client_api::{Backend, HeaderBackend};
use pezsp_blockchain::{Backend as BlockchainBackend, TreeRoute};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents";
/// Parameters when searching for suitable parents to build on top of.
#[derive(Debug)]
pub struct ParentSearchParams {
/// The relay-parent that is intended to be used.
pub relay_parent: RelayHash,
/// The ID of the teyrchain.
pub para_id: ParaId,
/// A limitation on the age of relay parents for teyrchain blocks that are being
/// considered. This is relative to the `relay_parent` number.
pub ancestry_lookback: usize,
/// How "deep" parents can be relative to the included teyrchain block at the relay-parent.
/// The included block has depth 0.
pub max_depth: usize,
/// Whether to only ignore "alternative" branches, i.e. branches of the chain
/// which do not contain the block pending availability.
pub ignore_alternative_branches: bool,
}
/// A potential parent block returned from [`find_potential_parents`]
#[derive(PartialEq)]
pub struct PotentialParent<B: BlockT> {
/// The hash of the block.
pub hash: B::Hash,
/// The header of the block.
pub header: B::Header,
/// The depth of the block with respect to the included block.
pub depth: usize,
/// Whether the block is the included block, is itself pending on-chain, or descends
/// from the block pending availability.
pub aligned_with_pending: bool,
}
impl<B: BlockT> std::fmt::Debug for PotentialParent<B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PotentialParent")
.field("hash", &self.hash)
.field("depth", &self.depth)
.field("aligned_with_pending", &self.aligned_with_pending)
.field("number", &self.header.number())
.finish()
}
}
/// Perform a recursive search through blocks to find potential
/// parent blocks for a new block.
///
/// This accepts a relay-chain block to be used as an anchor and a maximum search depth,
/// along with some arguments for filtering teyrchain blocks and performs a recursive search
/// for teyrchain blocks. The search begins at the last included teyrchain block and returns
/// a set of [`PotentialParent`]s which could be potential parents of a new block with this
/// relay-parent according to the search parameters.
///
/// A teyrchain block is a potential parent if it is either the last included teyrchain block, the
/// pending teyrchain block (when `max_depth` >= 1), or all of the following hold:
/// * its parent is a potential parent
/// * its relay-parent is within `ancestry_lookback` of the targeted relay-parent.
/// * its relay-parent is within the same session as the targeted relay-parent.
/// * the block number is within `max_depth` blocks of the included block
pub async fn find_potential_parents<B: BlockT>(
params: ParentSearchParams,
backend: &impl Backend<B>,
relay_client: &impl RelayChainInterface,
) -> Result<Vec<PotentialParent<B>>, RelayChainError> {
tracing::trace!("Parent search parameters: {params:?}");
// Get the included block.
let Some((included_header, included_hash)) =
fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent)
.await?
else {
return Ok(Default::default());
};
let only_included = vec![PotentialParent {
hash: included_hash,
header: included_header.clone(),
depth: 0,
aligned_with_pending: true,
}];
if params.max_depth == 0 {
return Ok(only_included);
};
// Pending header and hash.
let maybe_pending = {
// Fetch the most recent pending header from the relay chain. We use
// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
// before being returned to us.
let pending_header = relay_client
.persisted_validation_data(
params.relay_parent,
params.para_id,
OccupiedCoreAssumption::Included,
)
.await?
.and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok())
.filter(|x| x.hash() != included_hash);
// If the pending block is not locally known, we can't do anything.
if let Some(header) = pending_header {
let pending_hash = header.hash();
match backend.blockchain().header(pending_hash) {
// We are supposed to ignore branches that don't contain the pending block, but we
// do not know the pending block locally.
Ok(None) | Err(_) if params.ignore_alternative_branches => {
tracing::warn!(
target: PARENT_SEARCH_LOG_TARGET,
%pending_hash,
"Failed to get header for pending block.",
);
return Ok(Default::default());
},
Ok(Some(_)) => Some((header, pending_hash)),
_ => None,
}
} else {
None
}
};
let maybe_route_to_last_pending = maybe_pending
.as_ref()
.map(|(_, pending)| {
pezsp_blockchain::tree_route(backend.blockchain(), included_hash, *pending)
})
.transpose()?;
// If we want to ignore alternative branches there is no reason to start
// the parent search at the included block. We can add the included block and
// the path to the pending block to the potential parents directly (limited by max_depth).
let (frontier, potential_parents) = match (
&maybe_pending,
params.ignore_alternative_branches,
&maybe_route_to_last_pending,
) {
(Some((pending_header, pending_hash)), true, Some(ref route_to_pending)) => {
let mut potential_parents = only_included;
// This is a defensive check, should never happen.
if !route_to_pending.retracted().is_empty() {
tracing::warn!(target: PARENT_SEARCH_LOG_TARGET, "Included block not an ancestor of pending block. This should not happen.");
return Ok(Default::default());
}
// Add all items on the path included -> pending - 1 to the potential parents, but
// not more than `max_depth`.
let num_parents_on_path =
route_to_pending.enacted().len().saturating_sub(1).min(params.max_depth);
for (num, block) in
route_to_pending.enacted().iter().take(num_parents_on_path).enumerate()
{
let Ok(Some(header)) = backend.blockchain().header(block.hash) else { continue };
potential_parents.push(PotentialParent {
hash: block.hash,
header,
depth: 1 + num,
aligned_with_pending: true,
});
}
// The search for additional potential parents should now start at the children of
// the pending block.
(
vec![PotentialParent {
hash: *pending_hash,
header: pending_header.clone(),
depth: route_to_pending.enacted().len(),
aligned_with_pending: true,
}],
potential_parents,
)
},
_ => (only_included, Default::default()),
};
if potential_parents.len() > params.max_depth {
return Ok(potential_parents);
}
// Build up the ancestry record of the relay chain to compare against.
let rp_ancestry =
build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
.await?;
Ok(search_child_branches_for_parents(
frontier,
maybe_route_to_last_pending,
included_header,
maybe_pending.map(|(_, hash)| hash),
backend,
params.max_depth,
params.ignore_alternative_branches,
rp_ancestry,
potential_parents,
))
}
/// Fetch the included block from the relay chain.
async fn fetch_included_from_relay_chain<B: BlockT>(
relay_client: &impl RelayChainInterface,
backend: &impl Backend<B>,
para_id: ParaId,
relay_parent: RelayHash,
) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
// so that even if there is a pending candidate, we assume it is timed out and we get the
// included head.
let included_header = relay_client
.persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut)
.await?;
let included_header = match included_header {
Some(pvd) => pvd.parent_head,
None => return Ok(None), // this implies the para doesn't exist.
};
let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() {
None => return Ok(None),
Some(x) => x,
};
let included_hash = included_header.hash();
// If the included block is not locally known, we can't do anything.
match backend.blockchain().header(included_hash) {
Ok(None) => {
tracing::warn!(
target: PARENT_SEARCH_LOG_TARGET,
%included_hash,
"Failed to get header for included block.",
);
return Ok(None);
},
Err(e) => {
tracing::warn!(
target: PARENT_SEARCH_LOG_TARGET,
%included_hash,
%e,
"Failed to get header for included block.",
);
return Ok(None);
},
_ => {},
};
Ok(Some((included_header, included_hash)))
}
/// Build an ancestry of relay parents that are acceptable.
///
/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
/// relay parent we want to build on. Teyrchain blocks anchored on relay parents older than that can
/// not be considered potential parents for block building. They have no chance of still getting
/// included, so our newly build teyrchain block would also not get included.
///
/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
/// ancestry blocks.
async fn build_relay_parent_ancestry(
ancestry_lookback: usize,
relay_parent: RelayHash,
relay_client: &impl RelayChainInterface,
) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
let mut current_rp = relay_parent;
let mut required_session = None;
while ancestry.len() <= ancestry_lookback {
let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break };
let session = relay_client.session_index_for_child(current_rp).await?;
if required_session.get_or_insert(session) != &session {
// Respect the relay-chain rule not to cross session boundaries.
break;
}
ancestry.push((current_rp, *header.state_root()));
current_rp = *header.parent_hash();
// don't iterate back into the genesis block.
if header.number == 1 {
break;
}
}
Ok(ancestry)
}
/// Start search for child blocks that can be used as parents.
pub fn search_child_branches_for_parents<Block: BlockT>(
mut frontier: Vec<PotentialParent<Block>>,
maybe_route_to_last_pending: Option<TreeRoute<Block>>,
included_header: Block::Header,
pending_hash: Option<Block::Hash>,
backend: &impl Backend<Block>,
max_depth: usize,
ignore_alternative_branches: bool,
rp_ancestry: Vec<(RelayHash, RelayHash)>,
mut potential_parents: Vec<PotentialParent<Block>>,
) -> Vec<PotentialParent<Block>> {
let included_hash = included_header.hash();
let is_hash_in_ancestry = |hash| rp_ancestry.iter().any(|x| x.0 == hash);
let is_root_in_ancestry = |root| rp_ancestry.iter().any(|x| x.1 == root);
// The distance between pending and included block. Is later used to check if a child
// is aligned with pending when it is between pending and included block.
let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len());
// If a block is on the path included -> pending, we consider it `aligned_with_pending`.
let is_child_pending = |hash| {
maybe_route_to_last_pending
.as_ref()
.map_or(true, |route| route.enacted().iter().any(|x| x.hash == hash))
};
tracing::trace!(
target: PARENT_SEARCH_LOG_TARGET,
?included_hash,
included_num = ?included_header.number(),
?pending_hash ,
?rp_ancestry,
"Searching relay chain ancestry."
);
while let Some(entry) = frontier.pop() {
let is_pending = pending_hash.as_ref().map_or(false, |h| &entry.hash == h);
let is_included = included_hash == entry.hash;
// note: even if the pending block or included block have a relay parent
// outside of the expected part of the relay chain, they are always allowed
// because they have already been posted on chain.
let is_potential = is_pending || is_included || {
let digest = entry.header.digest();
let is_hash_in_ancestry_check = cumulus_primitives_core::extract_relay_parent(digest)
.map_or(false, is_hash_in_ancestry);
let is_root_in_ancestry_check =
cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
.map(|(r, _n)| r)
.map_or(false, is_root_in_ancestry);
is_hash_in_ancestry_check || is_root_in_ancestry_check
};
let parent_aligned_with_pending = entry.aligned_with_pending;
let child_depth = entry.depth + 1;
let hash = entry.hash;
tracing::trace!(
target: PARENT_SEARCH_LOG_TARGET,
?hash,
is_potential,
is_pending,
is_included,
"Checking potential parent."
);
if is_potential {
potential_parents.push(entry);
}
if !is_potential || child_depth > max_depth {
continue;
}
// push children onto search frontier.
for child in backend.blockchain().children(hash).ok().into_iter().flatten() {
tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, child_depth, ?pending_distance, "Looking at child.");
let aligned_with_pending = parent_aligned_with_pending &&
(pending_distance.map_or(true, |dist| child_depth > dist) ||
is_child_pending(child));
if ignore_alternative_branches && !aligned_with_pending {
tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, "Child is not aligned with pending block.");
continue;
}
let Ok(Some(header)) = backend.blockchain().header(child) else { continue };
frontier.push(PotentialParent {
hash: child,
header,
depth: child_depth,
aligned_with_pending,
});
}
}
potential_parents
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,544 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use cumulus_relay_chain_streams::{finalized_heads, new_best_heads};
use pezsc_client_api::{
Backend, BlockBackend, BlockImportNotification, BlockchainEvents, Finalizer, UsageProvider,
};
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
use schnellru::{ByLength, LruMap};
use pezsp_blockchain::Error as ClientError;
use pezsp_consensus::{BlockOrigin, BlockStatus};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
use cumulus_client_pov_recovery::{RecoveryKind, RecoveryRequest};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_primitives::Id as ParaId;
use codec::Decode;
use futures::{
channel::mpsc::{Sender, UnboundedSender},
pin_mut, select, FutureExt, SinkExt, Stream, StreamExt,
};
use pezsp_core::traits::SpawnEssentialNamed;
use std::sync::Arc;
const LOG_TARGET: &str = "pezcumulus-consensus";
const FINALIZATION_CACHE_SIZE: u32 = 40;
fn handle_new_finalized_head<P, Block, B>(
teyrchain: &Arc<P>,
header: Block::Header,
last_seen_finalized_hashes: &mut LruMap<Block::Hash, ()>,
) where
Block: BlockT,
B: Backend<Block>,
P: Finalizer<Block, B> + UsageProvider<Block> + BlockchainEvents<Block>,
{
let hash = header.hash();
last_seen_finalized_hashes.insert(hash, ());
// Only finalize if we are below the incoming finalized teyrchain head
if teyrchain.usage_info().chain.finalized_number < *header.number() {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Attempting to finalize header.",
);
if let Err(e) = teyrchain.finalize_block(hash, None, true) {
match e {
ClientError::UnknownBlock(_) => tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Could not finalize block because it is unknown.",
),
_ => tracing::warn!(
target: LOG_TARGET,
error = ?e,
block_hash = ?hash,
"Failed to finalize block",
),
}
}
}
}
/// Streams finalized teyrchain heads from the relay chain.
///
/// This worker continuously monitors the relay chain for finalized blocks and extracts
/// the corresponding teyrchain head data for the given `para_id`. The extracted head
/// data is sent through the provided channel for consumption by the consensus system.
///
/// This is necessary because finalization of blocks can take a long
/// time. During this blocking operation, we should not keep references to finality notifications,
/// because that prevents the corresponding blocks from getting pruned.
pub async fn finalized_head_stream_worker<R: RelayChainInterface + Clone, Block: BlockT>(
mut tx: UnboundedSender<Block::Header>,
para_id: ParaId,
relay_chain: R,
) {
let finalized_heads = match finalized_heads(relay_chain.clone(), para_id).await {
Ok(finalized_heads_stream) => finalized_heads_stream.fuse(),
Err(err) => {
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve finalized heads stream.");
return;
},
};
pin_mut!(finalized_heads);
loop {
if let Some((head_data, _)) = finalized_heads.next().await {
let header = match Block::Header::decode(&mut &head_data[..]) {
Ok(header) => header,
Err(err) => {
tracing::debug!(
target: LOG_TARGET,
error = ?err,
"Could not decode teyrchain header while following finalized heads.",
);
continue;
},
};
if let Err(e) = tx.send(header).await {
tracing::error!(target: LOG_TARGET, ?e, "Error while sending finalized head.");
return;
};
}
}
}
/// Follow the finalized head of the given teyrchain.
///
/// For every finalized block of the relay chain, it will get the included teyrchain header
/// corresponding to `para_id` and will finalize it in the teyrchain.
async fn follow_finalized_head<P, Block, B>(
teyrchain: Arc<P>,
finalized_head_stream: Box<impl Stream<Item = Block::Header> + Unpin + Send>,
) where
Block: BlockT,
P: Finalizer<Block, B> + UsageProvider<Block> + BlockchainEvents<Block>,
B: Backend<Block>,
{
let mut imported_blocks = teyrchain.import_notification_stream().fuse();
let mut finalized_head_stream = finalized_head_stream.fuse();
// We use this cache to finalize blocks that are imported late.
// For example, a block that has been recovered via PoV-Recovery
// on a full node can have several minutes delay. With this cache
// we have some "memory" of recently finalized blocks.
let mut last_seen_finalized_hashes = LruMap::new(ByLength::new(FINALIZATION_CACHE_SIZE));
loop {
select! {
fin = finalized_head_stream.next() => {
match fin {
Some(finalized_head) =>
handle_new_finalized_head(&teyrchain, finalized_head, &mut last_seen_finalized_hashes),
None => {
tracing::debug!(target: LOG_TARGET, "Stopping following finalized head.");
return
}
}
},
imported = imported_blocks.next() => {
match imported {
Some(imported_block) => {
// When we see a block import that is already finalized, we immediately finalize it.
if last_seen_finalized_hashes.peek(&imported_block.hash).is_some() {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?imported_block.hash,
"Setting newly imported block as finalized.",
);
if let Err(e) = teyrchain.finalize_block(imported_block.hash, None, true) {
match e {
ClientError::UnknownBlock(_) => tracing::debug!(
target: LOG_TARGET,
block_hash = ?imported_block.hash,
"Could not finalize block because it is unknown.",
),
_ => tracing::warn!(
target: LOG_TARGET,
error = ?e,
block_hash = ?imported_block.hash,
"Failed to finalize block",
),
}
}
}
},
None => {
tracing::debug!(
target: LOG_TARGET,
"Stopping following imported blocks.",
);
return
}
}
}
}
}
}
/// Spawns the essential finalization tasks for teyrchain consensus.
///
/// This function creates and spawns two critical background tasks:
/// 1. A finalized head stream worker that monitors relay chain finality and extracts included
/// headers
/// 2. The main teyrchain consensus task that handles finalization and best block updates
pub fn spawn_teyrchain_consensus_tasks<P, R, Block, B, S>(
para_id: ParaId,
teyrchain: Arc<P>,
relay_chain: R,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
spawn_handle: S,
) where
Block: BlockT,
P: Finalizer<Block, B>
+ UsageProvider<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>
+ 'static,
for<'a> &'a P: BlockImport<Block>,
R: RelayChainInterface + Clone + 'static,
S: SpawnEssentialNamed + 'static,
B: Backend<Block> + 'static,
{
let (tx, rx) = futures::channel::mpsc::unbounded();
let worker = finalized_head_stream_worker::<_, Block>(tx, para_id, relay_chain.clone());
let consensus = run_teyrchain_consensus(
para_id,
teyrchain,
relay_chain,
announce_block,
Box::new(rx),
recovery_chan_tx,
);
spawn_handle.spawn_essential_blocking("pezcumulus-consensus", None, Box::pin(consensus));
spawn_handle.spawn_essential_blocking(
"pezcumulus-consensus-finality-stream",
None,
Box::pin(worker),
);
}
/// Run the teyrchain consensus.
///
/// This will follow the given `relay_chain` to act as consensus for the teyrchain that corresponds
/// to the given `para_id`. It will set the new best block of the teyrchain as it gets aware of it.
/// The same happens for the finalized block.
///
/// # Note
///
/// This will access the backend of the teyrchain and thus, this future should be spawned as
/// blocking task.
pub async fn run_teyrchain_consensus<P, R, Block, B>(
para_id: ParaId,
teyrchain: Arc<P>,
relay_chain: R,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
finalized_head_stream: Box<impl Stream<Item = Block::Header> + Unpin + Send>,
recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
) where
Block: BlockT,
P: Finalizer<Block, B>
+ UsageProvider<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>,
for<'a> &'a P: BlockImport<Block>,
R: RelayChainInterface + Clone,
B: Backend<Block>,
{
let follow_new_best = follow_new_best(
para_id,
teyrchain.clone(),
relay_chain.clone(),
announce_block,
recovery_chan_tx,
);
let follow_finalized_head = follow_finalized_head(teyrchain, finalized_head_stream);
select! {
_ = follow_new_best.fuse() => {},
_ = follow_finalized_head.fuse() => {},
}
}
/// Follow the relay chain new best head, to update the Teyrchain new best head.
async fn follow_new_best<P, R, Block, B>(
para_id: ParaId,
teyrchain: Arc<P>,
relay_chain: R,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
mut recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
) where
Block: BlockT,
P: Finalizer<Block, B>
+ UsageProvider<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>,
for<'a> &'a P: BlockImport<Block>,
R: RelayChainInterface + Clone,
B: Backend<Block>,
{
let new_best_heads = match new_best_heads(relay_chain, para_id).await {
Ok(best_heads_stream) => best_heads_stream.fuse(),
Err(err) => {
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve best heads stream.");
return;
},
};
pin_mut!(new_best_heads);
let mut imported_blocks = teyrchain.import_notification_stream().fuse();
// The unset best header of the teyrchain. Will be `Some(_)` when we have imported a relay chain
// block before the associated teyrchain block. In this case we need to wait for this block to
// be imported to set it as new best.
let mut unset_best_header = None;
loop {
select! {
h = new_best_heads.next() => {
match h {
Some(h) => handle_new_best_teyrchain_head(
h,
&*teyrchain,
&mut unset_best_header,
recovery_chan_tx.as_mut(),
).await,
None => {
tracing::debug!(
target: LOG_TARGET,
"Stopping following new best.",
);
return
}
}
},
i = imported_blocks.next() => {
match i {
Some(i) => handle_new_block_imported(
i,
&mut unset_best_header,
&*teyrchain,
&*announce_block,
).await,
None => {
tracing::debug!(
target: LOG_TARGET,
"Stopping following imported blocks.",
);
return
}
}
},
}
}
}
/// Handle a new import block of the teyrchain.
async fn handle_new_block_imported<Block, P>(
notification: BlockImportNotification<Block>,
unset_best_header_opt: &mut Option<Block::Header>,
teyrchain: &P,
announce_block: &(dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync),
) where
Block: BlockT,
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
for<'a> &'a P: BlockImport<Block>,
{
// HACK
//
// Remove after https://github.com/pezkuwichain/kurdistan-sdk/issues/76 or similar is merged
if notification.origin != BlockOrigin::Own {
announce_block(notification.hash, None);
}
let unset_best_header = match (notification.is_new_best, &unset_best_header_opt) {
// If this is the new best block or we don't have any unset block, we can end it here.
(true, _) | (_, None) => return,
(false, Some(ref u)) => u,
};
let unset_hash = if notification.header.number() < unset_best_header.number() {
return;
} else if notification.header.number() == unset_best_header.number() {
let unset_hash = unset_best_header.hash();
if unset_hash != notification.hash {
return;
} else {
unset_hash
}
} else {
unset_best_header.hash()
};
match teyrchain.block_status(unset_hash) {
Ok(BlockStatus::InChainWithState) => {
let unset_best_header = unset_best_header_opt
.take()
.expect("We checked above that the value is set; qed");
tracing::debug!(
target: LOG_TARGET,
?unset_hash,
"Importing block as new best for teyrchain.",
);
import_block_as_new_best(unset_hash, unset_best_header, teyrchain).await;
},
state => tracing::debug!(
target: LOG_TARGET,
?unset_best_header,
?notification.header,
?state,
"Unexpected state for unset best header.",
),
}
}
/// Handle the new best teyrchain head as extracted from the new best relay chain.
async fn handle_new_best_teyrchain_head<Block, P>(
head: Vec<u8>,
teyrchain: &P,
unset_best_header: &mut Option<Block::Header>,
mut recovery_chan_tx: Option<&mut Sender<RecoveryRequest<Block>>>,
) where
Block: BlockT,
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
for<'a> &'a P: BlockImport<Block>,
{
let teyrchain_head = match <<Block as BlockT>::Header>::decode(&mut &head[..]) {
Ok(header) => header,
Err(err) => {
tracing::debug!(
target: LOG_TARGET,
error = ?err,
"Could not decode Teyrchain header while following best heads.",
);
return;
},
};
let hash = teyrchain_head.hash();
if teyrchain.usage_info().chain.best_hash == hash {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Skipping set new best block, because block is already the best.",
);
return;
}
// Make sure the block is already known or otherwise we skip setting new best.
match teyrchain.block_status(hash) {
Ok(BlockStatus::InChainWithState) => {
unset_best_header.take();
tracing::debug!(
target: LOG_TARGET,
included = ?hash,
"Importing block as new best for teyrchain.",
);
import_block_as_new_best(hash, teyrchain_head, teyrchain).await;
},
Ok(BlockStatus::InChainPruned) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
"Trying to set pruned block as new best!",
);
},
Ok(BlockStatus::Unknown) => {
*unset_best_header = Some(teyrchain_head);
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Teyrchain block not yet imported, waiting for import to enact as best block.",
);
if let Some(ref mut recovery_chan_tx) = recovery_chan_tx {
// Best effort channel to actively encourage block recovery.
// An error here is not fatal; the relay chain continuously re-announces
// the best block, thus we will have other opportunities to retry.
let req = RecoveryRequest { hash, kind: RecoveryKind::Full };
if let Err(err) = recovery_chan_tx.try_send(req) {
tracing::warn!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?err,
"Unable to notify block recovery subsystem"
)
}
}
},
Err(e) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?e,
"Failed to get block status of block.",
);
},
_ => {},
}
}
async fn import_block_as_new_best<Block, P>(hash: Block::Hash, header: Block::Header, teyrchain: &P)
where
Block: BlockT,
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
for<'a> &'a P: BlockImport<Block>,
{
let best_number = teyrchain.usage_info().chain.best_number;
if *header.number() < best_number {
tracing::debug!(
target: LOG_TARGET,
%best_number,
block_number = %header.number(),
"Skipping importing block as new best block, because there already exists a \
best block with an higher number",
);
return;
}
// Make it the new best block
let mut block_import_params = BlockImportParams::new(BlockOrigin::ConsensusBroadcast, header);
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(true));
block_import_params.import_existing = true;
if let Err(err) = teyrchain.import_block(block_import_params).await {
tracing::warn!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?err,
"Failed to set new best block.",
);
}
}
@@ -0,0 +1,45 @@
[package]
name = "pezcumulus-client-consensus-proposer"
description = "A Bizinikiwi `Proposer` for building teyrchain blocks"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true, default-features = true }
async-trait = { workspace = true }
thiserror = { workspace = true }
# Bizinikiwi
pezsc-basic-authorship = { workspace = true }
pezsc-block-builder = { workspace = true }
pezsc-transaction-pool-api = { workspace = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-primitives-teyrchain-inherent = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-teyrchain-inherent/runtime-benchmarks",
"pezsc-basic-authorship/runtime-benchmarks",
"pezsc-block-builder/runtime-benchmarks",
"pezsc-transaction-pool-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
@@ -0,0 +1,126 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! The Pezcumulus [`ProposerInterface`] is an extension of the Bizinikiwi [`ProposerFactory`]
//! for creating new teyrchain blocks.
//!
//! This utility is designed to be composed within any collator consensus algorithm.
use async_trait::async_trait;
use cumulus_primitives_teyrchain_inherent::TeyrchainInherentData;
use pezsc_basic_authorship::{ProposeArgs, ProposerFactory};
use pezsc_block_builder::BlockBuilderApi;
use pezsc_transaction_pool_api::TransactionPool;
use pezsp_api::{ApiExt, CallApiAt, ProvideRuntimeApi};
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus::{EnableProofRecording, Environment, Proposal};
use pezsp_inherents::{InherentData, InherentDataProvider};
use pezsp_runtime::{traits::Block as BlockT, Digest};
use pezsp_state_machine::StorageProof;
use std::{fmt::Debug, time::Duration};
/// Errors that can occur when proposing a teyrchain block.
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error {
inner: anyhow::Error,
}
impl Error {
/// Create an error tied to the creation of a proposer.
pub fn proposer_creation(err: impl Into<anyhow::Error>) -> Self {
Error { inner: err.into().context("Proposer Creation") }
}
/// Create an error tied to the proposing logic itself.
pub fn proposing(err: impl Into<anyhow::Error>) -> Self {
Error { inner: err.into().context("Proposing") }
}
}
/// A type alias for easily referring to the type of a proposal produced by a specific
/// [`ProposerInterface`].
pub type ProposalOf<B> = Proposal<B, StorageProof>;
/// An interface for proposers.
#[async_trait]
pub trait ProposerInterface<Block: BlockT> {
/// Propose a collation using the supplied `InherentData` and the provided
/// `TeyrchainInherentData`.
///
/// Also specify any required inherent digests, the maximum proposal duration,
/// and the block size limit in bytes. See the documentation on
/// [`pezsp_consensus::Proposer::propose`] for more details on how to interpret these parameters.
///
/// The `InherentData` and `Digest` are left deliberately general in order to accommodate
/// all possible collator selection algorithms or inherent creation mechanisms,
/// while the `TeyrchainInherentData` is made explicit so it will be constructed appropriately.
///
/// If the `InherentData` passed into this function already has a `TeyrchainInherentData`,
/// this should throw an error.
async fn propose(
&mut self,
parent_header: &Block::Header,
paras_inherent_data: &TeyrchainInherentData,
other_inherent_data: InherentData,
inherent_digests: Digest,
max_duration: Duration,
block_size_limit: Option<usize>,
) -> Result<Option<Proposal<Block, StorageProof>>, Error>;
}
#[async_trait]
impl<Block, A, C> ProposerInterface<Block> for ProposerFactory<A, C, EnableProofRecording>
where
A: TransactionPool<Block = Block> + 'static,
C: HeaderBackend<Block> + ProvideRuntimeApi<Block> + CallApiAt<Block> + Send + Sync + 'static,
C::Api: ApiExt<Block> + BlockBuilderApi<Block>,
Block: pezsp_runtime::traits::Block,
{
async fn propose(
&mut self,
parent_header: &Block::Header,
paras_inherent_data: &TeyrchainInherentData,
other_inherent_data: InherentData,
inherent_digests: Digest,
max_duration: Duration,
block_size_limit: Option<usize>,
) -> Result<Option<Proposal<Block, StorageProof>>, Error> {
let proposer = self
.init(parent_header)
.await
.map_err(|e| Error::proposer_creation(anyhow::Error::new(e)))?;
let mut inherent_data = other_inherent_data;
paras_inherent_data
.provide_inherent_data(&mut inherent_data)
.await
.map_err(|e| Error::proposing(anyhow::Error::new(e)))?;
proposer
.propose_block(ProposeArgs {
inherent_data,
inherent_digests,
max_duration,
block_size_limit,
ignored_nodes_by_proof_recording: None,
})
.await
.map(Some)
.map_err(|e| Error::proposing(anyhow::Error::new(e)).into())
}
}
@@ -0,0 +1,48 @@
[package]
name = "pezcumulus-client-consensus-relay-chain"
description = "The relay-chain provided consensus algorithm"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
futures = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-block-builder = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-block-builder/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,88 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use std::{marker::PhantomData, sync::Arc};
use pezsc_consensus::{import_queue::Verifier as VerifierT, BlockImportParams};
use pezsp_api::ProvideRuntimeApi;
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
/// A verifier that just checks the inherents.
pub struct Verifier<Client, Block, CIDP> {
client: Arc<Client>,
create_inherent_data_providers: CIDP,
_marker: PhantomData<Block>,
}
impl<Client, Block, CIDP> Verifier<Client, Block, CIDP> {
/// Create a new instance.
pub fn new(client: Arc<Client>, create_inherent_data_providers: CIDP) -> Self {
Self { client, create_inherent_data_providers, _marker: PhantomData }
}
}
#[async_trait::async_trait]
impl<Client, Block, CIDP> VerifierT<Block> for Verifier<Client, Block, CIDP>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block>,
CIDP: CreateInherentDataProviders<Block, ()>,
{
async fn verify(
&self,
mut block_params: BlockImportParams<Block>,
) -> Result<BlockImportParams<Block>, String> {
block_params.fork_choice = Some(pezsc_consensus::ForkChoiceStrategy::Custom(
block_params.origin == pezsp_consensus::BlockOrigin::NetworkInitialSync,
));
// Skip checks that include execution, if being told so, or when importing only state.
//
// This is done for example when gap syncing and it is expected that the block after the gap
// was checked/chosen properly, e.g. by warp syncing to this block using a finality proof.
if block_params.state_action.skip_execution_checks() || block_params.with_state() {
return Ok(block_params);
}
if let Some(inner_body) = block_params.body.take() {
let inherent_data_providers = self
.create_inherent_data_providers
.create_inherent_data_providers(*block_params.header.parent_hash(), ())
.await
.map_err(|e| e.to_string())?;
let block = Block::new(block_params.header.clone(), inner_body);
pezsp_block_builder::check_inherents(
self.client.clone(),
*block.header().parent_hash(),
block.clone(),
&inherent_data_providers,
)
.await
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
let (_, inner_body) = block.deconstruct();
block_params.body = Some(inner_body);
}
block_params.post_hash = Some(block_params.header.hash());
Ok(block_params)
}
}
@@ -0,0 +1,39 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! The relay-chain provided consensus algorithm for teyrchains.
//!
//! This is the simplest consensus algorithm you can use when developing a teyrchain. It is a
//! permission-less consensus algorithm that doesn't require any staking or similar to join as a
//! collator. In this algorithm the consensus is provided by the relay-chain. This works in the
//! following way.
//!
//! 1. Each node that sees itself as a collator is free to build a teyrchain candidate.
//!
//! 2. This teyrchain candidate is send to the teyrchain validators that are part of the relay
//! chain.
//!
//! 3. The teyrchain validators validate at most X different teyrchain candidates, where X is the
//! total number of teyrchain validators.
//!
//! 4. The teyrchain candidate that is backed by the most validators is chosen by the relay-chain
//! block producer to be added as backed candidate on chain.
//!
//! 5. After the teyrchain candidate got backed and included, all collators start at 1.
mod import_queue;
pub use import_queue::Verifier;
+80
View File
@@ -0,0 +1,80 @@
[package]
name = "pezcumulus-client-network"
version = "0.7.0"
authors.workspace = true
description = "Pezcumulus-specific networking protocol"
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
pezkuwi-primitives-test-helpers = { workspace = true, default-features = true }
pezkuwi-teyrchain-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[dev-dependencies]
rstest = { workspace = true }
tokio = { features = ["macros"], workspace = true, default-features = true }
# Bizinikiwi
pezsp-keyring = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-test-client = { workspace = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-inprocess-interface = { workspace = true, default-features = true }
pezcumulus-test-service = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-inprocess-interface/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-test-service/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-test-client/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
+543
View File
@@ -0,0 +1,543 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Teyrchain specific networking
//!
//! Provides a custom block announcement implementation for teyrchains
//! that use the relay chain provided consensus. See [`RequireSecondedInBlockAnnounce`]
//! and [`WaitToAnnounce`] for more information about this implementation.
use pezsp_api::RuntimeApiInfo;
use pezsp_consensus::block_validation::{
BlockAnnounceValidator as BlockAnnounceValidatorT, Validation,
};
use pezsp_core::traits::SpawnNamed;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::{CollationSecondedSignal, Statement};
use pezkuwi_node_subsystem::messages::RuntimeApiRequest;
use pezkuwi_primitives::{
CandidateReceiptV2 as CandidateReceipt, CompactStatement, Hash as PHash, Id as ParaId,
OccupiedCoreAssumption, SigningContext, UncheckedSigned,
};
use pezkuwi_teyrchain_primitives::primitives::HeadData;
use codec::{Decode, DecodeAll, Encode};
use futures::{channel::oneshot, future::FutureExt, Future};
use std::{fmt, marker::PhantomData, pin::Pin, sync::Arc};
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "sync::pezcumulus";
type BoxedError = Box<dyn std::error::Error + Send>;
#[derive(Debug)]
struct BlockAnnounceError(String);
impl std::error::Error for BlockAnnounceError {}
impl fmt::Display for BlockAnnounceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// The data that we attach to a block announcement.
///
/// This will be used to prove that a header belongs to a block that is probably being backed by
/// the relay chain.
#[derive(Encode, Debug)]
pub struct BlockAnnounceData {
/// The receipt identifying the candidate.
receipt: CandidateReceipt,
/// The seconded statement issued by a relay chain validator that approves the candidate.
statement: UncheckedSigned<CompactStatement>,
/// The relay parent that was used as context to sign the [`Self::statement`].
relay_parent: PHash,
}
impl Decode for BlockAnnounceData {
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
let receipt = CandidateReceipt::decode(input)?;
let statement = UncheckedSigned::<CompactStatement>::decode(input)?;
let relay_parent = match PHash::decode(input) {
Ok(p) => p,
// For being backwards compatible, we support missing relay-chain parent.
Err(_) => receipt.descriptor.relay_parent(),
};
Ok(Self { receipt, statement, relay_parent })
}
}
impl BlockAnnounceData {
/// Validate that the receipt, statement and announced header match.
///
/// This will not check the signature, for this you should use
/// [`BlockAnnounceData::check_signature`].
fn validate(&self, encoded_header: Vec<u8>) -> Result<(), Validation> {
let candidate_hash =
if let CompactStatement::Seconded(h) = self.statement.unchecked_payload() {
h
} else {
tracing::debug!(target: LOG_TARGET, "`CompactStatement` isn't the candidate variant!",);
return Err(Validation::Failure { disconnect: true });
};
if *candidate_hash != self.receipt.hash() {
tracing::debug!(
target: LOG_TARGET,
"Receipt candidate hash doesn't match candidate hash in statement",
);
return Err(Validation::Failure { disconnect: true });
}
if HeadData(encoded_header).hash() != self.receipt.descriptor.para_head() {
tracing::debug!(
target: LOG_TARGET,
"Receipt para head hash doesn't match the hash of the header in the block announcement",
);
return Err(Validation::Failure { disconnect: true });
}
Ok(())
}
/// Check the signature of the statement.
///
/// Returns an `Err(_)` if it failed.
async fn check_signature<RCInterface>(
self,
relay_chain_client: &RCInterface,
) -> Result<Validation, BlockAnnounceError>
where
RCInterface: RelayChainInterface + 'static,
{
let validator_index = self.statement.unchecked_validator_index();
let session_index =
match relay_chain_client.session_index_for_child(self.relay_parent).await {
Ok(r) => r,
Err(e) => return Err(BlockAnnounceError(format!("{:?}", e))),
};
let signing_context = SigningContext { parent_hash: self.relay_parent, session_index };
// Check that the signer is a legit validator.
let authorities = match relay_chain_client.validators(self.relay_parent).await {
Ok(r) => r,
Err(e) => return Err(BlockAnnounceError(format!("{:?}", e))),
};
let signer = match authorities.get(validator_index.0 as usize) {
Some(r) => r,
None => {
tracing::debug!(
target: LOG_TARGET,
"Block announcement justification signer is a validator index out of bound",
);
return Ok(Validation::Failure { disconnect: true });
},
};
// Check statement is correctly signed.
if self.statement.try_into_checked(&signing_context, signer).is_err() {
tracing::debug!(
target: LOG_TARGET,
"Block announcement justification signature is invalid.",
);
return Ok(Validation::Failure { disconnect: true });
}
Ok(Validation::Success { is_new_best: true })
}
}
impl TryFrom<&'_ CollationSecondedSignal> for BlockAnnounceData {
type Error = ();
fn try_from(signal: &CollationSecondedSignal) -> Result<BlockAnnounceData, ()> {
let receipt = if let Statement::Seconded(receipt) = signal.statement.payload() {
receipt.to_plain()
} else {
return Err(());
};
Ok(BlockAnnounceData {
receipt,
statement: signal.statement.convert_payload().into(),
relay_parent: signal.relay_parent,
})
}
}
/// A type alias for the [`RequireSecondedInBlockAnnounce`] validator.
#[deprecated = "This has been renamed to RequireSecondedInBlockAnnounce"]
pub type BlockAnnounceValidator<Block, RCInterface> =
RequireSecondedInBlockAnnounce<Block, RCInterface>;
/// Teyrchain specific block announce validator.
///
/// This is not required when the collation mechanism itself is sybil-resistant, as it is a spam
/// protection mechanism used to prevent nodes from dealing with unbounded numbers of blocks. For
/// sybil-resistant collation mechanisms, this will only slow things down.
///
/// This block announce validator is required if the teyrchain is running
/// with the relay chain provided consensus to make sure each node only
/// imports a reasonable number of blocks per round. The relay chain provided
/// consensus doesn't have any authorities and so it could happen that without
/// this special block announce validator a node would need to import *millions*
/// of blocks per round, which is clearly not doable.
///
/// To solve this problem, each block announcement is delayed until a collator
/// has received a [`Statement::Seconded`] for its `PoV`. This message tells the
/// collator that its `PoV` was validated successfully by a teyrchain validator and
/// that it is very likely that this `PoV` will be included in the relay chain. Every
/// collator that doesn't receive the message for its `PoV` will not announce its block.
/// For more information on the block announcement, see [`WaitToAnnounce`].
///
/// For each block announcement that is received, the generic block announcement validation
/// will call this validator and provides the extra data that was attached to the announcement.
/// We call this extra data `justification`.
/// It is expected that the attached data is a SCALE encoded [`BlockAnnounceData`]. The
/// statement is checked to be a [`CompactStatement::Seconded`] and that it is signed by an active
/// teyrchain validator.
///
/// If no justification was provided we check if the block announcement is at the tip of the known
/// chain. If it is at the tip, it is required to provide a justification or otherwise we reject
/// it. However, if the announcement is for a block below the tip the announcement is accepted
/// as it probably comes from a node that is currently syncing the chain.
#[derive(Clone)]
pub struct RequireSecondedInBlockAnnounce<Block, RCInterface> {
phantom: PhantomData<Block>,
relay_chain_interface: RCInterface,
para_id: ParaId,
}
impl<Block, RCInterface> RequireSecondedInBlockAnnounce<Block, RCInterface>
where
RCInterface: Clone,
{
/// Create a new [`RequireSecondedInBlockAnnounce`].
pub fn new(relay_chain_interface: RCInterface, para_id: ParaId) -> Self {
Self { phantom: Default::default(), relay_chain_interface, para_id }
}
}
impl<Block: BlockT, RCInterface> RequireSecondedInBlockAnnounce<Block, RCInterface>
where
RCInterface: RelayChainInterface + Clone,
{
/// Get the included block of the given teyrchain in the relay chain.
async fn included_block(
relay_chain_interface: &RCInterface,
hash: PHash,
para_id: ParaId,
) -> Result<Block::Header, BoxedError> {
let validation_data = relay_chain_interface
.persisted_validation_data(hash, para_id, OccupiedCoreAssumption::TimedOut)
.await
.map_err(|e| Box::new(BlockAnnounceError(format!("{:?}", e))) as Box<_>)?
.ok_or_else(|| {
Box::new(BlockAnnounceError("Could not find teyrchain head in relay chain".into()))
as Box<_>
})?;
let para_head =
Block::Header::decode(&mut &validation_data.parent_head.0[..]).map_err(|e| {
Box::new(BlockAnnounceError(format!("Failed to decode teyrchain head: {:?}", e)))
as Box<_>
})?;
Ok(para_head)
}
/// Get the backed block hashes of the given teyrchain in the relay chain.
async fn backed_block_hashes(
relay_chain_interface: &RCInterface,
hash: PHash,
para_id: ParaId,
) -> Result<impl Iterator<Item = PHash>, BoxedError> {
let runtime_api_version = relay_chain_interface
.version(hash)
.await
.map_err(|e| Box::new(BlockAnnounceError(format!("{:?}", e))) as Box<_>)?;
let teyrchain_host_runtime_api_version =
runtime_api_version
.api_version(
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<
pezkuwi_primitives::Block,
>>::ID,
)
.unwrap_or_default();
// If the relay chain runtime does not support the new runtime API, fallback to the
// deprecated one.
let candidate_receipts = if teyrchain_host_runtime_api_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
#[allow(deprecated)]
relay_chain_interface
.candidate_pending_availability(hash, para_id)
.await
.map(|c| c.into_iter().collect::<Vec<_>>())
} else {
relay_chain_interface.candidates_pending_availability(hash, para_id).await
}
.map_err(|e| Box::new(BlockAnnounceError(format!("{:?}", e))) as Box<_>)?;
Ok(candidate_receipts.into_iter().map(|cr| cr.descriptor.para_head()))
}
/// Handle a block announcement with empty data (no statement) attached to it.
async fn handle_empty_block_announce_data(
&self,
header: Block::Header,
) -> Result<Validation, BoxedError> {
let relay_chain_interface = self.relay_chain_interface.clone();
let para_id = self.para_id;
// Check if block is equal or higher than best (this requires a justification)
let relay_chain_best_hash = relay_chain_interface
.best_block_hash()
.await
.map_err(|e| Box::new(e) as Box<_>)?;
let block_number = header.number();
let best_head =
Self::included_block(&relay_chain_interface, relay_chain_best_hash, para_id).await?;
let known_best_number = best_head.number();
if best_head == header {
tracing::debug!(target: LOG_TARGET, "Announced block matches best block.",);
return Ok(Validation::Success { is_new_best: true });
}
let mut backed_blocks =
Self::backed_block_hashes(&relay_chain_interface, relay_chain_best_hash, para_id)
.await?;
let head_hash = HeadData(header.encode()).hash();
if backed_blocks.any(|block_hash| block_hash == head_hash) {
tracing::debug!(target: LOG_TARGET, "Announced block matches latest backed block.",);
Ok(Validation::Success { is_new_best: true })
} else if block_number >= known_best_number {
tracing::debug!(
target: LOG_TARGET,
"Validation failed because a justification is needed if the block at the top of the chain."
);
Ok(Validation::Failure { disconnect: false })
} else {
Ok(Validation::Success { is_new_best: false })
}
}
}
impl<Block: BlockT, RCInterface> BlockAnnounceValidatorT<Block>
for RequireSecondedInBlockAnnounce<Block, RCInterface>
where
RCInterface: RelayChainInterface + Clone + 'static,
{
fn validate(
&mut self,
header: &Block::Header,
data: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Validation, BoxedError>> + Send>> {
let relay_chain_interface = self.relay_chain_interface.clone();
let data = data.to_vec();
let header = header.clone();
let header_encoded = header.encode();
let block_announce_validator = self.clone();
async move {
let relay_chain_is_syncing = relay_chain_interface
.is_major_syncing()
.await
.map_err(
|e| tracing::error!(target: LOG_TARGET, "Unable to determine sync status. {}", e),
)
.unwrap_or(false);
if relay_chain_is_syncing {
return Ok(Validation::Success { is_new_best: false });
}
if data.is_empty() {
return block_announce_validator.handle_empty_block_announce_data(header).await;
}
let block_announce_data = match BlockAnnounceData::decode_all(&mut data.as_slice()) {
Ok(r) => r,
Err(err) =>
return Err(Box::new(BlockAnnounceError(format!(
"Can not decode the `BlockAnnounceData`: {:?}",
err
))) as Box<_>),
};
if let Err(e) = block_announce_data.validate(header_encoded) {
return Ok(e);
}
let relay_parent = block_announce_data.receipt.descriptor.relay_parent();
relay_chain_interface
.wait_for_block(relay_parent)
.await
.map_err(|e| Box::new(BlockAnnounceError(e.to_string())) as Box<_>)?;
block_announce_data
.check_signature(&relay_chain_interface)
.await
.map_err(|e| Box::new(e) as Box<_>)
}
.boxed()
}
}
/// Wait before announcing a block that a candidate message has been received for this block, then
/// add this message as justification for the block announcement.
///
/// This object will spawn a new task every time the method `wait_to_announce` is called and cancel
/// the previous task running.
pub struct WaitToAnnounce<Block: BlockT> {
spawner: Arc<dyn SpawnNamed + Send + Sync>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
}
impl<Block: BlockT> WaitToAnnounce<Block> {
/// Create the `WaitToAnnounce` object
pub fn new(
spawner: Arc<dyn SpawnNamed + Send + Sync>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
) -> WaitToAnnounce<Block> {
WaitToAnnounce { spawner, announce_block }
}
/// Wait for a candidate message for the block, then announce the block. The candidate
/// message will be added as justification to the block announcement.
pub fn wait_to_announce(
&mut self,
block_hash: <Block as BlockT>::Hash,
signed_stmt_recv: oneshot::Receiver<CollationSecondedSignal>,
) {
let announce_block = self.announce_block.clone();
self.spawner.spawn(
"pezcumulus-wait-to-announce",
None,
async move {
tracing::debug!(
target: "pezcumulus-network",
"waiting for announce block in a background task...",
);
wait_to_announce::<Block>(block_hash, announce_block, signed_stmt_recv).await;
tracing::debug!(
target: "pezcumulus-network",
"block announcement finished",
);
}
.boxed(),
);
}
}
async fn wait_to_announce<Block: BlockT>(
block_hash: <Block as BlockT>::Hash,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
signed_stmt_recv: oneshot::Receiver<CollationSecondedSignal>,
) {
let signal = match signed_stmt_recv.await {
Ok(s) => s,
Err(_) => {
tracing::debug!(
target: "pezcumulus-network",
block = ?block_hash,
"Wait to announce stopped, because sender was dropped.",
);
return;
},
};
if let Ok(data) = BlockAnnounceData::try_from(&signal) {
announce_block(block_hash, Some(data.encode()));
} else {
tracing::debug!(
target: "pezcumulus-network",
?signal,
block = ?block_hash,
"Received invalid statement while waiting to announce block.",
);
}
}
/// A [`BlockAnnounceValidator`] which accepts all block announcements, as it assumes
/// sybil resistance is handled elsewhere.
#[derive(Debug, Clone)]
pub struct AssumeSybilResistance(bool);
impl AssumeSybilResistance {
/// Instantiate this block announcement validator while permissively allowing (but ignoring)
/// announcements which come tagged with seconded messages.
///
/// This is useful for backwards compatibility when upgrading nodes: old nodes will continue
/// to broadcast announcements with seconded messages, so these announcements shouldn't be
/// rejected and the peers not punished.
pub fn allow_seconded_messages() -> Self {
AssumeSybilResistance(true)
}
/// Instantiate this block announcement validator while rejecting announcements that come with
/// data.
pub fn reject_seconded_messages() -> Self {
AssumeSybilResistance(false)
}
}
impl<Block: BlockT> BlockAnnounceValidatorT<Block> for AssumeSybilResistance {
fn validate(
&mut self,
_header: &Block::Header,
data: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Validation, BoxedError>> + Send>> {
let allow_seconded_messages = self.0;
let data = data.to_vec();
async move {
Ok(if data.is_empty() {
Validation::Success { is_new_best: false }
} else if !allow_seconded_messages {
Validation::Failure { disconnect: false }
} else {
match BlockAnnounceData::decode_all(&mut data.as_slice()) {
Ok(_) => Validation::Success { is_new_best: false },
Err(_) => Validation::Failure { disconnect: true },
}
})
}
.boxed()
}
}
+689
View File
@@ -0,0 +1,689 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use async_trait::async_trait;
use cumulus_primitives_core::relay_chain::{BlockId, CoreIndex};
use cumulus_relay_chain_inprocess_interface::{check_block_in_chain, BlockCheckStatus};
use cumulus_relay_chain_interface::{
OverseerHandle, PHeader, ParaId, RelayChainError, RelayChainResult,
};
use cumulus_test_service::runtime::{Block, Hash, Header};
use futures::{executor::block_on, poll, task::Poll, FutureExt, Stream, StreamExt};
use parking_lot::Mutex;
use pezkuwi_node_primitives::{SignedFullStatement, Statement};
use pezkuwi_primitives::{
BlockNumber, CandidateCommitments, CandidateDescriptorV2, CandidateEvent, CollatorPair,
CommittedCandidateReceiptV2, CoreState, Hash as PHash, HeadData, InboundDownwardMessage,
InboundHrmpMessage, OccupiedCoreAssumption, PersistedValidationData, SessionIndex,
SigningContext, ValidationCodeHash, ValidatorId,
};
use pezkuwi_primitives_test_helpers::{CandidateDescriptor, CommittedCandidateReceipt};
use pezkuwi_test_client::{
Client as PClient, ClientBlockImportExt, DefaultTestClientBuilderExt, FullBackend as PBackend,
InitPezkuwiBlockBuilder, TestClientBuilder, TestClientBuilderExt,
};
use rstest::rstest;
use pezsc_client_api::{Backend, BlockchainEvents};
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus::BlockOrigin;
use pezsp_core::{Pair, H256};
use pezsp_keyring::Sr25519Keyring;
use pezsp_keystore::{testing::MemoryKeystore, Keystore, KeystorePtr};
use pezsp_runtime::RuntimeAppPublic;
use pezsp_state_machine::StorageValue;
use pezsp_version::RuntimeVersion;
use std::{
borrow::Cow,
collections::{BTreeMap, VecDeque},
time::Duration,
};
fn check_error(error: crate::BoxedError, check_error: impl Fn(&BlockAnnounceError) -> bool) {
let error = *error
.downcast::<BlockAnnounceError>()
.expect("Downcasts error to `ClientError`");
if !check_error(&error) {
panic!("Invalid error: {:?}", error);
}
}
fn dummy_candidate() -> CommittedCandidateReceiptV2 {
CommittedCandidateReceiptV2 {
descriptor: CandidateDescriptorV2::new(
0u32.into(),
PHash::random(),
0.into(),
1,
PHash::random(),
PHash::random(),
PHash::random(),
pezkuwi_teyrchain_primitives::primitives::HeadData(default_header().encode()).hash(),
ValidationCodeHash::from(PHash::random()),
),
commitments: CandidateCommitments {
upward_messages: Default::default(),
horizontal_messages: Default::default(),
new_validation_code: None,
head_data: HeadData(Vec::new()),
processed_downward_messages: 0,
hrmp_watermark: 0,
},
}
}
#[derive(Clone)]
struct DummyRelayChainInterface {
data: Arc<Mutex<ApiData>>,
relay_client: Arc<PClient>,
relay_backend: Arc<PBackend>,
}
impl DummyRelayChainInterface {
fn new() -> Self {
let builder = TestClientBuilder::new();
let relay_backend = builder.backend();
Self {
data: Arc::new(Mutex::new(ApiData {
validators: vec![Sr25519Keyring::Alice.public().into()],
has_pending_availability: false,
runtime_version:
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT,
})),
relay_client: Arc::new(builder.build()),
relay_backend,
}
}
}
#[async_trait]
impl RelayChainInterface for DummyRelayChainInterface {
async fn validators(&self, _: PHash) -> RelayChainResult<Vec<ValidatorId>> {
Ok(self.data.lock().validators.clone())
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.relay_backend.blockchain().info().best_hash)
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.relay_backend.blockchain().info().finalized_hash)
}
async fn retrieve_dmq_contents(
&self,
_: ParaId,
_: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
unimplemented!("Not needed for test")
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
_: ParaId,
_: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
Ok(BTreeMap::new())
}
async fn persisted_validation_data(
&self,
_: PHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
Ok(Some(PersistedValidationData {
parent_head: HeadData(default_header().encode()),
..Default::default()
}))
}
async fn validation_code_hash(
&self,
_: PHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
unimplemented!("Not needed for test")
}
async fn candidate_pending_availability(
&self,
_: PHash,
_: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceiptV2>> {
if self.data.lock().runtime_version >=
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
panic!("Should have used candidates_pending_availability instead");
}
if self.data.lock().has_pending_availability {
Ok(Some(dummy_candidate()))
} else {
Ok(None)
}
}
async fn candidates_pending_availability(
&self,
_: PHash,
_: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceiptV2>> {
if self.data.lock().runtime_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
panic!("Should have used candidate_pending_availability instead");
}
if self.data.lock().has_pending_availability {
Ok(vec![dummy_candidate()])
} else {
Ok(vec![])
}
}
async fn session_index_for_child(&self, _: PHash) -> RelayChainResult<SessionIndex> {
Ok(0)
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
Ok(Box::pin(
self.relay_client
.import_notification_stream()
.map(|notification| notification.header),
))
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
Ok(Box::pin(
self.relay_client
.finality_notification_stream()
.map(|notification| notification.header),
))
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
Ok(false)
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
unimplemented!("Not needed for test")
}
async fn get_storage_by_key(
&self,
_: PHash,
_: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
unimplemented!("Not needed for test")
}
async fn prove_read(
&self,
_: PHash,
_: &Vec<Vec<u8>>,
) -> RelayChainResult<pezsc_client_api::StorageProof> {
unimplemented!("Not needed for test")
}
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()> {
let mut listener = match check_block_in_chain(
self.relay_backend.clone(),
self.relay_client.clone(),
hash,
)? {
BlockCheckStatus::InChain => return Ok(()),
BlockCheckStatus::Unknown(listener) => listener,
};
let mut timeout = futures_timer::Delay::new(Duration::from_secs(10)).fuse();
loop {
futures::select! {
_ = timeout => return Err(RelayChainError::WaitTimeout(hash)),
evt = listener.next() => match evt {
Some(evt) if evt.hash == hash => return Ok(()),
// Not the event we waited on.
Some(_) => continue,
None => return Err(RelayChainError::ImportListenerClosed(hash)),
}
}
}
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notifications_stream =
self.relay_client
.import_notification_stream()
.filter_map(|notification| async move {
if notification.is_new_best {
Some(notification.header)
} else {
None
}
});
Ok(Box::pin(notifications_stream))
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(num) =>
if let Some(hash) = self.relay_client.hash(num)? {
hash
} else {
return Ok(None);
},
};
let header = self.relay_client.header(hash)?;
Ok(header)
}
async fn availability_cores(
&self,
_relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
unimplemented!("Not needed for test");
}
async fn version(&self, _: PHash) -> RelayChainResult<RuntimeVersion> {
let version = self.data.lock().runtime_version;
let apis = pezsp_version::create_apis_vec!([(
<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<pezkuwi_primitives::Block>>::ID,
version
)])
.into_owned()
.to_vec();
Ok(RuntimeVersion {
spec_name: Cow::Borrowed("test"),
impl_name: Cow::Borrowed("test"),
authoring_version: 1,
spec_version: 1,
impl_version: 0,
apis: Cow::Owned(apis),
transaction_version: 5,
system_version: 1,
})
}
async fn claim_queue(
&self,
_: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
unimplemented!("Not needed for test");
}
async fn call_runtime_api(
&self,
_method_name: &'static str,
_hash: PHash,
_payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
unimplemented!("Not needed for test")
}
async fn scheduling_lookahead(&self, _: PHash) -> RelayChainResult<u32> {
unimplemented!("Not needed for test")
}
async fn candidate_events(&self, _: PHash) -> RelayChainResult<Vec<CandidateEvent>> {
unimplemented!("Not needed for test")
}
}
fn make_validator_and_api() -> (
RequireSecondedInBlockAnnounce<Block, Arc<DummyRelayChainInterface>>,
Arc<DummyRelayChainInterface>,
) {
let relay_chain_interface = Arc::new(DummyRelayChainInterface::new());
(
RequireSecondedInBlockAnnounce::new(relay_chain_interface.clone(), ParaId::from(56)),
relay_chain_interface,
)
}
fn default_header() -> Header {
Header {
number: 1,
digest: Default::default(),
extrinsics_root: Default::default(),
parent_hash: Default::default(),
state_root: Default::default(),
}
}
/// Same as [`make_gossip_message_and_header`], but using the genesis header as relay parent.
async fn make_gossip_message_and_header_using_genesis(
api: Arc<DummyRelayChainInterface>,
validator_index: u32,
) -> (CollationSecondedSignal, Header) {
let relay_parent = api.relay_client.hash(0).ok().flatten().expect("Genesis hash exists");
make_gossip_message_and_header(api, relay_parent, validator_index).await
}
async fn make_gossip_message_and_header(
relay_chain_interface: Arc<DummyRelayChainInterface>,
relay_parent: H256,
validator_index: u32,
) -> (CollationSecondedSignal, Header) {
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let alice_public = Keystore::sr25519_generate_new(
&*keystore,
ValidatorId::ID,
Some(&Sr25519Keyring::Alice.to_seed()),
)
.unwrap();
let session_index = relay_chain_interface.session_index_for_child(relay_parent).await.unwrap();
let signing_context = SigningContext { parent_hash: relay_parent, session_index };
let header = default_header();
let candidate_receipt = CommittedCandidateReceipt {
commitments: CandidateCommitments {
head_data: header.encode().into(),
..Default::default()
},
descriptor: CandidateDescriptor {
para_id: 0u32.into(),
relay_parent,
collator: CollatorPair::generate().0.public(),
persisted_validation_data_hash: PHash::random(),
pov_hash: PHash::random(),
erasure_root: PHash::random(),
signature: pezsp_core::sr25519::Signature::default().into(),
para_head: pezkuwi_teyrchain_primitives::primitives::HeadData(header.encode()).hash(),
validation_code_hash: ValidationCodeHash::from(PHash::random()),
},
};
let statement = Statement::Seconded(candidate_receipt.into());
let signed = SignedFullStatement::sign(
&keystore,
statement,
&signing_context,
validator_index.into(),
&alice_public.into(),
)
.ok()
.flatten()
.expect("Signing statement");
(CollationSecondedSignal { statement: signed, relay_parent }, header)
}
#[test]
fn valid_if_no_data_and_less_than_best_known_number() {
let mut validator = make_validator_and_api().0;
let header = Header { number: 0, ..default_header() };
let res = block_on(validator.validate(&header, &[]));
assert_eq!(
res.unwrap(),
Validation::Success { is_new_best: false },
"validating without data with block number < best known number is always a success",
);
}
#[test]
fn invalid_if_no_data_exceeds_best_known_number() {
let mut validator = make_validator_and_api().0;
let header = Header { number: 1, state_root: Hash::random(), ..default_header() };
let res = block_on(validator.validate(&header, &[]));
assert_eq!(
res.unwrap(),
Validation::Failure { disconnect: false },
"validation fails if no justification and block number >= best known number",
);
}
#[test]
fn valid_if_no_data_and_block_matches_best_known_block() {
let mut validator = make_validator_and_api().0;
let res = block_on(validator.validate(&default_header(), &[]));
assert_eq!(
res.unwrap(),
Validation::Success { is_new_best: true },
"validation is successful when the block hash matches the best known block",
);
}
#[test]
fn check_statement_is_encoded_correctly() {
let mut validator = make_validator_and_api().0;
let header = default_header();
let res = block_on(validator.validate(&header, &[0x42]))
.expect_err("Should fail on invalid encoded statement");
check_error(res, |error| {
matches!(
error,
BlockAnnounceError(x) if x.contains("Can not decode the `BlockAnnounceData`")
)
});
}
#[test]
fn block_announce_data_decoding_should_reject_extra_data() {
let (mut validator, api) = make_validator_and_api();
let (signal, header) = block_on(make_gossip_message_and_header_using_genesis(api, 1));
let mut data = BlockAnnounceData::try_from(&signal).unwrap().encode();
data.push(0x42);
let res = block_on(validator.validate(&header, &data)).expect_err("Should return an error ");
check_error(res, |error| {
matches!(
error,
BlockAnnounceError(x) if x.contains("Input buffer has still data left after decoding!")
)
});
}
#[derive(Encode, Decode, Debug)]
struct LegacyBlockAnnounceData {
receipt: CandidateReceipt,
statement: UncheckedSigned<CompactStatement>,
}
#[test]
fn legacy_block_announce_data_handling() {
let (_, api) = make_validator_and_api();
let (signal, _) = block_on(make_gossip_message_and_header_using_genesis(api, 1));
let receipt = if let Statement::Seconded(receipt) = signal.statement.payload() {
receipt.to_plain()
} else {
panic!("Invalid")
};
let legacy = LegacyBlockAnnounceData {
receipt: receipt.clone(),
statement: signal.statement.convert_payload().into(),
};
let data = legacy.encode();
let block_data =
BlockAnnounceData::decode(&mut &data[..]).expect("Decoding works from legacy works");
assert_eq!(receipt.descriptor.relay_parent(), block_data.relay_parent);
let data = block_data.encode();
LegacyBlockAnnounceData::decode(&mut &data[..]).expect("Decoding works");
}
#[test]
fn check_signer_is_legit_validator() {
let (mut validator, api) = make_validator_and_api();
let (signal, header) = block_on(make_gossip_message_and_header_using_genesis(api, 1));
let data = BlockAnnounceData::try_from(&signal).unwrap().encode();
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
#[test]
fn check_statement_is_correctly_signed() {
let (mut validator, api) = make_validator_and_api();
let (signal, header) = block_on(make_gossip_message_and_header_using_genesis(api, 0));
let mut data = BlockAnnounceData::try_from(&signal).unwrap().encode();
// The signature comes at the end of the type, so change a bit to make the signature invalid.
let last = data.len() - 1;
data[last] = data[last].wrapping_add(1);
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
#[tokio::test]
async fn check_statement_seconded() {
let (mut validator, relay_chain_interface) = make_validator_and_api();
let header = default_header();
let relay_parent = H256::from_low_u64_be(1);
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let alice_public = Keystore::sr25519_generate_new(
&*keystore,
ValidatorId::ID,
Some(&Sr25519Keyring::Alice.to_seed()),
)
.unwrap();
let session_index = relay_chain_interface.session_index_for_child(relay_parent).await.unwrap();
let signing_context = SigningContext { parent_hash: relay_parent, session_index };
let statement = Statement::Valid(Default::default());
let signed_statement = SignedFullStatement::sign(
&keystore,
statement,
&signing_context,
0.into(),
&alice_public.into(),
)
.ok()
.flatten()
.expect("Signs statement");
let data = BlockAnnounceData {
receipt: CandidateReceipt {
commitments_hash: PHash::random(),
descriptor: CandidateDescriptor {
para_head: HeadData(Vec::new()).hash(),
para_id: 0u32.into(),
relay_parent: PHash::random(),
collator: CollatorPair::generate().0.public(),
persisted_validation_data_hash: PHash::random(),
pov_hash: PHash::random(),
erasure_root: PHash::random(),
signature: pezsp_core::sr25519::Signature::default().into(),
validation_code_hash: ValidationCodeHash::from(PHash::random()),
}
.into(),
},
statement: signed_statement.convert_payload().into(),
relay_parent,
}
.encode();
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
#[test]
fn check_header_match_candidate_receipt_header() {
let (mut validator, api) = make_validator_and_api();
let (signal, mut header) = block_on(make_gossip_message_and_header_using_genesis(api, 0));
let data = BlockAnnounceData::try_from(&signal).unwrap().encode();
header.number = 300;
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
/// Test that ensures that we postpone the block announce verification until
/// a relay chain block is imported. This is important for when we receive a
/// block announcement before we have imported the associated relay chain block
/// which can happen on slow nodes or nodes with a slow network connection.
#[test]
fn relay_parent_not_imported_when_block_announce_is_processed() {
block_on(async move {
let (mut validator, api) = make_validator_and_api();
let client = api.relay_client.clone();
let block = client.init_pezkuwi_block_builder().build().expect("Build new block").block;
let (signal, header) = make_gossip_message_and_header(api, block.hash(), 0).await;
let data = BlockAnnounceData::try_from(&signal).unwrap().encode();
let mut validation = validator.validate(&header, &data);
// The relay chain block is not available yet, so the first poll should return
// that the future is still pending.
assert!(poll!(&mut validation).is_pending());
client.import(BlockOrigin::Own, block).await.expect("Imports the block");
assert!(matches!(
poll!(validation),
Poll::Ready(Ok(Validation::Success { is_new_best: true }))
));
});
}
/// Ensures that when we receive a block announcement without a statement included, while the block
/// is not yet included by the node checking the announcement, but the node is already backed.
#[rstest]
#[case(RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT)]
#[case(10)]
fn block_announced_without_statement_and_block_only_backed(#[case] runtime_version: u32) {
block_on(async move {
let (mut validator, api) = make_validator_and_api();
api.data.lock().has_pending_availability = true;
api.data.lock().runtime_version = runtime_version;
let header = default_header();
let validation = validator.validate(&header, &[]);
assert!(matches!(validation.await, Ok(Validation::Success { is_new_best: true })));
});
}
#[derive(Default)]
struct ApiData {
validators: Vec<ValidatorId>,
has_pending_availability: bool,
runtime_version: u32,
}
+72
View File
@@ -0,0 +1,72 @@
[package]
name = "pezcumulus-client-pov-recovery"
version = "0.7.0"
authors.workspace = true
description = "Teyrchain PoV recovery"
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
rand = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-maybe-compressed-blob = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
async-trait = { workspace = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
[dev-dependencies]
assert_matches = { workspace = true }
rstest = { workspace = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
tokio = { features = ["macros"], workspace = true, default-features = true }
# Pezcumulus
pezcumulus-test-client = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-streams/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,103 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use pezsp_runtime::traits::Block as BlockT;
use pezkuwi_node_primitives::PoV;
use pezkuwi_node_subsystem::messages::AvailabilityRecoveryMessage;
use futures::{channel::oneshot, stream::FuturesUnordered, Future, FutureExt, StreamExt};
use std::{pin::Pin, sync::Arc};
use crate::RecoveryHandle;
/// The active candidate recovery.
///
/// This handles the candidate recovery and tracks the activate recoveries.
pub(crate) struct ActiveCandidateRecovery<Block: BlockT> {
/// The recoveries that are currently being executed.
recoveries:
FuturesUnordered<Pin<Box<dyn Future<Output = (Block::Hash, Option<Arc<PoV>>)> + Send>>>,
recovery_handle: Box<dyn RecoveryHandle>,
}
impl<Block: BlockT> ActiveCandidateRecovery<Block> {
pub fn new(recovery_handle: Box<dyn RecoveryHandle>) -> Self {
Self { recoveries: Default::default(), recovery_handle }
}
/// Recover the given `candidate`.
pub async fn recover_candidate(
&mut self,
block_hash: Block::Hash,
candidate: &crate::Candidate<Block>,
) {
let (tx, rx) = oneshot::channel();
self.recovery_handle
.send_recovery_msg(
AvailabilityRecoveryMessage::RecoverAvailableData(
candidate.receipt.clone(),
candidate.session_index,
None,
None,
tx,
),
"ActiveCandidateRecovery",
)
.await;
self.recoveries.push(
async move {
match rx.await {
Ok(Ok(res)) => (block_hash, Some(res.pov)),
Ok(Err(error)) => {
tracing::debug!(
target: crate::LOG_TARGET,
?error,
?block_hash,
"Availability recovery failed",
);
(block_hash, None)
},
Err(_) => {
tracing::debug!(
target: crate::LOG_TARGET,
"Availability recovery oneshot channel closed",
);
(block_hash, None)
},
}
}
.boxed(),
);
}
/// Waits for the next recovery.
///
/// If the returned [`PoV`] is `None`, it means that the recovery failed.
pub async fn wait_for_recovery(&mut self) -> (Block::Hash, Option<Arc<PoV>>) {
loop {
if let Some(res) = self.recoveries.next().await {
return res;
} else {
futures::pending!()
}
}
}
}
+662
View File
@@ -0,0 +1,662 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Teyrchain PoV recovery
//!
//! A teyrchain needs to build PoVs that are send to the relay chain to progress. These PoVs are
//! erasure encoded and one piece of it is stored by each relay chain validator. As the relay chain
//! decides on which PoV per teyrchain to include and thus, to progress the teyrchain it can happen
//! that the block corresponding to this PoV isn't propagated in the teyrchain network. This can
//! have several reasons, either a malicious collator that managed to include its own PoV and
//! doesn't want to share it with the rest of the network or maybe a collator went down before it
//! could distribute the block in the network. When something like this happens we can use the PoV
//! recovery algorithm implemented in this crate to recover a PoV and to propagate it with the rest
//! of the network.
//!
//! It works in the following way:
//!
//! 1. For every included relay chain block we note the backed candidate of our teyrchain. If the
//! block belonging to the PoV is already known, we do nothing. Otherwise we start a timer that
//! waits for a randomized time inside a specified interval before starting to
//! recover the PoV.
//!
//! 2. If between starting and firing the timer the block is imported, we skip the recovery of the
//! PoV.
//!
//! 3. If the timer fired we recover the PoV using the relay chain PoV recovery protocol.
//!
//! 4a. After it is recovered, we restore the block and import it.
//!
//! 4b. Since we are trying to recover pending candidates, availability is not guaranteed. If the
//! block PoV is not yet available, we retry.
//!
//! If we need to recover multiple PoV blocks (which should hopefully not happen in real life), we
//! make sure that the blocks are imported in the correct order.
use pezsc_client_api::{BlockBackend, BlockchainEvents, UsageProvider};
use pezsc_consensus::import_queue::{ImportQueueService, IncomingBlock};
use pezsp_consensus::{BlockOrigin, BlockStatus, SyncOracle};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use pezkuwi_node_primitives::{PoV, POV_BOMB_LIMIT};
use pezkuwi_node_subsystem::messages::AvailabilityRecoveryMessage;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{
CandidateReceiptV2 as CandidateReceipt,
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, Id as ParaId, SessionIndex,
};
use cumulus_primitives_core::TeyrchainBlockData;
use cumulus_relay_chain_interface::RelayChainInterface;
use cumulus_relay_chain_streams::pending_candidates;
use codec::{Decode, DecodeAll};
use futures::{
channel::mpsc::Receiver, select, stream::FuturesUnordered, Future, FutureExt, StreamExt,
};
use futures_timer::Delay;
use rand::{distributions::Uniform, prelude::Distribution, thread_rng};
use std::{
collections::{HashMap, HashSet, VecDeque},
pin::Pin,
sync::Arc,
time::Duration,
};
#[cfg(test)]
mod tests;
mod active_candidate_recovery;
use active_candidate_recovery::ActiveCandidateRecovery;
const LOG_TARGET: &str = "pezcumulus-pov-recovery";
/// Test-friendly wrapper trait for the overseer handle.
/// Can be used to simulate failing recovery requests.
#[async_trait::async_trait]
pub trait RecoveryHandle: Send {
async fn send_recovery_msg(
&mut self,
message: AvailabilityRecoveryMessage,
origin: &'static str,
);
}
#[async_trait::async_trait]
impl RecoveryHandle for OverseerHandle {
async fn send_recovery_msg(
&mut self,
message: AvailabilityRecoveryMessage,
origin: &'static str,
) {
self.send_msg(message, origin).await;
}
}
/// Type of recovery to trigger.
#[derive(Debug, PartialEq)]
pub enum RecoveryKind {
/// Single block recovery.
Simple,
/// Full ancestry recovery.
Full,
}
/// Structure used to trigger an explicit recovery request via `PoVRecovery`.
pub struct RecoveryRequest<Block: BlockT> {
/// Hash of the last block to recover.
pub hash: Block::Hash,
/// Recovery type.
pub kind: RecoveryKind,
}
/// The delay between observing an unknown block and triggering the recovery of a block.
/// Randomizing the start of the recovery within this interval
/// can be used to prevent self-DOSing if the recovery request is part of a
/// distributed protocol and there is the possibility that multiple actors are
/// requiring to perform the recovery action at approximately the same time.
#[derive(Clone, Copy)]
pub struct RecoveryDelayRange {
/// Start recovering after `min` delay.
pub min: Duration,
/// Start recovering before `max` delay.
pub max: Duration,
}
impl RecoveryDelayRange {
/// Produce a randomized duration between `min` and `max`.
fn duration(&self) -> Duration {
Uniform::from(self.min..=self.max).sample(&mut thread_rng())
}
}
/// Represents an outstanding block candidate.
struct Candidate<Block: BlockT> {
receipt: CandidateReceipt,
session_index: SessionIndex,
block_number: NumberFor<Block>,
parent_hash: Block::Hash,
// Lazy recovery has been submitted.
// Should be true iff a block is either queued to be recovered or
// recovery is currently in progress.
waiting_recovery: bool,
}
/// Queue that is used to decide when to start PoV-recovery operations.
struct RecoveryQueue<Block: BlockT> {
recovery_delay_range: RecoveryDelayRange,
// Queue that keeps the hashes of blocks to be recovered.
recovery_queue: VecDeque<Block::Hash>,
// Futures that resolve when a new recovery should be started.
signaling_queue: FuturesUnordered<Pin<Box<dyn Future<Output = ()> + Send>>>,
}
impl<Block: BlockT> RecoveryQueue<Block> {
pub fn new(recovery_delay_range: RecoveryDelayRange) -> Self {
Self {
recovery_delay_range,
recovery_queue: Default::default(),
signaling_queue: Default::default(),
}
}
/// Add hash of a block that should go to the end of the recovery queue.
/// A new recovery will be signaled after `delay` has passed.
pub fn push_recovery(&mut self, hash: Block::Hash) {
let delay = self.recovery_delay_range.duration();
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Adding block to queue and adding new recovery slot in {:?} sec",
delay.as_secs(),
);
self.recovery_queue.push_back(hash);
self.signaling_queue.push(
async move {
Delay::new(delay).await;
}
.boxed(),
);
}
/// Get the next hash for block recovery.
pub async fn next_recovery(&mut self) -> Block::Hash {
loop {
if self.signaling_queue.next().await.is_some() {
if let Some(hash) = self.recovery_queue.pop_front() {
return hash;
} else {
tracing::error!(
target: LOG_TARGET,
"Recovery was signaled, but no candidate hash available. This is a bug."
);
};
}
futures::pending!()
}
}
}
/// Encapsulates the logic of the pov recovery.
pub struct PoVRecovery<Block: BlockT, PC, RC> {
/// All the pending candidates that we are waiting for to be imported or that need to be
/// recovered when `next_candidate_to_recover` tells us to do so.
candidates: HashMap<Block::Hash, Candidate<Block>>,
/// A stream of futures that resolve to hashes of candidates that need to be recovered.
///
/// The candidates to the hashes are stored in `candidates`. If a candidate is not
/// available anymore in this map, it means that it was already imported.
candidate_recovery_queue: RecoveryQueue<Block>,
active_candidate_recovery: ActiveCandidateRecovery<Block>,
/// Blocks that wait that the parent is imported.
///
/// Uses parent -> blocks mapping.
waiting_for_parent: HashMap<Block::Hash, Vec<Block>>,
teyrchain_client: Arc<PC>,
teyrchain_import_queue: Box<dyn ImportQueueService<Block>>,
relay_chain_interface: RC,
para_id: ParaId,
/// Explicit block recovery requests channel.
recovery_chan_rx: Receiver<RecoveryRequest<Block>>,
/// Blocks that we are retrying currently
candidates_in_retry: HashSet<Block::Hash>,
teyrchain_sync_service: Arc<dyn SyncOracle + Sync + Send>,
}
impl<Block: BlockT, PC, RCInterface> PoVRecovery<Block, PC, RCInterface>
where
PC: BlockBackend<Block> + BlockchainEvents<Block> + UsageProvider<Block>,
RCInterface: RelayChainInterface + Clone,
{
/// Create a new instance.
pub fn new(
recovery_handle: Box<dyn RecoveryHandle>,
recovery_delay_range: RecoveryDelayRange,
teyrchain_client: Arc<PC>,
teyrchain_import_queue: Box<dyn ImportQueueService<Block>>,
relay_chain_interface: RCInterface,
para_id: ParaId,
recovery_chan_rx: Receiver<RecoveryRequest<Block>>,
teyrchain_sync_service: Arc<dyn SyncOracle + Sync + Send>,
) -> Self {
Self {
candidates: HashMap::new(),
candidate_recovery_queue: RecoveryQueue::new(recovery_delay_range),
active_candidate_recovery: ActiveCandidateRecovery::new(recovery_handle),
waiting_for_parent: HashMap::new(),
teyrchain_client,
teyrchain_import_queue,
relay_chain_interface,
para_id,
candidates_in_retry: HashSet::new(),
recovery_chan_rx,
teyrchain_sync_service,
}
}
/// Handle a new pending candidate.
fn handle_pending_candidate(
&mut self,
receipt: CommittedCandidateReceipt,
session_index: SessionIndex,
) {
let header = match Block::Header::decode(&mut &receipt.commitments.head_data.0[..]) {
Ok(header) => header,
Err(e) => {
tracing::warn!(
target: LOG_TARGET,
error = ?e,
"Failed to decode teyrchain header from pending candidate",
);
return;
},
};
if *header.number() <= self.teyrchain_client.usage_info().chain.finalized_number {
return;
}
let hash = header.hash();
if self.candidates.contains_key(&hash) {
return;
}
tracing::debug!(target: LOG_TARGET, block_hash = ?hash, "Adding outstanding candidate");
self.candidates.insert(
hash,
Candidate {
block_number: *header.number(),
receipt: receipt.to_plain(),
session_index,
parent_hash: *header.parent_hash(),
waiting_recovery: false,
},
);
// If required, triggers a lazy recovery request that will eventually be blocked
// if in the meantime the block is imported.
self.recover(RecoveryRequest { hash, kind: RecoveryKind::Simple });
}
/// Block is no longer waiting for recovery
fn clear_waiting_recovery(&mut self, block_hash: &Block::Hash) {
if let Some(candidate) = self.candidates.get_mut(block_hash) {
// Prevents triggering an already enqueued recovery request
candidate.waiting_recovery = false;
}
}
/// Handle a finalized block with the given `block_number`.
fn handle_block_finalized(&mut self, block_number: NumberFor<Block>) {
self.candidates.retain(|_, pc| pc.block_number > block_number);
}
/// Recover the candidate for the given `block_hash`.
async fn recover_candidate(&mut self, block_hash: Block::Hash) {
match self.candidates.get(&block_hash) {
Some(candidate) if candidate.waiting_recovery => {
tracing::debug!(target: LOG_TARGET, ?block_hash, "Issuing recovery request");
self.active_candidate_recovery.recover_candidate(block_hash, candidate).await;
},
_ => (),
}
}
/// Clear `waiting_for_parent` and `waiting_recovery` for the candidate with `hash`.
/// Also clears children blocks waiting for this parent.
fn reset_candidate(&mut self, hash: Block::Hash) {
let mut blocks_to_delete = vec![hash];
while let Some(delete) = blocks_to_delete.pop() {
if let Some(children) = self.waiting_for_parent.remove(&delete) {
blocks_to_delete.extend(children.iter().map(BlockT::hash));
}
}
self.clear_waiting_recovery(&hash);
}
/// Try to decode [`TeyrchainBlockData`] from `data`.
///
/// Internally it will handle the decoding of the different versions.
fn decode_teyrchain_block_data(
data: &[u8],
expected_block_hash: Block::Hash,
) -> Option<TeyrchainBlockData<Block>> {
match TeyrchainBlockData::<Block>::decode_all(&mut &data[..]) {
Ok(block_data) => {
if block_data.blocks().last().map_or(false, |b| b.hash() == expected_block_hash) {
return Some(block_data);
}
tracing::debug!(
target: LOG_TARGET,
?expected_block_hash,
"Could not find the expected block hash as latest block in `TeyrchainBlockData`"
);
},
Err(error) => {
tracing::debug!(
target: LOG_TARGET,
?expected_block_hash,
?error,
"Could not decode `TeyrchainBlockData` from recovered PoV",
);
},
}
None
}
/// Handle a recovered candidate.
async fn handle_candidate_recovered(&mut self, block_hash: Block::Hash, pov: Option<&PoV>) {
let pov = match pov {
Some(pov) => {
self.candidates_in_retry.remove(&block_hash);
pov
},
None =>
if self.candidates_in_retry.insert(block_hash) {
tracing::debug!(target: LOG_TARGET, ?block_hash, "Recovery failed, retrying.");
self.candidate_recovery_queue.push_recovery(block_hash);
return;
} else {
tracing::warn!(
target: LOG_TARGET,
?block_hash,
"Unable to recover block after retry.",
);
self.candidates_in_retry.remove(&block_hash);
self.reset_candidate(block_hash);
return;
},
};
let raw_block_data =
match pezsp_maybe_compressed_blob::decompress(&pov.block_data.0, POV_BOMB_LIMIT) {
Ok(r) => r,
Err(error) => {
tracing::debug!(target: LOG_TARGET, ?error, "Failed to decompress PoV");
self.reset_candidate(block_hash);
return;
},
};
let Some(block_data) = Self::decode_teyrchain_block_data(&raw_block_data, block_hash)
else {
self.reset_candidate(block_hash);
return;
};
let blocks = block_data.into_blocks();
let Some(parent) = blocks.first().map(|b| *b.header().parent_hash()) else {
tracing::debug!(
target: LOG_TARGET,
?block_hash,
"Recovered candidate doesn't contain any blocks.",
);
self.reset_candidate(block_hash);
return;
};
match self.teyrchain_client.block_status(parent) {
Ok(BlockStatus::Unknown) => {
// If the parent block is currently being recovered or is scheduled to be recovered,
// we want to wait for the parent.
let parent_scheduled_for_recovery =
self.candidates.get(&parent).map_or(false, |parent| parent.waiting_recovery);
if parent_scheduled_for_recovery {
tracing::debug!(
target: LOG_TARGET,
?block_hash,
parent_hash = ?parent,
parent_scheduled_for_recovery,
waiting_blocks = self.waiting_for_parent.len(),
"Waiting for recovery of parent.",
);
blocks.into_iter().for_each(|b| {
self.waiting_for_parent
.entry(*b.header().parent_hash())
.or_default()
.push(b);
});
return;
} else {
tracing::debug!(
target: LOG_TARGET,
?block_hash,
parent_hash = ?parent,
"Parent not found while trying to import recovered block.",
);
self.reset_candidate(block_hash);
return;
}
},
Err(error) => {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?parent,
?error,
"Error while checking block status",
);
self.reset_candidate(block_hash);
return;
},
// Any other status is fine to "ignore/accept"
_ => (),
}
self.import_blocks(blocks.into_iter());
}
/// Import the given `blocks`.
///
/// This will also recursively drain `waiting_for_parent` and import them as well.
fn import_blocks(&mut self, blocks: impl Iterator<Item = Block>) {
let mut blocks = VecDeque::from_iter(blocks);
tracing::debug!(
target: LOG_TARGET,
blocks = ?blocks.iter().map(|b| b.hash()),
"Importing blocks retrieved using pov_recovery",
);
let mut incoming_blocks = Vec::new();
while let Some(block) = blocks.pop_front() {
let block_hash = block.hash();
let (header, body) = block.deconstruct();
incoming_blocks.push(IncomingBlock {
hash: block_hash,
header: Some(header),
body: Some(body),
import_existing: false,
allow_missing_state: false,
justifications: None,
origin: None,
skip_execution: false,
state: None,
indexed_body: None,
});
if let Some(waiting) = self.waiting_for_parent.remove(&block_hash) {
blocks.extend(waiting);
}
}
self.teyrchain_import_queue
// Use `ConsensusBroadcast` to inform the import pipeline that this blocks needs to be
// imported.
.import_blocks(BlockOrigin::ConsensusBroadcast, incoming_blocks);
}
/// Attempts an explicit recovery of one or more blocks.
pub fn recover(&mut self, req: RecoveryRequest<Block>) {
let RecoveryRequest { mut hash, kind } = req;
let mut to_recover = Vec::new();
loop {
let candidate = match self.candidates.get_mut(&hash) {
Some(candidate) => candidate,
None => {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Could not recover. Block was never announced as candidate"
);
return;
},
};
match self.teyrchain_client.block_status(hash) {
Ok(BlockStatus::Unknown) if !candidate.waiting_recovery => {
candidate.waiting_recovery = true;
to_recover.push(hash);
},
Ok(_) => break,
Err(e) => {
tracing::error!(
target: LOG_TARGET,
error = ?e,
block_hash = ?hash,
"Failed to get block status",
);
for hash in to_recover {
self.clear_waiting_recovery(&hash);
}
return;
},
}
if kind == RecoveryKind::Simple {
break;
}
hash = candidate.parent_hash;
}
for hash in to_recover.into_iter().rev() {
self.candidate_recovery_queue.push_recovery(hash);
}
}
/// Run the pov-recovery.
pub async fn run(mut self) {
let mut imported_blocks = self.teyrchain_client.import_notification_stream().fuse();
let mut finalized_blocks = self.teyrchain_client.finality_notification_stream().fuse();
let pending_candidates = match pending_candidates(
self.relay_chain_interface.clone(),
self.para_id,
self.teyrchain_sync_service.clone(),
)
.await
{
Ok(pending_candidates_stream) => pending_candidates_stream.fuse(),
Err(err) => {
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve pending candidate stream.");
return;
},
};
futures::pin_mut!(pending_candidates);
loop {
select! {
next_pending_candidates = pending_candidates.next() => {
if let Some((candidates, session_index, _)) = next_pending_candidates {
for candidate in candidates {
self.handle_pending_candidate(candidate, session_index);
}
} else {
tracing::debug!(target: LOG_TARGET, "Pending candidates stream ended");
return;
}
},
recovery_req = self.recovery_chan_rx.next() => {
if let Some(req) = recovery_req {
self.recover(req);
} else {
tracing::debug!(target: LOG_TARGET, "Recovery channel stream ended");
return;
}
},
imported = imported_blocks.next() => {
if let Some(imported) = imported {
self.clear_waiting_recovery(&imported.hash);
// We need to double check that no blocks are waiting for this block.
// Can happen when a waiting child block is queued to wait for parent while the parent block is still
// in the import queue.
if let Some(waiting_blocks) = self.waiting_for_parent.remove(&imported.hash) {
for block in waiting_blocks {
tracing::debug!(target: LOG_TARGET, block_hash = ?block.hash(), resolved_parent = ?imported.hash, "Found new waiting child block during import, queuing.");
self.import_blocks(std::iter::once(block));
}
};
} else {
tracing::debug!(target: LOG_TARGET, "Imported blocks stream ended");
return;
}
},
finalized = finalized_blocks.next() => {
if let Some(finalized) = finalized {
self.handle_block_finalized(*finalized.header.number());
} else {
tracing::debug!(target: LOG_TARGET, "Finalized blocks stream ended");
return;
}
},
next_to_recover = self.candidate_recovery_queue.next_recovery().fuse() => {
self.recover_candidate(next_to_recover).await;
},
(block_hash, pov) =
self.active_candidate_recovery.wait_for_recovery().fuse() =>
{
self.handle_candidate_recovered(block_hash, pov.as_deref()).await;
},
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-inprocess-interface"
version = "0.7.0"
edition.workspace = true
description = "Implementation of the RelayChainInterface trait for Pezkuwi full-nodes."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
# Bizinikiwi
pezsc-cli = { workspace = true, default-features = false }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-sysinfo = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-cli = { features = ["cli"], workspace = true }
pezkuwi-primitives = { workspace = true, default-features = true }
pezkuwi-service = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-bootnodes = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[dev-dependencies]
# Bizinikiwi
pezsp-keyring = { workspace = true, default-features = true }
# Pezkuwi
metered = { features = ["futures_channel"], workspace = true }
pezkuwi-test-client = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-bootnodes/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezkuwi-cli/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-service/runtime-benchmarks",
"pezkuwi-test-client/runtime-benchmarks",
"pezsc-cli/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-sysinfo/runtime-benchmarks",
"pezsc-tracing/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
@@ -0,0 +1,601 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use std::{
collections::{BTreeMap, HashSet, VecDeque},
pin::Pin,
sync::Arc,
time::Duration,
};
use async_trait::async_trait;
use cumulus_client_bootnodes::bootnode_request_response_config;
use cumulus_primitives_core::{
relay_chain::{
runtime_api::TeyrchainHost, Block as PBlock, BlockId, BlockNumber,
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex, CoreState,
Hash as PHash, Header as PHeader, InboundHrmpMessage, OccupiedCoreAssumption, SessionIndex,
ValidationCodeHash, ValidatorId,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
use futures::{FutureExt, Stream, StreamExt};
use pezkuwi_primitives::CandidateEvent;
use pezkuwi_service::{
builder::PezkuwiServiceBuilder, CollatorOverseerGen, CollatorPair, Configuration, FullBackend,
FullClient, Handle, NewFull, NewFullParams, TaskManager,
};
use pezsc_cli::{RuntimeVersion, BizinikiwiCli};
use pezsc_client_api::{
blockchain::BlockStatus, Backend, BlockchainEvents, HeaderBackend, ImportNotifications,
StorageProof, TrieCacheContext,
};
use pezsc_network::{
config::NetworkBackendType,
request_responses::IncomingRequest,
service::traits::{NetworkBackend, NetworkService},
};
use pezsc_telemetry::TelemetryWorkerHandle;
use pezsp_api::{CallApiAt, CallApiAtParams, CallContext, ProvideRuntimeApi};
use pezsp_consensus::SyncOracle;
use pezsp_core::Pair;
use pezsp_state_machine::{Backend as StateBackend, StorageValue};
/// The timeout in seconds after that the waiting for a block should be aborted.
const TIMEOUT_IN_SECONDS: u64 = 6;
/// Provides an implementation of the [`RelayChainInterface`] using a local in-process relay chain
/// node.
#[derive(Clone)]
pub struct RelayChainInProcessInterface {
full_client: Arc<FullClient>,
backend: Arc<FullBackend>,
sync_oracle: Arc<dyn SyncOracle + Send + Sync>,
overseer_handle: Handle,
}
impl RelayChainInProcessInterface {
/// Create a new instance of [`RelayChainInProcessInterface`]
pub fn new(
full_client: Arc<FullClient>,
backend: Arc<FullBackend>,
sync_oracle: Arc<dyn SyncOracle + Send + Sync>,
overseer_handle: Handle,
) -> Self {
Self { full_client, backend, sync_oracle, overseer_handle }
}
}
#[async_trait]
impl RelayChainInterface for RelayChainInProcessInterface {
async fn version(&self, relay_parent: PHash) -> RelayChainResult<RuntimeVersion> {
Ok(self.full_client.runtime_version_at(relay_parent)?)
}
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
Ok(self.full_client.runtime_api().dmq_contents(relay_parent, para_id)?)
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
Ok(self
.full_client
.runtime_api()
.inbound_hrmp_channels_contents(relay_parent, para_id)?)
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(num) =>
if let Some(hash) = self.full_client.hash(num)? {
hash
} else {
return Ok(None);
},
};
let header = self.full_client.header(hash)?;
Ok(header)
}
async fn persisted_validation_data(
&self,
hash: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
Ok(self.full_client.runtime_api().persisted_validation_data(
hash,
para_id,
occupied_core_assumption,
)?)
}
async fn validation_code_hash(
&self,
hash: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
Ok(self.full_client.runtime_api().validation_code_hash(
hash,
para_id,
occupied_core_assumption,
)?)
}
async fn candidate_pending_availability(
&self,
hash: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
Ok(self
.full_client
.runtime_api()
.candidate_pending_availability(hash, para_id)?
.map(|receipt| receipt.into()))
}
async fn session_index_for_child(&self, hash: PHash) -> RelayChainResult<SessionIndex> {
Ok(self.full_client.runtime_api().session_index_for_child(hash)?)
}
async fn validators(&self, hash: PHash) -> RelayChainResult<Vec<ValidatorId>> {
Ok(self.full_client.runtime_api().validators(hash)?)
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notification_stream = self
.full_client
.import_notification_stream()
.map(|notification| notification.header);
Ok(Box::pin(notification_stream))
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notification_stream = self
.full_client
.finality_notification_stream()
.map(|notification| notification.header);
Ok(Box::pin(notification_stream))
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.backend.blockchain().info().best_hash)
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.backend.blockchain().info().finalized_hash)
}
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: PHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
Ok(self.full_client.call_api_at(CallApiAtParams {
at: hash,
function: method_name,
arguments: payload.to_vec(),
overlayed_changes: &Default::default(),
call_context: CallContext::Offchain,
recorder: &None,
extensions: &Default::default(),
})?)
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
Ok(self.sync_oracle.is_major_syncing())
}
fn overseer_handle(&self) -> RelayChainResult<Handle> {
Ok(self.overseer_handle.clone())
}
async fn get_storage_by_key(
&self,
relay_parent: PHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
let state = self.backend.state_at(relay_parent, TrieCacheContext::Untrusted)?;
state.storage(key).map_err(RelayChainError::GenericError)
}
async fn prove_read(
&self,
relay_parent: PHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof> {
let state_backend = self.backend.state_at(relay_parent, TrieCacheContext::Untrusted)?;
pezsp_state_machine::prove_read(state_backend, relevant_keys)
.map_err(RelayChainError::StateMachineError)
}
/// Wait for a given relay chain block in an async way.
///
/// The caller needs to pass the hash of a block it waits for and the function will return when
/// the block is available or an error occurred.
///
/// The waiting for the block is implemented as follows:
///
/// 1. Get a read lock on the import lock from the backend.
///
/// 2. Check if the block is already imported. If yes, return from the function.
///
/// 3. If the block isn't imported yet, add an import notification listener.
///
/// 4. Poll the import notification listener until the block is imported or the timeout is
/// fired.
///
/// The timeout is set to 6 seconds. This should be enough time to import the block in the
/// current round and if not, the new round of the relay chain already started anyway.
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()> {
let mut listener =
match check_block_in_chain(self.backend.clone(), self.full_client.clone(), hash)? {
BlockCheckStatus::InChain => return Ok(()),
BlockCheckStatus::Unknown(listener) => listener,
};
let mut timeout = futures_timer::Delay::new(Duration::from_secs(TIMEOUT_IN_SECONDS)).fuse();
loop {
futures::select! {
_ = timeout => return Err(RelayChainError::WaitTimeout(hash)),
evt = listener.next() => match evt {
Some(evt) if evt.hash == hash => return Ok(()),
// Not the event we waited on.
Some(_) => continue,
None => return Err(RelayChainError::ImportListenerClosed(hash)),
}
}
}
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notifications_stream =
self.full_client
.import_notification_stream()
.filter_map(|notification| async move {
notification.is_new_best.then_some(notification.header)
});
Ok(Box::pin(notifications_stream))
}
async fn availability_cores(
&self,
relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
Ok(self
.full_client
.runtime_api()
.availability_cores(relay_parent)?
.into_iter()
.map(|core_state| core_state.into())
.collect::<Vec<_>>())
}
async fn candidates_pending_availability(
&self,
hash: PHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
Ok(self
.full_client
.runtime_api()
.candidates_pending_availability(hash, para_id)?
.into_iter()
.map(|receipt| receipt.into())
.collect::<Vec<_>>())
}
async fn claim_queue(
&self,
hash: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
Ok(self.full_client.runtime_api().claim_queue(hash)?)
}
async fn scheduling_lookahead(&self, hash: PHash) -> RelayChainResult<u32> {
Ok(self.full_client.runtime_api().scheduling_lookahead(hash)?)
}
async fn candidate_events(&self, hash: PHash) -> RelayChainResult<Vec<CandidateEvent>> {
Ok(self.full_client.runtime_api().candidate_events(hash)?)
}
}
pub enum BlockCheckStatus {
/// Block is in chain
InChain,
/// Block status is unknown, listener can be used to wait for notification
Unknown(ImportNotifications<PBlock>),
}
// Helper function to check if a block is in chain.
pub fn check_block_in_chain(
backend: Arc<FullBackend>,
client: Arc<FullClient>,
hash: PHash,
) -> RelayChainResult<BlockCheckStatus> {
let _lock = backend.get_import_lock().read();
if backend.blockchain().status(hash)? == BlockStatus::InChain {
return Ok(BlockCheckStatus::InChain);
}
let listener = client.import_notification_stream();
Ok(BlockCheckStatus::Unknown(listener))
}
/// Build Pezkuwi full node with teyrchain bootnode request-response protocol.
fn build_pezkuwi_with_paranode_protocol<Network>(
config: Configuration,
params: NewFullParams<CollatorOverseerGen>,
) -> Result<(NewFull, async_channel::Receiver<IncomingRequest>), pezkuwi_service::Error>
where
Network: NetworkBackend<PBlock, PHash>,
{
let fork_id = config.chain_spec.fork_id().map(ToString::to_string);
let mut pezkuwi_builder = PezkuwiServiceBuilder::<_, Network>::new(config, params)?;
let (config, request_receiver) = bootnode_request_response_config::<_, _, Network>(
pezkuwi_builder.genesis_hash(),
fork_id.as_deref(),
);
pezkuwi_builder.add_extra_request_response_protocol(config);
Ok((pezkuwi_builder.build()?, request_receiver))
}
/// Build the Pezkuwi full node using the given `config`.
#[pezsc_tracing::logging::prefix_logs_with("Relaychain")]
fn build_pezkuwi_full_node(
config: Configuration,
teyrchain_config: &Configuration,
telemetry_worker_handle: Option<TelemetryWorkerHandle>,
hwbench: Option<pezsc_sysinfo::HwBench>,
) -> Result<
(NewFull, Option<CollatorPair>, async_channel::Receiver<IncomingRequest>),
pezkuwi_service::Error,
> {
let (is_teyrchain_node, maybe_collator_key) = if teyrchain_config.role.is_authority() {
let collator_key = CollatorPair::generate().0;
(pezkuwi_service::IsTeyrchainNode::Collator(collator_key.clone()), Some(collator_key))
} else {
(pezkuwi_service::IsTeyrchainNode::FullNode, None)
};
let new_full_params = pezkuwi_service::NewFullParams {
is_teyrchain_node,
// Disable BEEFY. It should not be required by the internal relay chain node.
enable_beefy: false,
force_authoring_backoff: false,
telemetry_worker_handle,
// Pezcumulus doesn't spawn PVF workers, so we can disable version checks.
node_version: None,
secure_validator_mode: false,
workers_path: None,
workers_names: None,
overseer_gen: CollatorOverseerGen,
overseer_message_channel_capacity_override: None,
malus_finality_delay: None,
hwbench,
execute_workers_max_num: None,
prepare_workers_hard_max_num: None,
prepare_workers_soft_max_num: None,
keep_finalized_for: None,
invulnerable_ah_collators: HashSet::new(),
collator_protocol_hold_off: None,
};
let (relay_chain_full_node, paranode_req_receiver) = match config.network.network_backend {
NetworkBackendType::Libp2p => build_pezkuwi_with_paranode_protocol::<
pezsc_network::NetworkWorker<_, _>,
>(config, new_full_params)?,
NetworkBackendType::Litep2p => build_pezkuwi_with_paranode_protocol::<
pezsc_network::Litep2pNetworkBackend,
>(config, new_full_params)?,
};
Ok((relay_chain_full_node, maybe_collator_key, paranode_req_receiver))
}
/// Builds a relay chain interface by constructing a full relay chain node
pub fn build_inprocess_relay_chain(
mut pezkuwi_config: Configuration,
teyrchain_config: &Configuration,
telemetry_worker_handle: Option<TelemetryWorkerHandle>,
task_manager: &mut TaskManager,
hwbench: Option<pezsc_sysinfo::HwBench>,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<IncomingRequest>,
)> {
// This is essentially a hack, but we want to ensure that we send the correct node version
// to the telemetry.
pezkuwi_config.impl_version = pezkuwi_cli::Cli::impl_version();
pezkuwi_config.impl_name = pezkuwi_cli::Cli::impl_name();
let (full_node, collator_key, paranode_req_receiver) =
build_pezkuwi_full_node(pezkuwi_config, teyrchain_config, telemetry_worker_handle, hwbench)
.map_err(|e| RelayChainError::Application(Box::new(e) as Box<_>))?;
let relay_chain_interface = Arc::new(RelayChainInProcessInterface::new(
full_node.client,
full_node.backend,
full_node.sync_service,
full_node.overseer_handle.clone().ok_or(RelayChainError::GenericError(
"Overseer not running in full node.".to_string(),
))?,
));
task_manager.add_child(full_node.task_manager);
Ok((relay_chain_interface, collator_key, full_node.network, paranode_req_receiver))
}
#[cfg(test)]
mod tests {
use super::*;
use pezkuwi_primitives::Block as PBlock;
use pezkuwi_test_client::{
construct_transfer_extrinsic, BlockBuilderExt, Client, ClientBlockImportExt,
DefaultTestClientBuilderExt, InitPezkuwiBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use pezsp_consensus::{BlockOrigin, SyncOracle};
use pezsp_runtime::traits::Block as BlockT;
use std::sync::Arc;
use futures::{executor::block_on, poll, task::Poll};
struct DummyNetwork {}
impl SyncOracle for DummyNetwork {
fn is_major_syncing(&self) -> bool {
unimplemented!("Not needed for test")
}
fn is_offline(&self) -> bool {
unimplemented!("Not needed for test")
}
}
fn build_client_backend_and_block() -> (Arc<Client>, PBlock, RelayChainInProcessInterface) {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let block_builder = client.init_pezkuwi_block_builder();
let block = block_builder.build().expect("Finalizes the block").block;
let dummy_network: Arc<dyn SyncOracle + Sync + Send> = Arc::new(DummyNetwork {});
let (tx, _rx) = metered::channel(30);
let mock_handle = Handle::new(tx);
(
client.clone(),
block,
RelayChainInProcessInterface::new(client, backend, dummy_network, mock_handle),
)
}
#[test]
fn returns_directly_for_available_block() {
let (client, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
block_on(client.import(BlockOrigin::Own, block)).expect("Imports the block");
block_on(async move {
// Should be ready on the first poll
assert!(matches!(
poll!(relay_chain_interface.wait_for_block(hash)),
Poll::Ready(Ok(()))
));
});
}
#[test]
fn resolve_after_block_import_notification_was_received() {
let (client, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
block_on(async move {
let mut future = relay_chain_interface.wait_for_block(hash);
// As the block is not yet imported, the first poll should return `Pending`
assert!(poll!(&mut future).is_pending());
// Import the block that should fire the notification
client.import(BlockOrigin::Own, block).await.expect("Imports the block");
// Now it should have received the notification and report that the block was imported
assert!(matches!(poll!(future), Poll::Ready(Ok(()))));
});
}
#[test]
fn wait_for_block_time_out_when_block_is_not_imported() {
let (_, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
assert!(matches!(
block_on(relay_chain_interface.wait_for_block(hash)),
Err(RelayChainError::WaitTimeout(_))
));
}
#[test]
fn do_not_resolve_after_different_block_import_notification_was_received() {
let (client, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
let ext = construct_transfer_extrinsic(
&client,
pezsp_keyring::Sr25519Keyring::Alice,
pezsp_keyring::Sr25519Keyring::Bob,
1000,
);
let mut block_builder = client.init_pezkuwi_block_builder();
// Push an extrinsic to get a different block hash.
block_builder.push_pezkuwi_extrinsic(ext).expect("Push extrinsic");
let block2 = block_builder.build().expect("Build second block").block;
let hash2 = block2.hash();
block_on(async move {
let mut future = relay_chain_interface.wait_for_block(hash);
let mut future2 = relay_chain_interface.wait_for_block(hash2);
// As the block is not yet imported, the first poll should return `Pending`
assert!(poll!(&mut future).is_pending());
assert!(poll!(&mut future2).is_pending());
// Import the block that should fire the notification
client.import(BlockOrigin::Own, block2).await.expect("Imports the second block");
// The import notification of the second block should not make this one finish
assert!(poll!(&mut future).is_pending());
// Now it should have received the notification and report that the block was imported
assert!(matches!(poll!(future2), Poll::Ready(Ok(()))));
client.import(BlockOrigin::Own, block).await.expect("Imports the first block");
// Now it should be ready
assert!(matches!(poll!(future), Poll::Ready(Ok(()))));
});
}
}
@@ -0,0 +1,42 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-interface"
version = "0.7.0"
edition.workspace = true
description = "Common interface for different relay chain datasources."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
pezkuwi-overseer = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-version = { workspace = true }
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true }
jsonrpsee-core = { workspace = true }
thiserror = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,431 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use std::{
collections::{BTreeMap, VecDeque},
pin::Pin,
sync::Arc,
};
use futures::Stream;
use pezkuwi_overseer::prometheus::PrometheusError;
use pezsc_client_api::StorageProof;
use pezsp_version::RuntimeVersion;
use async_trait::async_trait;
use codec::{Decode, Encode, Error as CodecError};
use jsonrpsee_core::ClientError as JsonRpcError;
use pezsp_api::ApiError;
use cumulus_primitives_core::relay_chain::{BlockId, CandidateEvent, Hash as RelayHash};
pub use cumulus_primitives_core::{
relay_chain::{
BlockNumber, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex,
CoreState, Hash as PHash, Header as PHeader, InboundHrmpMessage, OccupiedCoreAssumption,
SessionIndex, ValidationCodeHash, ValidatorId,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
pub use pezkuwi_overseer::Handle as OverseerHandle;
pub use pezsp_state_machine::StorageValue;
pub type RelayChainResult<T> = Result<T, RelayChainError>;
#[derive(thiserror::Error, Debug)]
pub enum RelayChainError {
#[error("Error occurred while calling relay chain runtime: {0}")]
ApiError(#[from] ApiError),
#[error("Timeout while waiting for relay-chain block `{0}` to be imported.")]
WaitTimeout(PHash),
#[error("Import listener closed while waiting for relay-chain block `{0}` to be imported.")]
ImportListenerClosed(PHash),
#[error(
"Blockchain returned an error while waiting for relay-chain block `{0}` to be imported: {1}"
)]
WaitBlockchainError(PHash, pezsp_blockchain::Error),
#[error("Blockchain returned an error: {0}")]
BlockchainError(#[from] pezsp_blockchain::Error),
#[error("State machine error occurred: {0}")]
StateMachineError(Box<dyn pezsp_state_machine::Error>),
#[error("Unable to call RPC method '{0}'")]
RpcCallError(String),
#[error("RPC Error: '{0}'")]
JsonRpcError(#[from] JsonRpcError),
#[error("Unable to communicate with RPC worker: {0}")]
WorkerCommunicationError(String),
#[error("Scale codec deserialization error: {0}")]
DeserializationError(CodecError),
#[error(transparent)]
Application(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Prometheus error: {0}")]
PrometheusError(#[from] PrometheusError),
#[error("Unspecified error occurred: {0}")]
GenericError(String),
}
impl From<RelayChainError> for ApiError {
fn from(r: RelayChainError) -> Self {
pezsp_api::ApiError::Application(Box::new(r))
}
}
impl From<CodecError> for RelayChainError {
fn from(e: CodecError) -> Self {
RelayChainError::DeserializationError(e)
}
}
impl From<RelayChainError> for pezsp_blockchain::Error {
fn from(r: RelayChainError) -> Self {
pezsp_blockchain::Error::Application(Box::new(r))
}
}
impl<T: std::error::Error + Send + Sync + 'static> From<Box<T>> for RelayChainError {
fn from(r: Box<T>) -> Self {
RelayChainError::Application(r)
}
}
/// Trait that provides all necessary methods for interaction between collator and relay chain.
#[async_trait]
pub trait RelayChainInterface: Send + Sync {
/// Fetch a storage item by key.
async fn get_storage_by_key(
&self,
relay_parent: PHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>>;
/// Fetch a vector of current validators.
async fn validators(&self, block_id: PHash) -> RelayChainResult<Vec<ValidatorId>>;
/// Get the hash of the current best block.
async fn best_block_hash(&self) -> RelayChainResult<PHash>;
/// Fetch the block header of a given hash or height, if it exists.
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>>;
/// Get the hash of the finalized block.
async fn finalized_block_hash(&self) -> RelayChainResult<PHash>;
/// Call an arbitrary runtime api. The input and output are SCALE-encoded.
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>>;
/// Returns the whole contents of the downward message queue for the teyrchain we are collating
/// for.
///
/// Returns `None` in case of an error.
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>>;
/// Returns channels contents for each inbound HRMP channel addressed to the teyrchain we are
/// collating for.
///
/// Empty channels are also included.
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>>;
/// Yields the persisted validation data for the given `ParaId` along with an assumption that
/// should be used if the para currently occupies a core.
///
/// Returns `None` if either the para is not registered or the assumption is `Freed`
/// and the para already occupies a core.
async fn persisted_validation_data(
&self,
block_id: PHash,
para_id: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>>;
/// Get the receipt of the first candidate pending availability of this para_id. This returns
/// `Some` for any paras assigned to occupied cores in `availability_cores` and `None`
/// otherwise.
#[deprecated(
note = "`candidate_pending_availability` only returns one candidate and is deprecated. Use `candidates_pending_availability` instead."
)]
async fn candidate_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>>;
/// Returns the session index expected at a child of the block.
async fn session_index_for_child(&self, block_id: PHash) -> RelayChainResult<SessionIndex>;
/// Get a stream of import block notifications.
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>>;
/// Get a stream of new best block notifications.
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>>;
/// Wait for a block with a given hash in the relay chain.
///
/// This method returns immediately on error or if the block is already
/// reported to be in chain. Otherwise, it waits for the block to arrive.
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()>;
/// Get a stream of finality notifications.
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>>;
/// Whether the synchronization service is undergoing major sync.
/// Returns true if so.
async fn is_major_syncing(&self) -> RelayChainResult<bool>;
/// Get a handle to the overseer.
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle>;
/// Generate a storage read proof.
async fn prove_read(
&self,
relay_parent: PHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof>;
/// Returns the validation code hash for the given `para_id` using the given
/// `occupied_core_assumption`.
async fn validation_code_hash(
&self,
relay_parent: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>>;
/// Get the receipts of all candidates pending availability for this para_id.
async fn candidates_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>>;
/// Get the runtime version of the relay chain.
async fn version(&self, relay_parent: PHash) -> RelayChainResult<RuntimeVersion>;
/// Yields information on all availability cores as relevant to the child block.
///
/// Cores are either free, scheduled or occupied. Free cores can have paras assigned to them.
async fn availability_cores(
&self,
relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>>;
/// Fetch the claim queue.
async fn claim_queue(
&self,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>>;
/// Fetch the scheduling lookahead value.
async fn scheduling_lookahead(&self, relay_parent: PHash) -> RelayChainResult<u32>;
async fn candidate_events(&self, at: RelayHash) -> RelayChainResult<Vec<CandidateEvent>>;
}
#[async_trait]
impl<T> RelayChainInterface for Arc<T>
where
T: RelayChainInterface + ?Sized,
{
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
(**self).retrieve_dmq_contents(para_id, relay_parent).await
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
(**self).retrieve_all_inbound_hrmp_channel_contents(para_id, relay_parent).await
}
async fn persisted_validation_data(
&self,
block_id: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
(**self)
.persisted_validation_data(block_id, para_id, occupied_core_assumption)
.await
}
#[allow(deprecated)]
async fn candidate_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
(**self).candidate_pending_availability(block_id, para_id).await
}
async fn session_index_for_child(&self, block_id: PHash) -> RelayChainResult<SessionIndex> {
(**self).session_index_for_child(block_id).await
}
async fn validators(&self, block_id: PHash) -> RelayChainResult<Vec<ValidatorId>> {
(**self).validators(block_id).await
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
(**self).import_notification_stream().await
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
(**self).finality_notification_stream().await
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
(**self).best_block_hash().await
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
(**self).finalized_block_hash().await
}
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
(**self).call_runtime_api(method_name, hash, payload).await
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
(**self).is_major_syncing().await
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
(**self).overseer_handle()
}
async fn get_storage_by_key(
&self,
relay_parent: PHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
(**self).get_storage_by_key(relay_parent, key).await
}
async fn prove_read(
&self,
relay_parent: PHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof> {
(**self).prove_read(relay_parent, relevant_keys).await
}
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()> {
(**self).wait_for_block(hash).await
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
(**self).new_best_notification_stream().await
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
(**self).header(block_id).await
}
async fn validation_code_hash(
&self,
relay_parent: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
(**self)
.validation_code_hash(relay_parent, para_id, occupied_core_assumption)
.await
}
async fn availability_cores(
&self,
relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
(**self).availability_cores(relay_parent).await
}
async fn candidates_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
(**self).candidates_pending_availability(block_id, para_id).await
}
async fn version(&self, relay_parent: PHash) -> RelayChainResult<RuntimeVersion> {
(**self).version(relay_parent).await
}
async fn claim_queue(
&self,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
(**self).claim_queue(relay_parent).await
}
async fn scheduling_lookahead(&self, relay_parent: PHash) -> RelayChainResult<u32> {
(**self).scheduling_lookahead(relay_parent).await
}
async fn candidate_events(&self, at: RelayHash) -> RelayChainResult<Vec<CandidateEvent>> {
(**self).candidate_events(at).await
}
}
/// Helper function to call an arbitrary runtime API using a `RelayChainInterface` client.
/// Unlike the trait method, this function can be generic, so it handles the encoding of input and
/// output params.
pub async fn call_runtime_api<R>(
client: &(impl RelayChainInterface + ?Sized),
method_name: &'static str,
hash: RelayHash,
payload: impl Encode,
) -> RelayChainResult<R>
where
R: Decode,
{
let res = client.call_runtime_api(method_name, hash, &payload.encode()).await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
@@ -0,0 +1,76 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-minimal-node"
version = "0.7.0"
edition.workspace = true
description = "Minimal node implementation to be used in tandem with RPC or light-client mode."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# pezkuwi deps
pezkuwi-core-primitives = { workspace = true, default-features = true }
pezkuwi-node-network-protocol = { workspace = true, default-features = true }
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
pezkuwi-network-bridge = { workspace = true, default-features = true }
pezkuwi-service = { workspace = true, default-features = true }
# bizinikiwi deps
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-authority-discovery = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-common = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-babe = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# pezcumulus deps
pezcumulus-client-bootnodes = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-rpc-interface = { workspace = true, default-features = true }
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-bootnodes/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-rpc-interface/runtime-benchmarks",
"pezkuwi-core-primitives/runtime-benchmarks",
"pezkuwi-network-bridge/runtime-benchmarks",
"pezkuwi-node-network-protocol/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-service/runtime-benchmarks",
"pezsc-authority-discovery/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network-common/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsc-tracing/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-babe/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,540 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use std::{
collections::{BTreeMap, VecDeque},
pin::Pin,
};
use cumulus_primitives_core::{InboundDownwardMessage, ParaId, PersistedValidationData};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainResult};
use cumulus_relay_chain_rpc_interface::RelayChainRpcClient;
use futures::{Stream, StreamExt};
use pezkuwi_core_primitives::{Block, BlockNumber, Hash, Header};
use pezkuwi_overseer::{ChainApiBackend, RuntimeApiSubsystemClient};
use pezkuwi_primitives::{
async_backing::{AsyncBackingParams, BackingState, Constraints},
slashing, ApprovalVotingParams, CoreIndex, NodeFeatures,
};
use pezsc_authority_discovery::{AuthorityDiscovery, Error as AuthorityDiscoveryError};
use pezsc_client_api::AuxStore;
use pezsp_api::{ApiError, RuntimeApiInfo};
use pezsp_blockchain::Info;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
#[derive(Clone)]
pub struct BlockChainRpcClient {
rpc_client: RelayChainRpcClient,
}
impl BlockChainRpcClient {
pub fn new(rpc_client: RelayChainRpcClient) -> Self {
Self { rpc_client }
}
pub async fn chain_get_header(
&self,
hash: Option<Hash>,
) -> Result<Option<Header>, RelayChainError> {
self.rpc_client.chain_get_header(hash).await
}
pub async fn block_get_hash(
&self,
number: Option<BlockNumber>,
) -> Result<Option<Hash>, RelayChainError> {
self.rpc_client.chain_get_block_hash(number).await
}
}
#[async_trait::async_trait]
impl ChainApiBackend for BlockChainRpcClient {
async fn header(
&self,
hash: <Block as BlockT>::Hash,
) -> pezsp_blockchain::Result<Option<<Block as BlockT>::Header>> {
Ok(self.rpc_client.chain_get_header(Some(hash)).await?)
}
async fn info(&self) -> pezsp_blockchain::Result<Info<Block>> {
let (best_header_opt, genesis_hash, finalized_head) = futures::try_join!(
self.rpc_client.chain_get_header(None),
self.rpc_client.chain_get_head(Some(0)),
self.rpc_client.chain_get_finalized_head()
)?;
let best_header = best_header_opt.ok_or_else(|| {
RelayChainError::GenericError(
"Unable to retrieve best header from relay chain.".to_string(),
)
})?;
let finalized_header =
self.rpc_client.chain_get_header(Some(finalized_head)).await?.ok_or_else(|| {
RelayChainError::GenericError(
"Unable to retrieve finalized header from relay chain.".to_string(),
)
})?;
Ok(Info {
best_hash: best_header.hash(),
best_number: best_header.number,
genesis_hash,
finalized_hash: finalized_head,
finalized_number: finalized_header.number,
finalized_state: Some((finalized_header.hash(), finalized_header.number)),
number_leaves: 1,
block_gap: None,
})
}
async fn number(
&self,
hash: <Block as BlockT>::Hash,
) -> pezsp_blockchain::Result<Option<<<Block as BlockT>::Header as HeaderT>::Number>> {
Ok(self
.rpc_client
.chain_get_header(Some(hash))
.await?
.map(|maybe_header| maybe_header.number))
}
async fn hash(
&self,
number: NumberFor<Block>,
) -> pezsp_blockchain::Result<Option<<Block as BlockT>::Hash>> {
Ok(self.rpc_client.chain_get_block_hash(number.into()).await?)
}
}
#[async_trait::async_trait]
impl RuntimeApiSubsystemClient for BlockChainRpcClient {
async fn validators(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::ValidatorId>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_validators(at).await?)
}
async fn validator_groups(
&self,
at: Hash,
) -> Result<
(
Vec<Vec<pezkuwi_primitives::ValidatorIndex>>,
pezkuwi_primitives::GroupRotationInfo<BlockNumber>,
),
pezsp_api::ApiError,
> {
Ok(self.rpc_client.teyrchain_host_validator_groups(at).await?)
}
async fn availability_cores(
&self,
at: Hash,
) -> Result<
Vec<pezkuwi_primitives::CoreState<Hash, pezkuwi_core_primitives::BlockNumber>>,
pezsp_api::ApiError,
> {
Ok(self.rpc_client.teyrchain_host_availability_cores(at).await?)
}
async fn persisted_validation_data(
&self,
at: Hash,
para_id: ParaId,
assumption: pezkuwi_primitives::OccupiedCoreAssumption,
) -> Result<Option<PersistedValidationData<Hash, BlockNumber>>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_persisted_validation_data(at, para_id, assumption)
.await?)
}
async fn assumed_validation_data(
&self,
at: Hash,
para_id: ParaId,
expected_persisted_validation_data_hash: Hash,
) -> Result<
Option<(
PersistedValidationData<Hash, BlockNumber>,
pezkuwi_primitives::ValidationCodeHash,
)>,
pezsp_api::ApiError,
> {
Ok(self
.rpc_client
.teyrchain_host_assumed_validation_data(
at,
para_id,
expected_persisted_validation_data_hash,
)
.await?)
}
async fn check_validation_outputs(
&self,
at: Hash,
para_id: ParaId,
outputs: pezkuwi_primitives::CandidateCommitments,
) -> Result<bool, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_check_validation_outputs(at, para_id, outputs)
.await?)
}
async fn session_index_for_child(
&self,
at: Hash,
) -> Result<pezkuwi_primitives::SessionIndex, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_session_index_for_child(at).await?)
}
async fn validation_code(
&self,
at: Hash,
para_id: ParaId,
assumption: pezkuwi_primitives::OccupiedCoreAssumption,
) -> Result<Option<pezkuwi_primitives::ValidationCode>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_validation_code(at, para_id, assumption).await?)
}
async fn candidate_pending_availability(
&self,
at: Hash,
para_id: cumulus_primitives_core::ParaId,
) -> Result<Option<pezkuwi_primitives::CommittedCandidateReceiptV2<Hash>>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_candidate_pending_availability(at, para_id)
.await?)
}
async fn candidate_events(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::CandidateEvent<Hash>>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_candidate_events(at).await?)
}
async fn dmq_contents(
&self,
at: Hash,
recipient: ParaId,
) -> Result<Vec<InboundDownwardMessage<BlockNumber>>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_dmq_contents(recipient, at).await?)
}
async fn inbound_hrmp_channels_contents(
&self,
at: Hash,
recipient: ParaId,
) -> Result<
std::collections::BTreeMap<
ParaId,
Vec<pezkuwi_core_primitives::InboundHrmpMessage<BlockNumber>>,
>,
pezsp_api::ApiError,
> {
Ok(self
.rpc_client
.teyrchain_host_inbound_hrmp_channels_contents(recipient, at)
.await?)
}
async fn validation_code_by_hash(
&self,
at: Hash,
validation_code_hash: pezkuwi_primitives::ValidationCodeHash,
) -> Result<Option<pezkuwi_primitives::ValidationCode>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_validation_code_by_hash(at, validation_code_hash)
.await?)
}
async fn on_chain_votes(
&self,
at: Hash,
) -> Result<Option<pezkuwi_primitives::ScrapedOnChainVotes<Hash>>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_on_chain_votes(at).await?)
}
async fn session_info(
&self,
at: Hash,
index: pezkuwi_primitives::SessionIndex,
) -> Result<Option<pezkuwi_primitives::SessionInfo>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_session_info(at, index).await?)
}
async fn session_executor_params(
&self,
at: Hash,
session_index: pezkuwi_primitives::SessionIndex,
) -> Result<Option<pezkuwi_primitives::ExecutorParams>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_session_executor_params(at, session_index)
.await?)
}
async fn submit_pvf_check_statement(
&self,
at: Hash,
stmt: pezkuwi_primitives::PvfCheckStatement,
signature: pezkuwi_primitives::ValidatorSignature,
) -> Result<(), pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_submit_pvf_check_statement(at, stmt, signature)
.await?)
}
async fn pvfs_require_precheck(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::ValidationCodeHash>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_pvfs_require_precheck(at).await?)
}
async fn validation_code_hash(
&self,
at: Hash,
para_id: ParaId,
assumption: pezkuwi_primitives::OccupiedCoreAssumption,
) -> Result<Option<pezkuwi_primitives::ValidationCodeHash>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_validation_code_hash(at, para_id, assumption)
.await?)
}
async fn current_epoch(&self, at: Hash) -> Result<pezsp_consensus_babe::Epoch, pezsp_api::ApiError> {
Ok(self.rpc_client.babe_api_current_epoch(at).await?)
}
async fn authorities(
&self,
at: Hash,
) -> std::result::Result<Vec<pezkuwi_primitives::AuthorityDiscoveryId>, pezsp_api::ApiError> {
Ok(self.rpc_client.authority_discovery_authorities(at).await?)
}
async fn api_version_teyrchain_host(&self, at: Hash) -> Result<Option<u32>, pezsp_api::ApiError> {
let api_id = <dyn pezkuwi_primitives::runtime_api::TeyrchainHost<Block>>::ID;
Ok(self.rpc_client.runtime_version(at).await.map(|v| v.api_version(&api_id))?)
}
async fn disputes(
&self,
at: Hash,
) -> Result<
Vec<(
pezkuwi_primitives::SessionIndex,
pezkuwi_primitives::CandidateHash,
pezkuwi_primitives::DisputeState<pezkuwi_primitives::BlockNumber>,
)>,
ApiError,
> {
Ok(self.rpc_client.teyrchain_host_disputes(at).await?)
}
async fn unapplied_slashes(
&self,
at: Hash,
) -> Result<
Vec<(
pezkuwi_primitives::SessionIndex,
pezkuwi_primitives::CandidateHash,
slashing::LegacyPendingSlashes,
)>,
ApiError,
> {
Ok(self.rpc_client.teyrchain_host_unapplied_slashes(at).await?)
}
async fn unapplied_slashes_v2(
&self,
at: Hash,
) -> Result<
Vec<(
pezkuwi_primitives::SessionIndex,
pezkuwi_primitives::CandidateHash,
slashing::PendingSlashes,
)>,
ApiError,
> {
Ok(self.rpc_client.teyrchain_host_unapplied_slashes_v2(at).await?)
}
async fn key_ownership_proof(
&self,
at: Hash,
validator_id: pezkuwi_primitives::ValidatorId,
) -> Result<Option<slashing::OpaqueKeyOwnershipProof>, ApiError> {
Ok(self.rpc_client.teyrchain_host_key_ownership_proof(at, validator_id).await?)
}
async fn submit_report_dispute_lost(
&self,
at: Hash,
dispute_proof: slashing::DisputeProof,
key_ownership_proof: slashing::OpaqueKeyOwnershipProof,
) -> Result<Option<()>, ApiError> {
Ok(self
.rpc_client
.teyrchain_host_submit_report_dispute_lost(at, dispute_proof, key_ownership_proof)
.await?)
}
async fn minimum_backing_votes(
&self,
at: Hash,
session_index: pezkuwi_primitives::SessionIndex,
) -> Result<u32, ApiError> {
Ok(self.rpc_client.teyrchain_host_minimum_backing_votes(at, session_index).await?)
}
async fn disabled_validators(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::ValidatorIndex>, ApiError> {
Ok(self.rpc_client.teyrchain_host_disabled_validators(at).await?)
}
async fn async_backing_params(&self, at: Hash) -> Result<AsyncBackingParams, ApiError> {
Ok(self.rpc_client.teyrchain_host_async_backing_params(at).await?)
}
async fn para_backing_state(
&self,
at: Hash,
para_id: ParaId,
) -> Result<Option<BackingState>, ApiError> {
Ok(self.rpc_client.teyrchain_host_para_backing_state(at, para_id).await?)
}
/// Approval voting configuration parameters
async fn approval_voting_params(
&self,
at: Hash,
session_index: pezkuwi_primitives::SessionIndex,
) -> Result<ApprovalVotingParams, ApiError> {
Ok(self
.rpc_client
.teyrchain_host_staging_approval_voting_params(at, session_index)
.await?)
}
async fn node_features(&self, at: Hash) -> Result<NodeFeatures, ApiError> {
Ok(self.rpc_client.teyrchain_host_node_features(at).await?)
}
async fn claim_queue(
&self,
at: Hash,
) -> Result<BTreeMap<CoreIndex, VecDeque<ParaId>>, ApiError> {
Ok(self.rpc_client.teyrchain_host_claim_queue(at).await?)
}
async fn candidates_pending_availability(
&self,
at: Hash,
para_id: cumulus_primitives_core::ParaId,
) -> Result<Vec<pezkuwi_primitives::CommittedCandidateReceiptV2<Hash>>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_candidates_pending_availability(at, para_id)
.await?)
}
async fn backing_constraints(
&self,
at: Hash,
para_id: ParaId,
) -> Result<Option<Constraints>, ApiError> {
Ok(self.rpc_client.teyrchain_host_backing_constraints(at, para_id).await?)
}
async fn scheduling_lookahead(&self, at: Hash) -> Result<u32, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_scheduling_lookahead(at).await?)
}
async fn validation_code_bomb_limit(&self, at: Hash) -> Result<u32, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_validation_code_bomb_limit(at).await?)
}
async fn para_ids(&self, at: Hash) -> Result<Vec<ParaId>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_para_ids(at).await?)
}
}
#[async_trait::async_trait]
impl AuthorityDiscovery<Block> for BlockChainRpcClient {
async fn authorities(
&self,
at: Hash,
) -> std::result::Result<Vec<pezkuwi_primitives::AuthorityDiscoveryId>, pezsp_api::ApiError> {
let result = self.rpc_client.authority_discovery_authorities(at).await?;
Ok(result)
}
async fn best_hash(&self) -> std::result::Result<Hash, AuthorityDiscoveryError> {
self.block_get_hash(None)
.await
.ok()
.flatten()
.ok_or_else(|| AuthorityDiscoveryError::BestBlockFetchingError)
}
}
impl BlockChainRpcClient {
pub async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = Header> + Send>>> {
Ok(self.rpc_client.get_imported_heads_stream()?.boxed())
}
pub async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = Header> + Send>>> {
Ok(self.rpc_client.get_finalized_heads_stream()?.boxed())
}
}
// Implementation required by ChainApiSubsystem
// but never called in our case.
impl AuxStore for BlockChainRpcClient {
fn insert_aux<
'a,
'b: 'a,
'c: 'a,
I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
D: IntoIterator<Item = &'a &'b [u8]>,
>(
&self,
_insert: I,
_delete: D,
) -> pezsp_blockchain::Result<()> {
unimplemented!("Not supported on the RPC collator")
}
fn get_aux(&self, _key: &[u8]) -> pezsp_blockchain::Result<Option<Vec<u8>>> {
unimplemented!("Not supported on the RPC collator")
}
}
@@ -0,0 +1,150 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use futures::{select, StreamExt};
use std::sync::Arc;
use pezkuwi_overseer::{
BlockInfo, Handle, Overseer, OverseerConnector, OverseerHandle, SpawnGlue, UnpinHandle,
};
use pezkuwi_service::overseer::{collator_overseer_builder, OverseerGenArgs};
use pezsc_network::{request_responses::IncomingRequest, service::traits::NetworkService};
use pezsc_service::TaskManager;
use pezsc_utils::mpsc::tracing_unbounded;
use cumulus_relay_chain_interface::RelayChainError;
use crate::BlockChainRpcClient;
fn build_overseer(
connector: OverseerConnector,
args: OverseerGenArgs<pezsc_service::SpawnTaskHandle, BlockChainRpcClient>,
) -> Result<
(Overseer<SpawnGlue<pezsc_service::SpawnTaskHandle>, Arc<BlockChainRpcClient>>, OverseerHandle),
RelayChainError,
> {
let builder =
collator_overseer_builder(args).map_err(|e| RelayChainError::Application(e.into()))?;
builder
.build_with_connector(connector)
.map_err(|e| RelayChainError::Application(e.into()))
}
pub(crate) fn spawn_overseer(
overseer_args: OverseerGenArgs<pezsc_service::SpawnTaskHandle, BlockChainRpcClient>,
task_manager: &TaskManager,
relay_chain_rpc_client: Arc<BlockChainRpcClient>,
) -> Result<pezkuwi_overseer::Handle, RelayChainError> {
let (overseer, overseer_handle) = build_overseer(OverseerConnector::default(), overseer_args)
.map_err(|e| {
tracing::error!("Failed to initialize overseer: {}", e);
e
})?;
let overseer_handle = Handle::new(overseer_handle);
{
let handle = overseer_handle.clone();
task_manager.spawn_essential_handle().spawn_blocking(
"overseer",
None,
Box::pin(async move {
use futures::{pin_mut, FutureExt};
let forward = forward_collator_events(relay_chain_rpc_client, handle).fuse();
let overseer_fut = overseer.run().fuse();
pin_mut!(overseer_fut);
pin_mut!(forward);
select! {
_ = forward => (),
_ = overseer_fut => (),
}
}),
);
}
Ok(overseer_handle)
}
/// Minimal relay chain node representation
pub struct NewMinimalNode {
/// Task manager running all tasks for the minimal node
pub task_manager: TaskManager,
/// Overseer handle to interact with subsystems
pub overseer_handle: Handle,
/// Network service
pub network_service: Arc<dyn NetworkService>,
/// Teyrchain bootnode request-response protocol receiver
pub paranode_rx: async_channel::Receiver<IncomingRequest>,
}
/// Glues together the [`Overseer`] and `BlockchainEvents` by forwarding
/// import and finality notifications into the [`OverseerHandle`].
async fn forward_collator_events(
client: Arc<BlockChainRpcClient>,
mut handle: Handle,
) -> Result<(), RelayChainError> {
let mut finality = client.finality_notification_stream().await?.fuse();
let mut imports = client.import_notification_stream().await?.fuse();
// Collators do no need to pin any specific blocks
let (dummy_sink, _) = tracing_unbounded("does-not-matter", 42);
let dummy_unpin_handle = UnpinHandle::new(Default::default(), dummy_sink);
loop {
select! {
f = finality.next() => {
match f {
Some(header) => {
let hash = header.hash();
tracing::info!(
target: "minimal-pezkuwi-node",
"Received finalized block via RPC: #{} ({} -> {})",
header.number,
header.parent_hash,
hash,
);
let unpin_handle = dummy_unpin_handle.clone();
let block_info = BlockInfo { hash, parent_hash: header.parent_hash, number: header.number, unpin_handle };
handle.block_finalized(block_info).await;
}
None => return Err(RelayChainError::GenericError("Relay chain finality stream ended.".to_string())),
}
},
i = imports.next() => {
match i {
Some(header) => {
let hash = header.hash();
tracing::info!(
target: "minimal-pezkuwi-node",
"Received imported block via RPC: #{} ({} -> {})",
header.number,
header.parent_hash,
hash,
);
let unpin_handle = dummy_unpin_handle.clone();
let block_info = BlockInfo { hash, parent_hash: header.parent_hash, number: header.number, unpin_handle };
handle.block_imported(block_info).await;
}
None => return Err(RelayChainError::GenericError("Relay chain import stream ended.".to_string())),
}
}
}
}
}
@@ -0,0 +1,293 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use collator_overseer::NewMinimalNode;
use cumulus_client_bootnodes::bootnode_request_response_config;
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
use cumulus_relay_chain_rpc_interface::{RelayChainRpcClient, RelayChainRpcInterface, Url};
use network::build_collator_network;
use pezkuwi_network_bridge::{peer_sets_info, IsAuthority};
use pezkuwi_node_network_protocol::{
peer_set::{PeerSet, PeerSetProtocolNames},
request_response::{
v1, v2, IncomingRequest, IncomingRequestReceiver, Protocol, ReqProtocolNames,
},
};
use pezkuwi_core_primitives::{Block as RelayBlock, Hash as RelayHash};
use pezkuwi_node_subsystem_util::metrics::prometheus::Registry;
use pezkuwi_primitives::CollatorPair;
use pezkuwi_service::{overseer::OverseerGenArgs, IsTeyrchainNode};
use pezsc_authority_discovery::Service as AuthorityDiscoveryService;
use pezsc_network::{
config::FullNetworkConfiguration, request_responses::IncomingRequest as GenericIncomingRequest,
service::traits::NetworkService, Event, NetworkBackend, NetworkEventStream,
};
use pezsc_service::{config::PrometheusConfig, Configuration, TaskManager};
use pezsp_runtime::{app_crypto::Pair, traits::Block as BlockT};
use futures::{FutureExt, StreamExt};
use std::sync::Arc;
mod blockchain_rpc_client;
mod collator_overseer;
mod network;
pub use blockchain_rpc_client::BlockChainRpcClient;
const LOG_TARGET: &str = "minimal-relaychain-node";
fn build_authority_discovery_service<Block: BlockT>(
task_manager: &TaskManager,
client: Arc<BlockChainRpcClient>,
config: &Configuration,
network: Arc<dyn NetworkService>,
prometheus_registry: Option<Registry>,
) -> AuthorityDiscoveryService {
let auth_disc_publish_non_global_ips = config.network.allow_non_globals_in_dht;
let auth_disc_public_addresses = config.network.public_addresses.clone();
let authority_discovery_role = pezsc_authority_discovery::Role::Discover;
let dht_event_stream = network.event_stream("authority-discovery").filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
});
let net_config_path = config.network.net_config_path.clone();
let (worker, service) = pezsc_authority_discovery::new_worker_and_service_with_config(
pezsc_authority_discovery::WorkerConfig {
publish_non_global_ips: auth_disc_publish_non_global_ips,
public_addresses: auth_disc_public_addresses,
// Require that authority discovery records are signed.
strict_record_validation: true,
persisted_cache_directory: net_config_path,
..Default::default()
},
client,
Arc::new(network.clone()),
Box::pin(dht_event_stream),
authority_discovery_role,
prometheus_registry,
task_manager.spawn_handle(),
);
task_manager.spawn_handle().spawn(
"authority-discovery-worker",
Some("authority-discovery"),
worker.run(),
);
service
}
async fn build_interface(
pezkuwi_config: Configuration,
task_manager: &mut TaskManager,
client: RelayChainRpcClient,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<GenericIncomingRequest>,
)> {
let collator_pair = CollatorPair::generate().0;
let blockchain_rpc_client = Arc::new(BlockChainRpcClient::new(client.clone()));
let collator_node = match pezkuwi_config.network.network_backend {
pezsc_network::config::NetworkBackendType::Libp2p =>
new_minimal_relay_chain::<RelayBlock, pezsc_network::NetworkWorker<RelayBlock, RelayHash>>(
pezkuwi_config,
collator_pair.clone(),
blockchain_rpc_client,
)
.await?,
pezsc_network::config::NetworkBackendType::Litep2p =>
new_minimal_relay_chain::<RelayBlock, pezsc_network::Litep2pNetworkBackend>(
pezkuwi_config,
collator_pair.clone(),
blockchain_rpc_client,
)
.await?,
};
task_manager.add_child(collator_node.task_manager);
Ok((
Arc::new(RelayChainRpcInterface::new(client, collator_node.overseer_handle)),
Some(collator_pair),
collator_node.network_service,
collator_node.paranode_rx,
))
}
pub async fn build_minimal_relay_chain_node_with_rpc(
relay_chain_config: Configuration,
teyrchain_prometheus_registry: Option<&Registry>,
task_manager: &mut TaskManager,
relay_chain_url: Vec<Url>,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<GenericIncomingRequest>,
)> {
let client = cumulus_relay_chain_rpc_interface::create_client_and_start_worker(
relay_chain_url,
task_manager,
teyrchain_prometheus_registry,
)
.await?;
build_interface(relay_chain_config, task_manager, client).await
}
/// Builds a minimal relay chain node. Chain data is fetched
/// via [`BlockChainRpcClient`] and fed into the overseer and its subsystems.
///
/// Instead of spawning all subsystems, this minimal node will only spawn subsystems
/// required to collate:
/// - AvailabilityRecovery
/// - CollationGeneration
/// - CollatorProtocol
/// - NetworkBridgeRx
/// - NetworkBridgeTx
/// - RuntimeApi
#[pezsc_tracing::logging::prefix_logs_with("Relaychain")]
async fn new_minimal_relay_chain<Block: BlockT, Network: NetworkBackend<RelayBlock, RelayHash>>(
config: Configuration,
collator_pair: CollatorPair,
relay_chain_rpc_client: Arc<BlockChainRpcClient>,
) -> Result<NewMinimalNode, RelayChainError> {
let role = config.role;
let mut net_config = pezsc_network::config::FullNetworkConfiguration::<_, _, Network>::new(
&config.network,
config.prometheus_config.as_ref().map(|cfg| cfg.registry.clone()),
);
let metrics = Network::register_notification_metrics(
config.prometheus_config.as_ref().map(|cfg| &cfg.registry),
);
let peer_store_handle = net_config.peer_store_handle();
let prometheus_registry = config.prometheus_registry();
let task_manager = TaskManager::new(config.tokio_handle.clone(), prometheus_registry)?;
if let Some(PrometheusConfig { port, registry }) = config.prometheus_config.clone() {
task_manager.spawn_handle().spawn(
"prometheus-endpoint",
None,
prometheus_endpoint::init_prometheus(port, registry).map(drop),
);
}
let genesis_hash = relay_chain_rpc_client.block_get_hash(Some(0)).await?.unwrap_or_default();
let peerset_protocol_names =
PeerSetProtocolNames::new(genesis_hash, config.chain_spec.fork_id());
let is_authority = if role.is_authority() { IsAuthority::Yes } else { IsAuthority::No };
let notification_services = peer_sets_info::<_, Network>(
is_authority,
&peerset_protocol_names,
metrics.clone(),
Arc::clone(&peer_store_handle),
)
.into_iter()
.map(|(config, (peerset, service))| {
net_config.add_notification_protocol(config);
(peerset, service)
})
.collect::<std::collections::HashMap<PeerSet, Box<dyn pezsc_network::NotificationService>>>();
let request_protocol_names = ReqProtocolNames::new(genesis_hash, config.chain_spec.fork_id());
let (collation_req_v1_receiver, collation_req_v2_receiver, available_data_req_receiver) =
build_request_response_protocol_receivers(&request_protocol_names, &mut net_config);
let (cfg, paranode_rx) = bootnode_request_response_config::<_, _, Network>(
genesis_hash,
config.chain_spec.fork_id(),
);
net_config.add_request_response_protocol(cfg);
let best_header = relay_chain_rpc_client
.chain_get_header(None)
.await?
.ok_or_else(|| RelayChainError::RpcCallError("Unable to fetch best header".to_string()))?;
let (network, sync_service) = build_collator_network::<Network>(
&config,
net_config,
task_manager.spawn_handle(),
genesis_hash,
best_header,
metrics,
)
.map_err(|e| RelayChainError::Application(Box::new(e)))?;
let authority_discovery_service = build_authority_discovery_service::<Block>(
&task_manager,
relay_chain_rpc_client.clone(),
&config,
network.clone(),
prometheus_registry.cloned(),
);
let overseer_args = OverseerGenArgs {
runtime_client: relay_chain_rpc_client.clone(),
network_service: network.clone(),
sync_service,
authority_discovery_service,
collation_req_v1_receiver,
collation_req_v2_receiver,
available_data_req_receiver,
registry: prometheus_registry,
spawner: task_manager.spawn_handle(),
is_teyrchain_node: IsTeyrchainNode::Collator(collator_pair),
overseer_message_channel_capacity_override: None,
req_protocol_names: request_protocol_names,
peerset_protocol_names,
notification_services,
};
let overseer_handle =
collator_overseer::spawn_overseer(overseer_args, &task_manager, relay_chain_rpc_client)?;
Ok(NewMinimalNode { task_manager, overseer_handle, network_service: network, paranode_rx })
}
fn build_request_response_protocol_receivers<
Block: BlockT,
Network: NetworkBackend<Block, <Block as BlockT>::Hash>,
>(
request_protocol_names: &ReqProtocolNames,
config: &mut FullNetworkConfiguration<Block, <Block as BlockT>::Hash, Network>,
) -> (
IncomingRequestReceiver<v1::CollationFetchingRequest>,
IncomingRequestReceiver<v2::CollationFetchingRequest>,
IncomingRequestReceiver<v1::AvailableDataFetchingRequest>,
) {
let (collation_req_v1_receiver, cfg) =
IncomingRequest::get_config_receiver::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let (collation_req_v2_receiver, cfg) =
IncomingRequest::get_config_receiver::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let (available_data_req_receiver, cfg) =
IncomingRequest::get_config_receiver::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let cfg =
Protocol::ChunkFetchingV1.get_outbound_only_config::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let cfg =
Protocol::ChunkFetchingV2.get_outbound_only_config::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
(collation_req_v1_receiver, collation_req_v2_receiver, available_data_req_receiver)
}
@@ -0,0 +1,176 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use pezkuwi_core_primitives::{Block, Hash, Header};
use pezsp_runtime::traits::NumberFor;
use pezsc_network::{
config::{
NetworkConfiguration, NonReservedPeerMode, NotificationHandshake, PeerStore, ProtocolId,
SetConfig,
},
peer_store::PeerStoreProvider,
service::traits::NetworkService,
NotificationMetrics,
};
use pezsc_network::{config::FullNetworkConfiguration, NetworkBackend, NotificationService};
use pezsc_network_common::{role::Roles, sync::message::BlockAnnouncesHandshake};
use pezsc_service::{error::Error, Configuration, SpawnTaskHandle};
use std::{iter, sync::Arc};
/// Build the network service, the network status sinks and an RPC sender.
pub(crate) fn build_collator_network<Network: NetworkBackend<Block, Hash>>(
config: &Configuration,
mut network_config: FullNetworkConfiguration<Block, Hash, Network>,
spawn_handle: SpawnTaskHandle,
genesis_hash: Hash,
best_header: Header,
notification_metrics: NotificationMetrics,
) -> Result<(Arc<dyn NetworkService>, Arc<dyn pezsp_consensus::SyncOracle + Send + Sync>), Error> {
let protocol_id = config.protocol_id();
let (block_announce_config, notification_service) = get_block_announce_proto_config::<Network>(
protocol_id.clone(),
&None,
Roles::from(&config.role),
best_header.number,
best_header.hash(),
genesis_hash,
notification_metrics.clone(),
network_config.peer_store_handle(),
);
// Since this node has no syncing, we do not want light-clients to connect to it.
// Here we set any potential light-client slots to 0.
adjust_network_config_light_in_peers(&mut network_config.network_config);
let peer_store = network_config.take_peer_store();
spawn_handle.spawn("peer-store", Some("networking"), peer_store.run());
let network_params = pezsc_network::config::Params::<Block, Hash, Network> {
role: config.role,
executor: {
let spawn_handle = Clone::clone(&spawn_handle);
Box::new(move |fut| {
spawn_handle.spawn("libp2p-node", Some("networking"), fut);
})
},
fork_id: None,
network_config,
genesis_hash,
protocol_id,
metrics_registry: config.prometheus_config.as_ref().map(|config| config.registry.clone()),
block_announce_config,
bitswap_config: None,
notification_metrics,
};
let network_worker = Network::new(network_params)?;
let network_service = network_worker.network_service();
// The network worker is responsible for gathering all network messages and processing
// them. This is quite a heavy task, and at the time of the writing of this comment it
// frequently happens that this future takes several seconds or in some situations
// even more than a minute until it has processed its entire queue. This is clearly an
// issue, and ideally we would like to fix the network future to take as little time as
// possible, but we also take the extra harm-prevention measure to execute the networking
// future using `spawn_blocking`.
spawn_handle.spawn_blocking("network-worker", Some("networking"), async move {
// The notification service must be kept alive to allow litep2p to handle
// requests under the hood. It has been noted that without the notification
// service of the `/block-announces/1` protocol, collators are not advertised
// and their produced blocks do not propagate:
// https://github.com/pezkuwichain/pezkuwi-sdk/issues/154
//
// This is because the full nodes on the relay chain will attempt to establish
// a connection to the minimal relay chain. By dropping the notification service,
// litep2p would terminate the background task which handles the `/block-announces/1`
// notification protocol. The downstream effect of this is that the full node
// would ban and disconnect the the minimal relay chain node.
let _notification_service = notification_service;
network_worker.run().await;
});
Ok((network_service, Arc::new(SyncOracle {})))
}
fn adjust_network_config_light_in_peers(config: &mut NetworkConfiguration) {
let light_client_in_peers = (config.default_peers_set.in_peers +
config.default_peers_set.out_peers)
.saturating_sub(config.default_peers_set_num_full);
if light_client_in_peers > 0 {
tracing::debug!(target: crate::LOG_TARGET, "Detected {light_client_in_peers} peer slots for light clients. Since this minimal node does support\
neither syncing nor light-client request/response, we are setting them to 0.");
}
config.default_peers_set.in_peers =
config.default_peers_set.in_peers.saturating_sub(light_client_in_peers);
}
struct SyncOracle;
impl pezsp_consensus::SyncOracle for SyncOracle {
fn is_major_syncing(&self) -> bool {
false
}
fn is_offline(&self) -> bool {
true
}
}
fn get_block_announce_proto_config<Network: NetworkBackend<Block, Hash>>(
protocol_id: ProtocolId,
fork_id: &Option<String>,
roles: Roles,
best_number: NumberFor<Block>,
best_hash: Hash,
genesis_hash: Hash,
metrics: NotificationMetrics,
peer_store_handle: Arc<dyn PeerStoreProvider>,
) -> (Network::NotificationProtocolConfig, Box<dyn NotificationService>) {
let block_announces_protocol = {
let genesis_hash = genesis_hash.as_ref();
if let Some(ref fork_id) = fork_id {
format!("/{}/{}/block-announces/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
} else {
format!("/{}/block-announces/1", array_bytes::bytes2hex("", genesis_hash))
}
};
Network::notification_config(
block_announces_protocol.into(),
iter::once(format!("/{}/block-announces/1", protocol_id.as_ref()).into()).collect(),
1024 * 1024,
Some(NotificationHandshake::new(BlockAnnouncesHandshake::<Block>::build(
roles,
best_number,
best_hash,
genesis_hash,
))),
// NOTE: `set_config` will be ignored by `protocol.rs` as the block announcement
// protocol is still hardcoded into the peerset.
SetConfig {
in_peers: 0,
out_peers: 0,
reserved_nodes: Vec::new(),
non_reserved_mode: NonReservedPeerMode::Deny,
},
metrics,
peer_store_handle,
)
}
@@ -0,0 +1,62 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-rpc-interface"
version = "0.7.0"
edition.workspace = true
description = "Implementation of the RelayChainInterface trait that connects to a remote RPC-node."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
pezkuwi-overseer = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-rpc-api = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsp-authority-discovery = { workspace = true, default-features = true }
pezsp-consensus-babe = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-storage = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
tokio = { features = ["sync"], workspace = true, default-features = true }
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
jsonrpsee = { features = ["ws-client"], workspace = true }
prometheus = { workspace = true }
schnellru = { workspace = true }
serde = { workspace = true, default-features = true }
serde_json = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
url = { workspace = true }
[dev-dependencies]
portpicker = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-rpc-api/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-authority-discovery/runtime-benchmarks",
"pezsp-consensus-babe/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,292 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use async_trait::async_trait;
use core::time::Duration;
use cumulus_primitives_core::{
relay_chain::{
CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt,
Hash as RelayHash, Header as RelayHeader, InboundHrmpMessage, OccupiedCoreAssumption,
SessionIndex, ValidationCodeHash, ValidatorId,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
use cumulus_relay_chain_interface::{
BlockNumber, CoreState, PHeader, RelayChainError, RelayChainInterface, RelayChainResult,
};
use futures::{FutureExt, Stream, StreamExt};
use pezkuwi_overseer::Handle;
use pezsc_client_api::StorageProof;
use pezsp_state_machine::StorageValue;
use pezsp_storage::StorageKey;
use pezsp_version::RuntimeVersion;
use std::{collections::btree_map::BTreeMap, pin::Pin};
use cumulus_primitives_core::relay_chain::BlockId;
pub use url::Url;
mod metrics;
mod reconnecting_ws_client;
mod rpc_client;
pub use rpc_client::{create_client_and_start_worker, RelayChainRpcClient};
const TIMEOUT_IN_SECONDS: u64 = 6;
/// RelayChainRpcInterface is used to interact with a full node that is running locally
/// in the same process.
#[derive(Clone)]
pub struct RelayChainRpcInterface {
rpc_client: RelayChainRpcClient,
overseer_handle: Handle,
}
impl RelayChainRpcInterface {
pub fn new(rpc_client: RelayChainRpcClient, overseer_handle: Handle) -> Self {
Self { rpc_client, overseer_handle }
}
}
#[async_trait]
impl RelayChainInterface for RelayChainRpcInterface {
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: RelayHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
self.rpc_client.teyrchain_host_dmq_contents(para_id, relay_parent).await
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: RelayHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
self.rpc_client
.teyrchain_host_inbound_hrmp_channels_contents(para_id, relay_parent)
.await
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(num) => {
if let Some(hash) = self.rpc_client.chain_get_block_hash(Some(num)).await? {
hash
} else {
return Ok(None);
}
},
};
let header = self.rpc_client.chain_get_header(Some(hash)).await?;
Ok(header)
}
async fn persisted_validation_data(
&self,
hash: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
self.rpc_client
.teyrchain_host_persisted_validation_data(hash, para_id, occupied_core_assumption)
.await
}
async fn validation_code_hash(
&self,
hash: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
self.rpc_client
.validation_code_hash(hash, para_id, occupied_core_assumption)
.await
}
async fn candidate_pending_availability(
&self,
hash: RelayHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
self.rpc_client
.teyrchain_host_candidate_pending_availability(hash, para_id)
.await
}
async fn session_index_for_child(&self, hash: RelayHash) -> RelayChainResult<SessionIndex> {
self.rpc_client.teyrchain_host_session_index_for_child(hash).await
}
async fn validators(&self, block_id: RelayHash) -> RelayChainResult<Vec<ValidatorId>> {
self.rpc_client.teyrchain_host_validators(block_id).await
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = RelayHeader> + Send>>> {
let imported_headers_stream = self.rpc_client.get_imported_heads_stream()?;
Ok(imported_headers_stream.boxed())
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = RelayHeader> + Send>>> {
let imported_headers_stream = self.rpc_client.get_finalized_heads_stream()?;
Ok(imported_headers_stream.boxed())
}
async fn best_block_hash(&self) -> RelayChainResult<RelayHash> {
self.rpc_client.chain_get_head(None).await
}
async fn finalized_block_hash(&self) -> RelayChainResult<RelayHash> {
self.rpc_client.chain_get_finalized_head().await
}
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
self.rpc_client
.call_remote_runtime_function_encoded(method_name, hash, payload)
.await
.map(|bytes| bytes.to_vec())
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
self.rpc_client.system_health().await.map(|h| h.is_syncing)
}
fn overseer_handle(&self) -> RelayChainResult<Handle> {
Ok(self.overseer_handle.clone())
}
async fn get_storage_by_key(
&self,
relay_parent: RelayHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
let storage_key = StorageKey(key.to_vec());
self.rpc_client
.state_get_storage(storage_key, Some(relay_parent))
.await
.map(|storage_data| storage_data.map(|sv| sv.0))
}
async fn prove_read(
&self,
relay_parent: RelayHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof> {
let cloned = relevant_keys.clone();
let storage_keys: Vec<StorageKey> = cloned.into_iter().map(StorageKey).collect();
self.rpc_client
.state_get_read_proof(storage_keys, Some(relay_parent))
.await
.map(|read_proof| {
StorageProof::new(read_proof.proof.into_iter().map(|bytes| bytes.to_vec()))
})
}
/// Wait for a given relay chain block
///
/// The hash of the block to wait for is passed. We wait for the block to arrive or return after
/// a timeout.
///
/// Implementation:
/// 1. Register a listener to all new blocks.
/// 2. Check if the block is already in chain. If yes, succeed early.
/// 3. Wait for the block to be imported via subscription.
/// 4. If timeout is reached, we return an error.
async fn wait_for_block(&self, wait_for_hash: RelayHash) -> RelayChainResult<()> {
let mut head_stream = self.rpc_client.get_imported_heads_stream()?;
if self.rpc_client.chain_get_header(Some(wait_for_hash)).await?.is_some() {
return Ok(());
}
let mut timeout = futures_timer::Delay::new(Duration::from_secs(TIMEOUT_IN_SECONDS)).fuse();
loop {
futures::select! {
_ = timeout => return Err(RelayChainError::WaitTimeout(wait_for_hash)),
evt = head_stream.next().fuse() => match evt {
Some(evt) if evt.hash() == wait_for_hash => return Ok(()),
// Not the event we waited on.
Some(_) => continue,
None => return Err(RelayChainError::ImportListenerClosed(wait_for_hash)),
}
}
}
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = RelayHeader> + Send>>> {
let imported_headers_stream = self.rpc_client.get_best_heads_stream()?;
Ok(imported_headers_stream.boxed())
}
async fn candidates_pending_availability(
&self,
hash: RelayHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
self.rpc_client
.teyrchain_host_candidates_pending_availability(hash, para_id)
.await
}
async fn version(&self, relay_parent: RelayHash) -> RelayChainResult<RuntimeVersion> {
self.rpc_client.runtime_version(relay_parent).await
}
async fn availability_cores(
&self,
relay_parent: RelayHash,
) -> RelayChainResult<Vec<CoreState<RelayHash, BlockNumber>>> {
self.rpc_client.teyrchain_host_availability_cores(relay_parent).await
}
async fn claim_queue(
&self,
relay_parent: RelayHash,
) -> RelayChainResult<
BTreeMap<cumulus_relay_chain_interface::CoreIndex, std::collections::VecDeque<ParaId>>,
> {
self.rpc_client.teyrchain_host_claim_queue(relay_parent).await
}
async fn scheduling_lookahead(&self, relay_parent: RelayHash) -> RelayChainResult<u32> {
self.rpc_client.teyrchain_host_scheduling_lookahead(relay_parent).await
}
async fn candidate_events(
&self,
relay_parent: RelayHash,
) -> RelayChainResult<Vec<CandidateEvent>> {
self.rpc_client.teyrchain_host_candidate_events(relay_parent).await
}
}
@@ -0,0 +1,50 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use prometheus::{Error as PrometheusError, HistogramTimer, Registry};
use prometheus_endpoint::{HistogramOpts, HistogramVec, Opts};
/// Gathers metrics about the blockchain RPC client.
#[derive(Clone)]
pub(crate) struct RelaychainRpcMetrics {
rpc_request: HistogramVec,
}
impl RelaychainRpcMetrics {
pub(crate) fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
rpc_request: prometheus_endpoint::register(
HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"relay_chain_rpc_interface",
"Tracks stats about pezcumulus relay chain RPC interface",
),
buckets: prometheus::exponential_buckets(0.001, 4.0, 9)
.expect("function parameters are constant and always valid; qed"),
},
&["method"],
)?,
registry,
)?,
})
}
pub(crate) fn start_request_timer(&self, method: &str) -> HistogramTimer {
self.rpc_request.with_label_values(&[method]).start_timer()
}
}
@@ -0,0 +1,528 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use cumulus_primitives_core::relay_chain::{
Block as RelayBlock, BlockNumber as RelayNumber, Hash as RelayHash, Header as RelayHeader,
};
use futures::{
channel::{mpsc::Sender, oneshot::Sender as OneshotSender},
future::BoxFuture,
stream::FuturesUnordered,
FutureExt, StreamExt,
};
use jsonrpsee::{
core::{
client::{Client as JsonRpcClient, ClientT, Subscription},
params::ArrayParams,
ClientError as JsonRpseeError, JsonValue,
},
ws_client::WsClientBuilder,
};
use pezsc_rpc_api::chain::ChainApiClient;
use schnellru::{ByLength, LruMap};
use pezsp_runtime::generic::SignedBlock;
use std::{sync::Arc, time::Duration};
use tokio::sync::mpsc::{
channel as tokio_channel, Receiver as TokioReceiver, Sender as TokioSender,
};
use url::Url;
use crate::rpc_client::{distribute_header, RpcDispatcherMessage};
const LOG_TARGET: &str = "reconnecting-websocket-client";
const DEFAULT_EXTERNAL_RPC_CONN_RETRIES: usize = 5;
const DEFAULT_SLEEP_TIME_MS_BETWEEN_RETRIES: u64 = 1000;
const DEFAULT_SLEEP_EXP_BACKOFF_BETWEEN_RETRIES: i32 = 2;
/// Worker that should be used in combination with [`RelayChainRpcClient`].
///
/// Must be polled to distribute header notifications to listeners.
pub struct ReconnectingWebsocketWorker {
ws_urls: Vec<String>,
/// Communication channel with the RPC client
client_receiver: TokioReceiver<RpcDispatcherMessage>,
/// Senders to distribute incoming header notifications to
imported_header_listeners: Vec<Sender<RelayHeader>>,
finalized_header_listeners: Vec<Sender<RelayHeader>>,
best_header_listeners: Vec<Sender<RelayHeader>>,
}
/// Format url and force addition of a port
fn url_to_string_with_port(url: Url) -> Option<String> {
// This is already validated on CLI side, just defensive here
if (url.scheme() != "ws" && url.scheme() != "wss") || url.host_str().is_none() {
tracing::warn!(target: LOG_TARGET, ?url, "Non-WebSocket URL or missing host.");
return None;
}
// Either we have a user-supplied port or use the default for 'ws' or 'wss' here
Some(format!(
"{}://{}:{}{}{}",
url.scheme(),
url.host_str()?,
url.port_or_known_default()?,
url.path(),
url.query().map(|query| format!("?{}", query)).unwrap_or_default()
))
}
/// Manages the active websocket client.
/// Responsible for creating request futures, subscription streams
/// and reconnections.
#[derive(Debug)]
struct ClientManager {
urls: Vec<String>,
active_client: Arc<JsonRpcClient>,
active_index: usize,
}
struct RelayChainSubscriptions {
import_subscription: Subscription<RelayHeader>,
finalized_subscription: Subscription<RelayHeader>,
best_subscription: Subscription<RelayHeader>,
}
/// Try to find a new RPC server to connect to. Uses a naive retry
/// logic that does an exponential backoff in between iterations
/// through all URLs from the list. It uses a constant to tell how
/// many iterations of connection attempts to all URLs we allow. We
/// return early when a connection is made.
async fn connect_next_available_rpc_server(
urls: &Vec<String>,
starting_position: usize,
) -> Result<(usize, Arc<JsonRpcClient>), ()> {
tracing::debug!(target: LOG_TARGET, starting_position, "Connecting to RPC server.");
let mut prev_iteration: u32 = 0;
for (counter, url) in urls
.iter()
.cycle()
.skip(starting_position)
.take(urls.len() * DEFAULT_EXTERNAL_RPC_CONN_RETRIES)
.enumerate()
{
// If we reached the end of the urls list, backoff before retrying
// connections to the entire list once more.
let Ok(current_iteration) = (counter / urls.len()).try_into() else {
tracing::error!(target: LOG_TARGET, "Too many connection attempts to the RPC servers, aborting...");
break;
};
if current_iteration > prev_iteration {
// Safe conversion given we convert positive i32s which are lower than u64::MAX.
tokio::time::sleep(Duration::from_millis(
DEFAULT_SLEEP_TIME_MS_BETWEEN_RETRIES *
DEFAULT_SLEEP_EXP_BACKOFF_BETWEEN_RETRIES.pow(prev_iteration) as u64,
))
.await;
prev_iteration = current_iteration;
}
let index = (starting_position + counter) % urls.len();
tracing::info!(
target: LOG_TARGET,
attempt = current_iteration,
index,
url,
"Trying to connect to next external relaychain node.",
);
match WsClientBuilder::default().build(&url).await {
Ok(ws_client) => return Ok((index, Arc::new(ws_client))),
Err(err) => tracing::debug!(target: LOG_TARGET, url, ?err, "Unable to connect."),
};
}
tracing::error!(target: LOG_TARGET, "Retrying to connect to any external relaychain node failed.");
Err(())
}
impl ClientManager {
pub async fn new(urls: Vec<String>) -> Result<Self, ()> {
if urls.is_empty() {
return Err(());
}
let active_client = connect_next_available_rpc_server(&urls, 0).await?;
Ok(Self { urls, active_client: active_client.1, active_index: active_client.0 })
}
pub async fn connect_to_new_rpc_server(&mut self) -> Result<(), ()> {
let new_active =
connect_next_available_rpc_server(&self.urls, self.active_index + 1).await?;
self.active_client = new_active.1;
self.active_index = new_active.0;
Ok(())
}
async fn get_subscriptions(&self) -> Result<RelayChainSubscriptions, JsonRpseeError> {
let import_subscription = <JsonRpcClient as ChainApiClient<
RelayNumber,
RelayHash,
RelayHeader,
SignedBlock<RelayBlock>,
>>::subscribe_all_heads(&self.active_client)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
?e,
"Unable to open `chain_subscribeAllHeads` subscription."
);
e
})?;
let best_subscription = <JsonRpcClient as ChainApiClient<
RelayNumber,
RelayHash,
RelayHeader,
SignedBlock<RelayBlock>,
>>::subscribe_new_heads(&self.active_client)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
?e,
"Unable to open `chain_subscribeNewHeads` subscription."
);
e
})?;
let finalized_subscription = <JsonRpcClient as ChainApiClient<
RelayNumber,
RelayHash,
RelayHeader,
SignedBlock<RelayBlock>,
>>::subscribe_finalized_heads(&self.active_client)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
?e,
"Unable to open `chain_subscribeFinalizedHeads` subscription."
);
e
})?;
Ok(RelayChainSubscriptions {
import_subscription,
best_subscription,
finalized_subscription,
})
}
/// Create a request future that performs an RPC request and sends the results to the caller.
/// In case of a dead websocket connection, it returns the original request parameters to
/// enable retries.
fn create_request(
&self,
method: String,
params: ArrayParams,
response_sender: OneshotSender<Result<JsonValue, JsonRpseeError>>,
) -> BoxFuture<'static, Result<(), RpcDispatcherMessage>> {
let future_client = self.active_client.clone();
async move {
let resp = future_client.request(&method, params.clone()).await;
// We should only return the original request in case
// the websocket connection is dead and requires a restart.
// Other errors should be forwarded to the request caller.
if let Err(JsonRpseeError::RestartNeeded(_)) = resp {
return Err(RpcDispatcherMessage::Request(method, params, response_sender));
}
if let Err(err) = response_sender.send(resp) {
tracing::debug!(
target: LOG_TARGET,
?err,
"Recipient no longer interested in request result"
);
}
Ok(())
}
.boxed()
}
}
enum ConnectionStatus {
Connected,
ReconnectRequired(Option<RpcDispatcherMessage>),
}
impl ReconnectingWebsocketWorker {
/// Create new worker. Returns the worker and a channel to register new listeners.
pub async fn new(
urls: Vec<Url>,
) -> (ReconnectingWebsocketWorker, TokioSender<RpcDispatcherMessage>) {
let urls = urls.into_iter().filter_map(url_to_string_with_port).collect();
let (tx, rx) = tokio_channel(100);
let worker = ReconnectingWebsocketWorker {
ws_urls: urls,
client_receiver: rx,
imported_header_listeners: Vec::new(),
finalized_header_listeners: Vec::new(),
best_header_listeners: Vec::new(),
};
(worker, tx)
}
/// Reconnect via [`ClientManager`] and provide new notification streams.
async fn handle_reconnect(
&mut self,
client_manager: &mut ClientManager,
pending_requests: &mut FuturesUnordered<
BoxFuture<'static, Result<(), RpcDispatcherMessage>>,
>,
first_failed_request: Option<RpcDispatcherMessage>,
) -> Result<RelayChainSubscriptions, String> {
let mut requests_to_retry = Vec::new();
if let Some(req @ RpcDispatcherMessage::Request(_, _, _)) = first_failed_request {
requests_to_retry.push(req);
}
// At this point, all pending requests will return an error since the
// websocket connection is dead. So draining the pending requests should be fast.
while !pending_requests.is_empty() {
if let Some(Err(req)) = pending_requests.next().await {
requests_to_retry.push(req);
}
}
if client_manager.connect_to_new_rpc_server().await.is_err() {
return Err("Unable to find valid external RPC server, shutting down.".to_string());
};
for item in requests_to_retry.into_iter() {
if let RpcDispatcherMessage::Request(method, params, response_sender) = item {
pending_requests.push(client_manager.create_request(
method,
params,
response_sender,
));
};
}
client_manager.get_subscriptions().await.map_err(|e| {
format!("Not able to create streams from newly connected RPC server, shutting down. err: {:?}", e)
})
}
/// Run this worker to drive notification streams.
/// The worker does the following:
/// - Listen for [`RpcDispatcherMessage`], perform requests and register new listeners for the
/// notification streams
/// - Distribute incoming import, best head and finalization notifications to registered
/// listeners. If an error occurs during sending, the receiver has been closed and we remove
/// the sender from the list.
/// - Find a new valid RPC server to connect to in case the websocket connection is terminated.
/// If the worker is not able to connect to an RPC server from the list, the worker shuts
/// down.
pub async fn run(mut self) {
let mut pending_requests = FuturesUnordered::new();
let urls = std::mem::take(&mut self.ws_urls);
let Ok(mut client_manager) = ClientManager::new(urls).await else {
tracing::error!(target: LOG_TARGET, "No valid RPC url found. Stopping RPC worker.");
return;
};
let Ok(mut subscriptions) = client_manager.get_subscriptions().await else {
tracing::error!(target: LOG_TARGET, "Unable to fetch subscriptions on initial connection.");
return;
};
let mut imported_blocks_cache = LruMap::new(ByLength::new(40));
let mut should_reconnect = ConnectionStatus::Connected;
let mut last_seen_finalized_num: RelayNumber = 0;
loop {
// This branch is taken if the websocket connection to the current RPC server is closed.
if let ConnectionStatus::ReconnectRequired(maybe_failed_request) = should_reconnect {
match self
.handle_reconnect(
&mut client_manager,
&mut pending_requests,
maybe_failed_request,
)
.await
{
Ok(new_subscriptions) => {
subscriptions = new_subscriptions;
},
Err(message) => {
tracing::error!(
target: LOG_TARGET,
message,
"Unable to reconnect, stopping worker."
);
return;
},
}
should_reconnect = ConnectionStatus::Connected;
}
tokio::select! {
evt = self.client_receiver.recv() => match evt {
Some(RpcDispatcherMessage::RegisterBestHeadListener(tx)) => {
self.best_header_listeners.push(tx);
},
Some(RpcDispatcherMessage::RegisterImportListener(tx)) => {
self.imported_header_listeners.push(tx)
},
Some(RpcDispatcherMessage::RegisterFinalizationListener(tx)) => {
self.finalized_header_listeners.push(tx)
},
Some(RpcDispatcherMessage::Request(method, params, response_sender)) => {
pending_requests.push(client_manager.create_request(method, params, response_sender));
},
None => {
tracing::error!(target: LOG_TARGET, "RPC client receiver closed. Stopping RPC Worker.");
return;
}
},
should_retry = pending_requests.next(), if !pending_requests.is_empty() => {
if let Some(Err(req)) = should_retry {
should_reconnect = ConnectionStatus::ReconnectRequired(Some(req));
}
},
import_event = subscriptions.import_subscription.next() => {
match import_event {
Some(Ok(header)) => {
let hash = header.hash();
if imported_blocks_cache.peek(&hash).is_some() {
tracing::debug!(
target: LOG_TARGET,
number = header.number,
?hash,
"Duplicate imported block header. This might happen after switching to a new RPC node. Skipping distribution."
);
continue;
}
imported_blocks_cache.insert(hash, ());
distribute_header(header, &mut self.imported_header_listeners);
},
None => {
tracing::error!(target: LOG_TARGET, "Subscription closed.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
Some(Err(error)) => {
tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
}
},
best_header_event = subscriptions.best_subscription.next() => {
match best_header_event {
Some(Ok(header)) => distribute_header(header, &mut self.best_header_listeners),
None => {
tracing::error!(target: LOG_TARGET, "Subscription closed.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
Some(Err(error)) => {
tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
}
}
finalized_event = subscriptions.finalized_subscription.next() => {
match finalized_event {
Some(Ok(header)) if header.number > last_seen_finalized_num => {
last_seen_finalized_num = header.number;
distribute_header(header, &mut self.finalized_header_listeners);
},
Some(Ok(header)) => {
tracing::debug!(
target: LOG_TARGET,
number = header.number,
last_seen_finalized_num,
"Duplicate finalized block header. This might happen after switching to a new RPC node. Skipping distribution."
);
},
None => {
tracing::error!(target: LOG_TARGET, "Subscription closed.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
Some(Err(error)) => {
tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
}
}
}
}
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::{url_to_string_with_port, ClientManager};
use jsonrpsee::Methods;
use url::Url;
const SERVER_STARTUP_DELAY_SECONDS: u64 = 10;
#[test]
fn url_to_string_works() {
let url = Url::parse("wss://something/path").unwrap();
assert_eq!(Some("wss://something:443/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("ws://something/path").unwrap();
assert_eq!(Some("ws://something:80/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("wss://something:100/path").unwrap();
assert_eq!(Some("wss://something:100/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("wss://something:100/path").unwrap();
assert_eq!(Some("wss://something:100/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("wss://something/path?query=yes").unwrap();
assert_eq!(
Some("wss://something:443/path?query=yes".to_string()),
url_to_string_with_port(url)
);
let url = Url::parse("wss://something:9090/path?query=yes").unwrap();
assert_eq!(
Some("wss://something:9090/path?query=yes".to_string()),
url_to_string_with_port(url)
);
}
#[tokio::test]
// Testing the retry logic at full means increasing CI with half a minute according
// to the current logic, so lets test it best effort.
async fn client_manager_retry_logic() {
let port = portpicker::pick_unused_port().unwrap();
let server = jsonrpsee::server::Server::builder()
.build(format!("0.0.0.0:{}", port))
.await
.unwrap();
// Start the server.
let server = tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(SERVER_STARTUP_DELAY_SECONDS)).await;
server.start(Methods::default())
});
// Start the client. Not exitting right away with an error means it
// is handling gracefully received connections refused while the server
// is starting.
let res = ClientManager::new(vec![format!("ws://127.0.0.1:{}", port)]).await;
assert!(res.is_ok());
server.await.unwrap();
}
}
@@ -0,0 +1,795 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
use futures::channel::{
mpsc::{Receiver, Sender},
oneshot::Sender as OneshotSender,
};
use jsonrpsee::{
core::{params::ArrayParams, ClientError as JsonRpseeError},
rpc_params,
};
use prometheus::Registry;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value as JsonValue;
use std::collections::{btree_map::BTreeMap, VecDeque};
use tokio::sync::mpsc::Sender as TokioSender;
use codec::{Decode, Encode};
use cumulus_primitives_core::{
relay_chain::{
async_backing::{AsyncBackingParams, BackingState, Constraints},
slashing, ApprovalVotingParams, BlockNumber, CandidateCommitments, CandidateEvent,
CandidateHash, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex,
CoreState, DisputeState, ExecutorParams, GroupRotationInfo, Hash as RelayHash,
Header as RelayHeader, InboundHrmpMessage, NodeFeatures, OccupiedCoreAssumption,
PvfCheckStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode,
ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainResult};
use pezsc_client_api::StorageData;
use pezsc_rpc_api::{state::ReadProof, system::Health};
use pezsc_service::TaskManager;
use pezsp_consensus_babe::Epoch;
use pezsp_storage::StorageKey;
use pezsp_version::RuntimeVersion;
use crate::{metrics::RelaychainRpcMetrics, reconnecting_ws_client::ReconnectingWebsocketWorker};
pub use url::Url;
const LOG_TARGET: &str = "relay-chain-rpc-client";
const NOTIFICATION_CHANNEL_SIZE_LIMIT: usize = 20;
/// Messages for communication between [`RelayChainRpcClient`] and the RPC workers.
#[derive(Debug)]
pub enum RpcDispatcherMessage {
/// Register new listener for the best headers stream. Contains a sender which will be used
/// to send incoming headers.
RegisterBestHeadListener(Sender<RelayHeader>),
/// Register new listener for the import headers stream. Contains a sender which will be used
/// to send incoming headers.
RegisterImportListener(Sender<RelayHeader>),
/// Register new listener for the finalized headers stream. Contains a sender which will be
/// used to send incoming headers.
RegisterFinalizationListener(Sender<RelayHeader>),
/// Register new listener for the finalized headers stream.
/// Contains the following:
/// - [`String`] representing the RPC method to be called
/// - [`ArrayParams`] for the parameters to the RPC call
/// - [`OneshotSender`] for the return value of the request
Request(String, ArrayParams, OneshotSender<Result<JsonValue, JsonRpseeError>>),
}
/// Entry point to create [`RelayChainRpcClient`] and start a worker that communicates
/// to JsonRPC servers over the network.
pub async fn create_client_and_start_worker(
urls: Vec<Url>,
task_manager: &mut TaskManager,
prometheus_registry: Option<&Registry>,
) -> RelayChainResult<RelayChainRpcClient> {
let (worker, sender) = ReconnectingWebsocketWorker::new(urls).await;
task_manager
.spawn_essential_handle()
.spawn("relay-chain-rpc-worker", None, worker.run());
let client = RelayChainRpcClient::new(sender, prometheus_registry);
Ok(client)
}
#[derive(Serialize)]
struct PayloadToHex<'a>(#[serde(with = "pezsp_core::bytes")] &'a [u8]);
/// Client that maps RPC methods and deserializes results
#[derive(Clone)]
pub struct RelayChainRpcClient {
/// Sender to send messages to the worker.
worker_channel: TokioSender<RpcDispatcherMessage>,
metrics: Option<RelaychainRpcMetrics>,
}
impl RelayChainRpcClient {
/// Initialize new RPC Client.
///
/// This client expects a channel connected to a worker that processes
/// requests sent via this channel.
pub(crate) fn new(
worker_channel: TokioSender<RpcDispatcherMessage>,
prometheus_registry: Option<&Registry>,
) -> Self {
RelayChainRpcClient {
worker_channel,
metrics: prometheus_registry
.and_then(|inner| RelaychainRpcMetrics::register(inner).map_err(|err| {
tracing::warn!(target: LOG_TARGET, error = %err, "Unable to instantiate the RPC client metrics, continuing w/o metrics setup.");
}).ok()),
}
}
/// Same as `call_remote_runtime_function` but work on encoded data
pub async fn call_remote_runtime_function_encoded(
&self,
method_name: &str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<pezsp_core::Bytes> {
let payload = PayloadToHex(payload);
let params = rpc_params! {
method_name,
payload,
hash
};
self.request_tracing::<pezsp_core::Bytes, _>("state_call", params, |err| {
tracing::trace!(
target: LOG_TARGET,
%method_name,
%hash,
error = %err,
"Error during call to 'state_call'.",
);
})
.await
}
/// Call a call to `state_call` rpc method.
pub async fn call_remote_runtime_function<R: Decode>(
&self,
method_name: &str,
hash: RelayHash,
payload: Option<impl Encode>,
) -> RelayChainResult<R> {
let payload_bytes =
payload.map_or(pezsp_core::Bytes(Vec::new()), |v| pezsp_core::Bytes(v.encode()));
let res = self
.call_remote_runtime_function_encoded(method_name, hash, &payload_bytes)
.await?;
Decode::decode(&mut &*res.0).map_err(Into::into)
}
/// Perform RPC request
async fn request<'a, R>(
&self,
method: &'a str,
params: ArrayParams,
) -> Result<R, RelayChainError>
where
R: DeserializeOwned + std::fmt::Debug,
{
self.request_tracing(
method,
params,
|e| tracing::trace!(target:LOG_TARGET, error = %e, %method, "Unable to complete RPC request"),
)
.await
}
/// Perform RPC request
async fn request_tracing<'a, R, OR>(
&self,
method: &'a str,
params: ArrayParams,
trace_error: OR,
) -> Result<R, RelayChainError>
where
R: DeserializeOwned + std::fmt::Debug,
OR: Fn(&RelayChainError),
{
let _timer = self.metrics.as_ref().map(|inner| inner.start_request_timer(method));
let (tx, rx) = futures::channel::oneshot::channel();
let message = RpcDispatcherMessage::Request(method.into(), params, tx);
self.worker_channel.send(message).await.map_err(|err| {
RelayChainError::WorkerCommunicationError(format!(
"Unable to send message to RPC worker: {}",
err
))
})?;
let value = rx.await.map_err(|err| {
RelayChainError::WorkerCommunicationError(format!(
"RPC worker channel closed. This can hint and connectivity issues with the supplied RPC endpoints. Message: {}",
err
))
})??;
serde_json::from_value(value).map_err(|_| {
trace_error(&RelayChainError::GenericError("Unable to deserialize value".to_string()));
RelayChainError::RpcCallError(method.to_string())
})
}
/// Returns information regarding the current epoch.
pub async fn babe_api_current_epoch(&self, at: RelayHash) -> Result<Epoch, RelayChainError> {
self.call_remote_runtime_function("BabeApi_current_epoch", at, None::<()>).await
}
/// Scrape dispute relevant from on-chain, backing votes and resolved disputes.
pub async fn teyrchain_host_on_chain_votes(
&self,
at: RelayHash,
) -> Result<Option<ScrapedOnChainVotes<RelayHash>>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_on_chain_votes", at, None::<()>)
.await
}
/// Returns code hashes of PVFs that require pre-checking by validators in the active set.
pub async fn teyrchain_host_pvfs_require_precheck(
&self,
at: RelayHash,
) -> Result<Vec<ValidationCodeHash>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_pvfs_require_precheck", at, None::<()>)
.await
}
/// Submits a PVF pre-checking statement into the transaction pool.
pub async fn teyrchain_host_submit_pvf_check_statement(
&self,
at: RelayHash,
stmt: PvfCheckStatement,
signature: ValidatorSignature,
) -> Result<(), RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_submit_pvf_check_statement",
at,
Some((stmt, signature)),
)
.await
}
/// Get system health information
pub async fn system_health(&self) -> Result<Health, RelayChainError> {
self.request("system_health", rpc_params![]).await
}
/// Get read proof for `storage_keys`
pub async fn state_get_read_proof(
&self,
storage_keys: Vec<StorageKey>,
at: Option<RelayHash>,
) -> Result<ReadProof<RelayHash>, RelayChainError> {
let params = rpc_params![storage_keys, at];
self.request("state_getReadProof", params).await
}
/// Retrieve storage item at `storage_key`
pub async fn state_get_storage(
&self,
storage_key: StorageKey,
at: Option<RelayHash>,
) -> Result<Option<StorageData>, RelayChainError> {
let params = rpc_params![storage_key, at];
self.request("state_getStorage", params).await
}
/// Get hash of the n-th block in the canon chain.
///
/// By default returns latest block hash.
pub async fn chain_get_head(&self, at: Option<u64>) -> Result<RelayHash, RelayChainError> {
let params = rpc_params![at];
self.request("chain_getHead", params).await
}
/// Returns the validator groups and rotation info localized based on the hypothetical child
/// of a block whose state this is invoked on. Note that `now` in the `GroupRotationInfo`
/// should be the successor of the number of the block.
pub async fn teyrchain_host_validator_groups(
&self,
at: RelayHash,
) -> Result<(Vec<Vec<ValidatorIndex>>, GroupRotationInfo), RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_validator_groups", at, None::<()>)
.await
}
/// Get a vector of events concerning candidates that occurred within a block.
pub async fn teyrchain_host_candidate_events(
&self,
at: RelayHash,
) -> Result<Vec<CandidateEvent>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_candidate_events", at, None::<()>)
.await
}
/// Checks if the given validation outputs pass the acceptance criteria.
pub async fn teyrchain_host_check_validation_outputs(
&self,
at: RelayHash,
para_id: ParaId,
outputs: CandidateCommitments,
) -> Result<bool, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_check_validation_outputs",
at,
Some((para_id, outputs)),
)
.await
}
/// Returns the persisted validation data for the given `ParaId` along with the corresponding
/// validation code hash. Instead of accepting assumption about the para, matches the validation
/// data hash against an expected one and yields `None` if they're not equal.
pub async fn teyrchain_host_assumed_validation_data(
&self,
at: RelayHash,
para_id: ParaId,
expected_hash: RelayHash,
) -> Result<Option<(PersistedValidationData, ValidationCodeHash)>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_persisted_assumed_validation_data",
at,
Some((para_id, expected_hash)),
)
.await
}
/// Get hash of last finalized block.
pub async fn chain_get_finalized_head(&self) -> Result<RelayHash, RelayChainError> {
self.request("chain_getFinalizedHead", rpc_params![]).await
}
/// Get hash of n-th block.
pub async fn chain_get_block_hash(
&self,
block_number: Option<BlockNumber>,
) -> Result<Option<RelayHash>, RelayChainError> {
let params = rpc_params![block_number];
self.request("chain_getBlockHash", params).await
}
/// Yields the persisted validation data for the given `ParaId` along with an assumption that
/// should be used if the para currently occupies a core.
///
/// Returns `None` if either the para is not registered or the assumption is `Freed`
/// and the para already occupies a core.
pub async fn teyrchain_host_persisted_validation_data(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<PersistedValidationData>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_persisted_validation_data",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
/// Get the validation code from its hash.
pub async fn teyrchain_host_validation_code_by_hash(
&self,
at: RelayHash,
validation_code_hash: ValidationCodeHash,
) -> Result<Option<ValidationCode>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_by_hash",
at,
Some(validation_code_hash),
)
.await
}
/// Yields information on all availability cores as relevant to the child block.
/// Cores are either free or occupied. Free cores can have paras assigned to them.
pub async fn teyrchain_host_availability_cores(
&self,
at: RelayHash,
) -> Result<Vec<CoreState<RelayHash, BlockNumber>>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_availability_cores", at, None::<()>)
.await
}
/// Get runtime version
pub async fn runtime_version(&self, at: RelayHash) -> Result<RuntimeVersion, RelayChainError> {
let params = rpc_params![at];
self.request("state_getRuntimeVersion", params).await
}
/// Returns all onchain disputes.
pub async fn teyrchain_host_disputes(
&self,
at: RelayHash,
) -> Result<Vec<(SessionIndex, CandidateHash, DisputeState<BlockNumber>)>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_disputes", at, None::<()>)
.await
}
/// Returns a list of validators that lost a past session dispute and need to be slashed.
///
/// This is a staging method! Do not use on production runtimes!
pub async fn teyrchain_host_unapplied_slashes(
&self,
at: RelayHash,
) -> Result<Vec<(SessionIndex, CandidateHash, slashing::LegacyPendingSlashes)>, RelayChainError>
{
self.call_remote_runtime_function("TeyrchainHost_unapplied_slashes", at, None::<()>)
.await
}
/// Returns a list of validators that lost a past session dispute and need to be slashed.
pub async fn teyrchain_host_unapplied_slashes_v2(
&self,
at: RelayHash,
) -> Result<Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_unapplied_slashes_v2", at, None::<()>)
.await
}
/// Returns a merkle proof of a validator session key in a past session.
///
/// This is a staging method! Do not use on production runtimes!
pub async fn teyrchain_host_key_ownership_proof(
&self,
at: RelayHash,
validator_id: ValidatorId,
) -> Result<Option<slashing::OpaqueKeyOwnershipProof>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_key_ownership_proof",
at,
Some(validator_id),
)
.await
}
/// Submits an unsigned extrinsic to slash validators who lost a dispute about
/// a candidate of a past session.
///
/// This is a staging method! Do not use on production runtimes!
pub async fn teyrchain_host_submit_report_dispute_lost(
&self,
at: RelayHash,
dispute_proof: slashing::DisputeProof,
key_ownership_proof: slashing::OpaqueKeyOwnershipProof,
) -> Result<Option<()>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_submit_report_dispute_lost",
at,
Some((dispute_proof, key_ownership_proof)),
)
.await
}
pub async fn authority_discovery_authorities(
&self,
at: RelayHash,
) -> Result<Vec<pezsp_authority_discovery::AuthorityId>, RelayChainError> {
self.call_remote_runtime_function("AuthorityDiscoveryApi_authorities", at, None::<()>)
.await
}
/// Fetch the validation code used by a para, making the given `OccupiedCoreAssumption`.
///
/// Returns `None` if either the para is not registered or the assumption is `Freed`
/// and the para already occupies a core.
pub async fn teyrchain_host_validation_code(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<ValidationCode>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
/// Fetch the hash of the validation code used by a para, making the given
/// `OccupiedCoreAssumption`.
pub async fn teyrchain_host_validation_code_hash(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<ValidationCodeHash>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_hash",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
/// Get the session info for the given session, if stored.
pub async fn teyrchain_host_session_info(
&self,
at: RelayHash,
index: SessionIndex,
) -> Result<Option<SessionInfo>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_session_info", at, Some(index))
.await
}
/// Get the executor parameters for the given session, if stored
pub async fn teyrchain_host_session_executor_params(
&self,
at: RelayHash,
session_index: SessionIndex,
) -> Result<Option<ExecutorParams>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_session_executor_params",
at,
Some(session_index),
)
.await
}
/// Get header at specified hash
pub async fn chain_get_header(
&self,
hash: Option<RelayHash>,
) -> Result<Option<RelayHeader>, RelayChainError> {
let params = rpc_params![hash];
self.request("chain_getHeader", params).await
}
/// Get the receipt of a candidate pending availability. This returns `Some` for any paras
/// assigned to occupied cores in `availability_cores` and `None` otherwise.
pub async fn teyrchain_host_candidate_pending_availability(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Option<CommittedCandidateReceipt>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_candidate_pending_availability",
at,
Some(para_id),
)
.await
}
/// Returns the session index expected at a child of the block.
///
/// This can be used to instantiate a `SigningContext`.
pub async fn teyrchain_host_session_index_for_child(
&self,
at: RelayHash,
) -> Result<SessionIndex, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_session_index_for_child", at, None::<()>)
.await
}
/// Get the current validators.
pub async fn teyrchain_host_validators(
&self,
at: RelayHash,
) -> Result<Vec<ValidatorId>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_validators", at, None::<()>)
.await
}
/// Get the contents of all channels addressed to the given recipient. Channels that have no
/// messages in them are also included.
pub async fn teyrchain_host_inbound_hrmp_channels_contents(
&self,
para_id: ParaId,
at: RelayHash,
) -> Result<BTreeMap<ParaId, Vec<InboundHrmpMessage>>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_inbound_hrmp_channels_contents",
at,
Some(para_id),
)
.await
}
/// Get all the pending inbound messages in the downward message queue for a para.
pub async fn teyrchain_host_dmq_contents(
&self,
para_id: ParaId,
at: RelayHash,
) -> Result<Vec<InboundDownwardMessage>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_dmq_contents", at, Some(para_id))
.await
}
/// Get the minimum number of backing votes for a candidate.
pub async fn teyrchain_host_minimum_backing_votes(
&self,
at: RelayHash,
_session_index: SessionIndex,
) -> Result<u32, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_minimum_backing_votes", at, None::<()>)
.await
}
pub async fn teyrchain_host_node_features(
&self,
at: RelayHash,
) -> Result<NodeFeatures, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_node_features", at, None::<()>)
.await
}
pub async fn teyrchain_host_disabled_validators(
&self,
at: RelayHash,
) -> Result<Vec<ValidatorIndex>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_disabled_validators", at, None::<()>)
.await
}
#[allow(missing_docs)]
pub async fn teyrchain_host_async_backing_params(
&self,
at: RelayHash,
) -> Result<AsyncBackingParams, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_async_backing_params", at, None::<()>)
.await
}
#[allow(missing_docs)]
pub async fn teyrchain_host_staging_approval_voting_params(
&self,
at: RelayHash,
_session_index: SessionIndex,
) -> Result<ApprovalVotingParams, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_staging_approval_voting_params",
at,
None::<()>,
)
.await
}
pub async fn teyrchain_host_para_backing_state(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Option<BackingState>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_para_backing_state", at, Some(para_id))
.await
}
pub async fn teyrchain_host_claim_queue(
&self,
at: RelayHash,
) -> Result<BTreeMap<CoreIndex, VecDeque<ParaId>>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_claim_queue", at, None::<()>)
.await
}
/// Get the receipt of all candidates pending availability.
pub async fn teyrchain_host_candidates_pending_availability(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Vec<CommittedCandidateReceipt>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_candidates_pending_availability",
at,
Some(para_id),
)
.await
}
pub async fn teyrchain_host_scheduling_lookahead(
&self,
at: RelayHash,
) -> Result<u32, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_scheduling_lookahead", at, None::<()>)
.await
}
pub async fn teyrchain_host_validation_code_bomb_limit(
&self,
at: RelayHash,
) -> Result<u32, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_bomb_limit",
at,
None::<()>,
)
.await
}
pub async fn validation_code_hash(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<ValidationCodeHash>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_hash",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
pub async fn teyrchain_host_backing_constraints(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Option<Constraints>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_backing_constraints", at, Some(para_id))
.await
}
fn send_register_message_to_worker(
&self,
message: RpcDispatcherMessage,
) -> Result<(), RelayChainError> {
self.worker_channel
.try_send(message)
.map_err(|e| RelayChainError::WorkerCommunicationError(e.to_string()))
}
/// Get a stream of all imported relay chain headers
pub fn get_imported_heads_stream(&self) -> Result<Receiver<RelayHeader>, RelayChainError> {
let (tx, rx) =
futures::channel::mpsc::channel::<RelayHeader>(NOTIFICATION_CHANNEL_SIZE_LIMIT);
self.send_register_message_to_worker(RpcDispatcherMessage::RegisterImportListener(tx))?;
Ok(rx)
}
/// Get a stream of new best relay chain headers
pub fn get_best_heads_stream(&self) -> Result<Receiver<RelayHeader>, RelayChainError> {
let (tx, rx) =
futures::channel::mpsc::channel::<RelayHeader>(NOTIFICATION_CHANNEL_SIZE_LIMIT);
self.send_register_message_to_worker(RpcDispatcherMessage::RegisterBestHeadListener(tx))?;
Ok(rx)
}
/// Get a stream of finalized relay chain headers
pub fn get_finalized_heads_stream(&self) -> Result<Receiver<RelayHeader>, RelayChainError> {
let (tx, rx) =
futures::channel::mpsc::channel::<RelayHeader>(NOTIFICATION_CHANNEL_SIZE_LIMIT);
self.send_register_message_to_worker(RpcDispatcherMessage::RegisterFinalizationListener(
tx,
))?;
Ok(rx)
}
pub async fn teyrchain_host_para_ids(
&self,
at: RelayHash,
) -> Result<Vec<ParaId>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_para_ids", at, None::<()>)
.await
}
}
/// Send `header` through all channels contained in `senders`.
/// If no one is listening to the sender, it is removed from the vector.
pub fn distribute_header(header: RelayHeader, senders: &mut Vec<Sender<RelayHeader>>) {
senders.retain_mut(|e| {
match e.try_send(header.clone()) {
// Receiver has been dropped, remove Sender from list.
Err(error) if error.is_disconnected() => false,
// Channel is full. This should not happen.
// TODO: Improve error handling here
// https://github.com/pezkuwichain/kurdistan-sdk/issues/90
Err(error) => {
tracing::error!(target: LOG_TARGET, ?error, "Event distribution channel has reached its limit. This can lead to missed notifications.");
true
},
_ => true,
}
});
}
@@ -0,0 +1,36 @@
[package]
name = "pezcumulus-relay-chain-streams"
version = "0.7.0"
authors.workspace = true
description = "Pezcumulus client common relay chain streams."
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
futures = { workspace = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
]
@@ -0,0 +1,168 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Common utilities for transforming relay chain streams.
use std::sync::Arc;
use cumulus_relay_chain_interface::{RelayChainInterface, RelayChainResult};
use futures::{Stream, StreamExt};
use pezkuwi_node_subsystem::messages::RuntimeApiRequest;
use pezkuwi_primitives::{
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, Hash as PHash, Id as ParaId,
OccupiedCoreAssumption, SessionIndex,
};
use pezsp_api::RuntimeApiInfo;
use pezsp_consensus::SyncOracle;
const LOG_TARGET: &str = "pezcumulus-relay-chain-streams";
pub type RelayHeader = pezkuwi_primitives::Header;
/// Returns a stream over pending candidates for the teyrchain corresponding to `para_id`.
pub async fn pending_candidates(
relay_chain_client: impl RelayChainInterface + Clone,
para_id: ParaId,
sync_service: Arc<dyn SyncOracle + Sync + Send>,
) -> RelayChainResult<impl Stream<Item = (Vec<CommittedCandidateReceipt>, SessionIndex, RelayHeader)>>
{
let import_notification_stream = relay_chain_client.import_notification_stream().await?;
let filtered_stream = import_notification_stream.filter_map(move |n| {
let client = relay_chain_client.clone();
let sync_oracle = sync_service.clone();
async move {
let hash = n.hash();
if sync_oracle.is_major_syncing() {
tracing::debug!(
target: LOG_TARGET,
relay_hash = ?hash,
"Skipping candidate due to sync.",
);
return None;
}
let runtime_api_version = client
.version(hash)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch relay chain runtime version.",
)
})
.ok()?;
let teyrchain_host_runtime_api_version =
runtime_api_version
.api_version(
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<
pezkuwi_primitives::Block,
>>::ID,
)
.unwrap_or_default();
// If the relay chain runtime does not support the new runtime API, fallback to the
// deprecated one.
let pending_availability_result = if teyrchain_host_runtime_api_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
#[allow(deprecated)]
client
.candidate_pending_availability(hash, para_id)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch pending candidates.",
)
})
.map(|candidate| candidate.into_iter().collect::<Vec<_>>())
} else {
client.candidates_pending_availability(hash, para_id).await.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch pending candidates.",
)
})
};
let session_index_result = client.session_index_for_child(hash).await.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch session index.",
)
});
if let Ok(candidates) = pending_availability_result {
session_index_result.map(|session_index| (candidates, session_index, n)).ok()
} else {
None
}
}
});
Ok(filtered_stream)
}
/// Returns a stream that will yield best heads for the given `para_id`.
pub async fn new_best_heads(
relay_chain: impl RelayChainInterface + Clone,
para_id: ParaId,
) -> RelayChainResult<impl Stream<Item = Vec<u8>>> {
let new_best_notification_stream =
relay_chain.new_best_notification_stream().await?.filter_map(move |n| {
let relay_chain = relay_chain.clone();
async move { teyrchain_head_at(&relay_chain, n.hash(), para_id).await.ok().flatten() }
});
Ok(new_best_notification_stream)
}
/// Returns a stream that will yield finalized heads for the given `para_id`.
pub async fn finalized_heads(
relay_chain: impl RelayChainInterface + Clone,
para_id: ParaId,
) -> RelayChainResult<impl Stream<Item = (Vec<u8>, RelayHeader)>> {
let finality_notification_stream =
relay_chain.finality_notification_stream().await?.filter_map(move |n| {
let relay_chain = relay_chain.clone();
async move {
teyrchain_head_at(&relay_chain, n.hash(), para_id)
.await
.ok()
.flatten()
.map(|h| (h, n))
}
});
Ok(finality_notification_stream)
}
/// Returns head of the teyrchain at the given relay chain block.
async fn teyrchain_head_at(
relay_chain: &impl RelayChainInterface,
at: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<Vec<u8>>> {
relay_chain
.persisted_validation_data(at, para_id, OccupiedCoreAssumption::TimedOut)
.await
.map(|s| s.map(|s| s.parent_head.0))
}
+91
View File
@@ -0,0 +1,91 @@
[package]
name = "pezcumulus-client-service"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Common functions used to assemble the components of a teyrchain node."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-channel = { workspace = true }
futures = { workspace = true }
prometheus = { workspace = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-sync = { workspace = true, default-features = true }
pezsc-network-transactions = { workspace = true, default-features = true }
pezsc-rpc = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsc-sysinfo = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsc-transaction-pool = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-transaction-pool = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-cli = { workspace = true, default-features = true }
pezcumulus-client-collator = { workspace = true, default-features = true }
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-client-network = { workspace = true, default-features = true }
pezcumulus-client-pov-recovery = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-primitives-proof-size-hostfunction = { workspace = true, default-features = true }
pezcumulus-relay-chain-inprocess-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-minimal-node = { workspace = true, default-features = true }
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-cli/runtime-benchmarks",
"pezcumulus-client-collator/runtime-benchmarks",
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-client-network/runtime-benchmarks",
"pezcumulus-client-pov-recovery/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-primitives-proof-size-hostfunction/runtime-benchmarks",
"pezcumulus-relay-chain-inprocess-interface/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-minimal-node/runtime-benchmarks",
"pezcumulus-relay-chain-streams/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network-sync/runtime-benchmarks",
"pezsc-network-transactions/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-rpc/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsc-sysinfo/runtime-benchmarks",
"pezsc-tracing/runtime-benchmarks",
"pezsc-transaction-pool/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-transaction-pool/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
]
+631
View File
@@ -0,0 +1,631 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <https://www.gnu.org/licenses/>.
//! Pezcumulus service
//!
//! Provides functions for starting a collator node or a normal full node.
use cumulus_client_cli::CollatorOptions;
use cumulus_client_network::{AssumeSybilResistance, RequireSecondedInBlockAnnounce};
use cumulus_client_pov_recovery::{PoVRecovery, RecoveryDelayRange, RecoveryHandle};
use cumulus_primitives_core::{CollectCollationInfo, ParaId};
pub use cumulus_primitives_proof_size_hostfunction::storage_proof_size;
use cumulus_relay_chain_inprocess_interface::build_inprocess_relay_chain;
use cumulus_relay_chain_interface::{RelayChainInterface, RelayChainResult};
use cumulus_relay_chain_minimal_node::build_minimal_relay_chain_node_with_rpc;
use futures::{channel::mpsc, StreamExt};
use pezkuwi_primitives::{CandidateEvent, CollatorPair, OccupiedCoreAssumption};
use prometheus::{Histogram, HistogramOpts, Registry};
use pezsc_client_api::{
Backend as BackendT, BlockBackend, BlockchainEvents, Finalizer, ProofProvider, UsageProvider,
};
use pezsc_consensus::{
import_queue::{ImportQueue, ImportQueueService},
BlockImport,
};
use pezsc_network::{
config::SyncMode, request_responses::IncomingRequest, service::traits::NetworkService,
NetworkBackend,
};
use pezsc_network_sync::SyncingService;
use pezsc_network_transactions::TransactionsHandlerController;
use pezsc_service::{Configuration, SpawnTaskHandle, TaskManager, WarpSyncConfig};
use pezsc_telemetry::{log, TelemetryWorkerHandle};
use pezsc_tracing::block::TracingExecuteBlock;
use pezsc_utils::mpsc::TracingUnboundedSender;
use pezsp_api::{ApiExt, Core, ProofRecorder, ProvideRuntimeApi};
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
use pezsp_core::Decode;
use pezsp_runtime::{
traits::{Block as BlockT, BlockIdTo, Header},
SaturatedConversion, Saturating,
};
use pezsp_trie::proof_size_extension::ProofSizeExt;
use std::{
sync::Arc,
time::{Duration, Instant},
};
/// Host functions that should be used in teyrchain nodes.
///
/// Contains the standard bizinikiwi host functions, as well as a
/// host function to enable PoV-reclaim on teyrchain nodes.
pub type TeyrchainHostFunctions = (
cumulus_primitives_proof_size_hostfunction::storage_proof_size::HostFunctions,
pezsp_io::BizinikiwiHostFunctions,
);
// Given the sporadic nature of the explicit recovery operation and the
// possibility to retry infinite times this value is more than enough.
// In practice here we expect no more than one queued messages.
const RECOVERY_CHAN_SIZE: usize = 8;
const LOG_TARGET_SYNC: &str = "sync::pezcumulus";
/// A hint about how long the node should wait before attempting to recover missing block data
/// from the data availability layer.
pub enum DARecoveryProfile {
/// Collators use an aggressive recovery profile by default.
Collator,
/// Full nodes use a passive recovery profile by default, as they are not direct
/// victims of withholding attacks.
FullNode,
/// Provide an explicit recovery profile.
Other(RecoveryDelayRange),
}
/// Parameters given to [`start_relay_chain_tasks`].
pub struct StartRelayChainTasksParams<'a, Block: BlockT, Client, RCInterface> {
pub client: Arc<Client>,
pub announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
pub para_id: ParaId,
pub relay_chain_interface: RCInterface,
pub task_manager: &'a mut TaskManager,
pub da_recovery_profile: DARecoveryProfile,
pub import_queue: Box<dyn ImportQueueService<Block>>,
pub relay_chain_slot_duration: Duration,
pub recovery_handle: Box<dyn RecoveryHandle>,
pub sync_service: Arc<SyncingService<Block>>,
pub prometheus_registry: Option<&'a Registry>,
}
/// Start necessary consensus tasks related to the relay chain.
///
/// Teyrchain nodes need to track the state of the relay chain and use the
/// relay chain's data availability service to fetch blocks if they don't
/// arrive via the normal p2p layer (i.e. when authors withhold their blocks deliberately).
///
/// This function spawns work for those side tasks.
///
/// It also spawns a teyrchain informant task that will log the relay chain state and some metrics.
pub fn start_relay_chain_tasks<Block, Client, Backend, RCInterface>(
StartRelayChainTasksParams {
client,
announce_block,
para_id,
task_manager,
da_recovery_profile,
relay_chain_interface,
import_queue,
relay_chain_slot_duration,
recovery_handle,
sync_service,
prometheus_registry,
}: StartRelayChainTasksParams<Block, Client, RCInterface>,
) -> pezsc_service::error::Result<()>
where
Block: BlockT,
Client: Finalizer<Block, Backend>
+ UsageProvider<Block>
+ HeaderBackend<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>
+ 'static,
for<'a> &'a Client: BlockImport<Block>,
Backend: BackendT<Block> + 'static,
RCInterface: RelayChainInterface + Clone + 'static,
{
let (recovery_chan_tx, recovery_chan_rx) = mpsc::channel(RECOVERY_CHAN_SIZE);
cumulus_client_consensus_common::spawn_teyrchain_consensus_tasks(
para_id,
client.clone(),
relay_chain_interface.clone(),
announce_block.clone(),
Some(recovery_chan_tx),
task_manager.spawn_essential_handle(),
);
let da_recovery_profile = match da_recovery_profile {
DARecoveryProfile::Collator => {
// We want that collators wait at maximum the relay chain slot duration before starting
// to recover blocks. Additionally, we wait at least half the slot time to give the
// relay chain the chance to increase availability.
RecoveryDelayRange {
min: relay_chain_slot_duration / 2,
max: relay_chain_slot_duration,
}
},
DARecoveryProfile::FullNode => {
// Full nodes should at least wait 2.5 minutes (assuming 6 seconds slot duration) and
// in maximum 5 minutes before starting to recover blocks. Collators should already
// start the recovery way before full nodes try to recover a certain block and then
// share the block with the network using "the normal way". Full nodes are just the
// "last resort" for block recovery.
RecoveryDelayRange {
min: relay_chain_slot_duration * 25,
max: relay_chain_slot_duration * 50,
}
},
DARecoveryProfile::Other(profile) => profile,
};
let pov_recovery = PoVRecovery::new(
recovery_handle,
da_recovery_profile,
client.clone(),
import_queue,
relay_chain_interface.clone(),
para_id,
recovery_chan_rx,
sync_service.clone(),
);
task_manager
.spawn_essential_handle()
.spawn("pezcumulus-pov-recovery", None, pov_recovery.run());
let teyrchain_informant = teyrchain_informant::<Block, _>(
para_id,
relay_chain_interface.clone(),
client.clone(),
prometheus_registry.map(TeyrchainInformantMetrics::new).transpose()?,
);
task_manager
.spawn_handle()
.spawn("teyrchain-informant", None, teyrchain_informant);
Ok(())
}
/// Prepare the teyrchain's node configuration
///
/// This function will:
/// * Disable the default announcement of Bizinikiwi for the teyrchain in favor of the one of
/// Pezcumulus.
/// * Set peers needed to start warp sync to 1.
pub fn prepare_node_config(mut teyrchain_config: Configuration) -> Configuration {
teyrchain_config.announce_block = false;
// Teyrchains only need 1 peer to start warp sync, because the target block is fetched from the
// relay chain.
teyrchain_config.network.min_peers_to_start_warp_sync = Some(1);
teyrchain_config
}
/// Build a relay chain interface.
/// Will return a minimal relay chain node with RPC
/// client or an inprocess node, based on the [`CollatorOptions`] passed in.
pub async fn build_relay_chain_interface(
relay_chain_config: Configuration,
teyrchain_config: &Configuration,
telemetry_worker_handle: Option<TelemetryWorkerHandle>,
task_manager: &mut TaskManager,
collator_options: CollatorOptions,
hwbench: Option<pezsc_sysinfo::HwBench>,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<IncomingRequest>,
)> {
match collator_options.relay_chain_mode {
cumulus_client_cli::RelayChainMode::Embedded => build_inprocess_relay_chain(
relay_chain_config,
teyrchain_config,
telemetry_worker_handle,
task_manager,
hwbench,
),
cumulus_client_cli::RelayChainMode::ExternalRpc(rpc_target_urls) =>
build_minimal_relay_chain_node_with_rpc(
relay_chain_config,
teyrchain_config.prometheus_registry(),
task_manager,
rpc_target_urls,
)
.await,
}
}
/// The expected level of collator sybil-resistance on the network. This is used to
/// configure the type of metadata passed alongside block announcements on the network.
pub enum CollatorSybilResistance {
/// There is a collator-selection protocol which provides sybil-resistance,
/// such as Aura. Sybil-resistant collator-selection protocols are able to
/// operate more efficiently.
Resistant,
/// There is no collator-selection protocol providing sybil-resistance.
/// In situations such as "free-for-all" collators, the network is unresistant
/// and needs to attach more metadata to block announcements, relying on relay-chain
/// validators to avoid handling unbounded numbers of blocks.
Unresistant,
}
/// Parameters given to [`build_network`].
pub struct BuildNetworkParams<
'a,
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ HeaderBackend<Block>
+ BlockIdTo<Block>
+ 'static,
Network: NetworkBackend<Block, <Block as BlockT>::Hash>,
RCInterface,
IQ,
> where
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
pub teyrchain_config: &'a Configuration,
pub net_config:
pezsc_network::config::FullNetworkConfiguration<Block, <Block as BlockT>::Hash, Network>,
pub client: Arc<Client>,
pub transaction_pool: Arc<pezsc_transaction_pool::TransactionPoolHandle<Block, Client>>,
pub para_id: ParaId,
pub relay_chain_interface: RCInterface,
pub spawn_handle: SpawnTaskHandle,
pub import_queue: IQ,
pub sybil_resistance_level: CollatorSybilResistance,
pub metrics: pezsc_network::NotificationMetrics,
}
/// Build the network service, the network status sinks and an RPC sender.
pub async fn build_network<'a, Block, Client, RCInterface, IQ, Network>(
BuildNetworkParams {
teyrchain_config,
net_config,
client,
transaction_pool,
para_id,
spawn_handle,
relay_chain_interface,
import_queue,
sybil_resistance_level,
metrics,
}: BuildNetworkParams<'a, Block, Client, Network, RCInterface, IQ>,
) -> pezsc_service::error::Result<(
Arc<dyn NetworkService>,
TracingUnboundedSender<pezsc_rpc::system::Request<Block>>,
TransactionsHandlerController<Block::Hash>,
Arc<SyncingService<Block>>,
)>
where
Block: BlockT,
Client: UsageProvider<Block>
+ HeaderBackend<Block>
+ pezsp_consensus::block_validation::Chain<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>
+ ProvideRuntimeApi<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ BlockIdTo<Block, Error = pezsp_blockchain::Error>
+ ProofProvider<Block>
+ 'static,
Client::Api: CollectCollationInfo<Block>
+ pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
for<'b> &'b Client: BlockImport<Block>,
RCInterface: RelayChainInterface + Clone + 'static,
IQ: ImportQueue<Block> + 'static,
Network: NetworkBackend<Block, <Block as BlockT>::Hash>,
{
let warp_sync_config = match teyrchain_config.network.sync_mode {
SyncMode::Warp => {
log::debug!(target: LOG_TARGET_SYNC, "waiting for announce block...");
let target_block =
wait_for_finalized_para_head::<Block, _>(para_id, relay_chain_interface.clone())
.await
.inspect_err(|e| {
log::error!(
target: LOG_TARGET_SYNC,
"Unable to determine teyrchain target block {:?}",
e
);
})?;
Some(WarpSyncConfig::WithTarget(target_block))
},
_ => None,
};
let block_announce_validator = match sybil_resistance_level {
CollatorSybilResistance::Resistant => {
let block_announce_validator = AssumeSybilResistance::allow_seconded_messages();
Box::new(block_announce_validator) as Box<_>
},
CollatorSybilResistance::Unresistant => {
let block_announce_validator =
RequireSecondedInBlockAnnounce::new(relay_chain_interface, para_id);
Box::new(block_announce_validator) as Box<_>
},
};
pezsc_service::build_network(pezsc_service::BuildNetworkParams {
config: teyrchain_config,
net_config,
client,
transaction_pool,
spawn_handle,
import_queue,
block_announce_validator_builder: Some(Box::new(move |_| block_announce_validator)),
warp_sync_config,
block_relay: None,
metrics,
})
}
/// Waits for the relay chain to have finished syncing and then gets the teyrchain header that
/// corresponds to the last finalized relay chain block.
async fn wait_for_finalized_para_head<B, RCInterface>(
para_id: ParaId,
relay_chain_interface: RCInterface,
) -> pezsc_service::error::Result<<B as BlockT>::Header>
where
B: BlockT + 'static,
RCInterface: RelayChainInterface + Send + 'static,
{
let mut imported_blocks = relay_chain_interface
.import_notification_stream()
.await
.map_err(|error| {
pezsc_service::Error::Other(format!(
"Relay chain import notification stream error when waiting for teyrchain head: \
{error}"
))
})?
.fuse();
while imported_blocks.next().await.is_some() {
let is_syncing = relay_chain_interface
.is_major_syncing()
.await
.map_err(|e| format!("Unable to determine sync status: {e}"))?;
if !is_syncing {
let relay_chain_best_hash = relay_chain_interface
.finalized_block_hash()
.await
.map_err(|e| Box::new(e) as Box<_>)?;
let validation_data = relay_chain_interface
.persisted_validation_data(
relay_chain_best_hash,
para_id,
OccupiedCoreAssumption::TimedOut,
)
.await
.map_err(|e| format!("{e:?}"))?
.ok_or("Could not find teyrchain head in relay chain")?;
let finalized_header = B::Header::decode(&mut &validation_data.parent_head.0[..])
.map_err(|e| format!("Failed to decode teyrchain head: {e}"))?;
log::info!(
"🎉 Received target teyrchain header #{} ({}) from the relay chain.",
finalized_header.number(),
finalized_header.hash()
);
return Ok(finalized_header);
}
}
Err("Stopping following imported blocks. Could not determine teyrchain target block".into())
}
/// Task for logging candidate events and some related metrics.
async fn teyrchain_informant<Block: BlockT, Client>(
para_id: ParaId,
relay_chain_interface: impl RelayChainInterface + Clone,
client: Arc<Client>,
metrics: Option<TeyrchainInformantMetrics>,
) where
Client: HeaderBackend<Block> + Send + Sync + 'static,
{
let mut import_notifications = match relay_chain_interface.import_notification_stream().await {
Ok(import_notifications) => import_notifications,
Err(e) => {
log::error!("Failed to get import notification stream: {e:?}. Teyrchain informant will not run!");
return;
},
};
let mut last_backed_block_time: Option<Instant> = None;
while let Some(n) = import_notifications.next().await {
let candidate_events = match relay_chain_interface.candidate_events(n.hash()).await {
Ok(candidate_events) => candidate_events,
Err(e) => {
log::warn!("Failed to get candidate events for block {}: {e:?}", n.hash());
continue;
},
};
let mut backed_candidates = Vec::new();
let mut included_candidates = Vec::new();
let mut timed_out_candidates = Vec::new();
for event in candidate_events {
match event {
CandidateEvent::CandidateBacked(receipt, head, _, _) => {
if receipt.descriptor.para_id() != para_id {
continue;
}
let backed_block = match Block::Header::decode(&mut &head.0[..]) {
Ok(header) => header,
Err(e) => {
log::warn!(
"Failed to decode teyrchain header from backed block: {e:?}"
);
continue;
},
};
let backed_block_time = Instant::now();
if let Some(last_backed_block_time) = &last_backed_block_time {
let duration = backed_block_time.duration_since(*last_backed_block_time);
if let Some(metrics) = &metrics {
metrics.teyrchain_block_backed_duration.observe(duration.as_secs_f64());
}
}
last_backed_block_time = Some(backed_block_time);
backed_candidates.push(backed_block);
},
CandidateEvent::CandidateIncluded(receipt, head, _, _) => {
if receipt.descriptor.para_id() != para_id {
continue;
}
let included_block = match Block::Header::decode(&mut &head.0[..]) {
Ok(header) => header,
Err(e) => {
log::warn!(
"Failed to decode teyrchain header from included block: {e:?}"
);
continue;
},
};
let unincluded_segment_size =
client.info().best_number.saturating_sub(*included_block.number());
let unincluded_segment_size: u32 = unincluded_segment_size.saturated_into();
if let Some(metrics) = &metrics {
metrics.unincluded_segment_size.observe(unincluded_segment_size.into());
}
included_candidates.push(included_block);
},
CandidateEvent::CandidateTimedOut(receipt, head, _) => {
if receipt.descriptor.para_id() != para_id {
continue;
}
let timed_out_block = match Block::Header::decode(&mut &head.0[..]) {
Ok(header) => header,
Err(e) => {
log::warn!(
"Failed to decode teyrchain header from timed out block: {e:?}"
);
continue;
},
};
timed_out_candidates.push(timed_out_block);
},
}
}
let mut log_parts = Vec::new();
if !backed_candidates.is_empty() {
let backed_candidates = backed_candidates
.into_iter()
.map(|c| format!("#{} ({})", c.number(), c.hash()))
.collect::<Vec<_>>()
.join(", ");
log_parts.push(format!("backed: {}", backed_candidates));
};
if !included_candidates.is_empty() {
let included_candidates = included_candidates
.into_iter()
.map(|c| format!("#{} ({})", c.number(), c.hash()))
.collect::<Vec<_>>()
.join(", ");
log_parts.push(format!("included: {}", included_candidates));
};
if !timed_out_candidates.is_empty() {
let timed_out_candidates = timed_out_candidates
.into_iter()
.map(|c| format!("#{} ({})", c.number(), c.hash()))
.collect::<Vec<_>>()
.join(", ");
log_parts.push(format!("timed out: {}", timed_out_candidates));
};
if !log_parts.is_empty() {
log::info!(
"Update at relay chain block #{} ({}) - {}",
n.number(),
n.hash(),
log_parts.join(", ")
);
}
}
}
struct TeyrchainInformantMetrics {
/// Time between teyrchain blocks getting backed by the relaychain.
teyrchain_block_backed_duration: Histogram,
/// Number of blocks between best block and last included block.
unincluded_segment_size: Histogram,
}
impl TeyrchainInformantMetrics {
fn new(prometheus_registry: &Registry) -> prometheus::Result<Self> {
let teyrchain_block_authorship_duration = Histogram::with_opts(HistogramOpts::new(
"teyrchain_block_backed_duration",
"Time between teyrchain blocks getting backed by the relaychain",
))?;
prometheus_registry.register(Box::new(teyrchain_block_authorship_duration.clone()))?;
let unincluded_segment_size = Histogram::with_opts(
HistogramOpts::new(
"teyrchain_unincluded_segment_size",
"Number of blocks between best block and last included block",
)
.buckets((0..=24).into_iter().map(|i| i as f64).collect()),
)?;
prometheus_registry.register(Box::new(unincluded_segment_size.clone()))?;
Ok(Self {
teyrchain_block_backed_duration: teyrchain_block_authorship_duration,
unincluded_segment_size,
})
}
}
/// Implementation of [`TracingExecuteBlock`] for teyrchains.
///
/// Ensures that all the required extensions required by teyrchain runtimes are registered and
/// available.
pub struct TeyrchainTracingExecuteBlock<Client> {
client: Arc<Client>,
}
impl<Client> TeyrchainTracingExecuteBlock<Client> {
/// Creates a new instance of `self`.
pub fn new(client: Arc<Client>) -> Self {
Self { client }
}
}
impl<Block, Client> TracingExecuteBlock<Block> for TeyrchainTracingExecuteBlock<Client>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync,
Client::Api: Core<Block>,
{
fn execute_block(&self, _: Block::Hash, block: Block) -> pezsp_blockchain::Result<()> {
let mut runtime_api = self.client.runtime_api();
let storage_proof_recorder = ProofRecorder::<Block>::default();
runtime_api.register_extension(ProofSizeExt::new(storage_proof_recorder.clone()));
runtime_api.record_proof_with_recorder(storage_proof_recorder);
runtime_api
.execute_block(*block.header().parent_hash(), block.into())
.map_err(Into::into)
}
}
@@ -0,0 +1,43 @@
[package]
name = "pezcumulus-client-teyrchain-inherent"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
description = "Inherent that needs to be present in every teyrchain block. Contains messages and a relay chain storage-proof."
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus-babe = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsp-crypto-hashing = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-storage = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-primitives-teyrchain-inherent = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-primitives-teyrchain-inherent/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus-babe/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
@@ -0,0 +1,235 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Client side code for generating the teyrchain inherent.
mod mock;
use codec::Decode;
use cumulus_primitives_core::{
relay_chain::{
self, ApprovedPeerId, Block as RelayBlock, Hash as PHash, Header as RelayHeader,
HrmpChannelId,
},
ParaId, PersistedValidationData,
};
pub use cumulus_primitives_teyrchain_inherent::{TeyrchainInherentData, INHERENT_IDENTIFIER};
use cumulus_relay_chain_interface::RelayChainInterface;
pub use mock::{MockValidationDataInherentDataProvider, MockXcmConfig};
use pezsc_network_types::PeerId;
const LOG_TARGET: &str = "teyrchain-inherent";
/// Collect the relevant relay chain state in form of a proof for putting it into the validation
/// data inherent.
async fn collect_relay_storage_proof(
relay_chain_interface: &impl RelayChainInterface,
para_id: ParaId,
relay_parent: PHash,
include_authorities: bool,
include_next_authorities: bool,
additional_relay_state_keys: Vec<Vec<u8>>,
) -> Option<pezsp_state_machine::StorageProof> {
use relay_chain::well_known_keys as relay_well_known_keys;
let ingress_channels = relay_chain_interface
.get_storage_by_key(
relay_parent,
&relay_well_known_keys::hrmp_ingress_channel_index(para_id),
)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"Cannot obtain the hrmp ingress channel."
)
})
.ok()?;
let ingress_channels = ingress_channels
.map(|raw| <Vec<ParaId>>::decode(&mut &raw[..]))
.transpose()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Cannot decode the hrmp ingress channel index.",
)
})
.ok()?
.unwrap_or_default();
let egress_channels = relay_chain_interface
.get_storage_by_key(
relay_parent,
&relay_well_known_keys::hrmp_egress_channel_index(para_id),
)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Cannot obtain the hrmp egress channel.",
)
})
.ok()?;
let egress_channels = egress_channels
.map(|raw| <Vec<ParaId>>::decode(&mut &raw[..]))
.transpose()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Cannot decode the hrmp egress channel index.",
)
})
.ok()?
.unwrap_or_default();
let mut relevant_keys = vec![
relay_well_known_keys::CURRENT_BLOCK_RANDOMNESS.to_vec(),
relay_well_known_keys::ONE_EPOCH_AGO_RANDOMNESS.to_vec(),
relay_well_known_keys::TWO_EPOCHS_AGO_RANDOMNESS.to_vec(),
relay_well_known_keys::CURRENT_SLOT.to_vec(),
relay_well_known_keys::ACTIVE_CONFIG.to_vec(),
relay_well_known_keys::dmq_mqc_head(para_id),
// TODO paritytech/pezkuwi#6283: Remove all usages of `relay_dispatch_queue_size`
// We need to keep this here until all teyrchains have migrated to
// `relay_dispatch_queue_remaining_capacity`.
#[allow(deprecated)]
relay_well_known_keys::relay_dispatch_queue_size(para_id),
relay_well_known_keys::relay_dispatch_queue_remaining_capacity(para_id).key,
relay_well_known_keys::hrmp_ingress_channel_index(para_id),
relay_well_known_keys::hrmp_egress_channel_index(para_id),
relay_well_known_keys::upgrade_go_ahead_signal(para_id),
relay_well_known_keys::upgrade_restriction_signal(para_id),
relay_well_known_keys::para_head(para_id),
];
relevant_keys.extend(ingress_channels.into_iter().map(|sender| {
relay_well_known_keys::hrmp_channels(HrmpChannelId { sender, recipient: para_id })
}));
relevant_keys.extend(egress_channels.into_iter().map(|recipient| {
relay_well_known_keys::hrmp_channels(HrmpChannelId { sender: para_id, recipient })
}));
if include_authorities {
relevant_keys.push(relay_well_known_keys::AUTHORITIES.to_vec());
}
if include_next_authorities {
relevant_keys.push(relay_well_known_keys::NEXT_AUTHORITIES.to_vec());
}
// Add additional relay state keys
let unique_keys: Vec<Vec<u8>> = additional_relay_state_keys
.into_iter()
.filter(|key| !relevant_keys.contains(key))
.collect();
relevant_keys.extend(unique_keys);
relay_chain_interface
.prove_read(relay_parent, &relevant_keys)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"Cannot obtain read proof from relay chain.",
);
})
.ok()
}
pub struct TeyrchainInherentDataProvider;
impl TeyrchainInherentDataProvider {
/// Create the [`TeyrchainInherentData`] at the given `relay_parent`.
///
/// Returns `None` if the creation failed.
pub async fn create_at(
relay_parent: PHash,
relay_chain_interface: &impl RelayChainInterface,
validation_data: &PersistedValidationData,
para_id: ParaId,
relay_parent_descendants: Vec<RelayHeader>,
additional_relay_state_keys: Vec<Vec<u8>>,
collator_peer_id: PeerId,
) -> Option<TeyrchainInherentData> {
let collator_peer_id = ApprovedPeerId::try_from(collator_peer_id.to_bytes())
.inspect_err(|_e| {
tracing::warn!(
target: LOG_TARGET,
"Could not convert collator_peer_id into ApprovedPeerId. The collator_peer_id \
should contain a sequence of at most 64 bytes",
);
})
.ok();
// Only include next epoch authorities when the descendants include an epoch digest.
// Skip the first entry because this is the relay parent itself.
let include_next_authorities = relay_parent_descendants
.iter()
.skip(1)
.any(pezsc_consensus_babe::contains_epoch_change::<RelayBlock>);
let relay_chain_state = collect_relay_storage_proof(
relay_chain_interface,
para_id,
relay_parent,
!relay_parent_descendants.is_empty(),
include_next_authorities,
additional_relay_state_keys,
)
.await?;
let downward_messages = relay_chain_interface
.retrieve_dmq_contents(para_id, relay_parent)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"An error occurred during requesting the downward messages.",
);
})
.ok()?;
let horizontal_messages = relay_chain_interface
.retrieve_all_inbound_hrmp_channel_contents(para_id, relay_parent)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"An error occurred during requesting the inbound HRMP messages.",
);
})
.ok()?;
Some(TeyrchainInherentData {
downward_messages,
horizontal_messages,
validation_data: validation_data.clone(),
relay_chain_state,
relay_parent_descendants,
collator_peer_id,
})
}
}
@@ -0,0 +1,258 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::TeyrchainInherentData;
use codec::Decode;
use cumulus_primitives_core::{
relay_chain,
relay_chain::{Slot, UpgradeGoAhead},
InboundDownwardMessage, InboundHrmpMessage, ParaId, PersistedValidationData,
};
use cumulus_primitives_teyrchain_inherent::MessageQueueChain;
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use pezsc_client_api::{Backend, StorageProvider};
use pezsp_crypto_hashing::twox_128;
use pezsp_inherents::{InherentData, InherentDataProvider};
use pezsp_runtime::traits::Block;
use std::collections::BTreeMap;
/// Inherent data provider that supplies mocked validation data.
///
/// This is useful when running a node that is not actually backed by any relay chain.
/// For example when running a local node, or running integration tests.
///
/// We mock a relay chain block number as follows:
/// relay_block_number = offset + relay_blocks_per_para_block * current_para_block
/// To simulate a teyrchain that starts in relay block 1000 and gets a block in every other relay
/// block, use 1000 and 2
///
/// Optionally, mock XCM messages can be injected into the runtime. When mocking XCM,
/// in addition to the messages themselves, you must provide some information about
/// your teyrchain's configuration in order to mock the MQC heads properly.
/// See [`MockXcmConfig`] for more information
#[derive(Default)]
pub struct MockValidationDataInherentDataProvider<R = ()> {
/// The current block number of the local block chain (the teyrchain).
pub current_para_block: u32,
/// The teyrchain ID of the teyrchain for that the inherent data is created.
pub para_id: ParaId,
/// The current block head data of the local block chain (the teyrchain).
pub current_para_block_head: Option<cumulus_primitives_core::relay_chain::HeadData>,
/// The relay block in which this teyrchain appeared to start. This will be the relay block
/// number in para block #P1.
pub relay_offset: u32,
/// The number of relay blocks that elapses between each parablock. Probably set this to 1 or 2
/// to simulate optimistic or realistic relay chain behavior.
pub relay_blocks_per_para_block: u32,
/// Number of teyrchain blocks per relay chain epoch
/// Mock epoch is computed by dividing `current_para_block` by this value.
pub para_blocks_per_relay_epoch: u32,
/// Function to mock BABE one epoch ago randomness.
pub relay_randomness_config: R,
/// XCM messages and associated configuration information.
pub xcm_config: MockXcmConfig,
/// Inbound downward XCM messages to be injected into the block.
pub raw_downward_messages: Vec<Vec<u8>>,
/// Inbound Horizontal messages sorted by channel.
pub raw_horizontal_messages: Vec<(ParaId, Vec<u8>)>,
/// Additional key-value pairs that should be injected.
pub additional_key_values: Option<Vec<(Vec<u8>, Vec<u8>)>>,
/// Whether upgrade go ahead should be set.
pub upgrade_go_ahead: Option<UpgradeGoAhead>,
}
/// Something that can generate randomness.
pub trait GenerateRandomness<I> {
/// Generate the randomness using the given `input`.
fn generate_randomness(&self, input: I) -> relay_chain::Hash;
}
impl GenerateRandomness<u64> for () {
/// Default implementation uses relay epoch as randomness value
/// A more seemingly random implementation may hash the relay epoch instead
fn generate_randomness(&self, input: u64) -> relay_chain::Hash {
let mut mock_randomness: [u8; 32] = [0u8; 32];
mock_randomness[..8].copy_from_slice(&input.to_be_bytes());
mock_randomness.into()
}
}
/// Parameters for how the Mock inherent data provider should inject XCM messages.
/// In addition to the messages themselves, some information about the teyrchain's
/// configuration is also required so that the MQC heads can be read out of the
/// teyrchain's storage, and the corresponding relay data mocked.
#[derive(Default)]
pub struct MockXcmConfig {
/// The starting state of the dmq_mqc_head.
pub starting_dmq_mqc_head: relay_chain::Hash,
/// The starting state of each teyrchain's mqc head
pub starting_hrmp_mqc_heads: BTreeMap<ParaId, relay_chain::Hash>,
}
/// The name of the teyrchain system in the runtime.
///
/// This name is used by frame to prefix storage items and will be required to read data from the
/// storage.
///
/// The `Default` implementation sets the name to `TeyrchainSystem`.
pub struct TeyrchainSystemName(pub Vec<u8>);
impl Default for TeyrchainSystemName {
fn default() -> Self {
Self(b"TeyrchainSystem".to_vec())
}
}
impl MockXcmConfig {
/// Create a MockXcmConfig by reading the mqc_heads directly
/// from the storage of a previous block.
pub fn new<B: Block, BE: Backend<B>, C: StorageProvider<B, BE>>(
client: &C,
parent_block: B::Hash,
teyrchain_system_name: TeyrchainSystemName,
) -> Self {
let starting_dmq_mqc_head = client
.storage(
parent_block,
&pezsp_storage::StorageKey(
[twox_128(&teyrchain_system_name.0), twox_128(b"LastDmqMqcHead")]
.concat()
.to_vec(),
),
)
.expect("We should be able to read storage from the parent block.")
.map(|ref mut raw_data| {
Decode::decode(&mut &raw_data.0[..]).expect("Stored data should decode correctly")
})
.unwrap_or_default();
let starting_hrmp_mqc_heads = client
.storage(
parent_block,
&pezsp_storage::StorageKey(
[twox_128(&teyrchain_system_name.0), twox_128(b"LastHrmpMqcHeads")]
.concat()
.to_vec(),
),
)
.expect("We should be able to read storage from the parent block.")
.map(|ref mut raw_data| {
Decode::decode(&mut &raw_data.0[..]).expect("Stored data should decode correctly")
})
.unwrap_or_default();
Self { starting_dmq_mqc_head, starting_hrmp_mqc_heads }
}
}
#[async_trait::async_trait]
impl<R: Send + Sync + GenerateRandomness<u64>> InherentDataProvider
for MockValidationDataInherentDataProvider<R>
{
async fn provide_inherent_data(
&self,
inherent_data: &mut InherentData,
) -> Result<(), pezsp_inherents::Error> {
// Use the "sproof" (spoof proof) builder to build valid mock state root and proof.
let mut sproof_builder =
RelayStateSproofBuilder { para_id: self.para_id, ..Default::default() };
// Calculate the mocked relay block based on the current para block
let relay_parent_number =
self.relay_offset + self.relay_blocks_per_para_block * self.current_para_block;
sproof_builder.current_slot = Slot::from(relay_parent_number as u64);
sproof_builder.upgrade_go_ahead = self.upgrade_go_ahead;
// Process the downward messages and set up the correct head
let mut downward_messages = Vec::new();
let mut dmq_mqc = MessageQueueChain::new(self.xcm_config.starting_dmq_mqc_head);
for msg in &self.raw_downward_messages {
let wrapped = InboundDownwardMessage { sent_at: relay_parent_number, msg: msg.clone() };
dmq_mqc.extend_downward(&wrapped);
downward_messages.push(wrapped);
}
sproof_builder.dmq_mqc_head = Some(dmq_mqc.head());
// Process the hrmp messages and set up the correct heads
// Begin by collecting them into a Map
let mut horizontal_messages = BTreeMap::<ParaId, Vec<InboundHrmpMessage>>::new();
for (para_id, msg) in &self.raw_horizontal_messages {
let wrapped = InboundHrmpMessage { sent_at: relay_parent_number, data: msg.clone() };
horizontal_messages.entry(*para_id).or_default().push(wrapped);
}
// Now iterate again, updating the heads as we go
for (para_id, messages) in &horizontal_messages {
let mut channel_mqc = MessageQueueChain::new(
*self
.xcm_config
.starting_hrmp_mqc_heads
.get(para_id)
.unwrap_or(&relay_chain::Hash::default()),
);
for message in messages {
channel_mqc.extend_hrmp(message);
}
sproof_builder.upsert_inbound_channel(*para_id).mqc_head = Some(channel_mqc.head());
}
// Epoch is set equal to current para block / blocks per epoch
sproof_builder.current_epoch = if self.para_blocks_per_relay_epoch == 0 {
// do not divide by 0 => set epoch to para block number
self.current_para_block.into()
} else {
(self.current_para_block / self.para_blocks_per_relay_epoch).into()
};
// Randomness is set by randomness generator
sproof_builder.randomness =
self.relay_randomness_config.generate_randomness(self.current_para_block.into());
if let Some(key_values) = &self.additional_key_values {
sproof_builder.additional_key_values = key_values.clone()
}
// Inject current para block head, if any
sproof_builder.included_para_head = self.current_para_block_head.clone();
let (relay_parent_storage_root, proof) = sproof_builder.into_state_root_and_proof();
let teyrchain_inherent_data = TeyrchainInherentData {
validation_data: PersistedValidationData {
parent_head: Default::default(),
relay_parent_storage_root,
relay_parent_number,
max_pov_size: Default::default(),
},
downward_messages,
horizontal_messages,
relay_chain_state: proof,
relay_parent_descendants: Default::default(),
collator_peer_id: None,
};
teyrchain_inherent_data.provide_inherent_data(inherent_data).await
}
// Copied from the real implementation
async fn try_handle_error(
&self,
_: &pezsp_inherents::InherentIdentifier,
_: &[u8],
) -> Option<Result<(), pezsp_inherents::Error>> {
None
}
}
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
cd "$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)"
dockerfile="$1"
if [ -z "$dockerfile" ]; then
dockerfile="./docker/test-teyrchain-collator.Dockerfile"
else
shift 1
fi
image_name="$(basename "$dockerfile" | rev | cut -d. -f2- | rev)"
echo "building $dockerfile as $image_name..."
time docker build \
-f "$dockerfile" \
-t "$image_name":latest \
"$@" \
.
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -e
cumulus_repo=$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)
pezkuwi_repo=$(dirname "$cumulus_repo")/pezkuwi
if [ ! -d "$pezkuwi_repo/.git" ]; then
echo "please clone pezkuwi in parallel to this repo:"
echo " (cd .. && git clone git@github.com:paritytech/pezkuwi.git)"
exit 1
fi
if [ -z "$BRANCH" ]; then
BRANCH=pezcumulus-branch
fi
cd "$pezkuwi_repo"
git fetch
git checkout "$BRANCH"
time docker build \
-f ./docker/Dockerfile \
--build-arg PROFILE=release \
-t pezkuwi:"$BRANCH" .
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# helper function to run docker-compose using the docker/docker-compose.yml file while
# retaining a context from the root of the repository
set -e
dc () {
cd "$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)"
docker-compose -f - "$@" < docker/docker-compose.yml
}
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
head () {
pezkuwi-js-api --ws ws://172.28.1.1:9944 query.teyrchains.heads 100 |\
jq -r .heads
}
start=$(head)
sleep 60
end=$(head)
[ "$start" != "$end" ]
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# this script runs the pezkuwi-teyrchain after fetching
# appropriate bootnode IDs
#
# this is _not_ a general-purpose script; it is closely tied to the
# root docker-compose.yml
set -e -o pipefail
ctpc="/usr/bin/pezkuwi-teyrchain"
if [ ! -x "$ctpc" ]; then
echo "FATAL: $ctpc does not exist or is not executable"
exit 1
fi
# name the variable with the incoming args so it isn't overwritten later by function calls
args=( "$@" )
alice="172.28.1.1"
bob="172.28.1.2"
p2p_port="30333"
rpc_port="9933"
get_id () {
node="$1"
/wait-for-it.sh "$node:$rpc_port" -t 10 -s -- \
curl -sS \
-H 'Content-Type: application/json' \
--data '{"id":1,"jsonrpc":"2.0","method":"system_networkState"}' \
"$node:$rpc_port" |\
jq -r '.result.peerId'
}
bootnode () {
node="$1"
id=$(get_id "$node")
if [ -z "$id" ]; then
echo >&2 "failed to get id for $node"
exit 1
fi
echo "/ip4/$node/tcp/$p2p_port/p2p/$id"
}
args+=( "--" "--bootnodes=$(bootnode "$alice")" "--bootnodes=$(bootnode "$bob")" )
set -x
"$ctpc" "${args[@]}"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -e -o pipefail
sizeof () {
stat --printf="%s" "$1"
}
wait_for_file () {
# Wait for a file to have a stable, non-zero size.
# Takes at least 0.2 seconds per run, but there's no upper bound if the
# file grows continuously. If the file doesn't exist, or stably has 0 size,
# this will take up to 10 seconds by default; this limit can be adjusted by
# the second input parameter.
path="$1"
limit="$2"
if [ -z "$limit" ]; then
limit=10
fi
count=0
while [ "$count" -lt "$limit" ]; do
if [ -s "$path" ]; then
echo "$path found after $count seconds"
# now ensure that the file size is stable: it's not still being written
oldsize=0
size="$(sizeof "$path")"
while [ "$oldsize" -ne "$size" ]; do
sleep 0.2
oldsize="$size"
size="$(sizeof "$path")"
done
return
fi
count=$((count+1))
sleep 1
done
echo "$path not found after $count seconds"
exit 1
}
wait_for_file /runtime/cumulus_test_teyrchain_runtime.compact.wasm
wait_for_file /genesis/genesis-state
# this is now straightforward: just send the sudo'd tx to the alice node,
# as soon as the node is ready to receive connections
/wait-for-it.sh 172.28.1.1:9944 \
--strict \
--timeout=10 \
-- \
pezkuwi-js-api \
--ws ws://172.28.1.1:9944 \
--sudo \
--seed "//Alice" \
tx.registrar.registerPara \
100 \
'{"scheduling":"Always"}' \
@/runtime/cumulus_test_teyrchain_runtime.compact.wasm \
"$(cat /genesis/genesis-state)"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
cd "$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)"
# shellcheck source=dc.sh
source docker/scripts/dc.sh
dc build
dc up -d
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
cd "$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)"
# shellcheck source=dc.sh
source docker/scripts/dc.sh
dc down --volumes --remove-orphans
+101
View File
@@ -0,0 +1,101 @@
# Pezcumulus Overview
This document provides high-level documentation for Pezcumulus.
## Runtime
Each Bizinikiwi blockchain provides a runtime. The runtime is the state transition function of the
blockchain. Pezcumulus provides interfaces and extensions to convert a Bizinikiwi FRAME runtime into a
Teyrchain runtime. Pezkuwi expects each runtime exposes an interface for validating a
Teyrchain's state transition and also provides interfaces for the Teyrchain to send and receive
messages of other Teyrchains.
To convert a Bizinikiwi runtime into a Teyrchain runtime, the following code needs to be added to the
runtime:
```rust
cumulus_pallet_teyrchain_system::register_validate_block!(Block, Executive);
```
This macro call expects the `Block` and `Executive` type. It generates the `validate_block` function
that is expected by Pezkuwi to validate the state transition.
When compiling a runtime that uses Pezcumulus, a WASM binary is generated that contains the *full* code
of the Teyrchain runtime plus the `validate_block` functionality. This binary is required to
register a Teyrchain on the relay chain.
When the Teyrchain validator calls the `validate_block` function, it passes the PoVBlock (See [Block
building](#block-building) for more information) and the parent header of the Teyrchain that is
stored on the relay chain. From the PoVBlock witness data, Pezcumulus reconstructs the partial trie.
This partial trie is used as storage while executing the block. Pezcumulus also redirects all storage
related host functions to use the witness data storage. After the setup is done, Pezcumulus calls
`execute_block` with the transactions and the header stored in the PoVBlock. On success, the new
Teyrchain header is returned as part of the `validate_block` result.
## Node
Teyrchains support light-clients, full nodes, and authority nodes. Authority nodes are called
Collators in the Pezkuwi ecosystem. Pezcumulus provides the consensus implementation for a
Teyrchain and the block production logic.
The Teyrchain consensus will follow the relay chain to get notified about which Teyrchain blocks are
included in the relay-chain and which are finalized. Each block that is built by a Collator is sent
to a validator that is assigned to the particular Teyrchain. Pezcumulus provides the block production
logic that notifies each Collator of the Teyrchain to build a Teyrchain block. The
notification is triggered on a relay-chain block import by the Collator. This means that every
Collator of the Teyrchain can send a block to the Teyrchain validators. For more sophisticated
authoring logic, the Teyrchain will be able to use Aura, BABE, etc. (Not supported at the moment)
A Teyrchain Collator will join the Teyrchain network and the relay-chain network. The Teyrchain
network will be used to gossip Teyrchain blocks and to gossip transactions. Collators will only
gossip blocks to the Teyrchain network that have a high chance of being included in the relay
chain. To prove that a block is probably going to be included, the Collator will send along side
the notification the so-called candidate message. This candidate message is issued by a Teyrchain
validator after approving a block. This proof of possible inclusion prevents spamming other collators
of the network with useless blocks.
The Collator joins the relay-chain network for two reasons. First, the Collator uses it to send the
Teyrchain blocks to the Teyrchain validators. Secondly, the Collator participates as a full-node
of the relay chain to be informed of new relay-chain blocks. This information will be used for the
consensus and the block production logic.
## Block Building
Pezkuwi requires that a Teyrchain block is transmitted in a fixed format. These blocks sent by a
Teyrchain to the Teyrchain validators are called proof-of-validity blocks (PoVBlock). Such a
PoVBlock contains the header and the transactions of the Teyrchain as opaque blobs (`Vec<u8>`). They
are opaque, because Pezkuwi can not and should not support all kinds of possible Teyrchain block
formats. Besides the header and the transactions, it also contains the witness data and the outgoing
messages.
A Teyrchain validator needs to validate a given PoVBlock, but without requiring the full state of
the Teyrchain. To still make it possible to validate the Teyrchain block, the PoVBlock contains the
witness data. The witness data is a proof that is collected while building the block. The proof will
contain all trie nodes that are read during the block production. Pezcumulus uses the witness data to
reconstruct a partial trie and uses this as storage when executing the block.
The outgoing messages are also collected at block production. These are messages from the Teyrchain
the block is built for to other Teyrchains or to the relay chain itself.
## Runtime Upgrade
Every Bizinikiwi blockchain supports runtime upgrades. Runtime upgrades enable a blockchain to update
its state transition function without requiring any client update. Such a runtime upgrade is applied
by a special transaction in a Bizinikiwi runtime. Pezkuwi and Pezcumulus provide support for these
runtime upgrades, but updating a Teyrchain runtime is not as easy as updating a standalone
blockchain runtime. In a standalone blockchain, the special transaction needs to be included in a
block and the runtime is updated.
A Teyrchain will follow the same paradigm, but the relay chain needs to be informed before
the update. Pezcumulus will provide functionality to notify the relay chain about the runtime update. The
update will not be enacted directly; instead it takes `X` relay blocks (a value that is configured
by the relay chain) before the relay chain allows the update to be applied. The first Teyrchain
block that will be included after `X` relay chain blocks needs to apply the upgrade.
If the update is applied before the waiting period is finished, the relay chain will reject the
Teyrchain block for inclusion. The Pezcumulus runtime pallet will provide the functionality to
register the runtime upgrade and will also make sure that the update is applied at the correct block.
After updating the Teyrchain runtime, a Teyrchain needs to wait a certain amount of time `Y`
(configured by the relay chain) before another update can be applied.
The WASM blob update not only contains the Teyrchain runtime, but also the `validate_block`
function provided by Pezcumulus. So, updating a Teyrchain runtime on the relay chain involves a
complete update of the validation WASM blob.
+14
View File
@@ -0,0 +1,14 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+64
View File
@@ -0,0 +1,64 @@
[package]
name = "pezpallet-ah-ops"
description = "Operations cleanup pallet for the post-migration Asset Hub"
license = "Apache-2.0"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
repository.workspace = true
[package.metadata.pezkuwi-sdk]
exclude-from-umbrella = true # for AHM only
[dependencies]
codec = { workspace = true, features = ["max-encoded-len"] }
pezframe-benchmarking = { workspace = true, optional = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
log = { workspace = true }
pezpallet-balances = { workspace = true }
pezpallet-timestamp = { workspace = true }
pezpallet-utility = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
pezsp-application-crypto = { workspace = true }
pezsp-core = { workspace = true }
pezsp-io = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-std = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezframe-benchmarking?/std",
"pezframe-support/std",
"pezframe-system/std",
"log/std",
"pezpallet-balances/std",
"pezpallet-timestamp/std",
"pezpallet-utility/std",
"scale-info/std",
"pezsp-application-crypto/std",
"pezsp-core/std",
"pezsp-io/std",
"pezsp-runtime/std",
"pezsp-std/std",
]
runtime-benchmarks = [
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezpallet-timestamp/runtime-benchmarks",
"pezpallet-utility/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-balances/try-runtime",
"pezpallet-timestamp/try-runtime",
"pezpallet-utility/try-runtime",
"pezsp-runtime/try-runtime",
]
@@ -0,0 +1,100 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::*;
use pezframe_benchmarking::{account, v2::*};
use pezframe_support::{dispatch::RawOrigin, traits::Currency};
#[benchmarks]
pub mod benchmarks {
use super::*;
#[benchmark]
fn unreserve_lease_deposit() {
let sender = account("sender", 0, 0);
let ed = <T::Currency as Currency<_>>::minimum_balance();
let _ = T::Currency::deposit_creating(&sender, ed + ed);
let _ = T::Currency::reserve(&sender, ed);
let block = T::RcBlockNumberProvider::current_block_number();
let para_id = ParaId::from(1u16);
RcLeaseReserve::<T>::insert((block, para_id, &sender), ed);
assert_eq!(T::Currency::reserved_balance(&sender), ed);
#[extrinsic_call]
_(RawOrigin::Signed(sender.clone()), block, None, para_id);
assert_eq!(T::Currency::reserved_balance(&sender), 0);
assert_eq!(RcLeaseReserve::<T>::get((block, para_id, &sender)), None);
}
#[benchmark]
fn withdraw_crowdloan_contribution() {
let pot = account("pot", 0, 0);
let ed = <T::Currency as Currency<_>>::minimum_balance();
let _ = T::Currency::deposit_creating(&pot, ed + ed);
let _ = T::Currency::reserve(&pot, ed);
let block = T::RcBlockNumberProvider::current_block_number();
let para_id = ParaId::from(1u16);
RcLeaseReserve::<T>::insert((block, para_id, &pot), ed);
let sender = account("sender", 0, 0);
RcCrowdloanContribution::<T>::insert((block, para_id, &sender), (pot.clone(), ed));
assert_eq!(T::Currency::free_balance(&sender), 0);
#[extrinsic_call]
_(RawOrigin::Signed(sender.clone()), block, None, para_id);
assert_eq!(RcCrowdloanContribution::<T>::get((block, para_id, &sender)), None);
assert_eq!(RcLeaseReserve::<T>::get((block, para_id, &pot)), None);
assert_eq!(T::Currency::free_balance(&pot), ed);
}
#[benchmark]
fn unreserve_crowdloan_reserve() {
let sender = account("sender", 0, 0);
let ed = <T::Currency as Currency<_>>::minimum_balance();
let _ = T::Currency::deposit_creating(&sender, ed + ed);
let _ = T::Currency::reserve(&sender, ed);
let block = T::RcBlockNumberProvider::current_block_number();
let para_id = ParaId::from(1u16);
RcCrowdloanReserve::<T>::insert((block, para_id, &sender), ed);
assert_eq!(T::Currency::reserved_balance(&sender), ed);
#[extrinsic_call]
_(RawOrigin::Signed(sender.clone()), block, None, para_id);
assert_eq!(T::Currency::reserved_balance(&sender), 0);
assert_eq!(RcCrowdloanReserve::<T>::get((block, para_id, &sender)), None);
}
#[cfg(feature = "std")]
pub fn test_unreserve_lease_deposit<T: Config>() {
_unreserve_lease_deposit::<T>(true)
}
#[cfg(feature = "std")]
pub fn test_withdraw_crowdloan_contribution<T: Config>() {
_withdraw_crowdloan_contribution::<T>(true)
}
#[cfg(feature = "std")]
pub fn test_unreserve_crowdloan_reserve<T: Config>() {
_unreserve_crowdloan_reserve::<T>(true)
}
}
+683
View File
@@ -0,0 +1,683 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The operational pallet for the Asset Hub, designed to manage and facilitate the migration of
//! subsystems such as Governance, Staking, Balances from the Relay Chain to the Asset Hub. This
//! pallet works alongside its counterpart, `pezpallet_rc_migrator`, which handles migration
//! processes on the Relay Chain side.
//!
//! This pallet is responsible for controlling the initiation, progression, and completion of the
//! migration process, including managing its various stages and transferring the necessary data.
//! The pallet directly accesses the storage of other pallets for read/write operations while
//! maintaining compatibility with their existing APIs.
//!
//! To simplify development and avoid the need to edit the original pallets, this pallet may
//! duplicate private items such as storage entries from the original pallets. This ensures that the
//! migration logic can be implemented without altering the original implementations.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
use codec::DecodeAll;
use pezframe_support::{
pezpallet_prelude::*,
traits::{
fungible::{Inspect, InspectFreeze, Mutate, MutateFreeze, MutateHold, Unbalanced},
tokens::{Fortitude, IdAmount, Precision, Preservation},
Defensive, LockableCurrency, ReservableCurrency, WithdrawReasons as LockWithdrawReasons,
},
};
use pezframe_system::pezpallet_prelude::*;
use pezpallet_balances::{AccountData, BalanceLock, Reasons as LockReasons};
use pezsp_application_crypto::ByteArray;
use pezsp_runtime::{traits::BlockNumberProvider, AccountId32};
use pezsp_std::prelude::*;
/// The log target of this pallet.
pub const LOG_TARGET: &str = "runtime::ah-ops";
pub type BalanceOf<T> = <T as pezpallet_balances::Config>::Balance;
pub type DerivationIndex = u16;
pub type ParaId = u16;
#[pezframe_support::pallet]
pub mod pallet {
use super::*;
#[pallet::config]
pub trait Config:
pezframe_system::Config<AccountData = AccountData<u128>, AccountId = AccountId32>
+ pezpallet_balances::Config<Balance = u128>
+ pezpallet_timestamp::Config<Moment = u64> // Needed for testing
{
/// Native asset type.
type Currency: Mutate<Self::AccountId, Balance = u128>
+ MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
+ InspectFreeze<Self::AccountId, Id = Self::FreezeIdentifier>
+ MutateFreeze<Self::AccountId>
+ Unbalanced<Self::AccountId>
+ ReservableCurrency<Self::AccountId, Balance = u128>
+ LockableCurrency<Self::AccountId, Balance = u128>;
/// Access the block number of the Relay Chain.
type RcBlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
/// The Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
/// Amount of balance that was reserved for winning a lease auction.
///
/// `unreserve_lease_deposit` can be permissionlessly called once the block number passed to
/// unreserve the deposit. It is implicitly called by `withdraw_crowdloan_contribution`.
///
/// The account here can either be a crowdloan account or a solo bidder. If it is a crowdloan
/// account, then the summed up contributions for it in the contributions map will equate the
/// reserved balance here.
///
/// The keys are as follows:
/// - Block number after which the deposit can be unreserved.
/// - The para_id of the lease slot.
/// - The account that will have the balance unreserved.
/// - The balance to be unreserved.
#[pallet::storage]
pub type RcLeaseReserve<T: Config> = StorageNMap<
_,
(
NMapKey<Twox64Concat, BlockNumberFor<T>>,
NMapKey<Twox64Concat, ParaId>,
NMapKey<Twox64Concat, T::AccountId>,
),
BalanceOf<T>,
OptionQuery,
>;
/// Amount of balance that a contributor made towards a crowdloan.
///
/// `withdraw_crowdloan_contribution` can be permissionlessly called once the block number
/// passed to unlock the balance for a specific account.
///
/// The keys are as follows:
/// - Block number after which the balance can be unlocked.
/// - The para_id of the crowdloan.
/// - The account that made the contribution.
///
/// The value is (fund_pot, balance). The contribution pot is the second key in the
/// `RcCrowdloanContribution` storage.
#[pallet::storage]
pub type RcCrowdloanContribution<T: Config> = StorageNMap<
_,
(
NMapKey<Twox64Concat, BlockNumberFor<T>>,
NMapKey<Twox64Concat, ParaId>,
NMapKey<Twox64Concat, T::AccountId>,
),
(T::AccountId, BalanceOf<T>),
OptionQuery,
>;
/// The reserve that was taken to create a crowdloan.
///
/// This is normally 500 HEZ and can be refunded as last step after all
/// `RcCrowdloanContribution`s of this loan have been withdrawn.
///
/// Keys:
/// - Block number after which this can be unreserved
/// - The para_id of the crowdloan
/// - The account that will have the balance unreserved
#[pallet::storage]
pub type RcCrowdloanReserve<T: Config> = StorageNMap<
_,
(
NMapKey<Twox64Concat, BlockNumberFor<T>>,
NMapKey<Twox64Concat, ParaId>,
NMapKey<Twox64Concat, T::AccountId>,
),
BalanceOf<T>,
OptionQuery,
>;
#[pallet::error]
pub enum Error<T> {
/// Either no lease deposit or already unreserved.
NoLeaseReserve,
/// Either no crowdloan contribution or already withdrawn.
NoCrowdloanContribution,
/// Either no crowdloan reserve or already unreserved.
NoCrowdloanReserve,
/// Failed to withdraw crowdloan contribution.
FailedToWithdrawCrowdloanContribution,
/// Block number is not yet reached.
NotYet,
/// Not all contributions are withdrawn.
ContributionsRemaining,
/// Passed account IDs are not matching unmigrated child and sibling accounts.
WrongSovereignTranslation,
/// The account is not a derived account.
WrongDerivedTranslation,
/// Account cannot be migrated since it is not a sovereign teyrchain account.
NotSovereign,
/// Internal error, please bug report.
InternalError,
/// The migrated account would get reaped in the process.
WouldReap,
/// Failed to put a hold on an account.
FailedToPutHold,
/// Failed to release a hold from an account.
FailedToReleaseHold,
/// Failed to thaw a frozen balance.
FailedToThaw,
/// Failed to set a freeze on an account.
FailedToSetFreeze,
/// Failed to transfer a balance.
FailedToTransfer,
/// Failed to reserve a balance.
FailedToReserve,
/// Failed to unreserve the full balance.
CannotUnreserve,
/// The from and to accounts are identical.
AccountIdentical,
}
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
/// Some lease reserve could not be unreserved and needs manual cleanup.
LeaseUnreserveRemaining {
depositor: T::AccountId,
para_id: ParaId,
remaining: BalanceOf<T>,
},
/// Some amount for a crowdloan reserve could not be unreserved and needs manual cleanup.
CrowdloanUnreserveRemaining {
depositor: T::AccountId,
para_id: ParaId,
remaining: BalanceOf<T>,
},
/// A sovereign teyrchain account has been migrated from its child to sibling
/// representation.
SovereignMigrated {
/// The teyrchain ID that had its account migrated.
para_id: ParaId,
/// The old account that was migrated out of.
from: T::AccountId,
/// The new account that was migrated into.
to: T::AccountId,
/// Set if this account was derived from a para sovereign account.
derivation_index: Option<DerivationIndex>,
},
/// An amount of fungible balance was put on hold.
HoldPlaced { account: T::AccountId, amount: BalanceOf<T>, reason: T::RuntimeHoldReason },
/// An amount of fungible balance was released from its hold.
HoldReleased { account: T::AccountId, amount: BalanceOf<T>, reason: T::RuntimeHoldReason },
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Unreserve the deposit that was taken for creating a crowdloan.
///
/// This can be called by any signed origin. It unreserves the lease deposit on the account
/// that won the lease auction. It can be unreserved once all leases expired. Note that it
/// will be called automatically from `withdraw_crowdloan_contribution` for the matching
/// crowdloan account.
///
/// Solo bidder accounts that won lease auctions can use this to unreserve their amount.
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::unreserve_lease_deposit())]
pub fn unreserve_lease_deposit(
origin: OriginFor<T>,
block: BlockNumberFor<T>,
depositor: Option<T::AccountId>,
para_id: ParaId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let depositor = depositor.unwrap_or(sender);
Self::do_unreserve_lease_deposit(block, depositor, para_id).map_err(Into::into)
}
/// Withdraw the contribution of a finished crowdloan.
///
/// A crowdloan contribution can be withdrawn if either:
/// - The crowdloan failed to in an auction and timed out
/// - Won an auction and all leases expired
///
/// Can be called by any signed origin.
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::withdraw_crowdloan_contribution())]
pub fn withdraw_crowdloan_contribution(
origin: OriginFor<T>,
block: BlockNumberFor<T>,
depositor: Option<T::AccountId>,
para_id: ParaId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let depositor = depositor.unwrap_or(sender);
Self::do_withdraw_crowdloan_contribution(block, depositor, para_id).map_err(Into::into)
}
/// Unreserve the deposit that was taken for creating a crowdloan.
///
/// This can be called once either:
/// - The crowdloan failed to win an auction and timed out
/// - Won an auction, all leases expired and all contributions are withdrawn
///
/// Can be called by any signed origin. The condition that all contributions are withdrawn
/// is in place since the reserve acts as a storage deposit.
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::unreserve_crowdloan_reserve())]
pub fn unreserve_crowdloan_reserve(
origin: OriginFor<T>,
block: BlockNumberFor<T>,
depositor: Option<T::AccountId>,
para_id: ParaId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let depositor = depositor.unwrap_or(sender);
Self::do_unreserve_crowdloan_reserve(block, depositor, para_id).map_err(Into::into)
}
/// Try to migrate a teyrchain sovereign child account to its respective sibling.
///
/// Takes the old and new account and migrates it only if they are as expected. An event of
/// `SovereignMigrated` will be emitted if the account was migrated successfully.
///
/// Callable by any signed origin.
#[pallet::call_index(3)]
#[pallet::weight(T::DbWeight::get().reads_writes(15, 15)
.saturating_add(Weight::from_parts(0, 50_000)))]
pub fn migrate_teyrchain_sovereign_acc(
origin: OriginFor<T>,
from: T::AccountId,
to: T::AccountId,
) -> DispatchResult {
ensure_root(origin)?;
Self::do_migrate_teyrchain_sovereign_derived_acc(&from, &to, None).map_err(Into::into)
}
/// Try to migrate a teyrchain sovereign child account to its respective sibling.
///
/// Takes the old and new account and migrates it only if they are as expected. An event of
/// `SovereignMigrated` will be emitted if the account was migrated successfully.
///
/// Callable by any signed origin.
#[pallet::call_index(5)]
#[pallet::weight(T::DbWeight::get().reads_writes(15, 15)
.saturating_add(Weight::from_parts(0, 50_000)))]
pub fn migrate_teyrchain_sovereign_derived_acc(
origin: OriginFor<T>,
from: T::AccountId,
to: T::AccountId,
derivation: (T::AccountId, DerivationIndex),
) -> DispatchResult {
ensure_root(origin)?;
Self::do_migrate_teyrchain_sovereign_derived_acc(&from, &to, Some(derivation))
.map_err(Into::into)
}
/// Force unreserve a named or unnamed reserve.
#[pallet::call_index(4)]
#[pallet::weight(T::DbWeight::get().reads_writes(10, 10)
.saturating_add(Weight::from_parts(0, 50_000)))]
pub fn force_unreserve(
origin: OriginFor<T>,
account: T::AccountId,
amount: BalanceOf<T>,
reason: Option<T::RuntimeHoldReason>,
) -> DispatchResult {
ensure_root(origin)?;
Self::do_force_unreserve(account, amount, reason).map_err(Into::into)
}
}
impl<T: Config> Pallet<T> {
pub fn do_unreserve_lease_deposit(
block: BlockNumberFor<T>,
depositor: T::AccountId,
para_id: ParaId,
) -> Result<(), Error<T>> {
ensure!(block <= T::RcBlockNumberProvider::current_block_number(), Error::<T>::NotYet);
let balance = RcLeaseReserve::<T>::take((block, para_id, &depositor))
.ok_or(Error::<T>::NoLeaseReserve)?;
let remaining = <T as Config>::Currency::unreserve(&depositor, balance);
if remaining > 0 {
defensive!("Should be able to unreserve all");
Self::deposit_event(Event::LeaseUnreserveRemaining {
depositor,
remaining,
para_id,
});
}
Ok(())
}
pub fn do_withdraw_crowdloan_contribution(
block: BlockNumberFor<T>,
depositor: T::AccountId,
para_id: ParaId,
) -> Result<(), Error<T>> {
ensure!(block <= T::RcBlockNumberProvider::current_block_number(), Error::<T>::NotYet);
let (pot, contribution) =
RcCrowdloanContribution::<T>::take((block, para_id, &depositor))
.ok_or(Error::<T>::NoCrowdloanContribution)?;
// Maybe this is the first one to withdraw and we need to unreserve it from the pot
match Self::do_unreserve_lease_deposit(block, pot.clone(), para_id) {
Ok(()) => (),
Err(Error::<T>::NoLeaseReserve) => (), // fine
Err(e) => return Err(e),
}
// Ideally this does not fail. But if it does, then we keep it for manual inspection.
let transferred = <T as Config>::Currency::transfer(
&pot,
&depositor,
contribution,
Preservation::Preserve,
)
.defensive()
.map_err(|_| Error::<T>::FailedToWithdrawCrowdloanContribution)?;
defensive_assert!(transferred == contribution);
// Need to reactivate since we deactivated it here https://github.com/pezkuwichain/pezkuwi-sdk/blob/04847d515ef56da4d0801c9b89a4241dfa827b33/polkadot/runtime/common/src/crowdloan/mod.rs#L793
<T as Config>::Currency::reactivate(transferred);
Ok(())
}
pub fn do_unreserve_crowdloan_reserve(
block: BlockNumberFor<T>,
depositor: T::AccountId,
para_id: ParaId,
) -> Result<(), Error<T>> {
ensure!(block <= T::RcBlockNumberProvider::current_block_number(), Error::<T>::NotYet);
ensure!(
Self::contributions_withdrawn(block, para_id),
Error::<T>::ContributionsRemaining
);
let amount = RcCrowdloanReserve::<T>::take((block, para_id, &depositor))
.ok_or(Error::<T>::NoCrowdloanReserve)?;
let remaining = <T as Config>::Currency::unreserve(&depositor, amount);
if remaining > 0 {
defensive!("Should be able to unreserve all");
Self::deposit_event(Event::CrowdloanUnreserveRemaining {
depositor,
remaining,
para_id,
});
}
Ok(())
}
pub(crate) fn contributions_withdrawn(block: BlockNumberFor<T>, para_id: ParaId) -> bool {
let mut contrib_iter = RcCrowdloanContribution::<T>::iter_prefix((block, para_id));
contrib_iter.next().is_none()
}
pub fn do_migrate_teyrchain_sovereign_derived_acc(
from: &T::AccountId,
to: &T::AccountId,
derivation: Option<(T::AccountId, DerivationIndex)>,
) -> Result<(), Error<T>> {
if pezframe_system::Account::<T>::get(from) == Default::default() {
// Nothing to do if the account does not exist
return Ok(());
}
if from == to {
return Err(Error::<T>::AccountIdentical);
}
pezpallet_balances::Pallet::<T>::ensure_upgraded(from); // prevent future headache
let (translated_acc, para_id, index) = if let Some((parent, index)) = derivation {
let (parent_translated, para_id) =
Self::try_rc_sovereign_derived_to_ah(from, &parent, index)?;
(parent_translated, para_id, Some(index))
} else {
let (translated_acc, para_id) = Self::try_translate_rc_sovereign_to_ah(from)?;
(translated_acc, para_id, None)
};
ensure!(translated_acc == *to, Error::<T>::WrongSovereignTranslation);
// Release all locks
let locks: Vec<BalanceLock<T::Balance>> =
pezpallet_balances::Locks::<T>::get(from).into_inner();
for lock in &locks {
let () = <T as Config>::Currency::remove_lock(lock.id, from);
}
// Thaw all the freezes
let freezes: Vec<IdAmount<T::FreezeIdentifier, T::Balance>> =
pezpallet_balances::Freezes::<T>::get(from).into();
for freeze in &freezes {
let () = <T as Config>::Currency::thaw(&freeze.id, from)
.map_err(|_| Error::<T>::FailedToThaw)?;
}
// Release all holds
let holds: Vec<IdAmount<T::RuntimeHoldReason, T::Balance>> =
pezpallet_balances::Holds::<T>::get(from).into();
for IdAmount { id, amount } in &holds {
let _ = <T as Config>::Currency::release(id, from, *amount, Precision::Exact)
.map_err(|_| Error::<T>::FailedToReleaseHold)?;
Self::deposit_event(Event::HoldReleased {
account: from.clone(),
amount: *amount,
reason: *id,
});
}
// Unreserve unnamed reserves
let unnamed_reserve = <T as Config>::Currency::reserved_balance(from);
let missing = <T as Config>::Currency::unreserve(from, unnamed_reserve);
defensive_assert!(missing == 0, "Should have unreserved the full amount");
// Set consumer refs to zero
let consumers = pezframe_system::Pallet::<T>::consumers(from);
pezframe_system::Account::<T>::mutate(from, |acc| {
acc.consumers = 0;
});
// We dont handle sufficients and there should be none
ensure!(pezframe_system::Pallet::<T>::sufficients(from) == 0, Error::<T>::InternalError);
// Sanity check
let total = <T as Config>::Currency::total_balance(from);
let reducible = <T as Config>::Currency::reducible_balance(
from,
Preservation::Expendable,
Fortitude::Polite,
);
defensive_assert!(
total >= <T as Config>::Currency::minimum_balance(),
"Must have at least ED"
);
defensive_assert!(total == reducible, "Total balance should be reducible");
// Now the actual balance transfer to the new account
<T as Config>::Currency::transfer(from, to, total, Preservation::Expendable)
.defensive()
.map_err(|_| Error::<T>::FailedToTransfer)?;
// Apply consumer refs
pezframe_system::Account::<T>::mutate(to, |acc| {
acc.consumers += consumers;
});
// Reapply the holds
for hold in &holds {
<T as Config>::Currency::hold(&hold.id, to, hold.amount)
.map_err(|_| Error::<T>::FailedToPutHold)?;
// Somehow there are no events for this being emitted... so we emit our own.
Self::deposit_event(Event::HoldPlaced {
account: to.clone(),
amount: hold.amount,
reason: hold.id,
});
}
// Reapply the reserve
<T as Config>::Currency::reserve(to, unnamed_reserve)
.defensive()
.map_err(|_| Error::<T>::FailedToReserve)?;
// Reapply the locks
for lock in &locks {
let reasons = map_lock_reason(lock.reasons);
<T as Config>::Currency::set_lock(lock.id, to, lock.amount, reasons);
}
// Reapply the freezes
for freeze in &freezes {
<T as Config>::Currency::set_freeze(&freeze.id, to, freeze.amount)
.map_err(|_| Error::<T>::FailedToSetFreeze)?;
}
defensive_assert!(
pezframe_system::Account::<T>::get(from) == Default::default(),
"Must reap old account"
);
// If new account would die from this, then lets rather not do it and check it manually.
ensure!(
pezframe_system::Account::<T>::get(to) != Default::default(),
Error::<T>::WouldReap
);
Self::deposit_event(Event::SovereignMigrated {
para_id,
from: from.clone(),
to: to.clone(),
derivation_index: index,
});
Ok(())
}
pub fn do_force_unreserve(
account: T::AccountId,
amount: BalanceOf<T>,
reason: Option<T::RuntimeHoldReason>,
) -> Result<(), Error<T>> {
if let Some(reason) = reason {
<T as Config>::Currency::release(&reason, &account, amount, Precision::Exact)
.map_err(|_| Error::<T>::FailedToReleaseHold)?;
Self::deposit_event(Event::HoldReleased {
account: account.clone(),
amount,
reason,
});
} else {
let remaining = <T as Config>::Currency::unreserve(&account, amount);
if remaining > 0 {
return Err(Error::<T>::CannotUnreserve);
}
}
Ok(())
}
/// Try to translate a Teyrchain sovereign account to the Teyrchain AH sovereign account.
///
/// Returns:
/// - `Ok(None)` if the account is not a Teyrchain sovereign account
/// - `Ok(Some((ah_account, para_id)))` with the translated account and the para id
/// - `Err(())` otherwise
///
/// The way that this normally works is through the configured
/// `SiblingTeyrchainConvertsVia`: <https://github.com/polkadot-fellows/runtimes/blob/7b096c14c2b16cc81ca4e2188eea9103f120b7a4/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs#L93-L94>
/// it passes the `Sibling` type into it which has type-ID `sibl`:
/// <https://github.com/pezkuwichain/pezkuwi-sdk/blob/c10e25aaa8b8afd8665b53f0a0b02e4ea44caa77/polkadot/parachain/src/primitives.rs#L272-L274>
/// This type-ID gets used by the converter here:
/// <https://github.com/pezkuwichain/pezkuwi-sdk/blob/7ecf3f757a5d6f622309cea7f788e8a547a5dce8/polkadot/xcm/xcm-builder/src/location_conversion.rs#L314>
/// and eventually ends up in the encoding here
/// <https://github.com/pezkuwichain/pezkuwi-sdk/blob/cdf107de700388a52a17b2fb852c98420c78278e/bizinikiwi/primitives/runtime/src/traits/mod.rs#L1997-L1999>
/// The `para` conversion is likewise with `ChildTeyrchainConvertsVia` and the `para`
/// type-ID <https://github.com/pezkuwichain/pezkuwi-sdk/blob/c10e25aaa8b8afd8665b53f0a0b02e4ea44caa77/polkadot/parachain/src/primitives.rs#L162-L164>
pub fn try_translate_rc_sovereign_to_ah(
from: &AccountId32,
) -> Result<(AccountId32, ParaId), Error<T>> {
let raw = from.to_raw_vec();
// Must start with "para"
let Some(raw) = raw.strip_prefix(b"para") else {
return Err(Error::<T>::NotSovereign);
};
// Must end with 26 zero bytes
let Some(raw) = raw.strip_suffix(&[0u8; 26]) else {
return Err(Error::<T>::NotSovereign);
};
let para_id = u16::decode_all(&mut &raw[..]).map_err(|_| Error::<T>::InternalError)?;
// Translate to AH sibling account
let mut ah_raw = [0u8; 32];
ah_raw[0..4].copy_from_slice(b"sibl");
ah_raw[4..6].copy_from_slice(&para_id.encode());
Ok((ah_raw.into(), para_id))
}
/// Same as `try_translate_rc_sovereign_to_ah` but for derived accounts.
pub fn try_rc_sovereign_derived_to_ah(
from: &AccountId32,
parent: &AccountId32,
index: DerivationIndex,
) -> Result<(AccountId32, ParaId), Error<T>> {
// check the derivation proof
{
let derived = pezpallet_utility::derivative_account_id(parent.clone(), index);
ensure!(derived == *from, Error::<T>::WrongDerivedTranslation);
}
let (parent_translated, para_id) = Self::try_translate_rc_sovereign_to_ah(parent)?;
let parent_translated_derived =
pezpallet_utility::derivative_account_id(parent_translated, index);
Ok((parent_translated_derived, para_id))
}
}
}
/// Backward mapping from <https://github.com/paritytech/polkadot-sdk/blob/74a5e1a242274ddaadac1feb3990fc95c8612079/bizinikiwi/pezframe/balances/src/types.rs#L38>
pub fn map_lock_reason(reasons: LockReasons) -> LockWithdrawReasons {
match reasons {
LockReasons::All => LockWithdrawReasons::TRANSACTION_PAYMENT | LockWithdrawReasons::RESERVE,
LockReasons::Fee => LockWithdrawReasons::TRANSACTION_PAYMENT,
LockReasons::Misc => LockWithdrawReasons::TIP,
}
}
+78
View File
@@ -0,0 +1,78 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate as pezpallet_ah_ops;
use crate::*;
use pezframe_support::derive_impl;
use pezsp_core::H256;
use pezsp_runtime::traits::{BlakeTwo256, IdentityLookup};
type Block = pezframe_system::mocking::MockBlock<Runtime>;
// For testing the pallet, we construct a mock runtime.
pezframe_support::construct_runtime!(
pub enum Runtime {
System: pezframe_system,
Balances: pezpallet_balances,
AhOps: pezpallet_ah_ops,
Timestamp: pezpallet_timestamp,
}
);
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for Runtime {
type BaseCallFilter = pezframe_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type Nonce = u64;
type Hash = H256;
type RuntimeCall = RuntimeCall;
type Hashing = BlakeTwo256;
type AccountId = AccountId32;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pezpallet_balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = pezframe_support::traits::ConstU32<16>;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for Runtime {
type Balance = u128;
type AccountStore = System;
}
impl pezpallet_timestamp::Config for Runtime {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ();
type WeightInfo = ();
}
impl Config for Runtime {
type Currency = Balances;
type RcBlockNumberProvider = System; // Wrong but unused
type WeightInfo = ();
}
+299
View File
@@ -0,0 +1,299 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use pezframe_support::{
hypothetically,
traits::{fungible::Mutate, InspectLockableCurrency, LockableCurrency, WithdrawReasons},
};
use pezsp_runtime::AccountId32;
use std::str::FromStr;
use crate::mock::Runtime as AssetHub;
#[test]
fn sovereign_account_translation() {
// https://docs.google.com/document/d/1DXYWPXEwi0DkDfG8Fb2ZTI4DQBAz87DBCIW7yQIVrj0
let bifrost_cases = [
// Bifrost Pezkuwi #1
(
// para 2030
"13YMK2eeopZtUNpeHnJ1Ws2HqMQG6Ts9PGCZYGyFbSYoZfcm",
// sibl 2030
"13cKp89TtYknbyYnqnF6dWN75q5ZosvFSuqzoEVkUAaNR47A",
None,
),
// Bifrost Pezkuwi #2
(
// para 2030 index 0
"14vtfeKAVKh1Jzb3s7e43SqZ3zB5MLsdCxZPoKDxeoCFKLu5",
// sibl 2030 index 0
"5ETehspFKFNpBbe5DsfuziN6BWq5Qwp1J8qcTQQoAxwa7BsS",
// derivation proof (para 2030, index 0)
Some(("13YMK2eeopZtUNpeHnJ1Ws2HqMQG6Ts9PGCZYGyFbSYoZfcm", 0u16)),
),
// Bifrost Pezkuwi #3
(
// para 2030 index 1
"14QkQ7wVVDRrhbC1UqHsFwKFUns1SRud94CXMWGHWB8Jhtro",
// sibl 2030 index 1
"5DNWZkkAxLhqF8tevcbRGyARAVM7abukftmqvoDFUN5dDDDz",
// derivation proof (para 2030, index 1)
Some(("13YMK2eeopZtUNpeHnJ1Ws2HqMQG6Ts9PGCZYGyFbSYoZfcm", 1u16)),
),
// Bifrost Pezkuwi #4
(
// para 2030 index 2
"13hLwqcVHqjiJMbZhR9LtfdhoxmTdssi7Kp8EJaW2yfk3knK",
// sibl 2030 index 2
"5EmiwjDYiackJma1GW3aBbQ74rLfWh756UKDb7Cm83XDkUUZ",
// derivation proof (para 2030, index 2)
Some(("13YMK2eeopZtUNpeHnJ1Ws2HqMQG6Ts9PGCZYGyFbSYoZfcm", 2u16)),
),
// Bifrost Kusama #1
(
// para 2001
"5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E",
// sibl 2001
"5Eg2fntJDju46yds4uKzu2zuQssqw7JZWohhLMj6mZZjg2pK",
None,
),
// Bifrost Kusama #2
(
// para 2001 index 0
"5E78xTBiaN3nAGYtcNnqTJQJqYAkSDGggKqaDfpNsKyPpbcb",
// sibl 2001 index 0
"5CzXNqgBZT5yMpMETdfH55saYNKQoJBXsSfnu4d2s1ejYFir",
// derivation proof (para 2001, index 0)
Some(("5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E", 0u16)),
),
// Bifrost Kusama #3
(
// para 2001 index 1
"5HXi9pzWnTQzk7VKzY6VQn92KfWCcA5NbSm53uKHrYU1VsjP",
// sibl 2001 index 1
"5GcexD4YNqcKTbW1YWDRczQzpxic61byeNeLaHgqQHk8pxQJ",
// derivation proof (para 2001, index 1)
Some(("5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E", 1u16)),
),
// Bifrost Kusama #4
(
// para 2001 index 2
"5CkKS3YMx64TguUYrMERc5Bn6Mn2aKMUkcozUFREQDgHS3Tv",
// sibl 2001 index 2
"5FoYMVucmT552GDMWfYNxcF2XnuuvLbJHt7mU6DfDCpUAS2Y",
// derivation proof (para 2001, index 2)
Some(("5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E", 2u16)),
),
// Bifrost Kusama #5
(
// para 2001 index 3
"5Crxhmiw5CQq3Mnfcu3dR3yJ3YpjbxjqaeDFtNNtqgmcnN4S",
// sibl 2001 index 3
"5FP39fgPYhJw3vcLwSMqMnwBuEVGexUMG6JQLPR9yPVhq6Wy",
// derivation proof (para 2001, index 3)
Some(("5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E", 3u16)),
),
// Bifrost Kusama #5
(
// para 2001 index 3
"5DAZP4gZKZafGv42uoWNTMau4tYuDd2XteJLGL4upermhQpn",
// sibl 2001 index 3
"5ExtLdYnjHLJbngU1QpumjPieCGaCXwwkH1JrFBQ9GATuNGv",
// derivation proof (para 2001, index 4)
Some(("5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E", 4u16)),
),
];
for (from, to, derivation) in bifrost_cases {
let from = AccountId32::from_str(from).unwrap();
let to = AccountId32::from_str(to).unwrap();
println!("Translating {from}/{derivation:?} -> {to}");
if let Some((parent, index)) = derivation {
let parent = AccountId32::from_str(parent).unwrap();
let (got_to, _) =
crate::Pallet::<AssetHub>::try_rc_sovereign_derived_to_ah(&from, &parent, index)
.unwrap();
assert_eq!(got_to, to);
} else {
let (got_to, _) =
crate::Pallet::<AssetHub>::try_translate_rc_sovereign_to_ah(&from).unwrap();
assert_eq!(got_to, to);
}
}
}
#[test]
fn translate_sovereign_acc_good() {
pezsp_io::TestExternalities::new(Default::default()).execute_with(move || {
let balance = 1000000000000000000;
let lock = balance / 20;
const LID: [u8; 8] = *b"lockID00";
// Test for Para 2030 index 0 (Bifrost Pezkuwi derived 0)
let from =
AccountId32::from_str("14vtfeKAVKh1Jzb3s7e43SqZ3zB5MLsdCxZPoKDxeoCFKLu5").unwrap();
let to = AccountId32::from_str("5ETehspFKFNpBbe5DsfuziN6BWq5Qwp1J8qcTQQoAxwa7BsS").unwrap();
let parent =
AccountId32::from_str("13YMK2eeopZtUNpeHnJ1Ws2HqMQG6Ts9PGCZYGyFbSYoZfcm").unwrap();
let derivation_proof = Some((parent, 0u16));
// Works if the account does not exist
hypothetically!({
crate::Pallet::<AssetHub>::do_migrate_teyrchain_sovereign_derived_acc(
&from,
&to,
derivation_proof.clone(),
)
.unwrap();
// Also twice
crate::Pallet::<AssetHub>::do_migrate_teyrchain_sovereign_derived_acc(
&from,
&to,
derivation_proof.clone(),
)
.unwrap();
});
// But also if it exists
<AssetHub as crate::Config>::Currency::mint_into(&from, balance).unwrap();
hypothetically!({
crate::Pallet::<AssetHub>::do_migrate_teyrchain_sovereign_derived_acc(
&from,
&to,
derivation_proof.clone(),
)
.unwrap();
// Also twice
crate::Pallet::<AssetHub>::do_migrate_teyrchain_sovereign_derived_acc(
&from,
&to,
derivation_proof.clone(),
)
.unwrap();
// Balance was moved
assert_eq!(<AssetHub as crate::Config>::Currency::free_balance(&from), 0);
assert_eq!(<AssetHub as crate::Config>::Currency::free_balance(&to), balance);
});
// Can also have locks
<AssetHub as crate::Config>::Currency::set_lock(LID, &from, lock, WithdrawReasons::FEE);
hypothetically!({
crate::Pallet::<AssetHub>::do_migrate_teyrchain_sovereign_derived_acc(
&from,
&to,
derivation_proof.clone(),
)
.unwrap();
// Balance was moved
assert_eq!(<AssetHub as crate::Config>::Currency::free_balance(&from), 0);
assert_eq!(<AssetHub as crate::Config>::Currency::free_balance(&to), balance);
// Lock was moved
assert_eq!(<AssetHub as crate::Config>::Currency::balance_locked(LID, &from), 0);
assert_eq!(<AssetHub as crate::Config>::Currency::balance_locked(LID, &to), lock);
});
});
}
#[test]
fn contributions_withdrawn_works() {
pezsp_io::TestExternalities::new(Default::default()).execute_with(|| {
let block_number: u64 = 100;
let para_id: u16 = 2000;
let contributor1 =
AccountId32::from_str("13YMK2eeopZtUNpeHnJ1Ws2HqMQG6Ts9PGCZYGyFbSYoZfcm").unwrap();
let contributor2 =
AccountId32::from_str("14vtfeKAVKh1Jzb3s7e43SqZ3zB5MLsdCxZPoKDxeoCFKLu5").unwrap();
let fund_pot =
AccountId32::from_str("5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E").unwrap();
// Initially no contributions exist, so should return true
assert!(
crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, para_id),
"Should return true when no contributions exist"
);
// Insert a contribution
crate::RcCrowdloanContribution::<AssetHub>::insert(
(block_number, para_id, &contributor1),
(fund_pot.clone(), 1000u128),
);
// Now should return false since there's a contribution
assert!(
!crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, para_id),
"Should return false when contributions exist"
);
// Insert another contribution
crate::RcCrowdloanContribution::<AssetHub>::insert(
(block_number, para_id, &contributor2),
(fund_pot.clone(), 2000u128),
);
// Still should return false
assert!(
!crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, para_id),
"Should return false when multiple contributions exist"
);
// Remove the first contribution
crate::RcCrowdloanContribution::<AssetHub>::remove((block_number, para_id, &contributor1));
// Still should return false (one contribution remains)
assert!(
!crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, para_id),
"Should return false when one contribution still exists"
);
// Remove the second contribution
crate::RcCrowdloanContribution::<AssetHub>::remove((block_number, para_id, &contributor2));
// Now should return true again
assert!(
crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, para_id),
"Should return true after all contributions are removed"
);
// Test with different para_id - should still be true (no contributions)
let other_para_id: u16 = 2001;
assert!(
crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, other_para_id),
"Should return true for different para_id with no contributions"
);
// Add contribution to original para_id but check different para_id
crate::RcCrowdloanContribution::<AssetHub>::insert(
(block_number, para_id, &contributor1),
(fund_pot, 500u128),
);
// Original para_id should now be false
assert!(
!crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, para_id),
"Should return false for para_id with contribution"
);
// Different para_id should still be true
assert!(
crate::Pallet::<AssetHub>::contributions_withdrawn(block_number, other_para_id),
"Should return true for different para_id"
);
});
}
+175
View File
@@ -0,0 +1,175 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Autogenerated weights for `pezpallet_ah_ops`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
//! DATE: 2025-04-28, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `cob`, CPU: `<UNKNOWN>`
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
// Executed Command:
// frame-omni-bencher
// v1
// benchmark
// pallet
// --runtime=target/production/wbuild/asset-hub-pezkuwi-runtime/asset_hub_pezkuwi_runtime.wasm
// --pallet
// pezpallet-ah-ops
// --extrinsic
// *
// --output=./pallets/ah-ops/src/weights_1.rs
// --template=./../../paritytech/pezkuwi-sdk/bizinikiwi/.maintain/frame-weight-template.hbs
// --no-median-slopes
// --no-min-squares
// --repeat=2
// --steps=50
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
#![allow(dead_code)]
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use core::marker::PhantomData;
/// Weight functions needed for `pezpallet_ah_ops`.
pub trait WeightInfo {
fn unreserve_lease_deposit() -> Weight;
fn withdraw_crowdloan_contribution() -> Weight;
fn unreserve_crowdloan_reserve() -> Weight;
}
/// Weights for `pezpallet_ah_ops` using the Bizinikiwi node and recommended hardware.
pub struct BizinikiwiWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Storage: `TeyrchainSystem::ValidationData` (r:1 w:0)
/// Proof: `TeyrchainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `TeyrchainSystem::LastRelayChainBlockNumber` (r:1 w:0)
/// Proof: `TeyrchainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `AhOps::RcLeaseReserve` (r:1 w:1)
/// Proof: `AhOps::RcLeaseReserve` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn unreserve_lease_deposit() -> Weight {
// Proof Size summary in bytes:
// Measured: `365`
// Estimated: `3593`
// Minimum execution time: 30_000_000 picoseconds.
Weight::from_parts(33_000_000, 3593)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: `TeyrchainSystem::ValidationData` (r:1 w:0)
/// Proof: `TeyrchainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `TeyrchainSystem::LastRelayChainBlockNumber` (r:1 w:0)
/// Proof: `TeyrchainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `AhOps::RcCrowdloanContribution` (r:1 w:1)
/// Proof: `AhOps::RcCrowdloanContribution` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
/// Storage: `AhOps::RcLeaseReserve` (r:1 w:1)
/// Proof: `AhOps::RcLeaseReserve` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:2 w:2)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn withdraw_crowdloan_contribution() -> Weight {
// Proof Size summary in bytes:
// Measured: `538`
// Estimated: `6196`
// Minimum execution time: 64_000_000 picoseconds.
Weight::from_parts(65_000_000, 6196)
.saturating_add(T::DbWeight::get().reads(6_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
/// Storage: `TeyrchainSystem::ValidationData` (r:1 w:0)
/// Proof: `TeyrchainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `TeyrchainSystem::LastRelayChainBlockNumber` (r:1 w:0)
/// Proof: `TeyrchainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `AhOps::RcCrowdloanContribution` (r:1 w:0)
/// Proof: `AhOps::RcCrowdloanContribution` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
/// Storage: `AhOps::RcCrowdloanReserve` (r:1 w:1)
/// Proof: `AhOps::RcCrowdloanReserve` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn unreserve_crowdloan_reserve() -> Weight {
// Proof Size summary in bytes:
// Measured: `365`
// Estimated: `3593`
// Minimum execution time: 32_000_000 picoseconds.
Weight::from_parts(62_000_000, 3593)
.saturating_add(T::DbWeight::get().reads(5_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: `TeyrchainSystem::ValidationData` (r:1 w:0)
/// Proof: `TeyrchainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `TeyrchainSystem::LastRelayChainBlockNumber` (r:1 w:0)
/// Proof: `TeyrchainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `AhOps::RcLeaseReserve` (r:1 w:1)
/// Proof: `AhOps::RcLeaseReserve` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn unreserve_lease_deposit() -> Weight {
// Proof Size summary in bytes:
// Measured: `365`
// Estimated: `3593`
// Minimum execution time: 30_000_000 picoseconds.
Weight::from_parts(33_000_000, 3593)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: `TeyrchainSystem::ValidationData` (r:1 w:0)
/// Proof: `TeyrchainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `TeyrchainSystem::LastRelayChainBlockNumber` (r:1 w:0)
/// Proof: `TeyrchainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `AhOps::RcCrowdloanContribution` (r:1 w:1)
/// Proof: `AhOps::RcCrowdloanContribution` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
/// Storage: `AhOps::RcLeaseReserve` (r:1 w:1)
/// Proof: `AhOps::RcLeaseReserve` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:2 w:2)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn withdraw_crowdloan_contribution() -> Weight {
// Proof Size summary in bytes:
// Measured: `538`
// Estimated: `6196`
// Minimum execution time: 64_000_000 picoseconds.
Weight::from_parts(65_000_000, 6196)
.saturating_add(RocksDbWeight::get().reads(6_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
/// Storage: `TeyrchainSystem::ValidationData` (r:1 w:0)
/// Proof: `TeyrchainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `TeyrchainSystem::LastRelayChainBlockNumber` (r:1 w:0)
/// Proof: `TeyrchainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `AhOps::RcCrowdloanContribution` (r:1 w:0)
/// Proof: `AhOps::RcCrowdloanContribution` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
/// Storage: `AhOps::RcCrowdloanReserve` (r:1 w:1)
/// Proof: `AhOps::RcCrowdloanReserve` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn unreserve_crowdloan_reserve() -> Weight {
// Proof Size summary in bytes:
// Measured: `365`
// Estimated: `3593`
// Minimum execution time: 32_000_000 picoseconds.
Weight::from_parts(62_000_000, 3593)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
}
+85
View File
@@ -0,0 +1,85 @@
[package]
name = "pezcumulus-pezpallet-aura-ext"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "AURA consensus extension pallet for teyrchains"
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
# Bizinikiwi
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezpallet-aura = { workspace = true }
pezpallet-timestamp = { workspace = true }
pezsp-application-crypto = { workspace = true }
pezsp-consensus-aura = { workspace = true }
pezsp-runtime = { workspace = true }
# Pezcumulus
pezcumulus-pezpallet-teyrchain-system = { workspace = true }
[dev-dependencies]
rstest = { workspace = true }
# Pezcumulus
pezcumulus-pezpallet-teyrchain-system = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-primitives-proof-size-hostfunction = { workspace = true, default-features = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
# Bizinikiwi
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-keyring = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezcumulus-pezpallet-teyrchain-system/std",
"pezframe-support/std",
"pezframe-system/std",
"pezpallet-aura/std",
"pezpallet-timestamp/std",
"scale-info/std",
"pezsp-application-crypto/std",
"pezsp-consensus-aura/std",
"pezsp-runtime/std",
]
try-runtime = [
"pezcumulus-pezpallet-teyrchain-system/try-runtime",
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-aura/try-runtime",
"pezpallet-timestamp/try-runtime",
"pezsp-runtime/try-runtime",
]
runtime-benchmarks = [
"pezcumulus-pezpallet-teyrchain-system/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-primitives-proof-size-hostfunction/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-aura/runtime-benchmarks",
"pezpallet-timestamp/runtime-benchmarks",
"pezsp-consensus-aura/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,166 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The definition of a [`FixedVelocityConsensusHook`] for consensus logic to manage
//! block velocity.
use super::{pallet, Aura};
use core::{marker::PhantomData, num::NonZeroU32};
use cumulus_pallet_teyrchain_system::{
self as teyrchain_system,
consensus_hook::{ConsensusHook, UnincludedSegmentCapacity},
relay_state_snapshot::RelayChainStateProof,
};
use pezframe_support::pezpallet_prelude::*;
use pezsp_consensus_aura::{Slot, SlotDuration};
/// A consensus hook that enforces fixed block production velocity and unincluded segment capacity.
///
/// It keeps track of relay chain slot information and teyrchain blocks authored per relay chain
/// slot.
///
/// # Type Parameters
/// - `T` - The runtime configuration trait
/// - `RELAY_CHAIN_SLOT_DURATION_MILLIS` - Duration of relay chain slots in milliseconds
/// - `V` - Maximum number of blocks that can be authored per relay chain parent (velocity)
/// - `C` - Maximum capacity of unincluded segment
///
/// # Example Configuration
/// ```ignore
/// type ConsensusHook = FixedVelocityConsensusHook<Runtime, 6000, 2, 8>;
/// ```
/// This configures:
/// - 6 second relay chain slots
/// - Maximum 2 blocks per slot
/// - Maximum 8 blocks in unincluded segment
pub struct FixedVelocityConsensusHook<
T,
const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32,
const V: u32,
const C: u32,
>(PhantomData<T>);
impl<
T: pallet::Config,
const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32,
const V: u32,
const C: u32,
> ConsensusHook for FixedVelocityConsensusHook<T, RELAY_CHAIN_SLOT_DURATION_MILLIS, V, C>
where
<T as pezpallet_timestamp::Config>::Moment: Into<u64>,
{
/// Consensus hook that performs validations on the provided relay chain state
/// proof:
/// - Ensures blocks are not produced faster than the specified velocity `V`
/// - Verifies teyrchain slot alignment with relay chain slot
///
/// # Panics
/// - When the relay chain slot from the state is smaller than the slot from the proof
/// - When the number of authored blocks exceeds velocity limit
/// - When teyrchain slot is ahead of the calculated slot from relay chain
fn on_state_proof(state_proof: &RelayChainStateProof) -> (Weight, UnincludedSegmentCapacity) {
// Ensure velocity is non-zero.
let velocity = V.max(1);
let relay_chain_slot = state_proof.read_slot().expect("failed to read relay chain slot");
let (relay_chain_slot, authored_in_relay) = match pallet::RelaySlotInfo::<T>::get() {
Some((slot, authored)) if slot == relay_chain_slot => (slot, authored),
Some((slot, _)) if slot < relay_chain_slot => (relay_chain_slot, 0),
Some((slot, _)) => {
panic!("Slot moved backwards: stored_slot={slot:?}, relay_chain_slot={relay_chain_slot:?}")
},
None => (relay_chain_slot, 0),
};
// We need to allow one additional block to be built to fill the unincluded segment.
if authored_in_relay > velocity {
panic!("authored blocks limit is reached for the slot: relay_chain_slot={relay_chain_slot:?}, authored={authored_in_relay:?}, velocity={velocity:?}");
}
pallet::RelaySlotInfo::<T>::put((relay_chain_slot, authored_in_relay + 1));
let para_slot = pezpallet_aura::CurrentSlot::<T>::get();
// Convert relay chain timestamp.
let relay_chain_timestamp =
u64::from(RELAY_CHAIN_SLOT_DURATION_MILLIS).saturating_mul(*relay_chain_slot);
let para_slot_duration = SlotDuration::from_millis(Aura::<T>::slot_duration().into());
let para_slot_from_relay =
Slot::from_timestamp(relay_chain_timestamp.into(), para_slot_duration);
if *para_slot != *para_slot_from_relay {
panic!(
"Teyrchain slot must match relay-derived slot: teyrchain_slot={:?}, derived_from_relay_slot={:?} velocity={:?}, relay_chain_slot={:?}",
para_slot,
para_slot_from_relay,
velocity,
relay_chain_slot
);
}
let weight = T::DbWeight::get().reads(1);
(
weight,
NonZeroU32::new(core::cmp::max(C, 1))
.expect("1 is the minimum value and non-zero; qed")
.into(),
)
}
}
impl<
T: pallet::Config + teyrchain_system::Config,
const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32,
const V: u32,
const C: u32,
> FixedVelocityConsensusHook<T, RELAY_CHAIN_SLOT_DURATION_MILLIS, V, C>
{
/// Whether it is legal to extend the chain, assuming the given block is the most
/// recently included one as-of the relay parent that will be built against, and
/// the given slot.
///
/// This should be consistent with the logic the runtime uses when validating blocks to
/// avoid issues.
///
/// When the unincluded segment is empty, i.e. `included_hash == at`, where at is the block
/// whose state we are querying against, this must always return `true` as long as the slot
/// is more recent than the included block itself.
pub fn can_build_upon(included_hash: T::Hash, new_slot: Slot) -> bool {
let velocity = V.max(1);
let (last_slot, authored_so_far) = match pallet::RelaySlotInfo::<T>::get() {
None => return true,
Some(x) => x,
};
let size_after_included =
teyrchain_system::Pallet::<T>::unincluded_segment_size_after(included_hash);
// can never author when the unincluded segment is full.
if size_after_included >= C {
return false;
}
// Check that we have not authored more than `V + 1` teyrchain blocks in the current relay
// chain slot.
if last_slot == new_slot {
authored_so_far < velocity + 1
} else {
// disallow slot from moving backwards.
last_slot < new_slot
}
}
}
+171
View File
@@ -0,0 +1,171 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Pezcumulus extension pallet for AuRa
//!
//! This pallet extends the Bizinikiwi AuRa pallet to make it compatible with teyrchains. It
//! provides the [`Pallet`], the [`Config`] and the [`GenesisConfig`].
//!
//! It is also required that the teyrchain runtime uses the provided [`BlockExecutor`] to properly
//! check the constructed block on the relay chain.
//!
//! ```
//! # struct Runtime;
//! # struct Executive;
//! cumulus_pallet_teyrchain_system::register_validate_block! {
//! Runtime = Runtime,
//! BlockExecutor = cumulus_pallet_aura_ext::BlockExecutor::<Runtime, Executive>,
//! }
//! ```
#![cfg_attr(not(feature = "std"), no_std)]
use pezframe_support::traits::{ExecuteBlock, FindAuthor};
use pezsp_application_crypto::RuntimeAppPublic;
use pezsp_consensus_aura::{digests::CompatibleDigestItem, Slot};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, LazyBlock};
pub mod consensus_hook;
pub mod migration;
#[cfg(test)]
mod test;
pub use consensus_hook::FixedVelocityConsensusHook;
type Aura<T> = pezpallet_aura::Pallet<T>;
pub use pallet::*;
#[pezframe_support::pallet]
pub mod pallet {
use super::*;
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
/// The configuration trait.
#[pallet::config]
pub trait Config: pezpallet_aura::Config + pezframe_system::Config {}
#[pallet::pallet]
#[pallet::storage_version(migration::STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_finalize(_: BlockNumberFor<T>) {
// Update to the latest AuRa authorities.
Authorities::<T>::put(pezpallet_aura::Authorities::<T>::get());
}
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
// Fetch the authorities once to get them into the storage proof of the PoV.
Authorities::<T>::get();
T::DbWeight::get().reads_writes(1, 0)
}
}
/// Serves as cache for the authorities.
///
/// The authorities in AuRa are overwritten in `on_initialize` when we switch to a new session,
/// but we require the old authorities to verify the seal when validating a PoV. This will
/// always be updated to the latest AuRa authorities in `on_finalize`.
#[pallet::storage]
pub(crate) type Authorities<T: Config> = StorageValue<
_,
BoundedVec<T::AuthorityId, <T as pezpallet_aura::Config>::MaxAuthorities>,
ValueQuery,
>;
/// Current relay chain slot paired with a number of authored blocks.
///
/// This is updated in [`FixedVelocityConsensusHook::on_state_proof`] with the current relay
/// chain slot as provided by the relay chain state proof.
#[pallet::storage]
pub(crate) type RelaySlotInfo<T: Config> = StorageValue<_, (Slot, u32), OptionQuery>;
#[pallet::genesis_config]
#[derive(pezframe_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
#[serde(skip)]
pub _config: core::marker::PhantomData<T>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
let authorities = pezpallet_aura::Authorities::<T>::get();
Authorities::<T>::put(authorities);
}
}
}
/// The block executor used when validating a PoV at the relay chain.
///
/// When executing the block it will verify the block seal to ensure that the correct author created
/// the block.
pub struct BlockExecutor<T, I>(core::marker::PhantomData<(T, I)>);
impl<Block, T, I> ExecuteBlock<Block> for BlockExecutor<T, I>
where
Block: BlockT,
T: Config,
I: ExecuteBlock<Block>,
{
fn verify_and_remove_seal(block: &mut <Block as BlockT>::LazyBlock) {
let header = block.header_mut();
// We need to fetch the authorities before we execute the block, to get the authorities
// before any potential update.
let authorities = Authorities::<T>::get();
let mut seal = None;
header.digest_mut().logs.retain(|s| {
let s =
CompatibleDigestItem::<<T::AuthorityId as RuntimeAppPublic>::Signature>::as_aura_seal(s);
match (s, seal.is_some()) {
(Some(_), true) => panic!("Found multiple AuRa seal digests"),
(None, _) => true,
(Some(s), false) => {
seal = Some(s);
false
},
}
});
let seal = seal.expect("Could not find an AuRa seal digest!");
let author = Aura::<T>::find_author(
header.digest().logs().iter().filter_map(|d| d.as_pre_runtime()),
)
.expect("Could not find AuRa author index!");
let pre_hash = header.hash();
if !authorities
.get(author as usize)
.unwrap_or_else(|| {
panic!("Invalid AuRa author index {} for authorities: {:?}", author, authorities)
})
.verify(&pre_hash, &seal)
{
panic!("Invalid AuRa seal");
}
}
fn execute_verified_block(block: Block::LazyBlock) {
I::execute_verified_block(block);
}
}
@@ -0,0 +1,74 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extern crate alloc;
use crate::{Config, Pallet};
#[cfg(feature = "try-runtime")]
use alloc::vec::Vec;
use pezframe_support::{migrations::VersionedMigration, pezpallet_prelude::StorageVersion};
/// The in-code storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
mod v0 {
use super::*;
use pezframe_support::{pezpallet_prelude::OptionQuery, storage_alias};
use pezsp_consensus_aura::Slot;
/// Current slot paired with a number of authored blocks.
///
/// Updated on each block initialization.
#[storage_alias]
pub(super) type SlotInfo<T: Config> = StorageValue<Pallet<T>, (Slot, u32), OptionQuery>;
}
mod v1 {
use super::*;
use pezframe_support::{pezpallet_prelude::*, traits::UncheckedOnRuntimeUpgrade};
pub struct UncheckedMigrationToV1<T: Config>(PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrationToV1<T> {
fn on_runtime_upgrade() -> Weight {
let mut weight: Weight = Weight::zero();
weight += migrate::<T>();
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::TryRuntimeError> {
Ok(Vec::new())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
ensure!(!v0::SlotInfo::<T>::exists(), "SlotInfo should not exist");
Ok(())
}
}
pub fn migrate<T: Config>() -> Weight {
v0::SlotInfo::<T>::kill();
T::DbWeight::get().writes(1)
}
}
/// Migrate `V0` to `V1`.
pub type MigrateV0ToV1<T> = VersionedMigration<
0,
1,
v1::UncheckedMigrationToV1<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
+523
View File
@@ -0,0 +1,523 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use core::num::NonZeroU32;
use cumulus_pallet_teyrchain_system::{
consensus_hook::ExpectParentIncluded, Ancestor, AnyRelayNumber, ConsensusHook,
RelayChainStateProof, TeyrchainSetCode, UsedBandwidth,
};
use cumulus_primitives_core::ParaId;
use pezframe_support::{
derive_impl,
pezpallet_prelude::ConstU32,
parameter_types,
traits::{ConstBool, EnqueueWithOrigin, ExecuteBlock, Hooks},
BoundedVec,
};
use rstest::rstest;
use pezsp_consensus_aura::{sr25519::AuthorityId, Slot};
use pezsp_core::{Blake2Hasher, Get, H256};
use pezsp_io::TestExternalities;
use pezsp_keyring::Sr25519Keyring::*;
use pezsp_runtime::{generic::Digest, traits::Block as BlockT};
use pezsp_trie::{proof_size_extension::ProofSizeExt, recorder::Recorder};
use pezsp_version::RuntimeVersion;
use std::cell::RefCell;
// Test pallet that reads storage and calls storage_proof_size
#[pezframe_support::pallet]
pub mod test_pallet {
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
#[pallet::config]
pub trait Config: pezframe_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::storage]
pub type TestStorage<T: Config> = StorageValue<_, u64, ValueQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
let proof_size =
cumulus_primitives_proof_size_hostfunction::storage_proof_size::storage_proof_size(
);
// We need to commit the `proof_size` to ensure that the test is failing if we are
// receiving a different proof size later on.
TestStorage::<T>::put(proof_size);
Weight::zero()
}
}
}
type Block = pezframe_system::mocking::MockBlock<Test>;
pezframe_support::construct_runtime!(
pub enum Test {
System: pezframe_system,
TeyrchainSystem: cumulus_pallet_teyrchain_system,
Aura: pezpallet_aura,
AuraExt: crate,
TestPallet: test_pallet,
}
);
parameter_types! {
pub Version: RuntimeVersion = RuntimeVersion {
spec_name: "test".into(),
impl_name: "system-test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: pezsp_version::create_apis_vec!([]),
transaction_version: 1,
system_version: 1,
};
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for Test {
type Block = Block;
type Version = Version;
type OnSetCode = TeyrchainSetCode<Test>;
type RuntimeEvent = ();
}
impl crate::Config for Test {}
impl test_pallet::Config for Test {}
std::thread_local! {
pub static PARA_SLOT_DURATION: RefCell<u64> = RefCell::new(6000);
}
pub struct TestSlotDuration;
impl TestSlotDuration {
pub fn set_slot_duration(slot_duration: u64) {
PARA_SLOT_DURATION.with(|v| *v.borrow_mut() = slot_duration);
}
}
impl Get<u64> for TestSlotDuration {
fn get() -> u64 {
PARA_SLOT_DURATION.with(|v| v.clone().into_inner())
}
}
impl pezpallet_aura::Config for Test {
type AuthorityId = pezsp_consensus_aura::sr25519::AuthorityId;
type MaxAuthorities = ConstU32<100_000>;
type DisabledValidators = ();
type AllowMultipleBlocksPerSlot = ConstBool<true>;
type SlotDuration = TestSlotDuration;
}
impl pezpallet_timestamp::Config for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ();
type WeightInfo = ();
}
impl cumulus_pallet_teyrchain_system::Config for Test {
type WeightInfo = ();
type RuntimeEvent = ();
type OnSystemEvent = ();
type SelfParaId = ();
type OutboundXcmpMessageSource = ();
// Ignore all DMP messages by enqueueing them into `()`:
type DmpQueue = EnqueueWithOrigin<(), pezsp_core::ConstU8<0>>;
type ReservedDmpWeight = ();
type XcmpMessageHandler = ();
type ReservedXcmpWeight = ();
type CheckAssociatedRelayNumber = AnyRelayNumber;
type ConsensusHook = ExpectParentIncluded;
type RelayParentOffset = ConstU32<0>;
}
fn set_ancestors() {
let mut ancestors = Vec::new();
for i in 0..3 {
let mut ancestor = Ancestor::new_unchecked(UsedBandwidth::default(), None);
ancestor.replace_para_head_hash(H256::repeat_byte(i + 1));
ancestors.push(ancestor);
}
cumulus_pallet_teyrchain_system::UnincludedSegment::<Test>::put(ancestors);
}
fn new_test_ext(para_slot: u64) -> pezsp_io::TestExternalities {
let mut ext = TestExternalities::new_empty();
ext.execute_with(|| {
set_ancestors();
// Set initial teyrchain slot
pezpallet_aura::CurrentSlot::<Test>::put(Slot::from(para_slot));
});
ext
}
fn set_relay_slot(slot: u64, authored: u32) {
RelaySlotInfo::<Test>::put((Slot::from(slot), authored))
}
fn relay_chain_state_proof(relay_slot: u64) -> RelayChainStateProof {
let mut builder = cumulus_test_relay_sproof_builder::RelayStateSproofBuilder::default();
builder.current_slot = relay_slot.into();
let (hash, state_proof) = builder.into_state_root_and_proof();
RelayChainStateProof::new(ParaId::from(200), hash, state_proof)
.expect("Should be able to construct state proof.")
}
fn assert_slot_info(expected_slot: u64, expected_authored: u32) {
let (slot, authored) = pallet::RelaySlotInfo::<Test>::get().unwrap();
assert_eq!(slot, Slot::from(expected_slot), "Slot stored in RelaySlotInfo is incorrect.");
assert_eq!(
authored, expected_authored,
"Number of authored blocks stored in RelaySlotInfo is incorrect."
);
}
const DEFAULT_TEST_VELOCITY: u32 = 2;
#[test]
fn test_velocity() {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 1>;
new_test_ext(10).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
let (_, capacity) = Hook::on_state_proof(&state_proof);
assert_eq!(capacity, NonZeroU32::new(1).unwrap().into());
assert_slot_info(10, 1);
let (_, capacity) = Hook::on_state_proof(&state_proof);
assert_eq!(capacity, NonZeroU32::new(1).unwrap().into());
assert_slot_info(10, 2);
});
}
#[test]
fn test_velocity_2() {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 3>;
new_test_ext(10).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
let (_, capacity) = Hook::on_state_proof(&state_proof);
assert_eq!(capacity, NonZeroU32::new(3).unwrap().into());
assert_slot_info(10, 1);
let (_, capacity) = Hook::on_state_proof(&state_proof);
assert_eq!(capacity, NonZeroU32::new(3).unwrap().into());
assert_slot_info(10, 2);
});
}
#[test]
#[should_panic(expected = "authored blocks limit is reached for the slot")]
fn test_exceeding_velocity_limit() {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 1>;
new_test_ext(10).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
for authored in 0..=DEFAULT_TEST_VELOCITY + 1 {
Hook::on_state_proof(&state_proof);
assert_slot_info(10, authored + 1);
}
});
}
#[test]
fn test_para_slot_calculated_from_slot_duration() {
type Hook = FixedVelocityConsensusHook<Test, 3000, DEFAULT_TEST_VELOCITY, 1>;
new_test_ext(5).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
Hook::on_state_proof(&state_proof);
});
}
#[rstest]
#[case::short_para_slot_okay(2000, 30, 10)]
#[case::normal_para_slot_okay(6000, 10, 10)]
// Test boundaries for long teyrchain slots.
#[case::long_para_slot_okay(24000, 1, 7)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(2), derived_from_relay_slot=Slot(1)"
)]
#[case::long_para_slot_mismatch(24000, 2, 7)]
#[case::long_para_slot_okay(24000, 2, 8)]
#[case::long_para_slot_okay(24000, 2, 9)]
#[case::long_para_slot_okay(24000, 2, 10)]
#[case::long_para_slot_okay(24000, 2, 11)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(2), derived_from_relay_slot=Slot(3)"
)]
#[case::long_para_slot_mismatch(24000, 2, 12)]
#[case::long_para_slot_okay(24000, 3, 12)]
#[case::short_para_slot(2000, 30, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(31), derived_from_relay_slot=Slot(30)"
)]
#[case::short_para_slot_mismatch(2000, 31, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(32), derived_from_relay_slot=Slot(30)"
)]
#[case::short_para_slot_mismatch(2000, 32, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(29), derived_from_relay_slot=Slot(30)"
)]
#[case::short_para_slot_mismatch(2000, 29, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(1), derived_from_relay_slot=Slot(30)"
)]
#[case::short_para_slot_mismatch(2000, 1, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(1), derived_from_relay_slot=Slot(10)"
)]
#[case::normal_para_slot_mismatch(6000, 1, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(9), derived_from_relay_slot=Slot(10)"
)]
#[case::normal_para_slot_mismatch(6000, 9, 10)]
#[should_panic(
expected = "must match relay-derived slot: teyrchain_slot=Slot(11), derived_from_relay_slot=Slot(10)"
)]
#[case::normal_para_slot_mismatch(6000, 11, 10)]
fn test_para_slot_too_high(
#[case] para_slot_duration: u64,
#[case] para_slot: u64,
#[case] relay_slot: u64,
) {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 1>;
TestSlotDuration::set_slot_duration(para_slot_duration);
new_test_ext(para_slot).execute_with(|| {
let state_proof = relay_chain_state_proof(relay_slot);
Hook::on_state_proof(&state_proof);
});
}
#[test]
fn test_velocity_at_least_one() {
// Even though this is 0, one block should always be allowed.
const VELOCITY: u32 = 0;
type Hook = FixedVelocityConsensusHook<Test, 6000, VELOCITY, 1>;
new_test_ext(10).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
Hook::on_state_proof(&state_proof);
});
}
#[test]
#[should_panic(
expected = "Teyrchain slot must match relay-derived slot: teyrchain_slot=Slot(8), derived_from_relay_slot=Slot(5) velocity=2"
)]
fn test_para_slot_calculated_from_slot_duration_2() {
// Note: In contrast to tests below, relay chain slot duration is 3000 here.
type Hook = FixedVelocityConsensusHook<Test, 3000, DEFAULT_TEST_VELOCITY, 1>;
new_test_ext(8).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
let (_, _) = Hook::on_state_proof(&state_proof);
});
}
#[test]
fn test_velocity_resets_on_new_relay_slot() {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 1>;
new_test_ext(10).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
for authored in 0..=DEFAULT_TEST_VELOCITY {
Hook::on_state_proof(&state_proof);
assert_slot_info(10, authored + 1);
}
// Change teyrchain slot to match the new relay slot
pezpallet_aura::CurrentSlot::<Test>::put(Slot::from(11));
let state_proof = relay_chain_state_proof(11);
for authored in 0..=DEFAULT_TEST_VELOCITY {
Hook::on_state_proof(&state_proof);
assert_slot_info(11, authored + 1);
}
});
}
#[test]
#[should_panic(expected = "Slot moved backwards: stored_slot=Slot(10), relay_chain_slot=Slot(9)")]
fn test_backward_relay_slot_not_tolerated() {
type Hook = FixedVelocityConsensusHook<Test, 6000, 2, 1>;
new_test_ext(10).execute_with(|| {
let state_proof = relay_chain_state_proof(10);
Hook::on_state_proof(&state_proof);
assert_slot_info(10, 1);
// Change teyrchain slot to match what would be derived from relay slot 9
pezpallet_aura::CurrentSlot::<Test>::put(Slot::from(9));
let state_proof = relay_chain_state_proof(9);
Hook::on_state_proof(&state_proof);
});
}
#[test]
fn test_can_build_upon_true_when_empty() {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 1>;
new_test_ext(1).execute_with(|| {
let hash = H256::repeat_byte(0x1);
assert!(Hook::can_build_upon(hash, Slot::from(1)));
});
}
#[rstest]
#[case::slot_higher_ok(10, 11, DEFAULT_TEST_VELOCITY, true)]
#[case::slot_same_ok(10, 10, DEFAULT_TEST_VELOCITY, true)]
#[case::slot_decrease_illegal(10, 9, DEFAULT_TEST_VELOCITY, false)]
#[case::velocity_small_ok(10, 10, DEFAULT_TEST_VELOCITY - 1 , true)]
#[case::velocity_small_ok(10, 10, DEFAULT_TEST_VELOCITY - 2 , true)]
#[case::velocity_too_high_illegal(10, 10, DEFAULT_TEST_VELOCITY + 1 , false)]
fn test_can_build_upon_slot_can_not_decrease(
#[case] state_relay_slot: u64,
#[case] test_relay_slot: u64,
#[case] authored_in_slot: u32,
#[case] expected_result: bool,
) {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 10>;
new_test_ext(1).execute_with(|| {
let hash = H256::repeat_byte(0x1);
set_relay_slot(state_relay_slot, authored_in_slot);
// Slot moves backwards
assert_eq!(Hook::can_build_upon(hash, Slot::from(test_relay_slot)), expected_result);
});
}
#[test]
fn test_can_build_upon_unincluded_segment_size() {
type Hook = FixedVelocityConsensusHook<Test, 6000, DEFAULT_TEST_VELOCITY, 2>;
new_test_ext(1).execute_with(|| {
let relay_slot = Slot::from(10);
set_relay_slot(10, DEFAULT_TEST_VELOCITY);
// Size after included is two, we can not build
assert!(!Hook::can_build_upon(H256::repeat_byte(0x1), relay_slot));
// Size after included is one, we can build
assert!(Hook::can_build_upon(H256::repeat_byte(0x2), relay_slot));
});
}
/// This test ensures that when we call `BlockExecutor::execute_block` in `validate_block`,
/// it doesn't change the proof size host function return values. Otherwise, it may breaks
/// logic that is fetching the proof size in `on_initialize`.
#[test]
fn block_executor_does_not_influence_proof_size_recordings() {
fn build_block(header: <Block as BlockT>::Header) -> <Block as BlockT>::Header {
// Initialize the block
pezframe_system::Pallet::<Test>::initialize(
&header.number,
&header.parent_hash,
&header.digest(),
);
// We omit `teyrchain-system` as it is not important here.
<pezframe_system::Pallet<Test> as Hooks<_>>::on_initialize(header.number);
<crate::Pallet<Test> as Hooks<_>>::on_initialize(header.number);
<test_pallet::Pallet<Test> as Hooks<_>>::on_initialize(header.number);
<test_pallet::Pallet<Test> as Hooks<_>>::on_finalize(header.number);
<crate::Pallet<Test> as Hooks<_>>::on_finalize(header.number);
<pezframe_system::Pallet<Test> as Hooks<_>>::on_finalize(header.number);
// Finalize the block
pezframe_system::Pallet::<Test>::finalize()
}
// Create a simple executive that calls on_initialize and on_finalize
struct TestExecutive;
impl ExecuteBlock<Block> for TestExecutive {
fn verify_and_remove_seal(_: &mut <Block as BlockT>::LazyBlock) {}
fn execute_verified_block(block: <Block as BlockT>::LazyBlock) {
let header = block.header();
let new_header = build_block(header.clone());
assert_eq!(*header, new_header);
}
}
let mut ext = new_test_ext(10);
ext.execute_with(|| {
// Let's setup some authorities
let authority_id = AuthorityId::from(Alice.public());
let authorities: BoundedVec<AuthorityId, ConstU32<100_000>> =
vec![authority_id.clone()].try_into().unwrap();
pezpallet_aura::Authorities::<Test>::put(authorities.clone());
Authorities::<Test>::put(authorities.clone());
});
ext.commit_all().unwrap();
let recorder = Recorder::<Blake2Hasher>::default();
// Register the ProofSizeExt extension
ext.register_extension(ProofSizeExt::new(recorder.clone()));
let mut header = ext.execute_with_recorder(recorder.clone(), || {
let mut digest = Digest::default();
digest.push(CompatibleDigestItem::<()>::aura_pre_digest(10u64.into()));
build_block(HeaderT::new(
1,
Default::default(),
Default::default(),
Default::default(),
digest,
))
});
let sig = Alice.sign(header.hash().as_ref());
let seal = CompatibleDigestItem::aura_seal(sig);
header.digest_mut().push(seal);
let mut block = Block::new(header, Default::default()).into();
ext.reset_overlay();
ext.execute_with_recorder(recorder, || {
BlockExecutor::<Test, TestExecutive>::verify_and_remove_seal(&mut block);
});
let recorder = Recorder::<Blake2Hasher>::default();
// Register the ProofSizeExt extension again to overwrite the old one.
ext.register_extension(ProofSizeExt::new(recorder.clone()));
ext.reset_overlay();
ext.execute_with_recorder(recorder, || {
BlockExecutor::<Test, TestExecutive>::execute_verified_block(block);
});
}
@@ -0,0 +1,82 @@
[package]
authors.workspace = true
description = "Simple pallet to select collators for a teyrchain."
edition.workspace = true
homepage.workspace = true
license = "Apache-2.0"
name = "pezpallet-collator-selection"
readme = "README.md"
repository.workspace = true
version = "9.0.0"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { features = ["derive"], workspace = true }
log = { workspace = true }
rand = { features = ["std_rng"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezpallet-authorship = { workspace = true }
pezpallet-balances = { workspace = true }
pezpallet-session = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-staking = { workspace = true }
pezframe-benchmarking = { optional = true, workspace = true }
[dev-dependencies]
pezpallet-aura = { workspace = true, default-features = true }
pezpallet-timestamp = { workspace = true, default-features = true }
pezsp-consensus-aura = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
[features]
default = ["std"]
runtime-benchmarks = [
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-aura/runtime-benchmarks",
"pezpallet-authorship/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezpallet-session/runtime-benchmarks",
"pezpallet-timestamp/runtime-benchmarks",
"pezsp-consensus-aura/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-staking/runtime-benchmarks",
]
std = [
"codec/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"log/std",
"pezpallet-authorship/std",
"pezpallet-balances/std",
"pezpallet-session/std",
"rand/std",
"scale-info/std",
"pezsp-runtime/std",
"pezsp-staking/std",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-aura/try-runtime",
"pezpallet-authorship/try-runtime",
"pezpallet-balances/try-runtime",
"pezpallet-session/try-runtime",
"pezpallet-timestamp/try-runtime",
"pezsp-runtime/try-runtime",
]
@@ -0,0 +1 @@
License: Apache-2.0
@@ -0,0 +1,464 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Benchmarking setup for pezpallet-collator-selection
#![cfg(feature = "runtime-benchmarks")]
use super::*;
#[allow(unused)]
use crate::Pallet as CollatorSelection;
use alloc::vec::Vec;
use codec::Decode;
use core::cmp;
use pezframe_benchmarking::{account, v2::*, whitelisted_caller, BenchmarkError};
use pezframe_support::traits::{Currency, EnsureOrigin, Get, ReservableCurrency};
use pezframe_system::{pezpallet_prelude::BlockNumberFor, EventRecord, RawOrigin};
use pezpallet_authorship::EventHandler;
use pezpallet_session::{self as session, SessionManager};
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
const SEED: u32 = 0;
fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
let events = pezframe_system::Pallet::<T>::events();
let system_event: <T as pezframe_system::Config>::RuntimeEvent = generic_event.into();
// compare to the last event record
let EventRecord { event, .. } = &events[events.len() - 1];
assert_eq!(event, &system_event);
}
fn create_funded_user<T: Config>(
string: &'static str,
n: u32,
balance_factor: u32,
) -> T::AccountId {
let user = account(string, n, SEED);
let balance = <T as pallet::Config>::Currency::minimum_balance() * balance_factor.into();
let _ = <T as pallet::Config>::Currency::make_free_balance_be(&user, balance);
user
}
fn keys<T: Config + session::Config>(c: u32) -> <T as session::Config>::Keys {
use rand::{RngCore, SeedableRng};
let keys = {
let mut keys = [0u8; 128];
if c > 0 {
let mut rng = rand::rngs::StdRng::seed_from_u64(c as u64);
rng.fill_bytes(&mut keys);
}
keys
};
Decode::decode(&mut &keys[..]).unwrap()
}
fn validator<T: Config + session::Config>(c: u32) -> (T::AccountId, <T as session::Config>::Keys) {
(create_funded_user::<T>("candidate", c, 1000), keys::<T>(c))
}
fn register_validators<T: Config + session::Config>(count: u32) -> Vec<T::AccountId> {
let validators = (0..count).map(|c| validator::<T>(c)).collect::<Vec<_>>();
for (who, keys) in validators.clone() {
<session::Pallet<T>>::ensure_can_pay_key_deposit(&who).unwrap();
<session::Pallet<T>>::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new()).unwrap();
}
validators.into_iter().map(|(who, _)| who).collect()
}
fn register_candidates<T: Config>(count: u32) {
let candidates = (0..count).map(|c| account("candidate", c, SEED)).collect::<Vec<_>>();
assert!(CandidacyBond::<T>::get() > 0u32.into(), "Bond cannot be zero!");
for who in candidates {
<T as pallet::Config>::Currency::make_free_balance_be(
&who,
CandidacyBond::<T>::get() * 3u32.into(),
);
<CollatorSelection<T>>::register_as_candidate(RawOrigin::Signed(who).into()).unwrap();
}
}
fn min_candidates<T: Config>() -> u32 {
let min_collators = T::MinEligibleCollators::get();
let invulnerable_length = Invulnerables::<T>::get().len();
min_collators.saturating_sub(invulnerable_length.try_into().unwrap())
}
fn min_invulnerables<T: Config>() -> u32 {
let min_collators = T::MinEligibleCollators::get();
let candidates_length = CandidateList::<T>::decode_len()
.unwrap_or_default()
.try_into()
.unwrap_or_default();
min_collators.saturating_sub(candidates_length)
}
#[benchmarks(where T: pezpallet_authorship::Config + session::Config)]
mod benchmarks {
use super::*;
#[benchmark]
fn set_invulnerables(
b: Linear<1, { T::MaxInvulnerables::get() }>,
) -> Result<(), BenchmarkError> {
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let new_invulnerables = register_validators::<T>(b);
let mut sorted_new_invulnerables = new_invulnerables.clone();
sorted_new_invulnerables.sort();
#[extrinsic_call]
_(origin as T::RuntimeOrigin, new_invulnerables.clone());
// assert that it comes out sorted
assert_last_event::<T>(
Event::NewInvulnerables { invulnerables: sorted_new_invulnerables }.into(),
);
Ok(())
}
#[benchmark]
fn add_invulnerable(
b: Linear<1, { T::MaxInvulnerables::get() - 1 }>,
c: Linear<1, { T::MaxCandidates::get() - 1 }>,
) -> Result<(), BenchmarkError> {
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
// need to fill up candidates
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
DesiredCandidates::<T>::put(c);
// get accounts and keys for the `c` candidates
let mut candidates = (0..c).map(|cc| validator::<T>(cc)).collect::<Vec<_>>();
// add one more to the list. should not be in `b` (invulnerables) because it's the account
// we will _add_ to invulnerables. we want it to be in `candidates` because we need the
// weight associated with removing it.
let (new_invulnerable, new_invulnerable_keys) = validator::<T>(b.max(c) + 1);
candidates.push((new_invulnerable.clone(), new_invulnerable_keys));
// set their keys ...
for (who, keys) in candidates.clone() {
<session::Pallet<T>>::ensure_can_pay_key_deposit(&who).unwrap();
<session::Pallet<T>>::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new())
.unwrap();
}
// ... and register them.
for (who, _) in candidates.iter() {
let deposit = CandidacyBond::<T>::get();
<T as pallet::Config>::Currency::make_free_balance_be(who, deposit * 1000_u32.into());
CandidateList::<T>::try_mutate(|list| {
list.try_push(CandidateInfo { who: who.clone(), deposit }).unwrap();
Ok::<(), BenchmarkError>(())
})
.unwrap();
<T as pallet::Config>::Currency::reserve(who, deposit)?;
LastAuthoredBlock::<T>::insert(
who.clone(),
pezframe_system::Pallet::<T>::block_number() + T::KickThreshold::get(),
);
}
// now we need to fill up invulnerables
let mut invulnerables = register_validators::<T>(b);
invulnerables.sort();
let invulnerables: pezframe_support::BoundedVec<_, T::MaxInvulnerables> =
pezframe_support::BoundedVec::try_from(invulnerables).unwrap();
Invulnerables::<T>::put(invulnerables);
#[extrinsic_call]
_(origin as T::RuntimeOrigin, new_invulnerable.clone());
assert_last_event::<T>(Event::InvulnerableAdded { account_id: new_invulnerable }.into());
Ok(())
}
#[benchmark]
fn remove_invulnerable(
b: Linear<{ min_invulnerables::<T>() + 1 }, { T::MaxInvulnerables::get() }>,
) -> Result<(), BenchmarkError> {
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let mut invulnerables = register_validators::<T>(b);
invulnerables.sort();
let invulnerables: pezframe_support::BoundedVec<_, T::MaxInvulnerables> =
pezframe_support::BoundedVec::try_from(invulnerables).unwrap();
Invulnerables::<T>::put(invulnerables);
let to_remove = Invulnerables::<T>::get().first().unwrap().clone();
#[extrinsic_call]
_(origin as T::RuntimeOrigin, to_remove.clone());
assert_last_event::<T>(Event::InvulnerableRemoved { account_id: to_remove }.into());
Ok(())
}
#[benchmark]
fn set_desired_candidates() -> Result<(), BenchmarkError> {
let max: u32 = T::MaxCandidates::get();
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, max);
assert_last_event::<T>(Event::NewDesiredCandidates { desired_candidates: max }.into());
Ok(())
}
#[benchmark]
fn set_candidacy_bond(
c: Linear<0, { T::MaxCandidates::get() }>,
k: Linear<0, { T::MaxCandidates::get() }>,
) -> Result<(), BenchmarkError> {
let initial_bond_amount: BalanceOf<T> =
<T as pallet::Config>::Currency::minimum_balance() * 2u32.into();
CandidacyBond::<T>::put(initial_bond_amount);
register_validators::<T>(c);
register_candidates::<T>(c);
let kicked = cmp::min(k, c);
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let bond_amount = if k > 0 {
CandidateList::<T>::mutate(|candidates| {
for info in candidates.iter_mut().skip(kicked as usize) {
info.deposit = <T as pallet::Config>::Currency::minimum_balance() * 3u32.into();
}
});
<T as pallet::Config>::Currency::minimum_balance() * 3u32.into()
} else {
<T as pallet::Config>::Currency::minimum_balance()
};
#[extrinsic_call]
_(origin as T::RuntimeOrigin, bond_amount);
assert_last_event::<T>(Event::NewCandidacyBond { bond_amount }.into());
Ok(())
}
#[benchmark]
fn update_bond(
c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>,
) -> Result<(), BenchmarkError> {
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
DesiredCandidates::<T>::put(c);
register_validators::<T>(c);
register_candidates::<T>(c);
let caller = CandidateList::<T>::get()[0].who.clone();
v2::whitelist!(caller);
let bond_amount: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() +
<T as pallet::Config>::Currency::minimum_balance();
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), bond_amount);
assert_last_event::<T>(
Event::CandidateBondUpdated { account_id: caller, deposit: bond_amount }.into(),
);
assert!(
CandidateList::<T>::get().iter().last().unwrap().deposit ==
<T as pallet::Config>::Currency::minimum_balance() * 2u32.into()
);
Ok(())
}
// worse case is when we have all the max-candidate slots filled except one, and we fill that
// one.
#[benchmark]
fn register_as_candidate(c: Linear<1, { T::MaxCandidates::get() - 1 }>) {
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
DesiredCandidates::<T>::put(c + 1);
register_validators::<T>(c);
register_candidates::<T>(c);
let caller: T::AccountId = whitelisted_caller();
let bond: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() * 2u32.into();
<T as pallet::Config>::Currency::make_free_balance_be(&caller, bond);
<session::Pallet<T>>::ensure_can_pay_key_deposit(&caller).unwrap();
<session::Pallet<T>>::set_keys(
RawOrigin::Signed(caller.clone()).into(),
keys::<T>(c + 1),
Vec::new(),
)
.unwrap();
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert_last_event::<T>(
Event::CandidateAdded { account_id: caller, deposit: bond / 2u32.into() }.into(),
);
}
#[benchmark]
fn take_candidate_slot(c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>) {
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
DesiredCandidates::<T>::put(1);
register_validators::<T>(c);
register_candidates::<T>(c);
let caller: T::AccountId = whitelisted_caller();
let bond: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() * 10u32.into();
<T as pallet::Config>::Currency::make_free_balance_be(&caller, bond);
<session::Pallet<T>>::ensure_can_pay_key_deposit(&caller).unwrap();
<session::Pallet<T>>::set_keys(
RawOrigin::Signed(caller.clone()).into(),
keys::<T>(c + 1),
Vec::new(),
)
.unwrap();
let target = CandidateList::<T>::get().iter().last().unwrap().who.clone();
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), bond / 2u32.into(), target.clone());
assert_last_event::<T>(
Event::CandidateReplaced { old: target, new: caller, deposit: bond / 2u32.into() }
.into(),
);
}
// worse case is the last candidate leaving.
#[benchmark]
fn leave_intent(c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>) {
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
DesiredCandidates::<T>::put(c);
register_validators::<T>(c);
register_candidates::<T>(c);
let leaving = CandidateList::<T>::get().iter().last().unwrap().who.clone();
v2::whitelist!(leaving);
#[extrinsic_call]
_(RawOrigin::Signed(leaving.clone()));
assert_last_event::<T>(Event::CandidateRemoved { account_id: leaving }.into());
}
// worse case is paying a non-existing candidate account.
#[benchmark]
fn note_author() {
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
<T as pallet::Config>::Currency::make_free_balance_be(
&<CollatorSelection<T>>::account_id(),
<T as pallet::Config>::Currency::minimum_balance() * 4u32.into(),
);
let author = account("author", 0, SEED);
let new_block: BlockNumberFor<T> = 10u32.into();
pezframe_system::Pallet::<T>::set_block_number(new_block);
assert!(<T as pallet::Config>::Currency::free_balance(&author) == 0u32.into());
#[block]
{
<CollatorSelection<T> as EventHandler<_, _>>::note_author(author.clone())
}
assert!(<T as pallet::Config>::Currency::free_balance(&author) > 0u32.into());
assert_eq!(pezframe_system::Pallet::<T>::block_number(), new_block);
}
// worst case for new session.
#[benchmark]
fn new_session(
r: Linear<1, { T::MaxCandidates::get() }>,
c: Linear<1, { T::MaxCandidates::get() }>,
) {
CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
DesiredCandidates::<T>::put(c);
pezframe_system::Pallet::<T>::set_block_number(0u32.into());
register_validators::<T>(c);
register_candidates::<T>(c);
let new_block: BlockNumberFor<T> = T::KickThreshold::get();
let zero_block: BlockNumberFor<T> = 0u32.into();
let candidates: Vec<T::AccountId> = CandidateList::<T>::get()
.iter()
.map(|candidate_info| candidate_info.who.clone())
.collect();
let non_removals = c.saturating_sub(r);
for i in 0..c {
LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), zero_block);
}
if non_removals > 0 {
for i in 0..non_removals {
LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), new_block);
}
} else {
for i in 0..c {
LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), new_block);
}
}
let min_candidates = min_candidates::<T>();
let pre_length = CandidateList::<T>::decode_len().unwrap_or_default();
pezframe_system::Pallet::<T>::set_block_number(new_block);
let current_length: u32 = CandidateList::<T>::decode_len()
.unwrap_or_default()
.try_into()
.unwrap_or_default();
assert!(c == current_length);
#[block]
{
<CollatorSelection<T> as SessionManager<_>>::new_session(0);
}
if c > r && non_removals >= min_candidates {
// candidates > removals and remaining candidates > min candidates
// => remaining candidates should be shorter than before removal, i.e. some were
// actually removed.
assert!(CandidateList::<T>::decode_len().unwrap_or_default() < pre_length);
} else if c > r && non_removals < min_candidates {
// candidates > removals and remaining candidates would be less than min candidates
// => remaining candidates should equal min candidates, i.e. some were removed up to
// the minimum, but then any more were "forced" to stay in candidates.
let current_length: u32 = CandidateList::<T>::decode_len()
.unwrap_or_default()
.try_into()
.unwrap_or_default();
assert!(min_candidates == current_length);
} else {
// removals >= candidates, non removals must == 0
// can't remove more than exist
assert!(CandidateList::<T>::decode_len().unwrap_or_default() == pre_length);
}
}
impl_benchmark_test_suite!(CollatorSelection, crate::mock::new_test_ext(), crate::mock::Test);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,321 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A module that is responsible for migration of storage for Collator Selection.
use super::*;
#[cfg(feature = "try-runtime")]
use alloc::vec::Vec;
use pezframe_support::traits::{OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade};
use log;
/// Migrate to v2. Should have been part of <https://github.com/pezkuwichain/kurdistan-sdk/issues/104>.
pub mod v2 {
use super::*;
use pezframe_support::{
pezpallet_prelude::*,
storage_alias,
traits::{Currency, ReservableCurrency},
};
use pezsp_runtime::traits::{Saturating, Zero};
/// [`UncheckedMigrationToV2`] wrapped in a
/// [`VersionedMigration`](pezframe_support::migrations::VersionedMigration), ensuring the
/// migration is only performed when on-chain version is 1.
pub type MigrationToV2<T> = pezframe_support::migrations::VersionedMigration<
1,
2,
UncheckedMigrationToV2<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
#[storage_alias]
pub type Candidates<T: Config> = StorageValue<
Pallet<T>,
BoundedVec<CandidateInfo<<T as pezframe_system::Config>::AccountId, <<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance>, <T as Config>::MaxCandidates>,
ValueQuery,
>;
/// Migrate to V2.
pub struct UncheckedMigrationToV2<T>(PhantomData<T>);
impl<T: Config + pezpallet_balances::Config> UncheckedOnRuntimeUpgrade for UncheckedMigrationToV2<T> {
fn on_runtime_upgrade() -> Weight {
let mut weight = Weight::zero();
let mut count: u64 = 0;
// candidates who exist under the old `Candidates` key
let candidates = Candidates::<T>::take();
// New candidates who have registered since the upgrade. Under normal circumstances,
// this should not exist because the migration should be applied when the upgrade
// happens. But in Pezkuwi/Kusama we messed this up, and people registered under
// `CandidateList` while their funds were locked in `Candidates`.
let new_candidate_list = CandidateList::<T>::get();
if new_candidate_list.len().is_zero() {
// The new list is empty, so this is essentially being applied correctly. We just
// put the candidates into the new storage item.
CandidateList::<T>::put(&candidates);
// 1 write for the new list
weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1));
} else {
// Oops, the runtime upgraded without the migration. There are new candidates in
// `CandidateList`. So, let's just refund the old ones and assume they have already
// started participating in the new system.
for candidate in candidates {
let err = T::Currency::unreserve(&candidate.who, candidate.deposit);
if err > Zero::zero() {
log::error!(
target: LOG_TARGET,
"{:?} balance was unable to be unreserved from {:?}",
err, &candidate.who,
);
}
count.saturating_inc();
}
weight.saturating_accrue(
<<T as pezpallet_balances::Config>::WeightInfo as pezpallet_balances::WeightInfo>::force_unreserve().saturating_mul(count.into()),
);
}
log::info!(
target: LOG_TARGET,
"Unreserved locked bond of {} candidates, upgraded storage to version 2",
count,
);
weight.saturating_accrue(T::DbWeight::get().reads_writes(3, 2));
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::DispatchError> {
let number_of_candidates = Candidates::<T>::get().to_vec().len();
Ok((number_of_candidates as u32).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_number_of_candidates: Vec<u8>) -> Result<(), pezsp_runtime::DispatchError> {
let new_number_of_candidates = Candidates::<T>::get().to_vec().len();
assert_eq!(
new_number_of_candidates, 0 as usize,
"after migration, the candidates map should be empty"
);
Ok(())
}
}
}
/// Version 1 Migration
/// This migration ensures that any existing `Invulnerables` storage lists are sorted.
pub mod v1 {
use super::*;
use pezframe_support::pezpallet_prelude::*;
pub struct MigrateToV1<T>(PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
fn on_runtime_upgrade() -> Weight {
let on_chain_version = Pallet::<T>::on_chain_storage_version();
if on_chain_version == 0 {
let invulnerables_len = Invulnerables::<T>::get().to_vec().len();
Invulnerables::<T>::mutate(|invulnerables| {
invulnerables.sort();
});
StorageVersion::new(1).put::<Pallet<T>>();
log::info!(
target: LOG_TARGET,
"Sorted {} Invulnerables, upgraded storage to version 1",
invulnerables_len,
);
// Similar complexity to `set_invulnerables` (put storage value)
// Plus 1 read for length, 1 read for `on_chain_version`, 1 write to put version
T::WeightInfo::set_invulnerables(invulnerables_len as u32)
.saturating_add(T::DbWeight::get().reads_writes(2, 1))
} else {
log::info!(
target: LOG_TARGET,
"Migration did not execute. This probably should be removed"
);
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::DispatchError> {
let number_of_invulnerables = Invulnerables::<T>::get().to_vec().len();
Ok((number_of_invulnerables as u32).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(number_of_invulnerables: Vec<u8>) -> Result<(), pezsp_runtime::DispatchError> {
let stored_invulnerables = Invulnerables::<T>::get().to_vec();
let mut sorted_invulnerables = stored_invulnerables.clone();
sorted_invulnerables.sort();
assert_eq!(
stored_invulnerables, sorted_invulnerables,
"after migration, the stored invulnerables should be sorted"
);
let number_of_invulnerables: u32 = Decode::decode(
&mut number_of_invulnerables.as_slice(),
)
.expect("the state parameter should be something that was generated by pre_upgrade");
let stored_invulnerables_len = stored_invulnerables.len() as u32;
assert_eq!(
number_of_invulnerables, stored_invulnerables_len,
"after migration, there should be the same number of invulnerables"
);
let on_chain_version = Pallet::<T>::on_chain_storage_version();
pezframe_support::ensure!(on_chain_version >= 1, "must_upgrade");
Ok(())
}
}
}
#[cfg(all(feature = "try-runtime", test))]
mod tests {
use super::*;
use crate::{
migration::v2::Candidates,
mock::{new_test_ext, Balances, Test},
};
use pezframe_support::{
traits::{Currency, ReservableCurrency, StorageVersion},
BoundedVec,
};
use pezsp_runtime::traits::ConstU32;
#[test]
fn migrate_to_v2_with_new_candidates() {
new_test_ext().execute_with(|| {
let storage_version = StorageVersion::new(1);
storage_version.put::<Pallet<Test>>();
let one = 1u64;
let two = 2u64;
let three = 3u64;
let deposit = 10u64;
// Set balance to 100
Balances::make_free_balance_be(&one, 100u64);
Balances::make_free_balance_be(&two, 100u64);
Balances::make_free_balance_be(&three, 100u64);
// Reservations: 10 for the "old" candidacy and 10 for the "new"
Balances::reserve(&one, 10u64).unwrap(); // old
Balances::reserve(&two, 20u64).unwrap(); // old + new
Balances::reserve(&three, 10u64).unwrap(); // new
// Candidate info
let candidate_one = CandidateInfo { who: one, deposit };
let candidate_two = CandidateInfo { who: two, deposit };
let candidate_three = CandidateInfo { who: three, deposit };
// Storage lists
let bounded_candidates =
BoundedVec::<CandidateInfo<u64, u64>, ConstU32<20>>::try_from(vec![
candidate_one.clone(),
candidate_two.clone(),
])
.expect("it works");
let bounded_candidate_list =
BoundedVec::<CandidateInfo<u64, u64>, ConstU32<20>>::try_from(vec![
candidate_two.clone(),
candidate_three.clone(),
])
.expect("it works");
// Set storage
Candidates::<Test>::put(bounded_candidates);
CandidateList::<Test>::put(bounded_candidate_list.clone());
// Sanity check
assert_eq!(Balances::free_balance(one), 90);
assert_eq!(Balances::free_balance(two), 80);
assert_eq!(Balances::free_balance(three), 90);
// Run migration
v2::MigrationToV2::<Test>::on_runtime_upgrade();
let new_storage_version = StorageVersion::get::<Pallet<Test>>();
assert_eq!(new_storage_version, 2);
// 10 should have been unreserved from the old candidacy
assert_eq!(Balances::free_balance(one), 100);
assert_eq!(Balances::free_balance(two), 90);
assert_eq!(Balances::free_balance(three), 90);
// The storage item should be gone
assert!(Candidates::<Test>::get().is_empty());
// The new storage item should be preserved
assert_eq!(CandidateList::<Test>::get(), bounded_candidate_list);
});
}
#[test]
fn migrate_to_v2_without_new_candidates() {
new_test_ext().execute_with(|| {
let storage_version = StorageVersion::new(1);
storage_version.put::<Pallet<Test>>();
let one = 1u64;
let two = 2u64;
let deposit = 10u64;
// Set balance to 100
Balances::make_free_balance_be(&one, 100u64);
Balances::make_free_balance_be(&two, 100u64);
// Reservations
Balances::reserve(&one, 10u64).unwrap(); // old
Balances::reserve(&two, 10u64).unwrap(); // old
// Candidate info
let candidate_one = CandidateInfo { who: one, deposit };
let candidate_two = CandidateInfo { who: two, deposit };
// Storage lists
let bounded_candidates =
BoundedVec::<CandidateInfo<u64, u64>, ConstU32<20>>::try_from(vec![
candidate_one.clone(),
candidate_two.clone(),
])
.expect("it works");
// Set storage
Candidates::<Test>::put(bounded_candidates.clone());
// Sanity check
assert_eq!(Balances::free_balance(one), 90);
assert_eq!(Balances::free_balance(two), 90);
// Run migration
v2::MigrationToV2::<Test>::on_runtime_upgrade();
let new_storage_version = StorageVersion::get::<Pallet<Test>>();
assert_eq!(new_storage_version, 2);
// Nothing changes deposit-wise
assert_eq!(Balances::free_balance(one), 90);
assert_eq!(Balances::free_balance(two), 90);
// The storage item should be gone
assert!(Candidates::<Test>::get().is_empty());
// The new storage item should have the info now
assert_eq!(CandidateList::<Test>::get(), bounded_candidates);
});
}
}

Some files were not shown because too many files have changed in this diff Show More