feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
doc
**/target
+26
View File
@@ -0,0 +1,26 @@
root = true
[*.rs]
indent_style=tab
indent_size=tab
tab_width=4
max_line_length=120
end_of_line=lf
charset=utf-8
trim_trailing_whitespace=true
insert_final_newline=true
[*.yml]
indent_style=space
indent_size=2
tab_width=8
end_of_line=lf
charset=utf-8
trim_trailing_whitespace=true
insert_final_newline=true
[*.sh]
indent_style=space
indent_size=2
tab_width=8
end_of_line=lf
+131
View File
@@ -0,0 +1,131 @@
[package]
name = "pezkuwi"
description = "Implementation of a `https://pezkuwichain.io` node in Rust based on the Substrate framework."
license = "GPL-3.0-only"
rust-version = "1.64.0"
readme = "README.md"
authors.workspace = true
edition.workspace = true
version = "6.0.0"
default-run = "pezkuwi"
homepage.workspace = true
repository.workspace = true
[badges]
maintenance = { status = "actively-developed" }
# Configuration for building a .deb package - for use with `cargo-deb`
[package.metadata.deb]
name = "pezkuwi"
extended-description = "Implementation of a https://pezkuwichain.io node in Rust based on the Substrate framework."
section = "misc"
maintainer = "security@parity.io"
license-file = ["LICENSE", "0"]
# https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html
maintainer-scripts = "scripts/packaging/deb-maintainer-scripts"
assets = [
[
"target/release/pezkuwi",
"/usr/bin/",
"755",
],
[
"target/release/pezkuwi-prepare-worker",
"/usr/lib/pezkuwi/",
"755",
],
[
"target/release/pezkuwi-execute-worker",
"/usr/lib/pezkuwi/",
"755",
],
[
"scripts/packaging/pezkuwi.service",
"/lib/systemd/system/",
"644",
],
]
conf-files = ["/etc/default/pezkuwi"]
[package.metadata.spellcheck]
config = "./scripts/ci/gitlab/spellcheck.toml"
[lints]
workspace = true
[[bin]]
name = "pezkuwi"
path = "src/main.rs"
[[bin]]
name = "pezkuwi-execute-worker"
path = "src/bin/execute-worker.rs"
[[bin]]
name = "pezkuwi-prepare-worker"
path = "src/bin/prepare-worker.rs"
[dependencies]
color-eyre = { workspace = true }
tikv-jemallocator = { optional = true, features = [
"unprefixed_malloc_on_supported_platforms",
], workspace = true }
# Crates in our workspace, defined as dependencies so we can pass them feature flags.
pezkuwi-cli = { features = [
"pezkuwichain-native",
"zagros-native",
], workspace = true, default-features = true }
pezkuwi-node-core-pvf = { workspace = true, default-features = true }
pezkuwi-node-core-pvf-prepare-worker = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
# Needed for worker binaries.
pezkuwi-node-core-pvf-common = { workspace = true, default-features = true }
pezkuwi-node-core-pvf-execute-worker = { workspace = true, default-features = true }
[target.'cfg(target_os = "linux")'.dependencies]
tikv-jemallocator = { workspace = true, features = [
"unprefixed_malloc_on_supported_platforms",
] }
[dev-dependencies]
assert_cmd = { workspace = true }
nix = { features = ["signal"], workspace = true }
pezkuwi-core-primitives = { workspace = true, default-features = true }
substrate-rpc-client = { workspace = true, default-features = true }
tempfile = { workspace = true }
tokio = { workspace = true, default-features = true }
[build-dependencies]
substrate-build-script-utils = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezkuwi-cli/runtime-benchmarks",
"pezkuwi-core-primitives/runtime-benchmarks",
"pezkuwi-node-core-pvf-common/runtime-benchmarks",
"pezkuwi-node-core-pvf-execute-worker/runtime-benchmarks",
"pezkuwi-node-core-pvf-prepare-worker/runtime-benchmarks",
"pezkuwi-node-core-pvf/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"substrate-rpc-client/runtime-benchmarks",
]
try-runtime = ["pezkuwi-cli/try-runtime"]
fast-runtime = ["pezkuwi-cli/fast-runtime"]
runtime-metrics = ["pezkuwi-cli/runtime-metrics"]
pyroscope = ["pezkuwi-cli/pyroscope"]
jemalloc-allocator = [
"dep:tikv-jemallocator",
"pezkuwi-node-core-pvf-prepare-worker/jemalloc-allocator",
"pezkuwi-node-core-pvf/jemalloc-allocator",
"pezkuwi-overseer/jemalloc-allocator",
]
# Generate the metadata hash needed for CheckMetadataHash
# in the builtin test runtimes (zagros and pezkuwichain).
metadata-hash = ["pezkuwi-cli/metadata-hash"]
# Enables timeout-based tests supposed to be run only in CI environment as they may be flaky
# when run locally depending on system load
ci-only-tests = ["pezkuwi-node-core-pvf/ci-only-tests"]
+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>.
+247
View File
@@ -0,0 +1,247 @@
# Pezkuwi
Implementation of a <https://pezkuwichain.app> node in Rust based on the Substrate framework.
The README provides information about installing the `pezkuwi` binary and developing on the codebase. For more specific
guides, like how to run a validator node, see the [Pezkuwi SDK docs website](https://docs.pezkuwichain.io/).
## Installation
### Using a pre-compiled binary
If you just wish to run a Pezkuwi node without compiling it yourself, you may either:
- run the [latest released binary](https://github.com/pezkuwichain/pezkuwi-sdk/releases/latest) (make sure to also
download all the `worker` binaries and put them in the same directory as `pezkuwi`), or
- install Pezkuwi from one of our package repositories.
### Debian-based (Debian, Ubuntu)
Currently supports Debian 10 (Buster) and Ubuntu 20.04 (Focal), and derivatives. Run the following
commands as the `root` user.
```bash
# Import the security@pezkuwichain.app GPG key
gpg --recv-keys --keyserver hkps://keys.mailvelope.com 9D4B2B6EB8F97156D19669A9FF0812D491B96798
gpg --export 9D4B2B6EB8F97156D19669A9FF0812D491B96798 > /usr/share/keyrings/pezkuwi.gpg
# Add the Pezkuwi repository and update the package index
echo 'deb [signed-by=/usr/share/keyrings/pezkuwi.gpg] https://releases.pezkuwichain.app/deb release main' > /etc/apt/sources.list.d/pezkuwi.list
apt update
# Install the `pezkuwi-keyring` package - This will ensure the GPG key
# used by APT remains up-to-date
apt install pezkuwi-keyring
# Install pezkuwi
apt install pezkuwi
```
### RPM-based
Currently supports Rocky Linux 10 and Alma Linux 10, and derivatives.
```bash
# Install dnf-plugins-core (This might already be installed)
dnf install dnf-plugins-core
# Add the repository and enable it
dnf config-manager --add-repo https://releases.pezkuwichain.app/rpm/pezkuwi.repo
dnf config-manager --set-enabled pezkuwi
# Install pezkuwi (You may have to confirm the import of the GPG key, which
# should have the following fingerprint: 90BD75EBBB8E95CB3DA6078F94A4029AB4B35DAE)
dnf install pezkuwi
```
Installation from Debian or RPM repository will create a `systemd` service that can be used to run a
Pezkuwi node. This is disabled by default, and can be started by running `systemctl start pezkuwi`
on demand (use `systemctl enable pezkuwi` to make it auto-start after reboot). By default, it will
run as the `pezkuwi` user. Command-line flags passed to the binary can be customized by editing
`/etc/default/pezkuwi`. This file will not be overwritten on updating Pezkuwi. You may also just
run the node directly from the command-line.
## Building
Since the Pezkuwi node is based on Substrate, first set up your build environment according to the
[Substrate installation instructions](https://docs.pezkuwichain.io/install/).
### Install via Cargo
Make sure you have the support software installed from the **Build from Source** section below this
section.
If you want to install Pezkuwi in your PATH, you can do so with:
```bash
cargo install --git https://github.com/pezkuwichain/pezkuwi-sdk --tag <version> pezkuwi --locked
```
### Build from Source
Build the client by cloning this repository and running the following commands from the root
directory of the repo:
```bash
git checkout <latest tagged release>
cargo build --release
```
**Note:** if you want to move the built `pezkuwi` binary somewhere (e.g. into $PATH) you will also
need to move `pezkuwi-execute-worker` and `pezkuwi-prepare-worker`. You can let cargo do all this
for you by running:
```sh
cargo install --path . --locked
```
#### Build from Source with Docker
You can also build from source using [Pezkuwi CI docker image](https://github.com/pezkuwichain/scripts/tree/master/dockerfiles/ci-linux):
```bash
git checkout <latest tagged release>
docker run --rm -it -w /shellhere/pezkuwi \
-v $(pwd):/shellhere/pezkuwi \
pezkuwichain/ci-linux:production cargo build --release
sudo chown -R $(id -u):$(id -g) target/
```
If you want to reproduce other steps of CI process you can use the following
[guide](https://github.com/pezkuwichain/scripts#gitlab-ci-for-building-docker-images).
## Networks
This repo supports runtimes for PezkuwiChain, Kusama, and Zagros.
### Connect to Pezkuwi Mainnet
Connect to the global Pezkuwi Mainnet network by running:
```bash
../target/release/pezkuwi --chain=pezkuwi
```
You can see your node on [Pezkuwi telemetry](https://telemetry.pezkuwichain.app/#list/0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3)
(set a custom name with `--name "my custom name"`).
### Connect to the "Kusama" Canary Network
Connect to the global Kusama canary network by running:
```bash
../target/release/pezkuwi --chain=kusama
```
You can see your node on [Kusama telemetry](https://telemetry.polkadot.io/#list/0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe)
(set a custom name with `--name "my custom name"`).
### Connect to the Zagros Testnet
Connect to the global Zagros testnet by running:
```bash
../target/release/pezkuwi --chain=zagros
```
You can see your node on [Zagros telemetry](https://telemetry.pezkuwichain.app/#list/0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e)
(set a custom name with `--name "my custom name"`).
### Obtaining HEZ, KSM, or TYR
If you want to do anything on PezkuwiChain, Kusama, or Zagros, then you'll need to get an account and
some HEZ, KSM, or TYR tokens, respectively. Follow the
[instructions](https://wiki.network.pezkuwichain.io/docs/learn-HEZ#obtaining-testnet-tokens) on the Wiki to obtain tokens for
your testnet of choice.
## Hacking on Pezkuwi
If you'd actually like to hack on Pezkuwi, you can grab the source code and build it. Ensure you
have Rust and the support software installed.
Then, grab the Pezkuwi source code:
```bash
git clone https://github.com/pezkuwichain/pezkuwi-sdk.git
cd pezkuwi-sdk
```
Then build the code. You will need to build in release mode (`--release`) to start a network. Only
use debug mode for development (faster compile times for development and testing).
```bash
cargo build
```
You can run the tests if you like:
```bash
cargo test --workspace --profile testnet
# Or run only the tests for specified crated
cargo test -p <crate-name> --profile testnet
```
You can start a development chain with:
```bash
cargo run --bin pezkuwi -- --dev
```
Detailed logs may be shown by running the node with the following environment variables set:
```bash
RUST_LOG=debug RUST_BACKTRACE=1 cargo run --bin pezkuwi-- --dev
```
### Development
You can run a simple single-node development "network" on your machine by running:
```bash
cargo run --bin pezkuwi --release -- --dev
```
You can muck around by heading to <https://js.pezkuwichain.app> and choosing "Local Node" from the
Settings menu.
### Local Two-node Testnet
If you want to see the multi-node consensus algorithm in action locally, then you can create a local
testnet. You'll need two terminals open. In one, run:
```bash
pezkuwi --dev --alice -d /tmp/alice
```
And in the other, run:
```bash
pezkuwi --dev --bob -d /tmp/bob --bootnodes '/ip4/127.0.0.1/tcp/30333/p2p/ALICE_BOOTNODE_ID_HERE'
```
Ensure you replace `ALICE_BOOTNODE_ID_HERE` with the node ID from the output of the first terminal.
### Monitoring
[Setup Prometheus and Grafana](https://docs.pezkuwichain.io/infrastructure/running-a-validator/operational-tasks/general-management/#monitor-your-node).
Once you set this up you can take a look at the [Pezkuwi Grafana dashboards](grafana/README.md)
that we currently maintain.
### Using Docker
[Using Docker](https://github.com/pezkuwichain/pezkuwi-sdk/blob/master/docs/contributor/docker.md)
### Shell Completion
[Shell Completion](https://github.com/paritytech/polkadot-sdk/blob/master/polkadot/doc/shell-completion.md)
## Contributing
### Contributing Guidelines
[Contribution Guidelines](https://github.com/pezkuwichain/pezkuwi-sdk/blob/master/docs/contributor/CONTRIBUTING.md)
### Contributor Code of Conduct
[Code of Conduct](https://github.com/pezkuwichain/pezkuwi-sdk/blob/master/docs/contributor/CODE_OF_CONDUCT.md)
## License
Pezkuwi is [GPL 3.0 licensed](https://github.com/paritytech/polkadot-sdk/blob/master/polkadot/LICENSE).
+22
View File
@@ -0,0 +1,22 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
fn main() {
substrate_build_script_utils::generate_cargo_keys();
// For the node/worker version check, make sure we always rebuild the node and binary workers
// when the version changes.
substrate_build_script_utils::rerun_if_git_head_changed();
}
+86
View File
@@ -0,0 +1,86 @@
[package]
name = "pezkuwi-cli"
description = "Pezkuwi Relay-chain Client Node"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[package.metadata.wasm-pack.profile.release]
# `wasm-opt` has some problems on Linux, see
# https://github.com/rustwasm/wasm-pack/issues/781 etc.
wasm-opt = false
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
clap = { features = ["derive"], optional = true, workspace = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
pyroscope = { optional = true, workspace = true }
pyroscope_pprofrs = { optional = true, workspace = true }
thiserror = { workspace = true }
pezkuwi-service = { optional = true, workspace = true }
frame-benchmarking-cli = { optional = true, workspace = true, default-features = true }
pezkuwi-node-metrics = { workspace = true, default-features = true }
pezkuwi-node-primitives = { workspace = true, default-features = true }
sc-cli = { optional = true, workspace = true, default-features = true }
sc-network-types = { workspace = true, default-features = true }
sc-service = { optional = true, workspace = true, default-features = true }
sc-storage-monitor = { workspace = true, default-features = true }
sc-sysinfo = { workspace = true, default-features = true }
sc-tracing = { optional = true, workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }
[build-dependencies]
substrate-build-script-utils = { workspace = true, default-features = true }
[features]
default = ["cli", "db", "full-node"]
db = ["pezkuwi-service/db"]
metadata-hash = ["pezkuwi-service/metadata-hash"]
service = ["dep:pezkuwi-service"]
cli = [
"clap",
"frame-benchmarking-cli",
"sc-cli",
"sc-service",
"sc-tracing",
"service",
]
runtime-benchmarks = [
"frame-benchmarking-cli?/runtime-benchmarks",
"pezkuwi-node-metrics/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-service?/runtime-benchmarks",
"sc-cli?/runtime-benchmarks",
"sc-service?/runtime-benchmarks",
"sc-sysinfo/runtime-benchmarks",
"sc-tracing?/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
full-node = ["pezkuwi-service/full-node"]
try-runtime = ["pezkuwi-service?/try-runtime", "sp-runtime/try-runtime"]
fast-runtime = ["pezkuwi-service/fast-runtime"]
pyroscope = ["dep:pyroscope", "pyroscope_pprofrs"]
# Configure the native runtimes to use.
zagros-native = ["pezkuwi-service/zagros-native"]
pezkuwichain-native = ["pezkuwi-service/pezkuwichain-native"]
malus = ["full-node", "pezkuwi-service/malus"]
runtime-metrics = [
"pezkuwi-node-metrics/runtime-metrics",
"pezkuwi-service/runtime-metrics",
]
+25
View File
@@ -0,0 +1,25 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
fn main() {
if let Ok(profile) = std::env::var("PROFILE") {
println!("cargo:rustc-cfg=build_type=\"{}\"", profile);
}
substrate_build_script_utils::generate_cargo_keys();
// For the node/worker version check, make sure we always rebuild the node when the version
// changes.
substrate_build_script_utils::rerun_if_git_head_changed();
}
+182
View File
@@ -0,0 +1,182 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Pezkuwi CLI library.
pub use pezkuwi_node_primitives::NODE_VERSION;
use clap::Parser;
use std::path::PathBuf;
#[allow(missing_docs)]
#[derive(Debug, Parser)]
pub enum Subcommand {
/// Build a chain specification.
/// DEPRECATED: `build-spec` command will be removed after 1/04/2026. Use `export-chain-spec`
/// command instead.
#[deprecated(
note = "build-spec command will be removed after 1/04/2026. Use export-chain-spec command instead"
)]
BuildSpec(sc_cli::BuildSpecCmd),
/// Export the chain specification.
ExportChainSpec(sc_cli::ExportChainSpecCmd),
/// Validate blocks.
CheckBlock(sc_cli::CheckBlockCmd),
/// Export blocks.
ExportBlocks(sc_cli::ExportBlocksCmd),
/// Export the state of a given block into a chain spec.
ExportState(sc_cli::ExportStateCmd),
/// Import blocks.
ImportBlocks(sc_cli::ImportBlocksCmd),
/// Remove the whole chain.
PurgeChain(sc_cli::PurgeChainCmd),
/// Revert the chain to a previous state.
Revert(sc_cli::RevertCmd),
/// Sub-commands concerned with benchmarking.
/// The pallet benchmarking moved to the `pallet` sub-command.
#[command(subcommand)]
Benchmark(frame_benchmarking_cli::BenchmarkCmd),
/// Key management CLI utilities
#[command(subcommand)]
Key(sc_cli::KeySubcommand),
/// Db meta columns information.
ChainInfo(sc_cli::ChainInfoCmd),
}
#[allow(missing_docs)]
#[derive(Debug, Parser)]
#[group(skip)]
pub struct RunCmd {
#[clap(flatten)]
pub base: sc_cli::RunCmd,
/// Force using Kusama native runtime.
#[arg(long = "force-kusama")]
pub force_kusama: bool,
/// Force using Zagros native runtime.
#[arg(long = "force-zagros")]
pub force_zagros: bool,
/// Force using Pezkuwichain native runtime.
#[arg(long = "force-pezkuwichain")]
pub force_pezkuwichain: bool,
/// Disable the BEEFY gadget.
///
/// Currently enabled by default.
#[arg(long)]
pub no_beefy: bool,
/// Allows a validator to run insecurely outside of Secure Validator Mode. Security features
/// are still enabled on a best-effort basis, but missing features are no longer required. For
/// more information see <https://github.com/w3f/polkadot-wiki/issues/4881>.
#[arg(long = "insecure-validator-i-know-what-i-do", requires = "validator")]
pub insecure_validator: bool,
/// Enable the block authoring backoff that is triggered when finality is lagging.
#[arg(long)]
pub force_authoring_backoff: bool,
/// Add the destination address to the `pyroscope` agent.
///
/// Must be valid socket address, of format `IP:Port` (commonly `127.0.0.1:4040`).
#[arg(long)]
pub pyroscope_server: Option<String>,
/// Disable automatic hardware benchmarks.
///
/// By default these benchmarks are automatically ran at startup and measure
/// the CPU speed, the memory bandwidth and the disk speed.
///
/// The results are then printed out in the logs, and also sent as part of
/// telemetry, if telemetry is enabled.
#[arg(long)]
pub no_hardware_benchmarks: bool,
/// Overseer message capacity override.
///
/// **Dangerous!** Do not touch unless explicitly advised to.
#[arg(long)]
pub overseer_channel_capacity_override: Option<usize>,
/// Path to the directory where auxiliary worker binaries reside.
///
/// If not specified, the main binary's directory is searched first, then
/// `/usr/lib/pezkuwi` is searched.
///
/// TESTING ONLY: if the path points to an executable rather then directory,
/// that executable is used both as preparation and execution worker.
#[arg(long, value_name = "PATH")]
pub workers_path: Option<PathBuf>,
/// Override the maximum number of pvf execute workers.
///
/// **Dangerous!** Do not touch unless explicitly advised to.
#[arg(long)]
pub execute_workers_max_num: Option<usize>,
/// Override the maximum number of pvf workers that can be spawned in the pvf prepare
/// pool for tasks with the priority below critical.
///
/// **Dangerous!** Do not touch unless explicitly advised to.
#[arg(long)]
pub prepare_workers_soft_max_num: Option<usize>,
/// Override the absolute number of pvf workers that can be spawned in the pvf prepare pool.
///
/// **Dangerous!** Do not touch unless explicitly advised to.
#[arg(long)]
pub prepare_workers_hard_max_num: Option<usize>,
/// TESTING ONLY: disable the version check between nodes and workers.
#[arg(long, hide = true)]
pub disable_worker_version_check: bool,
/// How long finalized data should be kept in the availability store (in hours).
/// Only used for testnets. If not specified, set to 1 hour. Always set to 25 hours for live
/// networks.
#[arg(long)]
pub keep_finalized_for: Option<u32>,
/// Overrides `HOLD_OFF_DURATION` in collator_protocol/validator_side. The value is in
/// milliseconds.
///
/// **Dangerous!** Do not touch unless explicitly advised to.
#[arg(long, hide = true)]
pub collator_protocol_hold_off: Option<u64>,
}
#[allow(missing_docs)]
#[derive(Debug, Parser)]
pub struct Cli {
#[command(subcommand)]
pub subcommand: Option<Subcommand>,
#[clap(flatten)]
pub run: RunCmd,
#[clap(flatten)]
pub storage_monitor: sc_storage_monitor::StorageMonitorParams,
}
+536
View File
@@ -0,0 +1,536 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use crate::cli::{Cli, Subcommand, NODE_VERSION};
use frame_benchmarking_cli::{
BenchmarkCmd, ExtrinsicFactory, SubstrateRemarkBuilder, SUBSTRATE_REFERENCE_HARDWARE,
};
use futures::future::TryFutureExt;
use log::{info, warn};
use pezkuwi_service::{
self,
benchmarking::{benchmark_inherent_data, TransferKeepAliveBuilder},
HeaderBackend, IdentifyVariant,
};
#[cfg(feature = "pyroscope")]
use pyroscope_pprofrs::{pprof_backend, PprofConfig};
use sc_cli::SubstrateCli;
use sc_network_types::PeerId;
use sp_core::crypto::Ss58AddressFormatRegistry;
use sp_keyring::Sr25519Keyring;
pub use crate::error::Error;
#[cfg(feature = "pyroscope")]
use std::net::ToSocketAddrs;
use std::{collections::HashSet, time::Duration};
type Result<T> = std::result::Result<T, Error>;
fn get_exec_name() -> Option<String> {
std::env::current_exe()
.ok()
.and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
.and_then(|s| s.into_string().ok())
}
fn get_invulnerable_ah_collators(
chain_spec: &Box<dyn pezkuwi_service::ChainSpec>,
) -> HashSet<PeerId> {
// A default set of invulnerable asset hub collators
const KUSAMA: [&str; 11] = [
"12D3KooWHNEENyCc4R3iDLLFaJiynUp9eDZp7TtS1G6DCp459vVK",
"12D3KooWAVqLdQEjSezy7CPEgMLMSTuyfSBdbxPGkmik5x2aL8u4",
"12D3KooWBxMiVQdYa5MaQjSWAu3YsfKdrs7vgX9cPk4cCwFVAXEu",
"12D3KooWGbRmQ9FjwkzTVTSxfUh854wxc3LUD5agjzcucDarZrNn",
"12D3KooWHwXftCGdp73t4BUxW3c9UKjYTvjc7tHsrinT5M8AUmXo",
"12D3KooWCTSAq83D99RcT64rrV5X3sGZxc9JQ8nVtd6GbZEKnDqC",
"12D3KooWF63ZxKtZMYs5247WQA8fcTiGJb2osXykc31cmjwNLwem",
"12D3KooWGowDwrXAh9cxkbPHPHuwMouFHrMcJhCVXcFS2B8vc5Ry",
"12D3KooWRhoxXsZypnp1Tady6XSRqXfxu7Bj6hGk8aj6FJ1iU6pt",
"12D3KooWJUs11H7S3Hv9BVh72w3yVmHoYTXaoBUg1KQyYk4hL2bB",
"12D3KooWAeLjabo2foz6gAQvLRfwF2d3WnpUGDjhg8V5AQUnv5AZ",
];
const PEZKUWI: [&str; 7] = [
"12D3KooWEyGg3oUwYfaLWM5AJ2pvXCUxBuXNapX1tQXLsbDmMV6z",
"12D3KooWD9dTKLW65NFFLVjqgaXNzb3zKXBfwRS5iovxV6XaoVX6",
"12D3KooWPJfGGisRMkiD5yhySZggEhyMSwELb34P2bEuAmUh9RYy",
"12D3KooWQB9RBoJEByMtXtD8aC1WR1DJQb3QMXRcsQmNxrghsQLv",
"12D3KooWFhBYG98e53DQB7W2JKBL9xWrP83ANkAjzvp4enEJAt3k",
"12D3KooWG3GrM6XKMM4gp3cvemdwUvu96ziYoJmqmetLZBXE8bSa",
"12D3KooWMRyTLrCEPcAQD6c4EnudL3vVzg9zji3whvsMYPUYevpq",
];
let invulnerables = if chain_spec.is_kusama() {
KUSAMA.to_vec()
} else if chain_spec.is_pezkuwi() {
PEZKUWI.to_vec()
} else {
vec![]
};
invulnerables
.iter()
.filter_map(|invuln_str| {
invuln_str
.parse::<PeerId>()
.map_err(|e| {
warn!("Failed to parse AssetHub invulnerable peer from the default list. This should never happen. {:?}", e)
})
.ok()
})
.collect()
}
impl SubstrateCli for Cli {
fn impl_name() -> String {
"Parity Pezkuwi".into()
}
fn impl_version() -> String {
let commit_hash = env!("SUBSTRATE_CLI_COMMIT_HASH");
format!("{}-{commit_hash}", NODE_VERSION)
}
fn description() -> String {
env!("CARGO_PKG_DESCRIPTION").into()
}
fn author() -> String {
env!("CARGO_PKG_AUTHORS").into()
}
fn support_url() -> String {
"https://github.com/pezkuwichain/pezkuwi-sdk/issues/new".into()
}
fn copyright_start_year() -> i32 {
2017
}
fn executable_name() -> String {
"pezkuwi".into()
}
fn load_spec(&self, id: &str) -> std::result::Result<Box<dyn sc_service::ChainSpec>, String> {
let id = if id == "" {
let n = get_exec_name().unwrap_or_default();
["pezkuwi", "kusama", "zagros", "pezkuwichain", "versi"]
.iter()
.cloned()
.find(|&chain| n.starts_with(chain))
.unwrap_or("pezkuwi")
} else {
id
};
Ok(match id {
"kusama" => Box::new(pezkuwi_service::chain_spec::kusama_config()?),
name if name.starts_with("kusama-") && !name.ends_with(".json") =>
Err(format!("`{name}` is not supported anymore as the kusama native runtime no longer part of the node."))?,
"pezkuwi" => Box::new(pezkuwi_service::chain_spec::pezkuwi_config()?),
name if name.starts_with("pezkuwi-") && !name.ends_with(".json") =>
Err(format!("`{name}` is not supported anymore as the pezkuwi native runtime no longer part of the node."))?,
"paseo" => Box::new(pezkuwi_service::chain_spec::paseo_config()?),
"pezkuwichain" => Box::new(pezkuwi_service::chain_spec::pezkuwichain_config()?),
#[cfg(feature = "pezkuwichain-native")]
"dev" | "pezkuwichain-dev" => Box::new(pezkuwi_service::chain_spec::pezkuwichain_development_config()?),
#[cfg(feature = "pezkuwichain-native")]
"pezkuwichain-local" => Box::new(pezkuwi_service::chain_spec::pezkuwichain_local_testnet_config()?),
#[cfg(feature = "pezkuwichain-native")]
"pezkuwichain-staging" => Box::new(pezkuwi_service::chain_spec::pezkuwichain_staging_testnet_config()?),
#[cfg(not(feature = "pezkuwichain-native"))]
name if name.starts_with("pezkuwichain-") && !name.ends_with(".json") || name == "dev" =>
Err(format!("`{}` only supported with `pezkuwichain-native` feature enabled.", name))?,
"zagros" => Box::new(pezkuwi_service::chain_spec::zagros_config()?),
#[cfg(feature = "zagros-native")]
"zagros-dev" => Box::new(pezkuwi_service::chain_spec::zagros_development_config()?),
#[cfg(feature = "zagros-native")]
"zagros-local" => Box::new(pezkuwi_service::chain_spec::zagros_local_testnet_config()?),
#[cfg(feature = "zagros-native")]
"zagros-staging" => Box::new(pezkuwi_service::chain_spec::zagros_staging_testnet_config()?),
#[cfg(feature = "pezkuwichain-native")]
"versi-dev" => Box::new(pezkuwi_service::chain_spec::versi_development_config()?),
#[cfg(feature = "pezkuwichain-native")]
"versi-local" => Box::new(pezkuwi_service::chain_spec::versi_local_testnet_config()?),
#[cfg(feature = "pezkuwichain-native")]
"versi-staging" => Box::new(pezkuwi_service::chain_spec::versi_staging_testnet_config()?),
#[cfg(not(feature = "pezkuwichain-native"))]
name if name.starts_with("versi-") =>
Err(format!("`{}` only supported with `pezkuwichain-native` feature enabled.", name))?,
path => {
let path = std::path::PathBuf::from(path);
let chain_spec = Box::new(pezkuwi_service::GenericChainSpec::from_json_file(path.clone())?)
as Box<dyn pezkuwi_service::ChainSpec>;
// When `force_*` is given or the file name starts with the name of one of the known
// chains, we use the chain spec for the specific chain.
if self.run.force_pezkuwichain ||
chain_spec.is_pezkuwichain() ||
chain_spec.is_versi()
{
Box::new(pezkuwi_service::PezkuwichainChainSpec::from_json_file(path)?)
} else if self.run.force_kusama || chain_spec.is_kusama() {
Box::new(pezkuwi_service::GenericChainSpec::from_json_file(path)?)
} else if self.run.force_zagros || chain_spec.is_zagros() {
Box::new(pezkuwi_service::ZagrosChainSpec::from_json_file(path)?)
} else {
chain_spec
}
},
})
}
}
fn set_default_ss58_version(spec: &Box<dyn pezkuwi_service::ChainSpec>) {
let ss58_version = if spec.is_kusama() {
Ss58AddressFormatRegistry::KusamaAccount
} else if spec.is_zagros() {
Ss58AddressFormatRegistry::SubstrateAccount
} else {
Ss58AddressFormatRegistry::PezkuwiAccount
}
.into();
sp_core::crypto::set_default_ss58_version(ss58_version);
}
/// Launch a node, accepting arguments just like a regular node,
/// accepts an alternative overseer generator, to adjust behavior
/// for integration tests as needed.
/// `malus_finality_delay` restrict finality votes of this node
/// to be at most `best_block - malus_finality_delay` height.
#[cfg(feature = "malus")]
pub fn run_node(
run: Cli,
overseer_gen: impl pezkuwi_service::OverseerGen,
malus_finality_delay: Option<u32>,
) -> Result<()> {
run_node_inner(run, overseer_gen, malus_finality_delay, |_logger_builder, _config| {})
}
fn run_node_inner<F>(
cli: Cli,
overseer_gen: impl pezkuwi_service::OverseerGen,
maybe_malus_finality_delay: Option<u32>,
logger_hook: F,
) -> Result<()>
where
F: FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration),
{
let runner = cli
.create_runner_with_logger_hook::<_, _, F>(&cli.run.base, logger_hook)
.map_err(Error::from)?;
let chain_spec = &runner.config().chain_spec;
// By default, enable BEEFY on all networks, unless explicitly disabled through CLI.
let enable_beefy = !cli.run.no_beefy;
set_default_ss58_version(chain_spec);
if chain_spec.is_kusama() {
info!("----------------------------");
info!("This chain is not in any way");
info!(" endorsed by the ");
info!(" KUSAMA FOUNDATION ");
info!("----------------------------");
}
let node_version =
if cli.run.disable_worker_version_check { None } else { Some(NODE_VERSION.to_string()) };
let secure_validator_mode = cli.run.base.validator && !cli.run.insecure_validator;
// Parse collator protocol hold off value and get the list of the invlunerable collators.
let collator_protocol_hold_off = cli.run.collator_protocol_hold_off.map(Duration::from_millis);
let invulnerable_ah_collators = get_invulnerable_ah_collators(&chain_spec);
runner.run_node_until_exit(move |config| async move {
let hwbench = (!cli.run.no_hardware_benchmarks)
.then(|| {
config.database.path().map(|database_path| {
let _ = std::fs::create_dir_all(&database_path);
sc_sysinfo::gather_hwbench(Some(database_path), &SUBSTRATE_REFERENCE_HARDWARE)
})
})
.flatten();
let database_source = config.database.clone();
let task_manager = pezkuwi_service::build_full(
config,
pezkuwi_service::NewFullParams {
is_teyrchain_node: pezkuwi_service::IsTeyrchainNode::No,
enable_beefy,
force_authoring_backoff: cli.run.force_authoring_backoff,
telemetry_worker_handle: None,
node_version,
secure_validator_mode,
workers_path: cli.run.workers_path,
workers_names: None,
overseer_gen,
overseer_message_channel_capacity_override: cli
.run
.overseer_channel_capacity_override,
malus_finality_delay: maybe_malus_finality_delay,
hwbench,
execute_workers_max_num: cli.run.execute_workers_max_num,
prepare_workers_hard_max_num: cli.run.prepare_workers_hard_max_num,
prepare_workers_soft_max_num: cli.run.prepare_workers_soft_max_num,
keep_finalized_for: cli.run.keep_finalized_for,
invulnerable_ah_collators,
collator_protocol_hold_off,
},
)
.map(|full| full.task_manager)?;
if let Some(path) = database_source.path() {
sc_storage_monitor::StorageMonitorService::try_spawn(
cli.storage_monitor,
path.to_path_buf(),
&task_manager.spawn_essential_handle(),
)?;
}
Ok(task_manager)
})
}
/// Parses pezkuwi specific CLI arguments and run the service.
pub fn run() -> Result<()> {
let cli: Cli = Cli::from_args();
#[cfg(feature = "pyroscope")]
let mut pyroscope_agent_maybe = if let Some(ref agent_addr) = cli.run.pyroscope_server {
let address = agent_addr
.to_socket_addrs()
.map_err(Error::AddressResolutionFailure)?
.next()
.ok_or_else(|| Error::AddressResolutionMissing)?;
// The pyroscope agent requires a `http://` prefix, so we just do that.
let agent = pyroscope::PyroscopeAgent::builder(
"http://".to_owned() + address.to_string().as_str(),
"pezkuwi".to_owned(),
)
.backend(pprof_backend(PprofConfig::new().sample_rate(113)))
.build()?;
Some(agent.start()?)
} else {
None
};
#[cfg(not(feature = "pyroscope"))]
if cli.run.pyroscope_server.is_some() {
return Err(Error::PyroscopeNotCompiledIn);
}
match &cli.subcommand {
None => run_node_inner(
cli,
pezkuwi_service::ValidatorOverseerGen,
None,
pezkuwi_node_metrics::logger_hook(),
),
#[allow(deprecated)]
Some(Subcommand::BuildSpec(cmd)) => {
let runner = cli.create_runner(cmd)?;
Ok(runner.sync_run(|config| cmd.run(config.chain_spec, config.network))?)
},
Some(Subcommand::ExportChainSpec(cmd)) => {
// Directly load the embedded chain spec using the CLIs load_spec method.
let spec = cli.load_spec(&cmd.chain)?;
cmd.run(spec).map_err(Into::into)
},
Some(Subcommand::CheckBlock(cmd)) => {
let runner = cli.create_runner(cmd).map_err(Error::SubstrateCli)?;
let chain_spec = &runner.config().chain_spec;
set_default_ss58_version(chain_spec);
runner.async_run(|mut config| {
let (client, _, import_queue, task_manager) =
pezkuwi_service::new_chain_ops(&mut config)?;
Ok((cmd.run(client, import_queue).map_err(Error::SubstrateCli), task_manager))
})
},
Some(Subcommand::ExportBlocks(cmd)) => {
let runner = cli.create_runner(cmd)?;
let chain_spec = &runner.config().chain_spec;
set_default_ss58_version(chain_spec);
Ok(runner.async_run(|mut config| {
let (client, _, _, task_manager) =
pezkuwi_service::new_chain_ops(&mut config).map_err(Error::PezkuwiService)?;
Ok((cmd.run(client, config.database).map_err(Error::SubstrateCli), task_manager))
})?)
},
Some(Subcommand::ExportState(cmd)) => {
let runner = cli.create_runner(cmd)?;
let chain_spec = &runner.config().chain_spec;
set_default_ss58_version(chain_spec);
Ok(runner.async_run(|mut config| {
let (client, _, _, task_manager) = pezkuwi_service::new_chain_ops(&mut config)?;
Ok((cmd.run(client, config.chain_spec).map_err(Error::SubstrateCli), task_manager))
})?)
},
Some(Subcommand::ImportBlocks(cmd)) => {
let runner = cli.create_runner(cmd)?;
let chain_spec = &runner.config().chain_spec;
set_default_ss58_version(chain_spec);
Ok(runner.async_run(|mut config| {
let (client, _, import_queue, task_manager) =
pezkuwi_service::new_chain_ops(&mut config)?;
Ok((cmd.run(client, import_queue).map_err(Error::SubstrateCli), task_manager))
})?)
},
Some(Subcommand::PurgeChain(cmd)) => {
let runner = cli.create_runner(cmd)?;
Ok(runner.sync_run(|config| cmd.run(config.database))?)
},
Some(Subcommand::Revert(cmd)) => {
let runner = cli.create_runner(cmd)?;
let chain_spec = &runner.config().chain_spec;
set_default_ss58_version(chain_spec);
Ok(runner.async_run(|mut config| {
let (client, backend, _, task_manager) =
pezkuwi_service::new_chain_ops(&mut config)?;
let task_handle = task_manager.spawn_handle();
let aux_revert = Box::new(|client, backend, blocks| {
pezkuwi_service::revert_backend(client, backend, blocks, config, task_handle)
.map_err(|err| {
match err {
pezkuwi_service::Error::Blockchain(err) => err.into(),
// Generic application-specific error.
err => sc_cli::Error::Application(err.into()),
}
})
});
Ok((
cmd.run(client, backend, Some(aux_revert)).map_err(Error::SubstrateCli),
task_manager,
))
})?)
},
Some(Subcommand::Benchmark(cmd)) => {
let runner = cli.create_runner(cmd)?;
let chain_spec = &runner.config().chain_spec;
match cmd {
#[cfg(not(feature = "runtime-benchmarks"))]
BenchmarkCmd::Storage(_) =>
return Err(sc_cli::Error::Input(
"Compile with --features=runtime-benchmarks \
to enable storage benchmarks."
.into(),
)
.into()),
#[cfg(feature = "runtime-benchmarks")]
BenchmarkCmd::Storage(cmd) => runner.sync_run(|mut config| {
let (client, backend, _, _) = pezkuwi_service::new_chain_ops(&mut config)?;
let db = backend.expose_db();
let storage = backend.expose_storage();
let shared_trie_cache = backend.expose_shared_trie_cache();
cmd.run(config, client.clone(), db, storage, shared_trie_cache).map_err(Error::SubstrateCli)
}),
BenchmarkCmd::Block(cmd) => runner.sync_run(|mut config| {
let (client, _, _, _) = pezkuwi_service::new_chain_ops(&mut config)?;
cmd.run(client.clone()).map_err(Error::SubstrateCli)
}),
BenchmarkCmd::Overhead(cmd) => runner.sync_run(|config| {
if cmd.params.runtime.is_some() {
return Err(sc_cli::Error::Input(
"Pezkuwi binary does not support `--runtime` flag for `benchmark overhead`. Please provide a chain spec or use the `frame-omni-bencher`."
.into(),
)
.into())
}
cmd.run_with_default_builder_and_spec::<pezkuwi_service::Block, ()>(
Some(config.chain_spec),
)
.map_err(Error::SubstrateCli)
}),
BenchmarkCmd::Extrinsic(cmd) => runner.sync_run(|mut config| {
let (client, _, _, _) = pezkuwi_service::new_chain_ops(&mut config)?;
let header = client.header(client.info().genesis_hash).unwrap().unwrap();
let inherent_data = benchmark_inherent_data(header)
.map_err(|e| format!("generating inherent data: {:?}", e))?;
let remark_builder = SubstrateRemarkBuilder::new_from_client(client.clone())?;
let tka_builder = TransferKeepAliveBuilder::new(
client.clone(),
Sr25519Keyring::Alice.to_account_id(),
config.chain_spec.identify_chain(),
);
let ext_factory =
ExtrinsicFactory(vec![Box::new(remark_builder), Box::new(tka_builder)]);
cmd.run(client.clone(), inherent_data, Vec::new(), &ext_factory)
.map_err(Error::SubstrateCli)
}),
BenchmarkCmd::Pallet(cmd) => {
set_default_ss58_version(chain_spec);
if cfg!(feature = "runtime-benchmarks") {
runner.sync_run(|config| {
cmd.run_with_spec::<sp_runtime::traits::HashingFor<pezkuwi_service::Block>, ()>(
Some(config.chain_spec),
)
.map_err(|e| Error::SubstrateCli(e))
})
} else {
Err(sc_cli::Error::Input(
"Benchmarking wasn't enabled when building the node. \
You can enable it with `--features runtime-benchmarks`."
.into(),
)
.into())
}
},
BenchmarkCmd::Machine(cmd) => runner.sync_run(|config| {
cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone())
.map_err(Error::SubstrateCli)
}),
// NOTE: this allows the Pezkuwi client to leniently implement
// new benchmark commands.
#[allow(unreachable_patterns)]
_ => Err(Error::CommandNotImplemented),
}
},
Some(Subcommand::Key(cmd)) => Ok(cmd.run(&cli)?),
Some(Subcommand::ChainInfo(cmd)) => {
let runner = cli.create_runner(cmd)?;
Ok(runner.sync_run(|config| cmd.run::<pezkuwi_service::Block>(&config))?)
},
}?;
#[cfg(feature = "pyroscope")]
if let Some(pyroscope_agent) = pyroscope_agent_maybe.take() {
let agent = pyroscope_agent.stop()?;
agent.shutdown();
}
Ok(())
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
PezkuwiService(#[from] pezkuwi_service::Error),
#[error(transparent)]
SubstrateCli(#[from] sc_cli::Error),
#[error(transparent)]
SubstrateService(#[from] sc_service::Error),
#[error(transparent)]
SubstrateTracing(#[from] sc_tracing::logging::Error),
#[cfg(not(feature = "pyroscope"))]
#[error("Binary was not compiled with `--feature=pyroscope`")]
PyroscopeNotCompiledIn,
#[cfg(feature = "pyroscope")]
#[error("Failed to connect to pyroscope agent")]
PyroscopeError(#[from] pyroscope::error::PyroscopeError),
#[error("Failed to resolve provided URL")]
AddressResolutionFailure(#[from] std::io::Error),
#[error("URL did not resolve to anything")]
AddressResolutionMissing,
#[error("Command is not implemented")]
CommandNotImplemented,
#[error(transparent)]
Storage(#[from] sc_storage_monitor::Error),
#[error("Other: {0}")]
Other(String),
#[error("This subcommand is only available when compiled with `{feature}`")]
FeatureNotEnabled { feature: &'static str },
}
impl From<String> for Error {
fn from(s: String) -> Self {
Self::Other(s)
}
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Pezkuwi CLI library.
#![warn(missing_docs)]
#[cfg(feature = "cli")]
mod cli;
#[cfg(feature = "cli")]
mod command;
#[cfg(feature = "cli")]
mod error;
#[cfg(feature = "service")]
pub use pezkuwi_service::{
self as service, Block, CoreApi, IdentifyVariant, ProvideRuntimeApi, TFullClient,
};
#[cfg(feature = "malus")]
pub use pezkuwi_service::overseer::validator_overseer_builder;
#[cfg(feature = "cli")]
pub use cli::*;
#[cfg(feature = "cli")]
pub use command::*;
#[cfg(feature = "cli")]
pub use sc_cli::{Error, Result};
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "pezkuwi-core-primitives"
version = "7.0.0"
description = "Core Pezkuwi types used by Relay Chains and teyrchains."
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
[features]
default = ["std"]
std = ["codec/std", "scale-info/std", "sp-core/std", "sp-runtime/std"]
runtime-benchmarks = ["sp-runtime/runtime-benchmarks"]
+186
View File
@@ -0,0 +1,186 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
#![cfg_attr(not(feature = "std"), no_std)]
//! Core Pezkuwi types.
//!
//! These core Pezkuwi types are used by the relay chain and the Teyrchains.
extern crate alloc;
use codec::{Decode, DecodeWithMemTracking, Encode};
use scale_info::TypeInfo;
use sp_runtime::{
generic,
traits::{IdentifyAccount, Verify},
MultiSignature,
};
pub use sp_runtime::traits::{BlakeTwo256, Hash as HashT};
/// The block number type used by Pezkuwi.
/// 32-bits will allow for 136 years of blocks assuming 1 block per second.
pub type BlockNumber = u32;
/// An instant or duration in time.
pub type Moment = u64;
/// Alias to type for a signature for a transaction on the relay chain. This allows one of several
/// kinds of underlying crypto to be used, so isn't a fixed size when encoded.
pub type Signature = MultiSignature;
/// Alias to the public key used for this chain, actually a `MultiSigner`. Like the signature, this
/// also isn't a fixed size when encoded, as different cryptos have different size public keys.
pub type AccountPublic = <Signature as Verify>::Signer;
/// Alias to the opaque account ID type for this chain, actually a `AccountId32`. This is always
/// 32 bytes.
pub type AccountId = <AccountPublic as IdentifyAccount>::AccountId;
/// The type for looking up accounts. We don't expect more than 4 billion of them.
pub type AccountIndex = u32;
/// Identifier for a chain. 32-bit should be plenty.
pub type ChainId = u32;
/// A hash of some data used by the relay chain.
pub type Hash = sp_core::H256;
/// Unit type wrapper around [`type@Hash`] that represents a candidate hash.
///
/// This type is produced by `CandidateReceipt::hash`.
///
/// This type makes it easy to enforce that a hash is a candidate hash on the type level.
#[derive(
Clone,
Copy,
Encode,
Decode,
DecodeWithMemTracking,
Hash,
Eq,
PartialEq,
Default,
PartialOrd,
Ord,
TypeInfo,
)]
pub struct CandidateHash(pub Hash);
#[cfg(feature = "std")]
impl std::ops::Deref for CandidateHash {
type Target = Hash;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(feature = "std")]
impl std::fmt::Display for CandidateHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl core::fmt::Debug for CandidateHash {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}", self.0)
}
}
/// Index of a transaction in the relay chain. 32-bit should be plenty.
pub type Nonce = u32;
/// The balance of an account.
/// 128-bits (or 38 significant decimal figures) will allow for 10 m currency (`10^7`) at a
/// resolution to all for one second's worth of an annualised 50% reward be paid to a unit holder
/// (`10^11` unit denomination), or `10^18` total atomic units, to grow at 50%/year for 51 years
/// (`10^9` multiplier) for an eventual total of `10^27` units (27 significant decimal figures).
/// We round denomination to `10^12` (12 SDF), and leave the other redundancy at the upper end so
/// that 32 bits may be multiplied with a balance in 128 bits without worrying about overflow.
pub type Balance = u128;
/// Header type.
pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
/// Block type.
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
/// Block ID.
pub type BlockId = generic::BlockId<Block>;
/// Opaque, encoded, unchecked extrinsic.
pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic;
/// The information that goes alongside a `transfer_into_teyrchain` operation. Entirely opaque, it
/// will generally be used for identifying the reason for the transfer. Typically it will hold the
/// destination account to which the transfer should be credited. If still more information is
/// needed, then this should be a hash with the pre-image presented via an off-chain mechanism on
/// the teyrchain.
pub type Remark = [u8; 32];
/// A message sent from the relay-chain down to a teyrchain.
///
/// The size of the message is limited by the `config.max_downward_message_size` parameter.
pub type DownwardMessage = alloc::vec::Vec<u8>;
/// A wrapped version of `DownwardMessage`. The difference is that it has attached the block number
/// when the message was sent.
#[derive(
Encode, Decode, DecodeWithMemTracking, Clone, sp_runtime::RuntimeDebug, PartialEq, TypeInfo,
)]
pub struct InboundDownwardMessage<BlockNumber = crate::BlockNumber> {
/// The block number at which these messages were put into the downward message queue.
pub sent_at: BlockNumber,
/// The actual downward message to processes.
pub msg: DownwardMessage,
}
/// An HRMP message seen from the perspective of a recipient.
#[derive(
Encode, Decode, DecodeWithMemTracking, Clone, sp_runtime::RuntimeDebug, PartialEq, TypeInfo,
)]
pub struct InboundHrmpMessage<BlockNumber = crate::BlockNumber> {
/// The block number at which this message was sent.
/// Specifically, it is the block number at which the candidate that sends this message was
/// enacted.
pub sent_at: BlockNumber,
/// The message payload.
pub data: alloc::vec::Vec<u8>,
}
/// An HRMP message seen from the perspective of a sender.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Clone,
sp_runtime::RuntimeDebug,
PartialEq,
Eq,
Hash,
TypeInfo,
)]
pub struct OutboundHrmpMessage<Id> {
/// The para that will get this message in its downward message queue.
pub recipient: Id,
/// The message payload.
pub data: alloc::vec::Vec<u8>,
}
/// `V2` primitives.
pub mod v2 {
pub use super::*;
}
+42
View File
@@ -0,0 +1,42 @@
# Shell completion
The Pezkuwi CLI command supports shell auto-completion. For this to work, you will need to run the completion script
matching you build and system.
Assuming you built a release version using `cargo build --release` and use `bash` run the following:
```bash
source target/release/completion-scripts/pezkuwi.bash
```
You can find completion scripts for:
- bash
- fish
- zsh
- elvish
- powershell
To make this change persistent, you can proceed as follow:
## First install
```bash
COMPL_DIR=$HOME/.completion
mkdir -p $COMPL_DIR
cp -f target/release/completion-scripts/pezkuwi.bash $COMPL_DIR/
echo "source $COMPL_DIR/pezkuwi.bash" >> $HOME/.bash_profile
source $HOME/.bash_profile
```
## Update
When you build a new version of Pezkuwi, the following will ensure you auto-completion script matches the current
binary:
```bash
COMPL_DIR=$HOME/.completion
mkdir -p $COMPL_DIR
cp -f target/release/completion-scripts/pezkuwi.bash $COMPL_DIR/
source $HOME/.bash_profile
```
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "pezkuwi-erasure-coding"
version = "7.0.0"
description = "Erasure coding used for Pezkuwi's availability system"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive", "std"], workspace = true }
novelpoly = { workspace = true }
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-trie = { workspace = true, default-features = true }
thiserror = { workspace = true }
[dev-dependencies]
criterion = { features = ["cargo_bench_support"], workspace = true }
quickcheck = { workspace = true }
[[bench]]
name = "scaling_with_validators"
harness = false
[features]
runtime-benchmarks = [
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"sp-trie/runtime-benchmarks",
]
+43
View File
@@ -0,0 +1,43 @@
# Run benches
```
cd erasure-coding # ensure you are in the right directory
cargo bench
```
## `scaling_with_validators`
This benchmark evaluates the performance of constructing the chunks and the erasure root from PoV and
reconstructing the PoV from chunks (either from systematic chunks or regular chunks).
You can see the results of running this bench on 5950x below (only including recovery from regular chunks).
Interestingly, with `10_000` chunks (validators) its slower than with `50_000` for both construction
and reconstruction.
```
construct/200 time: [93.924 ms 94.525 ms 95.214 ms]
thrpt: [52.513 MiB/s 52.896 MiB/s 53.234 MiB/s]
construct/500 time: [111.25 ms 111.52 ms 111.80 ms]
thrpt: [44.721 MiB/s 44.837 MiB/s 44.946 MiB/s]
construct/1000 time: [117.37 ms 118.28 ms 119.21 ms]
thrpt: [41.941 MiB/s 42.273 MiB/s 42.601 MiB/s]
construct/2000 time: [125.05 ms 125.72 ms 126.38 ms]
thrpt: [39.564 MiB/s 39.772 MiB/s 39.983 MiB/s]
construct/10000 time: [270.46 ms 275.11 ms 279.81 ms]
thrpt: [17.869 MiB/s 18.174 MiB/s 18.487 MiB/s]
construct/50000 time: [205.86 ms 209.66 ms 213.64 ms]
thrpt: [23.404 MiB/s 23.848 MiB/s 24.288 MiB/s]
reconstruct/200 time: [180.73 ms 184.09 ms 187.73 ms]
thrpt: [26.634 MiB/s 27.160 MiB/s 27.666 MiB/s]
reconstruct/500 time: [195.59 ms 198.58 ms 201.76 ms]
thrpt: [24.781 MiB/s 25.179 MiB/s 25.564 MiB/s]
reconstruct/1000 time: [207.92 ms 211.57 ms 215.57 ms]
thrpt: [23.195 MiB/s 23.633 MiB/s 24.048 MiB/s]
reconstruct/2000 time: [218.59 ms 223.68 ms 229.18 ms]
thrpt: [21.817 MiB/s 22.354 MiB/s 22.874 MiB/s]
reconstruct/10000 time: [496.35 ms 505.17 ms 515.42 ms]
thrpt: [9.7008 MiB/s 9.8977 MiB/s 10.074 MiB/s]
reconstruct/50000 time: [276.56 ms 277.53 ms 278.58 ms]
thrpt: [17.948 MiB/s 18.016 MiB/s 18.079 MiB/s]
```
Results from running on an Apple M2 Pro, systematic recovery is generally 40 times faster than
regular recovery, achieving 1 Gib/s.
@@ -0,0 +1,118 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use pezkuwi_primitives::Hash;
use std::time::Duration;
fn chunks(n_validators: usize, pov: &Vec<u8>) -> Vec<Vec<u8>> {
pezkuwi_erasure_coding::obtain_chunks(n_validators, pov).unwrap()
}
fn erasure_root(n_validators: usize, pov: &Vec<u8>) -> Hash {
let chunks = chunks(n_validators, pov);
pezkuwi_erasure_coding::branches(&chunks).root()
}
fn construct_and_reconstruct_5mb_pov(c: &mut Criterion) {
const N_VALIDATORS: [usize; 6] = [200, 500, 1000, 2000, 10_000, 50_000];
const KB: usize = 1024;
const MB: usize = 1024 * KB;
let pov = vec![0xfe; 5 * MB];
let mut group = c.benchmark_group("construct");
for n_validators in N_VALIDATORS {
let expected_root = erasure_root(n_validators, &pov);
group.throughput(Throughput::Bytes(pov.len() as u64));
group.bench_with_input(
BenchmarkId::from_parameter(n_validators),
&n_validators,
|b, &n| {
b.iter(|| {
let root = erasure_root(n, &pov);
assert_eq!(root, expected_root);
});
},
);
}
group.finish();
let mut group = c.benchmark_group("reconstruct_regular");
for n_validators in N_VALIDATORS {
let all_chunks = chunks(n_validators, &pov);
let chunks: Vec<_> = all_chunks
.iter()
.enumerate()
.take(pezkuwi_erasure_coding::recovery_threshold(n_validators).unwrap())
.map(|(i, c)| (&c[..], i))
.collect();
group.throughput(Throughput::Bytes(pov.len() as u64));
group.bench_with_input(
BenchmarkId::from_parameter(n_validators),
&n_validators,
|b, &n| {
b.iter(|| {
let _pov: Vec<u8> =
pezkuwi_erasure_coding::reconstruct(n, chunks.clone()).unwrap();
});
},
);
}
group.finish();
let mut group = c.benchmark_group("reconstruct_systematic");
for n_validators in N_VALIDATORS {
let all_chunks = chunks(n_validators, &pov);
let chunks = all_chunks
.into_iter()
.take(pezkuwi_erasure_coding::systematic_recovery_threshold(n_validators).unwrap())
.collect::<Vec<_>>();
group.throughput(Throughput::Bytes(pov.len() as u64));
group.bench_with_input(
BenchmarkId::from_parameter(n_validators),
&n_validators,
|b, &n| {
b.iter(|| {
let _pov: Vec<u8> =
pezkuwi_erasure_coding::reconstruct_from_systematic(n, chunks.clone())
.unwrap();
});
},
);
}
group.finish();
}
fn criterion_config() -> Criterion {
Criterion::default()
.sample_size(15)
.warm_up_time(Duration::from_millis(200))
.measurement_time(Duration::from_secs(3))
}
criterion_group!(
name = re_construct;
config = criterion_config();
targets = construct_and_reconstruct_5mb_pov,
);
criterion_main!(re_construct);
+3
View File
@@ -0,0 +1,3 @@
hfuzz_target/
hfuzz_workspace/
Cargo.lock
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "erasure_coding_fuzzer"
version = "1.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lints]
workspace = true
[dependencies]
honggfuzz = { workspace = true }
pezkuwi-erasure-coding = { workspace = true, default-features = true }
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
[[bin]]
name = "reconstruct"
path = "src/reconstruct.rs"
[[bin]]
name = "round_trip"
path = "src/round_trip.rs"
[features]
runtime-benchmarks = [
"pezkuwi-erasure-coding/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
]
@@ -0,0 +1,32 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use honggfuzz::fuzz;
use pezkuwi_erasure_coding::*;
use pezkuwi_node_primitives::AvailableData;
fn main() {
loop {
fuzz!(|data: (usize, Vec<(Vec<u8>, usize)>)| {
let (num_validators, chunk_input) = data;
let reconstructed: Result<AvailableData, _> = reconstruct_v1(
num_validators,
chunk_input.iter().map(|t| (&*t.0, t.1)).collect::<Vec<(&[u8], usize)>>(),
);
println!("reconstructed {:?}", reconstructed);
});
}
}
@@ -0,0 +1,49 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use honggfuzz::fuzz;
use pezkuwi_erasure_coding::*;
use pezkuwi_node_primitives::{AvailableData, BlockData, PoV};
use pezkuwi_primitives::PersistedValidationData;
use std::sync::Arc;
fn main() {
loop {
fuzz!(|data: &[u8]| {
let pov_block = PoV { block_data: BlockData(data.iter().cloned().collect()) };
let available_data = AvailableData {
pov: Arc::new(pov_block),
validation_data: PersistedValidationData::default(),
};
let chunks = obtain_chunks_v1(10, &available_data).unwrap();
assert_eq!(chunks.len(), 10);
// any 4 chunks should work.
let reconstructed: AvailableData = reconstruct_v1(
10,
[(&*chunks[1], 1), (&*chunks[4], 4), (&*chunks[6], 6), (&*chunks[9], 9)]
.iter()
.cloned(),
)
.unwrap();
assert_eq!(reconstructed, available_data);
println!("{:?}", reconstructed);
});
}
}
+451
View File
@@ -0,0 +1,451 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! As part of Pezkuwi's availability system, certain pieces of data
//! for each block are required to be kept available.
//!
//! The way we accomplish this is by erasure coding the data into n pieces
//! and constructing a merkle root of the data.
//!
//! Each of n validators stores their piece of data. We assume `n = 3f + k`, `0 < k ≤ 3`.
//! f is the maximum number of faulty validators in the system.
//! The data is coded so any f+1 chunks can be used to reconstruct the full data.
use codec::{Decode, Encode};
use pezkuwi_node_primitives::{AvailableData, Proof};
use pezkuwi_primitives::{BlakeTwo256, Hash as H256, HashT};
use sp_core::Blake2Hasher;
use sp_trie::{
trie_types::{TrieDBBuilder, TrieDBMutBuilderV0 as TrieDBMutBuilder},
LayoutV0, MemoryDB, Trie, TrieMut, EMPTY_PREFIX,
};
use thiserror::Error;
use novelpoly::{CodeParams, WrappedShard};
// we are limited to the field order of GF(2^16), which is 65536
const MAX_VALIDATORS: usize = novelpoly::f2e16::FIELD_SIZE;
/// Errors in erasure coding.
#[derive(Debug, Clone, PartialEq, Error)]
pub enum Error {
/// Returned when there are too many validators.
#[error("There are too many validators")]
TooManyValidators,
/// Cannot encode something for zero or one validator
#[error("Expected at least 2 validators")]
NotEnoughValidators,
/// Cannot reconstruct: wrong number of validators.
#[error("Validator count mismatches between encoding and decoding")]
WrongValidatorCount,
/// Not enough chunks present.
#[error("Not enough chunks to reconstruct message")]
NotEnoughChunks,
/// Too many chunks present.
#[error("Too many chunks present")]
TooManyChunks,
/// Chunks not of uniform length or the chunks are empty.
#[error("Chunks are not uniform, mismatch in length or are zero sized")]
NonUniformChunks,
/// An uneven byte-length of a shard is not valid for `GF(2^16)` encoding.
#[error("Uneven length is not valid for field GF(2^16)")]
UnevenLength,
/// Chunk index out of bounds.
#[error("Chunk is out of bounds: {chunk_index} not included in 0..{n_validators}")]
ChunkIndexOutOfBounds { chunk_index: usize, n_validators: usize },
/// Bad payload in reconstructed bytes.
#[error("Reconstructed payload invalid")]
BadPayload,
/// Unable to decode reconstructed bytes.
#[error("Unable to decode reconstructed payload: {0}")]
Decode(#[source] codec::Error),
/// Invalid branch proof.
#[error("Invalid branch proof")]
InvalidBranchProof,
/// Branch out of bounds.
#[error("Branch is out of bounds")]
BranchOutOfBounds,
/// Unknown error
#[error("An unknown error has appeared when reconstructing erasure code chunks")]
UnknownReconstruction,
/// Unknown error
#[error("An unknown error has appeared when deriving code parameters from validator count")]
UnknownCodeParam,
}
impl From<novelpoly::Error> for Error {
fn from(error: novelpoly::Error) -> Self {
match error {
novelpoly::Error::NeedMoreShards { .. } => Self::NotEnoughChunks,
novelpoly::Error::ParamterMustBePowerOf2 { .. } => Self::UnevenLength,
novelpoly::Error::WantedShardCountTooHigh(_) => Self::TooManyValidators,
novelpoly::Error::WantedShardCountTooLow(_) => Self::NotEnoughValidators,
novelpoly::Error::PayloadSizeIsZero { .. } => Self::BadPayload,
novelpoly::Error::InconsistentShardLengths { .. } => Self::NonUniformChunks,
_ => Self::UnknownReconstruction,
}
}
}
/// Obtain a threshold of chunks that should be enough to recover the data.
pub const fn recovery_threshold(n_validators: usize) -> Result<usize, Error> {
if n_validators > MAX_VALIDATORS {
return Err(Error::TooManyValidators);
}
if n_validators <= 1 {
return Err(Error::NotEnoughValidators);
}
let needed = n_validators.saturating_sub(1) / 3;
Ok(needed + 1)
}
/// Obtain the threshold of systematic chunks that should be enough to recover the data.
///
/// If the regular `recovery_threshold` is a power of two, then it returns the same value.
/// Otherwise, it returns the next lower power of two.
pub fn systematic_recovery_threshold(n_validators: usize) -> Result<usize, Error> {
code_params(n_validators).map(|params| params.k())
}
fn code_params(n_validators: usize) -> Result<CodeParams, Error> {
// we need to be able to reconstruct from 1/3 - eps
let n_wanted = n_validators;
let k_wanted = recovery_threshold(n_wanted)?;
if n_wanted > MAX_VALIDATORS as usize {
return Err(Error::TooManyValidators);
}
CodeParams::derive_parameters(n_wanted, k_wanted).map_err(|e| match e {
novelpoly::Error::WantedShardCountTooHigh(_) => Error::TooManyValidators,
novelpoly::Error::WantedShardCountTooLow(_) => Error::NotEnoughValidators,
_ => Error::UnknownCodeParam,
})
}
/// Reconstruct the v1 available data from the set of systematic chunks.
///
/// Provide a vector containing chunk data. If too few chunks are provided, recovery is not
/// possible.
pub fn reconstruct_from_systematic_v1(
n_validators: usize,
chunks: Vec<Vec<u8>>,
) -> Result<AvailableData, Error> {
reconstruct_from_systematic(n_validators, chunks)
}
/// Reconstruct the available data from the set of systematic chunks.
///
/// Provide a vector containing the first k chunks in order. If too few chunks are provided,
/// recovery is not possible.
pub fn reconstruct_from_systematic<T: Decode>(
n_validators: usize,
chunks: Vec<Vec<u8>>,
) -> Result<T, Error> {
let code_params = code_params(n_validators)?;
let k = code_params.k();
for chunk_data in chunks.iter().take(k) {
if !chunk_data.len().is_multiple_of(2) {
return Err(Error::UnevenLength);
}
}
let bytes = code_params.make_encoder().reconstruct_from_systematic(
chunks.into_iter().take(k).map(|data| WrappedShard::new(data)).collect(),
)?;
Decode::decode(&mut &bytes[..]).map_err(|err| Error::Decode(err))
}
/// Obtain erasure-coded chunks for v1 `AvailableData`, one for each validator.
///
/// Works only up to 65536 validators, and `n_validators` must be non-zero.
pub fn obtain_chunks_v1(n_validators: usize, data: &AvailableData) -> Result<Vec<Vec<u8>>, Error> {
obtain_chunks(n_validators, data)
}
/// Obtain erasure-coded chunks, one for each validator.
///
/// Works only up to 65536 validators, and `n_validators` must be non-zero.
pub fn obtain_chunks<T: Encode>(n_validators: usize, data: &T) -> Result<Vec<Vec<u8>>, Error> {
let params = code_params(n_validators)?;
let encoded = data.encode();
if encoded.is_empty() {
return Err(Error::BadPayload);
}
let shards = params
.make_encoder()
.encode::<WrappedShard>(&encoded[..])
.expect("Payload non-empty, shard sizes are uniform, and validator numbers checked; qed");
Ok(shards.into_iter().map(|w: WrappedShard| w.into_inner()).collect())
}
/// Reconstruct the v1 available data from a set of chunks.
///
/// Provide an iterator containing chunk data and the corresponding index.
/// The indices of the present chunks must be indicated. If too few chunks
/// are provided, recovery is not possible.
///
/// Works only up to 65536 validators, and `n_validators` must be non-zero.
pub fn reconstruct_v1<'a, I: 'a>(n_validators: usize, chunks: I) -> Result<AvailableData, Error>
where
I: IntoIterator<Item = (&'a [u8], usize)>,
{
reconstruct(n_validators, chunks)
}
/// Reconstruct decodable data from a set of chunks.
///
/// Provide an iterator containing chunk data and the corresponding index.
/// The indices of the present chunks must be indicated. If too few chunks
/// are provided, recovery is not possible.
///
/// Works only up to 65536 validators, and `n_validators` must be non-zero.
pub fn reconstruct<'a, I: 'a, T: Decode>(n_validators: usize, chunks: I) -> Result<T, Error>
where
I: IntoIterator<Item = (&'a [u8], usize)>,
{
let params = code_params(n_validators)?;
let mut received_shards: Vec<Option<WrappedShard>> = vec![None; n_validators];
for (chunk_data, chunk_idx) in chunks.into_iter().take(n_validators) {
if !chunk_data.len().is_multiple_of(2) {
return Err(Error::UnevenLength);
}
received_shards[chunk_idx] = Some(WrappedShard::new(chunk_data.to_vec()));
}
let payload_bytes = params.make_encoder().reconstruct(received_shards)?;
Decode::decode(&mut &payload_bytes[..]).map_err(|_| Error::BadPayload)
}
/// An iterator that yields merkle branches and chunk data for all chunks to
/// be sent to other validators.
pub struct Branches<'a, I> {
trie_storage: MemoryDB<Blake2Hasher>,
root: H256,
chunks: &'a [I],
current_pos: usize,
}
impl<'a, I: AsRef<[u8]>> Branches<'a, I> {
/// Get the trie root.
pub fn root(&self) -> H256 {
self.root
}
}
impl<'a, I: AsRef<[u8]>> Iterator for Branches<'a, I> {
type Item = (Proof, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
use sp_trie::Recorder;
let mut recorder = Recorder::<LayoutV0<Blake2Hasher>>::new();
let res = {
let trie = TrieDBBuilder::new(&self.trie_storage, &self.root)
.with_recorder(&mut recorder)
.build();
(self.current_pos as u32).using_encoded(|s| trie.get(s))
};
match res.expect("all nodes in trie present; qed") {
Some(_) => {
let nodes: Vec<Vec<u8>> = recorder.drain().into_iter().map(|r| r.data).collect();
let chunk = self.chunks.get(self.current_pos).expect(
"there is a one-to-one mapping of chunks to valid merkle branches; qed",
);
self.current_pos += 1;
Proof::try_from(nodes).ok().map(|proof| (proof, chunk.as_ref()))
},
None => None,
}
}
}
/// Construct a trie from chunks of an erasure-coded value. This returns the root hash and an
/// iterator of merkle proofs, one for each validator.
pub fn branches<'a, I: 'a>(chunks: &'a [I]) -> Branches<'a, I>
where
I: AsRef<[u8]>,
{
let mut trie_storage: MemoryDB<Blake2Hasher> = MemoryDB::default();
let mut root = H256::default();
// construct trie mapping each chunk's index to its hash.
{
let mut trie = TrieDBMutBuilder::new(&mut trie_storage, &mut root).build();
for (i, chunk) in chunks.as_ref().iter().enumerate() {
(i as u32).using_encoded(|encoded_index| {
let chunk_hash = BlakeTwo256::hash(chunk.as_ref());
trie.insert(encoded_index, chunk_hash.as_ref())
.expect("a fresh trie stored in memory cannot have errors loading nodes; qed");
})
}
}
Branches { trie_storage, root, chunks, current_pos: 0 }
}
/// Verify a merkle branch, yielding the chunk hash meant to be present at that
/// index.
pub fn branch_hash(root: &H256, branch_nodes: &Proof, index: usize) -> Result<H256, Error> {
let mut trie_storage: MemoryDB<Blake2Hasher> = MemoryDB::default();
for node in branch_nodes.iter() {
(&mut trie_storage as &mut sp_trie::HashDB<_>).insert(EMPTY_PREFIX, node);
}
let trie = TrieDBBuilder::new(&trie_storage, &root).build();
let res = (index as u32).using_encoded(|key| {
trie.get_with(key, |raw_hash: &[u8]| H256::decode(&mut &raw_hash[..]))
});
match res {
Ok(Some(Ok(hash))) => Ok(hash),
Ok(Some(Err(_))) => Err(Error::InvalidBranchProof), // hash failed to decode
Ok(None) => Err(Error::BranchOutOfBounds),
Err(_) => Err(Error::InvalidBranchProof),
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use pezkuwi_node_primitives::{AvailableData, BlockData, PoV};
use pezkuwi_primitives::{HeadData, PersistedValidationData};
use quickcheck::{Arbitrary, Gen, QuickCheck};
// In order to adequately compute the number of entries in the Merkle
// trie, we must account for the fixed 16-ary trie structure.
const KEY_INDEX_NIBBLE_SIZE: usize = 4;
#[derive(Clone, Debug)]
struct ArbitraryAvailableData(AvailableData);
impl Arbitrary for ArbitraryAvailableData {
fn arbitrary(g: &mut Gen) -> Self {
// Limit the POV len to 1 mib, otherwise the test will take forever
let pov_len = (u32::arbitrary(g) % (1024 * 1024)).max(2);
let pov = (0..pov_len).map(|_| u8::arbitrary(g)).collect();
let pvd = PersistedValidationData {
parent_head: HeadData((0..u16::arbitrary(g)).map(|_| u8::arbitrary(g)).collect()),
relay_parent_number: u32::arbitrary(g),
relay_parent_storage_root: [u8::arbitrary(g); 32].into(),
max_pov_size: u32::arbitrary(g),
};
ArbitraryAvailableData(AvailableData {
pov: Arc::new(PoV { block_data: BlockData(pov) }),
validation_data: pvd,
})
}
}
#[test]
fn field_order_is_right_size() {
assert_eq!(MAX_VALIDATORS, 65536);
}
#[test]
fn round_trip_works() {
let pov = PoV { block_data: BlockData((0..255).collect()) };
let available_data = AvailableData { pov: pov.into(), validation_data: Default::default() };
let chunks = obtain_chunks(10, &available_data).unwrap();
assert_eq!(chunks.len(), 10);
// any 4 chunks should work.
let reconstructed: AvailableData = reconstruct(
10,
[(&*chunks[1], 1), (&*chunks[4], 4), (&*chunks[6], 6), (&*chunks[9], 9)]
.iter()
.cloned(),
)
.unwrap();
assert_eq!(reconstructed, available_data);
}
#[test]
fn round_trip_systematic_works() {
fn property(available_data: ArbitraryAvailableData, n_validators: u16) {
let n_validators = n_validators.max(2);
let kpow2 = systematic_recovery_threshold(n_validators as usize).unwrap();
let chunks = obtain_chunks(n_validators as usize, &available_data.0).unwrap();
assert_eq!(
reconstruct_from_systematic_v1(
n_validators as usize,
chunks.into_iter().take(kpow2).collect()
)
.unwrap(),
available_data.0
);
}
QuickCheck::new().quickcheck(property as fn(ArbitraryAvailableData, u16))
}
#[test]
fn reconstruct_does_not_panic_on_low_validator_count() {
let reconstructed = reconstruct_v1(1, [].iter().cloned());
assert_eq!(reconstructed, Err(Error::NotEnoughValidators));
}
fn generate_trie_and_generate_proofs(magnitude: u32) {
let n_validators = 2_u32.pow(magnitude) as usize;
let pov = PoV { block_data: BlockData(vec![2; n_validators / KEY_INDEX_NIBBLE_SIZE]) };
let available_data = AvailableData { pov: pov.into(), validation_data: Default::default() };
let chunks = obtain_chunks(magnitude as usize, &available_data).unwrap();
assert_eq!(chunks.len() as u32, magnitude);
let branches = branches(chunks.as_ref());
let root = branches.root();
let proofs: Vec<_> = branches.map(|(proof, _)| proof).collect();
assert_eq!(proofs.len() as u32, magnitude);
for (i, proof) in proofs.into_iter().enumerate() {
let encode = Encode::encode(&proof);
let decode = Decode::decode(&mut &encode[..]).unwrap();
assert_eq!(proof, decode);
assert_eq!(encode, Encode::encode(&decode));
assert_eq!(branch_hash(&root, &proof, i).unwrap(), BlakeTwo256::hash(&chunks[i]));
}
}
#[test]
fn roundtrip_proof_encoding() {
for i in 2..16 {
generate_trie_and_generate_proofs(i);
}
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
+93
View File
@@ -0,0 +1,93 @@
# Do I need this ?
PezkuwiChain nodes collect and produce Prometheus metrics and logs. These include health, performance and debug
information such as last finalized block, height of the chain, and many other deeper implementation details
of the PezkuwiChain/Substrate node subsystems. These are crucial pieces of information that one needs to successfully
monitor the liveliness and performance of a network and its validators.
# How does it work ?
Just import the dashboard JSON files from this folder in your Grafana installation. All dashboards are grouped in
folder per category (like for example `teyrchains`). The files have been created by Grafana export functionality and
follow the data model specified [here](https://grafana.com/docs/grafana/latest/dashboards/json-model/).
We aim to keep the dashboards here in sync with the implementation, except dashboards for development and
testing.
# Contributing
**Your contributions are most welcome!**
Please make sure to follow the following design guidelines:
- Add a new entry in this file and describe the usecase and key metrics
- Ensure proper names and descriptions for dashboard panels and add relevant documentation when needed.
This is very important as not all users have similar depth of understanding of the implementation
- Have labels for axis
- All values have proper units of measurement
- A crisp and clear color scheme is used
# Prerequisites
Before you continue make sure you have Grafana set up, or otherwise follow this
[guide](https://wiki.network.pezkuwichain.io/docs/maintain-guides-how-to-monitor-your-node).
You might also need to [setup Loki](https://grafana.com/go/webinar/loki-getting-started/).
# Alerting
Alerts are currently out of the scope of the dashboards, but their setup can be done manually or automated
(see [installing and configuring Alert Manager](https://wiki.network.pezkuwichain.io/docs/maintain-guides-how-to-monitor-your-node#installing-and-configuring-alertmanager-optional))
# Dashboards
This section is a list of dashboards, their use case as well as the key metrics that are covered.
## Node Versions
Useful for monitoring versions and logs of validator nodes. Includes time series panels that
track node warning and error log rates. These can be further investigated in Grafana Loki.
Requires Loki for log aggregation and querying.
[Dashboard JSON](general/kusama_deployment.json)
## Teyrchain Status
This dashboard allows you to see at a glance how fast are candidates approved, disputed and
finalized. It was originally designed for observing liveliness after teyrchain deployment in
Kusama/PezkuwiChain, but can be useful generally in production or testing.
It includes panels covering key subsystems of the teyrchain node side implementation:
- Backing
- PVF execution
- Approval voting
- Disputes coordinator
- Chain selection
It is important to note that this dashboard applies only for validator nodes. The prometheus
queries assume the `instance` label value contains the string `validator` only for validator nodes.
[Dashboard JSON](teyrchains/status.json)
### Key liveliness indicators
- **Relay chain finality lag**. How far behind finality is compared to the current best block. By design,
GRANDPA never finalizes past last 2 blocks, so this value is always >=2 blocks.
- **Approval checking finality lag**. The distance (in blocks) between the chain head and the last block
on which Approval voting is happening. The block is generally the highest approved ancestor of the head
block and the metric is computed during relay chain selection.
- **Disputes finality lag**. How far behind the chain head is the last approved and non disputed block.
This value is always higher than approval checking lag as it further restricts finality to only undisputed
chains.
- **PVF preparation and execution time**. Each teyrchain has it's own PVF (teyrchain validation function):
a wasm blob that is executed by validators during backing, approval checking and disputing. The PVF
preparation time refers to the time it takes for the PVF wasm to be compiled. This step is done once and
then result cached. PVF execution will use the resulting artifact to execute the PVF for a given candidate.
PVFs are expected to have a limited execution time to ensure there is enough time left for the teyrchain
block to be included in the relay block.
- **Time to recover and check candidate**. This is part of approval voting and covers the time it takes
to recover the candidate block available data from other validators, check it (includes PVF execution time)
and issue statement or initiate dispute.
- **Assignment delay tranches**. Approval voting is designed such that validators assigned to check a specific
candidate are split up into equal delay tranches (0.5 seconds each). All validators checks are ordered by the delay
tranche index. Early tranches of validators have the opportunity to check the candidate first before later tranches
that act as backups in case of no shows.
@@ -0,0 +1,928 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Monitors deployed versions, warnings and errors logged.",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": 88,
"iteration": 1640613477360,
"links": [],
"liveNow": false,
"panels": [
{
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 36,
"title": "Overview",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": []
},
"overrides": []
},
"gridPos": {
"h": 13,
"w": 7,
"x": 0,
"y": 1
},
"id": 22,
"options": {
"displayLabels": [
"name",
"percent"
],
"legend": {
"displayMode": "hidden",
"placement": "bottom",
"values": [
"percent"
]
},
"pieType": "donut",
"reduceOptions": {
"calcs": [],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"exemplar": false,
"expr": "sort_desc (count by (version) (pezkuwi_build_info{chain=\"kusama\", instance=~\".*validator.*\"}))",
"instant": true,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{ version }}",
"refId": "A"
}
],
"title": "Versions",
"type": "piechart"
},
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": []
},
"overrides": []
},
"gridPos": {
"h": 13,
"w": 7,
"x": 7,
"y": 1
},
"id": 31,
"links": [],
"options": {
"displayLabels": [
"name",
"percent"
],
"legend": {
"displayMode": "hidden",
"placement": "bottom"
},
"pieType": "donut",
"reduceOptions": {
"calcs": [],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"expr": "sum(count_over_time({host=~\"kusama-validator.*\", level=\"WARN\"} [1h])) by (target)",
"instant": false,
"legendFormat": "{{ target }}",
"range": true,
"refId": "A"
}
],
"title": "Warnings / h / target",
"transformations": [],
"type": "piechart"
},
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": []
},
"overrides": []
},
"gridPos": {
"h": 13,
"w": 7,
"x": 14,
"y": 1
},
"id": 33,
"links": [],
"options": {
"displayLabels": [
"name",
"percent"
],
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"pieType": "donut",
"reduceOptions": {
"calcs": [],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"expr": "sum(count_over_time({host=~\"kusama-validator.*\", level=\"ERROR\"} [1h])) by (target)",
"instant": false,
"legendFormat": "{{ target }}",
"range": true,
"refId": "A"
}
],
"title": "Errors / h / target",
"transformations": [],
"type": "piechart"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 14
},
"id": 16,
"panels": [],
"title": "Validator versions",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"description": "Version information for all nodes.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-RdYlGr"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 14,
"x": 0,
"y": 15
},
"id": 13,
"options": {
"displayMode": "lcd",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showUnfilled": true,
"text": {
"titleSize": 12,
"valueSize": 22
}
},
"pluginVersion": "8.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"exemplar": false,
"expr": "sort_desc (count by (version) (pezkuwi_build_info{chain=\"kusama\", instance=~\".*validator.*\"}))",
"instant": true,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{ version }}",
"refId": "A"
}
],
"title": "Versions",
"type": "bargauge"
},
{
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Time"
},
"properties": [
{
"id": "custom.width",
"value": 232
}
]
},
{
"matcher": {
"id": "byName",
"options": "name"
},
"properties": [
{
"id": "custom.width",
"value": 220
}
]
}
]
},
"gridPos": {
"h": 12,
"w": 10,
"x": 14,
"y": 15
},
"id": 20,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"frameIndex": 2,
"showHeader": true,
"sortBy": [
{
"desc": false,
"displayName": "version (lastNotNull)"
}
]
},
"pluginVersion": "8.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"exemplar": false,
"expr": "pezkuwi_build_info{instance=~\".*validator.*\",chain=\"kusama\"}",
"format": "table",
"instant": false,
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Node versions ",
"transformations": [
{
"id": "groupBy",
"options": {
"fields": {
"name": {
"aggregations": [],
"operation": "groupby"
},
"version": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
}
}
}
}
],
"type": "table"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 27
},
"id": 18,
"panels": [],
"title": "Warnings and errors",
"type": "row"
},
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
"seriesBy": "last"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "hue",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 500
},
{
"color": "red",
"value": 1000
}
]
},
"unit": "warnings/h"
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 12,
"x": 0,
"y": 28
},
"id": 8,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"expr": "sum(count_over_time({host=~\"kusama-validator.*\", level=\"WARN\"} [1h])) by (target)",
"instant": false,
"legendFormat": "{{target}}",
"range": true,
"refId": "A"
}
],
"title": "All warnings / hour",
"transformations": [],
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
"seriesBy": "last"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "hue",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 500
},
{
"color": "red",
"value": 1000
}
]
},
"unit": "warnings/h"
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 12,
"x": 12,
"y": 28
},
"id": 30,
"links": [],
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"expr": "sum(count_over_time({host=~\"kusama-validator.*\", level=\"WARN\", target=\"teyrchain\"}[1h])) by (subtarget)",
"hide": false,
"instant": false,
"legendFormat": "{{subtarget}}",
"range": true,
"refId": "B"
}
],
"title": "Teyrchain warnings/hour",
"transformations": [],
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
"seriesBy": "last"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "hue",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "dark-red",
"value": 3
}
]
},
"unit": "warnings/h"
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 12,
"x": 0,
"y": 40
},
"id": 34,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"expr": "sum(count_over_time({host=~\"kusama-validator.*\", level=\"ERROR\"} [1h])) by (target)",
"instant": false,
"legendFormat": "{{target}}",
"range": true,
"refId": "A"
}
],
"title": "All errors / hour",
"transformations": [],
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
"seriesBy": "last"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "hue",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 5
},
{
"color": "red",
"value": 15
}
]
},
"unit": "warnings/h"
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 12,
"x": 12,
"y": 40
},
"id": 32,
"links": [],
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"pluginVersion": "8.1.3",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P367D1C7027A603FA"
},
"expr": "sum(count_over_time({host=~\"kusama-validator.*\", level=\"ERROR\", target=\"teyrchain\"}[1h])) by (subtarget)",
"hide": false,
"instant": false,
"legendFormat": "{{subtarget}}",
"range": true,
"refId": "B"
}
],
"title": "Teyrchain errors/hour",
"transformations": [],
"type": "timeseries"
}
],
"refresh": "15m",
"schemaVersion": 34,
"style": "dark",
"tags": [
"Kusama",
"Loki",
"Logs"
],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"definition": "pezkuwi_build_info{chain=\"$chain\"}",
"description": "Version of the node",
"hide": 0,
"includeAll": true,
"label": "Version",
"multi": true,
"name": "version",
"options": [],
"query": {
"query": "pezkuwi_build_info{chain=\"$chain\"}",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": ".*version=\"(.*?)\".*",
"skipUrlSync": false,
"sort": 5,
"type": "query"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "prometheus",
"uid": "P5CA6DFE95AABF258"
},
"definition": "pezkuwi_sync_peers{chain=\"$chain\"}",
"description": "Validator hosts",
"hide": 0,
"includeAll": true,
"label": "Instance",
"multi": true,
"name": "instance",
"options": [],
"query": {
"query": "pezkuwi_sync_peers{chain=\"$chain\"}",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": ".*instance=\"(.*validator.*)*",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-7d",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Kusama Validators Overview",
"uid": "0i-QjQ82j",
"version": 29,
"weekStart": ""
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
[package]
name = "pezkuwi-node-collation-generation"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Collator-side subsystem that handles incoming candidate submissions from the teyrchain."
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["bit-vec", "derive"], workspace = true }
futures = { workspace = true }
gum = { workspace = true, default-features = true }
pezkuwi-erasure-coding = { workspace = true, default-features = true }
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-primitives = { workspace = true, default-features = true }
schnellru = { workspace = true }
sp-core = { workspace = true, default-features = true }
thiserror = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives = { workspace = true, features = ["test"] }
pezkuwi-primitives-test-helpers = { workspace = true }
rstest = { workspace = true }
sp-keyring = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-erasure-coding/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
]
@@ -0,0 +1,42 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_primitives::CommittedCandidateReceiptError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Subsystem(#[from] pezkuwi_node_subsystem::SubsystemError),
#[error(transparent)]
OneshotRecv(#[from] futures::channel::oneshot::Canceled),
#[error(transparent)]
Runtime(#[from] pezkuwi_node_subsystem::errors::RuntimeApiError),
#[error(transparent)]
Util(#[from] pezkuwi_node_subsystem_util::Error),
#[error(transparent)]
UtilRuntime(#[from] pezkuwi_node_subsystem_util::runtime::Error),
#[error(transparent)]
Erasure(#[from] pezkuwi_erasure_coding::Error),
#[error("Collation submitted before initialization")]
SubmittedBeforeInit,
#[error("V2 core index check failed: {0}")]
CandidateReceiptCheck(CommittedCandidateReceiptError),
#[error("PoV size {0} exceeded maximum size of {1}")]
POVSizeExceeded(usize, usize),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -0,0 +1,637 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The collation generation subsystem is the interface between pezkuwi and the collators.
//!
//! # Protocol
//!
//! On every `ActiveLeavesUpdate`:
//!
//! * If there is no collation generation config, ignore.
//! * Otherwise, for each `activated` head in the update:
//! * Determine if the para is scheduled on any core by fetching the `availability_cores` Runtime
//! API.
//! * Use the Runtime API subsystem to fetch the full validation data.
//! * Invoke the `collator`, and use its outputs to produce a
//! [`pezkuwi_primitives::CandidateReceiptV2`], signed with the configuration's `key`.
//! * Dispatch a [`CollatorProtocolMessage::DistributeCollation`]`(receipt, pov)`.
#![deny(missing_docs)]
use codec::Encode;
use error::{Error, Result};
use futures::{channel::oneshot, future::FutureExt, select};
use pezkuwi_node_primitives::{
AvailableData, Collation, CollationGenerationConfig, CollationSecondedSignal, PoV,
SubmitCollationParams,
};
use pezkuwi_node_subsystem::{
messages::{CollationGenerationMessage, CollatorProtocolMessage, RuntimeApiMessage},
overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem,
SubsystemContext, SubsystemError, SubsystemResult, SubsystemSender,
};
use pezkuwi_node_subsystem_util::{
request_claim_queue, request_persisted_validation_data, request_session_index_for_child,
request_validation_code_hash, request_validators, runtime::ClaimQueueSnapshot,
};
use pezkuwi_primitives::{
transpose_claim_queue, CandidateCommitments, CandidateDescriptorV2,
CommittedCandidateReceiptV2, CoreIndex, Hash, Id as ParaId, OccupiedCoreAssumption,
PersistedValidationData, SessionIndex, TransposedClaimQueue, ValidationCodeHash,
};
use schnellru::{ByLength, LruMap};
use std::{collections::HashSet, sync::Arc};
mod error;
#[cfg(test)]
mod tests;
mod metrics;
use self::metrics::Metrics;
const LOG_TARGET: &'static str = "teyrchain::collation-generation";
/// Collation Generation Subsystem
pub struct CollationGenerationSubsystem {
config: Option<Arc<CollationGenerationConfig>>,
session_info_cache: SessionInfoCache,
metrics: Metrics,
}
#[overseer::contextbounds(CollationGeneration, prefix = self::overseer)]
impl CollationGenerationSubsystem {
/// Create a new instance of the `CollationGenerationSubsystem`.
pub fn new(metrics: Metrics) -> Self {
Self { config: None, metrics, session_info_cache: SessionInfoCache::new() }
}
/// Run this subsystem
///
/// Conceptually, this is very simple: it just loops forever.
///
/// - On incoming overseer messages, it starts or stops jobs as appropriate.
/// - On other incoming messages, if they can be converted into `Job::ToJob` and include a hash,
/// then they're forwarded to the appropriate individual job.
/// - On outgoing messages from the jobs, it forwards them to the overseer.
///
/// If `err_tx` is not `None`, errors are forwarded onto that channel as they occur.
/// Otherwise, most are logged and then discarded.
async fn run<Context>(mut self, mut ctx: Context) {
loop {
select! {
incoming = ctx.recv().fuse() => {
if self.handle_incoming::<Context>(incoming, &mut ctx).await {
break;
}
},
}
}
}
// handle an incoming message. return true if we should break afterwards.
// note: this doesn't strictly need to be a separate function; it's more an administrative
// function so that we don't clutter the run loop. It could in principle be inlined directly
// into there. it should hopefully therefore be ok that it's an async function mutably borrowing
// self.
async fn handle_incoming<Context>(
&mut self,
incoming: SubsystemResult<FromOrchestra<<Context as SubsystemContext>::Message>>,
ctx: &mut Context,
) -> bool {
match incoming {
Ok(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate {
activated,
..
}))) => {
if let Err(err) = self.handle_new_activation(activated.map(|v| v.hash), ctx).await {
gum::warn!(target: LOG_TARGET, err = ?err, "failed to handle new activation");
}
false
},
Ok(FromOrchestra::Signal(OverseerSignal::Conclude)) => true,
Ok(FromOrchestra::Communication {
msg: CollationGenerationMessage::Initialize(config),
}) => {
if self.config.is_some() {
gum::error!(target: LOG_TARGET, "double initialization");
} else {
self.config = Some(Arc::new(config));
}
false
},
Ok(FromOrchestra::Communication {
msg: CollationGenerationMessage::Reinitialize(config),
}) => {
self.config = Some(Arc::new(config));
false
},
Ok(FromOrchestra::Communication {
msg: CollationGenerationMessage::SubmitCollation(params),
}) => {
if let Err(err) = self.handle_submit_collation(params, ctx).await {
gum::error!(target: LOG_TARGET, ?err, "Failed to submit collation");
}
false
},
Ok(FromOrchestra::Signal(OverseerSignal::BlockFinalized(..))) => false,
Err(err) => {
gum::error!(
target: LOG_TARGET,
err = ?err,
"error receiving message from subsystem context: {:?}",
err
);
true
},
}
}
async fn handle_submit_collation<Context>(
&mut self,
params: SubmitCollationParams,
ctx: &mut Context,
) -> Result<()> {
let Some(config) = &self.config else {
return Err(Error::SubmittedBeforeInit);
};
let _timer = self.metrics.time_submit_collation();
let SubmitCollationParams {
relay_parent,
collation,
parent_head,
validation_code_hash,
result_sender,
core_index,
} = params;
let mut validation_data = match request_persisted_validation_data(
relay_parent,
config.para_id,
OccupiedCoreAssumption::TimedOut,
ctx.sender(),
)
.await
.await??
{
Some(v) => v,
None => {
gum::debug!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
our_para = %config.para_id,
"No validation data for para - does it exist at this relay-parent?",
);
return Ok(());
},
};
// We need to swap the parent-head data, but all other fields here will be correct.
validation_data.parent_head = parent_head;
let claim_queue = request_claim_queue(relay_parent, ctx.sender()).await.await??;
let session_index =
request_session_index_for_child(relay_parent, ctx.sender()).await.await??;
let session_info =
self.session_info_cache.get(relay_parent, session_index, ctx.sender()).await?;
let collation = PreparedCollation {
collation,
relay_parent,
para_id: config.para_id,
validation_data,
validation_code_hash,
n_validators: session_info.n_validators,
core_index,
session_index,
};
construct_and_distribute_receipt(
collation,
ctx.sender(),
result_sender,
&mut self.metrics,
&transpose_claim_queue(claim_queue),
)
.await?;
Ok(())
}
async fn handle_new_activation<Context>(
&mut self,
maybe_activated: Option<Hash>,
ctx: &mut Context,
) -> Result<()> {
let Some(config) = &self.config else {
return Ok(());
};
let Some(relay_parent) = maybe_activated else { return Ok(()) };
// If there is no collation function provided, bail out early.
// Important: Lookahead collator and slot based collator do not use `CollatorFn`.
if config.collator.is_none() {
return Ok(());
}
let para_id = config.para_id;
let _timer = self.metrics.time_new_activation();
let session_index =
request_session_index_for_child(relay_parent, ctx.sender()).await.await??;
let session_info =
self.session_info_cache.get(relay_parent, session_index, ctx.sender()).await?;
let n_validators = session_info.n_validators;
let claim_queue =
ClaimQueueSnapshot::from(request_claim_queue(relay_parent, ctx.sender()).await.await??);
let assigned_cores = claim_queue
.iter_all_claims()
.filter_map(|(core_idx, para_ids)| {
para_ids.iter().any(|&para_id| para_id == config.para_id).then_some(*core_idx)
})
.collect::<Vec<_>>();
// Nothing to do if no core is assigned to us at any depth.
if assigned_cores.is_empty() {
return Ok(());
}
// We are being very optimistic here, but one of the cores could be pending availability
// for some more blocks, or even time out. We assume all cores are being freed.
let mut validation_data = match request_persisted_validation_data(
relay_parent,
para_id,
// Just use included assumption always. If there are no pending candidates it's a
// no-op.
OccupiedCoreAssumption::Included,
ctx.sender(),
)
.await
.await??
{
Some(v) => v,
None => {
gum::debug!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
our_para = %para_id,
"validation data is not available",
);
return Ok(());
},
};
let validation_code_hash = match request_validation_code_hash(
relay_parent,
para_id,
// Just use included assumption always. If there are no pending candidates it's a
// no-op.
OccupiedCoreAssumption::Included,
ctx.sender(),
)
.await
.await??
{
Some(v) => v,
None => {
gum::debug!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
our_para = %para_id,
"validation code hash is not found.",
);
return Ok(());
},
};
let task_config = config.clone();
let metrics = self.metrics.clone();
let mut task_sender = ctx.sender().clone();
ctx.spawn(
"chained-collation-builder",
Box::pin(async move {
let transposed_claim_queue = transpose_claim_queue(claim_queue.0.clone());
// Track used core indexes not to submit collations on the same core.
let mut used_cores = HashSet::new();
for i in 0..assigned_cores.len() {
// Get the collation.
let collator_fn = match task_config.collator.as_ref() {
Some(x) => x,
None => return,
};
let (collation, result_sender) =
match collator_fn(relay_parent, &validation_data).await {
Some(collation) => collation.into_inner(),
None => {
gum::debug!(
target: LOG_TARGET,
?para_id,
"collator returned no collation on collate",
);
return;
},
};
// Use the core_selector method from CandidateCommitments to extract
// CoreSelector and ClaimQueueOffset.
let mut commitments = CandidateCommitments::default();
commitments.upward_messages = collation.upward_messages.clone();
let ump_signals = match commitments.ump_signals() {
Ok(signals) => signals,
Err(err) => {
gum::debug!(
target: LOG_TARGET,
?para_id,
"error processing UMP signals: {}",
err
);
return;
},
};
let (cs_index, cq_offset) = ump_signals
.core_selector()
.map(|(cs_index, cq_offset)| (cs_index.0 as usize, cq_offset.0 as usize))
.unwrap_or((i, 0));
// Identify the cores to build collations on using the given claim queue offset.
let cores_to_build_on = claim_queue
.iter_claims_at_depth(cq_offset)
.filter_map(|(core_idx, para_id)| {
(para_id == task_config.para_id).then_some(core_idx)
})
.collect::<Vec<_>>();
if cores_to_build_on.is_empty() {
gum::debug!(
target: LOG_TARGET,
?para_id,
"no core is assigned to para at depth {}",
cq_offset,
);
return;
}
let descriptor_core_index =
cores_to_build_on[cs_index % cores_to_build_on.len()];
// Ensure the core index has not been used before.
if used_cores.contains(&descriptor_core_index.0) {
gum::warn!(
target: LOG_TARGET,
?para_id,
"teyrchain repeatedly selected the same core index: {}",
descriptor_core_index.0,
);
return;
}
used_cores.insert(descriptor_core_index.0);
gum::trace!(
target: LOG_TARGET,
?para_id,
"selected core index: {}",
descriptor_core_index.0,
);
// Distribute the collation.
let parent_head = collation.head_data.clone();
if let Err(err) = construct_and_distribute_receipt(
PreparedCollation {
collation,
para_id,
relay_parent,
validation_data: validation_data.clone(),
validation_code_hash,
n_validators,
core_index: descriptor_core_index,
session_index,
},
&mut task_sender,
result_sender,
&metrics,
&transposed_claim_queue,
)
.await
{
gum::error!(
target: LOG_TARGET,
"Failed to construct and distribute collation: {}",
err
);
return;
}
// Chain the collations. All else stays the same as we build the chained
// collation on same relay parent.
validation_data.parent_head = parent_head;
}
}),
)?;
Ok(())
}
}
#[overseer::subsystem(CollationGeneration, error=SubsystemError, prefix=self::overseer)]
impl<Context> CollationGenerationSubsystem {
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = async move {
self.run(ctx).await;
Ok(())
}
.boxed();
SpawnedSubsystem { name: "collation-generation-subsystem", future }
}
}
#[derive(Clone)]
struct PerSessionInfo {
n_validators: usize,
}
struct SessionInfoCache(LruMap<SessionIndex, PerSessionInfo>);
impl SessionInfoCache {
fn new() -> Self {
Self(LruMap::new(ByLength::new(2)))
}
async fn get<Sender: SubsystemSender<RuntimeApiMessage>>(
&mut self,
relay_parent: Hash,
session_index: SessionIndex,
sender: &mut Sender,
) -> Result<PerSessionInfo> {
if let Some(info) = self.0.get(&session_index) {
return Ok(info.clone());
}
let n_validators =
request_validators(relay_parent, &mut sender.clone()).await.await??.len();
let info = PerSessionInfo { n_validators };
self.0.insert(session_index, info);
Ok(self.0.get(&session_index).expect("Just inserted").clone())
}
}
struct PreparedCollation {
collation: Collation,
para_id: ParaId,
relay_parent: Hash,
validation_data: PersistedValidationData,
validation_code_hash: ValidationCodeHash,
n_validators: usize,
core_index: CoreIndex,
session_index: SessionIndex,
}
/// Takes a prepared collation, along with its context, and produces a candidate receipt
/// which is distributed to validators.
async fn construct_and_distribute_receipt(
collation: PreparedCollation,
sender: &mut impl overseer::CollationGenerationSenderTrait,
result_sender: Option<oneshot::Sender<CollationSecondedSignal>>,
metrics: &Metrics,
transposed_claim_queue: &TransposedClaimQueue,
) -> Result<()> {
let PreparedCollation {
collation,
para_id,
relay_parent,
validation_data,
validation_code_hash,
n_validators,
core_index,
session_index,
} = collation;
let persisted_validation_data_hash = validation_data.hash();
let parent_head_data = validation_data.parent_head.clone();
let parent_head_data_hash = validation_data.parent_head.hash();
// Apply compression to the block data.
let pov = {
let pov = collation.proof_of_validity.into_compressed();
let encoded_size = pov.encoded_size();
// As long as `POV_BOMB_LIMIT` is at least `max_pov_size`, this ensures
// that honest collators never produce a PoV which is uncompressed.
//
// As such, honest collators never produce an uncompressed PoV which starts with
// a compression magic number, which would lead validators to reject the collation.
if encoded_size > validation_data.max_pov_size as usize {
return Err(Error::POVSizeExceeded(encoded_size, validation_data.max_pov_size as usize));
}
pov
};
let pov_hash = pov.hash();
let erasure_root = erasure_root(n_validators, validation_data, pov.clone())?;
let commitments = CandidateCommitments {
upward_messages: collation.upward_messages,
horizontal_messages: collation.horizontal_messages,
new_validation_code: collation.new_validation_code,
head_data: collation.head_data,
processed_downward_messages: collation.processed_downward_messages,
hrmp_watermark: collation.hrmp_watermark,
};
let receipt = {
let ccr = CommittedCandidateReceiptV2 {
descriptor: CandidateDescriptorV2::new(
para_id,
relay_parent,
core_index,
session_index,
persisted_validation_data_hash,
pov_hash,
erasure_root,
commitments.head_data.hash(),
validation_code_hash,
),
commitments: commitments.clone(),
};
ccr.parse_ump_signals(&transposed_claim_queue)
.map_err(Error::CandidateReceiptCheck)?;
ccr.to_plain()
};
gum::debug!(
target: LOG_TARGET,
candidate_hash = ?receipt.hash(),
?pov_hash,
?relay_parent,
para_id = %para_id,
?core_index,
"Candidate generated",
);
gum::trace!(
target: LOG_TARGET,
?commitments,
candidate_hash = ?receipt.hash(),
"Candidate commitments",
);
metrics.on_collation_generated();
sender
.send_message(CollatorProtocolMessage::DistributeCollation {
candidate_receipt: receipt,
parent_head_data_hash,
pov,
parent_head_data,
result_sender,
core_index,
})
.await;
Ok(())
}
fn erasure_root(
n_validators: usize,
persisted_validation: PersistedValidationData,
pov: PoV,
) -> Result<Hash> {
let available_data =
AvailableData { validation_data: persisted_validation, pov: Arc::new(pov) };
let chunks = pezkuwi_erasure_coding::obtain_chunks_v1(n_validators, &available_data)?;
Ok(pezkuwi_erasure_coding::branches(&chunks).root())
}
@@ -0,0 +1,75 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
#[derive(Clone)]
pub(crate) struct MetricsInner {
pub(crate) collations_generated_total: prometheus::Counter<prometheus::U64>,
pub(crate) new_activation: prometheus::Histogram,
pub(crate) submit_collation: prometheus::Histogram,
}
/// `CollationGenerationSubsystem` metrics.
#[derive(Default, Clone)]
pub struct Metrics(pub(crate) Option<MetricsInner>);
impl Metrics {
pub fn on_collation_generated(&self) {
if let Some(metrics) = &self.0 {
metrics.collations_generated_total.inc();
}
}
/// Provide a timer for new activations which updates on drop.
pub fn time_new_activation(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.new_activation.start_timer())
}
/// Provide a timer for submitting a collation which updates on drop.
pub fn time_submit_collation(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.submit_collation.start_timer())
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
collations_generated_total: prometheus::register(
prometheus::Counter::new(
"pezkuwi_teyrchain_collations_generated_total",
"Number of collations generated.",
)?,
registry,
)?,
new_activation: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_collation_generation_new_activations",
"Time spent within fn handle_new_activation",
))?,
registry,
)?,
submit_collation: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_collation_generation_submit_collation",
"Time spent preparing and submitting a collation to the network protocol",
))?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
@@ -0,0 +1,748 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use assert_matches::assert_matches;
use futures::{self, Future, StreamExt};
use pezkuwi_node_primitives::{
BlockData, Collation, CollationResult, CollatorFn, MaybeCompressedPoV, PoV,
};
use pezkuwi_node_subsystem::{
messages::{AllMessages, RuntimeApiMessage, RuntimeApiRequest},
ActivatedLeaf,
};
use pezkuwi_node_subsystem_test_helpers::TestSubsystemContextHandle;
use pezkuwi_node_subsystem_util::TimeoutExt;
use pezkuwi_primitives::{
CandidateDescriptorVersion, CandidateReceiptV2, ClaimQueueOffset, CollatorPair, CoreSelector,
PersistedValidationData, UMPSignal, UMP_SEPARATOR,
};
use pezkuwi_primitives_test_helpers::dummy_head_data;
use rstest::rstest;
use sp_core::Pair;
use sp_keyring::sr25519::Keyring as Sr25519Keyring;
use std::{
collections::{BTreeMap, VecDeque},
sync::Mutex,
};
type VirtualOverseer = TestSubsystemContextHandle<CollationGenerationMessage>;
fn test_harness<T: Future<Output = VirtualOverseer>>(test: impl FnOnce(VirtualOverseer) -> T) {
let pool = sp_core::testing::TaskExecutor::new();
let (context, virtual_overseer) =
pezkuwi_node_subsystem_test_helpers::make_subsystem_context(pool);
let subsystem = async move {
let subsystem = crate::CollationGenerationSubsystem::new(Metrics::default());
subsystem.run(context).await;
};
let test_fut = test(virtual_overseer);
futures::pin_mut!(test_fut);
futures::executor::block_on(futures::future::join(
async move {
let mut virtual_overseer = test_fut.await;
// Ensure we have handled all responses.
if let Some(msg) = virtual_overseer.rx.next().timeout(TIMEOUT).await {
panic!("Did not handle all responses: {:?}", msg);
}
// Conclude.
virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
},
subsystem,
));
}
fn test_collation() -> Collation {
Collation {
upward_messages: Default::default(),
horizontal_messages: Default::default(),
new_validation_code: None,
head_data: dummy_head_data(),
proof_of_validity: MaybeCompressedPoV::Raw(PoV { block_data: BlockData(Vec::new()) }),
processed_downward_messages: 0_u32,
hrmp_watermark: 0_u32.into(),
}
}
struct CoreSelectorData {
// The core selector index.
index: u8,
// The increment value for the core selector index. Normally 1, but can be set to 0 or another
// value for testing scenarios where a teyrchain repeatedly selects the same core index.
increment_index_by: u8,
// The claim queue offset.
cq_offset: u8,
}
impl CoreSelectorData {
fn new(index: u8, increment_index_by: u8, cq_offset: u8) -> Self {
Self { index, increment_index_by, cq_offset }
}
}
struct State {
core_selector_data: Option<CoreSelectorData>,
}
impl State {
fn new(core_selector_data: Option<CoreSelectorData>) -> Self {
Self { core_selector_data }
}
}
struct TestCollator {
state: Arc<Mutex<State>>,
}
impl TestCollator {
fn new(core_selector_data: Option<CoreSelectorData>) -> Self {
Self { state: Arc::new(Mutex::new(State::new(core_selector_data))) }
}
pub fn create_collation_function(&self) -> CollatorFn {
let state = Arc::clone(&self.state);
Box::new(move |_relay_parent: Hash, _validation_data: &PersistedValidationData| {
let mut collation = test_collation();
let mut state_guard = state.lock().unwrap();
if let Some(core_selector_data) = &mut state_guard.core_selector_data {
collation.upward_messages.force_push(UMP_SEPARATOR);
collation.upward_messages.force_push(
UMPSignal::SelectCore(
CoreSelector(core_selector_data.index),
ClaimQueueOffset(core_selector_data.cq_offset),
)
.encode(),
);
core_selector_data.index += core_selector_data.increment_index_by;
}
async move { Some(CollationResult { collation, result_sender: None }) }.boxed()
})
}
}
const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(2000);
async fn overseer_recv(overseer: &mut VirtualOverseer) -> AllMessages {
overseer
.recv()
.timeout(TIMEOUT)
.await
.expect(&format!("{:?} is long enough to receive messages", TIMEOUT))
}
fn test_config<Id: Into<ParaId>>(
para_id: Id,
core_selector_data: Option<CoreSelectorData>,
) -> CollationGenerationConfig {
let test_collator = TestCollator::new(core_selector_data);
CollationGenerationConfig {
key: CollatorPair::generate().0,
collator: Some(test_collator.create_collation_function()),
para_id: para_id.into(),
}
}
fn test_config_no_collator<Id: Into<ParaId>>(para_id: Id) -> CollationGenerationConfig {
CollationGenerationConfig {
key: CollatorPair::generate().0,
collator: None,
para_id: para_id.into(),
}
}
#[test]
fn submit_collation_is_no_op_before_initialization() {
test_harness(|mut virtual_overseer| async move {
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
relay_parent: Hash::repeat_byte(0),
collation: test_collation(),
parent_head: vec![1, 2, 3].into(),
validation_code_hash: Hash::repeat_byte(1).into(),
result_sender: None,
core_index: CoreIndex(0),
}),
})
.await;
virtual_overseer
});
}
#[test]
fn submit_collation_leads_to_distribution() {
let relay_parent = Hash::repeat_byte(0);
let validation_code_hash = ValidationCodeHash::from(Hash::repeat_byte(42));
let parent_head = dummy_head_data();
let para_id = ParaId::from(5);
let expected_pvd = PersistedValidationData {
parent_head: parent_head.clone(),
relay_parent_number: 10,
relay_parent_storage_root: Hash::repeat_byte(1),
max_pov_size: 1024,
};
test_harness(|mut virtual_overseer| async move {
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::Initialize(test_config_no_collator(para_id)),
})
.await;
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
relay_parent,
collation: test_collation(),
parent_head: dummy_head_data(),
validation_code_hash,
result_sender: None,
core_index: CoreIndex(0),
}),
})
.await;
helpers::handle_runtime_calls_on_submit_collation(
&mut virtual_overseer,
relay_parent,
para_id,
expected_pvd.clone(),
[(CoreIndex(0), VecDeque::from([para_id]))].into(),
)
.await;
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::CollatorProtocol(CollatorProtocolMessage::DistributeCollation {
candidate_receipt,
parent_head_data_hash,
..
}) => {
let CandidateReceiptV2 { descriptor, .. } = candidate_receipt;
assert_eq!(parent_head_data_hash, parent_head.hash());
assert_eq!(descriptor.persisted_validation_data_hash(), expected_pvd.hash());
assert_eq!(descriptor.para_head(), dummy_head_data().hash());
assert_eq!(descriptor.validation_code_hash(), validation_code_hash);
}
);
virtual_overseer
});
}
#[test]
fn distribute_collation_only_for_assigned_para_id_at_offset_0() {
let activated_hash: Hash = [1; 32].into();
let para_id = ParaId::from(5);
let claim_queue = (0..=5)
.into_iter()
// Set all cores assigned to para_id 5 at the second and third depths. This shouldn't
// matter.
.map(|idx| (CoreIndex(idx), VecDeque::from([ParaId::from(idx), para_id, para_id])))
.collect::<BTreeMap<_, _>>();
test_harness(|mut virtual_overseer| async move {
helpers::initialize_collator(&mut virtual_overseer, para_id, None).await;
helpers::activate_new_head(&mut virtual_overseer, activated_hash).await;
helpers::handle_runtime_calls_on_new_head_activation(
&mut virtual_overseer,
activated_hash,
claim_queue,
)
.await;
helpers::handle_cores_processing_for_a_leaf(
&mut virtual_overseer,
activated_hash,
para_id,
vec![5], // Only core 5 is assigned to paraid 5.
)
.await;
virtual_overseer
});
}
// There are variable number of cores assigned to the paraid.
// On new head activation `CollationGeneration` should produce and distribute the right number of
// new collations with proper assumption about the para candidate chain availability at next block.
#[rstest]
#[case(0)]
#[case(1)]
#[case(2)]
#[case(3)]
fn distribute_collation_with_elastic_scaling(#[case] total_cores: u32) {
let activated_hash: Hash = [1; 32].into();
let para_id = ParaId::from(5);
let claim_queue = (0..total_cores)
.into_iter()
.map(|idx| (CoreIndex(idx), VecDeque::from([para_id])))
.collect::<BTreeMap<_, _>>();
test_harness(|mut virtual_overseer| async move {
helpers::initialize_collator(&mut virtual_overseer, para_id, None).await;
helpers::activate_new_head(&mut virtual_overseer, activated_hash).await;
helpers::handle_runtime_calls_on_new_head_activation(
&mut virtual_overseer,
activated_hash,
claim_queue,
)
.await;
helpers::handle_cores_processing_for_a_leaf(
&mut virtual_overseer,
activated_hash,
para_id,
(0..total_cores).collect(),
)
.await;
virtual_overseer
});
}
// Tests when submission core indexes need to be selected using the core selectors provided in the
// UMP signals. The core selector index is an increasing number that can start with a non-negative
// value (even greater than the core index), but the collation generation protocol uses the
// remainder to select the core. UMP signals may also contain a claim queue offset, based on which
// we need to select the assigned core indexes for the para from that offset in the claim queue.
#[rstest]
#[case(1, 0, 0)]
#[case(2, 0, 1)]
fn distribute_collation_with_core_selectors(
#[case] total_cores: u32,
// The core selector index that will be obtained from the first collation.
#[case] init_cs_index: u8,
// Claim queue offset where the assigned cores will be stored.
#[case] cq_offset: u8,
) {
let activated_hash: Hash = [1; 32].into();
let para_id = ParaId::from(5);
let other_para_id = ParaId::from(10);
let claim_queue = (0..total_cores)
.into_iter()
.map(|idx| {
// Set all cores assigned to para_id 5 at the cq_offset depth.
let mut vec = VecDeque::from(vec![other_para_id; cq_offset as usize]);
vec.push_back(para_id);
(CoreIndex(idx), vec)
})
.collect::<BTreeMap<_, _>>();
test_harness(|mut virtual_overseer| async move {
helpers::initialize_collator(
&mut virtual_overseer,
para_id,
Some(CoreSelectorData::new(init_cs_index, 1, cq_offset)),
)
.await;
helpers::activate_new_head(&mut virtual_overseer, activated_hash).await;
helpers::handle_runtime_calls_on_new_head_activation(
&mut virtual_overseer,
activated_hash,
claim_queue,
)
.await;
let mut cores_assigned = (0..total_cores).collect::<Vec<_>>();
if total_cores > 1 && init_cs_index > 0 {
// We need to rotate the list of cores because the first core selector index was
// non-zero, which should change the sequence of submissions. However, collations should
// still be submitted on all cores.
cores_assigned.rotate_left((init_cs_index as u32 % total_cores) as usize);
}
helpers::handle_cores_processing_for_a_leaf(
&mut virtual_overseer,
activated_hash,
para_id,
cores_assigned,
)
.await;
virtual_overseer
});
}
// Tests the behavior when a teyrchain repeatedly selects the same core index.
// Ensures that the system handles this behavior correctly while maintaining expected functionality.
#[rstest]
#[case(3, 0, vec![0])]
#[case(3, 1, vec![0, 1, 2])]
#[case(3, 2, vec![0, 2, 1])]
#[case(3, 3, vec![0])]
#[case(3, 4, vec![0, 1, 2])]
fn distribute_collation_with_repeated_core_selector_index(
#[case] total_cores: u32,
#[case] increment_cs_index_by: u8,
#[case] expected_selected_cores: Vec<u32>,
) {
let activated_hash: Hash = [1; 32].into();
let para_id = ParaId::from(5);
let claim_queue = (0..total_cores)
.into_iter()
.map(|idx| (CoreIndex(idx), VecDeque::from([para_id])))
.collect::<BTreeMap<_, _>>();
test_harness(|mut virtual_overseer| async move {
helpers::initialize_collator(
&mut virtual_overseer,
para_id,
Some(CoreSelectorData::new(0, increment_cs_index_by, 0)),
)
.await;
helpers::activate_new_head(&mut virtual_overseer, activated_hash).await;
helpers::handle_runtime_calls_on_new_head_activation(
&mut virtual_overseer,
activated_hash,
claim_queue,
)
.await;
helpers::handle_cores_processing_for_a_leaf(
&mut virtual_overseer,
activated_hash,
para_id,
expected_selected_cores,
)
.await;
virtual_overseer
});
}
#[test]
fn v2_receipts_failed_core_index_check() {
let relay_parent = Hash::repeat_byte(0);
let validation_code_hash = ValidationCodeHash::from(Hash::repeat_byte(42));
let parent_head = dummy_head_data();
let para_id = ParaId::from(5);
let expected_pvd = PersistedValidationData {
parent_head: parent_head.clone(),
relay_parent_number: 10,
relay_parent_storage_root: Hash::repeat_byte(1),
max_pov_size: 1024,
};
test_harness(|mut virtual_overseer| async move {
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::Initialize(test_config_no_collator(para_id)),
})
.await;
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
relay_parent,
collation: test_collation(),
parent_head: dummy_head_data(),
validation_code_hash,
result_sender: None,
core_index: CoreIndex(0),
}),
})
.await;
helpers::handle_runtime_calls_on_submit_collation(
&mut virtual_overseer,
relay_parent,
para_id,
expected_pvd.clone(),
// Core index commitment is on core 0 but don't add any assignment for core 0.
[(CoreIndex(1), [para_id].into_iter().collect())].into_iter().collect(),
)
.await;
// No collation is distributed.
virtual_overseer
});
}
#[test]
// Verify that an ApprovedPeer UMP signal does not break the subsystem (DistributeCollation is
// sent), assuming CandidateReceiptV2 node feature is enabled.
fn approved_peer_signal() {
let relay_parent = Hash::repeat_byte(0);
let validation_code_hash = ValidationCodeHash::from(Hash::repeat_byte(42));
let parent_head = dummy_head_data();
let para_id = ParaId::from(5);
let expected_pvd = PersistedValidationData {
parent_head: parent_head.clone(),
relay_parent_number: 10,
relay_parent_storage_root: Hash::repeat_byte(1),
max_pov_size: 1024,
};
test_harness(|mut virtual_overseer| async move {
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::Initialize(test_config_no_collator(para_id)),
})
.await;
let mut collation = test_collation();
collation.upward_messages.force_push(UMP_SEPARATOR);
collation
.upward_messages
.force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3, 4, 5].try_into().unwrap()).encode());
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
relay_parent,
collation,
parent_head: dummy_head_data(),
validation_code_hash,
result_sender: None,
core_index: CoreIndex(0),
}),
})
.await;
helpers::handle_runtime_calls_on_submit_collation(
&mut virtual_overseer,
relay_parent,
para_id,
expected_pvd.clone(),
[(CoreIndex(0), [para_id].into_iter().collect())].into_iter().collect(),
)
.await;
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::CollatorProtocol(CollatorProtocolMessage::DistributeCollation {
candidate_receipt,
parent_head_data_hash,
..
}) => {
let CandidateReceiptV2 { descriptor, .. } = candidate_receipt;
assert_eq!(parent_head_data_hash, parent_head.hash());
assert_eq!(descriptor.persisted_validation_data_hash(), expected_pvd.hash());
assert_eq!(descriptor.para_head(), dummy_head_data().hash());
assert_eq!(descriptor.validation_code_hash(), validation_code_hash);
assert_eq!(descriptor.version(), CandidateDescriptorVersion::V2);
}
);
virtual_overseer
});
}
mod helpers {
use super::*;
use std::collections::{BTreeMap, VecDeque};
// Sends `Initialize` with a collator config
pub async fn initialize_collator(
virtual_overseer: &mut VirtualOverseer,
para_id: ParaId,
core_selector_data: Option<CoreSelectorData>,
) {
virtual_overseer
.send(FromOrchestra::Communication {
msg: CollationGenerationMessage::Initialize(test_config(
para_id,
core_selector_data,
)),
})
.await;
}
// Sends `ActiveLeaves` for a single leaf with the specified hash. Block number is hardcoded.
pub async fn activate_new_head(virtual_overseer: &mut VirtualOverseer, activated_hash: Hash) {
virtual_overseer
.send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate {
activated: Some(ActivatedLeaf {
hash: activated_hash,
number: 10,
unpin_handle: pezkuwi_node_subsystem_test_helpers::mock::dummy_unpin_handle(
activated_hash,
),
}),
..Default::default()
})))
.await;
}
// Handle all runtime calls performed in `handle_new_activation`.
pub async fn handle_runtime_calls_on_new_head_activation(
virtual_overseer: &mut VirtualOverseer,
activated_hash: Hash,
claim_queue: BTreeMap<CoreIndex, VecDeque<ParaId>>,
) {
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(hash, RuntimeApiRequest::SessionIndexForChild(tx))) => {
assert_eq!(hash, activated_hash);
tx.send(Ok(1)).unwrap();
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(hash, RuntimeApiRequest::Validators(tx))) => {
assert_eq!(hash, activated_hash);
tx.send(Ok(vec![
Sr25519Keyring::Alice.public().into(),
Sr25519Keyring::Bob.public().into(),
Sr25519Keyring::Charlie.public().into(),
])).unwrap();
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(hash, RuntimeApiRequest::ClaimQueue(tx))) => {
assert_eq!(hash, activated_hash);
tx.send(Ok(claim_queue)).unwrap();
}
);
}
// Handles all runtime requests performed in `handle_new_activation` for the case when a
// collation should be prepared for the new leaf
pub async fn handle_cores_processing_for_a_leaf(
virtual_overseer: &mut VirtualOverseer,
activated_hash: Hash,
para_id: ParaId,
cores_assigned: Vec<u32>,
) {
// Expect no messages if no cores is assigned to the para
if cores_assigned.is_empty() {
return;
}
// Some hardcoded data - if needed, extract to parameters
let validation_code_hash = ValidationCodeHash::from(Hash::repeat_byte(42));
let parent_head = dummy_head_data();
let pvd = PersistedValidationData {
parent_head: parent_head.clone(),
relay_parent_number: 10,
relay_parent_storage_root: Hash::repeat_byte(1),
max_pov_size: 1024,
};
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(hash, RuntimeApiRequest::PersistedValidationData(id, a, tx))) => {
assert_eq!(hash, activated_hash);
assert_eq!(id, para_id);
assert_eq!(a, OccupiedCoreAssumption::Included);
let _ = tx.send(Ok(Some(pvd.clone())));
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
hash,
RuntimeApiRequest::ValidationCodeHash(
id,
assumption,
tx,
),
)) => {
assert_eq!(hash, activated_hash);
assert_eq!(id, para_id);
assert_eq!(assumption, OccupiedCoreAssumption::Included);
let _ = tx.send(Ok(Some(validation_code_hash)));
}
);
for core in cores_assigned {
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::CollatorProtocol(CollatorProtocolMessage::DistributeCollation{
candidate_receipt,
parent_head_data_hash,
core_index,
..
}) => {
assert_eq!(CoreIndex(core), core_index);
assert_eq!(parent_head_data_hash, parent_head.hash());
assert_eq!(candidate_receipt.descriptor().persisted_validation_data_hash(), pvd.hash());
assert_eq!(candidate_receipt.descriptor().para_head(), dummy_head_data().hash());
assert_eq!(candidate_receipt.descriptor().validation_code_hash(), validation_code_hash);
}
);
}
}
// Handles all runtime requests performed in `handle_submit_collation`
pub async fn handle_runtime_calls_on_submit_collation(
virtual_overseer: &mut VirtualOverseer,
relay_parent: Hash,
para_id: ParaId,
expected_pvd: PersistedValidationData,
claim_queue: BTreeMap<CoreIndex, VecDeque<ParaId>>,
) {
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(rp, RuntimeApiRequest::PersistedValidationData(id, a, tx))) => {
assert_eq!(rp, relay_parent);
assert_eq!(id, para_id);
assert_eq!(a, OccupiedCoreAssumption::TimedOut);
tx.send(Ok(Some(expected_pvd))).unwrap();
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
rp,
RuntimeApiRequest::ClaimQueue(tx),
)) => {
assert_eq!(rp, relay_parent);
tx.send(Ok(claim_queue)).unwrap();
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(rp, RuntimeApiRequest::SessionIndexForChild(tx))) => {
assert_eq!(rp, relay_parent);
tx.send(Ok(1)).unwrap();
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(rp, RuntimeApiRequest::Validators(tx))) => {
assert_eq!(rp, relay_parent);
tx.send(Ok(vec![
Sr25519Keyring::Alice.public().into(),
Sr25519Keyring::Bob.public().into(),
Sr25519Keyring::Charlie.public().into(),
])).unwrap();
}
);
}
}
+1
View File
@@ -0,0 +1 @@
This folder contains core subsystems, each with their own crate.
@@ -0,0 +1,62 @@
[package]
name = "pezkuwi-node-core-approval-voting-parallel"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Approval Voting Subsystem running approval work in parallel"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
futures = { workspace = true }
gum = { workspace = true }
itertools = { workspace = true }
pezkuwi-approval-distribution = { workspace = true, default-features = true }
pezkuwi-node-core-approval-voting = { workspace = true, default-features = true }
pezkuwi-node-metrics = { workspace = true, default-features = true }
pezkuwi-node-network-protocol = { workspace = true, default-features = true }
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 }
sc-keystore = { workspace = true, default-features = false }
sp-consensus = { workspace = true, default-features = false }
rand = { workspace = true }
rand_core = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
kvdb-memorydb = { workspace = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true, default-features = true }
schnorrkel = { workspace = true, default-features = true }
sp-consensus-babe = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-tracing = { workspace = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-approval-distribution/runtime-benchmarks",
"pezkuwi-node-core-approval-voting/runtime-benchmarks",
"pezkuwi-node-metrics/runtime-benchmarks",
"pezkuwi-node-network-protocol/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"sp-consensus-babe/runtime-benchmarks",
"sp-consensus/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
]
@@ -0,0 +1,878 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The Approval Voting Parallel Subsystem.
//!
//! This subsystem is responsible for orchestrating the work done by
//! approval-voting and approval-distribution subsystem, so they can
//! do their work in parallel, rather than serially, when they are run
//! as independent subsystems.
use itertools::Itertools;
use metrics::{Meters, MetricsWatcher};
use pezkuwi_node_core_approval_voting::{Config, RealAssignmentCriteria};
use pezkuwi_node_metrics::metered::{
self, channel, unbounded, MeteredReceiver, MeteredSender, UnboundedMeteredReceiver,
UnboundedMeteredSender,
};
use pezkuwi_node_primitives::{
approval::time::{Clock, SystemClock},
DISPUTE_WINDOW,
};
use pezkuwi_node_subsystem::{
messages::{ApprovalDistributionMessage, ApprovalVotingMessage, ApprovalVotingParallelMessage},
overseer, FromOrchestra, SpawnedSubsystem, SubsystemError, SubsystemResult,
};
use pezkuwi_node_subsystem_util::{
self,
database::Database,
runtime::{Config as RuntimeInfoConfig, RuntimeInfo},
};
use pezkuwi_overseer::{OverseerSignal, Priority, SubsystemSender, TimeoutExt};
use pezkuwi_primitives::{CandidateIndex, Hash, ValidatorIndex, ValidatorSignature};
use rand::SeedableRng;
use sc_keystore::LocalKeystore;
use sp_consensus::SyncOracle;
use futures::{channel::oneshot, prelude::*, StreamExt};
pub use metrics::Metrics;
use pezkuwi_node_core_approval_voting::{
approval_db::common::Config as DatabaseConfig, ApprovalVotingWorkProvider,
};
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
sync::Arc,
time::Duration,
};
use stream::{select_with_strategy, PollNext, SelectWithStrategy};
pub mod metrics;
#[cfg(test)]
mod tests;
pub(crate) const LOG_TARGET: &str = "teyrchain::approval-voting-parallel";
// Value rather arbitrarily: Should not be hit in practice, it exists to more easily diagnose dead
// lock issues for example.
const WAIT_FOR_SIGS_GATHER_TIMEOUT: Duration = Duration::from_millis(2000);
/// The number of workers used for running the approval-distribution logic.
pub const APPROVAL_DISTRIBUTION_WORKER_COUNT: usize = 4;
/// The default channel size for the workers, can be overridden by the user through
/// `overseer_channel_capacity_override`
pub const DEFAULT_WORKERS_CHANNEL_SIZE: usize = 64000 / APPROVAL_DISTRIBUTION_WORKER_COUNT;
fn prio_right<'a>(_val: &'a mut ()) -> PollNext {
PollNext::Right
}
/// The approval voting parallel subsystem.
pub struct ApprovalVotingParallelSubsystem {
/// `LocalKeystore` is needed for assignment keys, but not necessarily approval keys.
///
/// We do a lot of VRF signing and need the keys to have low latency.
keystore: Arc<LocalKeystore>,
db_config: DatabaseConfig,
slot_duration_millis: u64,
db: Arc<dyn Database>,
sync_oracle: Box<dyn SyncOracle + Send>,
metrics: Metrics,
spawner: Arc<dyn overseer::gen::Spawner + 'static>,
clock: Arc<dyn Clock + Send + Sync>,
overseer_message_channel_capacity_override: Option<usize>,
}
impl ApprovalVotingParallelSubsystem {
/// Create a new approval voting subsystem with the given keystore, config, and database.
pub fn with_config(
config: Config,
db: Arc<dyn Database>,
keystore: Arc<LocalKeystore>,
sync_oracle: Box<dyn SyncOracle + Send>,
metrics: Metrics,
spawner: impl overseer::gen::Spawner + 'static + Clone,
overseer_message_channel_capacity_override: Option<usize>,
) -> Self {
ApprovalVotingParallelSubsystem::with_config_and_clock(
config,
db,
keystore,
sync_oracle,
metrics,
Arc::new(SystemClock {}),
spawner,
overseer_message_channel_capacity_override,
)
}
/// Create a new approval voting subsystem with the given keystore, config, clock, and database.
pub fn with_config_and_clock(
config: Config,
db: Arc<dyn Database>,
keystore: Arc<LocalKeystore>,
sync_oracle: Box<dyn SyncOracle + Send>,
metrics: Metrics,
clock: Arc<dyn Clock + Send + Sync>,
spawner: impl overseer::gen::Spawner + 'static,
overseer_message_channel_capacity_override: Option<usize>,
) -> Self {
ApprovalVotingParallelSubsystem {
keystore,
slot_duration_millis: config.slot_duration_millis,
db,
db_config: DatabaseConfig { col_approval_data: config.col_approval_data },
sync_oracle,
metrics,
spawner: Arc::new(spawner),
clock,
overseer_message_channel_capacity_override,
}
}
/// The size of the channel used for the workers.
fn workers_channel_size(&self) -> usize {
self.overseer_message_channel_capacity_override
.unwrap_or(DEFAULT_WORKERS_CHANNEL_SIZE)
}
}
#[overseer::subsystem(ApprovalVotingParallel, error = SubsystemError, prefix = self::overseer)]
impl<Context: Send> ApprovalVotingParallelSubsystem {
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = run::<Context>(ctx, self)
.map_err(|e| SubsystemError::with_origin("approval-voting-parallel", e))
.boxed();
SpawnedSubsystem { name: "approval-voting-parallel-subsystem", future }
}
}
// It starts worker for the approval voting subsystem and the `APPROVAL_DISTRIBUTION_WORKER_COUNT`
// workers for the approval distribution subsystem.
//
// It returns handles that can be used to send messages to the workers.
#[overseer::contextbounds(ApprovalVotingParallel, prefix = self::overseer)]
async fn start_workers<Context>(
ctx: &mut Context,
subsystem: ApprovalVotingParallelSubsystem,
metrics_watcher: &mut MetricsWatcher,
) -> SubsystemResult<(ToWorker<ApprovalVotingMessage>, Vec<ToWorker<ApprovalDistributionMessage>>)>
where
{
gum::info!(target: LOG_TARGET, "Starting approval distribution workers");
// Build approval voting handles.
let (to_approval_voting_worker, approval_voting_work_provider) = build_worker_handles(
"approval-voting-parallel-db".into(),
subsystem.workers_channel_size(),
metrics_watcher,
prio_right,
);
let mut to_approval_distribution_workers = Vec::new();
let slot_duration_millis = subsystem.slot_duration_millis;
for i in 0..APPROVAL_DISTRIBUTION_WORKER_COUNT {
let mut network_sender = ctx.sender().clone();
let mut runtime_api_sender = ctx.sender().clone();
let mut approval_distribution_to_approval_voting = to_approval_voting_worker.clone();
let approval_distr_instance =
pezkuwi_approval_distribution::ApprovalDistribution::new_with_clock(
subsystem.metrics.approval_distribution_metrics(),
subsystem.slot_duration_millis,
subsystem.clock.clone(),
Arc::new(RealAssignmentCriteria {}),
);
let task_name = format!("approval-voting-parallel-{}", i);
let (to_approval_distribution_worker, mut approval_distribution_work_provider) =
build_worker_handles(
task_name.clone(),
subsystem.workers_channel_size(),
metrics_watcher,
prio_right,
);
metrics_watcher.watch(task_name.clone(), to_approval_distribution_worker.meter());
subsystem.spawner.spawn_blocking(
task_name.leak(),
Some("approval-voting-parallel"),
Box::pin(async move {
let mut state =
pezkuwi_approval_distribution::State::with_config(slot_duration_millis);
let mut rng = rand::rngs::StdRng::from_entropy();
let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig {
keystore: None,
session_cache_lru_size: DISPUTE_WINDOW.get(),
});
loop {
let message = match approval_distribution_work_provider.next().await {
Some(message) => message,
None => {
gum::info!(
target: LOG_TARGET,
"Approval distribution stream finished, most likely shutting down",
);
break;
},
};
if approval_distr_instance
.handle_from_orchestra(
message,
&mut approval_distribution_to_approval_voting,
&mut network_sender,
&mut runtime_api_sender,
&mut state,
&mut rng,
&mut session_info_provider,
)
.await
{
gum::info!(
target: LOG_TARGET,
"Approval distribution worker {}, exiting because of shutdown", i
);
};
}
}),
);
to_approval_distribution_workers.push(to_approval_distribution_worker);
}
gum::info!(target: LOG_TARGET, "Starting approval voting workers");
let sender = ctx.sender().clone();
let to_approval_distribution = ApprovalVotingToApprovalDistribution(sender.clone());
pezkuwi_node_core_approval_voting::start_approval_worker(
approval_voting_work_provider,
sender.clone(),
to_approval_distribution,
pezkuwi_node_core_approval_voting::Config {
slot_duration_millis: subsystem.slot_duration_millis,
col_approval_data: subsystem.db_config.col_approval_data,
},
subsystem.db.clone(),
subsystem.keystore.clone(),
subsystem.sync_oracle,
subsystem.metrics.approval_voting_metrics(),
subsystem.spawner.clone(),
"approval-voting-parallel-db",
"approval-voting-parallel",
subsystem.clock.clone(),
)
.await?;
Ok((to_approval_voting_worker, to_approval_distribution_workers))
}
// The main run function of the approval parallel voting subsystem.
#[overseer::contextbounds(ApprovalVotingParallel, prefix = self::overseer)]
async fn run<Context>(
mut ctx: Context,
subsystem: ApprovalVotingParallelSubsystem,
) -> SubsystemResult<()> {
let mut metrics_watcher = MetricsWatcher::new(subsystem.metrics.clone());
gum::info!(
target: LOG_TARGET,
"Starting workers"
);
let (to_approval_voting_worker, to_approval_distribution_workers) =
start_workers(&mut ctx, subsystem, &mut metrics_watcher).await?;
gum::info!(
target: LOG_TARGET,
"Starting main subsystem loop"
);
run_main_loop(ctx, to_approval_voting_worker, to_approval_distribution_workers, metrics_watcher)
.await
}
// Main loop of the subsystem, it shouldn't include any logic just dispatching of messages to
// the workers.
//
// It listens for messages from the overseer and dispatches them to the workers.
#[overseer::contextbounds(ApprovalVotingParallel, prefix = self::overseer)]
async fn run_main_loop<Context>(
mut ctx: Context,
mut to_approval_voting_worker: ToWorker<ApprovalVotingMessage>,
mut to_approval_distribution_workers: Vec<ToWorker<ApprovalDistributionMessage>>,
metrics_watcher: MetricsWatcher,
) -> SubsystemResult<()> {
loop {
futures::select! {
next_msg = ctx.recv().fuse() => {
let next_msg = match next_msg {
Ok(msg) => msg,
Err(err) => {
gum::info!(target: LOG_TARGET, ?err, "Approval voting parallel subsystem received an error");
return Err(err);
}
};
match next_msg {
FromOrchestra::Signal(msg) => {
if matches!(msg, OverseerSignal::ActiveLeaves(_)) {
metrics_watcher.collect_metrics();
}
for worker in to_approval_distribution_workers.iter_mut() {
worker
.send_signal(msg.clone()).await?;
}
to_approval_voting_worker.send_signal(msg.clone()).await?;
if matches!(msg, OverseerSignal::Conclude) {
break;
}
},
FromOrchestra::Communication { msg } => match msg {
// The message the approval voting subsystem would've handled.
ApprovalVotingParallelMessage::ApprovedAncestor(_, _,_) |
ApprovalVotingParallelMessage::GetApprovalSignaturesForCandidate(_, _) => {
to_approval_voting_worker.send_message_with_priority::<overseer::HighPriority>(
msg.try_into().expect(
"Message is one of ApprovedAncestor, GetApprovalSignaturesForCandidate
and that can be safely converted to ApprovalVotingMessage; qed"
)
).await;
},
// Now the message the approval distribution subsystem would've handled and need to
// be forwarded to the workers.
ApprovalVotingParallelMessage::NewBlocks(msg) => {
for worker in to_approval_distribution_workers.iter_mut() {
worker
.send_message(
ApprovalDistributionMessage::NewBlocks(msg.clone()),
)
.await;
}
},
ApprovalVotingParallelMessage::DistributeAssignment(assignment, claimed) => {
let worker = assigned_worker_for_validator(assignment.validator, &mut to_approval_distribution_workers);
worker
.send_message(
ApprovalDistributionMessage::DistributeAssignment(assignment, claimed)
)
.await;
},
ApprovalVotingParallelMessage::DistributeApproval(vote) => {
let worker = assigned_worker_for_validator(vote.validator, &mut to_approval_distribution_workers);
worker
.send_message(
ApprovalDistributionMessage::DistributeApproval(vote)
).await;
},
ApprovalVotingParallelMessage::NetworkBridgeUpdate(msg) => {
if let pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerMessage(
peer_id,
msg,
) = msg
{
let (all_msgs_from_same_validator, messages_split_by_validator) = validator_index_for_msg(msg);
for (validator_index, msg) in all_msgs_from_same_validator.into_iter().chain(messages_split_by_validator.into_iter().flatten()) {
let worker = assigned_worker_for_validator(validator_index, &mut to_approval_distribution_workers);
worker
.send_message(
ApprovalDistributionMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerMessage(
peer_id, msg,
),
),
).await;
}
} else {
for worker in to_approval_distribution_workers.iter_mut() {
worker
.send_message_with_priority::<overseer::HighPriority>(
ApprovalDistributionMessage::NetworkBridgeUpdate(msg.clone()),
).await;
}
}
},
ApprovalVotingParallelMessage::GetApprovalSignatures(indices, tx) => {
handle_get_approval_signatures(&mut ctx, &mut to_approval_distribution_workers, indices, tx).await;
},
ApprovalVotingParallelMessage::ApprovalCheckingLagUpdate(lag) => {
for worker in to_approval_distribution_workers.iter_mut() {
worker
.send_message(
ApprovalDistributionMessage::ApprovalCheckingLagUpdate(lag)
).await;
}
},
},
};
},
};
}
Ok(())
}
// It sends a message to all approval workers to get the approval signatures for the requested
// candidates and then merges them all together and sends them back to the requester.
#[overseer::contextbounds(ApprovalVotingParallel, prefix = self::overseer)]
async fn handle_get_approval_signatures<Context>(
ctx: &mut Context,
to_approval_distribution_workers: &mut Vec<ToWorker<ApprovalDistributionMessage>>,
requested_candidates: HashSet<(Hash, CandidateIndex)>,
result_channel: oneshot::Sender<
HashMap<ValidatorIndex, (Hash, Vec<CandidateIndex>, ValidatorSignature)>,
>,
) {
let mut sigs = HashMap::new();
let mut signatures_channels = Vec::new();
for worker in to_approval_distribution_workers.iter_mut() {
let (tx, rx) = oneshot::channel();
worker.send_unbounded_message(ApprovalDistributionMessage::GetApprovalSignatures(
requested_candidates.clone(),
tx,
));
signatures_channels.push(rx);
}
let gather_signatures = async move {
let Some(results) = futures::future::join_all(signatures_channels)
.timeout(WAIT_FOR_SIGS_GATHER_TIMEOUT)
.await
else {
gum::warn!(
target: LOG_TARGET,
"Waiting for approval signatures timed out - dead lock?"
);
return;
};
for result in results {
let worker_sigs = match result {
Ok(sigs) => sigs,
Err(_) => {
gum::error!(
target: LOG_TARGET,
"Getting approval signatures failed, oneshot got closed"
);
continue;
},
};
sigs.extend(worker_sigs);
}
if let Err(_) = result_channel.send(sigs) {
gum::debug!(
target: LOG_TARGET,
"Sending back approval signatures failed, oneshot got closed"
);
}
};
if let Err(err) = ctx.spawn("approval-voting-gather-signatures", Box::pin(gather_signatures)) {
gum::warn!(target: LOG_TARGET, "Failed to spawn gather signatures task: {:?}", err);
}
}
// Returns the worker that should receive the message for the given validator.
fn assigned_worker_for_validator(
validator: ValidatorIndex,
to_approval_distribution_workers: &mut Vec<ToWorker<ApprovalDistributionMessage>>,
) -> &mut ToWorker<ApprovalDistributionMessage> {
let worker_index = validator.0 as usize % to_approval_distribution_workers.len();
to_approval_distribution_workers
.get_mut(worker_index)
.expect("Worker index is obtained modulo len; qed")
}
// Returns the validators that initially created this assignments/votes, the validator index
// is later used to decide which approval-distribution worker should receive the message.
//
// Because this is on the hot path and we don't want to be unnecessarily slow, it contains two logic
// paths. The ultra fast path where all messages have the same validator index and we don't do
// any cloning or allocation and the path where we need to split the messages into multiple
// messages, because they have different validator indices, where we do need to clone and allocate.
// In practice most of the message will fall on the ultra fast path.
fn validator_index_for_msg(
msg: pezkuwi_node_network_protocol::ApprovalDistributionMessage,
) -> (
Option<(ValidatorIndex, pezkuwi_node_network_protocol::ApprovalDistributionMessage)>,
Option<Vec<(ValidatorIndex, pezkuwi_node_network_protocol::ApprovalDistributionMessage)>>,
) {
match msg {
pezkuwi_node_network_protocol::ValidationProtocols::V3(ref message) => match message {
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Assignments(msgs) =>
if let Ok(validator) = msgs.iter().map(|(msg, _)| msg.validator).all_equal_value() {
(Some((validator, msg)), None)
} else {
let split = msgs
.iter()
.map(|(msg, claimed_candidates)| {
(
msg.validator,
pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Assignments(
vec![(msg.clone(), claimed_candidates.clone())]
),
),
)
})
.collect_vec();
(None, Some(split))
},
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(msgs) =>
if let Ok(validator) = msgs.iter().map(|msg| msg.validator).all_equal_value() {
(Some((validator, msg)), None)
} else {
let split = msgs
.iter()
.map(|vote| {
(
vote.validator,
pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(
vec![vote.clone()]
),
),
)
})
.collect_vec();
(None, Some(split))
},
},
}
}
/// A handler object that both type of workers use for receiving work.
///
/// In practive this is just a wrapper over two channels Receiver, that is injected into
/// approval-voting worker and approval-distribution workers.
type WorkProvider<M, Clos, State> = WorkProviderImpl<
SelectWithStrategy<
MeteredReceiver<FromOrchestra<M>>,
UnboundedMeteredReceiver<FromOrchestra<M>>,
Clos,
State,
>,
>;
pub struct WorkProviderImpl<T>(T);
impl<T, M> Stream for WorkProviderImpl<T>
where
T: Stream<Item = FromOrchestra<M>> + Unpin + Send,
{
type Item = FromOrchestra<M>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.0.poll_next_unpin(cx)
}
}
#[async_trait::async_trait]
impl<T> ApprovalVotingWorkProvider for WorkProviderImpl<T>
where
T: Stream<Item = FromOrchestra<ApprovalVotingMessage>> + Unpin + Send,
{
async fn recv(&mut self) -> SubsystemResult<FromOrchestra<ApprovalVotingMessage>> {
self.0.next().await.ok_or(SubsystemError::Context(
"ApprovalVotingWorkProviderImpl: Channel closed".to_string(),
))
}
}
impl<M, Clos, State> WorkProvider<M, Clos, State>
where
M: Send + Sync + 'static,
Clos: FnMut(&mut State) -> PollNext,
State: Default,
{
// Constructs a work providers from the channels handles.
fn from_rx_worker(rx: RxWorker<M>, prio: Clos) -> Self {
let prioritised = select_with_strategy(rx.0, rx.1, prio);
WorkProviderImpl(prioritised)
}
}
/// Just a wrapper for implementing `overseer::SubsystemSender<ApprovalVotingMessage>` and
/// `overseer::SubsystemSender<ApprovalDistributionMessage>`.
///
/// The instance of this struct can be injected into the workers, so they can talk
/// directly with each other without intermediating in this subsystem loop.
pub struct ToWorker<T: Send + Sync + 'static>(
MeteredSender<FromOrchestra<T>>,
UnboundedMeteredSender<FromOrchestra<T>>,
);
impl<T: Send + Sync + 'static> Clone for ToWorker<T> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1.clone())
}
}
impl<T: Send + Sync + 'static> ToWorker<T> {
async fn send_signal(&mut self, signal: OverseerSignal) -> Result<(), SubsystemError> {
self.1
.unbounded_send(FromOrchestra::Signal(signal))
.map_err(|err| SubsystemError::QueueError(err.into_send_error()))
}
fn meter(&self) -> Meters {
Meters::new(self.0.meter(), self.1.meter())
}
}
impl<T: Send + Sync + 'static + Debug> overseer::SubsystemSender<T> for ToWorker<T> {
fn send_message<'life0, 'async_trait>(
&'life0 mut self,
msg: T,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
async {
if let Err(err) =
self.0.send(pezkuwi_overseer::FromOrchestra::Communication { msg }).await
{
gum::error!(
target: LOG_TARGET,
"Failed to send message to approval voting worker: {:?}, subsystem is probably shutting down.",
err
);
}
}
.boxed()
}
fn try_send_message(&mut self, msg: T) -> Result<(), metered::TrySendError<T>> {
self.0
.try_send(pezkuwi_overseer::FromOrchestra::Communication { msg })
.map_err(|result| {
let is_full = result.is_full();
let msg = match result.into_inner() {
pezkuwi_overseer::FromOrchestra::Signal(_) => {
panic!("Cannot happen variant is never built")
},
pezkuwi_overseer::FromOrchestra::Communication { msg } => msg,
};
if is_full {
metered::TrySendError::Full(msg)
} else {
metered::TrySendError::Closed(msg)
}
})
}
fn send_messages<'life0, 'async_trait, I>(
&'life0 mut self,
msgs: I,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
I: IntoIterator<Item = T> + Send,
I::IntoIter: Send,
I: 'async_trait,
'life0: 'async_trait,
Self: 'async_trait,
{
async {
for msg in msgs {
self.send_message(msg).await;
}
}
.boxed()
}
fn send_unbounded_message(&mut self, msg: T) {
if let Err(err) =
self.1.unbounded_send(pezkuwi_overseer::FromOrchestra::Communication { msg })
{
gum::error!(
target: LOG_TARGET,
"Failed to send unbounded message to approval voting worker: {:?}, subsystem is probably shutting down.",
err
);
}
}
fn send_message_with_priority<'life0, 'async_trait, P>(
&'life0 mut self,
msg: T,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
P: 'async_trait + Priority,
'life0: 'async_trait,
Self: 'async_trait,
{
match P::priority() {
pezkuwi_overseer::PriorityLevel::Normal => self.send_message(msg),
pezkuwi_overseer::PriorityLevel::High =>
async { self.send_unbounded_message(msg) }.boxed(),
}
}
fn try_send_message_with_priority<P: Priority>(
&mut self,
msg: T,
) -> Result<(), metered::TrySendError<T>> {
match P::priority() {
pezkuwi_overseer::PriorityLevel::Normal => self.try_send_message(msg),
pezkuwi_overseer::PriorityLevel::High => Ok(self.send_unbounded_message(msg)),
}
}
}
/// Handles that are used by an worker to receive work.
pub struct RxWorker<T: Send + Sync + 'static>(
MeteredReceiver<FromOrchestra<T>>,
UnboundedMeteredReceiver<FromOrchestra<T>>,
);
// Build all the necessary channels for sending messages to an worker
// and for the worker to receive them.
fn build_channels<T: Send + Sync + 'static>(
channel_name: String,
channel_size: usize,
metrics_watcher: &mut MetricsWatcher,
) -> (ToWorker<T>, RxWorker<T>) {
let (tx_work, rx_work) = channel::<FromOrchestra<T>>(channel_size);
let (tx_work_unbounded, rx_work_unbounded) = unbounded::<FromOrchestra<T>>();
let to_worker = ToWorker(tx_work, tx_work_unbounded);
metrics_watcher.watch(channel_name, to_worker.meter());
(to_worker, RxWorker(rx_work, rx_work_unbounded))
}
/// Build the worker handles used for interacting with the workers.
///
/// `ToWorker` is used for sending messages to the workers.
/// `WorkProvider` is used by the workers for receiving the messages.
fn build_worker_handles<M, Clos, State>(
channel_name: String,
channel_size: usize,
metrics_watcher: &mut MetricsWatcher,
prio_right: Clos,
) -> (ToWorker<M>, WorkProvider<M, Clos, State>)
where
M: Send + Sync + 'static,
Clos: FnMut(&mut State) -> PollNext,
State: Default,
{
let (to_worker, rx_worker) = build_channels(channel_name, channel_size, metrics_watcher);
(to_worker, WorkProviderImpl::from_rx_worker(rx_worker, prio_right))
}
/// Just a wrapper for implementing `overseer::SubsystemSender<ApprovalDistributionMessage>`, so
/// that we can inject into the approval voting subsystem.
#[derive(Clone)]
pub struct ApprovalVotingToApprovalDistribution<S: SubsystemSender<ApprovalVotingParallelMessage>>(
S,
);
impl<S: SubsystemSender<ApprovalVotingParallelMessage>>
overseer::SubsystemSender<ApprovalDistributionMessage>
for ApprovalVotingToApprovalDistribution<S>
{
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn send_message<'life0, 'async_trait>(
&'life0 mut self,
msg: ApprovalDistributionMessage,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
self.0.send_message(msg.into())
}
fn try_send_message(
&mut self,
msg: ApprovalDistributionMessage,
) -> Result<(), metered::TrySendError<ApprovalDistributionMessage>> {
self.0.try_send_message(msg.into()).map_err(|err| match err {
// Safe to unwrap because it was built from the same type.
metered::TrySendError::Closed(msg) =>
metered::TrySendError::Closed(msg.try_into().unwrap()),
metered::TrySendError::Full(msg) =>
metered::TrySendError::Full(msg.try_into().unwrap()),
})
}
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn send_messages<'life0, 'async_trait, I>(
&'life0 mut self,
msgs: I,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
I: IntoIterator<Item = ApprovalDistributionMessage> + Send,
I::IntoIter: Send,
I: 'async_trait,
'life0: 'async_trait,
Self: 'async_trait,
{
self.0.send_messages(msgs.into_iter().map(|msg| msg.into()))
}
fn send_unbounded_message(&mut self, msg: ApprovalDistributionMessage) {
self.0.send_unbounded_message(msg.into())
}
fn send_message_with_priority<'life0, 'async_trait, P>(
&'life0 mut self,
msg: ApprovalDistributionMessage,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
P: 'async_trait + Priority,
'life0: 'async_trait,
Self: 'async_trait,
{
self.0.send_message_with_priority::<P>(msg.into())
}
fn try_send_message_with_priority<P: Priority>(
&mut self,
msg: ApprovalDistributionMessage,
) -> Result<(), metered::TrySendError<ApprovalDistributionMessage>> {
self.0.try_send_message_with_priority::<P>(msg.into()).map_err(|err| match err {
// Safe to unwrap because it was built from the same type.
metered::TrySendError::Closed(msg) =>
metered::TrySendError::Closed(msg.try_into().unwrap()),
metered::TrySendError::Full(msg) =>
metered::TrySendError::Full(msg.try_into().unwrap()),
})
}
}
@@ -0,0 +1,234 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The Metrics for Approval Voting Parallel Subsystem.
use std::collections::HashMap;
use pezkuwi_node_metrics::{metered::Meter, metrics};
use pezkuwi_overseer::prometheus;
#[derive(Default, Clone)]
pub struct Metrics(Option<MetricsInner>);
/// Approval Voting parallel metrics.
#[derive(Clone)]
pub struct MetricsInner {
// The inner metrics of the approval distribution workers.
approval_distribution: pezkuwi_approval_distribution::metrics::Metrics,
// The inner metrics of the approval voting workers.
approval_voting: pezkuwi_node_core_approval_voting::Metrics,
// Time of flight metrics for bounded channels.
to_worker_bounded_tof: prometheus::HistogramVec,
// Number of elements sent to the worker's bounded queue.
to_worker_bounded_sent: prometheus::GaugeVec<prometheus::U64>,
// Number of elements received by the worker's bounded queue.
to_worker_bounded_received: prometheus::GaugeVec<prometheus::U64>,
// Number of times senders blocked while sending messages to the worker.
to_worker_bounded_blocked: prometheus::GaugeVec<prometheus::U64>,
// Time of flight metrics for unbounded channels.
to_worker_unbounded_tof: prometheus::HistogramVec,
// Number of elements sent to the worker's unbounded queue.
to_worker_unbounded_sent: prometheus::GaugeVec<prometheus::U64>,
// Number of elements received by the worker's unbounded queue.
to_worker_unbounded_received: prometheus::GaugeVec<prometheus::U64>,
}
impl Metrics {
/// Get the approval distribution metrics.
pub fn approval_distribution_metrics(&self) -> pezkuwi_approval_distribution::metrics::Metrics {
self.0
.as_ref()
.map(|metrics_inner| metrics_inner.approval_distribution.clone())
.unwrap_or_default()
}
/// Get the approval voting metrics.
pub fn approval_voting_metrics(&self) -> pezkuwi_node_core_approval_voting::Metrics {
self.0
.as_ref()
.map(|metrics_inner| metrics_inner.approval_voting.clone())
.unwrap_or_default()
}
}
impl metrics::Metrics for Metrics {
/// Try to register the metrics.
fn try_register(
registry: &prometheus::Registry,
) -> std::result::Result<Self, prometheus::PrometheusError> {
Ok(Metrics(Some(MetricsInner {
approval_distribution: pezkuwi_approval_distribution::metrics::Metrics::try_register(
registry,
)?,
approval_voting: pezkuwi_node_core_approval_voting::Metrics::try_register(registry)?,
to_worker_bounded_tof: prometheus::register(
prometheus::HistogramVec::new(
prometheus::HistogramOpts::new(
"pezkuwi_approval_voting_parallel_worker_bounded_tof",
"Duration spent in a particular approval voting worker channel from entrance to removal",
)
.buckets(vec![
0.0001, 0.0004, 0.0016, 0.0064, 0.0256, 0.1024, 0.4096, 1.6384, 3.2768,
4.9152, 6.5536,
]),
&["worker_name"],
)?,
registry,
)?,
to_worker_bounded_sent: prometheus::register(
prometheus::GaugeVec::<prometheus::U64>::new(
prometheus::Opts::new(
"pezkuwi_approval_voting_parallel_worker_bounded_sent",
"Number of elements sent to approval voting workers' bounded queues",
),
&["worker_name"],
)?,
registry,
)?,
to_worker_bounded_received: prometheus::register(
prometheus::GaugeVec::<prometheus::U64>::new(
prometheus::Opts::new(
"pezkuwi_approval_voting_parallel_worker_bounded_received",
"Number of elements received by approval voting workers' bounded queues",
),
&["worker_name"],
)?,
registry,
)?,
to_worker_bounded_blocked: prometheus::register(
prometheus::GaugeVec::<prometheus::U64>::new(
prometheus::Opts::new(
"pezkuwi_approval_voting_parallel_worker_bounded_blocked",
"Number of times approval voting workers blocked while sending messages to a subsystem",
),
&["worker_name"],
)?,
registry,
)?,
to_worker_unbounded_tof: prometheus::register(
prometheus::HistogramVec::new(
prometheus::HistogramOpts::new(
"pezkuwi_approval_voting_parallel_worker_unbounded_tof",
"Duration spent in a particular approval voting worker channel from entrance to removal",
)
.buckets(vec![
0.0001, 0.0004, 0.0016, 0.0064, 0.0256, 0.1024, 0.4096, 1.6384, 3.2768,
4.9152, 6.5536,
]),
&["worker_name"],
)?,
registry,
)?,
to_worker_unbounded_sent: prometheus::register(
prometheus::GaugeVec::<prometheus::U64>::new(
prometheus::Opts::new(
"pezkuwi_approval_voting_parallel_worker_unbounded_sent",
"Number of elements sent to approval voting workers' unbounded queues",
),
&["worker_name"],
)?,
registry,
)?,
to_worker_unbounded_received: prometheus::register(
prometheus::GaugeVec::<prometheus::U64>::new(
prometheus::Opts::new(
"pezkuwi_approval_voting_parallel_worker_unbounded_received",
"Number of elements received by approval voting workers' unbounded queues",
),
&["worker_name"],
)?,
registry,
)?,
})))
}
}
/// The meters to watch.
#[derive(Clone)]
pub struct Meters {
bounded: Meter,
unbounded: Meter,
}
impl Meters {
pub fn new(bounded: &Meter, unbounded: &Meter) -> Self {
Self { bounded: bounded.clone(), unbounded: unbounded.clone() }
}
}
/// A metrics watcher that watches the meters and updates the metrics.
pub struct MetricsWatcher {
to_watch: HashMap<String, Meters>,
metrics: Metrics,
}
impl MetricsWatcher {
/// Create a new metrics watcher.
pub fn new(metrics: Metrics) -> Self {
Self { to_watch: HashMap::new(), metrics }
}
/// Watch the meters of a worker with this name.
pub fn watch(&mut self, worker_name: String, meters: Meters) {
self.to_watch.insert(worker_name, meters);
}
/// Collect all the metrics.
pub fn collect_metrics(&self) {
for (name, meter) in &self.to_watch {
let bounded_readouts = meter.bounded.read();
let unbounded_readouts = meter.unbounded.read();
if let Some(metrics) = self.metrics.0.as_ref() {
metrics
.to_worker_bounded_sent
.with_label_values(&[name])
.set(bounded_readouts.sent as u64);
metrics
.to_worker_bounded_received
.with_label_values(&[name])
.set(bounded_readouts.received as u64);
metrics
.to_worker_bounded_blocked
.with_label_values(&[name])
.set(bounded_readouts.blocked as u64);
metrics
.to_worker_unbounded_sent
.with_label_values(&[name])
.set(unbounded_readouts.sent as u64);
metrics
.to_worker_unbounded_received
.with_label_values(&[name])
.set(unbounded_readouts.received as u64);
let hist_bounded = metrics.to_worker_bounded_tof.with_label_values(&[name]);
for tof in bounded_readouts.tof {
hist_bounded.observe(tof.as_f64());
}
let hist_unbounded = metrics.to_worker_unbounded_tof.with_label_values(&[name]);
for tof in unbounded_readouts.tof {
hist_unbounded.observe(tof.as_f64());
}
}
}
}
}
@@ -0,0 +1,982 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The tests for Approval Voting Parallel Subsystem.
use std::{
collections::{HashMap, HashSet},
future::Future,
sync::Arc,
time::Duration,
};
use crate::{
build_worker_handles, metrics::MetricsWatcher, prio_right, run_main_loop, start_workers,
validator_index_for_msg, ApprovalVotingParallelSubsystem, Metrics, WorkProvider,
};
use assert_matches::assert_matches;
use futures::{channel::oneshot, future, stream::PollNext, StreamExt};
use itertools::Itertools;
use pezkuwi_node_core_approval_voting::{ApprovalVotingWorkProvider, Config};
use pezkuwi_node_network_protocol::{peer_set::ValidationVersion, ObservedRole, PeerId, View};
use pezkuwi_node_primitives::approval::{
time::SystemClock,
v1::RELAY_VRF_MODULO_CONTEXT,
v2::{
AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, IndirectAssignmentCertV2,
IndirectSignedApprovalVoteV2,
},
};
use pezkuwi_node_subsystem::{
messages::{ApprovalDistributionMessage, ApprovalVotingMessage, ApprovalVotingParallelMessage},
FromOrchestra,
};
use pezkuwi_node_subsystem_test_helpers::{mock::new_leaf, TestSubsystemContext};
use pezkuwi_overseer::{ActiveLeavesUpdate, OverseerSignal, SpawnGlue, TimeoutExt};
use pezkuwi_primitives::{CandidateHash, CoreIndex, Hash, ValidatorIndex};
use sc_keystore::{Keystore, LocalKeystore};
use sp_consensus::SyncOracle;
use sp_consensus_babe::{VrfPreOutput, VrfProof, VrfSignature};
use sp_core::{testing::TaskExecutor, H256};
use sp_keyring::Sr25519Keyring;
type VirtualOverseer =
pezkuwi_node_subsystem_test_helpers::TestSubsystemContextHandle<ApprovalVotingParallelMessage>;
const SLOT_DURATION_MILLIS: u64 = 6000;
pub mod test_constants {
pub(crate) const DATA_COL: u32 = 0;
pub(crate) const NUM_COLUMNS: u32 = 1;
}
fn fake_assignment_cert_v2(
block_hash: Hash,
validator: ValidatorIndex,
core_bitfield: CoreBitfield,
) -> IndirectAssignmentCertV2 {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"WhenTeyrchains?";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let preout = inout.to_preout();
IndirectAssignmentCertV2 {
block_hash,
validator,
cert: AssignmentCertV2 {
kind: AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield },
vrf: VrfSignature { pre_output: VrfPreOutput(preout), proof: VrfProof(proof) },
},
}
}
/// Creates a meaningless signature
pub fn dummy_signature() -> pezkuwi_primitives::ValidatorSignature {
sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64])
}
fn build_subsystem(
sync_oracle: Box<dyn SyncOracle + Send>,
) -> (
ApprovalVotingParallelSubsystem,
TestSubsystemContext<ApprovalVotingParallelMessage, SpawnGlue<TaskExecutor>>,
VirtualOverseer,
) {
sp_tracing::init_for_tests();
let pool = sp_core::testing::TaskExecutor::new();
let (context, virtual_overseer) = pezkuwi_node_subsystem_test_helpers::make_subsystem_context::<
ApprovalVotingParallelMessage,
_,
>(pool.clone());
let keystore = LocalKeystore::in_memory();
let _ = keystore.sr25519_generate_new(
pezkuwi_primitives::TEYRCHAIN_KEY_TYPE_ID,
Some(&Sr25519Keyring::Alice.to_seed()),
);
let clock = Arc::new(SystemClock {});
let db = kvdb_memorydb::create(test_constants::NUM_COLUMNS);
let db = pezkuwi_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[]);
(
ApprovalVotingParallelSubsystem::with_config_and_clock(
Config {
col_approval_data: test_constants::DATA_COL,
slot_duration_millis: SLOT_DURATION_MILLIS,
},
Arc::new(db),
Arc::new(keystore),
sync_oracle,
Metrics::default(),
clock.clone(),
SpawnGlue(pool),
None,
),
context,
virtual_overseer,
)
}
#[derive(Clone)]
struct TestSyncOracle {}
impl SyncOracle for TestSyncOracle {
fn is_major_syncing(&self) -> bool {
false
}
fn is_offline(&self) -> bool {
unimplemented!("not used in network bridge")
}
}
fn test_harness<T, Clos, State>(
num_approval_distro_workers: usize,
prio_right: Clos,
subsystem_gracefully_exits: bool,
test_fn: impl FnOnce(
VirtualOverseer,
WorkProvider<ApprovalVotingMessage, Clos, State>,
Vec<WorkProvider<ApprovalDistributionMessage, Clos, State>>,
) -> T,
) where
T: Future<Output = VirtualOverseer>,
Clos: Clone + FnMut(&mut State) -> PollNext,
State: Default,
{
let (subsystem, context, virtual_overseer) = build_subsystem(Box::new(TestSyncOracle {}));
let mut metrics_watcher = MetricsWatcher::new(subsystem.metrics.clone());
let channel_size = 5;
let (to_approval_voting_worker, approval_voting_work_provider) =
build_worker_handles::<ApprovalVotingMessage, _, _>(
"to_approval_voting_worker".into(),
channel_size,
&mut metrics_watcher,
prio_right.clone(),
);
let approval_distribution_channels = { 0..num_approval_distro_workers }
.into_iter()
.map(|worker_index| {
build_worker_handles::<ApprovalDistributionMessage, _, _>(
format!("to_approval_distro/{}", worker_index),
channel_size,
&mut metrics_watcher,
prio_right.clone(),
)
})
.collect_vec();
let to_approval_distribution_workers =
approval_distribution_channels.iter().map(|(tx, _)| tx.clone()).collect_vec();
let approval_distribution_work_providers =
approval_distribution_channels.into_iter().map(|(_, rx)| rx).collect_vec();
let subsystem = async move {
let result = run_main_loop(
context,
to_approval_voting_worker,
to_approval_distribution_workers,
metrics_watcher,
)
.await;
if subsystem_gracefully_exits && result.is_err() {
result
} else {
Ok(())
}
};
let test_fut = test_fn(
virtual_overseer,
approval_voting_work_provider,
approval_distribution_work_providers,
);
futures::pin_mut!(test_fut);
futures::pin_mut!(subsystem);
futures::executor::block_on(future::join(
async move {
let _overseer = test_fut.await;
},
subsystem,
))
.1
.unwrap();
}
const TIMEOUT: Duration = Duration::from_millis(2000);
async fn overseer_signal(overseer: &mut VirtualOverseer, signal: OverseerSignal) {
overseer
.send(FromOrchestra::Signal(signal))
.timeout(TIMEOUT)
.await
.expect(&format!("{:?} is more than enough for sending signals.", TIMEOUT));
}
async fn overseer_message(overseer: &mut VirtualOverseer, msg: ApprovalVotingParallelMessage) {
overseer
.send(FromOrchestra::Communication { msg })
.timeout(TIMEOUT)
.await
.expect(&format!("{:?} is more than enough for sending signals.", TIMEOUT));
}
async fn run_start_workers() {
let (subsystem, mut context, _) = build_subsystem(Box::new(TestSyncOracle {}));
let mut metrics_watcher = MetricsWatcher::new(subsystem.metrics.clone());
let _workers = start_workers(&mut context, subsystem, &mut metrics_watcher).await.unwrap();
}
// Test starting the workers succeeds.
#[test]
fn start_workers_succeeds() {
futures::executor::block_on(run_start_workers());
}
// Test main loop forwards messages to the correct worker for all type of messages.
#[test]
fn test_main_loop_forwards_correctly() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
true,
|mut overseer, mut approval_voting_work_provider, mut rx_approval_distribution_workers| async move {
// 1. Check Signals are correctly forwarded to the workers.
let signal = OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work(new_leaf(
Hash::random(),
1,
)));
overseer_signal(&mut overseer, signal.clone()).await;
let approval_voting_receives = approval_voting_work_provider.recv().await.unwrap();
assert_matches!(approval_voting_receives, FromOrchestra::Signal(_));
for rx_approval_distribution_worker in rx_approval_distribution_workers.iter_mut() {
let approval_distribution_receives =
rx_approval_distribution_worker.next().await.unwrap();
assert_matches!(approval_distribution_receives, FromOrchestra::Signal(_));
}
let (test_tx, _rx) = oneshot::channel();
let test_hash = Hash::random();
let test_block_nr = 2;
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::ApprovedAncestor(test_hash, test_block_nr, test_tx),
)
.await;
assert_matches!(
approval_voting_work_provider.recv().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalVotingMessage::ApprovedAncestor(hash, block_nr, _)
} => {
assert_eq!(hash, test_hash);
assert_eq!(block_nr, test_block_nr);
}
);
for rx_approval_distribution_worker in rx_approval_distribution_workers.iter_mut() {
assert!(rx_approval_distribution_worker
.next()
.timeout(Duration::from_millis(200))
.await
.is_none());
}
// 2. Check GetApprovalSignaturesForCandidate is correctly forwarded to the workers.
let (test_tx, _rx) = oneshot::channel();
let test_hash = CandidateHash(Hash::random());
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::GetApprovalSignaturesForCandidate(
test_hash, test_tx,
),
)
.await;
assert_matches!(
approval_voting_work_provider.recv().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalVotingMessage::GetApprovalSignaturesForCandidate(hash, _)
} => {
assert_eq!(hash, test_hash);
}
);
for rx_approval_distribution_worker in rx_approval_distribution_workers.iter_mut() {
assert!(rx_approval_distribution_worker
.next()
.timeout(Duration::from_millis(200))
.await
.is_none());
}
// 3. Check NewBlocks is correctly forwarded to the workers.
overseer_message(&mut overseer, ApprovalVotingParallelMessage::NewBlocks(vec![])).await;
for rx_approval_distribution_worker in rx_approval_distribution_workers.iter_mut() {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::NewBlocks(blocks)
} => {
assert!(blocks.is_empty());
}
);
}
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
// 4. Check DistributeAssignment is correctly forwarded to the workers.
let validator_index = ValidatorIndex(17);
let assignment =
fake_assignment_cert_v2(Hash::random(), validator_index, CoreIndex(1).into());
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::DistributeAssignment(assignment.clone(), 1.into()),
)
.await;
for (index, rx_approval_distribution_worker) in
rx_approval_distribution_workers.iter_mut().enumerate()
{
if index == validator_index.0 as usize % num_approval_distro_workers {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::DistributeAssignment(cert, bitfield)
} => {
assert_eq!(cert, assignment);
assert_eq!(bitfield, 1.into());
}
);
} else {
assert!(rx_approval_distribution_worker
.next()
.timeout(Duration::from_millis(200))
.await
.is_none());
}
}
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
// 5. Check DistributeApproval is correctly forwarded to the workers.
let validator_index = ValidatorIndex(26);
let expected_vote = IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 1.into(),
validator: validator_index,
signature: dummy_signature(),
};
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::DistributeApproval(expected_vote.clone()),
)
.await;
for (index, rx_approval_distribution_worker) in
rx_approval_distribution_workers.iter_mut().enumerate()
{
if index == validator_index.0 as usize % num_approval_distro_workers {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::DistributeApproval(vote)
} => {
assert_eq!(vote, expected_vote);
}
);
} else {
assert!(rx_approval_distribution_worker
.next()
.timeout(Duration::from_millis(200))
.await
.is_none());
}
}
// 6. Check NetworkBridgeUpdate::PeerMessage is correctly forwarded just to one of the
// workers.
let approvals = vec![
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 1.into(),
validator: validator_index,
signature: dummy_signature(),
},
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 2.into(),
validator: validator_index,
signature: dummy_signature(),
},
];
let expected_msg = pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(
approvals.clone(),
),
);
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerMessage(
PeerId::random(),
expected_msg.clone(),
),
),
)
.await;
for (index, rx_approval_distribution_worker) in
rx_approval_distribution_workers.iter_mut().enumerate()
{
if index == validator_index.0 as usize % num_approval_distro_workers {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerMessage(
_,
msg,
),
)
} => {
assert_eq!(msg, expected_msg);
}
);
} else {
assert!(rx_approval_distribution_worker
.next()
.timeout(Duration::from_millis(200))
.await
.is_none());
}
}
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
// 7. Check NetworkBridgeUpdate::PeerConnected is correctly forwarded to all workers.
let expected_peer_id = PeerId::random();
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerConnected(
expected_peer_id,
ObservedRole::Authority,
ValidationVersion::V3.into(),
None,
),
),
)
.await;
for rx_approval_distribution_worker in rx_approval_distribution_workers.iter_mut() {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerConnected(
peer_id,
role,
version,
authority_id,
),
)
} => {
assert_eq!(peer_id, expected_peer_id);
assert_eq!(role, ObservedRole::Authority);
assert_eq!(version, ValidationVersion::V3.into());
assert_eq!(authority_id, None);
}
);
}
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
// 8. Check ApprovalCheckingLagUpdate is correctly forwarded to all workers.
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::ApprovalCheckingLagUpdate(7),
)
.await;
for rx_approval_distribution_worker in rx_approval_distribution_workers.iter_mut() {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::ApprovalCheckingLagUpdate(
lag
)
} => {
assert_eq!(lag, 7);
}
);
}
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
overseer_signal(&mut overseer, OverseerSignal::Conclude).await;
overseer
},
);
}
/// Test GetApprovalSignatures correctly gatheres the signatures from all workers.
#[test]
fn test_handle_get_approval_signatures() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
true,
|mut overseer, mut approval_voting_work_provider, mut rx_approval_distribution_workers| async move {
let (tx, rx) = oneshot::channel();
let first_block = Hash::random();
let second_block = Hash::random();
let expected_candidates: HashSet<_> =
vec![(first_block, 2), (second_block, 3)].into_iter().collect();
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::GetApprovalSignatures(
expected_candidates.clone(),
tx,
),
)
.await;
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
let mut all_votes = HashMap::new();
for (index, rx_approval_distribution_worker) in
rx_approval_distribution_workers.iter_mut().enumerate()
{
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::GetApprovalSignatures(
candidates, tx
)
} => {
assert_eq!(candidates, expected_candidates);
let to_send: HashMap<_, _> = {0..10}.into_iter().map(|validator| {
let validator_index = ValidatorIndex(validator as u32 * num_approval_distro_workers as u32 + index as u32);
(validator_index, (first_block, vec![2, 4], dummy_signature()))
}).collect();
tx.send(to_send.clone()).unwrap();
all_votes.extend(to_send.clone());
}
);
}
let received_votes = rx.await.unwrap();
assert_eq!(received_votes, all_votes);
overseer_signal(&mut overseer, OverseerSignal::Conclude).await;
overseer
},
)
}
/// Test subsystem exits with error when approval_voting_work_provider exits.
#[test]
fn test_subsystem_exits_with_error_if_approval_voting_worker_errors() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
false,
|overseer, approval_voting_work_provider, _rx_approval_distribution_workers| async move {
// Drop the approval_voting_work_provider to simulate an error.
std::mem::drop(approval_voting_work_provider);
overseer
},
)
}
/// Test subsystem exits with error when approval_distribution_workers exits.
#[test]
fn test_subsystem_exits_with_error_if_approval_distribution_worker_errors() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
false,
|overseer, _approval_voting_work_provider, rx_approval_distribution_workers| async move {
// Drop the approval_distribution_workers to simulate an error.
std::mem::drop(rx_approval_distribution_workers.into_iter().next().unwrap());
overseer
},
)
}
/// Test signals sent before messages are processed in order.
#[test]
fn test_signal_before_message_keeps_receive_order() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
true,
|mut overseer, mut approval_voting_work_provider, mut rx_approval_distribution_workers| async move {
let signal = OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work(new_leaf(
Hash::random(),
1,
)));
overseer_signal(&mut overseer, signal.clone()).await;
let validator_index = ValidatorIndex(17);
let assignment =
fake_assignment_cert_v2(Hash::random(), validator_index, CoreIndex(1).into());
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::DistributeAssignment(assignment.clone(), 1.into()),
)
.await;
let approval_voting_receives = approval_voting_work_provider.recv().await.unwrap();
assert_matches!(approval_voting_receives, FromOrchestra::Signal(_));
let rx_approval_distribution_worker = rx_approval_distribution_workers
.get_mut(validator_index.0 as usize % num_approval_distro_workers)
.unwrap();
let approval_distribution_receives =
rx_approval_distribution_worker.next().await.unwrap();
assert_matches!(approval_distribution_receives, FromOrchestra::Signal(_));
assert_matches!(
rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::DistributeAssignment(_, _)
}
);
overseer_signal(&mut overseer, OverseerSignal::Conclude).await;
overseer
},
)
}
/// Test signals sent after messages are processed with the highest priority.
#[test]
fn test_signal_is_prioritized_when_unread_messages_in_the_queue() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
true,
|mut overseer, mut approval_voting_work_provider, mut rx_approval_distribution_workers| async move {
let validator_index = ValidatorIndex(17);
let assignment =
fake_assignment_cert_v2(Hash::random(), validator_index, CoreIndex(1).into());
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::DistributeAssignment(assignment.clone(), 1.into()),
)
.await;
let signal = OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work(new_leaf(
Hash::random(),
1,
)));
overseer_signal(&mut overseer, signal.clone()).await;
let approval_voting_receives = approval_voting_work_provider.recv().await.unwrap();
assert_matches!(approval_voting_receives, FromOrchestra::Signal(_));
let rx_approval_distribution_worker = rx_approval_distribution_workers
.get_mut(validator_index.0 as usize % num_approval_distro_workers)
.unwrap();
let approval_distribution_receives =
rx_approval_distribution_worker.next().await.unwrap();
assert_matches!(approval_distribution_receives, FromOrchestra::Signal(_));
assert_matches!(
rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::DistributeAssignment(_, _)
}
);
overseer_signal(&mut overseer, OverseerSignal::Conclude).await;
overseer
},
)
}
/// Test peer view updates have higher priority than normal messages.
#[test]
fn test_peer_view_is_prioritized_when_unread_messages_in_the_queue() {
let num_approval_distro_workers = 4;
test_harness(
num_approval_distro_workers,
prio_right,
true,
|mut overseer, mut approval_voting_work_provider, mut rx_approval_distribution_workers| async move {
let validator_index = ValidatorIndex(17);
let approvals = vec![
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 1.into(),
validator: validator_index,
signature: dummy_signature(),
},
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 2.into(),
validator: validator_index,
signature: dummy_signature(),
},
];
let expected_msg = pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(
approvals.clone(),
),
);
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerMessage(
PeerId::random(),
expected_msg.clone(),
),
),
)
.await;
overseer_message(
&mut overseer,
ApprovalVotingParallelMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerViewChange(
PeerId::random(),
View::default(),
),
),
)
.await;
for (index, rx_approval_distribution_worker) in
rx_approval_distribution_workers.iter_mut().enumerate()
{
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerViewChange(
_,
_,
),
)
} => {
}
);
if index == validator_index.0 as usize % num_approval_distro_workers {
assert_matches!(rx_approval_distribution_worker.next().await.unwrap(),
FromOrchestra::Communication {
msg: ApprovalDistributionMessage::NetworkBridgeUpdate(
pezkuwi_node_subsystem::messages::NetworkBridgeEvent::PeerMessage(
_,
msg,
),
)
} => {
assert_eq!(msg, expected_msg);
}
);
} else {
assert!(rx_approval_distribution_worker
.next()
.timeout(Duration::from_millis(200))
.await
.is_none());
}
}
assert!(approval_voting_work_provider
.recv()
.timeout(Duration::from_millis(200))
.await
.is_none());
overseer_signal(&mut overseer, OverseerSignal::Conclude).await;
overseer
},
)
}
// Test validator_index_for_msg with empty messages.
#[test]
fn test_validator_index_with_empty_message() {
let result = validator_index_for_msg(pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Assignments(vec![]),
));
assert_eq!(result, (None, Some(vec![])));
let result = validator_index_for_msg(pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(vec![]),
));
assert_eq!(result, (None, Some(vec![])));
}
// Test validator_index_for_msg when all the messages are originating from the same validator.
#[test]
fn test_validator_index_with_all_messages_from_the_same_validator() {
let validator_index = ValidatorIndex(3);
let v3_assignment = pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Assignments(vec![
(
fake_assignment_cert_v2(H256::random(), validator_index, CoreIndex(1).into()),
1.into(),
),
(
fake_assignment_cert_v2(H256::random(), validator_index, CoreIndex(3).into()),
3.into(),
),
]),
);
let result = validator_index_for_msg(v3_assignment.clone());
assert_eq!(result, (Some((validator_index, v3_assignment)), None));
let v3_approval = pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(vec![
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 1.into(),
validator: validator_index,
signature: dummy_signature(),
},
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 1.into(),
validator: validator_index,
signature: dummy_signature(),
},
]),
);
let result = validator_index_for_msg(v3_approval.clone());
assert_eq!(result, (Some((validator_index, v3_approval)), None));
}
// Test validator_index_for_msg when all the messages are originating from different validators,
// so the function should split them by validator index, so we can forward them separately to the
// worker they are assigned to.
#[test]
fn test_validator_index_with_messages_from_different_validators() {
let first_validator_index = ValidatorIndex(3);
let second_validator_index = ValidatorIndex(4);
let assignments = vec![
(
fake_assignment_cert_v2(H256::random(), first_validator_index, CoreIndex(1).into()),
1.into(),
),
(
fake_assignment_cert_v2(H256::random(), second_validator_index, CoreIndex(3).into()),
3.into(),
),
];
let v3_assignment = pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Assignments(
assignments.clone(),
),
);
let result = validator_index_for_msg(v3_assignment.clone());
assert_matches!(result, (None, Some(_)));
let messsages_split_by_validator = result.1.unwrap();
assert_eq!(messsages_split_by_validator.len(), assignments.len());
for (index, (validator_index, message)) in messsages_split_by_validator.into_iter().enumerate()
{
assert_eq!(validator_index, assignments[index].0.validator);
assert_eq!(
message,
pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Assignments(
assignments.get(index).into_iter().cloned().collect(),
),
)
);
}
let approvals = vec![
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 1.into(),
validator: first_validator_index,
signature: dummy_signature(),
},
IndirectSignedApprovalVoteV2 {
block_hash: H256::random(),
candidate_indices: 2.into(),
validator: second_validator_index,
signature: dummy_signature(),
},
];
let v3_approvals = pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(
approvals.clone(),
),
);
let result = validator_index_for_msg(v3_approvals.clone());
assert_matches!(result, (None, Some(_)));
let messsages_split_by_validator = result.1.unwrap();
assert_eq!(messsages_split_by_validator.len(), approvals.len());
for (index, (validator_index, message)) in messsages_split_by_validator.into_iter().enumerate()
{
assert_eq!(validator_index, approvals[index].validator);
assert_eq!(
message,
pezkuwi_node_network_protocol::ValidationProtocols::V3(
pezkuwi_node_network_protocol::v3::ApprovalDistributionMessage::Approvals(
approvals.get(index).into_iter().cloned().collect(),
),
)
);
}
}
@@ -0,0 +1,82 @@
[package]
name = "pezkuwi-node-core-approval-voting"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Approval Voting Subsystem of the Pezkuwi node"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[[bench]]
name = "approval-voting-regression-bench"
path = "benches/approval-voting-regression-bench.rs"
harness = false
required-features = ["subsystem-benchmarks"]
[dependencies]
async-trait = { workspace = true }
bitvec = { features = ["alloc"], workspace = true }
codec = { features = ["bit-vec", "derive"], workspace = true }
derive_more = { workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
gum = { workspace = true, default-features = true }
itertools = { workspace = true }
merlin = { workspace = true, default-features = true }
schnellru = { workspace = true }
schnorrkel = { workspace = true, default-features = true }
thiserror = { workspace = true }
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 }
rand = { workspace = true, default-features = true }
rand_chacha = { workspace = true, default-features = true }
# rand_core should match schnorrkel
rand_core = { workspace = true }
sc-keystore = { workspace = true }
sp-application-crypto = { features = ["full_crypto"], workspace = true }
sp-consensus = { workspace = true }
sp-consensus-slots = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
kvdb-memorydb = { workspace = true }
parking_lot = { workspace = true, default-features = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives = { workspace = true, features = ["test"] }
pezkuwi-primitives-test-helpers = { workspace = true }
sp-consensus-babe = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-keystore = { workspace = true, default-features = true }
sp-tracing = { workspace = true }
pezkuwi-subsystem-bench = { workspace = true }
[features]
subsystem-benchmarks = []
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-subsystem-bench/runtime-benchmarks",
"sp-consensus-babe/runtime-benchmarks",
"sp-consensus-slots/runtime-benchmarks",
"sp-consensus/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,93 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! approval-voting throughput test
//!
//! Approval Voting benchmark based on Kusama parameters and scale.
//!
//! Subsystems involved:
//! - approval-distribution
//! - approval-voting
use pezkuwi_subsystem_bench::{
self,
approval::{bench_approvals, prepare_test, ApprovalsOptions},
configuration::TestConfiguration,
usage::BenchmarkUsage,
utils::save_to_file,
};
use std::io::Write;
const BENCH_COUNT: usize = 10;
fn main() -> Result<(), String> {
let mut messages = vec![];
let mut config = TestConfiguration::default();
config.n_cores = 100;
config.n_validators = 500;
config.num_blocks = 10;
config.peer_bandwidth = 524288000000;
config.bandwidth = 524288000000;
config.latency = None;
config.connectivity = 100;
config.generate_pov_sizes();
let options = ApprovalsOptions {
last_considered_tranche: 89,
coalesce_mean: 3.0,
coalesce_std_dev: 1.0,
coalesce_tranche_diff: 12,
enable_assignments_v2: true,
stop_when_approved: false,
workdir_prefix: "/tmp".to_string(),
num_no_shows_per_candidate: 0,
approval_voting_parallel_enabled: true,
};
println!("Benchmarking...");
let usages: Vec<BenchmarkUsage> = (0..BENCH_COUNT)
.map(|n| {
print!("\r[{}{}]", "#".repeat(n), "_".repeat(BENCH_COUNT - n));
std::io::stdout().flush().unwrap();
let (mut env, state) = prepare_test(config.clone(), options.clone(), false);
env.runtime().block_on(bench_approvals(&mut env, state))
})
.collect();
println!("\rDone!{}", " ".repeat(BENCH_COUNT));
let average_usage = BenchmarkUsage::average(&usages);
save_to_file(
"charts/approval-voting-regression-bench.json",
average_usage.to_chart_json().map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
println!("{}", average_usage);
// We expect some small variance for received and sent because the
// test messages are generated at every benchmark run and they contain
// random data so use 0.01 as the accepted variance.
messages.extend(average_usage.check_network_usage(&[
("Received from peers", 52941.6071, 0.01),
("Sent to peers", 63995.2200, 0.01),
]));
messages.extend(average_usage.check_cpu_usage(&[("approval-voting-parallel", 12.3817, 0.1)]));
if messages.is_empty() {
Ok(())
} else {
eprintln!("{}", messages.join("\n"));
Err("Regressions found".to_string())
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use pezkuwi_node_primitives::approval::{
v1::{AssignmentCert, AssignmentCertKind, VrfProof, VrfSignature, RELAY_VRF_MODULO_CONTEXT},
v2::VrfPreOutput,
};
pub fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
pub fn dummy_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"test-garbage";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let preout = inout.to_preout();
AssignmentCert {
kind,
vrf: VrfSignature { pre_output: VrfPreOutput(preout), proof: VrfProof(proof) },
}
}
@@ -0,0 +1,293 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Common helper functions for all versions of approval-voting database.
use std::sync::Arc;
use codec::{Decode, Encode};
use pezkuwi_node_subsystem::{SubsystemError, SubsystemResult};
use pezkuwi_node_subsystem_util::database::{DBTransaction, Database};
use pezkuwi_primitives::{BlockNumber, CandidateHash, CandidateIndex, Hash};
use crate::{
backend::{Backend, BackendWriteOp, V1ReadBackend, V2ReadBackend},
persisted_entries,
};
use super::{
v2::{load_block_entry_v1, load_candidate_entry_v1},
v3::{load_block_entry_v2, load_candidate_entry_v2, BlockEntry, CandidateEntry},
};
pub mod migration_helpers;
const STORED_BLOCKS_KEY: &[u8] = b"Approvals_StoredBlocks";
/// A range from earliest..last block number stored within the DB.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct StoredBlockRange(pub BlockNumber, pub BlockNumber);
/// The database config.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column family in the database where data is stored.
pub col_approval_data: u32,
}
/// `DbBackend` is a concrete implementation of the higher-level Backend trait
pub struct DbBackend {
inner: Arc<dyn Database>,
config: Config,
}
impl DbBackend {
/// Create a new [`DbBackend`] with the supplied key-value store and
/// config.
pub fn new(db: Arc<dyn Database>, config: Config) -> Self {
DbBackend { inner: db, config }
}
}
/// Errors while accessing things from the DB.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
InvalidDecoding(codec::Error),
InternalError(SubsystemError),
}
impl std::error::Error for Error {}
/// Result alias for DB errors.
pub type Result<T> = std::result::Result<T, Error>;
impl Backend for DbBackend {
fn load_block_entry(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry(&*self.inner, &self.config, candidate_hash).map(|e| e.map(Into::into))
}
fn load_blocks_at_height(&self, block_height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
load_blocks_at_height(&*self.inner, &self.config, block_height)
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
load_all_blocks(&*self.inner, &self.config)
}
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
load_stored_blocks(&*self.inner, &self.config)
}
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
for op in ops {
match op {
BackendWriteOp::WriteStoredBlockRange(stored_block_range) => {
tx.put_vec(
self.config.col_approval_data,
&STORED_BLOCKS_KEY,
stored_block_range.encode(),
);
},
BackendWriteOp::DeleteStoredBlockRange => {
tx.delete(self.config.col_approval_data, &STORED_BLOCKS_KEY);
},
BackendWriteOp::WriteBlocksAtHeight(h, blocks) => {
tx.put_vec(
self.config.col_approval_data,
&blocks_at_height_key(h),
blocks.encode(),
);
},
BackendWriteOp::DeleteBlocksAtHeight(h) => {
tx.delete(self.config.col_approval_data, &blocks_at_height_key(h));
},
BackendWriteOp::WriteBlockEntry(block_entry) => {
let block_entry: BlockEntry = block_entry.into();
tx.put_vec(
self.config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
},
BackendWriteOp::DeleteBlockEntry(hash) => {
tx.delete(self.config.col_approval_data, &block_entry_key(&hash));
},
BackendWriteOp::WriteCandidateEntry(candidate_entry) => {
let candidate_entry: CandidateEntry = candidate_entry.into();
tx.put_vec(
self.config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
},
BackendWriteOp::DeleteCandidateEntry(candidate_hash) => {
tx.delete(self.config.col_approval_data, &candidate_entry_key(&candidate_hash));
},
}
}
self.inner.write(tx).map_err(|e| e.into())
}
}
impl V1ReadBackend for DbBackend {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry_v1(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(|e| persisted_entries::CandidateEntry::from_v1(e, candidate_index)))
}
fn load_block_entry_v1(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry_v1(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
}
impl V2ReadBackend for DbBackend {
fn load_candidate_entry_v2(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry_v2(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(|e| persisted_entries::CandidateEntry::from_v2(e, candidate_index)))
}
fn load_block_entry_v2(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry_v2(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
}
pub(crate) fn load_decode<D: Decode>(
store: &dyn Database,
col_approval_data: u32,
key: &[u8],
) -> Result<Option<D>> {
match store.get(col_approval_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
/// The key a given block entry is stored under.
pub(crate) fn block_entry_key(block_hash: &Hash) -> [u8; 46] {
const BLOCK_ENTRY_PREFIX: [u8; 14] = *b"Approvals_blck";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&BLOCK_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(block_hash.as_ref());
key
}
/// The key a given candidate entry is stored under.
pub(crate) fn candidate_entry_key(candidate_hash: &CandidateHash) -> [u8; 46] {
const CANDIDATE_ENTRY_PREFIX: [u8; 14] = *b"Approvals_cand";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&CANDIDATE_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(candidate_hash.0.as_ref());
key
}
/// The key a set of block hashes corresponding to a block number is stored under.
pub(crate) fn blocks_at_height_key(block_number: BlockNumber) -> [u8; 16] {
const BLOCKS_AT_HEIGHT_PREFIX: [u8; 12] = *b"Approvals_at";
let mut key = [0u8; 12 + 4];
key[0..12].copy_from_slice(&BLOCKS_AT_HEIGHT_PREFIX);
block_number.using_encoded(|s| key[12..16].copy_from_slice(s));
key
}
/// Return all blocks which have entries in the DB, ascending, by height.
pub fn load_all_blocks(store: &dyn Database, config: &Config) -> SubsystemResult<Vec<Hash>> {
let mut hashes = Vec::new();
if let Some(stored_blocks) = load_stored_blocks(store, config)? {
for height in stored_blocks.0..stored_blocks.1 {
let blocks = load_blocks_at_height(store, config, &height)?;
hashes.extend(blocks);
}
}
Ok(hashes)
}
/// Load the stored-blocks key from the state.
pub fn load_stored_blocks(
store: &dyn Database,
config: &Config,
) -> SubsystemResult<Option<StoredBlockRange>> {
load_decode(store, config.col_approval_data, STORED_BLOCKS_KEY)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a blocks-at-height entry for a given block number.
pub fn load_blocks_at_height(
store: &dyn Database,
config: &Config,
block_number: &BlockNumber,
) -> SubsystemResult<Vec<Hash>> {
load_decode(store, config.col_approval_data, &blocks_at_height_key(*block_number))
.map(|x| x.unwrap_or_default())
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store.
pub fn load_block_entry(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store in current version format.
pub fn load_candidate_entry(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -0,0 +1,36 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Approval DB accessors and writers for on-disk persisted approval storage
//! data.
//!
//! We persist data to disk although it is not intended to be used across runs of the
//! program. This is because under medium to long periods of finality stalling, for whatever
//! reason that may be, the amount of data we'd need to keep would be potentially too large
//! for memory.
//!
//! With tens or hundreds of teyrchains, hundreds of validators, and parablocks
//! in every relay chain block, there can be a humongous amount of information to reference
//! at any given time.
//!
//! As such, we provide a function from this module to clear the database on start-up.
//! In the future, we may use a temporary DB which doesn't need to be wiped, but for the
//! time being we share the same DB with the rest of Substrate.
pub mod common;
pub mod v1;
pub mod v2;
pub mod v3;
@@ -0,0 +1,91 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Version 1 of the DB schema.
//!
//! Note that the version here differs from the actual version of the teyrchains
//! database (check `CURRENT_VERSION` in `node/service/src/teyrchains_db/upgrade.rs`).
//! The code in this module implements the way approval voting works with
//! its data in the database. Any breaking changes here will still
//! require a db migration (check `node/service/src/teyrchains_db/upgrade.rs`).
use codec::{Decode, Encode};
use pezkuwi_node_primitives::approval::v1::{AssignmentCert, DelayTranche};
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateReceiptV2 as CandidateReceipt, CoreIndex, GroupIndex,
Hash, SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use std::collections::BTreeMap;
use super::v2::Bitfield;
/// Details pertaining to our assignment on a block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurAssignment {
pub cert: AssignmentCert,
pub tranche: DelayTranche,
pub validator_index: ValidatorIndex,
// Whether the assignment has been triggered already.
pub triggered: bool,
}
use super::v2::TrancheEntry;
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct ApprovalEntry {
pub tranches: Vec<TrancheEntry>,
pub backing_group: GroupIndex,
pub our_assignment: Option<OurAssignment>,
pub our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
pub assignments: Bitfield,
pub approved: bool,
}
/// Metadata regarding approval of a particular candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct CandidateEntry {
pub candidate: CandidateReceipt,
pub session: SessionIndex,
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: Bitfield,
}
/// Metadata regarding approval of a particular block, by way of approval of the
/// candidates contained within it.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct BlockEntry {
pub block_hash: Hash,
pub block_number: BlockNumber,
pub parent_hash: Hash,
pub session: SessionIndex,
pub slot: Slot,
/// Random bytes derived from the VRF submitted within the block by the block
/// author as a credential and used as input to approval assignment criteria.
pub relay_vrf_story: [u8; 32],
// The candidates included as-of this block and the index of the core they are
// leaving. Sorted ascending by core index.
pub candidates: Vec<(CoreIndex, CandidateHash)>,
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
}
@@ -0,0 +1,567 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the aux-schema of approval voting.
use super::{DbBackend, StoredBlockRange, *};
use crate::{
backend::{Backend, OverlayedBackend},
ops::{add_block_entry, canonicalize, force_approve, NewCandidateInfo},
};
use pezkuwi_node_subsystem_util::database::Database;
use pezkuwi_primitives::Id as ParaId;
use std::{collections::HashMap, sync::Arc};
use pezkuwi_primitives_test_helpers::{
dummy_candidate_receipt, dummy_candidate_receipt_bad_sig, dummy_hash,
};
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: Config = Config { col_approval_data: DATA_COL };
fn make_db() -> (DbBackend, Arc<dyn Database>) {
let db = kvdb_memorydb::create(NUM_COLUMNS);
let db = pezkuwi_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[]);
let db_writer: Arc<dyn Database> = Arc::new(db);
(DbBackend::new(db_writer.clone(), TEST_CONFIG), db_writer)
}
fn make_block_entry(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> BlockEntry {
BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
}
}
fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
let mut c = dummy_candidate_receipt(dummy_hash());
c.descriptor.para_id = para_id;
c.descriptor.relay_parent = relay_parent;
c
}
#[test]
fn read_write() {
let (mut db, store) = make_db();
let hash_a = Hash::repeat_byte(1);
let hash_b = Hash::repeat_byte(2);
let candidate_hash = dummy_candidate_receipt_bad_sig(dummy_hash(), None).hash();
let range = StoredBlockRange(10, 20);
let at_height = vec![hash_a, hash_b];
let block_entry =
make_block_entry(hash_a, Default::default(), 1, vec![(CoreIndex(0), candidate_hash)]);
let candidate_entry = CandidateEntry {
candidate: dummy_candidate_receipt_bad_sig(dummy_hash(), None),
session: 5,
block_assignments: vec![(
hash_a,
ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: None,
our_approval_sig: None,
assignments: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(range.clone());
overlay_db.write_blocks_at_height(1, at_height.clone());
overlay_db.write_block_entry(block_entry.clone().into());
overlay_db.write_candidate_entry(candidate_entry.clone().into());
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap(), at_height);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
Some(block_entry.into())
);
assert_eq!(
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry.into()),
);
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.delete_blocks_at_height(1);
overlay_db.delete_block_entry(&hash_a);
overlay_db.delete_candidate_entry(&candidate_hash);
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap().is_empty());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash)
.unwrap()
.is_none());
}
#[test]
fn add_block_entry_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let candidate_receipt_a = make_candidate(ParaId::from(1_u32), parent_hash);
let candidate_receipt_b = make_candidate(ParaId::from(2_u32), parent_hash);
let candidate_hash_a = candidate_receipt_a.hash();
let candidate_hash_b = candidate_receipt_b.hash();
let block_number = 10;
let block_entry_a = make_block_entry(
block_hash_a,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a)],
);
let block_entry_b = make_block_entry(
block_hash_b,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a), (CoreIndex(1), candidate_hash_b)],
);
let n_validators = 10;
let mut new_candidate_info = HashMap::new();
new_candidate_info
.insert(candidate_hash_a, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(0), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
new_candidate_info
.insert(candidate_hash_b, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(1), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
let candidate_entry_a = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
assert_eq!(
candidate_entry_a.block_assignments.keys().collect::<Vec<_>>(),
vec![&block_hash_a, &block_hash_b]
);
let candidate_entry_b = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
assert_eq!(candidate_entry_b.block_assignments.keys().collect::<Vec<_>>(), vec![&block_hash_b]);
}
#[test]
fn add_block_entry_adds_child() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let mut block_entry_a = make_block_entry(block_hash_a, parent_hash, 1, Vec::new());
let block_entry_b = make_block_entry(block_hash_b, block_hash_a, 2, Vec::new());
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
block_entry_a.children.push(block_hash_b);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
}
#[test]
fn canonicalize_works() {
let (mut db, store) = make_db();
// -> B1 -> C1 -> D1
// A -> B2 -> C2 -> D2
//
// We'll canonicalize C1. Everything except D1 should disappear.
//
// Candidates:
// Cand1 in B2
// Cand2 in C2
// Cand3 in C2 and D1
// Cand4 in D1
// Cand5 in D2
// Only Cand3 and Cand4 should remain after canonicalize.
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 5));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let genesis = Hash::repeat_byte(0);
let block_hash_a = Hash::repeat_byte(1);
let block_hash_b1 = Hash::repeat_byte(2);
let block_hash_b2 = Hash::repeat_byte(3);
let block_hash_c1 = Hash::repeat_byte(4);
let block_hash_c2 = Hash::repeat_byte(5);
let block_hash_d1 = Hash::repeat_byte(6);
let block_hash_d2 = Hash::repeat_byte(7);
let candidate_receipt_genesis = make_candidate(ParaId::from(1_u32), genesis);
let candidate_receipt_a = make_candidate(ParaId::from(2_u32), block_hash_a);
let candidate_receipt_b = make_candidate(ParaId::from(3_u32), block_hash_a);
let candidate_receipt_b1 = make_candidate(ParaId::from(4_u32), block_hash_b1);
let candidate_receipt_c1 = make_candidate(ParaId::from(5_u32), block_hash_c1);
let cand_hash_1 = candidate_receipt_genesis.hash();
let cand_hash_2 = candidate_receipt_a.hash();
let cand_hash_3 = candidate_receipt_b.hash();
let cand_hash_4 = candidate_receipt_b1.hash();
let cand_hash_5 = candidate_receipt_c1.hash();
let block_entry_a = make_block_entry(block_hash_a, genesis, 1, Vec::new());
let block_entry_b1 = make_block_entry(block_hash_b1, block_hash_a, 2, Vec::new());
let block_entry_b2 =
make_block_entry(block_hash_b2, block_hash_a, 2, vec![(CoreIndex(0), cand_hash_1)]);
let block_entry_c1 = make_block_entry(block_hash_c1, block_hash_b1, 3, Vec::new());
let block_entry_c2 = make_block_entry(
block_hash_c2,
block_hash_b2,
3,
vec![(CoreIndex(0), cand_hash_2), (CoreIndex(1), cand_hash_3)],
);
let block_entry_d1 = make_block_entry(
block_hash_d1,
block_hash_c1,
4,
vec![(CoreIndex(0), cand_hash_3), (CoreIndex(1), cand_hash_4)],
);
let block_entry_d2 =
make_block_entry(block_hash_d2, block_hash_c2, 4, vec![(CoreIndex(0), cand_hash_5)]);
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
cand_hash_1,
NewCandidateInfo::new(candidate_receipt_genesis, GroupIndex(1), None),
);
candidate_info
.insert(cand_hash_2, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(2), None));
candidate_info
.insert(cand_hash_3, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(3), None));
candidate_info
.insert(cand_hash_4, NewCandidateInfo::new(candidate_receipt_b1, GroupIndex(4), None));
candidate_info
.insert(cand_hash_5, NewCandidateInfo::new(candidate_receipt_c1, GroupIndex(5), None));
candidate_info
};
// now insert all the blocks.
let blocks = vec![
block_entry_a.clone(),
block_entry_b1.clone(),
block_entry_b2.clone(),
block_entry_c1.clone(),
block_entry_c2.clone(),
block_entry_d1.clone(),
block_entry_d2.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let check_candidates_in_store = |expected: Vec<(CandidateHash, Option<Vec<_>>)>| {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.is_none());
continue
},
Some(i) => (
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash).unwrap().unwrap(),
i,
),
};
assert_eq!(entry.block_assignments.len(), in_blocks.len());
for x in in_blocks {
assert!(entry.block_assignments.contains_key(&x));
}
}
};
let check_blocks_in_store = |expected: Vec<(Hash, Option<Vec<_>>)>| {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.is_none());
continue
},
Some(i) =>
(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
};
assert_eq!(entry.candidates.len(), with_candidates.len());
for x in with_candidates {
assert!(entry.candidates.iter().any(|(_, c)| c == &x));
}
}
};
check_candidates_in_store(vec![
(cand_hash_1, Some(vec![block_hash_b2])),
(cand_hash_2, Some(vec![block_hash_c2])),
(cand_hash_3, Some(vec![block_hash_c2, block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, Some(vec![block_hash_d2])),
]);
check_blocks_in_store(vec![
(block_hash_a, Some(vec![])),
(block_hash_b1, Some(vec![])),
(block_hash_b2, Some(vec![cand_hash_1])),
(block_hash_c1, Some(vec![])),
(block_hash_c2, Some(vec![cand_hash_2, cand_hash_3])),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, Some(vec![cand_hash_5])),
]);
let mut overlay_db = OverlayedBackend::new(&db);
canonicalize(&mut overlay_db, 3, block_hash_c1).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap().unwrap(),
StoredBlockRange(4, 5)
);
check_candidates_in_store(vec![
(cand_hash_1, None),
(cand_hash_2, None),
(cand_hash_3, Some(vec![block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, None),
]);
check_blocks_in_store(vec![
(block_hash_a, None),
(block_hash_b1, None),
(block_hash_b2, None),
(block_hash_c1, None),
(block_hash_c2, None),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, None),
]);
}
#[test]
fn force_approve_works() {
let (mut db, store) = make_db();
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 4));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let single_candidate_vec = vec![(CoreIndex(0), candidate_hash)];
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
candidate_hash,
NewCandidateInfo::new(
make_candidate(ParaId::from(1_u32), Default::default()),
GroupIndex(1),
None,
),
);
candidate_info
};
let block_hash_a = Hash::repeat_byte(1); // 1
let block_hash_b = Hash::repeat_byte(2);
let block_hash_c = Hash::repeat_byte(3);
let block_hash_d = Hash::repeat_byte(4); // 4
let block_entry_a =
make_block_entry(block_hash_a, Default::default(), 1, single_candidate_vec.clone());
let block_entry_b =
make_block_entry(block_hash_b, block_hash_a, 2, single_candidate_vec.clone());
let block_entry_c =
make_block_entry(block_hash_c, block_hash_b, 3, single_candidate_vec.clone());
let block_entry_d =
make_block_entry(block_hash_d, block_hash_c, 4, single_candidate_vec.clone());
let blocks = vec![
block_entry_a.clone(),
block_entry_b.clone(),
block_entry_c.clone(),
block_entry_d.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let approved_hashes = force_approve(&mut overlay_db, block_hash_d, 2).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert_eq!(approved_hashes, vec![block_hash_b, block_hash_a]);
}
#[test]
fn load_all_blocks_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let block_hash_c = Hash::repeat_byte(42);
let block_number = 10;
let block_entry_a = make_block_entry(block_hash_a, parent_hash, block_number, vec![]);
let block_entry_b = make_block_entry(block_hash_b, parent_hash, block_number, vec![]);
let block_entry_c = make_block_entry(block_hash_c, block_hash_a, block_number + 1, vec![]);
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
// add C before B to test sorting.
add_block_entry(&mut overlay_db, block_entry_c.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_all_blocks(store.as_ref(), &TEST_CONFIG).unwrap(),
vec![block_hash_a, block_hash_b, block_hash_c],
)
}
@@ -0,0 +1,202 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Approval DB migration helpers.
use super::*;
use crate::{
approval_db::common::{
migration_helpers::{dummy_assignment_cert, make_bitvec},
Error, Result, StoredBlockRange,
},
backend::Backend,
};
use pezkuwi_node_primitives::approval::v1::AssignmentCertKind;
use pezkuwi_node_subsystem_util::database::Database;
use sp_application_crypto::sp_core::H256;
use std::{collections::HashSet, sync::Arc};
fn make_block_entry_v1(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> crate::approval_db::v1::BlockEntry {
crate::approval_db::v1::BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
}
}
/// Migrates `OurAssignment`, `CandidateEntry` and `ApprovalEntry` to version 2.
/// Returns on any error.
/// Must only be used in teyrchains DB migration code - `pezkuwi-service` crate.
pub fn v1_to_latest(db: Arc<dyn Database>, config: Config) -> Result<()> {
let mut backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.map_err(|e| Error::InternalError(e))?
.iter()
.filter_map(|block_hash| {
backend
.load_block_entry_v1(block_hash)
.map_err(|e| Error::InternalError(e))
.ok()?
})
.collect::<Vec<_>>();
gum::info!(
target: crate::LOG_TARGET,
"Migrating candidate entries on top of {} blocks",
all_blocks.len()
);
let mut overlay = crate::OverlayedBackend::new(&backend);
let mut counter = 0;
// Get all candidate entries, approval entries and convert each of them.
for block in all_blocks {
for (candidate_index, (_core_index, candidate_hash)) in
block.candidates().iter().enumerate()
{
// Loading the candidate will also perform the conversion to the updated format and
// return that representation.
if let Some(candidate_entry) = backend
.load_candidate_entry_v1(&candidate_hash, candidate_index as CandidateIndex)
.map_err(|e| Error::InternalError(e))?
{
// Write the updated representation.
overlay.write_candidate_entry(candidate_entry);
counter += 1;
}
}
overlay.write_block_entry(block);
}
gum::info!(target: crate::LOG_TARGET, "Migrated {} entries", counter);
// Commit all changes to DB.
let write_ops = overlay.into_write_ops();
backend.write(write_ops).unwrap();
Ok(())
}
// Fills the db with dummy data in v1 scheme.
pub fn v1_fill_test_data<F>(
db: Arc<dyn Database>,
config: Config,
dummy_candidate_create: F,
) -> Result<HashSet<CandidateHash>>
where
F: Fn(H256) -> CandidateReceipt<H256>,
{
let mut backend = crate::DbBackend::new(db.clone(), config);
let mut overlay_db = crate::OverlayedBackend::new(&backend);
let mut expected_candidates = HashSet::new();
const RELAY_BLOCK_COUNT: u32 = 10;
let range = StoredBlockRange(1, 11);
overlay_db.write_stored_block_range(range.clone());
for relay_number in 1..=RELAY_BLOCK_COUNT {
let relay_hash = Hash::repeat_byte(relay_number as u8);
let assignment_core_index = CoreIndex(relay_number);
let candidate = dummy_candidate_create(relay_hash);
let candidate_hash = candidate.hash();
let at_height = vec![relay_hash];
let block_entry = make_block_entry_v1(
relay_hash,
Default::default(),
relay_number,
vec![(assignment_core_index, candidate_hash)],
);
let dummy_assignment = crate::approval_db::v1::OurAssignment {
cert: dummy_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }).into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
};
let candidate_entry = crate::approval_db::v1::CandidateEntry {
candidate,
session: 123,
block_assignments: vec![(
relay_hash,
crate::approval_db::v1::ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: Some(dummy_assignment),
our_approval_sig: None,
assignments: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
overlay_db.write_blocks_at_height(relay_number, at_height.clone());
expected_candidates.insert(candidate_entry.candidate.hash());
db.write(write_candidate_entry_v1(candidate_entry, config)).unwrap();
db.write(write_block_entry_v1(block_entry, config)).unwrap();
}
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
Ok(expected_candidates)
}
// Low level DB helper to write a candidate entry in v1 scheme.
fn write_candidate_entry_v1(
candidate_entry: crate::approval_db::v1::CandidateEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
tx
}
// Low level DB helper to write a block entry in v1 scheme.
fn write_block_entry_v1(
block_entry: crate::approval_db::v1::BlockEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
tx
}
@@ -0,0 +1,153 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Version 2 of the DB schema.
use codec::{Decode, Encode};
use pezkuwi_node_primitives::approval::{v1::DelayTranche, v2::AssignmentCertV2};
use pezkuwi_node_subsystem::{SubsystemError, SubsystemResult};
use pezkuwi_node_subsystem_util::database::{DBTransaction, Database};
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateIndex, CandidateReceiptV2 as CandidateReceipt, CoreIndex,
GroupIndex, Hash, SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use std::collections::BTreeMap;
use crate::backend::V1ReadBackend;
use super::common::{block_entry_key, candidate_entry_key, load_decode, Config};
pub mod migration_helpers;
#[cfg(test)]
pub mod tests;
// slot_duration * 2 + DelayTranche gives the number of delay tranches since the
// unix epoch.
#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq)]
pub struct Tick(u64);
/// Convenience type definition
pub type Bitfield = BitVec<u8, BitOrderLsb0>;
/// Details pertaining to our assignment on a block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurAssignment {
/// Our assignment certificate.
pub cert: AssignmentCertV2,
/// The tranche for which the assignment refers to.
pub tranche: DelayTranche,
/// Our validator index for the session in which the candidates were included.
pub validator_index: ValidatorIndex,
/// Whether the assignment has been triggered already.
pub triggered: bool,
}
/// Metadata regarding a specific tranche of assignments for a specific candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct TrancheEntry {
pub tranche: DelayTranche,
// Assigned validators, and the instant we received their assignment, rounded
// to the nearest tick.
pub assignments: Vec<(ValidatorIndex, Tick)>,
}
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct ApprovalEntry {
pub tranches: Vec<TrancheEntry>,
pub backing_group: GroupIndex,
pub our_assignment: Option<OurAssignment>,
pub our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
pub assigned_validators: Bitfield,
pub approved: bool,
}
/// Metadata regarding approval of a particular candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct CandidateEntry {
pub candidate: CandidateReceipt,
pub session: SessionIndex,
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: Bitfield,
}
/// Metadata regarding approval of a particular block, by way of approval of the
/// candidates contained within it.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct BlockEntry {
pub block_hash: Hash,
pub block_number: BlockNumber,
pub parent_hash: Hash,
pub session: SessionIndex,
pub slot: Slot,
/// Random bytes derived from the VRF submitted within the block by the block
/// author as a credential and used as input to approval assignment criteria.
pub relay_vrf_story: [u8; 32],
// The candidates included as-of this block and the index of the core they are
// leaving. Sorted ascending by core index.
pub candidates: Vec<(CoreIndex, CandidateHash)>,
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// Assignments we already distributed. A 1 bit means the candidate index for which
// we already have sent out an assignment. We need this to avoid distributing
// multiple core assignments more than once.
pub distributed_assignments: Bitfield,
}
impl From<crate::Tick> for Tick {
fn from(tick: crate::Tick) -> Tick {
Tick(tick)
}
}
impl From<Tick> for crate::Tick {
fn from(tick: Tick) -> crate::Tick {
tick.0
}
}
/// Load a candidate entry from the aux store in v1 format.
pub fn load_candidate_entry_v1(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<super::v1::CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<super::v1::CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store in v1 format.
pub fn load_block_entry_v1(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<super::v1::BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<super::v1::BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -0,0 +1,586 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the aux-schema of approval voting.
use crate::{
approval_db::{
common::{migration_helpers::make_bitvec, DbBackend, StoredBlockRange, *},
v2::*,
v3::{load_block_entry_v2, load_candidate_entry_v2},
},
backend::{Backend, OverlayedBackend},
ops::{add_block_entry, canonicalize, force_approve, NewCandidateInfo},
};
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateReceiptV2 as CandidateReceipt, CoreIndex, GroupIndex,
Hash, MutateDescriptorV2,
};
use pezkuwi_node_subsystem_util::database::Database;
use pezkuwi_primitives::Id as ParaId;
use sp_consensus_slots::Slot;
use std::{collections::HashMap, sync::Arc};
use pezkuwi_primitives_test_helpers::{
dummy_candidate_receipt_bad_sig, dummy_candidate_receipt_v2,
dummy_candidate_receipt_v2_bad_sig, dummy_hash,
};
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: Config = Config { col_approval_data: DATA_COL };
fn make_db() -> (DbBackend, Arc<dyn Database>) {
let db = kvdb_memorydb::create(NUM_COLUMNS);
let db = pezkuwi_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[]);
let db_writer: Arc<dyn Database> = Arc::new(db);
(DbBackend::new(db_writer.clone(), TEST_CONFIG), db_writer)
}
fn make_block_entry(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> BlockEntry {
BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
distributed_assignments: Default::default(),
}
}
fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
let mut c = dummy_candidate_receipt_v2(dummy_hash());
c.descriptor.set_para_id(para_id);
c.descriptor.set_relay_parent(relay_parent);
c
}
#[test]
fn read_write() {
let (mut db, store) = make_db();
let hash_a = Hash::repeat_byte(1);
let hash_b = Hash::repeat_byte(2);
let candidate_hash = dummy_candidate_receipt_bad_sig(dummy_hash(), None).hash();
let range = StoredBlockRange(10, 20);
let at_height = vec![hash_a, hash_b];
let block_entry =
make_block_entry(hash_a, Default::default(), 1, vec![(CoreIndex(0), candidate_hash)]);
let candidate_entry = CandidateEntry {
candidate: dummy_candidate_receipt_v2_bad_sig(dummy_hash(), None),
session: 5,
block_assignments: vec![(
hash_a,
ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: None,
our_approval_sig: None,
assigned_validators: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(range.clone());
overlay_db.write_blocks_at_height(1, at_height.clone());
overlay_db.write_block_entry(block_entry.clone().into());
overlay_db.write_candidate_entry(crate::persisted_entries::CandidateEntry::from_v2(
candidate_entry.clone(),
0,
));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap(), at_height);
assert_eq!(
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
Some(block_entry.into())
);
assert_eq!(
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry.into()),
);
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.delete_blocks_at_height(1);
overlay_db.delete_block_entry(&hash_a);
overlay_db.delete_candidate_entry(&candidate_hash);
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap().is_empty());
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash)
.unwrap()
.is_none());
}
#[test]
fn add_block_entry_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let candidate_receipt_a = make_candidate(ParaId::from(1_u32), parent_hash);
let candidate_receipt_b = make_candidate(ParaId::from(2_u32), parent_hash);
let candidate_hash_a = candidate_receipt_a.hash();
let candidate_hash_b = candidate_receipt_b.hash();
let block_number = 10;
let block_entry_a = make_block_entry(
block_hash_a,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a)],
);
let block_entry_b = make_block_entry(
block_hash_b,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a), (CoreIndex(1), candidate_hash_b)],
);
let n_validators = 10;
let mut new_candidate_info = HashMap::new();
new_candidate_info
.insert(candidate_hash_a, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(0), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
new_candidate_info
.insert(candidate_hash_b, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(1), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
let candidate_entry_a =
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
assert_eq!(
candidate_entry_a.block_assignments.keys().collect::<Vec<_>>(),
vec![&block_hash_a, &block_hash_b]
);
let candidate_entry_b =
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
assert_eq!(candidate_entry_b.block_assignments.keys().collect::<Vec<_>>(), vec![&block_hash_b]);
}
#[test]
fn add_block_entry_adds_child() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let mut block_entry_a = make_block_entry(block_hash_a, parent_hash, 1, Vec::new());
let block_entry_b = make_block_entry(block_hash_b, block_hash_a, 2, Vec::new());
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
block_entry_a.children.push(block_hash_b);
assert_eq!(
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
}
#[test]
fn canonicalize_works() {
let (mut db, store) = make_db();
// -> B1 -> C1 -> D1
// A -> B2 -> C2 -> D2
//
// We'll canonicalize C1. Everything except D1 should disappear.
//
// Candidates:
// Cand1 in B2
// Cand2 in C2
// Cand3 in C2 and D1
// Cand4 in D1
// Cand5 in D2
// Only Cand3 and Cand4 should remain after canonicalize.
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 5));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let genesis = Hash::repeat_byte(0);
let block_hash_a = Hash::repeat_byte(1);
let block_hash_b1 = Hash::repeat_byte(2);
let block_hash_b2 = Hash::repeat_byte(3);
let block_hash_c1 = Hash::repeat_byte(4);
let block_hash_c2 = Hash::repeat_byte(5);
let block_hash_d1 = Hash::repeat_byte(6);
let block_hash_d2 = Hash::repeat_byte(7);
let candidate_receipt_genesis = make_candidate(ParaId::from(1_u32), genesis);
let candidate_receipt_a = make_candidate(ParaId::from(2_u32), block_hash_a);
let candidate_receipt_b = make_candidate(ParaId::from(3_u32), block_hash_a);
let candidate_receipt_b1 = make_candidate(ParaId::from(4_u32), block_hash_b1);
let candidate_receipt_c1 = make_candidate(ParaId::from(5_u32), block_hash_c1);
let cand_hash_1 = candidate_receipt_genesis.hash();
let cand_hash_2 = candidate_receipt_a.hash();
let cand_hash_3 = candidate_receipt_b.hash();
let cand_hash_4 = candidate_receipt_b1.hash();
let cand_hash_5 = candidate_receipt_c1.hash();
let block_entry_a = make_block_entry(block_hash_a, genesis, 1, Vec::new());
let block_entry_b1 = make_block_entry(block_hash_b1, block_hash_a, 2, Vec::new());
let block_entry_b2 =
make_block_entry(block_hash_b2, block_hash_a, 2, vec![(CoreIndex(0), cand_hash_1)]);
let block_entry_c1 = make_block_entry(block_hash_c1, block_hash_b1, 3, Vec::new());
let block_entry_c2 = make_block_entry(
block_hash_c2,
block_hash_b2,
3,
vec![(CoreIndex(0), cand_hash_2), (CoreIndex(1), cand_hash_3)],
);
let block_entry_d1 = make_block_entry(
block_hash_d1,
block_hash_c1,
4,
vec![(CoreIndex(0), cand_hash_3), (CoreIndex(1), cand_hash_4)],
);
let block_entry_d2 =
make_block_entry(block_hash_d2, block_hash_c2, 4, vec![(CoreIndex(0), cand_hash_5)]);
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
cand_hash_1,
NewCandidateInfo::new(candidate_receipt_genesis, GroupIndex(1), None),
);
candidate_info
.insert(cand_hash_2, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(2), None));
candidate_info
.insert(cand_hash_3, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(3), None));
candidate_info
.insert(cand_hash_4, NewCandidateInfo::new(candidate_receipt_b1, GroupIndex(4), None));
candidate_info
.insert(cand_hash_5, NewCandidateInfo::new(candidate_receipt_c1, GroupIndex(5), None));
candidate_info
};
// now insert all the blocks.
let blocks = vec![
block_entry_a.clone(),
block_entry_b1.clone(),
block_entry_b2.clone(),
block_entry_c1.clone(),
block_entry_c2.clone(),
block_entry_d1.clone(),
block_entry_d2.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let check_candidates_in_store = |expected: Vec<(CandidateHash, Option<Vec<_>>)>| {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.is_none());
continue;
},
Some(i) => (
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.unwrap(),
i,
),
};
assert_eq!(entry.block_assignments.len(), in_blocks.len());
for x in in_blocks {
assert!(entry.block_assignments.contains_key(&x));
}
}
};
let check_blocks_in_store = |expected: Vec<(Hash, Option<Vec<_>>)>| {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.is_none());
continue;
},
Some(i) =>
(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
};
assert_eq!(entry.candidates.len(), with_candidates.len());
for x in with_candidates {
assert!(entry.candidates.iter().any(|(_, c)| c == &x));
}
}
};
check_candidates_in_store(vec![
(cand_hash_1, Some(vec![block_hash_b2])),
(cand_hash_2, Some(vec![block_hash_c2])),
(cand_hash_3, Some(vec![block_hash_c2, block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, Some(vec![block_hash_d2])),
]);
check_blocks_in_store(vec![
(block_hash_a, Some(vec![])),
(block_hash_b1, Some(vec![])),
(block_hash_b2, Some(vec![cand_hash_1])),
(block_hash_c1, Some(vec![])),
(block_hash_c2, Some(vec![cand_hash_2, cand_hash_3])),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, Some(vec![cand_hash_5])),
]);
let mut overlay_db = OverlayedBackend::new(&db);
canonicalize(&mut overlay_db, 3, block_hash_c1).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap().unwrap(),
StoredBlockRange(4, 5)
);
check_candidates_in_store(vec![
(cand_hash_1, None),
(cand_hash_2, None),
(cand_hash_3, Some(vec![block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, None),
]);
check_blocks_in_store(vec![
(block_hash_a, None),
(block_hash_b1, None),
(block_hash_b2, None),
(block_hash_c1, None),
(block_hash_c2, None),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, None),
]);
}
#[test]
fn force_approve_works() {
let (mut db, store) = make_db();
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 4));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let single_candidate_vec = vec![(CoreIndex(0), candidate_hash)];
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
candidate_hash,
NewCandidateInfo::new(
make_candidate(ParaId::from(1_u32), Default::default()),
GroupIndex(1),
None,
),
);
candidate_info
};
let block_hash_a = Hash::repeat_byte(1); // 1
let block_hash_b = Hash::repeat_byte(2);
let block_hash_c = Hash::repeat_byte(3);
let block_hash_d = Hash::repeat_byte(4); // 4
let block_entry_a =
make_block_entry(block_hash_a, Default::default(), 1, single_candidate_vec.clone());
let block_entry_b =
make_block_entry(block_hash_b, block_hash_a, 2, single_candidate_vec.clone());
let block_entry_c =
make_block_entry(block_hash_c, block_hash_b, 3, single_candidate_vec.clone());
let block_entry_d =
make_block_entry(block_hash_d, block_hash_c, 4, single_candidate_vec.clone());
let blocks = vec![
block_entry_a.clone(),
block_entry_b.clone(),
block_entry_c.clone(),
block_entry_d.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let approved_hashes = force_approve(&mut overlay_db, block_hash_d, 2).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert_eq!(approved_hashes, vec![block_hash_b, block_hash_a]);
}
#[test]
fn load_all_blocks_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let block_hash_c = Hash::repeat_byte(42);
let block_number = 10;
let block_entry_a = make_block_entry(block_hash_a, parent_hash, block_number, vec![]);
let block_entry_b = make_block_entry(block_hash_b, parent_hash, block_number, vec![]);
let block_entry_c = make_block_entry(block_hash_c, block_hash_a, block_number + 1, vec![]);
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
// add C before B to test sorting.
add_block_entry(&mut overlay_db, block_entry_c.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_all_blocks(store.as_ref(), &TEST_CONFIG).unwrap(),
vec![block_hash_a, block_hash_b, block_hash_c],
)
}
@@ -0,0 +1,237 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Approval DB migration helpers.
use super::*;
use crate::{
approval_db::common::{
block_entry_key, candidate_entry_key,
migration_helpers::{dummy_assignment_cert, make_bitvec},
Config, Error, Result, StoredBlockRange,
},
backend::{Backend, V2ReadBackend},
};
use pezkuwi_node_primitives::approval::v1::AssignmentCertKind;
use pezkuwi_node_subsystem_util::database::Database;
use sp_application_crypto::sp_core::H256;
use std::{collections::HashSet, sync::Arc};
/// Migrates `BlockEntry`, `CandidateEntry`, `ApprovalEntry` and `OurApproval` to version 3.
/// Returns on any error.
/// Must only be used in teyrchains DB migration code - `pezkuwi-service` crate.
pub fn v2_to_latest(db: Arc<dyn Database>, config: Config) -> Result<()> {
let mut backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.map_err(|e| Error::InternalError(e))?
.iter()
.filter_map(|block_hash| {
backend
.load_block_entry_v2(block_hash)
.map_err(|e| Error::InternalError(e))
.ok()?
})
.collect::<Vec<_>>();
gum::info!(
target: crate::LOG_TARGET,
"Migrating candidate entries on top of {} blocks",
all_blocks.len()
);
let mut overlay = crate::OverlayedBackend::new(&backend);
let mut counter = 0;
// Get all candidate entries, approval entries and convert each of them.
for block in all_blocks {
for (candidate_index, (_core_index, candidate_hash)) in
block.candidates().iter().enumerate()
{
// Loading the candidate will also perform the conversion to the updated format and
// return that representation.
if let Some(candidate_entry) = backend
.load_candidate_entry_v2(&candidate_hash, candidate_index as CandidateIndex)
.map_err(|e| Error::InternalError(e))?
{
// Write the updated representation.
overlay.write_candidate_entry(candidate_entry);
counter += 1;
}
}
overlay.write_block_entry(block);
}
gum::info!(target: crate::LOG_TARGET, "Migrated {} entries", counter);
// Commit all changes to DB.
let write_ops = overlay.into_write_ops();
backend.write(write_ops).unwrap();
Ok(())
}
// Checks if the migration doesn't leave the DB in an unsane state.
// This function is to be used in tests.
pub fn v1_to_latest_sanity_check(
db: Arc<dyn Database>,
config: Config,
expected_candidates: HashSet<CandidateHash>,
) -> Result<()> {
let backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.unwrap()
.iter()
.map(|block_hash| backend.load_block_entry(block_hash).unwrap().unwrap())
.collect::<Vec<_>>();
let mut candidates = HashSet::new();
// Iterate all blocks and approval entries.
for block in all_blocks {
for (_core_index, candidate_hash) in block.candidates() {
// Loading the candidate will also perform the conversion to the updated format and
// return that representation.
if let Some(candidate_entry) = backend.load_candidate_entry(&candidate_hash).unwrap() {
candidates.insert(candidate_entry.candidate.hash());
}
}
}
assert_eq!(candidates, expected_candidates);
Ok(())
}
// Fills the db with dummy data in v2 scheme.
pub fn v2_fill_test_data<F>(
db: Arc<dyn Database>,
config: Config,
dummy_candidate_create: F,
) -> Result<HashSet<CandidateHash>>
where
F: Fn(H256) -> CandidateReceipt<H256>,
{
let mut backend = crate::DbBackend::new(db.clone(), config);
let mut overlay_db = crate::OverlayedBackend::new(&backend);
let mut expected_candidates = HashSet::new();
const RELAY_BLOCK_COUNT: u32 = 10;
let range = StoredBlockRange(1, 11);
overlay_db.write_stored_block_range(range.clone());
for relay_number in 1..=RELAY_BLOCK_COUNT {
let relay_hash = Hash::repeat_byte(relay_number as u8);
let assignment_core_index = CoreIndex(relay_number);
let candidate = dummy_candidate_create(relay_hash);
let candidate_hash = candidate.hash();
let at_height = vec![relay_hash];
let block_entry = make_block_entry_v2(
relay_hash,
Default::default(),
relay_number,
vec![(assignment_core_index, candidate_hash)],
);
let dummy_assignment = crate::approval_db::v2::OurAssignment {
cert: dummy_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }).into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
};
let candidate_entry = crate::approval_db::v2::CandidateEntry {
candidate,
session: 123,
block_assignments: vec![(
relay_hash,
crate::approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: Some(dummy_assignment),
our_approval_sig: None,
approved: false,
assigned_validators: make_bitvec(1),
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
overlay_db.write_blocks_at_height(relay_number, at_height.clone());
expected_candidates.insert(candidate_entry.candidate.hash());
db.write(write_candidate_entry_v2(candidate_entry, config)).unwrap();
db.write(write_block_entry_v2(block_entry, config)).unwrap();
}
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
Ok(expected_candidates)
}
fn make_block_entry_v2(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> crate::approval_db::v2::BlockEntry {
crate::approval_db::v2::BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
distributed_assignments: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
}
}
// Low level DB helper to write a candidate entry in v1 scheme.
fn write_candidate_entry_v2(
candidate_entry: crate::approval_db::v2::CandidateEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
tx
}
// Low level DB helper to write a block entry in v1 scheme.
fn write_block_entry_v2(
block_entry: crate::approval_db::v2::BlockEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
tx
}
@@ -0,0 +1,137 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Version 3 of the DB schema.
//!
//! Version 3 modifies the `our_approval` format of `ApprovalEntry`
//! and adds a new field `pending_signatures` for `BlockEntry`
use codec::{Decode, Encode};
use pezkuwi_node_primitives::approval::v2::CandidateBitfield;
use pezkuwi_node_subsystem::SubsystemResult;
use pezkuwi_node_subsystem_util::database::{DBTransaction, Database};
use pezkuwi_overseer::SubsystemError;
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateIndex, CandidateReceiptV2 as CandidateReceipt, CoreIndex,
GroupIndex, Hash, SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use std::collections::BTreeMap;
use super::common::{block_entry_key, candidate_entry_key, load_decode, Config};
/// Re-export this structs as v3 since they did not change between v2 and v3.
pub use super::v2::{Bitfield, OurAssignment, Tick, TrancheEntry};
pub mod migration_helpers;
#[cfg(test)]
pub mod tests;
/// Metadata about our approval signature
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurApproval {
/// The signature for the candidates hashes pointed by indices.
pub signature: ValidatorSignature,
/// The indices of the candidates signed in this approval.
pub signed_candidates_indices: CandidateBitfield,
}
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct ApprovalEntry {
pub tranches: Vec<TrancheEntry>,
pub backing_group: GroupIndex,
pub our_assignment: Option<OurAssignment>,
pub our_approval_sig: Option<OurApproval>,
// `n_validators` bits.
pub assigned_validators: Bitfield,
pub approved: bool,
}
/// Metadata regarding approval of a particular candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct CandidateEntry {
pub candidate: CandidateReceipt,
pub session: SessionIndex,
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: Bitfield,
}
/// Metadata regarding approval of a particular block, by way of approval of the
/// candidates contained within it.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct BlockEntry {
pub block_hash: Hash,
pub block_number: BlockNumber,
pub parent_hash: Hash,
pub session: SessionIndex,
pub slot: Slot,
/// Random bytes derived from the VRF submitted within the block by the block
/// author as a credential and used as input to approval assignment criteria.
pub relay_vrf_story: [u8; 32],
// The candidates included as-of this block and the index of the core they are
// leaving. Sorted ascending by core index.
pub candidates: Vec<(CoreIndex, CandidateHash)>,
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// A list of candidates we have checked, but didn't not sign and
// advertise the vote yet.
pub candidates_pending_signature: BTreeMap<CandidateIndex, CandidateSigningContext>,
// Assignments we already distributed. A 1 bit means the candidate index for which
// we already have sent out an assignment. We need this to avoid distributing
// multiple core assignments more than once.
pub distributed_assignments: Bitfield,
}
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
/// Context needed for creating an approval signature for a given candidate.
pub struct CandidateSigningContext {
/// The candidate hash, to be included in the signature.
pub candidate_hash: CandidateHash,
/// The latest tick we have to create and send the approval.
pub sign_no_later_than_tick: Tick,
}
/// Load a candidate entry from the aux store in v2 format.
pub fn load_candidate_entry_v2(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<super::v2::CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<super::v2::CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store in v2 format.
pub fn load_block_entry_v2(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<super::v2::BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<super::v2::BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -0,0 +1,624 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the aux-schema of approval voting.
use crate::{
approval_db::{
common::{migration_helpers::make_bitvec, DbBackend, StoredBlockRange, *},
v3::*,
},
backend::{Backend, OverlayedBackend},
ops::{add_block_entry, canonicalize, force_approve, NewCandidateInfo},
};
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateReceiptV2 as CandidateReceipt, CoreIndex, GroupIndex,
Hash, MutateDescriptorV2,
};
use pezkuwi_node_subsystem_util::database::Database;
use pezkuwi_primitives::Id as ParaId;
use sp_consensus_slots::Slot;
use std::{collections::HashMap, sync::Arc};
use pezkuwi_primitives_test_helpers::{
dummy_candidate_receipt_v2, dummy_candidate_receipt_v2_bad_sig, dummy_hash,
};
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: Config = Config { col_approval_data: DATA_COL };
fn make_db() -> (DbBackend, Arc<dyn Database>) {
let db = kvdb_memorydb::create(NUM_COLUMNS);
let db = pezkuwi_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[]);
let db_writer: Arc<dyn Database> = Arc::new(db);
(DbBackend::new(db_writer.clone(), TEST_CONFIG), db_writer)
}
fn make_block_entry(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> BlockEntry {
BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
candidates_pending_signature: Default::default(),
distributed_assignments: Default::default(),
}
}
fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
let mut c = dummy_candidate_receipt_v2(dummy_hash());
c.descriptor.set_para_id(para_id);
c.descriptor.set_relay_parent(relay_parent);
c.into()
}
#[test]
fn read_write() {
let (mut db, store) = make_db();
let hash_a = Hash::repeat_byte(1);
let hash_b = Hash::repeat_byte(2);
let candidate_hash = dummy_candidate_receipt_v2_bad_sig(dummy_hash(), None).hash();
let range = StoredBlockRange(10, 20);
let at_height = vec![hash_a, hash_b];
let block_entry =
make_block_entry(hash_a, Default::default(), 1, vec![(CoreIndex(0), candidate_hash)]);
let candidate_entry = CandidateEntry {
candidate: dummy_candidate_receipt_v2_bad_sig(dummy_hash(), None),
session: 5,
block_assignments: vec![(
hash_a,
ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: None,
our_approval_sig: None,
assigned_validators: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(range.clone());
overlay_db.write_blocks_at_height(1, at_height.clone());
overlay_db.write_block_entry(block_entry.clone().into());
overlay_db.write_candidate_entry(candidate_entry.clone().into());
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap(), at_height);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
Some(block_entry.into())
);
assert_eq!(
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry.into()),
);
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.delete_blocks_at_height(1);
overlay_db.delete_block_entry(&hash_a);
overlay_db.delete_candidate_entry(&candidate_hash);
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap().is_empty());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash)
.unwrap()
.is_none());
}
#[test]
fn add_block_entry_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let candidate_receipt_a = make_candidate(ParaId::from(1_u32), parent_hash);
let candidate_receipt_b = make_candidate(ParaId::from(2_u32), parent_hash);
let candidate_hash_a = candidate_receipt_a.hash();
let candidate_hash_b = candidate_receipt_b.hash();
let block_number = 10;
let block_entry_a = make_block_entry(
block_hash_a,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a)],
);
let block_entry_b = make_block_entry(
block_hash_b,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a), (CoreIndex(1), candidate_hash_b)],
);
let n_validators = 10;
let mut new_candidate_info = HashMap::new();
new_candidate_info
.insert(candidate_hash_a, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(0), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
new_candidate_info
.insert(candidate_hash_b, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(1), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
let candidate_entry_a = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
assert_eq!(
candidate_entry_a.block_assignments.keys().collect::<Vec<_>>(),
vec![&block_hash_a, &block_hash_b]
);
let candidate_entry_b = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
assert_eq!(candidate_entry_b.block_assignments.keys().collect::<Vec<_>>(), vec![&block_hash_b]);
}
#[test]
fn add_block_entry_adds_child() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let mut block_entry_a = make_block_entry(block_hash_a, parent_hash, 1, Vec::new());
let block_entry_b = make_block_entry(block_hash_b, block_hash_a, 2, Vec::new());
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
block_entry_a.children.push(block_hash_b);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
}
#[test]
fn canonicalize_works() {
let (mut db, store) = make_db();
// -> B1 -> C1 -> D1 -> E1
// A -> B2 -> C2 -> D2 -> E2
//
// We'll canonicalize C1. Everything except D1 should disappear.
//
// Candidates:
// Cand1 in B2
// Cand2 in C2
// Cand3 in C2 and D1
// Cand4 in D1
// Cand5 in D2
// Only Cand3 and Cand4 should remain after canonicalize.
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 5));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let genesis = Hash::repeat_byte(0);
let block_hash_a = Hash::repeat_byte(1);
let block_hash_b1 = Hash::repeat_byte(2);
let block_hash_b2 = Hash::repeat_byte(3);
let block_hash_c1 = Hash::repeat_byte(4);
let block_hash_c2 = Hash::repeat_byte(5);
let block_hash_d1 = Hash::repeat_byte(6);
let block_hash_d2 = Hash::repeat_byte(7);
let block_hash_e1 = Hash::repeat_byte(8);
let block_hash_e2 = Hash::repeat_byte(9);
let candidate_receipt_genesis = make_candidate(ParaId::from(1_u32), genesis);
let candidate_receipt_a = make_candidate(ParaId::from(2_u32), block_hash_a);
let candidate_receipt_b = make_candidate(ParaId::from(3_u32), block_hash_a);
let candidate_receipt_b1 = make_candidate(ParaId::from(4_u32), block_hash_b1);
let candidate_receipt_c1 = make_candidate(ParaId::from(5_u32), block_hash_c1);
let candidate_receipt_e1 = make_candidate(ParaId::from(6_u32), block_hash_e1);
let cand_hash_1 = candidate_receipt_genesis.hash();
let cand_hash_2 = candidate_receipt_a.hash();
let cand_hash_3 = candidate_receipt_b.hash();
let cand_hash_4 = candidate_receipt_b1.hash();
let cand_hash_5 = candidate_receipt_c1.hash();
let cand_hash_6 = candidate_receipt_e1.hash();
let block_entry_a = make_block_entry(block_hash_a, genesis, 1, Vec::new());
let block_entry_b1 = make_block_entry(block_hash_b1, block_hash_a, 2, Vec::new());
let block_entry_b2 =
make_block_entry(block_hash_b2, block_hash_a, 2, vec![(CoreIndex(0), cand_hash_1)]);
let block_entry_c1 = make_block_entry(block_hash_c1, block_hash_b1, 3, Vec::new());
let block_entry_c2 = make_block_entry(
block_hash_c2,
block_hash_b2,
3,
vec![(CoreIndex(0), cand_hash_2), (CoreIndex(1), cand_hash_3)],
);
let block_entry_d1 = make_block_entry(
block_hash_d1,
block_hash_c1,
4,
vec![(CoreIndex(0), cand_hash_3), (CoreIndex(1), cand_hash_4)],
);
let block_entry_d2 =
make_block_entry(block_hash_d2, block_hash_c2, 4, vec![(CoreIndex(0), cand_hash_5)]);
let block_entry_e1 =
make_block_entry(block_hash_e1, block_hash_d1, 5, vec![(CoreIndex(0), cand_hash_6)]);
let block_entry_e2 =
make_block_entry(block_hash_e2, block_hash_d2, 5, vec![(CoreIndex(0), cand_hash_6)]);
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
cand_hash_1,
NewCandidateInfo::new(candidate_receipt_genesis, GroupIndex(1), None),
);
candidate_info
.insert(cand_hash_2, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(2), None));
candidate_info
.insert(cand_hash_3, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(3), None));
candidate_info
.insert(cand_hash_4, NewCandidateInfo::new(candidate_receipt_b1, GroupIndex(4), None));
candidate_info
.insert(cand_hash_5, NewCandidateInfo::new(candidate_receipt_c1, GroupIndex(5), None));
candidate_info
.insert(cand_hash_6, NewCandidateInfo::new(candidate_receipt_e1, GroupIndex(6), None));
candidate_info
};
// now insert all the blocks.
let blocks = vec![
block_entry_a.clone(),
block_entry_b1.clone(),
block_entry_b2.clone(),
block_entry_c1.clone(),
block_entry_c2.clone(),
block_entry_d1.clone(),
block_entry_d2.clone(),
block_entry_e1.clone(),
block_entry_e2.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let check_candidates_in_store = |expected: Vec<(CandidateHash, Option<Vec<_>>)>| {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.is_none());
continue;
},
Some(i) => (
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash).unwrap().unwrap(),
i,
),
};
assert_eq!(entry.block_assignments.len(), in_blocks.len());
for x in in_blocks {
assert!(entry.block_assignments.contains_key(&x));
}
}
};
let check_blocks_in_store = |expected: Vec<(Hash, Option<Vec<_>>)>| {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.is_none());
continue;
},
Some(i) =>
(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
};
assert_eq!(entry.candidates.len(), with_candidates.len());
for x in with_candidates {
assert!(entry.candidates.iter().any(|(_, c)| c == &x));
}
}
};
check_candidates_in_store(vec![
(cand_hash_1, Some(vec![block_hash_b2])),
(cand_hash_2, Some(vec![block_hash_c2])),
(cand_hash_3, Some(vec![block_hash_c2, block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, Some(vec![block_hash_d2])),
]);
check_blocks_in_store(vec![
(block_hash_a, Some(vec![])),
(block_hash_b1, Some(vec![])),
(block_hash_b2, Some(vec![cand_hash_1])),
(block_hash_c1, Some(vec![])),
(block_hash_c2, Some(vec![cand_hash_2, cand_hash_3])),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, Some(vec![cand_hash_5])),
]);
let mut overlay_db = OverlayedBackend::new(&db);
canonicalize(&mut overlay_db, 3, block_hash_c1).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap().unwrap(),
StoredBlockRange(4, 6)
);
check_candidates_in_store(vec![
(cand_hash_1, None),
(cand_hash_2, None),
(cand_hash_3, Some(vec![block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, None),
(cand_hash_6, Some(vec![block_hash_e1])),
]);
check_blocks_in_store(vec![
(block_hash_a, None),
(block_hash_b1, None),
(block_hash_b2, None),
(block_hash_c1, None),
(block_hash_c2, None),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_e1, Some(vec![cand_hash_6])),
(block_hash_d2, None),
]);
let mut overlay_db = OverlayedBackend::new(&db);
canonicalize(&mut overlay_db, 4, block_hash_d1).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap().unwrap(),
StoredBlockRange(5, 6)
);
check_candidates_in_store(vec![
(cand_hash_1, None),
(cand_hash_2, None),
(cand_hash_3, None),
(cand_hash_4, None),
(cand_hash_5, None),
(cand_hash_6, Some(vec![block_hash_e1])),
]);
check_blocks_in_store(vec![
(block_hash_a, None),
(block_hash_b1, None),
(block_hash_b2, None),
(block_hash_c1, None),
(block_hash_c2, None),
(block_hash_d1, None),
(block_hash_e1, Some(vec![cand_hash_6])),
(block_hash_d2, None),
]);
}
#[test]
fn force_approve_works() {
let (mut db, store) = make_db();
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 4));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let single_candidate_vec = vec![(CoreIndex(0), candidate_hash)];
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
candidate_hash,
NewCandidateInfo::new(
make_candidate(ParaId::from(1_u32), Default::default()),
GroupIndex(1),
None,
),
);
candidate_info
};
let block_hash_a = Hash::repeat_byte(1); // 1
let block_hash_b = Hash::repeat_byte(2);
let block_hash_c = Hash::repeat_byte(3);
let block_hash_d = Hash::repeat_byte(4); // 4
let block_entry_a =
make_block_entry(block_hash_a, Default::default(), 1, single_candidate_vec.clone());
let block_entry_b =
make_block_entry(block_hash_b, block_hash_a, 2, single_candidate_vec.clone());
let block_entry_c =
make_block_entry(block_hash_c, block_hash_b, 3, single_candidate_vec.clone());
let block_entry_d =
make_block_entry(block_hash_d, block_hash_c, 4, single_candidate_vec.clone());
let blocks = vec![
block_entry_a.clone(),
block_entry_b.clone(),
block_entry_c.clone(),
block_entry_d.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let approved_hashes = force_approve(&mut overlay_db, block_hash_d, 2).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert_eq!(approved_hashes, vec![block_hash_b, block_hash_a]);
}
#[test]
fn load_all_blocks_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let block_hash_c = Hash::repeat_byte(42);
let block_number = 10;
let block_entry_a = make_block_entry(block_hash_a, parent_hash, block_number, vec![]);
let block_entry_b = make_block_entry(block_hash_b, parent_hash, block_number, vec![]);
let block_entry_c = make_block_entry(block_hash_c, block_hash_a, block_number + 1, vec![]);
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
// add C before B to test sorting.
add_block_entry(&mut overlay_db, block_entry_c.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_all_blocks(store.as_ref(), &TEST_CONFIG).unwrap(),
vec![block_hash_a, block_hash_b, block_hash_c],
)
}
@@ -0,0 +1,249 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! An abstraction over storage used by the chain selection subsystem.
//!
//! This provides both a [`Backend`] trait and an [`OverlayedBackend`]
//! struct which allows in-memory changes to be applied on top of a
//! [`Backend`], maintaining consistency between queries and temporary writes,
//! before any commit to the underlying storage is made.
use pezkuwi_node_subsystem::SubsystemResult;
use pezkuwi_primitives::{BlockNumber, CandidateHash, CandidateIndex, Hash};
use std::collections::HashMap;
use super::{
approval_db::common::StoredBlockRange,
persisted_entries::{BlockEntry, CandidateEntry},
};
#[derive(Debug)]
pub enum BackendWriteOp {
WriteStoredBlockRange(StoredBlockRange),
WriteBlocksAtHeight(BlockNumber, Vec<Hash>),
WriteBlockEntry(BlockEntry),
WriteCandidateEntry(CandidateEntry),
DeleteStoredBlockRange,
DeleteBlocksAtHeight(BlockNumber),
DeleteBlockEntry(Hash),
DeleteCandidateEntry(CandidateHash),
}
/// An abstraction over backend storage for the logic of this subsystem.
/// Implementation must always target latest storage version.
pub trait Backend {
/// Load a block entry from the DB.
fn load_block_entry(&self, hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
/// Load a candidate entry from the DB.
fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load all blocks at a specific height.
fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>>;
/// Load all block from the DB.
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>>;
/// Load stored block range form the DB.
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>>;
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
where
I: IntoIterator<Item = BackendWriteOp>;
}
/// A read only backend to enable db migration from version 1 of DB.
pub trait V1ReadBackend: Backend {
/// Load a candidate entry from the DB with scheme version 1.
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load a block entry from the DB with scheme version 1.
fn load_block_entry_v1(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
}
/// A read only backend to enable db migration from version 2 of DB.
pub trait V2ReadBackend: Backend {
/// Load a candidate entry from the DB with scheme version 1.
fn load_candidate_entry_v2(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load a block entry from the DB with scheme version 1.
fn load_block_entry_v2(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
}
// Status of block range in the `OverlayedBackend`.
#[derive(PartialEq)]
enum BlockRangeStatus {
// Value has not been modified.
NotModified,
// Value has been deleted
Deleted,
// Value has been updated.
Inserted(StoredBlockRange),
}
/// An in-memory overlay over the backend.
///
/// This maintains read-only access to the underlying backend, but can be
/// converted into a set of write operations which will, when written to
/// the underlying backend, give the same view as the state of the overlay.
pub struct OverlayedBackend<'a, B: 'a> {
inner: &'a B,
// `Some(None)` means deleted. Missing (`None`) means query inner.
stored_block_range: BlockRangeStatus,
// `None` means 'deleted', missing means query inner.
blocks_at_height: HashMap<BlockNumber, Option<Vec<Hash>>>,
// `None` means 'deleted', missing means query inner.
block_entries: HashMap<Hash, Option<BlockEntry>>,
// `None` means 'deleted', missing means query inner.
candidate_entries: HashMap<CandidateHash, Option<CandidateEntry>>,
}
impl<'a, B: 'a + Backend> OverlayedBackend<'a, B> {
pub fn new(backend: &'a B) -> Self {
OverlayedBackend {
inner: backend,
stored_block_range: BlockRangeStatus::NotModified,
blocks_at_height: HashMap::new(),
block_entries: HashMap::new(),
candidate_entries: HashMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.block_entries.is_empty() &&
self.candidate_entries.is_empty() &&
self.blocks_at_height.is_empty() &&
self.stored_block_range == BlockRangeStatus::NotModified
}
pub fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
let mut hashes = Vec::new();
if let Some(stored_blocks) = self.load_stored_blocks()? {
for height in stored_blocks.0..stored_blocks.1 {
hashes.extend(self.load_blocks_at_height(&height)?);
}
}
Ok(hashes)
}
pub fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
match self.stored_block_range {
BlockRangeStatus::Inserted(ref value) => Ok(Some(value.clone())),
BlockRangeStatus::Deleted => Ok(None),
BlockRangeStatus::NotModified => self.inner.load_stored_blocks(),
}
}
pub fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
if let Some(val) = self.blocks_at_height.get(&height) {
return Ok(val.clone().unwrap_or_default());
}
self.inner.load_blocks_at_height(height)
}
pub fn load_block_entry(&self, hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
if let Some(val) = self.block_entries.get(&hash) {
return Ok(val.clone());
}
self.inner.load_block_entry(hash)
}
pub fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
if let Some(val) = self.candidate_entries.get(&candidate_hash) {
return Ok(val.clone());
}
self.inner.load_candidate_entry(candidate_hash)
}
pub fn write_stored_block_range(&mut self, range: StoredBlockRange) {
self.stored_block_range = BlockRangeStatus::Inserted(range);
}
pub fn delete_stored_block_range(&mut self) {
self.stored_block_range = BlockRangeStatus::Deleted;
}
pub fn write_blocks_at_height(&mut self, height: BlockNumber, blocks: Vec<Hash>) {
self.blocks_at_height.insert(height, Some(blocks));
}
pub fn delete_blocks_at_height(&mut self, height: BlockNumber) {
self.blocks_at_height.insert(height, None);
}
pub fn write_block_entry(&mut self, entry: BlockEntry) {
self.block_entries.insert(entry.block_hash(), Some(entry));
}
pub fn delete_block_entry(&mut self, hash: &Hash) {
self.block_entries.insert(*hash, None);
}
pub fn write_candidate_entry(&mut self, entry: CandidateEntry) {
self.candidate_entries.insert(entry.candidate_receipt().hash(), Some(entry));
}
pub fn delete_candidate_entry(&mut self, hash: &CandidateHash) {
self.candidate_entries.insert(*hash, None);
}
/// Transform this backend into a set of write-ops to be written to the
/// inner backend.
pub fn into_write_ops(self) -> impl Iterator<Item = BackendWriteOp> {
let blocks_at_height_ops = self.blocks_at_height.into_iter().map(|(h, v)| match v {
Some(v) => BackendWriteOp::WriteBlocksAtHeight(h, v),
None => BackendWriteOp::DeleteBlocksAtHeight(h),
});
let block_entry_ops = self.block_entries.into_iter().map(|(h, v)| match v {
Some(v) => BackendWriteOp::WriteBlockEntry(v),
None => BackendWriteOp::DeleteBlockEntry(h),
});
let candidate_entry_ops = self.candidate_entries.into_iter().map(|(h, v)| match v {
Some(v) => BackendWriteOp::WriteCandidateEntry(v),
None => BackendWriteOp::DeleteCandidateEntry(h),
});
let stored_block_range_ops = match self.stored_block_range {
BlockRangeStatus::Inserted(val) => Some(BackendWriteOp::WriteStoredBlockRange(val)),
BlockRangeStatus::Deleted => Some(BackendWriteOp::DeleteStoredBlockRange),
BlockRangeStatus::NotModified => None,
};
stored_block_range_ops
.into_iter()
.chain(blocks_at_height_ops)
.chain(block_entry_ops)
.chain(candidate_entry_ops)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,428 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Middleware interface that leverages low-level database operations
//! to provide a clean API for processing block and candidate imports.
use pezkuwi_node_subsystem::{SubsystemError, SubsystemResult};
use bitvec::order::Lsb0 as BitOrderLsb0;
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateReceiptV2 as CandidateReceipt, GroupIndex, Hash,
};
use std::collections::{hash_map::Entry, BTreeMap, HashMap};
use super::{
approval_db::{common::StoredBlockRange, v2::OurAssignment},
backend::{Backend, OverlayedBackend},
persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry},
LOG_TARGET,
};
/// Information about a new candidate necessary to instantiate the requisite
/// candidate and approval entries.
#[derive(Clone)]
pub struct NewCandidateInfo {
candidate: CandidateReceipt,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
}
impl NewCandidateInfo {
/// Convenience constructor
pub fn new(
candidate: CandidateReceipt,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
) -> Self {
Self { candidate, backing_group, our_assignment }
}
}
fn visit_and_remove_block_entry(
block_hash: Hash,
overlayed_db: &mut OverlayedBackend<'_, impl Backend>,
visited_candidates: &mut HashMap<CandidateHash, CandidateEntry>,
) -> SubsystemResult<Vec<Hash>> {
let block_entry = match overlayed_db.load_block_entry(&block_hash)? {
None => return Ok(Vec::new()),
Some(b) => b,
};
overlayed_db.delete_block_entry(&block_hash);
for (_, candidate_hash) in block_entry.candidates() {
let candidate = match visited_candidates.entry(*candidate_hash) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
e.insert(match overlayed_db.load_candidate_entry(candidate_hash)? {
None => continue, // Should not happen except for corrupt DB
Some(c) => c,
})
},
};
candidate.block_assignments.remove(&block_hash);
}
Ok(block_entry.children)
}
/// Canonicalize some particular block, pruning everything before it and
/// pruning any competing branches at the same height.
pub fn canonicalize(
overlay_db: &mut OverlayedBackend<'_, impl Backend>,
canon_number: BlockNumber,
canon_hash: Hash,
) -> SubsystemResult<()> {
let range = match overlay_db.load_stored_blocks()? {
None => return Ok(()),
Some(range) if range.0 > canon_number => return Ok(()),
Some(range) => range,
};
// Storing all candidates in memory is potentially heavy, but should be fine
// as long as finality doesn't stall for a long while. We could optimize this
// by keeping only the metadata about which blocks reference each candidate.
let mut visited_candidates = HashMap::new();
// All the block heights we visited but didn't necessarily delete everything from.
let mut visited_heights = HashMap::new();
// First visit everything before the height.
for i in range.0..canon_number {
let at_height = overlay_db.load_blocks_at_height(&i)?;
overlay_db.delete_blocks_at_height(i);
for b in at_height {
visit_and_remove_block_entry(b, overlay_db, &mut visited_candidates)?;
}
}
// Then visit everything at the height.
let pruned_branches = {
let at_height = overlay_db.load_blocks_at_height(&canon_number)?;
overlay_db.delete_blocks_at_height(canon_number);
// Note that while there may be branches descending from blocks at earlier heights,
// we have already covered them by removing everything at earlier heights.
let mut pruned_branches = Vec::new();
for b in at_height {
let children = visit_and_remove_block_entry(b, overlay_db, &mut visited_candidates)?;
if b != canon_hash {
pruned_branches.extend(children);
}
}
pruned_branches
};
// Follow all children of non-canonicalized blocks.
{
let mut frontier: Vec<(BlockNumber, Hash)> =
pruned_branches.into_iter().map(|h| (canon_number + 1, h)).collect();
while let Some((height, next_child)) = frontier.pop() {
let children =
visit_and_remove_block_entry(next_child, overlay_db, &mut visited_candidates)?;
// extend the frontier of branches to include the given height.
frontier.extend(children.into_iter().map(|h| (height + 1, h)));
// visit the at-height key for this deleted block's height.
let at_height = match visited_heights.entry(height) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => e.insert(overlay_db.load_blocks_at_height(&height)?),
};
if let Some(i) = at_height.iter().position(|x| x == &next_child) {
at_height.remove(i);
}
}
}
// Update all `CandidateEntry`s, deleting all those which now have empty `block_assignments`.
for (candidate_hash, candidate) in visited_candidates.into_iter() {
if candidate.block_assignments.is_empty() {
overlay_db.delete_candidate_entry(&candidate_hash);
} else {
overlay_db.write_candidate_entry(candidate);
}
}
// Update all blocks-at-height keys, deleting all those which now have empty
// `block_assignments`.
for (h, at) in visited_heights.into_iter() {
if at.is_empty() {
overlay_db.delete_blocks_at_height(h);
} else {
overlay_db.write_blocks_at_height(h, at);
}
}
// due to the fork pruning, this range actually might go too far above where our actual highest
// block is, if a relatively short fork is canonicalized.
// TODO https://github.com/paritytech/polkadot/issues/3389
let new_range = StoredBlockRange(canon_number + 1, std::cmp::max(range.1, canon_number + 2));
overlay_db.write_stored_block_range(new_range);
Ok(())
}
/// Record a new block entry.
///
/// This will update the blocks-at-height mapping, the stored block range, if necessary,
/// and add block and candidate entries. It will also add approval entries to existing
/// candidate entries and add this as a child of any block entry corresponding to the
/// parent hash.
///
/// Has no effect if there is already an entry for the block or `candidate_info` returns
/// `None` for any of the candidates referenced by the block entry. In these cases,
/// no information about new candidates will be referred to by this function.
pub fn add_block_entry(
store: &mut OverlayedBackend<'_, impl Backend>,
entry: BlockEntry,
n_validators: usize,
candidate_info: impl Fn(&CandidateHash) -> Option<NewCandidateInfo>,
) -> SubsystemResult<Vec<(CandidateHash, CandidateEntry)>> {
let session = entry.session();
let parent_hash = entry.parent_hash();
let number = entry.block_number();
// Update the stored block range.
{
let new_range = match store.load_stored_blocks()? {
None => Some(StoredBlockRange(number, number + 1)),
Some(range) if range.1 <= number => Some(StoredBlockRange(range.0, number + 1)),
Some(_) => None,
};
new_range.map(|n| store.write_stored_block_range(n));
};
// Update the blocks at height meta key.
{
let mut blocks_at_height = store.load_blocks_at_height(&number)?;
if blocks_at_height.contains(&entry.block_hash()) {
// seems we already have a block entry for this block. nothing to do here.
return Ok(Vec::new());
}
blocks_at_height.push(entry.block_hash());
store.write_blocks_at_height(number, blocks_at_height)
};
let mut candidate_entries = Vec::with_capacity(entry.candidates().len());
// read and write all updated entries.
{
for (_, candidate_hash) in entry.candidates() {
let NewCandidateInfo { candidate, backing_group, our_assignment } =
match candidate_info(candidate_hash) {
None => return Ok(Vec::new()),
Some(info) => info,
};
let mut candidate_entry =
store.load_candidate_entry(&candidate_hash)?.unwrap_or_else(move || {
CandidateEntry {
candidate,
session,
block_assignments: BTreeMap::new(),
approvals: bitvec::bitvec![u8, BitOrderLsb0; 0; n_validators],
}
});
candidate_entry.block_assignments.insert(
entry.block_hash(),
ApprovalEntry::new(
Vec::new(),
backing_group,
our_assignment.map(|v| v.into()),
None,
bitvec::bitvec![u8, BitOrderLsb0; 0; n_validators],
false,
),
);
store.write_candidate_entry(candidate_entry.clone());
candidate_entries.push((*candidate_hash, candidate_entry));
}
};
// Update the child index for the parent.
store.load_block_entry(&parent_hash)?.map(|mut e| {
e.children.push(entry.block_hash());
store.write_block_entry(e);
});
// Put the new block entry in.
store.write_block_entry(entry);
Ok(candidate_entries)
}
/// Forcibly approve all candidates included at up to the given relay-chain height in the indicated
/// chain.
pub fn force_approve(
store: &mut OverlayedBackend<'_, impl Backend>,
chain_head: Hash,
up_to: BlockNumber,
) -> SubsystemResult<Vec<Hash>> {
#[derive(PartialEq, Eq)]
enum State {
WalkTo,
Approving,
}
let mut approved_hashes = Vec::new();
let mut cur_hash = chain_head;
let mut state = State::WalkTo;
let mut cur_block_number: BlockNumber = 0;
// iterate back to the `up_to` block, and then iterate backwards until all blocks
// are updated.
while let Some(mut entry) = store.load_block_entry(&cur_hash)? {
cur_block_number = entry.block_number();
if cur_block_number <= up_to {
if state == State::WalkTo {
gum::debug!(
target: LOG_TARGET,
block_hash = ?chain_head,
?cur_hash,
?cur_block_number,
"Start forced approval from block",
);
}
state = State::Approving;
}
cur_hash = entry.parent_hash();
match state {
State::WalkTo => {},
State::Approving => {
entry.approved_bitfield.iter_mut().for_each(|mut b| *b = true);
approved_hashes.push(entry.block_hash());
store.write_block_entry(entry);
},
}
}
if state == State::WalkTo {
gum::warn!(
target: LOG_TARGET,
?chain_head,
?cur_hash,
?cur_block_number,
?up_to,
"Missing block in the chain, cannot start force approval"
);
}
Ok(approved_hashes)
}
/// Revert to the block corresponding to the specified `hash`.
/// The operation is not allowed for blocks older than the last finalized one.
pub fn revert_to(
overlay: &mut OverlayedBackend<'_, impl Backend>,
hash: Hash,
) -> SubsystemResult<()> {
let mut stored_range = overlay.load_stored_blocks()?.ok_or_else(|| {
SubsystemError::Context("no available blocks to infer revert point height".to_string())
})?;
let (children, children_height) = match overlay.load_block_entry(&hash)? {
Some(mut entry) => {
let children_height = entry.block_number() + 1;
let children = std::mem::take(&mut entry.children);
// Write revert point block entry without the children.
overlay.write_block_entry(entry);
(children, children_height)
},
None => {
let children_height = stored_range.0;
let children = overlay.load_blocks_at_height(&children_height)?;
let child_entry = children
.first()
.and_then(|hash| overlay.load_block_entry(hash).ok())
.flatten()
.ok_or_else(|| {
SubsystemError::Context("lookup failure for first block".to_string())
})?;
// The parent is expected to be the revert point
if child_entry.parent_hash() != hash {
return Err(SubsystemError::Context(
"revert below last finalized block or corrupted storage".to_string(),
));
}
(children, children_height)
},
};
let mut stack: Vec<_> = children.into_iter().map(|h| (h, children_height)).collect();
let mut range_end = stored_range.1;
while let Some((hash, number)) = stack.pop() {
let mut blocks_at_height = overlay.load_blocks_at_height(&number)?;
blocks_at_height.retain(|h| h != &hash);
// Check if we need to update the range top
if blocks_at_height.is_empty() && number < range_end {
range_end = number;
}
overlay.write_blocks_at_height(number, blocks_at_height);
if let Some(entry) = overlay.load_block_entry(&hash)? {
overlay.delete_block_entry(&hash);
// Cleanup the candidate entries by removing any reference to the
// removed block. If for a candidate entry the block block_assignments
// drops to zero then we remove the entry.
for (_, candidate_hash) in entry.candidates() {
if let Some(mut candidate_entry) = overlay.load_candidate_entry(candidate_hash)? {
candidate_entry.block_assignments.remove(&hash);
if candidate_entry.block_assignments.is_empty() {
overlay.delete_candidate_entry(candidate_hash);
} else {
overlay.write_candidate_entry(candidate_entry);
}
}
}
stack.extend(entry.children.into_iter().map(|h| (h, number + 1)));
}
}
// Check if our modifications to the dag has reduced the range top
if range_end != stored_range.1 {
if stored_range.0 < range_end {
stored_range.1 = range_end;
overlay.write_stored_block_range(stored_range);
} else {
overlay.delete_stored_block_range();
}
}
Ok(())
}
@@ -0,0 +1,769 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Entries pertaining to approval which need to be persisted.
//!
//! The actual persisting of data is handled by the `approval_db` module.
//! Within that context, things are plain-old-data. Within this module,
//! data and logic are intertwined.
use itertools::Itertools;
use pezkuwi_node_primitives::approval::{
v1::{DelayTranche, RelayVRFStory},
v2::{AssignmentCertV2, CandidateBitfield},
};
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateIndex, CandidateReceiptV2 as CandidateReceipt, CoreIndex,
GroupIndex, Hash, SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice};
use std::collections::BTreeMap;
use crate::approval_db::v2::Bitfield;
use super::criteria::OurAssignment;
use pezkuwi_node_primitives::approval::time::Tick;
/// Metadata regarding a specific tranche of assignments for a specific candidate.
#[derive(Debug, Clone, PartialEq)]
pub struct TrancheEntry {
tranche: DelayTranche,
// Assigned validators, and the instant we received their assignment, rounded
// to the nearest tick.
assignments: Vec<(ValidatorIndex, Tick)>,
}
impl TrancheEntry {
/// Get the tranche of this entry.
pub fn tranche(&self) -> DelayTranche {
self.tranche
}
/// Get the assignments for this entry.
pub fn assignments(&self) -> &[(ValidatorIndex, Tick)] {
&self.assignments
}
}
impl From<crate::approval_db::v2::TrancheEntry> for TrancheEntry {
fn from(entry: crate::approval_db::v2::TrancheEntry) -> Self {
TrancheEntry {
tranche: entry.tranche,
assignments: entry.assignments.into_iter().map(|(v, t)| (v, t.into())).collect(),
}
}
}
impl From<TrancheEntry> for crate::approval_db::v2::TrancheEntry {
fn from(entry: TrancheEntry) -> Self {
Self {
tranche: entry.tranche,
assignments: entry.assignments.into_iter().map(|(v, t)| (v, t.into())).collect(),
}
}
}
impl From<crate::approval_db::v3::OurApproval> for OurApproval {
fn from(approval: crate::approval_db::v3::OurApproval) -> Self {
Self {
signature: approval.signature,
signed_candidates_indices: approval.signed_candidates_indices,
}
}
}
impl From<OurApproval> for crate::approval_db::v3::OurApproval {
fn from(approval: OurApproval) -> Self {
Self {
signature: approval.signature,
signed_candidates_indices: approval.signed_candidates_indices,
}
}
}
/// Metadata about our approval signature
#[derive(Debug, Clone, PartialEq)]
pub struct OurApproval {
/// The signature for the candidates hashes pointed by indices.
pub signature: ValidatorSignature,
/// The indices of the candidates signed in this approval.
pub signed_candidates_indices: CandidateBitfield,
}
impl OurApproval {
/// Converts a ValidatorSignature to an OurApproval.
/// It used in converting the database from v1 to latest.
pub fn from_v1(value: ValidatorSignature, candidate_index: CandidateIndex) -> Self {
Self { signature: value, signed_candidates_indices: candidate_index.into() }
}
/// Converts a ValidatorSignature to an OurApproval.
/// It used in converting the database from v2 to latest.
pub fn from_v2(value: ValidatorSignature, candidate_index: CandidateIndex) -> Self {
Self::from_v1(value, candidate_index)
}
}
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Debug, Clone, PartialEq)]
pub struct ApprovalEntry {
tranches: Vec<TrancheEntry>,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<OurApproval>,
// `n_validators` bits.
assigned_validators: Bitfield,
approved: bool,
}
impl ApprovalEntry {
/// Convenience constructor
pub fn new(
tranches: Vec<TrancheEntry>,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<OurApproval>,
// `n_validators` bits.
assigned_validators: Bitfield,
approved: bool,
) -> Self {
Self {
tranches,
backing_group,
our_assignment,
our_approval_sig,
assigned_validators,
approved,
}
}
// Access our assignment for this approval entry.
pub fn our_assignment(&self) -> Option<&OurAssignment> {
self.our_assignment.as_ref()
}
// Note that our assignment is triggered. No-op if already triggered.
pub fn trigger_our_assignment(
&mut self,
tick_now: Tick,
) -> Option<(AssignmentCertV2, ValidatorIndex, DelayTranche)> {
let our = self.our_assignment.as_mut().and_then(|a| {
if a.triggered() {
return None;
}
a.mark_triggered();
Some(a.clone())
});
our.map(|a| {
self.import_assignment(a.tranche(), a.validator_index(), tick_now, false);
(a.cert().clone(), a.validator_index(), a.tranche())
})
}
/// Import our local approval vote signature for this candidate.
pub fn import_approval_sig(&mut self, approval_sig: OurApproval) {
self.our_approval_sig = Some(approval_sig);
}
/// Whether a validator is already assigned.
pub fn is_assigned(&self, validator_index: ValidatorIndex) -> bool {
self.assigned_validators
.get(validator_index.0 as usize)
.map(|b| *b)
.unwrap_or(false)
}
/// Import an assignment. No-op if already assigned on the same tranche.
pub fn import_assignment(
&mut self,
tranche: DelayTranche,
validator_index: ValidatorIndex,
tick_now: Tick,
is_duplicate: bool,
) {
// linear search probably faster than binary. not many tranches typically.
let idx = match self.tranches.iter().position(|t| t.tranche >= tranche) {
Some(pos) => {
if self.tranches[pos].tranche > tranche {
self.tranches.insert(pos, TrancheEntry { tranche, assignments: Vec::new() });
}
pos
},
None => {
self.tranches.push(TrancheEntry { tranche, assignments: Vec::new() });
self.tranches.len() - 1
},
};
// At restart we might have duplicate assignments because approval-distribution is not
// persistent across restarts, so avoid adding duplicates.
// We already know if we have seen an assignment from this validator and since this
// function is on the hot path we can avoid iterating through tranches by using
// !is_duplicate to determine if it is already present in the vector and does not need
// adding.
if !is_duplicate {
self.tranches[idx].assignments.push((validator_index, tick_now));
}
self.assigned_validators.set(validator_index.0 as _, true);
}
// Produce a bitvec indicating the assignments of all validators up to and
// including `tranche`.
pub fn assignments_up_to(&self, tranche: DelayTranche) -> Bitfield {
self.tranches.iter().take_while(|e| e.tranche <= tranche).fold(
bitvec::bitvec![u8, BitOrderLsb0; 0; self.assigned_validators.len()],
|mut a, e| {
for &(v, _) in &e.assignments {
a.set(v.0 as _, true);
}
a
},
)
}
/// Whether the approval entry is approved
pub fn is_approved(&self) -> bool {
self.approved
}
/// Mark the approval entry as approved.
pub fn mark_approved(&mut self) {
self.approved = true;
}
/// Access the tranches.
pub fn tranches(&self) -> &[TrancheEntry] {
&self.tranches
}
/// Get the number of validators in this approval entry.
pub fn n_validators(&self) -> usize {
self.assigned_validators.len()
}
/// Get the number of assignments by validators, including the local validator.
pub fn n_assignments(&self) -> usize {
self.assigned_validators.count_ones()
}
/// Get the backing group index of the approval entry.
pub fn backing_group(&self) -> GroupIndex {
self.backing_group
}
/// Get the assignment cert & approval signature.
///
/// The approval signature will only be `Some` if the assignment is too.
pub fn local_statements(&self) -> (Option<OurAssignment>, Option<OurApproval>) {
let approval_sig = self.our_approval_sig.clone();
if let Some(our_assignment) = self.our_assignment.as_ref().filter(|a| a.triggered()) {
(Some(our_assignment.clone()), approval_sig)
} else {
(None, None)
}
}
// Convert an ApprovalEntry from v1 version to latest version
pub fn from_v1(
value: crate::approval_db::v1::ApprovalEntry,
candidate_index: CandidateIndex,
) -> Self {
ApprovalEntry {
tranches: value.tranches.into_iter().map(|tranche| tranche.into()).collect(),
backing_group: value.backing_group,
our_assignment: value.our_assignment.map(|assignment| assignment.into()),
our_approval_sig: value
.our_approval_sig
.map(|sig| OurApproval::from_v1(sig, candidate_index)),
assigned_validators: value.assignments,
approved: value.approved,
}
}
// Convert an ApprovalEntry from v1 version to latest version
pub fn from_v2(
value: crate::approval_db::v2::ApprovalEntry,
candidate_index: CandidateIndex,
) -> Self {
ApprovalEntry {
tranches: value.tranches.into_iter().map(|tranche| tranche.into()).collect(),
backing_group: value.backing_group,
our_assignment: value.our_assignment.map(|assignment| assignment.into()),
our_approval_sig: value
.our_approval_sig
.map(|sig| OurApproval::from_v2(sig, candidate_index)),
assigned_validators: value.assigned_validators,
approved: value.approved,
}
}
}
impl From<crate::approval_db::v3::ApprovalEntry> for ApprovalEntry {
fn from(entry: crate::approval_db::v3::ApprovalEntry) -> Self {
ApprovalEntry {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
our_assignment: entry.our_assignment.map(Into::into),
our_approval_sig: entry.our_approval_sig.map(Into::into),
assigned_validators: entry.assigned_validators,
approved: entry.approved,
}
}
}
impl From<ApprovalEntry> for crate::approval_db::v3::ApprovalEntry {
fn from(entry: ApprovalEntry) -> Self {
Self {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
our_assignment: entry.our_assignment.map(Into::into),
our_approval_sig: entry.our_approval_sig.map(Into::into),
assigned_validators: entry.assigned_validators,
approved: entry.approved,
}
}
}
/// Metadata regarding approval of a particular candidate.
#[derive(Debug, Clone, PartialEq)]
pub struct CandidateEntry {
pub candidate: CandidateReceipt,
pub session: SessionIndex,
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: Bitfield,
}
impl CandidateEntry {
/// Access the bit-vec of approvals.
pub fn approvals(&self) -> &BitSlice<u8, BitOrderLsb0> {
&self.approvals
}
/// Note that a given validator has approved. Return the previous approval state.
pub fn mark_approval(&mut self, validator: ValidatorIndex) -> bool {
let prev = self.has_approved(validator);
self.approvals.set(validator.0 as usize, true);
prev
}
/// Query whether a given validator has approved the candidate.
pub fn has_approved(&self, validator: ValidatorIndex) -> bool {
self.approvals.get(validator.0 as usize).map(|b| *b).unwrap_or(false)
}
/// Get the candidate receipt.
pub fn candidate_receipt(&self) -> &CandidateReceipt {
&self.candidate
}
/// Get the approval entry, mutably, for this candidate under a specific block.
pub fn approval_entry_mut(&mut self, block_hash: &Hash) -> Option<&mut ApprovalEntry> {
self.block_assignments.get_mut(block_hash)
}
/// Get the approval entry for this candidate under a specific block.
pub fn approval_entry(&self, block_hash: &Hash) -> Option<&ApprovalEntry> {
self.block_assignments.get(block_hash)
}
/// Convert a CandidateEntry from a v1 to its latest equivalent.
pub fn from_v1(
value: crate::approval_db::v1::CandidateEntry,
candidate_index: CandidateIndex,
) -> Self {
Self {
approvals: value.approvals,
block_assignments: value
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ApprovalEntry::from_v1(ae, candidate_index)))
.collect(),
candidate: value.candidate,
session: value.session,
}
}
/// Convert a CandidateEntry from a v2 to its latest equivalent.
pub fn from_v2(
value: crate::approval_db::v2::CandidateEntry,
candidate_index: CandidateIndex,
) -> Self {
Self {
approvals: value.approvals,
block_assignments: value
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ApprovalEntry::from_v2(ae, candidate_index)))
.collect(),
candidate: value.candidate,
session: value.session,
}
}
}
impl From<crate::approval_db::v3::CandidateEntry> for CandidateEntry {
fn from(entry: crate::approval_db::v3::CandidateEntry) -> Self {
CandidateEntry {
candidate: entry.candidate,
session: entry.session,
block_assignments: entry
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ae.into()))
.collect(),
approvals: entry.approvals,
}
}
}
impl From<CandidateEntry> for crate::approval_db::v3::CandidateEntry {
fn from(entry: CandidateEntry) -> Self {
Self {
candidate: entry.candidate,
session: entry.session,
block_assignments: entry
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ae.into()))
.collect(),
approvals: entry.approvals,
}
}
}
/// Metadata regarding approval of a particular block, by way of approval of the
/// candidates contained within it.
#[derive(Debug, Clone, PartialEq)]
pub struct BlockEntry {
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
session: SessionIndex,
slot: Slot,
relay_vrf_story: RelayVRFStory,
// The candidates included as-of this block and the index of the core they are
// leaving.
candidates: Vec<(CoreIndex, CandidateHash)>,
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// A list of candidates we have checked, but didn't not sign and
// advertise the vote yet.
candidates_pending_signature: BTreeMap<CandidateIndex, CandidateSigningContext>,
// A list of assignments for which we already distributed the assignment.
// We use this to ensure we don't distribute multiple core assignments twice as we track
// individual wakeups for each core.
distributed_assignments: Bitfield,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CandidateSigningContext {
pub candidate_hash: CandidateHash,
pub sign_no_later_than_tick: Tick,
}
impl BlockEntry {
/// Mark a candidate as fully approved in the bitfield.
pub fn mark_approved_by_hash(&mut self, candidate_hash: &CandidateHash) {
if let Some(p) = self.candidates.iter().position(|(_, h)| h == candidate_hash) {
self.approved_bitfield.set(p, true);
}
}
/// Whether a candidate is approved in the bitfield.
pub fn is_candidate_approved(&self, candidate_hash: &CandidateHash) -> bool {
self.candidates
.iter()
.position(|(_, h)| h == candidate_hash)
.and_then(|p| self.approved_bitfield.get(p).map(|b| *b))
.unwrap_or(false)
}
/// Whether the block entry is fully approved.
pub fn is_fully_approved(&self) -> bool {
self.approved_bitfield.all()
}
/// Iterate over all unapproved candidates.
pub fn unapproved_candidates(&self) -> impl Iterator<Item = CandidateHash> + '_ {
self.approved_bitfield.iter().enumerate().filter_map(move |(i, a)| {
if !*a {
Some(self.candidates[i].1)
} else {
None
}
})
}
/// Get the slot of the block.
pub fn slot(&self) -> Slot {
self.slot
}
/// Get the relay-vrf-story of the block.
pub fn relay_vrf_story(&self) -> RelayVRFStory {
self.relay_vrf_story.clone()
}
/// Get the session index of the block.
pub fn session(&self) -> SessionIndex {
self.session
}
/// Get the i'th candidate.
pub fn candidate(&self, i: usize) -> Option<&(CoreIndex, CandidateHash)> {
self.candidates.get(i)
}
/// Access the underlying candidates as a slice.
pub fn candidates(&self) -> &[(CoreIndex, CandidateHash)] {
&self.candidates
}
/// Access the block number of the block entry.
pub fn block_number(&self) -> BlockNumber {
self.block_number
}
/// Access the block hash of the block entry.
pub fn block_hash(&self) -> Hash {
self.block_hash
}
/// Access the parent hash of the block entry.
pub fn parent_hash(&self) -> Hash {
self.parent_hash
}
/// Mark distributed assignment for many candidate indices.
/// Returns `true` if an assignment was already distributed for the `candidates`.
pub fn mark_assignment_distributed(&mut self, candidates: CandidateBitfield) -> bool {
let bitfield = candidates.into_inner();
let total_one_bits = self.distributed_assignments.count_ones();
let new_len = std::cmp::max(self.distributed_assignments.len(), bitfield.len());
self.distributed_assignments.resize(new_len, false);
self.distributed_assignments |= bitfield;
// If an operation did not change our current bitfield, we return true.
let distributed = total_one_bits == self.distributed_assignments.count_ones();
distributed
}
/// Defer signing and issuing an approval for a candidate no later than the specified tick
pub fn defer_candidate_signature(
&mut self,
candidate_index: CandidateIndex,
candidate_hash: CandidateHash,
sign_no_later_than_tick: Tick,
) -> Option<CandidateSigningContext> {
self.candidates_pending_signature.insert(
candidate_index,
CandidateSigningContext { candidate_hash, sign_no_later_than_tick },
)
}
/// Returns the number of candidates waiting for an approval to be issued.
pub fn num_candidates_pending_signature(&self) -> usize {
self.candidates_pending_signature.len()
}
/// Return if we have candidates waiting for signature to be issued
pub fn has_candidates_pending_signature(&self) -> bool {
!self.candidates_pending_signature.is_empty()
}
/// Returns true if candidate hash is in the queue for a signature.
pub fn candidate_is_pending_signature(&self, candidate_hash: CandidateHash) -> bool {
self.candidates_pending_signature
.values()
.any(|context| context.candidate_hash == candidate_hash)
}
/// Candidate hashes for candidates pending signatures
fn candidate_hashes_pending_signature(&self) -> Vec<CandidateHash> {
self.candidates_pending_signature
.values()
.map(|unsigned_approval| unsigned_approval.candidate_hash)
.collect()
}
/// Candidate indices for candidates pending signature
fn candidate_indices_pending_signature(&self) -> Option<CandidateBitfield> {
self.candidates_pending_signature
.keys()
.map(|val| *val)
.collect_vec()
.try_into()
.ok()
}
/// Returns a list of candidates hashes that need need signature created at the current tick:
/// This might happen in other of the two reasons:
/// 1. We queued more than max_approval_coalesce_count candidates.
/// 2. We have candidates that waiting in the queue past their `sign_no_later_than_tick`
///
/// Additionally, we also return the first tick when we will have to create a signature,
/// so that the caller can arm the timer if it is not already armed.
pub fn get_candidates_that_need_signature(
&self,
tick_now: Tick,
max_approval_coalesce_count: u32,
) -> (Option<(Vec<CandidateHash>, CandidateBitfield)>, Option<Tick>) {
let sign_no_later_than_tick = self
.candidates_pending_signature
.values()
.min_by(|a, b| a.sign_no_later_than_tick.cmp(&b.sign_no_later_than_tick))
.map(|val| val.sign_no_later_than_tick);
if let Some(sign_no_later_than_tick) = sign_no_later_than_tick {
if sign_no_later_than_tick <= tick_now ||
self.num_candidates_pending_signature() >= max_approval_coalesce_count as usize
{
(
self.candidate_indices_pending_signature().and_then(|candidate_indices| {
Some((self.candidate_hashes_pending_signature(), candidate_indices))
}),
Some(sign_no_later_than_tick),
)
} else {
// We can still wait for other candidates to queue in, so just make sure
// we wake up at the tick we have to sign the longest waiting candidate.
(Default::default(), Some(sign_no_later_than_tick))
}
} else {
// No cached candidates, nothing to do here, this just means the timer fired,
// but the signatures were already sent because we gathered more than
// max_approval_coalesce_count.
(Default::default(), sign_no_later_than_tick)
}
}
/// Clears the candidates pending signature because the approval was issued.
pub fn issued_approval(&mut self) {
self.candidates_pending_signature.clear();
}
}
impl From<crate::approval_db::v3::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v3::BlockEntry) -> Self {
BlockEntry {
block_hash: entry.block_hash,
parent_hash: entry.parent_hash,
block_number: entry.block_number,
session: entry.session,
slot: entry.slot,
relay_vrf_story: RelayVRFStory(entry.relay_vrf_story),
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
candidates_pending_signature: entry
.candidates_pending_signature
.into_iter()
.map(|(candidate_index, signing_context)| (candidate_index, signing_context.into()))
.collect(),
distributed_assignments: entry.distributed_assignments,
}
}
}
impl From<crate::approval_db::v1::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v1::BlockEntry) -> Self {
BlockEntry {
block_hash: entry.block_hash,
parent_hash: entry.parent_hash,
block_number: entry.block_number,
session: entry.session,
slot: entry.slot,
relay_vrf_story: RelayVRFStory(entry.relay_vrf_story),
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: Default::default(),
candidates_pending_signature: Default::default(),
}
}
}
impl From<crate::approval_db::v2::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v2::BlockEntry) -> Self {
BlockEntry {
block_hash: entry.block_hash,
parent_hash: entry.parent_hash,
block_number: entry.block_number,
session: entry.session,
slot: entry.slot,
relay_vrf_story: RelayVRFStory(entry.relay_vrf_story),
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: entry.distributed_assignments,
candidates_pending_signature: Default::default(),
}
}
}
impl From<BlockEntry> for crate::approval_db::v3::BlockEntry {
fn from(entry: BlockEntry) -> Self {
Self {
block_hash: entry.block_hash,
parent_hash: entry.parent_hash,
block_number: entry.block_number,
session: entry.session,
slot: entry.slot,
relay_vrf_story: entry.relay_vrf_story.0,
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
candidates_pending_signature: entry
.candidates_pending_signature
.into_iter()
.map(|(candidate_index, signing_context)| (candidate_index, signing_context.into()))
.collect(),
distributed_assignments: entry.distributed_assignments,
}
}
}
impl From<crate::approval_db::v3::CandidateSigningContext> for CandidateSigningContext {
fn from(signing_context: crate::approval_db::v3::CandidateSigningContext) -> Self {
Self {
candidate_hash: signing_context.candidate_hash,
sign_no_later_than_tick: signing_context.sign_no_later_than_tick.into(),
}
}
}
impl From<CandidateSigningContext> for crate::approval_db::v3::CandidateSigningContext {
fn from(signing_context: CandidateSigningContext) -> Self {
Self {
candidate_hash: signing_context.candidate_hash,
sign_no_later_than_tick: signing_context.sign_no_later_than_tick.into(),
}
}
}
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
[package]
name = "pezkuwi-node-core-av-store"
description = "The Availability Store subsystem. Wrapper over the DB that stores availability data and chunks."
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
bitvec = { workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
gum = { workspace = true, default-features = true }
thiserror = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
pezkuwi-erasure-coding = { workspace = true, default-features = true }
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-primitives = { workspace = true, default-features = true }
sp-consensus = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
kvdb-memorydb = { workspace = true }
sp-tracing = { workspace = true }
parking_lot = { workspace = true, default-features = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives-test-helpers = { workspace = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-erasure-coding/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"sp-consensus/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
]
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
#[derive(Clone)]
pub(crate) struct MetricsInner {
received_availability_chunks_total: prometheus::Counter<prometheus::U64>,
pruning: prometheus::Histogram,
process_block_finalized: prometheus::Histogram,
block_activated: prometheus::Histogram,
process_message: prometheus::Histogram,
store_available_data: prometheus::Histogram,
store_chunk: prometheus::Histogram,
get_chunk: prometheus::Histogram,
}
/// Availability metrics.
#[derive(Default, Clone)]
pub struct Metrics(Option<MetricsInner>);
impl Metrics {
pub(crate) fn on_chunks_received(&self, count: usize) {
if let Some(metrics) = &self.0 {
// assume usize fits into u64
let by = u64::try_from(count).unwrap_or_default();
metrics.received_availability_chunks_total.inc_by(by);
}
}
/// Provide a timer for `prune_povs` which observes on drop.
pub(crate) fn time_pruning(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.pruning.start_timer())
}
/// Provide a timer for `process_block_finalized` which observes on drop.
pub(crate) fn time_process_block_finalized(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.process_block_finalized.start_timer())
}
/// Provide a timer for `block_activated` which observes on drop.
pub(crate) fn time_block_activated(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.block_activated.start_timer())
}
/// Provide a timer for `process_message` which observes on drop.
pub(crate) fn time_process_message(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.process_message.start_timer())
}
/// Provide a timer for `store_available_data` which observes on drop.
pub(crate) fn time_store_available_data(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.store_available_data.start_timer())
}
/// Provide a timer for `store_chunk` which observes on drop.
pub(crate) fn time_store_chunk(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.store_chunk.start_timer())
}
/// Provide a timer for `get_chunk` which observes on drop.
pub(crate) fn time_get_chunk(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.get_chunk.start_timer())
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
received_availability_chunks_total: prometheus::register(
prometheus::Counter::new(
"pezkuwi_teyrchain_received_availability_chunks_total",
"Number of availability chunks received.",
)?,
registry,
)?,
pruning: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_pruning",
"Time spent within `av_store::prune_all`",
))?,
registry,
)?,
process_block_finalized: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_process_block_finalized",
"Time spent within `av_store::process_block_finalized`",
))?,
registry,
)?,
block_activated: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_block_activated",
"Time spent within `av_store::process_block_activated`",
))?,
registry,
)?,
process_message: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_process_message",
"Time spent within `av_store::process_message`",
))?,
registry,
)?,
store_available_data: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_store_available_data",
"Time spent within `av_store::store_available_data`",
))?,
registry,
)?,
store_chunk: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_store_chunk",
"Time spent within `av_store::store_chunk`",
))?,
registry,
)?,
get_chunk: prometheus::register(
prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_av_store_get_chunk",
"Time spent fetching requested chunks.`",
)
.buckets(vec![
0.000625, 0.00125, 0.0025, 0.005, 0.0075, 0.01, 0.025, 0.05, 0.1, 0.25,
0.5, 1.0, 2.5, 5.0, 10.0,
]),
)?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
[package]
name = "pezkuwi-node-core-backing"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "The Candidate Backing Subsystem. Tracks teyrchain candidates that can be backed, as well as the issuance of statements about candidates."
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
bitvec = { features = ["alloc"], workspace = true }
fatality = { workspace = true }
futures = { workspace = true }
gum = { workspace = true, default-features = true }
pezkuwi-erasure-coding = { workspace = true, default-features = true }
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-primitives = { workspace = true, default-features = true }
pezkuwi-statement-table = { workspace = true, default-features = true }
pezkuwi-teyrchain-primitives = { workspace = true, default-features = true }
schnellru = { workspace = true }
sp-keystore = { workspace = true, default-features = true }
thiserror = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
futures = { features = ["thread-pool"], workspace = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives = { workspace = true, features = ["test"] }
pezkuwi-primitives-test-helpers = { workspace = true }
sc-keystore = { workspace = true, default-features = true }
sp-application-crypto = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-tracing = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-erasure-coding/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-statement-table/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
]
+132
View File
@@ -0,0 +1,132 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashMap;
use fatality::Nested;
use futures::channel::{mpsc, oneshot};
use pezkuwi_node_subsystem::{
messages::{StoreAvailableDataError, ValidationFailed},
RuntimeApiError, SubsystemError,
};
use pezkuwi_node_subsystem_util::{runtime, Error as UtilError};
use pezkuwi_primitives::{BackedCandidate, ValidationCodeHash};
use crate::{ParaId, LOG_TARGET};
pub type Result<T> = std::result::Result<T, Error>;
pub type FatalResult<T> = std::result::Result<T, FatalError>;
/// Errors that can occur in candidate backing.
#[allow(missing_docs)]
#[fatality::fatality(splitable)]
pub enum Error {
#[fatal]
#[error("Failed to spawn background task")]
FailedToSpawnBackgroundTask,
#[fatal(forward)]
#[error("Error while accessing runtime information")]
Runtime(#[from] runtime::Error),
#[fatal]
#[error(transparent)]
BackgroundValidationMpsc(#[from] mpsc::SendError),
#[error("Candidate is not found")]
CandidateNotFound,
#[error("CoreIndex cannot be determined for a candidate")]
CoreIndexUnavailable,
#[error("Signature is invalid")]
InvalidSignature,
#[error("Failed to send candidates {0:?}")]
Send(HashMap<ParaId, Vec<BackedCandidate>>),
#[error("FetchPoV failed")]
FetchPoV,
#[error("Fetching validation code by hash failed {0:?}, {1:?}")]
FetchValidationCode(ValidationCodeHash, RuntimeApiError),
#[error("Fetching Runtime API version failed {0:?}")]
FetchRuntimeApiVersion(RuntimeApiError),
#[error("No validation code {0:?}")]
NoValidationCode(ValidationCodeHash),
#[error("Candidate rejected by prospective teyrchains subsystem")]
RejectedByProspectiveTeyrchains,
#[error("ValidateFromExhaustive channel closed before receipt")]
ValidateFromExhaustive(#[source] oneshot::Canceled),
#[error("StoreAvailableData channel closed before receipt")]
StoreAvailableDataChannel(#[source] oneshot::Canceled),
#[error("RuntimeAPISubsystem channel closed before receipt")]
RuntimeApiUnavailable(#[source] oneshot::Canceled),
#[error("a channel was closed before receipt in try_join!")]
#[fatal]
JoinMultiple(#[source] oneshot::Canceled),
#[error("Obtaining erasure chunks failed")]
ObtainErasureChunks(#[from] pezkuwi_erasure_coding::Error),
#[error(transparent)]
ValidationFailed(#[from] ValidationFailed),
#[error(transparent)]
UtilError(#[from] UtilError),
#[error(transparent)]
SubsystemError(#[from] SubsystemError),
#[fatal]
#[error(transparent)]
OverseerExited(SubsystemError),
#[error("Availability store error")]
StoreAvailableData(#[source] StoreAvailableDataError),
#[error("Runtime API returned None for executor params")]
MissingExecutorParams,
}
/// Utility for eating top level errors and log them.
///
/// We basically always want to try and continue on error. This utility function is meant to
/// consume top-level errors by simply logging them
pub fn log_error(result: Result<()>) -> std::result::Result<(), FatalError> {
match result.into_nested()? {
Ok(()) => Ok(()),
Err(jfyi) => {
jfyi.log();
Ok(())
},
}
}
impl JfyiError {
/// Log a `JfyiError`.
pub fn log(self) {
gum::debug!(target: LOG_TARGET, error = ?self);
}
}
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
#[derive(Clone)]
pub(crate) struct MetricsInner {
pub(crate) signed_statements_total: prometheus::Counter<prometheus::U64>,
pub(crate) candidates_seconded_total: prometheus::Counter<prometheus::U64>,
pub(crate) process_second: prometheus::Histogram,
pub(crate) process_statement: prometheus::Histogram,
pub(crate) get_backed_candidates: prometheus::Histogram,
}
/// Candidate backing metrics.
#[derive(Default, Clone)]
pub struct Metrics(pub(crate) Option<MetricsInner>);
impl Metrics {
pub fn on_statement_signed(&self) {
if let Some(metrics) = &self.0 {
metrics.signed_statements_total.inc();
}
}
pub fn on_candidate_seconded(&self) {
if let Some(metrics) = &self.0 {
metrics.candidates_seconded_total.inc();
}
}
/// Provide a timer for handling `CandidateBackingMessage:Second` which observes on drop.
pub fn time_process_second(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.process_second.start_timer())
}
/// Provide a timer for handling `CandidateBackingMessage::Statement` which observes on drop.
pub fn time_process_statement(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.process_statement.start_timer())
}
/// Provide a timer for handling `CandidateBackingMessage::GetBackedCandidates` which observes
/// on drop.
pub fn time_get_backed_candidates(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.get_backed_candidates.start_timer())
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
signed_statements_total: prometheus::register(
prometheus::Counter::new(
"pezkuwi_teyrchain_candidate_backing_signed_statements_total",
"Number of statements signed.",
)?,
registry,
)?,
candidates_seconded_total: prometheus::register(
prometheus::Counter::new(
"pezkuwi_teyrchain_candidate_backing_candidates_seconded_total",
"Number of candidates seconded.",
)?,
registry,
)?,
process_second: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_candidate_backing_process_second",
"Time spent within `candidate_backing::process_second`",
))?,
registry,
)?,
process_statement: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_candidate_backing_process_statement",
"Time spent within `candidate_backing::process_statement`",
))?,
registry,
)?,
get_backed_candidates: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_candidate_backing_get_backed_candidates",
"Time spent within `candidate_backing::get_backed_candidates`",
))?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
[package]
name = "pezkuwi-node-core-bitfield-signing"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Bitfield signing subsystem for the Pezkuwi node"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
futures = { workspace = true }
gum = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
sp-keystore = { workspace = true, default-features = true }
thiserror = { workspace = true }
wasm-timer = { workspace = true }
[dev-dependencies]
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives-test-helpers = { workspace = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
]
@@ -0,0 +1,277 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The bitfield signing subsystem produces `SignedAvailabilityBitfield`s once per block.
#![deny(unused_crate_dependencies)]
#![warn(missing_docs)]
#![recursion_limit = "256"]
use futures::{
channel::{mpsc, oneshot},
future,
lock::Mutex,
FutureExt,
};
use pezkuwi_node_subsystem::{
messages::{AvailabilityStoreMessage, BitfieldDistributionMessage},
overseer, ActivatedLeaf, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError,
SubsystemResult,
};
use pezkuwi_node_subsystem_util::{
self as util, request_availability_cores, runtime::recv_runtime, Validator,
};
use pezkuwi_primitives::{AvailabilityBitfield, CoreState, Hash, ValidatorIndex};
use sp_keystore::{Error as KeystoreError, KeystorePtr};
use std::{collections::HashMap, time::Duration};
use wasm_timer::{Delay, Instant};
mod metrics;
use self::metrics::Metrics;
#[cfg(test)]
mod tests;
/// Delay between starting a bitfield signing job and its attempting to create a bitfield.
const SPAWNED_TASK_DELAY: Duration = Duration::from_millis(1500);
const LOG_TARGET: &str = "teyrchain::bitfield-signing";
// TODO: use `fatality` (https://github.com/paritytech/polkadot/issues/5540).
/// Errors we may encounter in the course of executing the `BitfieldSigningSubsystem`.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
Util(#[from] util::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error(transparent)]
MpscSend(#[from] mpsc::SendError),
#[error(transparent)]
Runtime(#[from] util::runtime::Error),
#[error("Keystore failed: {0:?}")]
Keystore(KeystoreError),
}
/// If there is a candidate pending availability, query the Availability Store
/// for whether we have the availability chunk for our validator index.
async fn get_core_availability(
core: &CoreState,
validator_index: ValidatorIndex,
sender: &Mutex<&mut impl overseer::BitfieldSigningSenderTrait>,
) -> Result<bool, Error> {
if let CoreState::Occupied(core) = core {
let (tx, rx) = oneshot::channel();
sender
.lock()
.await
.send_message(AvailabilityStoreMessage::QueryChunkAvailability(
core.candidate_hash,
validator_index,
tx,
))
.await;
let res = rx.await.map_err(Into::into);
gum::trace!(
target: LOG_TARGET,
para_id = %core.para_id(),
availability = ?res,
?core.candidate_hash,
"Candidate availability",
);
res
} else {
Ok(false)
}
}
/// - get the list of core states from the runtime
/// - for each core, concurrently determine chunk availability (see `get_core_availability`)
/// - return the bitfield if there were no errors at any point in this process (otherwise, it's
/// prone to false negatives)
async fn construct_availability_bitfield(
relay_parent: Hash,
validator_idx: ValidatorIndex,
sender: &mut impl overseer::BitfieldSigningSenderTrait,
) -> Result<AvailabilityBitfield, Error> {
// get the set of availability cores from the runtime
let availability_cores =
{ recv_runtime(request_availability_cores(relay_parent, sender).await).await? };
// Wrap the sender in a Mutex to share it between the futures.
//
// We use a `Mutex` here to not `clone` the sender inside the future, because
// cloning the sender will always increase the capacity of the channel by one.
// (for the lifetime of the sender)
let sender = Mutex::new(sender);
// Handle all cores concurrently
// `try_join_all` returns all results in the same order as the input futures.
let results = future::try_join_all(
availability_cores
.iter()
.map(|core| get_core_availability(core, validator_idx, &sender)),
)
.await?;
let core_bits = FromIterator::from_iter(results.into_iter());
gum::debug!(
target: LOG_TARGET,
?relay_parent,
"Signing Bitfield for {core_count} cores: {core_bits}",
core_count = availability_cores.len(),
core_bits = core_bits,
);
Ok(AvailabilityBitfield(core_bits))
}
/// The bitfield signing subsystem.
pub struct BitfieldSigningSubsystem {
keystore: KeystorePtr,
metrics: Metrics,
}
impl BitfieldSigningSubsystem {
/// Create a new instance of the `BitfieldSigningSubsystem`.
pub fn new(keystore: KeystorePtr, metrics: Metrics) -> Self {
Self { keystore, metrics }
}
}
#[overseer::subsystem(BitfieldSigning, error=SubsystemError, prefix=self::overseer)]
impl<Context> BitfieldSigningSubsystem {
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = async move {
run(ctx, self.keystore, self.metrics)
.await
.map_err(|e| SubsystemError::with_origin("bitfield-signing", e))
}
.boxed();
SpawnedSubsystem { name: "bitfield-signing-subsystem", future }
}
}
#[overseer::contextbounds(BitfieldSigning, prefix = self::overseer)]
async fn run<Context>(
mut ctx: Context,
keystore: KeystorePtr,
metrics: Metrics,
) -> SubsystemResult<()> {
// Track spawned jobs per active leaf.
let mut running = HashMap::<Hash, future::AbortHandle>::new();
loop {
match ctx.recv().await? {
FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => {
// Abort jobs for deactivated leaves.
for leaf in &update.deactivated {
if let Some(handle) = running.remove(leaf) {
handle.abort();
}
}
if let Some(leaf) = update.activated {
let sender = ctx.sender().clone();
let leaf_hash = leaf.hash;
let (fut, handle) = future::abortable(handle_active_leaves_update(
sender,
leaf,
keystore.clone(),
metrics.clone(),
));
running.insert(leaf_hash, handle);
ctx.spawn("bitfield-signing-job", fut.map(drop).boxed())?;
}
},
FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {},
FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()),
FromOrchestra::Communication { .. } => {},
}
}
}
async fn handle_active_leaves_update<Sender>(
mut sender: Sender,
leaf: ActivatedLeaf,
keystore: KeystorePtr,
metrics: Metrics,
) -> Result<(), Error>
where
Sender: overseer::BitfieldSigningSenderTrait,
{
let wait_until = Instant::now() + SPAWNED_TASK_DELAY;
// now do all the work we can before we need to wait for the availability store
// if we're not a validator, we can just succeed effortlessly
let validator = match Validator::new(leaf.hash, keystore.clone(), &mut sender).await {
Ok(validator) => validator,
Err(util::Error::NotAValidator) => return Ok(()),
Err(err) => return Err(Error::Util(err)),
};
// wait a bit before doing anything else
Delay::new_at(wait_until).await?;
// this timer does not appear at the head of the function because we don't want to include
// SPAWNED_TASK_DELAY each time.
let _timer = metrics.time_run();
let bitfield =
match construct_availability_bitfield(leaf.hash, validator.index(), &mut sender).await {
Err(Error::Runtime(runtime_err)) => {
// Don't take down the node on runtime API errors.
gum::warn!(target: LOG_TARGET, err = ?runtime_err, "Encountered a runtime API error");
return Ok(());
},
Err(err) => return Err(err),
Ok(bitfield) => bitfield,
};
let signed_bitfield =
match validator.sign(keystore, bitfield).map_err(|e| Error::Keystore(e))? {
Some(b) => b,
None => {
gum::error!(
target: LOG_TARGET,
"Key was found at construction, but while signing it could not be found.",
);
return Ok(());
},
};
metrics.on_bitfield_signed();
sender
.send_message(BitfieldDistributionMessage::DistributeBitfield(leaf.hash, signed_bitfield))
.await;
Ok(())
}
@@ -0,0 +1,68 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
#[derive(Clone)]
pub(crate) struct MetricsInner {
pub(crate) bitfields_signed_total: prometheus::Counter<prometheus::U64>,
pub(crate) run: prometheus::Histogram,
}
/// Bitfield signing metrics.
#[derive(Default, Clone)]
pub struct Metrics(pub(crate) Option<MetricsInner>);
impl Metrics {
pub fn on_bitfield_signed(&self) {
if let Some(metrics) = &self.0 {
metrics.bitfields_signed_total.inc();
}
}
/// Provide a timer for `prune_povs` which observes on drop.
pub fn time_run(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.run.start_timer())
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
bitfields_signed_total: prometheus::register(
prometheus::Counter::new(
"pezkuwi_teyrchain_bitfields_signed_total",
"Number of bitfields signed.",
)?,
registry,
)?,
run: prometheus::register(
prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_bitfield_signing_run",
"Time spent within `bitfield_signing::run`",
)
.buckets(vec![
0.000625, 0.00125, 0.0025, 0.005, 0.0075, 0.01, 0.025, 0.05, 0.1, 0.25,
0.5, 1.0, 2.5, 5.0, 10.0,
]),
)?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
@@ -0,0 +1,80 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use futures::{executor::block_on, pin_mut, StreamExt};
use pezkuwi_node_subsystem::messages::{AllMessages, RuntimeApiMessage, RuntimeApiRequest};
use pezkuwi_primitives::{CandidateHash, OccupiedCore};
use pezkuwi_primitives_test_helpers::dummy_candidate_descriptor_v2;
fn occupied_core(para_id: u32, candidate_hash: CandidateHash) -> CoreState {
CoreState::Occupied(OccupiedCore {
group_responsible: para_id.into(),
next_up_on_available: None,
occupied_since: 100_u32,
time_out_at: 200_u32,
next_up_on_time_out: None,
availability: Default::default(),
candidate_hash,
candidate_descriptor: dummy_candidate_descriptor_v2(Hash::zero()),
})
}
#[test]
fn construct_availability_bitfield_works() {
block_on(async move {
let relay_parent = Hash::default();
let validator_index = ValidatorIndex(1u32);
let (mut sender, mut receiver) = pezkuwi_node_subsystem_test_helpers::sender_receiver();
let future =
construct_availability_bitfield(relay_parent, validator_index, &mut sender).fuse();
pin_mut!(future);
let hash_a = CandidateHash(Hash::repeat_byte(1));
let hash_b = CandidateHash(Hash::repeat_byte(2));
loop {
futures::select! {
m = receiver.next() => match m.unwrap() {
AllMessages::RuntimeApi(
RuntimeApiMessage::Request(rp, RuntimeApiRequest::AvailabilityCores(tx)),
) => {
assert_eq!(relay_parent, rp);
tx.send(Ok(vec![CoreState::Free, occupied_core(1, hash_a), occupied_core(2, hash_b)])).unwrap();
}
AllMessages::AvailabilityStore(
AvailabilityStoreMessage::QueryChunkAvailability(c_hash, vidx, tx),
) => {
assert_eq!(validator_index, vidx.into());
tx.send(c_hash == hash_a).unwrap();
},
o => panic!("Unknown message: {:?}", o),
},
r = future => match r {
Ok(r) => {
assert!(!r.0.get(0).unwrap());
assert!(r.0.get(1).unwrap());
assert!(!r.0.get(2).unwrap());
break
},
Err(e) => panic!("Failed: {:?}", e),
},
}
}
});
}
@@ -0,0 +1,60 @@
[package]
name = "pezkuwi-node-core-candidate-validation"
description = "Pezkuwi crate that implements the Candidate Validation subsystem. Handles requests to validate candidates according to a PVF."
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
gum = { workspace = true, default-features = true }
codec = { features = ["bit-vec", "derive"], workspace = true }
sp-application-crypto = { workspace = true }
sp-keystore = { workspace = true }
pezkuwi-node-metrics = { workspace = true, default-features = true }
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 }
pezkuwi-teyrchain-primitives = { workspace = true, default-features = true }
[target.'cfg(not(any(target_os = "android", target_os = "unknown")))'.dependencies]
pezkuwi-node-core-pvf = { workspace = true, default-features = true }
[dev-dependencies]
assert_matches = { workspace = true }
futures = { features = ["thread-pool"], workspace = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives = { workspace = true, features = ["test"] }
pezkuwi-primitives-test-helpers = { workspace = true }
rstest = { workspace = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-maybe-compressed-blob = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-core-pvf/runtime-benchmarks",
"pezkuwi-node-metrics/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use super::{ValidationFailed, ValidationResult};
use pezkuwi_node_metrics::metrics::{self, prometheus};
#[derive(Clone)]
pub(crate) struct MetricsInner {
pub(crate) validation_requests: prometheus::CounterVec<prometheus::U64>,
pub(crate) validate_from_exhaustive: prometheus::Histogram,
pub(crate) validate_candidate_exhaustive: prometheus::Histogram,
}
/// Candidate validation metrics.
#[derive(Default, Clone)]
pub struct Metrics(Option<MetricsInner>);
impl Metrics {
pub fn on_validation_event(&self, event: &Result<ValidationResult, ValidationFailed>) {
if let Some(metrics) = &self.0 {
match event {
Ok(ValidationResult::Valid(_, _)) => {
metrics.validation_requests.with_label_values(&["valid"]).inc();
},
Ok(ValidationResult::Invalid(_)) => {
metrics.validation_requests.with_label_values(&["invalid"]).inc();
},
Err(_) => {
metrics.validation_requests.with_label_values(&["validation failure"]).inc();
},
}
}
}
/// Provide a timer for `validate_from_exhaustive` which observes on drop.
pub fn time_validate_from_exhaustive(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.validate_from_exhaustive.start_timer())
}
/// Provide a timer for `validate_candidate_exhaustive` which observes on drop.
pub fn time_validate_candidate_exhaustive(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0
.as_ref()
.map(|metrics| metrics.validate_candidate_exhaustive.start_timer())
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
validation_requests: prometheus::register(
prometheus::CounterVec::new(
prometheus::Opts::new(
"pezkuwi_teyrchain_validation_requests_total",
"Number of validation requests served.",
),
&["validity"],
)?,
registry,
)?,
validate_from_exhaustive: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_candidate_validation_validate_from_exhaustive",
"Time spent within `candidate_validation::validate_from_exhaustive`",
))?,
registry,
)?,
validate_candidate_exhaustive: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_candidate_validation_validate_candidate_exhaustive",
"Time spent within `candidate_validation::validate_candidate_exhaustive`",
))?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "pezkuwi-node-core-chain-api"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "The Chain API subsystem provides access to chain related utility functions like block number to hash conversions."
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
futures = { workspace = true }
gum = { workspace = true, default-features = true }
pezkuwi-node-metrics = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-node-subsystem-types = { workspace = true, default-features = true }
sc-client-api = { workspace = true, default-features = true }
sc-consensus-babe = { workspace = true, default-features = true }
[dev-dependencies]
codec = { workspace = true, default-features = true }
futures = { features = ["thread-pool"], workspace = true }
maplit = { workspace = true }
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives = { workspace = true, default-features = true }
sp-blockchain = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-metrics/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-types/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"sc-client-api/runtime-benchmarks",
"sc-consensus-babe/runtime-benchmarks",
"sp-blockchain/runtime-benchmarks",
]
+169
View File
@@ -0,0 +1,169 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Implements the Chain API Subsystem
//!
//! Provides access to the chain data. Every request may return an error.
//! At the moment, the implementation requires `Client` to implement `HeaderBackend`,
//! we may add more bounds in the future if we will need e.g. block bodies.
//!
//! Supported requests:
//! * Block hash to number
//! * Block hash to header
//! * Block weight (cumulative)
//! * Finalized block number to hash
//! * Last finalized block number
//! * Ancestors
#![deny(unused_crate_dependencies, unused_results)]
#![warn(missing_docs)]
use std::sync::Arc;
use futures::prelude::*;
use sc_client_api::AuxStore;
use futures::stream::StreamExt;
use pezkuwi_node_subsystem::{
messages::ChainApiMessage, overseer, FromOrchestra, OverseerSignal, SpawnedSubsystem,
SubsystemError, SubsystemResult,
};
use pezkuwi_node_subsystem_types::ChainApiBackend;
mod metrics;
use self::metrics::Metrics;
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "teyrchain::chain-api";
/// The Chain API Subsystem implementation.
pub struct ChainApiSubsystem<Client> {
client: Arc<Client>,
metrics: Metrics,
}
impl<Client> ChainApiSubsystem<Client> {
/// Create a new Chain API subsystem with the given client.
pub fn new(client: Arc<Client>, metrics: Metrics) -> Self {
ChainApiSubsystem { client, metrics }
}
}
#[overseer::subsystem(ChainApi, error = SubsystemError, prefix = self::overseer)]
impl<Client, Context> ChainApiSubsystem<Client>
where
Client: ChainApiBackend + AuxStore + 'static,
{
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = run::<Client, Context>(ctx, self)
.map_err(|e| SubsystemError::with_origin("chain-api", e))
.boxed();
SpawnedSubsystem { future, name: "chain-api-subsystem" }
}
}
#[overseer::contextbounds(ChainApi, prefix = self::overseer)]
async fn run<Client, Context>(
mut ctx: Context,
subsystem: ChainApiSubsystem<Client>,
) -> SubsystemResult<()>
where
Client: ChainApiBackend + AuxStore,
{
loop {
match ctx.recv().await? {
FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()),
FromOrchestra::Signal(OverseerSignal::ActiveLeaves(_)) => {},
FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {},
FromOrchestra::Communication { msg } => match msg {
ChainApiMessage::BlockNumber(hash, response_channel) => {
let _timer = subsystem.metrics.time_block_number();
let result =
subsystem.client.number(hash).await.map_err(|e| e.to_string().into());
subsystem.metrics.on_request(result.is_ok());
let _ = response_channel.send(result);
},
ChainApiMessage::BlockHeader(hash, response_channel) => {
let _timer = subsystem.metrics.time_block_header();
let result =
subsystem.client.header(hash).await.map_err(|e| e.to_string().into());
subsystem.metrics.on_request(result.is_ok());
let _ = response_channel.send(result);
},
ChainApiMessage::BlockWeight(hash, response_channel) => {
let _timer = subsystem.metrics.time_block_weight();
let result = sc_consensus_babe::block_weight(&*subsystem.client, hash)
.map_err(|e| e.to_string().into());
subsystem.metrics.on_request(result.is_ok());
let _ = response_channel.send(result);
},
ChainApiMessage::FinalizedBlockHash(number, response_channel) => {
let _timer = subsystem.metrics.time_finalized_block_hash();
// Note: we don't verify it's finalized
let result =
subsystem.client.hash(number).await.map_err(|e| e.to_string().into());
subsystem.metrics.on_request(result.is_ok());
let _ = response_channel.send(result);
},
ChainApiMessage::FinalizedBlockNumber(response_channel) => {
let _timer = subsystem.metrics.time_finalized_block_number();
let result = subsystem
.client
.info()
.await
.map_err(|e| e.to_string().into())
.map(|info| info.finalized_number);
subsystem.metrics.on_request(result.is_ok());
let _ = response_channel.send(result);
},
ChainApiMessage::Ancestors { hash, k, response_channel } => {
let _timer = subsystem.metrics.time_ancestors();
gum::trace!(target: LOG_TARGET, hash=%hash, k=k, "ChainApiMessage::Ancestors");
let next_parent_stream = futures::stream::unfold(
(hash, subsystem.client.clone()),
|(hash, client)| async move {
let maybe_header = client.header(hash).await;
match maybe_header {
// propagate the error
Err(e) => {
let e = e.to_string().into();
Some((Err(e), (hash, client)))
},
// fewer than `k` ancestors are available
Ok(None) => None,
Ok(Some(header)) => {
// stop at the genesis header.
if header.number == 0 {
None
} else {
Some((Ok(header.parent_hash), (header.parent_hash, client)))
}
},
}
},
);
let result = next_parent_stream.take(k).try_collect().await;
subsystem.metrics.on_request(result.is_ok());
let _ = response_channel.send(result);
},
},
}
}
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_node_metrics::metrics::{self, prometheus};
#[derive(Clone)]
pub(crate) struct MetricsInner {
pub(crate) chain_api_requests: prometheus::CounterVec<prometheus::U64>,
pub(crate) block_number: prometheus::Histogram,
pub(crate) block_header: prometheus::Histogram,
pub(crate) block_weight: prometheus::Histogram,
pub(crate) finalized_block_hash: prometheus::Histogram,
pub(crate) finalized_block_number: prometheus::Histogram,
pub(crate) ancestors: prometheus::Histogram,
}
/// Chain API metrics.
#[derive(Default, Clone)]
pub struct Metrics(pub(crate) Option<MetricsInner>);
impl Metrics {
pub fn on_request(&self, succeeded: bool) {
if let Some(metrics) = &self.0 {
if succeeded {
metrics.chain_api_requests.with_label_values(&["succeeded"]).inc();
} else {
metrics.chain_api_requests.with_label_values(&["failed"]).inc();
}
}
}
/// Provide a timer for `block_number` which observes on drop.
pub fn time_block_number(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.block_number.start_timer())
}
/// Provide a timer for `block_header` which observes on drop.
pub fn time_block_header(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.block_header.start_timer())
}
/// Provide a timer for `block_weight` which observes on drop.
pub fn time_block_weight(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.block_weight.start_timer())
}
/// Provide a timer for `finalized_block_hash` which observes on drop.
pub fn time_finalized_block_hash(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.finalized_block_hash.start_timer())
}
/// Provide a timer for `finalized_block_number` which observes on drop.
pub fn time_finalized_block_number(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.finalized_block_number.start_timer())
}
/// Provide a timer for `ancestors` which observes on drop.
pub fn time_ancestors(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.ancestors.start_timer())
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
chain_api_requests: prometheus::register(
prometheus::CounterVec::new(
prometheus::Opts::new(
"pezkuwi_teyrchain_chain_api_requests_total",
"Number of Chain API requests served.",
),
&["success"],
)?,
registry,
)?,
block_number: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_chain_api_block_number",
"Time spent within `chain_api::block_number`",
))?,
registry,
)?,
block_header: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_chain_api_block_headers",
"Time spent within `chain_api::block_headers`",
))?,
registry,
)?,
block_weight: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_chain_api_block_weight",
"Time spent within `chain_api::block_weight`",
))?,
registry,
)?,
finalized_block_hash: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_chain_api_finalized_block_hash",
"Time spent within `chain_api::finalized_block_hash`",
))?,
registry,
)?,
finalized_block_number: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_chain_api_finalized_block_number",
"Time spent within `chain_api::finalized_block_number`",
))?,
registry,
)?,
ancestors: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_chain_api_ancestors",
"Time spent within `chain_api::ancestors`",
))?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
+373
View File
@@ -0,0 +1,373 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use codec::Encode;
use futures::{channel::oneshot, future::BoxFuture};
use std::collections::BTreeMap;
use pezkuwi_node_primitives::BlockWeight;
use pezkuwi_node_subsystem_test_helpers::{make_subsystem_context, TestSubsystemContextHandle};
use pezkuwi_node_subsystem_types::ChainApiBackend;
use pezkuwi_primitives::{Block, BlockNumber, Hash, Header};
use sp_blockchain::Info as BlockInfo;
use sp_core::testing::TaskExecutor;
#[derive(Clone)]
struct TestClient {
blocks: BTreeMap<Hash, BlockNumber>,
block_weights: BTreeMap<Hash, BlockWeight>,
finalized_blocks: BTreeMap<BlockNumber, Hash>,
headers: BTreeMap<Hash, Header>,
}
const GENESIS: Hash = Hash::repeat_byte(0xAA);
const ONE: Hash = Hash::repeat_byte(0x01);
const TWO: Hash = Hash::repeat_byte(0x02);
const THREE: Hash = Hash::repeat_byte(0x03);
const FOUR: Hash = Hash::repeat_byte(0x04);
const ERROR_PATH: Hash = Hash::repeat_byte(0xFF);
fn default_header() -> Header {
Header {
parent_hash: Hash::zero(),
number: 100500,
state_root: Hash::zero(),
extrinsics_root: Hash::zero(),
digest: Default::default(),
}
}
impl Default for TestClient {
fn default() -> Self {
Self {
blocks: maplit::btreemap! {
GENESIS => 0,
ONE => 1,
TWO => 2,
THREE => 3,
FOUR => 4,
},
block_weights: maplit::btreemap! {
ONE => 0,
TWO => 1,
THREE => 1,
FOUR => 2,
},
finalized_blocks: maplit::btreemap! {
1 => ONE,
3 => THREE,
},
headers: maplit::btreemap! {
GENESIS => Header {
parent_hash: Hash::zero(), // Dummy parent with zero hash.
number: 0,
..default_header()
},
ONE => Header {
parent_hash: GENESIS,
number: 1,
..default_header()
},
TWO => Header {
parent_hash: ONE,
number: 2,
..default_header()
},
THREE => Header {
parent_hash: TWO,
number: 3,
..default_header()
},
FOUR => Header {
parent_hash: THREE,
number: 4,
..default_header()
},
ERROR_PATH => Header {
..default_header()
}
},
}
}
}
fn last_key_value<K: Clone, V: Clone>(map: &BTreeMap<K, V>) -> (K, V) {
assert!(!map.is_empty());
map.iter().last().map(|(k, v)| (k.clone(), v.clone())).unwrap()
}
impl sp_blockchain::HeaderBackend<Block> for TestClient {
fn info(&self) -> BlockInfo<Block> {
let genesis_hash = self.blocks.iter().next().map(|(h, _)| *h).unwrap();
let (best_hash, best_number) = last_key_value(&self.blocks);
let (finalized_number, finalized_hash) = last_key_value(&self.finalized_blocks);
BlockInfo {
best_hash,
best_number,
genesis_hash,
finalized_hash,
finalized_number,
number_leaves: 0,
finalized_state: None,
block_gap: None,
}
}
fn number(&self, hash: Hash) -> sp_blockchain::Result<Option<BlockNumber>> {
Ok(self.blocks.get(&hash).copied())
}
fn hash(&self, number: BlockNumber) -> sp_blockchain::Result<Option<Hash>> {
Ok(self.finalized_blocks.get(&number).copied())
}
fn header(&self, hash: Hash) -> sp_blockchain::Result<Option<Header>> {
if hash.is_zero() {
Err(sp_blockchain::Error::Backend("Zero hashes are illegal!".into()))
} else {
Ok(self.headers.get(&hash).cloned())
}
}
fn status(&self, _hash: Hash) -> sp_blockchain::Result<sp_blockchain::BlockStatus> {
unimplemented!()
}
}
fn test_harness(
test: impl FnOnce(
Arc<TestClient>,
TestSubsystemContextHandle<ChainApiMessage>,
) -> BoxFuture<'static, ()>,
) {
let (ctx, ctx_handle) = make_subsystem_context(TaskExecutor::new());
let client = Arc::new(TestClient::default());
let subsystem = ChainApiSubsystem::new(client.clone(), Metrics(None));
let chain_api_task = run(ctx, subsystem).map(|x| x.unwrap());
let test_task = test(client, ctx_handle);
futures::executor::block_on(future::join(chain_api_task, test_task));
}
impl AuxStore for TestClient {
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,
) -> sp_blockchain::Result<()> {
unimplemented!()
}
fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
Ok(self
.block_weights
.iter()
.find(|(hash, _)| sc_consensus_babe::aux_schema::block_weight_key(hash) == key)
.map(|(_, weight)| weight.encode()))
}
}
#[test]
fn request_block_number() {
test_harness(|client, mut sender| {
async move {
let zero = Hash::zero();
let test_cases = [
(TWO, client.number(TWO).await.unwrap()),
(zero, client.number(zero).await.unwrap()), // not here
];
for (hash, expected) in &test_cases {
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::BlockNumber(*hash, tx),
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), *expected);
}
sender.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
}
.boxed()
})
}
#[test]
fn request_block_header() {
test_harness(|client, mut sender| {
async move {
const NOT_HERE: Hash = Hash::repeat_byte(0x5);
let test_cases = [
(TWO, client.header(TWO).await.unwrap()),
(NOT_HERE, client.header(NOT_HERE).await.unwrap()),
];
for (hash, expected) in &test_cases {
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::BlockHeader(*hash, tx),
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), *expected);
}
sender.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
}
.boxed()
})
}
#[test]
fn request_block_weight() {
test_harness(|client, mut sender| {
async move {
const NOT_HERE: Hash = Hash::repeat_byte(0x5);
let test_cases = [
(TWO, sc_consensus_babe::block_weight(&*client, TWO).unwrap()),
(FOUR, sc_consensus_babe::block_weight(&*client, FOUR).unwrap()),
(NOT_HERE, sc_consensus_babe::block_weight(&*client, NOT_HERE).unwrap()),
];
for (hash, expected) in &test_cases {
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::BlockWeight(*hash, tx),
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), *expected);
}
sender.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
}
.boxed()
})
}
#[test]
fn request_finalized_hash() {
test_harness(|client, mut sender| {
async move {
let test_cases = [
(1, client.hash(1).await.unwrap()), // not here
(2, client.hash(2).await.unwrap()),
];
for (number, expected) in &test_cases {
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::FinalizedBlockHash(*number, tx),
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), *expected);
}
sender.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
}
.boxed()
})
}
#[test]
fn request_last_finalized_number() {
test_harness(|client, mut sender| {
async move {
let (tx, rx) = oneshot::channel();
let expected = client.info().await.unwrap().finalized_number;
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::FinalizedBlockNumber(tx),
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), expected);
sender.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
}
.boxed()
})
}
#[test]
fn request_ancestors() {
test_harness(|_client, mut sender| {
async move {
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::Ancestors { hash: THREE, k: 4, response_channel: tx },
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), vec![TWO, ONE, GENESIS]);
// Limit the number of ancestors.
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::Ancestors { hash: TWO, k: 1, response_channel: tx },
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), vec![ONE]);
// Ancestor of block #1 is returned.
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::Ancestors { hash: ONE, k: 10, response_channel: tx },
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), vec![GENESIS]);
// No ancestors of genesis block.
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::Ancestors { hash: GENESIS, k: 10, response_channel: tx },
})
.await;
assert_eq!(rx.await.unwrap().unwrap(), Vec::new());
let (tx, rx) = oneshot::channel();
sender
.send(FromOrchestra::Communication {
msg: ChainApiMessage::Ancestors {
hash: ERROR_PATH,
k: 2,
response_channel: tx,
},
})
.await;
assert!(rx.await.unwrap().is_err());
sender.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
}
.boxed()
})
}
@@ -0,0 +1,40 @@
[package]
name = "pezkuwi-node-core-chain-selection"
description = "Chain Selection Subsystem"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
gum = { workspace = true, default-features = true }
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-primitives = { workspace = true, default-features = true }
thiserror = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
kvdb-memorydb = { workspace = true }
parking_lot = { workspace = true, default-features = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
sp-core = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
]
@@ -0,0 +1,237 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! An abstraction over storage used by the chain selection subsystem.
//!
//! This provides both a [`Backend`] trait and an [`OverlayedBackend`]
//! struct which allows in-memory changes to be applied on top of a
//! [`Backend`], maintaining consistency between queries and temporary writes,
//! before any commit to the underlying storage is made.
use pezkuwi_primitives::{BlockNumber, Hash};
use std::collections::HashMap;
use crate::{BlockEntry, Error, LeafEntrySet, Timestamp};
pub(super) enum BackendWriteOp {
WriteBlockEntry(BlockEntry),
WriteBlocksByNumber(BlockNumber, Vec<Hash>),
WriteViableLeaves(LeafEntrySet),
WriteStagnantAt(Timestamp, Vec<Hash>),
DeleteBlocksByNumber(BlockNumber),
DeleteBlockEntry(Hash),
DeleteStagnantAt(Timestamp),
}
/// An abstraction over backend storage for the logic of this subsystem.
pub(super) trait Backend {
/// Load a block entry from the DB.
fn load_block_entry(&self, hash: &Hash) -> Result<Option<BlockEntry>, Error>;
/// Load the active-leaves set.
fn load_leaves(&self) -> Result<LeafEntrySet, Error>;
/// Load the stagnant list at the given timestamp.
fn load_stagnant_at(&self, timestamp: Timestamp) -> Result<Vec<Hash>, Error>;
/// Load all stagnant lists up to and including the given Unix timestamp
/// in ascending order. Stop fetching stagnant entries upon reaching `max_elements`.
fn load_stagnant_at_up_to(
&self,
up_to: Timestamp,
max_elements: usize,
) -> Result<Vec<(Timestamp, Vec<Hash>)>, Error>;
/// Load the earliest kept block number.
fn load_first_block_number(&self) -> Result<Option<BlockNumber>, Error>;
/// Load blocks by number.
fn load_blocks_by_number(&self, number: BlockNumber) -> Result<Vec<Hash>, Error>;
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> Result<(), Error>
where
I: IntoIterator<Item = BackendWriteOp>;
}
/// An in-memory overlay over the backend.
///
/// This maintains read-only access to the underlying backend, but can be
/// converted into a set of write operations which will, when written to
/// the underlying backend, give the same view as the state of the overlay.
pub(super) struct OverlayedBackend<'a, B: 'a> {
inner: &'a B,
// `None` means 'deleted', missing means query inner.
block_entries: HashMap<Hash, Option<BlockEntry>>,
// `None` means 'deleted', missing means query inner.
blocks_by_number: HashMap<BlockNumber, Option<Vec<Hash>>>,
// 'None' means 'deleted', missing means query inner.
stagnant_at: HashMap<Timestamp, Option<Vec<Hash>>>,
// 'None' means query inner.
leaves: Option<LeafEntrySet>,
}
impl<'a, B: 'a + Backend> OverlayedBackend<'a, B> {
pub(super) fn new(backend: &'a B) -> Self {
OverlayedBackend {
inner: backend,
block_entries: HashMap::new(),
blocks_by_number: HashMap::new(),
stagnant_at: HashMap::new(),
leaves: None,
}
}
pub(super) fn load_block_entry(&self, hash: &Hash) -> Result<Option<BlockEntry>, Error> {
if let Some(val) = self.block_entries.get(&hash) {
return Ok(val.clone());
}
self.inner.load_block_entry(hash)
}
pub(super) fn load_blocks_by_number(&self, number: BlockNumber) -> Result<Vec<Hash>, Error> {
if let Some(val) = self.blocks_by_number.get(&number) {
return Ok(val.as_ref().map_or(Vec::new(), Clone::clone));
}
self.inner.load_blocks_by_number(number)
}
pub(super) fn load_leaves(&self) -> Result<LeafEntrySet, Error> {
if let Some(ref set) = self.leaves {
return Ok(set.clone());
}
self.inner.load_leaves()
}
pub(super) fn load_stagnant_at(&self, timestamp: Timestamp) -> Result<Vec<Hash>, Error> {
if let Some(val) = self.stagnant_at.get(&timestamp) {
return Ok(val.as_ref().map_or(Vec::new(), Clone::clone));
}
self.inner.load_stagnant_at(timestamp)
}
pub(super) fn write_block_entry(&mut self, entry: BlockEntry) {
self.block_entries.insert(entry.block_hash, Some(entry));
}
pub(super) fn delete_block_entry(&mut self, hash: &Hash) {
self.block_entries.insert(*hash, None);
}
pub(super) fn write_blocks_by_number(&mut self, number: BlockNumber, blocks: Vec<Hash>) {
if blocks.is_empty() {
self.blocks_by_number.insert(number, None);
} else {
self.blocks_by_number.insert(number, Some(blocks));
}
}
pub(super) fn delete_blocks_by_number(&mut self, number: BlockNumber) {
self.blocks_by_number.insert(number, None);
}
pub(super) fn write_leaves(&mut self, leaves: LeafEntrySet) {
self.leaves = Some(leaves);
}
pub(super) fn write_stagnant_at(&mut self, timestamp: Timestamp, hashes: Vec<Hash>) {
self.stagnant_at.insert(timestamp, Some(hashes));
}
pub(super) fn delete_stagnant_at(&mut self, timestamp: Timestamp) {
self.stagnant_at.insert(timestamp, None);
}
/// Transform this backend into a set of write-ops to be written to the
/// inner backend.
pub(super) fn into_write_ops(self) -> impl Iterator<Item = BackendWriteOp> {
let block_entry_ops = self.block_entries.into_iter().map(|(h, v)| match v {
Some(v) => BackendWriteOp::WriteBlockEntry(v),
None => BackendWriteOp::DeleteBlockEntry(h),
});
let blocks_by_number_ops = self.blocks_by_number.into_iter().map(|(n, v)| match v {
Some(v) => BackendWriteOp::WriteBlocksByNumber(n, v),
None => BackendWriteOp::DeleteBlocksByNumber(n),
});
let leaf_ops = self.leaves.into_iter().map(BackendWriteOp::WriteViableLeaves);
let stagnant_at_ops = self.stagnant_at.into_iter().map(|(n, v)| match v {
Some(v) => BackendWriteOp::WriteStagnantAt(n, v),
None => BackendWriteOp::DeleteStagnantAt(n),
});
block_entry_ops
.chain(blocks_by_number_ops)
.chain(leaf_ops)
.chain(stagnant_at_ops)
}
}
/// Attempt to find the given ancestor in the chain with given head.
///
/// If the ancestor is the most recently finalized block, and the `head` is
/// a known unfinalized block, this will return `true`.
///
/// If the ancestor is an unfinalized block and `head` is known, this will
/// return true if `ancestor` is in `head`'s chain.
///
/// If the ancestor is an older finalized block, this will return `false`.
fn contains_ancestor(backend: &impl Backend, head: Hash, ancestor: Hash) -> Result<bool, Error> {
let mut current_hash = head;
loop {
if current_hash == ancestor {
return Ok(true);
}
match backend.load_block_entry(&current_hash)? {
Some(e) => current_hash = e.parent_hash,
None => break,
}
}
Ok(false)
}
/// This returns the best unfinalized leaf containing the required block.
///
/// If the required block is finalized but not the most recent finalized block,
/// this will return `None`.
///
/// If the required block is unfinalized but not an ancestor of any viable leaf,
/// this will return `None`.
//
// Note: this is O(N^2) in the depth of `required` and the number of leaves.
// We expect the number of unfinalized blocks to be small, as in, to not exceed
// single digits in practice, and exceedingly unlikely to surpass 1000.
//
// However, if we need to, we could implement some type of skip-list for
// fast ancestry checks.
pub(super) fn find_best_leaf_containing(
backend: &impl Backend,
required: Hash,
) -> Result<Option<Hash>, Error> {
let leaves = backend.load_leaves()?;
for leaf in leaves.into_hashes_descending() {
if contains_ancestor(backend, leaf, required)? {
return Ok(Some(leaf));
}
}
// If there are no viable leaves containing the ancestor
Ok(None)
}
@@ -0,0 +1,19 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! A database [`Backend`][crate::backend::Backend] for the chain selection subsystem.
pub(super) mod v1;
@@ -0,0 +1,631 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! A database [`Backend`][crate::backend::Backend] for the chain selection subsystem.
//!
//! This stores the following schema:
//!
//! ```ignore
//! ("CS_block_entry", Hash) -> BlockEntry;
//! ("CS_block_height", BigEndianBlockNumber) -> Vec<Hash>;
//! ("CS_stagnant_at", BigEndianTimestamp) -> Vec<Hash>;
//! ("CS_leaves") -> LeafEntrySet;
//! ```
//!
//! The big-endian encoding is used for creating iterators over the key-value DB which are
//! accessible by prefix, to find the earliest block number stored as well as the all stagnant
//! blocks.
//!
//! The `Vec`s stored are always non-empty. Empty `Vec`s are not stored on disk so there is no
//! semantic difference between `None` and an empty `Vec`.
use crate::{
backend::{Backend, BackendWriteOp},
Error,
};
use pezkuwi_node_primitives::BlockWeight;
use pezkuwi_primitives::{BlockNumber, Hash};
use codec::{Decode, Encode};
use pezkuwi_node_subsystem_util::database::{DBTransaction, Database};
use std::sync::Arc;
const BLOCK_ENTRY_PREFIX: &[u8; 14] = b"CS_block_entry";
const BLOCK_HEIGHT_PREFIX: &[u8; 15] = b"CS_block_height";
const STAGNANT_AT_PREFIX: &[u8; 14] = b"CS_stagnant_at";
const LEAVES_KEY: &[u8; 9] = b"CS_leaves";
type Timestamp = u64;
#[derive(Debug, Encode, Decode, Clone, PartialEq)]
enum Approval {
#[codec(index = 0)]
Approved,
#[codec(index = 1)]
Unapproved,
#[codec(index = 2)]
Stagnant,
}
impl From<crate::Approval> for Approval {
fn from(x: crate::Approval) -> Self {
match x {
crate::Approval::Approved => Approval::Approved,
crate::Approval::Unapproved => Approval::Unapproved,
crate::Approval::Stagnant => Approval::Stagnant,
}
}
}
impl From<Approval> for crate::Approval {
fn from(x: Approval) -> crate::Approval {
match x {
Approval::Approved => crate::Approval::Approved,
Approval::Unapproved => crate::Approval::Unapproved,
Approval::Stagnant => crate::Approval::Stagnant,
}
}
}
#[derive(Debug, Encode, Decode, Clone, PartialEq)]
struct ViabilityCriteria {
explicitly_reverted: bool,
approval: Approval,
earliest_unviable_ancestor: Option<Hash>,
}
impl From<crate::ViabilityCriteria> for ViabilityCriteria {
fn from(x: crate::ViabilityCriteria) -> Self {
ViabilityCriteria {
explicitly_reverted: x.explicitly_reverted,
approval: x.approval.into(),
earliest_unviable_ancestor: x.earliest_unviable_ancestor,
}
}
}
impl From<ViabilityCriteria> for crate::ViabilityCriteria {
fn from(x: ViabilityCriteria) -> crate::ViabilityCriteria {
crate::ViabilityCriteria {
explicitly_reverted: x.explicitly_reverted,
approval: x.approval.into(),
earliest_unviable_ancestor: x.earliest_unviable_ancestor,
}
}
}
#[derive(Encode, Decode)]
struct LeafEntry {
weight: BlockWeight,
block_number: BlockNumber,
block_hash: Hash,
}
impl From<crate::LeafEntry> for LeafEntry {
fn from(x: crate::LeafEntry) -> Self {
LeafEntry { weight: x.weight, block_number: x.block_number, block_hash: x.block_hash }
}
}
impl From<LeafEntry> for crate::LeafEntry {
fn from(x: LeafEntry) -> crate::LeafEntry {
crate::LeafEntry {
weight: x.weight,
block_number: x.block_number,
block_hash: x.block_hash,
}
}
}
#[derive(Encode, Decode)]
struct LeafEntrySet {
inner: Vec<LeafEntry>,
}
impl From<crate::LeafEntrySet> for LeafEntrySet {
fn from(x: crate::LeafEntrySet) -> Self {
LeafEntrySet { inner: x.inner.into_iter().map(Into::into).collect() }
}
}
impl From<LeafEntrySet> for crate::LeafEntrySet {
fn from(x: LeafEntrySet) -> crate::LeafEntrySet {
crate::LeafEntrySet { inner: x.inner.into_iter().map(Into::into).collect() }
}
}
#[derive(Debug, Encode, Decode, Clone, PartialEq)]
struct BlockEntry {
block_hash: Hash,
block_number: BlockNumber,
parent_hash: Hash,
children: Vec<Hash>,
viability: ViabilityCriteria,
weight: BlockWeight,
}
impl From<crate::BlockEntry> for BlockEntry {
fn from(x: crate::BlockEntry) -> Self {
BlockEntry {
block_hash: x.block_hash,
block_number: x.block_number,
parent_hash: x.parent_hash,
children: x.children,
viability: x.viability.into(),
weight: x.weight,
}
}
}
impl From<BlockEntry> for crate::BlockEntry {
fn from(x: BlockEntry) -> crate::BlockEntry {
crate::BlockEntry {
block_hash: x.block_hash,
block_number: x.block_number,
parent_hash: x.parent_hash,
children: x.children,
viability: x.viability.into(),
weight: x.weight,
}
}
}
/// Configuration for the database backend.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column where block metadata is stored.
pub col_data: u32,
}
/// The database backend.
pub struct DbBackend {
inner: Arc<dyn Database>,
config: Config,
}
impl DbBackend {
/// Create a new [`DbBackend`] with the supplied key-value store and
/// config.
pub fn new(db: Arc<dyn Database>, config: Config) -> Self {
DbBackend { inner: db, config }
}
}
impl Backend for DbBackend {
fn load_block_entry(&self, hash: &Hash) -> Result<Option<crate::BlockEntry>, Error> {
load_decode::<BlockEntry>(&*self.inner, self.config.col_data, &block_entry_key(hash))
.map(|o| o.map(Into::into))
}
fn load_leaves(&self) -> Result<crate::LeafEntrySet, Error> {
load_decode::<LeafEntrySet>(&*self.inner, self.config.col_data, LEAVES_KEY)
.map(|o| o.map(Into::into).unwrap_or_default())
}
fn load_stagnant_at(&self, timestamp: crate::Timestamp) -> Result<Vec<Hash>, Error> {
load_decode::<Vec<Hash>>(
&*self.inner,
self.config.col_data,
&stagnant_at_key(timestamp.into()),
)
.map(|o| o.unwrap_or_default())
}
fn load_stagnant_at_up_to(
&self,
up_to: crate::Timestamp,
max_elements: usize,
) -> Result<Vec<(crate::Timestamp, Vec<Hash>)>, Error> {
let stagnant_at_iter =
self.inner.iter_with_prefix(self.config.col_data, &STAGNANT_AT_PREFIX[..]);
let val = stagnant_at_iter
.filter_map(|r| match r {
Ok((k, v)) => {
match (decode_stagnant_at_key(&mut &k[..]), <Vec<_>>::decode(&mut &v[..]).ok())
{
(Some(at), Some(stagnant_at)) => Some(Ok((at, stagnant_at))),
_ => None,
}
},
Err(e) => Some(Err(e)),
})
.enumerate()
.take_while(|(idx, r)| {
r.as_ref().map_or(true, |(at, _)| *at <= up_to.into() && *idx < max_elements)
})
.map(|(_, v)| v)
.collect::<Result<Vec<_>, _>>()?;
Ok(val)
}
fn load_first_block_number(&self) -> Result<Option<BlockNumber>, Error> {
let blocks_at_height_iter =
self.inner.iter_with_prefix(self.config.col_data, &BLOCK_HEIGHT_PREFIX[..]);
let val = blocks_at_height_iter
.filter_map(|r| match r {
Ok((k, _)) => decode_block_height_key(&k[..]).map(Ok),
Err(e) => Some(Err(e)),
})
.next();
val.transpose().map_err(Error::from)
}
fn load_blocks_by_number(&self, number: BlockNumber) -> Result<Vec<Hash>, Error> {
load_decode::<Vec<Hash>>(&*self.inner, self.config.col_data, &block_height_key(number))
.map(|o| o.unwrap_or_default())
}
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> Result<(), Error>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
for op in ops {
match op {
BackendWriteOp::WriteBlockEntry(block_entry) => {
let block_entry: BlockEntry = block_entry.into();
tx.put_vec(
self.config.col_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
},
BackendWriteOp::WriteBlocksByNumber(block_number, v) =>
if v.is_empty() {
tx.delete(self.config.col_data, &block_height_key(block_number));
} else {
tx.put_vec(
self.config.col_data,
&block_height_key(block_number),
v.encode(),
);
},
BackendWriteOp::WriteViableLeaves(leaves) => {
let leaves: LeafEntrySet = leaves.into();
if leaves.inner.is_empty() {
tx.delete(self.config.col_data, &LEAVES_KEY[..]);
} else {
tx.put_vec(self.config.col_data, &LEAVES_KEY[..], leaves.encode());
}
},
BackendWriteOp::WriteStagnantAt(timestamp, stagnant_at) => {
let timestamp: Timestamp = timestamp.into();
if stagnant_at.is_empty() {
tx.delete(self.config.col_data, &stagnant_at_key(timestamp));
} else {
tx.put_vec(
self.config.col_data,
&stagnant_at_key(timestamp),
stagnant_at.encode(),
);
}
},
BackendWriteOp::DeleteBlocksByNumber(block_number) => {
tx.delete(self.config.col_data, &block_height_key(block_number));
},
BackendWriteOp::DeleteBlockEntry(hash) => {
tx.delete(self.config.col_data, &block_entry_key(&hash));
},
BackendWriteOp::DeleteStagnantAt(timestamp) => {
let timestamp: Timestamp = timestamp.into();
tx.delete(self.config.col_data, &stagnant_at_key(timestamp));
},
}
}
self.inner.write(tx).map_err(Into::into)
}
}
fn load_decode<D: Decode>(
db: &dyn Database,
col_data: u32,
key: &[u8],
) -> Result<Option<D>, Error> {
match db.get(col_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
fn block_entry_key(hash: &Hash) -> [u8; 14 + 32] {
let mut key = [0; 14 + 32];
key[..14].copy_from_slice(BLOCK_ENTRY_PREFIX);
hash.using_encoded(|s| key[14..].copy_from_slice(s));
key
}
fn block_height_key(number: BlockNumber) -> [u8; 15 + 4] {
let mut key = [0; 15 + 4];
key[..15].copy_from_slice(BLOCK_HEIGHT_PREFIX);
key[15..].copy_from_slice(&number.to_be_bytes());
key
}
fn stagnant_at_key(timestamp: Timestamp) -> [u8; 14 + 8] {
let mut key = [0; 14 + 8];
key[..14].copy_from_slice(STAGNANT_AT_PREFIX);
key[14..].copy_from_slice(&timestamp.to_be_bytes());
key
}
fn decode_block_height_key(key: &[u8]) -> Option<BlockNumber> {
if key.len() != 15 + 4 {
return None;
}
if !key.starts_with(BLOCK_HEIGHT_PREFIX) {
return None;
}
let mut bytes = [0; 4];
bytes.copy_from_slice(&key[15..]);
Some(BlockNumber::from_be_bytes(bytes))
}
fn decode_stagnant_at_key(key: &[u8]) -> Option<Timestamp> {
if key.len() != 14 + 8 {
return None;
}
if !key.starts_with(STAGNANT_AT_PREFIX) {
return None;
}
let mut bytes = [0; 8];
bytes.copy_from_slice(&key[14..]);
Some(Timestamp::from_be_bytes(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
fn test_db() -> Arc<dyn Database> {
let db = kvdb_memorydb::create(1);
let db = pezkuwi_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[0]);
Arc::new(db)
}
#[test]
fn block_height_key_decodes() {
let key = block_height_key(5);
assert_eq!(decode_block_height_key(&key), Some(5));
}
#[test]
fn stagnant_at_key_decodes() {
let key = stagnant_at_key(5);
assert_eq!(decode_stagnant_at_key(&key), Some(5));
}
#[test]
fn lower_block_height_key_lesser() {
for i in 0..256 {
for j in 1..=256 {
let key_a = block_height_key(i);
let key_b = block_height_key(i + j);
assert!(key_a < key_b);
}
}
}
#[test]
fn lower_stagnant_at_key_lesser() {
for i in 0..256 {
for j in 1..=256 {
let key_a = stagnant_at_key(i);
let key_b = stagnant_at_key(i + j);
assert!(key_a < key_b);
}
}
}
#[test]
fn write_read_block_entry() {
let db = test_db();
let config = Config { col_data: 0 };
let mut backend = DbBackend::new(db, config);
let block_entry = BlockEntry {
block_hash: Hash::repeat_byte(1),
block_number: 1,
parent_hash: Hash::repeat_byte(0),
children: vec![],
viability: ViabilityCriteria {
earliest_unviable_ancestor: None,
explicitly_reverted: false,
approval: Approval::Unapproved,
},
weight: 100,
};
backend
.write(vec![BackendWriteOp::WriteBlockEntry(block_entry.clone().into())])
.unwrap();
assert_eq!(
backend.load_block_entry(&block_entry.block_hash).unwrap().map(BlockEntry::from),
Some(block_entry),
);
}
#[test]
fn delete_block_entry() {
let db = test_db();
let config = Config { col_data: 0 };
let mut backend = DbBackend::new(db, config);
let block_entry = BlockEntry {
block_hash: Hash::repeat_byte(1),
block_number: 1,
parent_hash: Hash::repeat_byte(0),
children: vec![],
viability: ViabilityCriteria {
earliest_unviable_ancestor: None,
explicitly_reverted: false,
approval: Approval::Unapproved,
},
weight: 100,
};
backend
.write(vec![BackendWriteOp::WriteBlockEntry(block_entry.clone().into())])
.unwrap();
backend
.write(vec![BackendWriteOp::DeleteBlockEntry(block_entry.block_hash)])
.unwrap();
assert!(backend.load_block_entry(&block_entry.block_hash).unwrap().is_none());
}
#[test]
fn earliest_block_number() {
let db = test_db();
let config = Config { col_data: 0 };
let mut backend = DbBackend::new(db, config);
assert!(backend.load_first_block_number().unwrap().is_none());
backend
.write(vec![
BackendWriteOp::WriteBlocksByNumber(2, vec![Hash::repeat_byte(0)]),
BackendWriteOp::WriteBlocksByNumber(5, vec![Hash::repeat_byte(0)]),
BackendWriteOp::WriteBlocksByNumber(10, vec![Hash::repeat_byte(0)]),
])
.unwrap();
assert_eq!(backend.load_first_block_number().unwrap(), Some(2));
backend
.write(vec![
BackendWriteOp::WriteBlocksByNumber(2, vec![]),
BackendWriteOp::DeleteBlocksByNumber(5),
])
.unwrap();
assert_eq!(backend.load_first_block_number().unwrap(), Some(10));
}
#[test]
fn stagnant_at_up_to() {
let db = test_db();
let config = Config { col_data: 0 };
let mut backend = DbBackend::new(db, config);
// Prove that it's cheap
assert!(backend
.load_stagnant_at_up_to(Timestamp::max_value(), usize::MAX)
.unwrap()
.is_empty());
backend
.write(vec![
BackendWriteOp::WriteStagnantAt(2, vec![Hash::repeat_byte(1)]),
BackendWriteOp::WriteStagnantAt(5, vec![Hash::repeat_byte(2)]),
BackendWriteOp::WriteStagnantAt(10, vec![Hash::repeat_byte(3)]),
])
.unwrap();
assert_eq!(
backend.load_stagnant_at_up_to(Timestamp::max_value(), usize::MAX).unwrap(),
vec![
(2, vec![Hash::repeat_byte(1)]),
(5, vec![Hash::repeat_byte(2)]),
(10, vec![Hash::repeat_byte(3)]),
]
);
assert_eq!(
backend.load_stagnant_at_up_to(10, usize::MAX).unwrap(),
vec![
(2, vec![Hash::repeat_byte(1)]),
(5, vec![Hash::repeat_byte(2)]),
(10, vec![Hash::repeat_byte(3)]),
]
);
assert_eq!(
backend.load_stagnant_at_up_to(9, usize::MAX).unwrap(),
vec![(2, vec![Hash::repeat_byte(1)]), (5, vec![Hash::repeat_byte(2)]),]
);
assert_eq!(
backend.load_stagnant_at_up_to(9, 1).unwrap(),
vec![(2, vec![Hash::repeat_byte(1)]),]
);
backend.write(vec![BackendWriteOp::DeleteStagnantAt(2)]).unwrap();
assert_eq!(
backend.load_stagnant_at_up_to(5, usize::MAX).unwrap(),
vec![(5, vec![Hash::repeat_byte(2)]),]
);
backend.write(vec![BackendWriteOp::WriteStagnantAt(5, vec![])]).unwrap();
assert_eq!(
backend.load_stagnant_at_up_to(10, usize::MAX).unwrap(),
vec![(10, vec![Hash::repeat_byte(3)]),]
);
}
#[test]
fn write_read_blocks_at_height() {
let db = test_db();
let config = Config { col_data: 0 };
let mut backend = DbBackend::new(db, config);
backend
.write(vec![
BackendWriteOp::WriteBlocksByNumber(2, vec![Hash::repeat_byte(1)]),
BackendWriteOp::WriteBlocksByNumber(5, vec![Hash::repeat_byte(2)]),
BackendWriteOp::WriteBlocksByNumber(10, vec![Hash::repeat_byte(3)]),
])
.unwrap();
assert_eq!(backend.load_blocks_by_number(2).unwrap(), vec![Hash::repeat_byte(1)]);
assert_eq!(backend.load_blocks_by_number(3).unwrap(), vec![]);
backend
.write(vec![
BackendWriteOp::WriteBlocksByNumber(2, vec![]),
BackendWriteOp::DeleteBlocksByNumber(5),
])
.unwrap();
assert_eq!(backend.load_blocks_by_number(2).unwrap(), vec![]);
assert_eq!(backend.load_blocks_by_number(5).unwrap(), vec![]);
assert_eq!(backend.load_blocks_by_number(10).unwrap(), vec![Hash::repeat_byte(3)]);
}
}
@@ -0,0 +1,743 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Implements the Chain Selection Subsystem.
use pezkuwi_node_primitives::BlockWeight;
use pezkuwi_node_subsystem::{
errors::ChainApiError,
messages::{ChainApiMessage, ChainSelectionMessage},
overseer::{self, SubsystemSender},
FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError,
};
use pezkuwi_node_subsystem_util::database::Database;
use pezkuwi_primitives::{BlockNumber, ConsensusLog, Hash, Header};
use codec::Error as CodecError;
use futures::{channel::oneshot, future::Either, prelude::*};
use std::{
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::backend::{Backend, BackendWriteOp, OverlayedBackend};
mod backend;
mod db_backend;
mod tree;
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "teyrchain::chain-selection";
/// Timestamp based on the 1 Jan 1970 UNIX base, which is persistent across node restarts and OS
/// reboots.
type Timestamp = u64;
// If a block isn't approved in 120 seconds, nodes will abandon it
// and begin building on another chain.
const STAGNANT_TIMEOUT: Timestamp = 120;
// Delay pruning of the stagnant keys in prune only mode by 25 hours to avoid interception with the
// finality
const STAGNANT_PRUNE_DELAY: Timestamp = 25 * 60 * 60;
// Maximum number of stagnant entries cleaned during one `STAGNANT_TIMEOUT` iteration
const MAX_STAGNANT_ENTRIES: usize = 1000;
#[derive(Debug, Clone)]
enum Approval {
// Approved
Approved,
// Unapproved but not stagnant
Unapproved,
// Unapproved and stagnant.
Stagnant,
}
impl Approval {
fn is_stagnant(&self) -> bool {
matches!(*self, Approval::Stagnant)
}
}
#[derive(Debug, Clone)]
struct ViabilityCriteria {
// Whether this block has been explicitly reverted by one of its descendants.
explicitly_reverted: bool,
// The approval state of this block specifically.
approval: Approval,
// The earliest unviable ancestor - the hash of the earliest unfinalized
// block in the ancestry which is explicitly reverted or stagnant.
earliest_unviable_ancestor: Option<Hash>,
}
impl ViabilityCriteria {
fn is_viable(&self) -> bool {
self.is_parent_viable() && self.is_explicitly_viable()
}
// Whether the current block is explicitly viable.
// That is, whether the current block is neither reverted nor stagnant.
fn is_explicitly_viable(&self) -> bool {
!self.explicitly_reverted && !self.approval.is_stagnant()
}
// Whether the parent is viable. This assumes that the parent
// descends from the finalized chain.
fn is_parent_viable(&self) -> bool {
self.earliest_unviable_ancestor.is_none()
}
}
// Light entries describing leaves of the chain.
//
// These are ordered first by weight and then by block number.
#[derive(Debug, Clone, PartialEq)]
struct LeafEntry {
weight: BlockWeight,
block_number: BlockNumber,
block_hash: Hash,
}
impl PartialOrd for LeafEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let ord = self.weight.cmp(&other.weight).then(self.block_number.cmp(&other.block_number));
if !matches!(ord, std::cmp::Ordering::Equal) {
Some(ord)
} else {
None
}
}
}
#[derive(Debug, Default, Clone)]
struct LeafEntrySet {
inner: Vec<LeafEntry>,
}
impl LeafEntrySet {
fn remove(&mut self, hash: &Hash) -> bool {
match self.inner.iter().position(|e| &e.block_hash == hash) {
None => false,
Some(i) => {
self.inner.remove(i);
true
},
}
}
fn insert(&mut self, new: LeafEntry) {
let mut pos = None;
for (i, e) in self.inner.iter().enumerate() {
if e == &new {
return;
}
if e < &new {
pos = Some(i);
break;
}
}
match pos {
None => self.inner.push(new),
Some(i) => self.inner.insert(i, new),
}
}
fn into_hashes_descending(self) -> impl Iterator<Item = Hash> {
self.inner.into_iter().map(|e| e.block_hash)
}
}
#[derive(Debug, Clone)]
struct BlockEntry {
block_hash: Hash,
block_number: BlockNumber,
parent_hash: Hash,
children: Vec<Hash>,
viability: ViabilityCriteria,
weight: BlockWeight,
}
impl BlockEntry {
fn leaf_entry(&self) -> LeafEntry {
LeafEntry {
block_hash: self.block_hash,
block_number: self.block_number,
weight: self.weight,
}
}
fn non_viable_ancestor_for_child(&self) -> Option<Hash> {
if self.viability.is_viable() {
None
} else {
self.viability.earliest_unviable_ancestor.or(Some(self.block_hash))
}
}
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
ChainApi(#[from] ChainApiError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error(transparent)]
Subsystem(#[from] SubsystemError),
#[error(transparent)]
Codec(#[from] CodecError),
}
impl Error {
fn trace(&self) {
match self {
// don't spam the log with spurious errors
Self::Oneshot(_) => gum::debug!(target: LOG_TARGET, err = ?self),
// it's worth reporting otherwise
_ => gum::warn!(target: LOG_TARGET, err = ?self),
}
}
}
/// A clock used for fetching the current timestamp.
pub trait Clock {
/// Get the current timestamp.
fn timestamp_now(&self) -> Timestamp;
}
struct SystemClock;
impl Clock for SystemClock {
fn timestamp_now(&self) -> Timestamp {
// `SystemTime` is notoriously non-monotonic, so our timers might not work
// exactly as expected. Regardless, stagnation is detected on the order of minutes,
// and slippage of a few seconds in either direction won't cause any major harm.
//
// The exact time that a block becomes stagnant in the local node is always expected
// to differ from other nodes due to network asynchrony and delays in block propagation.
// Non-monotonicity exacerbates that somewhat, but not meaningfully.
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(e) => {
gum::warn!(
target: LOG_TARGET,
err = ?e,
"Current time is before unix epoch. Validation will not work correctly."
);
0
},
}
}
}
/// The interval, in seconds to check for stagnant blocks.
#[derive(Debug, Clone)]
pub struct StagnantCheckInterval(Option<Duration>);
impl Default for StagnantCheckInterval {
fn default() -> Self {
// 5 seconds is a reasonable balance between avoiding DB reads and
// ensuring validators are generally in agreement on stagnant blocks.
//
// Assuming a network delay of D, the longest difference in view possible
// between 2 validators is D + 5s.
const DEFAULT_STAGNANT_CHECK_INTERVAL: Duration = Duration::from_secs(5);
StagnantCheckInterval(Some(DEFAULT_STAGNANT_CHECK_INTERVAL))
}
}
impl StagnantCheckInterval {
/// Create a new stagnant-check interval wrapping the given duration.
pub fn new(interval: Duration) -> Self {
StagnantCheckInterval(Some(interval))
}
/// Create a `StagnantCheckInterval` which never triggers.
pub fn never() -> Self {
StagnantCheckInterval(None)
}
fn timeout_stream(&self) -> impl Stream<Item = ()> {
match self.0 {
Some(interval) => Either::Left({
let mut delay = futures_timer::Delay::new(interval);
futures::stream::poll_fn(move |cx| {
let poll = delay.poll_unpin(cx);
if poll.is_ready() {
delay.reset(interval)
}
poll.map(Some)
})
}),
None => Either::Right(futures::stream::pending()),
}
}
}
/// Mode of the stagnant check operations: check and prune or prune only
#[derive(Debug, Clone)]
pub enum StagnantCheckMode {
CheckAndPrune,
PruneOnly,
}
impl Default for StagnantCheckMode {
fn default() -> Self {
StagnantCheckMode::PruneOnly
}
}
/// Configuration for the chain selection subsystem.
#[derive(Debug, Clone)]
pub struct Config {
/// The column in the database that the storage should use.
pub col_data: u32,
/// How often to check for stagnant blocks.
pub stagnant_check_interval: StagnantCheckInterval,
/// Mode of stagnant checks
pub stagnant_check_mode: StagnantCheckMode,
}
/// The chain selection subsystem.
pub struct ChainSelectionSubsystem {
config: Config,
db: Arc<dyn Database>,
}
impl ChainSelectionSubsystem {
/// Create a new instance of the subsystem with the given config
/// and key-value store.
pub fn new(config: Config, db: Arc<dyn Database>) -> Self {
ChainSelectionSubsystem { config, db }
}
/// Revert to the block corresponding to the specified `hash`.
/// The operation is not allowed for blocks older than the last finalized one.
pub fn revert_to(&self, hash: Hash) -> Result<(), Error> {
let config = db_backend::v1::Config { col_data: self.config.col_data };
let mut backend = db_backend::v1::DbBackend::new(self.db.clone(), config);
let ops = tree::revert_to(&backend, hash)?.into_write_ops();
backend.write(ops)
}
}
#[overseer::subsystem(ChainSelection, error = SubsystemError, prefix = self::overseer)]
impl<Context> ChainSelectionSubsystem {
fn start(self, ctx: Context) -> SpawnedSubsystem {
let backend = db_backend::v1::DbBackend::new(
self.db,
db_backend::v1::Config { col_data: self.config.col_data },
);
SpawnedSubsystem {
future: run(
ctx,
backend,
self.config.stagnant_check_interval,
self.config.stagnant_check_mode,
Box::new(SystemClock),
)
.map(Ok)
.boxed(),
name: "chain-selection-subsystem",
}
}
}
#[overseer::contextbounds(ChainSelection, prefix = self::overseer)]
async fn run<Context, B>(
mut ctx: Context,
mut backend: B,
stagnant_check_interval: StagnantCheckInterval,
stagnant_check_mode: StagnantCheckMode,
clock: Box<dyn Clock + Send + Sync>,
) where
B: Backend,
{
#![allow(clippy::all)]
loop {
let res = run_until_error(
&mut ctx,
&mut backend,
&stagnant_check_interval,
&stagnant_check_mode,
&*clock,
)
.await;
match res {
Err(e) => {
e.trace();
// All errors are considered fatal right now:
break;
},
Ok(()) => {
gum::info!(target: LOG_TARGET, "received `Conclude` signal, exiting");
break;
},
}
}
}
// Run the subsystem until an error is encountered or a `conclude` signal is received.
// Most errors are non-fatal and should lead to another call to this function.
//
// A return value of `Ok` indicates that an exit should be made, while non-fatal errors
// lead to another call to this function.
#[overseer::contextbounds(ChainSelection, prefix = self::overseer)]
async fn run_until_error<Context, B>(
ctx: &mut Context,
backend: &mut B,
stagnant_check_interval: &StagnantCheckInterval,
stagnant_check_mode: &StagnantCheckMode,
clock: &(dyn Clock + Sync),
) -> Result<(), Error>
where
B: Backend,
{
let mut stagnant_check_stream = stagnant_check_interval.timeout_stream();
loop {
futures::select! {
msg = ctx.recv().fuse() => {
let msg = msg?;
match msg {
FromOrchestra::Signal(OverseerSignal::Conclude) => {
return Ok(())
}
FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => {
if let Some(leaf) = update.activated {
let write_ops = handle_active_leaf(
ctx.sender(),
&*backend,
clock.timestamp_now() + STAGNANT_TIMEOUT,
leaf.hash,
).await?;
backend.write(write_ops)?;
}
}
FromOrchestra::Signal(OverseerSignal::BlockFinalized(h, n)) => {
handle_finalized_block(backend, h, n)?
}
FromOrchestra::Communication { msg } => match msg {
ChainSelectionMessage::Approved(hash) => {
handle_approved_block(backend, hash)?
}
ChainSelectionMessage::Leaves(tx) => {
let leaves = load_leaves(ctx.sender(), &*backend).await?;
let _ = tx.send(leaves);
}
ChainSelectionMessage::BestLeafContaining(required, tx) => {
let best_containing = backend::find_best_leaf_containing(
&*backend,
required,
)?;
// note - this may be none if the finalized block is
// a leaf. this is fine according to the expected usage of the
// function. `None` responses should just `unwrap_or(required)`,
// so if the required block is the finalized block, then voilá.
let _ = tx.send(best_containing);
}
ChainSelectionMessage::RevertBlocks(blocks_to_revert) => {
let write_ops = handle_revert_blocks(backend, blocks_to_revert)?;
backend.write(write_ops)?;
}
}
}
}
_ = stagnant_check_stream.next().fuse() => {
match stagnant_check_mode {
StagnantCheckMode::CheckAndPrune => detect_stagnant(backend, clock.timestamp_now(), MAX_STAGNANT_ENTRIES),
StagnantCheckMode::PruneOnly => {
let now_timestamp = clock.timestamp_now();
prune_only_stagnant(backend, now_timestamp - STAGNANT_PRUNE_DELAY, MAX_STAGNANT_ENTRIES)
},
}?;
}
}
}
}
async fn fetch_finalized(
sender: &mut impl SubsystemSender<ChainApiMessage>,
) -> Result<Option<(Hash, BlockNumber)>, Error> {
let (number_tx, number_rx) = oneshot::channel();
sender.send_message(ChainApiMessage::FinalizedBlockNumber(number_tx)).await;
let number = match number_rx.await? {
Ok(number) => number,
Err(err) => {
gum::warn!(target: LOG_TARGET, ?err, "Fetching finalized number failed");
return Ok(None);
},
};
let (hash_tx, hash_rx) = oneshot::channel();
sender.send_message(ChainApiMessage::FinalizedBlockHash(number, hash_tx)).await;
match hash_rx.await? {
Err(err) => {
gum::warn!(target: LOG_TARGET, number, ?err, "Fetching finalized block number failed");
Ok(None)
},
Ok(None) => {
gum::warn!(target: LOG_TARGET, number, "Missing hash for finalized block number");
Ok(None)
},
Ok(Some(h)) => Ok(Some((h, number))),
}
}
async fn fetch_header(
sender: &mut impl SubsystemSender<ChainApiMessage>,
hash: Hash,
) -> Result<Option<Header>, Error> {
let (tx, rx) = oneshot::channel();
sender.send_message(ChainApiMessage::BlockHeader(hash, tx)).await;
Ok(rx.await?.unwrap_or_else(|err| {
gum::warn!(target: LOG_TARGET, ?hash, ?err, "Missing hash for finalized block number");
None
}))
}
async fn fetch_block_weight(
sender: &mut impl overseer::SubsystemSender<ChainApiMessage>,
hash: Hash,
) -> Result<Option<BlockWeight>, Error> {
let (tx, rx) = oneshot::channel();
sender.send_message(ChainApiMessage::BlockWeight(hash, tx)).await;
let res = rx.await?;
Ok(res.unwrap_or_else(|err| {
gum::warn!(target: LOG_TARGET, ?hash, ?err, "Missing hash for finalized block number");
None
}))
}
// Handle a new active leaf.
async fn handle_active_leaf(
sender: &mut impl overseer::ChainSelectionSenderTrait,
backend: &impl Backend,
stagnant_at: Timestamp,
hash: Hash,
) -> Result<Vec<BackendWriteOp>, Error> {
let lower_bound = match backend.load_first_block_number()? {
Some(l) => {
// We want to iterate back to finalized, and first block number
// is assumed to be 1 above finalized - the implicit root of the
// tree.
l.saturating_sub(1)
},
None => fetch_finalized(sender).await?.map_or(1, |(_, n)| n),
};
let header = match fetch_header(sender, hash).await? {
None => {
gum::warn!(target: LOG_TARGET, ?hash, "Missing header for new head");
return Ok(Vec::new());
},
Some(h) => h,
};
let new_blocks = pezkuwi_node_subsystem_util::determine_new_blocks(
sender,
|h| backend.load_block_entry(h).map(|b| b.is_some()),
hash,
&header,
lower_bound,
)
.await?;
let mut overlay = OverlayedBackend::new(backend);
// determine_new_blocks gives blocks in descending order.
// for this, we want ascending order.
for (hash, header) in new_blocks.into_iter().rev() {
let weight = match fetch_block_weight(sender, hash).await? {
None => {
gum::warn!(
target: LOG_TARGET,
?hash,
"Missing block weight for new head. Skipping chain.",
);
// If we don't know the weight, we can't import the block.
// And none of its descendants either.
break;
},
Some(w) => w,
};
let reversion_logs = extract_reversion_logs(&header);
tree::import_block(
&mut overlay,
hash,
header.number,
header.parent_hash,
reversion_logs,
weight,
stagnant_at,
)?;
}
Ok(overlay.into_write_ops().collect())
}
// Extract all reversion logs from a header in ascending order.
//
// Ignores logs with number > the block header number.
fn extract_reversion_logs(header: &Header) -> Vec<BlockNumber> {
let number = header.number;
let mut logs = header
.digest
.logs()
.iter()
.enumerate()
.filter_map(|(i, d)| match ConsensusLog::from_digest_item(d) {
Err(e) => {
gum::warn!(
target: LOG_TARGET,
err = ?e,
index = i,
block_hash = ?header.hash(),
"Digest item failed to encode"
);
None
},
Ok(Some(ConsensusLog::Revert(b))) if b <= number => Some(b),
Ok(Some(ConsensusLog::Revert(b))) => {
gum::warn!(
target: LOG_TARGET,
revert_target = b,
block_number = number,
block_hash = ?header.hash(),
"Block issued invalid revert digest targeting future"
);
None
},
Ok(_) => None,
})
.collect::<Vec<_>>();
logs.sort();
logs
}
/// Handle a finalized block event.
fn handle_finalized_block(
backend: &mut impl Backend,
finalized_hash: Hash,
finalized_number: BlockNumber,
) -> Result<(), Error> {
let ops = tree::finalize_block(&*backend, finalized_hash, finalized_number)?.into_write_ops();
backend.write(ops)
}
// Handle an approved block event.
fn handle_approved_block(backend: &mut impl Backend, approved_block: Hash) -> Result<(), Error> {
let ops = {
let mut overlay = OverlayedBackend::new(&*backend);
tree::approve_block(&mut overlay, approved_block)?;
overlay.into_write_ops()
};
backend.write(ops)
}
// Here we revert a provided group of blocks. The most common cause for this is that
// the dispute coordinator has notified chain selection of a dispute which concluded
// against a candidate.
fn handle_revert_blocks(
backend: &impl Backend,
blocks_to_revert: Vec<(BlockNumber, Hash)>,
) -> Result<Vec<BackendWriteOp>, Error> {
let mut overlay = OverlayedBackend::new(backend);
for (block_number, block_hash) in blocks_to_revert {
tree::apply_single_reversion(&mut overlay, block_hash, block_number)?;
}
Ok(overlay.into_write_ops().collect())
}
fn detect_stagnant(
backend: &mut impl Backend,
now: Timestamp,
max_elements: usize,
) -> Result<(), Error> {
let ops = {
let overlay = tree::detect_stagnant(&*backend, now, max_elements)?;
overlay.into_write_ops()
};
backend.write(ops)
}
fn prune_only_stagnant(
backend: &mut impl Backend,
up_to: Timestamp,
max_elements: usize,
) -> Result<(), Error> {
let ops = {
let overlay = tree::prune_only_stagnant(&*backend, up_to, max_elements)?;
overlay.into_write_ops()
};
backend.write(ops)
}
// Load the leaves from the backend. If there are no leaves, then return
// the finalized block.
async fn load_leaves(
sender: &mut impl overseer::SubsystemSender<ChainApiMessage>,
backend: &impl Backend,
) -> Result<Vec<Hash>, Error> {
let leaves: Vec<_> = backend.load_leaves()?.into_hashes_descending().collect();
if leaves.is_empty() {
Ok(fetch_finalized(sender).await?.map_or(Vec::new(), |(h, _)| vec![h]))
} else {
Ok(leaves)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,782 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Implements the tree-view over the data backend which we use to determine
//! viable leaves.
//!
//! The metadata is structured as a tree, with the root implicitly being the
//! finalized block, which is not stored as part of the tree.
//!
//! Each direct descendant of the finalized block acts as its own sub-tree,
//! and as the finalized block advances, orphaned sub-trees are entirely pruned.
use pezkuwi_node_primitives::BlockWeight;
use pezkuwi_node_subsystem::ChainApiError;
use pezkuwi_primitives::{BlockNumber, Hash};
use std::collections::HashMap;
use super::{Approval, BlockEntry, Error, LeafEntry, Timestamp, ViabilityCriteria, LOG_TARGET};
use crate::backend::{Backend, OverlayedBackend};
// A viability update to be applied to a block.
struct ViabilityUpdate(Option<Hash>);
impl ViabilityUpdate {
// Apply the viability update to a single block, yielding the updated
// block entry along with a vector of children and the updates to apply
// to them.
fn apply(self, mut entry: BlockEntry) -> (BlockEntry, Vec<(Hash, ViabilityUpdate)>) {
// 1. When an ancestor has changed from unviable to viable,
// we erase the `earliest_unviable_ancestor` of all descendants
// until encountering a explicitly unviable descendant D.
//
// We then update the `earliest_unviable_ancestor` for all
// descendants of D to be equal to D.
//
// 2. When an ancestor A has changed from viable to unviable,
// we update the `earliest_unviable_ancestor` for all blocks
// to A.
//
// The following algorithm covers both cases.
//
// Furthermore, if there has been any change in viability,
// it is necessary to visit every single descendant of the root
// block.
//
// If a block B was unviable and is now viable, then every descendant
// has an `earliest_unviable_ancestor` which must be updated either
// to nothing or to the new earliest unviable ancestor.
//
// If a block B was viable and is now unviable, then every descendant
// has an `earliest_unviable_ancestor` which needs to be set to B.
let maybe_earliest_unviable = self.0;
let next_earliest_unviable = {
if maybe_earliest_unviable.is_none() && !entry.viability.is_explicitly_viable() {
Some(entry.block_hash)
} else {
maybe_earliest_unviable
}
};
entry.viability.earliest_unviable_ancestor = maybe_earliest_unviable;
let recurse = entry
.children
.iter()
.cloned()
.map(move |c| (c, ViabilityUpdate(next_earliest_unviable)))
.collect();
(entry, recurse)
}
}
// Propagate viability update to descendants of the given block. This writes
// the `base` entry as well as all descendants. If the parent of the block
// entry is not viable, this will not affect any descendants.
//
// If the block entry provided is self-unviable, then it's assumed that an
// unviability update needs to be propagated to descendants.
//
// If the block entry provided is self-viable, then it's assumed that a
// viability update needs to be propagated to descendants.
fn propagate_viability_update(
backend: &mut OverlayedBackend<impl Backend>,
base: BlockEntry,
) -> Result<(), Error> {
enum BlockEntryRef {
Explicit(BlockEntry),
Hash(Hash),
}
if !base.viability.is_parent_viable() {
// If the parent of the block is still unviable,
// then the `earliest_viable_ancestor` will not change
// regardless of the change in the block here.
//
// Furthermore, in such cases, the set of viable leaves
// does not change at all.
backend.write_block_entry(base);
return Ok(());
}
let mut viable_leaves = backend.load_leaves()?;
// A mapping of Block Hash -> number
// Where the hash is the hash of a viable block which has
// at least 1 unviable child.
//
// The number is the number of known unviable children which is known
// as the pivot count.
let mut viability_pivots = HashMap::new();
// If the base block is itself explicitly unviable,
// this will change to a `Some(base_hash)` after the first
// invocation.
let viability_update = ViabilityUpdate(None);
// Recursively apply update to tree.
//
// As we go, we remove any blocks from the leaves which are no longer viable
// leaves. We also add blocks to the leaves-set which are obviously viable leaves.
// And we build up a frontier of blocks which may either be viable leaves or
// the ancestors of one.
let mut tree_frontier = vec![(BlockEntryRef::Explicit(base), viability_update)];
while let Some((entry_ref, update)) = tree_frontier.pop() {
let entry = match entry_ref {
BlockEntryRef::Explicit(entry) => entry,
BlockEntryRef::Hash(hash) => match backend.load_block_entry(&hash)? {
None => {
gum::warn!(
target: LOG_TARGET,
block_hash = ?hash,
"Missing expected block entry"
);
continue;
},
Some(entry) => entry,
},
};
let (new_entry, children) = update.apply(entry);
if new_entry.viability.is_viable() {
// A block which is viable has a parent which is obviously not
// in the viable leaves set.
viable_leaves.remove(&new_entry.parent_hash);
// Furthermore, if the block is viable and has no children,
// it is viable by definition.
if new_entry.children.is_empty() {
viable_leaves.insert(new_entry.leaf_entry());
}
} else {
// A block which is not viable is certainly not a viable leaf.
viable_leaves.remove(&new_entry.block_hash);
// When the parent is viable but the entry itself is not, that means
// that the parent is a viability pivot. As we visit the children
// of a viability pivot, we build up an exhaustive pivot count.
if new_entry.viability.is_parent_viable() {
*viability_pivots.entry(new_entry.parent_hash).or_insert(0) += 1;
}
}
backend.write_block_entry(new_entry);
tree_frontier
.extend(children.into_iter().map(|(h, update)| (BlockEntryRef::Hash(h), update)));
}
// Revisit the viability pivots now that we've traversed the entire subtree.
// After this point, the viable leaves set is fully updated. A proof follows.
//
// If the base has become unviable, then we've iterated into all descendants,
// made them unviable and removed them from the set. We know that the parent is
// viable as this function is a no-op otherwise, so we need to see if the parent
// has other children or not.
//
// If the base has become viable, then we've iterated into all descendants,
// and found all blocks which are viable and have no children. We've already added
// those blocks to the leaf set, but what we haven't detected
// is blocks which are viable and have children, but all of the children are
// unviable.
//
// The solution of viability pivots addresses both of these:
//
// When the base has become unviable, the parent's viability is unchanged and therefore
// any leaves descending from parent but not base are still in the viable leaves set.
// If the parent has only one child which is the base, the parent is now a viable leaf.
// We've already visited the base in recursive search so the set of pivots should
// contain only a single entry `(parent, 1)`. qed.
//
// When the base has become viable, we've already iterated into every descendant
// of the base and thus have collected a set of pivots whose corresponding pivot
// counts have already been exhaustively computed from their children. qed.
for (pivot, pivot_count) in viability_pivots {
match backend.load_block_entry(&pivot)? {
None => {
// This means the block is finalized. We might reach this
// code path when the base is a child of the finalized block
// and has become unviable.
//
// Each such child is the root of its own tree
// which, as an invariant, does not depend on the viability
// of the finalized block. So no siblings need to be inspected
// and we can ignore it safely.
//
// Furthermore, if the set of viable leaves is empty, the
// finalized block is implicitly the viable leaf.
continue;
},
Some(entry) =>
if entry.children.len() == pivot_count {
viable_leaves.insert(entry.leaf_entry());
},
}
}
backend.write_leaves(viable_leaves);
Ok(())
}
/// Imports a new block and applies any reversions to ancestors or the block itself.
pub(crate) fn import_block(
backend: &mut OverlayedBackend<impl Backend>,
block_hash: Hash,
block_number: BlockNumber,
parent_hash: Hash,
reversion_logs: Vec<BlockNumber>,
weight: BlockWeight,
stagnant_at: Timestamp,
) -> Result<(), Error> {
let block_entry =
add_block(backend, block_hash, block_number, parent_hash, weight, stagnant_at)?;
apply_reversions(backend, block_entry, reversion_logs)?;
Ok(())
}
// Load the given ancestor's block entry, in descending order from the `block_hash`.
// The ancestor_number must be not higher than the `block_entry`'s.
//
// The returned entry will be `None` if the range is invalid or any block in the path had
// no entry present. If any block entry was missing, it can safely be assumed to
// be finalized.
fn load_ancestor(
backend: &mut OverlayedBackend<impl Backend>,
block_entry: &BlockEntry,
ancestor_number: BlockNumber,
) -> Result<Option<BlockEntry>, Error> {
let block_hash = block_entry.block_hash;
let block_number = block_entry.block_number;
if block_number == ancestor_number {
return Ok(Some(block_entry.clone()));
} else if block_number < ancestor_number {
return Ok(None);
}
let mut current_hash = block_hash;
let mut current_entry = None;
let segment_length = (block_number - ancestor_number) + 1;
for _ in 0..segment_length {
match backend.load_block_entry(&current_hash)? {
None => return Ok(None),
Some(entry) => {
let parent_hash = entry.parent_hash;
current_entry = Some(entry);
current_hash = parent_hash;
},
}
}
// Current entry should always be `Some` here.
Ok(current_entry)
}
// Add a new block to the tree, which is assumed to be unreverted and unapproved,
// but not stagnant. It inherits viability from its parent, if any.
//
// This updates the parent entry, if any, and updates the viable leaves set accordingly.
// This also schedules a stagnation-check update and adds the block to the blocks-by-number
// mapping.
fn add_block(
backend: &mut OverlayedBackend<impl Backend>,
block_hash: Hash,
block_number: BlockNumber,
parent_hash: Hash,
weight: BlockWeight,
stagnant_at: Timestamp,
) -> Result<BlockEntry, Error> {
let mut leaves = backend.load_leaves()?;
let parent_entry = backend.load_block_entry(&parent_hash)?;
let inherited_viability =
parent_entry.as_ref().and_then(|parent| parent.non_viable_ancestor_for_child());
// 1. Add the block to the DB assuming it's not reverted.
let block_entry = BlockEntry {
block_hash,
block_number,
parent_hash,
children: Vec::new(),
viability: ViabilityCriteria {
earliest_unviable_ancestor: inherited_viability,
explicitly_reverted: false,
approval: Approval::Unapproved,
},
weight,
};
backend.write_block_entry(block_entry.clone());
// 2. Update leaves if inherited viability is fine.
if inherited_viability.is_none() {
leaves.remove(&parent_hash);
leaves.insert(LeafEntry { block_hash, block_number, weight });
backend.write_leaves(leaves);
}
// 3. Update and write the parent
if let Some(mut parent_entry) = parent_entry {
parent_entry.children.push(block_hash);
backend.write_block_entry(parent_entry);
}
// 4. Add to blocks-by-number.
let mut blocks_by_number = backend.load_blocks_by_number(block_number)?;
blocks_by_number.push(block_hash);
backend.write_blocks_by_number(block_number, blocks_by_number);
// 5. Add stagnation timeout.
let mut stagnant_at_list = backend.load_stagnant_at(stagnant_at)?;
stagnant_at_list.push(block_hash);
backend.write_stagnant_at(stagnant_at, stagnant_at_list);
Ok(block_entry)
}
/// Assuming that a block is already imported, accepts the number of the block
/// as well as a list of reversions triggered by the block in ascending order.
fn apply_reversions(
backend: &mut OverlayedBackend<impl Backend>,
block_entry: BlockEntry,
reversions: Vec<BlockNumber>,
) -> Result<(), Error> {
// Note: since revert numbers are in ascending order, the expensive propagation
// of unviability is only heavy on the first log.
for revert_number in reversions {
let maybe_block_entry = load_ancestor(backend, &block_entry, revert_number)?;
if let Some(entry) = &maybe_block_entry {
gum::trace!(
target: LOG_TARGET,
?revert_number,
revert_hash = ?entry.block_hash,
"Block marked as reverted via scraped on-chain reversions"
);
}
revert_single_block_entry_if_present(
backend,
maybe_block_entry,
None,
revert_number,
Some(block_entry.block_hash),
Some(block_entry.block_number),
)?;
}
Ok(())
}
/// Marks a single block as explicitly reverted, then propagates viability updates
/// to all its children. This is triggered when the disputes subsystem signals that
/// a dispute has concluded against a candidate.
pub(crate) fn apply_single_reversion(
backend: &mut OverlayedBackend<impl Backend>,
revert_hash: Hash,
revert_number: BlockNumber,
) -> Result<(), Error> {
gum::trace!(
target: LOG_TARGET,
?revert_number,
?revert_hash,
"Block marked as reverted via ChainSelectionMessage::RevertBlocks"
);
let maybe_block_entry = backend.load_block_entry(&revert_hash)?;
revert_single_block_entry_if_present(
backend,
maybe_block_entry,
Some(revert_hash),
revert_number,
None,
None,
)?;
Ok(())
}
fn revert_single_block_entry_if_present(
backend: &mut OverlayedBackend<impl Backend>,
maybe_block_entry: Option<BlockEntry>,
maybe_revert_hash: Option<Hash>,
revert_number: BlockNumber,
maybe_reporting_hash: Option<Hash>,
maybe_reporting_number: Option<BlockNumber>,
) -> Result<(), Error> {
match maybe_block_entry {
None => {
gum::warn!(
target: LOG_TARGET,
?maybe_revert_hash,
revert_target = revert_number,
?maybe_reporting_hash,
?maybe_reporting_number,
"The hammer has dropped. \
The protocol has indicated that a finalized block be reverted. \
Please inform an adult.",
);
},
Some(mut block_entry) => {
gum::info!(
target: LOG_TARGET,
?maybe_revert_hash,
revert_target = revert_number,
?maybe_reporting_hash,
?maybe_reporting_number,
"Unfinalized block reverted due to a bad teyrchain block.",
);
block_entry.viability.explicitly_reverted = true;
// Marks children of reverted block as non-viable
propagate_viability_update(backend, block_entry)?;
},
}
Ok(())
}
/// Finalize a block with the given number and hash.
///
/// This will prune all sub-trees not descending from the given block,
/// all block entries at or before the given height,
/// and will update the viability of all sub-trees descending from the given
/// block if the finalized block was not viable.
///
/// This is assumed to start with a fresh backend, and will produce
/// an overlay over the backend with all the changes applied.
pub(super) fn finalize_block<'a, B: Backend + 'a>(
backend: &'a B,
finalized_hash: Hash,
finalized_number: BlockNumber,
) -> Result<OverlayedBackend<'a, B>, Error> {
let earliest_stored_number = backend.load_first_block_number()?;
let mut backend = OverlayedBackend::new(backend);
let earliest_stored_number = match earliest_stored_number {
None => {
// This implies that there are no unfinalized blocks and hence nothing
// to update.
return Ok(backend);
},
Some(e) => e,
};
let mut viable_leaves = backend.load_leaves()?;
// Walk all numbers up to the finalized number and remove those entries.
for number in earliest_stored_number..finalized_number {
let blocks_at = backend.load_blocks_by_number(number)?;
backend.delete_blocks_by_number(number);
for block in blocks_at {
viable_leaves.remove(&block);
backend.delete_block_entry(&block);
}
}
// Remove all blocks at the finalized height, with the exception of the finalized block,
// and their descendants, recursively.
{
let blocks_at_finalized_height = backend.load_blocks_by_number(finalized_number)?;
backend.delete_blocks_by_number(finalized_number);
let mut frontier: Vec<_> = blocks_at_finalized_height
.into_iter()
.filter(|h| h != &finalized_hash)
.map(|h| (h, finalized_number))
.collect();
while let Some((dead_hash, dead_number)) = frontier.pop() {
let entry = backend.load_block_entry(&dead_hash)?;
backend.delete_block_entry(&dead_hash);
viable_leaves.remove(&dead_hash);
// This does a few extra `clone`s but is unlikely to be
// a bottleneck. Code complexity is very low as a result.
let mut blocks_at_height = backend.load_blocks_by_number(dead_number)?;
blocks_at_height.retain(|h| h != &dead_hash);
backend.write_blocks_by_number(dead_number, blocks_at_height);
// Add all children to the frontier.
let next_height = dead_number + 1;
frontier.extend(entry.into_iter().flat_map(|e| e.children).map(|h| (h, next_height)));
}
}
// Visit and remove the finalized block, fetching its children.
let children_of_finalized = {
let finalized_entry = backend.load_block_entry(&finalized_hash)?;
backend.delete_block_entry(&finalized_hash);
viable_leaves.remove(&finalized_hash);
finalized_entry.into_iter().flat_map(|e| e.children)
};
backend.write_leaves(viable_leaves);
// Update the viability of each child.
for child in children_of_finalized {
if let Some(mut child) = backend.load_block_entry(&child)? {
// Finalized blocks are always viable.
child.viability.earliest_unviable_ancestor = None;
propagate_viability_update(&mut backend, child)?;
} else {
gum::debug!(
target: LOG_TARGET,
?finalized_hash,
finalized_number,
child_hash = ?child,
"Missing child of finalized block",
);
// No need to do anything, but this is an inconsistent state.
}
}
Ok(backend)
}
/// Mark a block as approved and update the viability of itself and its
/// descendants accordingly.
pub(super) fn approve_block(
backend: &mut OverlayedBackend<impl Backend>,
approved_hash: Hash,
) -> Result<(), Error> {
if let Some(mut entry) = backend.load_block_entry(&approved_hash)? {
let was_viable = entry.viability.is_viable();
entry.viability.approval = Approval::Approved;
let is_viable = entry.viability.is_viable();
// Approval can change the viability in only one direction.
// If the viability has changed, then we propagate that to children
// and recalculate the viable leaf set.
if !was_viable && is_viable {
propagate_viability_update(backend, entry)?;
} else {
backend.write_block_entry(entry);
}
} else {
gum::debug!(
target: LOG_TARGET,
block_hash = ?approved_hash,
"Missing entry for freshly-approved block. Ignoring"
);
}
Ok(())
}
/// Check whether any blocks up to the given timestamp are stagnant and update
/// accordingly.
///
/// This accepts a fresh backend and returns an overlay on top of it representing
/// all changes made.
pub(super) fn detect_stagnant<'a, B: 'a + Backend>(
backend: &'a B,
up_to: Timestamp,
max_elements: usize,
) -> Result<OverlayedBackend<'a, B>, Error> {
let stagnant_up_to = backend.load_stagnant_at_up_to(up_to, max_elements)?;
let mut backend = OverlayedBackend::new(backend);
let (min_ts, max_ts) = match stagnant_up_to.len() {
0 => (0 as Timestamp, 0 as Timestamp),
1 => (stagnant_up_to[0].0, stagnant_up_to[0].0),
n => (stagnant_up_to[0].0, stagnant_up_to[n - 1].0),
};
// As this is in ascending order, only the earliest stagnant
// blocks will involve heavy viability propagations.
gum::debug!(
target: LOG_TARGET,
?up_to,
?min_ts,
?max_ts,
"Prepared {} stagnant entries for checking/pruning",
stagnant_up_to.len()
);
for (timestamp, maybe_stagnant) in stagnant_up_to {
backend.delete_stagnant_at(timestamp);
for block_hash in maybe_stagnant {
if let Some(mut entry) = backend.load_block_entry(&block_hash)? {
let was_viable = entry.viability.is_viable();
if let Approval::Unapproved = entry.viability.approval {
entry.viability.approval = Approval::Stagnant;
}
let is_viable = entry.viability.is_viable();
gum::trace!(
target: LOG_TARGET,
?block_hash,
?timestamp,
?was_viable,
?is_viable,
"Found existing stagnant entry"
);
if was_viable && !is_viable {
propagate_viability_update(&mut backend, entry)?;
} else {
backend.write_block_entry(entry);
}
} else {
gum::trace!(
target: LOG_TARGET,
?block_hash,
?timestamp,
"Found non-existing stagnant entry"
);
}
}
}
Ok(backend)
}
/// Prune stagnant entries at some timestamp without other checks
/// This function is intended just to clean leftover entries when the real
/// stagnant checks are disabled
pub(super) fn prune_only_stagnant<'a, B: 'a + Backend>(
backend: &'a B,
up_to: Timestamp,
max_elements: usize,
) -> Result<OverlayedBackend<'a, B>, Error> {
let stagnant_up_to = backend.load_stagnant_at_up_to(up_to, max_elements)?;
let mut backend = OverlayedBackend::new(backend);
let (min_ts, max_ts) = match stagnant_up_to.len() {
0 => (0 as Timestamp, 0 as Timestamp),
1 => (stagnant_up_to[0].0, stagnant_up_to[0].0),
n => (stagnant_up_to[0].0, stagnant_up_to[n - 1].0),
};
gum::debug!(
target: LOG_TARGET,
?up_to,
?min_ts,
?max_ts,
"Prepared {} stagnant entries for pruning",
stagnant_up_to.len()
);
for (timestamp, _) in stagnant_up_to {
backend.delete_stagnant_at(timestamp);
}
Ok(backend)
}
/// Revert the tree to the block relative to `hash`.
///
/// This accepts a fresh backend and returns an overlay on top of it representing
/// all changes made.
pub(super) fn revert_to<'a, B: Backend + 'a>(
backend: &'a B,
hash: Hash,
) -> Result<OverlayedBackend<'a, B>, Error> {
let first_number = backend.load_first_block_number()?.unwrap_or_default();
let mut backend = OverlayedBackend::new(backend);
let mut entry = match backend.load_block_entry(&hash)? {
Some(entry) => entry,
None => {
// May be a revert to the last finalized block. If this is the case,
// then revert to this block should be handled specially since no
// information about finalized blocks is persisted within the tree.
//
// We use part of the information contained in the finalized block
// children (that are expected to be in the tree) to construct a
// dummy block entry for the last finalized block. This will be
// wiped as soon as the next block is finalized.
let blocks = backend.load_blocks_by_number(first_number)?;
let block = blocks
.first()
.and_then(|hash| backend.load_block_entry(hash).ok())
.flatten()
.ok_or_else(|| {
ChainApiError::from(format!(
"Lookup failure for block at height {}",
first_number
))
})?;
// The parent is expected to be the last finalized block.
if block.parent_hash != hash {
return Err(ChainApiError::from("Can't revert below last finalized block").into());
}
// The weight is set to the one of the first child. Even though this is
// not accurate, it does the job. The reason is that the revert point is
// the last finalized block, i.e. this is the best and only choice.
let block_number = first_number.saturating_sub(1);
let viability = ViabilityCriteria {
explicitly_reverted: false,
approval: Approval::Approved,
earliest_unviable_ancestor: None,
};
let entry = BlockEntry {
block_hash: hash,
block_number,
parent_hash: Hash::default(),
children: blocks,
viability,
weight: block.weight,
};
// This becomes the first entry according to the block number.
backend.write_blocks_by_number(block_number, vec![hash]);
entry
},
};
let mut stack: Vec<_> = std::mem::take(&mut entry.children)
.into_iter()
.map(|h| (h, entry.block_number + 1))
.collect();
// Write revert point block entry without the children.
backend.write_block_entry(entry.clone());
let mut viable_leaves = backend.load_leaves()?;
viable_leaves.insert(LeafEntry {
block_hash: hash,
block_number: entry.block_number,
weight: entry.weight,
});
while let Some((hash, number)) = stack.pop() {
let entry = backend.load_block_entry(&hash)?;
backend.delete_block_entry(&hash);
viable_leaves.remove(&hash);
let mut blocks_at_height = backend.load_blocks_by_number(number)?;
blocks_at_height.retain(|h| h != &hash);
backend.write_blocks_by_number(number, blocks_at_height);
stack.extend(entry.into_iter().flat_map(|e| e.children).map(|h| (h, number + 1)));
}
backend.write_leaves(viable_leaves);
Ok(backend)
}
@@ -0,0 +1,64 @@
[package]
name = "pezkuwi-node-core-dispute-coordinator"
version = "7.0.0"
description = "The node-side components that participate in disputes"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[[bench]]
name = "dispute-coordinator-regression-bench"
path = "benches/dispute-coordinator-regression-bench.rs"
harness = false
required-features = ["subsystem-benchmarks"]
[dependencies]
codec = { workspace = true, default-features = true }
fatality = { workspace = true }
futures = { workspace = true }
gum = { workspace = true, default-features = true }
schnellru = { workspace = true }
thiserror = { workspace = true }
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-primitives = { workspace = true, default-features = true }
sc-keystore = { workspace = true, default-features = true }
[dev-dependencies]
assert_matches = { workspace = true }
futures-timer = { workspace = true }
kvdb-memorydb = { workspace = true }
pezkuwi-node-subsystem-test-helpers = { workspace = true }
pezkuwi-primitives = { workspace = true, features = ["test"] }
pezkuwi-primitives-test-helpers = { workspace = true }
sp-application-crypto = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-keystore = { workspace = true, default-features = true }
sp-tracing = { workspace = true, default-features = true }
pezkuwi-subsystem-bench = { workspace = true }
[features]
# If not enabled, the dispute coordinator will do nothing.
disputes = []
subsystem-benchmarks = []
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-subsystem-bench/runtime-benchmarks",
"sp-keyring/runtime-benchmarks",
]
@@ -0,0 +1,86 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! dispute-coordinator throughput test
//!
//! Dispute Coordinator benchmark based on Kusama parameters and scale.
//!
//! Subsystems involved:
//! - dispute-coordinator
//! - dispute-distribution
use pezkuwi_subsystem_bench::{
configuration::TestConfiguration,
disputes::{benchmark_dispute_coordinator, prepare_test, DisputesOptions, TestState},
usage::BenchmarkUsage,
utils::save_to_file,
};
use std::io::Write;
const BENCH_COUNT: usize = 10;
fn main() -> Result<(), String> {
let mut messages = vec![];
let mut config = TestConfiguration::default();
config.n_cores = 100;
config.n_validators = 500;
config.num_blocks = 10;
config.peer_bandwidth = 524288000000;
config.bandwidth = 524288000000;
config.latency = None;
config.connectivity = 100;
config.generate_pov_sizes();
let options = DisputesOptions { n_disputes: 50 };
println!("Benchmarking...");
let usages: Vec<BenchmarkUsage> = (0..BENCH_COUNT)
.map(|n| {
print!("\r[{}{}]", "#".repeat(n), "_".repeat(BENCH_COUNT - n));
std::io::stdout().flush().unwrap();
let state = TestState::new(&config, &options);
let mut env = prepare_test(&state, false);
env.runtime().block_on(benchmark_dispute_coordinator(&mut env, &state))
})
.collect();
println!("\rDone!{}", " ".repeat(BENCH_COUNT));
let average_usage = BenchmarkUsage::average(&usages);
save_to_file(
"charts/dispute-coordinator-regression-bench.json",
average_usage.to_chart_json().map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
println!("{}", average_usage);
// We expect some small variance for received and sent because the
// test messages are generated at every benchmark run and they contain
// random data so use 0.01 as the accepted variance.
messages.extend(average_usage.check_network_usage(&[
("Received from peers", 23.8, 0.01),
("Sent to peers", 227.1, 0.01),
]));
messages.extend(average_usage.check_cpu_usage(&[
("dispute-coordinator", 0.0026, 0.1),
("dispute-distribution", 0.0086, 0.1),
]));
if messages.is_empty() {
Ok(())
} else {
eprintln!("{}", messages.join("\n"));
Err("Regressions found".to_string())
}
}
@@ -0,0 +1,171 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! An abstraction over storage used by the chain selection subsystem.
//!
//! This provides both a [`Backend`] trait and an [`OverlayedBackend`]
//! struct which allows in-memory changes to be applied on top of a
//! [`Backend`], maintaining consistency between queries and temporary writes,
//! before any commit to the underlying storage is made.
use pezkuwi_primitives::{CandidateHash, SessionIndex};
use std::collections::HashMap;
use super::db::v1::{CandidateVotes, RecentDisputes};
use crate::error::FatalResult;
#[derive(Debug)]
pub enum BackendWriteOp {
WriteEarliestSession(SessionIndex),
WriteRecentDisputes(RecentDisputes),
WriteCandidateVotes(SessionIndex, CandidateHash, CandidateVotes),
DeleteCandidateVotes(SessionIndex, CandidateHash),
}
/// An abstraction over backend storage for the logic of this subsystem.
pub trait Backend {
/// Load the earliest session, if any.
fn load_earliest_session(&self) -> FatalResult<Option<SessionIndex>>;
/// Load the recent disputes, if any.
fn load_recent_disputes(&self) -> FatalResult<Option<RecentDisputes>>;
/// Load the candidate votes for the specific session-candidate pair, if any.
fn load_candidate_votes(
&self,
session: SessionIndex,
candidate_hash: &CandidateHash,
) -> FatalResult<Option<CandidateVotes>>;
/// Atomically writes the list of operations, with later operations taking precedence over
/// prior.
fn write<I>(&mut self, ops: I) -> FatalResult<()>
where
I: IntoIterator<Item = BackendWriteOp>;
}
/// An in-memory overlay for the backend.
///
/// This maintains read-only access to the underlying backend, but can be converted into a set of
/// write operations which will, when written to the underlying backend, give the same view as the
/// state of the overlay.
pub struct OverlayedBackend<'a, B: 'a> {
inner: &'a B,
// `None` means unchanged.
earliest_session: Option<SessionIndex>,
// `None` means unchanged.
recent_disputes: Option<RecentDisputes>,
// `None` means deleted, missing means query inner.
candidate_votes: HashMap<(SessionIndex, CandidateHash), Option<CandidateVotes>>,
}
impl<'a, B: 'a + Backend> OverlayedBackend<'a, B> {
pub fn new(backend: &'a B) -> Self {
Self {
inner: backend,
earliest_session: None,
recent_disputes: None,
candidate_votes: HashMap::new(),
}
}
/// Returns true if the are no write operations to perform.
pub fn is_empty(&self) -> bool {
self.earliest_session.is_none() &&
self.recent_disputes.is_none() &&
self.candidate_votes.is_empty()
}
/// Load the earliest session, if any.
pub fn load_earliest_session(&self) -> FatalResult<Option<SessionIndex>> {
if let Some(val) = self.earliest_session {
return Ok(Some(val));
}
self.inner.load_earliest_session()
}
/// Load the recent disputes, if any.
pub fn load_recent_disputes(&self) -> FatalResult<Option<RecentDisputes>> {
if let Some(val) = &self.recent_disputes {
return Ok(Some(val.clone()));
}
self.inner.load_recent_disputes()
}
/// Load the candidate votes for the specific session-candidate pair, if any.
pub fn load_candidate_votes(
&self,
session: SessionIndex,
candidate_hash: &CandidateHash,
) -> FatalResult<Option<CandidateVotes>> {
if let Some(val) = self.candidate_votes.get(&(session, *candidate_hash)) {
return Ok(val.clone());
}
self.inner.load_candidate_votes(session, candidate_hash)
}
/// Prepare a write to the "earliest session" field of the DB.
///
/// Later calls to this function will override earlier ones.
pub fn write_earliest_session(&mut self, session: SessionIndex) {
self.earliest_session = Some(session);
}
/// Prepare a write to the recent disputes stored in the DB.
///
/// Later calls to this function will override earlier ones.
pub fn write_recent_disputes(&mut self, recent_disputes: RecentDisputes) {
self.recent_disputes = Some(recent_disputes)
}
/// Prepare a write of the candidate votes under the indicated candidate.
///
/// Later calls to this function for the same candidate will override earlier ones.
pub fn write_candidate_votes(
&mut self,
session: SessionIndex,
candidate_hash: CandidateHash,
votes: CandidateVotes,
) {
self.candidate_votes.insert((session, candidate_hash), Some(votes));
}
/// Transform this backend into a set of write-ops to be written to the inner backend.
pub fn into_write_ops(self) -> impl Iterator<Item = BackendWriteOp> {
let earliest_session_ops = self
.earliest_session
.map(|s| BackendWriteOp::WriteEarliestSession(s))
.into_iter();
let recent_dispute_ops =
self.recent_disputes.map(|d| BackendWriteOp::WriteRecentDisputes(d)).into_iter();
let candidate_vote_ops =
self.candidate_votes
.into_iter()
.map(|((session, candidate), votes)| match votes {
Some(votes) => BackendWriteOp::WriteCandidateVotes(session, candidate, votes),
None => BackendWriteOp::DeleteCandidateVotes(session, candidate),
});
earliest_session_ops.chain(recent_dispute_ops).chain(candidate_vote_ops)
}
}
@@ -0,0 +1,19 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Database component for the dispute coordinator.
pub(super) mod v1;
@@ -0,0 +1,689 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! `V1` database for the dispute coordinator.
//!
//! Note that the version here differs from the actual version of the teyrchains
//! database (check `CURRENT_VERSION` in `node/service/src/teyrchains_db/upgrade.rs`).
//! The code in this module implements the way dispute coordinator works with
//! the dispute data in the database. Any breaking changes here will still
//! require a db migration (check `node/service/src/teyrchains_db/upgrade.rs`).
use pezkuwi_node_primitives::DisputeStatus;
use pezkuwi_node_subsystem_util::database::{DBTransaction, Database};
use pezkuwi_primitives::{
CandidateHash, CandidateReceiptV2 as CandidateReceipt, Hash, InvalidDisputeStatementKind,
SessionIndex, ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature,
};
use std::sync::Arc;
use codec::{Decode, Encode};
use crate::{
backend::{Backend, BackendWriteOp, OverlayedBackend},
error::{FatalError, FatalResult},
metrics::Metrics,
LOG_TARGET,
};
const RECENT_DISPUTES_KEY: &[u8; 15] = b"recent-disputes";
const EARLIEST_SESSION_KEY: &[u8; 16] = b"earliest-session";
const CANDIDATE_VOTES_SUBKEY: &[u8; 15] = b"candidate-votes";
/// Until what session have votes been cleaned up already?
const CLEANED_VOTES_WATERMARK_KEY: &[u8; 23] = b"cleaned-votes-watermark";
/// Restrict number of cleanup operations.
///
/// On the first run we are starting at session 0 going up all the way to the current session -
/// this should not be done at once, but rather in smaller batches so nodes won't get stalled by
/// this.
///
/// 300 is with session duration of 1 hour and 30 teyrchains around <3_000_000 key purges in the
/// worst case. Which is already quite a lot, at the same time we have around 21_000 sessions on
/// Kusama. This means at 300 purged sessions per session, cleaning everything up will take
/// around 3 days. Depending on how severe disk usage becomes, we might want to bump the batch
/// size, at the cost of risking issues at session boundaries (performance).
#[cfg(test)]
const MAX_CLEAN_BATCH_SIZE: u32 = 10;
#[cfg(not(test))]
const MAX_CLEAN_BATCH_SIZE: u32 = 300;
pub struct DbBackend {
inner: Arc<dyn Database>,
config: ColumnConfiguration,
metrics: Metrics,
}
impl DbBackend {
pub fn new(db: Arc<dyn Database>, config: ColumnConfiguration, metrics: Metrics) -> Self {
Self { inner: db, config, metrics }
}
/// Cleanup old votes.
///
/// Should be called whenever a new earliest session gets written.
fn add_vote_cleanup_tx(
&mut self,
tx: &mut DBTransaction,
earliest_session: SessionIndex,
) -> FatalResult<()> {
// Cleanup old votes in db:
let watermark = load_cleaned_votes_watermark(&*self.inner, &self.config)?.unwrap_or(0);
let clean_until = if earliest_session.saturating_sub(watermark) > MAX_CLEAN_BATCH_SIZE {
watermark + MAX_CLEAN_BATCH_SIZE
} else {
earliest_session
};
gum::trace!(
target: LOG_TARGET,
?watermark,
?clean_until,
?earliest_session,
?MAX_CLEAN_BATCH_SIZE,
"WriteEarliestSession"
);
for index in watermark..clean_until {
gum::trace!(
target: LOG_TARGET,
?index,
encoded = ?candidate_votes_session_prefix(index),
"Cleaning votes for session index"
);
tx.delete_prefix(self.config.col_dispute_data, &candidate_votes_session_prefix(index));
}
// New watermark:
tx.put_vec(self.config.col_dispute_data, CLEANED_VOTES_WATERMARK_KEY, clean_until.encode());
Ok(())
}
}
impl Backend for DbBackend {
/// Load the earliest session, if any.
fn load_earliest_session(&self) -> FatalResult<Option<SessionIndex>> {
load_earliest_session(&*self.inner, &self.config)
}
/// Load the recent disputes, if any.
fn load_recent_disputes(&self) -> FatalResult<Option<RecentDisputes>> {
load_recent_disputes(&*self.inner, &self.config)
}
/// Load the candidate votes for the specific session-candidate pair, if any.
fn load_candidate_votes(
&self,
session: SessionIndex,
candidate_hash: &CandidateHash,
) -> FatalResult<Option<CandidateVotes>> {
load_candidate_votes(&*self.inner, &self.config, session, candidate_hash)
}
/// Atomically writes the list of operations, with later operations taking precedence over
/// prior.
///
/// This also takes care of purging old votes (of obsolete sessions).
fn write<I>(&mut self, ops: I) -> FatalResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
// Make sure the whole process is timed, including the actual transaction flush:
let mut cleanup_timer = None;
for op in ops {
match op {
BackendWriteOp::WriteEarliestSession(session) => {
cleanup_timer = match cleanup_timer.take() {
None => Some(self.metrics.time_vote_cleanup()),
Some(t) => Some(t),
};
self.add_vote_cleanup_tx(&mut tx, session)?;
// Actually write the earliest session.
tx.put_vec(
self.config.col_dispute_data,
EARLIEST_SESSION_KEY,
session.encode(),
);
},
BackendWriteOp::WriteRecentDisputes(recent_disputes) => {
tx.put_vec(
self.config.col_dispute_data,
RECENT_DISPUTES_KEY,
recent_disputes.encode(),
);
},
BackendWriteOp::WriteCandidateVotes(session, candidate_hash, votes) => {
gum::trace!(target: LOG_TARGET, ?session, "Writing candidate votes");
tx.put_vec(
self.config.col_dispute_data,
&candidate_votes_key(session, &candidate_hash),
votes.encode(),
);
},
BackendWriteOp::DeleteCandidateVotes(session, candidate_hash) => {
tx.delete(
self.config.col_dispute_data,
&candidate_votes_key(session, &candidate_hash),
);
},
}
}
self.inner.write(tx).map_err(FatalError::DbWriteFailed)
}
}
fn candidate_votes_key(session: SessionIndex, candidate_hash: &CandidateHash) -> [u8; 15 + 4 + 32] {
let mut buf = [0u8; 15 + 4 + 32];
buf[..15].copy_from_slice(CANDIDATE_VOTES_SUBKEY);
// big-endian encoding is used to ensure lexicographic ordering.
buf[15..][..4].copy_from_slice(&session.to_be_bytes());
candidate_hash.using_encoded(|s| buf[(15 + 4)..].copy_from_slice(s));
buf
}
fn candidate_votes_session_prefix(session: SessionIndex) -> [u8; 15 + 4] {
let mut buf = [0u8; 15 + 4];
buf[..15].copy_from_slice(CANDIDATE_VOTES_SUBKEY);
// big-endian encoding is used to ensure lexicographic ordering.
buf[15..][..4].copy_from_slice(&session.to_be_bytes());
buf
}
/// Column configuration information for the DB.
#[derive(Debug, Clone)]
pub struct ColumnConfiguration {
/// The column in the key-value DB where data is stored.
pub col_dispute_data: u32,
}
/// Tracked votes on candidates, for the purposes of dispute resolution.
#[derive(Debug, Clone, Encode, Decode)]
pub struct CandidateVotes {
/// The receipt of the candidate itself.
pub candidate_receipt: CandidateReceipt,
/// Votes of validity, sorted by validator index.
pub valid: Vec<(ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
/// Votes of invalidity, sorted by validator index.
pub invalid: Vec<(InvalidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
}
impl From<CandidateVotes> for pezkuwi_node_primitives::CandidateVotes {
fn from(db_votes: CandidateVotes) -> pezkuwi_node_primitives::CandidateVotes {
pezkuwi_node_primitives::CandidateVotes {
candidate_receipt: db_votes.candidate_receipt,
valid: db_votes.valid.into_iter().map(|(kind, i, sig)| (i, (kind, sig))).collect(),
invalid: db_votes.invalid.into_iter().map(|(kind, i, sig)| (i, (kind, sig))).collect(),
}
}
}
impl From<pezkuwi_node_primitives::CandidateVotes> for CandidateVotes {
fn from(primitive_votes: pezkuwi_node_primitives::CandidateVotes) -> CandidateVotes {
CandidateVotes {
candidate_receipt: primitive_votes.candidate_receipt,
valid: primitive_votes
.valid
.into_iter()
.map(|(i, (kind, sig))| (kind, i, sig))
.collect(),
invalid: primitive_votes.invalid.into_iter().map(|(i, (k, sig))| (k, i, sig)).collect(),
}
}
}
/// The mapping for recent disputes; any which have not yet been pruned for being ancient.
pub type RecentDisputes = std::collections::BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>;
/// Errors while accessing things from the DB.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Codec(#[from] codec::Error),
}
impl From<Error> for crate::error::Error {
fn from(err: Error) -> Self {
match err {
Error::Io(io) => Self::Io(io),
Error::Codec(e) => Self::Codec(e),
}
}
}
/// Result alias for DB errors.
pub type Result<T> = std::result::Result<T, Error>;
fn load_decode<D: Decode>(
db: &dyn Database,
col_dispute_data: u32,
key: &[u8],
) -> Result<Option<D>> {
match db.get(col_dispute_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
/// Load the candidate votes for the specific session-candidate pair, if any.
pub(crate) fn load_candidate_votes(
db: &dyn Database,
config: &ColumnConfiguration,
session: SessionIndex,
candidate_hash: &CandidateHash,
) -> FatalResult<Option<CandidateVotes>> {
load_decode(db, config.col_dispute_data, &candidate_votes_key(session, candidate_hash))
.map_err(|e| FatalError::DbReadFailed(e))
}
/// Load the earliest session, if any.
pub(crate) fn load_earliest_session(
db: &dyn Database,
config: &ColumnConfiguration,
) -> FatalResult<Option<SessionIndex>> {
load_decode(db, config.col_dispute_data, EARLIEST_SESSION_KEY)
.map_err(|e| FatalError::DbReadFailed(e))
}
/// Load the recent disputes, if any.
pub(crate) fn load_recent_disputes(
db: &dyn Database,
config: &ColumnConfiguration,
) -> FatalResult<Option<RecentDisputes>> {
load_decode(db, config.col_dispute_data, RECENT_DISPUTES_KEY)
.map_err(|e| FatalError::DbReadFailed(e))
}
/// Maybe prune data in the DB based on the provided session index.
///
/// This is intended to be called on every block, and as such will be used to populate the DB on
/// first launch. If the on-disk data does not need to be pruned, only a single storage read
/// will be performed.
///
/// If one or more ancient sessions are pruned, all metadata on candidates within the ancient
/// session will be deleted.
pub(crate) fn note_earliest_session(
overlay_db: &mut OverlayedBackend<'_, impl Backend>,
new_earliest_session: SessionIndex,
) -> FatalResult<()> {
match overlay_db.load_earliest_session()? {
None => {
// First launch - write new-earliest.
overlay_db.write_earliest_session(new_earliest_session);
},
Some(prev_earliest) if new_earliest_session > prev_earliest => {
// Prune all data in the outdated sessions.
overlay_db.write_earliest_session(new_earliest_session);
// Clear recent disputes metadata.
{
let mut recent_disputes = overlay_db.load_recent_disputes()?.unwrap_or_default();
let lower_bound = (new_earliest_session, CandidateHash(Hash::repeat_byte(0x00)));
let new_recent_disputes = recent_disputes.split_off(&lower_bound);
// Any remaining disputes are considered ancient and must be pruned.
let pruned_disputes = recent_disputes;
if pruned_disputes.len() != 0 {
overlay_db.write_recent_disputes(new_recent_disputes);
// Note: Deleting old candidate votes is handled in `write` based on the
// earliest session.
}
}
},
Some(_) => {
// nothing to do.
},
}
Ok(())
}
/// Until what session votes have been cleaned up already.
///
/// That is the db has already been purged of votes for sessions older than the returned
/// `SessionIndex`.
fn load_cleaned_votes_watermark(
db: &dyn Database,
config: &ColumnConfiguration,
) -> FatalResult<Option<SessionIndex>> {
load_decode(db, config.col_dispute_data, CLEANED_VOTES_WATERMARK_KEY)
.map_err(|e| FatalError::DbReadFailed(e))
}
#[cfg(test)]
mod tests {
use super::*;
use pezkuwi_node_primitives::DISPUTE_WINDOW;
use pezkuwi_primitives::{Hash, Id as ParaId};
use pezkuwi_primitives_test_helpers::{
dummy_candidate_receipt, dummy_candidate_receipt_v2, dummy_hash,
};
fn make_db() -> DbBackend {
let db = kvdb_memorydb::create(1);
let db = pezkuwi_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[0]);
let store = Arc::new(db);
let config = ColumnConfiguration { col_dispute_data: 0 };
DbBackend::new(store, config, Metrics::default())
}
#[test]
fn max_clean_batch_size_is_honored() {
let mut backend = make_db();
let mut overlay_db = OverlayedBackend::new(&backend);
let current_session = MAX_CLEAN_BATCH_SIZE + DISPUTE_WINDOW.get() + 3;
let earliest_session = current_session - DISPUTE_WINDOW.get();
overlay_db.write_earliest_session(0);
let candidate_hash = CandidateHash(Hash::repeat_byte(1));
for session in 0..current_session + 1 {
overlay_db.write_candidate_votes(
session,
candidate_hash,
CandidateVotes {
candidate_receipt: dummy_candidate_receipt_v2(dummy_hash()),
valid: Vec::new(),
invalid: Vec::new(),
},
);
}
assert!(overlay_db.load_candidate_votes(0, &candidate_hash).unwrap().is_some());
assert!(overlay_db
.load_candidate_votes(MAX_CLEAN_BATCH_SIZE - 1, &candidate_hash)
.unwrap()
.is_some());
assert!(overlay_db
.load_candidate_votes(MAX_CLEAN_BATCH_SIZE, &candidate_hash)
.unwrap()
.is_some());
// Cleanup only works for votes that have been written already - so write.
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
let mut overlay_db = OverlayedBackend::new(&backend);
gum::trace!(target: LOG_TARGET, ?current_session, "Noting current session");
note_earliest_session(&mut overlay_db, earliest_session).unwrap();
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
let mut overlay_db = OverlayedBackend::new(&backend);
assert!(overlay_db
.load_candidate_votes(MAX_CLEAN_BATCH_SIZE - 1, &candidate_hash)
.unwrap()
.is_none());
// After batch size votes should still be there:
assert!(overlay_db
.load_candidate_votes(MAX_CLEAN_BATCH_SIZE, &candidate_hash)
.unwrap()
.is_some());
let current_session = current_session + 1;
let earliest_session = earliest_session + 1;
note_earliest_session(&mut overlay_db, earliest_session).unwrap();
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
let overlay_db = OverlayedBackend::new(&backend);
// All should be gone now:
assert!(overlay_db
.load_candidate_votes(earliest_session - 1, &candidate_hash)
.unwrap()
.is_none());
// Earliest session should still be there:
assert!(overlay_db
.load_candidate_votes(earliest_session, &candidate_hash)
.unwrap()
.is_some());
// Old current session should still be there as well:
assert!(overlay_db
.load_candidate_votes(current_session - 1, &candidate_hash)
.unwrap()
.is_some());
}
#[test]
fn overlay_pre_and_post_commit_consistency() {
let mut backend = make_db();
let mut overlay_db = OverlayedBackend::new(&backend);
overlay_db.write_earliest_session(0);
overlay_db.write_earliest_session(1);
overlay_db.write_recent_disputes(
vec![((0, CandidateHash(Hash::repeat_byte(0))), DisputeStatus::Active)]
.into_iter()
.collect(),
);
overlay_db.write_recent_disputes(
vec![((1, CandidateHash(Hash::repeat_byte(1))), DisputeStatus::Active)]
.into_iter()
.collect(),
);
overlay_db.write_candidate_votes(
1,
CandidateHash(Hash::repeat_byte(1)),
CandidateVotes {
candidate_receipt: dummy_candidate_receipt_v2(dummy_hash()),
valid: Vec::new(),
invalid: Vec::new(),
},
);
overlay_db.write_candidate_votes(
1,
CandidateHash(Hash::repeat_byte(1)),
CandidateVotes {
candidate_receipt: {
let mut receipt = dummy_candidate_receipt(dummy_hash());
receipt.descriptor.para_id = ParaId::from(5_u32);
receipt.into()
},
valid: Vec::new(),
invalid: Vec::new(),
},
);
// Test that overlay returns the correct values before committing.
assert_eq!(overlay_db.load_earliest_session().unwrap().unwrap(), 1);
assert_eq!(
overlay_db.load_recent_disputes().unwrap().unwrap(),
vec![((1, CandidateHash(Hash::repeat_byte(1))), DisputeStatus::Active),]
.into_iter()
.collect()
);
assert_eq!(
overlay_db
.load_candidate_votes(1, &CandidateHash(Hash::repeat_byte(1)))
.unwrap()
.unwrap()
.candidate_receipt
.descriptor
.para_id(),
ParaId::from(5),
);
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
// Test that subsequent writes were written.
assert_eq!(backend.load_earliest_session().unwrap().unwrap(), 1);
assert_eq!(
backend.load_recent_disputes().unwrap().unwrap(),
vec![((1, CandidateHash(Hash::repeat_byte(1))), DisputeStatus::Active),]
.into_iter()
.collect()
);
assert_eq!(
backend
.load_candidate_votes(1, &CandidateHash(Hash::repeat_byte(1)))
.unwrap()
.unwrap()
.candidate_receipt
.descriptor
.para_id(),
ParaId::from(5),
);
}
#[test]
fn overlay_preserves_candidate_votes_operation_order() {
let mut backend = make_db();
let mut overlay_db = OverlayedBackend::new(&backend);
overlay_db.write_candidate_votes(
1,
CandidateHash(Hash::repeat_byte(1)),
CandidateVotes {
candidate_receipt: dummy_candidate_receipt_v2(Hash::random()),
valid: Vec::new(),
invalid: Vec::new(),
},
);
let receipt = dummy_candidate_receipt_v2(dummy_hash());
overlay_db.write_candidate_votes(
1,
CandidateHash(Hash::repeat_byte(1)),
CandidateVotes {
candidate_receipt: receipt.clone(),
valid: Vec::new(),
invalid: Vec::new(),
},
);
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
assert_eq!(
backend
.load_candidate_votes(1, &CandidateHash(Hash::repeat_byte(1)))
.unwrap()
.unwrap()
.candidate_receipt,
receipt,
);
}
#[test]
fn note_earliest_session_prunes_old() {
let mut backend = make_db();
let hash_a = CandidateHash(Hash::repeat_byte(0x0a));
let hash_b = CandidateHash(Hash::repeat_byte(0x0b));
let hash_c = CandidateHash(Hash::repeat_byte(0x0c));
let hash_d = CandidateHash(Hash::repeat_byte(0x0d));
let prev_earliest_session = 0;
let new_earliest_session = 5;
let current_session = 5 + DISPUTE_WINDOW.get();
let super_old_no_dispute = 1;
let very_old = 3;
let slightly_old = 4;
let very_recent = current_session - 1;
let blank_candidate_votes = || CandidateVotes {
candidate_receipt: dummy_candidate_receipt_v2(dummy_hash()),
valid: Vec::new(),
invalid: Vec::new(),
};
let mut overlay_db = OverlayedBackend::new(&backend);
overlay_db.write_earliest_session(prev_earliest_session);
overlay_db.write_recent_disputes(
vec![
((very_old, hash_a), DisputeStatus::Active),
((slightly_old, hash_b), DisputeStatus::Active),
((new_earliest_session, hash_c), DisputeStatus::Active),
((very_recent, hash_d), DisputeStatus::Active),
]
.into_iter()
.collect(),
);
overlay_db.write_candidate_votes(super_old_no_dispute, hash_a, blank_candidate_votes());
overlay_db.write_candidate_votes(very_old, hash_a, blank_candidate_votes());
overlay_db.write_candidate_votes(slightly_old, hash_b, blank_candidate_votes());
overlay_db.write_candidate_votes(new_earliest_session, hash_c, blank_candidate_votes());
overlay_db.write_candidate_votes(very_recent, hash_d, blank_candidate_votes());
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
let mut overlay_db = OverlayedBackend::new(&backend);
note_earliest_session(&mut overlay_db, new_earliest_session).unwrap();
assert_eq!(overlay_db.load_earliest_session().unwrap(), Some(new_earliest_session));
assert_eq!(
overlay_db.load_recent_disputes().unwrap().unwrap(),
vec![
((new_earliest_session, hash_c), DisputeStatus::Active),
((very_recent, hash_d), DisputeStatus::Active),
]
.into_iter()
.collect(),
);
// Votes are only cleaned up after actual write:
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
let overlay_db = OverlayedBackend::new(&backend);
assert!(overlay_db
.load_candidate_votes(super_old_no_dispute, &hash_a)
.unwrap()
.is_none());
assert!(overlay_db.load_candidate_votes(very_old, &hash_a).unwrap().is_none());
assert!(overlay_db.load_candidate_votes(slightly_old, &hash_b).unwrap().is_none());
assert!(overlay_db
.load_candidate_votes(new_earliest_session, &hash_c)
.unwrap()
.is_some());
assert!(overlay_db.load_candidate_votes(very_recent, &hash_d).unwrap().is_some());
}
}
@@ -0,0 +1,132 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use fatality::Nested;
use futures::channel::oneshot;
use pezkuwi_node_subsystem::{errors::ChainApiError, SubsystemError};
use pezkuwi_node_subsystem_util::runtime;
use crate::{db, participation, LOG_TARGET};
use codec::Error as CodecError;
pub type Result<T> = std::result::Result<T, Error>;
pub type FatalResult<T> = std::result::Result<T, FatalError>;
pub type JfyiResult<T> = std::result::Result<T, JfyiError>;
#[allow(missing_docs)]
#[fatality::fatality(splitable)]
pub enum Error {
/// We received a legacy `SubystemError::Context` error which is considered fatal.
#[fatal]
#[error("SubsystemError::Context error: {0}")]
SubsystemContext(String),
/// `ctx.spawn` failed with an error.
#[fatal]
#[error("Spawning a task failed: {0}")]
SpawnFailed(#[source] SubsystemError),
#[fatal]
#[error("Participation worker receiver exhausted.")]
ParticipationWorkerReceiverExhausted,
/// Receiving subsystem message from overseer failed.
#[fatal]
#[error("Receiving message from overseer failed: {0}")]
SubsystemReceive(#[source] SubsystemError),
#[fatal]
#[error("Writing to database failed: {0}")]
DbWriteFailed(std::io::Error),
#[fatal]
#[error("Reading from database failed: {0}")]
DbReadFailed(db::v1::Error),
#[fatal]
#[error("Oneshot for receiving block number from chain API got cancelled")]
CanceledBlockNumber,
#[fatal]
#[error("Retrieving block number from chain API failed with error: {0}")]
ChainApiBlockNumber(ChainApiError),
#[fatal]
#[error(transparent)]
ChainApiAncestors(ChainApiError),
#[fatal]
#[error("Chain API dropped response channel sender")]
ChainApiSenderDropped,
#[fatal(forward)]
#[error("Error while accessing runtime information {0}")]
Runtime(#[from] runtime::Error),
#[error(transparent)]
ChainApi(#[from] ChainApiError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error("Could not send import confirmation (receiver canceled)")]
DisputeImportOneshotSend,
#[error(transparent)]
Subsystem(#[from] SubsystemError),
#[error(transparent)]
Codec(#[from] CodecError),
/// `RollingSessionWindow` was not able to retrieve `SessionInfo`s.
#[error("Session can't be fetched via `RuntimeInfo`")]
SessionInfo,
#[error(transparent)]
QueueError(#[from] participation::QueueError),
}
/// Utility for eating top level errors and log them.
///
/// We basically always want to try and continue on error. This utility function is meant to
/// consume top-level errors by simply logging them
pub fn log_error(result: Result<()>) -> std::result::Result<(), FatalError> {
match result.into_nested()? {
Ok(()) => Ok(()),
Err(jfyi) => {
jfyi.log();
Ok(())
},
}
}
impl JfyiError {
/// Log a `JfyiError`.
pub fn log(self) {
match self {
// don't spam the log with spurious errors
Self::Runtime(runtime::Error::RuntimeRequestCanceled(_)) | Self::Oneshot(_) => {
gum::debug!(target: LOG_TARGET, error = ?self)
},
// it's worth reporting otherwise
_ => gum::warn!(target: LOG_TARGET, error = ?self),
}
}
}
@@ -0,0 +1,636 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Vote import logic.
//!
//! This module encapsulates the actual logic for importing new votes and provides easy access of
//! the current state for votes for a particular candidate.
//!
//! In particular there is `CandidateVoteState` which tells what can be concluded for a particular
//! set of votes. E.g. whether a dispute is ongoing, whether it is confirmed, concluded, ..
//!
//! Then there is `ImportResult` which reveals information about what changed once additional votes
//! got imported on top of an existing `CandidateVoteState` and reveals "dynamic" information, like
//! whether due to the import a dispute was raised/got confirmed, ...
use std::collections::{BTreeMap, HashMap, HashSet};
use pezkuwi_node_primitives::{
disputes::ValidCandidateVotes, CandidateVotes, DisputeStatus, SignedDisputeStatement, Timestamp,
};
use pezkuwi_node_subsystem::overseer;
use pezkuwi_node_subsystem_util::{runtime::RuntimeInfo, ControlledValidatorIndices};
use pezkuwi_primitives::{
CandidateHash, CandidateReceiptV2 as CandidateReceipt, DisputeStatement, ExecutorParams, Hash,
IndexedVec, SessionIndex, SessionInfo, ValidDisputeStatementKind, ValidatorId, ValidatorIndex,
ValidatorSignature,
};
use crate::LOG_TARGET;
/// (Session) environment of a candidate.
pub struct CandidateEnvironment<'a> {
/// The session the candidate appeared in.
session_index: SessionIndex,
/// Session for above index.
session: &'a SessionInfo,
/// Executor parameters for the session.
executor_params: &'a ExecutorParams,
/// Validator indices controlled by this node.
controlled_indices: HashSet<ValidatorIndex>,
/// Indices of on-chain disabled validators at the `relay_parent` combined
/// with the off-chain state.
disabled_indices: HashSet<ValidatorIndex>,
}
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
impl<'a> CandidateEnvironment<'a> {
/// Create `CandidateEnvironment`.
///
/// Return: `None` in case session is outside of session window.
pub async fn new<Context>(
ctx: &mut Context,
runtime_info: &'a mut RuntimeInfo,
session_index: SessionIndex,
relay_parent: Hash,
disabled_offchain: impl IntoIterator<Item = ValidatorIndex>,
controlled_indices: &mut ControlledValidatorIndices,
) -> Option<CandidateEnvironment<'a>> {
let disabled_onchain = runtime_info
.get_disabled_validators(ctx.sender(), relay_parent)
.await
.unwrap_or_else(|err| {
gum::info!(target: LOG_TARGET, ?err, "Failed to get disabled validators");
Vec::new()
});
let (session, executor_params) = match runtime_info
.get_session_info_by_index(ctx.sender(), relay_parent, session_index)
.await
{
Ok(extended_session_info) =>
(&extended_session_info.session_info, &extended_session_info.executor_params),
Err(_) => return None,
};
let n_validators = session.validators.len();
let byzantine_threshold = pezkuwi_primitives::byzantine_threshold(n_validators);
// combine on-chain with off-chain disabled validators
// process disabled validators in the following order:
// - on-chain disabled validators
// - prioritized order of off-chain disabled validators
// deduplicate the list and take at most `byzantine_threshold` validators
let disabled_indices = {
let mut d: HashSet<ValidatorIndex> = HashSet::new();
for v in disabled_onchain.into_iter().chain(disabled_offchain.into_iter()) {
if d.len() == byzantine_threshold {
break;
}
d.insert(v);
}
d
};
let controlled_indices = controlled_indices
.get(session_index, &session.validators)
.map_or(HashSet::new(), |index| HashSet::from([index]));
Some(Self { session_index, session, executor_params, controlled_indices, disabled_indices })
}
/// Validators in the candidate's session.
pub fn validators(&self) -> &IndexedVec<ValidatorIndex, ValidatorId> {
&self.session.validators
}
/// `SessionInfo` for the candidate's session.
pub fn session_info(&self) -> &SessionInfo {
&self.session
}
/// Executor parameters for the candidate's session
pub fn executor_params(&self) -> &ExecutorParams {
&self.executor_params
}
/// Retrieve `SessionIndex` for this environment.
pub fn session_index(&self) -> SessionIndex {
self.session_index
}
/// Indices controlled by this node.
pub fn controlled_indices(&'a self) -> &'a HashSet<ValidatorIndex> {
&self.controlled_indices
}
/// Indices of off-chain and on-chain disabled validators.
pub fn disabled_indices(&'a self) -> &'a HashSet<ValidatorIndex> {
&self.disabled_indices
}
}
/// Whether or not we already issued some statement about a candidate.
pub enum OwnVoteState {
/// Our votes, if any.
Voted(Vec<(ValidatorIndex, (DisputeStatement, ValidatorSignature))>),
/// We are not a teyrchain validator in the session.
///
/// Hence we cannot vote.
CannotVote,
}
impl OwnVoteState {
fn new(votes: &CandidateVotes, env: &CandidateEnvironment) -> Self {
let controlled_indices = env.controlled_indices();
if controlled_indices.is_empty() {
return Self::CannotVote;
}
let our_valid_votes = controlled_indices
.iter()
.filter_map(|i| votes.valid.raw().get_key_value(i))
.map(|(index, (kind, sig))| {
(*index, (DisputeStatement::Valid(kind.clone()), sig.clone()))
});
let our_invalid_votes = controlled_indices
.iter()
.filter_map(|i| votes.invalid.get_key_value(i))
.map(|(index, (kind, sig))| (*index, (DisputeStatement::Invalid(*kind), sig.clone())));
Self::Voted(our_valid_votes.chain(our_invalid_votes).collect())
}
/// Is a vote from us missing but we are a validator able to vote?
fn vote_missing(&self) -> bool {
match self {
Self::Voted(votes) if votes.is_empty() => true,
Self::Voted(_) | Self::CannotVote => false,
}
}
/// Get own approval votes, if any.
///
/// Empty iterator means, no approval votes. `None` means, there will never be any (we cannot
/// vote).
fn approval_votes(
&self,
) -> Option<impl Iterator<Item = (ValidatorIndex, &ValidatorSignature)>> {
match self {
Self::Voted(votes) => Some(votes.iter().filter_map(|(index, (kind, sig))| {
if let DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking) = kind {
Some((*index, sig))
} else {
None
}
})),
Self::CannotVote => None,
}
}
/// Get our votes if there are any.
///
/// Empty iterator means, no votes. `None` means, there will never be any (we cannot
/// vote).
fn votes(&self) -> Option<&Vec<(ValidatorIndex, (DisputeStatement, ValidatorSignature))>> {
match self {
Self::Voted(votes) => Some(&votes),
Self::CannotVote => None,
}
}
}
/// Complete state of votes for a candidate.
///
/// All votes + information whether a dispute is ongoing, confirmed, concluded, whether we already
/// voted, ...
pub struct CandidateVoteState<Votes> {
/// Votes already existing for the candidate + receipt.
votes: Votes,
/// Information about own votes:
own_vote: OwnVoteState,
/// Current dispute status, if there is any.
dispute_status: Option<DisputeStatus>,
/// Are there `byzantine threshold + 1` invalid votes
byzantine_threshold_against: bool,
}
impl CandidateVoteState<CandidateVotes> {
/// Create an empty `CandidateVoteState`
///
/// in case there have not been any previous votes.
pub fn new_from_receipt(candidate_receipt: CandidateReceipt) -> Self {
let votes = CandidateVotes {
candidate_receipt,
valid: ValidCandidateVotes::new(),
invalid: BTreeMap::new(),
};
Self {
votes,
own_vote: OwnVoteState::CannotVote,
dispute_status: None,
byzantine_threshold_against: false,
}
}
/// Create a new `CandidateVoteState` from already existing votes.
pub fn new(votes: CandidateVotes, env: &CandidateEnvironment, now: Timestamp) -> Self {
let own_vote = OwnVoteState::new(&votes, env);
let n_validators = env.validators().len();
let supermajority_threshold = pezkuwi_primitives::supermajority_threshold(n_validators);
// We have a dispute, if we have votes on both sides, with at least one invalid vote
// from non-disabled validator or with votes on both sides and confirmed.
let has_non_disabled_invalid_votes =
votes.invalid.keys().any(|i| !env.disabled_indices().contains(i));
let byzantine_threshold = pezkuwi_primitives::byzantine_threshold(n_validators);
let votes_on_both_sides = !votes.valid.raw().is_empty() && !votes.invalid.is_empty();
let is_confirmed =
votes_on_both_sides && (votes.voted_indices().len() > byzantine_threshold);
let is_disputed =
is_confirmed || (has_non_disabled_invalid_votes && !votes.valid.raw().is_empty());
let (dispute_status, byzantine_threshold_against) = if is_disputed {
let mut status = DisputeStatus::active();
if is_confirmed {
status = status.confirm();
};
let concluded_for = votes.valid.raw().len() >= supermajority_threshold;
if concluded_for {
status = status.conclude_for(now);
};
let concluded_against = votes.invalid.len() >= supermajority_threshold;
if concluded_against {
status = status.conclude_against(now);
};
(Some(status), votes.invalid.len() > byzantine_threshold)
} else {
(None, false)
};
Self { votes, own_vote, dispute_status, byzantine_threshold_against }
}
/// Import fresh statements.
///
/// Result will be a new state plus information about things that changed due to the import.
pub fn import_statements(
self,
env: &CandidateEnvironment,
statements: Vec<(SignedDisputeStatement, ValidatorIndex)>,
now: Timestamp,
) -> ImportResult {
let (mut votes, old_state) = self.into_old_state();
let mut new_invalid_voters = Vec::new();
let mut imported_invalid_votes = 0;
let mut imported_valid_votes = 0;
let expected_candidate_hash = votes.candidate_receipt.hash();
for (statement, val_index) in statements {
if env
.validators()
.get(val_index)
.map_or(true, |v| v != statement.validator_public())
{
gum::error!(
target: LOG_TARGET,
?val_index,
session= ?env.session_index,
claimed_key = ?statement.validator_public(),
"Validator index doesn't match claimed key",
);
continue;
}
if statement.candidate_hash() != &expected_candidate_hash {
gum::error!(
target: LOG_TARGET,
?val_index,
session= ?env.session_index,
given_candidate_hash = ?statement.candidate_hash(),
?expected_candidate_hash,
"Vote is for unexpected candidate!",
);
continue;
}
if statement.session_index() != env.session_index() {
gum::error!(
target: LOG_TARGET,
?val_index,
session= ?env.session_index,
given_candidate_hash = ?statement.candidate_hash(),
?expected_candidate_hash,
"Vote is for unexpected session!",
);
continue;
}
match statement.statement() {
DisputeStatement::Valid(valid_kind) => {
let fresh = votes.valid.insert_vote(
val_index,
valid_kind.clone(),
statement.into_validator_signature(),
);
if fresh {
imported_valid_votes += 1;
}
},
DisputeStatement::Invalid(invalid_kind) => {
let fresh = votes
.invalid
.insert(val_index, (*invalid_kind, statement.into_validator_signature()))
.is_none();
if fresh {
new_invalid_voters.push(val_index);
imported_invalid_votes += 1;
}
},
}
}
let new_state = Self::new(votes, env, now);
ImportResult {
old_state,
new_state,
imported_invalid_votes,
imported_valid_votes,
imported_approval_votes: 0,
new_invalid_voters,
}
}
/// Retrieve `CandidateReceipt` in `CandidateVotes`.
pub fn candidate_receipt(&self) -> &CandidateReceipt {
&self.votes.candidate_receipt
}
/// Returns true if all the invalid votes are from disabled validators.
pub fn invalid_votes_all_disabled(
&self,
mut is_disabled: impl FnMut(&ValidatorIndex) -> bool,
) -> bool {
self.votes.invalid.keys().all(|i| is_disabled(i))
}
/// Extract `CandidateVotes` for handling import of new statements.
fn into_old_state(self) -> (CandidateVotes, CandidateVoteState<()>) {
let CandidateVoteState { votes, own_vote, dispute_status, byzantine_threshold_against } =
self;
(
votes,
CandidateVoteState { votes: (), own_vote, dispute_status, byzantine_threshold_against },
)
}
}
impl<V> CandidateVoteState<V> {
/// Whether or not we have an ongoing dispute.
pub fn is_disputed(&self) -> bool {
self.dispute_status.is_some()
}
/// Whether there is an ongoing confirmed dispute.
///
/// This checks whether there is a dispute ongoing and we have more than byzantine threshold
/// votes.
pub fn is_confirmed(&self) -> bool {
self.dispute_status.map_or(false, |s| s.is_confirmed_concluded())
}
/// Are we a validator in the session, but have not yet voted?
pub fn own_vote_missing(&self) -> bool {
self.own_vote.vote_missing()
}
/// Own approval votes if any:
pub fn own_approval_votes(
&self,
) -> Option<impl Iterator<Item = (ValidatorIndex, &ValidatorSignature)>> {
self.own_vote.approval_votes()
}
/// Get own votes if there are any.
pub fn own_votes(
&self,
) -> Option<&Vec<(ValidatorIndex, (DisputeStatement, ValidatorSignature))>> {
self.own_vote.votes()
}
/// Whether or not there is a dispute and it has already enough valid votes to conclude.
pub fn has_concluded_for(&self) -> bool {
self.dispute_status.map_or(false, |s| s.has_concluded_for())
}
/// Whether or not there is a dispute and it has already enough invalid votes to conclude.
pub fn has_concluded_against(&self) -> bool {
self.dispute_status.map_or(false, |s| s.has_concluded_against())
}
/// Get access to the dispute status, in case there is one.
pub fn dispute_status(&self) -> &Option<DisputeStatus> {
&self.dispute_status
}
/// Access to underlying votes.
pub fn votes(&self) -> &V {
&self.votes
}
}
/// An ongoing statement/vote import.
pub struct ImportResult {
/// The state we had before importing new statements.
old_state: CandidateVoteState<()>,
/// The new state after importing the new statements.
new_state: CandidateVoteState<CandidateVotes>,
/// New invalid voters as of this import.
new_invalid_voters: Vec<ValidatorIndex>,
/// Number of successfully imported valid votes.
imported_invalid_votes: u32,
/// Number of successfully imported invalid votes.
imported_valid_votes: u32,
/// Number of approval votes imported via `import_approval_votes()`.
///
/// And only those: If normal import included approval votes, those are not counted here.
///
/// In other words, without a call `import_approval_votes()` this will always be 0.
imported_approval_votes: u32,
}
impl ImportResult {
/// Whether or not anything has changed due to the import.
pub fn votes_changed(&self) -> bool {
self.imported_valid_votes != 0 || self.imported_invalid_votes != 0
}
/// The dispute state has changed in some way.
///
/// - freshly disputed
/// - freshly confirmed
/// - freshly concluded (valid or invalid)
pub fn dispute_state_changed(&self) -> bool {
self.is_freshly_disputed() || self.is_freshly_confirmed() || self.is_freshly_concluded()
}
/// State as it was before import.
pub fn old_state(&self) -> &CandidateVoteState<()> {
&self.old_state
}
/// State after import
pub fn new_state(&self) -> &CandidateVoteState<CandidateVotes> {
&self.new_state
}
/// New "invalid" voters encountered during import.
pub fn new_invalid_voters(&self) -> &Vec<ValidatorIndex> {
&self.new_invalid_voters
}
/// Number of imported valid votes.
pub fn imported_valid_votes(&self) -> u32 {
self.imported_valid_votes
}
/// Number of imported invalid votes.
pub fn imported_invalid_votes(&self) -> u32 {
self.imported_invalid_votes
}
/// Number of imported approval votes.
pub fn imported_approval_votes(&self) -> u32 {
self.imported_approval_votes
}
/// Whether we now have a dispute and did not prior to the import.
pub fn is_freshly_disputed(&self) -> bool {
!self.old_state().is_disputed() && self.new_state().is_disputed()
}
/// Whether we just surpassed the byzantine threshold.
pub fn is_freshly_confirmed(&self) -> bool {
!self.old_state().is_confirmed() && self.new_state().is_confirmed()
}
/// Whether or not any dispute just concluded valid due to the import.
pub fn is_freshly_concluded_for(&self) -> bool {
!self.old_state().has_concluded_for() && self.new_state().has_concluded_for()
}
/// Whether or not any dispute just concluded invalid due to the import.
pub fn is_freshly_concluded_against(&self) -> bool {
!self.old_state().has_concluded_against() && self.new_state().has_concluded_against()
}
/// Whether or not any dispute just concluded either invalid or valid due to the import.
pub fn is_freshly_concluded(&self) -> bool {
self.is_freshly_concluded_against() || self.is_freshly_concluded_for()
}
/// Whether or not the invalid vote count for the dispute went beyond the byzantine threshold
/// after the last import
pub fn has_fresh_byzantine_threshold_against(&self) -> bool {
!self.old_state().byzantine_threshold_against &&
self.new_state().byzantine_threshold_against
}
/// Modify this `ImportResult`s, by importing additional approval votes.
///
/// Both results and `new_state` will be changed as if those approval votes had been in the
/// original import.
pub fn import_approval_votes(
self,
env: &CandidateEnvironment,
approval_votes: HashMap<ValidatorIndex, (Vec<CandidateHash>, ValidatorSignature)>,
now: Timestamp,
) -> Self {
let Self {
old_state,
new_state,
new_invalid_voters,
mut imported_valid_votes,
imported_invalid_votes,
mut imported_approval_votes,
} = self;
let (mut votes, _) = new_state.into_old_state();
for (index, (candidate_hashes, sig)) in approval_votes.into_iter() {
debug_assert!(
{
let pub_key = &env.session_info().validators.get(index).expect("indices are validated by approval-voting subsystem; qed");
let session_index = env.session_index();
candidate_hashes.contains(&votes.candidate_receipt.hash()) && DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(candidate_hashes.clone()))
.check_signature(pub_key, *candidate_hashes.first().expect("Valid votes have at least one candidate; qed"), session_index, &sig)
.is_ok()
},
"Signature check for imported approval votes failed! This is a serious bug. Session: {:?}, candidate hash: {:?}, validator index: {:?}", env.session_index(), votes.candidate_receipt.hash(), index
);
if votes.valid.insert_vote(
index,
// There is a hidden dependency here between approval-voting and this subsystem.
// We should be able to start emitting
// ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates only after:
// 1. Runtime have been upgraded to know about the new format.
// 2. All nodes have been upgraded to know about the new format.
// Once those two requirements have been met we should be able to increase
// max_approval_coalesce_count to values greater than 1.
if candidate_hashes.len() > 1 {
ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(candidate_hashes)
} else {
ValidDisputeStatementKind::ApprovalChecking
},
sig,
) {
imported_valid_votes += 1;
imported_approval_votes += 1;
}
}
let new_state = CandidateVoteState::new(votes, env, now);
Self {
old_state,
new_state,
new_invalid_voters,
imported_valid_votes,
imported_invalid_votes,
imported_approval_votes,
}
}
/// All done, give me those votes.
///
/// Returns: `None` in case nothing has changed (import was redundant).
pub fn into_updated_votes(self) -> Option<CandidateVotes> {
if self.votes_changed() {
let CandidateVoteState { votes, .. } = self.new_state;
Some(votes)
} else {
None
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,649 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Implements the dispute coordinator subsystem.
//!
//! This is the central subsystem of the node-side components which participate in disputes.
//! This subsystem wraps a database which tracks all statements observed by all validators over some
//! window of sessions. Votes older than this session window are pruned.
//!
//! This subsystem will be the point which produce dispute votes, either positive or negative, based
//! on locally-observed validation results as well as a sink for votes received by other subsystems.
//! When importing a dispute vote from another node, this will trigger dispute participation to
//! recover and validate the block.
use std::sync::Arc;
use error::FatalError;
use futures::FutureExt;
use gum::CandidateHash;
use sc_keystore::LocalKeystore;
use pezkuwi_node_primitives::{
CandidateVotes, DisputeMessage, DisputeMessageCheckError, SignedDisputeStatement,
DISPUTE_WINDOW,
};
use pezkuwi_node_subsystem::{
messages::DisputeDistributionMessage, overseer, ActivatedLeaf, FromOrchestra, OverseerSignal,
SpawnedSubsystem, SubsystemError,
};
use pezkuwi_node_subsystem_util::{
database::Database,
runtime::{Config as RuntimeInfoConfig, RuntimeInfo},
ControlledValidatorIndices,
};
use pezkuwi_primitives::{
DisputeStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidatorIndex,
};
use crate::{
error::{FatalResult, Result},
metrics::Metrics,
status::{get_active_with_status, SystemClock},
};
use backend::{Backend, OverlayedBackend};
use db::v1::DbBackend;
use fatality::Split;
use self::{
import::{CandidateEnvironment, CandidateVoteState},
participation::{ParticipationPriority, ParticipationRequest},
spam_slots::{SpamSlots, UnconfirmedDisputes},
};
pub(crate) mod backend;
pub(crate) mod db;
pub(crate) mod error;
/// Subsystem after receiving the first active leaf.
mod initialized;
use initialized::{InitialData, Initialized};
/// Provider of data scraped from chain.
///
/// If we have seen a candidate included somewhere, we should treat it as priority and will be able
/// to provide an ordering for participation. Thus a dispute for a candidate where we can get some
/// ordering is high-priority (we know it is a valid dispute) and those can be ordered by
/// `participation` based on `relay_parent` block number and other metrics, so each validator will
/// participate in disputes in a similar order, which ensures we will be resolving disputes, even
/// under heavy load.
mod scraping;
use scraping::ChainScraper;
/// When importing votes we will check via the `ordering` module, whether or not we know of the
/// candidate to be included somewhere. If not, the votes might be spam, in this case we want to
/// limit the amount of locally imported votes, to prevent DoS attacks/resource exhaustion. The
/// `spam_slots` module helps keeping track of unconfirmed disputes per validators, if a spam slot
/// gets full, we will drop any further potential spam votes from that validator and report back
/// that the import failed. Which will lead to any honest validator to retry, thus the spam slots
/// can be relatively small, as a drop is not fatal.
mod spam_slots;
/// Handling of participation requests via `Participation`.
///
/// `Participation` provides an API (`Participation::queue_participation`) for queuing of dispute
/// participations and will process those participation requests, such that most important/urgent
/// disputes will be resolved and processed first and more importantly it will order requests in a
/// way so disputes will get resolved, even if there are lots of them.
pub(crate) mod participation;
/// Pure processing of vote imports.
pub(crate) mod import;
/// Metrics types.
mod metrics;
/// Status tracking of disputes (`DisputeStatus`).
mod status;
use crate::status::Clock;
#[cfg(test)]
mod tests;
pub(crate) const LOG_TARGET: &str = "teyrchain::dispute-coordinator";
/// An implementation of the dispute coordinator subsystem.
pub struct DisputeCoordinatorSubsystem {
config: Config,
store: Arc<dyn Database>,
keystore: Arc<LocalKeystore>,
metrics: Metrics,
}
/// Configuration for the dispute coordinator subsystem.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The data column in the store to use for dispute data.
pub col_dispute_data: u32,
}
impl Config {
fn column_config(&self) -> db::v1::ColumnConfiguration {
db::v1::ColumnConfiguration { col_dispute_data: self.col_dispute_data }
}
}
#[overseer::subsystem(DisputeCoordinator, error=SubsystemError, prefix=self::overseer)]
impl<Context: Send> DisputeCoordinatorSubsystem {
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = async {
let backend = DbBackend::new(
self.store.clone(),
self.config.column_config(),
self.metrics.clone(),
);
self.run(ctx, backend, Box::new(SystemClock))
.await
.map_err(|e| SubsystemError::with_origin("dispute-coordinator", e))
}
.boxed();
SpawnedSubsystem { name: "dispute-coordinator-subsystem", future }
}
}
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
impl DisputeCoordinatorSubsystem {
/// Create a new instance of the subsystem.
pub fn new(
store: Arc<dyn Database>,
config: Config,
keystore: Arc<LocalKeystore>,
metrics: Metrics,
) -> Self {
Self { store, config, keystore, metrics }
}
/// Initialize and afterwards run `Initialized::run`.
async fn run<B, Context>(
self,
mut ctx: Context,
backend: B,
clock: Box<dyn Clock>,
) -> FatalResult<()>
where
B: Backend + 'static,
{
let res = self.initialize(&mut ctx, backend, &*clock).await?;
let (participations, votes, first_leaf, initialized, backend) = match res {
// Concluded:
None => return Ok(()),
Some(r) => r,
};
initialized
.run(ctx, backend, Some(InitialData { participations, votes, leaf: first_leaf }), clock)
.await
}
/// Make sure to recover participations properly on startup.
async fn initialize<B, Context>(
self,
ctx: &mut Context,
mut backend: B,
clock: &dyn Clock,
) -> FatalResult<
Option<(
Vec<(ParticipationPriority, ParticipationRequest)>,
Vec<ScrapedOnChainVotes>,
ActivatedLeaf,
Initialized,
B,
)>,
>
where
B: Backend + 'static,
{
loop {
let first_leaf = match wait_for_first_leaf(ctx).await {
Ok(Some(activated_leaf)) => activated_leaf,
Ok(None) => continue,
Err(e) => {
e.split()?.log();
continue;
},
};
// `RuntimeInfo` cache should match `DISPUTE_WINDOW` so that we can
// keep all sessions for a dispute window
let mut runtime_info = RuntimeInfo::new_with_config(RuntimeInfoConfig {
keystore: None,
session_cache_lru_size: DISPUTE_WINDOW.get(),
});
let mut overlay_db = OverlayedBackend::new(&mut backend);
let (
participations,
votes,
spam_slots,
ordering_provider,
highest_session_seen,
gaps_in_cache,
offchain_disabled_validators,
controlled_validator_indices,
) = match self
.handle_startup(ctx, first_leaf.clone(), &mut runtime_info, &mut overlay_db, clock)
.await
{
Ok(v) => v,
Err(e) => {
e.split()?.log();
continue;
},
};
if !overlay_db.is_empty() {
let ops = overlay_db.into_write_ops();
backend.write(ops)?;
}
return Ok(Some((
participations,
votes,
first_leaf,
Initialized::new(
self,
runtime_info,
spam_slots,
ordering_provider,
highest_session_seen,
gaps_in_cache,
offchain_disabled_validators,
controlled_validator_indices,
),
backend,
)));
}
}
// Restores the subsystem's state before proceeding with the main event loop.
//
// - Prune any old disputes.
// - Find disputes we need to participate in.
// - Initialize spam slots & OrderingProvider.
async fn handle_startup<Context>(
&self,
ctx: &mut Context,
initial_head: ActivatedLeaf,
runtime_info: &mut RuntimeInfo,
overlay_db: &mut OverlayedBackend<'_, impl Backend>,
clock: &dyn Clock,
) -> Result<(
Vec<(ParticipationPriority, ParticipationRequest)>,
Vec<ScrapedOnChainVotes>,
SpamSlots,
ChainScraper,
SessionIndex,
bool,
initialized::OffchainDisabledValidators,
ControlledValidatorIndices,
)> {
let now = clock.now();
// We assume the highest session is the passed leaf. If we can't get the session index
// we can't initialize the subsystem so we'll wait for a new leaf
let highest_session = runtime_info
.get_session_index_for_child(ctx.sender(), initial_head.hash)
.await?;
let earliest_session = highest_session.saturating_sub(DISPUTE_WINDOW.get() - 1);
// Load recent disputes from the database
let recent_disputes = match overlay_db.load_recent_disputes() {
Ok(disputes) => disputes.unwrap_or_default(),
Err(e) => {
gum::error!(target: LOG_TARGET, "Failed initial load of recent disputes: {:?}", e);
return Err(e.into());
},
};
// Initialize offchain disabled validators from recent disputes
let offchain_disabled_validators = initialized::OffchainDisabledValidators::new_from_state(
&recent_disputes,
|session, candidate_hash| match overlay_db.load_candidate_votes(session, candidate_hash)
{
Ok(Some(votes)) => Some(votes.into()),
_ => None,
},
earliest_session,
);
let active_disputes = get_active_with_status(recent_disputes.into_iter(), now);
let mut gap_in_cache = false;
// Cache the sessions. A failure to fetch a session here is not that critical so we
// won't abort the initialization
for idx in earliest_session..=highest_session {
// Print disabled validators on startup if any
let disabled: Vec<u32> = offchain_disabled_validators.iter(idx).map(|i| i.0).collect();
if !disabled.is_empty() {
gum::info!(
target: LOG_TARGET,
disabled = ?disabled,
session = idx,
"Detected disabled validators on startup",
);
}
if let Err(e) = runtime_info
.get_session_info_by_index(ctx.sender(), initial_head.hash, idx)
.await
{
gum::debug!(
target: LOG_TARGET,
leaf_hash = ?initial_head.hash,
session_idx = idx,
err = ?e,
"Can't cache SessionInfo during subsystem initialization. Skipping session."
);
gap_in_cache = true;
continue;
};
}
// Prune obsolete disputes:
db::v1::note_earliest_session(overlay_db, earliest_session)?;
let mut participation_requests = Vec::new();
let mut spam_disputes: UnconfirmedDisputes = UnconfirmedDisputes::new();
let mut controlled_indices =
ControlledValidatorIndices::new(self.keystore.clone(), DISPUTE_WINDOW.get());
let leaf_hash = initial_head.hash;
let (scraper, votes) = ChainScraper::new(ctx.sender(), initial_head).await?;
for ((session, ref candidate_hash), _) in active_disputes {
let env = match CandidateEnvironment::new(
ctx,
runtime_info,
highest_session,
leaf_hash,
offchain_disabled_validators.iter(session),
&mut controlled_indices,
)
.await
{
None => {
gum::warn!(
target: LOG_TARGET,
session,
"We are lacking a `SessionInfo` for handling db votes on startup."
);
continue;
},
Some(env) => env,
};
let votes: CandidateVotes =
match overlay_db.load_candidate_votes(session, candidate_hash) {
Ok(Some(votes)) => votes.into(),
Ok(None) => continue,
Err(e) => {
gum::error!(
target: LOG_TARGET,
"Failed initial load of candidate votes: {:?}",
e
);
continue;
},
};
let vote_state = CandidateVoteState::new(votes, &env, now);
let is_disabled = |v: &ValidatorIndex| env.disabled_indices().contains(v);
let potential_spam =
is_potential_spam(&scraper, &vote_state, candidate_hash, is_disabled);
let is_included =
scraper.is_candidate_included(&vote_state.votes().candidate_receipt.hash());
if potential_spam {
gum::trace!(
target: LOG_TARGET,
?session,
?candidate_hash,
"Found potential spam dispute on startup"
);
spam_disputes
.insert((session, *candidate_hash), vote_state.votes().voted_indices());
} else {
// Participate if need be:
if vote_state.own_vote_missing() {
gum::trace!(
target: LOG_TARGET,
?session,
?candidate_hash,
"Found valid dispute, with no vote from us on startup - participating."
);
let request_timer = self.metrics.time_participation_pipeline();
participation_requests.push((
ParticipationPriority::with_priority_if(is_included),
ParticipationRequest::new(
vote_state.votes().candidate_receipt.clone(),
session,
env.executor_params().clone(),
request_timer,
),
));
}
// Else make sure our own vote is distributed:
else {
gum::trace!(
target: LOG_TARGET,
?session,
?candidate_hash,
"Found valid dispute, with vote from us on startup - send vote."
);
send_dispute_messages(ctx, &env, &vote_state).await;
}
}
}
Ok((
participation_requests,
votes,
SpamSlots::recover_from_state(spam_disputes),
scraper,
highest_session,
gap_in_cache,
offchain_disabled_validators,
controlled_indices,
))
}
}
/// Wait for `ActiveLeavesUpdate`, returns `None` if `Conclude` signal came first.
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
async fn wait_for_first_leaf<Context>(ctx: &mut Context) -> Result<Option<ActivatedLeaf>> {
loop {
match ctx.recv().await.map_err(FatalError::SubsystemReceive)? {
FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(None),
FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => {
if let Some(activated) = update.activated {
return Ok(Some(activated));
}
},
FromOrchestra::Signal(OverseerSignal::BlockFinalized(_, _)) => {},
FromOrchestra::Communication { msg } =>
// NOTE: We could technically actually handle a couple of message types, even if
// not initialized (e.g. all requests that only query the database). The problem
// is, we would deliver potentially outdated information, especially in the event
// of bugs where initialization fails for a while (e.g. `SessionInfo`s are not
// available). So instead of telling subsystems, everything is fine, because of an
// hour old database state, we should rather cancel contained oneshots and delay
// finality until we are fully functional.
{
gum::warn!(
target: LOG_TARGET,
?msg,
"Received msg before first active leaves update. This is not expected - message will be dropped."
)
},
}
}
}
/// Check whether a dispute for the given candidate could be spam.
///
/// That is the candidate could be made up.
pub fn is_potential_spam(
scraper: &ChainScraper,
vote_state: &CandidateVoteState<CandidateVotes>,
candidate_hash: &CandidateHash,
is_disabled: impl FnMut(&ValidatorIndex) -> bool,
) -> bool {
let is_disputed = vote_state.is_disputed();
let is_included = scraper.is_candidate_included(candidate_hash);
let is_backed = scraper.is_candidate_backed(candidate_hash);
let is_confirmed = vote_state.is_confirmed();
let all_invalid_votes_disabled = vote_state.invalid_votes_all_disabled(is_disabled);
let ignore_disabled = !is_confirmed && all_invalid_votes_disabled;
gum::trace!(
target: LOG_TARGET,
?candidate_hash,
?is_disputed,
?is_included,
?is_backed,
?is_confirmed,
?all_invalid_votes_disabled,
?ignore_disabled,
"Checking for potential spam"
);
(is_disputed && !is_included && !is_backed && !is_confirmed) || ignore_disabled
}
/// Tell dispute-distribution to send all our votes.
///
/// Should be called on startup for all active disputes where there are votes from us already.
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
async fn send_dispute_messages<Context>(
ctx: &mut Context,
env: &CandidateEnvironment<'_>,
vote_state: &CandidateVoteState<CandidateVotes>,
) {
for own_vote in vote_state.own_votes().into_iter().flatten() {
let (validator_index, (kind, sig)) = own_vote;
let public_key = if let Some(key) = env.session_info().validators.get(*validator_index) {
key.clone()
} else {
gum::error!(
target: LOG_TARGET,
?validator_index,
session_index = ?env.session_index(),
"Could not find our own key in `SessionInfo`"
);
continue;
};
let our_vote_signed = SignedDisputeStatement::new_checked(
kind.clone(),
vote_state.votes().candidate_receipt.hash(),
env.session_index(),
public_key,
sig.clone(),
);
let our_vote_signed = match our_vote_signed {
Ok(signed) => signed,
Err(()) => {
gum::error!(
target: LOG_TARGET,
"Checking our own signature failed - db corruption?"
);
continue;
},
};
let dispute_message = match make_dispute_message(
env.session_info(),
vote_state.votes(),
our_vote_signed,
*validator_index,
) {
Err(err) => {
gum::debug!(target: LOG_TARGET, ?err, "Creating dispute message failed.");
continue;
},
Ok(dispute_message) => dispute_message,
};
ctx.send_message(DisputeDistributionMessage::SendDispute(dispute_message)).await;
}
}
#[derive(Debug, thiserror::Error)]
pub enum DisputeMessageCreationError {
#[error("There was no opposite vote available")]
NoOppositeVote,
#[error("Found vote had an invalid validator index that could not be found")]
InvalidValidatorIndex,
#[error("Statement found in votes had invalid signature.")]
InvalidStoredStatement,
#[error(transparent)]
InvalidStatementCombination(DisputeMessageCheckError),
}
/// Create a `DisputeMessage` to be sent to `DisputeDistribution`.
pub fn make_dispute_message(
info: &SessionInfo,
votes: &CandidateVotes,
our_vote: SignedDisputeStatement,
our_index: ValidatorIndex,
) -> std::result::Result<DisputeMessage, DisputeMessageCreationError> {
let validators = &info.validators;
let (valid_statement, valid_index, invalid_statement, invalid_index) =
if let DisputeStatement::Valid(_) = our_vote.statement() {
let (validator_index, (statement_kind, validator_signature)) =
votes.invalid.iter().next().ok_or(DisputeMessageCreationError::NoOppositeVote)?;
let other_vote = SignedDisputeStatement::new_checked(
DisputeStatement::Invalid(*statement_kind),
*our_vote.candidate_hash(),
our_vote.session_index(),
validators
.get(*validator_index)
.ok_or(DisputeMessageCreationError::InvalidValidatorIndex)?
.clone(),
validator_signature.clone(),
)
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
(our_vote, our_index, other_vote, *validator_index)
} else {
let (validator_index, (statement_kind, validator_signature)) = votes
.valid
.raw()
.iter()
.next()
.ok_or(DisputeMessageCreationError::NoOppositeVote)?;
let other_vote = SignedDisputeStatement::new_checked(
DisputeStatement::Valid(statement_kind.clone()),
*our_vote.candidate_hash(),
our_vote.session_index(),
validators
.get(*validator_index)
.ok_or(DisputeMessageCreationError::InvalidValidatorIndex)?
.clone(),
validator_signature.clone(),
)
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
(other_vote, *validator_index, our_vote, our_index)
};
DisputeMessage::from_signed_statements(
valid_statement,
valid_index,
invalid_statement,
invalid_index,
votes.candidate_receipt.clone(),
info,
)
.map_err(DisputeMessageCreationError::InvalidStatementCombination)
}
@@ -0,0 +1,252 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
#[derive(Clone)]
struct MetricsInner {
/// Number of opened disputes.
open: prometheus::Counter<prometheus::U64>,
/// Votes of all disputes.
votes: prometheus::CounterVec<prometheus::U64>,
/// Number of approval votes explicitly fetched from approval voting.
approval_votes: prometheus::Counter<prometheus::U64>,
/// Conclusion across all disputes.
concluded: prometheus::CounterVec<prometheus::U64>,
/// Number of participations that have been queued.
queued_participations: prometheus::CounterVec<prometheus::U64>,
/// How long vote cleanup batches take.
vote_cleanup_time: prometheus::Histogram,
/// Number of refrained participations.
refrained_participations: prometheus::Counter<prometheus::U64>,
/// Number of unactivated disputes.
unactivated: prometheus::Counter<prometheus::U64>,
/// Distribution of participation durations.
participation_durations: prometheus::Histogram,
/// Measures the duration of the full participation pipeline: From when
/// a participation request is first queued to when participation in the
/// requested dispute is complete.
participation_pipeline_durations: prometheus::Histogram,
/// Size of participation priority queue
participation_priority_queue_size: prometheus::Gauge<prometheus::U64>,
/// Size of participation best effort queue
participation_best_effort_queue_size: prometheus::Gauge<prometheus::U64>,
}
/// Candidate validation metrics.
#[derive(Default, Clone)]
pub struct Metrics(Option<MetricsInner>);
impl Metrics {
pub(crate) fn on_open(&self) {
if let Some(metrics) = &self.0 {
metrics.open.inc();
}
}
pub(crate) fn on_valid_votes(&self, vote_count: u32) {
if let Some(metrics) = &self.0 {
metrics.votes.with_label_values(&["valid"]).inc_by(vote_count as _);
}
}
pub(crate) fn on_invalid_votes(&self, vote_count: u32) {
if let Some(metrics) = &self.0 {
metrics.votes.with_label_values(&["invalid"]).inc_by(vote_count as _);
}
}
pub(crate) fn on_approval_votes(&self, vote_count: u32) {
if let Some(metrics) = &self.0 {
metrics.approval_votes.inc_by(vote_count as _);
}
}
pub(crate) fn on_concluded_valid(&self) {
if let Some(metrics) = &self.0 {
metrics.concluded.with_label_values(&["valid"]).inc();
}
}
pub(crate) fn on_concluded_invalid(&self) {
if let Some(metrics) = &self.0 {
metrics.concluded.with_label_values(&["invalid"]).inc();
}
}
pub(crate) fn on_queued_priority_participation(&self) {
if let Some(metrics) = &self.0 {
metrics.queued_participations.with_label_values(&["priority"]).inc();
}
}
pub(crate) fn on_queued_best_effort_participation(&self) {
if let Some(metrics) = &self.0 {
metrics.queued_participations.with_label_values(&["best-effort"]).inc();
}
}
pub(crate) fn time_vote_cleanup(&self) -> Option<prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.vote_cleanup_time.start_timer())
}
pub(crate) fn on_refrained_participation(&self) {
if let Some(metrics) = &self.0 {
metrics.refrained_participations.inc();
}
}
/// Provide a timer for participation durations which updates on drop.
pub(crate) fn time_participation(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0.as_ref().map(|metrics| metrics.participation_durations.start_timer())
}
/// Provide a timer for participation pipeline durations which updates on drop.
pub(crate) fn time_participation_pipeline(
&self,
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
self.0
.as_ref()
.map(|metrics| metrics.participation_pipeline_durations.start_timer())
}
/// Set the `priority_queue_size` metric
pub fn report_priority_queue_size(&self, size: u64) {
if let Some(metrics) = &self.0 {
metrics.participation_priority_queue_size.set(size);
}
}
/// Set the `best_effort_queue_size` metric
pub fn report_best_effort_queue_size(&self, size: u64) {
if let Some(metrics) = &self.0 {
metrics.participation_best_effort_queue_size.set(size);
}
}
pub(crate) fn on_unactivated_dispute(&self) {
if let Some(metrics) = &self.0 {
metrics.unactivated.inc();
}
}
}
impl metrics::Metrics for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
open: prometheus::register(
prometheus::Counter::with_opts(prometheus::Opts::new(
"pezkuwi_teyrchain_candidate_disputes_total",
"Total number of raised disputes.",
))?,
registry,
)?,
concluded: prometheus::register(
prometheus::CounterVec::new(
prometheus::Opts::new(
"pezkuwi_teyrchain_candidate_dispute_concluded",
"Concluded dispute votes, sorted by candidate is `valid` and `invalid`.",
),
&["validity"],
)?,
registry,
)?,
votes: prometheus::register(
prometheus::CounterVec::new(
prometheus::Opts::new(
"pezkuwi_teyrchain_candidate_dispute_votes",
"Accumulated dispute votes, sorted by candidate is `valid` and `invalid`.",
),
&["validity"],
)?,
registry,
)?,
approval_votes: prometheus::register(
prometheus::Counter::with_opts(prometheus::Opts::new(
"pezkuwi_teyrchain_dispute_candidate_approval_votes_fetched_total",
"Number of approval votes fetched from approval voting.",
))?,
registry,
)?,
queued_participations: prometheus::register(
prometheus::CounterVec::new(
prometheus::Opts::new(
"pezkuwi_teyrchain_dispute_participations",
"Total number of queued participations, grouped by priority and best-effort. (Not every queueing will necessarily lead to an actual participation because of duplicates.)",
),
&["priority"],
)?,
registry,
)?,
vote_cleanup_time: prometheus::register(
prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_dispute_coordinator_vote_cleanup",
"Time spent cleaning up old votes per batch.",
)
.buckets([0.01, 0.1, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0].into()),
)?,
registry,
)?,
refrained_participations: prometheus::register(
prometheus::Counter::with_opts(
prometheus::Opts::new(
"pezkuwi_teyrchain_dispute_refrained_participations",
"Number of refrained participations. We refrain from participation if all of the following conditions are met: disputed candidate is not included, not backed and not confirmed.",
))?,
registry,
)?,
participation_durations: prometheus::register(
prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_dispute_participation_durations",
"Time spent within fn Participation::participate",
)
)?,
registry,
)?,
participation_pipeline_durations: prometheus::register(
prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new(
"pezkuwi_teyrchain_dispute_participation_pipeline_durations",
"Measures the duration of the full participation pipeline: From when a participation request is first queued to when participation in the requested dispute is complete.",
)
)?,
registry,
)?,
participation_priority_queue_size: prometheus::register(
prometheus::Gauge::new("pezkuwi_teyrchain_dispute_participation_priority_queue_size",
"Number of disputes waiting for local participation in the priority queue.")?,
registry,
)?,
participation_best_effort_queue_size: prometheus::register(
prometheus::Gauge::new("pezkuwi_teyrchain_dispute_participation_best_effort_queue_size",
"Number of disputes waiting for local participation in the best effort queue.")?,
registry,
)?,
unactivated: prometheus::register(
prometheus::Counter::with_opts(prometheus::Opts::new(
"pezkuwi_teyrchain_dispute_unactivated_total",
"Total number of disputes that were unactivated due to all raising parties being disabled.",
))?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
}
@@ -0,0 +1,447 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashSet;
#[cfg(test)]
use std::time::Duration;
use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt,
};
#[cfg(test)]
use futures_timer::Delay;
use pezkuwi_node_primitives::ValidationResult;
use pezkuwi_node_subsystem::{
messages::{AvailabilityRecoveryMessage, CandidateValidationMessage, PvfExecKind},
overseer, ActiveLeavesUpdate, RecoveryError,
};
use pezkuwi_node_subsystem_util::runtime::get_validation_code_by_hash;
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateReceiptV2 as CandidateReceipt, Hash, SessionIndex,
};
use crate::LOG_TARGET;
use crate::error::{FatalError, FatalResult, Result};
#[cfg(test)]
mod tests;
#[cfg(test)]
pub use tests::{participation_full_happy_path, participation_missing_availability};
mod queues;
use queues::Queues;
pub use queues::{ParticipationPriority, ParticipationRequest, QueueError};
use crate::metrics::Metrics;
use pezkuwi_node_subsystem_util::metrics::prometheus::prometheus;
/// How many participation processes do we want to run in parallel the most.
///
/// This should be a relatively low value, while we might have a speedup once we fetched the data,
/// due to multi-core architectures, but the fetching itself can not be improved by parallel
/// requests. This means that higher numbers make it harder for a single dispute to resolve fast.
#[cfg(not(test))]
const MAX_PARALLEL_PARTICIPATIONS: usize = 3;
#[cfg(test)]
pub(crate) const MAX_PARALLEL_PARTICIPATIONS: usize = 1;
/// Keep track of disputes we need to participate in.
///
/// - Prioritize and queue participations
/// - Dequeue participation requests in order and launch participation worker.
pub struct Participation {
/// Participations currently being processed.
running_participations: HashSet<CandidateHash>,
/// Priority and best effort queues.
queue: Queues,
/// Sender to be passed to worker tasks.
worker_sender: WorkerMessageSender,
/// Some recent block for retrieving validation code from chain.
recent_block: Option<(BlockNumber, Hash)>,
/// Metrics handle cloned from Initialized
metrics: Metrics,
}
/// Message from worker tasks.
#[derive(Debug)]
pub struct WorkerMessage(ParticipationStatement);
/// Sender use by worker tasks.
pub type WorkerMessageSender = mpsc::Sender<WorkerMessage>;
/// Receiver to receive messages from worker tasks.
pub type WorkerMessageReceiver = mpsc::Receiver<WorkerMessage>;
/// Statement as result of the validation process.
#[derive(Debug)]
pub struct ParticipationStatement {
/// Relevant session.
pub session: SessionIndex,
/// The candidate the worker has been spawned for.
pub candidate_hash: CandidateHash,
/// Used receipt.
pub candidate_receipt: CandidateReceipt,
/// Actual result.
pub outcome: ParticipationOutcome,
}
/// Outcome of the validation process.
#[derive(Copy, Clone, Debug)]
pub enum ParticipationOutcome {
/// Candidate was found to be valid.
Valid,
/// Candidate was found to be invalid.
Invalid,
/// Candidate was found to be unavailable.
Unavailable,
/// Something went wrong (bug), details can be found in the logs.
Error,
}
impl ParticipationOutcome {
/// If validation was successful, get whether the candidate was valid or invalid.
pub fn validity(self) -> Option<bool> {
match self {
Self::Valid => Some(true),
Self::Invalid => Some(false),
Self::Unavailable | Self::Error => None,
}
}
}
impl WorkerMessage {
fn from_request(req: ParticipationRequest, outcome: ParticipationOutcome) -> Self {
let session = req.session();
let (candidate_hash, candidate_receipt) = req.into_candidate_info();
Self(ParticipationStatement { session, candidate_hash, candidate_receipt, outcome })
}
}
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
impl Participation {
/// Get ready for managing dispute participation requests.
///
/// The passed in sender will be used by background workers to communicate back their results.
/// The calling context should make sure to call `Participation::on_worker_message()` for the
/// received messages.
pub fn new(sender: WorkerMessageSender, metrics: Metrics) -> Self {
Self {
running_participations: HashSet::new(),
queue: Queues::new(metrics.clone()),
worker_sender: sender,
recent_block: None,
metrics,
}
}
/// Queue a dispute for the node to participate in.
///
/// If capacity is available right now and we already got some relay chain head via
/// `on_active_leaves_update`, the participation will be launched right away.
///
/// Returns: false, if queues are already full.
pub async fn queue_participation<Context>(
&mut self,
ctx: &mut Context,
priority: ParticipationPriority,
mut req: ParticipationRequest,
) -> Result<()> {
// Participation already running - we can ignore that request, discarding its timer:
if self.running_participations.contains(req.candidate_hash()) {
req.discard_timer();
return Ok(());
}
// Available capacity - participate right away (if we already have a recent block):
if let Some((_, h)) = self.recent_block {
if self.running_participations.len() < MAX_PARALLEL_PARTICIPATIONS {
self.fork_participation(ctx, req, h)?;
return Ok(());
}
}
// Out of capacity/no recent block yet - queue:
self.queue.queue(ctx.sender(), priority, req).await
}
/// Message from a worker task was received - get the outcome.
///
/// Call this function to keep participations going and to receive `ParticipationStatement`s.
///
/// This message has to be called for each received worker message, in order to make sure
/// enough participation processes are running at any given time.
///
/// Returns: The received `ParticipationStatement` or a fatal error, in case
/// something went wrong when dequeuing more requests (tasks could not be spawned).
pub async fn get_participation_result<Context>(
&mut self,
ctx: &mut Context,
msg: WorkerMessage,
) -> FatalResult<ParticipationStatement> {
let WorkerMessage(statement) = msg;
self.running_participations.remove(&statement.candidate_hash);
let recent_block = self.recent_block.expect("We never ever reset recent_block to `None` and we already received a result, so it must have been set before. qed.");
self.dequeue_until_capacity(ctx, recent_block.1).await?;
Ok(statement)
}
/// Process active leaves update.
///
/// Make sure we to dequeue participations if that became possible and update most recent
/// block.
pub async fn process_active_leaves_update<Context>(
&mut self,
ctx: &mut Context,
update: &ActiveLeavesUpdate,
) -> FatalResult<()> {
if let Some(activated) = &update.activated {
match self.recent_block {
None => {
self.recent_block = Some((activated.number, activated.hash));
// Work got potentially unblocked:
self.dequeue_until_capacity(ctx, activated.hash).await?;
},
Some((number, _)) if activated.number > number => {
self.recent_block = Some((activated.number, activated.hash));
},
Some(_) => {},
}
}
Ok(())
}
/// Moving any request concerning the given candidates from best-effort to
/// priority, ignoring any candidates that don't have any queued participation requests.
pub async fn bump_to_priority_for_candidates<Context>(
&mut self,
ctx: &mut Context,
included_receipts: &Vec<CandidateReceipt>,
) -> Result<()> {
for receipt in included_receipts {
self.queue.prioritize_if_present(ctx.sender(), receipt).await?;
}
Ok(())
}
/// Dequeue until `MAX_PARALLEL_PARTICIPATIONS` is reached.
async fn dequeue_until_capacity<Context>(
&mut self,
ctx: &mut Context,
recent_head: Hash,
) -> FatalResult<()> {
while self.running_participations.len() < MAX_PARALLEL_PARTICIPATIONS {
if let Some(req) = self.queue.dequeue() {
self.fork_participation(ctx, req, recent_head)?;
} else {
break;
}
}
Ok(())
}
/// Fork a participation task in the background.
fn fork_participation<Context>(
&mut self,
ctx: &mut Context,
req: ParticipationRequest,
recent_head: Hash,
) -> FatalResult<()> {
gum::trace!(
target: LOG_TARGET,
candidate_hash = ?req.candidate_hash(),
session = req.session(),
"Forking participation"
);
let participation_timer = self.metrics.time_participation();
if self.running_participations.insert(*req.candidate_hash()) {
let sender = ctx.sender().clone();
ctx.spawn(
"participation-worker",
participate(
self.worker_sender.clone(),
sender,
recent_head,
req,
participation_timer,
)
.boxed(),
)
.map_err(FatalError::SpawnFailed)?;
}
Ok(())
}
}
async fn participate(
mut result_sender: WorkerMessageSender,
mut sender: impl overseer::DisputeCoordinatorSenderTrait,
block_hash: Hash,
req: ParticipationRequest, // Sends metric data via request_timer field when dropped
_participation_timer: Option<prometheus::HistogramTimer>, // Sends metric data when dropped
) {
#[cfg(test)]
// Hack for tests, so we get recovery messages not too early.
Delay::new(Duration::from_millis(100)).await;
// in order to validate a candidate we need to start by recovering the
// available data
let (recover_available_data_tx, recover_available_data_rx) = oneshot::channel();
sender
.send_message(AvailabilityRecoveryMessage::RecoverAvailableData(
req.candidate_receipt().clone(),
req.session(),
None,
None,
recover_available_data_tx,
))
.await;
let available_data = match recover_available_data_rx.await {
Err(oneshot::Canceled) => {
gum::warn!(
target: LOG_TARGET,
"`Oneshot` got cancelled when recovering available data {:?}",
req.candidate_hash(),
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return;
},
Ok(Ok(data)) => data,
Ok(Err(RecoveryError::Invalid)) => {
gum::debug!(
target: LOG_TARGET,
candidate_hash = ?req.candidate_hash(),
session = req.session(),
"Invalid availability data during participation"
);
// the available data was recovered but it is invalid, therefore we'll
// vote negatively for the candidate dispute
send_result(&mut result_sender, req, ParticipationOutcome::Invalid).await;
return;
},
Ok(Err(RecoveryError::Unavailable)) | Ok(Err(RecoveryError::ChannelClosed)) => {
gum::debug!(
target: LOG_TARGET,
candidate_hash = ?req.candidate_hash(),
session = req.session(),
"Can't fetch availability data in participation"
);
send_result(&mut result_sender, req, ParticipationOutcome::Unavailable).await;
return;
},
};
// we also need to fetch the validation code which we can reference by its
// hash as taken from the candidate descriptor
let validation_code = match get_validation_code_by_hash(
&mut sender,
block_hash,
req.candidate_receipt().descriptor.validation_code_hash(),
)
.await
{
Ok(Some(code)) => code,
Ok(None) => {
gum::warn!(
target: LOG_TARGET,
"Validation code unavailable for code hash {:?} in the state of block {:?}",
req.candidate_receipt().descriptor.validation_code_hash(),
block_hash,
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return;
},
Err(err) => {
gum::warn!(target: LOG_TARGET, ?err, "Error when fetching validation code.");
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return;
},
};
// Issue a request to validate the candidate with the provided exhaustive
// parameters
//
// We use the approval execution timeout because this is intended to
// be run outside of backing and therefore should be subject to the
// same level of leeway.
let (validation_tx, validation_rx) = oneshot::channel();
sender
.send_message(CandidateValidationMessage::ValidateFromExhaustive {
validation_data: available_data.validation_data,
validation_code,
candidate_receipt: req.candidate_receipt().clone(),
pov: available_data.pov,
executor_params: req.executor_params(),
exec_kind: PvfExecKind::Dispute,
response_sender: validation_tx,
})
.await;
// we cast votes (either positive or negative) depending on the outcome of
// the validation and if valid, whether the commitments hash matches
match validation_rx.await {
Err(oneshot::Canceled) => {
gum::warn!(
target: LOG_TARGET,
"`Oneshot` got cancelled when validating candidate {:?}",
req.candidate_hash(),
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return;
},
Ok(Err(err)) => {
gum::warn!(
target: LOG_TARGET,
"Candidate {:?} validation failed with: {:?}",
req.candidate_hash(),
err,
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
},
Ok(Ok(ValidationResult::Invalid(invalid))) => {
gum::warn!(
target: LOG_TARGET,
"Candidate {:?} considered invalid: {:?}",
req.candidate_hash(),
invalid,
);
send_result(&mut result_sender, req, ParticipationOutcome::Invalid).await;
},
Ok(Ok(ValidationResult::Valid(_, _))) => {
send_result(&mut result_sender, req, ParticipationOutcome::Valid).await;
},
}
}
/// Helper function for sending the result back and report any error.
async fn send_result(
sender: &mut WorkerMessageSender,
req: ParticipationRequest,
outcome: ParticipationOutcome,
) {
if let Err(err) = sender.feed(WorkerMessage::from_request(req, outcome)).await {
gum::error!(
target: LOG_TARGET,
?err,
"Sending back participation result failed. Dispute coordinator not working properly!"
);
}
}
@@ -0,0 +1,474 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use std::{
cmp::Ordering,
collections::{btree_map::Entry, BTreeMap},
};
use futures::channel::oneshot;
use pezkuwi_node_subsystem::{messages::ChainApiMessage, overseer};
use pezkuwi_primitives::{
BlockNumber, CandidateHash, CandidateReceiptV2 as CandidateReceipt, ExecutorParams, Hash,
SessionIndex,
};
use crate::{
error::{FatalError, FatalResult, Result},
LOG_TARGET,
};
use crate::metrics::Metrics;
use pezkuwi_node_subsystem_util::metrics::prometheus::prometheus;
#[cfg(test)]
mod tests;
/// How many potential garbage disputes we want to queue, before starting to drop requests.
#[cfg(not(test))]
const BEST_EFFORT_QUEUE_SIZE: usize = 100;
#[cfg(test)]
const BEST_EFFORT_QUEUE_SIZE: usize = 3;
/// How many priority disputes can be queued.
///
/// Once the queue exceeds that size, we will start to drop the newest participation requests in
/// the queue. Note that for each vote import the request will be re-added, if there is free
/// capacity. This limit just serves as a safe guard, it is not expected to ever really be reached.
///
/// For 100 teyrchains, this would allow for every single candidate in 100 blocks on
/// two forks to get disputed, which should be plenty to deal with any realistic attack.
#[cfg(not(test))]
const PRIORITY_QUEUE_SIZE: usize = 20_000;
#[cfg(test)]
const PRIORITY_QUEUE_SIZE: usize = 2;
/// Queues for dispute participation.
/// In both queues we have a strict ordering of candidates and participation will
/// happen in that order. Refer to `CandidateComparator` for details on the ordering.
pub struct Queues {
/// Set of best effort participation requests.
best_effort: BTreeMap<CandidateComparator, ParticipationRequest>,
/// Priority queue.
priority: BTreeMap<CandidateComparator, ParticipationRequest>,
/// Handle for recording queues data in metrics
metrics: Metrics,
}
/// A dispute participation request that can be queued.
#[derive(Debug)]
pub struct ParticipationRequest {
candidate_hash: CandidateHash,
candidate_receipt: CandidateReceipt,
session: SessionIndex,
executor_params: ExecutorParams,
request_timer: Option<prometheus::HistogramTimer>, // Sends metric data when request is dropped
}
/// Whether a `ParticipationRequest` should be put on best-effort or the priority queue.
#[derive(Debug)]
pub enum ParticipationPriority {
BestEffort,
Priority,
}
impl ParticipationPriority {
/// Create `ParticipationPriority` with either `Priority`
///
/// or `BestEffort`.
pub fn with_priority_if(is_priority: bool) -> Self {
if is_priority {
Self::Priority
} else {
Self::BestEffort
}
}
/// Whether or not this is a priority entry.
///
/// If false, it is best effort.
pub fn is_priority(&self) -> bool {
match self {
Self::Priority => true,
Self::BestEffort => false,
}
}
}
/// What can go wrong when queuing a request.
#[derive(Debug, thiserror::Error)]
pub enum QueueError {
#[error("Request could not be queued, because best effort queue was already full.")]
BestEffortFull,
#[error("Request could not be queued, because priority queue was already full.")]
PriorityFull,
}
impl ParticipationRequest {
/// Create a new `ParticipationRequest` to be queued.
pub fn new(
candidate_receipt: CandidateReceipt,
session: SessionIndex,
executor_params: ExecutorParams,
request_timer: Option<prometheus::HistogramTimer>,
) -> Self {
Self {
candidate_hash: candidate_receipt.hash(),
candidate_receipt,
session,
executor_params,
request_timer,
}
}
pub fn candidate_receipt(&'_ self) -> &'_ CandidateReceipt {
&self.candidate_receipt
}
pub fn candidate_hash(&'_ self) -> &'_ CandidateHash {
&self.candidate_hash
}
pub fn session(&self) -> SessionIndex {
self.session
}
pub fn executor_params(&self) -> ExecutorParams {
self.executor_params.clone()
}
pub fn discard_timer(&mut self) {
if let Some(timer) = self.request_timer.take() {
timer.stop_and_discard();
}
}
pub fn into_candidate_info(self) -> (CandidateHash, CandidateReceipt) {
let Self { candidate_hash, candidate_receipt, .. } = self;
(candidate_hash, candidate_receipt)
}
}
// We want to compare and clone participation requests in unit tests, so we
// only implement Eq and Clone for tests.
#[cfg(test)]
impl PartialEq for ParticipationRequest {
fn eq(&self, other: &Self) -> bool {
let ParticipationRequest {
candidate_receipt,
candidate_hash,
session,
executor_params,
request_timer: _,
} = self;
candidate_receipt == other.candidate_receipt() &&
candidate_hash == other.candidate_hash() &&
*session == other.session() &&
executor_params.hash() == other.executor_params.hash()
}
}
#[cfg(test)]
impl Eq for ParticipationRequest {}
impl Queues {
/// Create new `Queues`.
pub fn new(metrics: Metrics) -> Self {
Self { best_effort: BTreeMap::new(), priority: BTreeMap::new(), metrics }
}
/// Will put message in queue, either priority or best effort depending on priority.
///
/// If the message was already previously present on best effort, it will be moved to priority
/// if it is considered priority now.
///
/// Returns error in case a queue was found full already.
pub async fn queue(
&mut self,
sender: &mut impl overseer::DisputeCoordinatorSenderTrait,
priority: ParticipationPriority,
req: ParticipationRequest,
) -> Result<()> {
let comparator = CandidateComparator::new(sender, &req.candidate_receipt).await?;
self.queue_with_comparator(comparator, priority, req)?;
Ok(())
}
/// Get the next best request for dispute participation if any.
/// First the priority queue is considered and then the best effort one.
pub fn dequeue(&mut self) -> Option<ParticipationRequest> {
if let Some(req) = self.pop_priority() {
self.metrics.report_priority_queue_size(self.priority.len() as u64);
return Some(req.1);
}
if let Some(req) = self.pop_best_effort() {
self.metrics.report_best_effort_queue_size(self.best_effort.len() as u64);
return Some(req.1);
}
None
}
/// Reprioritizes any participation requests pertaining to the
/// passed candidates from best effort to priority.
pub async fn prioritize_if_present(
&mut self,
sender: &mut impl overseer::DisputeCoordinatorSenderTrait,
receipt: &CandidateReceipt,
) -> Result<()> {
let comparator = CandidateComparator::new(sender, receipt).await?;
self.prioritize_with_comparator(comparator)?;
Ok(())
}
fn prioritize_with_comparator(
&mut self,
comparator: CandidateComparator,
) -> std::result::Result<(), QueueError> {
if self.priority.len() >= PRIORITY_QUEUE_SIZE {
return Err(QueueError::PriorityFull);
}
if let Some(request) = self.best_effort.remove(&comparator) {
self.priority.insert(comparator, request);
// Report changes to both queue sizes
self.metrics.report_priority_queue_size(self.priority.len() as u64);
self.metrics.report_best_effort_queue_size(self.best_effort.len() as u64);
}
Ok(())
}
/// Will put message in queue, either priority or best effort depending on priority.
///
/// If the message was already previously present on best effort, it will be moved to priority
/// if it is considered priority now.
///
/// Returns error in case a queue was found full already.
///
/// # Request timers
///
/// [`ParticipationRequest`]s contain request timers.
/// Where an old request would be replaced by a new one, we keep the old request.
/// This prevents request timers from resetting on each new request.
fn queue_with_comparator(
&mut self,
comparator: CandidateComparator,
priority: ParticipationPriority,
mut req: ParticipationRequest,
) -> std::result::Result<(), QueueError> {
if priority.is_priority() {
if self.priority.len() >= PRIORITY_QUEUE_SIZE {
return Err(QueueError::PriorityFull);
}
// Remove any best effort entry, using it to replace our new
// request.
if let Some(older_request) = self.best_effort.remove(&comparator) {
req.discard_timer();
req = older_request;
}
// Keeping old request if any.
match self.priority.entry(comparator) {
Entry::Occupied(_) => req.discard_timer(),
Entry::Vacant(vac) => {
gum::trace!(
target: LOG_TARGET,
candidate_hash = ?req.candidate_hash(),
"Added to priority participation queue"
);
vac.insert(req);
},
}
self.metrics.report_priority_queue_size(self.priority.len() as u64);
self.metrics.report_best_effort_queue_size(self.best_effort.len() as u64);
} else {
if self.priority.contains_key(&comparator) {
// The candidate is already in priority queue - don't
// add in in best effort too.
return Ok(());
}
if self.best_effort.len() >= BEST_EFFORT_QUEUE_SIZE {
return Err(QueueError::BestEffortFull);
}
// Keeping old request if any.
match self.best_effort.entry(comparator) {
Entry::Occupied(_) => req.discard_timer(),
Entry::Vacant(vac) => {
gum::trace!(
target: LOG_TARGET,
candidate_hash = ?req.candidate_hash(),
"Added to best effort participation queue"
);
vac.insert(req);
},
}
self.metrics.report_best_effort_queue_size(self.best_effort.len() as u64);
}
Ok(())
}
/// Get best from the best effort queue.
fn pop_best_effort(&mut self) -> Option<(CandidateComparator, ParticipationRequest)> {
return Self::pop_impl(&mut self.best_effort);
}
/// Get best priority queue entry.
fn pop_priority(&mut self) -> Option<(CandidateComparator, ParticipationRequest)> {
return Self::pop_impl(&mut self.priority);
}
// `pop_best_effort` and `pop_priority` do the same but on different `BTreeMap`s. This function
// has the extracted implementation
fn pop_impl(
target: &mut BTreeMap<CandidateComparator, ParticipationRequest>,
) -> Option<(CandidateComparator, ParticipationRequest)> {
// Once https://github.com/rust-lang/rust/issues/62924 is there, we can use a simple:
// target.pop_first().
if let Some((comparator, _)) = target.iter().next() {
let comparator = *comparator;
target
.remove(&comparator)
.map(|participation_request| (comparator, participation_request))
} else {
None
}
}
}
/// `Comparator` for ordering of disputes for candidates.
///
/// This `comparator` makes it possible to order disputes based on age and to ensure some fairness
/// between chains in case of equally old disputes.
///
/// Objective ordering between nodes is important in case of lots disputes, so nodes will pull in
/// the same direction and work on resolving the same disputes first. This ensures that we will
/// conclude some disputes, even if there are lots of them. While any objective ordering would
/// suffice for this goal, ordering by age ensures we are not only resolving disputes, but also
/// resolve the oldest one first, which are also the most urgent and important ones to resolve.
///
/// Note: That by `oldest` we mean oldest in terms of relay chain block number, for any block
/// number that has not yet been finalized. If a block has been finalized already it should be
/// treated as low priority when it comes to disputes, as even in the case of a negative outcome,
/// we are already too late. The ordering mechanism here serves to prevent this from happening in
/// the first place.
#[derive(Copy, Clone)]
#[cfg_attr(test, derive(Debug))]
struct CandidateComparator {
/// Block number of the relay parent. It's wrapped in an `Option<>` because there are cases
/// when it can't be obtained. For example when the node is lagging behind and new leaves are
/// received with a slight delay. Candidates with unknown relay parent are treated with the
/// lowest priority.
///
/// The order enforced by `CandidateComparator` is important because we want to participate in
/// the oldest disputes first.
///
/// Note: In theory it would make more sense to use the `BlockNumber` of the including
/// block, as inclusion time is the actual relevant event when it comes to ordering. The
/// problem is, that a candidate can get included multiple times on forks, so the `BlockNumber`
/// of the including block is not unique. We could theoretically work around that problem, by
/// just using the lowest `BlockNumber` of all available including blocks - the problem is,
/// that is not stable. If a new fork appears after the fact, we would start ordering the same
/// candidate differently, which would result in the same candidate getting queued twice.
relay_parent_block_number: Option<BlockNumber>,
/// By adding the `CandidateHash`, we can guarantee a unique ordering across candidates with
/// the same relay parent block number. Candidates without `relay_parent_block_number` are
/// ordered by the `candidate_hash` (and treated with the lowest priority, as already
/// mentioned).
candidate_hash: CandidateHash,
}
impl CandidateComparator {
/// Create a candidate comparator based on given (fake) values.
///
/// Useful for testing.
#[cfg(test)]
pub fn new_dummy(block_number: Option<BlockNumber>, candidate_hash: CandidateHash) -> Self {
Self { relay_parent_block_number: block_number, candidate_hash }
}
/// Create a candidate comparator for a given candidate.
///
/// Returns:
/// - `Ok(CandidateComparator{Some(relay_parent_block_number), candidate_hash})` when the
/// relay parent can be obtained. This is the happy case.
/// - `Ok(CandidateComparator{None, candidate_hash})` in case the candidate's relay parent
/// can't be obtained.
/// - `FatalError` in case the chain API call fails with an unexpected error.
pub async fn new(
sender: &mut impl overseer::DisputeCoordinatorSenderTrait,
candidate: &CandidateReceipt,
) -> FatalResult<Self> {
let candidate_hash = candidate.hash();
let n = get_block_number(sender, candidate.descriptor().relay_parent()).await?;
if n.is_none() {
gum::warn!(
target: LOG_TARGET,
candidate_hash = ?candidate_hash,
"Candidate's relay_parent could not be found via chain API - `CandidateComparator` \
with an empty relay parent block number will be provided!"
);
}
Ok(CandidateComparator { relay_parent_block_number: n, candidate_hash })
}
}
impl PartialEq for CandidateComparator {
fn eq(&self, other: &CandidateComparator) -> bool {
Ordering::Equal == self.cmp(other)
}
}
impl Eq for CandidateComparator {}
impl PartialOrd for CandidateComparator {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CandidateComparator {
fn cmp(&self, other: &Self) -> Ordering {
return match (self.relay_parent_block_number, other.relay_parent_block_number) {
(None, None) => {
// No relay parents for both -> compare hashes
self.candidate_hash.cmp(&other.candidate_hash)
},
(Some(self_relay_parent_block_num), Some(other_relay_parent_block_num)) => {
match self_relay_parent_block_num.cmp(&other_relay_parent_block_num) {
// if the relay parent is the same for both -> compare hashes
Ordering::Equal => self.candidate_hash.cmp(&other.candidate_hash),
// if not - return the result from comparing the relay parent block numbers
o => return o,
}
},
(Some(_), None) => {
// Candidates with known relay parents are always with priority
Ordering::Less
},
(None, Some(_)) => {
// Ditto
Ordering::Greater
},
};
}
}
async fn get_block_number(
sender: &mut impl overseer::DisputeCoordinatorSenderTrait,
relay_parent: Hash,
) -> FatalResult<Option<BlockNumber>> {
let (tx, rx) = oneshot::channel();
sender.send_message(ChainApiMessage::BlockNumber(relay_parent, tx)).await;
rx.await
.map_err(|_| FatalError::ChainApiSenderDropped)?
.map_err(FatalError::ChainApiAncestors)
}
@@ -0,0 +1,214 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use crate::{metrics::Metrics, ParticipationPriority};
use assert_matches::assert_matches;
use pezkuwi_primitives::{BlockNumber, Hash};
use pezkuwi_primitives_test_helpers::{dummy_candidate_receipt_v2, dummy_hash};
use super::{CandidateComparator, ParticipationRequest, QueueError, Queues};
/// Make a `ParticipationRequest` based on the given commitments hash.
fn make_participation_request(hash: Hash) -> ParticipationRequest {
let mut receipt = dummy_candidate_receipt_v2(dummy_hash());
// make it differ:
receipt.commitments_hash = hash;
let request_timer = Metrics::default().time_participation_pipeline();
ParticipationRequest::new(receipt, 1, Default::default(), request_timer)
}
/// Make dummy comparator for request, based on the given block number.
fn make_dummy_comparator(
req: &ParticipationRequest,
relay_parent: Option<BlockNumber>,
) -> CandidateComparator {
CandidateComparator::new_dummy(relay_parent, *req.candidate_hash())
}
/// Make a partial clone of the given `ParticipationRequest`, just missing
/// the `request_timer` field. We prefer this helper to implementing Clone
/// for `ParticipationRequest`, since we only clone requests in tests.
fn clone_request(request: &ParticipationRequest) -> ParticipationRequest {
ParticipationRequest {
candidate_receipt: request.candidate_receipt.clone(),
candidate_hash: request.candidate_hash,
session: request.session,
executor_params: request.executor_params.clone(),
request_timer: None,
}
}
/// Check that dequeuing acknowledges order.
///
/// Any priority item will be dequeued before any best effort items, priority and best effort with
/// known parent block number items will be processed in order. Best effort items without known
/// parent block number should be treated with lowest priority.
#[test]
fn ordering_works_as_expected() {
let metrics = Metrics::default();
let mut queue = Queues::new(metrics.clone());
let req1 = make_participation_request(Hash::repeat_byte(0x01));
let req_prio = make_participation_request(Hash::repeat_byte(0x02));
let req3 = make_participation_request(Hash::repeat_byte(0x03));
let req_prio_2 = make_participation_request(Hash::repeat_byte(0x04));
let req5_unknown_parent = make_participation_request(Hash::repeat_byte(0x05));
let req_full = make_participation_request(Hash::repeat_byte(0x06));
let req_prio_full = make_participation_request(Hash::repeat_byte(0x07));
queue
.queue_with_comparator(
make_dummy_comparator(&req1, Some(1)),
ParticipationPriority::BestEffort,
clone_request(&req1),
)
.unwrap();
queue
.queue_with_comparator(
make_dummy_comparator(&req_prio, Some(1)),
ParticipationPriority::Priority,
clone_request(&req_prio),
)
.unwrap();
queue
.queue_with_comparator(
make_dummy_comparator(&req3, Some(2)),
ParticipationPriority::BestEffort,
clone_request(&req3),
)
.unwrap();
queue
.queue_with_comparator(
make_dummy_comparator(&req_prio_2, Some(2)),
ParticipationPriority::Priority,
clone_request(&req_prio_2),
)
.unwrap();
queue
.queue_with_comparator(
make_dummy_comparator(&req5_unknown_parent, None),
ParticipationPriority::BestEffort,
clone_request(&req5_unknown_parent),
)
.unwrap();
assert_matches!(
queue.queue_with_comparator(
make_dummy_comparator(&req_prio_full, Some(3)),
ParticipationPriority::Priority,
req_prio_full,
),
Err(QueueError::PriorityFull)
);
assert_matches!(
queue.queue_with_comparator(
make_dummy_comparator(&req_full, Some(3)),
ParticipationPriority::BestEffort,
req_full,
),
Err(QueueError::BestEffortFull)
);
// Prioritized queue is ordered correctly
assert_eq!(queue.dequeue(), Some(req_prio));
assert_eq!(queue.dequeue(), Some(req_prio_2));
// So is the best-effort
assert_eq!(queue.dequeue(), Some(req1));
assert_eq!(queue.dequeue(), Some(req3));
assert_eq!(queue.dequeue(), Some(req5_unknown_parent));
assert_matches!(queue.dequeue(), None);
}
/// No matter how often a candidate gets queued, it should only ever get dequeued once.
#[test]
fn candidate_is_only_dequeued_once() {
let metrics = Metrics::default();
let mut queue = Queues::new(metrics.clone());
let req1 = make_participation_request(Hash::repeat_byte(0x01));
let req_prio = make_participation_request(Hash::repeat_byte(0x02));
let req_best_effort_then_prio = make_participation_request(Hash::repeat_byte(0x03));
let req_prio_then_best_effort = make_participation_request(Hash::repeat_byte(0x04));
queue
.queue_with_comparator(
make_dummy_comparator(&req1, None),
ParticipationPriority::BestEffort,
clone_request(&req1),
)
.unwrap();
queue
.queue_with_comparator(
make_dummy_comparator(&req_prio, Some(1)),
ParticipationPriority::Priority,
clone_request(&req_prio),
)
.unwrap();
// Insert same best effort again:
queue
.queue_with_comparator(
make_dummy_comparator(&req1, None),
ParticipationPriority::BestEffort,
clone_request(&req1),
)
.unwrap();
// insert same prio again:
queue
.queue_with_comparator(
make_dummy_comparator(&req_prio, Some(1)),
ParticipationPriority::Priority,
clone_request(&req_prio),
)
.unwrap();
// Insert first as best effort:
queue
.queue_with_comparator(
make_dummy_comparator(&req_best_effort_then_prio, Some(2)),
ParticipationPriority::BestEffort,
clone_request(&req_best_effort_then_prio),
)
.unwrap();
// Then as prio:
queue
.queue_with_comparator(
make_dummy_comparator(&req_best_effort_then_prio, Some(2)),
ParticipationPriority::Priority,
clone_request(&req_best_effort_then_prio),
)
.unwrap();
// Make space in prio:
assert_eq!(queue.dequeue(), Some(req_prio));
// Insert first as prio:
queue
.queue_with_comparator(
make_dummy_comparator(&req_prio_then_best_effort, Some(3)),
ParticipationPriority::Priority,
clone_request(&req_prio_then_best_effort),
)
.unwrap();
// Then as best effort:
queue
.queue_with_comparator(
make_dummy_comparator(&req_prio_then_best_effort, Some(3)),
ParticipationPriority::BestEffort,
clone_request(&req_prio_then_best_effort),
)
.unwrap();
assert_eq!(queue.dequeue(), Some(req_best_effort_then_prio));
assert_eq!(queue.dequeue(), Some(req_prio_then_best_effort));
assert_eq!(queue.dequeue(), Some(req1));
assert_matches!(queue.dequeue(), None);
}
@@ -0,0 +1,543 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use assert_matches::assert_matches;
use futures::StreamExt;
use pezkuwi_node_subsystem_util::TimeoutExt;
use std::{sync::Arc, time::Duration};
use sp_core::testing::TaskExecutor;
use super::*;
use codec::Encode;
use pezkuwi_node_primitives::{AvailableData, BlockData, InvalidCandidate, PoV};
use pezkuwi_node_subsystem::{
messages::{
AllMessages, ChainApiMessage, DisputeCoordinatorMessage, PvfExecKind, RuntimeApiMessage,
RuntimeApiRequest,
},
ActiveLeavesUpdate, SpawnGlue,
};
use pezkuwi_node_subsystem_test_helpers::{
make_subsystem_context, mock::new_leaf, TestSubsystemContext, TestSubsystemContextHandle,
};
use pezkuwi_primitives::{
BlakeTwo256, CandidateCommitments, HashT, Header, PersistedValidationData, ValidationCode,
};
use pezkuwi_primitives_test_helpers::{
dummy_candidate_commitments, dummy_candidate_receipt_bad_sig, dummy_digest, dummy_hash,
};
type VirtualOverseer = TestSubsystemContextHandle<DisputeCoordinatorMessage>;
pub fn make_our_subsystem_context<S>(
spawner: S,
) -> (
TestSubsystemContext<DisputeCoordinatorMessage, SpawnGlue<S>>,
TestSubsystemContextHandle<DisputeCoordinatorMessage>,
) {
make_subsystem_context(spawner)
}
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
async fn participate<Context>(ctx: &mut Context, participation: &mut Participation) -> Result<()> {
let commitments = CandidateCommitments::default();
participate_with_commitments_hash(ctx, participation, commitments.hash()).await
}
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
async fn participate_with_commitments_hash<Context>(
ctx: &mut Context,
participation: &mut Participation,
commitments_hash: Hash,
) -> Result<()> {
let candidate_receipt = {
let mut receipt = dummy_candidate_receipt_bad_sig(dummy_hash(), dummy_hash());
receipt.commitments_hash = commitments_hash;
receipt
}
.into();
let session = 1;
let request_timer = participation.metrics.time_participation_pipeline();
let req =
ParticipationRequest::new(candidate_receipt, session, Default::default(), request_timer);
participation
.queue_participation(ctx, ParticipationPriority::BestEffort, req)
.await
}
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
async fn activate_leaf<Context>(
ctx: &mut Context,
participation: &mut Participation,
block_number: BlockNumber,
) -> FatalResult<()> {
let block_header = Header {
parent_hash: BlakeTwo256::hash(&block_number.encode()),
number: block_number,
digest: dummy_digest(),
state_root: dummy_hash(),
extrinsics_root: dummy_hash(),
};
let block_hash = block_header.hash();
participation
.process_active_leaves_update(
ctx,
&ActiveLeavesUpdate::start_work(new_leaf(block_hash, block_number)),
)
.await
}
/// Full participation happy path as seen via the overseer.
pub async fn participation_full_happy_path(
ctx_handle: &mut VirtualOverseer,
expected_commitments_hash: Hash,
) {
recover_available_data(ctx_handle).await;
fetch_validation_code(ctx_handle).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive { candidate_receipt, exec_kind, response_sender, .. }
) if exec_kind == PvfExecKind::Dispute => {
if expected_commitments_hash != candidate_receipt.commitments_hash {
response_sender.send(Ok(ValidationResult::Invalid(InvalidCandidate::CommitmentsHashMismatch))).unwrap();
} else {
response_sender.send(Ok(ValidationResult::Valid(dummy_candidate_commitments(None), PersistedValidationData::default()))).unwrap();
}
},
"overseer did not receive candidate validation message",
);
}
/// Full participation with failing availability recovery.
pub async fn participation_missing_availability(ctx_handle: &mut VirtualOverseer) {
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
}
async fn recover_available_data(virtual_overseer: &mut VirtualOverseer) {
let pov_block = PoV { block_data: BlockData(Vec::new()) };
let available_data = AvailableData {
pov: Arc::new(pov_block),
validation_data: PersistedValidationData::default(),
};
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, tx)
) => {
tx.send(Ok(available_data)).unwrap();
},
"overseer did not receive recover available data message",
);
}
/// Handles validation code fetch, returns the received relay parent hash.
async fn fetch_validation_code(virtual_overseer: &mut VirtualOverseer) -> Hash {
let validation_code = ValidationCode(Vec::new());
assert_matches!(
virtual_overseer.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
hash,
RuntimeApiRequest::ValidationCodeByHash(
_,
tx,
)
)) => {
tx.send(Ok(Some(validation_code))).unwrap();
hash
},
"overseer did not receive runtime API request for validation code",
)
}
#[test]
fn same_req_wont_get_queued_if_participation_is_already_running() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
for _ in 0..MAX_PARALLEL_PARTICIPATIONS {
participate(&mut ctx, &mut participation).await.unwrap();
}
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Unavailable => {}
);
// we should not have any further results nor recovery requests:
assert_matches!(ctx_handle.recv().timeout(Duration::from_millis(10)).await, None);
assert_matches!(worker_receiver.next().timeout(Duration::from_millis(10)).await, None);
})
}
#[test]
fn reqs_get_queued_when_out_of_capacity() {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let test = async {
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
for i in 0..MAX_PARALLEL_PARTICIPATIONS {
participate_with_commitments_hash(
&mut ctx,
&mut participation,
Hash::repeat_byte(i as _),
)
.await
.unwrap();
}
for _ in 0..MAX_PARALLEL_PARTICIPATIONS + 1 {
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Unavailable => {}
);
}
// we should not have any further recovery requests:
assert_matches!(worker_receiver.next().timeout(Duration::from_millis(10)).await, None);
};
let request_handler = async {
let mut recover_available_data_msg_count = 0;
let mut block_number_msg_count = 0;
while recover_available_data_msg_count < MAX_PARALLEL_PARTICIPATIONS + 1 ||
block_number_msg_count < 1
{
match ctx_handle.recv().await {
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, tx),
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
recover_available_data_msg_count += 1;
},
AllMessages::ChainApi(ChainApiMessage::BlockNumber(_, tx)) => {
tx.send(Ok(None)).unwrap();
block_number_msg_count += 1;
},
_ => assert!(false, "Received unexpected message"),
}
}
// we should not have any further results
assert_matches!(ctx_handle.recv().timeout(Duration::from_millis(10)).await, None);
};
futures::executor::block_on(async {
futures::join!(test, request_handler);
});
}
#[test]
fn reqs_get_queued_on_no_recent_block() {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (mut unblock_test, mut wait_for_verification) = mpsc::channel(0);
let test = async {
let (sender, _worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
participate(&mut ctx, &mut participation).await.unwrap();
// We have initiated participation but we'll block `active_leaf` so that we can check that
// the participation is queued in race-free way
let _ = wait_for_verification.next().await.unwrap();
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
};
// Responds to messages from the test and verifies its behaviour
let request_handler = async {
// If we receive `BlockNumber` request this implicitly proves that the participation is
// queued
assert_matches!(
ctx_handle.recv().await,
AllMessages::ChainApi(ChainApiMessage::BlockNumber(_, tx)) => {
tx.send(Ok(None)).unwrap();
},
"overseer did not receive `ChainApiMessage::BlockNumber` message",
);
assert!(ctx_handle.recv().timeout(Duration::from_millis(10)).await.is_none());
// No activity so the participation is queued => unblock the test
unblock_test.send(()).await.unwrap();
// after activating at least one leaf the recent block
// state should be available which should lead to trying
// to participate by first trying to recover the available
// data
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(AvailabilityRecoveryMessage::RecoverAvailableData(
..
)),
"overseer did not receive recover available data message",
);
};
futures::executor::block_on(async {
futures::join!(test, request_handler);
});
}
#[test]
fn cannot_participate_if_cannot_recover_available_data() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Unavailable => {}
);
})
}
#[test]
fn cannot_participate_if_cannot_recover_validation_code() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::ValidationCodeByHash(
_,
tx,
)
)) => {
tx.send(Ok(None)).unwrap();
},
"overseer did not receive runtime API request for validation code",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Error => {}
);
})
}
#[test]
fn cast_invalid_vote_if_available_data_is_invalid() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, tx)
) => {
tx.send(Err(RecoveryError::Invalid)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
#[test]
fn cast_invalid_vote_if_validation_fails_or_is_invalid() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive { exec_kind, response_sender, .. }
) if exec_kind == PvfExecKind::Dispute => {
response_sender.send(Ok(ValidationResult::Invalid(InvalidCandidate::Timeout))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
#[test]
fn cast_invalid_vote_if_commitments_dont_match() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive { exec_kind, response_sender, .. }
) if exec_kind == PvfExecKind::Dispute => {
response_sender.send(Ok(ValidationResult::Invalid(InvalidCandidate::CommitmentsHashMismatch))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
#[test]
fn cast_valid_vote_if_validation_passes() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender, Metrics::default());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive { exec_kind, response_sender, .. }
) if exec_kind == PvfExecKind::Dispute => {
response_sender.send(Ok(ValidationResult::Valid(dummy_candidate_commitments(None), PersistedValidationData::default()))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Valid => {}
);
})
}

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