Skip to content

Commit 6f51f6f

Browse files
alindimapepoviola
authored andcommitted
Add availability-recovery from systematic chunks (paritytech#1644)
**Don't look at the commit history, it's confusing, as this branch is based on another branch that was merged** Fixes paritytech#598 Also implements [RFC paritytech#47](polkadot-fellows/RFCs#47) ## Description - Availability-recovery now first attempts to request the systematic chunks for large POVs (which are the first ~n/3 chunks, which can recover the full data without doing the costly reed-solomon decoding process). This has a fallback of recovering from all chunks, if for some reason the process fails. Additionally, backers are also used as a backup for requesting the systematic chunks if the assigned validator is not offering the chunk (each backer is only used for one systematic chunk, to not overload them). - Quite obviously, recovering from systematic chunks is much faster than recovering from regular chunks (4000% faster as measured on my apple M2 Pro). - Introduces a `ValidatorIndex` -> `ChunkIndex` mapping which is different for every core, in order to avoid only querying the first n/3 validators over and over again in the same session. The mapping is the one described in RFC 47. - The mapping is feature-gated by the [NodeFeatures runtime API](paritytech#2177) so that it can only be enabled via a governance call once a sufficient majority of validators have upgraded their client. If the feature is not enabled, the mapping will be the identity mapping and backwards-compatibility will be preserved. - Adds a new chunk request protocol version (v2), which adds the ChunkIndex to the response. This may or may not be checked against the expected chunk index. For av-distribution and systematic recovery, this will be checked, but for regular recovery, no. This is backwards compatible. First, a v2 request is attempted. If that fails during protocol negotiation, v1 is used. - Systematic recovery is only attempted during approval-voting, where we have easy access to the core_index. For disputes and collator pov_recovery, regular chunk requests are used, just as before. ## Performance results Some results from subsystem-bench: with regular chunk recovery: CPU usage per block 39.82s with recovery from backers: CPU usage per block 16.03s with systematic recovery: CPU usage per block 19.07s End-to-end results here: paritytech#598 (comment) #### TODO: - [x] [RFC paritytech#47](polkadot-fellows/RFCs#47) - [x] merge paritytech#2177 and rebase on top of those changes - [x] merge paritytech#2771 and rebase - [x] add tests - [x] preliminary performance measure on Versi: see paritytech#598 (comment) - [x] Rewrite the implementer's guide documentation - [x] paritytech#3065 - [x] paritytech/zombienet#1705 and fix zombienet tests - [x] security audit - [x] final versi test and performance measure --------- Signed-off-by: alindima <[email protected]> Co-authored-by: Javier Viola <[email protected]>
1 parent 645b0f3 commit 6f51f6f

File tree

84 files changed

+7540
-2338
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+7540
-2338
lines changed

.gitlab/pipeline/zombienet.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.zombienet-refs:
22
extends: .build-refs
33
variables:
4-
ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.104"
4+
ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.105"
55
PUSHGATEWAY_URL: "http://zombienet-prometheus-pushgateway.managed-monitoring:9091/metrics/job/zombie-metrics"
66
DEBUG: "zombie,zombie::network-node,zombie::kube::client::logs"
77

.gitlab/pipeline/zombienet/polkadot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,22 @@ zombienet-polkadot-functional-0012-spam-statement-distribution-requests:
183183
--local-dir="${LOCAL_DIR}/functional"
184184
--test="0012-spam-statement-distribution-requests.zndsl"
185185

186+
zombienet-polkadot-functional-0013-systematic-chunk-recovery:
187+
extends:
188+
- .zombienet-polkadot-common
189+
script:
190+
- /home/nonroot/zombie-net/scripts/ci/run-test-local-env-manager.sh
191+
--local-dir="${LOCAL_DIR}/functional"
192+
--test="0013-systematic-chunk-recovery.zndsl"
193+
194+
zombienet-polkadot-functional-0014-chunk-fetching-network-compatibility:
195+
extends:
196+
- .zombienet-polkadot-common
197+
script:
198+
- /home/nonroot/zombie-net/scripts/ci/run-test-local-env-manager.sh
199+
--local-dir="${LOCAL_DIR}/functional"
200+
--test="0014-chunk-fetching-network-compatibility.zndsl"
201+
186202
zombienet-polkadot-smoke-0001-parachains-smoke-test:
187203
extends:
188204
- .zombienet-polkadot-common

Cargo.lock

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cumulus/client/pov-recovery/src/active_candidate_recovery.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ impl<Block: BlockT> ActiveCandidateRecovery<Block> {
5656
candidate.receipt.clone(),
5757
candidate.session_index,
5858
None,
59+
None,
5960
tx,
6061
),
6162
"ActiveCandidateRecovery",

cumulus/client/relay-chain-minimal-node/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,5 +285,8 @@ fn build_request_response_protocol_receivers<
285285
let cfg =
286286
Protocol::ChunkFetchingV1.get_outbound_only_config::<_, Network>(request_protocol_names);
287287
config.add_request_response_protocol(cfg);
288+
let cfg =
289+
Protocol::ChunkFetchingV2.get_outbound_only_config::<_, Network>(request_protocol_names);
290+
config.add_request_response_protocol(cfg);
288291
(collation_req_v1_receiver, collation_req_v2_receiver, available_data_req_receiver)
289292
}

cumulus/test/service/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,16 @@ impl RecoveryHandle for FailingRecoveryHandle {
152152
message: AvailabilityRecoveryMessage,
153153
origin: &'static str,
154154
) {
155-
let AvailabilityRecoveryMessage::RecoverAvailableData(ref receipt, _, _, _) = message;
155+
let AvailabilityRecoveryMessage::RecoverAvailableData(ref receipt, _, _, _, _) = message;
156156
let candidate_hash = receipt.hash();
157157

158158
// For every 3rd block we immediately signal unavailability to trigger
159159
// a retry. The same candidate is never failed multiple times to ensure progress.
160160
if self.counter % 3 == 0 && self.failed_hashes.insert(candidate_hash) {
161161
tracing::info!(target: LOG_TARGET, ?candidate_hash, "Failing pov recovery.");
162162

163-
let AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, back_sender) = message;
163+
let AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, _, back_sender) =
164+
message;
164165
back_sender
165166
.send(Err(RecoveryError::Unavailable))
166167
.expect("Return channel should work here.");

polkadot/erasure-coding/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ sp-trie = { path = "../../substrate/primitives/trie" }
1919
thiserror = { workspace = true }
2020

2121
[dev-dependencies]
22+
quickcheck = { version = "1.0.3", default-features = false }
2223
criterion = { version = "0.5.1", default-features = false, features = ["cargo_bench_support"] }
2324

2425
[[bench]]

polkadot/erasure-coding/benches/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ cargo bench
77
## `scaling_with_validators`
88

99
This benchmark evaluates the performance of constructing the chunks and the erasure root from PoV and
10-
reconstructing the PoV from chunks. You can see the results of running this bench on 5950x below.
10+
reconstructing the PoV from chunks (either from systematic chunks or regular chunks).
11+
You can see the results of running this bench on 5950x below (only including recovery from regular chunks).
1112
Interestingly, with `10_000` chunks (validators) its slower than with `50_000` for both construction
1213
and reconstruction.
1314
```
@@ -37,3 +38,6 @@ reconstruct/10000 time: [496.35 ms 505.17 ms 515.42 ms]
3738
reconstruct/50000 time: [276.56 ms 277.53 ms 278.58 ms]
3839
thrpt: [17.948 MiB/s 18.016 MiB/s 18.079 MiB/s]
3940
```
41+
42+
Results from running on an Apple M2 Pro, systematic recovery is generally 40 times faster than
43+
regular recovery, achieving 1 Gib/s.

polkadot/erasure-coding/benches/scaling_with_validators.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@ fn construct_and_reconstruct_5mb_pov(c: &mut Criterion) {
5353
}
5454
group.finish();
5555

56-
let mut group = c.benchmark_group("reconstruct");
56+
let mut group = c.benchmark_group("reconstruct_regular");
5757
for n_validators in N_VALIDATORS {
5858
let all_chunks = chunks(n_validators, &pov);
5959

60-
let mut c: Vec<_> = all_chunks.iter().enumerate().map(|(i, c)| (&c[..], i)).collect();
61-
let last_chunks = c.split_off((c.len() - 1) * 2 / 3);
60+
let chunks: Vec<_> = all_chunks
61+
.iter()
62+
.enumerate()
63+
.take(polkadot_erasure_coding::recovery_threshold(n_validators).unwrap())
64+
.map(|(i, c)| (&c[..], i))
65+
.collect();
6266

6367
group.throughput(Throughput::Bytes(pov.len() as u64));
6468
group.bench_with_input(
@@ -67,7 +71,31 @@ fn construct_and_reconstruct_5mb_pov(c: &mut Criterion) {
6771
|b, &n| {
6872
b.iter(|| {
6973
let _pov: Vec<u8> =
70-
polkadot_erasure_coding::reconstruct(n, last_chunks.clone()).unwrap();
74+
polkadot_erasure_coding::reconstruct(n, chunks.clone()).unwrap();
75+
});
76+
},
77+
);
78+
}
79+
group.finish();
80+
81+
let mut group = c.benchmark_group("reconstruct_systematic");
82+
for n_validators in N_VALIDATORS {
83+
let all_chunks = chunks(n_validators, &pov);
84+
85+
let chunks = all_chunks
86+
.into_iter()
87+
.take(polkadot_erasure_coding::systematic_recovery_threshold(n_validators).unwrap())
88+
.collect::<Vec<_>>();
89+
90+
group.throughput(Throughput::Bytes(pov.len() as u64));
91+
group.bench_with_input(
92+
BenchmarkId::from_parameter(n_validators),
93+
&n_validators,
94+
|b, &n| {
95+
b.iter(|| {
96+
let _pov: Vec<u8> =
97+
polkadot_erasure_coding::reconstruct_from_systematic(n, chunks.clone())
98+
.unwrap();
7199
});
72100
},
73101
);

polkadot/erasure-coding/src/lib.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ pub enum Error {
6969
/// Bad payload in reconstructed bytes.
7070
#[error("Reconstructed payload invalid")]
7171
BadPayload,
72+
/// Unable to decode reconstructed bytes.
73+
#[error("Unable to decode reconstructed payload: {0}")]
74+
Decode(#[source] parity_scale_codec::Error),
7275
/// Invalid branch proof.
7376
#[error("Invalid branch proof")]
7477
InvalidBranchProof,
@@ -110,6 +113,14 @@ pub const fn recovery_threshold(n_validators: usize) -> Result<usize, Error> {
110113
Ok(needed + 1)
111114
}
112115

116+
/// Obtain the threshold of systematic chunks that should be enough to recover the data.
117+
///
118+
/// If the regular `recovery_threshold` is a power of two, then it returns the same value.
119+
/// Otherwise, it returns the next lower power of two.
120+
pub fn systematic_recovery_threshold(n_validators: usize) -> Result<usize, Error> {
121+
code_params(n_validators).map(|params| params.k())
122+
}
123+
113124
fn code_params(n_validators: usize) -> Result<CodeParams, Error> {
114125
// we need to be able to reconstruct from 1/3 - eps
115126

@@ -127,6 +138,41 @@ fn code_params(n_validators: usize) -> Result<CodeParams, Error> {
127138
})
128139
}
129140

141+
/// Reconstruct the v1 available data from the set of systematic chunks.
142+
///
143+
/// Provide a vector containing chunk data. If too few chunks are provided, recovery is not
144+
/// possible.
145+
pub fn reconstruct_from_systematic_v1(
146+
n_validators: usize,
147+
chunks: Vec<Vec<u8>>,
148+
) -> Result<AvailableData, Error> {
149+
reconstruct_from_systematic(n_validators, chunks)
150+
}
151+
152+
/// Reconstruct the available data from the set of systematic chunks.
153+
///
154+
/// Provide a vector containing the first k chunks in order. If too few chunks are provided,
155+
/// recovery is not possible.
156+
pub fn reconstruct_from_systematic<T: Decode>(
157+
n_validators: usize,
158+
chunks: Vec<Vec<u8>>,
159+
) -> Result<T, Error> {
160+
let code_params = code_params(n_validators)?;
161+
let k = code_params.k();
162+
163+
for chunk_data in chunks.iter().take(k) {
164+
if chunk_data.len() % 2 != 0 {
165+
return Err(Error::UnevenLength)
166+
}
167+
}
168+
169+
let bytes = code_params.make_encoder().reconstruct_from_systematic(
170+
chunks.into_iter().take(k).map(|data| WrappedShard::new(data)).collect(),
171+
)?;
172+
173+
Decode::decode(&mut &bytes[..]).map_err(|err| Error::Decode(err))
174+
}
175+
130176
/// Obtain erasure-coded chunks for v1 `AvailableData`, one for each validator.
131177
///
132178
/// Works only up to 65536 validators, and `n_validators` must be non-zero.
@@ -285,13 +331,41 @@ pub fn branch_hash(root: &H256, branch_nodes: &Proof, index: usize) -> Result<H2
285331

286332
#[cfg(test)]
287333
mod tests {
334+
use std::sync::Arc;
335+
288336
use super::*;
289337
use polkadot_node_primitives::{AvailableData, BlockData, PoV};
338+
use polkadot_primitives::{HeadData, PersistedValidationData};
339+
use quickcheck::{Arbitrary, Gen, QuickCheck};
290340

291341
// In order to adequately compute the number of entries in the Merkle
292342
// trie, we must account for the fixed 16-ary trie structure.
293343
const KEY_INDEX_NIBBLE_SIZE: usize = 4;
294344

345+
#[derive(Clone, Debug)]
346+
struct ArbitraryAvailableData(AvailableData);
347+
348+
impl Arbitrary for ArbitraryAvailableData {
349+
fn arbitrary(g: &mut Gen) -> Self {
350+
// Limit the POV len to 1 mib, otherwise the test will take forever
351+
let pov_len = (u32::arbitrary(g) % (1024 * 1024)).max(2);
352+
353+
let pov = (0..pov_len).map(|_| u8::arbitrary(g)).collect();
354+
355+
let pvd = PersistedValidationData {
356+
parent_head: HeadData((0..u16::arbitrary(g)).map(|_| u8::arbitrary(g)).collect()),
357+
relay_parent_number: u32::arbitrary(g),
358+
relay_parent_storage_root: [u8::arbitrary(g); 32].into(),
359+
max_pov_size: u32::arbitrary(g),
360+
};
361+
362+
ArbitraryAvailableData(AvailableData {
363+
pov: Arc::new(PoV { block_data: BlockData(pov) }),
364+
validation_data: pvd,
365+
})
366+
}
367+
}
368+
295369
#[test]
296370
fn field_order_is_right_size() {
297371
assert_eq!(MAX_VALIDATORS, 65536);
@@ -318,6 +392,25 @@ mod tests {
318392
assert_eq!(reconstructed, available_data);
319393
}
320394

395+
#[test]
396+
fn round_trip_systematic_works() {
397+
fn property(available_data: ArbitraryAvailableData, n_validators: u16) {
398+
let n_validators = n_validators.max(2);
399+
let kpow2 = systematic_recovery_threshold(n_validators as usize).unwrap();
400+
let chunks = obtain_chunks(n_validators as usize, &available_data.0).unwrap();
401+
assert_eq!(
402+
reconstruct_from_systematic_v1(
403+
n_validators as usize,
404+
chunks.into_iter().take(kpow2).collect()
405+
)
406+
.unwrap(),
407+
available_data.0
408+
);
409+
}
410+
411+
QuickCheck::new().quickcheck(property as fn(ArbitraryAvailableData, u16))
412+
}
413+
321414
#[test]
322415
fn reconstruct_does_not_panic_on_low_validator_count() {
323416
let reconstructed = reconstruct_v1(1, [].iter().cloned());

0 commit comments

Comments
 (0)