feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
doc
|
||||
**/target
|
||||
@@ -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
|
||||
@@ -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
@@ -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>.
|
||||
|
||||
@@ -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).
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 CLI’s 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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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"]
|
||||
@@ -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::*;
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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);
|
||||
@@ -0,0 +1,3 @@
|
||||
hfuzz_target/
|
||||
hfuzz_workspace/
|
||||
Cargo.lock
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/>.
|
||||
@@ -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(|¶_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();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
]
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
]
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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(×tamp) {
|
||||
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(¤t_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(×tamp.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(¤t_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
Reference in New Issue
Block a user