From c586f850674517975e2c97b9e2a61f6eca25bdf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Fran=C3=A7a?= Date: Thu, 21 Nov 2024 01:10:21 +0000 Subject: [PATCH] fix: Fix high vote on informal spec (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ - Fixed high_vote on informal spec. - Added comments to explain reasoning behind ignoring 2+ subquorums. - Added link to blog post that explains why rounds are with 5f+1 fault tolerance. --- node/Cargo.lock | 104 ++++++++++++++++++------------------ spec/README.md | 2 +- spec/informal-spec/types.rs | 29 ++++++++-- 3 files changed, 78 insertions(+), 57 deletions(-) diff --git a/node/Cargo.lock b/node/Cargo.lock index 9d9bb9fa..c9b74ebd 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -206,9 +206,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +checksum = "fe7c2840b66236045acd2607d5866e274380afd87ef99d6226e961e2cb47df45" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -218,9 +218,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +checksum = "ad3a619a9de81e1d7de1f1186dcba4506ed661a0e483d84410fdef0ee87b2f96" dependencies = [ "bindgen 0.69.5", "cc", @@ -440,9 +440,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -555,9 +555,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -565,9 +565,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -589,9 +589,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "cmake" @@ -1235,9 +1235,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -1443,9 +1443,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -1472,7 +1472,7 @@ dependencies = [ "futures-util", "headers", "http 1.1.0", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-rustls", "hyper-util", "pin-project-lite", @@ -1490,7 +1490,7 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "log", "rustls", @@ -1507,7 +1507,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "pin-project-lite", "tokio", @@ -1525,7 +1525,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.5.1", "pin-project-lite", "socket2", "tokio", @@ -1757,9 +1757,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "7a73e9fe3c49d7afb2ace819fa181a287ce54a0983eda4e0eb05c22f82ffe534" [[package]] name = "jni" @@ -1884,7 +1884,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-rustls", "hyper-util", "jsonrpsee-core", @@ -1911,7 +1911,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -2005,7 +2005,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-http-proxy", "hyper-rustls", "hyper-timeout", @@ -2101,9 +2101,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libloading" @@ -2975,9 +2975,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", @@ -2988,9 +2988,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" dependencies = [ "aws-lc-rs", "log", @@ -3099,9 +3099,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -3232,9 +3232,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -3485,7 +3485,7 @@ dependencies = [ [[package]] name = "tester" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "clap", @@ -3689,9 +3689,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "base64 0.22.1", "bitflags 2.6.0", @@ -3799,9 +3799,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" @@ -4268,7 +4268,7 @@ dependencies = [ [[package]] name = "zksync_concurrency" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "assert_matches", @@ -4286,7 +4286,7 @@ dependencies = [ [[package]] name = "zksync_consensus_bft" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "assert_matches", @@ -4310,7 +4310,7 @@ dependencies = [ [[package]] name = "zksync_consensus_crypto" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "blst", @@ -4330,7 +4330,7 @@ dependencies = [ [[package]] name = "zksync_consensus_executor" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "async-trait", @@ -4352,7 +4352,7 @@ dependencies = [ [[package]] name = "zksync_consensus_network" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "assert_matches", @@ -4362,7 +4362,7 @@ dependencies = [ "bytesize", "http-body-util", "human-repr", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "im", "once_cell", @@ -4390,7 +4390,7 @@ dependencies = [ [[package]] name = "zksync_consensus_roles" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "assert_matches", @@ -4411,7 +4411,7 @@ dependencies = [ [[package]] name = "zksync_consensus_storage" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "assert_matches", @@ -4433,7 +4433,7 @@ dependencies = [ [[package]] name = "zksync_consensus_tools" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "async-trait", @@ -4468,7 +4468,7 @@ dependencies = [ [[package]] name = "zksync_consensus_utils" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "rand", @@ -4478,7 +4478,7 @@ dependencies = [ [[package]] name = "zksync_protobuf" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "bit-vec", @@ -4500,7 +4500,7 @@ dependencies = [ [[package]] name = "zksync_protobuf_build" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "heck", diff --git a/spec/README.md b/spec/README.md index 4380820a..7ebd974e 100644 --- a/spec/README.md +++ b/spec/README.md @@ -25,7 +25,7 @@ We find that most recent research on consensus algorithms unfortunately has beco For our particular use case, there are a few lessons that we learned from researching and implementing previous consensus algorithms: - Chained consensus is not worth it. It doesn’t improve the throughput or the latency while increasing systemic complexity. We always finalize every block. -- Lower fault tolerance to reduce voting rounds. This we learned from FaB Paxos. Decreasing our fault tolerance from *3f+1* to *5f+1* allows us to finalize in just one voting round. +- Lower fault tolerance to reduce voting rounds. This we learned from FaB Paxos. Decreasing our fault tolerance from *3f+1* to *5f+1* allows us to finalize in just one voting round. This [blog post from Decentralized Thoughts](https://decentralizedthoughts.github.io/2021-03-03-2-round-bft-smr-with-n-equals-4-f-equals-1/) explains intuitively how this works. - Linear communication is not worth it. Quadratic communication for replicas simplifies security (there are fewer cases where we need to consider the effect of a malicious leader), implementation (you can fully separate the leader component) and view changes (constant timeouts are enough, [Jolteon/Ditto](https://arxiv.org/abs/2106.10362) ended up going in that direction after trying to implement HotStuff). Further, the performance drop is likely not significant (see [ParBFT](https://eprint.iacr.org/2023/679.pdf)). - Re-proposals as a way of guaranteeing that there are no “rogue” blocks. This is a problem that didn’t get any attention so far (as far as we know), and is probably somewhat unique to public blockchains. The issue is that in all committee-based consensus algorithms it is possible that a commit QC (to use HotStuff’s terminology) is formed but that not enough replicas receive it. This will cause a timeout and another block to be proposed. Most algorithms just solve this by saying that the old block is no longer valid. All honest replicas will be in agreement about which block is canonical, but someone who just receives that single block and is not aware of the timeout will think that that particular block was finalized. This breaks the very desirable property of being able to verify that a given block is part of the chain just from seeing the block, without being required to have the entire chain. The way we solve this is to require that block proposals after a timeout (where a commit QC might have been formed) re-propose the previous block. This guarantees that if we see a block with a valid commit QC, then that block is part of the chain (maybe it wasn’t finalized in that particular view, but it was certainly finalized). - Always justify messages to remove time dependencies. That’s something we got from Fast-HotStuff. Messages should have enough information by themselves that any replica is capable of verifying their validity without any other information (with the exception of having previous blocks, but that’s external to the consensus algorithm anyway). If we don’t, then we introduce subtle timing dependencies. For example, Tendermint had a bug that was only discovered years later, where the solution was that the leader had to wait for the maximum network delay at the end of every round. If that wait doesn’t happen, a lock can occur. Funnily enough, Hotstuff-2 reintroduces this timing dependency in order to get rid of one round-trip, which significantly worsens the difficulty of modelling and implementing such a system. diff --git a/spec/informal-spec/types.rs b/spec/informal-spec/types.rs index 65989df8..d8074005 100644 --- a/spec/informal-spec/types.rs +++ b/spec/informal-spec/types.rs @@ -241,10 +241,12 @@ impl TimeoutQC { let map: Map = Map::new(); for (pk, vote) in votes { - if map.exists(vote) { - map.get_value(vote).add(get_weight(pk)); - } else { - map.insert(vote, get_weight(pk)); + if let Some(commit_vote) = vote.high_vote { + if map.exists(commit_vote) { + map.get_value(commit_vote).add(get_weight(pk)); + } else { + map.insert(commit_vote, get_weight(pk)); + } } } @@ -256,6 +258,25 @@ impl TimeoutQC { } } + // We only consider there's a high vote if exactly one subquorum has been found. + // If there are 0 or 2+ subquorums, we consider there's no high vote. + // + // For the case of 0 subquorums, we consider there's no high vote because + // if the previous proposal received n-f votes (thus being finalized) then the + // intersection of those n-f nodes with the n-f nodes that voted in this timeout + // would have at least n-3f *honest* nodes that committed to the previous proposal + // (f honest nodes that didn't commit in the previous round, f honest nodes that didn't + // timeout this round, and f dishonest nodes that did timeout this round), and thus we + // would have a subquorum. + // + // For the case of 2 subquorums, we consider there's no high vote because if we have + // n-3f votes for one block and n-3f votes for another block, for a total of 2n-6f votes, + // then there not enough remaining votes for any of the blocks to be finalized. Note that + // we have a total of n+f votes (one vote for each node, plus an extra one from each of the + // dishonest nodes since they can vote twice). So we have n+f - (2n-6f) = 7f-n, since n>=5f+1 + // we have 7f-5f-1 = 2f-1, so there's at most 2f-1 votes remaining, which is not enough to + // finalize any block. + // For >2 subquorums, the argument is similar. if subquorums.len() == 1 { Some(subquorums[0]) } else {