diff --git a/.ci/devnet_ci.sh b/.ci/devnet_ci.sh index 40a7364e5a..93cca16d3e 100755 --- a/.ci/devnet_ci.sh +++ b/.ci/devnet_ci.sh @@ -47,7 +47,7 @@ trap child_exit_handler CHLD # Define a trap handler that prints a message when an error occurs trap 'echo "⛔️ Error in $BASH_SOURCE at line $LINENO: \"$BASH_COMMAND\" failed (exit $?)"' ERR -# Flags used by all ndoes +# Flags used by all nodes. common_flags=( --nodisplay --nobanner --noupdater "--network=$network_id" --verbosity=1 "--dev-num-validators=$total_validators" @@ -81,11 +81,12 @@ for ((validator_index = 0; validator_index < total_validators; validator_index++ sleep 1 done -# Start all client nodes in the background +# Start all client nodes in the background. for ((client_index = 0; client_index < total_clients; client_index++)); do + # compute the absolute index for this node. node_index=$((client_index + total_validators)) - snarkos clean --dev $node_index + snarkos clean "--dev=$node_index" "--network=$network_id" log_file="$log_dir/client-$client_index.log" snarkos start "${common_flags[@]}" "--dev=$node_index" \ diff --git a/Cargo.lock b/Cargo.lock index b2c39d3a25..9e79af2138 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,22 +153,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -297,7 +297,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "itoa", "matchit 0.8.4", @@ -393,7 +393,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -556,9 +556,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bzip2-sys" @@ -587,9 +587,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.45" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -627,7 +627,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1423,9 +1423,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" @@ -1946,9 +1946,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -1974,7 +1974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "rustls", "rustls-pki-types", @@ -1990,7 +1990,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -2018,7 +2018,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -2028,9 +2028,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -2039,7 +2039,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.8.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", @@ -2392,7 +2392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2910,7 +2910,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3476,7 +3476,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -3787,7 +3787,7 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d832c086ece0dacc29fb2947bb4219b8f6e12fe9e40b7108f9e57c4224e47b5c" dependencies = [ - "hyper 1.8.0", + "hyper 1.8.1", "indicatif", "log", "quick-xml", @@ -4458,6 +4458,7 @@ dependencies = [ "snarkos-node-metrics", "snarkos-node-network", "snarkos-node-router", + "snarkos-node-sync", "snarkos-node-sync-communication-service", "snarkos-node-sync-locators", "snarkos-node-tcp", @@ -4506,7 +4507,7 @@ dependencies = [ [[package]] name = "snarkvm" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "dotenvy", @@ -4529,7 +4530,7 @@ dependencies = [ [[package]] name = "snarkvm-algorithms" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -4557,7 +4558,7 @@ dependencies = [ [[package]] name = "snarkvm-algorithms-cuda" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "blst", "cc", @@ -4568,7 +4569,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -4582,7 +4583,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-account" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-network", "snarkvm-circuit-types", @@ -4592,7 +4593,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-algorithms" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-types", "snarkvm-console-algorithms", @@ -4602,7 +4603,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-collections" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-types", @@ -4612,7 +4613,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "indexmap 2.12.0", "itertools 0.14.0", @@ -4630,12 +4631,12 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment-witness" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" [[package]] name = "snarkvm-circuit-network" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -4646,7 +4647,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-program" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -4660,7 +4661,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-address", @@ -4675,7 +4676,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-address" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -4688,7 +4689,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-boolean" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-console-types-boolean", @@ -4697,7 +4698,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-field" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -4707,7 +4708,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-group" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -4719,7 +4720,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-integers" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -4731,7 +4732,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-scalar" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -4742,7 +4743,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-string" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -4754,7 +4755,7 @@ dependencies = [ [[package]] name = "snarkvm-console" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-account", "snarkvm-console-algorithms", @@ -4767,7 +4768,7 @@ dependencies = [ [[package]] name = "snarkvm-console-account" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "bs58", "snarkvm-console-network", @@ -4778,7 +4779,7 @@ dependencies = [ [[package]] name = "snarkvm-console-algorithms" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "blake2s_simd", "hex", @@ -4793,7 +4794,7 @@ dependencies = [ [[package]] name = "snarkvm-console-collections" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "rayon", @@ -4804,7 +4805,7 @@ dependencies = [ [[package]] name = "snarkvm-console-network" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "enum-iterator", @@ -4824,7 +4825,7 @@ dependencies = [ [[package]] name = "snarkvm-console-network-environment" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "bech32", @@ -4842,7 +4843,7 @@ dependencies = [ [[package]] name = "snarkvm-console-program" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "enum-iterator", "enum_index", @@ -4863,7 +4864,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-address", @@ -4878,7 +4879,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-address" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -4889,7 +4890,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-boolean" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", ] @@ -4897,7 +4898,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-field" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -4907,7 +4908,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-group" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -4918,7 +4919,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-integers" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -4929,7 +4930,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-scalar" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -4940,7 +4941,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-string" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -4951,7 +4952,7 @@ dependencies = [ [[package]] name = "snarkvm-curves" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "rand 0.8.5", "rayon", @@ -4965,7 +4966,7 @@ dependencies = [ [[package]] name = "snarkvm-fields" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -4982,7 +4983,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5012,7 +5013,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-authority" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "rand 0.8.5", @@ -5024,7 +5025,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-block" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "indexmap 2.12.0", @@ -5046,7 +5047,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-committee" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "indexmap 2.12.0", @@ -5065,7 +5066,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-ledger-narwhal-batch-certificate", "snarkvm-ledger-narwhal-batch-header", @@ -5078,7 +5079,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-certificate" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "indexmap 2.12.0", "rayon", @@ -5091,7 +5092,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-header" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "indexmap 2.12.0", "rayon", @@ -5104,7 +5105,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-data" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "bytes", "serde_json", @@ -5115,7 +5116,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-subdag" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "indexmap 2.12.0", "rayon", @@ -5130,7 +5131,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "bytes", "serde_json", @@ -5143,7 +5144,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission-id" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "snarkvm-console", "snarkvm-ledger-puzzle", @@ -5152,7 +5153,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5172,7 +5173,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle-epoch" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5195,7 +5196,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-query" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "anyhow", "async-trait", @@ -5212,7 +5213,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-store" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std-storage", "anyhow", @@ -5240,7 +5241,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-test-helpers" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5258,7 +5259,7 @@ dependencies = [ [[package]] name = "snarkvm-metrics" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "metrics", ] @@ -5266,7 +5267,7 @@ dependencies = [ [[package]] name = "snarkvm-parameters" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5289,7 +5290,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5323,7 +5324,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-process" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "colored 3.0.0", @@ -5348,7 +5349,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-program" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "enum-iterator", "indexmap 2.12.0", @@ -5367,7 +5368,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-snark" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "bincode", "serde_json", @@ -5380,7 +5381,7 @@ dependencies = [ [[package]] name = "snarkvm-utilities" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "aleo-std", "anyhow", @@ -5403,7 +5404,7 @@ dependencies = [ [[package]] name = "snarkvm-utilities-derives" version = "4.3.0" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=6bec0e0e#6bec0e0e165f7604afa69bce0fc383d50bed9577" +source = "git+https://github.com/ProvableHQ/snarkVM.git?branch=fix%2Frestore-pending-blocks#24467a208bbb64af1af01909ac855e6c8a18284a" dependencies = [ "proc-macro2", "quote 1.0.42", @@ -5980,7 +5981,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-timeout", "hyper-util", "percent-encoding", @@ -6529,9 +6530,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -6556,12 +6557,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -6570,22 +6565,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -6594,16 +6580,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -6612,7 +6589,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6648,7 +6625,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6673,7 +6650,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", diff --git a/Cargo.toml b/Cargo.toml index cb746d94a3..2dd12612f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,8 +47,9 @@ default-features = false [workspace.dependencies.snarkvm] #path = "../snarkVM" git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "6bec0e0e" +#rev = "6bec0e0e" #version = "=4.3.0" +branch="fix/restore-pending-blocks" default-features = false [workspace.dependencies.anyhow] diff --git a/cli/src/helpers/logger.rs b/cli/src/helpers/logger.rs index bf3465bc33..3142d59092 100644 --- a/cli/src/helpers/logger.rs +++ b/cli/src/helpers/logger.rs @@ -107,7 +107,9 @@ fn parse_log_filter(filter_str: &str) -> Result { } /// Sets the log filter based on the given verbosity level. +/// Initializes the logger with the specified verbosity level, where 0 is the lowest verbosity and 6 the highest. /// +/// The following shows what messages are enabled at each level. /// ```ignore /// 0 => info /// 1 => info, debug diff --git a/node/bft/Cargo.toml b/node/bft/Cargo.toml index 25e79280a3..1685b570ff 100644 --- a/node/bft/Cargo.toml +++ b/node/bft/Cargo.toml @@ -185,7 +185,7 @@ features = [ "test" ] [dev-dependencies.snarkos-node-sync] workspace = true -features = [ "test" ] +features = [ "test-helpers" ] [dev-dependencies.test-strategy] workspace = true diff --git a/node/bft/events/Cargo.toml b/node/bft/events/Cargo.toml index c6a6793183..83f47a7ad1 100644 --- a/node/bft/events/Cargo.toml +++ b/node/bft/events/Cargo.toml @@ -65,4 +65,4 @@ workspace = true [dev-dependencies.snarkos-node-sync-locators] workspace = true -features = [ "test" ] +features = [ "test-helpers" ] diff --git a/node/bft/events/src/block_locators.rs b/node/bft/events/src/block_locators.rs new file mode 100644 index 0000000000..4c5d984104 --- /dev/null +++ b/node/bft/events/src/block_locators.rs @@ -0,0 +1,78 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkOS library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{BlockLocators, EventTrait, FromBytes, IoResult, Network, ToBytes}; + +use std::{ + borrow::Cow, + io::{Read, Write}, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlockLocatorsRequest { + pub start_height: u32, + pub end_height: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlockLocatorsResponse { + pub locators: BlockLocators, +} + +impl EventTrait for BlockLocatorsRequest { + /// Returns the event name. + #[inline] + fn name(&self) -> Cow<'static, str> { + "BlockLocatorsRequest".into() + } +} + +impl FromBytes for BlockLocatorsRequest { + fn read_le(mut reader: R) -> IoResult { + let start_height = u32::read_le(&mut reader)?; + let end_height = u32::read_le(&mut reader)?; + + Ok(Self { start_height, end_height }) + } +} + +impl ToBytes for BlockLocatorsRequest { + fn write_le(&self, mut writer: W) -> IoResult<()> { + self.start_height.write_le(&mut writer)?; + self.end_height.write_le(&mut writer)?; + Ok(()) + } +} + +impl EventTrait for BlockLocatorsResponse { + /// Returns the event name. + #[inline] + fn name(&self) -> Cow<'static, str> { + "BlockLocatorsResponse".into() + } +} + +impl FromBytes for BlockLocatorsResponse { + fn read_le(mut reader: R) -> IoResult { + let locators = BlockLocators::read_le(&mut reader)?; + Ok(Self { locators }) + } +} + +impl ToBytes for BlockLocatorsResponse { + fn write_le(&self, mut writer: W) -> IoResult<()> { + self.locators.write_le(&mut writer) + } +} diff --git a/node/bft/events/src/lib.rs b/node/bft/events/src/lib.rs index 5a116f5b3f..10212c48bd 100644 --- a/node/bft/events/src/lib.rs +++ b/node/bft/events/src/lib.rs @@ -66,6 +66,9 @@ pub use validators_response::ValidatorsResponse; mod worker_ping; pub use worker_ping::WorkerPing; +mod block_locators; +pub use block_locators::{BlockLocatorsRequest, BlockLocatorsResponse}; + use snarkos_node_sync_locators::BlockLocators; use snarkvm::{ console::prelude::{FromBytes, Network, Read, ToBytes, Write, error, io_error}, @@ -90,7 +93,6 @@ pub trait EventTrait: ToBytes + FromBytes { #[derive(Clone, Debug, PartialEq, Eq)] // TODO (howardwu): For mainnet - Remove this clippy lint. The CertificateResponse should not // be a large enum variant, after removing the versioning. -#[allow(clippy::large_enum_variant)] pub enum Event { BatchPropose(BatchPropose), BatchSignature(BatchSignature), @@ -108,6 +110,8 @@ pub enum Event { ValidatorsRequest(ValidatorsRequest), ValidatorsResponse(ValidatorsResponse), WorkerPing(WorkerPing), + BlockLocatorsRequest(BlockLocatorsRequest), + BlockLocatorsResponse(BlockLocatorsResponse), } impl From for Event { @@ -140,6 +144,8 @@ impl Event { Self::ValidatorsRequest(event) => event.name(), Self::ValidatorsResponse(event) => event.name(), Self::WorkerPing(event) => event.name(), + Self::BlockLocatorsRequest(event) => event.name(), + Self::BlockLocatorsResponse(event) => event.name(), } } @@ -163,6 +169,8 @@ impl Event { Self::ValidatorsRequest(..) => 13, Self::ValidatorsResponse(..) => 14, Self::WorkerPing(..) => 15, + Self::BlockLocatorsRequest(..) => 16, + Self::BlockLocatorsResponse(..) => 17, } } } @@ -188,6 +196,8 @@ impl ToBytes for Event { Self::ValidatorsRequest(event) => event.write_le(writer), Self::ValidatorsResponse(event) => event.write_le(writer), Self::WorkerPing(event) => event.write_le(writer), + Self::BlockLocatorsRequest(event) => event.write_le(writer), + Self::BlockLocatorsResponse(event) => event.write_le(writer), } } } @@ -215,7 +225,9 @@ impl FromBytes for Event { 13 => Self::ValidatorsRequest(ValidatorsRequest::read_le(&mut reader)?), 14 => Self::ValidatorsResponse(ValidatorsResponse::read_le(&mut reader)?), 15 => Self::WorkerPing(WorkerPing::read_le(&mut reader)?), - 16.. => return Err(error(format!("Unknown event ID {id}"))), + 16 => Self::BlockLocatorsRequest(BlockLocatorsRequest::read_le(&mut reader)?), + 17 => Self::BlockLocatorsResponse(BlockLocatorsResponse::read_le(&mut reader)?), + 18.. => return Err(error(format!("Unknown event ID {id}"))), }; // Ensure that there are no "dangling" bytes. diff --git a/node/bft/events/src/primary_ping.rs b/node/bft/events/src/primary_ping.rs index 76f90fe2c4..ac7de0e4fe 100644 --- a/node/bft/events/src/primary_ping.rs +++ b/node/bft/events/src/primary_ping.rs @@ -18,25 +18,21 @@ use super::*; #[derive(Clone, Debug, PartialEq, Eq)] pub struct PrimaryPing { pub version: u32, - pub block_locators: BlockLocators, + pub current_block_height: u32, pub primary_certificate: Data>, } impl PrimaryPing { /// Initializes a new ping event. - pub const fn new( - version: u32, - block_locators: BlockLocators, - primary_certificate: Data>, - ) -> Self { - Self { version, block_locators, primary_certificate } + pub const fn new(version: u32, current_block_height: u32, primary_certificate: Data>) -> Self { + Self { version, current_block_height, primary_certificate } } } -impl From<(u32, BlockLocators, BatchCertificate)> for PrimaryPing { +impl From<(u32, u32, BatchCertificate)> for PrimaryPing { /// Initializes a new ping event. - fn from((version, block_locators, primary_certificate): (u32, BlockLocators, BatchCertificate)) -> Self { - Self::new(version, block_locators, Data::Object(primary_certificate)) + fn from((version, current_block_height, primary_certificate): (u32, u32, BatchCertificate)) -> Self { + Self::new(version, current_block_height, Data::Object(primary_certificate)) } } @@ -50,11 +46,8 @@ impl EventTrait for PrimaryPing { impl ToBytes for PrimaryPing { fn write_le(&self, mut writer: W) -> IoResult<()> { - // Write the version. self.version.write_le(&mut writer)?; - // Write the block locators. - self.block_locators.write_le(&mut writer)?; - // Write the primary certificate. + self.current_block_height.write_le(&mut writer)?; self.primary_certificate.write_le(&mut writer)?; Ok(()) @@ -66,19 +59,18 @@ impl FromBytes for PrimaryPing { // Read the version. let version = u32::read_le(&mut reader)?; // Read the block locators. - let block_locators = BlockLocators::read_le(&mut reader)?; + let current_block_height = u32::read_le(&mut reader)?; // Read the primary certificate. let primary_certificate = Data::read_le(&mut reader)?; // Return the ping event. - Ok(Self::new(version, block_locators, primary_certificate)) + Ok(Self::new(version, current_block_height, primary_certificate)) } } #[cfg(test)] pub mod prop_tests { use crate::{PrimaryPing, certificate_response::prop_tests::any_batch_certificate}; - use snarkos_node_sync_locators::{BlockLocators, test_helpers::sample_block_locators}; use snarkvm::utilities::{FromBytes, ToBytes}; use bytes::{Buf, BufMut, BytesMut}; @@ -87,14 +79,10 @@ pub mod prop_tests { type CurrentNetwork = snarkvm::prelude::MainnetV0; - pub fn any_block_locators() -> BoxedStrategy> { - any::().prop_map(sample_block_locators).boxed() - } - pub fn any_primary_ping() -> BoxedStrategy> { - (any::(), any_block_locators(), any_batch_certificate()) - .prop_map(|(version, block_locators, batch_certificate)| { - PrimaryPing::from((version, block_locators, batch_certificate.clone())) + (any::(), any::(), any_batch_certificate()) + .prop_map(|(version, current_block_height, batch_certificate)| { + PrimaryPing::from((version, current_block_height, batch_certificate.clone())) }) .boxed() } @@ -105,7 +93,7 @@ pub mod prop_tests { primary_ping.write_le(&mut bytes).unwrap(); let decoded = PrimaryPing::::read_le(&mut bytes.into_inner().reader()).unwrap(); assert_eq!(primary_ping.version, decoded.version); - assert_eq!(primary_ping.block_locators, decoded.block_locators); + assert_eq!(primary_ping.current_block_height, decoded.current_block_height); assert_eq!( primary_ping.primary_certificate.deserialize_blocking().unwrap(), decoded.primary_certificate.deserialize_blocking().unwrap(), diff --git a/node/bft/ledger-service/src/ledger.rs b/node/bft/ledger-service/src/ledger.rs index 503d911c0f..7b84f31feb 100644 --- a/node/bft/ledger-service/src/ledger.rs +++ b/node/bft/ledger-service/src/ledger.rs @@ -16,8 +16,10 @@ use crate::{LedgerService, fmt_id, spawn_blocking}; use snarkvm::{ ledger::{ + Block, Ledger, - block::{Block, Transaction}, + PendingBlock, + Transaction, committee::Committee, narwhal::{BatchCertificate, Data, Subdag, Transmission, TransmissionID}, puzzle::{Solution, SolutionID}, @@ -355,6 +357,14 @@ impl> LedgerService for CoreLedgerService< spawn_blocking!(ledger.check_transaction_basic(&transaction, None, &mut rand::thread_rng())) } + fn check_block_subdag(&self, block: Block, prefix: &[PendingBlock]) -> Result> { + self.ledger.check_block_subdag(block, prefix) + } + + fn check_block_content(&self, block: PendingBlock) -> Result> { + self.ledger.check_block_content(block, &mut rand::thread_rng()) + } + /// Checks the given block is valid next block. fn check_next_block(&self, block: &Block) -> Result<()> { self.ledger.check_next_block(block, &mut rand::thread_rng()) diff --git a/node/bft/ledger-service/src/mock.rs b/node/bft/ledger-service/src/mock.rs index a34e28d1e6..a0c335f2a5 100644 --- a/node/bft/ledger-service/src/mock.rs +++ b/node/bft/ledger-service/src/mock.rs @@ -16,7 +16,9 @@ use crate::{LedgerService, fmt_id}; use snarkvm::{ ledger::{ - block::{Block, Transaction}, + Block, + PendingBlock, + Transaction, committee::Committee, narwhal::{BatchCertificate, Data, Subdag, Transmission, TransmissionID}, puzzle::{Solution, SolutionID}, @@ -211,6 +213,14 @@ impl LedgerService for MockLedgerService { Ok(()) } + fn check_block_subdag(&self, _block: Block, _prefix: &[PendingBlock]) -> Result> { + unimplemented!(); + } + + fn check_block_content(&self, _block: PendingBlock) -> Result> { + unimplemented!(); + } + /// Checks the given block is valid next block. fn check_next_block(&self, _block: &Block) -> Result<()> { Ok(()) diff --git a/node/bft/ledger-service/src/prover.rs b/node/bft/ledger-service/src/prover.rs index 02854f3682..b496c58c95 100644 --- a/node/bft/ledger-service/src/prover.rs +++ b/node/bft/ledger-service/src/prover.rs @@ -16,7 +16,9 @@ use crate::LedgerService; use snarkvm::{ ledger::{ - block::{Block, Transaction}, + Block, + PendingBlock, + Transaction, committee::Committee, narwhal::{BatchCertificate, Data, Subdag, Transmission, TransmissionID}, puzzle::{Solution, SolutionID}, @@ -166,6 +168,14 @@ impl LedgerService for ProverLedgerService { Ok(()) } + fn check_block_subdag(&self, _block: Block, _prefix: &[PendingBlock]) -> Result> { + bail!("Cannot check block subDAG in prover") + } + + fn check_block_content(&self, _pending_block: PendingBlock) -> Result> { + bail!("Cannot check block content in prover") + } + /// Checks the given block is valid next block. fn check_next_block(&self, _block: &Block) -> Result<()> { Ok(()) diff --git a/node/bft/ledger-service/src/traits.rs b/node/bft/ledger-service/src/traits.rs index 842fb19f6f..72e2fb25ee 100644 --- a/node/bft/ledger-service/src/traits.rs +++ b/node/bft/ledger-service/src/traits.rs @@ -15,15 +15,21 @@ use snarkvm::{ ledger::{ - block::{Block, Transaction}, + Block, + PendingBlock, + Transaction, committee::Committee, - narwhal::{BatchCertificate, Data, Subdag, Transmission, TransmissionID}, + narwhal::{BatchCertificate, Data, Transmission, TransmissionID}, puzzle::{Solution, SolutionID}, }, prelude::{Address, ConsensusVersion, Field, Network, Result}, }; +#[cfg(feature = "ledger-write")] use indexmap::IndexMap; +#[cfg(feature = "ledger-write")] +use snarkvm::ledger::narwhal::Subdag; + use std::{fmt::Debug, ops::Range}; #[async_trait] @@ -106,6 +112,12 @@ pub trait LedgerService: Debug + Send + Sync { transaction: Transaction, ) -> Result<()>; + /// Checks that the subDAG in a given block is valid, but does not fully verify the block. + fn check_block_subdag(&self, block: Block, prefix: &[PendingBlock]) -> Result>; + + /// Takes a pending block and performs the remaining checks to full verify it. + fn check_block_content(&self, _block: PendingBlock) -> Result>; + /// Checks the given block is valid next block. fn check_next_block(&self, block: &Block) -> Result<()>; diff --git a/node/bft/ledger-service/src/translucent.rs b/node/bft/ledger-service/src/translucent.rs index 7c3a5fcd26..f741935b69 100644 --- a/node/bft/ledger-service/src/translucent.rs +++ b/node/bft/ledger-service/src/translucent.rs @@ -18,8 +18,10 @@ use async_trait::async_trait; use indexmap::IndexMap; use snarkvm::{ ledger::{ + Block, Ledger, - block::{Block, Transaction}, + PendingBlock, + Transaction, committee::Committee, narwhal::{Data, Subdag, Transmission, TransmissionID}, puzzle::{Solution, SolutionID}, @@ -177,6 +179,14 @@ impl> LedgerService for TranslucentLedgerS Ok(()) } + fn check_block_subdag(&self, _block: Block, _prefix: &[PendingBlock]) -> Result> { + unimplemented!(); + } + + fn check_block_content(&self, _block: PendingBlock) -> Result> { + unimplemented!(); + } + /// Always succeeds. fn check_next_block(&self, _block: &Block) -> Result<()> { Ok(()) diff --git a/node/bft/src/bft.rs b/node/bft/src/bft.rs index 742b9732eb..7cd26cc8ea 100644 --- a/node/bft/src/bft.rs +++ b/node/bft/src/bft.rs @@ -747,7 +747,7 @@ impl BFT { } info!( - "\n\nCommitting a subdag from round {anchor_round} with {num_transmissions} transmissions: {subdag_metadata:?}\n" + "\n\nCommitting a subDAG with anchor round {anchor_round} and {num_transmissions} transmissions: {subdag_metadata:?} (syncing={IS_SYNCING})\n", ); } diff --git a/node/bft/src/gateway.rs b/node/bft/src/gateway.rs index 091219ed01..a984ad5a13 100644 --- a/node/bft/src/gateway.rs +++ b/node/bft/src/gateway.rs @@ -27,6 +27,8 @@ use crate::{ use aleo_std::StorageMode; use snarkos_account::Account; use snarkos_node_bft_events::{ + BlockLocatorsRequest, + BlockLocatorsResponse, BlockRequest, BlockResponse, CertificateRequest, @@ -70,6 +72,7 @@ use snarkvm::{ prelude::{Address, Field}, }; +use anyhow::anyhow; use colored::Colorize; use futures::SinkExt; use indexmap::IndexMap; @@ -337,6 +340,11 @@ impl CommunicationService for Gateway { Event::BlockRequest(BlockRequest { start_height, end_height }) } + /// Prepare a block locators request to be sent + fn prepare_block_locators_request(start_height: u32, end_height: u32) -> Self::Message { + Event::BlockLocatorsRequest(BlockLocatorsRequest { start_height, end_height }) + } + /// Sends the given message to specified peer. /// /// This function returns as soon as the message is queued to be sent, @@ -689,7 +697,7 @@ impl Gateway { Ok(false) } Event::PrimaryPing(ping) => { - let PrimaryPing { version, block_locators, primary_certificate } = ping; + let PrimaryPing { version, current_block_height, primary_certificate } = ping; // Ensure the event version is not outdated. if version < Event::::VERSION { @@ -699,7 +707,7 @@ impl Gateway { // Update the peer locators. Except for some tests, there is always a sync sender. if let Some(sync_sender) = self.sync_sender.get() { // Check the block locators are valid, and update the validators in the sync module. - if let Err(error) = sync_sender.update_peer_locators(peer_ip, block_locators).await { + if let Err(error) = sync_sender.update_peer_block_height(peer_ip, current_block_height).await { bail!("Validator '{peer_ip}' sent invalid block locators - {error}"); } } @@ -811,6 +819,30 @@ impl Gateway { } Ok(true) } + Event::BlockLocatorsRequest(BlockLocatorsRequest { start_height, end_height }) => { + ensure!(start_height < end_height, "Invalid block locators range"); + + let Some(sync_sender) = self.sync_sender.get() else { + bail!("Cannot process block locators request: no sync sender"); + }; + + let locators = sync_sender.get_block_locators(start_height, end_height).await?; + let event = Event::BlockLocatorsResponse(BlockLocatorsResponse { locators }); + + let Some(result) = Transport::send(self, peer_ip, event).await else { + bail!("Failed to send block locator response to peer {peer_ip}"); + }; + + result.await?.map(|_| true).map_err(|err| anyhow!("Send failed: {err}")) + } + Event::BlockLocatorsResponse(BlockLocatorsResponse { locators }) => { + if let Some(sync_sender) = self.sync_sender.get() { + sync_sender.update_peer_block_locators(peer_ip, locators).await? + } else { + bail!("Cannot update peer block locators: no sync sender"); + } + Ok(true) + } } } diff --git a/node/bft/src/helpers/channels.rs b/node/bft/src/helpers/channels.rs index 5cca4ddddf..13d776ed38 100644 --- a/node/bft/src/helpers/channels.rs +++ b/node/bft/src/helpers/channels.rs @@ -234,21 +234,47 @@ pub struct SyncSender { pub tx_block_sync_insert_block_response: mpsc::Sender<(SocketAddr, Vec>, Option, oneshot::Sender>)>, pub tx_block_sync_remove_peer: mpsc::Sender, - pub tx_block_sync_update_peer_locators: mpsc::Sender<(SocketAddr, BlockLocators, oneshot::Sender>)>, + pub tx_block_sync_get_block_locators: mpsc::Sender<(u32, u32, oneshot::Sender>>)>, + pub tx_block_sync_update_peer_block_height: mpsc::Sender<(SocketAddr, u32, oneshot::Sender>)>, + pub tx_block_sync_update_peer_block_locators: + mpsc::Sender<(SocketAddr, BlockLocators, oneshot::Sender>)>, pub tx_certificate_request: mpsc::Sender<(SocketAddr, CertificateRequest)>, pub tx_certificate_response: mpsc::Sender<(SocketAddr, CertificateResponse)>, } impl SyncSender { + pub async fn get_block_locators(&self, start: u32, end: u32) -> Result> { + let (callback_sender, callback_receiver) = oneshot::channel(); + self.tx_block_sync_get_block_locators.send((start, end, callback_sender)).await?; + callback_receiver.await? + } + /// Sends the request to update the peer locators. - pub async fn update_peer_locators(&self, peer_ip: SocketAddr, block_locators: BlockLocators) -> Result<()> { + pub async fn update_peer_block_height(&self, peer_ip: SocketAddr, block_height: u32) -> Result<()> { + // Initialize a callback sender and receiver. + let (callback_sender, callback_receiver) = oneshot::channel(); + // Send the request to update the peer block height. + // This `tx_block_sync_update_peer_block_height.send()` call + // causes the `rx_block_sync_update_peer_block_height.recv()` call + // in one of the loops in [`Sync::run()`] to return. + self.tx_block_sync_update_peer_block_height.send((peer_ip, block_height, callback_sender)).await?; + // Await the callback to continue. + callback_receiver.await? + } + + /// Sends the request to update the peer locators. + pub async fn update_peer_block_locators( + &self, + peer_ip: SocketAddr, + block_locators: BlockLocators, + ) -> Result<()> { // Initialize a callback sender and receiver. let (callback_sender, callback_receiver) = oneshot::channel(); - // Send the request to update the peer locators. + // Send the request to update the peer block locators. // This `tx_block_sync_update_peer_locators.send()` call // causes the `rx_block_sync_update_peer_locators.recv()` call // in one of the loops in [`Sync::run()`] to return. - self.tx_block_sync_update_peer_locators.send((peer_ip, block_locators, callback_sender)).await?; + self.tx_block_sync_update_peer_block_locators.send((peer_ip, block_locators, callback_sender)).await?; // Await the callback to continue. callback_receiver.await? } @@ -279,7 +305,10 @@ pub struct SyncReceiver { pub rx_block_sync_insert_block_response: mpsc::Receiver<(SocketAddr, Vec>, Option, oneshot::Sender>)>, pub rx_block_sync_remove_peer: mpsc::Receiver, - pub rx_block_sync_update_peer_locators: mpsc::Receiver<(SocketAddr, BlockLocators, oneshot::Sender>)>, + pub rx_block_sync_get_block_locators: mpsc::Receiver<(u32, u32, oneshot::Sender>>)>, + pub rx_block_sync_update_peer_block_height: mpsc::Receiver<(SocketAddr, u32, oneshot::Sender>)>, + pub rx_block_sync_update_peer_block_locators: + mpsc::Receiver<(SocketAddr, BlockLocators, oneshot::Sender>)>, pub rx_certificate_request: mpsc::Receiver<(SocketAddr, CertificateRequest)>, pub rx_certificate_response: mpsc::Receiver<(SocketAddr, CertificateResponse)>, } @@ -288,21 +317,29 @@ pub struct SyncReceiver { pub fn init_sync_channels() -> (SyncSender, SyncReceiver) { let (tx_block_sync_insert_block_response, rx_block_sync_insert_block_response) = mpsc::channel(MAX_CHANNEL_SIZE); let (tx_block_sync_remove_peer, rx_block_sync_remove_peer) = mpsc::channel(MAX_CHANNEL_SIZE); - let (tx_block_sync_update_peer_locators, rx_block_sync_update_peer_locators) = mpsc::channel(MAX_CHANNEL_SIZE); + let (tx_block_sync_get_block_locators, rx_block_sync_get_block_locators) = mpsc::channel(MAX_CHANNEL_SIZE); + let (tx_block_sync_update_peer_block_height, rx_block_sync_update_peer_block_height) = + mpsc::channel(MAX_CHANNEL_SIZE); + let (tx_block_sync_update_peer_block_locators, rx_block_sync_update_peer_block_locators) = + mpsc::channel(MAX_CHANNEL_SIZE); let (tx_certificate_request, rx_certificate_request) = mpsc::channel(MAX_CHANNEL_SIZE); let (tx_certificate_response, rx_certificate_response) = mpsc::channel(MAX_CHANNEL_SIZE); let sender = SyncSender { tx_block_sync_insert_block_response, tx_block_sync_remove_peer, - tx_block_sync_update_peer_locators, + tx_block_sync_get_block_locators, + tx_block_sync_update_peer_block_height, + tx_block_sync_update_peer_block_locators, tx_certificate_request, tx_certificate_response, }; let receiver = SyncReceiver { rx_block_sync_insert_block_response, rx_block_sync_remove_peer, - rx_block_sync_update_peer_locators, + rx_block_sync_get_block_locators, + rx_block_sync_update_peer_block_height, + rx_block_sync_update_peer_block_locators, rx_certificate_request, rx_certificate_response, }; diff --git a/node/bft/src/helpers/signed_proposals.rs b/node/bft/src/helpers/signed_proposals.rs index 547296f7a8..349cf036b1 100644 --- a/node/bft/src/helpers/signed_proposals.rs +++ b/node/bft/src/helpers/signed_proposals.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use snarkos_node_sync::locators::NUM_RECENT_BLOCKS; +use snarkos_node_sync::locators::MAX_LOCATOR_SIZE; use snarkvm::{ console::{ account::{Address, Signature}, @@ -65,9 +65,9 @@ impl FromBytes for SignedProposals { let max_certificates = N::LATEST_MAX_CERTIFICATES() .map_err(|e| error(format!("Failed to extract the maximum number of certificates: {e}")))?; // Ensure the number of signed proposals is within bounds - if num_signed_proposals as usize > max_certificates as usize * NUM_RECENT_BLOCKS { + if num_signed_proposals as usize > max_certificates as usize * MAX_LOCATOR_SIZE { return Err(error(format!( - "Number of signed proposals ({num_signed_proposals}) is greater than the maximum ({max_certificates} * {NUM_RECENT_BLOCKS})", + "Number of signed proposals ({num_signed_proposals}) is greater than the maximum ({max_certificates} * {MAX_LOCATOR_SIZE})", ))); } // Deserialize the signed proposals. diff --git a/node/bft/src/primary.rs b/node/bft/src/primary.rs index b7bfca7516..3af4b736fe 100644 --- a/node/bft/src/primary.rs +++ b/node/bft/src/primary.rs @@ -1242,14 +1242,14 @@ impl Primary { tokio::time::sleep(Duration::from_millis(PRIMARY_PING_IN_MS)).await; // Retrieve the block locators. - let self__ = self_.clone(); - let block_locators = match spawn_blocking!(self__.sync.get_block_locators()) { + /* let self__ = self_.clone(); + let block_locators = match spawn_blocking!(self__.sync.get_block_locators()) { Ok(block_locators) => block_locators, Err(e) => { warn!("Failed to retrieve block locators - {e}"); continue; } - }; + };*/ // Retrieve the latest certificate of the primary. let primary_certificate = { @@ -1284,7 +1284,8 @@ impl Primary { }; // Construct the primary ping. - let primary_ping = PrimaryPing::from((>::VERSION, block_locators, primary_certificate)); + let primary_ping = + PrimaryPing::from((>::VERSION, self_.ledger.latest_block_height(), primary_certificate)); // Broadcast the event. self_.gateway.broadcast(Event::PrimaryPing(primary_ping)); } @@ -2474,7 +2475,11 @@ mod tests { // The primary will only consider itself synced if we received // block locators from a peer. - primary.sync.testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0)).unwrap(); + primary + .sync + .testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0, 0)) + .await + .unwrap(); primary.sync.testing_only_try_block_sync_testing_only().await; // Try to process the batch proposal from the peer, should succeed. @@ -2512,7 +2517,11 @@ mod tests { primary.gateway.resolver().write().insert_peer(peer_ip, peer_ip, Some(peer_account.1.address())); // Add a high block locator to indicate we are not synced. - primary.sync.testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(20)).unwrap(); + primary + .sync + .testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0, 20)) + .await + .unwrap(); // Try to process the batch proposal from the peer, should fail assert!( @@ -2553,7 +2562,11 @@ mod tests { // The primary will only consider itself synced if we received // block locators from a peer. - primary.sync.testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0)).unwrap(); + primary + .sync + .testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0, 0)) + .await + .unwrap(); primary.sync.testing_only_try_block_sync_testing_only().await; // Try to process the batch proposal from the peer, should succeed. @@ -2738,11 +2751,19 @@ mod tests { primary_v5.gateway.resolver().write().insert_peer(peer_ip, peer_ip, Some(peer_account.1.address())); // primary v4 must be considered synced. - primary_v4.sync.testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0)).unwrap(); + primary_v4 + .sync + .testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0, 0)) + .await + .unwrap(); primary_v4.sync.testing_only_try_block_sync_testing_only().await; // primary v5 must be ocnsidered synced. - primary_v5.sync.testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0)).unwrap(); + primary_v5 + .sync + .testing_only_update_peer_locators_testing_only(peer_ip, sample_block_locators(0, 0)) + .await + .unwrap(); primary_v5.sync.testing_only_try_block_sync_testing_only().await; // Check the spend limit is enforced from V5 onwards. diff --git a/node/bft/src/sync/mod.rs b/node/bft/src/sync/mod.rs index 4d84da0028..452272f98f 100644 --- a/node/bft/src/sync/mod.rs +++ b/node/bft/src/sync/mod.rs @@ -17,12 +17,11 @@ use crate::{ Gateway, MAX_FETCH_TIMEOUT_IN_MS, Transport, - events::DataBlocks, + events::{CertificateRequest, CertificateResponse, DataBlocks, Event}, helpers::{BFTSender, Pending, Storage, SyncReceiver, fmt_id, max_redundant_requests}, + ledger_service::LedgerService, spawn_blocking, }; -use snarkos_node_bft_events::{CertificateRequest, CertificateResponse, Event}; -use snarkos_node_bft_ledger_service::LedgerService; use snarkos_node_network::PeerPoolHandling; use snarkos_node_sync::{BLOCK_REQUEST_BATCH_DELAY, BlockSync, Ping, PrepareSyncRequest, locators::BlockLocators}; @@ -31,12 +30,12 @@ use snarkvm::{ network::{ConsensusVersion, Network}, types::Field, }, - ledger::{authority::Authority, block::Block, narwhal::BatchCertificate}, + ledger::{PendingBlock, authority::Authority, block::Block, narwhal::BatchCertificate}, prelude::{cfg_into_iter, cfg_iter}, - utilities::flatten_error, + utilities::{ensure_equals, flatten_error}, }; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail, ensure}; use indexmap::IndexMap; #[cfg(feature = "locktick")] use locktick::{parking_lot::Mutex, tokio::Mutex as TMutex}; @@ -45,7 +44,7 @@ use parking_lot::Mutex; #[cfg(not(feature = "serial"))] use rayon::prelude::*; use std::{ - collections::{BTreeMap, HashMap}, + collections::{HashMap, VecDeque}, future::Future, net::SocketAddr, sync::Arc, @@ -92,12 +91,12 @@ pub struct Sync { sync_lock: Arc>, /// The latest block responses. /// - /// This is used in [`Sync::sync_storage_with_block()`] to accumulate blocks whose addition to the ledger is - /// deferred until certain checks pass. + /// This is used in [`Sync::sync_storage_with_block()`] to accumulate blocks + /// whose addition to the ledger is deferred until certain checks pass. /// Blocks need to be processed in order, hence a BTree map. /// /// Whenever a new block is added to this map, BlockSync::set_sync_height needs to be called. - latest_block_responses: Arc>>>, + pending_blocks: Arc>>>, } impl Sync { @@ -123,7 +122,7 @@ impl Sync { handles: Default::default(), response_lock: Default::default(), sync_lock: Default::default(), - latest_block_responses: Default::default(), + pending_blocks: Default::default(), } } @@ -197,7 +196,17 @@ impl Sync { let _ = tokio::time::timeout(Self::MAX_SYNC_INTERVAL, self_.block_sync.wait_for_block_responses()).await; - self_.try_advancing_block_synchronization(&ping).await; + let ping = ping.clone(); + let self_ = self_.clone(); + let hdl = tokio::spawn(async move { + self_.try_advancing_block_synchronization(&ping).await; + }); + + if let Err(err) = hdl.await + && let Ok(panic) = err.try_into_panic() + { + error!("Sync block advancement panicked: {panic:?}"); + } // We perform no additional rate limiting here as // requests are already rate-limited. @@ -226,7 +235,9 @@ impl Sync { let SyncReceiver { mut rx_block_sync_insert_block_response, mut rx_block_sync_remove_peer, - mut rx_block_sync_update_peer_locators, + mut rx_block_sync_get_block_locators, + mut rx_block_sync_update_peer_block_height, + mut rx_block_sync_update_peer_block_locators, mut rx_certificate_request, mut rx_certificate_response, } = sync_receiver; @@ -242,7 +253,22 @@ impl Sync { while let Some((peer_ip, blocks, latest_consensus_version, callback)) = rx_block_sync_insert_block_response.recv().await { - callback.send(self_.insert_block_response(peer_ip, blocks, latest_consensus_version).await).ok(); + let result = self_.insert_block_response(peer_ip, blocks, latest_consensus_version).await; + //TODO remove this once channels are gone + if let Err(err) = &result { + warn!("Failed to insret block response: {err:?}"); + } + + callback.send(result).ok(); + } + }); + + // Fetch block locators for a specified range. + let self_ = self.clone(); + self.spawn(async move { + while let Some((start, end, callback)) = rx_block_sync_get_block_locators.recv().await { + let result = self_.block_sync.get_block_locators(start, end); + callback.send(result).ok(); } }); @@ -262,10 +288,22 @@ impl Sync { // which causes the `rx_block_sync_update_peer_locators.recv()` call below to return. let self_ = self.clone(); self.spawn(async move { - while let Some((peer_ip, locators, callback)) = rx_block_sync_update_peer_locators.recv().await { - let self_clone = self_.clone(); + while let Some((peer_ip, block_locators, callback)) = rx_block_sync_update_peer_block_locators.recv().await + { + let self_ = self_.clone(); tokio::spawn(async move { - callback.send(self_clone.update_peer_locators(peer_ip, locators)).ok(); + callback.send(self_.update_peer_block_locators(peer_ip, block_locators).await).ok(); + }); + } + }); + + let self_ = self.clone(); + self.spawn(async move { + while let Some((peer_ip, peer_height, callback)) = rx_block_sync_update_peer_block_height.recv().await { + let self_ = self_.clone(); + tokio::spawn(async move { + let result = self_.update_peer_block_height(peer_ip, peer_height); + callback.send(result).ok(); }); } }); @@ -331,7 +369,7 @@ impl Sync { // Prepare the block requests, if any. // In the process, we update the state of `is_block_synced` for the sync module. - let (requests, sync_peers) = self.block_sync.prepare_block_requests(); + let (requests, sync_peers) = self.block_sync.prepare_block_requests(&self.gateway).await; // If there are no block requests, return early. if requests.is_empty() { @@ -370,9 +408,13 @@ impl Sync { // notify the incoming task. } + fn update_peer_block_height(&self, peer_ip: SocketAddr, new_advertised: u32) -> Result<()> { + self.block_sync.update_peer_block_height(peer_ip, new_advertised) + } + /// We received new peer locators during a Ping. - fn update_peer_locators(&self, peer_ip: SocketAddr, locators: BlockLocators) -> Result<()> { - self.block_sync.update_peer_locators(peer_ip, &locators) + async fn update_peer_block_locators(&self, peer_ip: SocketAddr, block_locators: BlockLocators) -> Result<()> { + self.block_sync.update_peer_block_locators(peer_ip, block_locators).await } /// A peer disconnected. @@ -381,12 +423,12 @@ impl Sync { } #[cfg(test)] - pub fn testing_only_update_peer_locators_testing_only( + pub async fn testing_only_update_peer_locators_testing_only( &self, peer_ip: SocketAddr, locators: BlockLocators, ) -> Result<()> { - self.update_peer_locators(peer_ip, locators) + self.update_peer_block_locators(peer_ip, locators).await } } @@ -394,7 +436,7 @@ impl Sync { impl Sync { /// Syncs the storage with the ledger at bootup. /// - /// This is called when starting the validator and after finishing a sync without BFT. + /// This is called when first starting the validator. async fn sync_storage_with_ledger_at_bootup(&self) -> Result<()> { // Retrieve the latest block in the ledger. let latest_block = self.ledger.latest_block(); @@ -488,33 +530,35 @@ impl Sync { /// If there are queued block responses, this might be higher than the latest block in the ledger. async fn compute_sync_height(&self) -> u32 { let ledger_height = self.ledger.latest_block_height(); - let mut responses = self.latest_block_responses.lock().await; + let mut pending_blocks = self.pending_blocks.lock().await; // Remove any old responses. - responses.retain(|height, _| *height > ledger_height); + while let Some(b) = pending_blocks.front() + && b.height() <= ledger_height + { + pending_blocks.pop_front(); + } // Ensure the returned value is always greater or equal than ledger height. - responses.last_key_value().map(|(height, _)| *height).unwrap_or(0).max(ledger_height) + pending_blocks.back().map(|b| b.height()).unwrap_or(0).max(ledger_height) } /// BFT-version of [`snarkos_node_client::Client::try_advancing_block_synchronization`]. async fn try_advancing_block_synchronization(&self, ping: &Option>>) { // Process block responses and advance the ledger. - let new_blocks = match self.try_advancing_block_synchronization_inner().await { + let had_new_blocks = match self.try_advancing_block_synchronization_inner().await { Ok(new_blocks) => new_blocks, Err(err) => { - error!("Block synchronization failed - {err}"); + error!("{}", flatten_error(err.context("Block synchronization failed"))); false } }; if let Some(ping) = &ping - && new_blocks + && had_new_blocks { - match self.get_block_locators() { - Ok(locators) => ping.update_block_locators(locators), - Err(err) => error!("Failed to update block locators: {err}"), - } + let height = self.ledger.latest_block_height(); + ping.update_block_height(height); } } @@ -526,12 +570,9 @@ impl Sync { /// This returns Ok(true) if we successfully advanced the ledger by at least one new block. /// /// A key difference to `BlockSync`'s versions is that it will only add blocks to the ledger once they have been confirmed by the network. - /// If blocks are not confirmed yet, they will be kept in [`Self::latest_block_responses`]. + /// If blocks are not confirmed yet, they will be kept in [`Self::pending_blocks`]. /// It will also pass certificates from synced blocks to the BFT module so that consensus can progress as expected /// (see [`Self::sync_storage_with_block`] for more details). - /// - /// If the node falls behind more than GC rounds, this function calls [`Self::sync_storage_without_bft`] instead, - /// which syncs without updating the BFT state. async fn try_advancing_block_synchronization_inner(&self) -> Result { // Acquire the response lock. let _lock = self.response_lock.lock().await; @@ -542,148 +583,132 @@ impl Sync { self.block_sync.set_sync_height(ledger_height); // Retrieve the maximum block height of the peers. - let tip = self - .block_sync - .find_sync_peers() - .map(|(sync_peers, _)| *sync_peers.values().max().unwrap_or(&0)) - .unwrap_or(0); + let _tip = self.block_sync.get_peer_heights().values().max().copied().unwrap_or(0); - // Determine the maximum number of blocks corresponding to rounds - // that would not have been garbage collected, i.e. that would be kept in storage. - // Since at most one block is created every two rounds, - // this is half of the maximum number of rounds kept in storage. - let max_gc_blocks = u32::try_from(self.storage.max_gc_rounds())?.saturating_div(2); + // Retrieve the current height, based on the ledger height and the + // (unconfirmed) blocks that are already queued up. + let start_height = self.compute_sync_height().await; - // Updates sync state and returns the error (if any). - let cleanup = |start_height, current_height, error| { - let new_blocks = current_height > start_height; + // For sanity, update the sync height before starting. + // (if this is lower or equal to the current sync height, this is a noop) + self.block_sync.set_sync_height(start_height); - // Make the underlying `BlockSync` instance aware of the new sync height. - if new_blocks { - self.block_sync.set_sync_height(current_height); - } - - if let Some(err) = error { Err(err) } else { Ok(new_blocks) } - }; - - // Determine the earliest height of blocks corresponding to rounds kept in storage, - // conservatively set to the block height minus the maximum number of blocks calculated above. - // By virtue of the BFT protocol, we can guarantee that all GC range blocks will be loaded. - let max_gc_height = tip.saturating_sub(max_gc_blocks); - let within_gc = (ledger_height + 1) > max_gc_height; + // The height is incremented as blocks are added. + let mut current_height = start_height; + trace!("Try advancing with block responses (at block {current_height})"); - if within_gc { - // Retrieve the current height, based on the ledger height and the - // (unconfirmed) blocks that are already queued up. - let start_height = self.compute_sync_height().await; - - // For sanity, update the sync height before starting. - // (if this is lower or equal to the current sync height, this is a noop) - self.block_sync.set_sync_height(start_height); - - // The height is incremented as blocks are added. - let mut current_height = start_height; - trace!( - "Try advancing blocks responses with BFT (starting at block {current_height}, current sync speed is {})", - self.block_sync.get_sync_speed() - ); + // If we already were within GC or successfully caught up with GC, try to advance BFT normally again. + loop { + let next_height = current_height + 1; + let Some(block) = self.block_sync.peek_next_block(next_height) else { + break; + }; - // If we already were within GC or successfully caught up with GC, try to advance BFT normally again. - loop { - let next_height = current_height + 1; - let Some(block) = self.block_sync.peek_next_block(next_height) else { - break; - }; - info!("Syncing the BFT to block {}...", block.height()); - // Sync the storage with the block. - match self.sync_storage_with_block(block).await { - Ok(_) => { - // Update the current height if sync succeeds. - current_height = next_height; - } - Err(err) => { - // Mark the current height as processed in block_sync. - self.block_sync.remove_block_response(next_height); - return cleanup(start_height, current_height, Some(err)); - } + info!("Syncing the BFT to block {}...", block.height()); + // Sync the storage with the block. + match self.sync_storage_with_block(block).await { + Ok(_) => { + // Update the current height if sync succeeds. + current_height += 1; + } + Err(e) => { + // Mark the current height as processed in block_sync. + self.block_sync.remove_block_response(current_height); + return Err(e); } } + } - cleanup(start_height, current_height, None) - } else { - // For non-BFT sync we need to start at the current height of the ledger,as blocks are immediately - // added to it and not queue up in `latest_block_responses`. - let start_height = ledger_height; - let mut current_height = start_height; + let new_blocks = current_height > start_height; - trace!("Try advancing block responses without BFT (starting at block {current_height})"); + // Make the underlying `BlockSync` instance aware of the new sync height. + if new_blocks { + self.block_sync.set_sync_height(current_height); + } - // For sanity, update the sync height before starting. - // (if this is lower or equal to the current sync height, this is a noop) - self.block_sync.set_sync_height(start_height); + Ok(new_blocks) + } - // Try to advance the ledger *to tip* without updating the BFT. - // TODO(kaimast): why to tip and not to tip-GC? - loop { - let next_height = current_height + 1; + /// Helper function for [`Self::sync_storage_with_block`]. + /// It syncs the batch certificates with the BFT, if the block's authority is a sub-DAG. + /// + /// Note that the block authority is always a sub-DAG in production; beacon signatures are only used for testing, + /// and as placeholder (irrelevant) block authority in the genesis block.i + async fn add_block_subdag_to_bft(&self, block: &Block) -> Result<()> { + // Nothing to do if this is a beacon block + let Authority::Quorum(subdag) = block.authority() else { + return Ok(()); + }; - let Some(block) = self.block_sync.peek_next_block(next_height) else { - break; - }; - info!("Syncing the ledger to block {}...", block.height()); - - // Sync the ledger with the block without BFT. - match self.sync_ledger_with_block_without_bft(block).await { - Ok(_) => { - // Update the current height if sync succeeds. - current_height = next_height; - self.block_sync.count_request_completed(); - } - Err(err) => { - // Mark the current height as processed in block_sync. - self.block_sync.remove_block_response(next_height); - return cleanup(start_height, current_height, Some(err)); - } - } - } + // Reconstruct the unconfirmed transactions. + let unconfirmed_transactions = cfg_iter!(block.transactions()) + .filter_map(|tx| tx.to_unconfirmed_transaction().map(|unconfirmed| (unconfirmed.id(), unconfirmed)).ok()) + .collect::>(); - // Sync the storage with the ledger if we should transition to the BFT sync. - let within_gc = (current_height + 1) > max_gc_height; - if within_gc { - info!("Finished catching up with the network. Switching back to BFT sync."); - if let Err(err) = self.sync_storage_with_ledger_at_bootup().await { - error!("BFT sync (with bootup routine) failed - {err}"); + // Iterate over the certificates. + for certificates in subdag.values().cloned() { + cfg_into_iter!(certificates.clone()).for_each(|certificate| { + // Sync the batch certificate with the block. + self.storage.sync_certificate_with_block(block, certificate.clone(), &unconfirmed_transactions); + }); + + // Sync the BFT DAG with the certificates. + for certificate in certificates { + // If a BFT sender was provided, send the certificate to the BFT. + // For validators, BFT spawns a receiver task in `BFT::start_handlers`. + if let Some(bft_sender) = self.bft_sender.get() { + let (callback_tx, callback_rx) = oneshot::channel(); + bft_sender + .tx_sync_bft + .send((certificate, callback_tx)) + .await + .with_context(|| "Failed to sync certificate")?; + callback_rx.await?.with_context(|| "Failed to sync certificate")?; } } - - cleanup(start_height, current_height, None) } + Ok(()) } - /// Syncs the ledger with the given block without updating the BFT. + /// Helper function for [`Self::sync_storage_with_block`]. /// - /// This is only used by `[Self::try_advancing_block_synchronization`]. - async fn sync_ledger_with_block_without_bft(&self, block: Block) -> Result<()> { - // Acquire the sync lock. - let _lock = self.sync_lock.lock().await; + /// It checks that successor of a given block contains enough votes to commit it. + /// This can only return `Ok(true)` if the certificates of the block's successor were added to the storage. + fn is_block_availability_threshold_reached(&self, block: &PendingBlock) -> Result { + // Fetch the leader certificate and the relevant rounds. + let leader_certificate = match block.authority() { + Authority::Quorum(subdag) => subdag.leader_certificate().clone(), + _ => bail!("Received a block with an unexpected authority type."), + }; + let commit_round = leader_certificate.round(); + let certificate_round = + commit_round.checked_add(1).ok_or_else(|| anyhow!("Integer overflow on round number"))?; + + // Get the committee lookback for the round just after the leader. + let certificate_committee_lookback = self.ledger.get_committee_lookback_for_round(certificate_round)?; + // Retrieve all of the certificates for the round just after the leader. + let certificates = self.storage.get_certificates_for_round(certificate_round); + // Construct a set over the authors, at the round just after the leader, + // who included the leader's certificate in their previous certificate IDs. + let authors = certificates + .iter() + .filter_map(|c| match c.previous_certificate_ids().contains(&leader_certificate.id()) { + true => Some(c.author()), + false => None, + }) + .collect(); - let self_ = self.clone(); - tokio::task::spawn_blocking(move || { - // Check the next block. - self_.ledger.check_next_block(&block)?; - // Attempt to advance to the next block. - self_.ledger.advance_to_next_block(&block)?; - - // Sync the height with the block. - self_.storage.sync_height_with_block(block.height()); - // Sync the round with the block. - self_.storage.sync_round_with_block(block.round()); - // Mark the block height as processed in block_sync. - self_.block_sync.remove_block_response(block.height()); - - Ok(()) - }) - .await? + // Check if the leader is ready to be committed. + if certificate_committee_lookback.is_availability_threshold_reached(&authors) { + trace!( + "Block {hash} at height {height} has reached availability threshold", + hash = block.hash(), + height = block.height() + ); + Ok(true) + } else { + Ok(false) + } } /// Advances the ledger by the given block and updates the storage accordingly. @@ -692,209 +717,118 @@ impl Sync { /// meets the voter availability threshold (i.e. > f voting stake) /// or is reachable via a DAG path from a later leader certificate that does. /// Since performing this check requires DAG certificates from later blocks, - /// the block is stored in `Sync::latest_block_responses`, + /// the block is stored in `Sync::pending_blocks`, /// and its addition to the ledger is deferred until the check passes. - /// Several blocks may be stored in `Sync::latest_block_responses` + /// Several blocks may be stored in `Sync::pending_blocks` /// before they can be all checked and added to the ledger. - async fn sync_storage_with_block(&self, block: Block) -> Result<()> { + /// + /// # Usage + /// This function assumes that blocks are passed in order, i.e., + /// that the given block is a direct successor of the block that was last passed to this function. + async fn sync_storage_with_block(&self, new_block: Block) -> Result<()> { // Acquire the sync lock. let _lock = self.sync_lock.lock().await; // If this block has already been processed, return early. // TODO(kaimast): Should we remove the response here? - if self.ledger.contains_block_height(block.height()) { - debug!("Ledger is already synced with block at height {}. Will not sync.", block.height()); + if self.ledger.contains_block_height(new_block.height()) { + debug!( + "Ledger is already synced with block at height {height}. Will not sync.", + height = new_block.height() + ); return Ok(()); } - // Acquire the latest block responses lock. - let mut latest_block_responses = self.latest_block_responses.lock().await; + // Append the certificates to the storage. + self.add_block_subdag_to_bft(&new_block).await?; - if latest_block_responses.contains_key(&block.height()) { - debug!("An unconfirmed block is queued already for height {}. Will not sync.", block.height()); - return Ok(()); - } + // Acquire the pending blocks lock. + let mut pending_blocks = self.pending_blocks.lock().await; - // If the block authority is a sub-DAG, then sync the batch certificates with the block. - // Note that the block authority is always a sub-DAG in production; - // beacon signatures are only used for testing, - // and as placeholder (irrelevant) block authority in the genesis block. - if let Authority::Quorum(subdag) = block.authority() { - // Reconstruct the unconfirmed transactions. - let unconfirmed_transactions = cfg_iter!(block.transactions()) - .filter_map(|tx| { - tx.to_unconfirmed_transaction().map(|unconfirmed| (unconfirmed.id(), unconfirmed)).ok() - }) - .collect::>(); - - // Iterate over the certificates. - for certificates in subdag.values().cloned() { - cfg_into_iter!(certificates.clone()).for_each(|certificate| { - // Sync the batch certificate with the block. - self.storage.sync_certificate_with_block(&block, certificate.clone(), &unconfirmed_transactions); - }); + // Fetch the latest block height. + let ledger_block_height = self.ledger.latest_block_height(); - // Sync the BFT DAG with the certificates. - for certificate in certificates { - // If a BFT sender was provided, send the certificate to the BFT. - // For validators, BFT spawns a receiver task in `BFT::start_handlers`. - if let Some(bft_sender) = self.bft_sender.get() { - // Await the callback to continue. - if let Err(err) = bft_sender.send_sync_bft(certificate).await { - bail!("Failed to sync certificate - {err}"); - }; - } - } + // First, clear any older pending blocks. + // TODO(kaimast): ensure there are no dangling block requests + while let Some(pending_block) = pending_blocks.front() { + if pending_block.height() > ledger_block_height { + break; } + + pending_blocks.pop_front(); } - // Fetch the latest block height. - let ledger_block_height = self.ledger.latest_block_height(); + if let Some(tail) = pending_blocks.back() { + if tail.height() >= new_block.height() { + debug!( + "A unconfirmed block is queued already for height {height}. \ + Will not sync.", + height = new_block.height() + ); + return Ok(()); + } - // Insert the latest block response. - latest_block_responses.insert(block.height(), block); - // Clear the latest block responses of older blocks. - latest_block_responses.retain(|height, _| *height > ledger_block_height); + ensure_equals!(tail.height() + 1, new_block.height(), "Got an out-of-order block"); + } - // Get a list of contiguous blocks from the latest block responses. - let contiguous_blocks: Vec> = (ledger_block_height.saturating_add(1)..) - .take_while(|&k| latest_block_responses.contains_key(&k)) - .filter_map(|k| latest_block_responses.get(&k).cloned()) - .collect(); + // Check the block against the chain of pending blocks and append it on success. + // TODO(kaimast): handle the case where the ledger already advance better + let new_block = self + .ledger + .check_block_subdag(new_block, pending_blocks.make_contiguous()) + .with_context(|| "SubDAG check failed")?; + pending_blocks.push_back(new_block); + + // Now, figure out if and which pending block we can commit. + // To do this effectively and because commits are transitive, + // we iterate in reverse so that we can stop at the first successful check. + // + // Note, that if the storage already contains certificates for the round after new block, + // the availability threshold for the new block could also be reached. + let mut commit_height = None; + for block in pending_blocks.iter().rev() { + if self + .is_block_availability_threshold_reached(block) + .with_context(|| "Availability threshold check failed")? + { + commit_height = Some(block.height()); + break; + } + } - // Check if each block response, from the contiguous sequence just constructed, - // is ready to be added to the ledger. - // Ensure that the block's leader certificate meets the availability threshold - // based on the certificates in the DAG just after the block's round. - // If the availability threshold is not met, - // process the next block and check if it is linked to the current block, - // in the sense that there is a path in the DAG - // from the next block's leader certificate - // to the current block's leader certificate. - // Note: We do not advance to the most recent block response because we would be unable to - // validate if the leader certificate in the block has been certified properly. - for next_block in contiguous_blocks.into_iter() { - // Retrieve the height of the next block. - let next_block_height = next_block.height(); - - // Fetch the leader certificate and the relevant rounds. - let leader_certificate = match next_block.authority() { - Authority::Quorum(subdag) => subdag.leader_certificate().clone(), - _ => bail!("Received a block with an unexpected authority type."), - }; - let commit_round = leader_certificate.round(); - let certificate_round = - commit_round.checked_add(1).ok_or_else(|| anyhow!("Integer overflow on round number"))?; - - // Get the committee lookback for the round just after the leader. - let certificate_committee_lookback = self.ledger.get_committee_lookback_for_round(certificate_round)?; - // Retrieve all of the certificates for the round just after the leader. - let certificates = self.storage.get_certificates_for_round(certificate_round); - // Construct a set over the authors, at the round just after the leader, - // who included the leader's certificate in their previous certificate IDs. - let authors = certificates - .iter() - .filter_map(|c| match c.previous_certificate_ids().contains(&leader_certificate.id()) { - true => Some(c.author()), - false => None, - }) - .collect(); - - debug!("Validating sync block {next_block_height} at round {commit_round}..."); - // Check if the leader is ready to be committed. - if certificate_committee_lookback.is_availability_threshold_reached(&authors) { - // Initialize the current certificate. - let mut current_certificate = leader_certificate; - // Check if there are any linked blocks that need to be added. - let mut blocks_to_add = vec![next_block]; - - // Check if there are other blocks to process based on `is_linked`. - for height in (self.ledger.latest_block_height().saturating_add(1)..next_block_height).rev() { - // Retrieve the previous block. - let Some(previous_block) = latest_block_responses.get(&height) else { - bail!("Block {height} is missing from the latest block responses."); - }; - // Retrieve the previous block's leader certificate. - let previous_certificate = match previous_block.authority() { - Authority::Quorum(subdag) => subdag.leader_certificate().clone(), - _ => bail!("Received a block with an unexpected authority type."), - }; - // Determine if there is a path between the previous certificate and the current certificate. - if self.is_linked(previous_certificate.clone(), current_certificate.clone())? { - debug!("Previous sync block {height} is linked to the current block {next_block_height}"); - // Add the previous leader certificate to the list of certificates to commit. - blocks_to_add.insert(0, previous_block.clone()); - // Update the current certificate to the previous leader certificate. - current_certificate = previous_certificate; - } - } + if let Some(commit_height) = commit_height { + let start_height = ledger_block_height + 1; + ensure!(commit_height >= start_height, "Invalid commit height"); + let num_blocks = (commit_height - start_height + 1) as usize; - // Add the blocks to the ledger. - for block in blocks_to_add { - // Check that the blocks are sequential and can be added to the ledger. - let block_height = block.height(); - if block_height != self.ledger.latest_block_height().saturating_add(1) { - warn!("Skipping block {block_height} from the latest block responses - not sequential."); - continue; - } - #[cfg(feature = "telemetry")] - let block_authority = block.authority().clone(); - - let self_ = self.clone(); - tokio::task::spawn_blocking(move || { - // Check the next block. - self_.ledger.check_next_block(&block)?; - // Attempt to advance to the next block. - self_.ledger.advance_to_next_block(&block)?; - - // Sync the height with the block. - self_.storage.sync_height_with_block(block.height()); - // Sync the round with the block. - self_.storage.sync_round_with_block(block.round()); - - Ok::<(), anyhow::Error>(()) - }) - .await??; - // Remove the block height from the latest block responses. - latest_block_responses.remove(&block_height); - - // Update the validator telemetry. - #[cfg(feature = "telemetry")] - if let Authority::Quorum(subdag) = block_authority { - self_.gateway.validator_telemetry().insert_subdag(&subdag); - } - } - } else { - debug!( - "Availability threshold was not reached for block {next_block_height} at round {commit_round}. Checking next block..." + // Create a more detailed log message if we are committing more than one block at a time. + if num_blocks > 1 { + trace!( + "Attempting to commit {chain_length} pending block(s) starting at height {start_height}.", + chain_length = pending_blocks.len(), ); } - // Don't remove the response from BlockSync yet as we might fall back to non-BFT sync. - } + for pending_block in pending_blocks.drain(0..num_blocks) { + let ledger = self.ledger.clone(); + let hash = pending_block.hash(); + let height = pending_block.height(); - Ok(()) - } + spawn_blocking!({ + match ledger.check_block_content(pending_block) { + Ok(block) => { + trace!("Adding pending block {hash} at height {height} to the ledger"); + ledger.advance_to_next_block(&block)?; + } + Err(err) => bail!("Failed to check contents of pending block {hash} at height {height}: {err}"), + } - /// Returns `true` if there is a path from the previous certificate to the current certificate. - fn is_linked( - &self, - previous_certificate: BatchCertificate, - current_certificate: BatchCertificate, - ) -> Result { - // Initialize the list containing the traversal. - let mut traversal = vec![current_certificate.clone()]; - // Iterate over the rounds from the current certificate to the previous certificate. - for round in (previous_certificate.round()..current_certificate.round()).rev() { - // Retrieve all of the certificates for this past round. - let certificates = self.storage.get_certificates_for_round(round); - // Filter the certificates to only include those that are in the traversal. - traversal = certificates - .into_iter() - .filter(|p| traversal.iter().any(|c| c.previous_certificate_ids().contains(&p.id()))) - .collect(); + Ok(()) + })? + } } - Ok(traversal.contains(&previous_certificate)) + + Ok(()) } } @@ -918,7 +852,10 @@ impl Sync { /// Returns the current block locators of the node. pub fn get_block_locators(&self) -> Result> { - self.block_sync.get_block_locators() + // Use the ledger's latest height as the range + let end = self.ledger.latest_block_height(); + let start = if end > 0 { end.saturating_sub(100) } else { 0 }; + self.block_sync.get_block_locators(start, end) } } @@ -1042,12 +979,23 @@ mod tests { type CurrentLedger = Ledger>; type CurrentConsensusStore = ConsensusStore>; + /// Tests that commits work as expected when some anchors are not committed immediately. #[tokio::test] - async fn test_commit_via_is_linked() -> anyhow::Result<()> { + async fn test_commit_chain() -> anyhow::Result<()> { let rng = &mut TestRng::default(); // Initialize the round parameters. let max_gc_rounds = BatchHeader::::MAX_GC_ROUNDS as u64; - let commit_round = 2; + + // The first round of the first block. + let first_round = 1; + // The total number of blocks we test + let num_blocks = 3; + // The number of certificate rounds needed. + // There is one additional round to provide availability for the inal block. + let num_rounds = first_round + num_blocks * 2 + 1; + // The first round that has at least N-f certificates referencing the anchor from the previous round. + // This is also the last round we use in the test. + let first_committed_round = num_rounds - 1; // Initialize the store. let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap(); @@ -1090,77 +1038,70 @@ mod tests { HashMap::new(); let mut previous_certificates: IndexSet> = IndexSet::with_capacity(4); - for round in 0..=commit_round + 8 { + for round in first_round..=first_committed_round { let mut current_certificates = IndexSet::new(); let previous_certificate_ids: IndexSet<_> = if round == 0 || round == 1 { IndexSet::new() } else { previous_certificates.iter().map(|c| c.id()).collect() }; + let committee_id = committee.id(); + let prev_leader = committee.get_leader(round - 1).unwrap(); + + // For the first two blocks non-leaders will not reference the leader certificate. + // This means, while there is an anchor, it is isn't committed + // until later. + for (i, private_key) in private_keys.iter().enumerate() { + let leader_index = addresses.iter().position(|&address| address == prev_leader).unwrap(); + let is_certificate_round = round % 2 == 1; + let is_leader = i == leader_index; + + let previous_certs = if round < first_committed_round && is_certificate_round && !is_leader { + previous_certificate_ids + .iter() + .cloned() + .enumerate() + .filter(|(idx, _)| *idx != leader_index) + .map(|(_, id)| id) + .collect() + } else { + previous_certificate_ids.clone() + }; - // Create a certificate for the leader. - if round <= 5 { - let leader = committee.get_leader(round).unwrap(); - let leader_index = addresses.iter().position(|&address| address == leader).unwrap(); - let non_leader_index = addresses.iter().position(|&address| address != leader).unwrap(); - for i in [leader_index, non_leader_index].into_iter() { - let batch_header = BatchHeader::new( - &private_keys[i], - round, - now(), - committee_id, - Default::default(), - previous_certificate_ids.clone(), - rng, - ) - .unwrap(); - // Sign the batch header. - let mut signatures = IndexSet::with_capacity(4); - for (j, private_key_2) in private_keys.iter().enumerate() { - if i != j { - signatures.insert(private_key_2.sign(&[batch_header.batch_id()], rng).unwrap()); - } - } - current_certificates.insert(BatchCertificate::from(batch_header, signatures).unwrap()); - } - } + let batch_header = BatchHeader::new( + private_key, + round, + now(), + committee_id, + Default::default(), + previous_certs, + rng, + ) + .unwrap(); - // Create a certificate for each validator. - if round > 5 { - for (i, private_key_1) in private_keys.iter().enumerate() { - let batch_header = BatchHeader::new( - private_key_1, - round, - now(), - committee_id, - Default::default(), - previous_certificate_ids.clone(), - rng, - ) - .unwrap(); - // Sign the batch header. - let mut signatures = IndexSet::with_capacity(4); - for (j, private_key_2) in private_keys.iter().enumerate() { - if i != j { - signatures.insert(private_key_2.sign(&[batch_header.batch_id()], rng).unwrap()); - } + // Sign the batch header. + let mut signatures = IndexSet::with_capacity(4); + for (j, private_key_2) in private_keys.iter().enumerate() { + if i != j { + signatures.insert(private_key_2.sign(&[batch_header.batch_id()], rng).unwrap()); } - current_certificates.insert(BatchCertificate::from(batch_header, signatures).unwrap()); } + current_certificates.insert(BatchCertificate::from(batch_header, signatures).unwrap()); } + // Update the map of certificates. round_to_certificates_map.insert(round, current_certificates.clone()); - previous_certificates = current_certificates.clone(); + previous_certificates = current_certificates; } (round_to_certificates_map, committee) }; // Initialize the storage. let storage = Storage::new(core_ledger.clone(), Arc::new(BFTMemoryService::new()), max_gc_rounds); - // Insert certificates into storage. + // Insert all certificates into storage. let mut certificates: Vec> = Vec::new(); - for i in 1..=commit_round + 8 { + for i in first_round..=first_committed_round { let c = (*round_to_certificates_map.get(&i).unwrap()).clone(); certificates.extend(c); } @@ -1168,94 +1109,64 @@ mod tests { storage.testing_only_insert_certificate_testing_only(certificate.clone()); } - // Create block 1. - let leader_round_1 = commit_round; - let leader_1 = committee.get_leader(leader_round_1).unwrap(); - let leader_certificate = storage.get_certificate_for_round_with_author(commit_round, leader_1).unwrap(); - let block_1 = { + // Create the blocks + let mut previous_leader_cert = None; + let mut blocks = vec![]; + + for block_height in 1..=num_blocks { + let leader_round = block_height * 2; + + let leader = committee.get_leader(leader_round).unwrap(); + let leader_certificate = storage.get_certificate_for_round_with_author(leader_round, leader).unwrap(); + let mut subdag_map: BTreeMap>> = BTreeMap::new(); let mut leader_cert_map = IndexSet::new(); leader_cert_map.insert(leader_certificate.clone()); - let mut previous_cert_map = IndexSet::new(); - for cert in storage.get_certificates_for_round(commit_round - 1) { - previous_cert_map.insert(cert); - } - subdag_map.insert(commit_round, leader_cert_map.clone()); - subdag_map.insert(commit_round - 1, previous_cert_map.clone()); - let subdag = Subdag::from(subdag_map.clone())?; - let ledger = core_ledger.clone(); - spawn_blocking!(ledger.prepare_advance_to_next_quorum_block(subdag, Default::default()))? - }; - // Insert block 1. - let ledger = core_ledger.clone(); - let block = block_1.clone(); - spawn_blocking!(ledger.advance_to_next_block(&block))?; - // Create block 2. - let leader_round_2 = commit_round + 2; - let leader_2 = committee.get_leader(leader_round_2).unwrap(); - let leader_certificate_2 = storage.get_certificate_for_round_with_author(leader_round_2, leader_2).unwrap(); - let block_2 = { - let mut subdag_map_2: BTreeMap>> = BTreeMap::new(); - let mut leader_cert_map_2 = IndexSet::new(); - leader_cert_map_2.insert(leader_certificate_2.clone()); - let mut previous_cert_map_2 = IndexSet::new(); - for cert in storage.get_certificates_for_round(leader_round_2 - 1) { - previous_cert_map_2.insert(cert); - } - let mut prev_commit_cert_map_2 = IndexSet::new(); - for cert in storage.get_certificates_for_round(leader_round_2 - 2) { - if cert != leader_certificate { - prev_commit_cert_map_2.insert(cert); - } - } - subdag_map_2.insert(leader_round_2, leader_cert_map_2.clone()); - subdag_map_2.insert(leader_round_2 - 1, previous_cert_map_2.clone()); - subdag_map_2.insert(leader_round_2 - 2, prev_commit_cert_map_2.clone()); - let subdag_2 = Subdag::from(subdag_map_2.clone())?; - let ledger = core_ledger.clone(); - spawn_blocking!(ledger.prepare_advance_to_next_quorum_block(subdag_2, Default::default()))? - }; - // Insert block 2. - let ledger = core_ledger.clone(); - let block = block_2.clone(); - spawn_blocking!(ledger.advance_to_next_block(&block))?; + let previous_cert_map = storage.get_certificates_for_round(leader_round - 1); - // Create block 3 - let leader_round_3 = commit_round + 4; - let leader_3 = committee.get_leader(leader_round_3).unwrap(); - let leader_certificate_3 = storage.get_certificate_for_round_with_author(leader_round_3, leader_3).unwrap(); - let block_3 = { - let mut subdag_map_3: BTreeMap>> = BTreeMap::new(); - let mut leader_cert_map_3 = IndexSet::new(); - leader_cert_map_3.insert(leader_certificate_3.clone()); - let mut previous_cert_map_3 = IndexSet::new(); - for cert in storage.get_certificates_for_round(leader_round_3 - 1) { - previous_cert_map_3.insert(cert); - } - let mut prev_commit_cert_map_3 = IndexSet::new(); - for cert in storage.get_certificates_for_round(leader_round_3 - 2) { - if cert != leader_certificate_2 { - prev_commit_cert_map_3.insert(cert); - } + subdag_map.insert(leader_round, leader_cert_map.clone()); + subdag_map.insert(leader_round - 1, previous_cert_map.clone()); + + if leader_round > 2 { + let previous_commit_cert_map: IndexSet<_> = storage + .get_certificates_for_round(leader_round - 2) + .into_iter() + .filter(|cert| { + if let Some(previous_leader_cert) = &previous_leader_cert { + cert != previous_leader_cert + } else { + true + } + }) + .collect(); + subdag_map.insert(leader_round - 2, previous_commit_cert_map); } - subdag_map_3.insert(leader_round_3, leader_cert_map_3.clone()); - subdag_map_3.insert(leader_round_3 - 1, previous_cert_map_3.clone()); - subdag_map_3.insert(leader_round_3 - 2, prev_commit_cert_map_3.clone()); - let subdag_3 = Subdag::from(subdag_map_3.clone())?; - let ledger = core_ledger.clone(); - spawn_blocking!(ledger.prepare_advance_to_next_quorum_block(subdag_3, Default::default()))? + + previous_leader_cert = Some(leader_certificate); + + let core_ledger = core_ledger.clone(); + let block = spawn_blocking!({ + let subdag = Subdag::from(subdag_map.clone())?; + let block = core_ledger.prepare_advance_to_next_quorum_block(subdag, Default::default())?; + core_ledger.advance_to_next_block(&block)?; + Ok(block) + })?; + blocks.push(block); + } + + // ### Test that sync works as expected ### + let storage_mode = StorageMode::Test(None); + + // Create a new ledger to test with, but use the existing storage + // so that the certificates exist. + let syncing_ledger = { + let storage_mode = storage_mode.clone(); + let syncing_ledger = spawn_blocking!(CurrentLedger::load(genesis, storage_mode)).unwrap(); + Arc::new(CoreLedgerService::new(syncing_ledger, Default::default())) }; - // Insert block 3. - let ledger = core_ledger.clone(); - let block = block_3.clone(); - spawn_blocking!(ledger.advance_to_next_block(&block))?; - // Initialize the syncing ledger. - let syncing_ledger = spawn_blocking!(CurrentLedger::load(genesis, StorageMode::new_test(None))).unwrap(); - let syncing_ledger = Arc::new(CoreLedgerService::new(syncing_ledger, Default::default())); - // Initialize the gateway. - let storage_mode = StorageMode::new_test(None); + // Set up sync and its dependencies. let gateway = Gateway::new( account.clone(), storage.clone(), @@ -1266,19 +1177,25 @@ mod tests { storage_mode, None, )?; - // Initialize the block synchronization logic. let block_sync = Arc::new(BlockSync::new(syncing_ledger.clone())); - // Initialize the sync module. - let sync = Sync::new(gateway, storage, syncing_ledger.clone(), block_sync); - // Try to sync block 1. - sync.sync_storage_with_block(block_1).await?; - assert_eq!(syncing_ledger.latest_block_height(), 1); - // Try to sync block 2. - sync.sync_storage_with_block(block_2).await?; - assert_eq!(syncing_ledger.latest_block_height(), 2); - // Try to sync block 3. - sync.sync_storage_with_block(block_3).await?; + let sync = Sync::new(gateway.clone(), storage.clone(), syncing_ledger.clone(), block_sync); + + let mut block_iter = blocks.into_iter(); + + // Insert the blocks into the new sync module + for _ in 0..num_blocks - 1 { + let block = block_iter.next().unwrap(); + sync.sync_storage_with_block(block).await?; + + // Availability threshold is not met, so we should not advance yet. + assert_eq!(syncing_ledger.latest_block_height(), 0); + } + + // Only for the final block, the availability threshold is met, + // because certificates for the subsequent round are already in storage. + sync.sync_storage_with_block(block_iter.next().unwrap()).await?; assert_eq!(syncing_ledger.latest_block_height(), 3); + // Ensure blocks 1 and 2 were added to the ledger. assert!(syncing_ledger.contains_block_height(1)); assert!(syncing_ledger.contains_block_height(2)); @@ -1287,7 +1204,6 @@ mod tests { } #[tokio::test] - #[tracing_test::traced_test] async fn test_pending_certificates() -> anyhow::Result<()> { let rng = &mut TestRng::default(); // Initialize the round parameters. diff --git a/node/bft/src/worker.rs b/node/bft/src/worker.rs index a4c2fda37e..fd2f5c9d32 100644 --- a/node/bft/src/worker.rs +++ b/node/bft/src/worker.rs @@ -26,7 +26,7 @@ use snarkos_node_bft_ledger_service::LedgerService; use snarkvm::{ console::prelude::*, ledger::{ - block::Transaction, + Transaction, narwhal::{BatchHeader, Data, Transmission, TransmissionID}, puzzle::{Solution, SolutionID}, }, @@ -560,7 +560,8 @@ mod tests { use snarkvm::{ console::{network::Network, types::Field}, ledger::{ - block::Block, + Block, + PendingBlock, committee::Committee, narwhal::{BatchCertificate, Subdag, Transmission, TransmissionID}, test_helpers::sample_execution_transaction_with_fee, @@ -626,6 +627,8 @@ mod tests { transaction_id: N::TransactionID, transaction: Transaction, ) -> Result<()>; + fn check_block_subdag(&self, _block: Block, _prefix: &[PendingBlock]) -> Result>; + fn check_block_content(&self, _block: PendingBlock) -> Result>; fn check_next_block(&self, block: &Block) -> Result<()>; fn prepare_advance_to_next_quorum_block( &self, diff --git a/node/consensus/src/lib.rs b/node/consensus/src/lib.rs index e7a8877543..e32b76c3ef 100644 --- a/node/consensus/src/lib.rs +++ b/node/consensus/src/lib.rs @@ -347,13 +347,16 @@ impl Consensus { let solution_id = solution.id(); trace!("Adding unconfirmed solution '{}' to the memory pool...", fmt_id(solution_id)); // Send the unconfirmed solution to the primary. - if let Err(e) = self.primary_sender.send_unconfirmed_solution(solution_id, Data::Object(solution)).await { + if let Err(err) = self.primary_sender.send_unconfirmed_solution(solution_id, Data::Object(solution)).await { // If the BFT is synced, then log the warning. if self.bft.is_synced() { // If error occurs after the first 10 blocks of the epoch, log it as a warning, otherwise ignore. if self.ledger.latest_block_height() % N::NUM_BLOCKS_PER_EPOCH > 10 { - warn!("Failed to add unconfirmed solution '{}' to the memory pool - {e}", fmt_id(solution_id)) - }; + warn!( + "Failed to add unconfirmed solution '{}' to the memory pool - {err}", + fmt_id(solution_id) + ); + } } } } @@ -446,13 +449,13 @@ impl Consensus { }; trace!("Adding unconfirmed {tx_type_str} transaction '{}' to the memory pool...", fmt_id(transaction_id)); // Send the unconfirmed transaction to the primary. - if let Err(e) = + if let Err(err) = self.primary_sender.send_unconfirmed_transaction(transaction_id, Data::Object(transaction)).await { // If the BFT is synced, then log the warning. if self.bft.is_synced() { warn!( - "Failed to add unconfirmed {tx_type_str} transaction '{}' to the memory pool - {e}", + "Failed to add unconfirmed {tx_type_str} transaction '{}' to the memory pool - {err}", fmt_id(transaction_id) ); } @@ -487,12 +490,12 @@ impl Consensus { // Sleep briefly. tokio::time::sleep(Duration::from_millis(MAX_BATCH_DELAY_IN_MS)).await; // Process the unconfirmed transactions in the memory pool. - if let Err(e) = self_.process_unconfirmed_transactions().await { - warn!("Cannot process unconfirmed transactions - {e}"); + if let Err(err) = self_.process_unconfirmed_transactions().await { + warn!("Cannot process unconfirmed transactions - {err}"); } // Process the unconfirmed solutions in the memory pool. - if let Err(e) = self_.process_unconfirmed_solutions().await { - warn!("Cannot process unconfirmed solutions - {e}"); + if let Err(err) = self_.process_unconfirmed_solutions().await { + warn!("Cannot process unconfirmed solutions - {err}"); } } }); @@ -553,8 +556,7 @@ impl Consensus { } // Notify peers that we have a new block. - let locators = self.block_sync.get_block_locators()?; - self.ping.update_block_locators(locators); + self.ping.update_block_height(next_block.height()); // Make block sync aware of the new block. self.block_sync.set_sync_height(next_block.height()); diff --git a/node/network/src/peering.rs b/node/network/src/peering.rs index cbe2c90c71..e72294e853 100644 --- a/node/network/src/peering.rs +++ b/node/network/src/peering.rs @@ -452,6 +452,10 @@ pub trait PeerPoolHandling: P2P { /// Loads any previously cached peer addresses so they can be introduced as initial /// candidate peers to connect to. fn load_cached_peers(storage_mode: &StorageMode, filename: &str) -> Result> { + if matches!(storage_mode, StorageMode::Test(_)) { + return Ok(vec![]); + } + let mut peer_cache_path = aleo_ledger_dir(N::ID, storage_mode); peer_cache_path.push(filename); diff --git a/node/rest/src/routes.rs b/node/rest/src/routes.rs index 813e19e637..8a11ae5a90 100644 --- a/node/rest/src/routes.rs +++ b/node/rest/src/routes.rs @@ -14,8 +14,10 @@ // limitations under the License. use super::*; + use snarkos_node_network::PeerPoolHandling; use snarkos_node_router::messages::UnconfirmedSolution; + use snarkvm::{ ledger::puzzle::Solution, prelude::{Address, Identifier, LimitedWriter, Plaintext, Program, ToBytes, VM, block::Transaction}, diff --git a/node/router/Cargo.toml b/node/router/Cargo.toml index b85d4d38d7..954a5d25b5 100644 --- a/node/router/Cargo.toml +++ b/node/router/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" [features] test = [ ] -locktick = [ "dep:locktick", "snarkos-node-tcp/locktick", "snarkvm/locktick" ] +locktick = [ "dep:locktick", "snarkos-node-tcp/locktick", "snarkvm/locktick", "snarkos-node-network/locktick" ] metrics = [ "dep:snarkos-node-metrics" ] cuda = [ "snarkvm/cuda", "snarkos-account/cuda" ] serial = [ "snarkos-node-bft-ledger-service/serial" ] @@ -125,8 +125,8 @@ workspace = true features = [ "ledger-write", "test" ] [dev-dependencies.snarkos-node-sync] -path = "../sync" -features = [ "test" ] +workspace = true +features = [ "test-helpers" ] [dev-dependencies.snarkos-node-router] path = "." diff --git a/node/router/messages/Cargo.toml b/node/router/messages/Cargo.toml index 32604f7a00..4c58ffa6f1 100644 --- a/node/router/messages/Cargo.toml +++ b/node/router/messages/Cargo.toml @@ -44,7 +44,7 @@ workspace = true [dev-dependencies.snarkos-node-sync-locators] workspace = true -features = [ "test" ] +features = [ "test-helpers" ] [dev-dependencies.snarkvm] workspace = true diff --git a/node/router/messages/src/block_locators.rs b/node/router/messages/src/block_locators.rs new file mode 100644 index 0000000000..1a71df5b65 --- /dev/null +++ b/node/router/messages/src/block_locators.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkOS library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +use std::borrow::Cow; + +use snarkos_node_bft_events::EventTrait; + +pub use snarkos_node_bft_events::{BlockLocatorsRequest, BlockLocatorsResponse}; + +impl MessageTrait for BlockLocatorsRequest { + #[inline] + fn name(&self) -> Cow<'static, str> { + EventTrait::name(self) + } +} + +impl MessageTrait for BlockLocatorsResponse { + #[inline] + fn name(&self) -> Cow<'static, str> { + EventTrait::name(self) + } +} diff --git a/node/router/messages/src/lib.rs b/node/router/messages/src/lib.rs index 144be28538..bc0c37047b 100644 --- a/node/router/messages/src/lib.rs +++ b/node/router/messages/src/lib.rs @@ -24,6 +24,9 @@ pub use helpers::*; mod block_request; pub use block_request::BlockRequest; +mod block_locators; +pub use block_locators::{BlockLocatorsRequest, BlockLocatorsResponse}; + mod block_response; pub use block_response::BlockResponse; @@ -62,7 +65,6 @@ pub use unconfirmed_transaction::UnconfirmedTransaction; pub use snarkos_node_bft_events::DataBlocks; -use snarkos_node_sync_locators::BlockLocators; use snarkvm::prelude::{ Address, ConsensusVersion, @@ -91,12 +93,14 @@ pub enum Message { Disconnect(Disconnect), PeerRequest(PeerRequest), PeerResponse(PeerResponse), - Ping(Ping), + Ping(Ping), Pong(Pong), PuzzleRequest(PuzzleRequest), PuzzleResponse(PuzzleResponse), UnconfirmedSolution(UnconfirmedSolution), UnconfirmedTransaction(UnconfirmedTransaction), + BlockLocatorsRequest(BlockLocatorsRequest), + BlockLocatorsResponse(BlockLocatorsResponse), } impl From for Message { @@ -167,6 +171,8 @@ impl Message { Self::PuzzleResponse(message) => message.name(), Self::UnconfirmedSolution(message) => message.name(), Self::UnconfirmedTransaction(message) => message.name(), + Self::BlockLocatorsRequest(message) => message.name(), + Self::BlockLocatorsResponse(message) => message.name(), } } @@ -187,6 +193,8 @@ impl Message { Self::PuzzleResponse(..) => 10, Self::UnconfirmedSolution(..) => 11, Self::UnconfirmedTransaction(..) => 12, + Self::BlockLocatorsRequest(..) => 13, + Self::BlockLocatorsResponse(..) => 14, } } @@ -231,6 +239,8 @@ impl ToBytes for Message { Self::PuzzleResponse(message) => message.write_le(writer), Self::UnconfirmedSolution(message) => message.write_le(writer), Self::UnconfirmedTransaction(message) => message.write_le(writer), + Self::BlockLocatorsRequest(message) => message.write_le(writer), + Self::BlockLocatorsResponse(message) => message.write_le(writer), } } } @@ -257,7 +267,9 @@ impl FromBytes for Message { 10 => Self::PuzzleResponse(PuzzleResponse::read_le(&mut reader)?), 11 => Self::UnconfirmedSolution(UnconfirmedSolution::read_le(&mut reader)?), 12 => Self::UnconfirmedTransaction(UnconfirmedTransaction::read_le(&mut reader)?), - 13.. => return Err(error("Unknown message ID {id}")), + 13 => Self::BlockLocatorsRequest(BlockLocatorsRequest::read_le(&mut reader)?), + 14 => Self::BlockLocatorsResponse(BlockLocatorsResponse::read_le(&mut reader)?), + 15.. => return Err(error("Unknown message ID {id}")), }; // Ensure that there are no "dangling" bytes. diff --git a/node/router/messages/src/ping.rs b/node/router/messages/src/ping.rs index 87383e81de..11a642f117 100644 --- a/node/router/messages/src/ping.rs +++ b/node/router/messages/src/ping.rs @@ -21,13 +21,13 @@ use snarkvm::prelude::{FromBytes, ToBytes}; use std::borrow::Cow; #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Ping { +pub struct Ping { pub version: u32, pub node_type: NodeType, - pub block_locators: Option>, + pub block_height: Option, } -impl MessageTrait for Ping { +impl MessageTrait for Ping { /// Returns the message name. #[inline] fn name(&self) -> Cow<'static, str> { @@ -35,13 +35,13 @@ impl MessageTrait for Ping { } } -impl ToBytes for Ping { +impl ToBytes for Ping { fn write_le(&self, mut writer: W) -> io::Result<()> { self.version.write_le(&mut writer)?; self.node_type.write_le(&mut writer)?; - if let Some(locators) = &self.block_locators { + if let Some(height) = &self.block_height { 1u8.write_le(&mut writer)?; - locators.write_le(&mut writer)?; + height.write_le(&mut writer)?; } else { 0u8.write_le(&mut writer)?; } @@ -50,55 +50,48 @@ impl ToBytes for Ping { } } -impl FromBytes for Ping { +impl FromBytes for Ping { fn read_le(mut reader: R) -> io::Result { let version = u32::read_le(&mut reader)?; let node_type = NodeType::read_le(&mut reader)?; let selector = u8::read_le(&mut reader)?; - let block_locators = match selector { + let block_height = match selector { 0 => None, - 1 => Some(BlockLocators::read_le(&mut reader)?), + 1 => Some(u32::read_le(&mut reader)?), _ => return Err(error("Invalid block locators marker")), }; - Ok(Self { version, node_type, block_locators }) + Ok(Self { version, node_type, block_height }) } } -impl Ping { - pub fn new(node_type: NodeType, block_locators: Option>) -> Self { - Self { version: >::latest_message_version(), node_type, block_locators } +impl Ping { + pub fn new(node_type: NodeType, block_height: Option) -> Self { + Self { version: >::latest_message_version(), node_type, block_height } } } #[cfg(test)] pub mod prop_tests { use crate::{Ping, challenge_request::prop_tests::any_node_type}; - use snarkos_node_sync_locators::{BlockLocators, test_helpers::sample_block_locators}; use snarkvm::utilities::{FromBytes, ToBytes}; use bytes::{Buf, BufMut, BytesMut}; use proptest::prelude::{BoxedStrategy, Strategy, any}; use test_strategy::proptest; - type CurrentNetwork = snarkvm::prelude::MainnetV0; - - pub fn any_block_locators() -> BoxedStrategy> { - any::().prop_map(sample_block_locators).boxed() - } - - pub fn any_ping() -> BoxedStrategy> { - (any::(), any_block_locators(), any_node_type()) - .prop_map(|(version, bls, node_type)| Ping { version, block_locators: Some(bls), node_type }) + pub fn any_ping() -> BoxedStrategy { + (any::(), any::(), any_node_type()) + .prop_map(|(version, height, node_type)| Ping { version, block_height: Some(height), node_type }) .boxed() } #[proptest] - fn ping_roundtrip(#[strategy(any_ping())] ping: Ping) { + fn ping_roundtrip(#[strategy(any_ping())] ping: Ping) { let mut bytes = BytesMut::default().writer(); ping.write_le(&mut bytes).unwrap(); - let decoded = Ping::::read_le(&mut bytes.into_inner().reader()).unwrap(); + let decoded = Ping::read_le(&mut bytes.into_inner().reader()).unwrap(); assert_eq!(ping, decoded); } } diff --git a/node/router/src/inbound.rs b/node/router/src/inbound.rs index d3d9e51ebd..5e422a8b7b 100644 --- a/node/router/src/inbound.rs +++ b/node/router/src/inbound.rs @@ -17,6 +17,8 @@ use crate::{ Outbound, PeerPoolHandling, messages::{ + BlockLocatorsRequest, + BlockLocatorsResponse, BlockRequest, BlockResponse, DataBlocks, @@ -28,6 +30,7 @@ use crate::{ UnconfirmedTransaction, }, }; +use snarkos_node_sync_locators::BlockLocators; use snarkos_node_tcp::protocols::Reading; use snarkvm::prelude::{ ConsensusVersion, @@ -36,7 +39,7 @@ use snarkvm::prelude::{ puzzle::Solution, }; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; use std::net::SocketAddr; use tokio::task::spawn_blocking; @@ -120,10 +123,9 @@ pub trait Inbound: Reading + Outbound { } let node = self.clone(); - match spawn_blocking(move || node.block_request(peer_ip, message)).await? { - true => Ok(true), - false => bail!("Peer '{peer_ip}' sent an invalid block request"), - } + spawn_blocking(move || node.block_request(peer_ip, message)) + .await? + .map_err(|err| anyhow!("Peer '{peer_ip}' sent an invalid block request: {err}")) } Message::BlockResponse(BlockResponse { request, latest_consensus_version, blocks, .. }) => { // Remove the block request, checking if this node previously sent a block request to this peer. @@ -150,10 +152,9 @@ pub trait Inbound: Reading + Outbound { // Process the block response. let node = self.clone(); - match spawn_blocking(move || node.block_response(peer_ip, blocks.0, latest_consensus_version)).await? { - true => Ok(true), - false => bail!("Peer '{peer_ip}' sent an invalid block response"), - } + spawn_blocking(move || node.block_response(peer_ip, blocks.0, latest_consensus_version)) + .await? + .map_err(|err| anyhow!("Peer '{peer_ip}' sent an invalid block response: {err}")) } Message::ChallengeRequest(..) | Message::ChallengeResponse(..) => { // Disconnect as the peer is not following the protocol. @@ -191,24 +192,22 @@ pub trait Inbound: Reading + Outbound { // If the peer is a client or validator, ensure there are block locators. let is_client_or_validator = message.node_type.is_client() || message.node_type.is_validator(); - if is_client_or_validator && message.block_locators.is_none() { - bail!("Peer '{peer_ip}' is a {}, but no block locators were provided", message.node_type); + if is_client_or_validator && message.block_height.is_none() { + bail!("Peer '{peer_ip}' is a {}, but no block height was provided", message.node_type); } // If the peer is a prover, ensure there are no block locators. - else if message.node_type.is_prover() && message.block_locators.is_some() { - bail!("Peer '{peer_ip}' is a prover, but block locators were provided"); + else if message.node_type.is_prover() && message.block_height.is_some() { + bail!("Peer '{peer_ip}' is a prover, but a block height was provided"); } // Process the ping message. - match self.ping(peer_ip, message) { - true => Ok(true), - false => bail!("Peer '{peer_ip}' sent an invalid ping"), - } + self.ping(peer_ip, message).await.with_context(|| format!("Peer '{peer_ip}' sent an invalid ping"))?; + Ok(true) + } + Message::Pong(message) => { + self.pong(peer_ip, message).with_context(|| format!("Peer '{peer_ip}' sent an invalid pong"))?; + Ok(true) } - Message::Pong(message) => match self.pong(peer_ip, message) { - true => Ok(true), - false => bail!("Peer '{peer_ip}' sent an invalid pong"), - }, Message::PuzzleRequest(..) => { // Insert the puzzle request for the peer, and fetch the recent frequency. let frequency = self.router().cache.insert_inbound_puzzle_request(peer_ip); @@ -303,19 +302,37 @@ pub trait Inbound: Reading + Outbound { false => bail!("Peer '{peer_ip}' sent an invalid unconfirmed transaction"), } } + Message::BlockLocatorsRequest(BlockLocatorsRequest { start_height, end_height }) => { + self.block_locators_request(peer_ip, start_height, end_height).await + } + Message::BlockLocatorsResponse(BlockLocatorsResponse { locators }) => { + self.block_locators_response(peer_ip, locators).await + } } } /// Handles a `BlockRequest` message. - fn block_request(&self, peer_ip: SocketAddr, _message: BlockRequest) -> bool; + fn block_request(&self, peer_ip: SocketAddr, message: BlockRequest) -> Result; /// Handles a `BlockResponse` message. + /// + /// Returns true if the response was valid fn block_response( &self, peer_ip: SocketAddr, blocks: Vec>, latest_consensus_version: Option, - ) -> bool; + ) -> Result; + + /// Handles a `BlockRequest` message. + /// + /// Returns true if the request was valid. + async fn block_locators_request(&self, peer_ip: SocketAddr, start_height: u32, end_height: u32) -> Result; + + /// Handles a `BlockResponse` message. + /// + /// Returns + async fn block_locators_response(&self, peer_ip: SocketAddr, locators: BlockLocators) -> Result; /// Handles a `PeerRequest` message. fn peer_request(&self, peer_ip: SocketAddr) -> bool { @@ -345,10 +362,10 @@ pub trait Inbound: Reading + Outbound { } /// Handles a `Ping` message. - fn ping(&self, peer_ip: SocketAddr, message: Ping) -> bool; + async fn ping(&self, peer_ip: SocketAddr, message: Ping) -> Result<()>; /// Sleeps for a period and then sends a `Ping` message to the peer. - fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> bool; + fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> Result<()>; /// Handles a `PuzzleRequest` message. fn puzzle_request(&self, peer_ip: SocketAddr) -> bool; diff --git a/node/router/src/lib.rs b/node/router/src/lib.rs index a9d1f9d39c..ef3b8c0397 100644 --- a/node/router/src/lib.rs +++ b/node/router/src/lib.rs @@ -44,7 +44,7 @@ pub use routing::*; mod writing; -use crate::messages::{BlockRequest, Message, MessageCodec}; +use crate::messages::{BlockLocatorsRequest, BlockRequest, Message, MessageCodec}; use snarkos_account::Account; use snarkos_node_bft_ledger_service::LedgerService; @@ -65,7 +65,7 @@ use snarkvm::prelude::{Address, Network, PrivateKey, ViewKey}; use aleo_std::StorageMode; use anyhow::Result; #[cfg(feature = "locktick")] -use locktick::parking_lot::{Mutex, RwLock}; +use locktick::{parking_lot::Mutex, parking_lot::RwLock}; #[cfg(not(feature = "locktick"))] use parking_lot::{Mutex, RwLock}; use std::{collections::HashMap, future::Future, io, net::SocketAddr, ops::Deref, sync::Arc, time::Duration}; @@ -146,7 +146,7 @@ impl Router { /// The minimum permitted interval between connection attempts for an IP; anything shorter is considered malicious. #[cfg(not(feature = "test"))] const CONNECTION_ATTEMPTS_SINCE_SECS: i64 = 10; - /// The maximum amount of connection attempts within a 10 second threshold + /// The maximum amount of connection attempts within a 10 second threshold. #[cfg(not(feature = "test"))] const MAX_CONNECTION_ATTEMPTS: usize = 10; /// The duration after which a connected peer is considered inactive or @@ -300,6 +300,11 @@ impl CommunicationService for Router { Message::BlockRequest(BlockRequest { start_height, end_height }) } + /// Prepare a block locators request to be sent + fn prepare_block_locators_request(start_height: u32, end_height: u32) -> Self::Message { + Message::BlockLocatorsRequest(BlockLocatorsRequest { start_height, end_height }) + } + /// Sends the given message to specified peer. /// /// This function returns as soon as the message is queued to be sent, diff --git a/node/router/src/writing.rs b/node/router/src/writing.rs index 95ac21c746..bcd68f383f 100644 --- a/node/router/src/writing.rs +++ b/node/router/src/writing.rs @@ -15,7 +15,7 @@ use super::*; -use snarkos_node_sync_locators::BlockLocators; +use messages::Ping; use snarkos_node_tcp::protocols::Writing; use std::io; @@ -26,8 +26,8 @@ impl Router { /// /// Returns false if the peer does not exist or disconnected. #[must_use] - pub fn send_ping(&self, peer_ip: SocketAddr, block_locators: Option>) -> bool { - let result = self.send(peer_ip, Message::Ping(messages::Ping::new(self.node_type(), block_locators))); + pub fn send_ping(&self, peer_ip: SocketAddr, block_height: Option) -> bool { + let result = self.send(peer_ip, Message::Ping(Ping::new::(self.node_type(), block_height))); result.is_some() } diff --git a/node/router/tests/common/router.rs b/node/router/tests/common/router.rs index 385eb7c28c..6272f1b381 100644 --- a/node/router/tests/common/router.rs +++ b/node/router/tests/common/router.rs @@ -14,6 +14,7 @@ // limitations under the License. use crate::common::sample_genesis_block; + use snarkos_node_network::{NodeType, Peer, PeerPoolHandling, Resolver}; use snarkos_node_router::{ Heartbeat, @@ -32,6 +33,7 @@ use snarkos_node_router::{ UnconfirmedTransaction, }, }; +use snarkos_node_sync::locators::BlockLocators; use snarkos_node_tcp::{ Connection, ConnectionSide, @@ -47,6 +49,7 @@ use snarkvm::prelude::{ puzzle::Solution, }; +use anyhow::Result; use async_trait::async_trait; #[cfg(feature = "locktick")] use locktick::parking_lot::RwLock; @@ -209,8 +212,8 @@ impl Inbound for TestRouter { } /// Handles a `BlockRequest` message. - fn block_request(&self, _peer_ip: SocketAddr, _message: BlockRequest) -> bool { - true + fn block_request(&self, _peer_ip: SocketAddr, _message: BlockRequest) -> Result { + Ok(true) } /// Handles a `BlockResponse` message. @@ -219,18 +222,26 @@ impl Inbound for TestRouter { _peer_ip: SocketAddr, _blocks: Vec>, _latest_consensus_version: Option, - ) -> bool { - true + ) -> Result { + Ok(true) + } + + async fn block_locators_request(&self, _peer_ip: SocketAddr, _start_height: u32, _end_height: u32) -> Result { + Ok(true) + } + + async fn block_locators_response(&self, _peer_ip: SocketAddr, _locators: BlockLocators) -> Result { + Ok(true) } /// Handles an `Ping` message. - fn ping(&self, _peer_ip: SocketAddr, _message: Ping) -> bool { - true + async fn ping(&self, _peer_ip: SocketAddr, _message: Ping) -> Result<()> { + Ok(()) } /// Handles an `Pong` message. - fn pong(&self, _peer_ip: SocketAddr, _message: Pong) -> bool { - true + fn pong(&self, _peer_ip: SocketAddr, _message: Pong) -> Result<()> { + Ok(()) } /// Handles an `PuzzleRequest` message. diff --git a/node/src/client/mod.rs b/node/src/client/mod.rs index a99a3a13ba..2e3cf25fea 100644 --- a/node/src/client/mod.rs +++ b/node/src/client/mod.rs @@ -173,8 +173,8 @@ impl> Client { let sync = Arc::new(BlockSync::new(ledger_service.clone())); // Set up the ping logic. - let locators = sync.get_block_locators()?; - let ping = Arc::new(Ping::new(router.clone(), locators)); + let current_height = ledger.latest_height(); + let ping = Arc::new(Ping::new(router.clone(), current_height)); // Initialize the node. let mut node = Self { @@ -293,7 +293,7 @@ impl> Client { /// Client-side version of [`snarkvm_node_bft::Sync::try_advancing_block_synchronization`]. async fn try_advancing_block_synchronization(&self) { - let has_new_blocks = match self.sync.try_advancing_block_synchronization().await { + let had_new_blocks = match self.sync.try_advancing_block_synchronization().await { Ok(val) => val, Err(err) => { error!("Block synchronization failed - {err}"); @@ -302,11 +302,9 @@ impl> Client { }; // If there are new blocks, we need to update the block locators. - if has_new_blocks { - match self.sync.get_block_locators() { - Ok(locators) => self.ping.update_block_locators(locators), - Err(err) => error!("Failed to get block locators: {err}"), - } + if had_new_blocks { + let height = self.ledger.latest_height(); + self.ping.update_block_height(height); } } @@ -340,9 +338,27 @@ impl> Client { return; } + // First, try to advance the ledger with new responses. + let has_new_blocks = match self.sync.try_advancing_block_synchronization().await { + Ok(val) => val, + Err(err) => { + error!("{err}"); + return; + } + }; + + if has_new_blocks { + self.ping.update_block_height(self.ledger.latest_height()); + + // If these were the last blocks to process, do not continue. + if !self.sync.can_block_sync() { + return; + } + } + // Prepare the block requests, if any. // In the process, we update the state of `is_block_synced` for the sync module. - let (block_requests, sync_peers) = self.sync.prepare_block_requests(); + let (block_requests, sync_peers) = self.sync.prepare_block_requests(self).await; // If there are no block requests, but there are pending block responses in the sync pool, // then try to advance the ledger using these pending block responses. diff --git a/node/src/client/router.rs b/node/src/client/router.rs index 7607248e2c..485ff6c9f2 100644 --- a/node/src/client/router.rs +++ b/node/src/client/router.rs @@ -18,6 +18,8 @@ use snarkos_node_network::PeerPoolHandling; use snarkos_node_router::{ Routing, messages::{ + BlockLocatorsRequest, + BlockLocatorsResponse, BlockRequest, BlockResponse, DataBlocks, @@ -30,13 +32,17 @@ use snarkos_node_router::{ UnconfirmedTransaction, }, }; + +use snarkos_node_sync::{communication_service::CommunicationService, locators::BlockLocators}; use snarkos_node_tcp::{Connection, ConnectionSide, Tcp}; + use snarkvm::{ console::network::{ConsensusVersion, Network}, ledger::{block::Transaction, narwhal::Data}, utilities::flatten_error, }; +use anyhow::{Context, bail, ensure}; use std::{io, net::SocketAddr}; impl> P2P for Client { @@ -141,6 +147,36 @@ impl> Client { } } +#[async_trait] +impl> CommunicationService for Client { + /// The message type. + type Message = Message; + + /// Prepares a block request to be sent. + fn prepare_block_request(start_height: u32, end_height: u32) -> Self::Message { + debug_assert!(start_height < end_height, "Invalid block request format"); + Message::BlockRequest(BlockRequest { start_height, end_height }) + } + + /// Prepares a block locators request to be sent. + fn prepare_block_locators_request(start_height: u32, end_height: u32) -> Self::Message { + Message::BlockLocatorsRequest(BlockLocatorsRequest { start_height, end_height }) + } + + /// Sends the given message to specified peer. + /// + /// This function returns as soon as the message is queued to be sent, + /// without waiting for the actual delivery; instead, the caller is provided with a [`oneshot::Receiver`] + /// which can be used to determine when and whether the message has been delivered. + async fn send( + &self, + peer_ip: SocketAddr, + message: Self::Message, + ) -> Option>> { + self.router().send(peer_ip, message) + } +} + #[async_trait] impl> Routing for Client {} @@ -177,34 +213,26 @@ impl> Inbound for Client { } /// Handles a `BlockRequest` message. - fn block_request(&self, peer_ip: SocketAddr, message: BlockRequest) -> bool { + fn block_request(&self, peer_ip: SocketAddr, message: BlockRequest) -> Result { let BlockRequest { start_height, end_height } = &message; // Get the latest consensus version, i.e., the one for the last block's height. - let latest_consensus_version = match N::CONSENSUS_VERSION(end_height.saturating_sub(1)) { - Ok(version) => version, - Err(err) => { - let err = err.context("Failed to retrieve consensus version"); - error!("{}", flatten_error(&err)); - return false; - } - }; + let latest_consensus_version = N::CONSENSUS_VERSION(end_height.saturating_sub(1)) + .with_context(|| format!("Failed to retrieve consensus version for height {end_height}"))?; // Retrieve the blocks within the requested range. - let blocks = match self.ledger.get_blocks(*start_height..*end_height) { - Ok(blocks) => DataBlocks(blocks), - Err(error) => { - let err = - error.context(format!("Failed to retrieve blocks {start_height} to {end_height} from the ledger")); - error!("{}", flatten_error(&err)); - return false; - } - }; + let blocks = self + .ledger + .get_blocks(*start_height..*end_height) + .with_context(|| format!("Failed to retrieve blocks {start_height} to {end_height} from the ledger"))?; // Send the `BlockResponse` message to the peer. - self.router() - .send(peer_ip, Message::BlockResponse(BlockResponse::new(message, blocks, latest_consensus_version))); - true + self.router().send( + peer_ip, + Message::BlockResponse(BlockResponse::new(message, DataBlocks(blocks), latest_consensus_version)), + ); + + Ok(true) } /// Handles a `BlockResponse` message. @@ -213,39 +241,35 @@ impl> Inbound for Client { peer_ip: SocketAddr, blocks: Vec>, latest_consensus_version: Option, - ) -> bool { + ) -> Result { // We do not need to explicitly sync here because insert_block_response, will wake up the sync task. - if let Err(err) = self.sync.insert_block_responses(peer_ip, blocks, latest_consensus_version) { - warn!("{}", flatten_error(err.context("Failed to insert block response"))); - false - } else { - true - } + self.sync + .insert_block_responses(peer_ip, blocks, latest_consensus_version) + .with_context(|| "Failed to insert block response")?; + Ok(true) } /// Processes the block locators and sends back a `Pong` message. - fn ping(&self, peer_ip: SocketAddr, message: Ping) -> bool { + async fn ping(&self, peer_ip: SocketAddr, message: Ping) -> Result<()> { // If block locators were provided, then update the peer in the sync pool. - if let Some(block_locators) = message.block_locators { + if let Some(height) = message.block_height { // Check the block locators are valid, and update the peer in the sync pool. - if let Err(err) = self.sync.update_peer_locators(peer_ip, &block_locators) { - warn!("{}", flatten_error(err.context(format!("Peer '{peer_ip}' sent invalid block locators")))); - return false; - } + let sync = self.sync.clone(); + sync.update_peer_block_height(peer_ip, height) + .with_context(|| format!("Peer '{peer_ip}' sent invalid block height"))?; - let last_peer_height = Some(block_locators.latest_locator_height()); - self.router().update_connected_peer(&peer_ip, |peer| peer.last_height_seen = last_peer_height); + self.router().update_connected_peer(&peer_ip, |peer| peer.last_height_seen = Some(height)); } // Send a `Pong` message to the peer. self.router().send(peer_ip, Message::Pong(Pong { is_fork: Some(false) })); - true + Ok(()) } /// Sleeps for a period and then sends a `Ping` message to the peer. - fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> bool { + fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> Result<()> { self.ping.on_pong_received(peer_ip); - true + Ok(()) } /// Retrieves the latest epoch hash and latest block header, and returns the puzzle response to the peer. @@ -314,4 +338,25 @@ impl> Inbound for Client { true // Maintain the connection } + + /// Handles a `BlockRequest` message. + async fn block_locators_request(&self, peer_ip: SocketAddr, start_height: u32, end_height: u32) -> Result { + ensure!(start_height < end_height, "Invalid block locators range"); + + let locators = self.sync.get_block_locators(start_height, end_height)?; + let event = Message::BlockLocatorsResponse(BlockLocatorsResponse { locators }); + + let Some(result) = self.send(peer_ip, event).await else { + bail!("Failed to send block locator response to peer {peer_ip}"); + }; + + result.await.with_context(|| "Tokio error")?.with_context(|| "Sending block request failed")?; + Ok(true) + } + + /// Handles a `BlockResponse` message. + async fn block_locators_response(&self, peer_ip: SocketAddr, locators: BlockLocators) -> Result { + self.sync.update_peer_block_locators(peer_ip, locators).await?; + Ok(true) + } } diff --git a/node/src/prover/router.rs b/node/src/prover/router.rs index 4c13198785..bc10806558 100644 --- a/node/src/prover/router.rs +++ b/node/src/prover/router.rs @@ -25,9 +25,11 @@ use snarkos_node_router::messages::{ PuzzleRequest, UnconfirmedTransaction, }; +use snarkos_node_sync::locators::BlockLocators; use snarkos_node_tcp::{Connection, ConnectionSide, Tcp}; use snarkvm::prelude::{ConsensusVersion, Field, Network, Zero, block::Transaction}; +use anyhow::{Context, Result, bail}; use std::{io, net::SocketAddr}; impl> P2P for Prover { @@ -118,13 +120,11 @@ impl> Routing for Prover {} impl> Heartbeat for Prover { /// This function updates the puzzle if network has updated. fn handle_puzzle_request(&self) { - // Find the sync peers. - if let Some((sync_peers, _)) = self.sync.find_sync_peers() { - // Choose the peer with the highest block height. - if let Some((peer_ip, _)) = sync_peers.into_iter().max_by_key(|(_, height)| *height) { - // Request the puzzle from the peer. - self.router().send(peer_ip, Message::PuzzleRequest(PuzzleRequest)); - } + // Get connected peers and request puzzle from any available peer. + let connected_peers = self.router().connected_peers(); + if let Some(peer_ip) = connected_peers.first() { + // Request the puzzle from the peer. + self.router().send(*peer_ip, Message::PuzzleRequest(PuzzleRequest)); } } } @@ -161,9 +161,8 @@ impl> Inbound for Prover { } /// Handles a `BlockRequest` message. - fn block_request(&self, peer_ip: SocketAddr, _message: BlockRequest) -> bool { - debug!("Disconnecting '{peer_ip}' for the following reason - {:?}", DisconnectReason::ProtocolViolation); - false + fn block_request(&self, peer_ip: SocketAddr, _message: BlockRequest) -> Result { + bail!("Disconnecting '{peer_ip}' for the following reason - {:?}", DisconnectReason::ProtocolViolation); } /// Handles a `BlockResponse` message. @@ -172,31 +171,40 @@ impl> Inbound for Prover { peer_ip: SocketAddr, _blocks: Vec>, _latest_consensus_version: Option, - ) -> bool { - debug!("Disconnecting '{peer_ip}' for the following reason - {:?}", DisconnectReason::ProtocolViolation); - false + ) -> Result { + bail!("Disconnecting '{peer_ip}' for the following reason - {:?}", DisconnectReason::ProtocolViolation); + } + + /// Handles a `BlocklocatorsRequest` message. + async fn block_locators_request(&self, peer_ip: SocketAddr, _start_height: u32, _end_height: u32) -> Result { + bail!("Disconnecting '{peer_ip}' for the following reason - {:?}", DisconnectReason::ProtocolViolation); + } + + /// Handles a `BlockLocatorsResponse` message. + async fn block_locators_response(&self, peer_ip: SocketAddr, _locators: BlockLocators) -> Result { + bail!("Disconnecting '{peer_ip}' for the following reason - {:?}", DisconnectReason::ProtocolViolation); } /// Processes the block locators and sends back a `Pong` message. - fn ping(&self, peer_ip: SocketAddr, message: Ping) -> bool { + async fn ping(&self, peer_ip: SocketAddr, message: Ping) -> Result<()> { // If block locators were provided, then update the peer in the sync pool. - if let Some(block_locators) = message.block_locators { + if let Some(height) = message.block_height { // Check the block locators are valid, and update the peer in the sync pool. - if let Err(error) = self.sync.update_peer_locators(peer_ip, &block_locators) { - warn!("Peer '{peer_ip}' sent invalid block locators: {error}"); - return false; - } + let sync = self.sync.clone(); + sync.update_peer_block_height(peer_ip, height) + .with_context(|| format!("Peer '{peer_ip}' sent invalid block locators"))?; } // Send a `Pong` message to the peer. self.router().send(peer_ip, Message::Pong(Pong { is_fork: Some(false) })); - true + + Ok(()) } /// Sleeps for a period and then sends a `Ping` message to the peer. - fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> bool { + fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> Result<()> { self.ping.on_pong_received(peer_ip); - true + Ok(()) } /// Disconnects on receipt of a `PuzzleRequest` message. diff --git a/node/src/validator/mod.rs b/node/src/validator/mod.rs index e9af06db2a..d7ff66046a 100644 --- a/node/src/validator/mod.rs +++ b/node/src/validator/mod.rs @@ -123,8 +123,7 @@ impl> Validator { // Initialize the block synchronization logic. let sync = Arc::new(BlockSync::new(ledger_service.clone())); - let locators = sync.get_block_locators()?; - let ping = Arc::new(Ping::new(router.clone(), locators)); + let ping = Arc::new(Ping::new(router.clone(), ledger.latest_height())); // Initialize the consensus layer. let consensus = Consensus::new( diff --git a/node/src/validator/router.rs b/node/src/validator/router.rs index a8ad2026ba..73e1577342 100644 --- a/node/src/validator/router.rs +++ b/node/src/validator/router.rs @@ -14,8 +14,10 @@ // limitations under the License. use super::*; + use snarkos_node_network::PeerPoolHandling; use snarkos_node_router::messages::{ + BlockLocatorsResponse, BlockRequest, BlockResponse, DataBlocks, @@ -26,13 +28,16 @@ use snarkos_node_router::messages::{ Pong, UnconfirmedTransaction, }; +use snarkos_node_sync::locators::BlockLocators; use snarkos_node_tcp::{Connection, ConnectionSide, Tcp}; + use snarkvm::{ console::network::{ConsensusVersion, Network}, ledger::{block::Transaction, narwhal::Data}, - utilities::{flatten_error, io_error}, + utilities::io_error, }; +use anyhow::{Context, bail, ensure}; use std::{io, net::SocketAddr}; impl> P2P for Validator { @@ -65,13 +70,13 @@ where { async fn on_connect(&self, peer_addr: SocketAddr) { // Resolve the peer address to the listener address. - if let Some(listener_addr) = self.router().resolve_to_listener(peer_addr) { - if let Some(peer) = self.router().get_connected_peer(listener_addr) { - if peer.node_type != NodeType::BootstrapClient { - // Send the first `Ping` message to the peer. - self.ping.on_peer_connected(listener_addr); - } - } + let Some(listener_addr) = self.router.resolve_to_listener(peer_addr) else { return }; + + if let Some(peer) = self.router().get_connected_peer(listener_addr) + && peer.node_type != NodeType::BootstrapClient + { + // Send the first `Ping` message to the peer. + self.ping.on_peer_connected(listener_addr); } } } @@ -173,32 +178,25 @@ impl> Inbound for Validator { } /// Retrieves the blocks within the block request range, and returns the block response to the peer. - fn block_request(&self, peer_ip: SocketAddr, message: BlockRequest) -> bool { + fn block_request(&self, peer_ip: SocketAddr, message: BlockRequest) -> Result { let BlockRequest { start_height, end_height } = &message; // Get the latest consensus version, i.e., the one for the last block's height. - let latest_consensus_version = match N::CONSENSUS_VERSION(end_height.saturating_sub(1)) { - Ok(version) => version, - Err(err) => { - error!("{}", flatten_error(err.context("Failed to retrieve consensus version"))); - return false; - } - }; + let latest_consensus_version = N::CONSENSUS_VERSION(end_height.saturating_sub(1)) + .with_context(|| format!("Failed to retrieve consensus version for height {end_height}"))?; // Retrieve the blocks within the requested range. - let blocks = match self.ledger.get_blocks(*start_height..*end_height) { - Ok(blocks) => DataBlocks(blocks), - Err(err) => { - let err = - err.context(format!("Failed to retrieve blocks {start_height} to {end_height} from the ledger")); - error!("{}", flatten_error(err)); - return false; - } - }; + let blocks = self + .ledger + .get_blocks(*start_height..*end_height) + .with_context(|| format!("Failed to retrieve blocks {start_height} to {end_height} from the ledger"))?; + // Send the `BlockResponse` message to the peer. - self.router() - .send(peer_ip, Message::BlockResponse(BlockResponse::new(message, blocks, latest_consensus_version))); - true + self.router().send( + peer_ip, + Message::BlockResponse(BlockResponse::new(message, DataBlocks(blocks), latest_consensus_version)), + ); + Ok(true) } /// Handles a `BlockResponse` message. @@ -207,25 +205,24 @@ impl> Inbound for Validator { peer_ip: SocketAddr, _blocks: Vec>, _latest_consensus_version: Option, - ) -> bool { - warn!("Received a block response through P2P, not BFT, from {peer_ip}"); - false + ) -> Result { + bail!("Received a block response through P2P, not BFT, from {peer_ip}"); } /// Processes a ping message from a client (or prover) and sends back a `Pong` message. - fn ping(&self, peer_ip: SocketAddr, _message: Ping) -> bool { + async fn ping(&self, peer_ip: SocketAddr, _message: Ping) -> Result<()> { // In gateway/validator mode, we do not need to process client block locators. // Instead, locators are fetched from other validators in `Gateway` using `PrimaryPing` messages. // Send a `Pong` message to the peer. self.router().send(peer_ip, Message::Pong(Pong { is_fork: Some(false) })); - true + Ok(()) } /// Process a Pong message (response to a Ping). - fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> bool { + fn pong(&self, peer_ip: SocketAddr, _message: Pong) -> Result<()> { self.ping.on_pong_received(peer_ip); - true + Ok(()) } /// Retrieves the latest epoch hash and latest block header, and returns the puzzle response to the peer. @@ -286,4 +283,27 @@ impl> Inbound for Validator { self.propagate_to_validators(message, &[peer_ip]); true } + + /// Handles a `BlockLocatorsRequest` message. + async fn block_locators_request(&self, peer_ip: SocketAddr, start_height: u32, end_height: u32) -> Result { + ensure!(start_height < end_height, "Invalid block locators range"); + + let locators = self.sync.get_block_locators(start_height, end_height)?; + let event = Message::BlockLocatorsResponse(BlockLocatorsResponse { locators }); + + if let Some(result) = self.router.send(peer_ip, event) { + if let Err(err) = result.await { + bail!("Send failed: {err}"); + } + } else { + bail!("Failed to send block locator response to peer {peer_ip}"); + } + Ok(true) + } + + /// Handles a `BlockResponse` message. + async fn block_locators_response(&self, peer_ip: SocketAddr, locators: BlockLocators) -> Result { + self.sync.update_peer_block_locators(peer_ip, locators).await?; + Ok(true) + } } diff --git a/node/sync/Cargo.toml b/node/sync/Cargo.toml index a2a95ec252..14c5bd78ea 100644 --- a/node/sync/Cargo.toml +++ b/node/sync/Cargo.toml @@ -26,7 +26,7 @@ locktick = [ serial = ["snarkos-node-bft-ledger-service/serial"] metrics = [ "dep:snarkos-node-metrics" ] cuda = [ "snarkvm/cuda", "snarkos-node-bft-ledger-service/cuda", "snarkos-node-router/cuda" ] -test = [ "snarkos-node-sync-locators/test" ] +test-helpers = [ "snarkos-node-sync-locators/test-helpers" ] [dependencies.anyhow] workspace = true @@ -89,9 +89,9 @@ workspace = true workspace = true features = [ "test" ] -[dev-dependencies.snarkos-node-sync-locators] +[dev-dependencies.snarkos-node-sync] workspace = true -features = [ "test" ] +features = [ "test-helpers" ] [dev-dependencies.snarkos-node-tcp] workspace = true diff --git a/node/sync/communication-service/src/lib.rs b/node/sync/communication-service/src/lib.rs index 7793ab8a51..1478a7af50 100644 --- a/node/sync/communication-service/src/lib.rs +++ b/node/sync/communication-service/src/lib.rs @@ -32,6 +32,9 @@ pub trait CommunicationService: Send + Sync { /// Generates the service-specific message for a block request. fn prepare_block_request(start: u32, end: u32) -> Self::Message; + /// Generates the service-specific message for a block locators request. + fn prepare_block_locators_request(start: u32, end: u32) -> Self::Message; + /// Sends the given message to specified peer. /// /// This function returns as soon as the message is queued to be sent, @@ -60,6 +63,10 @@ pub mod test_helpers { Self::Message {} } + fn prepare_block_locators_request(_start: u32, _end: u32) -> Self::Message { + Self::Message {} + } + async fn send( &self, _peer_ip: SocketAddr, diff --git a/node/sync/locators/Cargo.toml b/node/sync/locators/Cargo.toml index f5f62cd644..a84ccfa7b9 100644 --- a/node/sync/locators/Cargo.toml +++ b/node/sync/locators/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" [features] default = [ ] -test = [ ] +test-helpers = [ ] [dependencies.anyhow] workspace = true diff --git a/node/sync/locators/README.md b/node/sync/locators/README.md index fb656dd523..774fb242db 100644 --- a/node/sync/locators/README.md +++ b/node/sync/locators/README.md @@ -5,30 +5,33 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE.md) The `snarkos-node-sync-locators` crate provides _block locators_, -which are data structures used by nodes to advertise to other nodes the blocks in their possession, -which can be provided to other nodes to help the latter -sync their blockchain with the rest of the network. - -In general, a block is uniquely identified by its height, -i.e. its position in the blockchain, -starting with 0 for the genesis block, -up to $N-1$ for the latest block, -if $N$ is the number of blocks in the blockchain. -A single block locator consists of a block height and a block hash; -the hash is conceptually redundant, -but it is used by nodes to check some level of consistency -among the block locators from different nodes. - -The `BlockLocators` struct in this crate is a collection of block locators, -organized as two maps from block heights to block hashes: -a `checkpoints` map, and a `recents` map, -which can be illustrated as follows. -![Block Locators](block-locators.png) -The rectangular bar represents the whole blockchain; -each circle represents a block locator. -See the documentation of `BlockLocators` for details. +which are data structures used by nodes to advertise to other nodes the blocks in their possession. +Block locators are then provided to other nodes to help the latter sync their blockchain with the rest of the network. + +In the absence of Byzantine failures, e.g., malicious nodes, a block's height, i.e. its position in the chain, uniquely identifies it, because it is impossible for there to be forks or conflicting blocks. +However, such assumptions cannot be made in production and a attacker may attempt to propagate an invalid block. +Block locators protect against this by including the hash of a block in addition to its height. +If two nodes advertise the same block has for the same height, we know with very high probability that they advertise the same block. +As a result, each block locator contains not only a block's height, but also a block's hash. + +The `BlockLocators` struct in this crate represents a set of block locators. +More concretely `BlockLocators` contains a continuous sequence of block locators starting at some block height. -Besides the `BlockLocators` struct, this crate provides operations -to construct block locators, -to check them for well-formedness and consistency, +Besides the `BlockLocators` struct, this crate provides operations +to construct block locators, to check them for well-formedness and consistency, and to serialize and deserialize them to and from bytes. + +## Usage During Sync + +These locators are generated on demand when a peer requests locators for a specific range. +Peers will request specific block locator ranges based on their current sync height and the maximum locator height a node advertises in its Ping message. + +## Previous BlockLocator Design +In the past, BlockLocators were considerably more complex. +They were organized as two maps from block heights to block hashes: a `checkpoints` map and a `recents` map, which the following figure illustrates. + +![Block Locators](block-locators.png) +The rectangular bar represents the whole blockchain; each circle represents a block locator. + +This enabled checking consistency between block locators to detect malicious peers. +snarkOS has since moved to verifying every block on sync, so such checks are no longer needed. diff --git a/node/sync/locators/src/block_locators.rs b/node/sync/locators/src/block_locators.rs index 120d9e0788..fa7adcd4a2 100644 --- a/node/sync/locators/src/block_locators.rs +++ b/node/sync/locators/src/block_locators.rs @@ -16,18 +16,10 @@ use snarkvm::prelude::{FromBytes, IoResult, Network, Read, ToBytes, Write, error, has_duplicates}; use anyhow::{Result, bail, ensure}; -use indexmap::{IndexMap, indexmap}; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, btree_map::IntoIter}; -/// The number of recent blocks (near tip). -pub const NUM_RECENT_BLOCKS: usize = 100; // 100 blocks -/// The interval between recent blocks. -const RECENT_INTERVAL: u32 = 1; // 1 block intervals -/// The interval between block checkpoints. -pub const CHECKPOINT_INTERVAL: u32 = 10_000; // 10,000 block intervals -// The maximum number of checkpoints that there can be -const MAX_CHECKPOINTS: usize = (u32::MAX / CHECKPOINT_INTERVAL) as usize; +/// The maximum number of block hashes within a single locator. +pub const MAX_LOCATOR_SIZE: usize = 100; // 100 blocks /// Block locator maps. /// @@ -41,13 +33,13 @@ const MAX_CHECKPOINTS: usize = (u32::MAX / CHECKPOINT_INTERVAL) as usize; /// /// If a validator has `N` blocks, the `recents` and `checkpoints` maps are as follows: /// - The `recents` map contains entries for blocks at heights -/// `N - 1 - (NUM_RECENT_BLOCKS - 1) * RECENT_INTERVAL`, -/// `N - 1 - (NUM_RECENT_BLOCKS - 2) * RECENT_INTERVAL`, +/// `N - 1 - (NUM_RECENT_BLOCKS - 1)`, +/// `N - 1 - (NUM_RECENT_BLOCKS - 2)`, /// ..., /// `N - 1`. /// If any of the just listed heights are negative, there are no entries for them of course, /// and the `recents` map has fewer than `NUM_RECENT_BLOCKS` entries. -/// If `RECENT_INTERVAL` is 1, the `recents` map contains entries +/// The `recents` map contains entries /// for the last `NUM_RECENT_BLOCKS` blocks, i.e. from `N - NUM_RECENT_BLOCKS` to `N - 1`; /// if additionally `N < NUM_RECENT_BLOCKS`, the `recents` map contains /// entries for all the blocks, from `0` to `N - 1`. @@ -72,57 +64,57 @@ const MAX_CHECKPOINTS: usize = (u32::MAX / CHECKPOINT_INTERVAL) as usize; /// So this well-formedness is an invariant of `BlockLocators` instances. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BlockLocators { - /// The map of recent blocks. - pub recents: IndexMap, - /// The map of block checkpoints. - pub checkpoints: IndexMap, + pub start_height: u32, + pub block_hashes: Vec, } impl BlockLocators { /// Initializes a new instance of the block locators, checking the validity of the block locators. - pub fn new(recents: IndexMap, checkpoints: IndexMap) -> Result { - // Construct the block locators. - let locators = Self { recents, checkpoints }; - // Ensure the block locators are well-formed. - locators.ensure_is_valid()?; - // Return the block locators. - Ok(locators) - } - - /// Initializes a new instance of the block locators, without checking the validity of the block locators. - /// This is only used for testing; note that it is non-public. - #[cfg(test)] - fn new_unchecked(recents: IndexMap, checkpoints: IndexMap) -> Self { - Self { recents, checkpoints } + pub fn new(start_height: u32, block_hashes: Vec) -> Result { + Ok(Self { start_height, block_hashes }) } /// Initializes a new genesis instance of the block locators. pub fn new_genesis(genesis_hash: N::BlockHash) -> Self { - Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] } + Self { start_height: 0, block_hashes: vec![genesis_hash] } } } impl IntoIterator for BlockLocators { - type IntoIter = IntoIter; + type IntoIter = as IntoIterator>::IntoIter; type Item = (u32, N::BlockHash); - // TODO (howardwu): Consider using `BTreeMap::from_par_iter` if it is more performant. - // Check by sorting 300-1000 items and comparing the performance. - // (https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html#method.from_par_iter) fn into_iter(self) -> Self::IntoIter { - BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter() + let data: Vec<_> = self + .block_hashes + .into_iter() + .enumerate() + .map(|(idx, hash)| (self.start_height + (idx as u32), hash)) + .collect(); + + data.into_iter() } } impl BlockLocators { - /// Returns the latest locator height. - pub fn latest_locator_height(&self) -> u32 { - self.recents.keys().last().copied().unwrap_or_default() + /// The first height in this set of locators. + pub fn start_height(&self) -> u32 { + self.start_height + } + + /// The last height in this set of locators (inclusive). + pub fn end_height(&self) -> u32 { + self.start_height + (self.block_hashes.len() as u32) - 1 } /// Returns the block hash for the given block height, if it exists. pub fn get_hash(&self, height: u32) -> Option { - self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied()) + if height < self.start_height { + return None; + } + + let index = (height - self.start_height) as usize; + self.block_hashes.get(index).copied() } /// Returns `true` if the block locators are well-formed. @@ -148,8 +140,15 @@ impl BlockLocators { /// Checks that this block locators instance is well-formed. pub fn ensure_is_valid(&self) -> Result<()> { - // Ensure the block locators are well-formed. - Self::check_block_locators(&self.recents, &self.checkpoints) + if self.block_hashes.is_empty() { + bail!("Block locators cannot be empty!"); + } + + if has_duplicates(&self.block_hashes) { + bail!("Block locators cannot contain duplicate hashes!"); + } + + Ok(()) } /// Returns `true` if the given block locators are consistent with this one. @@ -166,226 +165,67 @@ impl BlockLocators { old_locators: &BlockLocators, new_locators: &BlockLocators, ) -> Result<()> { - // For the overlapping recent blocks, ensure their block hashes match. - for (height, hash) in new_locators.recents.iter() { - if let Some(recent_hash) = old_locators.recents.get(height) { - if recent_hash != hash { - bail!("Recent block hash mismatch at height {height}") - } - } - } - // For the overlapping block checkpoints, ensure their block hashes match. - for (height, hash) in new_locators.checkpoints.iter() { - if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) { - if checkpoint_hash != hash { - bail!("Block checkpoint hash mismatch for height {height}") - } - } - } - Ok(()) - } - - /// Checks that the block locators are well-formed. - pub fn check_block_locators( - recents: &IndexMap, - checkpoints: &IndexMap, - ) -> Result<()> { - // Ensure the recent blocks are well-formed. - let last_recent_height = Self::check_recent_blocks(recents)?; - // Ensure the block checkpoints are well-formed. - let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?; - - // Ensure that `last_checkpoint_height` is - // the largest multiple of `CHECKPOINT_INTERVAL` that does not exceed `last_recent_height`. - // That is, we must have - // `last_checkpoint_height <= last_recent_height < last_checkpoint_height + CHECKPOINT_INTERVAL`. - // Although we do not expect to run out of `u32` for block heights, - // `last_checkpoint_height` is an untrusted value that may come from a faulty validator, - // and thus we use a saturating addition; - // only a faulty validator would send block locators with such high block heights, - // under the assumption that the blockchain is always well below the `u32` limit for heights. - if !(last_checkpoint_height..last_checkpoint_height.saturating_add(CHECKPOINT_INTERVAL)) - .contains(&last_recent_height) + if old_locators.end_height() < new_locators.start_height() + || new_locators.end_height() < old_locators.start_height() { - bail!( - "Last checkpoint height ({last_checkpoint_height}) is not the largest multiple of \ - {CHECKPOINT_INTERVAL} that does not exceed the last recent height ({last_recent_height})" - ) - } - - // Ensure that if the recents and checkpoints maps overlap, they agree on the hash: - // we calculate the distance from the last recent to the last checkpoint; - // if that distance is `NUM_RECENT_BLOCKS` or more, there is no overlap; - // otherwise, the overlap is at the last checkpoint, - // which is exactly at the last recent height minus its distance from the last checkpoint. - // All of this also works if the last checkpoint is 0: - // in this case, there is an overlap (at 0) exactly when the last recent height, - // which is the same as its distance from the last checkpoint (0), - // is less than `NUM_RECENT_BLOCKS`. - // All of this only works if `NUM_RECENT_BLOCKS < CHECKPOINT_INTERVAL`, - // because it is only under this condition that there is at most one overlapping height. - // TODO: generalize check for RECENT_INTERVAL > 1, or remove this comment if we hardwire that to 1 - let last_recent_to_last_checkpoint_distance = last_recent_height % CHECKPOINT_INTERVAL; - if last_recent_to_last_checkpoint_distance < NUM_RECENT_BLOCKS as u32 { - let common = last_recent_height - last_recent_to_last_checkpoint_distance; - if recents.get(&common).unwrap() != checkpoints.get(&common).unwrap() { - bail!("Recent block hash and checkpoint hash mismatch at height {common}") - } - } - - Ok(()) - } - - /// Checks the recent blocks, returning the last block height from the map. - /// - /// This function checks the following: - /// 1. The map is not empty. - /// 2. The map is at the correct interval. - /// 3. The map is at the correct height. - /// 4. The map is in the correct order. - /// 5. The map does not contain too many entries. - fn check_recent_blocks(recents: &IndexMap) -> Result { - // Ensure the number of recent blocks is at least 1. - if recents.is_empty() { - bail!("There must be at least 1 recent block") - } - // Ensure the number of recent blocks is at most NUM_RECENT_BLOCKS. - // This redundant check ensures we early exit if the number of recent blocks is too large. - if recents.len() > NUM_RECENT_BLOCKS { - bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map") - } - - // Ensure the given recent blocks increment in height, and at the correct interval. - let mut last_height = 0; - for (i, current_height) in recents.keys().enumerate() { - if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height > 0 { - bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0") - } - if i > 0 && *current_height <= last_height { - bail!("Recent blocks must increment in height") - } - if i > 0 && *current_height - last_height != RECENT_INTERVAL { - bail!("Recent blocks must increment by {RECENT_INTERVAL}") - } - last_height = *current_height; + return Ok(()); } - // At this point, if last_height < NUM_RECENT_BLOCKS`, - // we know that the `recents` map consists of exactly block heights from 0 to last_height, - // because the loop above has ensured that the first entry is for height 0, - // and at the end of the loop `last_height` is the last key in `recents`, - // and all the keys in `recents` are consecutive in increments of 1. - // So the `recents` map consists of NUM_RECENT_BLOCKS or fewer entries. - - // If last height >= NUM_RECENT_BLOCKS, ensure the number of recent blocks matches NUM_RECENT_BLOCKS. - // TODO: generalize check for RECENT_INTERVAL > 1, or remove this comment if we hardwire that to 1 - if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS { - bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}") - } + // Figure out the range the locators overlap + let start_height = old_locators.start_height().max(new_locators.start_height()); + let end_height = old_locators.end_height().min(new_locators.end_height()); - // Ensure the block hashes are unique. - if has_duplicates(recents.values()) { - bail!("Recent block hashes must be unique") - } - - Ok(last_height) - } + let mut old_idx = (start_height - old_locators.start_height()) as usize; + let mut new_idx = (start_height - new_locators.start_height()) as usize; - /// Checks the block checkpoints, returning the last block height from the checkpoints. - /// - /// This function checks the following: - /// 1. The block checkpoints are not empty. - /// 2. The block checkpoints are at the correct interval. - /// 3. The block checkpoints are at the correct height. - /// 4. The block checkpoints are in the correct order. - fn check_block_checkpoints(checkpoints: &IndexMap) -> Result { - // Ensure the block checkpoints are not empty. - ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint"); - - // Ensure the given checkpoints increment in height, and at the correct interval. - let mut last_height = 0; - for (i, current_height) in checkpoints.keys().enumerate() { - if i == 0 && *current_height != 0 { - bail!("First block checkpoint must be at height 0") - } - if i > 0 && *current_height <= last_height { - bail!("Block checkpoints must increment in height") - } - if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL { - bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}") - } - last_height = *current_height; - } - - // Ensure the block hashes are unique. - if has_duplicates(checkpoints.values()) { - bail!("Block checkpoints must be unique") + for _ in start_height..end_height { + ensure!( + old_locators.block_hashes[old_idx] == new_locators.block_hashes[new_idx], + "Block hashes do not match" + ); + old_idx += 1; + new_idx += 1; } - Ok(last_height) + Ok(()) } } impl FromBytes for BlockLocators { fn read_le(mut reader: R) -> IoResult { - // Read the number of recent block hashes. - let num_recents = u32::read_le(&mut reader)?; - // Ensure the number of recent blocks is within bounds - if num_recents as usize > NUM_RECENT_BLOCKS { - return Err(error(format!( - "Number of recent blocks ({num_recents}) is greater than the maximum ({NUM_RECENT_BLOCKS})" - ))); - } - // Read the recent block hashes. - let mut recents = IndexMap::with_capacity(num_recents as usize); - for _ in 0..num_recents { - let height = u32::read_le(&mut reader)?; - let hash = N::BlockHash::read_le(&mut reader)?; - recents.insert(height, hash); - } + // Read the number of recent block hashes and start height. + let num_blocks = u32::read_le(&mut reader)?; + let start_height = u32::read_le(&mut reader)?; - // Read the number of checkpoints. - let num_checkpoints = u32::read_le(&mut reader)?; - // Ensure the number of checkpoints is within bounds - if num_checkpoints as usize > MAX_CHECKPOINTS { - return Err(error(format!( - "Number of checkpoints ({num_checkpoints}) is greater than the maximum ({MAX_CHECKPOINTS})" - ))); - } - // Read the checkpoints. - let mut checkpoints = IndexMap::new(); - for _ in 0..num_checkpoints { - let height = u32::read_le(&mut reader)?; - let hash = N::BlockHash::read_le(&mut reader)?; - checkpoints.insert(height, hash); + // Read the recent block hashes. + let mut hashes = Vec::with_capacity(num_blocks as usize); + for _ in 0..num_blocks { + hashes.push(N::BlockHash::read_le(&mut reader)?); } - Self::new(recents, checkpoints).map_err(error) + Self::new(start_height, hashes).map_err(error) } } impl ToBytes for BlockLocators { fn write_le(&self, mut writer: W) -> IoResult<()> { - // Write the number of recent block hashes. - u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?; - // Write the recent block hashes. - for (height, hash) in &self.recents { - height.write_le(&mut writer)?; - hash.write_le(&mut writer)?; - } + // Write the number of blocks + let num_blocks = self.block_hashes.len() as u32; + num_blocks.write_le(&mut writer)?; + + // Write the start height. + self.start_height.write_le(&mut writer)?; - // Write the number of checkpoints. - u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?; - // Write the checkpoints. - for (height, hash) in &self.checkpoints { - height.write_le(&mut writer)?; + // Write the hashes + for hash in &self.block_hashes { hash.write_le(&mut writer)?; } + Ok(()) } } -#[cfg(any(test, feature = "test"))] +#[cfg(any(test, feature = "test-helpers"))] pub mod test_helpers { use super::*; use snarkvm::prelude::Field; @@ -395,61 +235,35 @@ pub mod test_helpers { /// Simulates a block locator at the given height. /// /// The returned block locator is checked to be well-formed. - pub fn sample_block_locators(height: u32) -> BlockLocators { - // Create the recent locators. - let mut recents = IndexMap::new(); - let recents_range = match height < NUM_RECENT_BLOCKS as u32 { - true => 0..=height, - false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height, - }; - for i in recents_range { - recents.insert(i, (Field::::from_u32(i)).into()); - } - - // Create the checkpoint locators. - let mut checkpoints = IndexMap::new(); - for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) { - checkpoints.insert(i, (Field::::from_u32(i)).into()); - } + pub fn sample_block_locators(start: u32, end: u32) -> BlockLocators { + // Create the block hashes + let hashes: Vec<_> = (start..=end).map(|i| Field::::from_u32(i).into()).collect(); - // Construct the block locators. - BlockLocators::new(recents, checkpoints).unwrap() + BlockLocators::new(start, hashes).unwrap() } /// Simulates a block locator at the given height, with a fork within NUM_RECENT_BLOCKS of the given height. /// /// The returned block locator is checked to be well-formed. - pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators { - assert!(fork_height <= height, "Fork height must be less than or equal to the given height"); - assert!( - height - fork_height < NUM_RECENT_BLOCKS as u32, - "Fork must be within NUM_RECENT_BLOCKS of the given height" - ); + pub fn sample_block_locators_with_fork(start: u32, end: u32, fork_height: u32) -> BlockLocators { + assert!(fork_height <= end, "Fork must be in the given range"); + assert!(fork_height >= start, "Fork must be in the given range"); // Create the recent locators. - let mut recents = IndexMap::new(); - let recents_range = match height < NUM_RECENT_BLOCKS as u32 { - true => 0..=height, - false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height, - }; - for i in recents_range { - if i >= fork_height { - recents.insert(i, (-Field::::from_u32(i)).into()); - } else { - recents.insert(i, (Field::::from_u32(i)).into()); - } - } - - // Create the checkpoint locators. - let mut checkpoints = IndexMap::new(); - for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) { - checkpoints.insert(i, (Field::::from_u32(i)).into()); - } + let hashes: Vec<_> = (start..=end) + .map(|i| { + if i >= fork_height { + (-Field::::from_u32(i)).into() + } else { + Field::::from_u32(i).into() + } + }) + .collect(); - // Construct the block locators. - BlockLocators::new(recents, checkpoints).unwrap() + BlockLocators::new(start, hashes).unwrap() } + /* /// A test to ensure that the sample block locators are valid. #[test] fn test_sample_block_locators() { @@ -469,9 +283,10 @@ pub mod test_helpers { // Note that `sample_block_locators` always returns well-formed block locators, // so we don't need to check `is_valid()` here. } - } + }*/ } +/* #[cfg(test)] mod tests { use super::*; @@ -524,7 +339,7 @@ mod tests { recents.insert(height + i, (Field::::from_u32(height + i)).into()); let block_locators = - BlockLocators::::new_unchecked(recents.clone(), checkpoints.clone()); + BlockLocators::::new_unchecked(recents.clone()e, checkpoints.clone()); block_locators.ensure_is_consistent_with(&block_locators).unwrap(); // Only test consistency when the block locators are valid to begin with. @@ -690,3 +505,4 @@ mod tests { wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err(); } } +*/ diff --git a/node/sync/src/block_sync.rs b/node/sync/src/block_sync.rs index 61d1af034d..5d7537954c 100644 --- a/node/sync/src/block_sync.rs +++ b/node/sync/src/block_sync.rs @@ -21,7 +21,6 @@ use snarkos_node_bft_ledger_service::LedgerService; use snarkos_node_network::PeerPoolHandling; use snarkos_node_router::messages::DataBlocks; use snarkos_node_sync_communication_service::CommunicationService; -use snarkos_node_sync_locators::{CHECKPOINT_INTERVAL, NUM_RECENT_BLOCKS}; use snarkvm::{ console::network::{ConsensusVersion, Network}, @@ -33,11 +32,9 @@ use anyhow::{Result, bail, ensure}; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; #[cfg(feature = "locktick")] -use locktick::parking_lot::RwLock; -#[cfg(feature = "locktick")] -use locktick::tokio::Mutex as TMutex; +use locktick::{parking_lot::Mutex, parking_lot::RwLock, tokio::Mutex as TMutex}; #[cfg(not(feature = "locktick"))] -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use rand::seq::{IteratorRandom, SliceRandom}; use std::{ collections::{BTreeMap, HashMap, HashSet, hash_map}, @@ -45,6 +42,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; + #[cfg(not(feature = "locktick"))] use tokio::sync::Mutex as TMutex; use tokio::sync::Notify; @@ -131,6 +129,35 @@ impl OutstandingRequest { } } +struct BlockHeights { + /// Advertised block height and last requested sync height for each peers. + peer_heights: HashMap, + /// The position at which we are syncing right now. + sync_height: u32, +} + +enum RemovePeerHeightResult { + Unchanged, + NoSuchPeer, + NewHeight(u32), + NoPeersLeft, +} + +impl BlockHeights { + /// Removes a peer and its peer height. + fn remove_peer(&mut self, peer_ip: &SocketAddr) -> RemovePeerHeightResult { + let Some((removed, _)) = self.peer_heights.remove(peer_ip) else { + return RemovePeerHeightResult::NoSuchPeer; + }; + + let Some(new_max) = self.peer_heights.values().map(|(h, _)| h).max().copied() else { + return RemovePeerHeightResult::NoPeersLeft; + }; + + if new_max < removed { RemovePeerHeightResult::NewHeight(new_max) } else { RemovePeerHeightResult::Unchanged } + } +} + /// A struct that keeps track of synchronizing blocks with other nodes. /// /// It generates requests to send to other peers and processes responses to those requests. @@ -176,6 +203,9 @@ pub struct BlockSync { /// Tracks sync speed metrics: BlockSyncMetrics, + + /// The peer heights and current sync height. + block_heights: Arc>, } impl BlockSync { @@ -183,12 +213,14 @@ impl BlockSync { pub fn new(ledger: Arc>) -> Self { // Make sync state aware of the blocks that already exist on disk at startup. let sync_state = SyncState::new_with_height(ledger.latest_block_height()); + let block_heights = BlockHeights { peer_heights: Default::default(), sync_height: 0 }; Self { ledger, sync_state: RwLock::new(sync_state), peer_notify: Default::default(), response_notify: Default::default(), + block_heights: Arc::new(Mutex::new(block_heights)), locators: Default::default(), requests: Default::default(), common_ancestors: Default::default(), @@ -262,7 +294,7 @@ impl BlockSync { //// Returns the latest locator height for all known peers. pub fn get_peer_heights(&self) -> HashMap { - self.locators.read().iter().map(|(addr, locators)| (*addr, locators.latest_locator_height())).collect() + self.block_heights.lock().peer_heights.iter().map(|(addr, (h, _))| (*addr, *h)).collect() } //// Returns information about all in-flight block requests. @@ -301,20 +333,19 @@ impl BlockSync { pub fn get_sync_speed(&self) -> f64 { self.metrics.get_sync_speed() } -} -// Helper functions needed for testing -#[cfg(test)] -impl BlockSync { /// Returns the latest block height of the given peer IP. - fn get_peer_height(&self, peer_ip: &SocketAddr) -> Option { - self.locators.read().get(peer_ip).map(|locators| locators.latest_locator_height()) + pub fn get_peer_height(&self, peer_ip: &SocketAddr) -> Option { + self.block_heights.lock().peer_heights.get(peer_ip).map(|(h, _)| *h) } +} - /// Returns the common ancestor for the given peer pair, if it exists. +#[cfg(test)] +impl BlockSync { + /* /// Returns the common ancestor for the given peer pair, if it exists. fn get_common_ancestor(&self, peer_a: SocketAddr, peer_b: SocketAddr) -> Option { self.common_ancestors.read().get(&PeerPair(peer_a, peer_b)).copied() - } + }*/ /// Returns the block request for the given height, if it exists. fn get_block_request(&self, height: u32) -> Option> { @@ -328,29 +359,19 @@ impl BlockSync { } impl BlockSync { - /// Returns the block locators. + /// Returns block locators for the specified range. #[inline] - pub fn get_block_locators(&self) -> Result> { - // Retrieve the latest block height. - let latest_height = self.ledger.latest_block_height(); - - // Initialize the recents map. - // TODO: generalize this for RECENT_INTERVAL > 1, or remove this comment if we hardwire that to 1 - let mut recents = IndexMap::with_capacity(NUM_RECENT_BLOCKS); - // Retrieve the recent block hashes. - for height in latest_height.saturating_sub((NUM_RECENT_BLOCKS - 1) as u32)..=latest_height { - recents.insert(height, self.ledger.get_block_hash(height)?); - } + pub fn get_block_locators(&self, start: u32, end: u32) -> Result> { + ensure!(start < end, "Invalid locator range"); + ensure!((end - start) < 1000, "Locator range too big"); - // Initialize the checkpoints map. - let mut checkpoints = IndexMap::with_capacity((latest_height / CHECKPOINT_INTERVAL + 1).try_into()?); - // Retrieve the checkpoint block hashes. - for height in (0..=latest_height).step_by(CHECKPOINT_INTERVAL as usize) { - checkpoints.insert(height, self.ledger.get_block_hash(height)?); + let mut hashes = vec![]; + + for h in start..end { + hashes.push(self.ledger.get_block_hash(h)?); } - // Construct the block locators. - BlockLocators::new(recents, checkpoints) + BlockLocators::new(start, hashes) } /// Returns true if there are pending responses to block requests that need to be processed. @@ -607,27 +628,6 @@ impl BlockSync { } impl BlockSync { - /// Returns the sync peers with their latest heights, and their minimum common ancestor, if the node can sync. - /// This function returns peers that are consistent with each other, and have a block height - /// that is greater than the ledger height of this node. - /// - /// # Locking - /// This will read-lock `common_ancestors` and `sync_state`, but not at the same time. - pub fn find_sync_peers(&self) -> Option<(IndexMap, u32)> { - // Retrieve the current sync height. - let current_height = self.get_sync_height(); - - if let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers_inner(current_height) { - // Map the locators into the latest height. - let sync_peers = - sync_peers.into_iter().map(|(ip, locators)| (ip, locators.latest_locator_height())).collect(); - // Return the sync peers and their minimum common ancestor. - Some((sync_peers, min_common_ancestor)) - } else { - None - } - } - /// Updates the block locators and common ancestors for the given peer IP. /// /// This function does not need to check that the block locators are well-formed, @@ -635,18 +635,39 @@ impl BlockSync { /// /// This function does **not** check /// that the block locators are consistent with the peer's previous block locators or other peers' block locators. - pub fn update_peer_locators(&self, peer_ip: SocketAddr, locators: &BlockLocators) -> Result<()> { + pub async fn update_peer_block_locators(&self, peer_ip: SocketAddr, locators: BlockLocators) -> Result<()> { + let peer_height = locators.end_height(); + + // Update the peer height in the block_heights structure first + { + let mut block_heights = self.block_heights.lock(); + match block_heights.peer_heights.entry(peer_ip) { + hash_map::Entry::Occupied(mut e) => { + let (_, last_sync) = e.get(); + e.insert((peer_height, *last_sync)); + } + hash_map::Entry::Vacant(e) => { + e.insert((peer_height, 0)); + } + } + } + + // If the locators match the existing locators for the peer, return early. + if self.locators.read().get(&peer_ip) == Some(&locators) { + return Ok(()); + } + // Update the locators entry for the given peer IP. // We perform this update atomically, and drop the lock as soon as we are done with the update. match self.locators.write().entry(peer_ip) { hash_map::Entry::Occupied(mut e) => { // Return early if the block locators did not change. - if e.get() == locators { + if *e.get() == locators { return Ok(()); } - let old_height = e.get().latest_locator_height(); - let new_height = locators.latest_locator_height(); + let old_height = e.get().end_height(); + let new_height = locators.end_height(); if old_height > new_height { debug!("Block height for peer {peer_ip} decreased from {old_height} to {new_height}",); @@ -718,13 +739,38 @@ impl BlockSync { for (peer_pair, new_ancestor) in ancestor_updates.into_iter() { common_ancestors.insert(peer_pair, new_ancestor); } + + // Also ensure all peer-to-peer relationships are computed + // This is needed for find_sync_peers to work correctly + let current_locators = self.locators.read(); + for (peer_a, locators_a) in current_locators.iter() { + for (peer_b, locators_b) in current_locators.iter() { + if peer_a >= peer_b { + continue; // Skip duplicate pairs and self-pairs + } + + let pair = PeerPair(*peer_a, *peer_b); + if !common_ancestors.contains_key(&pair) { + // Compute common ancestor between these two peers + let mut ancestor = 0; + for (height, hash_a) in locators_a.clone().into_iter() { + if let Some(hash_b) = locators_b.get_hash(height) { + if hash_a == hash_b { + ancestor = height; + } else { + break; + } + } + } + common_ancestors.insert(pair, ancestor); + } + } + } } - // Update sync state, because the greatest peer height may have decreased. - if let Some(greatest_peer_height) = self.locators.read().values().map(|l| l.latest_locator_height()).max() { - self.sync_state.write().set_greatest_peer_height(greatest_peer_height); - } else { - error!("Got new block locators but greatest peer height is zero."); + // Update `is_synced`. + if let Some(greatest_peer_height) = self.block_heights.lock().peer_heights.values().map(|(h, _)| h).max() { + self.sync_state.write().set_greatest_peer_height(*greatest_peer_height); } // Notify the sync loop that something changed. @@ -746,12 +792,17 @@ impl BlockSync { // Remove all block requests to the peer. self.remove_block_requests_to_peer(peer_ip); - // Update sync state, because the greatest peer height may have decreased. - if let Some(greatest_peer_height) = self.locators.read().values().map(|l| l.latest_locator_height()).max() { - self.sync_state.write().set_greatest_peer_height(greatest_peer_height); - } else { - // There are no more peers left. - self.sync_state.write().clear_greatest_peer_height(); + // Remove from block_heights + // Use try_lock to avoid blocking, but if it fails we'll spawn a task + let peer_ip = *peer_ip; + match self.block_heights.lock().remove_peer(&peer_ip) { + RemovePeerHeightResult::Unchanged | RemovePeerHeightResult::NoSuchPeer => (), + RemovePeerHeightResult::NewHeight(new_height) => { + self.sync_state.write().set_greatest_peer_height(new_height); + } + RemovePeerHeightResult::NoPeersLeft => { + self.sync_state.write().clear_greatest_peer_height(); + } } // Notify the sync loop that something changed. @@ -776,7 +827,7 @@ impl BlockSync { /// - For clients, `Client::initialize_sync` spawn exactly one task that periodically calls /// `Client::try_issuing_block_requests` which calls this function. /// - Provers do not call this function. - pub fn prepare_block_requests(&self) -> BlockRequestBatch { + pub async fn prepare_block_requests(&self, communication: &C) -> BlockRequestBatch { // Used to print more information when we max out on requests. let print_requests = || { if tracing::enabled!(tracing::Level::TRACE) { @@ -800,8 +851,8 @@ impl BlockSync { let max_new_blocks_to_request = max_outstanding_block_requests.saturating_sub(self.num_outstanding_block_requests() as u32); - // Prepare the block requests and sync peers, or returns an empty result if there is nothing to request. - if self.num_total_block_requests() >= max_total_requests as usize { + // Prepare the block requests. + let (block_requests, sync_peers) = if self.num_total_block_requests() >= max_total_requests as usize { trace!( "We are already requested at least {max_total_requests} blocks that have not been fully processed yet. Will not issue more." ); @@ -814,35 +865,178 @@ impl BlockSync { ); print_requests(); - Default::default() - } else if let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers_inner(current_height) { - // Retrieve the greatest block height of any connected peer. - // We do not need to update the sync state here, as that already happens when the block locators are received. - let greatest_peer_height = sync_peers.values().map(|l| l.latest_locator_height()).max().unwrap_or(0); - - // Construct the list of block requests. - let requests = self.construct_requests( - &sync_peers, - current_height, - min_common_ancestor, - max_new_blocks_to_request, - greatest_peer_height, - ); - (requests, sync_peers) - } else if self.requests.read().is_empty() { - // This can happen during a race condition where the node just finished syncing. - // It does not make sense to log or change the sync status here. - // Checking the sync status here also does not make sense, as the node might as well have switched back - // from `synced` to `syncing` between calling `find_sync_peers_inner` and this line. - - Default::default() + // Return an empty list of block requests. + (Default::default(), Default::default()) + } else if let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers(current_height) { + // Retrieve the highest block height. + let greatest_peer_height = sync_peers.values().map(|l| l.end_height()).max().unwrap_or(0); + // Update the state of `is_block_synced` for the sync module. + self.sync_state.write().set_greatest_peer_height(greatest_peer_height); + // Return the list of block requests. + ( + self.construct_requests( + &sync_peers, + current_height, + min_common_ancestor, + max_new_blocks_to_request, + greatest_peer_height, + ), + sync_peers, + ) } else { - // This happens if we already requested all advertised blocks. - trace!("No new blocks can be requested, but there are still outstanding requests."); + // Update `is_block_synced` if there are no pending requests. + if self.requests.read().is_empty() { + trace!("All requests have been processed. Will set block synced to true."); + // Update the state of `is_block_synced` for the sync module. + self.sync_state.write().set_greatest_peer_height(0); + } else { + trace!("No new blocks can be requests, but there are still outstanding requests."); + } - print_requests(); - Default::default() + // Return an empty list of block requests. + (Default::default(), Default::default()) + }; + + // Can we advance with block locators? + if block_requests.is_empty() { + self.fetch_new_block_locators(communication).await; + } + + (block_requests, sync_peers) + } + + /* async fn fetch_new_block_locators(&self, communication: &C) { + let (block_requests, new_sync_height) = { + let lock = self.block_heights.read(); + let ledger_height = self.ledger.latest_block_height(); + + // Check if we are synced with current block locators and can advance. + if lock.sync_height > ledger_height { + // Not ready yet. + return; + } + + let max_peer_height = *lock.peer_heights.values().map(|(advertised, _)| advertised).max().unwrap_or(&0); + + let new_sync_height = (lock.sync_height + 100).min(max_peer_height); + trace!("Moving from sync_height {} to {new_sync_height}", lock.sync_height); + + let mut messages = vec![]; + + for (peer_ip, (advertised, last_sync)) in lock.peer_heights.iter() { + if *last_sync < new_sync_height && *advertised > *last_sync { + let new_sync = new_sync_height.min(*advertised); + let msg = C::prepare_block_locators_request(new_sync.saturating_sub(100), new_sync); + + messages.push((*peer_ip, new_sync, msg)); + } + } + + (messages, new_sync_height) + }; + + // Avoid holding the lock across await points. + let mut results = vec![]; + for (peer_ip, new_sync, message) in block_requests.into_iter() { + let Some(fut) = communication.send(peer_ip, message).await else { + error!("Failed to send message to peer {peer_ip}"); + continue; + }; + + results.push((peer_ip, new_sync, fut.await)); + } + + // The number of peers we successfully request new block locators from. + let mut count = 0; + + { + let mut lock = self.block_heights.lock().await; + for (peer_ip, new_sync, result) in results.into_iter() { + match result { + Ok(_) => { + if let Some((_, last_sync)) = lock.peer_heights.get_mut(&peer_ip) { + *last_sync = new_sync; + count += 1; + } else { + warn!("Missing entry for {peer_ip}"); + } + } + Err(err) => { + error!("Failed to request block locators: {err}"); + } + } + } + + //TODO (kaimast): can count be zero here, ever? + if count > 0 { + debug!("Requested new block locators from {count} peers"); + } + + lock.sync_height = new_sync_height; + } + + // Can we advance with block locators? + if .is_empty() { + self.fetch_new_block_locators(communication).await; + } + + (block_requests, sync_peers) + } + */ + async fn fetch_new_block_locators(&self, communication: &C) { + // Generate new requests. + let futures = { + let mut lock = self.block_heights.lock(); + let max_peer_height = *lock.peer_heights.values().map(|(advertised, _)| advertised).max().unwrap_or(&0); + let ledger_height = self.ledger.latest_block_height(); + + // Check if we are synced with current block locators and can advance. + if lock.sync_height > ledger_height { + // Not ready yet. + return; + } + + let new_sync_height = (lock.sync_height + 1000).min(max_peer_height); + trace!("Moving from sync_height {} to {new_sync_height}", lock.sync_height); + + let futures: Vec<_> = lock + .peer_heights + .iter_mut() + .filter_map(|(peer_ip, (advertised, last_sync))| { + if *last_sync < new_sync_height && *advertised > *last_sync { + *last_sync = new_sync_height.min(*advertised); + let msg = C::prepare_block_locators_request(ledger_height, *last_sync); + + Some((*peer_ip, communication.send(*peer_ip, msg))) + } else { + None + } + }) + .collect(); + + lock.sync_height = new_sync_height; + futures + }; + + // Wait for requests to complete. + let mut count = 0; + for (peer_ip, fut) in futures.into_iter() { + let Some(fut) = fut.await else { + error!("Failed to send message to peer {peer_ip}"); + continue; + }; + + if let Err(err) = fut.await { + error!("Failed to request block locators: {err}"); + } else { + count += 1; + } + } + + //TODO (kaimast): can count be zero here, ever? + if count > 0 { + debug!("Requested new block locators from {count} peers"); } } @@ -929,12 +1123,31 @@ impl BlockSync { entry.response = Some(block.clone()); } + trace!("Received a new and valid block response for height {height}"); + // Notify the sync loop that something changed. self.response_notify.notify_one(); Ok(()) } + pub fn update_peer_block_height(&self, peer_ip: SocketAddr, new_advertised: u32) -> Result<()> { + let mut lock = self.block_heights.lock(); + + match lock.peer_heights.entry(peer_ip) { + hash_map::Entry::Occupied(mut e) => { + let (last_advertised, last_sync) = e.get(); + ensure!(new_advertised >= *last_advertised, "Peer height cannot decrease!"); + e.insert((new_advertised, *last_sync)); + } + hash_map::Entry::Vacant(e) => { + e.insert((new_advertised, 0)); + } + } + + Ok(()) + } + /// Checks that a block request for the given height does not already exist. fn check_block_request(&self, height: u32) -> Result<()> { // Ensure the block height is not already in the ledger. @@ -1092,20 +1305,20 @@ impl BlockSync { return Ok(None); }; - // Set the maximum number of blocks, so that they do not exceed the end height. - let max_new_blocks_to_request = end_height - start_height; - - let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers_inner(start_height) else { - // This generally shouldn't happen, because there cannot be outstanding requests when no peers are connected. - bail!("Cannot re-request blocks because no or not enough peers are connected"); + let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers(start_height) else { + warn!("Block requests timed out, but found no other peers to re-request from"); + return Ok(None); }; // Retrieve the greatest block height of any connected peer. - let Some(greatest_peer_height) = sync_peers.values().map(|l| l.latest_locator_height()).max() else { + let Some(greatest_peer_height) = sync_peers.values().map(|l| l.end_height()).max() else { // This should never happen because `sync_peers` is guaranteed to be non-empty. bail!("Cannot re-request blocks because no or not enough peers are connected"); }; + // Set the maximum number of blocks, so that they do not exceed the end height. + let max_new_blocks_to_request = end_height - start_height; + // (Try to) construct the requests. let requests = self.construct_requests( &sync_peers, @@ -1127,13 +1340,7 @@ impl BlockSync { } /// Finds the peers to sync from and the shared common ancestor, starting at the give height. - /// - /// Unlike [`Self::find_sync_peers`] this does not only return the latest locators height, but the full BlockLocators for each peer. - /// Returns `None` if there are no peers to sync from. - /// - /// # Locking - /// This function will read-lock `common_ancstors`. - fn find_sync_peers_inner(&self, current_height: u32) -> Option<(IndexMap>, u32)> { + fn find_sync_peers(&self, current_height: u32) -> Option<(IndexMap>, u32)> { // Retrieve the latest ledger height. let latest_ledger_height = self.ledger.latest_block_height(); @@ -1143,8 +1350,8 @@ impl BlockSync { .locators .read() .iter() - .filter(|(_, locators)| locators.latest_locator_height() > current_height) - .sorted_by(|(_, a), (_, b)| b.latest_locator_height().cmp(&a.latest_locator_height())) + .filter(|(_, locators)| locators.end_height() > current_height) + .sorted_by(|(_, a), (_, b)| b.end_height().cmp(&a.end_height())) .take(NUM_SYNC_CANDIDATE_PEERS) .map(|(peer_ip, locators)| (*peer_ip, locators.clone())) .collect(); @@ -1167,7 +1374,7 @@ impl BlockSync { // and a cohort of peers who share a common ancestor above this node's latest ledger height. for (idx, (peer_ip, peer_locators)) in candidate_locators.iter().enumerate() { // The height of the common ancestor shared by all selected peers. - let mut min_common_ancestor = peer_locators.latest_locator_height(); + let mut min_common_ancestor = peer_locators.end_height(); // The peers we will synchronize from. // As the previous iteration did not succeed, restart with the next candidate peers. @@ -1204,6 +1411,7 @@ impl BlockSync { } /// Given the sync peers and their minimum common ancestor, return a list of block requests. + #[allow(dead_code)] fn construct_requests( &self, sync_peers: &IndexMap>, @@ -1289,6 +1497,7 @@ impl BlockSync { /// If any peer is detected to be dishonest in this function, it will not set the hash or previous hash, /// in order to allow the caller to determine what to do. +#[allow(dead_code)] fn construct_request( height: u32, sync_peers: &IndexMap>, @@ -1359,689 +1568,8 @@ fn construct_request( (hash, previous_hash, num_sync_ips, is_honest) } -#[cfg(test)] -mod tests { - use super::*; - use crate::locators::{ - CHECKPOINT_INTERVAL, - NUM_RECENT_BLOCKS, - test_helpers::{sample_block_locators, sample_block_locators_with_fork}, - }; - - use snarkos_node_bft_ledger_service::MockLedgerService; - use snarkos_node_network::{NodeType, Peer, Resolver}; - use snarkos_node_tcp::{P2P, Tcp}; - use snarkvm::{ - ledger::committee::Committee, - prelude::{Field, TestRng}, - }; - - use indexmap::{IndexSet, indexset}; - #[cfg(feature = "locktick")] - use locktick::parking_lot::RwLock; - #[cfg(not(feature = "locktick"))] - use parking_lot::RwLock; - use rand::Rng; - use std::net::{IpAddr, Ipv4Addr}; - - type CurrentNetwork = snarkvm::prelude::MainnetV0; - - #[derive(Default)] - struct DummyPeerPoolHandler { - peers_to_ban: RwLock>, - } - - impl P2P for DummyPeerPoolHandler { - fn tcp(&self) -> &Tcp { - unreachable!(); - } - } - - impl PeerPoolHandling for DummyPeerPoolHandler { - const MAXIMUM_POOL_SIZE: usize = 10; - const OWNER: &str = "[DummyPeerPoolHandler]"; - const PEER_SLASHING_COUNT: usize = 0; - - fn peer_pool(&self) -> &RwLock>> { - unreachable!(); - } - - fn resolver(&self) -> &RwLock> { - unreachable!(); - } - - fn is_dev(&self) -> bool { - true - } - - fn trusted_peers_only(&self) -> bool { - false - } - - fn node_type(&self) -> NodeType { - NodeType::Client - } - - fn ip_ban_peer(&self, listener_addr: SocketAddr, _reason: Option<&str>) { - self.peers_to_ban.write().push(listener_addr); - } - } - - /// Returns the peer IP for the sync pool. - fn sample_peer_ip(id: u16) -> SocketAddr { - assert_ne!(id, 0, "The peer ID must not be 0 (reserved for local IP in testing)"); - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), id) - } - - /// Returns a sample committee. - fn sample_committee() -> Committee { - let rng = &mut TestRng::default(); - snarkvm::ledger::committee::test_helpers::sample_committee(rng) - } - - /// Returns the ledger service, initialized to the given height. - fn sample_ledger_service(height: u32) -> MockLedgerService { - MockLedgerService::new_at_height(sample_committee(), height) - } - - /// Returns the sync pool, with the ledger initialized to the given height. - fn sample_sync_at_height(height: u32) -> BlockSync { - BlockSync::::new(Arc::new(sample_ledger_service(height))) - } - - /// Returns a vector of randomly sampled block heights in [0, max_height]. - /// - /// The maximum value will always be included in the result. - fn generate_block_heights(max_height: u32, num_values: usize) -> Vec { - assert!(num_values > 0, "Cannot generate an empty vector"); - assert!((max_height as usize) >= num_values); - - let mut rng = TestRng::default(); - - let mut heights: Vec = (0..(max_height - 1)).choose_multiple(&mut rng, num_values); - - heights.push(max_height); - - heights - } - - /// Returns a duplicate (deep copy) of the sync pool with a different ledger height. - fn duplicate_sync_at_new_height(sync: &BlockSync, height: u32) -> BlockSync { - BlockSync:: { - peer_notify: Notify::new(), - response_notify: Default::default(), - ledger: Arc::new(sample_ledger_service(height)), - locators: RwLock::new(sync.locators.read().clone()), - common_ancestors: RwLock::new(sync.common_ancestors.read().clone()), - requests: RwLock::new(sync.requests.read().clone()), - sync_state: RwLock::new(sync.sync_state.read().clone()), - advance_with_sync_blocks_lock: Default::default(), - metrics: Default::default(), - } - } - - /// Checks that the sync pool (starting at genesis) returns the correct requests. - fn check_prepare_block_requests( - sync: BlockSync, - min_common_ancestor: u32, - peers: IndexSet, - ) { - let rng = &mut TestRng::default(); - - // Check test assumptions are met. - assert_eq!(sync.ledger.latest_block_height(), 0, "This test assumes the sync pool is at genesis"); - - // Determine the number of peers within range of this sync pool. - let num_peers_within_recent_range_of_ledger = { - // If no peers are within range, then set to 0. - if min_common_ancestor >= NUM_RECENT_BLOCKS as u32 { - 0 - } - // Otherwise, manually check the number of peers within range. - else { - peers.iter().filter(|peer_ip| sync.get_peer_height(peer_ip).unwrap() < NUM_RECENT_BLOCKS as u32).count() - } - }; - - // Prepare the block requests. - let (requests, sync_peers) = sync.prepare_block_requests(); - - // If there are no peers, then there should be no requests. - if peers.is_empty() { - assert!(requests.is_empty()); - return; - } - - // Otherwise, there should be requests. - let expected_num_requests = core::cmp::min(min_common_ancestor as usize, MAX_BLOCK_REQUESTS); - assert_eq!(requests.len(), expected_num_requests); - - for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - assert_eq!(height, 1 + idx as u32); - assert_eq!(hash, Some((Field::::from_u32(height)).into())); - assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); - - if num_peers_within_recent_range_of_ledger >= REDUNDANCY_FACTOR { - assert_eq!(sync_ips.len(), 1); - } else { - assert_eq!(sync_ips.len(), num_peers_within_recent_range_of_ledger); - assert_eq!(sync_ips, peers); - } - } - } - - /// Tests that height and hash values are set correctly using many different maximum block heights. - #[test] - fn test_latest_block_height() { - for height in generate_block_heights(100_001, 5000) { - let sync = sample_sync_at_height(height); - // Check that the latest block height is the maximum height. - assert_eq!(sync.ledger.latest_block_height(), height); - - // Check the hash to height mapping - assert_eq!(sync.ledger.get_block_height(&(Field::::from_u32(0)).into()).unwrap(), 0); - assert_eq!( - sync.ledger.get_block_height(&(Field::::from_u32(height)).into()).unwrap(), - height - ); - } - } - - #[test] - fn test_get_block_hash() { - for height in generate_block_heights(100_001, 5000) { - let sync = sample_sync_at_height(height); - - // Check the height to hash mapping - assert_eq!(sync.ledger.get_block_hash(0).unwrap(), (Field::::from_u32(0)).into()); - assert_eq!(sync.ledger.get_block_hash(height).unwrap(), (Field::::from_u32(height)).into()); - } - } - - #[test] - fn test_prepare_block_requests() { - for num_peers in 0..111 { - println!("Testing with {num_peers} peers"); - - let sync = sample_sync_at_height(0); - - let mut peers = indexset![]; - - for peer_id in 1..=num_peers { - // Add a peer. - sync.update_peer_locators(sample_peer_ip(peer_id), &sample_block_locators(10)).unwrap(); - // Add the peer to the set of peers. - peers.insert(sample_peer_ip(peer_id)); - } - - // If all peers are ahead, then requests should be prepared. - check_prepare_block_requests(sync, 10, peers); - } - } - - #[test] - fn test_prepare_block_requests_with_leading_fork_at_11() { - let sync = sample_sync_at_height(0); - - // Intuitively, peer 1's fork is above peer 2 and peer 3's height. - // So from peer 2 and peer 3's perspective, they don't even realize that peer 1 is on a fork. - // Thus, you can sync up to block 10 from any of the 3 peers. - - // When there are NUM_REDUNDANCY peers ahead, and 1 peer is on a leading fork at 11, - // then the sync pool should request blocks 1..=10 from the NUM_REDUNDANCY peers. - // This is safe because the leading fork is at 11, and the sync pool is at 0, - // so all candidate peers are at least 10 blocks ahead of the sync pool. - - // Add a peer (fork). - let peer_1 = sample_peer_ip(1); - sync.update_peer_locators(peer_1, &sample_block_locators_with_fork(20, 11)).unwrap(); - - // Add a peer. - let peer_2 = sample_peer_ip(2); - sync.update_peer_locators(peer_2, &sample_block_locators(10)).unwrap(); - - // Add a peer. - let peer_3 = sample_peer_ip(3); - sync.update_peer_locators(peer_3, &sample_block_locators(10)).unwrap(); - - // Prepare the block requests. - let (requests, _) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 10); - - // Check the requests. - for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { - assert_eq!(height, 1 + idx as u32); - assert_eq!(hash, Some((Field::::from_u32(height)).into())); - assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); - assert_eq!(num_sync_ips, 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash. - } - } - - #[test] - fn test_prepare_block_requests_with_leading_fork_at_10() { - let rng = &mut TestRng::default(); - let sync = sample_sync_at_height(0); - - // Intuitively, peer 1's fork is at peer 2 and peer 3's height. - // So from peer 2 and peer 3's perspective, they recognize that peer 1 has forked. - // Thus, you don't have NUM_REDUNDANCY peers to sync to block 10. - // - // Now, while you could in theory sync up to block 9 from any of the 3 peers, - // we choose not to do this as either side is likely to disconnect from us, - // and we would rather wait for enough redundant peers before syncing. - - // When there are NUM_REDUNDANCY peers ahead, and 1 peer is on a leading fork at 10, - // then the sync pool should not request blocks as 1 peer conflicts with the other NUM_REDUNDANCY-1 peers. - // We choose to sync with a cohort of peers that are *consistent* with each other, - // and prioritize from descending heights (so the highest peer gets priority). - - // Add a peer (fork). - let peer_1 = sample_peer_ip(1); - sync.update_peer_locators(peer_1, &sample_block_locators_with_fork(20, 10)).unwrap(); - - // Add a peer. - let peer_2 = sample_peer_ip(2); - sync.update_peer_locators(peer_2, &sample_block_locators(10)).unwrap(); - - // Add a peer. - let peer_3 = sample_peer_ip(3); - sync.update_peer_locators(peer_3, &sample_block_locators(10)).unwrap(); - - // Prepare the block requests. - let (requests, _) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 0); - - // When there are NUM_REDUNDANCY+1 peers ahead, and 1 is on a fork, then there should be block requests. - - // Add a peer. - let peer_4 = sample_peer_ip(4); - sync.update_peer_locators(peer_4, &sample_block_locators(10)).unwrap(); - - // Prepare the block requests. - let (requests, sync_peers) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 10); - - // Check the requests. - for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - assert_eq!(height, 1 + idx as u32); - assert_eq!(hash, Some((Field::::from_u32(height)).into())); - assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); - assert_eq!(sync_ips.len(), 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash. - assert_ne!(sync_ips[0], peer_1); // It should never be the forked peer. - } - } - - #[test] - fn test_prepare_block_requests_with_trailing_fork_at_9() { - let rng = &mut TestRng::default(); - let sync = sample_sync_at_height(0); - - // Peer 1 and 2 diverge from peer 3 at block 10. We only sync when there are NUM_REDUNDANCY peers - // who are *consistent* with each other. So if you add a 4th peer that is consistent with peer 1 and 2, - // then you should be able to sync up to block 10, thereby biasing away from peer 3. - - // Add a peer (fork). - let peer_1 = sample_peer_ip(1); - sync.update_peer_locators(peer_1, &sample_block_locators(10)).unwrap(); - - // Add a peer. - let peer_2 = sample_peer_ip(2); - sync.update_peer_locators(peer_2, &sample_block_locators(10)).unwrap(); - - // Add a peer. - let peer_3 = sample_peer_ip(3); - sync.update_peer_locators(peer_3, &sample_block_locators_with_fork(20, 10)).unwrap(); - - // Prepare the block requests. - let (requests, _) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 0); - - // When there are NUM_REDUNDANCY+1 peers ahead, and peer 3 is on a fork, then there should be block requests. - - // Add a peer. - let peer_4 = sample_peer_ip(4); - sync.update_peer_locators(peer_4, &sample_block_locators(10)).unwrap(); - - // Prepare the block requests. - let (requests, sync_peers) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 10); - - // Check the requests. - for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - assert_eq!(height, 1 + idx as u32); - assert_eq!(hash, Some((Field::::from_u32(height)).into())); - assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); - assert_eq!(sync_ips.len(), 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash. - assert_ne!(sync_ips[0], peer_3); // It should never be the forked peer. - } - } - - #[test] - fn test_insert_block_requests() { - let rng = &mut TestRng::default(); - let sync = sample_sync_at_height(0); - - // Add a peer. - sync.update_peer_locators(sample_peer_ip(1), &sample_block_locators(10)).unwrap(); +#[cfg(any(feature = "test-helpers", test))] +pub mod test_helpers; - // Prepare the block requests. - let (requests, sync_peers) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 10); - - for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - // Insert the block request. - sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); - // Check that the block requests were inserted. - assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); - assert!(sync.get_block_request_timestamp(height).is_some()); - } - - for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - // Check that the block requests are still inserted. - assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); - assert!(sync.get_block_request_timestamp(height).is_some()); - } - - for (height, (hash, previous_hash, num_sync_ips)) in requests { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - // Ensure that the block requests cannot be inserted twice. - sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap_err(); - // Check that the block requests are still inserted. - assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); - assert!(sync.get_block_request_timestamp(height).is_some()); - } - } - - #[test] - fn test_insert_block_requests_fails() { - let sync = sample_sync_at_height(9); - - // Add a peer. - sync.update_peer_locators(sample_peer_ip(1), &sample_block_locators(10)).unwrap(); - - // Inserting a block height that is already in the ledger should fail. - sync.insert_block_request(9, (None, None, indexset![sample_peer_ip(1)])).unwrap_err(); - // Inserting a block height that is not in the ledger should succeed. - sync.insert_block_request(10, (None, None, indexset![sample_peer_ip(1)])).unwrap(); - } - - #[test] - fn test_update_peer_locators() { - let sync = sample_sync_at_height(0); - - // Test 2 peers. - let peer1_ip = sample_peer_ip(1); - for peer1_height in 0..500u32 { - sync.update_peer_locators(peer1_ip, &sample_block_locators(peer1_height)).unwrap(); - assert_eq!(sync.get_peer_height(&peer1_ip), Some(peer1_height)); - - let peer2_ip = sample_peer_ip(2); - for peer2_height in 0..500u32 { - println!("Testing peer 1 height at {peer1_height} and peer 2 height at {peer2_height}"); - - sync.update_peer_locators(peer2_ip, &sample_block_locators(peer2_height)).unwrap(); - assert_eq!(sync.get_peer_height(&peer2_ip), Some(peer2_height)); - - // Compute the distance between the peers. - let distance = peer1_height.abs_diff(peer2_height); - - // Check the common ancestor. - if distance < NUM_RECENT_BLOCKS as u32 { - let expected_ancestor = core::cmp::min(peer1_height, peer2_height); - assert_eq!(sync.get_common_ancestor(peer1_ip, peer2_ip), Some(expected_ancestor)); - assert_eq!(sync.get_common_ancestor(peer2_ip, peer1_ip), Some(expected_ancestor)); - } else { - let min_checkpoints = - core::cmp::min(peer1_height / CHECKPOINT_INTERVAL, peer2_height / CHECKPOINT_INTERVAL); - let expected_ancestor = min_checkpoints * CHECKPOINT_INTERVAL; - assert_eq!(sync.get_common_ancestor(peer1_ip, peer2_ip), Some(expected_ancestor)); - assert_eq!(sync.get_common_ancestor(peer2_ip, peer1_ip), Some(expected_ancestor)); - } - } - } - } - - #[test] - fn test_remove_peer() { - let sync = sample_sync_at_height(0); - - let peer_ip = sample_peer_ip(1); - sync.update_peer_locators(peer_ip, &sample_block_locators(100)).unwrap(); - assert_eq!(sync.get_peer_height(&peer_ip), Some(100)); - - sync.remove_peer(&peer_ip); - assert_eq!(sync.get_peer_height(&peer_ip), None); - - sync.update_peer_locators(peer_ip, &sample_block_locators(200)).unwrap(); - assert_eq!(sync.get_peer_height(&peer_ip), Some(200)); - - sync.remove_peer(&peer_ip); - assert_eq!(sync.get_peer_height(&peer_ip), None); - } - - #[test] - fn test_locators_insert_remove_insert() { - let sync = sample_sync_at_height(0); - - let peer_ip = sample_peer_ip(1); - sync.update_peer_locators(peer_ip, &sample_block_locators(100)).unwrap(); - assert_eq!(sync.get_peer_height(&peer_ip), Some(100)); - - sync.remove_peer(&peer_ip); - assert_eq!(sync.get_peer_height(&peer_ip), None); - - sync.update_peer_locators(peer_ip, &sample_block_locators(200)).unwrap(); - assert_eq!(sync.get_peer_height(&peer_ip), Some(200)); - } - - #[test] - fn test_requests_insert_remove_insert() { - let rng = &mut TestRng::default(); - let sync = sample_sync_at_height(0); - - // Add a peer. - let peer_ip = sample_peer_ip(1); - sync.update_peer_locators(peer_ip, &sample_block_locators(10)).unwrap(); - - // Prepare the block requests. - let (requests, sync_peers) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 10); - - for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - // Insert the block request. - sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); - // Check that the block requests were inserted. - assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); - assert!(sync.get_block_request_timestamp(height).is_some()); - } - - // Remove the peer. - sync.remove_peer(&peer_ip); - - for (height, _) in requests { - // Check that the block requests were removed. - assert_eq!(sync.get_block_request(height), None); - assert!(sync.get_block_request_timestamp(height).is_none()); - } - - // As there is no peer, it should not be possible to prepare block requests. - let (requests, _) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 0); - - // Add the peer again. - sync.update_peer_locators(peer_ip, &sample_block_locators(10)).unwrap(); - - // Prepare the block requests. - let (requests, _) = sync.prepare_block_requests(); - assert_eq!(requests.len(), 10); - - for (height, (hash, previous_hash, num_sync_ips)) in requests { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - // Insert the block request. - sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); - // Check that the block requests were inserted. - assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); - assert!(sync.get_block_request_timestamp(height).is_some()); - } - } - - #[test] - fn test_obsolete_block_requests() { - let rng = &mut TestRng::default(); - let sync = sample_sync_at_height(0); - - let locator_height = rng.gen_range(0..50); - - // Add a peer. - let locators = sample_block_locators(locator_height); - sync.update_peer_locators(sample_peer_ip(1), &locators).unwrap(); - - // Construct block requests - let (requests, sync_peers) = sync.prepare_block_requests(); - assert_eq!(requests.len(), locator_height as usize); - - // Add the block requests to the sync module. - for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { - // Construct the sync IPs. - let sync_ips: IndexSet<_> = - sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); - // Insert the block request. - sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); - // Check that the block requests were inserted. - assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); - assert!(sync.get_block_request_timestamp(height).is_some()); - } - - // Duplicate a new sync module with a different height to simulate block advancement. - // This range needs to be inclusive, so that the range is never empty, - // even with a locator height of 0. - let ledger_height = rng.gen_range(0..=locator_height); - let new_sync = duplicate_sync_at_new_height(&sync, ledger_height); - - // Check that the number of requests is the same. - assert_eq!(new_sync.requests.read().len(), requests.len()); - - // Remove timed out block requests. - let c = DummyPeerPoolHandler::default(); - new_sync.handle_block_request_timeouts(&c).unwrap(); - - // Check that the number of requests is reduced based on the ledger height. - assert_eq!(new_sync.requests.read().len(), (locator_height - ledger_height) as usize); - } - - #[test] - fn test_timed_out_block_request() { - let sync = sample_sync_at_height(0); - let peer_ip = sample_peer_ip(1); - let locators = sample_block_locators(10); - let block_hash = locators.get_hash(1); - - sync.update_peer_locators(peer_ip, &locators).unwrap(); - - let timestamp = Instant::now() - BLOCK_REQUEST_TIMEOUT - Duration::from_secs(1); - - // Add a timed-out request - sync.requests.write().insert(1, OutstandingRequest { - request: (block_hash, None, [peer_ip].into()), - timestamp, - response: None, - }); - - assert_eq!(sync.requests.read().len(), 1); - assert_eq!(sync.locators.read().len(), 1); - - // Remove timed out block requests. - let c = DummyPeerPoolHandler::default(); - sync.handle_block_request_timeouts(&c).unwrap(); - - // let ban_list = c.peers_to_ban.write(); - // assert_eq!(ban_list.len(), 1); - // assert_eq!(ban_list.iter().next(), Some(&peer_ip)); - - assert!(sync.requests.read().is_empty()); - assert!(sync.locators.read().is_empty()); - } - - #[test] - fn test_reissue_timed_out_block_request() { - let sync = sample_sync_at_height(0); - let peer_ip1 = sample_peer_ip(1); - let peer_ip2 = sample_peer_ip(2); - let peer_ip3 = sample_peer_ip(3); - - let locators = sample_block_locators(10); - let block_hash1 = locators.get_hash(1); - let block_hash2 = locators.get_hash(2); - - sync.update_peer_locators(peer_ip1, &locators).unwrap(); - sync.update_peer_locators(peer_ip2, &locators).unwrap(); - sync.update_peer_locators(peer_ip3, &locators).unwrap(); - - assert_eq!(sync.locators.read().len(), 3); - - let timestamp = Instant::now() - BLOCK_REQUEST_TIMEOUT - Duration::from_secs(1); - - // Add a timed-out request - sync.requests.write().insert(1, OutstandingRequest { - request: (block_hash1, None, [peer_ip1].into()), - timestamp, - response: None, - }); - - // Add a timed-out request - sync.requests.write().insert(2, OutstandingRequest { - request: (block_hash2, None, [peer_ip2].into()), - timestamp: Instant::now(), - response: None, - }); - - assert_eq!(sync.requests.read().len(), 2); - - // Remove timed out block requests. - let c = DummyPeerPoolHandler::default(); - - let re_requests = sync.handle_block_request_timeouts(&c).unwrap(); - - // let ban_list = c.peers_to_ban.write(); - // assert_eq!(ban_list.len(), 1); - // assert_eq!(ban_list.iter().next(), Some(&peer_ip1)); - - assert_eq!(sync.requests.read().len(), 1); - assert_eq!(sync.locators.read().len(), 2); - - let (new_requests, new_sync_ips) = re_requests.unwrap(); - assert_eq!(new_requests.len(), 1); - - let (height, (hash, _, _)) = new_requests.first().unwrap(); - assert_eq!(*height, 1); - assert_eq!(*hash, block_hash1); - assert_eq!(new_sync_ips.len(), 2); - - // Make sure the removed peer is not in the sync_peer set. - let mut iter = new_sync_ips.iter(); - assert_ne!(iter.next().unwrap().0, &peer_ip1); - assert_ne!(iter.next().unwrap().0, &peer_ip1); - } -} +#[cfg(test)] +mod tests; diff --git a/node/sync/src/block_sync/test_helpers.rs b/node/sync/src/block_sync/test_helpers.rs new file mode 100644 index 0000000000..5e5c826c70 --- /dev/null +++ b/node/sync/src/block_sync/test_helpers.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkOS library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +use snarkos_node_bft_ledger_service::MockLedgerService; +use snarkos_node_sync_communication_service::CommunicationService; +use snarkvm::{ + ledger::committee::Committee, + prelude::{Field, TestRng}, +}; + +use indexmap::IndexSet; +#[cfg(feature = "locktick")] +use locktick::parking_lot::RwLock; +#[cfg(not(feature = "locktick"))] +use parking_lot::RwLock; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, +}; +use tokio::sync::Notify; + +type CurrentNetwork = snarkvm::prelude::MainnetV0; + +pub use crate::locators::test_helpers::*; + +/// Returns the peer IP for the sync pool. +pub fn sample_peer_ip(id: u16) -> SocketAddr { + assert_ne!(id, 0, "The peer ID must not be 0 (reserved for local IP in testing)"); + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), id) +} + +/// Returns a sample committee. +pub fn sample_committee() -> Committee { + let rng = &mut TestRng::default(); + snarkvm::ledger::committee::test_helpers::sample_committee(rng) +} + +/// Returns the ledger service, initialized to the given height. +pub fn sample_ledger_service(height: u32) -> MockLedgerService { + MockLedgerService::new_at_height(sample_committee(), height) +} + +/// Returns the sync pool, with the ledger initialized to the given height. +pub fn sample_sync_at_height(height: u32) -> BlockSync { + BlockSync::::new(Arc::new(sample_ledger_service(height))) +} + +/// Returns a vector of randomly sampled block heights in [0, max_height]. +/// +/// The maximum value will always be included in the result. +pub fn generate_block_heights(max_height: u32, num_values: usize) -> Vec { + assert!(num_values > 0, "Cannot generate an empty vector"); + assert!((max_height as usize) >= num_values); + + let mut rng = TestRng::default(); + + let mut heights: Vec = (0..(max_height - 1)).choose_multiple(&mut rng, num_values); + + heights.push(max_height); + + heights +} + +/// Returns a duplicate (deep copy) of the sync pool with a different ledger height. +pub fn duplicate_sync_at_new_height(sync: &BlockSync, height: u32) -> BlockSync { + BlockSync:: { + peer_notify: Notify::new(), + response_notify: Notify::new(), + metrics: Default::default(), + block_heights: sync.block_heights.clone(), + ledger: Arc::new(sample_ledger_service(height)), + locators: RwLock::new(sync.locators.read().clone()), + common_ancestors: RwLock::new(sync.common_ancestors.read().clone()), + requests: RwLock::new(sync.requests.read().clone()), + sync_state: RwLock::new(sync.sync_state.read().clone()), + advance_with_sync_blocks_lock: Default::default(), + } +} + +/// Checks that the sync pool (starting at genesis) returns the correct requests. +pub async fn check_prepare_block_requests( + communication: &C, + sync: BlockSync, + min_common_ancestor: u32, + peers: IndexSet, +) { + // Check test assumptions are met. + assert_eq!(sync.ledger.latest_block_height(), 0, "This test assumes the sync pool is at genesis"); + + // Prepare the block requests. + let (requests, _sync_peers) = sync.prepare_block_requests(communication).await; + + // If there are no peers, then there should be no requests. + if peers.is_empty() { + assert!(requests.is_empty()); + return; + } + + // Otherwise, there should be requests. + let expected_num_requests = (min_common_ancestor as usize).min(MAX_BLOCK_REQUESTS); + assert_eq!(requests.len(), expected_num_requests); + + for (idx, (height, (hash, previous_hash, _num_sync_ips))) in requests.into_iter().enumerate() { + assert_eq!(height, 1 + idx as u32); + assert_eq!(hash, Some((Field::::from_u32(height)).into())); + assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); + } +} diff --git a/node/sync/src/block_sync/tests.rs b/node/sync/src/block_sync/tests.rs new file mode 100644 index 0000000000..59894f8cc1 --- /dev/null +++ b/node/sync/src/block_sync/tests.rs @@ -0,0 +1,529 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkOS library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::test_helpers::*; + +use snarkos_node_sync_communication_service::test_helpers::DummyCommunicationService; + +use snarkvm::prelude::*; + +use indexmap::{IndexSet, indexset}; +use rand::seq::IteratorRandom; + +type CurrentNetwork = snarkvm::prelude::MainnetV0; + +/// Tests that height and hash values are set correctly using many different maximum block heights. +#[test] +fn test_latest_block_height() { + for height in generate_block_heights(100_001, 5000) { + let sync = sample_sync_at_height(height); + // Check that the latest block height is the maximum height. + assert_eq!(sync.ledger.latest_block_height(), height); + } +} + +#[test] +fn test_get_block_height() { + for height in generate_block_heights(100_001, 5000) { + let sync = sample_sync_at_height(height); + assert_eq!(sync.ledger.get_block_height(&(Field::::from_u32(0)).into()).unwrap(), 0); + assert_eq!(sync.ledger.get_block_height(&(Field::::from_u32(height)).into()).unwrap(), height); + } +} + +#[test] +fn test_get_block_hash() { + for height in generate_block_heights(100_001, 5000) { + let sync = sample_sync_at_height(height); + assert_eq!(sync.ledger.get_block_hash(0).unwrap(), (Field::::from_u32(0)).into()); + assert_eq!(sync.ledger.get_block_hash(height).unwrap(), (Field::::from_u32(height)).into()); + } +} + +#[tokio::test] +async fn test_requests_insert_remove_insert() { + let rng = &mut TestRng::default(); + let sync = sample_sync_at_height(0); + + // Add a peer. + let peer_ip = sample_peer_ip(1); + sync.update_peer_block_locators(peer_ip, sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let comm = DummyCommunicationService; + let (requests, sync_peers) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 10); + + for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + // Insert the block request. + sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); + // Check that the block requests were inserted. + assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); + assert!(sync.get_block_request_timestamp(height).is_some()); + } + + // Remove the peer. + sync.remove_peer(&peer_ip); + + for (height, _) in requests { + // Check that the block requests were removed. + assert_eq!(sync.get_block_request(height), None); + assert!(sync.get_block_request_timestamp(height).is_none()); + } + + // As there is no peer, it should not be possible to prepare block requests. + let (requests, _) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 0); + + // Add the peer again. + sync.update_peer_block_locators(peer_ip, sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let (requests, _) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 10); + + for (height, (hash, previous_hash, num_sync_ips)) in requests { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + // Insert the block request. + sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); + // Check that the block requests were inserted. + assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); + assert!(sync.get_block_request_timestamp(height).is_some()); + } +} +#[tokio::test] +async fn test_insert_block_requests() { + let rng = &mut TestRng::default(); + let sync = sample_sync_at_height(0); + + // Add a peer. + sync.update_peer_block_locators(sample_peer_ip(1), sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let comm = DummyCommunicationService; + let (requests, sync_peers) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 10); + + for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + // Insert the block request. + sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); + // Check that the block requests were inserted. + assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); + assert!(sync.get_block_request_timestamp(height).is_some()); + } + + for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + // Check that the block requests are still inserted. + assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); + assert!(sync.get_block_request_timestamp(height).is_some()); + } + + for (height, (hash, previous_hash, num_sync_ips)) in requests { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + // Ensure that the block requests cannot be inserted twice. + sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap_err(); + // Check that the block requests are still inserted. + assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); + assert!(sync.get_block_request_timestamp(height).is_some()); + } +} + +/* TODO fix these +#[tokio::test] +async fn test_obsolete_block_requests() { + let rng = &mut TestRng::default(); + let sync = sample_sync_at_height(0); + + let locator_height = rng.gen_range(0..50); + + // Add a peer. + let locators = sample_block_locators(0, locator_height); + sync.update_peer_block_locators(sample_peer_ip(1), locators.clone()).await.unwrap(); + + // Construct block requests + let comm = DummyCommunicationService::default(); + let (requests, sync_peers) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), locator_height as usize); + + // Add the block requests to the sync module. + for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + // Insert the block request. + sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap(); + // Check that the block requests were inserted. + assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips))); + assert!(sync.get_block_request_timestamp(height).is_some()); + } + + // Duplicate a new sync module with a different height to simulate block advancement. + // This range needs to be inclusive, so that the range is never empty, + // even with a locator height of 0. + let ledger_height = rng.gen_range(0..=locator_height); + let new_sync = duplicate_sync_at_new_height(&sync, ledger_height); + + // Check that the number of requests is the same. + assert_eq!(new_sync.requests.read().len(), requests.len()); + + // Remove timed out block requests. + let c = DummyCommunicationService::default(); + new_sync.handle_block_request_timeouts(&c); + + // Check that the number of requests is reduced based on the ledger height. + assert_eq!(new_sync.requests.read().len(), (locator_height - ledger_height) as usize); +} + +#[tokio::test] +async fn test_timed_out_block_request() { + let sync = sample_sync_at_height(0); + let peer_ip = sample_peer_ip(1); + let locators = sample_block_locators(0, 10); + let block_hash = locators.get_hash(1); + + sync.update_peer_block_locators(peer_ip, locators.clone()).await.unwrap(); + + let timestamp = Instant::now() - BLOCK_REQUEST_TIMEOUT - Duration::from_secs(1); + + // Add a timed-out request + sync.requests.write().insert(1, OutstandingRequest { + request: (block_hash, None, [peer_ip].into()), + timestamp, + response: None, + }); + + assert_eq!(sync.requests.read().len(), 1); + assert_eq!(sync.locators.read().len(), 1); + + // Remove timed out block requests. + let c = DummyCommunicationService::default(); + sync.handle_block_request_timeouts(&c); + + let ban_list = c.peers_to_ban.lock(); + assert_eq!(ban_list.len(), 1); + assert_eq!(ban_list.iter().next(), Some(&peer_ip)); + + assert!(sync.requests.read().is_empty()); + assert!(sync.locators.read().is_empty()); +} + +#[tokio::test] +async fn test_reissue_timed_out_block_request() { + let sync = sample_sync_at_height(0); + let peer_ip1 = sample_peer_ip(1); + let peer_ip2 = sample_peer_ip(2); + let peer_ip3 = sample_peer_ip(3); + + let locators = sample_block_locators(0, 10); + let block_hash1 = locators.get_hash(1); + let block_hash2 = locators.get_hash(2); + + sync.update_peer_block_locators(peer_ip1, locators.clone()).await.unwrap(); + sync.update_peer_block_locators(peer_ip2, locators.clone()).await.unwrap(); + sync.update_peer_block_locators(peer_ip3, locators.clone()).await.unwrap(); + + assert_eq!(sync.locators.read().len(), 3); + + let timestamp = Instant::now() - BLOCK_REQUEST_TIMEOUT - Duration::from_secs(1); + + // Add a timed-out request + sync.requests.write().insert(1, OutstandingRequest { + request: (block_hash1, None, [peer_ip1].into()), + timestamp, + response: None, + }); + + // Add a timed-out request + sync.requests.write().insert(2, OutstandingRequest { + request: (block_hash2, None, [peer_ip2].into()), + timestamp: Instant::now(), + response: None, + }); + + assert_eq!(sync.requests.read().len(), 2); + + // Remove timed out block requests. + let c = DummyCommunicationService::default(); + let re_requests = sync.handle_block_request_timeouts(&c); + + let ban_list = c.peers_to_ban.lock(); + assert_eq!(ban_list.len(), 1); + assert_eq!(ban_list.iter().next(), Some(&peer_ip1)); + + assert_eq!(sync.requests.read().len(), 1); + assert_eq!(sync.locators.read().len(), 2); + + let (new_requests, new_sync_ips) = re_requests.unwrap(); + assert_eq!(new_requests.len(), 1); + + let (height, (hash, _, _)) = new_requests.first().unwrap(); + assert_eq!(*height, 1); + assert_eq!(*hash, block_hash1); + assert_eq!(new_sync_ips.len(), 2); + + // Make sure the removed peer is not in the sync_peer set. + let mut iter = new_sync_ips.iter(); + assert_ne!(iter.next().unwrap().0, &peer_ip1); + assert_ne!(iter.next().unwrap().0, &peer_ip1); +}*/ + +#[tokio::test] +async fn test_insert_block_requests_fails() { + let sync = sample_sync_at_height(9); + + // Add a peer. + sync.update_peer_block_locators(sample_peer_ip(1), sample_block_locators(0, 10)).await.unwrap(); + + // Inserting a block height that is already in the ledger should fail. + sync.insert_block_request(9, (None, None, indexset![sample_peer_ip(1)])).unwrap_err(); + // Inserting a block height that is not in the ledger should succeed. + sync.insert_block_request(10, (None, None, indexset![sample_peer_ip(1)])).unwrap(); +} + +#[tokio::test] +async fn test_prepare_block_requests() { + for num_peers in 0..111 { + println!("Testing with {num_peers} peers"); + + let sync = sample_sync_at_height(0); + + let mut peers = indexset![]; + + for peer_id in 1..=num_peers { + // Add a peer. + sync.update_peer_block_locators(sample_peer_ip(peer_id), sample_block_locators(0, 10)).await.unwrap(); + // Add the peer to the set of peers. + peers.insert(sample_peer_ip(peer_id)); + } + + // If all peers are ahead, then requests should be prepared. + let comm = DummyCommunicationService; + check_prepare_block_requests(&comm, sync, 10, peers).await; + } +} + +#[tokio::test] +async fn test_prepare_block_requests_with_leading_fork_at_11() { + let sync = sample_sync_at_height(0); + + // Intuitively, peer 1's fork is above peer 2 and peer 3's height. + // So from peer 2 and peer 3's perspective, they don't even realize that peer 1 is on a fork. + // Thus, you can sync up to block 10 from any of the 3 peers. + + // When there are NUM_REDUNDANCY peers ahead, and 1 peer is on a leading fork at 11, + // then the sync pool should request blocks 1..=10 from the NUM_REDUNDANCY peers. + // This is safe because the leading fork is at 11, and the sync pool is at 0, + // so all candidate peers are at least 10 blocks ahead of the sync pool. + + // Add a peer (fork). + let peer_1 = sample_peer_ip(1); + sync.update_peer_block_locators(peer_1, sample_block_locators_with_fork(0, 20, 11)).await.unwrap(); + + // Add a peer. + let peer_2 = sample_peer_ip(2); + sync.update_peer_block_locators(peer_2, sample_block_locators(0, 10)).await.unwrap(); + + // Add a peer. + let peer_3 = sample_peer_ip(3); + sync.update_peer_block_locators(peer_3, sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let comm = DummyCommunicationService; + let (requests, _) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 10); + + // Check the requests. + for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { + assert_eq!(height, 1 + idx as u32); + assert_eq!(hash, Some((Field::::from_u32(height)).into())); + assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); + assert_eq!(num_sync_ips, 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash. + } +} + +/*#[tokio::test] +async fn test_prepare_block_requests_with_leading_fork_at_10() { + let rng = &mut TestRng::default(); + let sync = sample_sync_at_height(0); + + // Intuitively, peer 1's fork is at peer 2 and peer 3's height. + // So from peer 2 and peer 3's perspective, they recognize that peer 1 has forked. + // Thus, you don't have NUM_REDUNDANCY peers to sync to block 10. + // + // Now, while you could in theory sync up to block 9 from any of the 3 peers, + // we choose not to do this as either side is likely to disconnect from us, + // and we would rather wait for enough redundant peers before syncing. + + // When there are NUM_REDUNDANCY peers ahead, and 1 peer is on a leading fork at 10, + // then the sync pool should not request blocks as 1 peer conflicts with the other NUM_REDUNDANCY-1 peers. + // We choose to sync with a cohort of peers that are *consistent* with each other, + // and prioritize from descending heights (so the highest peer gets priority). + + // Add a peer (fork). + let peer_1 = sample_peer_ip(1); + sync.update_peer_block_locators(peer_1, sample_block_locators_with_fork(0, 20, 10)).await.unwrap(); + + // Add a peer. + let peer_2 = sample_peer_ip(2); + sync.update_peer_block_locators(peer_2, sample_block_locators(0, 10)).await.unwrap(); + + // Add a peer. + let peer_3 = sample_peer_ip(3); + sync.update_peer_block_locators(peer_3, sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let comm = DummyCommunicationService::default(); + let (requests, _) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 0); + + // When there are NUM_REDUNDANCY+1 peers ahead, and 1 is on a fork, then there should be block requests. + + // Add a peer. + let peer_4 = sample_peer_ip(4); + sync.update_peer_block_locators(peer_4, sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let (requests, sync_peers) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 10); + + // Check the requests. + for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + assert_eq!(height, 1 + idx as u32); + assert_eq!(hash, Some((Field::::from_u32(height)).into())); + assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); + assert_eq!(sync_ips.len(), 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash. + assert_ne!(sync_ips[0], peer_1); // It should never be the forked peer. + } +} + +#[tokio::test] +async fn test_prepare_block_requests_with_trailing_fork_at_9() { + let rng = &mut TestRng::default(); + let sync = sample_sync_at_height(0); + + // Peer 1 and 2 diverge from peer 3 at block 10. We only sync when there are NUM_REDUNDANCY peers + // who are *consistent* with each other. So if you add a 4th peer that is consistent with peer 1 and 2, + // then you should be able to sync up to block 10, thereby biasing away from peer 3. + + // Add a peer (fork). + let peer_1 = sample_peer_ip(1); + sync.update_peer_block_locators(peer_1, sample_block_locators(0, 10)).await.unwrap(); + + // Add a peer. + let peer_2 = sample_peer_ip(2); + sync.update_peer_block_locators(peer_2, sample_block_locators(0, 10)).await.unwrap(); + + // Add a peer. + let peer_3 = sample_peer_ip(3); + sync.update_peer_block_locators(peer_3, sample_block_locators_with_fork(0, 20, 10)).await.unwrap(); + + // Prepare the block requests. + let comm = DummyCommunicationService::default(); + let (requests, _) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 0); + + // When there are NUM_REDUNDANCY+1 peers ahead, and peer 3 is on a fork, then there should be block requests. + + // Add a peer. + let peer_4 = sample_peer_ip(4); + sync.update_peer_block_locators(peer_4, sample_block_locators(0, 10)).await.unwrap(); + + // Prepare the block requests. + let (requests, sync_peers) = sync.prepare_block_requests(&comm).await; + assert_eq!(requests.len(), 10); + + // Check the requests. + for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() { + // Construct the sync IPs. + let sync_ips: IndexSet<_> = sync_peers.keys().choose_multiple(rng, num_sync_ips).into_iter().copied().collect(); + assert_eq!(height, 1 + idx as u32); + assert_eq!(hash, Some((Field::::from_u32(height)).into())); + assert_eq!(previous_hash, Some((Field::::from_u32(height - 1)).into())); + assert_eq!(sync_ips.len(), 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash. + assert_ne!(sync_ips[0], peer_3); // It should never be the forked peer. + } +}*/ + +#[tokio::test] +async fn test_update_peer_locators() { + let sync = sample_sync_at_height(0); + + // Test 2 peers. + let peer1_ip = sample_peer_ip(1); + for peer1_height in 0..500u32 { + sync.update_peer_block_locators( + peer1_ip, + sample_block_locators(peer1_height.saturating_sub(100), peer1_height), + ) + .await + .unwrap(); + assert_eq!(sync.get_peer_height(&peer1_ip), Some(peer1_height)); + + let peer2_ip = sample_peer_ip(2); + for peer2_height in 0..500u32 { + println!("Testing peer 1 height at {peer1_height} and peer 2 height at {peer2_height}"); + + sync.update_peer_block_locators( + peer2_ip, + sample_block_locators(peer2_height.saturating_sub(0), peer2_height), + ) + .await + .unwrap(); + assert_eq!(sync.get_peer_height(&peer2_ip), Some(peer2_height)); + } + } +} + +#[tokio::test] +async fn test_remove_peer() { + let sync = sample_sync_at_height(0); + + let peer_ip = sample_peer_ip(1); + sync.update_peer_block_locators(peer_ip, sample_block_locators(0, 100)).await.unwrap(); + assert_eq!(sync.get_peer_height(&peer_ip), Some(100)); + + sync.remove_peer(&peer_ip); + assert_eq!(sync.get_peer_height(&peer_ip), None); + + sync.update_peer_block_locators(peer_ip, sample_block_locators(0, 200)).await.unwrap(); + assert_eq!(sync.get_peer_height(&peer_ip), Some(200)); + + sync.remove_peer(&peer_ip); + assert_eq!(sync.get_peer_height(&peer_ip), None); +} + +#[tokio::test] +async fn test_locators_insert_remove_insert() { + let sync = sample_sync_at_height(0); + + let peer_ip = sample_peer_ip(1); + sync.update_peer_block_locators(peer_ip, sample_block_locators(0, 100)).await.unwrap(); + assert_eq!(sync.get_peer_height(&peer_ip), Some(100)); + + sync.remove_peer(&peer_ip); + assert_eq!(sync.get_peer_height(&peer_ip), None); + + sync.update_peer_block_locators(peer_ip, sample_block_locators(0, 200)).await.unwrap(); + assert_eq!(sync.get_peer_height(&peer_ip), Some(200)); +} diff --git a/node/sync/src/ping.rs b/node/sync/src/ping.rs index fddd6b0f4e..4134c3aaaa 100644 --- a/node/sync/src/ping.rs +++ b/node/sync/src/ping.rs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::locators::BlockLocators; use snarkos_node_router::Router; use snarkvm::prelude::Network; @@ -36,24 +35,24 @@ use tokio::{sync::Notify, time::timeout}; /// for when a peer should next be pinged. /// /// TODO (kaimast): maybe keep track of the last ping too, to not trigger spam detection? -struct PingInner { +struct PingInner { /// The next time we should ping a peer. next_ping: BTreeMap, - /// The most recent block locators. + /// The current block height. /// (or None if this node does not offer block sync) - block_locators: Option>, + block_height: Option, } /// Manages sending Ping messages to all connected peers. pub struct Ping { router: Router, - inner: Arc>>, + inner: Arc>, notify: Arc, } -impl PingInner { - fn new(block_locators: Option>) -> Self { - Self { block_locators, next_ping: Default::default() } +impl PingInner { + fn new(block_height: Option) -> Self { + Self { block_height, next_ping: Default::default() } } } @@ -67,9 +66,9 @@ impl Ping { /// # Usage /// Initialize this with the most up-to-date block locators and call /// update_block_locators, whenever a new block is received/created. - pub fn new(router: Router, block_locators: BlockLocators) -> Self { + pub fn new(router: Router, block_height: u32) -> Self { let notify = Arc::new(Notify::default()); - let inner = Arc::new(Mutex::new(PingInner::new(Some(block_locators)))); + let inner = Arc::new(Mutex::new(PingInner::new(Some(block_height)))); { let inner = inner.clone(); @@ -116,22 +115,22 @@ impl Ping { /// Notify the ping logic that a new peer connected. pub fn on_peer_connected(&self, peer_ip: SocketAddr) { // Send the first ping. - let locators = self.inner.lock().block_locators.clone(); - if !self.router.send_ping(peer_ip, locators) { + let block_height = self.inner.lock().block_height; + if !self.router.send_ping(peer_ip, block_height) { warn!("Peer {peer_ip} connected and immediately disconnected?"); } } /// Notify the ping logic that new blocks were created or synced. - pub fn update_block_locators(&self, locators: BlockLocators) { - self.inner.lock().block_locators = Some(locators); + pub fn update_block_height(&self, block_height: u32) { + self.inner.lock().block_height = Some(block_height); // wake up the ping task self.notify.notify_one(); } /// Background task that periodically sends out new ping messages. - async fn ping_task(inner: &Mutex>, router: &Router, notify: &Notify) { + async fn ping_task(inner: &Mutex, router: &Router, notify: &Notify) { let mut new_block = false; loop { @@ -165,7 +164,7 @@ impl Ping { } /// Ping all peers that have an expired timer. - fn ping_expired_peers(now: Instant, inner: &mut PingInner, router: &Router) { + fn ping_expired_peers(now: Instant, inner: &mut PingInner, router: &Router) { loop { // Find next peer to contact. let peer_ip = { @@ -181,8 +180,7 @@ impl Ping { }; // Send new ping - let locators = inner.block_locators.clone(); - let success = router.send_ping(peer_ip, locators.clone()); + let success = router.send_ping(peer_ip, inner.block_height); inner.next_ping.pop_first(); if !success { @@ -192,13 +190,12 @@ impl Ping { } /// Ping all known peers. - fn ping_all_peers(inner: &mut PingInner, router: &Router) { + fn ping_all_peers(inner: &mut PingInner, router: &Router) { let peers: Vec = inner.next_ping.values().copied().collect(); inner.next_ping.clear(); for peer_ip in peers { - let locators = inner.block_locators.clone(); - let success = router.send_ping(peer_ip, locators); + let success = router.send_ping(peer_ip, inner.block_height); if !success { trace!("Failed to send ping to peer {peer_ip}. Disconnected.");