From b163f0989c7748b688b9aaf544cb7842cf558dc2 Mon Sep 17 00:00:00 2001 From: ViacheslavB Date: Mon, 23 Mar 2026 12:57:36 +0300 Subject: [PATCH 01/48] Bump node version up for upcoming release --- src/node/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/Cargo.toml b/src/node/Cargo.toml index ac23208..d77fb2b 100644 --- a/src/node/Cargo.toml +++ b/src/node/Cargo.toml @@ -3,7 +3,7 @@ build = '../common/build/build.rs' edition = '2021' license = 'GPL-3.0' name = 'node' -version = '0.3.0' +version = '0.4.0' [[bin]] name = 'adnl_resolve' @@ -128,4 +128,4 @@ telemetry = ['adnl/telemetry', 'storage/telemetry'] trace_alloc = [] trace_alloc_detail = ['trace_alloc'] xp25 = ['ton_block/xp25', 'adnl/xp25'] -mirrornet = ['ton_block/mirrornet'] \ No newline at end of file +mirrornet = ['ton_block/mirrornet'] From 995188f856c87b3f0d4aa6aca7d3c820f4659439 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Tue, 24 Mar 2026 14:15:27 +0300 Subject: [PATCH 02/48] Validate query uses version and capabilities from config instead of block --- src/Cargo.lock | 2 +- src/node/src/validator/validate_query.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index 01423f0..c7e6140 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -3245,7 +3245,7 @@ dependencies = [ [[package]] name = "node" -version = "0.3.0" +version = "0.4.0" dependencies = [ "adnl", "ahash", diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 8f91d99..0479bf9 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -5544,8 +5544,8 @@ impl ValidateQuery { tasks: &mut TasksVec, ) -> Result<()> { // log::debug!(target: "validate_query", "({}): checking all transactions", base.next_block_descr); - let (capabilities, block_version) = - base.info.gen_software().map_or((0, 0), |v| (v.capabilities, v.version)); + let block_version = base.config_params.global_version(); + let capabilities = base.config_params.capabilities(); let config = BlockchainConfig::with_params(capabilities, block_version, base.config_params.clone())?; base.account_blocks.iterate_with_keys_and_aug(|account_addr, acc_block, _fee| { From 9cf68c5b5173410fcd3227ee0e798fbc5b5a8e65 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Wed, 25 Mar 2026 12:08:41 +0300 Subject: [PATCH 03/48] Validate query uses capabilities from blockchain config, not candidate block --- src/node/src/tests/static/zerostate.boc | Bin 25204 -> 23441 bytes src/node/src/validator/validate_query.rs | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node/src/tests/static/zerostate.boc b/src/node/src/tests/static/zerostate.boc index a7f24b451f07e3b28f0fb143dc51043e6cac38b4..040620a36c546e1baaa87cc990a4f846de0a328e 100644 GIT binary patch delta 2431 zcmX|D30PIf6`p_Yd%Q)K2NGrXsR5K-KoIq@iGawy1Q1cQ8jPVDwGoL`fhO8)R5&%J z)>so`qp8M)AXPCMLz<>>*Tm4IsYa8SxS){)#D&hewte6C=D+iwf6h5GXYS1H=|poo zY;89-E|yY83kXrsAOy`ZEHuiJV?iMVqZiqXCYPfjGY=Qc*kF^`hjt1w+CAi8AS?D> z){>`%S@es$(w#OM{R5<1e)RdBFg+#34zW{-Of&j(11gp%(GTJ(`8q{v5i*i;oIbKi zH}T2x6R#dVViutyOpTvs%CbEoh5$#zP^q1?3ondrJ!&#Rv3|_@>udlP3>!!Fx9U%@C?>fi3p~vf1Gq8j%!;jP$T4elL)}HL8i=ke0Gt@+PLj7$1;x>zW zbn#D092mxH(92|>PTo=Ey0EC>_r!fwoX@Fih&<>ML8%T+SgO1 zaRffR%3knhcm~Ua>lmhn zC=bCrbT~%9sw9dI;{HfHN{gabxCL@26k!bcD4>*>x!Ne@czaB6NQlr*+1U_|2v`ny zj65(3S=^6A6hl15N!f)lh^GFSl^9EPu@kfy+#|mXUpf;zM~f9Z=vGD&)u#=n7ssyF z;zS2w@(>zGG*V>TUTXq-aUfAXpyZrVsv0}Wfbp0>3*yTx6NU4~*Na#6Rg-(}I5Le( zu%=>-L>ZEhOc(O1O?CKiPj15(G%TSCDde5-I5Md|p-4-`B-)g}0%>$Cw+!jDb9^Hv zQ`CeHv9ISotl(HrH)igr40@rWK)}?QCV|nd}0G~sUxv5 zE>BqXOgP&@6fr!GYKa9XhI(D?sK9iX_uiOOgU4WQ4X902Y4+2T$qq^^N|q?4+ey7B zr>n^;F^%S@WSi^o>AqlX(n>((U@}8V)orfnlou$}KizctK&X<^vCs89V z6Ux1*JJFMZ)3Z&#MBAQueY=#iB-)?ujYV`ey%7uOv5Yb-q+J<}+EXG-)oci5Wj3g# zSVAW<^YAMQn9`;_Eyhz`y7Dz;xmJsL)IC1h&}_#t+LBdhY(<@Dq%m+lN0N~~gN#BZo{>NeE3WH)LJV$=V(!{+Q)%+IpoMZ83bIdyoBcL7$b>MwF`!Wvkc z>Mt3(@LOuib>MmOD=3$0H-#6LQ+?h{yv$nyzt=ZF6Mm;}fVKJtXyFZDXcn`tkXvaO z1sARc(L04{KI`!+$NMwhf@S|}$6j8GKXCt#*htPr4cI_UMF-5Uaa}g!b-gZ|xGwk; zxt7%84O(52uf3_x4%eb6a!X5TNpdw{i^!tpoFH1C)}U<_i)s3_cza*9luNM<+u8dr z?1pLg#4~kxTVcV$)7VLsrS$y=sPY!+n2cd1- zjDK>c1BVnylQeVhJsf8EL_fyWnejf*gO8XxiVvWw`8Rc-qm!EtMKYbPQ0JB1NY^Sd z&3|Xr$M^?LoE|XtxPAfPBu+7V8GJe5CT=mD#upg9pU%r%1A?0y=iGQ*BEBu?9Dr2n|aFM%T;}RXK>|FINtG|1Azc@dfBGlz^JQ-K;JqvsB zgHUtj$E&!;a9s%$bYpQ&!{I*pIsm_jKP<;J!eaBK~VutPnUSkSUY|=a?4CDx0NII?=3| z(GeDDWxbE|Wf&x#8PpdFgNt-kg^=!?qq;}z(njyk%=h$^kFeECdNHTIIqY&M1y*f@ zHyy2t@br`ZY_`kcYzg3I7#U_|d5n-Fp|T5N=230}scKfj@L*PtlA{?yWT={}jFMxx X7be3+>f!eP-NYs~@fXmAS!@3TI%Jgd delta 4188 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 RPdMZ_3vnef!_{)k{{qz$=eGa= diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 0479bf9..29b9d86 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -5544,10 +5544,11 @@ impl ValidateQuery { tasks: &mut TasksVec, ) -> Result<()> { // log::debug!(target: "validate_query", "({}): checking all transactions", base.next_block_descr); - let block_version = base.config_params.global_version(); - let capabilities = base.config_params.capabilities(); - let config = - BlockchainConfig::with_params(capabilities, block_version, base.config_params.clone())?; + let config = BlockchainConfig::with_params( + base.config_params.capabilities(), + base.info.gen_software().map_or(0, |v| v.version), + base.config_params.clone() + )?; base.account_blocks.iterate_with_keys_and_aug(|account_addr, acc_block, _fee| { let base = base.clone(); let libraries = libraries.clone(); From 72f7bb62fc3349e651c206722949cc4571bb9b99 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Wed, 25 Mar 2026 13:00:29 +0300 Subject: [PATCH 04/48] Validate query uses capabilities from blockchain config, not candidate block --- src/node/src/validator/validate_query.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 29b9d86..849771c 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -764,7 +764,8 @@ impl ValidateQuery { let mc_state_extra = mc_state.shard_state_extra()?.clone(); let config_params = mc_state_extra.config(); CHECK!(config_params, inited); - let block_version = base.info.gen_software().map_or(0, |v| v.version); + let (capabilities, block_version) = + base.info.gen_software().map_or((0, 0), |v| (v.capabilities, v.version)); if block_version < config_params.global_version() { reject_query!( "This block version {} is too old, node_version: {} net version: {}", @@ -5547,7 +5548,7 @@ impl ValidateQuery { let config = BlockchainConfig::with_params( base.config_params.capabilities(), base.info.gen_software().map_or(0, |v| v.version), - base.config_params.clone() + base.config_params.clone(), )?; base.account_blocks.iterate_with_keys_and_aug(|account_addr, acc_block, _fee| { let base = base.clone(); From 349ad012ed6422720256c17ed3095cf50e526dee Mon Sep 17 00:00:00 2001 From: Slava Date: Wed, 25 Mar 2026 22:49:08 +0300 Subject: [PATCH 05/48] Simplex fixes and upgrades --- src/.cargo/audit.toml | 13 + src/.gitignore | 3 +- src/Cargo.lock | 49 +- src/Cargo.toml | 3 +- src/adnl/src/adnl/transport.rs | 31 +- src/adnl/src/overlay/mod.rs | 9 +- src/adnl/src/quic/mod.rs | 97 +- src/adnl/tests/test_quic.rs | 4 +- src/audit.toml | 7 - src/block-json/src/deserialize.rs | 1 + src/block-json/src/serialize.rs | 3 + src/block-json/src/tests/test_deserialize.rs | 2 + src/block/src/config_params.rs | 13 +- src/block/src/error.rs | 2 + src/block/src/out_actions.rs | 71 +- src/block/src/storage_stat.rs | 37 + src/block/src/tests/test_config_params.rs | 20 + src/block/src/tests/test_out_actions.rs | 35 +- src/common/config/log_cfg_debug.yml | 2 + ...ad_action_with_ignore_flag_account_new.boc | Bin 0 -> 756 bytes ...ad_action_with_ignore_flag_account_old.boc | Bin 0 -> 756 bytes ...ad_action_with_ignore_flag_transaction.boc | Bin 0 -> 532 bytes ...est_transaction_executor_with_real_data.rs | 10 + src/executor/src/transaction_executor.rs | 57 +- src/node/Cargo.toml | 5 +- .../tests/test_adnl_overlay.rs | 2 + src/node/simplex/CHANGELOG.md | 27 +- src/node/simplex/Cargo.toml | 2 +- src/node/simplex/README.md | 48 +- src/node/simplex/src/block.rs | 17 +- src/node/simplex/src/database.rs | 336 ++++- src/node/simplex/src/lib.rs | 6 + src/node/simplex/src/receiver.rs | 334 +++-- src/node/simplex/src/session.rs | 31 +- src/node/simplex/src/session_processor.rs | 776 +++++++--- src/node/simplex/src/simplex_state.rs | 611 +++++--- src/node/simplex/src/startup_recovery.rs | 411 +----- src/node/simplex/src/tests/test_block.rs | 7 +- .../src/tests/test_candidate_resolver.rs | 90 +- src/node/simplex/src/tests/test_receiver.rs | 39 +- src/node/simplex/src/tests/test_restart.rs | 193 +-- .../src/tests/test_session_processor.rs | 516 ++++++- .../simplex/src/tests/test_simplex_state.rs | 208 ++- src/node/simplex/src/utils.rs | 17 +- src/node/simplex/tests/test_collation.rs | 15 +- src/node/simplex/tests/test_consensus.rs | 50 +- src/node/simplex/tests/test_restart.rs | 24 +- src/node/simplex/tests/test_validation.rs | 13 +- src/node/src/collator_test_bundle.rs | 6 + src/node/src/network/node_network.rs | 4 + src/node/src/tests/static/zerostate.boc | Bin 23441 -> 25204 bytes src/node/src/tests/test_helper.rs | 7 + src/node/src/validator/collator.rs | 97 +- src/node/src/validator/consensus.rs | 9 +- src/node/src/validator/consensus_overlay.rs | 3 +- src/node/src/validator/fabric.rs | 10 +- src/node/src/validator/mod.rs | 10 +- src/node/src/validator/tests/test_collator.rs | 51 +- src/node/src/validator/validate_query.rs | 157 +- src/node/src/validator/validator_group.rs | 17 +- src/node/src/validator/validator_manager.rs | 48 +- src/node/tests/compat_test/.gitignore | 7 + src/node/tests/compat_test/Cargo.toml | 79 + src/node/tests/compat_test/Makefile | 112 ++ src/node/tests/compat_test/README.md | 281 ++++ .../tests/compat_test/cpp_src/CMakeLists.txt | 258 ++++ .../compat_test/cpp_src/compat_test_node.cpp | 1296 +++++++++++++++++ .../compat_test/cpp_src/compat_test_node.hpp | 222 +++ .../tests/compat_test/incompatibilities.md | 62 + src/node/tests/compat_test/src/lib.rs | 930 ++++++++++++ src/node/tests/compat_test/src/overlay_id.rs | 67 + .../tests/compat_test/src/test_helpers.rs | 778 ++++++++++ .../compat_test/tests/test_boc_compression.rs | 343 +++++ .../tests/compat_test/tests/test_broadcast.rs | 188 +++ .../tests/test_broadcast_validation.rs | 171 +++ .../tests/test_candidate_id_to_sign.rs | 85 ++ .../tests/compat_test/tests/test_fec_relay.rs | 358 +++++ .../compat_test/tests/test_overlay_id.rs | 126 ++ .../compat_test/tests/test_overlay_message.rs | 293 ++++ .../compat_test/tests/test_public_overlay.rs | 137 ++ .../compat_test/tests/test_quic_overlay.rs | 290 ++++ .../tests/test_quic_private_overlay.rs | 360 +++++ .../compat_test/tests/test_quic_transport.rs | 181 +++ .../compat_test/tests/test_rldp_query.rs | 576 ++++++++ .../tests/test_twostep_fec_relay.rs | 428 ++++++ .../tests/test_run_net_py/log_cfg_blank.yml | 5 + .../tests/test_run_net_py/simplex_config.json | 2 +- .../tests/test_run_net_py/test_run_net.py | 115 +- src/rust-toolchain.toml | 1 - src/tl/ton_api/tl/ton_api.tl | 4 + src/vm/src/tests/test_executor.rs | 22 +- 91 files changed, 10882 insertions(+), 1561 deletions(-) create mode 100644 src/.cargo/audit.toml delete mode 100644 src/audit.toml create mode 100644 src/executor/real_boc/bad_action_with_ignore_flag_account_new.boc create mode 100644 src/executor/real_boc/bad_action_with_ignore_flag_account_old.boc create mode 100644 src/executor/real_boc/bad_action_with_ignore_flag_transaction.boc create mode 100644 src/node/tests/compat_test/.gitignore create mode 100644 src/node/tests/compat_test/Cargo.toml create mode 100644 src/node/tests/compat_test/Makefile create mode 100644 src/node/tests/compat_test/README.md create mode 100644 src/node/tests/compat_test/cpp_src/CMakeLists.txt create mode 100644 src/node/tests/compat_test/cpp_src/compat_test_node.cpp create mode 100644 src/node/tests/compat_test/cpp_src/compat_test_node.hpp create mode 100644 src/node/tests/compat_test/incompatibilities.md create mode 100644 src/node/tests/compat_test/src/lib.rs create mode 100644 src/node/tests/compat_test/src/overlay_id.rs create mode 100644 src/node/tests/compat_test/src/test_helpers.rs create mode 100644 src/node/tests/compat_test/tests/test_boc_compression.rs create mode 100644 src/node/tests/compat_test/tests/test_broadcast.rs create mode 100644 src/node/tests/compat_test/tests/test_broadcast_validation.rs create mode 100644 src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs create mode 100644 src/node/tests/compat_test/tests/test_fec_relay.rs create mode 100644 src/node/tests/compat_test/tests/test_overlay_id.rs create mode 100644 src/node/tests/compat_test/tests/test_overlay_message.rs create mode 100644 src/node/tests/compat_test/tests/test_public_overlay.rs create mode 100644 src/node/tests/compat_test/tests/test_quic_overlay.rs create mode 100644 src/node/tests/compat_test/tests/test_quic_private_overlay.rs create mode 100644 src/node/tests/compat_test/tests/test_quic_transport.rs create mode 100644 src/node/tests/compat_test/tests/test_rldp_query.rs create mode 100644 src/node/tests/compat_test/tests/test_twostep_fec_relay.rs diff --git a/src/.cargo/audit.toml b/src/.cargo/audit.toml new file mode 100644 index 0000000..9f16dff --- /dev/null +++ b/src/.cargo/audit.toml @@ -0,0 +1,13 @@ +[advisories] +ignore = [ + # RUSTSEC-2023-0071: Marvin Attack timing side-channel in RSA decryption. + # Not exploitable here: rsa is only used for encryption (RSA-OAEP wrapping of + # an AES key with a public key). No decryption is performed. + # Tracked: NODE-31 + "RUSTSEC-2023-0071", + # RUSTSEC-2026-0049: rustls-webpki CRL matching logic bug. + # Not exploitable here: QUIC transport uses Raw Public Keys (RPK), not X.509 + # certificates with CRLs. Waiting for upstream rustls fix. + # Tracked: NODE-47 + "RUSTSEC-2026-0049", +] diff --git a/src/.gitignore b/src/.gitignore index 04cb361..a340d68 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -9,5 +9,6 @@ node/tests/test_run_net_py/tmp node/tests/test_run_net_py/test_run_net.json node/tests/test_run_net_py/.ruff_cache .claude +node/tests/compat_test/cpp_src/build node/tests/mirrornet/mirrornet.json -node/tests/mirrornet/mirrornet_global_config.json \ No newline at end of file +node/tests/mirrornet/mirrornet_global_config.json diff --git a/src/Cargo.lock b/src/Cargo.lock index c7e6140..0bf1a75 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -246,9 +246,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" dependencies = [ "rustversion", ] @@ -920,6 +920,28 @@ dependencies = [ "yaml-rust2 0.10.4", ] +[[package]] +name = "compat_test" +version = "0.1.0" +dependencies = [ + "adnl", + "anyhow", + "async-trait", + "base64 0.22.1", + "env_logger", + "hex", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-test", + "tokio-util", + "ton_api", + "ton_block", +] + [[package]] name = "consensus-common" version = "0.2.0" @@ -2706,9 +2728,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -3245,7 +3267,7 @@ dependencies = [ [[package]] name = "node" -version = "0.4.0" +version = "0.3.0" dependencies = [ "adnl", "ahash", @@ -4612,9 +4634,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -5049,7 +5071,7 @@ dependencies = [ [[package]] name = "simplex" -version = "0.4.0" +version = "0.5.0" dependencies = [ "adnl", "anyhow", @@ -5603,6 +5625,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.18" diff --git a/src/Cargo.toml b/src/Cargo.toml index 7bfa8d6..cf32110 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -7,12 +7,13 @@ members = [ 'executor', 'emulator', 'lockfree', + 'node', 'node/catchain', 'node/consensus-common', 'node/simplex', 'node/storage', + 'node/tests/compat_test', 'node/validator-session', - 'node', 'node-control/commands', 'node-control/common', 'node-control/contracts', diff --git a/src/adnl/src/adnl/transport.rs b/src/adnl/src/adnl/transport.rs index 909d15f..50737d2 100644 --- a/src/adnl/src/adnl/transport.rs +++ b/src/adnl/src/adnl/transport.rs @@ -16,7 +16,7 @@ use std::{ io::{ErrorKind, Read}, net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{ - atomic::{AtomicU8, Ordering}, + atomic::{AtomicU8, AtomicUsize, Ordering}, mpsc::{channel, Receiver, Sender, TryRecvError}, Arc, }, @@ -114,6 +114,8 @@ impl Connections { pub(crate) struct SendQueue { buffer: lockfree::queue::Queue, sync: AtomicU8, + len: AtomicUsize, + capacity: usize, } impl SendQueue { @@ -122,9 +124,15 @@ impl SendQueue { const SYNC_CHECKING: u8 = 2; pub(crate) fn new() -> Arc { + Self::with_capacity(usize::MAX) + } + + pub(crate) fn with_capacity(capacity: usize) -> Arc { Arc::new(SendQueue { buffer: lockfree::queue::Queue::new(), sync: AtomicU8::new(Self::SYNC_INACTIVE), + len: AtomicUsize::new(0), + capacity, }) } @@ -147,13 +155,32 @@ impl SendQueue { } pub(crate) fn pop(&self) -> Option { - self.buffer.pop() + let item = self.buffer.pop(); + if item.is_some() { + self.len.fetch_sub(1, Ordering::Relaxed); + } + item } pub(crate) fn push(&self, data: Q) { + self.len.fetch_add(1, Ordering::Relaxed); self.buffer.push(data); } + /// Push data only if the queue has not reached its capacity. + /// Returns `true` if the data was enqueued, `false` if the queue is full. + pub(crate) fn try_push(&self, data: Q) -> bool { + if self.len.load(Ordering::Relaxed) >= self.capacity { + return false; + } + self.push(data); + true + } + + pub(crate) fn is_inactive(&self) -> bool { + self.sync.load(Ordering::Relaxed) == Self::SYNC_INACTIVE + } + fn switch(&self, from: u8, to: u8) -> bool { self.sync.compare_exchange(from, to, Ordering::Relaxed, Ordering::Relaxed).is_ok() } diff --git a/src/adnl/src/overlay/mod.rs b/src/adnl/src/overlay/mod.rs index 0707b31..4d51a11 100644 --- a/src/adnl/src/overlay/mod.rs +++ b/src/adnl/src/overlay/mod.rs @@ -35,8 +35,8 @@ use std::{ time::{Duration, Instant}, }; use ton_api::{ - deserialize_boxed_bundle_with_suffix, deserialize_boxed_with_suffix, serialize_boxed, - serialize_boxed_append, + deserialize_boxed, deserialize_boxed_bundle_with_suffix, deserialize_boxed_with_suffix, + serialize_boxed, serialize_boxed_append, ton::{ adnl::id::short::Short as AdnlShortId, catchain::{ @@ -1849,7 +1849,10 @@ impl OverlayNode { .await?; let mut data = overlay.query_prefix.clone(); serialize_boxed_append(&mut data, &query.object)?; - quic.query(data, Some(&self.adnl), peers.local(), peers.other(), timeout_ms).await + match quic.query(data, Some(&self.adnl), peers.local(), peers.other(), timeout_ms).await? { + Some(raw) => Ok(Some(deserialize_boxed(&raw)?)), + None => Ok(None), + } } /// Send query via RLDP diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index 470f513..44aafe6 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -27,7 +27,7 @@ use ton_api::{ request::{Message as QuicMessage, Query as QuicQuery}, Request, Response as QuicResponse, }, - IntoBoxed, TLObject, + IntoBoxed, }; use ton_block::{ ed25519_encode_private_key_to_pkcs8, error, fail, Ed25519KeyOption, KeyId, Result, @@ -340,6 +340,9 @@ impl QuicNode { const DEFAULT_MAX_STREAMS_PER_CONNECTION: usize = 256; + /// Maximum number of messages buffered per outbound peer + const SEND_QUEUE_CAPACITY: usize = 1024; + /// Register a local identity on a specific bind address. /// Creates a new endpoint if one doesn't exist for this port yet. pub fn add_key( @@ -445,7 +448,10 @@ impl QuicNode { match self.get_outbound_connection(src, dst, true).await? { QuicOutboundConnection { conn: Some(ref conn), ref send_queue } => { if send_queue.check(true) { - send_queue.push(data); + if !send_queue.try_push(data) { + send_queue.check(false); + fail!("QUIC send queue full for peer {dst}"); + } while !send_queue.check(false) { tokio::task::yield_now().await; } @@ -464,7 +470,11 @@ impl QuicNode { return Ok(Some(len)); } } - QuicOutboundConnection { conn: None, ref send_queue } => send_queue.push(data), + QuicOutboundConnection { conn: None, ref send_queue } => { + if !send_queue.try_push(data) { + fail!("QUIC send queue full for peer {dst} (connecting)"); + } + } } Ok(None) } @@ -489,7 +499,7 @@ impl QuicNode { src: &Arc, dst: &Arc, timeout_ms: Option, - ) -> Result> { + ) -> Result>> { self.ensure_peer_registered(adnl, src, dst)?; let timeout_ms = timeout_ms.unwrap_or(Self::DEFAULT_QUERY_TIMEOUT_MS); let wire = serialize_boxed(&QuicQuery { data: data.into() }.into_boxed())?; @@ -500,16 +510,27 @@ impl QuicNode { let obj = deserialize_boxed(&response) .map_err(|e| error!("Cannot deserialise QUIC answer: {e}"))?; match obj.downcast::() { - Ok(QuicResponse::Quic_Answer(answer)) => Ok(Some( - deserialize_boxed(&answer.data) - .map_err(|e| error!("Cannot deserialise QUIC answer payload: {e}"))?, - )), + Ok(QuicResponse::Quic_Answer(answer)) => Ok(Some(answer.data.to_vec())), Err(x) => fail!("Unexpected QUIC response type {x:?}"), } } - /// Shut down all QUIC endpoints. + /// Shut down all QUIC endpoints, cancel background tasks, and release resources. pub fn shutdown(&self) { + // Cancel all background tasks (accept loops, connection checkers, drain tasks) + self.cancellation_token.cancel(); + + // Close all outbound connections in every local key state + for entry in self.local_keys.iter() { + let outbound = &entry.val().outbound; + for conn_entry in outbound.map().iter() { + if let Some(ref conn) = conn_entry.val().conn { + conn.close(0u32.into(), b"shutdown"); + } + } + } + + // Close all endpoints if let Ok(endpoints) = self.endpoints.lock() { for (port, state) in endpoints.iter() { log::info!(target: TARGET, "Shutting down QUIC endpoint on port {port}"); @@ -698,6 +719,7 @@ impl QuicNode { self.subscribers.clone(), bind_addr, self.max_streams_per_connection, + self.cancellation_token.clone(), ); let state = Arc::new(EndpointState { @@ -741,7 +763,7 @@ impl QuicNode { }); } None => { - let queue = QuicSendQueue::new(); + let queue = QuicSendQueue::with_capacity(Self::SEND_QUEUE_CAPACITY); add_unbound_object_to_map(outbound.map(), addr, || { Ok(QuicOutboundConnection { conn: None, send_queue: queue.clone() }) })?; @@ -1209,27 +1231,43 @@ impl QuicNode { subscribers: Arc>>, bind_addr: SocketAddr, max_streams_per_connection: usize, + cancellation_token: tokio_util::sync::CancellationToken, ) { tokio::spawn(async move { let inbound: Arc> = Connections::new(); loop { log::trace!(target: TARGET, "Loop QUIC server on {bind_addr}"); - let Some(incoming) = endpoint.accept().await else { - log::info!(target: TARGET, "QUIC endpoint on {bind_addr} closed"); - break; - }; - let addr = incoming.remote_address(); - log::debug!(target: TARGET, "Accept in QUIC server on {bind_addr} from {addr}"); - - tokio::spawn(Self::handle_connection( - incoming, - local_key_names.clone(), - server_cert_resolver.clone(), - inbound.clone(), - subscribers.clone(), - bind_addr, - max_streams_per_connection, - )); + tokio::select! { + _ = cancellation_token.cancelled() => { + log::info!(target: TARGET, "QUIC accept loop on {bind_addr} cancelled"); + break; + } + incoming = endpoint.accept() => { + let Some(incoming) = incoming else { + log::info!(target: TARGET, "QUIC endpoint on {bind_addr} closed"); + break; + }; + let addr = incoming.remote_address(); + log::debug!(target: TARGET, "Accept in QUIC server on {bind_addr} from {addr}"); + + let token = cancellation_token.clone(); + let lkn = local_key_names.clone(); + let scr = server_cert_resolver.clone(); + let ib = inbound.clone(); + let subs = subscribers.clone(); + tokio::spawn(async move { + tokio::select! { + _ = token.cancelled() => { + log::debug!(target: TARGET, "QUIC connection handler for {addr} cancelled"); + } + _ = Self::handle_connection( + incoming, lkn, scr, ib, subs, bind_addr, + max_streams_per_connection, + ) => {} + } + }); + } + } } }); } @@ -1266,6 +1304,13 @@ impl QuicNode { conn.close_reason() ); Self::remove_dead_connection(outbound, addr, conn); + // Fully remove entry if queue is drained + if let Some(entry) = outbound.map().get(&addr) { + let s = entry.val(); + if s.conn.is_none() && s.send_queue.is_inactive() { + outbound.map().remove(&addr); + } + } removed += 1; } } diff --git a/src/adnl/tests/test_quic.rs b/src/adnl/tests/test_quic.rs index 308365e..72e1c54 100644 --- a/src/adnl/tests/test_quic.rs +++ b/src/adnl/tests/test_quic.rs @@ -88,8 +88,8 @@ fn make_ping_wire(value: i64) -> Vec { serialize_boxed(&QuicQuery { data: make_ping_data(value).into() }.into_boxed()).unwrap() } -fn parse_pong(obj: TLObject) -> i64 { - obj.downcast::().unwrap().only().value +fn parse_pong(data: Vec) -> i64 { + deserialize_boxed(&data).unwrap().downcast::().unwrap().only().value } /// Parse pong from raw wire bytes (for low-level stream tests) diff --git a/src/audit.toml b/src/audit.toml deleted file mode 100644 index bf3a76d..0000000 --- a/src/audit.toml +++ /dev/null @@ -1,7 +0,0 @@ -[advisories] -ignore = [ - # RUSTSEC-2023-0071: Marvin Attack timing side-channel in RSA decryption. - # Not exploitable here: rsa is only used for encryption (RSA-OAEP wrapping of - # an AES key with a public key). No decryption is performed. - "RUSTSEC-2023-0071", -] diff --git a/src/block-json/src/deserialize.rs b/src/block-json/src/deserialize.rs index 7af350a..d10d88a 100644 --- a/src/block-json/src/deserialize.rs +++ b/src/block-json/src/deserialize.rs @@ -666,6 +666,7 @@ impl StateParser { fn parse_simplex_config(p: &PathMap) -> Result { Ok(SimplexConfig { + use_quic: p.get_num32("use_quic").unwrap_or(0) != 0, target_rate_ms: p.get_num32("target_rate_ms")?, slots_per_leader_window: p.get_num32("slots_per_leader_window")?, first_block_timeout_ms: p.get_num32("first_block_timeout_ms")?, diff --git a/src/block-json/src/serialize.rs b/src/block-json/src/serialize.rs index f51ee3c..5faf3c8 100644 --- a/src/block-json/src/serialize.rs +++ b/src/block-json/src/serialize.rs @@ -1071,6 +1071,9 @@ fn serialize_accelerator(acc: &AcceleratedConsensusConfig) -> Result { fn serialize_simplex_config(cfg: &SimplexConfig) -> Result { let mut map = Map::new(); + if cfg.use_quic { + serialize_field(&mut map, "use_quic", 1u32); + } serialize_field(&mut map, "target_rate_ms", cfg.target_rate_ms); serialize_field(&mut map, "slots_per_leader_window", cfg.slots_per_leader_window); serialize_field(&mut map, "first_block_timeout_ms", cfg.first_block_timeout_ms); diff --git a/src/block-json/src/tests/test_deserialize.rs b/src/block-json/src/tests/test_deserialize.rs index e16edc9..9d2dbe5 100644 --- a/src/block-json/src/tests/test_deserialize.rs +++ b/src/block-json/src/tests/test_deserialize.rs @@ -297,12 +297,14 @@ fn get_config_param30() -> NewConsensusConfigAll { slots_per_leader_window: 4, first_block_timeout_ms: 1000, max_leader_window_desync: 100, + ..Default::default() }), shard: Some(SimplexConfig { target_rate_ms: 200, slots_per_leader_window: 8, first_block_timeout_ms: 500, max_leader_window_desync: 50, + ..Default::default() }), } } diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index 2446ffe..161b1f9 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -429,8 +429,7 @@ pub enum GlobalCapabilities { CapResolveMerkleCell = 0x0000_0200_0000, } -//TODO: LK: enable after change block version to 13 -pub const SUPPORTED_VERSION: u32 = 12; +pub const SUPPORTED_VERSION: u32 = 13; pub const LT_ALIGN: u64 = 1_000_000; impl ConfigParams { @@ -3778,16 +3777,20 @@ const SIMPLEX_CONFIG_TAG: u8 = 0x21; /// max_leader_window_desync:uint32 = NewConsensusConfig; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SimplexConfig { + pub use_quic: bool, pub target_rate_ms: u32, pub slots_per_leader_window: u32, pub first_block_timeout_ms: u32, pub max_leader_window_desync: u32, } +/// Byte layout: flags:(## 7) use_quic:Bool โ€” 7 flag bits (reserved) + 1 use_quic bit = 1 byte. +/// TLB writes MSB-first, so use_quic occupies the LSB: byte = (flags << 1) | use_quic. impl Serializable for SimplexConfig { fn write_to(&self, cell: &mut BuilderData) -> Result<()> { cell.append_u8(SIMPLEX_CONFIG_TAG)?; - cell.append_u8(0)?; // flags - reserved for future use + let flags_byte = if self.use_quic { 1u8 } else { 0u8 }; + cell.append_u8(flags_byte)?; self.target_rate_ms.write_to(cell)?; self.slots_per_leader_window.write_to(cell)?; self.first_block_timeout_ms.write_to(cell)?; @@ -3802,12 +3805,14 @@ impl Deserializable for SimplexConfig { if tag != SIMPLEX_CONFIG_TAG { fail!(Self::invalid_tag(tag as u32)); } - let _flags = slice.get_next_byte()?; // Reserved, ignore + let flags_byte = slice.get_next_byte()?; + let use_quic = (flags_byte & 1) != 0; let target_rate_ms = u32::construct_from(slice)?; let slots_per_leader_window = u32::construct_from(slice)?; let first_block_timeout_ms = u32::construct_from(slice)?; let max_leader_window_desync = u32::construct_from(slice)?; Ok(Self { + use_quic, target_rate_ms, slots_per_leader_window, first_block_timeout_ms, diff --git a/src/block/src/error.rs b/src/block/src/error.rs index 26b0c1c..c121ed0 100644 --- a/src/block/src/error.rs +++ b/src/block/src/error.rs @@ -54,6 +54,8 @@ pub enum BlockError { UnexpectedStructVariant(String, String), #[error("Mismatched serde options: {0} exp={1} real={2}")] MismatchedSerdeOptions(String, usize, usize), + #[error("OutAction deserialize error {0}, mode {1}")] + OutActionError(#[source] crate::Error, u8), } // Exception codes ***************************************************************** diff --git a/src/block/src/out_actions.rs b/src/block/src/out_actions.rs index 105b995..adb4cd3 100644 --- a/src/block/src/out_actions.rs +++ b/src/block/src/out_actions.rs @@ -9,11 +9,8 @@ * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ use crate::{ - error::{BlockError, Error}, - fail, - messages::Message, - types::CurrencyCollection, - BuilderData, Cell, Deserializable, IBitstring, Result, Serializable, SliceData, UInt256, + error::BlockError, fail, messages::Message, types::CurrencyCollection, BuilderData, Cell, + Deserializable, IBitstring, Result, Serializable, SliceData, UInt256, }; use std::collections::LinkedList; @@ -56,19 +53,6 @@ pub fn unpack_out_action_slices(mut cell: SliceData) -> Result> { Ok(slices_rev) } -pub fn deserialize_out_action_slices( - action_slices: Vec, -) -> std::result::Result, (usize, Error)> { - let mut parsed_actions = Vec::with_capacity(action_slices.len()); - for (i, mut action_slice) in action_slices.into_iter().enumerate() { - match OutAction::construct_from(&mut action_slice) { - Ok(action) => parsed_actions.push(action), - Err(err) => return Err((i, err)), - } - } - Ok(parsed_actions) -} - /// /// Implementation of Serializable for OutActions /// @@ -95,11 +79,10 @@ impl Serializable for OutActions { /// impl Deserializable for OutActions { fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { - let actions = match deserialize_out_action_slices(unpack_out_action_slices(cell.clone())?) { - Ok(actions) => actions, - Err((_, err)) => return Err(err), - }; - self.extend(actions); + let action_slices = unpack_out_action_slices(cell.clone())?; + for mut action_slice in action_slices { + self.push_back(OutAction::construct_from(&mut action_slice)?); + } Ok(()) } } @@ -123,7 +106,7 @@ pub enum OutAction { /// /// Action for reserving some account balance. /// It is roughly equivalent to creating an output - /// message carrying x nanocoins to oneself,so that + /// message carrying x nanocoins to oneself, so that /// the subsequent output actions would not be able /// to spend more money than the remainder. /// @@ -248,42 +231,46 @@ impl Serializable for OutAction { } impl Deserializable for OutAction { - fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { - if cell.remaining_bits() < std::mem::size_of::() * 8 { + fn construct_from(slice: &mut SliceData) -> Result { + if slice.remaining_bits() < std::mem::size_of::() * 8 { fail!(BlockError::InvalidArg("cell can't be shorter than 32 bits".to_string())) } - let tag = cell.get_next_u32()?; - match tag { + let tag = slice.get_next_u32()?; + let action = match tag { ACTION_SEND_MSG => { - let mode = cell.get_next_byte()?; - let msg = Message::construct_from_reference(cell)?; - *self = OutAction::new_send(mode, msg); + let mode = slice.get_next_byte()?; + match Message::construct_from_reference(slice) { + Ok(msg) => OutAction::new_send(mode, msg), + Err(err) => fail!(BlockError::OutActionError(err, mode)), + } } - ACTION_SET_CODE => *self = OutAction::new_set(cell.checked_drain_reference()?), + ACTION_SET_CODE => OutAction::new_set(slice.checked_drain_reference()?), ACTION_RESERVE => { - let mode = cell.get_next_byte()?; - let value = Deserializable::construct_from(cell)?; - *self = OutAction::new_reserve(mode, value); + let mode = slice.get_next_byte()?; + match Deserializable::construct_from(slice) { + Ok(value) => OutAction::new_reserve(mode, value), + Err(err) => fail!(BlockError::OutActionError(err, mode)), + } } ACTION_CHANGE_LIB => { - let mode = cell.get_next_byte()?; + let mode = slice.get_next_byte()?; let flags = (mode >> 1) & SET_LIB_CODE_ADD_PRIVATE_OR_PUBLIC_MASK; match (mode & CHANGE_SET_LIB_MASK, flags) { (CHANGE_LIB_MODE, 0) => { - let hash = UInt256::construct_from(cell)?; - *self = OutAction::new_change_library(mode, None, Some(hash)); + let hash = UInt256::construct_from(slice)?; + OutAction::new_change_library(mode, None, Some(hash)) } (SET_LIB_CODE_MODE, SET_LIB_CODE_REMOVE) | (SET_LIB_CODE_MODE, SET_LIB_CODE_ADD_PRIVATE) | (SET_LIB_CODE_MODE, SET_LIB_CODE_ADD_PUBLIC) => { - let code = cell.checked_drain_reference()?; - *self = OutAction::new_change_library(mode, Some(code), None); + let code = slice.checked_drain_reference()?; + OutAction::new_change_library(mode, Some(code), None) } _ => fail!("wrong mode for ChangeLibrary action: {mode}"), } } tag => fail!(BlockError::InvalidConstructorTag { t: tag, s: "OutAction".to_string() }), - } - Ok(()) + }; + Ok(action) } } diff --git a/src/block/src/storage_stat.rs b/src/block/src/storage_stat.rs index 2dc6e16..b96b152 100644 --- a/src/block/src/storage_stat.rs +++ b/src/block/src/storage_stat.rs @@ -17,6 +17,7 @@ use std::{collections::BTreeMap, ops::Not}; mod tests; const DICT_PROOF_TAG: u32 = 0x37c1e3fc; +const CONSENSUS_EXTRA_DATA_TAG: u32 = 0x638eb292; #[derive(Debug, Default, Clone, PartialEq)] pub struct StorageStatCellInfo { @@ -308,3 +309,39 @@ impl Deserializable for AccountStorageDictProof { Ok(()) } } + +/// consensus_extra_data#638eb292 flags:# gen_utime_ms:uint64 = ConsensusExtraData; +#[derive(Debug, Default, Clone)] +pub struct ConsensusExtraData { + pub flags: u32, + pub gen_utime_ms: u64, +} + +impl ConsensusExtraData { + pub const TAG: u32 = CONSENSUS_EXTRA_DATA_TAG; +} + +impl Serializable for ConsensusExtraData { + fn write_to(&self, cell: &mut BuilderData) -> Result<()> { + cell.append_u32(CONSENSUS_EXTRA_DATA_TAG)?; + cell.append_u32(self.flags)?; + cell.append_u64(self.gen_utime_ms)?; + Ok(()) + } +} + +impl Deserializable for ConsensusExtraData { + fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { + let tag = cell.get_next_u32()?; + if tag != CONSENSUS_EXTRA_DATA_TAG { + fail!( + "Invalid ConsensusExtraData tag: expected {:#x}, found {:#x}", + CONSENSUS_EXTRA_DATA_TAG, + tag + ); + } + self.flags = cell.get_next_u32()?; + self.gen_utime_ms = cell.get_next_u64()?; + Ok(()) + } +} diff --git a/src/block/src/tests/test_config_params.rs b/src/block/src/tests/test_config_params.rs index 2c9992f..c9eb765 100644 --- a/src/block/src/tests/test_config_params.rs +++ b/src/block/src/tests/test_config_params.rs @@ -937,12 +937,28 @@ fn test_simplex_config() { slots_per_leader_window: 4, first_block_timeout_ms: 1000, max_leader_window_desync: 100, + ..Default::default() }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); assert_eq!(config, config2); } +#[test] +fn test_simplex_config_with_quic() { + let config = SimplexConfig { + use_quic: true, + target_rate_ms: 300, + slots_per_leader_window: 4, + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, + }; + let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); + let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); + assert_eq!(config, config2); + assert!(config2.use_quic); +} + #[test] fn test_new_consensus_config_all_both() { let mc_config = SimplexConfig { @@ -950,12 +966,14 @@ fn test_new_consensus_config_all_both() { slots_per_leader_window: 4, first_block_timeout_ms: 1000, max_leader_window_desync: 100, + ..Default::default() }; let shard_config = SimplexConfig { target_rate_ms: 200, slots_per_leader_window: 8, first_block_timeout_ms: 500, max_leader_window_desync: 50, + ..Default::default() }; let config = NewConsensusConfigAll { mc: Some(mc_config), shard: Some(shard_config) }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); @@ -970,6 +988,7 @@ fn test_new_consensus_config_all_shard_only() { slots_per_leader_window: 8, first_block_timeout_ms: 500, max_leader_window_desync: 50, + ..Default::default() }; let config = NewConsensusConfigAll { mc: None, shard: Some(shard_config) }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); @@ -984,6 +1003,7 @@ fn test_new_consensus_config_all_mc_only() { slots_per_leader_window: 4, first_block_timeout_ms: 1000, max_leader_window_desync: 100, + ..Default::default() }; let config = NewConsensusConfigAll { mc: Some(mc_config), shard: None }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); diff --git a/src/block/src/tests/test_out_actions.rs b/src/block/src/tests/test_out_actions.rs index fe4fd7b..f236c21 100644 --- a/src/block/src/tests/test_out_actions.rs +++ b/src/block/src/tests/test_out_actions.rs @@ -129,39 +129,24 @@ fn test_unpack_out_action_slices_rejects_non_empty_tail() { #[test] fn test_deserialize_out_action_slices_valid_list() { let actions = get_out_actions(); - let slices = - unpack_out_action_slices(SliceData::load_cell(actions.serialize().unwrap()).unwrap()) - .unwrap(); - let parsed = deserialize_out_action_slices(slices).unwrap(); - assert_eq!(parsed.len(), actions.len()); - for (expected, actual) in actions.into_iter().zip(parsed.into_iter()) { + let slice = SliceData::load_cell(actions.serialize().unwrap()).unwrap(); + let slices = unpack_out_action_slices(slice).unwrap(); + assert_eq!(slices.len(), actions.len()); + for (expected, mut slice) in actions.into_iter().zip(slices.into_iter()) { + let actual = OutAction::construct_from(&mut slice).unwrap(); assert_eq!(expected, actual); } } #[test] -fn test_deserialize_out_action_slices_returns_indexed_error() { +fn test_deserialize_bad_out_action() { let valid_cell = OutAction::new_set(Cell::default()).serialize().unwrap(); - let valid_slice = SliceData::load_cell(valid_cell).unwrap(); + let mut valid_slice = SliceData::load_cell(valid_cell).unwrap(); + OutAction::construct_from(&mut valid_slice).unwrap(); // sanity check that the valid slice is indeed valid let mut invalid_builder = BuilderData::new(); 0xffff_ffffu32.write_to(&mut invalid_builder).unwrap(); - let invalid_slice = SliceData::load_cell(invalid_builder.into_cell().unwrap()).unwrap(); + let mut invalid_slice = SliceData::load_cell(invalid_builder.into_cell().unwrap()).unwrap(); - let err = deserialize_out_action_slices(vec![valid_slice, invalid_slice]).unwrap_err(); - assert_eq!(err.0, 1); + OutAction::construct_from(&mut invalid_slice).unwrap_err(); // sanity check that the invalid slice is indeed invalid } - -// TODO: move to anythere -// #[test] -// fn test_tvm_serialize_currency_collection() { -// let coins = 1u64<<63; -// let coins1 = int!(coins).as_coins().unwrap(); -// let coins1 = serialize_currency_collection(coins1, None).unwrap(); -// let coins1: CurrencyCollection = CurrencyCollection::construct_from(&mut coins1.into()).unwrap(); -// let coins2 = CurrencyCollection::with_coins(coins); -// assert_eq!(coins1, coins2); - -// assert_eq!(int!(1u128<<120).as_coins().expect_err("Expect range check error").code, -// ExceptionCode::RangeCheckError); -// } diff --git a/src/common/config/log_cfg_debug.yml b/src/common/config/log_cfg_debug.yml index f772902..7dc3bbd 100644 --- a/src/common/config/log_cfg_debug.yml +++ b/src/common/config/log_cfg_debug.yml @@ -45,6 +45,8 @@ loggers: level: debug rldp: level: info + quic: + level: warn ton_block: level: off diff --git a/src/executor/real_boc/bad_action_with_ignore_flag_account_new.boc b/src/executor/real_boc/bad_action_with_ignore_flag_account_new.boc new file mode 100644 index 0000000000000000000000000000000000000000..6a85f8bf93a6c7d731adb153a8dc4a4eac624740 GIT binary patch literal 756 zcmXw0ZAep57(VB2y1C^xw>2B6J9lRJL-CHX2qaheqs(BN2GV{Eim*hk!!P>h{LH?l zcZz`;lnDK*wDqMnxedzMmqUNtK(XtjFl14erE^y6%!&i=$2sSHpXYtv$6tr)AfbbR zSOr8aD_*SnQ5>}m8hkc=;mVV8Q|e9AyDI(3XU4m)z{;1sNAuc0iB7E!I04xB_4~U% zMKL(jsxdqKP~g0&)?sx&TNij>o&E3UsB_=%qvcw7|;z>`eC^K9j#uub^po ze{A2ukRK5ciIWI;JXrz>K}9SE5qIJRV(i`L5GOaFR@M1S4YZGJRn=dl8+@Z4hlu}s zH{pKdLL3Q$4ICLWQ?Zh0Zmfi=^f7imToP=7M>sN))Fz22YVVjBYT0GxNN4jx5pTi( zEre^Fp?GIVordwHDcH#5KtJVBR_|$-$`s?u+j#_}ccfxs3MJ6rd*3vC!Hm{=);i)W z&_Bpr$EhljcC#Seo3mxzpV1g-^iT63?crIR75ZHWW+f&_5U-*q1paDs8vN}379I|7 zk!@1WNMMtV5+lqT5(kr>1M>!i<2WY+vQ21yyQvz;?Q_T$Ov?g$`6J=ANJfQVyEO;5 z>#!WawlkVNDFnNWo+wy?Bh7)=;Ee&!>Sn?f$P-mAITwmPs731uOd=vCX-^I5E+a_p z5asM6r-9d<54l{_ly$5Ii^56;z87(&EV1n~lc&{wV52bN6|M}Iz>(D&b-QE|zv52- zyYT7I9DppqlCN=jQY~Vf{SXHk>X1z}wxYz%G}@h@AI8k^|9&uO=HGsHMUup~5}eIq zyr*)3zvkA8aAX(w>qaaANkcD2?+FDgqFJ3f0`OR%j=})D-T9=zd*;BlXjaXH5bUn7 jP>g8^wm`gSTrBYJ5N=($Ek>`v-UtCzuN^DNl?DF*!8A^4 literal 0 HcmV?d00001 diff --git a/src/executor/real_boc/bad_action_with_ignore_flag_account_old.boc b/src/executor/real_boc/bad_action_with_ignore_flag_account_old.boc new file mode 100644 index 0000000000000000000000000000000000000000..d8395689e29d2d734a2789b5103029a11bef1fad GIT binary patch literal 756 zcmXw0ZAep57(VB2y1C^xw>2B6J9lOZDc(^Qfz&F0lo@Q(K-!Oi5thhx_(lJmI?cYO zcZxw7lt}%mwDqMnyA8_q>ChiHQ0zJ>3|Yiw<($Pjv*N(}an5<)=Xsy^(YK*SNa!FS zRshk!iWh2s9EjPj8~iqX$@1e$Q~FiY`&#|6r^Y+4!1CApLxt|oqT?$)QUKO|2Wobv zDQ@Qr8Z#uIHUXj|n&>kjC>H^%3jpc!M51S^SXW+$UOMDYimlAA?o9vwlSOMy3Yv!Z z#&+!u2M_^~IE8>mQYDZORK!9EaYtVu#@>4hadHFdP@O*4O#8_WRnu9z*+1%ai1@#E z6P|}I#E~%6%#m?36)%ew#LK8^KV#=3WubO>kRzihosy`c;g*S^mRx3zbhgcx@+J(> zPPoPyig$+985m!hhK)=P^ivLH-43@@rWjY=EF>VkB^?t}D1rX&ht}aUX0+P3+Ld5| z{(kmPoT`#(FAFk#c^lTm@^<8Cpa09twZy>b=9?k0f%hfv?Q?SKM_8QWK;;bt$Daz zmt_yO-Lc$BA=G2^#=s&RX$!sqZw+up_c~IAyfNjXbH4PWTJ$rCNkqjY?X4$06$Huc zqnrceH1K(fAXk7|a}L*IQCO+K_am;HB{qF_>a;olY!pVkz*XQ9II>c&c1tGlEB*wq zi<}700>}a^`6^c^)gs0@2yu|54%<{?%Szl#qumMyVcZP=?+24+{_SU5G(~(P$=NK% zd#V)pOCGHVM|Od~Y{U|fH1uKgzHrbYn$fAF0FMP4DGadPQ$z~9cNT1lX4Omxq24MB i#h8X*JH(5|#RBgMOs#&--12N+H;`Z1<41u>;CWiS;m-D0j`zQeq=JRCi!tzW@FAkra9TD)Dn{9u4uOpRc`K%j{Hr&?e*X zg$+DLdL8=lk^8?d0IB2bSR*$3qR9gWkyL#P4n`(sM#cp!3{6EW2Asjw9FjTR!B;mGPo5w|dv1>&EH_Zuy){zf|_zVR@*^A8~cXyn|tKI-7UB zd$g8`pC^S);-hAZ11A^b(+-B&J^z;f=L$~#zRFo`PuZz7#ZAvF*F4rNX9@XnK&DpZ z1@mlg>*D|ae{}o4nB&2~z|eDC`EQ;K?(LZ) zH=Qr#ed{di18w~&zq0!d_6s&2H)jdI{`USkk?q%RGzhXXaq%5Kk-LbEoq+-9Ee?h> zCx#@2AIG+z|L=P7Q{|NFhZ_FQ=)bzYy>`EM&ZOTbuJ|))%sH|JXf-mJ>y()++yN6& zJ^*8pNh>f!LyTYo8oS7c8d3I_8yKl;wlvoH{@72_o)8I;|Fi0tp3GP IoYLtB0ICPZ5C8xG literal 0 HcmV?d00001 diff --git a/src/executor/src/tests/test_transaction_executor_with_real_data.rs b/src/executor/src/tests/test_transaction_executor_with_real_data.rs index 1ac4ca9..c1ecd1f 100644 --- a/src/executor/src/tests/test_transaction_executor_with_real_data.rs +++ b/src/executor/src/tests/test_transaction_executor_with_real_data.rs @@ -519,6 +519,16 @@ fn test_bad_action() { ) } +#[test] +fn test_bad_action_with_ignore_flag() { + replay_transaction_by_files( + "real_boc/bad_action_with_ignore_flag_account_old.boc", + "real_boc/bad_action_with_ignore_flag_account_new.boc", + "real_boc/bad_action_with_ignore_flag_transaction.boc", + "real_boc/config12.boc", + ) +} + #[test] fn test_size_limits_v12() { replay_transaction_by_files( diff --git a/src/executor/src/transaction_executor.rs b/src/executor/src/transaction_executor.rs index f8d5883..7c529d8 100644 --- a/src/executor/src/transaction_executor.rs +++ b/src/executor/src/transaction_executor.rs @@ -17,8 +17,8 @@ use std::{ sync::{Arc, LazyLock}, }; use ton_block::{ - deserialize_out_action_slices, error, fail, unpack_out_action_slices, AccStatusChange, Account, - AccountId, AccountStatus, AddSub, BouncedByPhase, Cell, ChildCell, Coins, ComputeSkipReason, + error, fail, unpack_out_action_slices, AccStatusChange, Account, AccountId, AccountStatus, + AddSub, BlockError, BouncedByPhase, Cell, ChildCell, Coins, ComputeSkipReason, CurrencyCollection, Deserializable, ExceptionCode, GasLimitsPrices, GetRepresentationHash, GlobalCapabilities, HashmapE, HashmapFilterResult, IBitstring, Mask, Message, MsgAddressInt, NewBounceBody, NewBounceComputePhaseInfo, NewBounceOriginalInfo, OutAction, Result, @@ -565,30 +565,43 @@ pub trait TransactionExecutor { } let limits = self.config().size_limits_config(); - let parsed_actions = match deserialize_out_action_slices(action_slices) { - Ok(actions) => actions, - Err((i, err)) => { - log::debug!( - target: "executor", - "invalid action {} found while preprocessing action list: {}", - i, - err - ); - phase.result_code = RESULT_CODE_UNKNOWN_OR_INVALID_ACTION; - if i != 0 { - phase.result_arg = Some(i as i32); + let mut parsed_actions = Vec::with_capacity(action_slices.len()); + for (i, mut slice) in action_slices.into_iter().enumerate() { + match OutAction::construct_from(&mut slice) { + Ok(action) => parsed_actions.push(Some(action)), + Err(err) => { + if let Some(BlockError::OutActionError(_, mode)) = err.downcast_ref() { + if mode.bit(SENDMSG_IGNORE_ERROR) { + phase.skipped_actions += 1; + parsed_actions.push(None); + continue; + } else if mode.bit(SENDMSG_BOUNCE_IF_FAIL) { + bounce = true; + } + }; + log::debug!( + target: "executor", + "invalid action {i} found while preprocessing action list: {err}" + ); + phase.result_code = RESULT_CODE_UNKNOWN_OR_INVALID_ACTION; + if i != 0 { + phase.result_arg = Some(i as i32); + } + return finish_action_phase_with_fine( + tr, + phase, + Some(msg_remaining_balance), + acc_balance, + bounce, + ); } - return finish_action_phase_with_fine( - tr, - phase, - Some(msg_remaining_balance), - acc_balance, - bounce, - ); } - }; + } for (i, action) in parsed_actions.into_iter().enumerate() { + let Some(action) = action else { + continue; + }; log::debug!(target: "executor", "\nAction #{}\nType: {}\nInitial balance: {}", i, action_type(&action), diff --git a/src/node/Cargo.toml b/src/node/Cargo.toml index d77fb2b..31e294c 100644 --- a/src/node/Cargo.toml +++ b/src/node/Cargo.toml @@ -3,7 +3,7 @@ build = '../common/build/build.rs' edition = '2021' license = 'GPL-3.0' name = 'node' -version = '0.4.0' +version = '0.3.0' [[bin]] name = 'adnl_resolve' @@ -123,9 +123,8 @@ cell_counter = ['ton_block/cell_counter'] ci_run = [] export_key = ['catchain/export_key', 'ton_block/export_key'] only_sorted_clean = [] -simplex = [] telemetry = ['adnl/telemetry', 'storage/telemetry'] trace_alloc = [] trace_alloc_detail = ['trace_alloc'] xp25 = ['ton_block/xp25', 'adnl/xp25'] -mirrornet = ['ton_block/mirrornet'] +mirrornet = ['ton_block/mirrornet'] \ No newline at end of file diff --git a/src/node/consensus-common/tests/test_adnl_overlay.rs b/src/node/consensus-common/tests/test_adnl_overlay.rs index 14ae25c..87de8dc 100644 --- a/src/node/consensus-common/tests/test_adnl_overlay.rs +++ b/src/node/consensus-common/tests/test_adnl_overlay.rs @@ -473,6 +473,8 @@ fn test_adnl_overlay_quic_delivery() -> Result<()> { true, // is_tcp_enabled true, // is_quic_enabled ); + // Quinn QUIC requires a Tokio runtime context on the calling thread + let _runtime_guard = test_network.get_runtime().enter(); let result = run_overlay_test(test_network.get_nodes().clone(), TRANSPORT_TYPE); test_network.shutdown(); diff --git a/src/node/simplex/CHANGELOG.md b/src/node/simplex/CHANGELOG.md index 3e93cfe..59b917d 100644 --- a/src/node/simplex/CHANGELOG.md +++ b/src/node/simplex/CHANGELOG.md @@ -2,20 +2,20 @@ All notable changes to the Simplex Consensus Protocol implementation will be documented in this file. -## [Unreleased] +## [0.5.0] - 2026-03-20 ### Added -- **GET-COMMITTED-1**: Download committed block via full-node proof for MC gap recovery. +- Download committed block via full-node proof for MC gap recovery. Replaces Rust-only `requestCandidate2` with C++-compatible mechanism. `SessionListener::get_committed_candidate` trait method, `CommittedBlockProof` type, `ValidatorGroup::on_get_committed_candidate` implementation using `download_block_proof()`. - `test_simplex_consensus_finalcert_recovery`: FinalCert-recovery gremlin test with per-node lossy overlay targeting (7 MC nodes, node 0 gets 40% broadcast + 30% message/query loss). - `lossy_overlay_node_indices` field in `LossyOverlayOpts` for per-node loss targeting. -- **NODE-20 (OBS-1)**: C++-parity standstill slot-grid dump (`standstill_slot_grid_dump()` +- C++-parity standstill slot-grid dump (`standstill_slot_grid_dump()` on `SimplexState`). Mirrors C++ `pool.cpp::alarm()` per-validator markers (F/I/N/S/.) and cert flags (notar/skip/final). Wired into `debug_dump()` on stall detection. -- **NODE-19 (HEALTH-1)**: Receiver-side anomaly checks with configurable thresholds. +- Receiver-side anomaly checks with configurable thresholds. Shared `ReceiverHealthCounters` (`Arc`) for cross-thread standstill trigger and candidate giveup counting. Delta-based anomaly detection in `run_health_checks()` for cert verify failures, standstill triggers, and candidate giveups with cooldown. @@ -44,7 +44,7 @@ All notable changes to the Simplex Consensus Protocol implementation will be doc - Max-base merge for `available_base`: align ordering/merge semantics with C++ `pool.cpp::add_available_base()` while preventing forward-progress regression from out-of-order notarizations and skip propagation. -- **TN-754**: Diagnostic dump no longer lists self (local validator) in the inactive nodes summary. `get_last_activity()` in receiver now reports self as always-active (consistent with `calculate_active_weight()`), and `debug_dump()` skips self index in the compact inactive list. +- Diagnostic dump no longer lists self (local validator) in the inactive nodes summary. `get_last_activity()` in receiver now reports self as always-active (consistent with `calculate_active_weight()`), and `debug_dump()` skips self index in the compact inactive list. - **Restart gremlin test enabled**: `test_simplex_consensus_restart_gremlin` now passes โ€” `first_non_finalized_slot` correctly advances on skip in all modes. - DB is now preserved on session stop (previously destroyed prematurely). - Overlay is registered before bootstrap load completes (prevents missed messages during startup). @@ -52,7 +52,7 @@ All notable changes to the Simplex Consensus Protocol implementation will be doc ### Removed - `requestCandidate2` / `candidateAndCert2` TL types and all v2 request paths. `ENABLE_REQUEST_CANDIDATE_V2` constant removed. `want_final` param removed from - `request_candidate()`. All FinalCert recovery now uses GET-COMMITTED-1. + `request_candidate()`. All FinalCert recovery now uses committed-block proof download. ### Planned - FinalCert proactive rebroadcast (C++ `cfd8850c` parity) @@ -64,12 +64,6 @@ All notable changes to the Simplex Consensus Protocol implementation will be doc --- -## [0.5.0] - 2026-02-01 - -**Baseline**: 0.4.0 release. - ---- - ## [0.4.0] - 2026-02-01 Major release focused on **C++ interoperability** (signatures/certificates/networking), **restart resilience**, and production-grade diagnostics/tests. @@ -433,7 +427,7 @@ Major release focusing on candidate resolution, certificate system, and operatio | Version | Date | Tag | Description | |---------|------|-----|-------------| -| 0.5.0 | 2026-02-28 | `simplex-0.5.0` | GET-COMMITTED-1, restart gremlin fix, requestCandidate2 removal | +| 0.5.0 | 2026-03-20 | `simplex-0.5.0` | Committed-block proof recovery, restart gremlin fix, requestCandidate2 removal, parity docs update | | 0.4.0 | 2026-02-01 | `simplex-0.4.0` | Block signature types, C++ compatibility, restart resilience | | 0.3.0 | 2026-01-14 | `simplex-0.3.0` | Candidate resolver, certificates, operational stability | | 0.2.0 | 2026-01-07 | `simplex-0.2.0` | consensus-common integration, dependency restructuring | @@ -441,10 +435,5 @@ Major release focusing on candidate resolution, certificate system, and operatio --- -[Unreleased]: https://github.com/RSquad/ton-node/compare/simplex-0.5.0...HEAD -[0.5.0]: https://github.com/RSquad/ton-node/compare/simplex-0.4.0...simplex-0.5.0 -[0.4.0]: https://github.com/RSquad/ton-node/compare/simplex-0.3.0...simplex-0.4.0 -[0.3.0]: https://github.com/RSquad/ton-node/compare/simplex-0.2.0...simplex-0.3.0 -[0.2.0]: https://github.com/RSquad/ton-node/compare/simplex-0.1.0...simplex-0.2.0 -[0.1.0]: https://github.com/RSquad/ton-node/releases/tag/simplex-0.1.0 + diff --git a/src/node/simplex/Cargo.toml b/src/node/simplex/Cargo.toml index 49a8bb1..3ca3440 100644 --- a/src/node/simplex/Cargo.toml +++ b/src/node/simplex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'simplex' -version = '0.4.0' +version = '0.5.0' edition = '2021' authors = ['RSquad'] description = 'Simplex consensus protocol implementation for TON blockchain' diff --git a/src/node/simplex/README.md b/src/node/simplex/README.md index bdc818a..da80006 100644 --- a/src/node/simplex/README.md +++ b/src/node/simplex/README.md @@ -1,6 +1,6 @@ # Simplex Consensus Protocol -**Version**: 0.5.0 (February 28, 2026) | [Changelog](CHANGELOG.md) +**Version**: 0.5.0 (March 20, 2026) | [Changelog](CHANGELOG.md) Rust implementation of the Simplex consensus protocol for TON blockchain. @@ -45,38 +45,32 @@ overlay / ADNL (lower level, network) This crate targets wire-compatibility with the upstream **C++ Simplex** implementation (`origin/testnet@e40d0e36`, Feb 28, 2026). -### Critical interop blockers - -- **SIG-1**: Block candidate signature input differs โ€” Rust signs `consensus.candidateParent(candidateId)`, C++ signs `consensus.candidateId` directly. **CRITICAL** - ### Protocol parity gaps (from C++ upstream) -- **FINALCERT-REBROADCAST**: C++ proactively rebroadcasts FinalCerts (`cfd8850c`) โ€” Rust standstill replay is less aggressive. **HIGH** -- **MC-FORK-PREVENTION**: C++ validator rejects MC candidates that would fork with real blocks (`9aac62b8`) โ€” Rust lacks symmetric check. **CRITICAL** -- **ADAPTIVE-SKIP-TIMEOUT**: C++ adaptively increases first block timeout after skip vote (`3c0cae03`) โ€” not implemented in Rust. - -### Network / transport differences - -- **TWOSTEP-1**: Candidate broadcasts: C++ uses twostep FEC (`send_twostep_broadcast_=true`), Rust uses single-step overlay broadcast (not implemented). -- **QUIC-1**: QUIC transport layer used by C++ for twostep sender; Rust simplex doesn't use QUIC (deferred). +- C++ proactively rebroadcasts FinalCerts (`cfd8850c`) โ€” Rust standstill replay is less aggressive. **HIGH** ### Implementation parity gaps -- **FLOW-1**: Committed-parent validation gate โ€” needs state-root caching / apply-block-to-state. -- **POOL-BASE-1**: Base selection should use "max available base" like C++ `SlotState::add_available_base` (audit needed). -- **ALGO-2**: C++ has `ImprovedStructureLZ4WithState` (BOC compression algo 2) โ€” Rust only supports algos 0 and 1. -- **STORE-HINT-1**: C++ has `StoreCellHint` for DB commit optimization during MerkleUpdate apply โ€” Rust lacks equivalent. -- **OVERLAY-BUFFER-1**: C++ overlay manager can buffer messages for unknown overlays (disabled by default) โ€” Rust lacks equivalent. +- Committed-parent validation gate โ€” needs state-root caching / apply-block-to-state. +- Base selection should use "max available base" like C++ `SlotState::add_available_base` (audit needed). +- C++ has `ImprovedStructureLZ4WithState` (BOC compression algo 2) โ€” Rust only supports algos 0 and 1. +- C++ has `StoreCellHint` for DB commit optimization during MerkleUpdate apply โ€” Rust lacks equivalent. +- C++ overlay manager can buffer messages for unknown overlays (disabled by default) โ€” Rust lacks equivalent. ### Resolved (for reference) -- **OVERLAY-1**: Overlay ID computation (node ordering, short ID) -- **INTEROP-1**: `candidateAndCert.notar` encoding (voteSignatureSet) -- **INTEROP-2**: Handle incoming `consensus.simplex.certificate` on vote channel -- **INTEROP-3**: `requestCandidate2` removed โ€” replaced by `get_committed_candidate` (GET-COMMITTED-1) -- **SPLIT-1**: Shard `before_split` empty block rule -- **U5.6**: Restart support (DB persistence + startup recovery) -- **CERT-1**: Certificate rebroadcast on restart +- Candidate signature now signs bare `consensus.candidateId` directly, matching C++ testnet. Regression test: `test_candidate_id_to_sign_is_bare_candidate_id`. +- MC stale-head rejection implemented in `validator_group.rs` (`should_reject_stale_mc_candidate`), matching C++ `block-validator.cpp` commit `9aac62b8`. +- Adaptive first-block timeout backoff after skip implemented in `simplex_state.rs` (`apply_adaptive_timeout_backoff`), matching C++ `consensus.cpp`. +- Twostep FEC broadcast implemented in `consensus-common/adnl_overlay.rs` (`BroadcastTwostepSimple`), with C++-compatible signing. +- QUIC transport supported via `SessionOptions::use_quic` and `OverlayTransportType::SimplexQuic`. Tested in `test_adnl_overlay_quic_delivery`. +- Overlay ID computation (node ordering, short ID) +- `candidateAndCert.notar` encoding (voteSignatureSet) +- Handle incoming `consensus.simplex.certificate` on vote channel +- `requestCandidate2` removed โ€” replaced by `get_committed_candidate` +- Shard `before_split` empty block rule +- Restart support (DB persistence + startup recovery) +- Certificate rebroadcast on restart ## Architecture @@ -356,7 +350,7 @@ Single-threaded consensus algorithm (crate-private): - โœ… Standstill coordination - calls `receiver.reschedule_standstill()` on finalization, `set_standstill_slots()` on finalization/skip - โœ… DB persistence - finalized blocks, candidate infos, notar certs, votes, pool state persisted to RocksDB - โœ… Startup recovery - bootstrap load, vote replay, receiver cache restore, recommit to ValidatorGroup -- โœ… GET-COMMITTED-1 - download committed block via full-node proof for MC gap recovery (replaces requestCandidate2) +- โœ… Download committed block via full-node proof for MC gap recovery (replaces requestCandidate2) - โš ๏ธ Precollation parent tracking - needs fix for cross-window scenarios **Key methods:** @@ -662,7 +656,7 @@ Multi-instance consensus tests with in-process overlay. |------|-------------|--------| | `test_simplex_consensus_basic` | Basic consensus with 7 nodes, 100 rounds | โœ… | | `test_simplex_consensus_with_failures` | Consensus with simulated failures | โœ… | -| `test_simplex_consensus_finalcert_recovery` | FinalCert recovery via `get_committed_candidate` (GET-COMMITTED-1) | โœ… | +| `test_simplex_consensus_finalcert_recovery` | FinalCert recovery via `get_committed_candidate` | โœ… | | `test_simplex_consensus_shard_with_mc_notifications` | MC finalization forwarding to shards | โœ… | | `test_simplex_consensus_adnl_overlay` | ADNL overlay-based consensus | โœ… | | `test_simplex_consensus_adnl_net_gremlin` | ADNL net gremlin (packet loss/delay simulation) | โœ… | diff --git a/src/node/simplex/src/block.rs b/src/node/simplex/src/block.rs index 2861d4f..08fa520 100644 --- a/src/node/simplex/src/block.rs +++ b/src/node/simplex/src/block.rs @@ -869,13 +869,22 @@ impl RawCandidate { leader_idx: ValidatorIndex, shard: &ShardIdent, max_size: usize, + proto_version: u32, ) -> Result { // Parse TL object let data_vec = data.to_vec(); let candidate_tl = consensus_common::utils::deserialize_tl_boxed_object::(&data_vec)?; - Self::from_tl(&candidate_tl, session_id, leader_key, leader_idx, shard, max_size) + Self::from_tl( + &candidate_tl, + session_id, + leader_key, + leader_idx, + shard, + max_size, + proto_version, + ) } /// Create from already-parsed TL object @@ -898,12 +907,14 @@ impl RawCandidate { leader_idx: ValidatorIndex, shard: &ShardIdent, max_size: usize, + proto_version: u32, ) -> Result { // Extract parent let parent_id = Self::extract_parent(candidate_tl)?; // Extract block data - returns CandidateBlockData - let block_data = Self::extract_block_data(candidate_tl, leader_key, shard, max_size)?; + let block_data = + Self::extract_block_data(candidate_tl, leader_key, shard, max_size, proto_version)?; // Validate invariant: empty blocks must have parent if block_data.is_empty() && parent_id.is_none() { @@ -985,6 +996,7 @@ impl RawCandidate { leader_key: &PublicKey, shard: &ShardIdent, max_size: usize, + proto_version: u32, ) -> Result { match candidate_tl { CandidateData::Consensus_Block(block) => { @@ -999,6 +1011,7 @@ impl RawCandidate { candidate_bytes, shard, max_size, + proto_version, )?; match block_info { diff --git a/src/node/simplex/src/database.rs b/src/node/simplex/src/database.rs index b38802f..7e51dff 100644 --- a/src/node/simplex/src/database.rs +++ b/src/node/simplex/src/database.rs @@ -41,7 +41,14 @@ use consensus_common::{ AsyncKeyValueStorageOptions, AsyncKeyValueStoragePtr, ConsensusCommonFactory, RawBuffer, StorageAsyncResultPtr, }; -use std::{path::Path, sync::Arc, time::Duration}; +use std::{ + path::Path, + sync::{ + atomic::{AtomicI64, Ordering}, + Arc, + }, + time::Duration, +}; use ton_api::{ deserialize_typed, serialize_boxed, ton::{ @@ -57,6 +64,7 @@ use ton_api::{ }, finalizedblock::FinalizedBlock as FinalizedBlockValue, key::{ + candidate::Candidate as CandidatePayloadKey, candidate_resolver::{ candidateinfo::CandidateInfo as CandidateInfoKey, notarcert::NotarCert as NotarCertKey, @@ -64,6 +72,7 @@ use ton_api::{ }, finalizedblock::FinalizedBlock as FinalizedBlockKey, vote::Vote as VoteKey, + Candidate as CandidatePayloadKeyBoxed, FinalizedBlock as FinalizedBlockKeyBoxed, PoolState as PoolStateKey, Vote as VoteKeyBoxed, }, @@ -88,7 +97,7 @@ use ton_block::{error, BlockIdExt, Result, UInt256}; // ============================================================================ /// Log target for database operations (matches simplex crate log target) -const LOG_TARGET: &str = "simplex"; +const TARGET: &str = "simplex"; /// Default sync timeout for blocking reads const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(5); @@ -123,6 +132,13 @@ fn prefix_pool_state() -> u32 { PoolStateKey::default().bare_object().constructor() } +/// Get key prefix for candidate payloads (full serialized CandidateData bytes). +/// +/// C++ parity: `consensus.simplex.db.key.candidate` TL type in candidate-resolver.cpp. +fn prefix_candidate_payload() -> u32 { + CandidatePayloadKey::constructor_const() +} + // ============================================================================ // Record Types // ============================================================================ @@ -166,7 +182,10 @@ pub struct NotarCertRecord { /// /// Stores votes by their hash for standstill recovery. /// Key: vote_hash (sha256 of serialized vote) -/// Value: raw vote data + node index +/// Value: raw vote data + node index + seqno +/// +/// C++ parity: db.cpp assigns monotonic seqno to each vote for replay ordering. +/// Votes must be replayed in the order they were originally cast. #[derive(Debug, Clone)] pub struct VoteRecord { /// Hash of the vote (key) @@ -175,6 +194,8 @@ pub struct VoteRecord { pub data: RawBuffer, /// Validator index that submitted this vote pub node_idx: ValidatorIndex, + /// Monotonic sequence number for replay ordering (C++ parity) + pub seqno: i64, } /// Pool state record for restart support @@ -207,6 +228,8 @@ pub struct Bootstrap { pub votes: Vec, /// Pool state (for skip vote generation) pub pool_state: Option, + /// Candidate payload bytes (serialized CandidateData, for requestCandidate serving) + pub candidate_payloads: Vec<(RawCandidateId, Vec)>, } /// Bootstrap data for recovery processor (session state only, no candidate_infos). @@ -234,7 +257,8 @@ impl Bootstrap { /// Split bootstrap into component-specific parts. /// /// Consumes self for zero-copy transfer of vectors. - pub fn split(self) -> (SessionBootstrap, ReceiverBootstrap) { + /// Returns (session_boot, receiver_boot, candidate_payloads). + pub fn split(self) -> (SessionBootstrap, ReceiverBootstrap, Vec<(RawCandidateId, Vec)>) { ( SessionBootstrap { finalized_blocks: self.finalized_blocks, @@ -242,6 +266,7 @@ impl Bootstrap { pool_state: self.pool_state, }, ReceiverBootstrap { notar_certs: self.notar_certs }, + self.candidate_payloads, ) } @@ -252,6 +277,7 @@ impl Bootstrap { && self.notar_certs.is_empty() && self.votes.is_empty() && self.pool_state.is_none() + && self.candidate_payloads.is_empty() } } @@ -338,6 +364,16 @@ fn deserialize_candidate_info(key_bytes: &[u8], value_bytes: &[u8]) -> Result Result> { + let key = CandidatePayloadKey { candidateId: raw_candidate_id_to_tl(candidate_id) }; + serialize_boxed(&key.into_boxed()).map_err(|e| error!("serialization failed: {}", e)) +} + +fn deserialize_candidate_payload_key(key_bytes: &[u8]) -> Result { + let key: CandidatePayloadKey = deserialize_typed::(key_bytes)?.only(); + Ok(raw_candidate_id_from_tl(key.candidateId)) +} + fn serialize_notar_cert_key(candidate_id: &RawCandidateId) -> Result> { let key = NotarCertKey { candidateId: raw_candidate_id_to_tl(candidate_id) }; serialize_boxed(&key.into_boxed()).map_err(|e| error!("serialization failed: {}", e)) @@ -367,8 +403,11 @@ fn serialize_vote_key(vote_hash: &UInt256) -> Result> { } fn serialize_vote_value(record: &VoteRecord) -> Result> { - let value = - VoteValue { data: record.data.clone(), node_idx: record.node_idx.value() as i32, seqno: 0 }; + let value = VoteValue { + data: record.data.clone(), + node_idx: record.node_idx.value() as i32, + seqno: record.seqno, + }; serialize_boxed(&value.into_boxed()).map_err(|e| error!("serialization failed: {}", e)) } @@ -379,6 +418,7 @@ fn deserialize_vote(key_bytes: &[u8], value_bytes: &[u8]) -> Result vote_hash: key.vote_hash.clone(), data: value.data, node_idx: ValidatorIndex::new(value.node_idx as u32), + seqno: value.seqno, }) } @@ -416,7 +456,7 @@ fn filter_finalized_chain(mut records: Vec) -> Vec) -> Vec { if record.parent.as_ref() != Some(expected) { log::warn!( - target: LOG_TARGET, + target: TARGET, "SimplexDb: skipping finalized block slot={} (parent mismatch)", record.candidate_id.slot.value() ); @@ -459,6 +499,8 @@ pub struct SimplexDb { storage: AsyncKeyValueStoragePtr, /// Storage ID (for logging) storage_id: String, + /// Monotonic vote seqno counter (C++ parity: db.cpp next_seqno_) + next_vote_seqno: AtomicI64, } impl SimplexDb { @@ -477,12 +519,23 @@ impl SimplexDb { let storage_id = storage_id.to_string(); log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: opening at {}", storage_id, db_path.display() ); + if let Some(parent) = db_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + error!( + "SimplexDb {}: failed to create parent dir {}: {}", + storage_id, + parent.display(), + e + ) + })?; + } + // SimplexDb does not use callbacks let options = AsyncKeyValueStorageOptions { use_callback_thread: false }; @@ -490,13 +543,13 @@ impl SimplexDb { ConsensusCommonFactory::create_async_key_value_storage(db_path, &storage_id, options)?; log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: opened at {}", storage_id, db_path.display() ); - Ok(Arc::new(Self { storage, storage_id })) + Ok(Arc::new(Self { storage, storage_id, next_vote_seqno: AtomicI64::new(0) })) } // ========================================================================= @@ -520,7 +573,7 @@ impl SimplexDb { /// Called when a block is finalized or notarized with a certificate. pub fn save_finalized_block(&self, record: &FinalizedBlockRecord) -> Result<()> { log::trace!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: save_finalized_block slot={} is_final={}", self.storage_id, record.candidate_id.slot.value(), @@ -547,7 +600,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn save_candidate_info(&self, record: &CandidateInfoRecord) -> Result<()> { log::trace!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: save_candidate_info slot={} leader={}", self.storage_id, record.candidate_id.slot.value(), @@ -575,7 +628,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn save_notar_cert(&self, candidate_id: &RawCandidateId, cert: &NotarCert) -> Result<()> { log::trace!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: save_notar_cert slot={} signatures={}", self.storage_id, candidate_id.slot.value(), @@ -587,9 +640,14 @@ impl SimplexDb { } /// Save vote record (async result). + /// + /// Assigns a monotonic seqno for replay ordering (C++ parity: db.cpp next_seqno_++). pub fn save_vote_async(&self, record: &VoteRecord) -> Result> { - let key = serialize_vote_key(&record.vote_hash)?; - let value = serialize_vote_value(record)?; + let seqno = self.next_vote_seqno.fetch_add(1, Ordering::Relaxed); + let mut record_with_seqno = record.clone(); + record_with_seqno.seqno = seqno; + let key = serialize_vote_key(&record_with_seqno.vote_hash)?; + let value = serialize_vote_value(&record_with_seqno)?; Ok(self.storage.set(key, value, None)) } @@ -599,7 +657,7 @@ impl SimplexDb { #[allow(dead_code)] // Convenience wrapper; prefer `_async()` in production code. pub fn save_vote(&self, record: &VoteRecord) -> Result<()> { log::trace!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: save_vote hash={} node_idx={}", self.storage_id, hex::encode(&record.vote_hash.as_slice()[..8]), @@ -626,7 +684,7 @@ impl SimplexDb { #[allow(dead_code)] // Convenience wrapper; prefer `_async()` in production code. pub fn save_pool_state(&self, record: &PoolStateRecord) -> Result<()> { log::trace!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: save_pool_state first_nonannounced_window={}", self.storage_id, record.first_nonannounced_window @@ -636,6 +694,100 @@ impl SimplexDb { Ok(()) } + // ========================================================================= + // Candidate Payload Storage (C++ CandidateResolver::store_candidate parity) + // ========================================================================= + + /// Save serialized CandidateData bytes (async, fire-and-forget). + /// + /// C++ parity: `candidate-resolver.cpp store_candidate()` persists the full + /// serialized candidate so `requestCandidate(want_candidate=true)` queries + /// can be served from DB after restart. + pub fn save_candidate_payload_async( + &self, + candidate_id: &RawCandidateId, + candidate_data_bytes: &[u8], + ) -> Result> { + let key = serialize_candidate_payload_key(candidate_id)?; + Ok(self.storage.set(key, candidate_data_bytes.to_vec(), None)) + } + + /// Load a single candidate payload by id (blocking, for query fallback). + pub fn load_candidate_payload_by_id( + &self, + candidate_id: &RawCandidateId, + timeout: Duration, + ) -> Result>> { + let key = serialize_candidate_payload_key(candidate_id)?; + let result = self.storage.get(key, None); + match result.wait_timeout(timeout) { + Some(Ok(Some(value))) => Ok(Some(value)), + Some(Ok(None)) => Ok(None), + Some(Err(e)) => Err(e), + None => Err(error!("SimplexDb: timeout loading candidate payload by id")), + } + } + + /// Load all candidate payloads asynchronously (for startup restore). + pub fn load_candidate_payloads_async(&self) -> StorageAsyncResultPtr, Vec)>> { + log::debug!( + target: TARGET, + "SimplexDb {}: load_candidate_payloads_async", + self.storage_id + ); + self.storage.get_by_prefix_u32(prefix_candidate_payload(), None) + } + + // ========================================================================= + // Single-Record Lookups (async, for live query fallback) + // ========================================================================= + + /// Look up a single candidate info record by candidate ID (blocking). + /// + /// Unlike `load_candidate_infos()` which scans all records, this looks up + /// a single record by its exact key. Used by the RequestCandidate fallback + /// when resolver_cache misses. + /// + /// Reference: C++ candidate-resolver.cpp `try_load_candidate_data_from_db()` + pub fn load_candidate_info_by_id( + &self, + candidate_id: &RawCandidateId, + timeout: Duration, + ) -> Result> { + let key = serialize_candidate_info_key(candidate_id)?; + let result = self.storage.get(key.clone(), None); + match result.wait_timeout(timeout) { + Some(Ok(Some(value))) => { + let record = deserialize_candidate_info(&key, &value)?; + Ok(Some(record)) + } + Some(Ok(None)) => Ok(None), + Some(Err(e)) => Err(e), + None => Err(error!("SimplexDb: timeout loading candidate info by id")), + } + } + + /// Look up a single notar cert record by candidate ID (blocking). + /// + /// Used by the RequestCandidate fallback for notar_cert recovery. + pub fn load_notar_cert_by_id( + &self, + candidate_id: &RawCandidateId, + timeout: Duration, + ) -> Result> { + let key = serialize_notar_cert_key(candidate_id)?; + let result = self.storage.get(key.clone(), None); + match result.wait_timeout(timeout) { + Some(Ok(Some(value))) => { + let record = deserialize_notar_cert(&key, &value)?; + Ok(Some(record)) + } + Some(Ok(None)) => Ok(None), + Some(Err(e)) => Err(e), + None => Err(error!("SimplexDb: timeout loading notar cert by id")), + } + } + // ========================================================================= // Async Read Operations (for cancellable bootstrap) // ========================================================================= @@ -646,7 +798,7 @@ impl SimplexDb { /// with cancellation support via `wait_cancellable()`. pub fn load_finalized_blocks_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_finalized_blocks_async", self.storage_id ); @@ -656,7 +808,7 @@ impl SimplexDb { /// Load all candidate infos asynchronously. pub fn load_candidate_infos_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_candidate_infos_async", self.storage_id ); @@ -666,7 +818,7 @@ impl SimplexDb { /// Load all notar certs asynchronously. pub fn load_notar_certs_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_notar_certs_async", self.storage_id ); @@ -676,7 +828,7 @@ impl SimplexDb { /// Load all votes asynchronously. pub fn load_votes_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_votes_async", self.storage_id ); @@ -688,7 +840,7 @@ impl SimplexDb { /// Returns raw key-value pairs; caller deserializes. pub fn load_pool_state_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_pool_state_async", self.storage_id ); @@ -705,7 +857,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn load_finalized_blocks(&self) -> Result> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_finalized_blocks", self.storage_id ); @@ -722,7 +874,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: failed to deserialize finalized block: {}", self.storage_id, e @@ -740,7 +892,7 @@ impl SimplexDb { let kept = records.len(); log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: loaded {} finalized blocks (kept {} after chain filter)", self.storage_id, total, @@ -756,7 +908,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn load_candidate_infos(&self) -> Result> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_candidate_infos", self.storage_id ); @@ -773,7 +925,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: failed to deserialize candidate info: {}", self.storage_id, e @@ -783,7 +935,7 @@ impl SimplexDb { } log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: loaded {} candidate infos", self.storage_id, records.len() @@ -798,7 +950,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn load_notar_certs(&self) -> Result> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_notar_certs", self.storage_id ); @@ -815,7 +967,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: failed to deserialize notar cert: {}", self.storage_id, e @@ -825,7 +977,7 @@ impl SimplexDb { } log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: loaded {} notar certs", self.storage_id, records.len() @@ -840,7 +992,7 @@ impl SimplexDb { #[allow(dead_code)] // Not used yet; kept for restart/debug parity. pub fn load_votes(&self) -> Result> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_votes", self.storage_id ); @@ -857,7 +1009,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: failed to deserialize vote: {}", self.storage_id, e @@ -866,11 +1018,20 @@ impl SimplexDb { } } + // C++ parity: sort by seqno for deterministic replay order (db.cpp init_votes) + records.sort_by_key(|r| r.seqno); + + // Initialize next_vote_seqno from max seqno + 1 (C++ parity: db.cpp next_seqno_) + if let Some(max_seqno) = records.last().map(|r| r.seqno) { + self.next_vote_seqno.store(max_seqno + 1, Ordering::Relaxed); + } + log::info!( - target: LOG_TARGET, - "SimplexDb {}: loaded {} votes", + target: TARGET, + "SimplexDb {}: loaded {} votes (next_seqno={})", self.storage_id, - records.len() + records.len(), + self.next_vote_seqno.load(Ordering::Relaxed) ); Ok(records) @@ -882,7 +1043,7 @@ impl SimplexDb { #[allow(dead_code)] // Not used yet; kept for restart/debug parity. pub fn load_pool_state(&self) -> Result> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_pool_state", self.storage_id ); @@ -895,7 +1056,7 @@ impl SimplexDb { if result.is_empty() { log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: no pool state found (first run)", self.storage_id ); @@ -905,7 +1066,7 @@ impl SimplexDb { // Should be exactly one record (singleton) if result.len() > 1 { log::warn!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: multiple pool state records found ({}), using first", self.storage_id, result.len() @@ -916,7 +1077,7 @@ impl SimplexDb { let record = deserialize_pool_state(value_bytes)?; log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: loaded pool state first_nonannounced_window={}", self.storage_id, record.first_nonannounced_window @@ -938,7 +1099,7 @@ impl SimplexDb { #[allow(dead_code)] // Prefer `load_bootstrap_cancellable()` in session startup. pub fn load_bootstrap(&self) -> Result { log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_bootstrap", self.storage_id ); @@ -949,17 +1110,40 @@ impl SimplexDb { let votes = self.load_votes()?; let pool_state = self.load_pool_state()?; - let bootstrap = - Bootstrap { finalized_blocks, candidate_infos, notar_certs, votes, pool_state }; + // Load candidate payloads (optional, graceful if absent) + let payloads_raw = self + .load_candidate_payloads_async() + .wait_timeout(DEFAULT_SYNC_TIMEOUT) + .ok_or_else(|| error!("SimplexDb: timeout loading candidate payloads"))??; + let mut candidate_payloads = Vec::with_capacity(payloads_raw.len()); + for (k, v) in payloads_raw { + match deserialize_candidate_payload_key(&k) { + Ok(id) => candidate_payloads.push((id, v)), + Err(e) => { + log::error!(target: TARGET, "SimplexDb: skip bad candidate payload key: {e}") + } + } + } + + let bootstrap = Bootstrap { + finalized_blocks, + candidate_infos, + notar_certs, + votes, + pool_state, + candidate_payloads, + }; log::info!( - target: LOG_TARGET, - "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, pool_state={}", + target: TARGET, + "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, \ + {} payloads, pool_state={}", self.storage_id, bootstrap.finalized_blocks.len(), bootstrap.candidate_infos.len(), bootstrap.notar_certs.len(), bootstrap.votes.len(), + bootstrap.candidate_payloads.len(), bootstrap.pool_state.is_some() ); @@ -979,7 +1163,7 @@ impl SimplexDb { step: Duration, ) -> Result { log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: load_bootstrap_cancellable", self.storage_id ); @@ -990,6 +1174,7 @@ impl SimplexDb { let certs_async = self.load_notar_certs_async(); let votes_async = self.load_votes_async(); let pool_state_async = self.load_pool_state_async(); + let payloads_async = self.load_candidate_payloads_async(); // Wait with cancellation support let finalized_raw = finalized_async.wait_cancellable(cancel, step)?; @@ -997,6 +1182,7 @@ impl SimplexDb { let certs_raw = certs_async.wait_cancellable(cancel, step)?; let votes_raw = votes_async.wait_cancellable(cancel, step)?; let pool_state_raw = pool_state_async.wait_cancellable(cancel, step)?; + let payloads_raw = payloads_async.wait_cancellable(cancel, step)?; // Deserialize results let mut finalized_blocks = Vec::with_capacity(finalized_raw.len()); @@ -1004,7 +1190,7 @@ impl SimplexDb { match deserialize_finalized_block(&k, &v) { Ok(r) => finalized_blocks.push(r), Err(e) => { - log::error!(target: LOG_TARGET, "SimplexDb: skip bad finalized block: {}", e) + log::error!(target: TARGET, "SimplexDb: skip bad finalized block: {e}") } } } @@ -1013,7 +1199,7 @@ impl SimplexDb { let finalized_blocks = filter_finalized_chain(finalized_blocks); if finalized_blocks.len() != total_finalized_blocks { log::warn!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: finalized blocks chain filter dropped {} records", self.storage_id, total_finalized_blocks - finalized_blocks.len() @@ -1025,7 +1211,7 @@ impl SimplexDb { match deserialize_candidate_info(&k, &v) { Ok(r) => candidate_infos.push(r), Err(e) => { - log::error!(target: LOG_TARGET, "SimplexDb: skip bad candidate info: {}", e) + log::error!(target: TARGET, "SimplexDb: skip bad candidate info: {e}") } } } @@ -1034,7 +1220,7 @@ impl SimplexDb { for (k, v) in certs_raw { match deserialize_notar_cert(&k, &v) { Ok(r) => notar_certs.push(r), - Err(e) => log::error!(target: LOG_TARGET, "SimplexDb: skip bad notar cert: {}", e), + Err(e) => log::error!(target: TARGET, "SimplexDb: skip bad notar cert: {e}"), } } @@ -1042,10 +1228,18 @@ impl SimplexDb { for (k, v) in votes_raw { match deserialize_vote(&k, &v) { Ok(r) => votes.push(r), - Err(e) => log::error!(target: LOG_TARGET, "SimplexDb: skip bad vote: {}", e), + Err(e) => log::error!(target: TARGET, "SimplexDb: skip bad vote: {e}"), } } + // C++ parity: sort by seqno for deterministic replay order (db.cpp init_votes) + votes.sort_by_key(|r| r.seqno); + + // Initialize next_vote_seqno from max seqno + 1 (C++ parity: db.cpp next_seqno_) + if let Some(max_seqno) = votes.last().map(|r| r.seqno) { + self.next_vote_seqno.store(max_seqno + 1, Ordering::Relaxed); + } + let pool_state = if pool_state_raw.is_empty() { None } else { @@ -1053,23 +1247,41 @@ impl SimplexDb { match deserialize_pool_state(v) { Ok(r) => Some(r), Err(e) => { - log::error!(target: LOG_TARGET, "SimplexDb: skip bad pool state: {}", e); + log::error!(target: TARGET, "SimplexDb: skip bad pool state: {e}"); None } } }; - let bootstrap = - Bootstrap { finalized_blocks, candidate_infos, notar_certs, votes, pool_state }; + let mut candidate_payloads = Vec::with_capacity(payloads_raw.len()); + for (k, v) in payloads_raw { + match deserialize_candidate_payload_key(&k) { + Ok(id) => candidate_payloads.push((id, v)), + Err(e) => { + log::error!(target: TARGET, "SimplexDb: skip bad candidate payload key: {e}") + } + } + } + + let bootstrap = Bootstrap { + finalized_blocks, + candidate_infos, + notar_certs, + votes, + pool_state, + candidate_payloads, + }; log::info!( - target: LOG_TARGET, - "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, pool_state={}", + target: TARGET, + "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, \ + {} payloads, pool_state={}", self.storage_id, bootstrap.finalized_blocks.len(), bootstrap.candidate_infos.len(), bootstrap.notar_certs.len(), bootstrap.votes.len(), + bootstrap.candidate_payloads.len(), bootstrap.pool_state.is_some() ); @@ -1083,7 +1295,7 @@ impl SimplexDb { /// Wait for all pending writes to complete. pub fn sync(&self, timeout: Option) -> Result<()> { log::debug!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: sync", self.storage_id ); @@ -1094,7 +1306,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn mark_for_destroy(&self) { log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: marked for destroy", self.storage_id ); @@ -1105,7 +1317,7 @@ impl SimplexDb { impl Drop for SimplexDb { fn drop(&mut self) { log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: dropping, syncing pending writes...", self.storage_id ); @@ -1113,14 +1325,14 @@ impl Drop for SimplexDb { // Force sync to flush all pending writes before closing if let Err(e) = self.sync(Some(DEFAULT_SYNC_TIMEOUT)) { log::error!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: sync on drop failed: {}", self.storage_id, e ); } else { log::info!( - target: LOG_TARGET, + target: TARGET, "SimplexDb {}: sync complete", self.storage_id ); diff --git a/src/node/simplex/src/lib.rs b/src/node/simplex/src/lib.rs index afc42f8..995fb29 100644 --- a/src/node/simplex/src/lib.rs +++ b/src/node/simplex/src/lib.rs @@ -471,6 +471,11 @@ pub struct SessionOptions { /// Default: `FullReplay` pub restart_recommit_strategy: RestartRecommitStrategy, + /// Use QUIC overlay transport instead of ADNL UDP for this session. + /// When true, overlay messages/queries are sent via QUIC streams. + /// Default: false + pub use_quic: bool, + /// Cooldown between repeated health alerts of the same anomaly type. /// Default: 30 seconds pub health_alert_cooldown: Duration, @@ -507,6 +512,7 @@ impl Default for SessionOptions { empty_block_mc_lag_threshold: None, wait_for_db_init: false, restart_recommit_strategy: RestartRecommitStrategy::default(), + use_quic: false, health_alert_cooldown: Duration::from_secs(30), health_stall_warning_secs: 15, health_stall_error_secs: 60, diff --git a/src/node/simplex/src/receiver.rs b/src/node/simplex/src/receiver.rs index fff02dc..94e14fb 100644 --- a/src/node/simplex/src/receiver.rs +++ b/src/node/simplex/src/receiver.rs @@ -67,7 +67,7 @@ use consensus_common::{ ConsensusCommonFactory, ConsensusNode, ConsensusOverlayPtr, QueryResponseCallback, }; use crossbeam::channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender}; -use rand::seq::SliceRandom; +use rand::{seq::SliceRandom, Rng}; use std::{ collections::HashMap, mem::discriminant, @@ -118,11 +118,17 @@ const RECEIVER_WARN_PROCESSING_LATENCY: Duration = Duration::from_millis(1000); const RECEIVER_LATENCY_WARN_DUMP_PERIOD: Duration = Duration::from_millis(2000); // Latency warning dump period const RECEIVER_PROCESSING_PERIOD_MS: u64 = 100; // Processing period (timeout for queue pull) const SHUFFLE_SEND_ORDER_PERIOD: Duration = Duration::from_secs(10); // Period to shuffle send order -const ACTIVE_WEIGHT_RECOMPUTE_PERIOD: Duration = Duration::from_secs(10); // Period to recompute active weight +const ACTIVE_WEIGHT_RECOMPUTE_PERIOD: Duration = Duration::from_secs(1); // Period to recompute active weight // Candidate request constants (block repair / candidate resolver) -const CANDIDATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); // Per-request timeout -const CANDIDATE_REQUEST_MAX_RETRIES: u32 = 5; // Maximum retry attempts before giving up +// Per-request network query timeout (overlay send_query deadline) +const CANDIDATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); +// C++ parity: candidate-resolver.cpp uses indefinite retry with exponential backoff. +// bus.h defaults: initial=0.5s, multiplier=1.5, max=30.0s +const CANDIDATE_REQUEST_INITIAL_TIMEOUT: Duration = Duration::from_millis(500); +const CANDIDATE_REQUEST_TIMEOUT_MULTIPLIER: f64 = 1.5; +const CANDIDATE_REQUEST_MAX_TIMEOUT: Duration = Duration::from_secs(30); +const CANDIDATE_REQUEST_MAX_RETRIES: u32 = 50; // Standstill initial range - used before first finalization calls set_standstill_slots() // After first finalization, SessionProcessor sets the actual range via set_standstill_slots() @@ -373,6 +379,24 @@ pub(crate) trait ReceiverListener: Send + Sync { /// - active_weight: sum of weights for validators with recent activity /// - last_activity: last receive time per validator (None if never received) fn on_activity(&self, active_weight: ValidatorWeight, last_activity: Vec>); + + /// Fallback for RequestCandidate queries when resolver_cache misses. + /// + /// Called by `handle_query()` when `want_candidate=true` but the resolver_cache + /// does not have the candidate data. Delegates to SessionProcessor which can + /// reconstruct the response from its in-memory `candidate_data_cache`, rebuild + /// an empty candidate from `CandidateInfo`, or load persisted payloads from SimplexDB. + /// + /// This achieves parity with C++ `CandidateResolver::try_load_candidate_data_from_db()`. + /// + /// Reference: Alpenglow-Implementation-Plan.md Section 7.14a + fn on_candidate_query_fallback( + &self, + slot: SlotIndex, + block_hash: UInt256, + want_notar: bool, + response_callback: QueryResponseCallback, + ); } /* @@ -412,8 +436,18 @@ struct CandidateRequestState { start_time: SystemTime, /// Number of retry attempts so far retry_count: u32, + /// Current timeout for this request (grows with exponential backoff) + current_timeout: Duration, /// Validator index of the peer being queried source_idx: ValidatorIndex, + /// Accumulated notar bytes from partial responses (C++ CandidateAndCert::merge parity). + /// Peers may return notar-only when the candidate body is unavailable; we cache it + /// here so that when a body-only response arrives later, the merged result is complete. + cached_notar: Option>, + /// Accumulated candidate bytes from partial responses. + /// Peers may return candidate-only while notar is still missing; cache the body so + /// a later notar-only response can complete the merged result. + cached_candidate: Option>, } /* @@ -462,6 +496,11 @@ impl CandidateResolverCache { self.notar_certs.get(&key) } + /// Remove a cached candidate entry (e.g. after deserialization failure) + fn remove_candidate(&mut self, slot: SlotIndex, block_hash: &UInt256) { + self.candidates.remove(&(slot, block_hash.clone())); + } + /// Cleanup old entries for slots less than the given slot fn cleanup_before(&mut self, up_to_slot: SlotIndex) { self.candidates.retain(|(s, _), _| *s >= up_to_slot); @@ -765,6 +804,8 @@ pub(crate) struct ReceiverImpl { shard: ShardIdent, /// Maximum block + collated data size for candidate verification max_candidate_size: usize, + /// Protocol version from consensus config (determines BOC serialization flags) + proto_version: u32, /// Metrics in_messages_bytes: metrics::Counter, out_messages_bytes: metrics::Counter, @@ -1074,6 +1115,7 @@ impl ReceiverImpl { &block.candidate, &self.shard, self.max_candidate_size, + self.proto_version, ) { Ok(Some(info)) => (Some(info.block_id), Some(info.collated_file_hash)), Ok(None) => (None, None), @@ -1306,15 +1348,9 @@ impl ReceiverImpl { /// Handle incoming query (requestCandidate) /// - /// Reference: C++ CandidateResolver processes requestCandidate queries - /// - /// TODO (INT-1): Add DB fallback when candidate not in resolver_cache. - /// If resolver_cache.get_candidate() returns None, call SessionProcessor's - /// notify_get_approved_candidate() to load from validator's persistent storage. - /// This handles the case where this node approved a candidate but restarted before - /// caching it, and now a peer is requesting it. - /// See: validator-session/src/session_processor.rs line 1620 (process_query) - /// Reference: Alpenglow-Implementation-Plan.md Section 7.14a + /// Reference: C++ CandidateResolver processes requestCandidate queries. + /// On cache miss, delegates to SessionProcessor via `on_candidate_query_fallback` + /// which can reconstruct the response from in-memory or DB-backed storage. fn handle_query( &mut self, _adnl_id: PublicKeyHash, @@ -1322,6 +1358,7 @@ impl ReceiverImpl { response_callback: QueryResponseCallback, ) { check_execution_time!(50_000); + let request_data = data.data(); let object = match deserialize_boxed(request_data) { Ok(object) => object, @@ -1352,15 +1389,39 @@ impl ReceiverImpl { want_notar ); - // Look up cached data from local cache let candidate_bytes = if want_candidate { - self.resolver_cache - .get_candidate(slot, &block_hash) - .cloned() - .unwrap_or_default() + self.resolver_cache.get_candidate(slot, &block_hash).cloned() } else { - Vec::new() + None }; + + let cache_miss = want_candidate && candidate_bytes.is_none(); + + if cache_miss { + if let Some(listener) = self.listener.upgrade() { + log::debug!( + "SimplexReceiver {}: requestCandidate cache MISS \ + for slot={slot} hash={}, delegating to SessionProcessor", + self.session_id.to_hex_string(), + &block_hash.to_hex_string()[..8], + ); + listener.on_candidate_query_fallback( + slot, + block_hash, + want_notar, + response_callback, + ); + } else { + log::warn!( + "SimplexReceiver {}: requestCandidate cache MISS but listener dropped", + self.session_id.to_hex_string(), + ); + response_callback(Err(error!("Session listener dropped"))); + } + return; + } + + let candidate_bytes = candidate_bytes.unwrap_or_default(); let notar_bytes = if want_notar { self.resolver_cache .get_notar_cert(slot, &block_hash) @@ -1547,8 +1608,14 @@ impl ReceiverImpl { self.candidate_requests_counter.increment(1); // Create request state - let request_state = - CandidateRequestState { start_time: SystemTime::now(), retry_count: 0, source_idx }; + let request_state = CandidateRequestState { + start_time: SystemTime::now(), + retry_count: 0, + current_timeout: CANDIDATE_REQUEST_INITIAL_TIMEOUT, + source_idx, + cached_notar: None, + cached_candidate: None, + }; self.pending_requests.insert(key.clone(), request_state); // Send the query @@ -1558,7 +1625,7 @@ impl ReceiverImpl { let slot_clone = slot; let hash_clone = block_hash.clone(); self.post_delayed_action( - SystemTime::now() + CANDIDATE_REQUEST_TIMEOUT, + SystemTime::now() + CANDIDATE_REQUEST_INITIAL_TIMEOUT, move |receiver: &mut ReceiverImpl| { receiver.handle_candidate_request_timeout(slot_clone, hash_clone); }, @@ -1570,8 +1637,6 @@ impl ReceiverImpl { /// This reduces repeated queries to the same peer across retries, improving /// convergence when only a subset of peers have the requested candidate. fn select_peer_for_candidate_request(&self, exclude: Option) -> Option { - use rand::Rng; - let len = self.send_order.len(); if len <= 1 { return None; // Only self or empty @@ -1692,6 +1757,57 @@ impl ReceiverImpl { self.task_queues.clone() } + /// Merge partial `requestCandidate` response pieces with pending-request state. + /// + /// C++ parity: + /// - cache partial candidate/notar parts as they arrive; + /// - merge with previously cached parts; + /// - completion is checked by caller via non-empty merged candidate+notar. + fn merge_candidate_response_parts( + resolver_cache: &mut CandidateResolverCache, + pending_state: Option<&mut CandidateRequestState>, + slot: SlotIndex, + block_hash: &UInt256, + candidate_bytes: &[u8], + notar_bytes: &[u8], + ) -> (Vec, Vec) { + let candidate_vec = candidate_bytes.to_vec(); + let notar_vec = notar_bytes.to_vec(); + + if !candidate_vec.is_empty() { + resolver_cache.cache_candidate(slot, block_hash.clone(), candidate_vec.clone()); + } + if !notar_vec.is_empty() { + resolver_cache.cache_notar_cert(slot, block_hash.clone(), notar_vec.clone()); + } + + if let Some(state) = pending_state { + if !candidate_vec.is_empty() { + state.cached_candidate = Some(candidate_vec); + } + if !notar_vec.is_empty() { + state.cached_notar = Some(notar_vec.clone()); + } + + let merged_candidate = state.cached_candidate.clone().unwrap_or_default(); + let merged_notar = if !notar_vec.is_empty() { + notar_vec + } else if let Some(cached_notar) = state.cached_notar.clone() { + cached_notar + } else { + resolver_cache.get_notar_cert(slot, block_hash).cloned().unwrap_or_default() + }; + return (merged_candidate, merged_notar); + } + + let merged_notar = if !notar_vec.is_empty() { + notar_vec + } else { + resolver_cache.get_notar_cert(slot, block_hash).cloned().unwrap_or_default() + }; + (candidate_bytes.to_vec(), merged_notar) + } + /// Handle response from requestCandidate query fn handle_candidate_response( &mut self, @@ -1744,33 +1860,68 @@ impl ReceiverImpl { source_idx ); - // Check candidate before removing from pending - // If empty, leave request pending so timeout handler can retry - if candidate_bytes.is_empty() { - log::warn!( - "SimplexReceiver {}: empty candidate in response for slot={} hash={}, will retry on timeout", + // C++ CandidateAndCert::merge parity: cache both partial fields and + // complete only when the merged result has both candidate+notar. + let (merged_candidate_bytes, merged_notar) = + Self::merge_candidate_response_parts( + &mut self.resolver_cache, + self.pending_requests.get_mut(&key), + slot, + &block_hash, + candidate_bytes, + notar_bytes, + ); + + // If body is still missing after merge, keep pending for retry. + if merged_candidate_bytes.is_empty() { + log::debug!( + "SimplexReceiver {}: body-empty response for slot={} hash={} \ + (notar_len={}), will retry on timeout", self.session_id.to_hex_string(), slot, - &block_hash.to_hex_string()[..8] + &block_hash.to_hex_string()[..8], + notar_bytes.len(), ); return; } - // Remove from pending - we have all required data - self.pending_requests.remove(&key); + if merged_notar.is_empty() { + log::debug!( + "SimplexReceiver {}: candidate-only partial response for \ + slot={} hash={}, keep pending until notar arrives", + self.session_id.to_hex_string(), + slot, + &block_hash.to_hex_string()[..8], + ); + return; + } - let candidate = match deserialize_boxed(candidate_bytes) { + let candidate = match deserialize_boxed( + merged_candidate_bytes.as_slice(), + ) { Ok(msg) => match msg.downcast::() { Ok(c) => c, Err(_) => { + // Drop cached candidate so retry can fetch a fresh body; + // also purge resolver_cache to avoid serving bad data to peers. + self.resolver_cache.remove_candidate(slot, &block_hash); + if let Some(state) = self.pending_requests.get_mut(&key) { + state.cached_candidate = None; + } log::warn!( - "SimplexReceiver {}: unexpected candidate type in response", - self.session_id.to_hex_string() - ); + "SimplexReceiver {}: unexpected candidate type in response", + self.session_id.to_hex_string() + ); return; } }, Err(e) => { + // Drop cached candidate so retry can fetch a fresh body; + // also purge resolver_cache to avoid serving bad data to peers. + self.resolver_cache.remove_candidate(slot, &block_hash); + if let Some(state) = self.pending_requests.get_mut(&key) { + state.cached_candidate = None; + } log::warn!( "SimplexReceiver {}: failed to deserialize candidate: {}", self.session_id.to_hex_string(), @@ -1780,29 +1931,16 @@ impl ReceiverImpl { } }; - // Cache candidate for query responses (in case others ask us) - self.resolver_cache.cache_candidate( - slot, - block_hash.clone(), - candidate_bytes.to_vec(), - ); - - // Cache notar bytes for query responses (C++ CandidateResolver parity). - // This ensures future `requestCandidate(want_notar=true)` queries can be served immediately. - let notar_vec = notar_bytes.to_vec(); - if !notar_vec.is_empty() { - self.resolver_cache.cache_notar_cert( - slot, - block_hash.clone(), - notar_vec.clone(), - ); - } + // Remove from pending only when merged candidate+notar is complete. + self.pending_requests.remove(&key); - // Call listener with source_idx - let notar_cert = - if notar_vec.is_empty() { None } else { Some(notar_vec) }; + // Call listener with source_idx, using merged notar. if let Some(listener) = self.listener.upgrade() { - listener.on_candidate_received(source_idx, candidate, notar_cert); + listener.on_candidate_received( + source_idx, + candidate, + Some(merged_notar), + ); } } else { log::warn!( @@ -1833,15 +1971,16 @@ impl ReceiverImpl { } } - /// Handle request timeout - retry with next peer or give up + /// Handle request timeout - retry with next peer using exponential backoff. + /// C++ parity: candidate-resolver.cpp retries indefinitely until resolved. fn handle_candidate_request_timeout(&mut self, slot: SlotIndex, block_hash: UInt256) { let key = (slot, block_hash.clone()); // Check if request is still pending and get current state - let (retry_count, prev_source_idx) = match self.pending_requests.get(&key) { - Some(state) => (state.retry_count, state.source_idx.value()), + let (retry_count, prev_source_idx, current_timeout) = match self.pending_requests.get(&key) + { + Some(state) => (state.retry_count, state.source_idx.value(), state.current_timeout), None => { - // Request was fulfilled or cancelled log::trace!( "SimplexReceiver {}: handle_candidate_request_timeout slot={} hash={} - request already fulfilled or cancelled", self.session_id.to_hex_string(), @@ -1853,34 +1992,48 @@ impl ReceiverImpl { }; self.candidate_request_timeouts_counter.increment(1); - // Check max retries let new_retry_count = retry_count + 1; - if new_retry_count >= CANDIDATE_REQUEST_MAX_RETRIES { - self.candidate_request_giveups_counter.increment(1); - self.health_counters.candidate_giveups.fetch_add(1, Ordering::Relaxed); + if new_retry_count % CANDIDATE_REQUEST_MAX_RETRIES == 0 { log::warn!( - "SimplexReceiver {}: giving up on candidate request slot={} hash={} after {} retries", + "SimplexReceiver {}: candidate request slot={slot} hash={} \ + still pending after {new_retry_count} retries, continuing", self.session_id.to_hex_string(), - slot, - &block_hash.to_hex_string()[..8], - new_retry_count + &block_hash.to_hex_string()[..8] ); - self.pending_requests.remove(&key); - return; } - // Select next peer (random) + // Exponential backoff: timeout * multiplier, capped at max + let next_timeout_ms = + (current_timeout.as_millis() as f64 * CANDIDATE_REQUEST_TIMEOUT_MULTIPLIER) as u128; + let next_timeout = Duration::from_millis( + next_timeout_ms.min(CANDIDATE_REQUEST_MAX_TIMEOUT.as_millis()) as u64, + ); + + // Select next peer (random, excluding previous) let next_source_idx = match self.select_peer_for_candidate_request(Some(prev_source_idx)) { Some(idx) => idx, None => { - self.candidate_request_giveups_counter.increment(1); + // No peers available right now -- schedule a retry after backoff anyway, + // peers may come back online. + self.candidate_request_retries_counter.increment(1); log::warn!( - "SimplexReceiver {}: no more peers for candidate request slot={} hash={}", + "SimplexReceiver {}: no peers for candidate request slot={slot} hash={}, \ + will retry in {next_timeout:?}", self.session_id.to_hex_string(), - slot, &block_hash.to_hex_string()[..8] ); - self.pending_requests.remove(&key); + if let Some(state) = self.pending_requests.get_mut(&key) { + state.retry_count = new_retry_count; + state.current_timeout = next_timeout; + } + let slot_clone = slot; + let hash_clone = block_hash; + self.post_delayed_action( + SystemTime::now() + next_timeout, + move |receiver: &mut ReceiverImpl| { + receiver.handle_candidate_request_timeout(slot_clone, hash_clone); + }, + ); return; } }; @@ -1890,25 +2043,24 @@ impl ReceiverImpl { if let Some(state) = self.pending_requests.get_mut(&key) { state.retry_count = new_retry_count; state.source_idx = next_source_idx; + state.current_timeout = next_timeout; } log::trace!( - "SimplexReceiver {}: retrying candidate request slot={} hash={} to validator {} (retry {})", + "SimplexReceiver {}: retrying candidate request slot={slot} hash={} \ + to validator {next_source_idx} (retry {new_retry_count}, timeout {next_timeout:?})", self.session_id.to_hex_string(), - slot, - &block_hash.to_hex_string()[..8], - next_source_idx, - new_retry_count + &block_hash.to_hex_string()[..8] ); // Send to next peer self.send_candidate_request(slot, block_hash.clone(), next_source_idx); - // Schedule next timeout + // Schedule next timeout with backoff let slot_clone = slot; let hash_clone = block_hash; self.post_delayed_action( - SystemTime::now() + CANDIDATE_REQUEST_TIMEOUT, + SystemTime::now() + next_timeout, move |receiver: &mut ReceiverImpl| { receiver.handle_candidate_request_timeout(slot_clone, hash_clone); }, @@ -2045,6 +2197,7 @@ impl ReceiverImpl { &block.candidate, &self.shard, self.max_candidate_size, + self.proto_version, ) { Ok(Some(info)) => (Some(info.block_id), Some(info.collated_file_hash)), Ok(None) => (None, None), @@ -2233,7 +2386,6 @@ impl ReceiverImpl { /// Get slot from vote (boxed enum version) fn get_vote_slot(vote: &TlVoteBoxed) -> u32 { - use UnsignedVote; match vote.vote() { UnsignedVote::Consensus_Simplex_NotarizeVote(v) => *v.id.slot() as u32, UnsignedVote::Consensus_Simplex_FinalizeVote(v) => *v.id.slot() as u32, @@ -2243,7 +2395,6 @@ impl ReceiverImpl { /// Get slot from inner vote struct (avoids clone+box overhead) fn get_vote_slot_from_inner(vote: &TlVote) -> u32 { - use UnsignedVote; match &vote.vote { UnsignedVote::Consensus_Simplex_NotarizeVote(v) => *v.id.slot() as u32, UnsignedVote::Consensus_Simplex_FinalizeVote(v) => *v.id.slot() as u32, @@ -2366,7 +2517,6 @@ impl ReceiverImpl { // if (notarize_.has_value() && !bundle.notarize_.has_value()) { ... } // if (skip_.has_value() && !bundle.skip_.has_value()) { ... } // if (finalize_.has_value() && !bundle.finalize_.has_value()) { ... } - use UnsignedVote; let votes_to_rebroadcast: Vec<_> = self .our_votes .iter() @@ -2401,7 +2551,8 @@ impl ReceiverImpl { self.standstill_votes_rebroadcast_counter.increment(votes_to_rebroadcast.len() as u64); log::warn!( - "SimplexReceiver {}: Standstill detected, re-broadcasting {} certs + {} votes (range [{}, {}))", + "SimplexReceiver {}: Standstill detected, re-broadcasting {} certs + {} votes \ + (range [{}, {}))", self.session_id.to_hex_string(), cert_count, votes_to_rebroadcast.len(), @@ -2981,12 +3132,14 @@ impl ReceiverWrapper { session_id: SessionId, shard: &ShardIdent, max_candidate_size: usize, + proto_version: u32, ids: &[SessionNode], local_key: &PrivateKey, overlay_manager: ConsensusOverlayManagerPtr, listener: ReceiverListenerPtr, standstill_timeout: Duration, panicked_flag: Arc, + use_quic: bool, health_counters: Arc, ) -> Result { log::info!( @@ -3069,7 +3222,11 @@ impl ReceiverWrapper { .collect(); // Start overlay - let transport_type = consensus_common::OverlayTransportType::Simplex; + let transport_type = if use_quic { + consensus_common::OverlayTransportType::SimplexQuic + } else { + consensus_common::OverlayTransportType::Simplex + }; let overlay = overlay_manager.start_overlay( local_key, &overlay_short_id, @@ -3158,6 +3315,7 @@ impl ReceiverWrapper { dedup_votes: HashMap::new(), shard: shard_clone, max_candidate_size, + proto_version, in_messages_bytes: in_messages_bytes_clone, out_messages_bytes: out_messages_bytes_clone, in_broadcasts_bytes: in_broadcasts_bytes_clone, diff --git a/src/node/simplex/src/session.rs b/src/node/simplex/src/session.rs index 1b37b09..7cd0601 100644 --- a/src/node/simplex/src/session.rs +++ b/src/node/simplex/src/session.rs @@ -69,7 +69,10 @@ use crate::{ }; use consensus_common::{ check_execution_time, - utils::{get_elapsed_time, MetricsDumper}, + utils::{ + add_compute_percentage_metric, add_compute_relative_metric, add_compute_result_metric, + get_elapsed_time, MetricsDumper, + }, }; use crossbeam::channel::{bounded, Sender}; use std::{ @@ -89,7 +92,7 @@ use ton_api::ton::consensus::{ simplex::{Certificate, Vote}, CandidateData, }; -use ton_block::{error, Error, Result, ShardIdent}; +use ton_block::{error, Error, Result, ShardIdent, UInt256}; /* Constants @@ -149,6 +152,24 @@ impl ReceiverListener for ReceiverListenerImpl { processor.on_certificate(source_idx, certificate); })); } + + /// Handle RequestCandidate cache miss by delegating to SessionProcessor + fn on_candidate_query_fallback( + &self, + slot: crate::block::SlotIndex, + block_hash: UInt256, + want_notar: bool, + response_callback: consensus_common::QueryResponseCallback, + ) { + self.task_queue.post_closure(Box::new(move |processor: &mut SessionProcessor| { + processor.handle_candidate_query_fallback( + slot, + block_hash, + want_notar, + response_callback, + ); + })); + } } impl ReceiverListenerImpl { @@ -538,12 +559,14 @@ impl SessionImpl { session_id.clone(), &shard, max_candidate_size, + options.proto_version, &ids, &local_key, overlay_manager.clone(), receiver_listener, options.standstill_timeout, panicked_flag.clone(), + options.use_quic, health_counters.clone(), ) { Ok(r) => r, @@ -860,10 +883,6 @@ impl SessionImpl { /// Configures derivative metrics (rate of change), percentage metrics, /// and result status metrics similar to validator-session. fn create_metrics_dumper() -> MetricsDumper { - use consensus_common::utils::{ - add_compute_percentage_metric, add_compute_relative_metric, add_compute_result_metric, - }; - let mut metrics_dumper = MetricsDumper::new(); // Derivative metrics for loop counters (rate per second) diff --git a/src/node/simplex/src/session_processor.rs b/src/node/simplex/src/session_processor.rs index 77029f6..b4025ea 100644 --- a/src/node/simplex/src/session_processor.rs +++ b/src/node/simplex/src/session_processor.rs @@ -110,19 +110,20 @@ use ton_api::{ candidateid::CandidateId, candidateparent::CandidateParent, simplex::{ - vote::Vote as TlVote, votesignature::VoteSignature as TlVoteSignature, + candidateandcert::CandidateAndCert, vote::Vote as TlVote, + votesignature::VoteSignature as TlVoteSignature, votesignatureset::VoteSignatureSet, Certificate, UnsignedVote, Vote as TlVoteBoxed, VoteSignatureSet as VoteSignatureSetBoxed, }, CandidateData, CandidateHashData, CandidateParent as CandidateParentBoxed, }, - validator_session::candidate::Candidate as TlCandidate, + validator_session::candidate::CompressedCandidate, }, IntoBoxed, }; use ton_block::{ error, fail, sha256_digest, BlockIdExt, BlockSignaturesPure, BlockSignaturesSimplex, - BlockSignaturesVariant, CryptoSignature, CryptoSignaturePair, Deserializable, Error, + BlockSignaturesVariant, BocFlags, CryptoSignature, CryptoSignaturePair, Deserializable, Error, HashmapType, KeyId, Result, UInt256, ValidatorBaseInfo, }; @@ -195,6 +196,7 @@ pub(crate) struct HealthAlertState { last_parent_aging_warn: SystemTime, last_progress_warn: SystemTime, last_standstill_warn: SystemTime, + last_isolation_warn: SystemTime, prev_candidate_giveups: u64, prev_cert_verify_fails: u64, prev_last_finalized_slot: f64, @@ -215,6 +217,7 @@ impl HealthAlertState { last_parent_aging_warn: warn_base, last_progress_warn: warn_base, last_standstill_warn: warn_base, + last_isolation_warn: warn_base, prev_candidate_giveups: 0, prev_cert_verify_fails: 0, prev_last_finalized_slot: 0.0, @@ -650,6 +653,9 @@ pub(crate) struct SessionProcessor { delayed_actions: Vec, /// SimplexState FSM - core consensus state machine simplex_state: SimplexState, + /// Slots for which "missing body" has already been logged (throttle). + /// Prevents multi-million-line log floods when a slot body never arrives. + missing_body_logged: HashSet, /* Collation state (session-level only) @@ -693,6 +699,12 @@ pub(crate) struct SessionProcessor { validated_candidates: VecDeque, /// All received block candidates: RawCandidateId(slot, candidate_id_hash) โ†’ candidate data received_candidates: HashMap, + /// Serialized CandidateData bytes cache for RequestCandidate query fallback. + /// + /// Populated in `on_candidate_received()` by re-serializing the TL `CandidateData` object. + /// Used by `handle_candidate_query_fallback()` when the receiver's `resolver_cache` misses. + /// This provides C++ parity with `CandidateResolver::try_load_candidate_data_from_db()`. + candidate_data_cache: HashMap>, /* Metrics @@ -781,11 +793,11 @@ pub(crate) struct SessionProcessor { Block SeqNo Tracking Tracks expected blockchain sequence number for next block */ - /// Last committed block seqno - updated from BlockFinalizedEvent. - /// Used by `should_generate_empty_block()`, commit gating, and validation checks. + /// Last committed block seqno - updated in commit_single_block(). + /// Used for strict commit sequencing and validation checks. last_committed_seqno: Option, - /// Last committed block slot - updated from BlockFinalizedEvent + /// Last committed block slot - updated in commit_single_block() /// Used to retrieve parent BlockIdExt for empty block generation last_committed_slot: Option, /// Last committed non-empty block id (parent for empty blocks) @@ -802,6 +814,17 @@ pub(crate) struct SessionProcessor { /// Reference: C++ block-producer.cpp `is_before_split()` + `should_generate_empty_block()` last_committed_before_split: bool, + /// Last consensus-finalized seqno - tracks the highest seqno of a block committed + /// with FinalCert (is_final=true) in this session. + /// + /// C++ parity: mirrors `last_consensus_finalized_seqno_` in block-producer.cpp, which + /// advances on FinalizeBlock(is_final=true) and on BlockFinalizedInMasterchain events. + /// Used for `should_generate_empty_block()` on masterchain. + /// + /// Updated in `commit_single_block()` when use_final_cert is true, and in + /// `set_mc_finalized_seqno()` (coupled max with last_mc_finalized_seqno). + last_consensus_finalized_seqno: Option, + /// Blocks that have been committed (finalized): RawCandidateId(slot, hash) /// /// Used during batch finalization to track which blocks in a parent chain @@ -1287,6 +1310,7 @@ impl SessionProcessor { last_activity: vec![None; num_validators], delayed_actions: Vec::new(), simplex_state, + missing_body_logged: HashSet::new(), // Collation state precollated_blocks: PrecollatedBlockMap::new(), precollated_blocks_next_request_id: 0, @@ -1301,6 +1325,7 @@ impl SessionProcessor { validation_attempt_map: HashMap::new(), validated_candidates: VecDeque::new(), received_candidates: HashMap::new(), + candidate_data_cache: HashMap::new(), // Metrics metrics_receiver, check_all_counter, @@ -1341,6 +1366,7 @@ impl SessionProcessor { last_committed_slot: None, last_committed_block_id: None, last_committed_before_split: false, + last_consensus_finalized_seqno: initial_block_seqno.checked_sub(1), // Batch finalization tracking finalized_blocks: HashSet::new(), finalized_journal_pending_commit: HashMap::new(), @@ -1389,16 +1415,9 @@ impl SessionProcessor { Ok(processor) - // TODO (INT-1): Add session startup recovery - when simplex session starts after restart, - // previously approved candidates may need to be restored from the validator's persistent - // storage via `notify_get_approved_candidate()`. This is needed because: - // 1. Consensus peers might not have the candidate anymore (too old) - // 2. The candidate was approved by this node but session restarted before commit - // See: validator-session's catchain_started() at line 1246 which iterates approved blocks - // and calls get_approved_candidate() to restore them. - // Implementation: After SimplexState is restored from DB (if persisted), call - // notify_get_approved_candidate for each approved block to populate resolver_cache. - // Reference: Alpenglow-Implementation-Plan.md Section 7.14a + // Note: C++ simplex resolves candidates from its own consensus DB, not via + // validator manager. The Rust implementation uses in-memory candidate_data_cache + // and peer overlay for candidate resolution. No get_approved_candidate delegation. } /* @@ -1934,10 +1953,11 @@ impl SessionProcessor { /// /// # Reference /// - /// C++ `block-producer.cpp` lines 89-92: + /// C++ `block-producer.cpp`: /// ```cpp - /// void handle(ConsensusBus::BlockFinalizedInMasterchain event) { - /// last_mc_finalized_seqno_ = event->block.seqno(); + /// void handle(BusHandle, std::shared_ptr event) { + /// last_mc_finalized_seqno_ = std::max(event->block.seqno(), last_mc_finalized_seqno_); + /// last_consensus_finalized_seqno_ = std::max(last_mc_finalized_seqno_, last_consensus_finalized_seqno_); /// } /// ``` pub fn set_mc_finalized_seqno(&mut self, seqno: u32) { @@ -1947,7 +1967,17 @@ impl SessionProcessor { seqno, self.last_mc_finalized_seqno ); - self.last_mc_finalized_seqno = Some(seqno); + // Keep last_mc_finalized_seqno monotonic, mirroring C++ behavior: + // last_mc_finalized_seqno_ = std::max(event->block.seqno(), last_mc_finalized_seqno_); + let prev_mc = self.last_mc_finalized_seqno.unwrap_or(0); + self.last_mc_finalized_seqno = Some(seqno.max(prev_mc)); + // C++ parity: BlockFinalizedInMasterchain also couples to last_consensus_finalized_seqno_ + let consensus = self.last_consensus_finalized_seqno.unwrap_or(0); + let mc = self.last_mc_finalized_seqno.unwrap_or(0); + let new_val = mc.max(consensus); + if new_val > consensus { + self.last_consensus_finalized_seqno = Some(new_val); + } } /// Get the last masterchain finalized seqno @@ -1958,15 +1988,6 @@ impl SessionProcessor { self.last_mc_finalized_seqno } - /// Get the last committed (consensus-finalized) block seqno - /// - /// This is updated from `BlockFinalizedEvent` and serves as - /// `last_consensus_finalized_seqno` for masterchain empty block decisions. - #[allow(dead_code)] - pub fn last_committed_seqno(&self) -> Option { - self.last_committed_seqno - } - /// Determines if an empty block should be generated for finalization recovery /// /// Empty blocks are a TON-specific extension (not in Alpenglow White Paper) that @@ -1980,8 +2001,8 @@ impl SessionProcessor { /// /// # Logic /// - /// - **Masterchain**: Generate empty if `last_committed_seqno + 1 < new_seqno` - /// (i.e., finalized is more than 1 behind) + /// - **Masterchain**: Generate empty if `last_consensus_finalized_seqno + 1 < new_seqno` + /// (i.e., consensus-finalized is more than 1 behind) /// - **Shardchain**: Generate empty if `last_mc_finalized_seqno + 8 < new_seqno` /// (i.e., MC is more than 8 behind) /// @@ -2008,7 +2029,7 @@ impl SessionProcessor { } // C++ parity: ALWAYS generate empty if previous block has before_split flag - // This is required for shard split/merge operations (SPLIT-1 fix). + // This is required for shard split/merge operations. // Reference: C++ block-producer.cpp is_before_split() check if self.last_committed_before_split { log::debug!( @@ -2024,9 +2045,10 @@ impl SessionProcessor { if self.description.get_shard().is_masterchain() || DISABLE_NON_FINALIZED_PARENTS_FOR_COLLATION { - // Masterchain: finalized seqno must be at most 1 behind new seqno - // Uses last_committed_seqno as `last_consensus_finalized_seqno_` - match self.last_committed_seqno { + // Masterchain: consensus-finalized seqno must be at most 1 behind new seqno. + // C++ parity: block-producer.cpp uses `last_consensus_finalized_seqno_` which + // advances on FinalizeBlock(is_final) and on BlockFinalizedInMasterchain. + match self.last_consensus_finalized_seqno { Some(finalized) => finalized + 1 < new_seqno, None => false, // No finalization yet, can't be behind } @@ -2368,6 +2390,33 @@ impl SessionProcessor { current_giveups ); } + + // 8. Validator isolation: only self is active for extended period + let isolation_threshold = Duration::from_secs(60); + let session_age = now.duration_since(self.session_creation_time()).unwrap_or_default(); + if session_age > isolation_threshold + && active_weight <= 1 + && total_weight > 1 + && now.duration_since(self.health_alert_state.last_isolation_warn).unwrap_or_default() + >= Duration::from_secs(300) + { + self.health_alert_state.last_isolation_warn = now; + self.health_warnings_counter.increment(1); + let peers_never_seen = self + .last_activity + .iter() + .enumerate() + .filter(|(i, ts)| *i != self.description.get_self_idx().0 as usize && ts.is_none()) + .count(); + log::error!( + "SIMPLEX_HEALTH anomaly=validator_isolated session={session_prefix} \ + active_weight={active_weight} total={total_weight} \ + session_age={:.0}s peers_never_seen={peers_never_seen}/{} โ€” \ + possible validator key mismatch or overlay connectivity failure", + session_age.as_secs_f64(), + total_weight - 1, + ); + } } /// Produce detailed debug dump of session state @@ -3701,11 +3750,30 @@ impl SessionProcessor { parent: &Option, ) -> Result { let root_hash = &candidate.id.root_hash; - let file_hash = &candidate.id.file_hash; - let collated_file_hash = &candidate.collated_file_hash; let data = &candidate.data; let collated_data = &candidate.collated_data; + // Compute hashes from canonical BOC representation to match C++ simplex behavior. + // C++ leader hashes the original serialized bytes; C++ receiver hashes decompressed + // bytes โ€” they match because BOC serialization is deterministic given the same mode + // flags (mode 31 for block data, mode 2 for collated data). + // We explicitly canonicalize (deserialize โ†’ re-serialize with target flags) to + // guarantee matching hashes even if the input BOC was serialized with different flags. + // + // Falls back to raw bytes if canonicalization fails (e.g., in unit tests with + // mock data that's not valid BOC). In production, all data is valid BOC. + let file_hash = + match consensus_common::compression::canonicalize_boc(data.data(), BocFlags::all()) { + Ok(canonical) => UInt256::from_slice(&sha256_digest(&canonical)), + Err(_) => UInt256::from_slice(&sha256_digest(data.data())), + }; + let collated_file_hash = match consensus_common::compression::canonicalize_boc( + collated_data.data(), + BocFlags::Crc32, + ) { + Ok(canonical) => UInt256::from_slice(&sha256_digest(&canonical)), + Err(_) => UInt256::from_slice(&sha256_digest(collated_data.data())), + }; log::trace!( "Session {} create_normal_block_desc: slot={}, root_hash={:x}", self.session_id().to_hex_string(), @@ -3781,7 +3849,7 @@ impl SessionProcessor { let candidate_hash = crate::utils::compute_candidate_id_hash( slot, Some(&block_id), - Some(collated_file_hash), + Some(&collated_file_hash), parent_info, ); @@ -3795,14 +3863,19 @@ impl SessionProcessor { .map_err(|e| error!("failed to sign candidate: {e}"))?; // Build TL candidate for broadcast - // Serialize block candidate as validatorSession.candidate (data + collated_data) - // Note: src must be zeros per C++ protocol (consensus-types.cpp) - let tl_block_candidate = TlCandidate { + // C++ simplex always uses compressed candidates (compression_enabled=true hardcoded). + // Serialize as validatorSession.compressedCandidate (LZ4+BOC merged roots). + let (compressed, decompressed_size) = + consensus_common::compression::compress_candidate_data( + data.data(), + collated_data.data(), + )?; + let tl_block_candidate = CompressedCandidate { src: UInt256::default(), round: candidate_seqno as i32, root_hash: root_hash.clone(), - data: data.data().clone(), - collated_data: collated_data.data().clone(), + data: compressed, + decompressed_size: decompressed_size as i32, }; let candidate_bytes = consensus_common::serialize_tl_boxed_object!(&tl_block_candidate.into_boxed()); @@ -4217,6 +4290,7 @@ impl SessionProcessor { vote_hash, data: raw_vote_for_db.to_raw_buffer(), node_idx: source_idx, + seqno: 0, // assigned by save_vote_async }; if let Err(e) = self.db.save_vote_async(&record) { log::error!( @@ -4226,6 +4300,24 @@ impl SessionProcessor { ); self.increment_error(); } + + // Proactively request missing candidate when receiving a NotarizeVote + // for a block we don't have. This handles the case where the candidate + // broadcast was lost (e.g., due to QUIC congestion stall with C++ ngtcp2). + // Without this, the node can't vote and NotarizationReached is never triggered, + // which is the normal trigger for candidate requests. + if let Some(ref hash) = tl_hash_opt { + let candidate_id = RawCandidateId { slot: tl_slot, hash: hash.clone() }; + if !self.has_real_candidate_body(&candidate_id) { + log::debug!( + "Session {} on_vote: NotarizeVote for missing candidate \ + slot={tl_slot} hash={} from source_idx={source_idx}, requesting", + &self.session_id().to_hex_string()[..8], + &hash.to_hex_string()[..8] + ); + self.request_candidate(tl_slot, hash.clone(), None); + } + } } VoteResult::Duplicate => { // Duplicate vote, silently ignore @@ -4387,9 +4479,9 @@ impl SessionProcessor { let fsm_first_non_finalized_slot = self.simplex_state.get_first_non_finalized_slot(); if tl_slot < fsm_first_non_finalized_slot { log::trace!( - "Session {} on_certificate: dropping old certificate slot={tl_slot} (< \ - first_non_finalized={fsm_first_non_finalized_slot}) kind={tl_kind} from \ - source_idx={source_idx}", + "Session {} on_certificate: dropping old certificate slot={tl_slot} \ + (< first_non_finalized={fsm_first_non_finalized_slot}) kind={tl_kind} \ + from source_idx={source_idx}", &self.session_id().to_hex_string()[..8], ); return; @@ -4398,8 +4490,8 @@ impl SessionProcessor { // Reject far-future slots before signature verification (DoS protection) if self.simplex_state.is_slot_too_far_ahead(tl_slot) { log::warn!( - "Session {} on_certificate: REJECTED - slot {tl_slot} too far ahead (max={}) \ - kind={tl_kind} from source_idx={source_idx}", + "Session {} on_certificate: REJECTED - slot {tl_slot} too far ahead \ + (max={}) kind={tl_kind} from source_idx={source_idx}", &self.session_id().to_hex_string()[..8], self.simplex_state.max_acceptable_slot(), ); @@ -4439,6 +4531,22 @@ impl SessionProcessor { cert.signatures.len() ); + // Proactively request missing candidate when receiving a certificate + // for a block we don't have. This handles the case where the candidate + // broadcast was lost (e.g., due to QUIC congestion stall with C++ ngtcp2). + if let Some(ref hash) = tl_hash_opt { + let candidate_id = RawCandidateId { slot: tl_slot, hash: hash.clone() }; + if !self.has_real_candidate_body(&candidate_id) { + log::debug!( + "Session {} on_certificate: {tl_kind} cert for missing candidate \ + slot={tl_slot} hash={} from source_idx={source_idx}, requesting", + &self.session_id().to_hex_string()[..8], + &hash.to_hex_string()[..8] + ); + self.request_candidate(tl_slot, hash.clone(), None); + } + } + // Dispatch based on vote type in certificate // If stored (new certificate), relay to other validators and cache for standstill match &cert.vote { @@ -4743,6 +4851,7 @@ impl SessionProcessor { leader_idx, self.description.get_shard(), max_size, + self.description.opts().proto_version, ) { Ok(c) => c, Err(e) => { @@ -4806,12 +4915,20 @@ impl SessionProcessor { candidate_id.slot ); - // Check if candidate already known - if self.pending_validations.contains_key(&candidate_id) - || self.pending_approve.contains(&candidate_id) - || self.approved.contains_key(&candidate_id) - || self.rejected.contains(&candidate_id) - || self.received_candidates.contains_key(&candidate_id) + // Check if candidate already known. + // A finalized-boundary stub (seeded by handle_block_finalized with empty data) is NOT + // "already known" for this purpose -- we want the real body to overwrite it. + let is_finalized_stub = self + .received_candidates + .get(&candidate_id) + .map(|r| r.candidate_hash_data_bytes.is_empty()) + .unwrap_or(false); + if !is_finalized_stub + && (self.pending_validations.contains_key(&candidate_id) + || self.pending_approve.contains(&candidate_id) + || self.approved.contains_key(&candidate_id) + || self.rejected.contains(&candidate_id) + || self.received_candidates.contains_key(&candidate_id)) { log::trace!( "Session {} on_candidate_received: candidate already known: {:?}", @@ -4845,6 +4962,32 @@ impl SessionProcessor { // Determine if this is an empty block from the TL variant let is_empty = matches!(candidate, CandidateData::Consensus_Empty(_)); + // Cache serialized CandidateData for RequestCandidate query fallback (C++ parity). + // This provides a secondary in-memory store that persists independently of + // the receiver's resolver_cache, enabling peers to retrieve candidates even + // after the resolver_cache is cleaned up. + match serialize_boxed(&candidate) { + Ok(bytes) => { + self.candidate_data_cache.insert(candidate_id.clone(), bytes.clone()); + // Persist to DB for restart serving (C++ CandidateResolver::store_candidate parity) + if let Err(e) = self.db.save_candidate_payload_async(&candidate_id, &bytes) { + log::error!( + "Session {} on_candidate_received: failed to persist candidate payload: {}", + &self.session_id().to_hex_string()[..8], + e + ); + self.increment_error(); + } + } + Err(e) => { + log::warn!( + "Session {} on_candidate_received: failed to serialize CandidateData for cache: {}", + &self.session_id().to_hex_string()[..8], + e + ); + } + } + // Seqno validation for on_candidate_received // Validate seqno is consistent with parent (if parent is already received) let received_seqno = block_id.seq_no; @@ -5313,7 +5456,7 @@ impl SessionProcessor { /// Verify notarization certificate from VoteSignatureSet (C++ wire format) /// - /// INTEROP-1: Parse VoteSignatureSet and verify signatures. + /// Parse VoteSignatureSet and verify signatures. /// Reference: C++ NotarCert::from_tl(voteSignatureSet&&, vote, bus) fn verify_notar_cert_from_vote_signature_set( &self, @@ -5486,8 +5629,10 @@ impl SessionProcessor { self.pending_parent_resolutions.entry(key).or_default().push(pending); - // Schedule a request for the missing parent (with delay to allow broadcast to arrive) - self.request_candidate(missing_parent.slot, missing_parent.hash, None); + // Request the missing parent immediately (no delay). Parent-cascade requests are + // catch-up traffic: the candidate was already produced long ago and won't arrive + // via broadcast, so the 1-second CANDIDATE_REQUEST_DELAY only adds latency. + self.request_candidate(missing_parent.slot, missing_parent.hash, Some(Duration::ZERO)); } /// Update the `is_fully_resolved` cache for a specific candidate and its descendants. @@ -5766,7 +5911,7 @@ impl SessionProcessor { /// /// Validates pending candidates whose parent slot has been notarized (or finalized) /// in the FSM. Genesis candidates (no parent) are always eligible. This enables - /// optimistic validation on notarized-only parents (OPTIMISTIC-VALID-1 / C++ parity). + /// optimistic validation on notarized-only parents (C++ parity). fn check_validation(&mut self) { check_execution_time!(10_000); instrument!(); @@ -6424,6 +6569,7 @@ impl SessionProcessor { vote_hash, data: serialized.into(), node_idx: self.description.get_self_idx(), + seqno: 0, // assigned by save_vote_async }; let result = match self.db.save_vote_async(&record) { @@ -6487,6 +6633,18 @@ impl SessionProcessor { โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ */ + /// Check whether we have a **real** candidate body (not a finalized-boundary stub). + /// + /// Finalized-boundary stubs are inserted by `handle_block_finalized` with empty + /// `candidate_hash_data_bytes` to serve as parent-resolution boundaries. They must + /// NOT suppress `requestCandidate` retries -- a stub is not a real body. + fn has_real_candidate_body(&self, id: &RawCandidateId) -> bool { + self.received_candidates + .get(id) + .map(|r| !r.candidate_hash_data_bytes.is_empty()) + .unwrap_or(false) + } + /// Schedule a candidate request with delay if not already requested /// /// Called by `try_commit_finalized_chains()` when a candidate body or NotarCert is missing. @@ -6529,8 +6687,8 @@ impl SessionProcessor { } } - // Check if we already have what we need - let have_body = self.received_candidates.contains_key(&key); + // Check if we already have what we need (stubs don't count as real bodies) + let have_body = self.has_real_candidate_body(&key); let have_notar = self.simplex_state.get_notarize_certificate(slot, &block_hash).is_some(); if have_body && have_notar { @@ -6567,7 +6725,7 @@ impl SessionProcessor { self.post_delayed_action(expiration_time, move |processor: &mut SessionProcessor| { let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; - let have_body = processor.received_candidates.contains_key(&candidate_id); + let have_body = processor.has_real_candidate_body(&candidate_id); let have_notar = processor.simplex_state.get_notarize_certificate(slot, &block_hash).is_some(); @@ -6656,7 +6814,7 @@ impl SessionProcessor { let received = match self.received_candidates.get(¤t_id) { Some(r) => r, None => { - log::debug!( + log::trace!( "Session {} collect_gapless_commit_chain: missing body for slot={} hash={}", &self.session_id().to_hex_string()[..8], current_id.slot, @@ -6666,6 +6824,36 @@ impl SessionProcessor { } }; + // 1b. Finalized-boundary stub detection. + // + // Stubs are inserted by handle_block_finalized() for parent-resolution boundaries. + // They are not committable bodies. + // + // - Triggered block is a stub: treat as missing body and request it. + // - Non-triggered ancestor is a stub: stop walking (boundary reached). + if received.candidate_hash_data_bytes.is_empty() { + if is_first { + log::debug!( + "Session {} collect_gapless_commit_chain: triggered finalized block is \ + still a boundary stub, waiting for body: slot={} hash={}", + &self.session_id().to_hex_string()[..8], + current_id.slot, + ¤t_id.hash.to_hex_string()[..8], + ); + return ChainCollectionResult::MissingCandidate { + missing_id: current_id.clone(), + }; + } + + log::trace!( + "Session {} collect_gapless_commit_chain: reached finalized boundary stub \ + at slot={}, stopping walk", + &self.session_id().to_hex_string()[..8], + current_id.slot, + ); + break; + } + let current_seqno = received.block_id.seq_no; if is_first && triggered_is_empty.is_none() { @@ -7147,7 +7335,7 @@ impl SessionProcessor { // For empty blocks this is the re-signed parent block id (same as previous non-empty id). self.last_committed_block_id = Some(received.block_id.clone()); - // Extract and track before_split flag for split/merge handling (SPLIT-1 fix) + // Extract and track before_split flag for split/merge handling // C++ parity: C++ checks `is_before_split(prev_block_data)` in should_generate_empty_block() // We extract it here during commit and cache it for the next collation decision. if !is_empty_block { @@ -7319,6 +7507,23 @@ impl SessionProcessor { // Increment commits counter self.commits_counter.success(); + + // C++ parity: block-producer.cpp advances last_consensus_finalized_seqno_ + // only on FinalizeBlock when is_final() is true. This is the Rust equivalent. + if use_final_cert { + let prev = self.last_consensus_finalized_seqno.unwrap_or(0); + if seqno > prev { + self.last_consensus_finalized_seqno = Some(seqno); + log::debug!( + "Session {} commit_single_block: advanced last_consensus_finalized_seqno \ + {} -> {} (slot={}, is_final=true)", + &self.session_id().to_hex_string()[..8], + prev, + seqno, + slot + ); + } + } } // ===== Common finalization (for both empty and non-empty) ===== @@ -7659,15 +7864,17 @@ impl SessionProcessor { } ChainCollectionResult::MissingCandidate { missing_id } => { - log::debug!( - "Session {} try_commit_finalized_chains: s{}:{} waiting for s{}:{} (body \ - or NotarCert)", - &self.session_id().to_hex_string()[..8], - finalized_id.slot, - &finalized_id.hash.to_hex_string()[..8], - missing_id.slot, - &missing_id.hash.to_hex_string()[..8], - ); + if self.missing_body_logged.insert(missing_id.slot.0) { + log::debug!( + "Session {} try_commit_finalized_chains: s{}:{} waiting for s{}:{} \ + (body or NotarCert)", + &self.session_id().to_hex_string()[..8], + finalized_id.slot, + &finalized_id.hash.to_hex_string()[..8], + missing_id.slot, + &missing_id.hash.to_hex_string()[..8], + ); + } // Request the missing candidate (includes body + NotarCert with want_notar=true) self.request_candidate(missing_id.slot, missing_id.hash, None); @@ -8028,6 +8235,54 @@ impl SessionProcessor { let entry = FinalizedEntry { event: event.clone(), finalized_at: self.now() }; self.finalized_journal_pending_commit.insert(finalized_id.clone(), entry); + // NOTE: last_consensus_finalized_seqno is NOT advanced here. + // C++ parity: block-producer.cpp advances last_consensus_finalized_seqno_ only on + // FinalizeBlock(is_final=true), which happens AFTER the state-resolver commits. + // In Rust, the equivalent is commit_single_block() with use_final_cert=true. + + // Seed a finalized-boundary entry into received_candidates for parent resolution. + // C++ parity: StateResolver::resolve_state_inner() treats finalized blocks as boundaries + // and stops recursing into their ancestors. Rust needs the same behavior for live sessions, + // not only for restart recovery. + if let Some(ref block_id) = event.block_id { + if !self.received_candidates.contains_key(&finalized_id) { + self.received_candidates.insert( + finalized_id.clone(), + ReceivedCandidate { + slot, + source_idx: self.description.get_self_idx(), + candidate_id_hash: block_hash.clone(), + candidate_hash_data_bytes: Vec::new(), + block_id: block_id.clone(), + root_hash: block_id.root_hash.clone(), + file_hash: block_id.file_hash.clone(), + data: consensus_common::ConsensusCommonFactory::create_block_payload( + Vec::new(), + ), + collated_data: + consensus_common::ConsensusCommonFactory::create_block_payload( + Vec::new(), + ), + receive_time: self.now(), + is_empty: false, + parent_id: None, + is_fully_resolved: true, + }, + ); + log::debug!( + "Session {} handle_block_finalized: seeded finalized boundary for slot={} \ + seqno={} (for parent resolution)", + &self.session_id().to_hex_string()[..8], + slot, + block_id.seq_no() + ); + + // Resolve any pending parent resolutions that were waiting for this candidate + self.update_resolution_cache_chain(&finalized_id); + self.try_resolve_waiting_candidates(&finalized_id); + } + } + log::debug!( "Session {} FINALIZED: slot={}, hash={} - recorded in journal, weight={}/{} ({:.0}%)", &self.session_id().to_hex_string()[..8], @@ -8115,6 +8370,12 @@ impl SessionProcessor { //TODO: implement cleanup of blocks for old candidates //self.received_candidates.retain(|_hash, c| c.slot >= up_to_slot); + // Clean up candidate_data_cache in sync with received_candidates + self.candidate_data_cache.retain(|id, _| id.slot >= up_to_slot); + + // Prune log-throttle set to prevent unbounded growth over long sessions + self.missing_body_logged.retain(|&slot| slot >= up_to_slot.value()); + // Remove pending candidate requests for slots < up_to_slot self.requested_candidates.retain(|id, _| id.slot >= up_to_slot); @@ -8220,8 +8481,8 @@ impl SessionProcessor { // unresolved parent chain, causing timeouts and skip cascades in single-host tests. // // C++ parity intent: CandidateResolver/Pool logic requests missing candidate data - // based on observed certificates. - if !self.received_candidates.contains_key(&candidate_id) { + // based on observed certificates. Finalized-boundary stubs don't count as real bodies. + if !self.has_real_candidate_body(&candidate_id) { self.request_candidate(event.slot, event.block_hash.clone(), None); } @@ -8299,12 +8560,10 @@ impl SessionProcessor { cert_bytes.len(), ); - // Send certificate to all validators unless this cert is learned via - // direct query/repair path (avoid redundant broadcast storms). - if event.should_broadcast { - self.certs_relayed_counter.increment(1); - self.receiver.send_certificate(tl_cert); - } + // C++ parity (pool.cpp handle_saved_certificate): relay every newly + // accepted certificate to all validators. Dedup is in SimplexState. + self.certs_relayed_counter.increment(1); + self.receiver.send_certificate(tl_cert); // Cache for standstill re-broadcast self.receiver.cache_standstill_certificate( @@ -8357,19 +8616,17 @@ impl SessionProcessor { // Serialize for caching match serialize_boxed(&tl_cert) { Ok(cert_bytes) => { - if event.should_broadcast { - log::trace!( - "Session {} handle_skip_certificate_reached: broadcasting skip cert for \ - slot={} ({}B)", - &self.session_id().to_hex_string()[..8], - event.slot, - cert_bytes.len(), - ); + log::trace!( + "Session {} handle_skip_certificate_reached: broadcasting skip cert for \ + slot={} ({}B)", + &self.session_id().to_hex_string()[..8], + event.slot, + cert_bytes.len(), + ); - // Send certificate to all validators - self.certs_relayed_counter.increment(1); - self.receiver.send_certificate(tl_cert); - } + // Send certificate to all validators + self.certs_relayed_counter.increment(1); + self.receiver.send_certificate(tl_cert); // Cache for standstill re-broadcast self.receiver.cache_standstill_certificate( @@ -8393,9 +8650,7 @@ impl SessionProcessor { /// /// Called when FSM determines finalization threshold reached for a block. /// Always caches the finalization certificate for standstill replay. - /// Broadcasts only when `event.should_broadcast` is true for locally-created - /// certificates. Peer-ingested certificates are cached but not re-broadcast - /// by this path to avoid amplification. + /// Relays certificate to all validators (C++ parity: handle_saved_certificate). fn handle_finalization_reached(&mut self, event: FinalizationReachedEvent) { check_execution_time!(1_000); @@ -8424,18 +8679,17 @@ impl SessionProcessor { // Serialize for broadcast + caching match serialize_boxed(&tl_cert) { Ok(cert_bytes) => { - // Broadcast to all validators (C++ parity: handle_our_certificate) - if event.should_broadcast { - log::trace!( - "Session {} handle_finalization_reached: \ - broadcasting final cert for slot={} ({}B)", - &self.session_id().to_hex_string()[..8], - event.slot, - cert_bytes.len(), - ); - self.certs_relayed_counter.increment(1); - self.receiver.send_certificate(tl_cert); - } + // C++ parity (pool.cpp handle_saved_certificate): relay every newly + // accepted certificate to all validators. Dedup is in SimplexState. + log::trace!( + "Session {} handle_finalization_reached: \ + broadcasting final cert for slot={} ({}B)", + &self.session_id().to_hex_string()[..8], + event.slot, + cert_bytes.len(), + ); + self.certs_relayed_counter.increment(1); + self.receiver.send_certificate(tl_cert); // Cache per-slot final certificate (for bundle replay) self.receiver.cache_standstill_certificate( @@ -9020,55 +9274,245 @@ impl SessionProcessor { }); } - /// Request approved candidate from validator + /// Handle RequestCandidate query fallback when receiver's resolver_cache misses. /// - /// Called to retrieve a previously approved block candidate from persistent storage. - /// Used for session restart recovery and as a fallback for candidate resolver queries. + /// Called from SXRCV thread via ReceiverListener when a peer's RequestCandidate query + /// cannot be answered from the in-memory resolver_cache. Attempts to reconstruct the + /// response from: + /// 1. `candidate_data_cache` (in-memory, fast path) + /// 2. SimplexDB `CandidateInfoRecord` (empty blocks only -- reconstructed from metadata) /// - /// TODO (INT-1): Remove #[allow(dead_code)] when integrated. Use cases: - /// 1. Session startup: Restore approved candidates from DB to resolver_cache - /// 2. Query fallback: When resolver_cache misses, try loading from DB - /// Reference: validator-session/src/session_processor.rs lines 1246, 1620 - /// See: Alpenglow-Implementation-Plan.md Section 7.14a - #[allow(dead_code)] - fn notify_get_approved_candidate( - &self, - source: crate::PublicKey, - root_hash: crate::BlockHash, - file_hash: crate::BlockHash, - collated_data_hash: crate::BlockHash, - callback: crate::ValidatorBlockCandidateCallback, + /// Non-empty blocks not in the in-memory cache return an empty response; the + /// querying peer will retry with other validators. This matches C++ behavior + /// where `CandidateResolver` only loads from its own consensus DB, never from + /// the validator manager. + /// + /// Reference: C++ `CandidateResolver::try_load_candidate_data_from_db()` + pub fn handle_candidate_query_fallback( + &mut self, + slot: SlotIndex, + block_hash: UInt256, + want_notar: bool, + response_callback: crate::QueryResponseCallback, ) { - check_execution_time!(20_000); + check_execution_time!(50_000); - log::trace!( - "Session {} notify_get_approved_candidate: posting get_approved_candidate event", - self.session_id().to_hex_string() + let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; + let session_hex = &self.session_id().to_hex_string()[..8]; + + // 1. Fast path: check in-memory candidate_data_cache + if let Some(candidate_bytes) = self.candidate_data_cache.get(&candidate_id) { + log::debug!( + "Session {} candidate_query_fallback: cache HIT for slot={} hash={} ({}B)", + session_hex, + slot, + &block_hash.to_hex_string()[..8], + candidate_bytes.len() + ); + let notar_bytes = if want_notar { + self.load_notar_cert_bytes_from_db(&candidate_id) + } else { + Vec::new() + }; + Self::send_candidate_and_cert_response( + candidate_bytes.clone(), + notar_bytes, + response_callback, + ); + return; + } + + // 2. DB path: load CandidateInfoRecord for metadata + let candidate_info = match self.load_candidate_info_from_db(&candidate_id) { + Some(info) => info, + None => { + log::debug!( + "Session {} candidate_query_fallback: NOT FOUND for slot={} hash={}", + session_hex, + slot, + &block_hash.to_hex_string()[..8] + ); + Self::send_empty_candidate_response(response_callback); + return; + } + }; + + let notar_bytes = + if want_notar { self.load_notar_cert_bytes_from_db(&candidate_id) } else { Vec::new() }; + + // 3. Try persisted payload from DB first (works for both empty and non-empty blocks, + // since save_candidate_payload_async persists payloads for all candidates). + { + const DB_TIMEOUT: Duration = Duration::from_secs(2); + match self.db.load_candidate_payload_by_id(&candidate_id, DB_TIMEOUT) { + Ok(Some(payload_bytes)) => { + log::debug!( + "Session {} candidate_query_fallback: loaded payload from DB for slot={} ({}B)", + session_hex, + slot, + payload_bytes.len() + ); + Self::send_candidate_and_cert_response( + payload_bytes, + notar_bytes, + response_callback, + ); + return; + } + Ok(None) => {} + Err(e) => { + log::warn!( + "Session {} candidate_query_fallback: DB payload load error for slot={}: {}", + session_hex, + slot, + e + ); + } + } + } + + // 4. DB payload not available: try metadata reconstruction for empty blocks + let is_empty = matches!( + candidate_info.candidate_hash_data, + CandidateHashData::Consensus_CandidateHashDataEmpty(_) ); - let listener = self.listener.clone(); + if is_empty { + match self.reconstruct_empty_candidate_data_from_info(&candidate_id, &candidate_info) { + Ok(bytes) => { + log::debug!( + "Session {} candidate_query_fallback: reconstructed empty block for slot={} ({}B)", + session_hex, + slot, + bytes.len() + ); + Self::send_candidate_and_cert_response(bytes, notar_bytes, response_callback); + return; + } + Err(e) => { + log::warn!( + "Session {} candidate_query_fallback: failed to reconstruct empty block \ + for slot={}: {}", + session_hex, + slot, + e + ); + } + } + } - self.invoke_session_callback(move || { - check_execution_time!(20_000); + // 5. Not in memory, DB, or reconstructable: return notar-only if available (partial merge). + log::debug!( + "Session {} candidate_query_fallback: block NOT FOUND for slot={} hash={}, \ + returning notar_only={}", + session_hex, + slot, + &block_hash.to_hex_string()[..8], + !notar_bytes.is_empty() + ); + Self::send_candidate_and_cert_response(Vec::new(), notar_bytes, response_callback); + } - if let Some(listener) = listener.upgrade() { - log::trace!( - "SessionProcessor::notify_get_approved_candidate: get_approved_candidate start" - ); + /// Load CandidateInfoRecord from DB (blocking, used for rare query fallback). + fn load_candidate_info_from_db( + &self, + candidate_id: &RawCandidateId, + ) -> Option { + const DB_TIMEOUT: Duration = Duration::from_secs(2); - listener.get_approved_candidate( - source, - root_hash, - file_hash, - collated_data_hash, - callback, + match self.db.load_candidate_info_by_id(candidate_id, DB_TIMEOUT) { + Ok(record) => record, + Err(e) => { + log::warn!( + "Session {} load_candidate_info_from_db: failed for slot={}: {}", + &self.session_id().to_hex_string()[..8], + candidate_id.slot, + e ); + None + } + } + } - log::trace!( - "SessionProcessor::notify_get_approved_candidate: get_approved_candidate finish" + /// Load notar cert bytes from DB (blocking, used for rare query fallback). + fn load_notar_cert_bytes_from_db(&self, candidate_id: &RawCandidateId) -> Vec { + const DB_TIMEOUT: Duration = Duration::from_secs(2); + + match self.db.load_notar_cert_by_id(candidate_id, DB_TIMEOUT) { + Ok(Some(record)) => record.notar_cert_bytes, + Ok(None) => Vec::new(), + Err(e) => { + log::debug!( + "Session {} load_notar_cert_bytes_from_db: failed for slot={}: {}", + &self.session_id().to_hex_string()[..8], + candidate_id.slot, + e ); + Vec::new() } - }); + } + } + + /// Build and send CandidateAndCert response. + fn send_candidate_and_cert_response( + candidate_bytes: Vec, + notar_bytes: Vec, + response_callback: crate::QueryResponseCallback, + ) { + use consensus_common::ConsensusCommonFactory; + + let response = + CandidateAndCert { candidate: candidate_bytes.into(), notar: notar_bytes.into() }; + + let result = match serialize_boxed(&response.into_boxed()) { + Ok(bytes) => Ok(ConsensusCommonFactory::create_block_payload(bytes)), + Err(e) => Err(error!("Failed to serialize fallback response: {}", e)), + }; + response_callback(result); + } + + /// Send empty CandidateAndCert response (when fallback has nothing to return). + fn send_empty_candidate_response(response_callback: crate::QueryResponseCallback) { + Self::send_candidate_and_cert_response(Vec::new(), Vec::new(), response_callback); + } + + /// Reconstruct CandidateData::Consensus_Empty bytes from CandidateInfoRecord. + fn reconstruct_empty_candidate_data_from_info( + &self, + candidate_id: &RawCandidateId, + candidate_info: &crate::database::CandidateInfoRecord, + ) -> Result> { + let parent_id = match &candidate_info.candidate_hash_data { + CandidateHashData::Consensus_CandidateHashDataEmpty(empty) => { + let slot = SlotIndex(empty.parent.slot as u32); + let hash = empty.parent.hash.clone(); + (slot, hash) + } + _ => return Err(error!("Expected empty hash data")), + }; + + let block_id = if let Some(rc) = self.received_candidates.get(candidate_id) { + rc.block_id.clone() + } else { + return Err(error!( + "Cannot reconstruct empty block: no block_id available for slot={}", + candidate_id.slot + )); + }; + + let parent = + CandidateId { slot: parent_id.0.value() as i32, hash: parent_id.1 }.into_boxed(); + + let tl_empty = CandidateDataEmpty { + slot: candidate_id.slot.value() as i32, + parent, + block: block_id, + signature: candidate_info.signature.clone(), + }; + + let candidate_data = CandidateData::Consensus_Empty(tl_empty); + serialize_boxed(&candidate_data) + .map_err(|e| error!("Failed to serialize empty CandidateData: {}", e)) } } @@ -9157,6 +9601,7 @@ impl SessionStartupRecoveryListener for SessionProcessor { let mut dropped_finalized = 0u32; let mut dropped_skipped = 0u32; let mut dropped_notarization = 0u32; + let mut dropped_skip_cert_reached = 0u32; let mut dropped_finalization_reached = 0u32; while let Some(event) = self.simplex_state.pull_event() { @@ -9174,11 +9619,9 @@ impl SessionStartupRecoveryListener for SessionProcessor { dropped_notarization += 1; } SimplexEvent::SkipCertificateReached(_) => { - // Skip certificate events during recovery - they'll be regenerated - dropped_skipped += 1; + dropped_skip_cert_reached += 1; } SimplexEvent::FinalizationReached(_) => { - // Finalization reached events during recovery - they'll be regenerated dropped_finalization_reached += 1; } } @@ -9187,6 +9630,7 @@ impl SessionStartupRecoveryListener for SessionProcessor { log::info!( "Session {}: drained startup events: kept {} votes, dropped {dropped_finalized} \ finalized, {dropped_skipped} skipped, {dropped_notarization} notarization, \ + {dropped_skip_cert_reached} skip_cert_reached, \ {dropped_finalization_reached} finalization_reached", self.session_id().to_hex_string(), kept_votes.len(), @@ -9342,7 +9786,7 @@ impl SessionStartupRecoveryListener for SessionProcessor { ) { log::info!( target: "startup_recovery", - "Session {}: CERT-1 last finalized notification: slot={}, seqno={}, hash={}", + "Session {}: last finalized notification on restart: slot={}, seqno={}, hash={}", self.session_id().to_hex_string(), slot.value(), seqno, @@ -9376,6 +9820,7 @@ impl SessionStartupRecoveryListener for SessionProcessor { self.last_committed_seqno = Some(seqno); self.last_committed_slot = Some(slot); self.last_committed_block_id = Some(block_id.clone()); + self.last_consensus_finalized_seqno = Some(seqno); // Note: We do NOT set available_base here anymore. This is now done in // recovery_finalize_parent_chain() after all kept votes are restored, @@ -9672,43 +10117,6 @@ impl SessionStartupRecoveryListener for SessionProcessor { ); } - fn recovery_notify_get_approved_candidate( - &self, - source: crate::PublicKey, - root_hash: crate::BlockHash, - file_hash: crate::BlockHash, - collated_data_hash: crate::BlockHash, - callback: Box) + Send>, - ) { - log::trace!( - "Session {}: recovery_notify_get_approved_candidate(root_hash={})", - self.session_id().to_hex_string(), - root_hash.to_hex_string() - ); - // IMPORTANT (restart / wait_for_db_init): - // - // Startup recovery may need to synchronously fetch approved candidates (Step 10 / Step 11) - // while `SessionImpl::create()` is still blocked waiting for initialization to complete. - // In that mode, the callbacks thread is NOT started yet, so posting into the callbacks - // queue would deadlock (startup recovery waits for the callback result). - // - // Therefore, call the session listener directly here (bypassing callback queue). - // This is only used by startup recovery via `SessionStartupRecoveryListener`. - if let Some(listener) = self.listener.upgrade() { - listener.get_approved_candidate( - source, - root_hash, - file_hash, - collated_data_hash, - callback, - ); - } else { - callback(Err(error!( - "recovery_notify_get_approved_candidate: session listener dropped" - ))); - } - } - fn recovery_apply_restart_recommit_actions( &mut self, actions: &[RestartRoundAction], diff --git a/src/node/simplex/src/simplex_state.rs b/src/node/simplex/src/simplex_state.rs index 1985918..07b91c7 100644 --- a/src/node/simplex/src/simplex_state.rs +++ b/src/node/simplex/src/simplex_state.rs @@ -6,6 +6,7 @@ * * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ + //! SimplexState - Core Consensus State Machine //! //! This module implements the core consensus state machine based on: @@ -256,6 +257,16 @@ use crate::{ session_description::SessionDescription, RawVoteData, ValidatorWeight, }; + +/// Maximum number of slots ahead of `first_non_finalized_slot` that the FSM +/// will accept. Any vote, candidate, or certificate referencing a slot beyond +/// this horizon is rejected to prevent a Byzantine validator from triggering +/// unbounded window/slot allocation (DoS). +/// +/// Note: C++ has no equivalent cap. This is a Rust-only defense-in-depth measure. +/// 10,000 is generous enough to never affect liveness under normal conditions. +pub const MAX_FUTURE_SLOTS: u32 = 10_000; + use std::{ cmp, collections::{BinaryHeap, HashMap, HashSet, VecDeque}, @@ -266,12 +277,6 @@ use std::{ }; use ton_block::{error, fail, BlockIdExt, Result, UInt256}; -/// Maximum number of slots ahead of `first_non_finalized_slot` that the FSM -/// will accept. Any vote, candidate, or certificate referencing a slot beyond -/// this horizon is rejected to prevent a Byzantine validator from triggering -/// unbounded window/slot allocation (DoS). C++ parity: commit 4c5eda7c. -pub const MAX_FUTURE_SLOTS: u32 = 10_000; - /* ============================================================================ SimplexState Options @@ -398,12 +403,6 @@ impl SimplexStateOptions { /// Reference: C++ pool.cpp TooManyFallbackVotesMisbehaviorProof (>3 = misbehavior) const MAX_NOTAR_FALLBACK_VOTES_PER_VALIDATOR: usize = 3; -/// Default timeout for first block in window -const DEFAULT_FIRST_BLOCK_TIMEOUT: Duration = Duration::from_secs(3); - -/// Default target rate between blocks -const DEFAULT_TARGET_RATE_TIMEOUT: Duration = Duration::from_secs(1); - /* ============================================================================ Vote Types for FSM @@ -580,12 +579,6 @@ pub struct NotarizationReachedEvent { pub block_hash: UInt256, /// Notarization certificate with signatures pub certificate: NotarCertPtr, - /// Whether SessionProcessor should broadcast the full certificate to peers. - /// - /// Some notarization certificates are learned via direct query/repair paths - /// (e.g. requestCandidate response). In such cases we still need to update - /// local caches/DB, but re-broadcasting may be undesirable. - pub should_broadcast: bool, } /// Event: Skip certificate threshold reached for a slot @@ -602,8 +595,6 @@ pub struct SkipCertificateReachedEvent { pub slot: SlotIndex, /// Skip certificate with signatures pub certificate: SkipCertPtr, - /// Whether SessionProcessor should broadcast the full certificate to peers. - pub should_broadcast: bool, } /// Event: Finalization threshold reached for a block @@ -625,12 +616,6 @@ pub struct FinalizationReachedEvent { pub block_hash: UInt256, /// Finalization certificate with signatures pub certificate: FinalCertPtr, - /// Whether SessionProcessor should broadcast the full certificate to peers. - /// - /// C++ parity (commit `cfd8850c`): `handle_our_certificate()` broadcasts - /// locally-created certificates. Set to `true` for local creation, `false` - /// for external ingest (avoids relay amplification). - pub should_broadcast: bool, } /// Events produced by SimplexState @@ -642,7 +627,7 @@ pub struct FinalizationReachedEvent { /// - `SlotSkipped` โ†’ Notify SessionListener::on_block_skipped /// - `NotarizationReached` โ†’ Cache serialized notarization certificate in receiver /// - `SkipCertificateReached` โ†’ Broadcast skip certificate to validators (C++ mode only) -/// - `FinalizationReached` โ†’ Cache finalization certificate; broadcast when `should_broadcast` is true +/// - `FinalizationReached` โ†’ Cache finalization certificate and relay to peers #[derive(Clone, Debug)] pub enum SimplexEvent { /// Broadcast a vote to all validators @@ -675,10 +660,8 @@ pub enum SimplexEvent { /// A finalization threshold was reached (certificate created) /// - /// Reference: C++ ConsensusBus::FinalizationObserved / handle_our_certificate - /// Caches finalization certificate for standstill replay; broadcasts for locally-created - /// certificates only (`should_broadcast = true`). Externally ingested certificates are - /// cached without broadcast (`should_broadcast = false`). + /// Reference: C++ ConsensusBus::FinalizationObserved / handle_saved_certificate + /// Caches finalization certificate for standstill replay and relays to peers. FinalizationReached(FinalizationReachedEvent), } @@ -728,6 +711,16 @@ struct Slot { /// - restore local skip state on restart (bootstrap) voted_skip: bool, + /// Have we voted to finalize this slot? + /// + /// Reference: C++ `struct SlotState` in `consensus.cpp` (`voted_final`). + /// + /// This is a **local** flag (our node only). In C++, `alarm()` checks + /// `!voted_final` before voting skip โ€” once a node votes final, it cannot + /// vote skip for that slot. This prevents split-brain deadlocks where some + /// nodes vote skip and others vote final, neither reaching the 67% threshold. + voted_final: bool, + /// Observed notarization certificate for a block /// Alpenglow: BlockNotarized(hash(b)) โˆˆ state[s] observed_notar_certificate: Option, @@ -1409,6 +1402,10 @@ pub(crate) struct SimplexState { /// SimplexState options (fallback protocol, etc.) opts: SimplexStateOptions, + + /// Throttle counter for `ensure_window_exists` rejection warnings. + /// Prevents log flooding when standstill re-broadcasts reference far-future windows. + window_reject_count: u64, } impl SimplexState { @@ -1449,11 +1446,11 @@ impl SimplexState { // Validate parameters at construction time if slots_per_window == 0 { - fail!("SimplexState::new: slots_per_leader_window must be > 0") + fail!("SimplexState::new: slots_per_leader_window must be > 0"); } if num_validators == 0 { - fail!("SimplexState::new: num_validators must be > 0") + fail!("SimplexState::new: num_validators must be > 0"); } log::trace!( @@ -1463,8 +1460,8 @@ impl SimplexState { opts ); - let first_block_timeout = desc.opts().first_block_timeout.max(DEFAULT_FIRST_BLOCK_TIMEOUT); - let target_rate_timeout = desc.opts().target_rate.max(DEFAULT_TARGET_RATE_TIMEOUT); + let first_block_timeout = desc.opts().first_block_timeout; + let target_rate_timeout = desc.opts().target_rate; let mut state = Self { events: VecDeque::new(), @@ -1483,6 +1480,7 @@ impl SimplexState { target_rate_timeout, slots_per_leader_window: slots_per_window, opts, + window_reject_count: 0, }; // Initialize first window with genesis (None) as available base @@ -1558,10 +1556,16 @@ impl SimplexState { let max_slot = self.first_non_finalized_slot.value() + MAX_FUTURE_SLOTS; let max_window = WindowIndex(max_slot / self.slots_per_leader_window + 1); if idx > max_window { - log::warn!( - "SimplexState::ensure_window_exists: REJECTED window {} > max {} (defense-in-depth)", - idx, max_window - ); + self.window_reject_count += 1; + if self.window_reject_count <= 3 || self.window_reject_count % 10000 == 0 { + log::warn!( + "SimplexState::ensure_window_exists: REJECTED window {} > max {} \ + (defense-in-depth, occurrence #{})", + idx, + max_window, + self.window_reject_count, + ); + } return; } @@ -1755,6 +1759,7 @@ impl SimplexState { // C++: slot->state->voted_final = true window.slots[offset].is_voted = true; window.slots[offset].its_over = true; + window.slots[offset].voted_final = true; log::trace!( "SimplexState::mark_slot_voted_on_restart: slot {} marked voted_final=true", slot.value() @@ -2190,21 +2195,25 @@ impl SimplexState { self.skip_timestamp = Some(desc.get_time() + self.first_block_timeout + self.target_rate_timeout); - log::trace!( - "SimplexState::set_timeouts: ({}/{}) scheduling timeout in {:.3}s", + log::warn!( + "SimplexState::set_timeouts: ({}/{}) scheduling timeout in {:.3}s \ + (first_block={:.3}s, target_rate={:.3}s)", self.current_leader_window_idx, self.skip_slot, - (self.first_block_timeout + self.target_rate_timeout).as_secs_f64() + (self.first_block_timeout + self.target_rate_timeout).as_secs_f64(), + self.first_block_timeout.as_secs_f64(), + self.target_rate_timeout.as_secs_f64(), ); } /// Restore default timeouts (reset adaptive backoff) fn restore_default_timeouts(&mut self, desc: &SessionDescription) { - self.target_rate_timeout = desc.opts().target_rate.max(DEFAULT_TARGET_RATE_TIMEOUT); - self.first_block_timeout = desc.opts().first_block_timeout.max(DEFAULT_FIRST_BLOCK_TIMEOUT); + self.target_rate_timeout = desc.opts().target_rate; + self.first_block_timeout = desc.opts().first_block_timeout; log::trace!( - "SimplexState::restore_default_timeouts: reset to first_block={:.3}s, target_rate={:.3}s", + "SimplexState::restore_default_timeouts: reset to first_block={:.3}s, \ + target_rate={:.3}s", self.first_block_timeout.as_secs_f64(), self.target_rate_timeout.as_secs_f64() ); @@ -2281,9 +2290,11 @@ impl SimplexState { // Check if we should skip the timeout: // - Alpenglow (enable_fallback_protocol=true): Check is_voted (any vote blocks skip) - // - C++ compatible (enable_fallback_protocol=false): Check its_over (only finalize blocks skip) + // - C++ compatible (enable_fallback_protocol=false): Check voted_final OR voted_skip // - // C++ alarm() only checks voted_final, allowing skip after notarize. + // C++ alarm() checks voted_final and fires once per window (one-shot alarm). + // Rust process_timeouts fires per-slot, so we must also check voted_skip to + // prevent repeated skip vote broadcasts for the same window. // Reference: C++ consensus.cpp alarm(): if (!affected_slot->voted_final) let should_skip_timeout = { let window = self.get_window(window_idx); @@ -2292,8 +2303,10 @@ impl SimplexState { // Alpenglow: Any vote blocks timeout (Voted โˆˆ state[s]) window.slots[offset].is_voted } else { - // C++: Only finalize blocks timeout - window.slots[offset].its_over + // C++: voted_final or voted_skip blocks timeout. + // C++ alarm is one-shot so only checks voted_final, but Rust fires + // per-slot so we also check voted_skip to avoid re-broadcasting. + window.slots[offset].voted_final || window.slots[offset].voted_skip } } else { continue; @@ -2323,6 +2336,31 @@ impl SimplexState { // Alpenglow: trySkipWindow(s) self.try_skip_window(window_idx); + + // C++ compatibility: skip entire remaining window at once, then BREAK. + // Reference: C++ consensus.cpp alarm() lines 120-133: + // C++ fires alarm once and skips ALL remaining slots in the window, + // then sets timeout_slot_ = window_end and reschedules. + // Between alarm firings, incoming events (NotarizationObserved, + // skip certs from peers) can advance timeout_slot_ past active slots. + // We break after one window to give incoming events a chance to + // advance skip_slot before we vote skip for more slots. + if !self.opts.enable_fallback_protocol { + let window_end_slot = (window_idx + 1) * self.slots_per_leader_window; + if self.skip_slot < window_end_slot { + log::debug!( + "SimplexState::process_timeouts: C++ window skip: \ + advancing skip_slot {} -> {} (window_end)", + self.skip_slot, + window_end_slot + ); + self.skip_slot = window_end_slot; + } + // Schedule next timeout at target_rate from now (not accumulated) + skip_timestamp = desc.get_time() + self.target_rate_timeout; + self.skip_timestamp = Some(skip_timestamp); + break; + } } } } @@ -2353,8 +2391,12 @@ impl SimplexState { let factor = desc.opts().timeout_increase_factor; let max_delay = desc.opts().max_backoff_delay; + // Only back off first_block_timeout, not target_rate_timeout. + // C++ reference (consensus.cpp:98-99) only backs off first_block_timeout_s_, + // keeping target_rate_s_ constant. Backing off target_rate causes the full + // rotation of 16 slots to take 16s instead of 8s, making blocks from remote + // leaders arrive after the skip timeout and preventing finalization. self.first_block_timeout = (self.first_block_timeout.mul_f64(factor)).min(max_delay); - self.target_rate_timeout = (self.target_rate_timeout.mul_f64(factor)).min(max_delay); log::trace!( "{}: ({}/{}) adaptive backoff applied -> first={:.3}s target={:.3}s", @@ -2422,15 +2464,19 @@ impl SimplexState { ); fail!( "SimplexState::on_candidate: invalid leader {} (max={}), dropping candidate for slot {}", - leader, self.num_validators, slot - ) + leader, + self.num_validators, + slot + ); } // Ignore finalized slots (not an error, just skip) if slot < self.first_non_finalized_slot { log::trace!( "SimplexState::on_candidate: ({}/{}) slot already finalized (first_non_finalized={}), ignoring", - window_idx, slot, self.first_non_finalized_slot + window_idx, + slot, + self.first_non_finalized_slot ); return Ok(()); } @@ -2446,42 +2492,33 @@ impl SimplexState { return Ok(()); } - // Validate: non-first slot must have parent - // Reference: C++ handle CandidateReceived - let is_first = desc.is_first_in_window(slot); - if !is_first && candidate.parent_id.is_none() { - log::trace!( - "SimplexState::on_candidate: ({}/{}) non-first slot without parent, MISBEHAVIOR", - window_idx, - slot - ); - fail!( - "SimplexState::on_candidate: MISBEHAVIOR: Dropping candidate {:?} for slot {} which builds upon genesis but is not first in window", - candidate.id, slot - ) + // C++ consensus.cpp: if parent exists, parent_slot must be < candidate_slot + if let Some(ref parent) = candidate.parent_id { + if parent.slot >= slot { + fail!( + "SimplexState::on_candidate: MISBEHAVIOR: parent slot {} >= candidate slot {}", + parent.slot, + slot + ); + } } // Convert parent to CandidateParent for matching - // Note: For first slot in window, parent can be None (genesis) let parent: CandidateParent = candidate .parent_id .as_ref() .map(|p| CandidateParentInfo { slot: p.slot, hash: p.hash.clone() }); // Save candidate hash -> CandidateId mapping for BlockFinalizedEvent - // This allows us to provide full CandidateId (including seqno) when finalizing self.candidate_ids.insert(candidate.id.hash.clone(), candidate.id.clone()); log::trace!( - "SimplexState::on_candidate: slot={}, is_first={}, parent={:?}, calling try_notar", + "SimplexState::on_candidate: slot={}, parent={:?}, calling try_notar", slot, - is_first, parent ); // Alpenglow: if tryNotar(Block(s, hash, hashparent)) then - // Note: We use candidate.id.hash (candidate hash) not block.root_hash - // The candidate hash is what's used in votes and for consensus identification if self.try_notar(desc, slot, &candidate.id.hash, parent.as_ref()) { log::trace!( "SimplexState::on_candidate: ({}/{}) try_notar succeeded, checking pending blocks", @@ -2854,11 +2891,13 @@ impl SimplexState { slot_votes.notarize_weight_by_block.get(&vote.block_hash).copied().unwrap_or(0); let total_weight = desc.get_total_weight(); log::trace!( - "SimplexState::handle_notarize_vote: ({}/{}) {} +{} -> notar={}({:.0}%) n|s={}({:.0}%) for {}:{}", - window_idx, slot, validator_idx, weight, - total_notar, 100.0 * total_notar as f64 / total_weight as f64, - slot_votes.notarize_or_skip_weight, 100.0 * slot_votes.notarize_or_skip_weight as f64 / total_weight as f64, - slot, &vote.block_hash.to_hex_string()[..8] + "SimplexState::handle_notarize_vote: ({window_idx}/{slot}) {validator_idx}+{weight} \ + -> notar={total_notar}({:.0}%) n|s={}({:.0}%) for {}:{}", + 100.0 * total_notar as f64 / total_weight as f64, + slot_votes.notarize_or_skip_weight, + 100.0 * slot_votes.notarize_or_skip_weight as f64 / total_weight as f64, + slot, + &vote.block_hash.to_hex_string()[..8] ); } @@ -2926,8 +2965,8 @@ impl SimplexState { // When allow_skip_after_notarize=false (Alpenglow strict mode): // Skip + Notarize is MISBEHAVIOR (in Alpenglow, once you vote skip // you shouldn't also vote notarize for the same slot) - if let (true, Some(existing_notar)) = (!allow_skip_after_notarize, votes.notarize.as_ref()) - { + if !allow_skip_after_notarize && votes.notarize.is_some() { + let existing_notar = votes.notarize.as_ref().unwrap(); log::trace!( "SimplexState::handle_skip_vote: ({}/{}) {} has notarize, rejecting skip", window_idx, @@ -3224,7 +3263,9 @@ impl SimplexState { if let Some(ref finalize) = votes.finalize { log::trace!( "SimplexState::handle_notar_fallback_vote: ({}/{}) {} has finalize, rejecting notar-fb", - window_idx, slot, validator_idx + window_idx, + slot, + validator_idx ); // Use stored raw bytes from existing finalize vote and new raw bytes for proof let existing_raw = votes.finalize_raw.clone().unwrap_or_default(); @@ -3325,7 +3366,9 @@ impl SimplexState { if let Some(ref finalize) = votes.finalize { log::trace!( "SimplexState::handle_skip_fallback_vote: ({}/{}) {} has finalize, rejecting skip-fb", - window_idx, slot, validator_idx + window_idx, + slot, + validator_idx ); // Use stored raw bytes from existing finalize vote and new raw bytes for proof let existing_raw = votes.finalize_raw.clone().unwrap_or_default(); @@ -3442,7 +3485,10 @@ impl SimplexState { Ok(true) => { log::trace!( "SimplexState::check_thresholds: ({}/{}) cached notarization cert for {}:{}", - window_idx, slot_id, slot_id, &block.to_hex_string()[..8] + window_idx, + slot_id, + slot_id, + &block.to_hex_string()[..8] ); // Emit event for session processor to cache serialized cert in receiver self.push_event_back(SimplexEvent::NotarizationReached( @@ -3450,7 +3496,6 @@ impl SimplexState { slot: slot_id, block_hash: block.clone(), certificate: cert, - should_broadcast: true, }, )); } @@ -3486,9 +3531,14 @@ impl SimplexState { { log::trace!( "SimplexState::check_thresholds: ({}/{}) SAFE_TO_NOTAR {}:{} notar={}({:.0}%) skip+notar={}({:.0}%)", - window_idx, slot_id, slot_id, &block.to_hex_string()[..8], - weight, 100.0 * *weight as f64 / total_weight as f64, - skip_plus_notar_b, 100.0 * skip_plus_notar_b as f64 / total_weight as f64 + window_idx, + slot_id, + slot_id, + &block.to_hex_string()[..8], + weight, + 100.0 * *weight as f64 / total_weight as f64, + skip_plus_notar_b, + 100.0 * skip_plus_notar_b as f64 / total_weight as f64 ); if let Some(sv) = self.slot_votes.get_mut(&slot_id) { @@ -3519,8 +3569,10 @@ impl SimplexState { { log::trace!( "SimplexState::check_thresholds: ({}/{}) SAFE_TO_SKIP n|s={}({:.0}%) max_notar={}", - window_idx, slot_id, - notarize_or_skip_weight, 100.0 * notarize_or_skip_weight as f64 / total_weight as f64, + window_idx, + slot_id, + notarize_or_skip_weight, + 100.0 * notarize_or_skip_weight as f64 / total_weight as f64, max_notarize ); @@ -3574,7 +3626,10 @@ impl SimplexState { } else if is_new_cert { log::trace!( "SimplexState::check_thresholds: ({}/{}) cached finalization cert for {}:{}", - window_idx, slot_id, slot_id, &block.to_hex_string()[..8] + window_idx, + slot_id, + slot_id, + &block.to_hex_string()[..8] ); } @@ -3599,7 +3654,6 @@ impl SimplexState { slot: slot_id, block_hash: block.clone(), certificate, - should_broadcast: true, }, )); } @@ -3610,7 +3664,9 @@ impl SimplexState { if slot_id >= self.first_non_finalized_slot { log::trace!( "SimplexState::check_thresholds: ({}/{}) advancing first_non_finalized to {}", - window_idx, slot_id, slot_id + 1 + window_idx, + slot_id, + slot_id + 1 ); self.first_non_finalized_slot = slot_id + 1; @@ -3620,7 +3676,9 @@ impl SimplexState { log::trace!( "SimplexState::check_thresholds: ({}/{}) advanced first_non_progressed_slot to {} (finalized boundary)", - window_idx, slot_id, self.first_non_progressed_slot + window_idx, + slot_id, + self.first_non_progressed_slot ); } } @@ -3636,7 +3694,8 @@ impl SimplexState { if missing_notar { log::trace!( "SimplexState::check_thresholds: ({}/{}) finalized without prior notarization, recording cert", - window_idx, slot_id + window_idx, + slot_id ); if let Some(s) = self.get_slot_mut(desc, slot_id) { s.observed_notar_certificate = Some(parent_info.clone()); @@ -3673,7 +3732,11 @@ impl SimplexState { log::trace!( "SimplexState::check_thresholds: ({}/{}) triggering ParentReady for {} parent={}:{}", - window_idx, slot_id, next_window_idx, slot_id, &block.to_hex_string()[..8] + window_idx, + slot_id, + next_window_idx, + slot_id, + &block.to_hex_string()[..8] ); // Call on_window_base_ready to handle all the logic: @@ -3761,11 +3824,7 @@ impl SimplexState { if !self.opts.enable_fallback_protocol { if let Some(cert) = skip_cert { self.push_event_back(SimplexEvent::SkipCertificateReached( - SkipCertificateReachedEvent { - slot: slot_id, - certificate: cert, - should_broadcast: true, - }, + SkipCertificateReachedEvent { slot: slot_id, certificate: cert }, )); } } @@ -3789,34 +3848,23 @@ impl SimplexState { self.current_leader_window_idx, self.first_non_progressed_slot ); + // C++ parity: skip certificates do NOT advance first_non_finalized_slot. + // Only finalization advances it (see C++ state.h notify_finalized()). + // However, the progress cursor (first_non_progressed_slot, C++ `now_`) + // DOES advance on skip -- it tracks notarized-or-skipped progress. + // Only advance sequentially to avoid jumping past unresolved earlier slots. + if slot_id == self.first_non_progressed_slot { + self.first_non_progressed_slot = slot_id + 1; + log::trace!( + "SimplexState::check_thresholds: ({window_idx}/{slot_id}) \ + advanced first_non_progressed_slot to {} (skip)", + self.first_non_progressed_slot + ); + } + if self.opts.use_notarized_parent_chain { - // Advance first_non_finalized_slot only sequentially to avoid - // jumping past unresolved earlier slots whose votes would be rejected. - if slot_id == self.first_non_finalized_slot { - log::trace!( - "SimplexState::check_thresholds: ({}/{}) advancing first_non_finalized to {} (skip)", - window_idx, - slot_id, - slot_id + 1 - ); - self.first_non_finalized_slot = slot_id + 1; - } self.advance_leader_window_on_progress_cursor(desc); } else { - // Legacy mode: advance first_non_finalized_slot and progress cursor. - if slot_id >= self.first_non_finalized_slot { - log::trace!( - "SimplexState::check_thresholds: ({}/{}) advancing first_non_finalized to {} (skip)", - window_idx, - slot_id, - slot_id + 1 - ); - self.first_non_finalized_slot = slot_id + 1; - } - if self.first_non_finalized_slot > self.first_non_progressed_slot { - self.first_non_progressed_slot = self.first_non_finalized_slot; - } - // Check if this is the last slot in the window BEFORE cleanup // If so, and if no block was finalized in this window, we need to // propagate the available bases to the next window (including genesis/None) @@ -3868,11 +3916,11 @@ impl SimplexState { self.on_window_base_ready(desc, next_window_idx, parent.clone()) { log::error!( - "SimplexState: SlotSkipped failed to propagate parent {:?} to window {}: {}", - parent, - next_window_idx, - e - ); + "SimplexState: SlotSkipped failed to propagate parent {:?} to window {}: {}", + parent, + next_window_idx, + e + ); } } } @@ -3952,6 +4000,42 @@ impl SimplexState { self.advance_leader_window_on_progress_cursor(desc); } + // C++ compatibility: advance skip timer when NotarCert arrives + // Reference: C++ consensus.cpp lines 228-243 (NotarizationObserved handler) + // When a NotarCert is observed, C++ advances timeout_slot_ to slot+1 and + // reschedules the alarm to now + target_rate. This prevents the skip cascade + // from racing ahead of active block production. + // + // Important: do NOT shrink skip_timestamp below the current scheduled value. + // During the first_block_timeout window, the skip timer is intentionally set + // far in the future to give all nodes time to join the overlay. Setting it to + // now + target_rate here would bypass that protection entirely. + if !self.opts.enable_fallback_protocol { + let next_slot = slot + 1; + if self.skip_slot <= next_slot { + let new_timestamp = desc.get_time() + self.target_rate_timeout; + // Only update skip_timestamp if it would be later than current, + // preserving the first_block_timeout window. + let effective_timestamp = match self.skip_timestamp { + Some(current) if current > new_timestamp => current, + _ => new_timestamp, + }; + log::debug!( + "SimplexState::on_block_notarized: advancing skip timer: \ + skip_slot {} -> {next_slot}, new timeout in {:?}{}", + self.skip_slot, + self.target_rate_timeout, + if effective_timestamp != new_timestamp { + " (preserved first_block_timeout)" + } else { + "" + } + ); + self.skip_slot = next_slot; + self.skip_timestamp = Some(effective_timestamp); + } + } + // Alpenglow: tryFinal(s, hash(b)) self.try_final(desc, slot, &block_hash); } @@ -3994,7 +4078,10 @@ impl SimplexState { // Alpenglow: broadcast NotarFallbackVote(s, hash(b)) log::trace!( "SimplexState::on_safe_to_notar: ({}/{}) broadcasting notar-fb for {}:{}, marking BadWindow", - window_idx, slot, slot, &block_hash.to_hex_string()[..8] + window_idx, + slot, + slot, + &block_hash.to_hex_string()[..8] ); self.broadcast_vote(Vote::NotarizeFallback(NotarizeFallbackVote { slot, block_hash })); @@ -4078,10 +4165,10 @@ impl SimplexState { // Check for potential overflow in window_idx * slots_per_leader_window if window_idx.value().checked_mul(self.slots_per_leader_window).is_none() { fail!( - "SimplexState::on_window_base_ready: w{} would overflow with {} slots/window", - window_idx, + "SimplexState::on_window_base_ready: \ + w{window_idx} would overflow with {} slots/window", self.slots_per_leader_window - ) + ); } // Validate parent slot if present @@ -4090,11 +4177,10 @@ impl SimplexState { let window_start = window_idx.window_start(self.slots_per_leader_window); if parent_info.slot >= window_start { fail!( - "SimplexState::on_window_base_ready: parent s{} >= window start s{} for w{}", - parent_info.slot, - start_slot, - window_idx - ) + "SimplexState::on_window_base_ready: \ + parent s{} >= window start s{start_slot} for w{window_idx}", + parent_info.slot + ); } } @@ -4104,8 +4190,9 @@ impl SimplexState { <= self.first_non_finalized_slot { log::trace!( - "SimplexState::on_window_base_ready: ({}/{}) ignored: window fully finalized (first_non_finalized={})", - window_idx, start_slot, self.first_non_finalized_slot + "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) ignored: \ + window fully finalized (first_non_finalized={})", + self.first_non_finalized_slot ); return Ok(()); } @@ -4113,8 +4200,9 @@ impl SimplexState { // Reject far-future windows (DoS protection) if self.is_slot_too_far_ahead(start_slot) { log::warn!( - "SimplexState::on_window_base_ready: ({}/{}) REJECTED - window too far ahead (max={})", - window_idx, start_slot, self.max_acceptable_slot() + "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) REJECTED - \ + window too far ahead (max={})", + self.max_acceptable_slot() ); return Ok(()); } @@ -4125,9 +4213,8 @@ impl SimplexState { if let Some(window) = self.get_window_mut(window_idx) { let is_new = window.available_bases.insert(parent.clone()); log::trace!( - "SimplexState::on_window_base_ready: ({}/{}) {} parent={} to available_bases (count={})", - window_idx, - start_slot, + "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) \ + {} parent={} to available_bases (count={})", if is_new { "added" } else { "duplicate" }, Self::format_parent(parent.as_ref()), window.available_bases.len() @@ -4143,14 +4230,15 @@ impl SimplexState { .map(|p| CandidateParentInfo { slot: p.slot, hash: p.hash.clone() }); if pending_parent == parent { log::trace!( - "SimplexState::on_window_base_ready: ({}/{}) pending block {} matched parent, queuing for notarization", - window_idx, start_slot, Self::format_block(pending.id.slot, &pending.id.hash) + "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) \ + pending block {} matched parent, queuing for notarization", + Self::format_block(pending.id.slot, &pending.id.hash) ); self.pending_slots.push(PendingSlot(start_slot)); } else { log::trace!( - "SimplexState::on_window_base_ready: ({}/{}) pending block {} has different parent (expected={}, got={})", - window_idx, start_slot, + "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) \ + pending block {} has different parent (expected={}, got={})", Self::format_block(pending.id.slot, &pending.id.hash), Self::format_parent(parent.as_ref()), Self::format_parent(pending_parent.as_ref()) @@ -4300,10 +4388,8 @@ impl SimplexState { let matches_parent = base_known && expected_parent == candidate_parent; log::trace!( - "SimplexState::try_notar: ({}/{}) notarized-parent chain: base_known={} expected_base={} candidate_parent={} matches={}", - window_idx, - slot, - base_known, + "SimplexState::try_notar: ({window_idx}/{slot}) notarized-parent chain: \ + base_known={base_known} expected_base={} candidate_parent={} matches={}", Self::format_parent(expected_parent.as_ref()), Self::format_parent(parent), matches_parent @@ -4420,15 +4506,37 @@ impl SimplexState { slot_state.voted_notar.as_ref().map(|c| c.hash == *block_hash).unwrap_or(false); // Alpenglow: BadWindow โˆ‰ state[s] - let not_bad_window = !slot_state.is_bad_window; + // C++ try_vote_final does NOT check bad_window โ€” it only checks + // voted_skip, voted_final, and voted_notar==notar_cert. + let not_bad_window = if self.opts.enable_fallback_protocol { + !slot_state.is_bad_window + } else { + true // C++ doesn't check bad_window in try_vote_final + }; let not_its_over = !slot_state.its_over; - // C++: do not auto-finalize if we already voted skip for this slot + // C++: do not auto-finalize if we already voted skip for this slot. // Reference: C++ consensus.cpp: `!voted_skip && !voted_final && voted_notar==id` + // Both modes now match C++ strictly: once voted_skip, never finalize. let not_voted_skip = !slot_state.voted_skip; let result = has_notar_cert && voted_notar && not_bad_window && not_its_over && not_voted_skip; + // Log when finalize is blocked specifically by voted_skip (Alpenglow mode only) + if has_notar_cert && voted_notar && !not_voted_skip { + log::warn!( + "SimplexState::try_final: ({}/{}) FINALIZE BLOCKED by voted_skip! \ + cert={} notar={} bad_window={} its_over={} voted_skip={}", + window_idx, + slot, + has_notar_cert, + voted_notar, + slot_state.is_bad_window, + slot_state.its_over, + slot_state.voted_skip, + ); + } + // Only format debug info if trace logging is enabled if log::log_enabled!(log::Level::Trace) { // Build compact flags string @@ -4532,8 +4640,10 @@ impl SimplexState { })); // Alpenglow: state[s] โ† state[s] โˆช {ItsOver} + // C++: slot->state->voted_final = true if let Some(window) = self.get_window_mut(window_idx) { window.slots[offset].its_over = true; + window.slots[offset].voted_final = true; } } } @@ -4570,8 +4680,11 @@ impl SimplexState { // Alpenglow: if Voted โˆ‰ state[k] then !window.slots[i].is_voted } else { - // C++: if !voted_final (its_over in Rust) - !window.slots[i].its_over + // C++: if !voted_final โ€” once this node votes final, it cannot + // vote skip. This prevents split-brain deadlocks where some + // nodes vote skip and others vote final. + // Reference: C++ consensus.cpp alarm(): if (!affected_slot->voted_final) + !window.slots[i].voted_final }; if should_skip { slots_to_skip.push(start_slot + i as u32); @@ -4583,10 +4696,10 @@ impl SimplexState { return; } - if log::log_enabled!(log::Level::Trace) { + { let slots_str: Vec = slots_to_skip.iter().map(|s| format!("{}", s)).collect(); - log::trace!( - "SimplexState::try_skip_window: ({}) skipping {} unvoted slots: [{}]", + log::warn!( + "SimplexState::try_skip_window: ({}) SKIP VOTING for {} slots: [{}]", window_idx, slots_to_skip.len(), slots_str.join(",") @@ -5150,16 +5263,16 @@ impl SimplexState { // window/base/progress tracking updates for finalized slots. if slot < first_non_finalized_slot { log::trace!( - "SimplexState::set_notarize_certificate: slot={} < first_non_finalized={} - stored cert without slot tracking", - slot, - first_non_finalized_slot + "SimplexState::set_notarize_certificate: \ + slot={slot} < first_non_finalized={first_non_finalized_slot} - \ + stored cert without slot tracking" ); return Ok(true); } log::trace!( - "SimplexState::set_notarize_certificate: slot={} block={} - stored certificate with {} signatures", - slot, + "SimplexState::set_notarize_certificate: \ + slot={slot} block={} - stored certificate with {} signatures", &block_hash.to_hex_string()[..8], certificate.signatures.len() ); @@ -5171,11 +5284,13 @@ impl SimplexState { // // This mirrors the threshold-driven path (`check_thresholds_and_trigger`) where // notarization threshold stores the cert and emits NotarizationReached. + // C++ parity (pool.cpp handle_saved_certificate): re-gossip every newly + // accepted certificate regardless of origin. SimplexState deduplication + // (returns Ok(false) for already-stored certs) prevents amplification loops. self.push_event_back(SimplexEvent::NotarizationReached(NotarizationReachedEvent { slot, block_hash: block_hash.clone(), certificate: certificate.clone(), - should_broadcast: false, })); // Trigger the same internal FSM transition as the threshold-driven path. @@ -5233,17 +5348,15 @@ impl SimplexState { } Ok(false) => { log::trace!( - "SimplexState::set_finalize_certificate: slot={} - certificate already exists, skipping", - slot + "SimplexState::set_finalize_certificate: \ + slot={slot} - certificate already exists, skipping" ); return Ok(false); } Err(e) => { log::warn!( - "SimplexState::set_finalize_certificate: slot={} block={} - {}", - slot, - &block_hash.to_hex_string()[..8], - e + "SimplexState::set_finalize_certificate: slot={slot} block={} - {e}", + &block_hash.to_hex_string()[..8] ); return Err(e); } @@ -5254,8 +5367,7 @@ impl SimplexState { let idx = vote_sig.validator_idx; if idx.value() as usize >= sv.votes.len() { log::warn!( - "SimplexState::set_finalize_certificate: invalid validator index {} >= {}", - idx, + "SimplexState::set_finalize_certificate: invalid validator index {idx} >= {}", sv.votes.len() ); continue; @@ -5280,8 +5392,8 @@ impl SimplexState { sv.block_finalized_published = true; log::trace!( - "SimplexState::set_finalize_certificate: slot={} block={} - stored certificate with {} signatures", - slot, + "SimplexState::set_finalize_certificate: \ + slot={slot} block={} - stored certificate with {} signatures", &block_hash.to_hex_string()[..8], certificate.signatures.len() ); @@ -5289,9 +5401,9 @@ impl SimplexState { // For old slots, store cert only (no tracking / no events). if is_old_slot { log::trace!( - "SimplexState::set_finalize_certificate: slot={} < first_non_finalized={} - stored cert without slot tracking", - slot, - first_non_finalized_slot + "SimplexState::set_finalize_certificate: \ + slot={slot} < first_non_finalized={first_non_finalized_slot} - \ + stored cert without slot tracking" ); return Ok(true); } @@ -5308,11 +5420,12 @@ impl SimplexState { block_id, certificate: certificate.clone(), })); + // C++ parity (pool.cpp handle_saved_certificate): re-gossip every newly + // accepted certificate regardless of origin. self.push_event_back(SimplexEvent::FinalizationReached(FinalizationReachedEvent { slot, block_hash: block_hash.clone(), certificate: certificate.clone(), - should_broadcast: false, })); // C++ parity (pool.cpp handle_certificate(FinalCertRef)): @@ -5325,8 +5438,8 @@ impl SimplexState { .unwrap_or(true); if missing_notar_marker { log::trace!( - "SimplexState::set_finalize_certificate: slot={} block={} -> treat FinalCert as notarization for parent-chain tracking (missing marker)", - slot, + "SimplexState::set_finalize_certificate: slot={slot} block={} -> \ + treat FinalCert as notarization for parent-chain tracking (missing marker)", &block_hash.to_hex_string()[..8], ); @@ -5339,8 +5452,8 @@ impl SimplexState { } else { // Should not happen for non-old slots; keep trace only (avoid panic in foreign cert ingestion). log::trace!( - "SimplexState::set_finalize_certificate: slot={} block={} missing notar marker but slot state is missing", - slot, + "SimplexState::set_finalize_certificate: \ + slot={slot} block={} missing notar marker but slot state is missing", &block_hash.to_hex_string()[..8], ); } @@ -5348,10 +5461,10 @@ impl SimplexState { self.propagate_base_after_notarization(desc, parent_info.clone()); log::trace!( - "SimplexState::set_finalize_certificate: slot={} block={} FinalCert-as-notar applied (observed_marker_set={}, first_non_progressed_slot={}, first_non_finalized_slot={})", - slot, + "SimplexState::set_finalize_certificate: slot={slot} block={} \ + FinalCert-as-notar applied (observed_marker_set={observed_marker_set}, \ + first_non_progressed_slot={}, first_non_finalized_slot={})", &block_hash.to_hex_string()[..8], - observed_marker_set, self.first_non_progressed_slot, self.first_non_finalized_slot, ); @@ -5428,8 +5541,8 @@ impl SimplexState { } Ok(false) => { log::trace!( - "SimplexState::set_skip_certificate: slot={} - certificate already exists, skipping", - slot + "SimplexState::set_skip_certificate: \ + slot={slot} - certificate already exists, skipping" ); return Ok(false); } @@ -5476,8 +5589,8 @@ impl SimplexState { sv.slot_skipped_published = true; log::trace!( - "SimplexState::set_skip_certificate: slot={} - stored certificate with {} signatures", - slot, + "SimplexState::set_skip_certificate: \ + slot={slot} - stored certificate with {} signatures", certificate.signatures.len() ); @@ -5494,7 +5607,11 @@ impl SimplexState { // Propagate base after skip (C++ pool.cpp parity) self.propagate_base_after_skip_cert(desc, slot); - // If notarized-parent chain mode is enabled, advance progress cursor + // C++ parity: skip certificates do NOT advance first_non_finalized_slot. + // Only finalization advances it (C++ state.h notify_finalized()). + // The progress cursor (first_non_progressed_slot) DOES advance on skip. + + // Advance progress cursor if self.opts.use_notarized_parent_chain { // Advance first_non_progressed_slot if this slot was blocking progress if slot == self.first_non_progressed_slot { @@ -5508,18 +5625,14 @@ impl SimplexState { // skip certificate is created from votes. self.push_event_back(SimplexEvent::SlotSkipped(SlotSkippedEvent { slot })); - // Emit SkipCertificateReached for standstill caching; suppress broadcast for externally - // provided certificates (see `should_broadcast` flag). + // C++ parity (pool.cpp handle_saved_certificate): re-gossip every newly + // accepted certificate regardless of origin. // // SkipCertificateReached is only relevant in C++-compatible mode // (Alpenglow paper does not require explicit skip certificate broadcast). if !self.opts.enable_fallback_protocol { self.push_event_back(SimplexEvent::SkipCertificateReached( - SkipCertificateReachedEvent { - slot, - certificate: certificate.clone(), - should_broadcast: false, - }, + SkipCertificateReachedEvent { slot, certificate: certificate.clone() }, )); } @@ -5733,6 +5846,39 @@ impl SimplexState { } } + // C++ compatibility: advance skip timer when SkipCert arrives + // Reference: C++ consensus.cpp lines 228-248 (NotarizationObserved handler) + // C++ advances timeout_slot_ on both NotarCert and SkipCert (via LeaderWindowObserved). + // Without this, the Rust skip cascade takes ~27s for 27 slots (1s/slot) while + // C++ processes entire windows at once and advances the timer on each event. + // + // Important: do NOT shrink skip_timestamp below the current scheduled value + // to preserve the first_block_timeout window. + if !self.opts.enable_fallback_protocol { + let next_slot = slot + 1; + if self.skip_slot <= next_slot { + let new_timestamp = desc.get_time() + self.target_rate_timeout; + let effective_timestamp = match self.skip_timestamp { + Some(current) if current > new_timestamp => current, + _ => new_timestamp, + }; + log::debug!( + "SimplexState::propagate_base_after_skip_cert: advancing skip timer: \ + skip_slot {} -> {}, new timeout in {:?}{}", + self.skip_slot, + next_slot, + self.target_rate_timeout, + if effective_timestamp != new_timestamp { + " (preserved first_block_timeout)" + } else { + "" + } + ); + self.skip_slot = next_slot; + self.skip_timestamp = Some(effective_timestamp); + } + } + // Advance progress cursor through any progressed slots self.advance_progress_cursor(desc); @@ -5797,10 +5943,9 @@ impl SimplexState { // Should never happen under normal operation log::error!( - "SimplexState::find_next_nonskipped_slot: exceeded scan limit (MAX_SCAN={}) \ - from slot {} (first_non_finalized={}, first_non_progressed_slot={}, slots_per_window={})", - MAX_SCAN, - slot, + "SimplexState::find_next_nonskipped_slot: \ + exceeded scan limit (MAX_SCAN={MAX_SCAN}) from slot {slot} \ + (first_non_finalized={}, first_non_progressed_slot={}, slots_per_window={})", self.first_non_finalized_slot, self.first_non_progressed_slot, self.slots_per_leader_window @@ -5818,18 +5963,18 @@ impl SimplexState { let now_window = desc.get_window_idx(self.first_non_progressed_slot); if now_window <= self.current_leader_window_idx { log::trace!( - "SimplexState::advance_leader_window_on_progress_cursor: not advancing window (current={}, now_window={}, first_non_progressed_slot={})", + "SimplexState::advance_leader_window_on_progress_cursor: not advancing window \ + (current={}, now_window={now_window}, first_non_progressed_slot={})", self.current_leader_window_idx, - now_window, self.first_non_progressed_slot ); return; } log::trace!( - "SimplexState: first_non_progressed_slot {} crossed into window {}, advancing leader window", + "SimplexState: first_non_progressed_slot {} crossed into window {now_window}, \ + advancing leader window", self.first_non_progressed_slot, - now_window ); // C++ CHECK(base.has_value()) before publishing LeaderWindowObserved(now_, base) @@ -5983,23 +6128,27 @@ impl SimplexState { if !full_dump { // Compact one-line format for trace logs format!( - "SimplexState: {}/{} first_non_finalized={} first_non_progressed={} flags=[{}] notar={}({:.0}%) skip={}({:.0}%) final={}({:.0}%) n|s={}({:.0}%) s|fb={}({:.0}%) th66/33={}({:.0}%)/{}({:.0}%) bases=[{}] voted={} cert={} evts=[{}]", - current_window_idx, - current_slot, + "SimplexState: {current_window_idx}/{current_slot} \ + first_non_finalized={} first_non_progressed={} flags=[{slot_flags}] \ + notar={}({:.0}%) skip={}({:.0}%) final={}({:.0}%) n|s={}({:.0}%) \ + s|fb={}({:.0}%) th66/33={}({:.0}%)/{}({:.0}%) bases=[{bases_list}] \ + voted={voted_notar_short} cert={notar_cert_short} evts=[{events_list}]", self.first_non_finalized_slot, self.first_non_progressed_slot, - slot_flags, - notar_weight, pct(notar_weight), - skip_weight, pct(skip_weight), - final_weight, pct(final_weight), - notar_or_skip, pct(notar_or_skip), - skip_or_fb, pct(skip_or_fb), - threshold_66, pct(threshold_66), - threshold_33, pct(threshold_33), - bases_list, - voted_notar_short, - notar_cert_short, - events_list + notar_weight, + pct(notar_weight), + skip_weight, + pct(skip_weight), + final_weight, + pct(final_weight), + notar_or_skip, + pct(notar_or_skip), + skip_or_fb, + pct(skip_or_fb), + threshold_66, + pct(threshold_66), + threshold_33, + pct(threshold_33) ) } else { // Full multi-line format for debug dumps @@ -6028,18 +6177,20 @@ impl SimplexState { // Current slot weights result.push_str(&format!( - " - {} weights: notar={}({:.1}%), skip={}({:.1}%), final={}({:.1}%), n|s={}({:.1}%), s|fb={}({:.1}%)\n", - current_slot, - notar_weight, pct(notar_weight), - skip_weight, pct(skip_weight), - final_weight, pct(final_weight), - notar_or_skip, pct(notar_or_skip), - skip_or_fb, pct(skip_or_fb) + " - {current_slot} weights: notar={notar_weight}({:.1}%), \ + skip={skip_weight}({:.1}%), final={final_weight}({:.1}%), \ + n|s={notar_or_skip}({:.1}%), s|fb={skip_or_fb}({:.1}%)\n", + pct(notar_weight), + pct(skip_weight), + pct(final_weight), + pct(notar_or_skip), + pct(skip_or_fb) )); // State info result.push_str(&format!( - " - first_non_finalized: {}, first_non_progressed: {}, skip_slot: {}, pending_events: {}\n", + " - first_non_finalized: {}, first_non_progressed: {}, \ + skip_slot: {}, pending_events: {}\n", self.first_non_finalized_slot, self.first_non_progressed_slot, self.skip_slot, diff --git a/src/node/simplex/src/startup_recovery.rs b/src/node/simplex/src/startup_recovery.rs index 261612b..63b4c50 100644 --- a/src/node/simplex/src/startup_recovery.rs +++ b/src/node/simplex/src/startup_recovery.rs @@ -49,28 +49,18 @@ use crate::{ session_description::SessionDescription, simplex_state::Vote, utils::extract_vote_and_signature, - BlockHash, PublicKey, RawVoteData, RestartRecommitStrategy, SessionId, + BlockHash, RawVoteData, RestartRecommitStrategy, SessionId, }; use consensus_common::ValidatorBlockCandidatePtr; use std::{ - collections::HashMap, - sync::{ - mpsc::{channel, RecvTimeoutError}, - Arc, - }, - time::Duration, + collections::{HashMap, HashSet}, + sync::Arc, }; use ton_api::{ deserialize_boxed, serialize_boxed, - ton::{ - consensus::{ - candidatedata::{Block as CandidateDataBlock, Empty as CandidateDataEmpty}, - candidateid::CandidateId, - candidateparent::CandidateParent, - simplex::Vote as TlVoteBoxed, - CandidateData, CandidateHashData, CandidateParent as CandidateParentBoxed, - }, - validator_session::candidate::Candidate, + ton::consensus::{ + candidatedata::Empty as CandidateDataEmpty, candidateid::CandidateId, + simplex::Vote as TlVoteBoxed, CandidateData, CandidateHashData, }, IntoBoxed, }; @@ -120,7 +110,7 @@ pub(crate) enum RestartRoundAction { /// File hash for candidate lookup file_hash: BlockHash, /// Collated data hash for candidate lookup - collated_data_hash: BlockHash, + _collated_data_hash: BlockHash, /// Candidate hash for certificate lookup candidate_hash: CandidateHash, /// Pre-serialized CandidateHashData bytes (for BlockSignaturesVariant::Simplex) @@ -209,7 +199,7 @@ pub(crate) trait SessionStartupRecoveryListener { /// Seed the current round counter from finalized block count. /// - /// CERT-1 equivalent: After restart, `current_round` should reflect the number + /// After restart, `current_round` should reflect the number /// of finalized blocks so the first new block uses the correct round number. /// /// Reference: C++ publishes `BlockFinalized(last, true)` after loading finalized @@ -266,7 +256,7 @@ pub(crate) trait SessionStartupRecoveryListener { candidate_hash_data_bytes: Vec, ); - /// Notify about last finalized block after restart (Phase 6.5a / CERT-1). + /// Notify about last finalized block after restart (Phase 6.5a). /// /// C++ equivalent: `consensus.cpp::load_from_db()` publishes /// `BlockFinalized(last_known_finalized_block, true)` after loading. @@ -344,25 +334,6 @@ pub(crate) trait SessionStartupRecoveryListener { /// historical state from DB, whereas startup votes are freshly generated on restart. fn recovery_restore_receiver_standstill_cache(&mut self, votes: &[VoteRecord]); - // ======================================================================== - // Approved candidate fetch (delegated to SessionListener by SessionProcessor) - // ======================================================================== - - /// Request approved candidate from validator storage. - /// - /// This delegates to `SessionListener::get_approved_candidate` internally. - /// The coordinator uses this to fetch candidate payloads for recommit. - /// - /// Note: This is a non-blocking request; the callback is invoked when ready. - fn recovery_notify_get_approved_candidate( - &self, - source: PublicKey, - root_hash: BlockHash, - file_hash: BlockHash, - collated_data_hash: BlockHash, - callback: Box) + Send>, - ); - // ======================================================================== // Recommit replay (applies existing notify paths internally) // ======================================================================== @@ -426,7 +397,7 @@ pub(crate) struct SessionStartupRecoveryProcessor { session_id: SessionId, /// Session description (for leader key lookup during candidate reconstruction) - description: Arc, + _description: Arc, /// Recovery options (strategy, initial seqno) options: SessionStartupRecoveryOptions, @@ -475,7 +446,7 @@ impl SessionStartupRecoveryProcessor { Self { session_id, - description, + _description: description, options, self_idx, bootstrap: Some(bootstrap), @@ -560,8 +531,8 @@ impl SessionStartupRecoveryProcessor { self.session_id.to_hex_string() ); - // Split bootstrap into session and receiver parts - let (session_boot, receiver_boot) = bootstrap.split(); + // Split bootstrap into session, receiver, and candidate payload parts + let (session_boot, receiver_boot, candidate_payloads) = bootstrap.split(); // Step 1: Replay ALL votes (global pass - restores weights, certificates) log::debug!( @@ -670,7 +641,11 @@ impl SessionStartupRecoveryProcessor { "Session {}: step 10/12 - restoring candidate bytes cache", self.session_id.to_hex_string() ); - self.restore_candidate_cache(listener, &session_boot.finalized_blocks)?; + self.restore_candidate_cache( + listener, + &session_boot.finalized_blocks, + &candidate_payloads, + )?; // Step 10b: Rebuild receiver standstill caches (votes + cert bundles + last_final_cert) // @@ -1038,7 +1013,7 @@ impl SessionStartupRecoveryProcessor { ); } - /// Notify about the last finalized block (Phase 6.5a / CERT-1). + /// Notify about the last finalized block (Phase 6.5a). /// /// C++ equivalent: `consensus.cpp::load_from_db()` publishes /// `BlockFinalized(last_known_finalized_block, true)` after loading. @@ -1065,7 +1040,7 @@ impl SessionStartupRecoveryProcessor { log::info!( target: LOG_TARGET, - "Session {}: CERT-1 notifying last finalized block: slot={}, seqno={}, hash={}", + "Session {}: notifying last finalized block on restart: slot={}, seqno={}, hash={}", self.session_id.to_hex_string(), slot.value(), seqno, @@ -1077,7 +1052,7 @@ impl SessionStartupRecoveryProcessor { None => { log::debug!( target: LOG_TARGET, - "Session {}: no is_final=true block found, skipping CERT-1 notification", + "Session {}: no is_final=true block found, skipping last-finalized-cert notification", self.session_id.to_hex_string() ); } @@ -1089,38 +1064,46 @@ impl SessionStartupRecoveryProcessor { /// For each finalized block, reconstructs the CandidateData bytes and caches /// them so `requestCandidate(want_candidate=true)` queries can be answered. /// - /// Reference: C++ candidate-resolver.cpp loads candidate bytes from DB or - /// fetches via `get_approved_candidate` when needed. + /// Reference: C++ candidate-resolver.cpp loads full candidate bytes from its + /// own consensus DB. The Rust implementation only reconstructs empty blocks + /// from metadata; non-empty blocks are skipped and will be resolved via peer + /// overlay when requested. /// /// # Empty vs Non-empty blocks /// /// - **Empty blocks**: Reconstruct `CandidateData::Consensus_Empty` from FinalizedBlockRecord /// (block_id, parent info, signature from leader) - /// - **Non-empty blocks**: Fetch approved candidate via `recovery_notify_get_approved_candidate`, - /// then reconstruct `CandidateData::Consensus_Block` + /// - **Non-empty blocks**: Skipped (will be served from in-memory cache during + /// normal operation, or peers will query other validators) fn restore_candidate_cache( &self, listener: &mut dyn SessionStartupRecoveryListener, finalized_blocks: &[FinalizedBlockRecord], + candidate_payloads: &[(RawCandidateId, Vec)], ) -> Result<()> { - if finalized_blocks.is_empty() { - log::debug!( - target: LOG_TARGET, - "Session {}: no finalized blocks, skipping candidate cache restore", - self.session_id.to_hex_string() - ); - return Ok(()); - } - - let mut restored = 0u32; + let mut restored_empty = 0u32; + let mut restored_payload = 0u32; let mut skipped = 0u32; let mut errors = 0u32; + // 1. Restore from persisted candidate payloads (both empty and non-empty). + // C++ parity: CandidateResolver loads full candidate bytes from DB. + let payload_ids: HashSet<_> = candidate_payloads.iter().map(|(id, _)| id.clone()).collect(); + for (id, bytes) in candidate_payloads { + listener.recovery_cache_candidate_bytes(id.slot, id.hash.clone(), bytes.clone()); + restored_payload += 1; + } + + // 2. For finalized empty blocks not already covered by payloads, + // reconstruct from metadata (backward-compat for DBs without payloads). for block in finalized_blocks { let slot = block.candidate_id.slot; let candidate_hash = &block.candidate_id.hash; - // Look up candidate info for this block + if payload_ids.contains(&block.candidate_id) { + continue; + } + let candidate_info = match self.candidate_info_map.get(candidate_hash) { Some(info) => info, None => { @@ -1135,13 +1118,15 @@ impl SessionStartupRecoveryProcessor { } }; - // Determine if this is an empty block by checking candidate_hash_data variant - // Empty blocks use candidateHashDataEmpty, non-empty use candidateHashDataOrdinary let is_empty = Self::is_empty_block_candidate_hash_data(&candidate_info.candidate_hash_data); - let candidate_data_bytes = if is_empty { - // Reconstruct empty block CandidateData + if !is_empty { + skipped += 1; + continue; + } + + let candidate_data_bytes = match self.reconstruct_empty_candidate_data(block, candidate_info) { Ok(bytes) => bytes, Err(e) => { @@ -1155,147 +1140,23 @@ impl SessionStartupRecoveryProcessor { errors += 1; continue; } - } - } else { - // Non-empty blocks: fetch via blocking channel and reconstruct - let leader_idx = ValidatorIndex(candidate_info.leader_idx); - let source = self.description.get_source_public_key(leader_idx).clone(); - - // Extract hashes from candidate_hash_data for fetch - let (root_hash, file_hash, collated_data_hash) = - match Self::extract_hashes_from_candidate_hash_data( - &candidate_info.candidate_hash_data, - ) { - Some(hashes) => hashes, - None => { - log::warn!( - target: LOG_TARGET, - "Session {}: failed to extract hashes from candidate hash data for slot={}", - self.session_id.to_hex_string(), - slot.value() - ); - errors += 1; - continue; - } - }; - - // Create one-shot channel for blocking fetch - let (tx, rx) = channel(); - - log::debug!( - target: LOG_TARGET, - "Session {}: fetching non-empty candidate for slot={}, root_hash={}", - self.session_id.to_hex_string(), - slot.value(), - root_hash.to_hex_string() - ); - - // Request candidate via listener callback - listener.recovery_notify_get_approved_candidate( - source, - root_hash.clone(), - file_hash.clone(), - collated_data_hash.clone(), - Box::new(move |result| { - let _ = tx.send(result); - }), - ); - - // Block waiting for result with timeout - const FETCH_TIMEOUT: Duration = Duration::from_secs(30); - - let fetch_result = match rx.recv_timeout(FETCH_TIMEOUT) { - Ok(Ok(candidate)) => { - log::debug!( - target: LOG_TARGET, - "Session {}: fetched non-empty candidate for slot={}", - self.session_id.to_hex_string(), - slot.value() - ); - // Build validator_session.Candidate TL bytes from stored block data + collated data. - // - // IMPORTANT: CandidateData::Consensus_Block.candidate must contain serialized - // `validator_session.candidate::Candidate` bytes (C++ RawCandidate::serialize()). - // The approved candidate storage returns raw block data + collated_data, NOT TL candidate bytes. - let candidate_seqno = block.block_id.seq_no() as i32; - let tl_vs_candidate = Candidate { - src: UInt256::default(), - round: candidate_seqno, - root_hash: root_hash.clone(), - data: candidate.data.data().to_vec(), - collated_data: candidate.collated_data.data().to_vec(), - }; - let vs_candidate_bytes = consensus_common::serialize_tl_boxed_object!( - &tl_vs_candidate.into_boxed() - ); - - self.reconstruct_block_candidate_data( - block, - candidate_info, - vs_candidate_bytes, - ) - } - Ok(Err(e)) => { - log::warn!( - target: LOG_TARGET, - "Session {}: candidate fetch failed for slot={}: {}", - self.session_id.to_hex_string(), - slot.value(), - e - ); - Err(e) - } - Err(RecvTimeoutError::Timeout) => { - log::warn!( - target: LOG_TARGET, - "Session {}: candidate fetch TIMEOUT for slot={} ({}s)", - self.session_id.to_hex_string(), - slot.value(), - FETCH_TIMEOUT.as_secs() - ); - Err(error!("candidate fetch timeout for slot {}", slot.value())) - } - Err(RecvTimeoutError::Disconnected) => { - log::error!( - target: LOG_TARGET, - "Session {}: candidate fetch channel disconnected for slot={}", - self.session_id.to_hex_string(), - slot.value() - ); - Err(error!("candidate fetch channel disconnected")) - } }; - match fetch_result { - Ok(bytes) => bytes, - Err(e) => { - log::warn!( - target: LOG_TARGET, - "Session {}: failed to reconstruct block candidate for slot={}: {}", - self.session_id.to_hex_string(), - slot.value(), - e - ); - errors += 1; - continue; - } - } - }; - - // Cache the reconstructed candidate bytes listener.recovery_cache_candidate_bytes( slot, candidate_hash.clone(), candidate_data_bytes, ); - restored += 1; + restored_empty += 1; } log::info!( target: LOG_TARGET, - "Session {}: restored {} candidate bytes to cache ({} skipped, {} errors)", + "Session {}: restored candidate cache: {} from payload DB, {} empty reconstructed, \ + {} skipped, {} errors", self.session_id.to_hex_string(), - restored, + restored_payload, + restored_empty, skipped, errors ); @@ -1397,58 +1258,6 @@ impl SessionStartupRecoveryProcessor { } } - /// Reconstruct CandidateData::Consensus_Block bytes for a non-empty block. - /// - /// Reference: C++ RawCandidate::serialize() for block variant - fn reconstruct_block_candidate_data( - &self, - block: &FinalizedBlockRecord, - candidate_info: &CandidateInfoRecord, - candidate_bytes: Vec, - ) -> Result> { - let slot = block.candidate_id.slot; - - // Get parent info from candidate_hash_data - let parent_opt = - Self::extract_parent_id_from_ordinary_hash_data(&candidate_info.candidate_hash_data)?; - - // Build TL parent structure - let tl_parent = match parent_opt { - Some((parent_slot, parent_hash)) => CandidateParent { - id: CandidateId { slot: parent_slot.value() as i32, hash: parent_hash } - .into_boxed(), - } - .into_boxed(), - None => CandidateParentBoxed::Consensus_CandidateWithoutParents, - }; - - // Use signature from candidate_info (leader's original signature) - let signature = candidate_info.signature.clone(); - - // Build the TL Block structure - let tl_block = CandidateDataBlock { - slot: slot.value() as i32, - candidate: candidate_bytes.into(), - parent: tl_parent, - signature, - }; - - // Wrap in CandidateData enum and serialize - let tl_candidate_data = CandidateData::Consensus_Block(tl_block); - let bytes = serialize_boxed(&tl_candidate_data) - .map_err(|e| error!("Failed to serialize block CandidateData: {}", e))?; - - log::trace!( - target: LOG_TARGET, - "Session {}: reconstructed block candidate data for slot={}, size={}", - self.session_id.to_hex_string(), - slot.value(), - bytes.len() - ); - - Ok(bytes) - } - /// Build and apply restart recommit actions. fn apply_restart_recommit( &self, @@ -1475,111 +1284,21 @@ impl SessionStartupRecoveryProcessor { self.options.restart_recommit_strategy ); - // Pre-fetch candidates for non-empty replay actions. - // We do this before calling recovery_apply_restart_recommit_actions to avoid borrow conflicts - let mut prefetched: HashMap> = HashMap::new(); - - for action in &actions { - let RestartRoundAction::Commit { - slot, - leader_idx, - root_hash, - file_hash, - collated_data_hash, - is_empty, - .. - } = action; - - if *is_empty { - continue; - } - - let source = self.description.get_source_public_key(*leader_idx).clone(); - - // Create one-shot channel for blocking fetch - let (tx, rx) = channel(); - - log::debug!( - target: LOG_TARGET, - "Session {}: fetching candidate for slot={}, root_hash={}", - self.session_id.to_hex_string(), - slot.value(), - root_hash.to_hex_string() - ); - - // Request candidate via listener callback - listener.recovery_notify_get_approved_candidate( - source, - root_hash.clone(), - file_hash.clone(), - collated_data_hash.clone(), - Box::new(move |result| { - // Send result through channel (ignore send error if receiver dropped) - let _ = tx.send(result); - }), - ); - - // Block waiting for result with timeout - // Use a generous timeout (30s) for validator storage fetch - const FETCH_TIMEOUT: Duration = Duration::from_secs(30); - - let result = match rx.recv_timeout(FETCH_TIMEOUT) { - Ok(Ok(candidate)) => { - log::debug!( - target: LOG_TARGET, - "Session {}: fetched candidate for slot={}", - self.session_id.to_hex_string(), - slot.value() - ); - Ok(candidate) - } - Ok(Err(e)) => { - log::warn!( - target: LOG_TARGET, - "Session {}: candidate fetch failed for slot={}: {}", - self.session_id.to_hex_string(), - slot.value(), - e - ); - Err(e) - } - Err(RecvTimeoutError::Timeout) => { - log::warn!( - target: LOG_TARGET, - "Session {}: candidate fetch TIMEOUT for slot={} ({}s)", - self.session_id.to_hex_string(), - slot.value(), - FETCH_TIMEOUT.as_secs() - ); - Err(error!("candidate fetch timeout for slot {}", slot.value())) - } - Err(RecvTimeoutError::Disconnected) => { - log::error!( - target: LOG_TARGET, - "Session {}: candidate fetch channel disconnected for slot={}", - self.session_id.to_hex_string(), - slot.value() - ); - Err(error!("candidate fetch channel disconnected")) - } - }; - - prefetched.insert(*slot, result); - } - - // Apply actions through listener using prefetched candidates + // Apply actions through listener. + // Non-empty blocks cannot be fetched (no get_approved_candidate delegation in + // simplex -- C++ resolves candidates from its own DB, not validator manager). + // The get_candidate closure returns an error for non-empty blocks, causing + // the replay to skip them gracefully. listener.recovery_apply_restart_recommit_actions(&actions, &mut |action| { let RestartRoundAction::Commit { slot, is_empty, .. } = action; if *is_empty { fail!("fetch called for empty replay action at slot {}", slot.value()); } - // Look up prefetched candidate - match prefetched.remove(slot) { - Some(Ok(candidate)) => Ok(candidate), - Some(Err(e)) => Err(error!("prefetch failed: {}", e)), - None => Err(error!("no prefetched candidate for slot {}", slot.value())), - } + Err(error!( + "non-empty block candidate fetch not supported in simplex recovery (slot {})", + slot.value() + )) })?; Ok(()) @@ -1684,7 +1403,7 @@ impl SessionStartupRecoveryProcessor { leader_idx: ValidatorIndex(candidate_info.leader_idx), root_hash, file_hash, - collated_data_hash, + _collated_data_hash: collated_data_hash, candidate_hash, candidate_hash_data_bytes, is_empty, diff --git a/src/node/simplex/src/tests/test_block.rs b/src/node/simplex/src/tests/test_block.rs index 6a6518f..ec30eca 100644 --- a/src/node/simplex/src/tests/test_block.rs +++ b/src/node/simplex/src/tests/test_block.rs @@ -21,6 +21,7 @@ use crate::{ }, PublicKey, }; +use std::collections::HashMap; use ton_block::{BlockIdExt, Ed25519KeyOption, ShardIdent, UInt256}; /* @@ -166,8 +167,6 @@ fn test_slot_index_window_calculations() { /// Test SlotIndex hashing (for use in HashMap) #[test] fn test_slot_index_hash() { - use std::collections::HashMap; - let mut map: HashMap = HashMap::new(); map.insert(SlotIndex::new(0), "slot_0"); map.insert(SlotIndex::new(42), "slot_42"); @@ -262,8 +261,6 @@ fn test_window_index_slot_calculations() { /// Test WindowIndex hashing (for use in HashMap) #[test] fn test_window_index_hash() { - use std::collections::HashMap; - let mut map: HashMap = HashMap::new(); map.insert(WindowIndex::new(0), "window_0"); map.insert(WindowIndex::new(42), "window_42"); @@ -339,8 +336,6 @@ fn test_validator_index_conversions() { /// Test ValidatorIndex hashing (for use in HashMap) #[test] fn test_validator_index_hash() { - use std::collections::HashMap; - let mut map: HashMap = HashMap::new(); map.insert(ValidatorIndex::new(0), "validator_0"); map.insert(ValidatorIndex::new(42), "validator_42"); diff --git a/src/node/simplex/src/tests/test_candidate_resolver.rs b/src/node/simplex/src/tests/test_candidate_resolver.rs index 9fa3c6a..13e47b7 100644 --- a/src/node/simplex/src/tests/test_candidate_resolver.rs +++ b/src/node/simplex/src/tests/test_candidate_resolver.rs @@ -11,7 +11,11 @@ //! These tests verify the `CandidateResolverCache` correctly stores and retrieves //! candidate data and notarization certificates for responding to queries. -use crate::{block::SlotIndex, SessionId, SessionNode}; +use crate::{ + block::{SlotIndex, ValidatorIndex}, + SessionId, SessionNode, +}; +use std::time::{Duration, SystemTime}; use ton_api::{ ton::{consensus::overlayid::OverlayId, pub_::publickey::Overlay}, IntoBoxed, @@ -220,3 +224,87 @@ fn test_candidate_resolver_cache_cleanup_all() { assert!(cache.get_candidate(SlotIndex::new(i), &hash).is_none()); } } + +#[test] +fn test_merge_candidate_response_parts_body_then_notar_completes_merge() { + let slot = SlotIndex::new(42); + let block_hash = UInt256::rand(); + let candidate_bytes = vec![1, 2, 3, 4]; + let notar_bytes = vec![9, 8, 7]; + + let mut cache = super::CandidateResolverCache::new(); + let mut state = super::CandidateRequestState { + start_time: SystemTime::now(), + retry_count: 0, + current_timeout: Duration::from_millis(500), + source_idx: ValidatorIndex::new(0), + cached_notar: None, + cached_candidate: None, + }; + + // First partial response: candidate body only -> notar remains missing. + let (merged_candidate_1, merged_notar_1) = super::ReceiverImpl::merge_candidate_response_parts( + &mut cache, + Some(&mut state), + slot, + &block_hash, + &candidate_bytes, + &[], + ); + assert_eq!(merged_candidate_1, candidate_bytes); + assert!( + merged_notar_1.is_empty(), + "body-only partial response must not be considered complete" + ); + assert_eq!(state.cached_candidate.as_ref(), Some(&candidate_bytes)); + assert!(state.cached_notar.is_none()); + + // Second partial response: notar only -> merged output must include cached body + new notar. + let (merged_candidate_2, merged_notar_2) = super::ReceiverImpl::merge_candidate_response_parts( + &mut cache, + Some(&mut state), + slot, + &block_hash, + &[], + ¬ar_bytes, + ); + assert_eq!(merged_candidate_2, candidate_bytes); + assert_eq!(merged_notar_2, notar_bytes); + assert_eq!(state.cached_candidate.as_ref(), Some(&candidate_bytes)); + assert_eq!(state.cached_notar.as_ref(), Some(¬ar_bytes)); +} + +#[test] +fn test_merge_candidate_response_parts_uses_locally_cached_notar() { + let slot = SlotIndex::new(7); + let block_hash = UInt256::rand(); + let candidate_bytes = vec![11, 22, 33]; + let cached_notar = vec![44, 55]; + + let mut cache = super::CandidateResolverCache::new(); + cache.cache_notar_cert(slot, block_hash.clone(), cached_notar.clone()); + + let mut state = super::CandidateRequestState { + start_time: SystemTime::now(), + retry_count: 0, + current_timeout: Duration::from_millis(500), + source_idx: ValidatorIndex::new(1), + cached_notar: None, + cached_candidate: None, + }; + + // No notar in this response, but resolver cache already has one. + let (merged_candidate, merged_notar) = super::ReceiverImpl::merge_candidate_response_parts( + &mut cache, + Some(&mut state), + slot, + &block_hash, + &candidate_bytes, + &[], + ); + assert_eq!(merged_candidate, candidate_bytes); + assert_eq!( + merged_notar, cached_notar, + "candidate-only response should complete when notar already exists in local cache" + ); +} diff --git a/src/node/simplex/src/tests/test_receiver.rs b/src/node/simplex/src/tests/test_receiver.rs index 3519f06..f923745 100644 --- a/src/node/simplex/src/tests/test_receiver.rs +++ b/src/node/simplex/src/tests/test_receiver.rs @@ -53,7 +53,7 @@ use ton_api::{ }, IntoBoxed, }; -use ton_block::{sha256_digest, BlockIdExt, Ed25519KeyOption, Error, ShardIdent, UInt256}; +use ton_block::{error, sha256_digest, BlockIdExt, Ed25519KeyOption, Error, ShardIdent, UInt256}; include!("../../../../common/src/info.rs"); @@ -193,6 +193,20 @@ impl ReceiverListener for TestReceiverListener { certificate ); } + + fn on_candidate_query_fallback( + &self, + _slot: crate::block::SlotIndex, + _block_hash: UInt256, + _want_notar: bool, + response_callback: consensus_common::QueryResponseCallback, + ) { + log::trace!( + "Receiver {} candidate_query_fallback: no-op (test mock)", + self.stats.receiver_idx + ); + response_callback(Err(error!("Not implemented in test mock"))); + } } impl Drop for TestReceiverListener { @@ -237,12 +251,14 @@ impl ReceiverInstance { session_id.clone(), &shard, max_candidate_size, + 0, nodes, &private_key, overlay_manager, listener_weak, Duration::from_secs(10), // standstill_timeout panicked_flag, + false, health_counters, )?; @@ -667,12 +683,14 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_secs(10), panicked_flag0, + false, health_counters0, ) .expect("Failed to create receiver 0"); @@ -727,6 +745,9 @@ fn test_receiver_candidate_resolver() { // Send the broadcast (will be cached in receiver 0's resolver cache) receiver0.send_block_broadcast(slot, candidate_hash.clone(), broadcast); + // requestCandidate currently asks for both candidate+notar. Seed notar in + // resolver cache so late joiners can complete merged CandidateAndCert. + receiver0.cache_notarization_cert(slot, candidate_hash.clone(), vec![0xAA, 0xBB, 0xCC]); log::info!( "Receiver 0 broadcast candidate for slot {} with hash {}", slot, @@ -751,12 +772,14 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_secs(10), panicked_flag1, + false, health_counters1, ) .expect("Failed to create receiver 1"); @@ -769,12 +792,14 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[2], overlay_manager.clone(), Arc::downgrade(&listener2_arc), Duration::from_secs(10), panicked_flag2, + false, health_counters2, ) .expect("Failed to create receiver 2"); @@ -910,12 +935,14 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), + false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 0"); @@ -926,12 +953,14 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), + false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 1"); @@ -1010,12 +1039,14 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), + false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 0"); @@ -1026,12 +1057,14 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), + false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 1"); @@ -1087,12 +1120,14 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), + false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 0"); @@ -1103,12 +1138,14 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { session_id.clone(), &shard, max_candidate_size, + 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), + false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 1"); diff --git a/src/node/simplex/src/tests/test_restart.rs b/src/node/simplex/src/tests/test_restart.rs index 7009bcb..e4439ba 100644 --- a/src/node/simplex/src/tests/test_restart.rs +++ b/src/node/simplex/src/tests/test_restart.rs @@ -32,14 +32,7 @@ use crate::{ utils::sign_vote, RawBuffer, RestartRecommitStrategy, SessionId, SessionNode, SessionOptions, }; -use consensus_common::ConsensusCommonFactory; -use std::{ - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, - }, - time::SystemTime, -}; +use std::{sync::Arc, time::SystemTime}; use ton_api::{ deserialize_boxed, serialize_boxed, ton::{ @@ -49,13 +42,14 @@ use ton_api::{ candidateparent::CandidateParent as TlCandidateParent, CandidateData, CandidateHashData, CandidateId as CandidateIdBoxed, CandidateParent, }, - validator_session::{ - candidate::Candidate as TlCandidate, Candidate as ValidatorSessionCandidate, - }, + validator_session::candidate::Candidate as TlCandidate, }, IntoBoxed, }; -use ton_block::{error, sha256_digest, BlockIdExt, Ed25519KeyOption, Result, ShardIdent, UInt256}; +use ton_block::{ + sha256_digest, BlockIdExt, BocFlags, BocWriter, BuilderData, Ed25519KeyOption, Result, + ShardIdent, UInt256, +}; #[test] fn test_restart_recommit_strategy_default() { @@ -186,6 +180,16 @@ fn make_candidate_hash_data_empty( }) } +/// Create valid BOC bytes from raw data (for tests that need valid BOC input). +fn make_test_boc(data: &[u8], flags: BocFlags) -> Vec { + let mut b = BuilderData::new(); + b.append_raw(data, data.len() * 8).unwrap(); + let cell = b.into_cell().unwrap(); + let mut buf = Vec::new(); + BocWriter::with_flags([cell], flags).unwrap().write(&mut buf).unwrap(); + buf +} + fn make_validator_session_candidate_bytes( round: i32, root_hash: UInt256, @@ -675,17 +679,6 @@ impl SessionStartupRecoveryListener for MockRecoveryListener { self.cached_candidates.push((slot, candidate_hash, candidate_data_bytes)); } - fn recovery_notify_get_approved_candidate( - &self, - _source: crate::PublicKey, - _root_hash: crate::BlockHash, - _file_hash: crate::BlockHash, - _collated_data_hash: crate::BlockHash, - _callback: Box) + Send>, - ) { - panic!("unexpected recovery_notify_get_approved_candidate call in this test"); - } - fn recovery_apply_restart_recommit_actions( &mut self, _actions: &[RestartRoundAction], @@ -709,7 +702,7 @@ fn make_vote_record( let tl_vote = sign_vote(&vote, session_id, key).expect("sign_vote failed"); let serialized = serialize_boxed(&tl_vote).expect("serialize vote failed"); let vote_hash = UInt256::from_slice(&sha256_digest(&serialized)); - VoteRecord { vote_hash, data: serialized.into(), node_idx } + VoteRecord { vote_hash, data: serialized.into(), node_idx, seqno: 0 } } #[test] @@ -766,6 +759,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() notar_certs, votes: votes.clone(), pool_state, + candidate_payloads: vec![], }; let proc = SessionStartupRecoveryProcessor::new(session_id, desc, options, bootstrap); @@ -777,7 +771,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() proc.apply_bootstrap(&mut listener).expect("apply_bootstrap failed"); // ------------------------------------------------------------------------ - // Ordering invariants (Phase 6.6 + CERT-1 sequencing) + // Ordering invariants (Phase 6.6 + last-finalized-cert sequencing) // ------------------------------------------------------------------------ // // 1) All votes must be replayed BEFORE setting finalized boundary (Phase 6.6) @@ -807,7 +801,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() "Local flags must be applied after finalized boundary is set" ); - // 3) CERT-1 notification must happen AFTER seeding finalized tracking set + // 3) Last-finalized-cert notification must happen AFTER seeding finalized tracking set let last_seed_final_pos = listener .call_log .iter() @@ -820,10 +814,10 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() .expect("expected NotifyLastFinalized call"); assert!( last_seed_final_pos < cert1_pos, - "CERT-1 notification must happen after seeding finalized blocks set" + "Last-finalized-cert notification must happen after seeding finalized blocks set" ); - // 4) Standstill cache rebuild must happen after CERT-1. + // 4) Standstill cache rebuild must happen after last-finalized-cert notification. let standstill_pos = listener .call_log .iter() @@ -831,7 +825,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() .expect("expected RestoreStandstillCache call"); assert!( cert1_pos < standstill_pos, - "Standstill cache rebuild must happen after CERT-1 notification" + "Standstill cache rebuild must happen after last-finalized-cert notification" ); // Step 1: global replay @@ -904,6 +898,7 @@ fn test_apply_bootstrap_does_not_generate_skip_votes_when_first_nonannounced_win notar_certs: vec![], votes: vec![], pool_state: Some(PoolStateRecord { first_nonannounced_window: WindowIndex::new(0) }), + candidate_payloads: vec![], }; let desc = create_test_desc(); @@ -939,19 +934,10 @@ fn test_apply_bootstrap_does_not_generate_skip_votes_when_first_nonannounced_win // Candidate bytes cache restoration: TL roundtrip + invariants // ============================================================================ -/// Listener that supports non-empty candidate fetch and captures cached CandidateData bytes. +/// Listener that captures cached CandidateData bytes during candidate cache restoration. #[derive(Default)] struct CandidateCacheListener { - // Observations cached_candidates: Vec<(SlotIndex, UInt256, Vec)>, - fetched_non_empty: AtomicU32, - - // Expectations for non-empty fetch request - expected_root_hash: UInt256, - expected_file_hash: UInt256, - expected_collated_file_hash: UInt256, - candidate_block_data: Vec, - candidate_collated_data: Vec, } impl SessionStartupRecoveryListener for CandidateCacheListener { @@ -1038,37 +1024,6 @@ impl SessionStartupRecoveryListener for CandidateCacheListener { self.cached_candidates.push((slot, candidate_hash, candidate_data_bytes)); } - fn recovery_notify_get_approved_candidate( - &self, - source: crate::PublicKey, - root_hash: crate::BlockHash, - file_hash: crate::BlockHash, - collated_data_hash: crate::BlockHash, - callback: Box) + Send>, - ) { - // This must be called exactly for non-empty candidates only - self.fetched_non_empty.fetch_add(1, Ordering::SeqCst); - - assert_eq!(root_hash, self.expected_root_hash, "unexpected root_hash requested"); - assert_eq!(file_hash, self.expected_file_hash, "unexpected file_hash requested"); - assert_eq!( - collated_data_hash, self.expected_collated_file_hash, - "unexpected collated_data_hash requested" - ); - - let candidate = consensus_common::ValidatorBlockCandidate { - public_key: source, - id: BlockIdExt::default(), - collated_file_hash: collated_data_hash, - data: ConsensusCommonFactory::create_block_payload(self.candidate_block_data.clone()), - collated_data: ConsensusCommonFactory::create_block_payload( - self.candidate_collated_data.clone(), - ), - }; - - callback(Ok(Arc::new(candidate))); - } - fn recovery_apply_restart_recommit_actions( &mut self, actions: &[RestartRoundAction], @@ -1117,8 +1072,9 @@ fn test_restart_restore_candidate_bytes_roundtrip_empty_and_non_empty() { // ------------------------------------------------------------------------ let non_empty_round_seqno: i32 = 51; // used as block seqno by extract_block_info_from_candidate let non_empty_root_hash = UInt256::from([0x22; 32]); - let non_empty_data = b"block_data_bytes".to_vec(); - let non_empty_collated = b"collated_data_bytes".to_vec(); + // Use valid BOC bytes โ€” compress_candidate_data requires valid BOC input + let non_empty_data = make_test_boc(b"block_data_bytes", BocFlags::all()); + let non_empty_collated = make_test_boc(b"collated_data_bytes", BocFlags::Crc32); let candidate_payload_bytes = make_validator_session_candidate_bytes( non_empty_round_seqno, non_empty_root_hash.clone(), @@ -1132,6 +1088,7 @@ fn test_restart_restore_candidate_bytes_roundtrip_empty_and_non_empty() { Some((parent_id.slot, &parent_id.hash)), &shard, max_size, + 0, ) .expect("compute_candidate_id_hash_from_bytes failed"); let non_empty_candidate_id = RawCandidateId { slot: SlotIndex::new(11), hash: non_empty_hash }; @@ -1190,80 +1147,41 @@ fn test_restart_restore_candidate_bytes_roundtrip_empty_and_non_empty() { notar_certs: vec![], votes: vec![], pool_state: None, + candidate_payloads: vec![], }; let proc = SessionStartupRecoveryProcessor::new(session_id, desc, options, bootstrap); - let mut listener = CandidateCacheListener { - expected_root_hash: non_empty_root_hash.clone(), - expected_file_hash: non_empty_file_hash.clone(), - expected_collated_file_hash: non_empty_collated_file_hash.clone(), - candidate_block_data: non_empty_data.clone(), - candidate_collated_data: non_empty_collated.clone(), - ..Default::default() - }; + let mut listener = CandidateCacheListener::default(); proc.apply_bootstrap(&mut listener).expect("apply_bootstrap failed"); - // Invariant: exactly one non-empty fetch (empty must not trigger fetch) - assert_eq!(listener.fetched_non_empty.load(Ordering::SeqCst), 1); - - // Post-condition: both candidates cached - assert_eq!(listener.cached_candidates.len(), 2); + // Post-condition: only empty candidate cached; non-empty candidates are skipped + // (simplex resolves non-empty candidates via peer overlay, not validator manager) + assert_eq!(listener.cached_candidates.len(), 1); - // Decode and validate cached CandidateData bytes - for (slot, _hash, bytes) in &listener.cached_candidates { - let msg = deserialize_boxed(bytes).expect("deserialize CandidateData"); - let candidate_data = msg.downcast::().expect("downcast CandidateData"); + let (slot, _hash, bytes) = &listener.cached_candidates[0]; + let msg = deserialize_boxed(bytes).expect("deserialize CandidateData"); + let candidate_data = msg.downcast::().expect("downcast CandidateData"); - match candidate_data { - CandidateData::Consensus_Empty(empty) => { - assert_eq!(SlotIndex::new(empty.slot as u32), *slot); - assert_eq!(empty.signature, empty_info.signature); - assert_eq!(empty.block, empty_referenced_block); + match candidate_data { + CandidateData::Consensus_Empty(empty) => { + assert_eq!(SlotIndex::new(empty.slot as u32), *slot); + assert_eq!(empty.signature, empty_info.signature); + assert_eq!(empty.block, empty_referenced_block); - // Parent is a CandidateId (boxed), verify it matches the empty hash data parent - assert_eq!(SlotIndex::new(*empty.parent.slot() as u32), parent_id.slot); - assert_eq!(empty.parent.hash(), &parent_id.hash); - } - CandidateData::Consensus_Block(block) => { - assert_eq!(SlotIndex::new(block.slot as u32), *slot); - assert_eq!(block.signature, non_empty_info.signature); - assert_eq!(block.candidate.as_slice(), candidate_payload_bytes.as_slice()); - - // Nested candidate bytes MUST be validator_session.Candidate - let nested = consensus_common::utils::deserialize_tl_boxed_object::< - ValidatorSessionCandidate, - >(&block.candidate) - .expect("deserialize nested validator_session.Candidate"); - match nested { - ValidatorSessionCandidate::ValidatorSession_Candidate(c) => { - assert_eq!(c.src, UInt256::default()); - assert_eq!(c.round, non_empty_round_seqno); - assert_eq!(c.root_hash, non_empty_root_hash); - assert_eq!(c.data.to_vec(), non_empty_data); - assert_eq!(c.collated_data.to_vec(), non_empty_collated); - } - _ => panic!("unexpected nested Candidate variant"), - } - - // Parent is CandidateParent::CandidateParent with an id - match &block.parent { - CandidateParent::Consensus_CandidateParent(p) => { - assert_eq!(SlotIndex::new(*p.id.slot() as u32), parent_id.slot); - assert_eq!(p.id.hash(), &parent_id.hash); - } - CandidateParent::Consensus_CandidateWithoutParents => { - panic!("expected CandidateParent for non-empty candidate"); - } - } - } + // Parent is a CandidateId (boxed), verify it matches the empty hash data parent + assert_eq!(SlotIndex::new(*empty.parent.slot() as u32), parent_id.slot); + assert_eq!(empty.parent.hash(), &parent_id.hash); + } + CandidateData::Consensus_Block(_) => { + panic!("non-empty block should not be cached during startup recovery"); } } } #[test] -fn test_restart_restore_candidate_bytes_skips_non_empty_on_fetch_error_but_keeps_empty() { +fn test_restart_restore_candidate_bytes_skips_non_empty_and_keeps_empty() { let session_id = SessionId::default(); let options = SessionStartupRecoveryOptions::new(RestartRecommitStrategy::FirstCommitAfterFinalized, 0); @@ -1334,6 +1252,7 @@ fn test_restart_restore_candidate_bytes_skips_non_empty_on_fetch_error_but_keeps notar_certs: vec![], votes: vec![], pool_state: None, + candidate_payloads: vec![], }; let proc = SessionStartupRecoveryProcessor::new(session_id, desc, options, bootstrap); @@ -1414,16 +1333,6 @@ fn test_restart_restore_candidate_bytes_skips_non_empty_on_fetch_error_but_keeps ) { self.cached.push((slot, candidate_hash, candidate_data_bytes)); } - fn recovery_notify_get_approved_candidate( - &self, - _source: crate::PublicKey, - _root_hash: crate::BlockHash, - _file_hash: crate::BlockHash, - _collated_data_hash: crate::BlockHash, - callback: Box) + Send>, - ) { - callback(Err(error!("simulated fetch error"))); - } fn recovery_apply_restart_recommit_actions( &mut self, _actions: &[RestartRoundAction], @@ -1439,7 +1348,7 @@ fn test_restart_restore_candidate_bytes_skips_non_empty_on_fetch_error_but_keeps let mut listener = FailFetchListener::default(); proc.apply_bootstrap(&mut listener).expect("apply_bootstrap failed"); - // Post-condition: empty candidate cached, non-empty skipped due to fetch error + // Post-condition: empty candidate cached, non-empty skipped (not fetched) assert_eq!(listener.cached.len(), 1); assert_eq!(listener.cached[0].0, SlotIndex::new(10)); diff --git a/src/node/simplex/src/tests/test_session_processor.rs b/src/node/simplex/src/tests/test_session_processor.rs index 0e1124f..0600345 100644 --- a/src/node/simplex/src/tests/test_session_processor.rs +++ b/src/node/simplex/src/tests/test_session_processor.rs @@ -15,8 +15,9 @@ use super::*; use crate::{ block::ValidatorIndex, receiver::Receiver, + simplex_state::SimplexStateOptions, task_queue::{CallbackTaskQueuePtr, TaskQueuePtr}, - SessionId, SessionNode, SessionOptions, + SessionId, SessionNode, SessionOptions, SIMPLEX_ROUNDLESS, }; use consensus_common::{ AsyncRequestPtr, BlockPayloadPtr, BlockSourceInfo, CollationParentHint, PublicKey, @@ -28,29 +29,49 @@ use std::{ env, fs, mem, sync::{ atomic::{AtomicBool, Ordering}, + mpsc::channel, Arc, Mutex, }, time::{Duration, SystemTime}, }; use ton_api::{ - ton::consensus::{ - simplex::{ - certificate::Certificate, unsignedvote::SkipVote, vote::Vote as TlVote, - votesignature::VoteSignature, votesignatureset::VoteSignatureSet, - Certificate as CertificateBoxed, VoteSignature as VoteSignatureBoxed, + deserialize_boxed, + ton::{ + consensus::{ + candidatedata::Block as CandidateDataBlock, + simplex::{ + certificate::Certificate, unsignedvote::SkipVote, vote::Vote as TlVote, + votesignature::VoteSignature, votesignatureset::VoteSignatureSet, CandidateAndCert, + Certificate as CertificateBoxed, VoteSignature as VoteSignatureBoxed, + }, + CandidateData, CandidateParent, }, - CandidateData, + validator_session::candidate::Candidate as TlCandidate, }, IntoBoxed, }; use ton_block::{ - error, signature::BlockSignaturesVariant, BlockIdExt, Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, signature::BlockSignaturesVariant, BlockIdExt, BocFlags, BocWriter, + BuilderData, Ed25519KeyOption, ShardIdent, UInt256, }; // ============================================================================ // Test Helpers // ============================================================================ +/// Create valid BOC bytes from raw data (for tests that need valid BOC input). +/// +/// The compress/decompress pipeline requires valid BOC, so mock data must be +/// wrapped in a cell + serialized as BOC with appropriate flags. +fn make_test_boc(data: &[u8], flags: BocFlags) -> Vec { + let mut b = BuilderData::new(); + b.append_raw(data, data.len() * 8).unwrap(); + let cell = b.into_cell().unwrap(); + let mut buf = Vec::new(); + BocWriter::with_flags([cell], flags).unwrap().write(&mut buf).unwrap(); + buf +} + /// Create test validators with equal weights fn create_test_validators(count: u32) -> Vec { (0..count) @@ -113,6 +134,8 @@ enum ReceiverAction { CacheLastFinalCertificate { slot: u32, bytes_len: usize }, /// cleanup() was called Cleanup { up_to_slot: u32 }, + /// request_candidate() was called + RequestCandidate { slot: u32, block_hash: UInt256 }, } /// Mock receiver that records all outbound calls @@ -169,8 +192,11 @@ impl Receiver for MockReceiver { self.actions.lock().unwrap().push_back(ReceiverAction::Cleanup { up_to_slot }); } - fn request_candidate(&self, _slot: u32, _block_hash: UInt256) { - // No-op for tests + fn request_candidate(&self, slot: u32, block_hash: UInt256) { + self.actions + .lock() + .unwrap() + .push_back(ReceiverAction::RequestCandidate { slot, block_hash }); } fn reschedule_standstill(&self) { @@ -635,14 +661,15 @@ fn test_genesis_collation_expected_seqno_uses_initial_block_seqno() { let genesis_block_id = BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); + // Use valid BOC bytes โ€” compress_candidate_data requires valid BOC input + let block_boc = make_test_boc(&[0xAA], BocFlags::all()); + let collated_boc = make_test_boc(&[0xBB], BocFlags::Crc32); let candidate = crate::ValidatorBlockCandidate { public_key: fixture.nodes[0].public_key.clone(), id: genesis_block_id, - collated_file_hash: UInt256::rand(), - data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xAA].into()), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - vec![0xBB].into(), - ), + collated_file_hash: UInt256::from_slice(&sha256_digest(&collated_boc)), + data: consensus_common::ConsensusCommonFactory::create_block_payload(block_boc), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload(collated_boc), }; fixture @@ -706,9 +733,9 @@ fn test_should_generate_empty_block_uses_committed_head_at_session_start() { assert_eq!(processor.last_committed_seqno, Some(46)); // Slot 0 is the initial `first_non_progressed_slot` in fresh state. - // MC: new_seqno=48, committed=46 โ†’ 46+1=47 < 48 โ†’ empty + // MC: new_seqno=48, committed=46 -> 46+1=47 < 48 -> empty assert!(processor.should_generate_empty_block(SlotIndex::new(0), 48)); - // MC: new_seqno=47, committed=46 โ†’ 46+1=47 == 47 โ†’ NOT empty + // MC: new_seqno=47, committed=46 -> 46+1=47 == 47 -> NOT empty assert!(!processor.should_generate_empty_block(SlotIndex::new(0), 47)); } @@ -1029,12 +1056,9 @@ fn test_on_certificate_relays_and_caches_skip_certificate_once() { }) .count(); - // External skip certificates are stored for state consistency + standstill caching, - // but are NOT re-broadcast when ingested externally (should_broadcast=false). - assert_eq!( - send_cert_count, 0, - "expected no send_certificate for externally provided skip cert" - ); + // C++ parity (pool.cpp handle_saved_certificate): every newly accepted + // certificate is relayed once, regardless of origin. + assert_eq!(send_cert_count, 1, "C++ parity: foreign skip cert must be relayed once"); assert_eq!( cache_standstill_count, 1, "expected exactly one cache_standstill_certificate on first apply" @@ -1073,7 +1097,6 @@ fn test_handle_finalization_reached_caches_final_certificate_for_standstill() { slot, block_hash: block_hash.clone(), certificate: cert, - should_broadcast: true, }; fixture.processor.handle_finalization_reached(event); @@ -1130,8 +1153,6 @@ fn test_handle_notarization_reached_requests_missing_candidate_body() { slot, block_hash: block_hash.clone(), certificate: cert, - // Foreign cert ingestion path: do not re-broadcast. - should_broadcast: false, }; // Act: should schedule requestCandidate for missing body. @@ -1246,8 +1267,6 @@ fn test_batch_finalization_notarized_parents_finalized_descendant() { /// Test that SIMPLEX_ROUNDLESS constant is u32::MAX #[test] fn test_simplex_roundless_constant_value() { - use crate::SIMPLEX_ROUNDLESS; - assert_eq!(SIMPLEX_ROUNDLESS, u32::MAX, "SIMPLEX_ROUNDLESS should be u32::MAX"); assert_eq!(SIMPLEX_ROUNDLESS, 0xFFFFFFFF, "SIMPLEX_ROUNDLESS should be 0xFFFFFFFF"); } @@ -1259,8 +1278,6 @@ fn test_simplex_roundless_constant_value() { /// by forcing EMPTY collation on non-committed parents. #[test] fn test_simplex_state_options_require_finalized_parent() { - use crate::simplex_state::SimplexStateOptions; - // Default (cpp_compatible) should have require_finalized_parent=false let cpp_compat = SimplexStateOptions::cpp_compatible(); assert!( @@ -1268,7 +1285,7 @@ fn test_simplex_state_options_require_finalized_parent() { "cpp_compatible() should have require_finalized_parent=false" ); - // With optimistic validation (TN-822), the collation gate is disabled: + // With optimistic validation, the collation gate is disabled: // ValidatorGroup now uses candidate-native validation, so non-finalized parents are allowed. assert!( !DISABLE_NON_FINALIZED_PARENTS_FOR_COLLATION, @@ -1410,7 +1427,7 @@ fn test_candidate_decision_fail_drops_late_failure_for_committed_block() { } // ============================================================================ -// Optimistic Validation Tests (TN-822 / OPTIMISTIC-VALID-1) +// Optimistic Validation Tests // ============================================================================ /// Helper: create a non-empty RawCandidate for check_validation tests. @@ -1712,7 +1729,7 @@ fn test_check_validation_chains_notarized_parent_to_descendant() { } // ============================================================================ -// Health check anomaly tests (NODE-19) +// Health check anomaly tests // ============================================================================ /// Reset health alert timestamps to a deterministic base time so that @@ -1968,3 +1985,436 @@ fn test_check_collation_pacing_gate_is_idempotent() { fixture.processor.check_collation(); assert_eq!(fixture.processor.get_next_awake_time(), gate_time); } + +// ============================================================================ +// Candidate Query Fallback Tests (C++ parity: CandidateResolver DB fallback) +// ============================================================================ + +#[test] +fn test_candidate_query_fallback_cache_hit() { + let mut fixture = TestFixture::new(4); + let slot = SlotIndex::new(5); + let block_hash = UInt256::rand(); + let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; + + let fake_candidate_bytes = vec![0xCA, 0xFE, 0xBA, 0xBE]; + fixture.processor.candidate_data_cache.insert(candidate_id, fake_candidate_bytes.clone()); + + let (tx, rx) = channel(); + let callback: crate::QueryResponseCallback = Box::new(move |result| { + tx.send(result).unwrap(); + }); + + fixture.processor.handle_candidate_query_fallback(slot, block_hash, false, callback); + + let result = rx.recv_timeout(Duration::from_secs(2)).expect("callback not called"); + let payload = result.expect("response should be Ok"); + let response_bytes = payload.data(); + + assert!(!response_bytes.is_empty(), "response should contain serialized CandidateAndCert"); + + let deserialized = deserialize_boxed(response_bytes) + .expect("should deserialize response") + .downcast::() + .expect("should be CandidateAndCert"); + + let inner = match deserialized { + CandidateAndCert::Consensus_Simplex_CandidateAndCert(inner) => inner, + }; + + assert_eq!( + &inner.candidate[..], + &fake_candidate_bytes[..], + "candidate bytes should match the cached data" + ); +} + +#[test] +fn test_candidate_query_fallback_miss_returns_empty() { + let mut fixture = TestFixture::new(4); + let slot = SlotIndex::new(99); + let block_hash = UInt256::rand(); + + let (tx, rx) = channel(); + let callback: crate::QueryResponseCallback = Box::new(move |result| { + tx.send(result).unwrap(); + }); + + fixture.processor.handle_candidate_query_fallback(slot, block_hash, false, callback); + + let result = rx.recv_timeout(Duration::from_secs(5)).expect("callback not called"); + let payload = result.expect("response should be Ok even for empty"); + let response_bytes = payload.data(); + + let deserialized = deserialize_boxed(response_bytes) + .expect("should deserialize response") + .downcast::() + .expect("should be CandidateAndCert"); + + let inner = match deserialized { + CandidateAndCert::Consensus_Simplex_CandidateAndCert(inner) => inner, + }; + + assert!(inner.candidate.is_empty(), "candidate bytes should be empty when not found"); +} + +#[test] +fn test_candidate_data_cache_populated_on_candidate_received() { + let _ = env_logger::Builder::new().filter_level(log::LevelFilter::Debug).try_init(); + let mut fixture = TestFixture::new(4); + + // Use slot 0 so that validator 0 (local) is the slot leader + let slot = 0u32; + let block_data = vec![1u8, 2, 3, 4, 5]; + let collated_data: Vec = vec![]; + let root_hash = UInt256::from_slice(&sha256_digest(&block_data)); + let shard = ShardIdent::masterchain(); + + // Build uncompressed TL candidate (same approach as test_receiver_candidate_resolver) + let tl_inner = TlCandidate { + src: UInt256::default(), + round: slot as i32, + root_hash: root_hash.clone(), + data: block_data.clone().into(), + collated_data: collated_data.clone().into(), + }; + let candidate_bytes = consensus_common::serialize_tl_boxed_object!(&tl_inner.into_boxed()); + + let block_id = BlockIdExt { + shard_id: shard, + seq_no: slot, + root_hash: root_hash.clone(), + file_hash: root_hash.clone(), + }; + let collated_file_hash = UInt256::from_slice(&sha256_digest(&collated_data)); + + let candidate_hash = crate::utils::compute_candidate_id_hash_u32( + slot, + Some(&block_id), + Some(&collated_file_hash), + None, + ); + + let session_id = fixture.processor.session_id().clone(); + let leader_key = fixture.processor.description.get_source_public_key(ValidatorIndex::new(0)); + let signature = + crate::utils::sign_candidate_u32(&session_id, slot, &candidate_hash, leader_key) + .expect("signing failed"); + + let broadcast = CandidateData::Consensus_Block(CandidateDataBlock { + slot: slot as i32, + candidate: candidate_bytes.into(), + parent: CandidateParent::Consensus_CandidateWithoutParents, + signature: signature.into(), + }); + + let candidate_id = RawCandidateId { slot: SlotIndex::new(slot), hash: candidate_hash.clone() }; + + assert!( + !fixture.processor.candidate_data_cache.contains_key(&candidate_id), + "cache should be empty before on_candidate_received" + ); + + fixture.processor.on_candidate_received(0, broadcast, None); + + assert!( + fixture.processor.candidate_data_cache.contains_key(&candidate_id), + "cache should be populated after on_candidate_received" + ); + + assert!( + fixture.processor.received_candidates.contains_key(&candidate_id), + "received_candidates should also have the candidate" + ); +} + +// ============================================================================ +// Protocol Parity Tests (stub body, partial merge, finalized seqno) +// ============================================================================ + +#[test] +fn test_has_real_candidate_body_returns_false_for_stub() { + let mut fixture = TestFixture::new(4); + let slot = SlotIndex::new(10); + let hash = UInt256::rand(); + let candidate_id = RawCandidateId { slot, hash: hash.clone() }; + + // No entry => false + assert!(!fixture.processor.has_real_candidate_body(&candidate_id)); + + // Insert a finalized-boundary stub (empty candidate_hash_data_bytes) + fixture.processor.received_candidates.insert( + candidate_id.clone(), + ReceivedCandidate { + slot, + source_idx: ValidatorIndex::new(0), + candidate_id_hash: hash.clone(), + candidate_hash_data_bytes: Vec::new(), // stub marker + block_id: BlockIdExt::default(), + root_hash: UInt256::default(), + file_hash: UInt256::default(), + data: consensus_common::ConsensusCommonFactory::create_block_payload(Vec::new()), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + Vec::new(), + ), + receive_time: fixture.processor.now(), + is_empty: false, + parent_id: None, + is_fully_resolved: true, + }, + ); + + // Stub => false + assert!( + !fixture.processor.has_real_candidate_body(&candidate_id), + "finalized-boundary stub must NOT count as real body" + ); + + // Overwrite with real data + fixture + .processor + .received_candidates + .get_mut(&candidate_id) + .unwrap() + .candidate_hash_data_bytes = vec![1, 2, 3]; + + // Now should be true + assert!( + fixture.processor.has_real_candidate_body(&candidate_id), + "entry with non-empty candidate_hash_data_bytes must count as real body" + ); +} + +#[test] +fn test_handle_block_finalized_requests_triggered_stub_body_when_committed_head_exists() { + let mut fixture = TestFixture::new(4); + + // Simulate an already-committed head, so triggered finalized block enters + // collect_gapless_commit_chain() as a non-genesis continuation. + fixture.processor.last_committed_seqno = Some(100); + fixture.processor.last_committed_block_id = Some(BlockIdExt::with_params( + ShardIdent::masterchain(), + 100, + UInt256::rand(), + UInt256::rand(), + )); + + let slot = SlotIndex::new(555); + let block_hash = UInt256::rand(); + let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; + let finalized_block_id = + BlockIdExt::with_params(ShardIdent::masterchain(), 101, UInt256::rand(), UInt256::rand()); + + let final_cert = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { slot, block_hash: block_hash.clone() }, + signatures: vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), + ], + }); + + fixture.processor.handle_block_finalized(BlockFinalizedEvent { + slot, + block_hash: block_hash.clone(), + block_id: Some(finalized_block_id), + certificate: final_cert, + }); + + assert!( + fixture.processor.requested_candidates.contains_key(&candidate_id), + "triggered finalized-boundary stub must be treated as missing body and requested" + ); + + // The core regression guard is scheduling requestCandidate at processor level. + // (Receiver send timing is exercised by dedicated delayed-action tests.) +} + +#[test] +fn test_candidate_query_fallback_returns_notar_only_when_body_missing() { + let mut fixture = TestFixture::new(4); + let slot = SlotIndex::new(99); + let block_hash = UInt256::rand(); + + let (tx, rx) = channel(); + let callback: crate::QueryResponseCallback = Box::new(move |result| { + tx.send(result).unwrap(); + }); + + // No candidate in cache or DB => should return empty/empty + fixture.processor.handle_candidate_query_fallback(slot, block_hash, false, callback); + + let result = rx.recv().unwrap(); + assert!(result.is_ok(), "should return Ok even when nothing found"); +} + +#[test] +fn test_set_mc_finalized_seqno_couples_consensus_finalized_seqno() { + let mut fixture = TestFixture::new(4); + + // Initially 0 + assert_eq!(fixture.processor.last_consensus_finalized_seqno, Some(0)); + + // Set MC finalized to 42 + fixture.processor.set_mc_finalized_seqno(42); + + // C++ parity: consensus finalized should advance to max(mc, consensus) + assert_eq!( + fixture.processor.last_consensus_finalized_seqno, + Some(42), + "set_mc_finalized_seqno should couple to last_consensus_finalized_seqno via max()" + ); + + // Set consensus finalized higher via direct field (simulating a final commit) + fixture.processor.last_consensus_finalized_seqno = Some(100); + + // Set MC finalized lower => should NOT decrease consensus + fixture.processor.set_mc_finalized_seqno(50); + assert_eq!( + fixture.processor.last_consensus_finalized_seqno, + Some(100), + "set_mc_finalized_seqno must not decrease last_consensus_finalized_seqno" + ); + + // Monotonic MC seqno: out-of-order MC event with lower seqno must not regress + fixture.processor.last_mc_finalized_seqno = Some(200); + fixture.processor.set_mc_finalized_seqno(150); + assert_eq!( + fixture.processor.last_mc_finalized_seqno, + Some(200), + "set_mc_finalized_seqno must keep last_mc_finalized_seqno monotonic" + ); +} + +// ============================================================================ +// Foreign Certificate Relay Regression Tests (C++ parity) +// ============================================================================ + +/// Verify that a notarization certificate ingested via set_notarize_certificate +/// (foreign path) triggers relay to peers. +#[test] +fn test_foreign_notarization_cert_is_relayed() { + let mut fixture = TestFixture::new(4); + + let slot = crate::block::SlotIndex::new(3); + let block_hash = UInt256::rand(); + + let signatures = vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![10]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![11]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![12]), + ]; + let vote = crate::simplex_state::NotarizeVote { slot, block_hash: block_hash.clone() }; + let cert = Arc::new(crate::certificate::Certificate { vote, signatures }); + + let event = crate::simplex_state::NotarizationReachedEvent { + slot, + block_hash: block_hash.clone(), + certificate: cert, + }; + + fixture.processor.handle_notarization_reached(event); + + let actions = fixture.drain_receiver_actions(); + assert!( + actions.iter().any(|a| matches!(a, ReceiverAction::SendCertificate { .. })), + "foreign notarization cert must be relayed (C++ parity: handle_saved_certificate)" + ); +} + +/// Verify that a finalization certificate ingested via set_finalize_certificate +/// (foreign path) triggers relay to peers. +#[test] +fn test_foreign_finalization_cert_is_relayed() { + let mut fixture = TestFixture::new(4); + + let slot = crate::block::SlotIndex::new(5); + let block_hash = UInt256::rand(); + + let signatures = vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![20]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![21]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![22]), + ]; + let vote = crate::simplex_state::FinalizeVote { slot, block_hash: block_hash.clone() }; + let cert = Arc::new(crate::certificate::Certificate { vote, signatures }); + + let event = crate::simplex_state::FinalizationReachedEvent { + slot, + block_hash: block_hash.clone(), + certificate: cert, + }; + + fixture.processor.handle_finalization_reached(event); + + let actions = fixture.drain_receiver_actions(); + assert!( + actions.iter().any(|a| matches!(a, ReceiverAction::SendCertificate { .. })), + "foreign finalization cert must be relayed (C++ parity: handle_saved_certificate)" + ); +} + +#[test] +fn test_foreign_vote_is_not_rebroadcast() { + let mut fixture = TestFixture::new(4); + + let slot = crate::block::SlotIndex::new(2); + let block_hash = UInt256::from([0xAB; 32]); + let vote = crate::simplex_state::Vote::Notarize(crate::simplex_state::NotarizeVote { + slot, + block_hash, + }); + let tl_vote = crate::utils::sign_vote( + &vote, + fixture.description.get_session_id(), + &fixture.nodes[1].public_key, + ) + .expect("failed to sign foreign vote"); + let raw_vote: crate::RawVoteData = + consensus_common::serialize_tl_boxed_object!(&tl_vote).into(); + + fixture.processor.on_vote(1, tl_vote, raw_vote); + + let actions = fixture.drain_receiver_actions(); + assert!( + !actions.iter().any(|a| matches!(a, ReceiverAction::SendVote { .. })), + "foreign votes must not be re-broadcast" + ); +} + +#[test] +fn test_recovery_drain_startup_events_drops_certificate_relay_events() { + let mut fixture = TestFixture::new(4); + + let slot = crate::block::SlotIndex::new(3); + let block_hash = UInt256::from([0xCD; 32]); + let signatures = vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![1]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![2]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![3]), + ]; + let vote = crate::simplex_state::NotarizeVote { slot, block_hash: block_hash.clone() }; + let cert = Arc::new(crate::certificate::Certificate { vote, signatures }); + let stored = fixture + .processor + .simplex_state + .set_notarize_certificate(&fixture.description, slot, &block_hash, cert) + .expect("set_notarize_certificate should succeed"); + assert!(stored, "notar cert should be stored before startup drain"); + + let kept_votes = + crate::startup_recovery::SessionStartupRecoveryListener::recovery_drain_startup_events( + &mut fixture.processor, + ); + assert!( + kept_votes.is_empty(), + "this setup should produce only certificate events, no startup votes" + ); + + fixture.processor.check_all(); + let actions = fixture.drain_receiver_actions(); + assert!( + !actions.iter().any(|a| matches!(a, ReceiverAction::SendCertificate { .. })), + "drained startup certificate events must not be re-broadcast on first normal tick" + ); +} diff --git a/src/node/simplex/src/tests/test_simplex_state.rs b/src/node/simplex/src/tests/test_simplex_state.rs index 3fd6884..52663ae 100644 --- a/src/node/simplex/src/tests/test_simplex_state.rs +++ b/src/node/simplex/src/tests/test_simplex_state.rs @@ -261,11 +261,12 @@ fn test_on_candidate_stores_pending_when_no_parent() { let desc = create_test_desc(4, 2); let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - // Create candidate for slot 0 but window doesn't have this parent available + // Candidate for slot 1 with parent at slot 0, but parent isn't notarized yet + // so it can't be resolved โ†’ candidate stored as pending let parent_hash = UInt256::from_slice(&[1u8; 32]); let candidate = create_test_candidate( - 0, + 1, UInt256::default(), BlockIdExt::default(), Some((0, parent_hash)), @@ -274,7 +275,7 @@ fn test_on_candidate_stores_pending_when_no_parent() { state.on_candidate(&desc, candidate).expect("on_candidate should succeed"); - // Should NOT broadcast (parent not available) + // Should NOT broadcast (parent not available in window state) let events: Vec<_> = from_fn(|| state.pull_event()).collect(); assert!( !events.iter().any(|e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(_)))), @@ -282,8 +283,8 @@ fn test_on_candidate_stores_pending_when_no_parent() { events ); - // Should have pending block - assert!(state.get_window(WindowIndex::new(0)).unwrap().slots[0].pending_block.is_some()); + // Should have pending block (slot 1 = offset 1 in window 0) + assert!(state.get_window(WindowIndex::new(0)).unwrap().slots[1].pending_block.is_some()); } #[test] @@ -972,8 +973,10 @@ fn test_genesis_propagates_to_next_window_on_full_skip() { // Clear events while state.pull_event().is_some() {} - // Slot 0 should be skipped, first_non_finalized_slot = 1 - assert_eq!(state.first_non_finalized_slot, SlotIndex::new(1)); + // Slot 0 should be skipped; C++ parity: first_non_finalized_slot stays at 0 + assert_eq!(state.first_non_finalized_slot, SlotIndex::new(0)); + // But first_non_progressed_slot advances + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); // Window 1 should NOT have genesis yet (slot 0 was not the last slot in window 0) // Note: window 1 may or may not exist at this point @@ -995,8 +998,9 @@ fn test_genesis_propagates_to_next_window_on_full_skip() { // Clear events while state.pull_event().is_some() {} - // Now entire window 0 is skipped, first_non_finalized_slot = 2 - assert_eq!(state.first_non_finalized_slot, SlotIndex::new(2)); + // C++ parity: first_non_finalized_slot still at 0, progress cursor at 2 + assert_eq!(state.first_non_finalized_slot, SlotIndex::new(0)); + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); // Window 1 should now have genesis (None) as available base // This was propagated from window 0 since no finalization occurred @@ -1142,11 +1146,12 @@ fn test_skip_certificate_threshold_66_triggers_slot_skipped() { "Expected SlotSkipped event at skip certificate threshold" ); - // Skip advances first_non_finalized_slot (a skipped slot will never be finalized). + // C++ parity: skip does NOT advance first_non_finalized_slot (only finalization does). + // But first_non_progressed_slot (C++ `now_`) does advance on skip. assert_eq!( state.first_non_finalized_slot, - SlotIndex::new(1), - "first_non_finalized_slot should advance past skipped slot" + SlotIndex::new(0), + "first_non_finalized_slot should NOT advance on skip (C++ parity)" ); assert_eq!( state.first_non_progressed_slot, @@ -1195,10 +1200,6 @@ fn test_skip_certificate_reached_event_emitted_in_cpp_mode() { let ev = skip_cert_events[0]; assert_eq!(ev.slot, slot, "event slot must match"); assert_eq!(ev.certificate.vote.slot, slot, "certificate vote slot must match"); - assert!( - ev.should_broadcast, - "SkipCertificateReached should_broadcast must be true for vote-based emission" - ); assert_eq!( ev.certificate.signatures.len(), 3, @@ -1319,12 +1320,13 @@ fn test_slot_skipped_not_emitted_twice() { let skip_count = events.iter().filter(|e| matches!(e, SimplexEvent::SlotSkipped(_))).count(); assert_eq!(skip_count, 1, "Should emit exactly one SlotSkipped at skip certificate"); - // 5th vote: slot is now past first_non_finalized_slot, so it's rejected - // as SlotAlreadyFinalized (the slot was settled by the skip certificate). + // 5th vote: C++ parity -- first_non_finalized_slot does NOT advance on skip, + // so the slot is still "open" for vote reception (additional votes are accepted + // but won't re-trigger SlotSkipped since the cert is already formed). let result = state.on_vote_test(&desc, ValidatorIndex::new(4), vote, Vec::new()); assert!( - matches!(result, VoteResult::SlotAlreadyFinalized), - "Vote for settled (skipped) slot should be rejected, got: {:?}", + matches!(result, VoteResult::Applied), + "Vote for skipped slot should still be accepted (first_non_finalized_slot unchanged), got: {:?}", result ); @@ -1407,28 +1409,78 @@ fn test_ignore_finalized_slot_vote() { } #[test] -fn test_reject_non_first_slot_without_parent() { +fn test_candidate_without_parent_accepted() { + // C++ consensus.cpp:173 โ€” C++ never rejects a candidate for missing parent. + // It only validates parent_slot < candidate_slot when parent exists. let desc = create_test_desc(4, 2); let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - // Try to send candidate for slot 1 (non-first in window) without parent - // Note: We construct directly here because create_test_candidate enforces parent for block.id - // but we want to test the FSM's validation let candidate = Candidate::new( crate::block::CandidateId { - slot: SlotIndex::new(1), // Second slot in window + slot: SlotIndex::new(1), // Non-first slot hash: UInt256::default(), block: BlockIdExt::default(), }, - None, // No parent! + None, // No parent โ€” valid per C++ ValidatorIndex::new(0), Some(create_stub_block(BlockIdExt::default())), vec![], ); - // Should return error let result = state.on_candidate(&desc, candidate); - assert!(result.is_err(), "Non-first slot without parent should be rejected"); + assert!(result.is_ok(), "Candidate without parent must be accepted (C++ parity)"); +} + +#[test] +fn test_candidate_with_parent_slot_ge_rejected() { + // C++ consensus.cpp:173: parent_slot >= candidate_slot โ†’ misbehavior + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); + + let candidate = Candidate::new( + crate::block::CandidateId { + slot: SlotIndex::new(1), + hash: UInt256::from([0xAA; 32]), + block: BlockIdExt::default(), + }, + Some(crate::block::CandidateId { + slot: SlotIndex::new(1), // parent_slot == candidate_slot + hash: UInt256::from([0xBB; 32]), + block: BlockIdExt::default(), + }), + ValidatorIndex::new(0), + Some(create_stub_block(BlockIdExt::default())), + vec![], + ); + + let result = state.on_candidate(&desc, candidate); + assert!(result.is_err(), "Candidate with parent_slot >= candidate_slot must be rejected"); +} + +#[test] +fn test_candidate_with_valid_parent_accepted() { + // C++ consensus.cpp:173: parent_slot < candidate_slot โ†’ accepted + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); + + let candidate = Candidate::new( + crate::block::CandidateId { + slot: SlotIndex::new(1), + hash: UInt256::from([0xAA; 32]), + block: BlockIdExt::default(), + }, + Some(crate::block::CandidateId { + slot: SlotIndex::new(0), // parent_slot < candidate_slot + hash: UInt256::from([0xBB; 32]), + block: BlockIdExt::default(), + }), + ValidatorIndex::new(0), + Some(create_stub_block(BlockIdExt::default())), + vec![], + ); + + let result = state.on_candidate(&desc, candidate); + assert!(result.is_ok(), "Candidate with parent_slot < candidate_slot must be accepted"); } #[test] @@ -2124,10 +2176,6 @@ fn test_notarization_reached_event_emitted() { let event = notar_reached.unwrap(); assert_eq!(event.slot, SlotIndex::new(0)); assert_eq!(event.block_hash, block_hash); - assert!( - event.should_broadcast, - "NotarizationReached should_broadcast must be true for vote-based emission" - ); assert_eq!(event.certificate.signatures.len(), 3, "Certificate should have 3 signatures"); } @@ -2215,10 +2263,6 @@ fn test_finalization_reached_event_emitted() { let event = final_reached.unwrap(); assert_eq!(event.slot, SlotIndex::new(0)); assert_eq!(event.block_hash, block_hash); - assert!( - event.should_broadcast, - "FinalizationReached should_broadcast must be true for vote-based emission" - ); assert_eq!(event.certificate.signatures.len(), 3, "Certificate should have 3 signatures"); // Should also have BlockFinalized event (emitted after FinalizationReached) @@ -2584,11 +2628,12 @@ fn test_skip_certificate_created_at_threshold() { "SlotSkipped event should be emitted when skip threshold reached" ); - // Skip advances first_non_finalized_slot (a skipped slot will never be finalized). + // C++ parity: skip does NOT advance first_non_finalized_slot (only finalization does). + // But first_non_progressed_slot (C++ `now_`) does advance on skip. assert_eq!( state.first_non_finalized_slot, - SlotIndex::new(1), - "first_non_finalized_slot should advance past skipped slot" + SlotIndex::new(0), + "first_non_finalized_slot should NOT advance on skip (C++ parity)" ); assert_eq!( state.first_non_progressed_slot, @@ -2662,6 +2707,8 @@ fn test_set_notarize_certificate_idempotent() { .set_notarize_certificate(&desc, slot, &block_hash, cert.clone()) .expect("should not conflict"); let weight_after_first = state.get_notarize_weight(slot, &block_hash); + // Drain first-store events so we can assert duplicate store emits none. + while state.pull_event().is_some() {} let stored2 = state .set_notarize_certificate(&desc, slot, &block_hash, cert.clone()) @@ -2677,6 +2724,10 @@ fn test_set_notarize_certificate_idempotent() { "Weight should not change on second call (idempotent)" ); assert_eq!(weight_after_first, 3, "Weight should be 3"); + assert!( + !state.has_pending_events(), + "duplicate notar cert must not emit relay-triggering events" + ); } #[test] @@ -2799,10 +2850,6 @@ fn test_set_notarize_certificate_emits_notarization_reached_for_tracked_slot() { let ev = notar_reached.unwrap(); assert_eq!(ev.slot, slot); assert_eq!(ev.block_hash, block_hash); - assert!( - !ev.should_broadcast, - "External set_notarize_certificate must set should_broadcast=false" - ); assert!(Arc::ptr_eq(&ev.certificate, &cert), "Event should carry the stored cert"); } @@ -3117,11 +3164,12 @@ fn test_skip_events_emitted_when_threshold_reached() { "SlotSkipped(1) should be emitted immediately when threshold reached" ); - // first_non_finalized_slot should have advanced past slot 1 + // C++ parity: first_non_finalized_slot does NOT advance on skip. + // It stays at 0 since nothing was finalized. assert_eq!( state.first_non_finalized_slot, - SlotIndex::new(2), - "first_non_finalized_slot should advance to 2" + SlotIndex::new(0), + "first_non_finalized_slot should NOT advance on skip (C++ parity)" ); } @@ -4451,6 +4499,8 @@ fn test_set_finalize_certificate_deduplicates() { .set_finalize_certificate(&desc, slot, &block_hash, final_cert.clone()) .expect("should not conflict"); assert!(stored1, "first application should store"); + // Drain first-store events so we can assert duplicate store emits none. + while state.pull_event().is_some() {} // Apply second time let stored2 = state @@ -4460,6 +4510,35 @@ fn test_set_finalize_certificate_deduplicates() { // Weight should still be 3 assert_eq!(state.get_finalize_weight(slot, &block_hash), 3); + assert!( + !state.has_pending_events(), + "duplicate finalize cert must not emit relay-triggering events" + ); +} + +#[test] +fn test_set_skip_certificate_deduplicates_without_events() { + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); + + let slot = SlotIndex::new(2); + let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; + let skip_cert = create_test_skip_cert(&desc, slot, &signers); + + let stored1 = state + .set_skip_certificate(&desc, slot, skip_cert.clone()) + .expect("first set_skip_certificate should succeed"); + assert!(stored1, "first skip certificate application should store"); + while state.pull_event().is_some() {} + + let stored2 = state + .set_skip_certificate(&desc, slot, skip_cert) + .expect("second set_skip_certificate should succeed"); + assert!(!stored2, "second skip certificate application should be deduplicated"); + assert!( + !state.has_pending_events(), + "duplicate skip cert must not emit relay-triggering events" + ); } #[test] @@ -4570,6 +4649,33 @@ fn test_set_skip_certificate_emits_slot_skipped_event_for_tracked_slot() { ); } +/// C++ parity (pool.cpp handle_saved_certificate): set_skip_certificate must emit +/// SkipCertificateReached so SessionProcessor relays foreign skip certificates. +#[test] +fn test_set_skip_certificate_emits_skip_cert_reached() { + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); + + while state.pull_event().is_some() {} + + let slot = SlotIndex::new(1); + let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; + let skip_cert = create_test_skip_cert(&desc, slot, &signers); + + let stored = state.set_skip_certificate(&desc, slot, skip_cert).expect("should not error"); + assert!(stored, "skip certificate should be stored"); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + let skip_reached = events + .iter() + .find_map(|e| match e { + SimplexEvent::SkipCertificateReached(ev) if ev.slot == slot => Some(ev), + _ => None, + }) + .expect("Expected SkipCertificateReached event for foreign skip cert"); + assert_eq!(skip_reached.slot, slot); +} + #[test] fn test_set_skip_certificate_does_not_emit_slot_skipped_event_for_old_slot() { let desc = create_test_desc(4, 2); @@ -4595,6 +4701,11 @@ fn test_set_skip_certificate_does_not_emit_slot_skipped_event_for_old_slot() { "SlotSkipped must not be emitted for old slots, got {:?}", events ); + assert!( + !events.iter().any(|e| matches!(e, SimplexEvent::SkipCertificateReached(_))), + "SkipCertificateReached must not be emitted for old slots, got {:?}", + events + ); } #[test] @@ -4685,11 +4796,6 @@ fn test_set_finalize_certificate_emits_block_finalized_and_finalization_reached_ assert_eq!(final_reached.slot, slot); assert_eq!(final_reached.block_hash, block_hash); assert!(Arc::ptr_eq(&final_reached.certificate, &cert)); - assert!( - !final_reached.should_broadcast, - "External set_finalize_certificate must set should_broadcast=false \ - (only local creation broadcasts)" - ); } /* diff --git a/src/node/simplex/src/utils.rs b/src/node/simplex/src/utils.rs index 28e793b..1e0eee8 100644 --- a/src/node/simplex/src/utils.rs +++ b/src/node/simplex/src/utils.rs @@ -37,7 +37,7 @@ //! use crate::{PrivateKey, PublicKey, SessionId, ValidatorWeight}; -use std::{any::Any, backtrace::Backtrace, panic, sync::Once, thread, time::Duration}; +use std::{any::Any, backtrace::Backtrace, cmp::max, panic, sync::Once, thread, time::Duration}; use ton_api::{ ton::{ consensus::{ @@ -476,6 +476,7 @@ pub fn extract_block_info_from_candidate( candidate_bytes: &[u8], shard: &ShardIdent, max_size: usize, + proto_version: u32, ) -> Result> { // Empty candidate means empty block if candidate_bytes.is_empty() { @@ -509,13 +510,17 @@ pub fn extract_block_info_from_candidate( ) } - // Decompress using validator-session's decompression utility + // C++ simplex always uses mode 2 (CRC32 only) for collated data + // re-serialization, regardless of proto_version. The proto_version >= 5 + // gate in decompress_candidate_data selects mode 2; lower versions select + // mode 31. + let effective_proto = max(proto_version, 5); let (block_data, collated_data) = consensus_common::compression::decompress_candidate_data( &c.data, false, c.decompressed_size as usize, - 0, + effective_proto, )?; (c.round, c.root_hash.clone(), block_data, collated_data) @@ -574,8 +579,10 @@ pub fn compute_candidate_id_hash_from_bytes( parent: Option<(SlotIndex, &UInt256)>, shard: &ShardIdent, max_size: usize, + proto_version: u32, ) -> Result { - let block_info = extract_block_info_from_candidate(candidate_bytes, shard, max_size)?; + let block_info = + extract_block_info_from_candidate(candidate_bytes, shard, max_size, proto_version)?; let hash = match block_info { Some(info) => compute_candidate_id_hash( @@ -877,7 +884,7 @@ pub fn get_vote_slot(vote: &tl_simplex::UnsignedVote) -> i32 { } /* - Block Info Extraction (SPLIT-1 Support) + Block Info Extraction (before_split support) */ /// Extract before_split flag from block payload diff --git a/src/node/simplex/tests/test_collation.rs b/src/node/simplex/tests/test_collation.rs index 222e873..b785cc3 100644 --- a/src/node/simplex/tests/test_collation.rs +++ b/src/node/simplex/tests/test_collation.rs @@ -26,7 +26,8 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocWriter, BuilderData, + Ed25519KeyOption, ShardIdent, UInt256, }; include!("../../../common/src/info.rs"); @@ -115,8 +116,16 @@ impl SessionListener for CollationTestListener { // Generate dummy candidate with proper hashes // The collator must provide file_hash = sha256(data) and collated_file_hash = sha256(collated_data) - // to match what the receiver will compute from the data - let block_data = vec![1u8, 2, 3, 4]; + // to match what the receiver will compute from the data. + // Block data MUST be valid BOC โ€” compress_candidate_data requires it. + let block_data = { + let mut b = BuilderData::new(); + b.append_raw(&[1u8, 2, 3, 4], 32).unwrap(); + let cell = b.into_cell().unwrap(); + let mut buf = Vec::new(); + BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); + buf + }; let collated_data_bytes: Vec = vec![]; // Compute hashes that match what receiver will compute diff --git a/src/node/simplex/tests/test_consensus.rs b/src/node/simplex/tests/test_consensus.rs index 1922b1e..dfc9432 100644 --- a/src/node/simplex/tests/test_consensus.rs +++ b/src/node/simplex/tests/test_consensus.rs @@ -23,7 +23,7 @@ use spin::mutex::SpinMutex; use std::{ collections::HashMap, fs::{self, File}, - io::{self, LineWriter, Write}, + io::{self, Cursor, LineWriter, Write}, path::Path, sync::{ atomic::{AtomicBool, AtomicU32, Ordering}, @@ -37,7 +37,8 @@ use ton_api::{ IntoBoxed, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocReader, BocWriter, + BuilderData, Ed25519KeyOption, ShardIdent, UInt256, }; /* @@ -93,11 +94,22 @@ impl DummyCollatedData { } fn to_bytes(&self) -> Vec { - bincode::serialize(self).unwrap() + // Wrap in single-cell BOC โ€” compress_candidate_data requires valid BOC input + let raw = bincode::serialize(self).unwrap(); + let mut b = BuilderData::new(); + b.append_raw(&raw, raw.len() * 8).unwrap(); + let cell = b.into_cell().unwrap(); + let mut buf = Vec::new(); + BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); + buf } fn from_bytes(bytes: &[u8]) -> Self { - bincode::deserialize(bytes).unwrap() + // Extract from BOC wrapper + let boc = BocReader::new().read(&mut Cursor::new(bytes)).unwrap(); + let cell = &boc.roots[0]; + let raw = cell.data(); + bincode::deserialize(raw).unwrap() } } @@ -208,10 +220,10 @@ impl Default for TestConfig { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 200, - target_rate: Duration::from_millis(100), - first_block_timeout: Duration::from_millis(500), + target_rate: Duration::from_millis(200), + first_block_timeout: Duration::from_millis(1000), test_name: "simplex_consensus".to_string(), - test_timeout: Duration::from_secs(60), + test_timeout: Duration::from_secs(120), expect_timeout: false, shard: ShardIdent::masterchain(), mc_notification_interval: None, // No MC notifications for masterchain @@ -1969,8 +1981,8 @@ fn test_simplex_consensus_basic() { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 10000, - target_rate: Duration::from_millis(50), - first_block_timeout: Duration::from_millis(300), + target_rate: Duration::from_millis(200), + first_block_timeout: Duration::from_millis(1000), test_name: "simplex_basic".to_string(), test_timeout: Duration::from_secs(180), expect_timeout: false, // Expect completion, not timeout @@ -2016,14 +2028,14 @@ fn test_simplex_consensus_basic() { fn test_simplex_consensus_with_failures() { run_simplex_consensus_test( TestConfig { - total_rounds: 50, + total_rounds: 30, min_commit_percent: 0.3, // Lower threshold due to failures node_count: 11, generation_failure_probability: 0.1, candidate_rejection_probability: 0.1, max_collations: 150, - target_rate: Duration::from_millis(100), - first_block_timeout: Duration::from_millis(500), + target_rate: Duration::from_millis(300), + first_block_timeout: Duration::from_millis(2000), test_name: "simplex_with_failures".to_string(), // This scenario includes randomized generation/rejection failures and can // occasionally complete just above 120s on loaded CI/containers. @@ -2088,10 +2100,10 @@ fn test_simplex_consensus_finalcert_recovery() { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 200, - target_rate: Duration::from_millis(100), - first_block_timeout: Duration::from_millis(500), + target_rate: Duration::from_millis(300), + first_block_timeout: Duration::from_millis(2000), test_name: "simplex_finalcert_recovery".to_string(), - test_timeout: Duration::from_secs(180), + test_timeout: Duration::from_secs(240), expect_timeout: false, shard: ShardIdent::masterchain(), mc_notification_interval: None, @@ -2164,10 +2176,10 @@ fn test_simplex_consensus_shard_with_mc_notifications() { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 500, - target_rate: Duration::from_millis(100), - first_block_timeout: Duration::from_millis(500), + target_rate: Duration::from_millis(200), + first_block_timeout: Duration::from_millis(1000), test_name: "simplex_shard_mc".to_string(), - test_timeout: Duration::from_secs(120), + test_timeout: Duration::from_secs(180), expect_timeout: false, // Use a shard chain (workchain 0, full shard) shard: ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(), @@ -2340,7 +2352,7 @@ fn test_simplex_consensus_restart_gremlin() { candidate_rejection_probability: 0.0, max_collations: 2000, target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(500), + first_block_timeout: Duration::from_millis(1200), test_name: "simplex_restart_gremlin".to_string(), test_timeout: Duration::from_secs(180), // Longer timeout for restart cycles expect_timeout: false, diff --git a/src/node/simplex/tests/test_restart.rs b/src/node/simplex/tests/test_restart.rs index acd4169..264ba47 100644 --- a/src/node/simplex/tests/test_restart.rs +++ b/src/node/simplex/tests/test_restart.rs @@ -35,7 +35,8 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocWriter, BuilderData, + Ed25519KeyOption, ShardIdent, UInt256, }; include!("../../../common/src/info.rs"); @@ -217,7 +218,16 @@ impl SessionListener for RestartSingleSessionListener { ); // Block + collated data (keep small; hashes must match) - let block_data = vec![1u8, 2, 3, 4, (seqno % 255) as u8]; + // Block data must be valid BOC (compress_candidate_data deserializes it) + let block_data = { + let raw = [1u8, 2, 3, 4, (seqno % 255) as u8]; + let mut b = BuilderData::new(); + b.append_raw(&raw, raw.len() * 8).unwrap(); + let cell = b.into_cell().unwrap(); + let mut buf = Vec::new(); + BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); + buf + }; let collated_data: Vec = vec![]; let file_hash = UInt256::from_slice(&sha256_digest(&block_data)); @@ -520,7 +530,6 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate let last_committed_slot_before = listener.last_committed_slot(); let collation_before = listener.collation_count(); - let approved_fetch_before = listener.approved_candidate_requests(); // Stop session 1 and give some time for DB handles to close session_1.stop(); @@ -609,15 +618,6 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate ); } - // Post-condition: restart recovery should have requested approved candidates - // (for candidate cache restoration). We allow >=1 because small tests may have few blocks. - assert!( - listener.approved_candidate_requests() > approved_fetch_before, - "expected get_approved_candidate to be used during restart recovery (before={}, after={})", - approved_fetch_before, - listener.approved_candidate_requests() - ); - // Post-condition: no session errors recorded assert_eq!( listener.max_errors_count(), diff --git a/src/node/simplex/tests/test_validation.rs b/src/node/simplex/tests/test_validation.rs index 37934bd..c980ce7 100644 --- a/src/node/simplex/tests/test_validation.rs +++ b/src/node/simplex/tests/test_validation.rs @@ -26,7 +26,8 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocWriter, BuilderData, + Ed25519KeyOption, ShardIdent, UInt256, }; include!("../../../common/src/info.rs"); @@ -129,7 +130,15 @@ impl SessionListener for ValidationTestListener { // Generate dummy candidate with proper hashes // The collator must provide file_hash = sha256(data) and collated_file_hash = sha256(collated_data) // to match what the receiver will compute from the data - let block_data = vec![1u8, 2, 3, 4]; + // Block data must be valid BOC (compress_candidate_data deserializes it) + let block_data = { + let mut b = BuilderData::new(); + b.append_raw(&[1u8, 2, 3, 4], 32).unwrap(); + let cell = b.into_cell().unwrap(); + let mut buf = Vec::new(); + BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); + buf + }; let collated_data_bytes: Vec = vec![]; // Compute hashes that match what receiver will compute diff --git a/src/node/src/collator_test_bundle.rs b/src/node/src/collator_test_bundle.rs index e952b4a..51b6874 100644 --- a/src/node/src/collator_test_bundle.rs +++ b/src/node/src/collator_test_bundle.rs @@ -1497,6 +1497,10 @@ impl EngineOperations for CollatorTestBundle { self.index.now } + fn now_ms(&self) -> u64 { + self.index.now as u64 * 1000 + } + fn load_block_handle(&self, id: &BlockIdExt) -> Result>> { let handle = self.block_handle_storage.create_handle(id.clone(), BlockMeta::default(), None)?; @@ -1745,6 +1749,7 @@ pub async fn try_collate( engine, true, true, + false, ); validator_query.try_validate().await?; } @@ -1791,6 +1796,7 @@ pub async fn try_validate( engine, true, false, + false, ); validator_query.try_validate().await } diff --git a/src/node/src/network/node_network.rs b/src/node/src/network/node_network.rs index 02e6d9d..c3dbb62 100644 --- a/src/node/src/network/node_network.rs +++ b/src/node/src/network/node_network.rs @@ -252,6 +252,10 @@ impl NodeNetwork { pub async fn stop_adnl(&self) { log::info!("Stopping node network loops..."); self.cancellation_token.cancel(); + if let Some(quic) = &self.network_context.stack.quic { + log::info!("Stopping QUIC..."); + quic.shutdown(); + } log::info!("Node network loops stopped. Stopping adnl..."); self.network_context.stack.adnl.stop().await; log::info!("Stopped adnl"); diff --git a/src/node/src/tests/static/zerostate.boc b/src/node/src/tests/static/zerostate.boc index 040620a36c546e1baaa87cc990a4f846de0a328e..a7f24b451f07e3b28f0fb143dc51043e6cac38b4 100644 GIT binary patch delta 4188 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 RPdMZ_3vnef!_{)k{{qz$=eGa= delta 2431 zcmX|D30PIf6`p_Yd%Q)K2NGrXsR5K-KoIq@iGawy1Q1cQ8jPVDwGoL`fhO8)R5&%J z)>so`qp8M)AXPCMLz<>>*Tm4IsYa8SxS){)#D&hewte6C=D+iwf6h5GXYS1H=|poo zY;89-E|yY83kXrsAOy`ZEHuiJV?iMVqZiqXCYPfjGY=Qc*kF^`hjt1w+CAi8AS?D> z){>`%S@es$(w#OM{R5<1e)RdBFg+#34zW{-Of&j(11gp%(GTJ(`8q{v5i*i;oIbKi zH}T2x6R#dVViutyOpTvs%CbEoh5$#zP^q1?3ondrJ!&#Rv3|_@>udlP3>!!Fx9U%@C?>fi3p~vf1Gq8j%!;jP$T4elL)}HL8i=ke0Gt@+PLj7$1;x>zW zbn#D092mxH(92|>PTo=Ey0EC>_r!fwoX@Fih&<>ML8%T+SgO1 zaRffR%3knhcm~Ua>lmhn zC=bCrbT~%9sw9dI;{HfHN{gabxCL@26k!bcD4>*>x!Ne@czaB6NQlr*+1U_|2v`ny zj65(3S=^6A6hl15N!f)lh^GFSl^9EPu@kfy+#|mXUpf;zM~f9Z=vGD&)u#=n7ssyF z;zS2w@(>zGG*V>TUTXq-aUfAXpyZrVsv0}Wfbp0>3*yTx6NU4~*Na#6Rg-(}I5Le( zu%=>-L>ZEhOc(O1O?CKiPj15(G%TSCDde5-I5Md|p-4-`B-)g}0%>$Cw+!jDb9^Hv zQ`CeHv9ISotl(HrH)igr40@rWK)}?QCV|nd}0G~sUxv5 zE>BqXOgP&@6fr!GYKa9XhI(D?sK9iX_uiOOgU4WQ4X902Y4+2T$qq^^N|q?4+ey7B zr>n^;F^%S@WSi^o>AqlX(n>((U@}8V)orfnlou$}KizctK&X<^vCs89V z6Ux1*JJFMZ)3Z&#MBAQueY=#iB-)?ujYV`ey%7uOv5Yb-q+J<}+EXG-)oci5Wj3g# zSVAW<^YAMQn9`;_Eyhz`y7Dz;xmJsL)IC1h&}_#t+LBdhY(<@Dq%m+lN0N~~gN#BZo{>NeE3WH)LJV$=V(!{+Q)%+IpoMZ83bIdyoBcL7$b>MwF`!Wvkc z>Mt3(@LOuib>MmOD=3$0H-#6LQ+?h{yv$nyzt=ZF6Mm;}fVKJtXyFZDXcn`tkXvaO z1sARc(L04{KI`!+$NMwhf@S|}$6j8GKXCt#*htPr4cI_UMF-5Uaa}g!b-gZ|xGwk; zxt7%84O(52uf3_x4%eb6a!X5TNpdw{i^!tpoFH1C)}U<_i)s3_cza*9luNM<+u8dr z?1pLg#4~kxTVcV$)7VLsrS$y=sPY!+n2cd1- zjDK>c1BVnylQeVhJsf8EL_fyWnejf*gO8XxiVvWw`8Rc-qm!EtMKYbPQ0JB1NY^Sd z&3|Xr$M^?LoE|XtxPAfPBu+7V8GJe5CT=mD#upg9pU%r%1A?0y=iGQ*BEBu?9Dr2n|aFM%T;}RXK>|FINtG|1Azc@dfBGlz^JQ-K;JqvsB zgHUtj$E&!;a9s%$bYpQ&!{I*pIsm_jKP<;J!eaBK~VutPnUSkSUY|=a?4CDx0NII?=3| z(GeDDWxbE|Wf&x#8PpdFgNt-kg^=!?qq;}z(njyk%=h$^kFeECdNHTIIqY&M1y*f@ zHyy2t@br`ZY_`kcYzg3I7#U_|d5n-Fp|T5N=230}scKfj@L*PtlA{?yWT={}jFMxx X7be3+>f!eP-NYs~@fXmAS!@3TI%Jgd diff --git a/src/node/src/tests/test_helper.rs b/src/node/src/tests/test_helper.rs index 0ed3c0f..76942a8 100644 --- a/src/node/src/tests/test_helper.rs +++ b/src/node/src/tests/test_helper.rs @@ -808,6 +808,7 @@ impl TestEngine { self.clone(), true, false, + false, ); validator_query.try_validate().await?; Ok(()) @@ -848,6 +849,7 @@ impl TestEngine { self.clone(), true, false, + false, ); validator_query.try_validate().await?; @@ -957,6 +959,7 @@ impl TestEngine { self.clone(), true, false, + false, ); validator_query.try_validate().await?; @@ -983,6 +986,10 @@ impl EngineOperations for TestEngine { self.now.load(Ordering::Relaxed) } + fn now_ms(&self) -> u64 { + self.now() as u64 * 1000 + } + async fn lookup_block_by_seqno( &self, prefix: &AccountIdPrefixFull, diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index acfe9d9..b81ee2b 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -262,6 +262,7 @@ struct CollatorData { // determined fields gen_utime: u32, + gen_utime_ms: u64, config: BlockchainConfig, collated_block_descr: Arc, block_limit_class: ParamLimitIndex, @@ -312,6 +313,7 @@ struct CollatorData { impl CollatorData { pub fn new( gen_utime: u32, + gen_utime_ms: u64, config: BlockchainConfig, usage_tree: UsageTree, prev_data: &PrevData, @@ -338,6 +340,7 @@ impl CollatorData { dispatch_queue_total_limit_reached: false, have_unprocessed_account_dispatch_queue: false, gen_utime, + gen_utime_ms, config, collated_block_descr, block_limit_class: ParamLimitIndex::Underload, @@ -383,6 +386,10 @@ impl CollatorData { self.gen_utime } + fn gen_utime_ms(&self) -> u64 { + self.gen_utime_ms + } + // // Lists // @@ -1592,7 +1599,7 @@ impl Collator { let is_masterchain = self.shard.is_masterchain(); self.check_stop_flag()?; - let now = self.init_utime(&mc_data, &prev_data)?; + let (now, now_ms) = self.init_utime(&mc_data, &prev_data)?; let config = BlockchainConfig::with_params( mc_data.config().capabilities(), supported_version(), @@ -1600,6 +1607,7 @@ impl Collator { )?; let mut collator_data = CollatorData::new( now, + now_ms, config, usage_tree, &prev_data, @@ -2176,60 +2184,65 @@ impl Collator { Ok(usage_tree) } - fn init_utime(&self, mc_data: &McData, prev_data: &PrevData) -> Result { + fn init_utime(&self, mc_data: &McData, prev_data: &PrevData) -> Result<(u32, u64)> { // consider unixtime and lt from previous block(s) of the same shardchain let prev_now = prev_data.prev_state_utime(); let prev = max(mc_data.state().state()?.gen_time(), prev_now); log::trace!("{}: init_utime prev_time: {}", self.collated_block_descr, prev); let allow_same_timestamp = self.allow_same_timestamp(mc_data); - Ok(Self::calc_utime(prev, self.engine.now(), allow_same_timestamp)) + // Compute gen_utime_ms first, then derive gen_utime from it (like C++). + // This guarantees gen_utime_ms / 1000 == gen_utime, avoiding second-boundary + // mismatches in ConsensusExtraData validation. + let (gen_utime, gen_utime_ms) = + Self::calc_utime(prev, self.engine.now_ms(), allow_same_timestamp); + Ok((gen_utime, gen_utime_ms)) } /// Whether this shard is allowed to have `gen_utime` equal to the previous one. /// - /// C++ compatibility: - /// - non-simplex (catchain): always strict (`prev + 1`) - /// - simplex: allow equal timestamps starting from `global_version >= 13` + /// C++ parity: `allow_same_timestamp_ = global_version_ >= 13`. + /// Depends only on the global protocol version, not on consensus type. + /// When false (global_version < 13), gen_utime must strictly increase (prev + 1). + /// When true, gen_utime may equal the previous block's (non-decreasing). fn allow_same_timestamp(&self, mc_data: &McData) -> bool { - #[cfg(feature = "simplex")] + #[cfg(feature = "xp25")] { - let simplex_enabled_for_shard = if self.shard.is_masterchain() { - mc_data.config().get_mc_simplex_config().ok().flatten().is_some() - } else { - mc_data.config().get_shard_simplex_config().ok().flatten().is_some() - }; - - // C++-compatible gating: allow equal timestamps only starting from global_version >= 13. - // This must match validator-side checks (`validate_query.rs`). - simplex_enabled_for_shard - //TODO: LK: enable after change block version to 13 - //&& mc_data.config().global_version() - // >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION + let _ = mc_data; + true } - #[cfg(not(feature = "simplex"))] + #[cfg(not(feature = "xp25"))] { - let _ = mc_data; - #[cfg(feature = "xp25")] - { - true - } - #[cfg(not(feature = "xp25"))] - { - false - } + mc_data.config().global_version() + >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION } } + /// Compute gen_utime_ms and gen_utime from previous block time and current wall clock. + /// + /// Mirrors C++ collator.cpp: + /// ```cpp + /// now_ms_ = std::max((td::uint64)(prev + (allow_same ? 0 : 1)) * 1000, + /// (td::uint64)(td::Clocks::system() * 1000)); + /// now_ = (UnixTime)(now_ms_ / 1000); + /// ``` + /// + /// By computing milliseconds first and deriving seconds, we guarantee + /// `gen_utime_ms / 1000 == gen_utime` always holds. #[inline] - fn calc_utime(prev: u32, now: u32, allow_same_timestamp: bool) -> u32 { - if allow_same_timestamp { - // NOTE: keep gen_utime monotonic (non-decreasing), but do NOT force +1 when blocks - // are produced faster than 1/sec (otherwise chain time drifts into the future). - max(prev, now) + fn calc_utime(prev: u32, now_ms: u64, allow_same_timestamp: bool) -> (u32, u64) { + let prev_sec = if allow_same_timestamp { + // Non-decreasing: do NOT force +1 when blocks are produced faster than 1/sec. + prev } else { - // C++ non-simplex behavior: strictly increasing gen_utime - max(prev.saturating_add(1), now) - } + // Strictly increasing gen_utime (legacy behavior), saturating at u32::MAX. + prev.saturating_add(1) + }; + let prev_ms = prev_sec as u64 * 1000; + // Clamp to u32::MAX seconds range to prevent wraparound on cast. + let max_ms = u32::MAX as u64 * 1000; + let gen_utime_ms = max(prev_ms, now_ms).min(max_ms); + let gen_utime = (gen_utime_ms / 1000) as u32; + (gen_utime, gen_utime_ms) } fn check_utime( @@ -4863,7 +4876,15 @@ impl Collator { roots.push(tbds.serialize()?); } - // match collator's BoC flags to consensus version config + // 1.2 store info for simplex consensus (C++ parity) + if self.collator_settings.is_simplex { + let extra = ton_block::ConsensusExtraData { + flags: 0, + gen_utime_ms: collator_data.gen_utime_ms(), + }; + roots.push(extra.serialize()?); + } + let collated_data_flags = if collator_data .config .raw_config() diff --git a/src/node/src/validator/consensus.rs b/src/node/src/validator/consensus.rs index 9a3d508..0908cc7 100644 --- a/src/node/src/validator/consensus.rs +++ b/src/node/src/validator/consensus.rs @@ -89,9 +89,9 @@ pub use consensus_common::{ ConsensusOverlayListener, ConsensusOverlayListenerPtr, ConsensusOverlayLogReplayListener, ConsensusOverlayLogReplayListenerPtr, ConsensusOverlayManager, ConsensusOverlayManagerPtr, ConsensusOverlayPtr, ConsensusReplayListener, ConsensusReplayListenerPtr, LogPlayer, - LogPlayerPtr, LogReplayOptions, PrivateKey, PublicKey, PublicKeyHash, RawBuffer, Result, - Session, SessionId, SessionListener, SessionListenerPtr, SessionNode, SessionPtr, SessionStats, - ValidatorBlockCandidate, ValidatorBlockCandidateCallback, + LogPlayerPtr, LogReplayOptions, OverlayTransportType, PrivateKey, PublicKey, PublicKeyHash, + RawBuffer, Result, Session, SessionId, SessionListener, SessionListenerPtr, SessionNode, + SessionPtr, SessionStats, ValidatorBlockCandidate, ValidatorBlockCandidateCallback, ValidatorBlockCandidateDecisionCallback, ValidatorBlockCandidatePtr, ValidatorWeight, }; @@ -473,10 +473,13 @@ impl ConsensusFactory { pub fn create_simplex_options( max_block_size: usize, max_collated_data_size: usize, + proto_version: u32, ) -> SimplexSessionOptions { use super::consensus::*; SimplexSessionOptions { + proto_version, + // Core timing from testing constants (p30 reference) target_rate: Duration::from_millis(SIMPLEX_TARGET_RATE_MS), slots_per_leader_window: SIMPLEX_SLOTS_PER_LEADER_WINDOW, diff --git a/src/node/src/validator/consensus_overlay.rs b/src/node/src/validator/consensus_overlay.rs index bdd66f5..815c20c 100644 --- a/src/node/src/validator/consensus_overlay.rs +++ b/src/node/src/validator/consensus_overlay.rs @@ -8,11 +8,10 @@ */ use super::consensus::{ ConsensusNode, ConsensusOverlayListenerPtr, ConsensusOverlayLogReplayListenerPtr, - ConsensusOverlayManager, ConsensusOverlayPtr, PrivateKey, + ConsensusOverlayManager, ConsensusOverlayPtr, OverlayTransportType, PrivateKey, }; use crate::engine_traits::PrivateOverlayOperations; use adnl::PrivateOverlayShortId; -use consensus_common::OverlayTransportType; use std::sync::Arc; use ton_block::{Result, UInt256}; diff --git a/src/node/src/validator/fabric.rs b/src/node/src/validator/fabric.rs index 15dcc51..5853adb 100644 --- a/src/node/src/validator/fabric.rs +++ b/src/node/src/validator/fabric.rs @@ -27,7 +27,7 @@ use crate::{ validate_query::ValidateQuery, validator_group::PipelineContext, validator_utils::PrevBlockHistory, - BlockCandidate, + BlockCandidate, CollatorSettings, }, }; use std::{sync::Arc, time::SystemTime}; @@ -39,6 +39,7 @@ use ton_block::{ pub async fn run_validate_query_any_candidate( block_candidate: BlockCandidate, engine: Arc, + is_simplex: bool, ) -> Result { let block_id = block_candidate.block_id.clone(); let block_data = block_candidate.data.clone(); @@ -81,6 +82,7 @@ pub async fn run_validate_query_any_candidate( engine.clone(), false, true, + is_simplex, ); let validator_result = query.try_validate().await; @@ -149,6 +151,7 @@ pub async fn run_validate_query( block: BlockCandidate, set: ValidatorSet, engine: Arc, + is_simplex: bool, ) -> Result { let next_block_descr = fmt_next_block_descr(&block.block_id); @@ -174,6 +177,7 @@ pub async fn run_validate_query( engine.clone(), false, true, + is_simplex, ) .try_validate() .await @@ -187,6 +191,7 @@ pub async fn run_validate_query( engine.clone(), false, true, + is_simplex, ); let validator_result = query.try_validate().await; if let Err(err) = &validator_result { @@ -283,6 +288,7 @@ pub async fn run_collate_query( collator_id: PublicKey, set: ValidatorSet, engine: Arc, + is_simplex: bool, ) -> Result<(Arc, Arc, Block, Cell)> { let labels = [("shard", shard.to_string())]; metrics::gauge!("ton_node_collator_active", &labels).increment(1.0); @@ -298,7 +304,7 @@ pub async fn run_collate_query( UInt256::from(collator_id.pub_key()?), engine.clone(), None, - Default::default(), + CollatorSettings { is_simplex, ..Default::default() }, )?; let collate_result = collator.collate().await; diff --git a/src/node/src/validator/mod.rs b/src/node/src/validator/mod.rs index fe9c093..5d4e4f4 100644 --- a/src/node/src/validator/mod.rs +++ b/src/node/src/validator/mod.rs @@ -34,10 +34,12 @@ use ton_block::{ Libraries, McStateExtra, Result, UInt256, }; -/// C++ simplex collator/validator allows equal `gen_utime` starting from this global version. +/// Minimum global version that allows equal `gen_utime` between consecutive blocks. /// -/// Reference (C++): `allow_same_timestamp_ = global_version_ >= 13`. -#[allow(dead_code)] +/// C++ parity: `allow_same_timestamp_ = global_version_ >= 13`. +/// Applies to all consensus types (not simplex-specific despite the name). +/// Under `xp25` feature this constant is unused โ€” `allow_same_timestamp` is always true. +#[cfg(not(feature = "xp25"))] pub(super) const SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION: u32 = 13; #[derive(Clone, Default, Debug)] @@ -58,6 +60,8 @@ pub struct CollatorSettings { pub is_bundle: bool, // produce blocks identical to cpp-node - mostly for tests pub lt_compatible: bool, + // true when running under simplex consensus (passed from ValidatorGroup) + pub is_simplex: bool, } impl CollatorSettings { diff --git a/src/node/src/validator/tests/test_collator.rs b/src/node/src/validator/tests/test_collator.rs index a8115e3..0a01dae 100644 --- a/src/node/src/validator/tests/test_collator.rs +++ b/src/node/src/validator/tests/test_collator.rs @@ -180,13 +180,60 @@ impl EngineOperations for TestPipelineCollatorEngine { #[test] fn test_calc_utime_allow_same_timestamp_does_not_drift_when_prev_ahead() { // Simplex / allow_same_timestamp=true: monotonic, but no forced +1 drift. - assert_eq!(Collator::calc_utime(1015, 1000, true), 1015); + // prev=1015s, now_ms=1000_000ms (1000s) โ†’ gen_utime=1015, gen_utime_ms=1015_000 + let (gen_utime, gen_utime_ms) = Collator::calc_utime(1015, 1_000_000, true); + assert_eq!(gen_utime, 1015); + assert_eq!(gen_utime_ms, 1_015_000); + assert_eq!(gen_utime_ms / 1000, gen_utime as u64); } #[test] fn test_calc_utime_strict_timestamp_forces_increment_when_prev_ahead() { // Catchain / allow_same_timestamp=false: C++-compatible strict +1. - assert_eq!(Collator::calc_utime(1015, 1000, false), 1016); + // prev=1015s, now_ms=1000_000ms (1000s) โ†’ gen_utime=1016, gen_utime_ms=1016_000 + let (gen_utime, gen_utime_ms) = Collator::calc_utime(1015, 1_000_000, false); + assert_eq!(gen_utime, 1016); + assert_eq!(gen_utime_ms, 1_016_000); + assert_eq!(gen_utime_ms / 1000, gen_utime as u64); +} + +#[test] +fn test_calc_utime_ms_preserves_milliseconds_when_now_dominates() { + // now_ms=2000_500ms (2000.5s), prev=1000s โ†’ gen_utime=2000, gen_utime_ms=2000_500 + let (gen_utime, gen_utime_ms) = Collator::calc_utime(1000, 2_000_500, true); + assert_eq!(gen_utime, 2000); + assert_eq!(gen_utime_ms, 2_000_500); + assert_eq!(gen_utime_ms / 1000, gen_utime as u64); +} + +#[test] +fn test_calc_utime_invariant_ms_div_1000_equals_seconds() { + // Verify the core invariant across various inputs + for &(prev, now_ms, allow_same) in &[ + (100u32, 100_500u64, true), + (100, 100_500, false), + (100, 99_999, true), + (100, 99_999, false), + (100, 101_000, true), + (0, 1_000, true), + (0, 1_000, false), + ] { + let (gen_utime, gen_utime_ms) = Collator::calc_utime(prev, now_ms, allow_same); + assert_eq!( + gen_utime_ms / 1000, + gen_utime as u64, + "invariant violated: prev={prev}, now_ms={now_ms}, allow_same={allow_same}" + ); + } +} + +#[test] +fn test_calc_utime_strict_mode_saturates_at_u32_max() { + // Strict mode would normally add +1 second, but must not wrap at u32::MAX. + let (gen_utime, gen_utime_ms) = Collator::calc_utime(u32::MAX, 0, false); + assert_eq!(gen_utime, u32::MAX); + assert_eq!(gen_utime_ms, u32::MAX as u64 * 1000); + assert_eq!(gen_utime_ms / 1000, gen_utime as u64); } #[tokio::test(flavor = "multi_thread")] diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 849771c..671ca8a 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -53,15 +53,15 @@ use ton_block::{ read_boc, Account, AccountBlock, AccountDispatchQueue, AccountId, AccountIdPrefixFull, AccountStatus, AccountStorageDictProof, AddSub, Block, BlockCreateStats, BlockError, BlockExtra, BlockIdExt, BlockInfo, BlockLimits, Cell, CellType, Coins, ConfigParamEnum, - ConfigParams, Counters, CreatorStats, CurrencyCollection, DepthBalanceInfo, Deserializable, - EnqueuedMsg, FundamentalSmcAddresses, GlobalCapabilities, HashmapAugType, HashmapType, InMsg, - InMsgDescr, KeyExtBlkRef, KeyMaxLt, LibDescr, Libraries, McBlockExtra, McShardRecord, - McStateExtra, MerkleProof, MerkleUpdate, Message, MsgAddressInt, MsgEnvelope, MsgMetadata, - OutMsg, OutMsgDescr, OutMsgQueueKey, Result, Serializable, ShardAccount, ShardAccountBlocks, - ShardAccounts, ShardFeeCreated, ShardHashes, ShardIdent, ShardStateUnsplit, SizeLimitsConfig, - SliceData, StateInitLib, TopBlockDescrSet, TrComputePhase, Transaction, TransactionDescr, - UInt15, UInt256, ValidatorSet, ValueFlow, WorkchainDescr, INVALID_WORKCHAIN_ID, MASTERCHAIN_ID, - MAX_SPLIT_DEPTH, + ConfigParams, ConsensusExtraData, Counters, CreatorStats, CurrencyCollection, DepthBalanceInfo, + Deserializable, EnqueuedMsg, FundamentalSmcAddresses, GlobalCapabilities, HashmapAugType, + HashmapType, InMsg, InMsgDescr, KeyExtBlkRef, KeyMaxLt, LibDescr, Libraries, McBlockExtra, + McShardRecord, McStateExtra, MerkleProof, MerkleUpdate, Message, MsgAddressInt, MsgEnvelope, + MsgMetadata, OutMsg, OutMsgDescr, OutMsgQueueKey, Result, Serializable, ShardAccount, + ShardAccountBlocks, ShardAccounts, ShardFeeCreated, ShardHashes, ShardIdent, ShardStateUnsplit, + SizeLimitsConfig, SliceData, StateInitLib, TopBlockDescrSet, TrComputePhase, Transaction, + TransactionDescr, UInt15, UInt256, ValidatorSet, ValueFlow, WorkchainDescr, + INVALID_WORKCHAIN_ID, MASTERCHAIN_ID, MAX_SPLIT_DEPTH, }; use ton_executor::{ BlockchainConfig, ExecuteParams, OrdinaryTransactionExecutor, TickTockTransactionExecutor, @@ -142,6 +142,7 @@ impl Default for ValidateResult { struct ValidateBase { global_id: i32, is_fake: bool, + is_simplex: bool, created_by: UInt256, after_merge: bool, after_split: bool, @@ -167,6 +168,7 @@ struct ValidateBase { virt_states: HashMap, // prev state and neighbour out msg queues proofs by block root hash storage_dict_proofs: HashMap, full_collated_data: bool, + now_ms: Option, // gen_utime_ms from ConsensusExtraData (simplex consensus) gas_used: Arc, transactions_executed: Arc, @@ -257,6 +259,7 @@ pub struct ValidateQuery { validator_set: ValidatorSet, is_fake: bool, multithread: bool, + is_simplex: bool, // previous state can be as two states for merge prev_blocks_ids: Vec, old_mc_shards: ShardHashes, // old_shard_conf_ @@ -295,6 +298,7 @@ impl ValidateQuery { engine: Arc, is_fake: bool, multithread: bool, + is_simplex: bool, ) -> Self { let next_block_descr = Arc::new(fmt_next_block_descr(&block_candidate.block_id)); Self { @@ -305,6 +309,7 @@ impl ValidateQuery { validator_set, is_fake, multithread, + is_simplex, prev_blocks_ids, old_mc_shards: Default::default(), // new state after applying block_candidate @@ -327,6 +332,7 @@ impl ValidateQuery { let mut base = ValidateBase { next_block_descr: self.next_block_descr.clone(), is_fake: self.is_fake, + is_simplex: self.is_simplex, created_by: self.block_candidate.created_by.clone(), prev_blocks_ids: mem::take(&mut self.prev_blocks_ids), ..Default::default() @@ -626,17 +632,55 @@ impl ValidateQuery { if let Some(BlockError::InvalidConstructorTag { t: _, s: _ }) = err.downcast_ref() { - let dict_proof = - AccountStorageDictProof::construct_from_cell(croot.clone())?; - log::debug!( - target: "validate_query", - "({}): collated datum # {idx} is an AccountStorageDictProof", - base.next_block_descr - ); - let dict_proof = MerkleProof::construct_from_cell(dict_proof.proof)?; - base.storage_dict_proofs - .insert(dict_proof.hash, dict_proof.proof.virtualize(1)); - base.full_collated_data = true; + // Try AccountStorageDictProof first + match AccountStorageDictProof::construct_from_cell(croot.clone()) { + Ok(dict_proof) => { + log::debug!( + target: "validate_query", + "({}): collated datum # {idx} is an AccountStorageDictProof", + base.next_block_descr + ); + let dict_proof = + MerkleProof::construct_from_cell(dict_proof.proof)?; + base.storage_dict_proofs + .insert(dict_proof.hash, dict_proof.proof.virtualize(1)); + base.full_collated_data = true; + } + Err(_) => { + // Try ConsensusExtraData + match ConsensusExtraData::construct_from_cell(croot.clone()) { + Ok(extra) => { + log::debug!( + target: "validate_query", + "({}): collated datum # {idx} is a ConsensusExtraData, gen_utime_ms={}", + base.next_block_descr, + extra.gen_utime_ms + ); + if base.now_ms.is_some() { + reject_query!( + "duplicate ConsensusExtraData in collated data" + ) + } + // Check: ConsensusExtraData is only valid when simplex is enabled + if !base.is_simplex { + reject_query!("unexpected ConsensusExtraData") + } + base.now_ms = Some(extra.gen_utime_ms); + } + Err(_) => { + let tag = SliceData::load_cell_ref(croot)? + .get_next_u32() + .unwrap_or(0); + log::warn!( + target: "validate_query", + "({}): collated datum # {idx} has unknown type (tag {:#010x}), ignoring", + base.next_block_descr, + tag + ); + } + } + } + } } else { return Err(err); } @@ -764,8 +808,7 @@ impl ValidateQuery { let mc_state_extra = mc_state.shard_state_extra()?.clone(); let config_params = mc_state_extra.config(); CHECK!(config_params, inited); - let (capabilities, block_version) = - base.info.gen_software().map_or((0, 0), |v| (v.capabilities, v.version)); + let block_version = base.info.gen_software().map_or(0, |v| v.version); if block_version < config_params.global_version() { reject_query!( "This block version {} is too old, node_version: {} net version: {}", @@ -2023,30 +2066,19 @@ impl ValidateQuery { fn check_utime_lt(&self, base: &ValidateBase, mc_data: &McData) -> Result<()> { CHECK!(&base.config_params, inited); + // C++ parity: allow_same_timestamp_ = global_version_ >= 13. + // Depends only on the global protocol version, not on consensus type. + // When true, also skips the future-time check (base.now > engine.now + 15) + // which C++ simplex testnet does not have. let allow_same_timestamp = { - #[cfg(feature = "simplex")] + #[cfg(feature = "xp25")] { - let simplex_enabled_for_shard = if base.shard().is_masterchain() { - base.config_params.get_mc_simplex_config().ok().flatten().is_some() - } else { - base.config_params.get_shard_simplex_config().ok().flatten().is_some() - }; - - simplex_enabled_for_shard - //TODO: LK: enable after change block version to 13 - //&& base.config_params.global_version() - // >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION + true } - #[cfg(not(feature = "simplex"))] + #[cfg(not(feature = "xp25"))] { - #[cfg(feature = "xp25")] - { - true - } - #[cfg(not(feature = "xp25"))] - { - false - } + base.config_params.global_version() + >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION } }; let mut gen_lt = u64::MIN; @@ -2090,14 +2122,19 @@ impl ValidateQuery { ) } - let now = self.engine.now(); - if base.now() > now + 15 { - reject_query!( - "block has creation time {} too much in the future (it is only {} now)", - base.now(), - now - ) + // C++ parity: C++ variants (mainnet, simplex-testnet, alpenglow-work) do not + // reject blocks for being too far in the future. To stay compatible with blocks + // produced by C++ collators, we only emit a warning instead of rejecting. + if !allow_same_timestamp { + let now = self.engine.now(); + if base.now() > now + 15 { + log::warn!( + "block has creation time {} too much in the future (local time is {now})", + base.now(), + ); + } } + if base.info.start_lt() <= mc_data.state.state()?.gen_lt() { reject_query!( "block has start_lt {} less than or equal to lt {} \ @@ -2135,6 +2172,19 @@ impl ValidateQuery { delta_hard ) } + if base.is_simplex { + match base.now_ms { + None => reject_query!("now_ms is not set"), + Some(now_ms) if now_ms / 1000 != base.info.gen_utime() as u64 => { + reject_query!( + "gen_utime is {}, but gen_utime_ms in ConsensusExtraData is {}", + base.info.gen_utime(), + now_ms + ) + } + _ => {} + } + } Ok(()) } @@ -5545,11 +5595,10 @@ impl ValidateQuery { tasks: &mut TasksVec, ) -> Result<()> { // log::debug!(target: "validate_query", "({}): checking all transactions", base.next_block_descr); - let config = BlockchainConfig::with_params( - base.config_params.capabilities(), - base.info.gen_software().map_or(0, |v| v.version), - base.config_params.clone(), - )?; + let (capabilities, block_version) = + base.info.gen_software().map_or((0, 0), |v| (v.capabilities, v.version)); + let config = + BlockchainConfig::with_params(capabilities, block_version, base.config_params.clone())?; base.account_blocks.iterate_with_keys_and_aug(|account_addr, acc_block, _fee| { let base = base.clone(); let libraries = libraries.clone(); diff --git a/src/node/src/validator/validator_group.rs b/src/node/src/validator/validator_group.rs index 33d22a6..f31bb4d 100644 --- a/src/node/src/validator/validator_group.rs +++ b/src/node/src/validator/validator_group.rs @@ -319,8 +319,7 @@ impl ValidatorGroupImpl { } /// Get simplex session pointer for simplex-specific operations (e.g., MC finalization) - /// Returns None if this is not a simplex session or simplex feature is not enabled - #[cfg(feature = "simplex")] + /// Returns None if this is not a simplex session #[allow(dead_code)] pub fn get_simplex_session(&self) -> Option { self.session.as_ref().and_then(|s| s.get_simplex_session()) @@ -420,7 +419,7 @@ impl ValidatorGroupImpl { is_accelerated_consensus_enabled: bool, consensus_type: ConsensusType, ) -> ValidatorGroupImpl { - log::info!(target: "validator", "Initializing session {:x}, shard {}, consensus_type {}", + log::info!(target: "validator", "Initializing session {:x}, shard {}, consensus_type {}", session_id, shard, consensus_type); let prev_block_ids = PrevBlockHistory::with_shard(&shard); @@ -973,6 +972,7 @@ impl ValidatorGroup { let request_clone = request.clone(); let cc_seqno = self.general_session_info.catchain_seqno; let is_masterchain = self.shard.is_masterchain(); + let is_simplex = matches!(self.consensus_options, ConsensusOptions::Simplex(_)); let collation_task = tokio::spawn(async move { log::info!( @@ -1007,6 +1007,7 @@ impl ValidatorGroup { local_key, validator_set.clone(), engine.clone(), + is_simplex, ) .await { @@ -1173,6 +1174,7 @@ impl ValidatorGroup { let last_validation_time = self.last_validation_time.clone(); let cc_seqno = self.general_session_info.catchain_seqno; let is_masterchain = self.shard.is_masterchain(); + let is_simplex = matches!(self.consensus_options, ConsensusOptions::Simplex(_)); let ( expected_current_round, prev_block_ids, @@ -1273,8 +1275,12 @@ impl ValidatorGroup { candidate_block_id ); - let validation_completion_time = - run_validate_query_any_candidate(candidate.clone(), engine.clone()).await?; + let validation_completion_time = run_validate_query_any_candidate( + candidate.clone(), + engine.clone(), + is_simplex, + ) + .await?; // Post-validation: broadcast + save (shared with legacy path) Self::post_validation_actions( @@ -1353,6 +1359,7 @@ impl ValidatorGroup { candidate.clone(), validator_set.clone(), engine.clone(), + is_simplex, ) .await?; diff --git a/src/node/src/validator/validator_manager.rs b/src/node/src/validator/validator_manager.rs index de2451d..49deb54 100644 --- a/src/node/src/validator/validator_manager.rs +++ b/src/node/src/validator/validator_manager.rs @@ -29,25 +29,24 @@ use crate::{ }, }, }; -#[cfg(feature = "simplex")] -use std::sync::atomic::{AtomicU64, Ordering}; use std::{ cmp::{max, min}, collections::{HashMap, HashSet}, convert::TryFrom, fs, ops::RangeInclusive, - sync::Arc, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, time::{Duration, SystemTime}, }; use tokio::time::timeout; use ton_api::IntoBoxed; -#[cfg(feature = "simplex")] -use ton_block::SimplexConfig; use ton_block::{ error, fail, AcceleratedConsensusConfig, BlockIdExt, CatchainConfig, ConfigParamEnum, - ConsensusConfig, FutureSplitMerge, McStateExtra, Result, ShardDescr, ShardIdent, UInt256, - UnixTime, ValidatorDescr, ValidatorSet, + ConsensusConfig, FutureSplitMerge, McStateExtra, Result, ShardDescr, ShardIdent, SimplexConfig, + UInt256, UnixTime, ValidatorDescr, ValidatorSet, }; #[cfg(feature = "xp25")] @@ -56,9 +55,8 @@ const MC_ACCELERATED_CONSENSUS_ENABLED: bool = true; const MC_ACCELERATED_CONSENSUS_ENABLED: bool = false; // When true, use hardcoded testing constants for simplex instead of ConfigParam 30. -// Set to true during testing period for consistent behavior across all nodes. -#[cfg(feature = "simplex")] -const SIMPLEX_USE_TESTING_CONSTANTS: bool = true; +// Set to false for production: reads ConfigParam 30 from masterchain state. +const SIMPLEX_USE_TESTING_CONSTANTS: bool = false; // Magic tag for accelerated consensus session ID differentiation const ACCELERATED_CONSENSUS_MAGIC_TAG: u32 = 0xACCE1E8A; @@ -783,7 +781,6 @@ impl ValidatorManagerImpl { /// - Get `new_consensus_config` from masterchain state /// - If present, create bridge (simplex/null consensus) /// - If absent, create catchain - #[cfg(feature = "simplex")] fn select_consensus_options( &self, shard: &ShardIdent, @@ -794,10 +791,12 @@ impl ValidatorManagerImpl { // During testing period, use hardcoded constants instead of ConfigParam 30 if SIMPLEX_USE_TESTING_CONSTANTS { - let options = ConsensusFactory::create_simplex_options( + let mut options = ConsensusFactory::create_simplex_options( catchain_options.max_block_size as usize, catchain_options.max_collated_data_size as usize, + catchain_options.proto_version as u32, ); + options.proto_version = catchain_options.proto_version; static LAST_WARN: AtomicU64 = AtomicU64::new(0); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -834,7 +833,18 @@ impl ValidatorManagerImpl { } }; - // Get simplex config for mc or shard based on workchain + // C++ compatibility: simplex requires global_version >= 13 + let global_ver = config_params.global_version(); + if global_ver < 13 { + log::trace!( + target: "validator_manager", + "global_version={global_ver} < 13 for {shard}, using catchain" + ); + return ConsensusOptions::Catchain(catchain_options.clone()); + } + + // Get simplex config for mc or shard based on workchain. + // Consensus type is selected by global_version >= 13 AND ConfigParam 30 presence. let simplex_cfg: Option = if shard.is_masterchain() { config_params.get_mc_simplex_config().ok().flatten() } else { @@ -851,12 +861,14 @@ impl ValidatorManagerImpl { cfg.first_block_timeout_ms ); return ConsensusOptions::Simplex(SimplexSessionOptions { + proto_version: catchain_options.proto_version as u32, slots_per_leader_window: cfg.slots_per_leader_window, first_block_timeout: Duration::from_millis(cfg.first_block_timeout_ms as u64), target_rate: Duration::from_millis(cfg.target_rate_ms as u64), // max_block_size and max_collated_data_size come from ConfigParam 29 (via catchain_options) max_block_size: catchain_options.max_block_size as usize, max_collated_data_size: catchain_options.max_collated_data_size as usize, + use_quic: cfg.use_quic, ..Default::default() }); } @@ -869,16 +881,6 @@ impl ValidatorManagerImpl { ConsensusOptions::Catchain(catchain_options.clone()) } - #[cfg(not(feature = "simplex"))] - fn select_consensus_options( - &self, - _shard: &ShardIdent, - _mc_state: &ShardStateStuff, - catchain_options: &CatchainSessionOptions, - ) -> ConsensusOptions { - ConsensusOptions::Catchain(catchain_options.clone()) - } - async fn update_validation_status( &mut self, mc_state: &ShardStateStuff, diff --git a/src/node/tests/compat_test/.gitignore b/src/node/tests/compat_test/.gitignore new file mode 100644 index 0000000..5d3aa6f --- /dev/null +++ b/src/node/tests/compat_test/.gitignore @@ -0,0 +1,7 @@ +# Build artifacts +/build/ +/target/ +Cargo.lock + +# Temporary test databases +/tmp/ diff --git a/src/node/tests/compat_test/Cargo.toml b/src/node/tests/compat_test/Cargo.toml new file mode 100644 index 0000000..26adde0 --- /dev/null +++ b/src/node/tests/compat_test/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "compat_test" +version = "0.1.0" +edition = "2021" +description = "Cross-implementation compatibility tests for ADNL/overlay" +publish = false + +[features] +default = [] +telemetry = ["adnl/telemetry"] + +[dependencies] +adnl = { path = "../../../adnl" } +ton_api = { path = "../../../tl/ton_api" } +ton_block = { path = "../../../block" } + +tokio = { version = "1", features = ["full", "process", "io-util"] } +tokio-util = "0.7" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +base64 = "0.22" +hex = "0.4" +rand = "0.8" +log = "0.4" +env_logger = "0.11" +thiserror = "1" +async-trait = "0.1" +anyhow = "1" + +[dev-dependencies] +tokio-test = "0.4" + +[[test]] +name = "test_overlay_id" +path = "tests/test_overlay_id.rs" + +[[test]] +name = "test_broadcast" +path = "tests/test_broadcast.rs" + +[[test]] +name = "test_public_overlay" +path = "tests/test_public_overlay.rs" + +[[test]] +name = "test_broadcast_validation" +path = "tests/test_broadcast_validation.rs" + +[[test]] +name = "test_overlay_message" +path = "tests/test_overlay_message.rs" + +[[test]] +name = "test_boc_compression" +path = "tests/test_boc_compression.rs" + +[[test]] +name = "test_candidate_id_to_sign" +path = "tests/test_candidate_id_to_sign.rs" + +[[test]] +name = "test_fec_relay" +path = "tests/test_fec_relay.rs" + +[[test]] +name = "test_twostep_fec_relay" +path = "tests/test_twostep_fec_relay.rs" + +[[test]] +name = "test_rldp_query" +path = "tests/test_rldp_query.rs" + +[[test]] +name = "test_quic_transport" +path = "tests/test_quic_transport.rs" + +[[test]] +name = "test_quic_overlay" +path = "tests/test_quic_overlay.rs" diff --git a/src/node/tests/compat_test/Makefile b/src/node/tests/compat_test/Makefile new file mode 100644 index 0000000..f0f87a8 --- /dev/null +++ b/src/node/tests/compat_test/Makefile @@ -0,0 +1,112 @@ +# Cross-Implementation Compatibility Tests Makefile + +# Check if CPP_SRC_PATH is set +ifndef CPP_SRC_PATH +$(error CPP_SRC_PATH is not set. Usage: CPP_SRC_PATH=/path/to/ton-cpp-testnet make test) +endif + +# Directories +ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +WORKSPACE_ROOT := $(ROOT_DIR)/../../.. +BUILD_DIR := $(ROOT_DIR)/build +CPP_BUILD_DIR ?= $(BUILD_DIR)/cpp +RUST_TARGET_DIR := $(BUILD_DIR)/rust + +# C++ source files location +CPP_TEST_SRC := $(ROOT_DIR)/cpp_src + +# Output binary +CPP_TEST_BIN := $(CPP_BUILD_DIR)/compat_test_node + +# CMake settings +CMAKE_BUILD_TYPE ?= Release +CMAKE_JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + +# Rust test settings +RUST_TEST_THREADS ?= 1 +TEST ?= + +.PHONY: all build build-cpp build-rust test test-rust clean help check-cpp-path + +all: build + +help: + @echo "Cross-Implementation Compatibility Tests" + @echo "" + @echo "Usage: CPP_SRC_PATH=/path/to/ton-cpp-testnet make " + @echo "" + @echo "Targets:" + @echo " build - Build both C++ and Rust components" + @echo " build-cpp - Build C++ test harness only" + @echo " build-rust - Build Rust test code only" + @echo " test - Run all compatibility tests" + @echo " test-rust - Run Rust tests (requires C++ binary)" + @echo " clean - Remove build artifacts" + @echo " help - Show this help message" + @echo "" + @echo "Environment Variables:" + @echo " CPP_SRC_PATH - Path to C++ TON source (required)" + @echo " CPP_BUILD_DIR - C++ build directory (default: ./build/cpp)" + @echo " CMAKE_BUILD_TYPE - CMake build type (default: Release)" + @echo " TEST - Specific test to run (optional)" + @echo " RUST_LOG - Rust log level (optional)" + +check-cpp-path: + @if [ ! -d "$(CPP_SRC_PATH)/adnl" ]; then \ + echo "Error: $(CPP_SRC_PATH) does not appear to be a valid TON C++ source directory"; \ + echo "Expected to find $(CPP_SRC_PATH)/adnl"; \ + exit 1; \ + fi + @echo "Using C++ source: $(CPP_SRC_PATH)" + +build: build-cpp build-rust + +build-cpp: check-cpp-path + @echo "Building C++ test harness..." + @echo "Note: TON C++ libraries must be pre-built in $(CPP_SRC_PATH)/build" + @echo "If not built, run: cd $(CPP_SRC_PATH) && mkdir -p build && cd build && cmake .. && make overlay adnl dht" + @mkdir -p $(CPP_BUILD_DIR) + @cd $(CPP_BUILD_DIR) && cmake \ + -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \ + -DTON_SRC_PATH=$(CPP_SRC_PATH) \ + $(CPP_TEST_SRC) + @cd $(CPP_BUILD_DIR) && cmake --build . -j$(CMAKE_JOBS) + @mkdir -p $(CPP_TEST_SRC)/build + @cp $(CPP_TEST_BIN) $(CPP_TEST_SRC)/build/compat_test_node + @if [ "$$(uname)" = "Darwin" ]; then codesign --force --sign - $(CPP_TEST_SRC)/build/compat_test_node; fi + @echo "C++ test harness built: $(CPP_TEST_BIN)" + +build-rust: + @echo "Building Rust test code..." + @cd $(WORKSPACE_ROOT) && cargo build --package compat_test + @echo "Rust test code built" + +test: build test-rust + +test-rust: + @if [ ! -f "$(CPP_TEST_BIN)" ]; then \ + echo "Error: C++ test binary not found at $(CPP_TEST_BIN)"; \ + echo "Run 'make build-cpp' first"; \ + exit 1; \ + fi + @echo "Running compatibility tests..." + @cd $(WORKSPACE_ROOT) && \ + CPP_COMPAT_TEST_BIN=$(CPP_TEST_BIN) \ + RUST_TEST_THREADS=$(RUST_TEST_THREADS) \ + RUST_BACKTRACE=1 \ + cargo test --package compat_test \ + $(if $(TEST),--test $(TEST),) + +clean: + @echo "Cleaning build artifacts..." + @rm -rf $(BUILD_DIR) + @echo "Clean complete" + +# Development helpers +.PHONY: fmt clippy + +fmt: + @cd $(WORKSPACE_ROOT) && cargo fmt --package compat_test + +clippy: + @cd $(WORKSPACE_ROOT) && cargo clippy --package compat_test diff --git a/src/node/tests/compat_test/README.md b/src/node/tests/compat_test/README.md new file mode 100644 index 0000000..375ae2b --- /dev/null +++ b/src/node/tests/compat_test/README.md @@ -0,0 +1,281 @@ +# Cross-Implementation Compatibility Tests + +This directory contains tests that verify compatibility between the Rust ADNL/overlay implementation and the C++ reference implementation. + +## Prerequisites + +1. **C++ TON source code**: You need access to the C++ TON node source code (ton-cpp-testnet) +2. **Pre-built C++ libraries**: The C++ TON libraries must be built before running tests +3. **C++ build dependencies**: CMake, C++ compiler (with C++20 support), OpenSSL, ZLIB, etc. +4. **Rust toolchain**: cargo, rustc + +## Directory Structure + +``` +compat_test/ +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ Cargo.toml # Rust package manifest +โ”œโ”€โ”€ Makefile # Build and test automation +โ”œโ”€โ”€ incompatibilities.md # Detailed compatibility report and found bugs +โ”œโ”€โ”€ cpp_src/ # C++ test harness source code +โ”‚ โ”œโ”€โ”€ CMakeLists.txt +โ”‚ โ”œโ”€โ”€ compat_test_node.cpp +โ”‚ โ””โ”€โ”€ compat_test_node.hpp +โ”œโ”€โ”€ src/ # Rust library code +โ”‚ โ”œโ”€โ”€ lib.rs # CppTestNode wrapper and JSON protocol +โ”‚ โ”œโ”€โ”€ overlay_id.rs # Overlay ID computation helpers +โ”‚ โ””โ”€โ”€ test_helpers.rs # RustTestNode, RustQuicTestNode, and test utilities +โ”œโ”€โ”€ tests/ # Rust integration tests +โ”‚ โ”œโ”€โ”€ test_overlay_id.rs # Overlay ID computation compatibility +โ”‚ โ”œโ”€โ”€ test_broadcast.rs # Broadcast delivery (small + FEC, both directions) +โ”‚ โ”œโ”€โ”€ test_broadcast_validation.rs # 2-phase broadcast accept/reject callbacks +โ”‚ โ”œโ”€โ”€ test_public_overlay.rs # Overlay query/response compatibility +โ”‚ โ”œโ”€โ”€ test_overlay_message.rs # Point-to-point overlay messages +โ”‚ โ”œโ”€โ”€ test_boc_compression.rs # BOC compression interoperability +โ”‚ โ”œโ”€โ”€ test_candidate_id_to_sign.rs # Consensus candidate ID TL serialization +โ”‚ โ”œโ”€โ”€ test_rldp_query.rs # RLDP v1/v2 query/response (multiple sizes) +โ”‚ โ”œโ”€โ”€ test_fec_relay.rs # FEC broadcast relay (3-node topology) +โ”‚ โ”œโ”€โ”€ test_twostep_fec_relay.rs # TwostepFec broadcast relay (6-node topology) +โ”‚ โ”œโ”€โ”€ test_quic_transport.rs # QUIC transport: raw queries, large messages, TLS +โ”‚ โ”œโ”€โ”€ test_quic_overlay.rs # QUIC overlay: messages and queries via QUIC +โ”‚ โ””โ”€โ”€ test_quic_private_overlay.rs # QUIC private overlay: ADNL vs QUIC transport +โ””โ”€โ”€ build/ # Build artifacts (gitignored) + โ””โ”€โ”€ cpp/ # C++ binary output +``` + +## Usage + +### Building C++ TON Libraries (One-time Setup) + +Before running tests, you must build the C++ TON libraries: + +```bash +cd /path/to/ton-cpp-testnet +mkdir -p build && cd build +cmake .. +cmake --build . --target overlay adnl dht tl_api keys keyring fec rldp rldp2 tdutils tdactor tdnet ton_crypto +``` + +### Running All Tests + +```bash +CPP_SRC_PATH=/path/to/ton-cpp-testnet make test +``` + +### Running Specific Test Suite + +```bash +CPP_SRC_PATH=/path/to/ton-cpp-testnet make test TEST=test_broadcast +``` + +### Building Only + +```bash +CPP_SRC_PATH=/path/to/ton-cpp-testnet make build +``` + +### Cleaning Build Artifacts + +```bash +make clean +``` + +## Compatibility Status + +| Test Suite | Tests | Pass | Ignored | Status | +|------------|-------|------|---------|--------| +| `test_overlay_id` | 4 | 4 | 0 | Compatible | +| `test_broadcast` | 4 | 4 | 0 | Compatible | +| `test_broadcast_validation` | 4 | 4 | 0 | Compatible | +| `test_public_overlay` | 2 | 2 | 0 | Compatible | +| `test_overlay_message` | 5 | 4 | 1 | 1 ignored (Safe/RLDP) | +| `test_boc_compression` | 4 | 4 | 0 | Compatible | +| `test_candidate_id_to_sign` | 2 | 2 | 0 | Compatible | +| `test_rldp_query` | 8 | 8 | 0 | Compatible | +| `test_fec_relay` | 4 | 4 | 0 | Compatible | +| `test_twostep_fec_relay` | 4 | 4 | 0 | Compatible | +| `test_quic_transport` | 3 | 3 | 0 | Compatible | +| `test_quic_overlay` | 4 | 4 | 0 | Compatible | +| `test_quic_private_overlay` | 5 | 5 | 0 | Compatible | +| **Total** | **53** | **52** | **1** | | + +## Test Suites + +### 1. Overlay ID (`test_overlay_id`) +- Rust and C++ compute identical overlay short IDs for various name formats (ASCII, binary, Unicode) +- C++ harness infrastructure checks (ping, ADNL ID) + +### 2. Broadcasts (`test_broadcast`) +Small (inline) and FEC-encoded (2KB) broadcasts in both directions: + +| Test | Direction | Payload | Result | +|------|-----------|---------|--------| +| `test_broadcast_rust_to_cpp` | Rust โ†’ C++ | small (26 B) | PASS | +| `test_broadcast_cpp_to_rust` | C++ โ†’ Rust | small (25 B) | PASS | +| `test_fec_broadcast_rust_to_cpp` | Rust โ†’ C++ | FEC (2 KB) | PASS | +| `test_fec_broadcast_cpp_to_rust` | C++ โ†’ Rust | FEC (2 KB) | PASS | + +### 3. Broadcast Validation (`test_broadcast_validation`) +- 2-phase `check_broadcast` accept/reject callback (Rust sender โ†’ C++ receiver) +- Accept mode: broadcast delivered to application layer +- Reject mode: broadcast dropped, not delivered +- Validator mode toggling + +### 4. Overlay Queries (`test_public_overlay`) +- Query/response echo roundtrip (C++ โ†’ Rust) +- Query rejection behavior (Rust โ†’ C++, expects timeout โ€” C++ drops rejected queries silently) + +### 5. Overlay Messages (`test_overlay_message`) +Point-to-point overlay messages (the same path used by simplex consensus for votes and certificates): + +| Test | Direction | What | Result | +|------|-----------|------|--------| +| `test_overlay_message_cpp_to_rust` | C++ โ†’ Rust | Single message, receipt verified | PASS | +| `test_overlay_message_rust_to_cpp` | Rust โ†’ C++ | Single message via Fast/UDP | PASS | +| `test_overlay_message_rust_to_cpp_safe` | Rust โ†’ C++ | Single message via Safe/RLDP | IGNORED | +| `test_overlay_message_burst_rust_to_cpp` | Rust โ†’ C++ | 20 messages, โ‰ฅ90% delivery | PASS | +| `test_overlay_message_cpp_to_cpp_baseline` | C++ โ†” C++ | Baseline (no Rust) | PASS | + +### 6. BOC Compression (`test_boc_compression`) +- Bidirectional compress/decompress with `BaselineLZ4` and `ImprovedStructureLZ4` algorithms +- Three cell topologies: single cell, tree with shared refs (DAG), simple tree +- Full round-trip (Rust compress โ†’ C++ decompress โ†’ C++ compress โ†’ Rust decompress) +- Multi-root BOC in both directions + +### 7. Candidate ID Signing (`test_candidate_id_to_sign`) +- TL serialization byte match for `consensus.candidateId` across 4 (slot, hash) combos +- Negative check: verifies C++ signs `candidateId` directly, not `candidateParent` wrapper + +### 8. RLDP Query/Response (`test_rldp_query`) +Both RLDP v1 and v2, both directions, three payload sizes: + +| Test | Sender | Responder | RLDP | Payload | Result | +|------|--------|-----------|------|---------|--------| +| `test_rldp_v1_rust_to_cpp` | Rust | C++ | v1 | 256 B | PASS | +| `test_rldp_v1_cpp_to_rust` | C++ | Rust | v1 | 256 B | PASS | +| `test_rldp_v2_rust_to_cpp` | Rust | C++ | v2 | 256 B | PASS | +| `test_rldp_v2_cpp_to_rust` | C++ | Rust | v2 | 256 B | PASS | +| `test_rldp_v2_4kb_rust_to_cpp` | Rust | C++ | v2 | 4 KB | PASS | +| `test_rldp_v2_4kb_cpp_to_rust` | C++ | Rust | v2 | 4 KB | PASS | +| `test_rldp_v2_7kb_rust_to_cpp` | Rust | C++ | v2 | 7 KB | PASS | +| `test_rldp_v2_7kb_cpp_to_rust` | C++ | Rust | v2 | 7 KB | PASS | + +**Note**: Query data must be a valid TL-serialized object (not raw bytes) because Rust's `deserialize_boxed_bundle` requires it. + +**Payload size constraints**: +- RLDP v1 `default_mtu` = 1024 bytes โ€” limits unsolicited incoming transfers to ~928 bytes of user data +- RLDP v2 `DEFAULT_MTU` = 7680 bytes โ€” allows up to ~7.5 KB without configuration +- C++ overlay `huge_packet_max_size()` = 8192 bytes โ€” hard limit on query data before RLDP wrapping + +### 9. FEC Relay (`test_fec_relay`) +3-node linear topology (Sender โ†’ Relay โ†’ Receiver), sender and receiver NOT directly connected. Broadcast size: 2000 bytes (triggers FEC encoding at >768 bytes): + +| Test | Sender | Relay | Receiver | Result | +|------|--------|-------|----------|--------| +| `test_fec_relay_rust_cpp_rust` | Rust | C++ | Rust | PASS | +| `test_fec_relay_cpp_rust_cpp` | C++ | Rust | C++ | PASS | +| `test_fec_relay_rust_rust_cpp` | Rust | Rust | C++ | PASS | +| `test_fec_relay_cpp_cpp_rust` | C++ | C++ | Rust | PASS | + +### 10. TwostepFec Relay (`test_twostep_fec_relay`) +6-node topology (Sender โ†’ 4 Bridges โ†’ Leaf), leaf NOT directly connected to sender. Broadcast size: 2048 bytes (>= 513 bytes triggers TwostepFec with FEC encoding): + +| Test | Sender | Bridges | Leaf | Result | +|------|--------|---------|------|--------| +| `test_twostep_rust_sender_cpp_leaf` | Rust | 4 Rust | C++ | PASS | +| `test_twostep_cpp_sender_rust_leaf` | C++ | 4 C++ | Rust | PASS | +| `test_twostep_mixed_bridges_rust_leaf` | Rust | 2 Rust + 2 C++ | Rust | PASS | +| `test_twostep_mixed_bridges_cpp_leaf` | C++ | 2 Rust + 2 C++ | C++ | PASS | + +### 11. QUIC Transport (`test_quic_transport`) +- Raw QUIC query echo (C++ โ†’ Rust) with TL-serialized payload +- Large overlay message via QUIC (900B, near C++ 1024-byte per-stream limit) +- QUIC connection establishment โ€” TLS handshake with RPK (Raw Public Key) certificates + +### 12. QUIC Overlay (`test_quic_overlay`) +- C++ โ†” C++ QUIC overlay message baseline +- Overlay message via QUIC (Rust โ†’ C++, with UDP baseline comparison) +- Raw QUIC message delivery (C++ โ†’ Rust) +- Overlay query via QUIC (Rust โ†’ C++) with echo handler + +### 13. QUIC Private Overlay (`test_quic_private_overlay`) +- Private overlay message via ADNL (baseline) +- Private overlay message via QUIC transport (Rust โ†’ C++) +- QUIC message burst (20 messages, 100% delivery required โ€” stream-based, no UDP loss) +- QUIC overlay query (Rust โ†’ C++) +- Private overlay message (C++ โ†’ Rust, with receipt verification) + +## Environment Variables + +| Variable | Description | Required | +|----------|-------------|----------| +| `CPP_SRC_PATH` | Path to C++ TON source (ton-cpp-testnet) | Yes | +| `CPP_BUILD_DIR` | Path for C++ test binary build (default: `./build/cpp`) | No | +| `CMAKE_BUILD_TYPE` | CMake build type (default: `Release`) | No | +| `TEST` | Specific test suite name filter | No | +| `RUST_LOG` | Rust logging level (e.g., `debug`, `trace`) | No | +| `RUST_TEST_THREADS` | Number of test threads (default: `1` for serial execution) | No | + +## C++ Test Node Protocol + +The C++ test node (`compat_test_node`) communicates via JSON over stdin/stdout: + +```json +// Commands: +{"cmd": "ping"} +{"cmd": "add_peer", "pubkey": "BASE64_TL_PUBKEY", "ip": "127.0.0.1", "port": 14001} +{"cmd": "create_overlay", "type": "private", "overlay_name": "BASE64", "peers": ["ADNL_ID_HEX"]} +{"cmd": "send_broadcast", "overlay_id": "HEX", "data": "BASE64", "use_fec": false} +{"cmd": "send_message", "overlay_id": "HEX", "peer_adnl_id": "HEX", "data": "BASE64"} +{"cmd": "set_broadcast_validator", "overlay_id": "HEX", "mode": "accept_all|reject_all"} +{"cmd": "set_query_handler", "overlay_id": "HEX", "mode": "echo|reject"} +{"cmd": "get_received_broadcasts", "overlay_id": "HEX"} +{"cmd": "get_received_messages", "overlay_id": "HEX"} +{"cmd": "send_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", "data": "BASE64", "timeout_ms": 5000} +{"cmd": "send_rldp_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", "data": "BASE64", "max_answer_size": 1048576, "v2": true} +{"cmd": "enable_quic"} +{"cmd": "send_quic_message", "peer_adnl_id": "HEX", "data": "BASE64"} +{"cmd": "send_quic_query", "peer_adnl_id": "HEX", "data": "BASE64", "timeout_ms": 5000} +{"cmd": "shutdown"} + +// Responses: +{"result": ...} +{"error": "..."} +``` + +## Port Ranges + +Tests use different port ranges to avoid conflicts: +- `test_overlay_id`: 14010-14019 +- `test_broadcast`: 15100-15149 +- `test_public_overlay`: 15150-15199 +- `test_broadcast_validation`: 15300-15399 +- `test_overlay_message`: 15400-15499 +- `test_boc_compression`: 15500-15599 +- `test_fec_relay`: 15600-15699 +- `test_twostep_fec_relay`: 15700-15799 +- `test_rldp_query`: 15800-15899 +- `test_candidate_id_to_sign`: 15900-15909 +- `test_quic_transport`: 18000-18099 +- `test_quic_overlay`: 18100-18199 +- `test_quic_private_overlay`: 18200-18299 + +## Troubleshooting + +### C++ Build Fails +- Ensure C++ TON libraries are pre-built in `$CPP_SRC_PATH/build` +- Check that CMake can find OpenSSL and ZLIB +- Verify C++20 compiler support + +### Tests Timeout +- Ensure no firewall blocks UDP ports 14000-19000 on localhost +- Check that no other processes use the same ports +- Try increasing sleep durations in tests if running on slow hardware + +### "broadcast source certificate is invalid" +This error in C++ logs indicates the overlay privacy rules are too restrictive. The test node should use `AllowFec` flag without `Trusted` to enable 2-phase validation. + +### Overlay ID Mismatch +If Rust and C++ compute different overlay IDs: +- Verify the overlay name bytes are identical (check base64 encoding) +- Ensure both use the TL `pub.overlay{name}` wrapper before hashing diff --git a/src/node/tests/compat_test/cpp_src/CMakeLists.txt b/src/node/tests/compat_test/cpp_src/CMakeLists.txt new file mode 100644 index 0000000..f0f2749 --- /dev/null +++ b/src/node/tests/compat_test/cpp_src/CMakeLists.txt @@ -0,0 +1,258 @@ +cmake_minimum_required(VERSION 3.16) +project(compat_test_node) + +# Require paths to be set +if(NOT DEFINED TON_SRC_PATH) + message(FATAL_ERROR "TON_SRC_PATH must be defined. Pass -DTON_SRC_PATH=/path/to/ton-cpp-testnet") +endif() + +if(NOT DEFINED TON_BUILD_PATH) + # Default to build directory inside source + set(TON_BUILD_PATH "${TON_SRC_PATH}/build") +endif() + +if(NOT EXISTS "${TON_SRC_PATH}/adnl") + message(FATAL_ERROR "TON_SRC_PATH (${TON_SRC_PATH}) does not appear to be a valid TON source directory") +endif() + +if(NOT EXISTS "${TON_BUILD_PATH}") + message(FATAL_ERROR "TON_BUILD_PATH (${TON_BUILD_PATH}) does not exist. Please build TON first:\n" + " cd ${TON_SRC_PATH}\n" + " mkdir build && cd build\n" + " cmake ..\n" + " cmake --build . --target overlay adnl adnltest dht tl_api keys keyring fec rldp rldp2 tdutils tdactor tdnet ton_crypto") +endif() + +message(STATUS "Using TON source: ${TON_SRC_PATH}") +message(STATUS "Using TON build: ${TON_BUILD_PATH}") + +# Set C++ standard (TON requires C++20 for 'requires' expressions) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find required packages (same as TON uses) +find_package(OpenSSL REQUIRED) +find_package(ZLIB REQUIRED) +find_package(Threads REQUIRED) + +# Our test binary +add_executable(compat_test_node + compat_test_node.cpp +) + +# Include directories from TON source +target_include_directories(compat_test_node PRIVATE + ${TON_SRC_PATH} + ${TON_SRC_PATH}/tdutils + ${TON_SRC_PATH}/tdactor + ${TON_SRC_PATH}/tdnet + ${TON_SRC_PATH}/crypto + ${TON_SRC_PATH}/tl + ${TON_SRC_PATH}/tl-utils + ${TON_SRC_PATH}/keys + ${TON_SRC_PATH}/keyring + ${TON_SRC_PATH}/adnl + ${TON_SRC_PATH}/overlay + ${TON_SRC_PATH}/rldp + ${TON_SRC_PATH}/rldp2 + ${TON_SRC_PATH}/fec + ${TON_SRC_PATH}/quic + ${TON_SRC_PATH}/metrics + ${TON_SRC_PATH}/tddb + ${TON_SRC_PATH}/third-party/abseil-cpp + ${TON_SRC_PATH}/third-party/ngtcp2/lib/includes + ${TON_SRC_PATH}/third-party/ngtcp2/crypto/includes + ${TON_BUILD_PATH}/third-party/ngtcp2/lib/includes + ${TON_SRC_PATH}/tl/generate # For auto-generated TL headers + ${TON_BUILD_PATH} + ${TON_BUILD_PATH}/tdutils +) + +# Find libraries in TON build directory +set(TON_LIB_DIRS + ${TON_BUILD_PATH}/overlay + ${TON_BUILD_PATH}/adnl + ${TON_BUILD_PATH}/dht + ${TON_BUILD_PATH}/rldp + ${TON_BUILD_PATH}/rldp2 + ${TON_BUILD_PATH}/fec + ${TON_BUILD_PATH}/tl + ${TON_BUILD_PATH}/tl-utils + ${TON_BUILD_PATH}/keys + ${TON_BUILD_PATH}/keyring + ${TON_BUILD_PATH}/tdutils + ${TON_BUILD_PATH}/tdactor + ${TON_BUILD_PATH}/tdnet + ${TON_BUILD_PATH}/tddb + ${TON_BUILD_PATH}/tddb/td/db + ${TON_BUILD_PATH}/tdfec + ${TON_BUILD_PATH}/tdfec/td/fec + ${TON_BUILD_PATH}/crypto + ${TON_BUILD_PATH}/common + ${TON_BUILD_PATH}/quic + ${TON_BUILD_PATH}/metrics + ${TON_BUILD_PATH}/third-party/crc32c + ${TON_BUILD_PATH}/third-party/libraptorq + ${TON_BUILD_PATH}/third-party/rocksdb + ${TON_BUILD_PATH}/third-party/ngtcp2/lib + ${TON_BUILD_PATH}/third-party/ngtcp2/crypto/ossl +) + +# Function to find a library in TON build dirs +function(find_ton_library VAR_NAME LIB_NAME) + find_library(${VAR_NAME} + NAMES ${LIB_NAME} + PATHS ${TON_LIB_DIRS} + PATH_SUFFIXES Release Debug RelWithDebInfo + NO_DEFAULT_PATH + ) + if(NOT ${VAR_NAME}) + message(FATAL_ERROR "Could not find TON library: ${LIB_NAME}\n" + "Make sure you have built TON with: cmake --build . --target ${LIB_NAME}") + endif() + message(STATUS "Found ${LIB_NAME}: ${${VAR_NAME}}") +endfunction() + +# Find TON libraries +find_ton_library(TON_OVERLAY overlay) +find_ton_library(TON_ADNL adnl) +find_ton_library(TON_DHT dht) +find_ton_library(TON_RLDP rldp) +find_ton_library(TON_RLDP2 rldp2) +find_ton_library(TON_FEC fec) +find_ton_library(TON_TL_API tl_api) +find_ton_library(TON_TL_UTILS tl-utils) +find_ton_library(TON_KEYS keys) +find_ton_library(TON_KEYRING keyring) +find_ton_library(TON_TDUTILS tdutils) +find_ton_library(TON_TDACTOR tdactor) +find_ton_library(TON_TDNET tdnet) +find_ton_library(TON_TDDB tddb) +find_ton_library(TON_TDFEC tdfec) +find_ton_library(TON_CRYPTO ton_crypto) +find_ton_library(TON_CRYPTO_CORE ton_crypto_core) +find_ton_library(TON_COMMON common) +find_ton_library(TON_TON_BLOCK ton_block) +find_ton_library(TON_CRC32C crc32c) +find_ton_library(TON_ROCKSDB rocksdb) +find_ton_library(TON_QUIC quic) +find_ton_library(TON_METRICS metrics) + +# ngtcp2 libraries (required by quic) +find_library(NGTCP2_LIB + NAMES ngtcp2 + PATHS ${TON_BUILD_PATH}/third-party/ngtcp2/lib + NO_DEFAULT_PATH +) +find_library(NGTCP2_CRYPTO_OSSL_LIB + NAMES ngtcp2_crypto_ossl + PATHS ${TON_BUILD_PATH}/third-party/ngtcp2/crypto/ossl + NO_DEFAULT_PATH +) + +# Optional: RaptorQ library (for FEC) +find_library(RAPTORQ_LIB + NAMES RaptorQ + PATHS ${TON_BUILD_PATH}/third-party/libraptorq + NO_DEFAULT_PATH +) + +target_link_libraries(compat_test_node PRIVATE + ${TON_QUIC} + ${TON_METRICS} + ${TON_OVERLAY} + ${TON_ADNL} + ${TON_DHT} + ${TON_RLDP} + ${TON_RLDP2} + ${TON_FEC} + ${TON_TDFEC} + ${TON_TL_API} + ${TON_TL_UTILS} + ${TON_KEYS} + ${TON_KEYRING} + ${TON_COMMON} + ${TON_TON_BLOCK} + ${TON_TDDB} + ${TON_TDUTILS} + ${TON_TDACTOR} + ${TON_TDNET} + ${TON_CRYPTO} + ${TON_CRYPTO_CORE} + ${TON_CRC32C} + ${TON_ROCKSDB} + OpenSSL::Crypto + OpenSSL::SSL + ZLIB::ZLIB + Threads::Threads +) + +# Add ngtcp2 if found (required by quic) +if(NGTCP2_LIB) + target_link_libraries(compat_test_node PRIVATE ${NGTCP2_LIB}) +endif() +if(NGTCP2_CRYPTO_OSSL_LIB) + target_link_libraries(compat_test_node PRIVATE ${NGTCP2_CRYPTO_OSSL_LIB}) +endif() + +# Add RaptorQ if found +if(RAPTORQ_LIB) + target_link_libraries(compat_test_node PRIVATE ${RAPTORQ_LIB}) +endif() + +# Platform-specific settings +if(APPLE) + target_link_libraries(compat_test_node PRIVATE + "-framework Security" + "-framework CoreFoundation" + "-framework CoreServices" + ) + target_link_libraries(compat_test_node PRIVATE c++) +elseif(UNIX) + target_link_libraries(compat_test_node PRIVATE dl) +endif() + +# Add additional libraries that TON depends on +find_library(LZ4_LIBRARY lz4) +if(LZ4_LIBRARY) + target_link_libraries(compat_test_node PRIVATE ${LZ4_LIBRARY}) +endif() + +find_library(SODIUM_LIBRARY sodium) +if(SODIUM_LIBRARY) + target_link_libraries(compat_test_node PRIVATE ${SODIUM_LIBRARY}) +endif() + +# blst (BLS crypto, required by ton_crypto) +find_library(BLST_LIBRARY + NAMES blst + PATHS ${TON_BUILD_PATH}/third-party/blst + NO_DEFAULT_PATH +) +if(BLST_LIBRARY) + target_link_libraries(compat_test_node PRIVATE ${BLST_LIBRARY}) +endif() + +# secp256k1 (required by ton_crypto_core) +find_library(SECP256K1_LIBRARY + NAMES secp256k1 + PATHS ${TON_BUILD_PATH}/third-party/secp256k1/lib + NO_DEFAULT_PATH +) +if(SECP256K1_LIBRARY) + target_link_libraries(compat_test_node PRIVATE ${SECP256K1_LIBRARY}) +endif() + +# Abseil libraries (required by ton_block hash containers) +file(GLOB_RECURSE ABSEIL_LIBS "${TON_BUILD_PATH}/third-party/abseil-cpp/absl/*.a") +if(ABSEIL_LIBS) + target_link_libraries(compat_test_node PRIVATE ${ABSEIL_LIBS}) +endif() + +# macOS: re-sign binary to avoid dyld hang caused by com.apple.provenance xattr +if(APPLE) + add_custom_command(TARGET compat_test_node POST_BUILD + COMMAND codesign --force --sign - $ + COMMENT "Re-signing binary for macOS" + ) +endif() diff --git a/src/node/tests/compat_test/cpp_src/compat_test_node.cpp b/src/node/tests/compat_test/cpp_src/compat_test_node.cpp new file mode 100644 index 0000000..87e6af6 --- /dev/null +++ b/src/node/tests/compat_test/cpp_src/compat_test_node.cpp @@ -0,0 +1,1296 @@ +/* + * Cross-Implementation Compatibility Test Node + * + * A minimal ADNL/overlay node for testing compatibility between rust and cpp implementations. + * + * Controlled via stdin/stdout by end of line terminated JSON: + * + * {"cmd": "ping"} + * {"cmd": "get_info"} + * {"cmd": "compute_overlay_id", "name": "BASE64_BYTES"} + * {"cmd": "add_peer", "pubkey": "BASE64_TL_PUBKEY", "ip": "127.0.0.1", "port": 14001} + * {"cmd": "create_overlay", "type": "public|private|semiprivate", + * "overlay_name": "BASE64_TL_BYTES", + * "peers": ["ADNL_ID_HEX", ...], + * "root_pub_keys": ["HEX", ...], "certificate": "BASE64_TL", "max_slaves": 5} + * {"cmd": "delete_overlay", "overlay_id": "HEX"} + * {"cmd": "get_overlay_node_info", "overlay_id": "HEX"} + * {"cmd": "send_broadcast", "overlay_id": "HEX", "data": "BASE64", "use_fec": false} + * {"cmd": "send_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", + * "data": "BASE64", "timeout_ms": 5000} + * {"cmd": "send_rldp_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", + * "data": "BASE64", "max_answer_size": 1048576, "v2": false} + * {"cmd": "set_query_handler", "overlay_id": "HEX", "mode": "echo|capabilities|reject"} + * {"cmd": "set_broadcast_validator", "overlay_id": "HEX", "mode": "accept_all|reject_all"} + * {"cmd": "get_received_broadcasts", "overlay_id": "HEX"} + * {"cmd": "clear_received_broadcasts", "overlay_id": "HEX"} + * {"cmd": "compute_candidate_id_to_sign", "slot": 1, "hash": "HEX_64"} + * {"cmd": "compress_boc", "data": "BASE64_STANDARD_BOC", "algorithm": "baseline|improved"} + * {"cmd": "decompress_boc", "data": "BASE64_COMPRESSED", "max_size": 10485760} + * {"cmd": "shutdown"} + */ + +#include "compat_test_node.hpp" + +#include "td/utils/port/Stat.h" +#include "td/utils/port/path.h" +#include "td/utils/Random.h" +#include "crypto/Ed25519.h" +#include "vm/boc.h" +#include "vm/boc-compression.h" + +#include +#include + +namespace compat_test { + +// ---------- Helpers ---------- + +CompatTestNode::CompatTestNode(Config config) : config_(std::move(config)) {} + +std::string CompatTestNode::get_string(td::JsonObject &obj, const std::string &key) { + for (auto &kv : obj.field_values_) { + if (kv.first == key && kv.second.type() == td::JsonValue::Type::String) { + return kv.second.get_string().str(); + } + } + return ""; +} + +bool CompatTestNode::get_bool(td::JsonObject &obj, const std::string &key, bool def) { + for (auto &kv : obj.field_values_) { + if (kv.first == key && kv.second.type() == td::JsonValue::Type::Boolean) { + return kv.second.get_boolean(); + } + } + return def; +} + +td::int64 CompatTestNode::get_int(td::JsonObject &obj, const std::string &key, td::int64 def) { + for (auto &kv : obj.field_values_) { + if (kv.first == key && kv.second.type() == td::JsonValue::Type::Number) { + return td::to_integer(kv.second.get_number()); + } + } + return def; +} + +void CompatTestNode::respond(const std::string &json) { + std::cout << json << std::endl; + std::cout.flush(); +} + +void CompatTestNode::respond_ok() { + respond("{\"result\": \"ok\"}"); +} + +void CompatTestNode::respond_ok(const std::string &extra_fields) { + respond("{\"result\": {" + extra_fields + "}}"); +} + +void CompatTestNode::respond_error(const std::string &msg) { + // Escape quotes in msg + std::string escaped; + for (char c : msg) { + if (c == '"') escaped += "\\\""; + else if (c == '\\') escaped += "\\\\"; + else if (c == '\n') escaped += "\\n"; + else escaped += c; + } + respond("{\"error\": \"" + escaped + "\"}"); +} + +// ---------- Startup ---------- + +void CompatTestNode::start_up() { + LOG(INFO) << "Starting compat test node on UDP port " << config_.udp_port; + + // Create database directory + td::mkdir(config_.db_path).ignore(); + + // Generate local key + local_privkey_ = ton::PrivateKey{ton::privkeys::Ed25519::random()}; + local_pubkey_ = local_privkey_.compute_public_key(); + local_id_short_ = ton::adnl::AdnlNodeIdShort{local_pubkey_.compute_short_id()}; + + LOG(INFO) << "Local ADNL ID: " << local_id_short_.bits256_value().to_hex(); + + // Create keyring + keyring_ = ton::keyring::Keyring::create(config_.db_path + "/keyring"); + + // Add local key to keyring - generate a new key for keyring since we can't clone + auto keyring_privkey = ton::PrivateKey{ton::privkeys::Ed25519::random()}; + // Actually, we need to use the same key. Let's re-generate with the same approach + // and store export/import to share between local and keyring + auto key_slice = local_privkey_.export_as_slice(); + auto key_import = ton::PrivateKey::import(key_slice.as_slice()); + CHECK(key_import.is_ok()); + td::actor::send_closure(keyring_, &ton::keyring::Keyring::add_key, + key_import.move_as_ok(), true, + td::PromiseCreator::lambda([](td::Result) {})); + + // Create network manager with real UDP + network_manager_ = ton::adnl::AdnlNetworkManager::create(config_.udp_port); + + // Register self address on network manager + td::IPAddress self_addr; + self_addr.init_ipv4_port("127.0.0.1", config_.udp_port).ensure(); + + ton::adnl::AdnlCategoryMask cat_mask; + cat_mask[0] = true; + td::actor::send_closure(network_manager_, &ton::adnl::AdnlNetworkManager::add_self_addr, + self_addr, std::move(cat_mask), 0); + + // Create ADNL instance + adnl_ = ton::adnl::Adnl::create(config_.db_path, keyring_.get()); + + // Register network manager with ADNL + td::actor::send_closure(adnl_, &ton::adnl::Adnl::register_network_manager, + network_manager_.get()); + + // Build proper address list for our identity + ton::adnl::AdnlAddressList addr_list; + addr_list.add_udp_adnl_address(self_addr).ensure(); + addr_list.set_version(static_cast(td::Clocks::system())); + addr_list.set_reinit_date(ton::adnl::Adnl::adnl_start_time()); + + // Add local ID to ADNL with proper address list + td::actor::send_closure(adnl_, &ton::adnl::Adnl::add_id, + ton::adnl::AdnlNodeIdFull{local_pubkey_}, + std::move(addr_list), static_cast(0)); + + // Create RLDP v1 and v2 + rldp_ = ton::rldp::Rldp::create(adnl_.get()); + rldp2_ = ton::rldp2::Rldp::create(adnl_.get()); + td::actor::send_closure(rldp_, &ton::rldp::Rldp::add_id, local_id_short_); + td::actor::send_closure(rldp2_, &ton::rldp2::Rldp::add_id, local_id_short_); + + // Create overlay manager (without DHT for direct peering) + overlays_ = ton::overlay::Overlays::create(config_.db_path, keyring_.get(), + adnl_.get(), td::actor::ActorId{}); + + // Setup stdin polling + alarm_timestamp() = td::Timestamp::in(0.1); + + // Output ready message + auto pubkey_tl = local_pubkey_.tl(); + auto pubkey_serialized = ton::serialize_tl_object(pubkey_tl, true); + auto pubkey_b64 = td::base64_encode(pubkey_serialized.as_slice()); + std::ostringstream oss; + oss << "{\"status\": \"ready\"" + << ", \"adnl_id\": \"" << local_id_short_.bits256_value().to_hex() << "\"" + << ", \"pubkey\": \"" << pubkey_b64 << "\"" + << ", \"udp_port\": " << config_.udp_port + << "}"; + respond(oss.str()); +} + +void CompatTestNode::tear_down() { + LOG(INFO) << "Shutting down compat test node"; +} + +void CompatTestNode::alarm() { + process_stdin(); + alarm_timestamp() = td::Timestamp::in(0.1); +} + +// ---------- Control interface ---------- + +void CompatTestNode::process_stdin() { + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + if (select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &tv) > 0) { + std::string line; + if (std::getline(std::cin, line)) { + handle_command(line); + } else { + // stdin closed + LOG(INFO) << "stdin closed, shutting down"; + stop(); + } + } +} + +void CompatTestNode::handle_command(std::string cmd_line) { + if (cmd_line.empty()) return; + + LOG(INFO) << "CMD: " << cmd_line; + + auto json_res = td::json_decode(cmd_line); + if (json_res.is_error()) { + respond_error("Invalid JSON: " + json_res.error().message().str()); + return; + } + + auto &json = json_res.ok_ref(); + if (json.type() != td::JsonValue::Type::Object) { + respond_error("Expected JSON object"); + return; + } + + auto &obj = json.get_object(); + auto cmd = get_string(obj, "cmd"); + + if (cmd.empty()) { + respond_error("Missing 'cmd' field"); + return; + } + + if (cmd == "ping") { + respond("{\"result\": \"pong\"}"); + } else if (cmd == "get_info") { + cmd_get_info(obj); + } else if (cmd == "compute_overlay_id") { + cmd_compute_overlay_id(obj); + } else if (cmd == "add_peer") { + cmd_add_peer(obj); + } else if (cmd == "create_overlay") { + cmd_create_overlay(obj); + } else if (cmd == "delete_overlay") { + cmd_delete_overlay(obj); + } else if (cmd == "get_overlay_node_info") { + cmd_get_overlay_node_info(obj); + } else if (cmd == "send_broadcast") { + cmd_send_broadcast(obj); + } else if (cmd == "send_query") { + cmd_send_query(obj); + } else if (cmd == "send_rldp_query") { + cmd_send_rldp_query(obj); + } else if (cmd == "set_query_handler") { + cmd_set_query_handler(obj); + } else if (cmd == "set_broadcast_validator") { + cmd_set_broadcast_validator(obj); + } else if (cmd == "get_received_broadcasts") { + cmd_get_received_broadcasts(obj); + } else if (cmd == "clear_received_broadcasts") { + cmd_clear_received_broadcasts(obj); + } else if (cmd == "send_message") { + cmd_send_message(obj); + } else if (cmd == "get_received_messages") { + cmd_get_received_messages(obj); + } else if (cmd == "clear_received_messages") { + cmd_clear_received_messages(obj); + } else if (cmd == "compute_candidate_id_to_sign") { + cmd_compute_candidate_id_to_sign(obj); + } else if (cmd == "compress_boc") { + cmd_compress_boc(obj); + } else if (cmd == "decompress_boc") { + cmd_decompress_boc(obj); + } else if (cmd == "enable_quic") { + cmd_enable_quic(obj); + } else if (cmd == "send_quic_message") { + cmd_send_quic_message(obj); + } else if (cmd == "send_quic_query") { + cmd_send_quic_query(obj); + } else if (cmd == "shutdown") { + respond("{\"result\": \"shutting_down\"}"); + std::_Exit(0); // Force immediate exit + } else { + respond_error("Unknown command: " + cmd); + } +} + +// ---------- Command implementations ---------- + +void CompatTestNode::cmd_get_info(td::JsonObject &obj) { + auto pubkey_tl = local_pubkey_.tl(); + auto pubkey_serialized = ton::serialize_tl_object(pubkey_tl, true); + auto pubkey_b64 = td::base64_encode(pubkey_serialized.as_slice()); + std::ostringstream oss; + oss << "{\"result\": {" + << "\"adnl_id\": \"" << local_id_short_.bits256_value().to_hex() << "\"" + << ", \"pubkey\": \"" << pubkey_b64 << "\"" + << ", \"udp_port\": " << config_.udp_port + << "}}"; + respond(oss.str()); +} + +void CompatTestNode::cmd_compute_overlay_id(td::JsonObject &obj) { + auto name_b64 = get_string(obj, "name"); + if (name_b64.empty()) { + respond_error("Missing 'name' field (base64)"); + return; + } + auto name_res = td::base64_decode(name_b64); + if (name_res.is_error()) { + respond_error("Invalid base64 name"); + return; + } + auto name = name_res.move_as_ok(); + + ton::overlay::OverlayIdFull full_id{td::BufferSlice(name)}; + auto short_id = full_id.compute_short_id(); + + std::ostringstream oss; + oss << "{\"result\": {" + << "\"overlay_id\": \"" << short_id.bits256_value().to_hex() << "\"" + << "}}"; + respond(oss.str()); +} + +void CompatTestNode::cmd_add_peer(td::JsonObject &obj) { + auto pubkey_b64 = get_string(obj, "pubkey"); + auto ip = get_string(obj, "ip"); + auto port = static_cast(get_int(obj, "port")); + + if (pubkey_b64.empty() || ip.empty() || port == 0) { + respond_error("Missing 'pubkey', 'ip', or 'port'"); + return; + } + + auto pubkey_res = td::base64_decode(pubkey_b64); + if (pubkey_res.is_error()) { + respond_error("Invalid base64 pubkey"); + return; + } + auto pk_res = ton::PublicKey::import(pubkey_res.ok()); + if (pk_res.is_error()) { + respond_error("Invalid pubkey format: " + pk_res.error().message().str()); + return; + } + auto pk = pk_res.move_as_ok(); + + td::IPAddress addr; + auto addr_res = addr.init_ipv4_port(ip, port); + if (addr_res.is_error()) { + respond_error("Invalid address: " + addr_res.message().str()); + return; + } + + // Build address list for the peer + ton::adnl::AdnlAddressList peer_addr_list; + peer_addr_list.add_udp_adnl_address(addr).ensure(); + peer_addr_list.set_version(static_cast(td::Clocks::system())); + peer_addr_list.set_reinit_date(ton::adnl::Adnl::adnl_start_time()); + + auto peer_id = ton::adnl::AdnlNodeIdFull{pk}; + auto peer_short = peer_id.compute_short_id(); + + td::actor::send_closure(adnl_, &ton::adnl::Adnl::add_peer, + local_id_short_, peer_id, std::move(peer_addr_list)); + + respond_ok("\"peer_id\": \"" + peer_short.bits256_value().to_hex() + "\""); +} + +void CompatTestNode::cmd_create_overlay(td::JsonObject &obj) { + auto type = get_string(obj, "type"); + auto overlay_name_b64 = get_string(obj, "overlay_name"); + + if (type.empty()) { + respond_error("Missing 'type' (public|private|semiprivate)"); + return; + } + if (overlay_name_b64.empty()) { + respond_error("Missing 'overlay_name' (base64 TL bytes)"); + return; + } + + auto name_res = td::base64_decode(overlay_name_b64); + if (name_res.is_error()) { + respond_error("Invalid base64 overlay_name"); + return; + } + auto name = name_res.move_as_ok(); + + ton::overlay::OverlayIdFull id_full{td::BufferSlice(name)}; + auto id_short = id_full.compute_short_id(); + + LOG(INFO) << "Creating " << type << " overlay: " << id_short.bits256_value().to_hex(); + + auto callback = make_overlay_callback(id_short); + + // Use permissive rules: allow broadcasts up to 32MB with AllowFec flag. + // NOTE: We do NOT set CertificateFlags::Trusted here because that would skip + // the check_broadcast callback entirely. Without Trusted, all broadcasts go + // through the 2-phase validation (check_broadcast callback). + td::uint32 max_bcast_size = 32 << 20; // 32 MB + td::uint32 privacy_flags = ton::overlay::CertificateFlags::AllowFec; + + if (type == "public") { + ton::overlay::OverlayOptions opts; + opts.announce_self_ = false; // No DHT + + ton::overlay::OverlayPrivacyRules rules{max_bcast_size, privacy_flags, {}}; + td::actor::send_closure(overlays_, &ton::overlay::Overlays::create_public_overlay_ex, + local_id_short_, + id_full.clone(), + std::move(callback), + std::move(rules), + "compat_test", + opts); + } else if (type == "private") { + // Parse peer list + std::vector peers; + for (auto &kv : obj.field_values_) { + if (kv.first == "peers" && kv.second.type() == td::JsonValue::Type::Array) { + for (auto &p : kv.second.get_array()) { + if (p.type() == td::JsonValue::Type::String) { + td::Bits256 bits; + auto hex = p.get_string().str(); + if (bits.from_hex(hex) == 256) { + peers.push_back(ton::adnl::AdnlNodeIdShort{bits}); + } + } + } + break; + } + } + + ton::overlay::OverlayPrivacyRules rules{max_bcast_size, privacy_flags, {}}; + ton::overlay::OverlayOptions opts; + opts.announce_self_ = false; + + auto enable_twostep = get_bool(obj, "enable_twostep", false); + if (enable_twostep) { + opts.twostep_broadcast_sender_ = rldp2_.get(); + opts.send_twostep_broadcast_ = true; + LOG(INFO) << "TwostepFec enabled for private overlay"; + } + + td::actor::send_closure(overlays_, &ton::overlay::Overlays::create_private_overlay_ex, + local_id_short_, + id_full.clone(), + std::move(peers), + std::move(callback), + std::move(rules), + "compat_test", + std::move(opts)); + } else if (type == "semiprivate") { + // Parse peer list + std::vector peers; + for (auto &kv : obj.field_values_) { + if (kv.first == "peers" && kv.second.type() == td::JsonValue::Type::Array) { + for (auto &p : kv.second.get_array()) { + if (p.type() == td::JsonValue::Type::String) { + td::Bits256 bits; + auto hex = p.get_string().str(); + if (bits.from_hex(hex) == 256) { + peers.push_back(ton::adnl::AdnlNodeIdShort{bits}); + } + } + } + break; + } + } + + // Parse root public key hashes + std::vector root_keys; + for (auto &kv : obj.field_values_) { + if (kv.first == "root_pub_keys" && kv.second.type() == td::JsonValue::Type::Array) { + for (auto &r : kv.second.get_array()) { + if (r.type() == td::JsonValue::Type::String) { + td::Bits256 bits; + auto hex = r.get_string().str(); + if (bits.from_hex(hex) == 256) { + root_keys.push_back(ton::PublicKeyHash{bits}); + } + } + } + break; + } + } + + // Parse certificate + ton::overlay::OverlayMemberCertificate cert; + auto cert_b64 = get_string(obj, "certificate"); + if (!cert_b64.empty()) { + auto cert_res = td::base64_decode(cert_b64); + if (cert_res.is_ok()) { + auto cert_data = cert_res.move_as_ok(); + auto tl_res = ton::fetch_tl_object( + td::BufferSlice(cert_data), true); + if (tl_res.is_ok()) { + cert = ton::overlay::OverlayMemberCertificate(tl_res.ok().get()); + } + } + } + + auto max_slaves = static_cast(get_int(obj, "max_slaves", 5)); + + ton::overlay::OverlayOptions opts; + opts.announce_self_ = false; + opts.max_slaves_in_semiprivate_overlay_ = max_slaves; + + ton::overlay::OverlayPrivacyRules rules{max_bcast_size, privacy_flags, {}}; + td::actor::send_closure(overlays_, &ton::overlay::Overlays::create_semiprivate_overlay, + local_id_short_, + id_full.clone(), + std::move(peers), + std::move(root_keys), + std::move(cert), + std::move(callback), + std::move(rules), + "compat_test", + opts); + } else { + respond_error("Unknown overlay type: " + type); + return; + } + + // Store overlay state + OverlayState state; + state.id_full = std::move(id_full); + state.id_short = id_short; + state.type = type; + overlay_states_[id_short] = std::move(state); + + respond_ok("\"overlay_id\": \"" + id_short.bits256_value().to_hex() + "\""); +} + +void CompatTestNode::cmd_delete_overlay(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto id_short = ton::overlay::OverlayIdShort{bits}; + + td::actor::send_closure(overlays_, &ton::overlay::Overlays::delete_overlay, + local_id_short_, id_short); + overlay_states_.erase(id_short); + respond_ok(); +} + +void CompatTestNode::cmd_get_overlay_node_info(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + respond_error("Overlay not found"); + return; + } + + // Build OverlayNode, sign it via keyring, serialize as TL + auto node = ton::overlay::OverlayNode{local_id_short_, overlay_id, 0}; + auto to_sign = node.to_sign(); + + td::actor::send_closure( + keyring_, &ton::keyring::Keyring::sign_add_get_public_key, + local_id_short_.pubkey_hash(), std::move(to_sign), + [SelfId = actor_id(this), overlay_id]( + td::Result> R) { + if (R.is_error()) { + td::actor::send_closure(SelfId, &CompatTestNode::respond_error, + "Failed to sign: " + R.error().message().str()); + return; + } + auto V = R.move_as_ok(); + auto node = ton::overlay::OverlayNode{ + ton::adnl::AdnlNodeIdFull{V.second}, overlay_id, 0, + static_cast(td::Clocks::system()), V.first.as_slice()}; + auto tl = node.tl(); + auto serialized = ton::serialize_tl_object(tl, true); + auto b64 = td::base64_encode(serialized.as_slice()); + std::ostringstream oss; + oss << "{\"result\": {\"node_tl\": \"" << b64 << "\"}}"; + td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); + }); +} + +void CompatTestNode::cmd_send_broadcast(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + auto data_b64 = get_string(obj, "data"); + auto use_fec = get_bool(obj, "use_fec", false); + + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + if (data_b64.empty()) { + respond_error("Missing 'data'"); + return; + } + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data"); + return; + } + + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + auto data = td::BufferSlice(data_res.move_as_ok()); + + LOG(INFO) << "Sending " << (use_fec ? "FEC " : "") << "broadcast to overlay " + << overlay_id.bits256_value().to_hex() << " size=" << data.size(); + + if (use_fec) { + td::actor::send_closure(overlays_, &ton::overlay::Overlays::send_broadcast_fec_ex, + local_id_short_, overlay_id, local_id_short_.pubkey_hash(), + 0, std::move(data)); + } else { + td::actor::send_closure(overlays_, &ton::overlay::Overlays::send_broadcast_ex, + local_id_short_, overlay_id, local_id_short_.pubkey_hash(), + 0, std::move(data)); + } + respond_ok(); +} + +void CompatTestNode::cmd_send_query(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + auto peer_hex = get_string(obj, "peer_adnl_id"); + auto data_b64 = get_string(obj, "data"); + auto timeout_ms = get_int(obj, "timeout_ms", 5000); + + td::Bits256 overlay_bits, peer_bits; + if (overlay_bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + if (peer_bits.from_hex(peer_hex) != 256) { + respond_error("Invalid peer_adnl_id hex"); + return; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data"); + return; + } + + auto overlay_id = ton::overlay::OverlayIdShort{overlay_bits}; + auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; + auto data = td::BufferSlice(data_res.move_as_ok()); + auto timeout = td::Timestamp::in(timeout_ms / 1000.0); + + td::actor::send_closure( + overlays_, &ton::overlay::Overlays::send_query, + peer_id, local_id_short_, overlay_id, "compat_test_query", + td::PromiseCreator::lambda( + [SelfId = actor_id(this)](td::Result R) { + if (R.is_error()) { + td::actor::send_closure(SelfId, &CompatTestNode::respond_error, + "Query failed: " + R.error().message().str()); + return; + } + auto answer = R.move_as_ok(); + auto b64 = td::base64_encode(answer.as_slice()); + std::ostringstream oss; + oss << "{\"result\": {\"answer\": \"" << b64 << "\"}}"; + td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); + }), + timeout, std::move(data)); +} + +void CompatTestNode::cmd_send_rldp_query(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + auto peer_hex = get_string(obj, "peer_adnl_id"); + auto data_b64 = get_string(obj, "data"); + auto max_answer_size = static_cast(get_int(obj, "max_answer_size", 1 << 20)); + auto v2 = get_bool(obj, "v2", false); + + td::Bits256 overlay_bits, peer_bits; + if (overlay_bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + if (peer_bits.from_hex(peer_hex) != 256) { + respond_error("Invalid peer_adnl_id hex"); + return; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data"); + return; + } + + auto overlay_id = ton::overlay::OverlayIdShort{overlay_bits}; + auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; + auto data = td::BufferSlice(data_res.move_as_ok()); + auto timeout = td::Timestamp::in(10.0); + + auto promise = td::PromiseCreator::lambda( + [SelfId = actor_id(this)](td::Result R) { + if (R.is_error()) { + td::actor::send_closure(SelfId, &CompatTestNode::respond_error, + "RLDP query failed: " + R.error().message().str()); + return; + } + auto answer = R.move_as_ok(); + auto b64 = td::base64_encode(answer.as_slice()); + std::ostringstream oss; + oss << "{\"result\": {\"answer\": \"" << b64 << "\"}}"; + td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); + }); + + if (v2) { + td::actor::send_closure( + overlays_, &ton::overlay::Overlays::send_query_via, + peer_id, local_id_short_, overlay_id, "compat_rldp_query", + std::move(promise), + timeout, std::move(data), max_answer_size, + rldp2_.get()); + } else { + td::actor::send_closure( + overlays_, &ton::overlay::Overlays::send_query_via, + peer_id, local_id_short_, overlay_id, "compat_rldp_query", + std::move(promise), + timeout, std::move(data), max_answer_size, + rldp_.get()); + } +} + +void CompatTestNode::cmd_set_query_handler(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + auto mode = get_string(obj, "mode"); + + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + respond_error("Overlay not found"); + return; + } + + it->second.query_handler_mode = mode; + respond_ok(); +} + +void CompatTestNode::cmd_set_broadcast_validator(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + auto mode = get_string(obj, "mode"); + + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + respond_error("Overlay not found"); + return; + } + + it->second.broadcast_validator_mode = mode; + respond_ok(); +} + +void CompatTestNode::cmd_get_received_broadcasts(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + + LOG(INFO) << "get_received_broadcasts: overlay_hex='" << overlay_hex << "' len=" << overlay_hex.length(); + + td::Bits256 bits; + auto hex_result = bits.from_hex(overlay_hex); + if (hex_result != 256) { + LOG(INFO) << "from_hex returned " << hex_result << " (expected 64)"; + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + respond_error("Overlay not found"); + return; + } + + std::ostringstream oss; + oss << "{\"result\": ["; + bool first = true; + for (auto &b : it->second.received_broadcasts) { + if (!first) oss << ", "; + first = false; + oss << "{\"source\": \"" << b.source.bits256_value().to_hex() << "\"" + << ", \"size\": " << b.data.size() + << ", \"data\": \"" << td::base64_encode(td::Slice(b.data.data(), b.data.size())) << "\"" + << ", \"timestamp\": " << b.timestamp + << ", \"accepted\": " << (b.was_accepted ? "true" : "false") + << "}"; + } + oss << "]}"; + respond(oss.str()); +} + +void CompatTestNode::cmd_clear_received_broadcasts(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it != overlay_states_.end()) { + it->second.received_broadcasts.clear(); + } + respond_ok(); +} + +// ---------- Callback factory ---------- + +std::unique_ptr CompatTestNode::make_overlay_callback( + ton::overlay::OverlayIdShort overlay_id) { + return std::make_unique( + overlay_id, + [this, overlay_id](ton::PublicKeyHash src, td::BufferSlice data) { + on_broadcast_received(overlay_id, src, std::move(data)); + }, + [this, overlay_id](ton::adnl::AdnlNodeIdShort src, td::BufferSlice data, + td::Promise promise) { + on_query_received(overlay_id, src, std::move(data), std::move(promise)); + }, + [this, overlay_id](ton::PublicKeyHash src, td::BufferSlice data, + td::Promise promise) { + on_check_broadcast(overlay_id, src, std::move(data), std::move(promise)); + }, + [this, overlay_id](ton::adnl::AdnlNodeIdShort src, td::BufferSlice data) { + on_message_received(overlay_id, src, std::move(data)); + }); +} + +// ---------- Callback handlers ---------- + +void CompatTestNode::on_broadcast_received(ton::overlay::OverlayIdShort overlay_id, + ton::PublicKeyHash source, + td::BufferSlice data) { + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) return; + + ReceivedBroadcast record; + record.source = source; + record.overlay_id = overlay_id; + auto slice = data.as_slice(); + record.data = std::vector(slice.ubegin(), slice.uend()); + record.timestamp = static_cast(td::Clocks::system()); + record.was_accepted = true; + + it->second.received_broadcasts.push_back(std::move(record)); +} + +void CompatTestNode::on_query_received(ton::overlay::OverlayIdShort overlay_id, + ton::adnl::AdnlNodeIdShort src, + td::BufferSlice data, + td::Promise promise) { + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + promise.set_error(td::Status::Error("Overlay not found")); + return; + } + + it->second.received_queries.emplace_back( + src.bits256_value().to_hex(), data.size()); + + auto &mode = it->second.query_handler_mode; + if (mode == "echo") { + promise.set_value(std::move(data)); + } else if (mode == "capabilities") { + // Return a fixed capabilities response + std::string caps = "compat_test_cpp_node:v1"; + promise.set_value(td::BufferSlice(caps)); + } else if (mode == "reject") { + promise.set_error(td::Status::Error("Rejected by test query handler")); + } else { + // Default echo + promise.set_value(std::move(data)); + } +} + +void CompatTestNode::on_check_broadcast(ton::overlay::OverlayIdShort overlay_id, + ton::PublicKeyHash source, + td::BufferSlice data, + td::Promise promise) { + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + promise.set_value(td::Unit()); + return; + } + + auto &mode = it->second.broadcast_validator_mode; + if (mode == "accept_all") { + promise.set_value(td::Unit()); + } else if (mode == "reject_all") { + // Record the rejection + ReceivedBroadcast record; + record.source = source; + record.overlay_id = overlay_id; + auto slice = data.as_slice(); + record.data = std::vector(slice.ubegin(), slice.uend()); + record.timestamp = static_cast(td::Clocks::system()); + record.was_accepted = false; + it->second.received_broadcasts.push_back(std::move(record)); + + promise.set_error(td::Status::Error("Rejected by test broadcast validator")); + } else { + // Default: accept + promise.set_value(td::Unit()); + } +} + +// ---------- Message commands ---------- + +void CompatTestNode::on_message_received(ton::overlay::OverlayIdShort overlay_id, + ton::adnl::AdnlNodeIdShort source, + td::BufferSlice data) { + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) return; + + ReceivedMessage record; + record.source = source; + record.overlay_id = overlay_id; + auto slice = data.as_slice(); + record.data = std::vector(slice.ubegin(), slice.uend()); + record.timestamp = static_cast(td::Clocks::system()); + + it->second.received_messages.push_back(std::move(record)); + LOG(INFO) << "Message recorded for overlay " << overlay_id.bits256_value().to_hex() + << " from " << source.bits256_value().to_hex() + << " size=" << it->second.received_messages.back().data.size(); +} + +void CompatTestNode::cmd_send_message(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + auto peer_hex = get_string(obj, "peer_adnl_id"); + auto data_b64 = get_string(obj, "data"); + + td::Bits256 overlay_bits, peer_bits; + if (overlay_bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + if (peer_bits.from_hex(peer_hex) != 256) { + respond_error("Invalid peer_adnl_id hex"); + return; + } + if (data_b64.empty()) { + respond_error("Missing 'data'"); + return; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data"); + return; + } + + auto overlay_id = ton::overlay::OverlayIdShort{overlay_bits}; + auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; + auto data = td::BufferSlice(data_res.move_as_ok()); + + LOG(INFO) << "Sending message to overlay " << overlay_id.bits256_value().to_hex() + << " peer=" << peer_id.bits256_value().to_hex() + << " size=" << data.size(); + + td::actor::send_closure(overlays_, &ton::overlay::Overlays::send_message, + peer_id, local_id_short_, overlay_id, std::move(data)); + respond_ok(); +} + +void CompatTestNode::cmd_get_received_messages(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it == overlay_states_.end()) { + respond_error("Overlay not found"); + return; + } + + std::ostringstream oss; + oss << "{\"result\": ["; + bool first = true; + for (auto &m : it->second.received_messages) { + if (!first) oss << ", "; + first = false; + oss << "{\"source\": \"" << m.source.bits256_value().to_hex() << "\"" + << ", \"size\": " << m.data.size() + << ", \"data\": \"" << td::base64_encode(td::Slice(m.data.data(), m.data.size())) << "\"" + << ", \"timestamp\": " << m.timestamp + << "}"; + } + oss << "]}"; + respond(oss.str()); +} + +void CompatTestNode::cmd_clear_received_messages(td::JsonObject &obj) { + auto overlay_hex = get_string(obj, "overlay_id"); + + td::Bits256 bits; + if (bits.from_hex(overlay_hex) != 256) { + respond_error("Invalid overlay_id hex"); + return; + } + auto overlay_id = ton::overlay::OverlayIdShort{bits}; + + auto it = overlay_states_.find(overlay_id); + if (it != overlay_states_.end()) { + it->second.received_messages.clear(); + } + respond_ok(); +} + +void CompatTestNode::cmd_compute_candidate_id_to_sign(td::JsonObject &obj) { + auto slot = static_cast(get_int(obj, "slot", 0)); + auto hash_hex = get_string(obj, "hash"); + if (hash_hex.empty()) { + respond_error("Missing 'hash' (hex)"); + return; + } + + td::Bits256 hash_bits; + if (hash_bits.from_hex(hash_hex) != 256) { + respond_error("Invalid hash hex (expected 32 bytes)"); + return; + } + + // C++ simplex/catchain signs consensus.candidateId{slot,hash} directly. + auto tl = ton::create_tl_object(slot, hash_bits); + auto serialized = ton::serialize_tl_object(tl, true); + auto data_b64 = td::base64_encode(serialized.as_slice()); + + respond_ok("\"data\": \"" + data_b64 + "\""); +} + +// ---------- BOC Compression ---------- + +void CompatTestNode::cmd_compress_boc(td::JsonObject &obj) { + auto data_b64 = get_string(obj, "data"); + auto algorithm = get_string(obj, "algorithm"); + + if (data_b64.empty()) { + respond_error("Missing 'data' (base64 standard BOC)"); + return; + } + if (algorithm.empty()) { + algorithm = "baseline"; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data: " + data_res.error().message().str()); + return; + } + auto data = data_res.move_as_ok(); + + // Deserialize standard BOC to cell roots + auto roots_res = vm::std_boc_deserialize_multi(td::Slice(data)); + if (roots_res.is_error()) { + respond_error("Failed to deserialize BOC: " + roots_res.error().message().str()); + return; + } + auto roots = roots_res.move_as_ok(); + + // Determine algorithm + vm::CompressionAlgorithm algo; + if (algorithm == "baseline") { + algo = vm::CompressionAlgorithm::BaselineLZ4; + } else if (algorithm == "improved") { + algo = vm::CompressionAlgorithm::ImprovedStructureLZ4; + } else { + respond_error("Unknown algorithm: " + algorithm + " (use 'baseline' or 'improved')"); + return; + } + + // Compress + auto compressed_res = vm::boc_compress(roots, algo); + if (compressed_res.is_error()) { + respond_error("Compression failed: " + compressed_res.error().message().str()); + return; + } + auto compressed = compressed_res.move_as_ok(); + auto compressed_b64 = td::base64_encode(compressed.as_slice()); + + respond_ok("\"compressed\": \"" + compressed_b64 + "\""); +} + +void CompatTestNode::cmd_decompress_boc(td::JsonObject &obj) { + auto data_b64 = get_string(obj, "data"); + auto max_size = static_cast(get_int(obj, "max_size", 10 * 1024 * 1024)); + + if (data_b64.empty()) { + respond_error("Missing 'data' (base64 compressed BOC)"); + return; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data: " + data_res.error().message().str()); + return; + } + auto data = data_res.move_as_ok(); + + // Decompress + auto roots_res = vm::boc_decompress(td::Slice(data), max_size); + if (roots_res.is_error()) { + respond_error("Decompression failed: " + roots_res.error().message().str()); + return; + } + auto roots = roots_res.move_as_ok(); + + // Re-serialize as standard BOC + auto boc_res = vm::std_boc_serialize_multi(std::move(roots), 2); + if (boc_res.is_error()) { + respond_error("BOC re-serialization failed: " + boc_res.error().message().str()); + return; + } + auto boc = boc_res.move_as_ok(); + auto boc_b64 = td::base64_encode(boc.as_slice()); + + respond_ok("\"boc\": \"" + boc_b64 + "\""); +} + +// ---------- QUIC commands ---------- + +void CompatTestNode::cmd_enable_quic(td::JsonObject &obj) { + if (!quic_.empty()) { + respond_error("QUIC already enabled"); + return; + } + + auto peer_table = td::actor::actor_dynamic_cast(adnl_.get()); + if (peer_table.empty()) { + respond_error("ADNL peer table not available"); + return; + } + + quic_ = td::actor::create_actor("QuicSender", peer_table, keyring_.get()); + td::actor::send_closure(quic_, &ton::quic::QuicSender::add_id, local_id_short_); + + auto quic_port = config_.udp_port + 1000; + LOG(INFO) << "QUIC enabled, listening on port " << quic_port; + + respond_ok("\"quic_port\": " + std::to_string(quic_port)); +} + +void CompatTestNode::cmd_send_quic_message(td::JsonObject &obj) { + if (quic_.empty()) { + respond_error("QUIC not enabled. Call enable_quic first"); + return; + } + + auto peer_hex = get_string(obj, "peer_adnl_id"); + auto data_b64 = get_string(obj, "data"); + + td::Bits256 peer_bits; + if (peer_bits.from_hex(peer_hex) != 256) { + respond_error("Invalid peer_adnl_id hex"); + return; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data"); + return; + } + + auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; + auto data = td::BufferSlice(data_res.move_as_ok()); + + td::actor::send_closure(quic_, &ton::quic::QuicSender::send_message, + local_id_short_, peer_id, std::move(data)); + + respond_ok(); +} + +void CompatTestNode::cmd_send_quic_query(td::JsonObject &obj) { + if (quic_.empty()) { + respond_error("QUIC not enabled. Call enable_quic first"); + return; + } + + auto peer_hex = get_string(obj, "peer_adnl_id"); + auto data_b64 = get_string(obj, "data"); + auto timeout_ms = get_int(obj, "timeout_ms", 5000); + + td::Bits256 peer_bits; + if (peer_bits.from_hex(peer_hex) != 256) { + respond_error("Invalid peer_adnl_id hex"); + return; + } + + auto data_res = td::base64_decode(data_b64); + if (data_res.is_error()) { + respond_error("Invalid base64 data"); + return; + } + + auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; + auto data = td::BufferSlice(data_res.move_as_ok()); + auto timeout = td::Timestamp::in(timeout_ms / 1000.0); + + td::actor::send_closure( + quic_, &ton::quic::QuicSender::send_query, + local_id_short_, peer_id, std::string("compat_test_quic_query"), + td::PromiseCreator::lambda( + [SelfId = actor_id(this)](td::Result R) { + if (R.is_error()) { + td::actor::send_closure(SelfId, &CompatTestNode::respond_error, + "QUIC query failed: " + R.error().message().str()); + return; + } + auto answer = R.move_as_ok(); + auto b64 = td::base64_encode(answer.as_slice()); + std::ostringstream oss; + oss << "{\"result\": {\"answer\": \"" << b64 << "\"}}"; + td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); + }), + timeout, std::move(data)); +} + +} // namespace compat_test + +// ---------- Main ---------- + +int main(int argc, char** argv) { + SET_VERBOSITY_LEVEL(verbosity_INFO); + + td::uint16 udp_port = 14000; + std::string db_path = "/tmp/compat_test_node"; + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if (arg == "--port" && i + 1 < argc) { + udp_port = static_cast(std::stoi(argv[++i])); + } else if (arg == "--db" && i + 1 < argc) { + db_path = argv[++i]; + } else if (arg == "--help" || arg == "-h") { + std::cerr << "Usage: " << argv[0] << " [options]" << std::endl; + std::cerr << "Options:" << std::endl; + std::cerr << " --port PORT ADNL UDP listening port (default: 14000)" << std::endl; + std::cerr << " --db PATH Database path (default: /tmp/compat_test_node)" << std::endl; + return 0; + } + } + + td::actor::Scheduler scheduler({2}); + + compat_test::CompatTestNode::Config config; + config.udp_port = udp_port; + config.db_path = db_path; + + scheduler.run_in_context([&] { + td::actor::create_actor("compat_test_node", config).release(); + }); + + scheduler.run(); + + return 0; +} diff --git a/src/node/tests/compat_test/cpp_src/compat_test_node.hpp b/src/node/tests/compat_test/cpp_src/compat_test_node.hpp new file mode 100644 index 0000000..111d5fa --- /dev/null +++ b/src/node/tests/compat_test/cpp_src/compat_test_node.hpp @@ -0,0 +1,222 @@ +#pragma once + +#include "adnl/adnl.h" +#include "adnl/adnl-network-manager.h" +#include "adnl/adnl-address-list.h" +#include "overlay/overlays.h" +#include "overlay/overlay-id.hpp" +#include "rldp/rldp.h" +#include "rldp2/rldp.h" +#include "quic-sender.h" +#include "keys/keys.hpp" +#include "keyring/keyring.h" +#include "td/actor/actor.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/base64.h" +#include "auto/tl/ton_api.h" +#include "auto/tl/ton_api_json.h" +#include "tl-utils/tl-utils.hpp" + +#include +#include +#include +#include +#include + +namespace compat_test { + +// Received broadcast record +struct ReceivedBroadcast { + ton::PublicKeyHash source; + ton::overlay::OverlayIdShort overlay_id; + std::vector data; + td::int32 timestamp; + bool was_accepted; +}; + +// Received message record (point-to-point overlay messages) +struct ReceivedMessage { + ton::adnl::AdnlNodeIdShort source; + ton::overlay::OverlayIdShort overlay_id; + std::vector data; + td::int32 timestamp; +}; + +// Per-overlay state +struct OverlayState { + ton::overlay::OverlayIdFull id_full; + ton::overlay::OverlayIdShort id_short; + std::string type; // "public", "private", "semiprivate" + std::string query_handler_mode = "echo"; // "echo", "capabilities" + std::string broadcast_validator_mode = "accept_all"; // "accept_all", "reject_all" + std::vector received_broadcasts; + std::vector received_messages; + std::vector> received_queries; // (from_hex, data_size) +}; + +// Overlay callback implementation for testing +class TestOverlayCallback : public ton::overlay::Overlays::Callback { +public: + using BroadcastHandler = std::function; + using QueryHandler = std::function)>; + using CheckBroadcastHandler = std::function)>; + using MessageHandler = std::function; + + TestOverlayCallback( + ton::overlay::OverlayIdShort overlay_id, + BroadcastHandler on_broadcast, + QueryHandler on_query, + CheckBroadcastHandler check_broadcast, + MessageHandler on_message = nullptr + ) : overlay_id_(overlay_id) + , on_broadcast_(std::move(on_broadcast)) + , on_query_(std::move(on_query)) + , check_broadcast_(std::move(check_broadcast)) + , on_message_(std::move(on_message)) {} + + void receive_message(ton::adnl::AdnlNodeIdShort src, + ton::overlay::OverlayIdShort overlay_id, + td::BufferSlice data) override { + LOG(INFO) << "MSG_RECEIVED overlay=" << overlay_id.bits256_value().to_hex() + << " src=" << src.bits256_value().to_hex() + << " size=" << data.size(); + if (on_message_) { + on_message_(src, std::move(data)); + } + } + + void receive_query(ton::adnl::AdnlNodeIdShort src, + ton::overlay::OverlayIdShort overlay_id, + td::BufferSlice data, + td::Promise promise) override { + LOG(INFO) << "QUERY_RECEIVED overlay=" << overlay_id.bits256_value().to_hex() + << " src=" << src.bits256_value().to_hex() + << " size=" << data.size(); + if (on_query_) { + on_query_(src, std::move(data), std::move(promise)); + } else { + // Default: echo back + promise.set_value(std::move(data)); + } + } + + void receive_broadcast(ton::PublicKeyHash src, + ton::overlay::OverlayIdShort overlay_id, + td::BufferSlice data) override { + LOG(INFO) << "BROADCAST_DELIVERED overlay=" << overlay_id.bits256_value().to_hex() + << " src=" << src.bits256_value().to_hex() + << " size=" << data.size(); + if (on_broadcast_) { + on_broadcast_(src, std::move(data)); + } + } + + void check_broadcast(ton::PublicKeyHash src, + ton::overlay::OverlayIdShort overlay_id, + td::BufferSlice data, + td::Promise promise) override { + LOG(INFO) << "CHECK_BROADCAST overlay=" << overlay_id.bits256_value().to_hex() + << " src=" << src.bits256_value().to_hex() + << " size=" << data.size(); + if (check_broadcast_) { + check_broadcast_(src, std::move(data), std::move(promise)); + } else { + promise.set_value(td::Unit()); + } + } + +private: + ton::overlay::OverlayIdShort overlay_id_; + BroadcastHandler on_broadcast_; + QueryHandler on_query_; + CheckBroadcastHandler check_broadcast_; + MessageHandler on_message_; +}; + +// Main test node actor +class CompatTestNode : public td::actor::Actor { +public: + struct Config { + td::uint16 udp_port = 14000; + std::string db_path = "/tmp/compat_test_node"; + }; + + explicit CompatTestNode(Config config); + + void start_up() override; + void tear_down() override; + void alarm() override; + +private: + Config config_; + + // ADNL components + td::actor::ActorOwn network_manager_; + td::actor::ActorOwn adnl_; + td::actor::ActorOwn keyring_; + td::actor::ActorOwn overlays_; + td::actor::ActorOwn rldp_; + td::actor::ActorOwn rldp2_; + td::actor::ActorOwn quic_; + + // Local identity + ton::PrivateKey local_privkey_; + ton::PublicKey local_pubkey_; + ton::adnl::AdnlNodeIdShort local_id_short_; + + // Active overlays + std::map overlay_states_; + + // Control interface + void process_stdin(); + void handle_command(std::string cmd_line); + + // Command handlers + void cmd_get_info(td::JsonObject &obj); + void cmd_compute_overlay_id(td::JsonObject &obj); + void cmd_add_peer(td::JsonObject &obj); + void cmd_create_overlay(td::JsonObject &obj); + void cmd_delete_overlay(td::JsonObject &obj); + void cmd_get_overlay_node_info(td::JsonObject &obj); + void cmd_send_broadcast(td::JsonObject &obj); + void cmd_send_query(td::JsonObject &obj); + void cmd_send_rldp_query(td::JsonObject &obj); + void cmd_set_query_handler(td::JsonObject &obj); + void cmd_set_broadcast_validator(td::JsonObject &obj); + void cmd_get_received_broadcasts(td::JsonObject &obj); + void cmd_clear_received_broadcasts(td::JsonObject &obj); + void cmd_send_message(td::JsonObject &obj); + void cmd_get_received_messages(td::JsonObject &obj); + void cmd_clear_received_messages(td::JsonObject &obj); + void cmd_compress_boc(td::JsonObject &obj); + void cmd_decompress_boc(td::JsonObject &obj); + void cmd_compute_candidate_id_to_sign(td::JsonObject &obj); + void cmd_enable_quic(td::JsonObject &obj); + void cmd_send_quic_message(td::JsonObject &obj); + void cmd_send_quic_query(td::JsonObject &obj); + + // Helpers + std::string get_string(td::JsonObject &obj, const std::string &key); + bool get_bool(td::JsonObject &obj, const std::string &key, bool def = false); + td::int64 get_int(td::JsonObject &obj, const std::string &key, td::int64 def = 0); + + std::unique_ptr make_overlay_callback(ton::overlay::OverlayIdShort overlay_id); + + void on_broadcast_received(ton::overlay::OverlayIdShort overlay_id, + ton::PublicKeyHash source, td::BufferSlice data); + void on_message_received(ton::overlay::OverlayIdShort overlay_id, + ton::adnl::AdnlNodeIdShort source, td::BufferSlice data); + void on_query_received(ton::overlay::OverlayIdShort overlay_id, + ton::adnl::AdnlNodeIdShort src, td::BufferSlice data, + td::Promise promise); + void on_check_broadcast(ton::overlay::OverlayIdShort overlay_id, + ton::PublicKeyHash source, td::BufferSlice data, + td::Promise promise); + + void respond(const std::string &json); + void respond_ok(); + void respond_ok(const std::string &extra_fields); + void respond_error(const std::string &msg); +}; + +} // namespace compat_test diff --git a/src/node/tests/compat_test/incompatibilities.md b/src/node/tests/compat_test/incompatibilities.md new file mode 100644 index 0000000..aded1ef --- /dev/null +++ b/src/node/tests/compat_test/incompatibilities.md @@ -0,0 +1,62 @@ +# Rust โ†” C++ Compatibility Test Results + +Cross-implementation compatibility testing between the Rust (`adnl` crate) and C++ (`ton-cpp-testnet`) overlay/ADNL implementations. + +## Known Issues + +### Safe/RLDP Overlay Message Delivery (BROKEN) + +Overlay messages sent via Safe/RLDP transport (TCP-like) from Rust to C++ are not delivered. RLDP queries work fine in both directions; only fire-and-forget `overlay.message()` via RLDP is affected. + +- **Test**: `test_overlay_message::test_overlay_message_rust_to_cpp_safe` (ignored) +- **Workaround**: Use Fast/UDP for overlay messages (works correctly) + +## Test Summary + +| Test Suite | Tests | Pass | Ignored | Status | +|------------|-------|------|---------|--------| +| `test_overlay_id` | 4 | 4 | 0 | Compatible | +| `test_broadcast` | 4 | 4 | 0 | Compatible | +| `test_broadcast_validation` | 4 | 4 | 0 | Compatible | +| `test_public_overlay` | 2 | 2 | 0 | Compatible | +| `test_overlay_message` | 5 | 4 | 1 | 1 ignored (Safe/RLDP) | +| `test_boc_compression` | 4 | 4 | 0 | Compatible | +| `test_candidate_id_to_sign` | 2 | 2 | 0 | Compatible | +| `test_rldp_query` | 8 | 8 | 0 | Compatible | +| `test_fec_relay` | 4 | 4 | 0 | Compatible | +| `test_twostep_fec_relay` | 4 | 4 | 0 | Compatible | +| `test_quic_transport` | 3 | 3 | 0 | Compatible | +| `test_quic_overlay` | 4 | 4 | 0 | Compatible | +| `test_quic_private_overlay` | 5 | 5 | 0 | Compatible | +| **Total** | **53** | **52** | **1** | | + +## What Is Tested + +- **Overlay ID computation** โ€” identical IDs from same inputs (ASCII, binary, Unicode) +- **Broadcasts** โ€” small (inline) and FEC-encoded (2KB), both directions +- **Broadcast validation** โ€” 2-phase accept/reject callback +- **Overlay queries** โ€” echo roundtrip and rejection behavior +- **Overlay messages** โ€” point-to-point delivery, burst (20 msgs, โ‰ฅ90% required) +- **BOC compression** โ€” bidirectional, 2 algorithms, multiple cell topologies, round-trip +- **Candidate ID signing** โ€” TL serialization byte match +- **RLDP v1/v2** โ€” query/response at 256B, 4KB, 7KB payloads, both directions +- **FEC relay** โ€” 3-node redistribution, all 4 Rust/C++ role combinations +- **TwostepFec relay** โ€” 6-node redistribution with mixed Rust/C++ bridges +- **QUIC transport** โ€” TLS/RPK handshake, raw queries, large messages (900B) +- **QUIC overlay** โ€” overlay messages and queries routed via QUIC +- **QUIC private overlay** โ€” ADNL vs QUIC transport, burst delivery (100% required) + +## Reproduction + +```bash +export CPP_SRC_PATH=/path/to/ton-cpp-testnet + +# Run all tests +make test + +# Run a specific test suite +make test TEST=test_broadcast + +# Run with verbose output +RUST_LOG=debug make test TEST=test_rldp_query +``` diff --git a/src/node/tests/compat_test/src/lib.rs b/src/node/tests/compat_test/src/lib.rs new file mode 100644 index 0000000..27984f7 --- /dev/null +++ b/src/node/tests/compat_test/src/lib.rs @@ -0,0 +1,930 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Cross-Implementation Compatibility Test Library +//! +//! This crate provides utilities for testing compatibility between the rust +//! and cpp ADNL/overlay implementations. + +use base64::Engine; +use std::{ + io::{BufRead, BufReader, Write}, + path::Path, + process::{self, Child, ChildStdin, Command, Stdio}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{channel, Receiver, RecvTimeoutError}, + Arc, + }, + thread::{self, JoinHandle}, + time::{Duration, Instant}, +}; + +pub mod overlay_id; +pub mod test_helpers; + +/// Error type for compatibility tests +#[derive(thiserror::Error, Debug)] +pub enum CompatTestError { + #[error("C++ binary not found: {0}")] + BinaryNotFound(String), + + #[error("C++ node failed to start: {0}")] + NodeStartFailed(String), + + #[error("Command failed: {0}")] + CommandFailed(String), + + #[error("Invalid response: {0}")] + InvalidResponse(String), + + #[error("Timeout waiting for response")] + Timeout, + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), + + #[error("Node not ready")] + NotReady, +} + +pub type Result = std::result::Result; + +/// Default paths to look for the C++ test binary +const DEFAULT_CPP_BINARY_PATHS: &[&str] = + &["cpp_src/build/compat_test_node", "../compat_test/cpp_src/build/compat_test_node"]; + +/// Timeout for waiting for C++ node to become ready +const DEFAULT_READY_TIMEOUT: Duration = Duration::from_secs(10); + +/// Timeout for individual command responses +const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(5); + +/// Check if C++ test binary is available +pub fn cpp_binary_available() -> bool { + get_cpp_binary_path().is_ok() +} + +/// Get path to C++ test binary +pub fn get_cpp_binary_path() -> Result { + // First check environment variable + if let Ok(path) = std::env::var("CPP_COMPAT_TEST_BIN") { + if Path::new(&path).exists() { + return Ok(path); + } + } + + // Try default paths relative to CARGO_MANIFEST_DIR + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + for rel_path in DEFAULT_CPP_BINARY_PATHS { + let full_path = Path::new(&manifest_dir).join(rel_path); + if full_path.exists() { + return Ok(full_path.to_string_lossy().to_string()); + } + } + } + + // Try default paths relative to current directory + for rel_path in DEFAULT_CPP_BINARY_PATHS { + if Path::new(rel_path).exists() { + return Ok(rel_path.to_string()); + } + } + + Err(CompatTestError::BinaryNotFound( + "C++ binary not found. Set CPP_COMPAT_TEST_BIN or build cpp_src/build/compat_test_node" + .to_string(), + )) +} + +fn b64_encode(data: &[u8]) -> String { + base64::engine::general_purpose::STANDARD.encode(data) +} + +fn b64_decode(s: &str) -> std::result::Result, base64::DecodeError> { + base64::engine::general_purpose::STANDARD.decode(s) +} + +/// Command to send to C++ node (JSON over stdin) +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "cmd")] +pub enum CppCommand { + #[serde(rename = "ping")] + Ping, + + #[serde(rename = "get_info")] + GetInfo, + + #[serde(rename = "compute_overlay_id")] + ComputeOverlayId { + /// base64-encoded overlay name bytes + name: String, + }, + + #[serde(rename = "add_peer")] + AddPeer { + /// base64-encoded TL-serialized public key + pubkey: String, + ip: String, + port: u16, + }, + + #[serde(rename = "create_overlay")] + CreateOverlay { + /// "public", "private", or "semiprivate" + #[serde(rename = "type")] + overlay_type: String, + /// base64-encoded overlay name bytes + overlay_name: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + peers: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + root_pub_keys: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + certificate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_slaves: Option, + #[serde(skip_serializing_if = "std::ops::Not::not")] + #[serde(default)] + enable_twostep: bool, + }, + + #[serde(rename = "delete_overlay")] + DeleteOverlay { overlay_id: String }, + + #[serde(rename = "get_overlay_node_info")] + GetOverlayNodeInfo { overlay_id: String }, + + #[serde(rename = "send_broadcast")] + SendBroadcast { + overlay_id: String, + /// base64-encoded data + data: String, + #[serde(default)] + use_fec: bool, + }, + + #[serde(rename = "send_query")] + SendQuery { + overlay_id: String, + peer_adnl_id: String, + /// base64-encoded query data + data: String, + timeout_ms: i64, + }, + + #[serde(rename = "send_rldp_query")] + SendRldpQuery { + overlay_id: String, + peer_adnl_id: String, + /// base64-encoded query data + data: String, + max_answer_size: u64, + #[serde(default)] + v2: bool, + }, + + #[serde(rename = "set_query_handler")] + SetQueryHandler { + overlay_id: String, + /// "echo", "capabilities", or "reject" + mode: String, + }, + + #[serde(rename = "set_broadcast_validator")] + SetBroadcastValidator { + overlay_id: String, + /// "accept_all" or "reject_all" + mode: String, + }, + + #[serde(rename = "get_received_broadcasts")] + GetReceivedBroadcasts { overlay_id: String }, + + #[serde(rename = "clear_received_broadcasts")] + ClearReceivedBroadcasts { overlay_id: String }, + + #[serde(rename = "send_message")] + SendMessage { + overlay_id: String, + peer_adnl_id: String, + /// base64-encoded data + data: String, + }, + + #[serde(rename = "get_received_messages")] + GetReceivedMessages { overlay_id: String }, + + #[serde(rename = "clear_received_messages")] + ClearReceivedMessages { overlay_id: String }, + + #[serde(rename = "compress_boc")] + CompressBoc { + /// base64-encoded standard BOC data + data: String, + /// "baseline" or "improved" + algorithm: String, + }, + + #[serde(rename = "decompress_boc")] + DecompressBoc { + /// base64-encoded compressed BOC data + data: String, + /// Maximum decompressed size in bytes + max_size: u32, + }, + + #[serde(rename = "compute_candidate_id_to_sign")] + ComputeCandidateIdToSign { + slot: i32, + /// 32-byte candidate hash as hex + hash: String, + }, + + #[serde(rename = "enable_quic")] + EnableQuic {}, + + #[serde(rename = "send_quic_message")] + SendQuicMessage { + peer_adnl_id: String, + /// base64-encoded data + data: String, + }, + + #[serde(rename = "send_quic_query")] + SendQuicQuery { + peer_adnl_id: String, + /// base64-encoded data + data: String, + timeout_ms: i64, + }, + + #[serde(rename = "shutdown")] + Shutdown, +} + +/// Ready response from C++ node +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ReadyResponse { + pub status: String, + pub adnl_id: String, + pub pubkey: String, + pub udp_port: u16, +} + +/// Response from C++ node +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(untagged)] +pub enum CppResponse { + Ready(ReadyResponse), + Result { result: serde_json::Value }, + Error { error: String }, +} + +/// Received broadcast record from C++ node +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ReceivedBroadcast { + pub source: String, + pub size: usize, + pub data: String, // base64 encoded + pub timestamp: i32, + pub accepted: bool, +} + +/// Received message record from C++ node (point-to-point overlay messages) +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ReceivedMessage { + pub source: String, + pub size: usize, + pub data: String, // base64 encoded + pub timestamp: i32, +} + +/// Info about the C++ node +#[derive(Debug, Clone)] +pub struct NodeInfo { + pub adnl_id: String, + pub pubkey: String, + pub udp_port: u16, +} + +/// Handle to a running C++ test node +pub struct CppTestNode { + process: Child, + stdin: ChildStdin, + response_rx: Receiver, + _reader_thread: Option>, + info: NodeInfo, +} + +impl CppTestNode { + /// Spawn a new C++ test node on the given UDP port + pub fn spawn(udp_port: u16) -> Result { + let binary_path = get_cpp_binary_path()?; + + let db_path = format!("/tmp/compat_test_cpp_{}", udp_port); + + // Clean up old database + let _ = std::fs::remove_dir_all(&db_path); + + let mut process = Command::new(&binary_path) + .arg("--port") + .arg(udp_port.to_string()) + .arg("--db") + .arg(&db_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(|e| CompatTestError::NodeStartFailed(e.to_string()))?; + + let stdin = process + .stdin + .take() + .ok_or_else(|| CompatTestError::NodeStartFailed("Failed to get stdin".to_string()))?; + let stdout = process + .stdout + .take() + .ok_or_else(|| CompatTestError::NodeStartFailed("Failed to get stdout".to_string()))?; + + // Spawn a reader thread to avoid blocking on stdout + let (tx, rx) = channel(); + let reader_thread = thread::spawn(move || { + let mut reader = BufReader::new(stdout); + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => break, + Ok(_) => { + if tx.send(line).is_err() { + break; + } + } + Err(_) => break, + } + } + }); + + let mut node = Self { + process, + stdin, + response_rx: rx, + _reader_thread: Some(reader_thread), + info: NodeInfo { adnl_id: String::new(), pubkey: String::new(), udp_port }, + }; + + // Wait for ready message + node.wait_ready()?; + + Ok(node) + } + + /// Read one line from the response channel with a timeout + fn recv_line(&self, timeout: Duration) -> Result { + self.response_rx.recv_timeout(timeout).map_err(|e| match e { + RecvTimeoutError::Timeout => CompatTestError::Timeout, + RecvTimeoutError::Disconnected => CompatTestError::InvalidResponse( + "Reader thread disconnected (process may have crashed)".to_string(), + ), + }) + } + + /// Wait for the node to be ready + fn wait_ready(&mut self) -> Result<()> { + let line = self.recv_line(DEFAULT_READY_TIMEOUT).map_err(|e| { + CompatTestError::NodeStartFailed(format!( + "Timed out waiting for C++ node to become ready: {}", + e + )) + })?; + + let response: CppResponse = serde_json::from_str(&line)?; + + match response { + CppResponse::Ready(ready) => { + if ready.status != "ready" { + return Err(CompatTestError::NodeStartFailed(format!( + "Unexpected status: {}", + ready.status + ))); + } + self.info.adnl_id = ready.adnl_id; + self.info.pubkey = ready.pubkey; + self.info.udp_port = ready.udp_port; + Ok(()) + } + _ => Err(CompatTestError::NodeStartFailed(format!( + "Unexpected response: {:?}", + response + ))), + } + } + + /// Send a command and get response + pub fn send_command(&mut self, cmd: &CppCommand) -> Result { + let json = serde_json::to_string(cmd)?; + writeln!(self.stdin, "{}", json)?; + self.stdin.flush()?; + + let line = self.recv_line(DEFAULT_COMMAND_TIMEOUT)?; + + if line.is_empty() { + return Err(CompatTestError::InvalidResponse( + "Empty response (process may have crashed)".to_string(), + )); + } + + let response: CppResponse = serde_json::from_str(&line)?; + Ok(response) + } + + /// Extract result value, returning error if response is an error + fn expect_result(&mut self, cmd: &CppCommand) -> Result { + let response = self.send_command(cmd)?; + match response { + CppResponse::Result { result } => Ok(result), + CppResponse::Error { error } => Err(CompatTestError::CommandFailed(error)), + _ => Err(CompatTestError::InvalidResponse("Unexpected response type".to_string())), + } + } + + // ---- Info ---- + + /// Get node info + pub fn info(&self) -> &NodeInfo { + &self.info + } + + /// Get local ADNL ID (hex) + pub fn adnl_id(&self) -> &str { + &self.info.adnl_id + } + + /// Get local public key (base64 TL) + pub fn pubkey(&self) -> &str { + &self.info.pubkey + } + + /// Get UDP port + pub fn udp_port(&self) -> u16 { + self.info.udp_port + } + + // ---- Basic commands ---- + + /// Ping the node + pub fn ping(&mut self) -> Result<()> { + let result = self.expect_result(&CppCommand::Ping)?; + if result.as_str() == Some("pong") { + Ok(()) + } else { + Err(CompatTestError::InvalidResponse(format!("{:?}", result))) + } + } + + /// Get full info from running node + pub fn get_info(&mut self) -> Result { + let result = self.expect_result(&CppCommand::GetInfo)?; + Ok(NodeInfo { + adnl_id: result["adnl_id"].as_str().unwrap_or_default().to_string(), + pubkey: result["pubkey"].as_str().unwrap_or_default().to_string(), + udp_port: result["udp_port"].as_u64().unwrap_or_default() as u16, + }) + } + + // ---- Overlay ID ---- + + /// Compute overlay ID from name bytes (raw bytes, will be base64-encoded) + pub fn compute_overlay_id(&mut self, name: &[u8]) -> Result { + let result = + self.expect_result(&CppCommand::ComputeOverlayId { name: b64_encode(name) })?; + result["overlay_id"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) + } + + // ---- Peer management ---- + + /// Add a peer to the ADNL peer table + pub fn add_peer(&mut self, pubkey_tl_b64: &str, ip: &str, port: u16) -> Result { + let result = self.expect_result(&CppCommand::AddPeer { + pubkey: pubkey_tl_b64.to_string(), + ip: ip.to_string(), + port, + })?; + result["peer_id"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected peer_id".to_string())) + } + + // ---- Overlay creation ---- + + /// Create a public overlay + pub fn create_public_overlay(&mut self, overlay_name: &[u8]) -> Result { + let result = self.expect_result(&CppCommand::CreateOverlay { + overlay_type: "public".to_string(), + overlay_name: b64_encode(overlay_name), + peers: vec![], + root_pub_keys: vec![], + certificate: None, + max_slaves: None, + enable_twostep: false, + })?; + result["overlay_id"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) + } + + /// Create a private overlay with given peer ADNL IDs (hex) + pub fn create_private_overlay( + &mut self, + overlay_name: &[u8], + peers: Vec, + ) -> Result { + let result = self.expect_result(&CppCommand::CreateOverlay { + overlay_type: "private".to_string(), + overlay_name: b64_encode(overlay_name), + peers, + root_pub_keys: vec![], + certificate: None, + max_slaves: None, + enable_twostep: false, + })?; + result["overlay_id"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) + } + + /// Create a private overlay with TwostepFec enabled + pub fn create_private_overlay_twostep( + &mut self, + overlay_name: &[u8], + peers: Vec, + ) -> Result { + let result = self.expect_result(&CppCommand::CreateOverlay { + overlay_type: "private".to_string(), + overlay_name: b64_encode(overlay_name), + peers, + root_pub_keys: vec![], + certificate: None, + max_slaves: None, + enable_twostep: true, + })?; + result["overlay_id"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) + } + + /// Create a semiprivate overlay + pub fn create_semiprivate_overlay( + &mut self, + overlay_name: &[u8], + peers: Vec, + root_pub_keys: Vec, + certificate: Option<&[u8]>, + max_slaves: Option, + ) -> Result { + let result = self.expect_result(&CppCommand::CreateOverlay { + overlay_type: "semiprivate".to_string(), + overlay_name: b64_encode(overlay_name), + peers, + root_pub_keys, + certificate: certificate.map(b64_encode), + max_slaves, + enable_twostep: false, + })?; + result["overlay_id"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) + } + + /// Delete an overlay + pub fn delete_overlay(&mut self, overlay_id: &str) -> Result<()> { + self.expect_result(&CppCommand::DeleteOverlay { overlay_id: overlay_id.to_string() })?; + Ok(()) + } + + // ---- Overlay node info ---- + + /// Get TL-serialized overlay.node for this node in the given overlay + /// Returns base64-encoded TL bytes + pub fn get_overlay_node_info(&mut self, overlay_id: &str) -> Result { + let result = self.expect_result(&CppCommand::GetOverlayNodeInfo { + overlay_id: overlay_id.to_string(), + })?; + result["node_tl"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected node_tl".to_string())) + } + + // ---- Broadcasts ---- + + /// Send a broadcast (optionally FEC) + pub fn send_broadcast(&mut self, overlay_id: &str, data: &[u8], use_fec: bool) -> Result<()> { + self.expect_result(&CppCommand::SendBroadcast { + overlay_id: overlay_id.to_string(), + data: b64_encode(data), + use_fec, + })?; + Ok(()) + } + + /// Get received broadcasts for an overlay + pub fn get_received_broadcasts(&mut self, overlay_id: &str) -> Result> { + let result = self.expect_result(&CppCommand::GetReceivedBroadcasts { + overlay_id: overlay_id.to_string(), + })?; + let broadcasts: Vec = serde_json::from_value(result)?; + Ok(broadcasts) + } + + /// Clear received broadcasts for an overlay + pub fn clear_received_broadcasts(&mut self, overlay_id: &str) -> Result<()> { + self.expect_result(&CppCommand::ClearReceivedBroadcasts { + overlay_id: overlay_id.to_string(), + })?; + Ok(()) + } + + /// Set broadcast validator mode + pub fn set_broadcast_validator(&mut self, overlay_id: &str, mode: &str) -> Result<()> { + self.expect_result(&CppCommand::SetBroadcastValidator { + overlay_id: overlay_id.to_string(), + mode: mode.to_string(), + })?; + Ok(()) + } + + // ---- Queries ---- + + /// Send an overlay query, returns answer bytes + pub fn send_query( + &mut self, + overlay_id: &str, + peer_adnl_id: &str, + data: &[u8], + timeout_ms: i64, + ) -> Result> { + let result = self.expect_result(&CppCommand::SendQuery { + overlay_id: overlay_id.to_string(), + peer_adnl_id: peer_adnl_id.to_string(), + data: b64_encode(data), + timeout_ms, + })?; + let answer_b64 = result["answer"] + .as_str() + .ok_or_else(|| CompatTestError::InvalidResponse("Expected answer".to_string()))?; + b64_decode(answer_b64) + .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 answer: {}", e))) + } + + /// Send an RLDP query via overlay + pub fn send_rldp_query( + &mut self, + overlay_id: &str, + peer_adnl_id: &str, + data: &[u8], + max_answer_size: u64, + v2: bool, + ) -> Result> { + let result = self.expect_result(&CppCommand::SendRldpQuery { + overlay_id: overlay_id.to_string(), + peer_adnl_id: peer_adnl_id.to_string(), + data: b64_encode(data), + max_answer_size, + v2, + })?; + let answer_b64 = result["answer"] + .as_str() + .ok_or_else(|| CompatTestError::InvalidResponse("Expected answer".to_string()))?; + b64_decode(answer_b64) + .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 answer: {}", e))) + } + + // ---- Point-to-point messages ---- + + /// Send a point-to-point overlay message (not broadcast) + pub fn send_message( + &mut self, + overlay_id: &str, + peer_adnl_id: &str, + data: &[u8], + ) -> Result<()> { + self.expect_result(&CppCommand::SendMessage { + overlay_id: overlay_id.to_string(), + peer_adnl_id: peer_adnl_id.to_string(), + data: b64_encode(data), + })?; + Ok(()) + } + + /// Get received messages for an overlay + pub fn get_received_messages(&mut self, overlay_id: &str) -> Result> { + let result = self.expect_result(&CppCommand::GetReceivedMessages { + overlay_id: overlay_id.to_string(), + })?; + let messages: Vec = serde_json::from_value(result)?; + Ok(messages) + } + + /// Clear received messages for an overlay + pub fn clear_received_messages(&mut self, overlay_id: &str) -> Result<()> { + self.expect_result(&CppCommand::ClearReceivedMessages { + overlay_id: overlay_id.to_string(), + })?; + Ok(()) + } + + // ---- Queries ---- + + /// Set query handler mode + pub fn set_query_handler(&mut self, overlay_id: &str, mode: &str) -> Result<()> { + self.expect_result(&CppCommand::SetQueryHandler { + overlay_id: overlay_id.to_string(), + mode: mode.to_string(), + })?; + Ok(()) + } + + // ---- BOC Compression ---- + + /// Compress BOC data on the C++ side. + /// Takes base64-encoded standard BOC, returns base64-encoded compressed data. + pub fn compress_boc(&mut self, boc_b64: &str, algorithm: &str) -> Result { + let result = self.expect_result(&CppCommand::CompressBoc { + data: boc_b64.to_string(), + algorithm: algorithm.to_string(), + })?; + result["compressed"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected 'compressed'".to_string())) + } + + /// Decompress BOC data on the C++ side. + /// Takes base64-encoded compressed data, returns base64-encoded standard BOC. + pub fn decompress_boc(&mut self, compressed_b64: &str, max_size: u32) -> Result { + let result = self.expect_result(&CppCommand::DecompressBoc { + data: compressed_b64.to_string(), + max_size, + })?; + result["boc"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| CompatTestError::InvalidResponse("Expected 'boc'".to_string())) + } + + /// Build serialized TL bytes for consensus.candidateId(slot, hash) on C++ side. + pub fn compute_candidate_id_to_sign(&mut self, slot: i32, hash_hex: &str) -> Result> { + let result = self.expect_result(&CppCommand::ComputeCandidateIdToSign { + slot, + hash: hash_hex.to_string(), + })?; + let data_b64 = result["data"] + .as_str() + .ok_or_else(|| CompatTestError::InvalidResponse("Expected 'data'".to_string()))?; + b64_decode(data_b64) + .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 data: {}", e))) + } + + // ---- QUIC ---- + + /// Enable QUIC transport (creates QuicSender, listens on udp_port + 1000) + pub fn enable_quic(&mut self) -> Result { + let result = self.expect_result(&CppCommand::EnableQuic {})?; + let quic_port = result["quic_port"] + .as_u64() + .ok_or_else(|| CompatTestError::InvalidResponse("Expected quic_port".to_string()))?; + Ok(quic_port as u16) + } + + /// Send a message via QUIC transport (bypasses overlay, goes through ADNL) + pub fn send_quic_message(&mut self, peer_adnl_id: &str, data: &[u8]) -> Result<()> { + self.expect_result(&CppCommand::SendQuicMessage { + peer_adnl_id: peer_adnl_id.to_string(), + data: b64_encode(data), + })?; + Ok(()) + } + + /// Send a query via QUIC transport, returns answer bytes + pub fn send_quic_query( + &mut self, + peer_adnl_id: &str, + data: &[u8], + timeout_ms: i64, + ) -> Result> { + let result = self.expect_result(&CppCommand::SendQuicQuery { + peer_adnl_id: peer_adnl_id.to_string(), + data: b64_encode(data), + timeout_ms, + })?; + let answer_b64 = result["answer"] + .as_str() + .ok_or_else(|| CompatTestError::InvalidResponse("Expected answer".to_string()))?; + b64_decode(answer_b64) + .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 answer: {}", e))) + } + + // ---- Lifecycle ---- + + /// Shutdown the node + pub fn shutdown(&mut self) -> Result<()> { + let _ = self.send_command(&CppCommand::Shutdown); + let _ = self.process.wait(); + Ok(()) + } +} + +impl Drop for CppTestNode { + fn drop(&mut self) { + let _ = self.shutdown(); + } +} + +/// Default test timeout in seconds. Can be overridden via TEST_TIMEOUT env var. +const DEFAULT_TEST_TIMEOUT_SECS: u64 = 90; + +/// Guard that aborts the test process if it exceeds the timeout. +/// Create at the start of each test; the watchdog thread is cancelled on drop. +pub struct TestTimeout { + cancel: Arc, +} + +impl TestTimeout { + /// Create a new test timeout guard. + /// `timeout_secs` โ€” maximum duration for the test; 0 means use the default (90s). + /// The timeout can also be overridden globally via the `TEST_TIMEOUT` env var. + pub fn new(timeout_secs: u64) -> Self { + let secs = std::env::var("TEST_TIMEOUT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(if timeout_secs == 0 { DEFAULT_TEST_TIMEOUT_SECS } else { timeout_secs }); + + let cancel = Arc::new(AtomicBool::new(false)); + let cancel_clone = cancel.clone(); + let thread_name = thread::current().name().unwrap_or("unknown").to_string(); + + thread::spawn(move || { + let deadline = Instant::now() + Duration::from_secs(secs); + while Instant::now() < deadline { + if cancel_clone.load(Ordering::Relaxed) { + return; + } + thread::sleep(Duration::from_millis(500)); + } + if !cancel_clone.load(Ordering::Relaxed) { + eprintln!( + "\n\x1b[1;31mTEST TIMEOUT: '{}' exceeded {}s limit โ€” aborting process\x1b[0m", + thread_name, secs + ); + process::exit(1); + } + }); + + Self { cancel } + } +} + +impl Drop for TestTimeout { + fn drop(&mut self) { + self.cancel.store(true, Ordering::Relaxed); + } +} + +/// Skip test if C++ binary is not available +#[macro_export] +macro_rules! skip_if_no_cpp { + () => { + if !$crate::cpp_binary_available() { + eprintln!("Skipping test: CPP_COMPAT_TEST_BIN not set"); + return; + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpp_binary_check() { + // This just verifies the check works + let _ = cpp_binary_available(); + } +} diff --git a/src/node/tests/compat_test/src/overlay_id.rs b/src/node/tests/compat_test/src/overlay_id.rs new file mode 100644 index 0000000..fd95663 --- /dev/null +++ b/src/node/tests/compat_test/src/overlay_id.rs @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Overlay ID calculation utilities +//! +//! This module provides functions to compute overlay IDs in a way compatible +//! with the cpp implementation. + +use ton_api::{serialize_boxed, ton::pub_::publickey::Overlay as OverlayKey, IntoBoxed}; +use ton_block::sha256_digest; + +/// Compute overlay short ID from overlay name (same as C++ OverlayIdFull::compute_short_id) +/// +/// The overlay ID is computed by: +/// 1. Creating an "overlay pubkey" from the name using the overlay key type +/// 2. Computing the short ID (SHA256 hash) of that boxed pubkey +/// +/// The input `name` is the raw TL bytes that would be passed to OverlayIdFull. +pub fn compute_overlay_id(name: &[u8]) -> [u8; 32] { + // Use the same approach as adnl/src/overlay/mod.rs: + // OverlayKey { name: ... } then hash_boxed + let overlay_key = OverlayKey { name: name.to_vec().into() }; + let boxed = overlay_key.into_boxed(); + let serialized = serialize_boxed(&boxed).expect("serialize overlay key"); + sha256_digest(&serialized) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_overlay_id_basic() { + let name = b"test_overlay"; + let id = compute_overlay_id(name); + + // The ID should be a valid 32-byte hash + assert_eq!(id.len(), 32); + + // Same input should produce same output + let id2 = compute_overlay_id(name); + assert_eq!(id, id2); + + // Different input should produce different output + let id3 = compute_overlay_id(b"other_overlay"); + assert_ne!(id, id3); + } + + #[test] + fn test_overlay_id_empty() { + let id = compute_overlay_id(b""); + assert_eq!(id.len(), 32); + } + + #[test] + fn test_overlay_id_long_name() { + // Test with name > 254 bytes + let name = vec![b'x'; 300]; + let id = compute_overlay_id(&name); + assert_eq!(id.len(), 32); + } +} diff --git a/src/node/tests/compat_test/src/test_helpers.rs b/src/node/tests/compat_test/src/test_helpers.rs new file mode 100644 index 0000000..daa8efd --- /dev/null +++ b/src/node/tests/compat_test/src/test_helpers.rs @@ -0,0 +1,778 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Test helpers for cross-implementation compatibility tests. +//! +//! Provides utilities for creating Rust ADNL+overlay nodes and +//! exchanging peers with cpp test nodes. + +use crate::CppTestNode; +use adnl::{ + common::{ + hash, AdnlPeers, Answer, QueryAnswer, QueryResult, Subscriber, TaggedByteSlice, + TaggedTlObject, + }, + node::{AdnlNode, AdnlNodeConfig, AdnlSendMethod, IpAddress}, + OverlayNode, OverlayNodeInfo, OverlayParams, OverlayShortId, QuicNode, RldpNode, +}; +use std::{ + net::{Ipv4Addr, SocketAddr}, + sync::{Arc, Mutex}, + time::Duration, +}; +use ton_api::{ + deserialize_boxed, serialize_boxed, serialize_boxed_append, + ton::{ + overlay::{ + message::Message as OverlayMessage, node::Node as OverlayNodeV1, + nodev2::NodeV2 as OverlayNodeV2, Node as OverlayNodeBoxed, + }, + pub_::publickey::{Ed25519 as Ed25519PubKey, Overlay as OverlayKey}, + rpc::overlay::Query as OverlayQuery, + ton_node::data::Data as TonNodeData, + }, + IntoBoxed, TLObject, +}; +use ton_block::{ + base64_decode, base64_encode, sha256_digest, Ed25519KeyOption, KeyId, Result, UInt256, +}; + +const KEY_TAG_OVERLAY: usize = 2; + +/// A Rust ADNL + overlay test node +pub struct RustTestNode { + pub rt: tokio::runtime::Runtime, + pub adnl: Arc, + pub overlay: Arc, + pub addr: String, + pub port: u16, +} + +impl RustTestNode { + /// Create a new Rust ADNL+overlay node on the given IP:port. + /// If `with_rldp` is true, an RLDP node is created and registered, + /// enabling TwostepFec broadcasts. + pub fn new(ip: &str, port: u16, with_rldp: bool) -> Self { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(2) + .build() + .expect("Failed to create tokio runtime"); + + let addr = format!("{}:{}", ip, port); + let zero_state = [0u8; 32]; // Test zero state + + // Generate deterministic key from address + let key_data = sha256_digest(addr.as_bytes()); + let keys = vec![(key_data, KEY_TAG_OVERLAY)]; + let (_, config) = + AdnlNodeConfig::from_ip_address_and_private_keys(&addr, keys).expect("Config failed"); + + let adnl = rt.block_on(AdnlNode::with_config(config)).expect("ADNL node creation failed"); + + let overlay = OverlayNode::with_params(adnl.clone(), &zero_state, KEY_TAG_OVERLAY) + .expect("Overlay node creation failed"); + + if with_rldp { + let rldp = RldpNode::with_params(adnl.clone(), vec![overlay.clone()], None) + .expect("RLDP node creation failed"); + overlay.set_rldp(rldp.clone()).expect("set_rldp failed"); + + let subscribers: Vec> = vec![overlay.clone(), rldp]; + + rt.block_on(async { + adnl.start_over_udp(subscribers).await.expect("Failed to start ADNL UDP"); + }); + } else { + let subscribers: Vec> = vec![overlay.clone()]; + + rt.block_on(async { + adnl.start_over_udp(subscribers).await.expect("Failed to start ADNL UDP"); + }); + } + + Self { rt, adnl, overlay, addr, port } + } + + /// Get the ADNL key ID (hex) + pub fn adnl_id_hex(&self) -> String { + self.adnl + .key_by_tag(KEY_TAG_OVERLAY) + .expect("No key") + .id() + .data() + .iter() + .map(|b| format!("{:02x}", b)) + .collect() + } + + /// Get the ADNL public key as base64 TL-serialized + pub fn pubkey_tl_b64(&self) -> String { + let key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + let pub_key = key.pub_key().expect("No pub key"); + // Export as TL-serialized pub.ed25519{key:int256} + let tl_key = + Ed25519PubKey { key: UInt256::with_array(pub_key.try_into().expect("Wrong key size")) }; + let serialized = serialize_boxed(&tl_key.into_boxed()).expect("Serialization failed"); + base64_encode(&serialized) + } + + /// Get the ADNL key Arc + pub fn adnl_key_id(&self) -> Arc { + self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key").id().clone() + } + + /// Compute the overlay name TL bytes for a given workchain/shard + /// This is the bytes that should be passed to C++ as overlay_name + pub fn compute_overlay_name(&self, workchain: i32, shard: i64) -> Vec { + let overlay_id = self.overlay.calc_overlay_id(workchain, shard).expect("calc_overlay_id"); + overlay_id.to_vec() + } + + /// Compute overlay short ID + pub fn compute_overlay_short_id(&self, workchain: i32, shard: i64) -> Arc { + self.overlay.calc_overlay_short_id(workchain, shard).expect("calc_overlay_short_id") + } + + /// Compute overlay short ID from arbitrary name bytes. + /// This wraps the name in a TL pub.overlay{name} structure before hashing, + /// matching the C++ OverlayIdFull::compute_short_id() behavior. + pub fn compute_overlay_short_id_from_name(&self, name: &[u8]) -> Arc { + let overlay_key = OverlayKey { name: name.to_vec().into() }; + let id = hash(overlay_key).expect("hash overlay key"); + OverlayShortId::from_data(id) + } + + /// Add public overlay + pub fn add_public_overlay(&self, overlay_id: &Arc) { + self.rt.block_on(async { + let params = OverlayParams::with_id_only(overlay_id); + self.overlay.add_local_workchain_overlay(params).expect("Failed to add overlay"); + }); + } + + /// Add private overlay with given peer ADNL IDs (hex strings) + /// Note: For simplicity, this creates a public overlay internally since creating + /// a true private overlay requires a signing key. The C++ side creates a private + /// overlay which doesn't require DHT. + pub fn add_private_overlay(&self, overlay_id: &Arc, _peers: Vec) { + self.rt.block_on(async { + // For test purposes, just create as public overlay on Rust side + // The C++ side creates a true private overlay + let params = OverlayParams::with_id_only(overlay_id); + self.overlay.add_local_workchain_overlay(params).expect("Failed to add overlay"); + }); + } + + /// Add a true private overlay with signing key and peer list. + /// Unlike `add_private_overlay` (which creates a public overlay as shortcut), + /// this creates a real private overlay where `try_consume_custom` is dispatched + /// to the registered consumer. + pub fn add_true_private_overlay(&self, overlay_id: &Arc, peers: &[Arc]) { + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + let params = OverlayParams { + flags: 0, + hops: None, + overlay_id, + runtime: Some(self.rt.handle().clone()), + }; + self.overlay + .add_private_overlay(params, &local_key, peers) + .expect("add_private_overlay failed"); + } + + /// Parse C++ node's base64 TL public key to raw 32-byte key + fn parse_cpp_pubkey(pubkey_tl_b64: &str) -> [u8; 32] { + let tl_bytes = base64_decode(pubkey_tl_b64).expect("decode pubkey b64"); + // TL: pub.ed25519#4813b4c6 key:int256 = PublicKey + // Skip 4-byte constructor, take 32-byte key + assert!(tl_bytes.len() >= 36, "TL pubkey too short: {}", tl_bytes.len()); + let key_bytes: [u8; 32] = tl_bytes[4..36].try_into().expect("wrong key len"); + key_bytes + } + + /// Add the C++ node as an ADNL peer (but not to any overlay) + pub fn add_cpp_peer(&self, cpp: &CppTestNode) { + let raw_key = Self::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) + .expect("parse IP"); + + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + } + + /// Add the C++ node as an ADNL peer AND to a specific public overlay via signed node. + pub fn add_cpp_peer_to_overlay(&self, cpp: &mut CppTestNode, overlay_id: &Arc) { + let raw_key = Self::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) + .expect("parse IP"); + + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + + let signed_node = Self::get_cpp_signed_node(cpp, overlay_id); + self.overlay.add_public_peer(&ip, &signed_node, overlay_id).expect("add_public_peer"); + } + + /// Add another Rust node as an ADNL peer AND to a specific public overlay via signed node. + pub fn add_rust_peer_to_overlay(&self, other: &RustTestNode, overlay_id: &Arc) { + let other_key = other.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key on other node"); + let other_pubkey_data = other_key.pub_key().expect("No pub key on other node"); + let other_pubkey = Ed25519KeyOption::from_public_key( + other_pubkey_data.try_into().expect("Wrong key size"), + ); + let other_ip = IpAddress::from_versioned_string(&other.addr, None).expect("parse other IP"); + + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + self.adnl.add_peer(local_key.id(), &other_ip, &other_pubkey).expect("add_peer"); + + let signed_node = + other.overlay.get_signed_node(overlay_id, false).expect("get_signed_node"); + self.overlay.add_public_peer(&other_ip, &signed_node, overlay_id).expect("add_public_peer"); + } + + /// Get the KeyId for the C++ node (based on its public key) + pub fn cpp_key_id(cpp: &CppTestNode) -> Arc { + let raw_key = Self::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + pubkey.id().clone() + } + + /// Get a signed overlay node description from the C++ node. + fn get_cpp_signed_node( + cpp: &mut CppTestNode, + overlay_id: &Arc, + ) -> OverlayNodeInfo { + let node_b64 = cpp + .get_overlay_node_info(&hex::encode(overlay_id.data())) + .expect("get_overlay_node_info failed"); + let node_bytes = base64_decode(&node_b64).expect("decode node_tl"); + let tl_obj = deserialize_boxed(&node_bytes).expect("deserialize node TL"); + let node = tl_obj.downcast::().expect("downcast to overlay.Node"); + OverlayNodeInfo::V1(node.only()) + } + + /// Send a point-to-point overlay message (not broadcast) to a specific peer. + /// This uses overlay.message() - the same path as consensus votes/certificates. + pub fn send_message(&self, overlay_id: &Arc, dst: &Arc, data: &[u8]) { + self.rt.block_on(async { + let tagged = TaggedByteSlice::with_object(data); + self.overlay.message(dst, &tagged, overlay_id).await.expect("overlay message failed"); + println!("send_message: OK"); + }); + } + + /// Send broadcast via overlay + pub fn send_broadcast(&self, overlay_id: &Arc, data: &[u8]) { + self.rt.block_on(async { + let tagged = TaggedByteSlice::with_object(data); + self.overlay + .broadcast(overlay_id, &tagged, None, 0, AdnlSendMethod::Fast) + .await + .expect("broadcast failed"); + }); + } + + /// Send two-step FEC broadcast via overlay (requires RLDP) + pub fn send_broadcast_two_step(&self, overlay_id: &Arc, data: &[u8]) { + self.rt.block_on(async { + let tagged = TaggedByteSlice::with_object(data); + self.overlay + .broadcast_two_step(overlay_id, &tagged, None, 0) + .await + .expect("broadcast_two_step failed"); + }); + } + + /// Wait for a broadcast with timeout + pub fn wait_for_broadcast( + &self, + overlay_id: &Arc, + timeout_secs: u64, + ) -> Option> { + self.rt.block_on(async { + tokio::time::timeout( + Duration::from_secs(timeout_secs), + self.overlay.wait_for_broadcast(overlay_id), + ) + .await + .ok() + .and_then(|r| r.ok()) + .flatten() + .map(|info| info.data) + }) + } + + /// Register a query consumer (Subscriber) for an overlay. + /// This is required for receiving RLDP queries on the Rust side. + pub fn register_consumer( + &self, + overlay_id: &Arc, + consumer: Arc, + ) { + self.overlay.add_consumer(overlay_id, consumer).expect("add_consumer failed"); + } + + /// Send an RLDP query via overlay and return the answer. + /// Requires the node to have been created with `with_rldp=true`. + /// + /// The data is wrapped in a `tonNode.data` TL envelope and prepended with + /// the `overlay.query` prefix, matching the C++ `Overlays::send_query_via` behavior. + /// The answer is the raw bytes returned by the responder's echo handler. + pub fn send_rldp_query( + &self, + overlay_id: &Arc, + dst: &Arc, + data: &[u8], + max_answer_size: u64, + v2: bool, + ) -> Option> { + self.rt.block_on(async { + // Wrap data in tonNode.data TL envelope + let tl_data = TonNodeData { data: data.to_vec().into() }; + // Get overlay query prefix (overlay.query{overlay=id}) + let mut query = + self.overlay.get_query_prefix(overlay_id).expect("get_query_prefix failed"); + // Append the TL-serialized data object after the prefix + serialize_boxed_append(&mut query, &tl_data.into_boxed()) + .expect("serialize_boxed_append failed"); + let tagged = TaggedByteSlice::with_object(&query); + let (answer, _roundtrip) = self + .overlay + .query_via_rldp(dst, &tagged, overlay_id, Some(max_answer_size), v2, None) + .await + .expect("query_via_rldp failed"); + answer + }) + } + + /// Stop the node + pub fn stop(&self) { + self.rt.block_on(async { + self.adnl.stop().await; + }); + } +} + +/// A test consumer that echoes back queries +pub struct EchoConsumer; + +impl EchoConsumer { + pub fn new() -> Arc { + Arc::new(Self) + } +} + +#[async_trait::async_trait] +impl Subscriber for EchoConsumer { + async fn try_consume_query(&self, object: TLObject, _peers: &AdnlPeers) -> Result { + // Echo back - use .into() to properly handle telemetry feature + Ok(QueryResult::Consumed(QueryAnswer::Ready(Some(Answer::Object(object.into()))))) + } +} + +/// A subscriber that collects overlay messages (point-to-point, not broadcasts). +/// Used to verify delivery of overlay.message() calls on the receiving side. +pub struct MessageCollector { + messages: Mutex>>, + notify: tokio::sync::Notify, +} + +impl MessageCollector { + pub fn new() -> Arc { + Arc::new(Self { messages: Mutex::new(Vec::new()), notify: tokio::sync::Notify::new() }) + } + + /// Wait until at least `count` messages are collected, or timeout. + pub fn wait_for_messages( + &self, + rt: &tokio::runtime::Runtime, + count: usize, + timeout_secs: u64, + ) -> Vec> { + rt.block_on(async { + let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs); + loop { + { + let msgs = self.messages.lock().unwrap(); + if msgs.len() >= count { + return msgs.clone(); + } + } + if tokio::time::Instant::now() >= deadline { + return self.messages.lock().unwrap().clone(); + } + tokio::select! { + _ = self.notify.notified() => {} + _ = tokio::time::sleep_until(deadline) => { + return self.messages.lock().unwrap().clone(); + } + } + } + }) + } +} + +#[async_trait::async_trait] +impl Subscriber for MessageCollector { + async fn try_consume_custom(&self, data: &[u8], _peers: &AdnlPeers) -> Result { + self.messages.lock().unwrap().push(data.to_vec()); + self.notify.notify_waiters(); + Ok(true) + } +} + +/// A QUIC-capable subscriber that stores received messages and echoes queries. +/// Used for transport-level QUIC tests. +pub struct QuicTestSubscriber { + key_id: Arc, + msg_tx: tokio::sync::mpsc::UnboundedSender>, +} + +impl QuicTestSubscriber { + pub fn new(key_id: Arc) -> (Arc, tokio::sync::mpsc::UnboundedReceiver>) { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + (Arc::new(Self { key_id, msg_tx: tx }), rx) + } +} + +#[async_trait::async_trait] +impl Subscriber for QuicTestSubscriber { + async fn try_consume_custom(&self, data: &[u8], peers: &AdnlPeers) -> Result { + if peers.local() != &self.key_id { + return Ok(false); + } + let _ = self.msg_tx.send(data.to_vec()); + Ok(true) + } + + async fn try_consume_query(&self, object: TLObject, peers: &AdnlPeers) -> Result { + if peers.local() != &self.key_id { + return Ok(QueryResult::Rejected(object)); + } + // Echo back + Ok(QueryResult::Consumed(QueryAnswer::Ready(Some(Answer::Object(object.into()))))) + } +} + +/// A Rust ADNL + overlay + QUIC test node. +/// Extends RustTestNode with QuicNode for cross-implementation QUIC testing. +pub struct RustQuicTestNode { + pub rt: tokio::runtime::Runtime, + pub adnl: Arc, + pub overlay: Arc, + pub quic: Arc, + pub addr: String, + pub port: u16, + #[allow(dead_code)] + key_data: [u8; 32], + cancellation_token: tokio_util::sync::CancellationToken, + quic_msg_rx: Mutex>>, +} + +impl RustQuicTestNode { + /// Create a new Rust ADNL+overlay+QUIC node on the given IP:port. + /// ADNL listens on `port` (UDP), QUIC listens on `port+1000`. + pub fn new(ip: &str, port: u16) -> Self { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(2) + .build() + .expect("Failed to create tokio runtime"); + + let addr = format!("{}:{}", ip, port); + let zero_state = [0u8; 32]; + + // Generate deterministic key from address + let key_data = sha256_digest(addr.as_bytes()); + let keys = vec![(key_data, KEY_TAG_OVERLAY)]; + let (_, config) = + AdnlNodeConfig::from_ip_address_and_private_keys(&addr, keys).expect("Config failed"); + + let key_id = config.key_by_tag(KEY_TAG_OVERLAY).expect("No key").id().clone(); + + let adnl = rt.block_on(AdnlNode::with_config(config)).expect("ADNL node creation failed"); + + let overlay = OverlayNode::with_params(adnl.clone(), &zero_state, KEY_TAG_OVERLAY) + .expect("Overlay node creation failed"); + + // Start ADNL over UDP with overlay as subscriber + let subscribers: Vec> = vec![overlay.clone()]; + rt.block_on(async { + adnl.start_over_udp(subscribers).await.expect("Failed to start ADNL UDP"); + }); + + // Create QuicNode with both a test subscriber and overlay + let cancellation_token = tokio_util::sync::CancellationToken::new(); + let (test_sub, quic_msg_rx) = QuicTestSubscriber::new(key_id.clone()); + + let quic = { + let _guard = rt.enter(); + let quic_subscribers: Vec> = + vec![test_sub as Arc, overlay.clone()]; + let quic = QuicNode::new(quic_subscribers, cancellation_token.clone()); + let bind_addr = SocketAddr::new( + Ipv4Addr::from(adnl.ip_address().ip()).into(), + adnl.ip_address().port() + QuicNode::OFFSET_PORT, + ); + quic.add_key(&key_data, &key_id, bind_addr).expect("QUIC add_key failed"); + quic + }; + + Self { + rt, + adnl, + overlay, + quic, + addr, + port, + key_data, + cancellation_token, + quic_msg_rx: Mutex::new(quic_msg_rx), + } + } + + /// Get the ADNL key ID (hex) + pub fn adnl_id_hex(&self) -> String { + self.adnl + .key_by_tag(KEY_TAG_OVERLAY) + .expect("No key") + .id() + .data() + .iter() + .map(|b| format!("{:02x}", b)) + .collect() + } + + /// Get the ADNL public key as base64 TL-serialized + pub fn pubkey_tl_b64(&self) -> String { + let key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + let pub_key = key.pub_key().expect("No pub key"); + let tl_key = + Ed25519PubKey { key: UInt256::with_array(pub_key.try_into().expect("Wrong key size")) }; + let serialized = serialize_boxed(&tl_key.into_boxed()).expect("Serialization failed"); + base64_encode(&serialized) + } + + /// Get the ADNL key Arc + pub fn adnl_key_id(&self) -> Arc { + self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key").id().clone() + } + + /// Add the C++ node as an ADNL peer (UDP) + pub fn add_cpp_peer(&self, cpp: &CppTestNode) { + let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) + .expect("parse IP"); + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + } + + /// Add the C++ node as a QUIC peer (registers its QUIC address = udp_port + 1000) + pub fn add_cpp_quic_peer(&self, cpp: &CppTestNode) { + let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + let quic_addr: SocketAddr = + format!("127.0.0.1:{}", cpp.udp_port() + 1000).parse().expect("parse QUIC addr"); + self.quic.add_peer_key(pubkey.id().clone(), quic_addr).expect("add_quic_peer"); + } + + /// Add C++ node as both ADNL peer (UDP) and QUIC peer, and to overlay + pub fn add_cpp_peer_full(&self, cpp: &mut CppTestNode, overlay_id: &Arc) { + let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) + .expect("parse IP"); + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + + // Add to overlay via signed node + let signed_node = RustTestNode::get_cpp_signed_node(cpp, overlay_id); + self.overlay.add_public_peer(&ip, &signed_node, overlay_id).expect("add_public_peer"); + + // Add QUIC peer + let quic_addr: SocketAddr = + format!("127.0.0.1:{}", cpp.udp_port() + 1000).parse().expect("parse QUIC addr"); + self.quic.add_peer_key(pubkey.id().clone(), quic_addr).expect("add_quic_peer"); + } + + /// Get the KeyId for the C++ node + pub fn cpp_key_id(cpp: &CppTestNode) -> Arc { + RustTestNode::cpp_key_id(cpp) + } + + /// Compute overlay name + pub fn compute_overlay_name(&self, workchain: i32, shard: i64) -> Vec { + let overlay_id = self.overlay.calc_overlay_id(workchain, shard).expect("calc_overlay_id"); + overlay_id.to_vec() + } + + /// Compute overlay short ID + pub fn compute_overlay_short_id(&self, workchain: i32, shard: i64) -> Arc { + self.overlay.calc_overlay_short_id(workchain, shard).expect("calc_overlay_short_id") + } + + /// Add public overlay + pub fn add_public_overlay(&self, overlay_id: &Arc) { + self.rt.block_on(async { + let params = OverlayParams::with_id_only(overlay_id); + self.overlay.add_local_workchain_overlay(params).expect("Failed to add overlay"); + }); + } + + /// Send a QUIC message (internally wrapped in quic.request.Message TL by QuicNode). + /// Note: `data` is sent as-is inside quic_message.data_. On C++ side this goes through + /// AdnlLocalId::deliver which requires matching TL prefix (e.g. overlay.message). + /// Use `send_quic_overlay_message` to properly format data for overlay delivery. + pub fn send_quic_message(&self, dst: &Arc, data: &[u8]) { + let src = self.adnl_key_id(); + self.rt.block_on(async { + self.quic + .message(data.to_vec(), Some(&*self.adnl), &src, dst) + .await + .expect("QUIC message failed"); + }); + } + + /// Send a QUIC message with overlay TL wrapping. + /// Data is formatted as: overlay.message { overlay_id } ++ payload + /// which matches the C++ AdnlLocalId callback prefix for overlay routing. + pub fn send_quic_overlay_message( + &self, + dst: &Arc, + overlay_id: &Arc, + payload: &[u8], + ) { + let src = self.adnl_key_id(); + let mut overlay_data = serialize_boxed( + &OverlayMessage { overlay: UInt256::with_array(*overlay_id.data()) }.into_boxed(), + ) + .expect("serialize overlay message prefix"); + overlay_data.extend_from_slice(payload); + + self.rt.block_on(async { + self.quic + .message(overlay_data, Some(&*self.adnl), &src, dst) + .await + .expect("QUIC message failed"); + }); + } + + /// Send a QUIC query (internally wrapped in quic.request.Query TL by QuicNode). + /// Returns the TL-deserialized answer bytes. + /// Use `send_quic_overlay_query` for overlay-routed queries. + pub fn send_quic_query(&self, dst: &Arc, data: &[u8]) -> Vec { + let src = self.adnl_key_id(); + self.rt.block_on(async { + self.quic + .query(data.to_vec(), Some(&*self.adnl), &src, dst, None) + .await + .expect("QUIC query failed") + .expect("empty QUIC query answer") + }) + } + + /// Send a QUIC query with overlay TL wrapping, with a timeout. + /// Data is formatted as: overlay.query { overlay_id } ++ payload + /// Returns Ok(answer) or Err if timeout/connection fails. + pub fn send_quic_overlay_query( + &self, + dst: &Arc, + overlay_id: &Arc, + payload: &[u8], + timeout_secs: u64, + ) -> std::result::Result, String> { + let src = self.adnl_key_id(); + let mut overlay_data = + serialize_boxed(&OverlayQuery { overlay: UInt256::with_array(*overlay_id.data()) }) + .expect("serialize overlay query prefix"); + overlay_data.extend_from_slice(payload); + + self.rt.block_on(async { + match tokio::time::timeout( + Duration::from_secs(timeout_secs), + self.quic.query(overlay_data, Some(&*self.adnl), &src, dst, None), + ) + .await + { + Ok(Ok(Some(answer))) => Ok(answer), + Ok(Ok(None)) => Err("empty QUIC query answer".to_string()), + Ok(Err(e)) => Err(format!("QUIC query failed: {}", e)), + Err(_) => Err("QUIC query timed out".to_string()), + } + }) + } + + /// Receive a QUIC message with timeout (from the test subscriber channel) + pub fn recv_quic_message(&self, timeout_secs: u64) -> Option> { + let mut rx = self.quic_msg_rx.lock().unwrap(); + self.rt.block_on(async { + tokio::time::timeout(Duration::from_secs(timeout_secs), rx.recv()).await.ok().flatten() + }) + } + + /// Send overlay message via ADNL (overlay.message()). + pub fn send_overlay_message( + &self, + overlay_id: &Arc, + dst: &Arc, + data: &[u8], + ) { + self.rt.block_on(async { + let tagged = TaggedByteSlice::with_object(data); + self.overlay.message(dst, &tagged, overlay_id).await.expect("overlay message failed"); + println!("send_overlay_message: OK"); + }); + } + + /// Send overlay query via ADNL (overlay.query()). + /// Returns deserialized TLObject response, or None on timeout. + pub fn send_overlay_query( + &self, + overlay_id: &Arc, + dst: &Arc, + query: &TaggedTlObject, + timeout_ms: Option, + ) -> Option { + self.rt.block_on(async { + self.overlay + .query(dst, query, overlay_id, timeout_ms) + .await + .expect("overlay query failed") + }) + } + + /// Create a private overlay with signing key and peer list. + pub fn add_private_overlay(&self, overlay_id: &Arc, peers: &[Arc]) { + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + let params = OverlayParams { + flags: 0, + hops: None, + overlay_id, + runtime: Some(self.rt.handle().clone()), + }; + self.overlay + .add_private_overlay(params, &local_key, peers) + .expect("add_private_overlay failed"); + } + + /// Stop the node. Shuts down the QUIC endpoint and ADNL node. + pub fn stop(self) { + self.cancellation_token.cancel(); + self.quic.shutdown(); + let adnl = self.adnl.clone(); + self.rt.block_on(async move { + adnl.stop().await; + // Give time for spawned tasks to observe cancellation and endpoint shutdown + tokio::time::sleep(Duration::from_millis(100)).await; + }); + } +} diff --git a/src/node/tests/compat_test/tests/test_boc_compression.rs b/src/node/tests/compat_test/tests/test_boc_compression.rs new file mode 100644 index 0000000..8cf4b78 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_boc_compression.rs @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! BOC Compression cross-implementation tests. +//! +//! Tests that BOC compressed by one implementation (Rust or C++) can be +//! decompressed by the other, verifying wire-format compatibility. +//! +//! The compress/decompress commands are standalone (no networking required), +//! so each test only spawns a single CppTestNode as a compression oracle. + +use compat_test::{skip_if_no_cpp, CppTestNode}; +use ton_block::{ + boc_compression::{boc_compress, boc_decompress, CompressionAlgorithm}, + read_boc, write_boc, write_boc_multi, BuilderData, Cell, IBitstring, +}; + +const PORT_BASE: u16 = 15500; +const MAX_DECOMPRESS_SIZE: u32 = 10 * 1024 * 1024; + +// ---- Cell construction helpers ---- + +fn build_single_cell(data: u64) -> Cell { + let mut builder = BuilderData::new(); + builder.append_u64(data).unwrap(); + builder.into_cell().unwrap() +} + +fn build_simple_tree() -> Cell { + let mut leaf = BuilderData::new(); + leaf.append_u64(0xDEADBEEF_CAFEBABE).unwrap(); + let leaf_cell = leaf.into_cell().unwrap(); + + let mut child1 = BuilderData::new(); + child1.append_u32(1).unwrap(); + child1.checked_append_reference(leaf_cell).unwrap(); + let child1_cell = child1.into_cell().unwrap(); + + let mut child2 = BuilderData::new(); + child2.append_u32(2).unwrap(); + let child2_cell = child2.into_cell().unwrap(); + + let mut root = BuilderData::new(); + root.append_u32(0).unwrap(); + root.checked_append_reference(child1_cell).unwrap(); + root.checked_append_reference(child2_cell).unwrap(); + root.into_cell().unwrap() +} + +fn build_dag_tree() -> Cell { + let mut shared = BuilderData::new(); + shared.append_u32(0x5AAED).unwrap(); + let shared_cell = shared.into_cell().unwrap(); + + let mut c1 = BuilderData::new(); + c1.append_u32(1).unwrap(); + c1.checked_append_reference(shared_cell.clone()).unwrap(); + let c1_cell = c1.into_cell().unwrap(); + + let mut c2 = BuilderData::new(); + c2.append_u32(2).unwrap(); + c2.checked_append_reference(shared_cell).unwrap(); + let c2_cell = c2.into_cell().unwrap(); + + let mut root = BuilderData::new(); + root.append_u32(0).unwrap(); + root.checked_append_reference(c1_cell).unwrap(); + root.checked_append_reference(c2_cell).unwrap(); + root.into_cell().unwrap() +} + +fn cells_to_boc_b64(cells: &[Cell]) -> String { + let boc_bytes = if cells.len() == 1 { + write_boc(&cells[0]).unwrap() + } else { + write_boc_multi(cells.to_vec()).unwrap() + }; + base64::engine::general_purpose::STANDARD.encode(&boc_bytes) +} + +fn b64_to_cells(b64: &str) -> Vec { + let bytes = base64::engine::general_purpose::STANDARD.decode(b64).unwrap(); + let result = read_boc(&bytes).unwrap(); + result.roots +} + +use base64::Engine; + +// ---- Tests ---- + +/// Rust compresses BOC, C++ decompresses it โ€” verify cell hashes match. +#[test] +fn test_boc_compress_rust_decompress_cpp() { + skip_if_no_cpp!(); + + let mut cpp = CppTestNode::spawn(PORT_BASE).expect("spawn C++"); + + let test_cases: Vec<(&str, Vec)> = vec![ + ("single_cell", vec![build_single_cell(0x12345678)]), + ("simple_tree", vec![build_simple_tree()]), + ("dag_tree", vec![build_dag_tree()]), + ]; + + for algo_name in &["baseline", "improved"] { + let rust_algo = match *algo_name { + "baseline" => CompressionAlgorithm::BaselineLZ4, + "improved" => CompressionAlgorithm::ImprovedStructureLZ4, + _ => unreachable!(), + }; + + for (name, cells) in &test_cases { + println!("Testing Rust compress -> C++ decompress: {} ({})", name, algo_name); + + // Rust compresses + let compressed = boc_compress(cells.clone(), rust_algo).unwrap(); + + // Send compressed to C++ for decompression + let compressed_b64 = base64::engine::general_purpose::STANDARD.encode(&compressed); + let decompressed_boc_b64 = + cpp.decompress_boc(&compressed_b64, MAX_DECOMPRESS_SIZE).expect("C++ decompress"); + + // Parse the decompressed BOC back into cells + let decompressed_cells = b64_to_cells(&decompressed_boc_b64); + + // Verify cell hashes match + assert_eq!( + cells.len(), + decompressed_cells.len(), + "{} ({}): root count mismatch", + name, + algo_name + ); + for (i, (original, decompressed)) in + cells.iter().zip(decompressed_cells.iter()).enumerate() + { + assert_eq!( + original.repr_hash(), + decompressed.repr_hash(), + "{} ({}): root {} hash mismatch", + name, + algo_name, + i + ); + } + println!(" OK: {} roots verified", cells.len()); + } + } + + cpp.shutdown().expect("shutdown"); +} + +/// C++ compresses BOC, Rust decompresses it โ€” verify cell hashes match. +#[test] +fn test_boc_compress_cpp_decompress_rust() { + skip_if_no_cpp!(); + + let mut cpp = CppTestNode::spawn(PORT_BASE + 10).expect("spawn C++"); + + let test_cases: Vec<(&str, Vec)> = vec![ + ("single_cell", vec![build_single_cell(0x12345678)]), + ("simple_tree", vec![build_simple_tree()]), + ("dag_tree", vec![build_dag_tree()]), + ]; + + for algo_name in &["baseline", "improved"] { + for (name, cells) in &test_cases { + println!("Testing C++ compress -> Rust decompress: {} ({})", name, algo_name); + + // Send standard BOC to C++ for compression + let boc_b64 = cells_to_boc_b64(cells); + let compressed_b64 = cpp.compress_boc(&boc_b64, algo_name).expect("C++ compress"); + + // Rust decompresses + let compressed_bytes = + base64::engine::general_purpose::STANDARD.decode(&compressed_b64).unwrap(); + let decompressed_cells = + boc_decompress(&compressed_bytes, MAX_DECOMPRESS_SIZE as usize).unwrap(); + + // Verify cell hashes match + assert_eq!( + cells.len(), + decompressed_cells.len(), + "{} ({}): root count mismatch", + name, + algo_name + ); + for (i, (original, decompressed)) in + cells.iter().zip(decompressed_cells.iter()).enumerate() + { + assert_eq!( + original.repr_hash(), + decompressed.repr_hash(), + "{} ({}): root {} hash mismatch", + name, + algo_name, + i + ); + } + println!(" OK: {} roots verified", cells.len()); + } + } + + cpp.shutdown().expect("shutdown"); +} + +/// Full round-trip: Rust compress -> C++ decompress -> C++ compress -> Rust decompress. +/// Verifies that data survives two cross-implementation transitions. +#[test] +fn test_boc_compression_roundtrip() { + skip_if_no_cpp!(); + + let mut cpp = CppTestNode::spawn(PORT_BASE + 20).expect("spawn C++"); + + let test_cases: Vec<(&str, Vec)> = vec![ + ("single_cell", vec![build_single_cell(0xAAAABBBB)]), + ("simple_tree", vec![build_simple_tree()]), + ("dag_tree", vec![build_dag_tree()]), + ]; + + for algo_name in &["baseline", "improved"] { + let rust_algo = match *algo_name { + "baseline" => CompressionAlgorithm::BaselineLZ4, + "improved" => CompressionAlgorithm::ImprovedStructureLZ4, + _ => unreachable!(), + }; + + for (name, cells) in &test_cases { + println!("Testing full round-trip: {} ({})", name, algo_name); + + // Step 1: Rust compresses + let compressed1 = boc_compress(cells.clone(), rust_algo).unwrap(); + let compressed1_b64 = base64::engine::general_purpose::STANDARD.encode(&compressed1); + + // Step 2: C++ decompresses + let decompressed_boc_b64 = + cpp.decompress_boc(&compressed1_b64, MAX_DECOMPRESS_SIZE).expect("C++ decompress"); + + // Step 3: C++ compresses again + let compressed2_b64 = + cpp.compress_boc(&decompressed_boc_b64, algo_name).expect("C++ compress"); + + // Step 4: Rust decompresses + let compressed2_bytes = + base64::engine::general_purpose::STANDARD.decode(&compressed2_b64).unwrap(); + let final_cells = + boc_decompress(&compressed2_bytes, MAX_DECOMPRESS_SIZE as usize).unwrap(); + + // Verify cell hashes match the original + assert_eq!( + cells.len(), + final_cells.len(), + "{} ({}): root count mismatch after round-trip", + name, + algo_name + ); + for (i, (original, final_cell)) in cells.iter().zip(final_cells.iter()).enumerate() { + assert_eq!( + original.repr_hash(), + final_cell.repr_hash(), + "{} ({}): root {} hash mismatch after round-trip", + name, + algo_name, + i + ); + } + println!(" OK: full round-trip verified"); + } + } + + cpp.shutdown().expect("shutdown"); +} + +/// Test with multiple root cells (multi-root BOC). +#[test] +fn test_boc_compression_multi_root() { + skip_if_no_cpp!(); + + let mut cpp = CppTestNode::spawn(PORT_BASE + 30).expect("spawn C++"); + + let roots = vec![build_single_cell(1), build_single_cell(2), build_simple_tree()]; + + for algo_name in &["baseline", "improved"] { + let rust_algo = match *algo_name { + "baseline" => CompressionAlgorithm::BaselineLZ4, + "improved" => CompressionAlgorithm::ImprovedStructureLZ4, + _ => unreachable!(), + }; + + println!("Testing multi-root BOC ({})", algo_name); + + // Rust compress -> C++ decompress + let compressed = boc_compress(roots.clone(), rust_algo).unwrap(); + let compressed_b64 = base64::engine::general_purpose::STANDARD.encode(&compressed); + let decompressed_boc_b64 = cpp + .decompress_boc(&compressed_b64, MAX_DECOMPRESS_SIZE) + .expect("C++ decompress multi-root"); + let decompressed = b64_to_cells(&decompressed_boc_b64); + + assert_eq!(roots.len(), decompressed.len(), "multi-root ({}): count mismatch", algo_name); + for (i, (orig, dec)) in roots.iter().zip(decompressed.iter()).enumerate() { + assert_eq!( + orig.repr_hash(), + dec.repr_hash(), + "multi-root ({}): root {} hash mismatch", + algo_name, + i + ); + } + + // C++ compress -> Rust decompress + let boc_b64 = cells_to_boc_b64(&roots); + let cpp_compressed_b64 = + cpp.compress_boc(&boc_b64, algo_name).expect("C++ compress multi-root"); + let cpp_compressed = + base64::engine::general_purpose::STANDARD.decode(&cpp_compressed_b64).unwrap(); + let cpp_decompressed = + boc_decompress(&cpp_compressed, MAX_DECOMPRESS_SIZE as usize).unwrap(); + + assert_eq!( + roots.len(), + cpp_decompressed.len(), + "multi-root ({}): count mismatch (C++ direction)", + algo_name + ); + for (i, (orig, dec)) in roots.iter().zip(cpp_decompressed.iter()).enumerate() { + assert_eq!( + orig.repr_hash(), + dec.repr_hash(), + "multi-root ({}): root {} hash mismatch (C++ direction)", + algo_name, + i + ); + } + println!(" OK: {} roots verified both directions", roots.len()); + } + + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_broadcast.rs b/src/node/tests/compat_test/tests/test_broadcast.rs new file mode 100644 index 0000000..7e0e649 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_broadcast.rs @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Broadcast compatibility tests between Rust and C++ implementations. +//! +//! Tests that overlay broadcasts sent from one implementation are correctly +//! received by the other. Covers both small (inline) and large (FEC-encoded) +//! broadcasts in both directions. + +use adnl::OverlayShortId; +use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; +use std::{sync::Arc, thread::sleep, time::Duration}; + +/// Port base for this test file (each test offsets by 10) +const PORT_BASE: u16 = 15100; + +/// Set up a Rust + C++ node pair on an overlay. +/// Uses private overlay on C++ side (no DHT required) and public overlay on Rust side. +/// Returns (cpp_node, rust_node, overlay_short_id, cpp_overlay_id_hex) +fn setup_overlay_pair( + port_offset: u16, +) -> (CppTestNode, RustTestNode, Arc, String) { + let cpp_port = PORT_BASE + port_offset; + let rust_port = PORT_BASE + port_offset + 1; + + // 1. Spawn C++ node + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + + // 2. Create Rust node + let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); + + // 3. Compute overlay ID on Rust side (workchain=0, shard=-9223372036854775808 i.e. 0x8000000000000000) + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + + // 4. Create overlay on both sides + // Use public overlay on Rust, private overlay on C++ (C++ public overlay requires DHT) + rust_node.add_public_overlay(&overlay_short_id); + + let rust_adnl_id = rust_node.adnl_id_hex(); + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_adnl_id]) + .expect("create C++ overlay"); + + // Verify overlay IDs match + let rust_overlay_hex = + overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); + assert_eq!( + cpp_overlay_id.to_lowercase(), + rust_overlay_hex.to_lowercase(), + "Overlay IDs should match between C++ and Rust" + ); + + // 5. Exchange ADNL peers AND add to overlay neighbours + // Rust -> C++: add C++ as ADNL peer and to overlay's known_peers/neighbours + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + + // C++ -> Rust: add Rust as ADNL peer + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add_peer"); + + (cpp, rust_node, overlay_short_id, cpp_overlay_id) +} + +/// Test small broadcast from Rust to C++ +#[test] +fn test_broadcast_rust_to_cpp() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(10); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send a small broadcast from Rust + let test_data = b"Hello from Rust broadcast!"; + rust_node.send_broadcast(&overlay_id, test_data); + + // Wait for C++ to receive it + sleep(Duration::from_secs(2)); + + let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); + + println!("C++ received {} broadcasts after Rust->C++ send", received.len()); + + // Broadcast MUST be delivered for the test to pass + assert!( + !received.is_empty(), + "Rust->C++ broadcast was not delivered. Expected at least 1 broadcast." + ); + assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); + println!("Rust->C++ broadcast delivered successfully!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test small broadcast from C++ to Rust +#[test] +fn test_broadcast_cpp_to_rust() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(20); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send a small broadcast from C++ + let test_data = b"Hello from C++ broadcast!"; + cpp.send_broadcast(&cpp_overlay_id, test_data, false).expect("C++ send broadcast"); + + // Wait for Rust to receive it + let received = rust_node.wait_for_broadcast(&overlay_id, 3); + + // Broadcast MUST be delivered for the test to pass + assert!(received.is_some(), "C++->Rust broadcast was not delivered within timeout"); + let data = received.unwrap(); + assert_eq!(data, test_data, "Broadcast data mismatch"); + println!("C++->Rust broadcast delivered successfully!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test FEC broadcast from Rust to C++ (large data, triggers FEC encoding at >768 bytes) +#[test] +fn test_fec_broadcast_rust_to_cpp() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(30); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send a large broadcast (triggers FEC path, > 768 bytes) + let test_data: Vec = (0..2000).map(|i| (i % 256) as u8).collect(); + rust_node.send_broadcast(&overlay_id, &test_data); + + // Wait for C++ to receive it + sleep(Duration::from_secs(3)); + + let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); + + println!("C++ received {} FEC broadcasts after Rust->C++ send", received.len()); + + // FEC broadcast MUST be delivered for the test to pass + assert!( + !received.is_empty(), + "Rust->C++ FEC broadcast was not delivered. Expected at least 1 broadcast." + ); + assert_eq!(received[0].size, test_data.len(), "FEC broadcast size mismatch"); + println!("Rust->C++ FEC broadcast delivered successfully!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test FEC broadcast from C++ to Rust (large data, triggers FEC encoding at >768 bytes) +#[test] +fn test_fec_broadcast_cpp_to_rust() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(40); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send a large broadcast from C++ (triggers FEC path, > 768 bytes) + let test_data: Vec = (0..2000).map(|i| (i % 256) as u8).collect(); + cpp.send_broadcast(&cpp_overlay_id, &test_data, true).expect("C++ send FEC broadcast"); + + // Wait for Rust to receive it + let received = rust_node.wait_for_broadcast(&overlay_id, 5); + + // FEC broadcast MUST be delivered for the test to pass + assert!(received.is_some(), "C++->Rust FEC broadcast was not delivered within timeout"); + let data = received.unwrap(); + assert_eq!(data, test_data, "FEC broadcast data mismatch"); + println!("C++->Rust FEC broadcast delivered successfully!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_broadcast_validation.rs b/src/node/tests/compat_test/tests/test_broadcast_validation.rs new file mode 100644 index 0000000..0288e8d --- /dev/null +++ b/src/node/tests/compat_test/tests/test_broadcast_validation.rs @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Broadcast Validation (2-Phase) Compatibility Tests +//! +//! Tests that the 2-phase broadcast validation (check_broadcast) works +//! consistently between Rust and C++ implementations. +//! +//! In 2-phase broadcast: +//! 1. Receiving node gets check_broadcast callback with data +//! 2. Callback accepts or rejects the broadcast +//! 3. If accepted: broadcast delivered to application + redistributed +//! 4. If rejected: broadcast dropped, NOT redistributed + +use adnl::OverlayShortId; +use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; +use std::{sync::Arc, thread::sleep, time::Duration}; + +/// Port base for broadcast validation tests +const PORT_BASE: u16 = 15300; + +/// Helper: set up a Rust+C++ pair with a private overlay and accept mode on C++ +fn setup_validation_pair( + port_offset: u16, + validator_mode: &str, +) -> (CppTestNode, RustTestNode, Arc, String) { + let cpp_port = PORT_BASE + port_offset; + let rust_port = PORT_BASE + port_offset + 1; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); + + // Use a unique overlay name for each test to avoid conflicts + let overlay_name = format!("validation_test_{}", port_offset); + let overlay_name_bytes = overlay_name.as_bytes(); + + // Compute overlay short ID from name + let overlay_short_id = rust_node.compute_overlay_short_id_from_name(overlay_name_bytes); + + // Create private overlay on both sides (private overlays don't require DHT) + let cpp_adnl_id = cpp.adnl_id(); + let rust_adnl_id = rust_node.adnl_id_hex(); + + rust_node.add_private_overlay(&overlay_short_id, vec![cpp_adnl_id.to_string()]); + let cpp_overlay_id = + cpp.create_private_overlay(overlay_name_bytes, vec![rust_adnl_id]).expect("create overlay"); + + // Set broadcast validator mode on C++ + cpp.set_broadcast_validator(&cpp_overlay_id, validator_mode).expect("set validator mode"); + + // Exchange peers - add to both ADNL and overlay neighbours + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("add peer"); + + (cpp, rust_node, overlay_short_id, cpp_overlay_id) +} + +/// Test: C++ in accept_all mode receives broadcast from Rust +#[test] +fn test_cpp_accept_all_receives_broadcast() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_validation_pair(0, "accept_all"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_secs(1)); + + // Send broadcast from Rust using the overlay we created + let test_data = b"Broadcast with accept_all validation"; + rust_node.send_broadcast(&overlay_id, test_data); + + // Wait for delivery + sleep(Duration::from_secs(3)); + + let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); + + // Broadcast MUST be delivered for the test to pass + assert!(!received.is_empty(), "Rust->C++ broadcast was not delivered in accept_all mode"); + assert!(received[0].accepted, "Broadcast should be accepted"); + assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); + println!("accept_all: broadcast correctly accepted and delivered!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: C++ in reject_all mode does NOT deliver broadcast from Rust +#[test] +fn test_cpp_reject_all_drops_broadcast() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_validation_pair(10, "reject_all"); + + // Allow time for ADNL channel + sleep(Duration::from_millis(500)); + + // Send broadcast from Rust using the overlay we created + let test_data = b"Broadcast with reject_all validation"; + rust_node.send_broadcast(&overlay_id, test_data); + + // Wait to ensure it would have arrived + sleep(Duration::from_secs(2)); + + let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); + + // In reject_all mode, no broadcasts should be delivered to application layer + println!("reject_all: C++ received {} broadcasts (should be 0)", received.len()); + + // If the broadcast was received at the ADNL level but rejected by validator, + // it should NOT appear in received_broadcasts + for bc in &received { + assert!(!bc.accepted, "In reject_all mode, no broadcast should be accepted"); + } + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: toggling validator mode between accept and reject +#[test] +fn test_cpp_toggle_validator_mode() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 20; + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + + let overlay_name = b"toggle_test_overlay"; + // Use private overlay (doesn't require DHT) + let overlay_id = cpp.create_private_overlay(overlay_name, vec![]).expect("create overlay"); + + // Toggle modes + for mode in ["accept_all", "reject_all", "accept_all", "reject_all", "accept_all"] { + cpp.set_broadcast_validator(&overlay_id, mode).expect(&format!("set mode={}", mode)); + println!("Set broadcast_validator mode={}", mode); + } + + cpp.shutdown().expect("shutdown"); +} + +/// Test: C++ node correctly reports acceptance state +#[test] +fn test_broadcast_acceptance_tracking() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 30; + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + + let overlay_name = b"acceptance_tracking_test"; + // Use private overlay (doesn't require DHT) + let overlay_id = cpp.create_private_overlay(overlay_name, vec![]).expect("create overlay"); + + // Initially should have no broadcasts + let received = cpp.get_received_broadcasts(&overlay_id).expect("get broadcasts"); + assert!(received.is_empty(), "Should have no broadcasts initially"); + + // Clear should work on empty list + cpp.clear_received_broadcasts(&overlay_id).expect("clear broadcasts"); + + let received = cpp.get_received_broadcasts(&overlay_id).expect("get broadcasts"); + assert!(received.is_empty(), "Should still be empty after clear"); + + println!("Broadcast acceptance tracking works correctly"); + + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs b/src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs new file mode 100644 index 0000000..bb382f4 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Cross-implementation tests for candidate_id_to_sign bytes. +//! +//! Verifies Rust and C++ build identical TL bytes for: +//! consensus.candidateId slot:int hash:int256 + +use compat_test::{skip_if_no_cpp, CppTestNode}; +use ton_api::{serialize_boxed, ton::consensus, IntoBoxed}; +use ton_block::UInt256; + +fn parse_uint256(hex_hash: &str) -> UInt256 { + let bytes = hex::decode(hex_hash).expect("hex decode failed"); + let arr: [u8; 32] = bytes.try_into().expect("hash must be exactly 32 bytes"); + UInt256::with_array(arr) +} + +fn rust_candidate_id_to_sign(slot: i32, hex_hash: &str) -> Vec { + let hash = parse_uint256(hex_hash); + let candidate_id = consensus::candidateid::CandidateId { slot, hash }; + serialize_boxed(&candidate_id.into_boxed()).expect("serialize candidateId") +} + +fn rust_candidate_parent_wrapped(slot: i32, hex_hash: &str) -> Vec { + let hash = parse_uint256(hex_hash); + let candidate_id = consensus::candidateid::CandidateId { slot, hash }; + let parent = consensus::candidateparent::CandidateParent { + id: consensus::CandidateId::Consensus_CandidateId(candidate_id), + }; + serialize_boxed(&parent.into_boxed()).expect("serialize candidateParent") +} + +#[test] +fn test_candidate_id_to_sign_matches_cpp() { + skip_if_no_cpp!(); + + let mut cpp = CppTestNode::spawn(15900).expect("spawn C++"); + let cases: &[(i32, &str)] = &[ + (0, "0000000000000000000000000000000000000000000000000000000000000000"), + (1, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + (17, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), + (777, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"), + ]; + + for (slot, hash_hex) in cases { + let cpp_bytes = + cpp.compute_candidate_id_to_sign(*slot, hash_hex).expect("cpp compute candidate id"); + let rust_bytes = rust_candidate_id_to_sign(*slot, hash_hex); + assert_eq!( + cpp_bytes, rust_bytes, + "candidateId bytes mismatch for slot={} hash={}", + slot, hash_hex + ); + } + + cpp.shutdown().expect("shutdown"); +} + +#[test] +fn test_candidate_id_to_sign_not_candidate_parent() { + skip_if_no_cpp!(); + + let mut cpp = CppTestNode::spawn(15901).expect("spawn C++"); + let slot = 42; + let hash_hex = "1111111111111111111111111111111111111111111111111111111111111111"; + + let cpp_bytes = + cpp.compute_candidate_id_to_sign(slot, hash_hex).expect("cpp compute candidate id"); + let rust_candidate_id = rust_candidate_id_to_sign(slot, hash_hex); + let rust_parent_wrapped = rust_candidate_parent_wrapped(slot, hash_hex); + + assert_eq!(cpp_bytes, rust_candidate_id); + assert_ne!( + cpp_bytes, rust_parent_wrapped, + "C++ side must sign candidateId directly, not candidateParent wrapper" + ); + + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_fec_relay.rs b/src/node/tests/compat_test/tests/test_fec_relay.rs new file mode 100644 index 0000000..556d175 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_fec_relay.rs @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! FEC broadcast relay tests between Rust and C++ implementations. +//! +//! Tests a 3-node linear topology: Sender -> Relay -> Receiver +//! where Sender and Receiver are NOT directly connected. +//! A large broadcast (>768 bytes) triggers FEC encoding. +//! The relay node must receive, reassemble, and redistribute the broadcast +//! to the receiver. +//! +//! Test matrix: +//! | Test | Sender | Relay | Receiver | +//! |------|--------|-------|----------| +//! | 1 | Rust | C++ | Rust | +//! | 2 | C++ | Rust | C++ | +//! | 3 | Rust | Rust | C++ | +//! | 4 | C++ | C++ | Rust | + +use adnl::OverlayShortId; +use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; +use std::{sync::Arc, thread::sleep, time::Duration}; + +/// Port base for this test file (each test offsets by 10) +const PORT_BASE: u16 = 15600; + +/// FEC broadcast data size (must be > 768 to trigger FEC) +const FEC_DATA_SIZE: usize = 2000; + +/// Generate test data of given size with a tag byte for identification +fn make_test_data(size: usize, tag: u8) -> Vec { + (0..size).map(|i| ((i % 251) as u8).wrapping_add(tag)).collect() +} + +/// Enum to track node role in the topology +enum Node { + Rust(RustTestNode), + Cpp(CppTestNode), +} + +/// Setup result containing the 3 nodes plus overlay info +struct RelayTopology { + sender: Node, + relay: Node, + receiver: Node, + /// Overlay short ID (for Rust nodes) + overlay_short_id: Arc, + /// Overlay ID hex string (for C++ nodes) + overlay_id_hex: String, + /// Overlay name bytes (for creating overlays) + _overlay_name: Vec, +} + +impl RelayTopology { + fn shutdown(self) { + match self.sender { + Node::Rust(r) => r.stop(), + Node::Cpp(mut c) => { + let _ = c.shutdown(); + } + } + match self.relay { + Node::Rust(r) => r.stop(), + Node::Cpp(mut c) => { + let _ = c.shutdown(); + } + } + match self.receiver { + Node::Rust(r) => r.stop(), + Node::Cpp(mut c) => { + let _ = c.shutdown(); + } + } + } +} + +/// Create a 3-node relay topology. +/// +/// `roles` is (sender_is_rust, relay_is_rust, receiver_is_rust). +/// Wiring: sender <-> relay, relay <-> receiver. NOT sender <-> receiver. +fn setup_relay_topology( + port_offset: u16, + sender_is_rust: bool, + relay_is_rust: bool, + receiver_is_rust: bool, +) -> RelayTopology { + let port0 = PORT_BASE + port_offset; // sender + let port1 = PORT_BASE + port_offset + 1; // relay + let port2 = PORT_BASE + port_offset + 2; // receiver + + // First, create a temporary Rust node just to compute overlay IDs consistently + // (we'll reuse it if sender is Rust, otherwise stop it) + let helper = RustTestNode::new("127.0.0.1", port0 + 5, false); + let overlay_short_id = helper.compute_overlay_short_id(0, i64::MIN); + let overlay_name_bytes = helper.compute_overlay_name(0, i64::MIN); + let overlay_id_hex = + overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); + helper.stop(); + + // Create all nodes + let sender_rust = + if sender_is_rust { Some(RustTestNode::new("127.0.0.1", port0, false)) } else { None }; + let mut sender_cpp = if !sender_is_rust { + Some(CppTestNode::spawn(port0).expect("spawn sender C++")) + } else { + None + }; + + let relay_rust = + if relay_is_rust { Some(RustTestNode::new("127.0.0.1", port1, false)) } else { None }; + let mut relay_cpp = if !relay_is_rust { + Some(CppTestNode::spawn(port1).expect("spawn relay C++")) + } else { + None + }; + + let receiver_rust = + if receiver_is_rust { Some(RustTestNode::new("127.0.0.1", port2, false)) } else { None }; + let mut receiver_cpp = if !receiver_is_rust { + Some(CppTestNode::spawn(port2).expect("spawn receiver C++")) + } else { + None + }; + + // Collect all ADNL IDs for C++ private overlay creation + let sender_adnl_id = match (&sender_rust, &sender_cpp) { + (Some(r), _) => r.adnl_id_hex(), + (_, Some(c)) => c.adnl_id().to_string(), + _ => unreachable!(), + }; + let relay_adnl_id = match (&relay_rust, &relay_cpp) { + (Some(r), _) => r.adnl_id_hex(), + (_, Some(c)) => c.adnl_id().to_string(), + _ => unreachable!(), + }; + let receiver_adnl_id = match (&receiver_rust, &receiver_cpp) { + (Some(r), _) => r.adnl_id_hex(), + (_, Some(c)) => c.adnl_id().to_string(), + _ => unreachable!(), + }; + + // Create overlays on all nodes + // Rust nodes use public overlay; C++ nodes use private overlay with their direct neighbors + if let Some(ref r) = sender_rust { + r.add_public_overlay(&overlay_short_id); + } + if let Some(ref mut c) = sender_cpp { + // Sender's only neighbor is relay + c.create_private_overlay(&overlay_name_bytes, vec![relay_adnl_id.clone()]) + .expect("sender C++ create overlay"); + } + + if let Some(ref r) = relay_rust { + r.add_public_overlay(&overlay_short_id); + } + if let Some(ref mut c) = relay_cpp { + // Relay's neighbors are sender and receiver + c.create_private_overlay( + &overlay_name_bytes, + vec![sender_adnl_id.clone(), receiver_adnl_id.clone()], + ) + .expect("relay C++ create overlay"); + } + + if let Some(ref r) = receiver_rust { + r.add_public_overlay(&overlay_short_id); + } + if let Some(ref mut c) = receiver_cpp { + // Receiver's only neighbor is relay + c.create_private_overlay(&overlay_name_bytes, vec![relay_adnl_id.clone()]) + .expect("receiver C++ create overlay"); + } + + // Wire ADNL peers: sender <-> relay, relay <-> receiver + // NOT sender <-> receiver (that's the whole point of the relay test) + + // === sender <-> relay === + wire_pair(&sender_rust, &mut sender_cpp, &relay_rust, &mut relay_cpp, &overlay_short_id); + + // === relay <-> receiver === + wire_pair(&relay_rust, &mut relay_cpp, &receiver_rust, &mut receiver_cpp, &overlay_short_id); + + // Package nodes + let sender = + if let Some(r) = sender_rust { Node::Rust(r) } else { Node::Cpp(sender_cpp.unwrap()) }; + let relay = + if let Some(r) = relay_rust { Node::Rust(r) } else { Node::Cpp(relay_cpp.unwrap()) }; + let receiver = + if let Some(r) = receiver_rust { Node::Rust(r) } else { Node::Cpp(receiver_cpp.unwrap()) }; + + RelayTopology { + sender, + relay, + receiver, + overlay_short_id, + overlay_id_hex, + _overlay_name: overlay_name_bytes, + } +} + +/// Wire two nodes as ADNL peers and overlay neighbors (bidirectional). +/// Handles all 4 combinations of Rust/C++ for each side. +fn wire_pair( + a_rust: &Option, + a_cpp: &mut Option, + b_rust: &Option, + b_cpp: &mut Option, + overlay_id: &Arc, +) { + match (a_rust.as_ref(), a_cpp.as_mut(), b_rust.as_ref(), b_cpp.as_mut()) { + // Both Rust + (Some(a), _, Some(b), _) => { + a.add_rust_peer_to_overlay(b, overlay_id); + b.add_rust_peer_to_overlay(a, overlay_id); + } + // A=Rust, B=C++ + (Some(a), _, _, Some(b)) => { + a.add_cpp_peer_to_overlay(b, overlay_id); + b.add_peer(&a.pubkey_tl_b64(), "127.0.0.1", a.port).expect("C++ add_peer"); + } + // A=C++, B=Rust + (_, Some(a), Some(b), _) => { + b.add_cpp_peer_to_overlay(a, overlay_id); + a.add_peer(&b.pubkey_tl_b64(), "127.0.0.1", b.port).expect("C++ add_peer"); + } + // Both C++ + (_, Some(a), _, Some(b)) => { + // C++ private overlays handle peering automatically via the peer list + // But we still need to add ADNL peers + let b_pubkey = b.pubkey().to_string(); + let b_port = b.udp_port(); + a.add_peer(&b_pubkey, "127.0.0.1", b_port).expect("C++ add_peer a->b"); + let a_pubkey = a.pubkey().to_string(); + let a_port = a.udp_port(); + b.add_peer(&a_pubkey, "127.0.0.1", a_port).expect("C++ add_peer b->a"); + } + _ => unreachable!("Invalid node combination"), + } +} + +// =================== Tests =================== + +/// Test 1: Rust sender -> C++ relay -> Rust receiver +#[test] +fn test_fec_relay_rust_cpp_rust() { + skip_if_no_cpp!(); + + let topo = setup_relay_topology(0, true, false, true); + sleep(Duration::from_secs(2)); + + let test_data = make_test_data(FEC_DATA_SIZE, 0x11); + + // Send from Rust sender, then immediately wait on Rust receiver. + // NOTE: wait_for_broadcast must be called soon after send because + // BroadcastReceiver drops data pushed before pop() is first called. + if let Node::Rust(ref sender) = topo.sender { + sender.send_broadcast(&topo.overlay_short_id, &test_data); + } + + if let Node::Rust(ref receiver) = topo.receiver { + let received = receiver.wait_for_broadcast(&topo.overlay_short_id, 20); + assert!(received.is_some(), "Rust receiver did not get FEC broadcast via C++ relay"); + assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); + println!("test_fec_relay_rust_cpp_rust: PASSED"); + } + + topo.shutdown(); +} + +/// Test 2: C++ sender -> Rust relay -> C++ receiver +#[test] +fn test_fec_relay_cpp_rust_cpp() { + skip_if_no_cpp!(); + + let mut topo = setup_relay_topology(10, false, true, false); + sleep(Duration::from_secs(2)); + + let test_data = make_test_data(FEC_DATA_SIZE, 0x22); + + // Send from C++ sender + if let Node::Cpp(ref mut sender) = topo.sender { + sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); + } + + // Wait for relay and redistribution + sleep(Duration::from_secs(12)); + + // Check C++ receiver got the broadcast + if let Node::Cpp(ref mut receiver) = topo.receiver { + let received = + receiver.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); + assert!(!received.is_empty(), "C++ receiver did not get FEC broadcast via Rust relay"); + assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); + println!("test_fec_relay_cpp_rust_cpp: PASSED"); + } + + topo.shutdown(); +} + +/// Test 3: Rust sender -> Rust relay -> C++ receiver +#[test] +fn test_fec_relay_rust_rust_cpp() { + skip_if_no_cpp!(); + + let mut topo = setup_relay_topology(20, true, true, false); + sleep(Duration::from_secs(2)); + + let test_data = make_test_data(FEC_DATA_SIZE, 0x33); + + // Send from Rust sender + if let Node::Rust(ref sender) = topo.sender { + sender.send_broadcast(&topo.overlay_short_id, &test_data); + } + + // Wait for relay and redistribution + sleep(Duration::from_secs(12)); + + // Check C++ receiver got the broadcast + if let Node::Cpp(ref mut receiver) = topo.receiver { + let received = + receiver.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); + assert!(!received.is_empty(), "C++ receiver did not get FEC broadcast via Rust relay"); + assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); + println!("test_fec_relay_rust_rust_cpp: PASSED"); + } + + topo.shutdown(); +} + +/// Test 4: C++ sender -> C++ relay -> Rust receiver +#[test] +fn test_fec_relay_cpp_cpp_rust() { + skip_if_no_cpp!(); + + let mut topo = setup_relay_topology(30, false, false, true); + sleep(Duration::from_secs(2)); + + let test_data = make_test_data(FEC_DATA_SIZE, 0x44); + + // Send from C++ sender, then immediately wait on Rust receiver. + if let Node::Cpp(ref mut sender) = topo.sender { + sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); + } + + if let Node::Rust(ref receiver) = topo.receiver { + let received = receiver.wait_for_broadcast(&topo.overlay_short_id, 20); + assert!(received.is_some(), "Rust receiver did not get FEC broadcast via C++ relay"); + assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); + println!("test_fec_relay_cpp_cpp_rust: PASSED"); + } + + topo.shutdown(); +} diff --git a/src/node/tests/compat_test/tests/test_overlay_id.rs b/src/node/tests/compat_test/tests/test_overlay_id.rs new file mode 100644 index 0000000..fdb1af8 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_overlay_id.rs @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Overlay ID Calculation Compatibility Tests +//! +//! Tests that verify both Rust and C++ implementations compute identical +//! overlay IDs from the same input. + +use compat_test::{skip_if_no_cpp, CppTestNode}; + +/// Test that overlay ID calculation matches between Rust and C++ +#[test] +fn test_overlay_id_calculation_matches() { + skip_if_no_cpp!(); + + let mut cpp_node = CppTestNode::spawn(14010).expect("Failed to spawn C++ node"); + + // Test various overlay names + let medium_name = "x".repeat(100); + let long_name = "y".repeat(300); + let test_names: Vec<&str> = vec![ + "test_overlay", + "catchain", + "validator_session", + // Note: Empty name is not tested here as C++ rejects empty base64 input + "a", // short name + &medium_name, // medium name + &long_name, // long name (> 254 bytes) + ]; + + for name in test_names { + // Get C++ computed overlay ID + let cpp_id = cpp_node + .compute_overlay_id(name.as_bytes()) + .expect(&format!("C++ failed to compute overlay ID for '{}'", name)); + + // Get Rust computed overlay ID + let rust_id = compat_test::overlay_id::compute_overlay_id(name.as_bytes()); + let rust_id_hex = hex::encode(rust_id); + + // Compare (C++ returns uppercase hex, Rust returns lowercase) + assert_eq!( + cpp_id.to_lowercase(), + rust_id_hex.to_lowercase(), + "Overlay ID mismatch for name '{}': C++={}, Rust={}", + name, + cpp_id, + rust_id_hex + ); + + println!( + "Overlay ID matches for '{}': {}", + if name.len() > 20 { &name[..20] } else { name }, + rust_id_hex + ); + } + + cpp_node.shutdown().expect("Failed to shutdown C++ node"); +} + +/// Test overlay ID with binary data +#[test] +fn test_overlay_id_binary_data() { + skip_if_no_cpp!(); + + let mut cpp_node = CppTestNode::spawn(14011).expect("Failed to spawn C++ node"); + + // Test with various byte patterns + let test_cases: Vec<&[u8]> = vec![ + b"binary\x00data", // embedded null + "unicode_ั‚ะตัั‚".as_bytes(), // unicode + b"\x01\x02\x03\x04", // low bytes + ]; + + for name in test_cases { + let cpp_id = cpp_node.compute_overlay_id(name).expect("C++ failed for binary test"); + + let rust_id = compat_test::overlay_id::compute_overlay_id(name); + let rust_id_hex = hex::encode(rust_id); + + assert_eq!( + cpp_id.to_lowercase(), + rust_id_hex.to_lowercase(), + "Overlay ID mismatch for binary data" + ); + } + + cpp_node.shutdown().expect("Failed to shutdown"); +} + +/// Test that C++ node responds to ping +#[test] +fn test_cpp_node_ping() { + skip_if_no_cpp!(); + + let mut cpp_node = CppTestNode::spawn(14012).expect("Failed to spawn C++ node"); + + cpp_node.ping().expect("Ping failed"); + println!("C++ node ping successful"); + + cpp_node.shutdown().expect("Failed to shutdown"); +} + +/// Test getting ADNL ID from C++ node +#[test] +fn test_cpp_node_adnl_id() { + skip_if_no_cpp!(); + + let mut cpp_node = CppTestNode::spawn(14013).expect("Failed to spawn C++ node"); + + let adnl_id = cpp_node.adnl_id(); + assert!(!adnl_id.is_empty(), "ADNL ID should not be empty"); + assert_eq!(adnl_id.len(), 64, "ADNL ID should be 64 hex chars (32 bytes)"); + + // Verify it's valid hex + hex::decode(adnl_id).expect("ADNL ID should be valid hex"); + + println!("C++ node ADNL ID: {}", adnl_id); + + cpp_node.shutdown().expect("Failed to shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_overlay_message.rs b/src/node/tests/compat_test/tests/test_overlay_message.rs new file mode 100644 index 0000000..a1b4c2f --- /dev/null +++ b/src/node/tests/compat_test/tests/test_overlay_message.rs @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Overlay point-to-point message delivery tests between Rust and C++ implementations. +//! +//! These tests verify that Rust and C++ nodes can exchange overlay messages +//! (not broadcasts) through overlays. This is the same path used by +//! simplex consensus for sending votes and certificates. +//! +//! The key difference from broadcast tests: +//! - Broadcasts use overlay.broadcast() / Overlays::send_broadcast_ex() +//! - Messages use overlay.message() / Overlays::send_message() + +use compat_test::{ + skip_if_no_cpp, + test_helpers::{MessageCollector, RustTestNode}, + CppTestNode, +}; +use std::{thread::sleep, time::Duration}; + +/// Port base for overlay message tests +const PORT_BASE: u16 = 15400; + +/// Test: Send overlay message from C++ to Rust (C++ โ†’ Rust) +/// C++ uses Overlays::send_message(), Rust receives via overlay consumer callback. +#[test] +fn test_overlay_message_cpp_to_rust() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE; + let rust_port = PORT_BASE + 1; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get both ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + println!("Rust ADNL ID: {}", rust_id); + println!("C++ ADNL ID: {}", cpp_id); + + // Create private overlay on C++ + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add true private overlay on Rust side with C++ peer in the member list. + // Must be private overlay โ€” the overlay dispatcher only calls try_consume_custom + // on the consumer for private overlays. + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + rust_node.add_true_private_overlay(&overlay_short_id, &[cpp_key_id]); + let collector = MessageCollector::new(); + rust_node.overlay.add_consumer(&overlay_short_id, collector.clone()).expect("add consumer"); + + // Exchange ADNL peers (just ADNL level, overlay peers are set at creation time) + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send overlay message from C++ to Rust + let test_data = b"Hello from C++ overlay message"; + cpp.send_message(&cpp_overlay_id, &rust_id, test_data).expect("C++ send message"); + + println!("C++ sent overlay message ({} bytes) to Rust", test_data.len()); + + // Wait for Rust to receive via MessageCollector + let received = collector.wait_for_messages(&rust_node.rt, 1, 5); + + assert!(!received.is_empty(), "C++->Rust overlay message was NOT delivered"); + assert_eq!(received[0], test_data, "Message data mismatch"); + println!("C++->Rust overlay message delivered and verified!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Send overlay message from Rust to C++ (Rust โ†’ C++) +/// This is the critical path: Rust overlay.message() โ†’ C++ receive_message callback. +/// This is what simplex consensus uses to send votes and certificates. +#[test] +fn test_overlay_message_rust_to_cpp() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 10; + let rust_port = PORT_BASE + 11; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get both ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + println!("Rust ADNL ID: {}", rust_id); + println!("C++ ADNL ID: {}", cpp_id); + + // Create private overlay on C++ + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add overlay on Rust side + rust_node.add_public_overlay(&overlay_short_id); + + // Exchange ADNL peers + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Get C++ key id for targeting + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send overlay message from Rust to C++ using Fast (UDP) method + let test_data = b"Hello from Rust overlay message (Fast/UDP)"; + println!("Sending overlay message from Rust to C++ ({} bytes, Fast/UDP)", test_data.len()); + rust_node.send_message(&overlay_short_id, &cpp_key_id, test_data); + + // Wait for C++ to receive + sleep(Duration::from_secs(2)); + + // Check C++ received messages + let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); + + println!("C++ received {} overlay messages", received.len()); + for (i, msg) in received.iter().enumerate() { + println!(" msg[{}]: source={}, size={}", i, msg.source, msg.size); + } + + assert!(!received.is_empty(), "Rust->C++ overlay message was NOT delivered via Fast/UDP"); + assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Send multiple overlay messages from Rust to C++ and check delivery rate +/// This simulates the real consensus scenario where many small messages are sent. +#[test] +fn test_overlay_message_burst_rust_to_cpp() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 30; + let rust_port = PORT_BASE + 31; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get both ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + // Create private overlay on C++ + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add overlay on Rust side + rust_node.add_public_overlay(&overlay_short_id); + + // Exchange ADNL peers + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Get C++ key id for targeting + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + + // Allow time for ADNL channel establishment + sleep(Duration::from_secs(1)); + + // Send a burst of messages (simulating consensus votes) + let num_messages = 20; + println!("Sending {} overlay messages from Rust to C++ (Fast/UDP)", num_messages); + + for i in 0..num_messages { + let msg = format!("vote_message_{:04}", i); + rust_node.send_message(&overlay_short_id, &cpp_key_id, msg.as_bytes()); + } + + // Wait for delivery + sleep(Duration::from_secs(3)); + + // Check delivery rate + let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); + + println!( + "Delivery rate: {}/{} ({:.1}%)", + received.len(), + num_messages, + (received.len() as f64 / num_messages as f64) * 100.0 + ); + + for (i, msg) in received.iter().enumerate() { + println!(" msg[{}]: source={}, size={}", i, msg.source, msg.size); + } + + // Require at least 90% delivery โ€” UDP on localhost should be reliable. + // Anything less indicates a real problem, not normal network loss. + let min_required = (num_messages as f64 * 0.9) as usize; + assert!( + received.len() >= min_required, + "Too many messages lost: {}/{} delivered ({:.1}% loss, need >=90%)", + received.len(), + num_messages, + (1.0 - received.len() as f64 / num_messages as f64) * 100.0 + ); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Send overlay message from C++ to C++ (C++ โ†’ C++) as baseline +/// This confirms C++ send_message works when both sides are C++. +#[test] +fn test_overlay_message_cpp_to_cpp_baseline() { + skip_if_no_cpp!(); + + let cpp1_port = PORT_BASE + 40; + let cpp2_port = PORT_BASE + 41; + + let mut cpp1 = CppTestNode::spawn(cpp1_port).expect("spawn C++ node 1"); + let mut cpp2 = CppTestNode::spawn(cpp2_port).expect("spawn C++ node 2"); + + // Use a simple overlay name for testing + let overlay_name = b"test_message_overlay_cpp2cpp"; + + let cpp1_id = cpp1.adnl_id().to_string(); + let cpp2_id = cpp2.adnl_id().to_string(); + + println!("C++ node 1 ADNL ID: {}", cpp1_id); + println!("C++ node 2 ADNL ID: {}", cpp2_id); + + // Create private overlay on both C++ nodes + let overlay_id_1 = cpp1 + .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) + .expect("C++ 1 create private overlay"); + let overlay_id_2 = cpp2 + .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) + .expect("C++ 2 create private overlay"); + + assert_eq!(overlay_id_1, overlay_id_2, "Overlay IDs should match"); + + // Exchange peers + let cpp1_pubkey = cpp1.pubkey().to_string(); + let cpp2_pubkey = cpp2.pubkey().to_string(); + cpp1.add_peer(&cpp2_pubkey, "127.0.0.1", cpp2_port).expect("C++ 1 add peer"); + cpp2.add_peer(&cpp1_pubkey, "127.0.0.1", cpp1_port).expect("C++ 2 add peer"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send overlay message from C++ node 1 to C++ node 2 + let test_data = b"C++ to C++ overlay message"; + cpp1.send_message(&overlay_id_1, &cpp2_id, test_data).expect("C++ 1 send message"); + + // Wait for delivery + sleep(Duration::from_secs(2)); + + // Check if C++ node 2 received + let received = cpp2.get_received_messages(&overlay_id_2).expect("get messages"); + + println!("C++ node 2 received {} overlay messages", received.len()); + + assert!(!received.is_empty(), "C++->C++ overlay message was NOT delivered (baseline test)"); + assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); + + cpp1.shutdown().expect("shutdown node 1"); + cpp2.shutdown().expect("shutdown node 2"); +} diff --git a/src/node/tests/compat_test/tests/test_public_overlay.rs b/src/node/tests/compat_test/tests/test_public_overlay.rs new file mode 100644 index 0000000..f6803d6 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_public_overlay.rs @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Overlay query compatibility tests between Rust and C++ implementations. +//! +//! Tests that overlay queries (request/response) work correctly between +//! Rust and C++ nodes in both directions. + +use adnl::{common::TaggedTlObject, OverlayShortId}; +use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; +use std::{sync::Arc, thread::sleep, time::Duration}; +use ton_api::{serialize_boxed, ton::rpc::adnl::Ping as AdnlPing, AnyBoxedSerialize}; + +/// Port base for this test file (each test offsets by 10) +const PORT_BASE: u16 = 15150; + +/// Set up a Rust + C++ node pair on an overlay. +/// Uses private overlay on C++ side (no DHT required) and public overlay on Rust side. +/// Returns (cpp_node, rust_node, overlay_short_id, cpp_overlay_id_hex) +fn setup_overlay_pair( + port_offset: u16, +) -> (CppTestNode, RustTestNode, Arc, String) { + let cpp_port = PORT_BASE + port_offset; + let rust_port = PORT_BASE + port_offset + 1; + + // 1. Spawn C++ node + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + + // 2. Create Rust node + let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); + + // 3. Compute overlay ID on Rust side (workchain=0, shard=-9223372036854775808 i.e. 0x8000000000000000) + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + + // 4. Create overlay on both sides + rust_node.add_public_overlay(&overlay_short_id); + + let rust_adnl_id = rust_node.adnl_id_hex(); + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_adnl_id]) + .expect("create C++ overlay"); + + // Verify overlay IDs match + let rust_overlay_hex = + overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); + assert_eq!( + cpp_overlay_id.to_lowercase(), + rust_overlay_hex.to_lowercase(), + "Overlay IDs should match between C++ and Rust" + ); + + // 5. Exchange ADNL peers AND add to overlay neighbours + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add_peer"); + + (cpp, rust_node, overlay_short_id, cpp_overlay_id) +} + +/// Deterministic positive query test: C++ sends a valid TL query and Rust echoes it back. +#[test] +fn test_query_cpp_to_rust_echo_roundtrip() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(0); + + // Add echo consumer on Rust side + let echo = compat_test::test_helpers::EchoConsumer::new(); + rust_node.overlay.add_consumer(&overlay_id, echo).expect("add consumer"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // C++ sends a valid TL-serialized query to Rust + let rust_adnl_id = rust_node.adnl_id_hex(); + let query = AdnlPing { value: 0x1122_3344_5566_7788 }; + let query_bytes = serialize_boxed(&query).expect("serialize query"); + + let answer = cpp + .send_query(&cpp_overlay_id, &rust_adnl_id, &query_bytes, 5000) + .expect("C++->Rust query should succeed"); + + assert_eq!(answer, query_bytes, "C++->Rust echo reply mismatch"); + println!("C++->Rust query echo roundtrip succeeded"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Deterministic negative query test: C++ rejects Rust query. +/// Note: C++ ADNL drops errors server-side (logs them but sends no response), +/// so the Rust side sees a timeout (Ok(None)) rather than an explicit error. +#[test] +fn test_query_rust_to_cpp_rejects_with_error() { + skip_if_no_cpp!(); + + let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(10); + + // Configure C++ side to explicitly reject queries. + cpp.set_query_handler(&cpp_overlay_id, "reject").expect("set reject handler"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Rust sends query to C++ + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + + // Build a valid TL query object + let query_data = AdnlPing { value: 0x0102_0304_0506_0708 }; + let tagged: TaggedTlObject = query_data.into_tl_object().into(); + + let result = rust_node.rt.block_on(async { + rust_node.overlay.query(&cpp_key_id, &tagged, &overlay_id, Some(5000)).await + }); + + // C++ ADNL drops rejected queries without sending a response back to the peer, + // so the Rust side either times out (Ok(None)) or gets a transport-level error. + match result { + Ok(None) => { + println!("Rust->C++ query timed out as expected (C++ dropped the rejected query)"); + } + Err(e) => { + println!("Rust->C++ query failed with error (expected): {}", e); + } + Ok(Some(_)) => panic!("Expected timeout or error from C++ reject mode, got a response"), + } + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_quic_overlay.rs b/src/node/tests/compat_test/tests/test_quic_overlay.rs new file mode 100644 index 0000000..b4dadbc --- /dev/null +++ b/src/node/tests/compat_test/tests/test_quic_overlay.rs @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Overlay-level QUIC compatibility tests between Rust and C++ implementations. +//! +//! Tests overlay operations where at least one direction uses QUIC transport. +//! The C++ QuicSender routes messages through ADNL to the overlay layer, +//! so overlay operations should work if the QUIC transport layer is compatible. +//! +//! Note: The overlay itself does not directly use QUIC. Instead: +//! - C++ sends via QuicSender.send_message() โ†’ QUIC โ†’ receiver's QuicSender +//! โ†’ receiver's ADNL.receive_message() โ†’ overlay callback +//! - Rust sends via QuicTransport.send_message() โ†’ QUIC stream โ†’ C++ QuicSender +//! โ†’ ADNL.receive_message() โ†’ overlay callback + +use compat_test::{skip_if_no_cpp, test_helpers::RustQuicTestNode, CppTestNode, TestTimeout}; +use std::{thread::sleep, time::Duration}; +use ton_api::{serialize_boxed, ton::overlay::message::Message as OverlayMessage, IntoBoxed}; +use ton_block::UInt256; + +/// Port base for QUIC overlay tests +const PORT_BASE: u16 = 18100; + +/// Test: C++ sends overlay message via QUIC to another C++ node +/// +/// Baseline test: verifies C++ QUIC works between two C++ nodes. +/// Both nodes enable QUIC and exchange messages. +#[test] +fn test_quic_overlay_cpp_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp1_port = PORT_BASE; + let cpp2_port = PORT_BASE + 1; + + let mut cpp1 = CppTestNode::spawn(cpp1_port).expect("spawn C++ node 1"); + let mut cpp2 = CppTestNode::spawn(cpp2_port).expect("spawn C++ node 2"); + + cpp1.enable_quic().expect("enable QUIC on C++ node 1"); + cpp2.enable_quic().expect("enable QUIC on C++ node 2"); + + let cpp1_id = cpp1.adnl_id().to_string(); + let cpp2_id = cpp2.adnl_id().to_string(); + println!("C++ node 1 ADNL ID: {}", cpp1_id); + println!("C++ node 2 ADNL ID: {}", cpp2_id); + + // Create overlay on both nodes + let overlay_name = b"test_quic_overlay_cpp2cpp"; + let overlay_id_1 = cpp1 + .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) + .expect("C++ 1 create private overlay"); + let overlay_id_2 = cpp2 + .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) + .expect("C++ 2 create private overlay"); + assert_eq!(overlay_id_1, overlay_id_2, "Overlay IDs should match"); + + // Exchange ADNL peers (needed for QuicSender address resolution) + let cpp1_pubkey = cpp1.pubkey().to_string(); + let cpp2_pubkey = cpp2.pubkey().to_string(); + cpp1.add_peer(&cpp2_pubkey, "127.0.0.1", cpp2_port).expect("C++ 1 add peer"); + cpp2.add_peer(&cpp1_pubkey, "127.0.0.1", cpp1_port).expect("C++ 2 add peer"); + + sleep(Duration::from_millis(500)); + + // C++ node 1 sends QUIC message to C++ node 2. + // Data must be prefixed with overlay.message TL for the receiver's ADNL + // to route it to the overlay callback. + let test_data = b"C++ to C++ overlay via QUIC"; + let overlay_bytes = hex::decode(&overlay_id_1).expect("decode overlay hex"); + let mut overlay_msg = serialize_boxed( + &OverlayMessage { overlay: UInt256::with_array(overlay_bytes.try_into().unwrap()) } + .into_boxed(), + ) + .expect("serialize overlay message prefix"); + overlay_msg.extend_from_slice(test_data); + + println!("C++ 1 sending QUIC message to C++ 2 ({} bytes)", overlay_msg.len()); + cpp1.send_quic_message(&cpp2_id, &overlay_msg).expect("C++ 1 send QUIC message"); + + sleep(Duration::from_secs(2)); + + // Check if C++ node 2 received (via ADNL โ†’ overlay callback) + let received = cpp2.get_received_messages(&overlay_id_2).expect("get messages"); + println!("C++ node 2 received {} messages", received.len()); + + assert!( + !received.is_empty(), + "C++โ†’C++ QUIC overlay message not received: QuicSender may not match overlay expectations" + ); + println!("SUCCESS: C++โ†’C++ QUIC overlay message delivered"); + + cpp1.shutdown().expect("shutdown node 1"); + cpp2.shutdown().expect("shutdown node 2"); +} + +/// Test: Rust sends overlay message via both UDP and QUIC, C++ receives both +/// +/// First sends a baseline UDP overlay message to confirm overlay routing works, +/// then sends via QUIC transport and verifies C++ receives it through the +/// ADNL โ†’ overlay callback path. +#[test] +fn test_quic_overlay_message_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 10; + let rust_port = PORT_BASE + 11; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + // Create overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + + // Exchange peers (ADNL + QUIC) + rust_node.add_cpp_peer_full(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_secs(1)); + + // Send overlay message from Rust via regular ADNL (as baseline) + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + let baseline_data = b"Baseline: Rust overlay message via UDP"; + println!("Sending baseline overlay message via UDP ({} bytes)", baseline_data.len()); + rust_node.send_overlay_message(&overlay_short_id, &cpp_key_id, baseline_data); + + sleep(Duration::from_secs(2)); + + let baseline_received = + cpp.get_received_messages(&cpp_overlay_id).expect("get baseline messages"); + println!("Baseline (UDP): C++ received {} overlay messages", baseline_received.len()); + + // Clear for QUIC test + cpp.clear_received_messages(&cpp_overlay_id).expect("clear messages"); + + // Now send via QUIC transport with overlay TL wrapping + let quic_data = b"QUIC: Rust to C++ overlay test"; + println!("Sending QUIC overlay message from Rust to C++ ({} bytes)", quic_data.len()); + rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, quic_data); + + sleep(Duration::from_secs(2)); + + let quic_received = cpp.get_received_messages(&cpp_overlay_id).expect("get QUIC messages"); + println!("QUIC transport: C++ received {} messages via overlay", quic_received.len()); + + assert!(!baseline_received.is_empty(), "Baseline UDP overlay message not received"); + assert!( + !quic_received.is_empty(), + "UDP overlay messages work but QUIC messages don't reach overlay" + ); + println!("SUCCESS: Both UDP and QUIC messages delivered to C++ overlay"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: C++ sends QUIC message to Rust, Rust receives via QUIC transport +/// +/// Verifies raw QUIC message delivery from C++ to Rust. The message arrives +/// at the QuicTestSubscriber (transport level), not through overlay routing. +/// Expected: PASS โ€” Rust server accepts C++ connections without SNI. +#[test] +fn test_quic_message_cpp_to_rust() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 20; + let rust_port = PORT_BASE + 21; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + // Create overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let _cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + + // Exchange peers + rust_node.add_cpp_peer_full(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_secs(1)); + + // C++ sends QUIC message to Rust + let test_data = b"C++ overlay message via QUIC to Rust"; + println!("C++ sending QUIC message to Rust ({} bytes)", test_data.len()); + let send_result = cpp.send_quic_message(&rust_id, test_data); + println!("C++ send_quic_message: {:?}", send_result.is_ok()); + + // Try to receive on Rust QUIC subscriber + let received = rust_node.recv_quic_message(3); + + let data = received.expect("C++โ†’Rust QUIC overlay message should be received"); + println!("SUCCESS: Rust received QUIC message from C++ ({} bytes)", data.len()); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: QUIC overlay query from Rust to C++ with echo handler +/// +/// Sends a QUIC query from Rust to C++ where C++ has an echo handler set up. +/// The query goes through QuicTransport โ†’ C++ QuicSender โ†’ ADNL โ†’ overlay query handler. +#[test] +fn test_quic_overlay_query_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 30; + let rust_port = PORT_BASE + 31; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + // Create overlay with echo handler + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set echo handler"); + + rust_node.add_public_overlay(&overlay_short_id); + + // Exchange peers + rust_node.add_cpp_peer_full(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_secs(1)); + + // First verify baseline: overlay query via UDP works + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + println!("Baseline: testing overlay query via UDP..."); + let baseline_result = + cpp.send_query(&cpp_overlay_id, &rust_node.adnl_id_hex(), b"baseline query", 5000); + println!("Baseline overlay query result: {:?}", baseline_result.is_ok()); + + // Now try QUIC query with overlay wrapping + let query_data = b"QUIC overlay query from Rust"; + println!("Sending QUIC overlay query from Rust to C++ ({} bytes)", query_data.len()); + + let result = rust_node.send_quic_overlay_query(&cpp_key_id, &overlay_short_id, query_data, 10); + + let answer = result.expect("QUIC overlay query should succeed"); + println!("SUCCESS: Got QUIC overlay query answer ({} bytes)", answer.len()); + assert!(!answer.is_empty(), "QUIC overlay query answer should not be empty"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_quic_private_overlay.rs b/src/node/tests/compat_test/tests/test_quic_private_overlay.rs new file mode 100644 index 0000000..8c85a61 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_quic_private_overlay.rs @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! Private overlay tests with ADNL and QUIC transport. +//! +//! These tests create **true private overlays** on the Rust side using +//! `OverlayNode::add_private_overlay()` with a signing key โ€” matching how +//! validator consensus overlays are created in production. +//! +//! QUIC transport is used by calling `send_quic_overlay_message` / +//! `send_quic_overlay_query` directly, which bypass the overlay layer and +//! send via `QuicNode`. The overlay's own transport is always ADNL. +//! +//! Test matrix: +//! - Private overlay + ADNL send: baseline +//! - Private overlay + QUIC send: message and query delivery through QUIC +//! - Private overlay + C++โ†’Rust: inbound message delivery via ADNL + +use compat_test::{ + skip_if_no_cpp, + test_helpers::{MessageCollector, RustQuicTestNode}, + CppTestNode, +}; +use std::{thread::sleep, time::Duration}; + +/// Port base for QUIC private overlay tests (must not conflict with other test suites) +const PORT_BASE: u16 = 18200; + +/// Test: Private overlay message via ADNL (baseline). +/// +/// Creates a true private overlay on Rust side and sends a message via ADNL. +/// This verifies that `add_private_overlay()` + `overlay.message()` work +/// correctly with C++ โ€” the same code path validators use. +#[test] +fn test_private_overlay_adnl_message_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + + let cpp_port = PORT_BASE; + let rust_port = PORT_BASE + 1; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + println!("Rust ADNL ID: {rust_id}"); + println!("C++ ADNL ID: {cpp_id}"); + + // Compute overlay (same method as existing tests) + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // C++ creates private overlay + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Rust creates TRUE private overlay (not public shortcut) with ADNL transport + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + + // Exchange ADNL peers + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Add C++ peer to overlay's known peers + // Peer already registered in overlay via add_private_overlay + + sleep(Duration::from_millis(500)); + + // Send message through overlay.message() โ€” routes via ADNL + let test_data = b"Private overlay message via ADNL (baseline)"; + println!("Sending private overlay message via ADNL ({} bytes)", test_data.len()); + rust_node.send_overlay_message(&overlay_short_id, &cpp_key_id, test_data); + + sleep(Duration::from_secs(2)); + + let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); + println!("C++ received {} overlay messages (ADNL)", received.len()); + for (i, msg) in received.iter().enumerate() { + println!(" msg[{i}]: source={}, size={}", msg.source, msg.size); + } + + assert!(!received.is_empty(), "Private overlay message via ADNL was NOT delivered (Rustโ†’C++)"); + assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Private overlay message via QUIC transport. +/// +/// Sends a message via `send_quic_overlay_message` which uses `QuicNode` +/// directly (bypassing `overlay.message()` which always uses ADNL). +#[test] +fn test_private_overlay_quic_message_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + + let cpp_port = PORT_BASE + 10; + let rust_port = PORT_BASE + 11; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + println!("Rust ADNL ID: {rust_id}"); + println!("C++ ADNL ID: {cpp_id}"); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // C++ creates private overlay (with QUIC enabled) + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Rust creates private overlay with QUIC transport + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + + // Exchange ADNL peers (needed for peer identity resolution) + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Register QUIC peer address + rust_node.add_cpp_quic_peer(&cpp); + + // Add C++ peer to overlay's known peers + // Peer already registered in overlay via add_private_overlay + + sleep(Duration::from_secs(1)); + + // Send message directly via QUIC transport (bypassing overlay.message() which + // always routes via ADNL/UDP โ€” the overlay layer has no QUIC transport config). + let test_data = b"Private overlay message via QUIC transport"; + println!("Sending private overlay message via QUIC ({} bytes)", test_data.len()); + rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, test_data); + + sleep(Duration::from_secs(2)); + + let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); + println!("C++ received {} overlay messages (QUIC)", received.len()); + for (i, msg) in received.iter().enumerate() { + println!(" msg[{i}]: source={}, size={}", msg.source, msg.size); + } + + assert!(!received.is_empty(), "Private overlay message via QUIC was NOT delivered (Rustโ†’C++)"); + assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Multiple messages via QUIC transport in private overlay. +/// +/// Sends a burst of messages (simulating consensus votes) through a QUIC-backed +/// private overlay and checks delivery rate. +#[test] +fn test_private_overlay_quic_message_burst() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + + let cpp_port = PORT_BASE + 20; + let rust_port = PORT_BASE + 21; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + rust_node.add_cpp_quic_peer(&cpp); + // Peer already registered in overlay via add_private_overlay + + sleep(Duration::from_secs(1)); + + // Send burst of messages (simulating consensus votes) + let num_messages = 20; + println!("Sending {num_messages} overlay messages via QUIC"); + + for i in 0..num_messages { + let msg = format!("quic_vote_{i:04}"); + rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, msg.as_bytes()); + } + + sleep(Duration::from_secs(3)); + + let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); + println!( + "Delivery rate: {}/{} ({:.1}%)", + received.len(), + num_messages, + (received.len() as f64 / num_messages as f64) * 100.0 + ); + + assert!(!received.is_empty(), "No QUIC overlay messages delivered in burst of {num_messages}"); + // QUIC should deliver reliably โ€” stream-based, no UDP loss + assert_eq!( + received.len(), + num_messages, + "QUIC should deliver all messages (stream-based, no UDP loss)" + ); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Private overlay query via QUIC transport. +/// +/// Sends an overlay query from Rust to C++ through `overlay.query()` which +/// routes through the QUIC transport. The C++ echo handler returns the query +/// data in the response. +#[test] +fn test_private_overlay_quic_query_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + + let cpp_port = PORT_BASE + 30; + let rust_port = PORT_BASE + 31; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + println!("Rust ADNL ID: {rust_id}"); + println!("C++ ADNL ID: {cpp_id}"); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // C++ creates overlay with echo handler + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set echo handler"); + + // Rust creates private overlay with QUIC transport + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + rust_node.add_cpp_quic_peer(&cpp); + // Peer already registered in overlay via add_private_overlay + + sleep(Duration::from_secs(1)); + + // Send query through overlay.query() โ€” routes via QUIC transport + let query_data = b"QUIC private overlay query"; + println!("Sending overlay query via QUIC ({} bytes)", query_data.len()); + + // Use raw QUIC overlay query (overlay.query() needs a proper TL object, + // so we use the lower-level send_quic_overlay_query for now) + let result = rust_node.send_quic_overlay_query(&cpp_key_id, &overlay_short_id, query_data, 10); + + match result { + Ok(answer) => { + println!("SUCCESS: Got QUIC overlay query answer ({} bytes)", answer.len()); + assert!(!answer.is_empty(), "Answer should not be empty"); + } + Err(e) => { + panic!("QUIC overlay query via private overlay failed: {e}"); + } + } + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: C++ sends overlay message to Rust via private overlay (C++โ†’Rust direction). +/// +/// Verifies that messages from C++ arrive at Rust through the overlay callback +/// when Rust uses a private overlay. The C++ send_message goes through ADNL, +/// Rust receives via its overlay consumer regardless of its outbound transport. +#[test] +fn test_private_overlay_message_cpp_to_rust() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + + let cpp_port = PORT_BASE + 40; + let rust_port = PORT_BASE + 41; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + println!("Rust ADNL ID: {rust_id}"); + println!("C++ ADNL ID: {cpp_id}"); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // C++ creates private overlay + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Rust creates private overlay with ADNL (inbound transport doesn't matter โ€” + // incoming messages arrive via ADNL subscriber regardless) + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + + // Register message collector to verify receipt on Rust side + let collector = MessageCollector::new(); + rust_node.overlay.add_consumer(&overlay_short_id, collector.clone()).expect("add consumer"); + + // Exchange peers + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // C++ sends overlay message to Rust + let test_data = b"C++ to Rust private overlay message"; + cpp.send_message(&cpp_overlay_id, &rust_id, test_data).expect("C++ send overlay message"); + println!("C++ sent overlay message to Rust ({} bytes)", test_data.len()); + + // Wait for Rust to receive via MessageCollector + let received = collector.wait_for_messages(&rust_node.rt, 1, 5); + + assert!(!received.is_empty(), "C++โ†’Rust private overlay message was NOT delivered"); + assert_eq!(received[0], test_data, "Message data mismatch"); + println!("C++โ†’Rust private overlay message delivered and verified!"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_quic_transport.rs b/src/node/tests/compat_test/tests/test_quic_transport.rs new file mode 100644 index 0000000..a5224ea --- /dev/null +++ b/src/node/tests/compat_test/tests/test_quic_transport.rs @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! QUIC transport compatibility tests between Rust and C++ implementations. +//! +//! Tests raw QUIC query exchange (C++โ†’Rust), large overlay message delivery +//! via QUIC, and QUIC connection establishment (TLS handshake with RPK certs). +//! +//! For overlay-routed QUIC messages/queries (Rustโ†’C++ and C++โ†’Rust), see +//! `test_quic_overlay.rs`. For private overlay QUIC tests, see +//! `test_quic_private_overlay.rs`. + +use compat_test::{skip_if_no_cpp, test_helpers::RustQuicTestNode, CppTestNode, TestTimeout}; +use std::{ + panic::{catch_unwind, AssertUnwindSafe}, + thread::sleep, + time::Duration, +}; +use ton_api::{serialize_boxed, ton::ton_node::data::Data as TonNodeData, IntoBoxed}; + +/// Port base for QUIC transport tests +const PORT_BASE: u16 = 18000; + +/// Test: C++ sends QUIC query to Rust, expects echo answer +/// +/// Expected: PASS โ€” the Rust QUIC server processes the query and echoes it back. +/// Note: Query data must be a valid TL-serialized object because the Rust +/// query processing pipeline deserializes inner data via deserialize_boxed_bundle(). +#[test] +fn test_quic_query_cpp_to_rust() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 30; + let rust_port = PORT_BASE + 31; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + + // Exchange peers + rust_node.add_cpp_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // C++ sends QUIC query to Rust. + // The inner data must be a valid TL-serialized object because the Rust + // Query::process() calls deserialize_boxed_bundle() on the raw bytes. + let payload = b"QUIC query from C++ to Rust"; + let tl_query = TonNodeData { data: payload.to_vec().into() }; + let query_data = serialize_boxed(&tl_query.into_boxed()).expect("serialize query TL"); + println!("C++ sending QUIC query to Rust ({} bytes TL-wrapped)", query_data.len()); + + let result = cpp.send_quic_query(&rust_id, &query_data, 5000); + + match result { + Ok(answer) => { + println!("SUCCESS: C++ got QUIC query answer ({} bytes)", answer.len()); + // The echo subscriber returns the same TL object; verify it matches + assert_eq!(answer, query_data, "Query echo data mismatch"); + } + Err(e) => { + panic!("C++โ†’Rust QUIC query failed: {}", e); + } + } + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Rust sends a QUIC message close to the C++ stream size limit (900 bytes payload). +/// +/// The C++ QuicSender enforces a 1024-byte per-stream limit for messages. +/// With overlay prefix (~36 bytes) and quic.message TL wrapper (~8 bytes), +/// 900 bytes of payload stays just under the limit. +/// The message is overlay-routed so C++ can verify receipt. +#[test] +fn test_quic_large_overlay_message_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 40; + let rust_port = PORT_BASE + 41; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + // Create overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + + // Exchange peers + rust_node.add_cpp_peer(&cpp); + rust_node.add_cpp_quic_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // Send 900-byte message with overlay wrapping (under C++ 1024-byte stream limit) + let large_data: Vec = (0..900u32).map(|i| (i % 256) as u8).collect(); + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + println!("Sending {} byte QUIC overlay message from Rust to C++", large_data.len()); + rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, &large_data); + + sleep(Duration::from_secs(3)); + + let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); + println!("C++ received {} messages", received.len()); + + assert!(!received.is_empty(), "QUIC overlay message not received"); + println!("SUCCESS: QUIC message delivered ({} bytes)", received[0].size); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: QUIC connection establishment between Rust and C++ +/// +/// Minimal test that just verifies whether a QUIC connection can be established +/// from Rust to C++ (TLS handshake with RPK certificates). +#[test] +fn test_quic_connection_establishment() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 50; + let rust_port = PORT_BASE + 51; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let quic_port = cpp.enable_quic().expect("enable QUIC on C++"); + println!("C++ QUIC port: {}", quic_port); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + println!("Rust QUIC port: {}", rust_port + 1000); + + // Exchange ADNL peers + rust_node.add_cpp_peer(&cpp); + rust_node.add_cpp_quic_peer(&cpp); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // Try to send a small message โ€” the connection attempt itself is the test + let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); + let result = catch_unwind(AssertUnwindSafe(|| { + rust_node.send_quic_message(&cpp_key_id, b"ping"); + })); + + result.expect("QUIC connection should succeed (Rust โ†’ C++)"); + println!("SUCCESS: QUIC connection established (Rust โ†’ C++)"); + println!("TLS handshake with RPK certificates succeeded"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_rldp_query.rs b/src/node/tests/compat_test/tests/test_rldp_query.rs new file mode 100644 index 0000000..0cc3447 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_rldp_query.rs @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! RLDP query/response cross-implementation compatibility tests. +//! +//! Tests verify that Rust and C++ nodes can exchange RLDP queries and receive +//! correct echo answers, for both RLDP v1 and v2 protocols. +//! +//! Topology: 2 nodes (sender + responder), echo handler on responder side. +//! The sender sends a query, the responder echoes it back, and the sender +//! verifies the answer matches the original data. +//! +//! Important: All query data is wrapped in a `tonNode.data` TL envelope because +//! the Rust overlay requires valid TL-serialized objects in the query bundle +//! (overlay.query prefix + TL-serialized inner object). The C++ overlay treats +//! query data as opaque bytes, but Rust's `deserialize_boxed_bundle` must +//! successfully parse both TL objects. + +use compat_test::{ + skip_if_no_cpp, + test_helpers::{EchoConsumer, RustTestNode}, + CppTestNode, +}; +use std::{thread::sleep, time::Duration}; +use ton_api::{ + deserialize_boxed, serialize_boxed, + ton::ton_node::{data::Data as TonNodeData, Data as TonNodeDataBoxed}, + IntoBoxed, +}; + +/// Port base for RLDP query tests (each test offsets by 10) +const PORT_BASE: u16 = 15800; + +/// Wrap test data in tonNode.data TL envelope and serialize to bytes. +/// This is needed because overlay RLDP queries must carry valid TL objects. +fn wrap_in_tl(data: &[u8]) -> Vec { + let tl_data = TonNodeData { data: data.to_vec().into() }; + serialize_boxed(&tl_data.into_boxed()).expect("serialize tonNode.data") +} + +/// Extract inner data from a TL-serialized tonNode.data response. +fn unwrap_from_tl(tl_bytes: &[u8]) -> Vec { + let obj = deserialize_boxed(tl_bytes).expect("deserialize TL answer"); + match obj.downcast::() { + Ok(data) => data.only().data.to_vec(), + Err(obj) => panic!("Unexpected TL type in answer: {:?}", obj), + } +} + +// --------------------------------------------------------------------------- +// RLDP v1 tests +// --------------------------------------------------------------------------- + +/// Test: RLDP v1 query from Rust to C++ (Rust โ†’ C++) +/// Rust sends an RLDP query, C++ echoes it back. +#[test] +fn test_rldp_v1_rust_to_cpp() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE; + let rust_port = PORT_BASE + 1; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + println!("Rust ADNL ID: {}", rust_id); + println!("C++ ADNL ID: {}", cpp_id); + + // Create private overlay on C++ (echo handler is default) + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add overlay on Rust side + rust_node.add_public_overlay(&overlay_short_id); + + // Set echo query handler on C++ + cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); + + // Exchange ADNL peers + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Get C++ key id for targeting + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send RLDP v1 query from Rust to C++ + // send_rldp_query wraps in tonNode.data + overlay.query prefix internally + let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); + println!("Sending RLDP v1 query from Rust to C++ ({} bytes)", test_data.len()); + + let answer = rust_node.send_rldp_query( + &overlay_short_id, + &cpp_key_id, + &test_data, + 1 << 20, // 1MB max answer + false, // v1 + ); + + assert!(answer.is_some(), "RLDP v1 Rustโ†’C++ query got no answer"); + let answer_bytes = answer.unwrap(); + println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); + + // C++ echo handler returns the raw bytes it received (= TL-serialized tonNode.data) + // Unwrap the TL envelope to get the original data + let answer_data = unwrap_from_tl(&answer_bytes); + assert_eq!(answer_data, test_data, "RLDP v1 Rustโ†’C++ echo mismatch"); + + println!("PASS: RLDP v1 Rustโ†’C++ query/response works"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: RLDP v1 query from C++ to Rust (C++ โ†’ Rust) +/// C++ sends an RLDP query, Rust echoes it back via EchoConsumer. +#[test] +fn test_rldp_v1_cpp_to_rust() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 10; + let rust_port = PORT_BASE + 11; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + println!("Rust ADNL ID: {}", rust_id); + println!("C++ ADNL ID: {}", cpp_id); + + // Create private overlay on C++ + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add overlay on Rust side and register echo consumer + rust_node.add_public_overlay(&overlay_short_id); + rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); + + // Exchange ADNL peers + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send RLDP v1 query from C++ to Rust + // Pre-wrap data in TL so Rust's deserialize_boxed_bundle can parse it + let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); + let tl_data = wrap_in_tl(&test_data); + println!( + "Sending RLDP v1 query from C++ to Rust ({} bytes, {} TL-wrapped)", + test_data.len(), + tl_data.len() + ); + + let answer = cpp + .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, false) + .expect("C++ send_rldp_query failed"); + + println!("Got answer: {} bytes", answer.len()); + // Rust EchoConsumer echoes back the TLObject, which gets TL-serialized + let answer_data = unwrap_from_tl(&answer); + assert_eq!(answer_data, test_data, "RLDP v1 C++โ†’Rust echo mismatch"); + + println!("PASS: RLDP v1 C++โ†’Rust query/response works"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +// --------------------------------------------------------------------------- +// RLDP v2 tests +// --------------------------------------------------------------------------- + +/// Test: RLDP v2 query from Rust to C++ (Rust โ†’ C++) +/// Same as v1 test but using RLDP v2 (BBR congestion control, selective ACKs). +#[test] +fn test_rldp_v2_rust_to_cpp() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 20; + let rust_port = PORT_BASE + 21; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + println!("Rust ADNL ID: {}", rust_id); + println!("C++ ADNL ID: {}", cpp_id); + + // Create private overlay on C++ + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add overlay on Rust side + rust_node.add_public_overlay(&overlay_short_id); + + // Set echo query handler on C++ + cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); + + // Exchange ADNL peers + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Get C++ key id for targeting + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send RLDP v2 query from Rust to C++ + let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); + println!("Sending RLDP v2 query from Rust to C++ ({} bytes)", test_data.len()); + + let answer = rust_node.send_rldp_query( + &overlay_short_id, + &cpp_key_id, + &test_data, + 1 << 20, // 1MB max answer + true, // v2 + ); + + assert!(answer.is_some(), "RLDP v2 Rustโ†’C++ query got no answer"); + let answer_bytes = answer.unwrap(); + println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); + let answer_data = unwrap_from_tl(&answer_bytes); + assert_eq!(answer_data, test_data, "RLDP v2 Rustโ†’C++ echo mismatch"); + + println!("PASS: RLDP v2 Rustโ†’C++ query/response works"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: RLDP v2 query from C++ to Rust (C++ โ†’ Rust) +/// C++ sends an RLDP v2 query, Rust echoes it back via EchoConsumer. +#[test] +fn test_rldp_v2_cpp_to_rust() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 30; + let rust_port = PORT_BASE + 31; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + // Compute overlay + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + // Get ADNL IDs + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + println!("Rust ADNL ID: {}", rust_id); + println!("C++ ADNL ID: {}", cpp_id); + + // Create private overlay on C++ + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + // Add overlay on Rust side and register echo consumer + rust_node.add_public_overlay(&overlay_short_id); + rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); + + // Exchange ADNL peers + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + // Allow time for ADNL channel establishment + sleep(Duration::from_millis(500)); + + // Send RLDP v2 query from C++ to Rust + let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); + let tl_data = wrap_in_tl(&test_data); + println!( + "Sending RLDP v2 query from C++ to Rust ({} bytes, {} TL-wrapped)", + test_data.len(), + tl_data.len() + ); + + let answer = cpp + .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, true) + .expect("C++ send_rldp_query v2 failed"); + + println!("Got answer: {} bytes", answer.len()); + let answer_data = unwrap_from_tl(&answer); + assert_eq!(answer_data, test_data, "RLDP v2 C++โ†’Rust echo mismatch"); + + println!("PASS: RLDP v2 C++โ†’Rust query/response works"); + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +// --------------------------------------------------------------------------- +// Larger payload tests (multi-symbol FEC, RLDP v2 only) +// --------------------------------------------------------------------------- +// 256-byte tests above fit in a single 768-byte FEC symbol. +// These tests exercise multi-symbol RaptorQ encoding/decoding (4KB โ‰ˆ 6 symbols). +// +// IMPORTANT: These tests must use RLDP v2 because of MTU limits: +// - C++ RLDP v1 default_mtu_ = 1024 bytes (adnl::Adnl::get_mtu()) โ€” drops incoming +// transfers with total_size > 1024 unless pre-registered via max_size_ or set_default_mtu +// - C++ RLDP v2 DEFAULT_MTU = 7680 bytes (RldpConnection::DEFAULT_MTU) โ€” allows larger +// unsolicited transfers, sufficient for our test payloads +// - Rust RLDP has no incoming MTU check (accepts any size) +// - In production, C++ uses PeersMtuLimitGuard to raise limits to max_block_size + 1024 + +/// Test: 4KB RLDP v2 query from Rust to C++ (multi-symbol FEC) +#[test] +fn test_rldp_v2_4kb_rust_to_cpp() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 40; + let rust_port = PORT_BASE + 41; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); + + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + sleep(Duration::from_millis(500)); + + // 4096 bytes โ‰ˆ 6 FEC symbols (768 bytes each) + let test_data: Vec = (0..4096).map(|i| (i % 256) as u8).collect(); + println!("Sending RLDP v2 4KB query from Rust to C++ ({} bytes)", test_data.len()); + + let answer = rust_node.send_rldp_query( + &overlay_short_id, + &cpp_key_id, + &test_data, + 1 << 20, + true, // v2 โ€” required for 4KB (RLDP v1 default_mtu=1024 would reject) + ); + + assert!(answer.is_some(), "RLDP v2 4KB Rustโ†’C++ query got no answer"); + let answer_bytes = answer.unwrap(); + println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); + let answer_data = unwrap_from_tl(&answer_bytes); + assert_eq!(answer_data, test_data, "RLDP v2 4KB Rustโ†’C++ echo mismatch"); + + println!("PASS: RLDP v2 4KB Rustโ†’C++ query/response works"); + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: 4KB RLDP v2 query from C++ to Rust (multi-symbol FEC) +#[test] +fn test_rldp_v2_4kb_cpp_to_rust() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 50; + let rust_port = PORT_BASE + 51; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); + + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // 4096 bytes + TL wrapper โ‰ˆ 4104 bytes (under C++ 8192-byte overlay limit) + let test_data: Vec = (0..4096).map(|i| (i % 256) as u8).collect(); + let tl_data = wrap_in_tl(&test_data); + println!( + "Sending RLDP v2 4KB query from C++ to Rust ({} bytes, {} TL-wrapped)", + test_data.len(), + tl_data.len() + ); + + let answer = cpp + .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, true) + .expect("C++ send_rldp_query v2 4KB failed"); + + println!("Got answer: {} bytes", answer.len()); + let answer_data = unwrap_from_tl(&answer); + assert_eq!(answer_data, test_data, "RLDP v2 4KB C++โ†’Rust echo mismatch"); + + println!("PASS: RLDP v2 4KB C++โ†’Rust query/response works"); + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Near-limit (7KB) RLDP v2 query from Rust to C++ (multi-symbol FEC) +/// 7168 bytes โ†’ rldp.query total โ‰ˆ 7300 bytes (under RLDP v2 DEFAULT_MTU=7680) +#[test] +fn test_rldp_v2_7kb_rust_to_cpp() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 60; + let rust_port = PORT_BASE + 61; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); + + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + let cpp_key_id = RustTestNode::cpp_key_id(&cpp); + sleep(Duration::from_millis(500)); + + // 7168 bytes โ‰ˆ 10 FEC symbols, near RLDP v2 DEFAULT_MTU limit + let test_data: Vec = (0..7168).map(|i| (i % 256) as u8).collect(); + println!("Sending RLDP v2 7KB query from Rust to C++ ({} bytes)", test_data.len()); + + let answer = rust_node.send_rldp_query( + &overlay_short_id, + &cpp_key_id, + &test_data, + 1 << 20, + true, // v2 + ); + + assert!(answer.is_some(), "RLDP v2 7KB Rustโ†’C++ query got no answer"); + let answer_bytes = answer.unwrap(); + println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); + let answer_data = unwrap_from_tl(&answer_bytes); + assert_eq!(answer_data, test_data, "RLDP v2 7KB Rustโ†’C++ echo mismatch"); + + println!("PASS: RLDP v2 7KB Rustโ†’C++ query/response works"); + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: Near-limit (7KB) RLDP v2 query from C++ to Rust (multi-symbol FEC) +#[test] +fn test_rldp_v2_7kb_cpp_to_rust() { + skip_if_no_cpp!(); + + let cpp_port = PORT_BASE + 70; + let rust_port = PORT_BASE + 71; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); + + let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); + let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); + + let rust_id = rust_node.adnl_id_hex(); + let cpp_id = cpp.adnl_id().to_string(); + + let cpp_overlay_id = cpp + .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) + .expect("C++ create private overlay"); + + rust_node.add_public_overlay(&overlay_short_id); + rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); + + rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // 7168 bytes + TL wrapper โ‰ˆ 7176 bytes (under C++ 8192-byte overlay limit) + let test_data: Vec = (0..7168).map(|i| (i % 256) as u8).collect(); + let tl_data = wrap_in_tl(&test_data); + println!( + "Sending RLDP v2 7KB query from C++ to Rust ({} bytes, {} TL-wrapped)", + test_data.len(), + tl_data.len() + ); + + let answer = cpp + .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, true) + .expect("C++ send_rldp_query v2 7KB failed"); + + println!("Got answer: {} bytes", answer.len()); + let answer_data = unwrap_from_tl(&answer); + assert_eq!(answer_data, test_data, "RLDP v2 7KB C++โ†’Rust echo mismatch"); + + println!("PASS: RLDP v2 7KB C++โ†’Rust query/response works"); + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +// Note on payload size limits: +// +// RLDP v1 default_mtu = 1024 bytes (total transfer size for unsolicited incoming): +// - Only ~928 bytes of user data fits (after rldp.query + overlay.query + tonNode.data overhead) +// - Production nodes call set_default_mtu() to raise this +// +// RLDP v2 DEFAULT_MTU = 7680 bytes: +// - Up to ~7.5KB user data fits without configuration +// - Production nodes use PeersMtuLimitGuard to raise to max_block_size + 1024 +// +// C++ overlay CHECK: query.size() <= huge_packet_max_size() (8192 bytes): +// - Hard limit on data passed to Overlays::send_query_via before RLDP wrapping +// - Applies to C++ sender side only +// +// Rust RaptorQ NEON alignment bug (aarch64 only): +// - Pre-existing bug triggered by certain larger payload sizes +// - Not related to cross-implementation compatibility diff --git a/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs b/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs new file mode 100644 index 0000000..373a297 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! TwostepFec broadcast relay tests between Rust and C++ implementations. +//! +//! Tests a 6-node topology: Sender -> 4 Bridges -> Leaf +//! - Sender connected to all 4 bridges (TwostepFec requires >=4 neighbors) +//! - Each bridge connected to leaf +//! - Leaf NOT directly connected to sender +//! - Data >= 513 bytes triggers TwostepFec (if RLDP + enough neighbors) +//! - All nodes need RLDP enabled +//! +//! TwostepFec sends unique FEC parts to each neighbor via RLDP. +//! Each receiver redistributes received parts to its neighbors. +//! The leaf can only receive through redistribution from bridges. + +use adnl::OverlayShortId; +use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; +use std::{sync::Arc, thread::sleep, time::Duration}; + +/// Port base for this test file +const PORT_BASE: u16 = 15700; + +/// Number of bridge nodes (must be >= 4 for TwostepFec) +const NUM_BRIDGES: usize = 4; + +/// Data size for TwostepFec (>= 513 bytes) +const TWOSTEP_DATA_SIZE: usize = 2048; + +fn make_test_data(size: usize, tag: u8) -> Vec { + (0..size).map(|i| ((i % 251) as u8).wrapping_add(tag)).collect() +} + +enum Node { + Rust(RustTestNode), + Cpp(CppTestNode), +} + +struct TwostepTopology { + sender: Node, + bridges: Vec, + leaf: Node, + overlay_short_id: Arc, + overlay_id_hex: String, +} + +impl TwostepTopology { + fn shutdown(self) { + match self.sender { + Node::Rust(r) => r.stop(), + Node::Cpp(mut c) => { + let _ = c.shutdown(); + } + } + for node in self.bridges { + match node { + Node::Rust(r) => r.stop(), + Node::Cpp(mut c) => { + let _ = c.shutdown(); + } + } + } + match self.leaf { + Node::Rust(r) => r.stop(), + Node::Cpp(mut c) => { + let _ = c.shutdown(); + } + } + } +} + +/// Create a 6-node topology for TwostepFec relay testing. +/// +/// `sender_is_rust`: whether sender is Rust (true) or C++ (false) +/// `bridge_rust_mask`: bitmask of which bridges are Rust (bit 0 = bridge 0, etc.) +/// `leaf_is_rust`: whether leaf is Rust (true) or C++ (false) +fn setup_twostep_topology( + port_offset: u16, + sender_is_rust: bool, + bridge_rust_mask: u8, + leaf_is_rust: bool, +) -> TwostepTopology { + let base = PORT_BASE + port_offset; + // Ports: sender=base, bridges=base+1..base+4, leaf=base+5, helper=base+8 + let sender_port = base; + let bridge_ports: Vec = (0..NUM_BRIDGES).map(|i| base + 1 + i as u16).collect(); + let leaf_port = base + 1 + NUM_BRIDGES as u16; + let helper_port = base + 8; + + // Compute overlay IDs using a helper node + let helper = RustTestNode::new("127.0.0.1", helper_port, false); + let overlay_short_id = helper.compute_overlay_short_id(0, i64::MIN); + let overlay_name_bytes = helper.compute_overlay_name(0, i64::MIN); + let overlay_id_hex = + overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); + helper.stop(); + + // Create all nodes (with RLDP for Rust, twostep for C++) + let sender_rust = + if sender_is_rust { Some(RustTestNode::new("127.0.0.1", sender_port, true)) } else { None }; + let mut sender_cpp = if !sender_is_rust { + Some(CppTestNode::spawn(sender_port).expect("spawn sender C++")) + } else { + None + }; + + let mut bridge_rusts: Vec> = Vec::new(); + let mut bridge_cpps: Vec> = Vec::new(); + for i in 0..NUM_BRIDGES { + let is_rust = (bridge_rust_mask >> i) & 1 == 1; + if is_rust { + bridge_rusts.push(Some(RustTestNode::new("127.0.0.1", bridge_ports[i], true))); + bridge_cpps.push(None); + } else { + bridge_rusts.push(None); + bridge_cpps.push(Some(CppTestNode::spawn(bridge_ports[i]).expect("spawn bridge C++"))); + } + } + + let leaf_rust = + if leaf_is_rust { Some(RustTestNode::new("127.0.0.1", leaf_port, true)) } else { None }; + let mut leaf_cpp = if !leaf_is_rust { + Some(CppTestNode::spawn(leaf_port).expect("spawn leaf C++")) + } else { + None + }; + + // Collect ADNL IDs + let sender_id = match (&sender_rust, &sender_cpp) { + (Some(r), _) => r.adnl_id_hex(), + (_, Some(c)) => c.adnl_id().to_string(), + _ => unreachable!(), + }; + let bridge_ids: Vec = (0..NUM_BRIDGES) + .map(|i| match (&bridge_rusts[i], &bridge_cpps[i]) { + (Some(r), _) => r.adnl_id_hex(), + (_, Some(c)) => c.adnl_id().to_string(), + _ => unreachable!(), + }) + .collect(); + let leaf_id = match (&leaf_rust, &leaf_cpp) { + (Some(r), _) => r.adnl_id_hex(), + (_, Some(c)) => c.adnl_id().to_string(), + _ => unreachable!(), + }; + + // Create overlays + // Sender: neighbors are all 4 bridges + if let Some(ref r) = sender_rust { + r.add_public_overlay(&overlay_short_id); + } + if let Some(ref mut c) = sender_cpp { + c.create_private_overlay_twostep(&overlay_name_bytes, bridge_ids.clone()) + .expect("sender C++ create overlay"); + } + + // Each bridge: neighbors are sender + leaf + other bridges + for i in 0..NUM_BRIDGES { + let mut bridge_peers = vec![sender_id.clone(), leaf_id.clone()]; + for j in 0..NUM_BRIDGES { + if i != j { + bridge_peers.push(bridge_ids[j].clone()); + } + } + if let Some(ref r) = bridge_rusts[i] { + r.add_public_overlay(&overlay_short_id); + } + if let Some(ref mut c) = bridge_cpps[i] { + c.create_private_overlay_twostep(&overlay_name_bytes, bridge_peers) + .expect("bridge C++ create overlay"); + } + } + + // Leaf: neighbors are all 4 bridges (NOT sender) + if let Some(ref r) = leaf_rust { + r.add_public_overlay(&overlay_short_id); + } + if let Some(ref mut c) = leaf_cpp { + c.create_private_overlay_twostep(&overlay_name_bytes, bridge_ids.clone()) + .expect("leaf C++ create overlay"); + } + + // Wire ADNL peers + // Sender <-> each bridge + for i in 0..NUM_BRIDGES { + wire_nodes( + &sender_rust, + sender_cpp.as_mut(), + &bridge_rusts[i], + bridge_cpps[i].as_mut(), + &overlay_short_id, + ); + } + + // Each bridge <-> leaf + for i in 0..NUM_BRIDGES { + wire_nodes( + &bridge_rusts[i], + bridge_cpps[i].as_mut(), + &leaf_rust, + leaf_cpp.as_mut(), + &overlay_short_id, + ); + } + + // Bridges <-> each other (for redistribution of FEC parts) + for i in 0..NUM_BRIDGES { + for j in (i + 1)..NUM_BRIDGES { + wire_nodes_by_idx(&bridge_rusts, &mut bridge_cpps, i, j, &overlay_short_id); + } + } + + // Package nodes + let sender = + if let Some(r) = sender_rust { Node::Rust(r) } else { Node::Cpp(sender_cpp.unwrap()) }; + + let bridges: Vec = (0..NUM_BRIDGES) + .map(|i| { + if let Some(r) = bridge_rusts[i].take() { + Node::Rust(r) + } else { + Node::Cpp(bridge_cpps[i].take().unwrap()) + } + }) + .collect(); + + let leaf = if let Some(r) = leaf_rust { Node::Rust(r) } else { Node::Cpp(leaf_cpp.unwrap()) }; + + TwostepTopology { sender, bridges, leaf, overlay_short_id, overlay_id_hex } +} + +/// Wire two nodes as ADNL peers and overlay neighbors (bidirectional). +fn wire_nodes( + a_rust: &Option, + a_cpp: Option<&mut CppTestNode>, + b_rust: &Option, + b_cpp: Option<&mut CppTestNode>, + overlay_id: &Arc, +) { + match (a_rust.as_ref(), a_cpp, b_rust.as_ref(), b_cpp) { + (Some(a), _, Some(b), _) => { + a.add_rust_peer_to_overlay(b, overlay_id); + b.add_rust_peer_to_overlay(a, overlay_id); + } + (Some(a), _, _, Some(b)) => { + a.add_cpp_peer_to_overlay(b, overlay_id); + b.add_peer(&a.pubkey_tl_b64(), "127.0.0.1", a.port).expect("C++ add_peer"); + } + (_, Some(a), Some(b), _) => { + b.add_cpp_peer_to_overlay(a, overlay_id); + a.add_peer(&b.pubkey_tl_b64(), "127.0.0.1", b.port).expect("C++ add_peer"); + } + (_, Some(a), _, Some(b)) => { + let b_pubkey = b.pubkey().to_string(); + let b_port = b.udp_port(); + a.add_peer(&b_pubkey, "127.0.0.1", b_port).expect("C++ add_peer a->b"); + let a_pubkey = a.pubkey().to_string(); + let a_port = a.udp_port(); + b.add_peer(&a_pubkey, "127.0.0.1", a_port).expect("C++ add_peer b->a"); + } + _ => unreachable!(), + } +} + +/// Wire two bridge nodes by index from the bridge arrays. +fn wire_nodes_by_idx( + rusts: &[Option], + cpps: &mut [Option], + i: usize, + j: usize, + overlay_id: &Arc, +) { + // Can't borrow two elements mutably at once, so handle it carefully + match (rusts[i].as_ref(), rusts[j].as_ref()) { + (Some(a), Some(b)) => { + a.add_rust_peer_to_overlay(b, overlay_id); + b.add_rust_peer_to_overlay(a, overlay_id); + } + (Some(a), None) => { + let b = cpps[j].as_mut().unwrap(); + a.add_cpp_peer_to_overlay(b, overlay_id); + b.add_peer(&a.pubkey_tl_b64(), "127.0.0.1", a.port).expect("C++ add_peer"); + } + (None, Some(b)) => { + let a = cpps[i].as_mut().unwrap(); + b.add_cpp_peer_to_overlay(a, overlay_id); + a.add_peer(&b.pubkey_tl_b64(), "127.0.0.1", b.port).expect("C++ add_peer"); + } + (None, None) => { + // Both C++ - split borrow by using pointers + let (left, right) = cpps.split_at_mut(j); + let a = left[i].as_mut().unwrap(); + let b = right[0].as_mut().unwrap(); + let b_pubkey = b.pubkey().to_string(); + let b_port = b.udp_port(); + a.add_peer(&b_pubkey, "127.0.0.1", b_port).expect("C++ add_peer"); + let a_pubkey = a.pubkey().to_string(); + let a_port = a.udp_port(); + b.add_peer(&a_pubkey, "127.0.0.1", a_port).expect("C++ add_peer"); + } + } +} + +// =================== Tests =================== + +/// Test A: Rust sender, all Rust bridges, C++ leaf +/// Verifies Rust TwostepFec reaches C++ through Rust redistribution +#[test] +fn test_twostep_rust_sender_cpp_leaf() { + skip_if_no_cpp!(); + + let mut topo = setup_twostep_topology(0, true, 0b1111, false); + sleep(Duration::from_millis(500)); + + let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xA1); + + // Send TwostepFec from Rust sender + if let Node::Rust(ref sender) = topo.sender { + sender.send_broadcast_two_step(&topo.overlay_short_id, &test_data); + } + + // Wait for redistribution and delivery + sleep(Duration::from_secs(5)); + + // Check C++ leaf received the broadcast + if let Node::Cpp(ref mut leaf) = topo.leaf { + let received = leaf.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); + assert!(!received.is_empty(), "C++ leaf did not get TwostepFec broadcast via Rust bridges"); + assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); + println!("test_twostep_rust_sender_cpp_leaf: PASSED"); + } + + topo.shutdown(); +} + +/// Test B: C++ sender, all C++ bridges, Rust leaf +/// Verifies C++ TwostepFec reaches Rust through C++ redistribution +#[test] +fn test_twostep_cpp_sender_rust_leaf() { + skip_if_no_cpp!(); + + let mut topo = setup_twostep_topology(10, false, 0b0000, true); + sleep(Duration::from_millis(500)); + + let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xB2); + + // Send FEC broadcast from C++ sender (C++ will use twostep if enabled) + if let Node::Cpp(ref mut sender) = topo.sender { + sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); + } + + // Check Rust leaf - call wait_for_broadcast immediately to avoid BroadcastReceiver drop + if let Node::Rust(ref leaf) = topo.leaf { + let received = leaf.wait_for_broadcast(&topo.overlay_short_id, 10); + assert!(received.is_some(), "Rust leaf did not get TwostepFec broadcast via C++ bridges"); + assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); + println!("test_twostep_cpp_sender_rust_leaf: PASSED"); + } + + topo.shutdown(); +} + +/// Test C: Rust sender, mixed bridges (2 Rust, 2 C++), Rust leaf +/// Verifies mixed Rust/C++ redistribution works +#[test] +fn test_twostep_mixed_bridges_rust_leaf() { + skip_if_no_cpp!(); + + // Bridges 0,1 are Rust, 2,3 are C++ + let topo = setup_twostep_topology(20, true, 0b0011, true); + sleep(Duration::from_millis(500)); + + let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xC3); + + // Send TwostepFec from Rust sender + if let Node::Rust(ref sender) = topo.sender { + sender.send_broadcast_two_step(&topo.overlay_short_id, &test_data); + } + + // Check Rust leaf + if let Node::Rust(ref leaf) = topo.leaf { + let received = leaf.wait_for_broadcast(&topo.overlay_short_id, 10); + assert!(received.is_some(), "Rust leaf did not get TwostepFec broadcast via mixed bridges"); + assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); + println!("test_twostep_mixed_bridges_rust_leaf: PASSED"); + } + + topo.shutdown(); +} + +/// Test D: C++ sender, mixed bridges (2 Rust, 2 C++), C++ leaf +/// Verifies C++ TwostepFec works with mixed redistribution to C++ leaf +#[test] +fn test_twostep_mixed_bridges_cpp_leaf() { + skip_if_no_cpp!(); + + // Bridges 0,1 are Rust, 2,3 are C++ + let mut topo = setup_twostep_topology(30, false, 0b0011, false); + sleep(Duration::from_millis(500)); + + let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xD4); + + // Send FEC broadcast from C++ sender + if let Node::Cpp(ref mut sender) = topo.sender { + sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); + } + + // Wait for redistribution and delivery + sleep(Duration::from_secs(5)); + + // Check C++ leaf received the broadcast + if let Node::Cpp(ref mut leaf) = topo.leaf { + let received = leaf.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); + assert!( + !received.is_empty(), + "C++ leaf did not get TwostepFec broadcast via mixed bridges" + ); + assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); + println!("test_twostep_mixed_bridges_cpp_leaf: PASSED"); + } + + topo.shutdown(); +} diff --git a/src/node/tests/test_run_net_py/log_cfg_blank.yml b/src/node/tests/test_run_net_py/log_cfg_blank.yml index 39355f0..c2d0117 100644 --- a/src/node/tests/test_run_net_py/log_cfg_blank.yml +++ b/src/node/tests/test_run_net_py/log_cfg_blank.yml @@ -99,4 +99,9 @@ loggers: simplex: level: debug + quic: + level: debug + + consensus_adnl_overlay: + level: debug diff --git a/src/node/tests/test_run_net_py/simplex_config.json b/src/node/tests/test_run_net_py/simplex_config.json index 600f48c..196f51a 100644 --- a/src/node/tests/test_run_net_py/simplex_config.json +++ b/src/node/tests/test_run_net_py/simplex_config.json @@ -1,6 +1,6 @@ { "target_rate_ms": 500, "slots_per_leader_window": 4, - "first_block_timeout_ms": 1000, + "first_block_timeout_ms": 3000, "max_leader_window_desync": 2 } diff --git a/src/node/tests/test_run_net_py/test_run_net.py b/src/node/tests/test_run_net_py/test_run_net.py index 8304114..68aca88 100644 --- a/src/node/tests/test_run_net_py/test_run_net.py +++ b/src/node/tests/test_run_net_py/test_run_net.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 import argparse +import base64 +import hashlib +import json import os -import subprocess import shutil -import json -import yaml +import subprocess import time from pathlib import Path -import base64 -import hashlib +import yaml node_proc_name: str rust_proc_suffix: str @@ -164,7 +164,12 @@ def print_current_branches(): print(f"Current C++ branch: {current_branch_cpp}") -def run_command(cmd: list[str], cwd: Path | None = None, check: bool = True, capture_output: bool = True): +def run_command( + cmd: list[str], + cwd: Path | None = None, + check: bool = True, + capture_output: bool = True, +): try: result = subprocess.run( cmd, @@ -312,6 +317,11 @@ def run_cpp_node( stderr_path = logs_path / f"output_{node_index}.log" working_dir = build_node_work_path(node_index) node_bin_path = bins_path / (node_proc_name + "_" + cpp_proc_suffix) + if not node_bin_path.exists(): + raise FileNotFoundError( + f"C++ binary not found at {node_bin_path}. " + f"Either build it or copy it to {bins_path}/ before running with cpp_nodes_count > 0." + ) if start_new_session: print(f"Starting C++ node {node_index}...") with stdout_path.open("w") as out_log, stderr_path.open("w") as err_log: @@ -436,9 +446,10 @@ def prepare_node( # Build full console config console_config_path = node_work_path / "console.json" - with open(console_part_config_path) as f, open( - console_config_path, "w" - ) as fout: + with ( + open(console_part_config_path) as f, + open(console_config_path, "w") as fout, + ): c = json.load(f) c["client_key"] = {"type_id": 1209251014, "pvt_key": console_key_json["secret"]} console_full_config = {"config": c, "wallet_id": "", "max_factor": 3} @@ -480,6 +491,7 @@ def prepare_node( return validator_pubkey_hex + def extract_keys_from_rust_config(rust_config: dict): dht_pvt_key = None fullnode_pvt_key = None @@ -671,6 +683,7 @@ def build_zerostate( validator_pub_key_hex: list[str], simplex_mc: bool = False, simplex_config: dict = None, + use_quic: bool = False, ) -> str: print("Building zerostate...", end="") zerostate = json.loads(zerostate_blank) @@ -695,35 +708,38 @@ def build_zerostate( zerostate["master"]["config"]["p34"]["list"] = validators # Add ConfigParam 30 (NewConsensusConfigAll) for simplex if enabled - if simplex_mc and simplex_config: + if simplex_config: # Simplex (C++/Rust) allows equal `gen_utime` only starting from global_version >= 13. # Our default zerostate template uses version=11, which forces strict `prev + 1` and # makes fast single-host nets drift into the future, triggering validation rejects. # # Keep behavior C++-compatible by bumping version to at least 13 when simplex is enabled. - #TODO: LK: enable after change block version to 13 - #zerostate["master"]["config"]["p8"]["version"] = max( - # int(zerostate["master"]["config"]["p8"].get("version", 0)), - # 13, - #) + zerostate["master"]["config"]["p8"]["version"] = max( + int(zerostate["master"]["config"]["p8"].get("version", 0)), + 13, + ) p30 = {} - # MC simplex config (enabled when --simplex-mc is specified) - p30["mc"] = { + simplex_entry = { "target_rate_ms": simplex_config.get("target_rate_ms", 500), "slots_per_leader_window": simplex_config.get("slots_per_leader_window", 4), - "first_block_timeout_ms": simplex_config.get("first_block_timeout_ms", 1000), - "max_leader_window_desync": simplex_config.get("max_leader_window_desync", 2), + "first_block_timeout_ms": simplex_config.get( + "first_block_timeout_ms", 1000 + ), + "max_leader_window_desync": simplex_config.get( + "max_leader_window_desync", 2 + ), } + if use_quic: + simplex_entry["use_quic"] = 1 + # MC simplex config (enabled when --simplex-mc is specified) + if simplex_mc: + p30["mc"] = dict(simplex_entry) # Shard simplex config (always enabled when simplex is used) - p30["shard"] = { - "target_rate_ms": simplex_config.get("target_rate_ms", 500), - "slots_per_leader_window": simplex_config.get("slots_per_leader_window", 4), - "first_block_timeout_ms": simplex_config.get("first_block_timeout_ms", 1000), - "max_leader_window_desync": simplex_config.get("max_leader_window_desync", 2), - } + p30["shard"] = dict(simplex_entry) zerostate["master"]["config"]["p30"] = p30 - print(f" [simplex enabled: mc={simplex_mc}]", end="") + quic_str = ", quic=true" if use_quic else "" + print(f" [simplex enabled: mc={simplex_mc}{quic_str}]", end="") zs_json_path = common_config_path / "zerostate.json" with zs_json_path.open("w") as fout: @@ -793,6 +809,7 @@ def build_global_config(zerostate_info: str): print(" done") + def build_nodectl_config(root_path): global run_fullnode, nodes_count, common_config_path @@ -804,9 +821,10 @@ def build_nodectl_config(root_path): c = json.load(f) node_control_servers["node" + str(n)] = c["config"] node_control_servers["node" + str(n)]["timeouts"] = 5 - with open(root_path / "nodectl_blank.json") as f, open( - common_config_path / "nodectl-local.json", "w" - ) as fout: + with ( + open(root_path / "nodectl_blank.json") as f, + open(common_config_path / "nodectl-local.json", "w") as fout, + ): c = json.load(f) # New nodectl config expects `nodes`. c["nodes"] = node_control_servers @@ -814,6 +832,7 @@ def build_nodectl_config(root_path): json.dump(c, fout, indent=2) print(" done") + def main(): parser = argparse.ArgumentParser() @@ -833,27 +852,40 @@ def main(): help="Start nodes (not all the network) with given numbers (whitespace separated) or all if not specified", ) parser.add_argument("--stop", action="store_true", help="Only kill nodes and exit") + parser.add_argument( + "--prepare", + action="store_true", + help="Kill, build, generate configs and zerostate, but do not start nodes", + ) parser.add_argument( "--simplex", action="store_true", - help="Build with simplex feature enabled", + help="Enable simplex consensus config in zerostate (ConfigParam 30)", ) parser.add_argument( "--simplex-mc", action="store_true", help="Enable simplex consensus for masterchain (implies --simplex)", ) + parser.add_argument( + "--quic", + action="store_true", + help="Enable QUIC overlay transport in ConfigParam 30 (use_quic flag). Implies --simplex.", + ) args = parser.parse_args() + # --quic implies --simplex + if args.quic: + args.simplex = True # --simplex-mc implies --simplex if args.simplex_mc: args.simplex = True if args.start is None: args.start = False - run_net = not args.stop and not args.start and not args.restart - stop = run_net or args.stop or args.restart - build = not args.nobuild and run_net or args.restart - gen_configs = run_net + run_net = not args.stop and not args.start and not args.restart and not args.prepare + stop = run_net or args.stop or args.restart or args.prepare + build = not args.nobuild and (run_net or args.restart or args.prepare) + gen_configs = run_net or args.prepare start = run_net or args.start or args.restart # Init script config @@ -871,11 +903,9 @@ def main(): cleanup() if build: - # Build with simplex feature if --simplex is specified - build_features = ["simplex"] if args.simplex else [] - build_rust(build_features) # always build rust because we need tools etc. - if cpp_nodes_count > 0: - build_cpp() + build_rust([]) # always build rust because we need tools etc. + #if cpp_nodes_count > 0: + # build_cpp() test_root_path = Path(__file__).parent @@ -916,13 +946,18 @@ def main(): print(f"Created default simplex config: {simplex_config_path}") # Build zerostate - zerostate_name = "zerostate_blank_elections.json" if args.elections else "zerostate_blank.json" + zerostate_name = ( + "zerostate_blank_elections.json" + if args.elections + else "zerostate_blank.json" + ) zerostate_blank = Path(test_root_path / zerostate_name).read_text() zerostate_info = build_zerostate( zerostate_blank, validator_pub_keys, simplex_mc=args.simplex_mc, simplex_config=simplex_config, + use_quic=args.quic, ) # Build global config diff --git a/src/rust-toolchain.toml b/src/rust-toolchain.toml index 11fd459..d6d3381 100644 --- a/src/rust-toolchain.toml +++ b/src/rust-toolchain.toml @@ -1,3 +1,2 @@ [toolchain] channel = "1.91.1" -components = ["clippy"] diff --git a/src/tl/ton_api/tl/ton_api.tl b/src/tl/ton_api/tl/ton_api.tl index 62a71a2..b60f263 100644 --- a/src/tl/ton_api/tl/ton_api.tl +++ b/src/tl/ton_api/tl/ton_api.tl @@ -1130,6 +1130,10 @@ consensus.simplex.db.candidateResolver.candidateInfo leader_id:int candidate_has consensus.simplex.db.key.candidateResolver.notarCert candidateId:consensus.candidateId = consensus.simplex.db.key.candidateResolver.NotarCert; consensus.simplex.db.candidateResolver.notarCert notar:consensus.simplex.voteSignatureSet = consensus.simplex.db.candidateResolver.NotarCert; +// Candidate resolver: full candidate payload (serialized CandidateData bytes) +// Reference: C++ candidate-resolver.cpp store_candidate() / try_load_candidate_data_from_db() +consensus.simplex.db.key.candidate candidateId:consensus.candidateId = consensus.simplex.db.key.Candidate; + ---functions--- consensus.simplex.requestCandidate id:consensus.CandidateId want_candidate:Bool want_notar:Bool = consensus.simplex.CandidateAndCert; consensus.simplex.vote unsignedVote:consensus.simplex.UnsignedVote signature:bytes = consensus.simplex.Vote; diff --git a/src/vm/src/tests/test_executor.rs b/src/vm/src/tests/test_executor.rs index 60979a5..9c6956e 100644 --- a/src/vm/src/tests/test_executor.rs +++ b/src/vm/src/tests/test_executor.rs @@ -9,6 +9,7 @@ * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ use crate::{ + error::tvm_exception_code, executor::{ engine::Engine, math::DivMode, @@ -24,7 +25,10 @@ use crate::{ }, }; use std::collections::HashSet; -use ton_block::{BuilderData, Cell, IBitstring, SliceData, Status}; +use ton_block::{ + BuilderData, Cell, CurrencyCollection, Deserializable, ExceptionCode, IBitstring, SliceData, + Status, +}; #[test] fn test_assert_stack() { @@ -257,3 +261,19 @@ fn test_currency_collection_ser() { let b2 = BuilderData::with_raw(vec![0x3b, 0xc6, 0x14, 0xe0], 29).unwrap(); assert_eq!(b1, b2); } + +#[test] +fn test_tvm_serialize_currency_collection() { + let coins = 1u64 << 63; + let coins1 = int!(coins).as_coins().unwrap(); + let builder = serialize_currency_collection(coins1, None).unwrap(); + let mut slice = SliceData::load_builder(builder).unwrap(); + let coins1 = CurrencyCollection::construct_from(&mut slice).unwrap(); + let coins2 = CurrencyCollection::with_coins(coins); + assert_eq!(coins1, coins2); + + assert_eq!( + tvm_exception_code(&int!(1u128 << 120).as_coins().expect_err("Expect range check error")), + Some(ExceptionCode::RangeCheckError) + ); +} From d3ce15ac7d23620265dc59feca0739945bfe89fc Mon Sep 17 00:00:00 2001 From: ViacheslavB Date: Wed, 25 Mar 2026 23:02:27 +0300 Subject: [PATCH 06/48] Adjust node version --- src/node/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/Cargo.toml b/src/node/Cargo.toml index 31e294c..8a0424f 100644 --- a/src/node/Cargo.toml +++ b/src/node/Cargo.toml @@ -3,7 +3,7 @@ build = '../common/build/build.rs' edition = '2021' license = 'GPL-3.0' name = 'node' -version = '0.3.0' +version = '0.4.0' [[bin]] name = 'adnl_resolve' @@ -127,4 +127,4 @@ telemetry = ['adnl/telemetry', 'storage/telemetry'] trace_alloc = [] trace_alloc_detail = ['trace_alloc'] xp25 = ['ton_block/xp25', 'adnl/xp25'] -mirrornet = ['ton_block/mirrornet'] \ No newline at end of file +mirrornet = ['ton_block/mirrornet'] From 0947899c9596a40d09472370dfeed97e17378b43 Mon Sep 17 00:00:00 2001 From: ViacheslavB Date: Wed, 25 Mar 2026 23:04:16 +0300 Subject: [PATCH 07/48] Fix for clip --- src/rust-toolchain.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rust-toolchain.toml b/src/rust-toolchain.toml index d6d3381..11fd459 100644 --- a/src/rust-toolchain.toml +++ b/src/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] channel = "1.91.1" +components = ["clippy"] From 4bec03306088149d7b438de237df58c3a02d3d65 Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 26 Mar 2026 21:49:04 +0300 Subject: [PATCH 08/48] CellsDB: cells cache --- src/node/Cargo.toml | 4 +- src/node/src/engine.rs | 43 ++++++++++++++-- src/node/storage/src/dynamic_boc_rc_db.rs | 54 ++++++++++++++++++--- src/node/storage/src/lib.rs | 25 ++++++++-- src/node/storage/src/shardstate_db_async.rs | 13 ++++- src/rust-toolchain.toml | 1 + 6 files changed, 120 insertions(+), 20 deletions(-) diff --git a/src/node/Cargo.toml b/src/node/Cargo.toml index 31e294c..afd08d6 100644 --- a/src/node/Cargo.toml +++ b/src/node/Cargo.toml @@ -3,7 +3,7 @@ build = '../common/build/build.rs' edition = '2021' license = 'GPL-3.0' name = 'node' -version = '0.3.0' +version = '0.4.0' [[bin]] name = 'adnl_resolve' @@ -122,9 +122,9 @@ default = ['telemetry', 'ton_block/export_key', 'validator_session/export_key'] cell_counter = ['ton_block/cell_counter'] ci_run = [] export_key = ['catchain/export_key', 'ton_block/export_key'] +mirrornet = ['ton_block/mirrornet'] only_sorted_clean = [] telemetry = ['adnl/telemetry', 'storage/telemetry'] trace_alloc = [] trace_alloc_detail = ['trace_alloc'] xp25 = ['ton_block/xp25', 'adnl/xp25'] -mirrornet = ['ton_block/mirrornet'] \ No newline at end of file diff --git a/src/node/src/engine.rs b/src/node/src/engine.rs index 1b1d070..f734c37 100644 --- a/src/node/src/engine.rs +++ b/src/node/src/engine.rs @@ -1453,10 +1453,16 @@ impl Engine { shardstates_queue: create_metric("Alloc NODE shardstates queue"), cached_cells_counters: create_metric("Alloc NODE cells counters"), - loaded_cells: create_metric_per_sec("NODE loaded from db cells/sec"), - load_cell_time_nanos: create_metric_with_total_average( + loaded_cells_from_db: create_metric_per_sec("NODE loaded from db cells/sec"), + load_cell_from_db_time_nanos: create_metric_with_total_average( "NODE cell load time from db, nanos", ), + load_cell_from_cache_time_nanos: create_metric_with_total_average( + "NODE cell load time from cache, nanos", + ), + store_cell_to_cache_time_nanos: create_metric_with_total_average( + "NODE cell store time to cache, nanos", + ), stored_new_cells: create_metric_per_sec("NODE stored new cells & counters/sec"), deleted_cells: create_metric_per_sec("NODE deleted cells & counters/sec"), @@ -1496,6 +1502,9 @@ impl Engine { delete_boc_commit_micros: create_metric_with_total_average( "NODE delete boc: commit time, micros", ), + cell_cache_hits: create_metric_per_sec("NODE cell cache hits/sec"), + cell_cache_misses: create_metric_per_sec("NODE cell cache misses/sec"), + cell_cache_len: create_metric("NODE cell cache len"), }); let engine_telemetry = Arc::new(EngineTelemetry { storage: storage_telemetry, @@ -1516,8 +1525,10 @@ impl Engine { TelemetryItem::Metric(engine_telemetry.storage.storing_cells.clone()), TelemetryItem::Metric(engine_telemetry.storage.shardstates_queue.clone()), TelemetryItem::Metric(engine_telemetry.storage.cached_cells_counters.clone()), - TelemetryItem::MetricBuilder(engine_telemetry.storage.loaded_cells.clone()), - TelemetryItem::Metric(engine_telemetry.storage.load_cell_time_nanos.clone()), + TelemetryItem::MetricBuilder(engine_telemetry.storage.loaded_cells_from_db.clone()), + TelemetryItem::Metric(engine_telemetry.storage.load_cell_from_db_time_nanos.clone()), + TelemetryItem::Metric(engine_telemetry.storage.load_cell_from_cache_time_nanos.clone()), + TelemetryItem::Metric(engine_telemetry.storage.store_cell_to_cache_time_nanos.clone()), TelemetryItem::MetricBuilder(engine_telemetry.storage.stored_new_cells.clone()), TelemetryItem::MetricBuilder(engine_telemetry.storage.deleted_cells.clone()), TelemetryItem::MetricBuilder(engine_telemetry.storage.loaded_counters.clone()), @@ -2407,6 +2418,30 @@ fn telemetry_logger(engine: Arc) { // print telemetry + { + let hits = engine + .engine_telemetry + .storage + .cell_cache_hits + .metric() + .total_amount() + .unwrap_or(0); + let misses = engine + .engine_telemetry + .storage + .cell_cache_misses + .metric() + .total_amount() + .unwrap_or(0); + let total = hits + misses; + let hit_rate = if total > 0 { hits * 100 / total } else { 0 }; + log::info!( + target: "telemetry", + "Cell cache hit_rate: {}%", + hit_rate + ); + } + engine.telemetry_printer.try_print(); let period = crate::full_node::telemetry::TPS_PERIOD_1; diff --git a/src/node/storage/src/dynamic_boc_rc_db.rs b/src/node/storage/src/dynamic_boc_rc_db.rs index 3b16d3a..64a63d0 100644 --- a/src/node/storage/src/dynamic_boc_rc_db.rs +++ b/src/node/storage/src/dynamic_boc_rc_db.rs @@ -118,6 +118,7 @@ pub struct DynamicBocDb { storing_cells: Arc>, storing_cells_count: AtomicU64, cells_counters: Option>>, + cell_cache: quick_cache::sync::Cache, #[cfg(feature = "telemetry")] telemetry: Arc, allocated: Arc, @@ -153,6 +154,7 @@ impl DynamicBocDb { storing_cells: Arc::new(lockfree::map::Map::new()), storing_cells_count: AtomicU64::new(0), cells_counters, + cell_cache: quick_cache::sync::Cache::new(config.cells_lru_cache_capacity), #[cfg(feature = "telemetry")] telemetry, allocated, @@ -251,8 +253,8 @@ impl DynamicBocDb { self.telemetry .stored_cells .update(self.allocated.storage_cells.load(Ordering::Relaxed)); - self.telemetry.loaded_cells.update(1); - self.telemetry.load_cell_time_nanos.update(now.elapsed().as_nanos() as u64); + self.telemetry.loaded_cells_from_db.update(1); + self.telemetry.load_cell_from_db_time_nanos.update(now.elapsed().as_nanos() as u64); } return Ok(Cell::with_cell_impl(cell)); } @@ -487,6 +489,39 @@ impl DynamicBocDb { } pub(crate) fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { + #[cfg(feature = "telemetry")] + let now = Instant::now(); + if let Some(cell) = self.cell_cache.get(cell_id) { + #[cfg(feature = "telemetry")] + { + self.telemetry.cell_cache_hits.update(1); + self.telemetry + .load_cell_from_cache_time_nanos + .update(now.elapsed().as_nanos() as u64); + } + return Ok(cell); + } + #[cfg(feature = "telemetry")] + self.telemetry.cell_cache_misses.update(1); + let cell = self.load_cell_uncached(cell_id, panic)?; + #[cfg(feature = "telemetry")] + let now_insert = Instant::now(); + self.cell_cache.insert(cell_id.clone(), cell.clone()); + #[cfg(feature = "telemetry")] + { + self.telemetry + .store_cell_to_cache_time_nanos + .update(now_insert.elapsed().as_nanos() as u64); + self.telemetry.cell_cache_len.update(self.cell_cache.len() as u64); + } + Ok(cell) + } + + pub(crate) fn load_cell_uncached( + self: &Arc, + cell_id: &UInt256, + panic: bool, + ) -> Result { #[cfg(feature = "telemetry")] let now = Instant::now(); let storage_cell_data = match self.db.get_pinned_cf(&self.cells_cf()?, cell_id.as_slice()) { @@ -517,7 +552,7 @@ impl DynamicBocDb { }; #[cfg(feature = "telemetry")] - let load_cell_time_nanos = now.elapsed().as_nanos() as u64; + let load_cell_from_db_time_nanos = now.elapsed().as_nanos() as u64; let storage_cell = match StoredCell::deserialize(self, cell_id, &storage_cell_data) { Ok(cell) => Arc::new(cell), @@ -548,8 +583,8 @@ impl DynamicBocDb { self.telemetry .stored_cells .update(self.allocated.storage_cells.load(Ordering::Relaxed)); - self.telemetry.load_cell_time_nanos.update(load_cell_time_nanos); - self.telemetry.loaded_cells.update(1); + self.telemetry.load_cell_from_db_time_nanos.update(load_cell_from_db_time_nanos); + self.telemetry.loaded_cells_from_db.update(1); } log::trace!( @@ -980,7 +1015,7 @@ impl CellByHashStorageAdapter { impl CellsStorage for CellByHashStorageAdapter { fn load_cell(&self, hash: &UInt256) -> Result { - if let Ok(c) = self.db.clone().load_cell(hash, false) { + if let Ok(c) = self.db.clone().load_cell_uncached(hash, false) { Ok(c) } else if let Some(data) = self.root_cells_data.get(hash) { StoredCell::deserialize(&self.db, hash, data).map(Cell::with_cell_impl) @@ -1000,8 +1035,11 @@ impl CellsStorage for CellByHashStorageAdapter { if let Ok(Some(data)) = self.db.db.get_pinned_cf(&self.db.cells_cf()?, hash.as_slice()) { #[cfg(feature = "telemetry")] { - self.db.telemetry.load_cell_time_nanos.update(now.elapsed().as_nanos() as u64); - self.db.telemetry.loaded_cells.update(1); + self.db + .telemetry + .load_cell_from_db_time_nanos + .update(now.elapsed().as_nanos() as u64); + self.db.telemetry.loaded_cells_from_db.update(1); } StoredCell::write_cell_data(&data, hash, write_hashes, dest) diff --git a/src/node/storage/src/lib.rs b/src/node/storage/src/lib.rs index 27c9408..4059f99 100644 --- a/src/node/storage/src/lib.rs +++ b/src/node/storage/src/lib.rs @@ -71,8 +71,10 @@ pub struct StorageTelemetry { pub shardstates_queue: Arc, pub cached_cells_counters: Arc, - pub loaded_cells: Arc, - pub load_cell_time_nanos: Arc, + pub loaded_cells_from_db: Arc, + pub load_cell_from_db_time_nanos: Arc, + pub load_cell_from_cache_time_nanos: Arc, + pub store_cell_to_cache_time_nanos: Arc, pub stored_new_cells: Arc, pub deleted_cells: Arc, @@ -92,6 +94,10 @@ pub struct StorageTelemetry { pub delete_boc_traverse_micros: Arc, pub delete_boc_tr_build_micros: Arc, pub delete_boc_commit_micros: Arc, + + pub cell_cache_hits: Arc, + pub cell_cache_misses: Arc, + pub cell_cache_len: Arc, } #[cfg(feature = "telemetry")] impl Default for StorageTelemetry { @@ -104,11 +110,13 @@ impl Default for StorageTelemetry { storing_cells: Metric::without_totals("", 1), shardstates_queue: Metric::without_totals("", 1), cached_cells_counters: Metric::without_totals("", 1), - loaded_cells: MetricBuilder::with_metric_and_period( + loaded_cells_from_db: MetricBuilder::with_metric_and_period( Metric::with_total_amount("", 1), 1000000000, ), - load_cell_time_nanos: Metric::with_total_average("", 1), + load_cell_from_db_time_nanos: Metric::with_total_average("", 1), + load_cell_from_cache_time_nanos: Metric::with_total_average("", 1), + store_cell_to_cache_time_nanos: Metric::with_total_average("", 1), stored_new_cells: MetricBuilder::with_metric_and_period( Metric::with_total_amount("", 1), 1000000000, @@ -136,6 +144,15 @@ impl Default for StorageTelemetry { delete_boc_traverse_micros: Metric::with_total_average("", 1), delete_boc_tr_build_micros: Metric::with_total_average("", 1), delete_boc_commit_micros: Metric::with_total_average("", 1), + cell_cache_hits: MetricBuilder::with_metric_and_period( + Metric::with_total_amount("", 1), + 1000000000, + ), + cell_cache_misses: MetricBuilder::with_metric_and_period( + Metric::with_total_amount("", 1), + 1000000000, + ), + cell_cache_len: Metric::without_totals("", 1), } } } diff --git a/src/node/storage/src/shardstate_db_async.rs b/src/node/storage/src/shardstate_db_async.rs index 809cd31..a3eac99 100644 --- a/src/node/storage/src/shardstate_db_async.rs +++ b/src/node/storage/src/shardstate_db_async.rs @@ -112,6 +112,14 @@ pub struct CellsDbConfig { pub prefill_cells_counters: bool, pub cells_cache_size_bytes: u64, pub counters_cache_size_bytes: u64, + #[serde(default = "CellsDbConfig::default_cells_lru_cache_capacity")] + pub cells_lru_cache_capacity: usize, +} + +impl CellsDbConfig { + fn default_cells_lru_cache_capacity() -> usize { + 5_000_000 + } } impl Default for CellsDbConfig { @@ -119,8 +127,9 @@ impl Default for CellsDbConfig { Self { states_db_queue_len: 1000, prefill_cells_counters: false, - cells_cache_size_bytes: 4_000_000_000, - counters_cache_size_bytes: 4_000_000_000, + cells_cache_size_bytes: 500_000_000, + counters_cache_size_bytes: 500_000_000, + cells_lru_cache_capacity: Self::default_cells_lru_cache_capacity(), } } } diff --git a/src/rust-toolchain.toml b/src/rust-toolchain.toml index d6d3381..11fd459 100644 --- a/src/rust-toolchain.toml +++ b/src/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] channel = "1.91.1" +components = ["clippy"] From d0e3928d7a5c569831fecc4602f104cf175d7ec1 Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 26 Mar 2026 22:35:24 +0300 Subject: [PATCH 09/48] Reliable fast sync overlay creation --- .../src/network/fast_sync_overlay_client.rs | 2 +- src/node/src/network/full_node_overlays.rs | 21 ++++++++++++++++--- src/node/src/network/overlay_client.rs | 9 +++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/node/src/network/fast_sync_overlay_client.rs b/src/node/src/network/fast_sync_overlay_client.rs index 83476d3..1975f5c 100644 --- a/src/node/src/network/fast_sync_overlay_client.rs +++ b/src/node/src/network/fast_sync_overlay_client.rs @@ -97,7 +97,7 @@ impl FastSyncOverlayClient { } pub fn stop(&self) { - log::debug!("Stopping fast sync overlay {} {}", self.shard, self.id); + log::info!("Stopping fast sync overlay {} {}", self.shard, self.id); self.client.delete().ok(); } diff --git a/src/node/src/network/full_node_overlays.rs b/src/node/src/network/full_node_overlays.rs index 3d99765..f96fdfd 100644 --- a/src/node/src/network/full_node_overlays.rs +++ b/src/node/src/network/full_node_overlays.rs @@ -491,6 +491,9 @@ impl FullNodeOverlaysRouter { shard_prefix, )?; if create { + if let Some(old) = self.fast_sync_overlays.remove(&shard) { + old.val().stop(); + } let overlay = create_overlay(&shard).await?; self.fast_sync_overlays.insert(shard, overlay) } else { @@ -501,24 +504,36 @@ impl FullNodeOverlaysRouter { Ok(()) }; + // Delete old overlays if monitor min split changed or we are not a validator anymore if (old_monitor_min_split != new_monitor_min_split) || key.is_none() { + if key.is_none() { + if let Some(old) = self.fast_sync_overlays.remove(&ShardIdent::MASTERCHAIN) { + old.val().stop(); + } + } update_monitor_min_split(old_monitor_min_split, false).await?; } if key.is_none() { self.monitor_min_split_for_fast_sync.store(new_monitor_min_split, Ordering::Relaxed); log::info!("We are not a validator"); + *cur_validators = new_validators.clone(); return Ok(()); } + // Update masterchain overlay if validators_changed { let shard = ShardIdent::MASTERCHAIN; - let overlay = create_overlay(&shard).await?; - if let Some(removed) = self.fast_sync_overlays.insert(shard, overlay) { - removed.val().stop(); + if let Some(old) = self.fast_sync_overlays.remove(&shard) { + old.val().stop(); } + let overlay = create_overlay(&shard).await?; + self.fast_sync_overlays.insert(shard, overlay); } + + // Create new shard overlays update_monitor_min_split(new_monitor_min_split, true).await?; + self.monitor_min_split_for_fast_sync.store(new_monitor_min_split, Ordering::Relaxed); *cur_validators = new_validators.clone(); diff --git a/src/node/src/network/overlay_client.rs b/src/node/src/network/overlay_client.rs index 5388cbb..9aa3b55 100644 --- a/src/node/src/network/overlay_client.rs +++ b/src/node/src/network/overlay_client.rs @@ -767,7 +767,14 @@ async fn resolve_peer_ips( } log::trace!("{}: resolve_peer_ips: searching IP for peer {}...", ctx.id, peer.key_id()); if let Some(ip) = peer.resolve(ctx.dht_node()).await? { - ctx.overlay_node().add_public_peer(&ip, peer.node(), &ctx.id)?; + if let Err(e) = ctx.overlay_node().add_public_peer(&ip, peer.node(), &ctx.id) { + log::warn!( + "{}: resolve_peer_ips: failed to add peer {}, IP {ip} to overlay: {e}", + ctx.id, + peer.key_id() + ); + continue; + } if ctx.neighbours_manager.add_overlay_peer(peer.key_id().clone()) { log::trace!("{}: resolve_peer_ips: added peer {}, IP {ip}", ctx.id, peer.key_id()); } else { From 8566cf7352a194831b325f83966c41879a7cae71 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Mon, 30 Mar 2026 22:23:55 +0300 Subject: [PATCH 10/48] Fix changed untouched account --- src/Cargo.lock | 2 +- src/node/src/types/accounts.rs | 8 ++++++-- src/node/src/validator/collator.rs | 32 +++++++++++++++--------------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index 0bf1a75..c87d8b2 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -3267,7 +3267,7 @@ dependencies = [ [[package]] name = "node" -version = "0.3.0" +version = "0.4.0" dependencies = [ "adnl", "ahash", diff --git a/src/node/src/types/accounts.rs b/src/node/src/types/accounts.rs index bea58e1..5b3b790 100644 --- a/src/node/src/types/accounts.rs +++ b/src/node/src/types/accounts.rs @@ -12,8 +12,8 @@ use crate::engine_traits::EngineOperations; use std::sync::Arc; use ton_block::{ fail, Account, AccountBlock, AccountId, AccountStorageStat, Augmentation, Cell, HashUpdate, - HashmapAugType, HashmapRemover, LibDescr, Libraries, Result, Serializable, ShardAccount, - ShardAccounts, StateInitLib, Transaction, Transactions, UInt256, UsageTree, + HashmapAugType, HashmapRemover, HashmapType, LibDescr, Libraries, Result, Serializable, + ShardAccount, ShardAccounts, StateInitLib, Transaction, Transactions, UInt256, UsageTree, }; pub struct ShardAccountStuff { @@ -126,6 +126,10 @@ impl ShardAccountStuff { &self.original_root } + pub fn is_touched(&self) -> bool { + !self.transactions.is_empty() + } + pub fn add_transaction( &mut self, transaction: &mut Transaction, diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index b81ee2b..bf988c5 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -3891,25 +3891,25 @@ impl Collator { new_config_opt = Some(Self::extract_new_config(shard_acc.account(), addr)?); } } - let acc_block = shard_acc.update_shard_state(&mut new_accounts)?; - if !acc_block.transactions().is_empty() { + if shard_acc.is_touched() { + let acc_block = shard_acc.update_shard_state(&mut new_accounts)?; accounts.insert(&acc_block)?; - } - let account = shard_acc.account(); - if let Some(storage_dict) = shard_acc.storage_dict() { - if account.dict_hash().is_some() { - let size = account.storage_info().map(|info| info.used().cells()).unwrap_or(0); - log::trace!( - "{}: updated storage dict with hash {:x} for account {:x} of size {}", - self.collated_block_descr, - storage_dict.repr_hash(), - account_id, - size - ); - self.engine.add_account_storage_dict(storage_dict, size) + let account = shard_acc.account(); + if let Some(storage_dict) = shard_acc.storage_dict() { + if account.dict_hash().is_some() { + let size = account.storage_info().map(|info| info.used().cells()).unwrap_or(0); + log::trace!( + "{}: updated storage dict with hash {:x} for account {:x} of size {}", + self.collated_block_descr, + storage_dict.repr_hash(), + account_id, + size + ); + self.engine.add_account_storage_dict(storage_dict, size) + } } + changed_accounts.insert(account_id, shard_acc); } - changed_accounts.insert(account_id, shard_acc); } if let Some(hardfork_config) = self.engine.get_config_for_hardfork() { From 4c7a30e0af56e4ee9761f6da8d700dab87e77db9 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Mon, 30 Mar 2026 22:31:02 +0300 Subject: [PATCH 11/48] fix fmt --- src/node/src/validator/collator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index bf988c5..a502254 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -3897,7 +3897,7 @@ impl Collator { let account = shard_acc.account(); if let Some(storage_dict) = shard_acc.storage_dict() { if account.dict_hash().is_some() { - let size = account.storage_info().map(|info| info.used().cells()).unwrap_or(0); + let size = account.storage_info().map_or(0, |info| info.used().cells()); log::trace!( "{}: updated storage dict with hash {:x} for account {:x} of size {}", self.collated_block_descr, From 13002995cc4adf7dabf5008af93557a220db472d Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Tue, 31 Mar 2026 14:21:07 +0300 Subject: [PATCH 12/48] Fix config params creation for emulator --- src/Cargo.lock | 1 + src/adnl/Cargo.toml | 1 + src/adnl/benches/bench_rldp.rs | 16 ++++------ src/block/src/config_params.rs | 18 +++++++---- src/executor/benches/benchmarks.rs | 9 +++--- src/executor/src/tests/common/mod.rs | 35 ++++++++++++++++++++-- src/executor/src/transaction_executor.rs | 6 ++-- src/node/storage/benches/shardstate_db1.rs | 4 +-- src/node/storage/benches/shardstate_db2.rs | 4 +-- src/node/storage/benches/shardstate_db3.rs | 4 +-- src/vm/benches/benchmarks.rs | 2 +- src/vm/src/executor/engine/core.rs | 18 +++++++++++ 12 files changed, 84 insertions(+), 34 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index c87d8b2..aee58d4 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -27,6 +27,7 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" name = "adnl" version = "0.11.38" dependencies = [ + "adnl", "anyhow", "async-trait", "chrono", diff --git a/src/adnl/Cargo.toml b/src/adnl/Cargo.toml index fedb3de..4da6d41 100644 --- a/src/adnl/Cargo.toml +++ b/src/adnl/Cargo.toml @@ -39,6 +39,7 @@ ton_api = { path = '../tl/ton_api' } [dev-dependencies] log4rs = '1.2' +adnl = { path = ".", features = [ 'server' ] } [features] client = [ ] diff --git a/src/adnl/benches/bench_rldp.rs b/src/adnl/benches/bench_rldp.rs index 9c958e4..c49f367 100644 --- a/src/adnl/benches/bench_rldp.rs +++ b/src/adnl/benches/bench_rldp.rs @@ -13,13 +13,12 @@ use adnl::{ AdnlPeers, Answer, QueryAnswer, QueryResult, Subscriber, TaggedByteSlice, TaggedByteVec, }, node::AdnlNode, - rldp::RldpNode, + RldpNode, }; use rand::Rng; -use std::sync::{ - atomic::{AtomicU32, AtomicU8, Ordering}, - Arc, -}; +#[cfg(feature = "debug")] +use std::sync::atomic::{AtomicU32, AtomicU8, Ordering}; +use std::sync::Arc; #[cfg(not(feature = "debug"))] use std::time::Instant; use ton_api::{ @@ -98,12 +97,7 @@ async fn bench_scenario( #[cfg(feature = "telemetry")] tag: 0, }; - let res = if v2 { - rldp1.query_v2(&data, Some(size as u64 + 1024), &peers, None).await - } else { - rldp1.query(&data, Some(size as u64 + 1024), &peers, None).await - } - .unwrap(); + let res = rldp1.query(&data, Some(size as u64 + 1024), &peers, v2, None).await.unwrap(); let (Some(reply), _) = res else { println!(" failed: empty response"); break; diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index 161b1f9..a765200 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -47,11 +47,17 @@ impl Default for ConfigParams { } impl ConfigParams { - pub const fn with_root(data: Cell) -> Self { - Self { - config_addr: AccountId::ZERO_ID, - config_params: HashmapE::with_hashmap(32, Some(data)), - } + pub fn with_root(data: Cell) -> Result { + let config_params = HashmapE::with_hashmap(32, Some(data)); + let cell = config_params + .get(0u32.write_to_bitstring()?)? + .ok_or_else(|| error!("config param 0 is missing"))? + .reference(0)?; + let result = ConfigParamEnum::construct_from_cell_and_number(cell, 0)?; + let ConfigParamEnum::ConfigParam0(ConfigParam0 { config_addr }) = result else { + fail!("config param 0 has invalid format"); + }; + Ok(Self { config_addr, config_params }) } pub const fn with_address_and_root(config_addr: AccountId, data: Cell) -> Self { @@ -3784,7 +3790,7 @@ pub struct SimplexConfig { pub max_leader_window_desync: u32, } -/// Byte layout: flags:(## 7) use_quic:Bool โ€” 7 flag bits (reserved) + 1 use_quic bit = 1 byte. +/// Byte layout: flags:(## 7) use_quic:Bool — 7 flag bits (reserved) + 1 use_quic bit = 1 byte. /// TLB writes MSB-first, so use_quic occupies the LSB: byte = (flags << 1) | use_quic. impl Serializable for SimplexConfig { fn write_to(&self, cell: &mut BuilderData) -> Result<()> { diff --git a/src/executor/benches/benchmarks.rs b/src/executor/benches/benchmarks.rs index a503194..a57b375 100644 --- a/src/executor/benches/benchmarks.rs +++ b/src/executor/benches/benchmarks.rs @@ -32,7 +32,7 @@ fn replay_transaction_by_files( ) { let config = read_config(config).unwrap(); let mc_state_proof = mc_state_proof_cell_with_config(config); - replay_transaction(c, acc, acc_after, tr, mc_state_proof); + replay_transaction(c, acc, acc_after, tr, "", mc_state_proof); } fn bench_simple_transaction(c: &mut Criterion) { @@ -326,7 +326,7 @@ fn load_blockchain_config(config_account: &Account) -> Result .ok_or_else(|| error!("config account data loading error"))? .reference(0) .unwrap(); - let config_params = ConfigParams::with_root(config_cell); + let config_params = ConfigParams::with_root(config_cell)?; BlockchainConfig::with_config(config_params) } @@ -351,7 +351,7 @@ fn load_block(block_filename: &str) -> Result { fn replay_block(data: BlockData) -> Status { for acc in data.accounts { - let mut account = acc.account_cell; + let mut account = Account::construct_from_cell(acc.account_cell.clone())?; for tr in acc.transactions { let executor: Box = match tr.read_description()? { TransactionDescr::TickTock(desc) => { @@ -372,7 +372,7 @@ fn replay_block(data: BlockData) -> Status { ..Default::default() }, )?; - if account.repr_hash() != tr.read_state_update()?.new_hash { + if account.serialize()?.repr_hash() != tr.read_state_update()?.new_hash { fail!("new hash mismatch"); } } @@ -414,7 +414,6 @@ criterion_group!( bench_tick_tock_message, bench_count_steps_vm, bench_not_aborted_accepted_transaction, - bench_ihr_fee_output_msg, bench_block_0, ); criterion_main!(benches); diff --git a/src/executor/src/tests/common/mod.rs b/src/executor/src/tests/common/mod.rs index ff71b5d..33ca559 100644 --- a/src/executor/src/tests/common/mod.rs +++ b/src/executor/src/tests/common/mod.rs @@ -8,7 +8,6 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -#![cfg(test)] #![allow(dead_code)] #![allow(clippy::duplicate_mod)] #![allow(clippy::field_reassign_with_default)] @@ -123,6 +122,7 @@ pub fn default_config() -> BlockchainConfig { BlockchainConfig::with_config(create_config("real_boc/default_config.boc").unwrap()).unwrap() } +#[cfg(feature = "cross_check")] pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { let debug = false; // let _ = cross_check::DisableCrossCheck::new(); @@ -137,6 +137,36 @@ pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { } } +#[cfg(not(feature = "cross_check"))] +pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { + // let _ = cross_check::DisableCrossCheck::new(); + enum DebugType { + None, + Simple, + Emulator, + } + let debug = DebugType::None; + let (verbosity, pattern, trace_callback) = match debug { + DebugType::None => (4, None, None), + DebugType::Simple => (2048 + 4, Some("{m}"), None), + DebugType::Emulator => { + let emulator_trace_callback: Option> = + Some(Arc::new(ton_vm::executor::Engine::emulator_trace_callback)); + (2048 + 4, Some("{m}"), emulator_trace_callback) + } + }; + init_log_without_config(pattern, log::LevelFilter::Debug, None); + cross_check::set_cross_check_verbosity(verbosity); + ExecuteParams { + block_unixtime: BLOCK_UT, + block_lt: last_tr_lt - last_tr_lt % 1_000_000, + last_tr_lt, + trace_callback, + debug: !matches!(debug, DebugType::None), + ..ExecuteParams::default() + } +} + pub fn execute_params_none() -> ExecuteParams { execute_params(BLOCK_LT + 1) } @@ -887,6 +917,7 @@ pub fn replay_transaction( // transaction.write_to_file(tr).unwrap(); // } // } + // pretty_assertions::assert_eq!(our_transaction, transaction); pretty_assertions::assert_eq!( our_transaction.read_description().unwrap(), transaction.read_description().unwrap() @@ -925,7 +956,7 @@ pub fn replay_transaction( pretty_assertions::assert_eq!(account, account_after); } -fn read_config(cfg: &str) -> Result { +pub fn read_config(cfg: &str) -> Result { println!("prepare to read config"); let config = if let Ok(data) = base64_decode(cfg) { println!("config read as base64"); diff --git a/src/executor/src/transaction_executor.rs b/src/executor/src/transaction_executor.rs index 7c529d8..88fda3a 100644 --- a/src/executor/src/transaction_executor.rs +++ b/src/executor/src/transaction_executor.rs @@ -1281,8 +1281,8 @@ fn outmsg_action_handler( let (force_body_to_ref, body) = match msg.body() { None => (false, None), Some(body) => { - let b = body.clone().into_builder().unwrap_or_default(); - (b.references_used() >= 2, Some(b)) + let b = body.clone().into_cell().unwrap_or_default(); + (b.references_count() >= 2, Some(b)) } }; let (force_init_to_ref, init) = match msg.state_init() { @@ -1318,7 +1318,7 @@ fn outmsg_action_handler( }; let mut sstat = StorageUsageCalc::with_limits(max_cells, limits.max_msg_bits as u64); if let Some(body) = &body { - sstat.append_builder(body, body_to_ref, &mut 0).map_err(|err| { + sstat.append_cell(body, body_to_ref, &mut 0).map_err(|err| { log::error!(target: "executor", "cannot calc msg storage used for body: {err}"); RESULT_CODE_UNKNOWN_OR_INVALID_ACTION })?; diff --git a/src/node/storage/benches/shardstate_db1.rs b/src/node/storage/benches/shardstate_db1.rs index a6e9234..d222286 100644 --- a/src/node/storage/benches/shardstate_db1.rs +++ b/src/node/storage/benches/shardstate_db1.rs @@ -11,7 +11,7 @@ use std::{collections::HashMap, sync::Arc}; #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - db::rocksdb::{destroy_rocks_db, RocksDb}, + db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::{CellsDbConfig, ShardStateDb, SsNotificationCallback}, StorageAlloc, @@ -49,7 +49,7 @@ async fn main() -> Result<()> { "counters".to_string(), DynamicBocDb::build_counters_cf_options(&CellsDbConfig::default()), ); - let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, None)?; + let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, AccessType::ReadOnly)?; let ss_db = ShardStateDb::new( db.clone(), "shardstate_db", diff --git a/src/node/storage/benches/shardstate_db2.rs b/src/node/storage/benches/shardstate_db2.rs index 6a89130..e46736b 100644 --- a/src/node/storage/benches/shardstate_db2.rs +++ b/src/node/storage/benches/shardstate_db2.rs @@ -10,7 +10,7 @@ use std::{collections::HashMap, io::Cursor, ops::Deref, sync::Arc}; #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - db::rocksdb::{destroy_rocks_db, RocksDb}, + db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::{CellsDbConfig, ShardStateDb}, StorageAlloc, @@ -40,7 +40,7 @@ async fn main() -> Result<()> { "counters".to_string(), DynamicBocDb::build_counters_cf_options(&CellsDbConfig::default()), ); - let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, None)?; + let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, AccessType::ReadOnly)?; let ss_db = ShardStateDb::new( db.clone(), "shardstate_db", diff --git a/src/node/storage/benches/shardstate_db3.rs b/src/node/storage/benches/shardstate_db3.rs index 05519e2..67e9cf1 100644 --- a/src/node/storage/benches/shardstate_db3.rs +++ b/src/node/storage/benches/shardstate_db3.rs @@ -10,7 +10,7 @@ use std::{collections::HashMap, fs::OpenOptions, path::Path, sync::Arc}; #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - db::rocksdb::RocksDb, + db::rocksdb::{AccessType, RocksDb}, dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::{CellsDbConfig, ShardStateDb}, StorageAlloc, @@ -37,7 +37,7 @@ async fn main() -> Result<()> { "counters".to_string(), DynamicBocDb::build_counters_cf_options(&CellsDbConfig::default()), ); - let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, None)?; + let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, AccessType::ReadOnly)?; let ss_db = ShardStateDb::new( db.clone(), "shardstate_db", diff --git a/src/vm/benches/benchmarks.rs b/src/vm/benches/benchmarks.rs index 43fb0fc..b374f9a 100644 --- a/src/vm/benches/benchmarks.rs +++ b/src/vm/benches/benchmarks.rs @@ -241,7 +241,7 @@ fn bench_mergesort_tuple(c: &mut Criterion) { engine.execute().unwrap(); assert_eq!(engine.gas_used(), 51_216_096); assert_eq!(engine.stack().depth(), 1); - assert_eq!(engine.stack().get(0), &expected); + assert_eq!(engine.stack().get(0).unwrap(), &expected); }) }); } diff --git a/src/vm/src/executor/engine/core.rs b/src/vm/src/executor/engine/core.rs index 6b8cb8b..0d10624 100644 --- a/src/vm/src/executor/engine/core.rs +++ b/src/vm/src/executor/engine/core.rs @@ -447,6 +447,24 @@ impl Engine { } } + pub fn emulator_trace_callback(&self, info: &EngineTraceInfo) { + if info.has_cmd() { + if self.trace_bit(Engine::TRACE_CODE) { + if let Ok(code_cell) = info.cmd_code.cell().as_ref() { + log::info!(target: "executor", "code cell hash: {:X} offset: {}\n", + code_cell.repr_hash(), info.cmd_code.pos()); + } + log::info!(target: "executor", "{}\n", info.cmd_str); + } + if self.trace_bit(Engine::TRACE_STACK) { + log::info!(target: "executor", " [ {} ] \n", self.get_stack_result_fift()); + } + if self.trace_bit(Engine::TRACE_GAS) { + log::info!(target: "executor", "gas - {}\n", info.gas_used); + } + } + } + #[allow(dead_code)] fn dump_stack_result(stack: &Stack) -> String { static PREV_STACK: LazyLock> = LazyLock::new(|| Mutex::new(Stack::new())); From de2717defa86372cddda4981c2ec3c4cbbc406c4 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Tue, 31 Mar 2026 14:27:57 +0300 Subject: [PATCH 13/48] Fix config params creation for emulator --- src/block/src/config_params.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index a765200..48edb70 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -3790,7 +3790,7 @@ pub struct SimplexConfig { pub max_leader_window_desync: u32, } -/// Byte layout: flags:(## 7) use_quic:Bool — 7 flag bits (reserved) + 1 use_quic bit = 1 byte. +/// Byte layout: flags:(## 7) use_quic:Bool - 7 flag bits (reserved) + 1 use_quic bit = 1 byte. /// TLB writes MSB-first, so use_quic occupies the LSB: byte = (flags << 1) | use_quic. impl Serializable for SimplexConfig { fn write_to(&self, cell: &mut BuilderData) -> Result<()> { From d3e702673db4cc3592cd0990ae5ab93434951208 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Fri, 27 Mar 2026 12:49:26 +0300 Subject: [PATCH 14/48] Fix config params creation for emulator --- src/assembler/src/complex.rs | 2 +- src/block/src/accounts.rs | 2 +- src/block/src/blocks.rs | 2 +- src/block/src/config_params.rs | 13 ++-- src/block/src/dictionary/hashmap.rs | 10 +-- src/block/src/dictionary/mod.rs | 4 +- src/block/src/dictionary/pfxhashmap.rs | 6 +- .../src/dictionary/tests/test_hashmap.rs | 6 +- src/block/src/transactions.rs | 2 +- src/emulator/src/lib.rs | 10 ++- src/executor/src/tests/common/mod.rs | 28 ++++---- .../src/tests/test_ordinary_libs_and_code.rs | 4 +- src/node/bin/print.rs | 2 +- src/vm/src/executor/dictionary.rs | 64 ++++++++----------- src/vm/src/executor/engine/core.rs | 15 +++-- src/vm/tests/test_config.rs | 10 +-- src/vm/tests/test_library.rs | 24 +++---- 17 files changed, 94 insertions(+), 110 deletions(-) diff --git a/src/assembler/src/complex.rs b/src/assembler/src/complex.rs index 023e0c2..6cbedf7 100644 --- a/src/assembler/src/complex.rs +++ b/src/assembler/src/complex.rs @@ -265,7 +265,7 @@ fn build_code_dict( } else { let value_cell = value_slice.clone().into_cell()?; info.append(&mut value_dbg); - dict.setref(key_slice.clone(), &value_cell) + dict.setref(key_slice.clone(), value_cell) .map_err(|e| OperationError::CodeDictConstruction(e.to_string()))?; } } diff --git a/src/block/src/accounts.rs b/src/block/src/accounts.rs index 01f7ad3..ef98957 100644 --- a/src/block/src/accounts.rs +++ b/src/block/src/accounts.rs @@ -1239,7 +1239,7 @@ impl Account { let config_cell = data .checked_drain_reference() .map_err(|_| error!("config SMC data doesn't contain reference with config"))?; - Ok(ConfigParams::with_root(config_cell)) + ConfigParams::with_root(config_cell) } } diff --git a/src/block/src/blocks.rs b/src/block/src/blocks.rs index e27ae57..b3b1f6e 100644 --- a/src/block/src/blocks.rs +++ b/src/block/src/blocks.rs @@ -1464,7 +1464,7 @@ impl TopBlockDescrSet { pub fn insert(&mut self, shard: &ShardIdent, descr: &TopBlockDescr) -> Result<()> { let key = shard.full_key_with_tag()?; let value = descr.serialize()?; - self.collection.0.setref(key, &value)?; + self.collection.0.setref(key, value)?; Ok(()) } pub fn is_empty(&self) -> bool { diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index 48edb70..24c3ac6 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -60,10 +60,6 @@ impl ConfigParams { Ok(Self { config_addr, config_params }) } - pub const fn with_address_and_root(config_addr: AccountId, data: Cell) -> Self { - Self { config_addr, config_params: HashmapE::with_hashmap(32, Some(data)) } - } - pub const fn with_address_and_params(config_addr: AccountId, data: Option) -> Self { Self { config_addr, config_params: HashmapE::with_hashmap(32, data) } } @@ -571,10 +567,11 @@ impl ConfigParams { } impl Deserializable for ConfigParams { - fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { - self.config_addr.read_from(cell)?; - *self.config_params.data_mut() = Some(cell.checked_drain_reference()?); - Ok(()) + fn construct_from(slice: &mut SliceData) -> Result { + let config_addr = slice.get_next_slice(256)?; + let data = slice.checked_drain_reference()?; + let config_params = HashmapE::with_hashmap(32, Some(data)); + Ok(Self { config_addr, config_params }) } } diff --git a/src/block/src/dictionary/hashmap.rs b/src/block/src/dictionary/hashmap.rs index e6d2d20..e2b01c4 100644 --- a/src/block/src/dictionary/hashmap.rs +++ b/src/block/src/dictionary/hashmap.rs @@ -136,13 +136,13 @@ impl HashmapE { self.hashmap_set_with_mode(key, value, gas_consumer, ADD) } /// sets value as reference - pub fn setref(&mut self, key: SliceData, value: &Cell) -> Leaf { + pub fn setref(&mut self, key: SliceData, value: Cell) -> Leaf { self.hashmap_setref_with_mode(key, value, &mut 0, ADD | REPLACE) } pub fn setref_with_gas( &mut self, key: SliceData, - value: &Cell, + value: Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, ADD | REPLACE) @@ -150,7 +150,7 @@ impl HashmapE { pub fn replaceref_with_gas( &mut self, key: SliceData, - value: &Cell, + value: Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, REPLACE) @@ -158,7 +158,7 @@ impl HashmapE { pub fn addref_with_gas( &mut self, key: SliceData, - value: &Cell, + value: Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, ADD) @@ -478,7 +478,7 @@ macro_rules! define_HashmapE { let value = value.write_to_new_cell()?; self.0.set_builder(key, &value) } - pub fn setref(&mut self, key: &K, value: &Cell) -> Result<()> { + pub fn setref(&mut self, key: &K, value: Cell) -> Result<()> { let key = key.write_to_bitstring()?; self.0.setref(key, value)?; Ok(()) diff --git a/src/block/src/dictionary/mod.rs b/src/block/src/dictionary/mod.rs index be312e4..4ba15c3 100644 --- a/src/block/src/dictionary/mod.rs +++ b/src/block/src/dictionary/mod.rs @@ -558,12 +558,12 @@ pub trait HashmapType { fn hashmap_setref_with_mode( &mut self, key: SliceData, - value: &Cell, + value: Cell, gas_consumer: &mut dyn GasConsumer, mode: u8, ) -> Leaf { let mut builder = BuilderData::default(); - builder.checked_append_reference(value.clone())?; + builder.checked_append_reference(value)?; self.hashmap_set_with_mode(key, &builder, gas_consumer, mode) } diff --git a/src/block/src/dictionary/pfxhashmap.rs b/src/block/src/dictionary/pfxhashmap.rs index 8436b35..8589565 100644 --- a/src/block/src/dictionary/pfxhashmap.rs +++ b/src/block/src/dictionary/pfxhashmap.rs @@ -86,13 +86,13 @@ impl PfxHashmapE { self.hashmap_set_with_mode(key, value, gas_consumer, REPLACE) } /// sets value as reference in empty SliceData - pub fn setref(&mut self, key: SliceData, value: &Cell) -> Leaf { + pub fn setref(&mut self, key: SliceData, value: Cell) -> Leaf { self.hashmap_setref_with_mode(key, value, &mut 0, ADD | REPLACE) } pub fn setref_with_gas( &mut self, key: SliceData, - value: &Cell, + value: Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, ADD | REPLACE) @@ -100,7 +100,7 @@ impl PfxHashmapE { pub fn replaceref_with_gas( &mut self, key: SliceData, - value: &Cell, + value: Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, REPLACE) diff --git a/src/block/src/dictionary/tests/test_hashmap.rs b/src/block/src/dictionary/tests/test_hashmap.rs index e16a362..87ac963 100644 --- a/src/block/src/dictionary/tests/test_hashmap.rs +++ b/src/block/src/dictionary/tests/test_hashmap.rs @@ -35,7 +35,7 @@ fn setref_and_get() { assert_eq!( tree.setref( SliceData::from_raw(vec![0b11111111], 8), - &BuilderData::with_raw(vec![0b11111111], 8).unwrap().into_cell().unwrap() + BuilderData::with_raw(vec![0b11111111], 8).unwrap().into_cell().unwrap() ) .unwrap(), None @@ -464,14 +464,14 @@ fn test_dictionary_of_dictionaries() { let mut root = HashmapE::with_bit_len(3); let key1 = SliceData::from_raw(vec![0xFF], 3); - assert_eq!(root.setref(key1.clone(), tree1.data().unwrap()).unwrap(), None); + assert_eq!(root.setref(key1.clone(), tree1.data().unwrap().clone()).unwrap(), None); assert_eq!( root.get(key1.clone()).unwrap().unwrap().reference(0).as_ref().unwrap(), tree1.data().unwrap() ); let key2 = SliceData::from_raw(vec![0xC0], 3); - assert_eq!(root.setref(key2.clone(), tree2.data().unwrap()).unwrap(), None); + assert_eq!(root.setref(key2.clone(), tree2.data().unwrap().clone()).unwrap(), None); assert_eq!( root.get(key2.clone()).unwrap().unwrap().reference(0).as_ref().unwrap(), tree2.data().unwrap() diff --git a/src/block/src/transactions.rs b/src/block/src/transactions.rs index f200424..7937c77 100644 --- a/src/block/src/transactions.rs +++ b/src/block/src/transactions.rs @@ -1414,7 +1414,7 @@ impl Transaction { /// add output message to Hashmap pub fn add_out_message(&mut self, msg: &Message) -> Result<()> { - self.out_msgs.setref(&UInt15(self.outmsg_cnt), &msg.serialize()?)?; + self.out_msgs.setref(&UInt15(self.outmsg_cnt), msg.serialize()?)?; self.outmsg_cnt += 1; Ok(()) } diff --git a/src/emulator/src/lib.rs b/src/emulator/src/lib.rs index 40f32e2..b8d8fdc 100644 --- a/src/emulator/src/lib.rs +++ b/src/emulator/src/lib.rs @@ -63,9 +63,8 @@ pub extern "C" fn transaction_emulator_create( vm_log_verbosity: u32, ) -> *mut c_void { init_log_without_config(None, log_level_from_verbosity(vm_log_verbosity), None); - match deserialize_boc(config_params_boc) { - Ok(config_params_root) => { - let config_params = ConfigParams::with_root(config_params_root); + match deserialize_boc(config_params_boc).and_then(ConfigParams::with_root) { + Ok(config_params) => { let emulator = Box::new(Emulator::new(config_params)); Box::into_raw(emulator) as *mut c_void } @@ -176,9 +175,8 @@ pub extern "C" fn transaction_emulator_set_config( log::error!("Received null pointer for transaction_emulator"); return; } - match deserialize_boc(config_params_boc) { - Ok(config_params_root) => { - let config_params = ConfigParams::with_root(config_params_root); + match deserialize_boc(config_params_boc).and_then(ConfigParams::with_root) { + Ok(config_params) => { let transaction_emulator = unsafe { &mut *(transaction_emulator as *mut Emulator) }; transaction_emulator.config_params = config_params; } diff --git a/src/executor/src/tests/common/mod.rs b/src/executor/src/tests/common/mod.rs index 33ca559..d424dee 100644 --- a/src/executor/src/tests/common/mod.rs +++ b/src/executor/src/tests/common/mod.rs @@ -18,6 +18,8 @@ use crate::{ BlockchainConfig, ExecuteParams, ExecutorError, OrdinaryTransactionExecutor, TickTockTransactionExecutor, TransactionExecutor, }; +#[cfg(feature = "cross_check")] +use std::sync::Arc; use std::sync::LazyLock; use ton_assembler::compile_code_to_cell; use ton_block::{ @@ -122,30 +124,25 @@ pub fn default_config() -> BlockchainConfig { BlockchainConfig::with_config(create_config("real_boc/default_config.boc").unwrap()).unwrap() } -#[cfg(feature = "cross_check")] +#[cfg(not(feature = "cross_check"))] pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { - let debug = false; - // let _ = cross_check::DisableCrossCheck::new(); - // init_log_without_config(None, log::LevelFilter::Debug, None); - // cross_check::set_cross_check_verbosity(if debug { 2048 + 4 } else { 4 }); ExecuteParams { block_unixtime: BLOCK_UT, block_lt: last_tr_lt - last_tr_lt % 1_000_000, last_tr_lt, - debug, ..ExecuteParams::default() } } -#[cfg(not(feature = "cross_check"))] +#[cfg(feature = "cross_check")] pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { - // let _ = cross_check::DisableCrossCheck::new(); enum DebugType { None, Simple, Emulator, } - let debug = DebugType::None; + let debug = DebugType::Emulator; + let _ = cross_check::DisableCrossCheck::new(); let (verbosity, pattern, trace_callback) = match debug { DebugType::None => (4, None, None), DebugType::Simple => (2048 + 4, Some("{m}"), None), @@ -959,26 +956,27 @@ pub fn replay_transaction( pub fn read_config(cfg: &str) -> Result { println!("prepare to read config"); let config = if let Ok(data) = base64_decode(cfg) { - println!("config read as base64"); let data = read_single_root_boc(data).unwrap(); if let Ok(config) = ConfigParams::construct_from_cell(data.clone()) { + println!("config params read as base64"); config } else { - ConfigParams::with_root(data) + println!("config hashmap read as base64"); + ConfigParams::with_root(data)? } } else if let Ok(config) = create_config(cfg) { - println!("config read from file {cfg}"); + println!("config read from file boc {cfg}"); config // let config = ton_block_json::debug_possible_config_params(&config).unwrap(); // std::fs::write("d:\\config.json", config).unwrap(); } else if let Ok(data) = read_single_root_boc(std::fs::read(cfg).unwrap()) { - println!("config read from file as hashmap"); - ConfigParams::with_root(data) + println!("config hashmap read from boc"); + ConfigParams::with_root(data)? } else { println!("config read from file as json"); let json: serde_json::Map = serde_json::from_str(&std::fs::read_to_string(cfg).unwrap()).unwrap(); - ton_block_json::parse_config(json.get("config").unwrap().as_object().unwrap()).unwrap() + ton_block_json::parse_config(json.get("config").unwrap().as_object().unwrap())? // let cfg = cfg.replace("json", "boc"); // config.write_to_file(&cfg).unwrap(); }; diff --git a/src/executor/src/tests/test_ordinary_libs_and_code.rs b/src/executor/src/tests/test_ordinary_libs_and_code.rs index 3d0bb8b..653ba63 100644 --- a/src/executor/src/tests/test_ordinary_libs_and_code.rs +++ b/src/executor/src/tests/test_ordinary_libs_and_code.rs @@ -192,7 +192,7 @@ fn set_library_test() { fn set_ext_library_test() { // library code and data let mut state_lib = HashmapE::with_bit_len(256); - state_lib.setref(LIBRARY_CELL.repr_hash().into(), &LIBRARY_CELL).unwrap(); + state_lib.setref(LIBRARY_CELL.repr_hash().into(), LIBRARY_CELL.clone()).unwrap(); let code = format!( " @@ -357,7 +357,7 @@ fn test_library_cell_code() { .unwrap(); let mut library = ton_block::HashmapE::with_bit_len(256); let key = my_code.repr_hash().write_to_bitstring().unwrap(); - library.setref(key.clone(), &my_code).unwrap(); + library.setref(key, my_code.clone()).unwrap(); let code = my_code.as_library_cell(); let data = Cell::default(); diff --git a/src/node/bin/print.rs b/src/node/bin/print.rs index 5ef39ae..3f87272 100644 --- a/src/node/bin/print.rs +++ b/src/node/bin/print.rs @@ -165,7 +165,7 @@ async fn main() -> Result<()> { print_state(&state, brief)?; } else if let Ok(account) = Account::construct_from_cell(res.roots[0].clone()) { if let Some(data) = account.data().and_then(|data| data.reference(0).ok()) { - let config_params = ConfigParams::with_root(data); + let config_params = ConfigParams::with_root(data)?; let mut json = Default::default(); let mode = ton_block_json::SerializationMode::Debug; if ton_block_json::serialize_config(&mut json, &config_params, mode).is_ok() { diff --git a/src/vm/src/executor/dictionary.rs b/src/vm/src/executor/dictionary.rs index 0e093c9..bd045d2 100644 --- a/src/vm/src/executor/dictionary.rs +++ b/src/vm/src/executor/dictionary.rs @@ -25,7 +25,7 @@ use ton_block::{ fn try_unref_leaf(slice: SliceData) -> Result { match slice.remaining_bits() == 0 && slice.remaining_references() != 0 { - true => Ok(StackItem::Cell(slice.reference(0)?)), + true => slice.reference(0).map(StackItem::Cell), false => fail!(ExceptionCode::DictionaryError), } } @@ -85,37 +85,27 @@ fn dict( let nbits = engine.cmd.var(0).as_integer_value(0..=1023)?; let mut dict = HashmapE::with_hashmap(nbits, engine.cmd.var(1).as_dict()?.cloned()); let key = keyreader(engine.cmd.var(2), nbits)?; - if key.is_empty_bitstring() { - if how.any(SET | DEL) { - fail!(ExceptionCode::RangeCheckError, "key cannot be empty for set or delete") - } else { - if how.bit(RET) { - engine.cc.stack.push(boolean!(false)); - } - Ok(()) - } - } else { + if !key.is_empty_bitstring() { let val = handler(engine, &mut dict, key)?; if how.any(SET | DEL) { engine.cc.stack.push(StackItem::dict(dict.data())); } - match val { - None => { - if how.bit(RET) { - engine.cc.stack.push(boolean!(ret)); - } + if let Some(val) = val { + if how.bit(GET) { + engine.cc.stack.push(val); } - Some(val) => { - if how.bit(GET) { - engine.cc.stack.push(val); - } - if how.bit(RET) { - engine.cc.stack.push(boolean!(!ret)); - } + if how.bit(RET) { + engine.cc.stack.push(boolean!(!ret)); } - }; - Ok(()) + } else if how.bit(RET) { + engine.cc.stack.push(boolean!(ret)); + } + } else if how.any(SET | DEL) { + fail!(ExceptionCode::RangeCheckError, "key cannot be empty for set or delete") + } else if how.bit(RET) { + engine.cc.stack.push(boolean!(false)); } + Ok(()) } // (key slice nbits - ) @@ -129,19 +119,17 @@ fn dictcont(engine: &mut Engine, name: &'static str, keyreader: KeyReader, how: engine.cmd.vars.push(StackItem::continuation(ContinuationData::with_code(data))); let n = engine.cmd.var_count() - 1; if how.bit(SWITCH) { - switch(engine, var!(n)) + switch(engine, var!(n))?; } else if how.bit(CALLX) { - callx(engine, n, false) + callx(engine, n, false)?; } else { fail!("dictcont: {:X}", how) } } else if how.bit(STAY) { let var = engine.cmd.vars.remove(2); engine.cc.stack.push(var); - Ok(()) - } else { - Ok(()) } + Ok(()) } // (key slice nbits - (value' key' -1) | (0)) @@ -393,8 +381,8 @@ fn valwriter_add_ref( dict: &mut HashmapE, key: SliceData, ) -> Result> { - let new_val = engine.cmd.var(3).as_cell()?.clone(); - match convert_dict_error(dict.addref_with_gas(key, &new_val, engine))? { + let value = engine.cmd.var(3).as_cell()?.clone(); + match convert_dict_error(dict.addref_with_gas(key, value, engine))? { Some(val) => Ok(Some(try_unref_leaf(val)?)), None => Ok(None), } @@ -409,7 +397,7 @@ fn valwriter_add_ref_without_unref( match convert_dict_error(dict.get_with_gas(key.clone(), engine))? { Some(val) => Ok(Some(StackItem::Slice(val))), None => { - convert_dict_error(dict.setref_with_gas(key, &new_val, engine))?; + convert_dict_error(dict.setref_with_gas(key, new_val, engine))?; Ok(None) } } @@ -421,7 +409,7 @@ fn valwriter_add_or_remove_refopt( key: SliceData, ) -> Result> { let old_value = match engine.cmd.var(3).as_dict()? { - Some(new_val) => convert_dict_error(dict.setref_with_gas(key, &new_val.clone(), engine))?, + Some(new_val) => convert_dict_error(dict.setref_with_gas(key, new_val.clone(), engine))?, None => convert_dict_error(dict.remove_with_gas(key, engine))?, }; old_value.map(try_unref_leaf).or(Some(Ok(StackItem::None))).transpose() @@ -472,8 +460,8 @@ fn valwriter_replace_ref( dict: &mut HashmapE, key: SliceData, ) -> Result> { - let val = engine.cmd.var(3).as_cell()?.clone(); - match convert_dict_error(dict.replaceref_with_gas(key, &val, engine))? { + let value = engine.cmd.var(3).as_cell()?.clone(); + match convert_dict_error(dict.replaceref_with_gas(key, value, engine))? { Some(val) => Some(try_unref_leaf(val)).transpose(), None => Ok(None), } @@ -503,8 +491,8 @@ fn valwriter_to_ref( dict: &mut HashmapE, key: SliceData, ) -> Result> { - let val = engine.cmd.var(3).as_cell()?.clone(); - convert_dict_error(dict.setref_with_gas(key, &val, engine))?.map(try_unref_leaf).transpose() + let value = engine.cmd.var(3).as_cell()?.clone(); + convert_dict_error(dict.setref_with_gas(key, value, engine))?.map(try_unref_leaf).transpose() } const PREV: u8 = 0x00; diff --git a/src/vm/src/executor/engine/core.rs b/src/vm/src/executor/engine/core.rs index 0d10624..239bb01 100644 --- a/src/vm/src/executor/engine/core.rs +++ b/src/vm/src/executor/engine/core.rs @@ -68,7 +68,7 @@ impl From<&SliceData> for SliceProto { } } -pub type TraceCallback = dyn Fn(&Engine, &EngineTraceInfo) + Send + Sync; +pub type TraceCallback = dyn Fn(&Engine, &EngineTraceInfo) + Send + Sync + 'static; #[derive(Debug)] pub struct RunChildVm { @@ -179,7 +179,7 @@ impl Engine { let trace = if cfg!(feature = "verbose") { Engine::TRACE_ALL } else if cfg!(feature = "fift_check") { - Engine::TRACE_CODE | Engine::TRACE_GAS + Engine::TRACE_ALL_BUT_CTRLS } else { Engine::TRACE_NONE }; @@ -450,10 +450,8 @@ impl Engine { pub fn emulator_trace_callback(&self, info: &EngineTraceInfo) { if info.has_cmd() { if self.trace_bit(Engine::TRACE_CODE) { - if let Ok(code_cell) = info.cmd_code.cell().as_ref() { - log::info!(target: "executor", "code cell hash: {:X} offset: {}\n", - code_cell.repr_hash(), info.cmd_code.pos()); - } + log::info!(target: "executor", "code cell hash: {:X} offset: {}\n", + info.cmd_code.cell().unwrap().repr_hash(), info.cmd_code.pos()); log::info!(target: "executor", "{}\n", info.cmd_str); } if self.trace_bit(Engine::TRACE_STACK) { @@ -462,6 +460,11 @@ impl Engine { if self.trace_bit(Engine::TRACE_GAS) { log::info!(target: "executor", "gas - {}\n", info.gas_used); } + // log::info!(target: "executor", "code cell hash: {:X} offset: {}\n", + // info.cmd_code.cell().unwrap().repr_hash(), info.cmd_code.pos()); + // log::info!(target: "executor", "{}\n", info.cmd_str); + // log::info!(target: "executor", " [ {} ] \n", self.get_stack_result_fift()); + // log::info!(target: "executor", "gas - {}\n", info.gas_used); } } diff --git a/src/vm/tests/test_config.rs b/src/vm/tests/test_config.rs index 78771dd..711f86f 100644 --- a/src/vm/tests/test_config.rs +++ b/src/vm/tests/test_config.rs @@ -218,7 +218,7 @@ mod getparam { let library_cell_code = code.as_library_cell(); let mut library = ton_block::HashmapE::with_bit_len(256); let key = code.repr_hash().write_to_bitstring().unwrap(); - library.setref(key, &code).unwrap(); + library.setref(key, code.clone()).unwrap(); // simple case test_case_with_bytecode(code) .with_account(SHARD_ACCOUNT.clone()) @@ -246,13 +246,13 @@ mod root { params .setref( SliceData::from_raw(2000i32.to_be_bytes().to_vec(), 32), - &SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), + SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), ) .unwrap(); params .setref( SliceData::from_raw((-1i32).to_be_bytes().to_vec(), 32), - &SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), + SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), ) .unwrap(); test_case_with_c7("CONFIGROOT").expect_item(StackItem::dict(params.data())); @@ -300,13 +300,13 @@ mod dict { params .setref( SliceData::from_raw(2000i32.to_be_bytes().to_vec(), 32), - &SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), + SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), ) .unwrap(); params .setref( SliceData::from_raw((-1i32).to_be_bytes().to_vec(), 32), - &SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), + SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), ) .unwrap(); test_case_with_c7("CONFIGDICT").expect_stack( diff --git a/src/vm/tests/test_library.rs b/src/vm/tests/test_library.rs index 8b50536..3f0c2b7 100644 --- a/src/vm/tests/test_library.rs +++ b/src/vm/tests/test_library.rs @@ -37,7 +37,7 @@ fn test_use_library_normal_load_cell_from_ref() { code_use_lib.set_type(CellType::LibraryReference); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case_with_ref( " @@ -65,7 +65,7 @@ fn test_use_library_normal_compose_cell() { ); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case( " @@ -96,7 +96,7 @@ fn test_use_library_normal_jmpref() { code_use_lib.set_type(CellType::LibraryReference); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case_with_ref( " @@ -118,7 +118,7 @@ fn test_use_library_with_wrong_cell_hash() { code_use_lib.set_type(CellType::LibraryReference); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case_with_ref( " @@ -151,8 +151,8 @@ fn test_use_library_with_cell_type_error() { code_use_lib.set_type(CellType::MerkleProof); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash1.into(), &lib_code1).unwrap(); - lib.setref(hash2.into(), &lib_code2).unwrap(); + lib.setref(hash1.into(), lib_code1.clone()).unwrap(); + lib.setref(hash2.into(), lib_code2.clone()).unwrap(); test_case_with_ref( " @@ -237,7 +237,7 @@ fn test_incorrect_library() { assert_ne!(hash, lib_code.repr_hash()); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case_with_ref( " @@ -698,7 +698,7 @@ fn test_compose_exotic_cell_and_load_as_cell() { let hash = lib_code.repr_hash(); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case_with_ref( " @@ -727,7 +727,7 @@ fn test_code_as_exotic_cell() { let lib_code = BuilderData::with_raw(vec![0x72], 8).unwrap().into_cell().unwrap(); let hash = lib_code.repr_hash(); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); let code = lib_code.as_library_cell(); // normal case with code as library cell @@ -739,7 +739,7 @@ fn test_code_as_exotic_cell() { let lib_code = code; let hash = lib_code.repr_hash(); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); let code = lib_code.as_library_cell(); // code as library cell with recursive library cell @@ -766,7 +766,7 @@ fn test_code_as_exotic_cell() { let hash = lib_code.repr_hash(); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); let code = lib_code.as_library_cell(); @@ -780,7 +780,7 @@ fn test_compose_exotic_cell_and_load_quite_as_cell() { let hash = lib_code.repr_hash(); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), &lib_code).unwrap(); + lib.setref(hash.into(), lib_code.clone()).unwrap(); test_case_with_ref( " From 5e5471ad30f17e1421348e8b58e156eba7cf0e9a Mon Sep 17 00:00:00 2001 From: Slava Date: Wed, 1 Apr 2026 09:25:13 +0300 Subject: [PATCH 15/48] Latest simplex changes --- src/adnl/src/adnl/transport.rs | 4 +- src/adnl/src/overlay/broadcast.rs | 74 +- src/adnl/src/overlay/mod.rs | 9 +- src/adnl/src/quic/mod.rs | 585 ++++--- src/adnl/src/rldp/mod.rs | 31 +- src/adnl/src/rldp/send.rs | 2 +- src/adnl/tests/test_overlay.rs | 4 +- src/adnl/tests/test_quic.rs | 261 ++- src/block-json/src/deserialize.rs | 39 +- src/block-json/src/serialize.rs | 33 +- src/block-json/src/tests/test_deserialize.rs | 95 +- src/block/src/config_params.rs | 209 ++- src/block/src/tests/test_config_params.rs | 197 ++- src/ci/sync-test/README.md | 224 +++ src/ci/sync-test/gen-node-config.sh | 47 + src/ci/sync-test/values.yaml | 56 + src/ci/sync-test/watcher.sh | 157 ++ src/node/catchain/src/receiver.rs | 6 +- src/node/consensus-common/src/adnl_overlay.rs | 424 +++-- .../src/dummy_catchain_overlay.rs | 1 + .../src/in_process_overlay.rs | 1 + src/node/consensus-common/src/lib.rs | 15 +- src/node/consensus-common/src/log_player.rs | 1 + .../consensus-common/src/node_test_network.rs | 6 +- .../tests/test_adnl_overlay.rs | 2 + .../tests/test_in_process_overlay.rs | 4 +- src/node/simplex/src/lib.rs | 98 +- src/node/simplex/src/receiver.rs | 34 +- src/node/simplex/src/session.rs | 106 +- src/node/simplex/src/session_processor.rs | 124 +- src/node/simplex/src/simplex_state.rs | 91 +- src/node/simplex/src/tests/test_receiver.rs | 162 ++ .../src/tests/test_session_processor.rs | 496 ++++++ .../simplex/src/tests/test_simplex_state.rs | 534 ++++++ src/node/simplex/tests/test_collation.rs | 2 +- src/node/simplex/tests/test_consensus.rs | 192 +- src/node/simplex/tests/test_restart.rs | 4 +- src/node/simplex/tests/test_validation.rs | 5 +- src/node/src/config.rs | 79 +- src/node/src/engine.rs | 22 +- src/node/src/engine_operations.rs | 110 +- src/node/src/engine_traits.rs | 66 +- src/node/src/internal_db/mod.rs | 16 + src/node/src/internal_db/restore.rs | 5 +- src/node/src/network/catchain_client.rs | 1 + src/node/src/network/control.rs | 1 + src/node/src/network/full_node_overlays.rs | 11 +- src/node/src/network/node_network.rs | 223 ++- .../tests/test_node_network_validator_list.rs | 121 ++ src/node/src/tests/test_engine_operations.rs | 28 + src/node/src/tests/test_internal_db.rs | 33 + src/node/src/validator/collator.rs | 2 +- src/node/src/validator/consensus.rs | 67 +- .../src/validator/tests/test_session_id.rs | 391 ++++- src/node/src/validator/validator_group.rs | 576 +++++- src/node/src/validator/validator_manager.rs | 1555 +++++++++++++---- .../validator/validator_session_listener.rs | 29 +- src/node/storage/src/block_handle_db.rs | 14 + .../tests/compat_test/src/test_helpers.rs | 8 +- .../tests/test_twostep_fec_relay.rs | 4 +- src/node/tests/test_sync/.dockerignore | 10 - src/node/tests/test_sync/.gitignore | 12 - src/node/tests/test_sync/Dockerfile | 89 - src/node/tests/test_sync/README.md | 118 -- src/node/tests/test_sync/package.json | 15 - src/node/tests/test_sync/server.js | 1353 -------------- src/node/validator-session/src/session.rs | 5 + src/tl/ton_api/tl/ton_api.tl | 10 +- 68 files changed, 6657 insertions(+), 2652 deletions(-) create mode 100644 src/ci/sync-test/README.md create mode 100644 src/ci/sync-test/gen-node-config.sh create mode 100644 src/ci/sync-test/values.yaml create mode 100644 src/ci/sync-test/watcher.sh create mode 100644 src/node/src/network/tests/test_node_network_validator_list.rs create mode 100644 src/node/src/tests/test_engine_operations.rs delete mode 100644 src/node/tests/test_sync/.dockerignore delete mode 100644 src/node/tests/test_sync/.gitignore delete mode 100644 src/node/tests/test_sync/Dockerfile delete mode 100644 src/node/tests/test_sync/README.md delete mode 100644 src/node/tests/test_sync/package.json delete mode 100644 src/node/tests/test_sync/server.js diff --git a/src/adnl/src/adnl/transport.rs b/src/adnl/src/adnl/transport.rs index 50737d2..d8cd3bf 100644 --- a/src/adnl/src/adnl/transport.rs +++ b/src/adnl/src/adnl/transport.rs @@ -177,8 +177,8 @@ impl SendQueue { true } - pub(crate) fn is_inactive(&self) -> bool { - self.sync.load(Ordering::Relaxed) == Self::SYNC_INACTIVE + pub(crate) fn is_empty(&self) -> bool { + self.len.load(Ordering::Relaxed) == 0 } fn switch(&self, from: u8, to: u8) -> bool { diff --git a/src/adnl/src/overlay/broadcast.rs b/src/adnl/src/overlay/broadcast.rs index db622fb..a8210ec 100644 --- a/src/adnl/src/overlay/broadcast.rs +++ b/src/adnl/src/overlay/broadcast.rs @@ -94,6 +94,7 @@ impl BroadcastNeighbours { pub struct BroadcastRecvInfo { pub packets: u32, pub data: Vec, + pub extra: Option>, pub recv_from: Arc, } @@ -212,6 +213,7 @@ declare_counted!( data_hash: [u8; 32], date: i32, encoder: RaptorqEncoder, + extra: Vec, flags: u32, seqno: u32, src_key: Arc, @@ -528,6 +530,9 @@ pub(crate) trait BroadcastProtocol: pub(crate) trait FecBroadcastParsed: BroadcastParsed { fn data_hash(&self) -> &[u8; 32]; fn data_size(&self) -> usize; + fn extra(&self) -> Option<&[u8]> { + None + } fn fec_type(&self) -> Option; fn part_data(&self) -> &[u8]; fn seqno(&self) -> u32; @@ -569,6 +574,7 @@ trait FecProtocol: BroadcastProtocol tokio::spawn(async move { let mut received = false; let mut packets = 0; + let mut extra: Option> = None; #[cfg(feature = "telemetry")] let mut flags = RecvTransferFecTelemetry::FLAG_RECEIVE_STARTED; #[cfg(feature = "telemetry")] @@ -579,6 +585,9 @@ trait FecProtocol: BroadcastProtocol Some(bcast) => bcast, None => break, }; + if extra.is_none() { + extra = bcast.extra().map(|extra| extra.to_vec()); + } packets += 1; let Some(fec_type) = bcast.fec_type() else { log::warn!( @@ -622,6 +631,7 @@ trait FecProtocol: BroadcastProtocol overlay.received_rawbytes.push(BroadcastRecvInfo { packets, data, + extra, recv_from: src_key_id.clone(), }); received = true; @@ -929,10 +939,12 @@ trait BroadcastTwostep { fn calc_broadcast_id( data_hash: [u8; 32], date: i32, + data_size: usize, part_size: usize, src_key: &Arc, src_adnl_key_id: &Arc, flags: u32, + extra: &[u8], ) -> Result { let bcast_id = BroadcastTwostepId { date, @@ -940,7 +952,9 @@ trait BroadcastTwostep { src: UInt256::from_slice(src_key.id().data()), src_adnl_id: UInt256::from_slice(src_adnl_key_id.data()), data_hash: UInt256::with_array(data_hash), + data_size: data_size as i32, part_size: part_size as i32, + extra: extra.to_vec(), }; hash(bcast_id) } @@ -948,15 +962,19 @@ trait BroadcastTwostep { fn calc_broadcast_id_when_send( ctx: &BroadcastSendContext, date: i32, + data_size: usize, part_size: usize, + extra: &[u8], ) -> Result<(BroadcastId, bool)> { let bcast_id = Self::calc_broadcast_id( sha256_digest(ctx.data.object), date, + data_size, part_size, &ctx.src_key, &ctx.src_adnl_key_id, ctx.flags, + extra, )?; Ok((bcast_id, true)) } @@ -1157,6 +1175,7 @@ impl BroadcastProtocol for BroadcastFecProtocol { data_hash: sha256_digest(ctx.data.object), date, encoder, + extra: Vec::new(), flags: ctx.flags, seqno: 0, src_key: ctx.src_key.clone(), @@ -1394,6 +1413,7 @@ impl BroadcastSimpleProtocol { let info = BroadcastRecvInfo { packets: 1, data: bcast.data.into(), + extra: None, recv_from: src_key.id().clone(), }; Ok((Some(info), true)) @@ -1634,6 +1654,7 @@ impl FecBroadcastParsed for BroadcastTwostepFec { symbol_size, }) } + fn extra(&self) -> Option<&[u8]> { Some(&self.extra) } fn part_data(&self) -> &[u8] { &self.part } fn seqno(&self) -> u32 { self.seqno as u32 } fn signature(&self) -> &[u8] { &self.signature } @@ -1645,6 +1666,7 @@ struct BroadcastTwostepSendContext { } pub(crate) struct BroadcastTwostepFecProtocol { + extra: Option>, send_ctx: Option, } @@ -1652,10 +1674,10 @@ impl BroadcastTwostepFecProtocol { const MAX_PART_SIZE: usize = 65536; pub(crate) fn for_recv() -> Self { - Self { send_ctx: None } + Self { extra: None, send_ctx: None } } - pub(crate) fn for_send(data: &[u8], neighbours: u32) -> Result { + pub(crate) fn for_send(data: &[u8], neighbours: u32, extra: Vec) -> Result { if neighbours <= 3 { fail!("Not enough neighbours to build {} broadcast", Self::broadcast_type()); } @@ -1668,7 +1690,7 @@ impl BroadcastTwostepFecProtocol { fail!("Too big part size {part_size} in {} broadcast", Self::broadcast_type()); } let ctx = BroadcastTwostepSendContext { neighbours, part_size }; - Ok(Self { send_ctx: Some(ctx) }) + Ok(Self { extra: Some(extra), send_ctx: Some(ctx) }) } } @@ -1716,10 +1738,12 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { bcast_id: ::calc_broadcast_id( *bcast.data_hash.as_slice(), bcast.date, + bcast.data_size as usize, bcast.part.len(), &src_key, &src_adnl_key_id, bcast.flags as u32, + &bcast.extra, )?, dup: false, data_len: bcast.part.len(), @@ -1748,7 +1772,13 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { let Some(send_ctx) = self.send_ctx.as_ref() else { fail!("No send context set for {} broadcast", Self::broadcast_type()); }; - ::calc_broadcast_id_when_send(ctx, date, send_ctx.part_size) + ::calc_broadcast_id_when_send( + ctx, + date, + ctx.data.object.len(), + send_ctx.part_size, + self.extra.as_deref().unwrap_or_default(), + ) } fn build_broadcast( @@ -1786,6 +1816,7 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { data_hash: sha256_digest(ctx.data.object), date, encoder: RaptorqEncoder::with_data(ctx.data.object, Some(send_ctx.part_size as u16)), + extra: self.extra.take().unwrap_or_default(), flags: ctx.flags, seqno: 0, src_key: ctx.src_key.clone(), @@ -1870,6 +1901,7 @@ impl FecProtocol for BroadcastTwostepFecProtocol { data_size: transfer.encoder.params().data_size as i32, seqno: transfer.seqno as i32, part: data, + extra: transfer.extra.clone(), signature, } .into_boxed()) @@ -1877,14 +1909,13 @@ impl FecProtocol for BroadcastTwostepFecProtocol { fn calc_to_sign( bcast_id: &BroadcastId, - data_size: usize, + _data_size: usize, part_data: &[u8], seqno: u32, _date: i32, ) -> Result> { let to_sign = BroadcastTwostepFecToSign { id: UInt256::from_slice(bcast_id), - data_size: data_size as i32, seqno: seqno as i32, part: part_data.to_vec(), }; @@ -1917,11 +1948,15 @@ impl BroadcastParsed for BroadcastTwostepSimple { pub(crate) struct BroadcastTwostepSimpleProtocol { big_data: bool, + extra: Option>, } impl BroadcastTwostepSimpleProtocol { - pub(crate) fn new(big_data: bool) -> Self { - Self { big_data } + pub(crate) fn for_recv(big_data: bool) -> Self { + Self { big_data, extra: None } + } + pub(crate) fn for_send(big_data: bool, extra: Vec) -> Self { + Self { big_data, extra: Some(extra) } } fn calc_to_sign(bcast_id: BroadcastId, data: &[u8]) -> Result> { let to_sign = @@ -1973,15 +2008,18 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco ctx: &BroadcastRecvContext, ) -> Result { let data_hash = sha256_digest(&bcast.data); + let data_size = bcast.data.len(); let src_key: Arc = (&bcast.src).try_into()?; let src_adnl_key_id = KeyId::from_data(*bcast.src_adnl_id.as_slice()); let bcast_id = ::calc_broadcast_id( data_hash, bcast.date, - bcast.data.len(), + data_size, + data_size, &src_key, &src_adnl_key_id, bcast.flags as u32, + &bcast.extra, )?; let dup = if add_unbound_object_to_map(&ctx.overlay.owned_broadcasts, bcast_id, || { Ok(OwnedBroadcast::Send) @@ -2016,8 +2054,12 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco ) -> Result<(Option, bool)> { let src_adnl_key_id = KeyId::from_data(*bcast.src_adnl_id.as_slice()); let resend = ctx.peers.other() == &src_adnl_key_id; - let info = - BroadcastRecvInfo { packets: 1, data: bcast.data.into(), recv_from: src_adnl_key_id }; + let info = BroadcastRecvInfo { + packets: 1, + data: bcast.data.into(), + extra: Some(bcast.extra), + recv_from: src_adnl_key_id, + }; Ok((Some(info), resend)) } @@ -2028,7 +2070,14 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco ctx: &BroadcastSendContext, date: i32, ) -> Result<(BroadcastId, bool)> { - ::calc_broadcast_id_when_send(ctx, date, ctx.data.object.len()) + let data_size = ctx.data.object.len(); + ::calc_broadcast_id_when_send( + ctx, + date, + data_size, + data_size, + self.extra.as_deref().unwrap_or_default(), + ) } fn build_broadcast( @@ -2049,6 +2098,7 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco src_adnl_id: UInt256::from_slice(ctx.src_adnl_key_id.data()), certificate: OverlayCertificate::Overlay_EmptyCertificate, data, + extra: self.extra.take().unwrap_or_default(), signature, } .into_boxed(), diff --git a/src/adnl/src/overlay/mod.rs b/src/adnl/src/overlay/mod.rs index 4d51a11..b4192ab 100644 --- a/src/adnl/src/overlay/mod.rs +++ b/src/adnl/src/overlay/mod.rs @@ -1594,12 +1594,13 @@ impl OverlayNode { } /// Two-step message broadcast - pub async fn broadcast_two_step( + pub async fn broadcast_twostep( &self, overlay_id: &Arc, data: &TaggedByteSlice<'_>, src_key: Option<&Arc>, flags: u32, + extra: Vec, ) -> Result { log::trace!(target: TARGET, "Two-step broadcast {} bytes", data.object.len()); let overlay = @@ -1614,9 +1615,9 @@ impl OverlayNode { let neighbours = overlay.calc_broadcast_twostep_neighbours(); let big_data = data.object.len() >= Self::MIN_BYTES_FEC_TWO_STEPS_BROADCAST; if big_data && (neighbours >= Self::MIN_NODES_FEC_TWO_STEPS_BROADCAST) { - BroadcastTwostepFecProtocol::for_send(data.object, neighbours)?.send(ctx).await + BroadcastTwostepFecProtocol::for_send(data.object, neighbours, extra)?.send(ctx).await } else { - BroadcastTwostepSimpleProtocol::new(big_data).send(ctx).await + BroadcastTwostepSimpleProtocol::for_send(big_data, extra).send(ctx).await } } @@ -2386,7 +2387,7 @@ impl Subscriber for OverlayNode { } Ok(Broadcast::Overlay_BroadcastTwostepSimple(bcast)) => { let big_data = bcast.data.len() >= Self::MIN_BYTES_FEC_TWO_STEPS_BROADCAST; - BroadcastTwostepSimpleProtocol::new(big_data).recv(bcast, ctx).await?; + BroadcastTwostepSimpleProtocol::for_recv(big_data).recv(bcast, ctx).await?; return Ok(true); } Ok(bcast) => fail!("Unsupported overlay broadcast message {:?}", bcast), diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index 44aafe6..98477a2 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -17,7 +17,10 @@ use crate::{ use std::{ fmt, net::SocketAddr, - sync::{Arc, Once}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Once, + }, time::Duration, }; use ton_api::{ @@ -56,6 +59,19 @@ fn key_id_from_spki(spki: &[u8]) -> Result> { struct QuicOutboundConnection { conn: Option, send_queue: Arc, + sender_state: Arc, +} + +/// Per-peer sender lifecycle guard. Uses an atomic flag to ensure exactly +/// one sender task runs per outbound peer. +struct SenderState { + active: AtomicBool, +} + +impl SenderState { + fn new() -> Arc { + Arc::new(Self { active: AtomicBool::new(false) }) + } } /// Presents a single fixed Ed25519 SPKI (RPK) as the client certificate. @@ -333,16 +349,41 @@ pub struct QuicNode { impl QuicNode { pub const OFFSET_PORT: u16 = 1000; - const DEFAULT_QUERY_TIMEOUT_MS: u64 = 5000; /// How often the background checker scans outbound connections for dead ones. const CONNECTION_CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); - const DEFAULT_MAX_STREAMS_PER_CONNECTION: usize = 256; - + const DEFAULT_QUERY_TIMEOUT_MS: u64 = 5000; /// Maximum number of messages buffered per outbound peer const SEND_QUEUE_CAPACITY: usize = 1024; + /// Create a new QuicNode. No endpoints are bound โ€” they are created lazily + /// by `add_key()` when the first identity for a given port is registered. + pub fn new( + subscribers: Vec>, + cancellation_token: tokio_util::sync::CancellationToken, + max_streams_per_connection: Option, + ) -> Arc { + let max_streams_per_connection = + max_streams_per_connection.unwrap_or(Self::DEFAULT_MAX_STREAMS_PER_CONNECTION); + static CRYPTO_INIT: Once = Once::new(); + CRYPTO_INIT.call_once(|| { + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install default Rustls CryptoProvider"); + }); + let transport = Arc::new(Self { + cancellation_token: cancellation_token.clone(), + local_keys: lockfree::map::Map::new(), + endpoints: std::sync::Mutex::new(std::collections::HashMap::new()), + subscribers: Arc::new(subscribers), + peer_keys: lockfree::map::Map::new(), + max_streams_per_connection, + }); + Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token); + transport + } + /// Register a local identity on a specific bind address. /// Creates a new endpoint if one doesn't exist for this port yet. pub fn add_key( @@ -445,51 +486,62 @@ impl QuicNode { ) -> Result> { self.ensure_peer_registered(adnl, src, dst)?; let data = serialize_boxed(&QuicMessage { data: data.into() }.into_boxed())?; - match self.get_outbound_connection(src, dst, true).await? { - QuicOutboundConnection { conn: Some(ref conn), ref send_queue } => { - if send_queue.check(true) { - if !send_queue.try_push(data) { - send_queue.check(false); - fail!("QUIC send queue full for peer {dst}"); - } - while !send_queue.check(false) { - tokio::task::yield_now().await; - } - } else { - let len = data.len(); - if let Err(e) = Self::send_via_stream(conn, &data).await { - log::warn!( - target: TARGET, - "QUIC send_message to {dst} failed: {e}, removing dead connection" - ); - let addr = self.addr_by_key(dst)?; - let state = self.local_key_state(src)?; - Self::remove_dead_connection(&state.outbound, addr, conn); - return Err(e); - } - return Ok(Some(len)); - } - } - QuicOutboundConnection { conn: None, ref send_queue } => { - if !send_queue.try_push(data) { - fail!("QUIC send queue full for peer {dst} (connecting)"); + let addr = self.addr_by_key(dst)?; + let state = self.local_key_state(src)?; + let outbound = Self::get_or_create_outbound_connection(&state.outbound, addr)?; + + // Fast path: if connection is alive, send directly without queue overhead + if let Some(ref conn) = outbound.conn { + match Self::send_via_stream(conn, &data).await { + Ok(_) => return Ok(Some(data.len())), + Err(e) => { + log::warn!( + target: TARGET, + "QUIC direct send to {dst} failed: {e}, removing dead connection, \ + falling back to queue" + ); + Self::remove_dead_connection(&state.outbound, addr, conn); } } } - Ok(None) - } - /// Create a new QuicNode. No endpoints are bound โ€” they are created lazily - /// by `add_key()` when the first identity for a given port is registered. - pub fn new( - subscribers: Vec>, - cancellation_token: tokio_util::sync::CancellationToken, - ) -> Arc { - Self::with_stream_limit( - subscribers, - cancellation_token, - Self::DEFAULT_MAX_STREAMS_PER_CONNECTION, - ) + // Slow path: no connection (or it just died) โ€” enqueue for the sender task + // which will establish the connection and deliver + if !outbound.send_queue.try_push(data) { + fail!("QUIC send queue full for peer {dst}"); + } + + // Spawn sender task if not already running (CAS guarantees at most one per peer) + if outbound + .sender_state + .active + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + let quic = self.clone(); + let src = src.clone(); + let dst = dst.clone(); + let send_queue = outbound.send_queue.clone(); + let sender_state = outbound.sender_state.clone(); + let outbound_conns = state.outbound.clone(); + let server_name = Self::key_id_to_server_name(&dst); + + spawn_cancelable( + self.cancellation_token.clone(), + Self::run_sender_task( + quic, + src, + dst, + addr, + server_name, + send_queue, + sender_state, + outbound_conns, + ), + ); + } + + Ok(None) } pub async fn query( @@ -539,30 +591,6 @@ impl QuicNode { } } - /// Like `new`, but with a custom per-connection stream concurrency limit. - pub fn with_stream_limit( - subscribers: Vec>, - cancellation_token: tokio_util::sync::CancellationToken, - max_streams_per_connection: usize, - ) -> Arc { - static CRYPTO_INIT: Once = Once::new(); - CRYPTO_INIT.call_once(|| { - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install default Rustls CryptoProvider"); - }); - let transport = Arc::new(Self { - cancellation_token: cancellation_token.clone(), - local_keys: lockfree::map::Map::new(), - endpoints: std::sync::Mutex::new(std::collections::HashMap::new()), - subscribers: Arc::new(subscribers), - peer_keys: lockfree::map::Map::new(), - max_streams_per_connection, - }); - Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token); - transport - } - fn addr_by_key(&self, key_id: &Arc) -> Result { match self.peer_keys.get(key_id) { Some(entry) => Ok(*entry.val()), @@ -578,7 +606,13 @@ impl QuicNode { server_name: &str, ) -> Result<()> { let state = self.local_key_state(src)?; - let endpoint = self.endpoint_for_port(state.bound_port)?; + let endpoint = { + let endpoints = self.endpoints.lock().map_err(|e| error!("Endpoints lock: {e}"))?; + endpoints + .get(&state.bound_port) + .map(|s| s.endpoint.clone()) + .ok_or_else(|| error!("No QUIC endpoint for port {}", state.bound_port))? + }; let conn = endpoint .connect_with(state.client_config.clone(), addr, server_name) .map_err(|e| error!("QUIC connect to {addr} (SNI={server_name}): {e}"))? @@ -599,6 +633,7 @@ impl QuicNode { Ok(Some(QuicOutboundConnection { conn: Some(conn.clone()), send_queue: found.send_queue.clone(), + sender_state: found.sender_state.clone(), })) } else { Ok(None) @@ -609,13 +644,25 @@ impl QuicNode { Ok(()) } - /// Get the endpoint for the given port. - fn endpoint_for_port(&self, port: u16) -> Result { - let endpoints = self.endpoints.lock().map_err(|e| error!("Endpoints lock: {e}"))?; - endpoints - .get(&port) - .map(|s| s.endpoint.clone()) - .ok_or_else(|| error!("No QUIC endpoint for port {port}")) + /// Obtain (or create) an outbound connection and connect in the foreground. + /// Used by the query path where a live connection is required synchronously. + async fn ensure_outbound_connection( + self: &Arc, + src: &Arc, + dst: &Arc, + ) -> Result { + let addr = self.addr_by_key(dst)?; + let server_name = Self::key_id_to_server_name(dst); + let state = self.local_key_state(src)?; + loop { + let conn = Self::get_or_create_outbound_connection(&state.outbound, addr)?; + if conn.conn.is_some() { + break Ok(conn); + } + log::info!(target: TARGET, "Try new QUIC connection to {addr} in foreground"); + self.connect(src, dst, addr, &server_name).await?; + log::info!(target: TARGET, "QUIC connected to {addr} in foreground"); + } } fn ensure_peer_registered( @@ -742,9 +789,6 @@ impl QuicNode { match outbound.map().get(&addr) { Some(entry) => { let found = entry.val(); - // Proactive liveness check: if the connection is dead, remove it - // and loop again โ€” the next iteration will see conn: None and - // trigger a reconnect. if let Some(ref c) = found.conn { if c.close_reason().is_some() { log::info!( @@ -760,97 +804,24 @@ impl QuicNode { break Ok(QuicOutboundConnection { conn: found.conn.clone(), send_queue: found.send_queue.clone(), + sender_state: found.sender_state.clone(), }); } None => { let queue = QuicSendQueue::with_capacity(Self::SEND_QUEUE_CAPACITY); + let sender_state = SenderState::new(); add_unbound_object_to_map(outbound.map(), addr, || { - Ok(QuicOutboundConnection { conn: None, send_queue: queue.clone() }) + Ok(QuicOutboundConnection { + conn: None, + send_queue: queue.clone(), + sender_state: sender_state.clone(), + }) })?; } } } } - async fn get_outbound_connection( - self: &Arc, - src: &Arc, - dst: &Arc, - create_async: bool, - ) -> Result { - let addr = self.addr_by_key(dst)?; - let server_name = Self::key_id_to_server_name(dst); - let state = self.local_key_state(src)?; - loop { - let conn = Self::get_or_create_outbound_connection(&state.outbound, addr)?; - if let QuicOutboundConnection { conn: Some(_), .. } = &conn { - break Ok(conn); - } - if create_async { - let queue = conn.send_queue.clone(); - let quic = self.clone(); - let src = src.clone(); - let dst = dst.clone(); - let server_name = server_name.clone(); - spawn_cancelable(self.cancellation_token.clone(), async move { - while !queue.activate(true) { - tokio::task::yield_now().await; - } - loop { - let Some(data) = queue.pop() else { - if queue.activate(false) { - break; - } - tokio::task::yield_now().await; - continue; - }; - loop { - let result = quic.local_key_state(&src).and_then(|s| { - Self::get_or_create_outbound_connection(&s.outbound, addr) - }); - let result = match result { - Ok(QuicOutboundConnection { conn: Some(ref conn), .. }) => { - Self::send_via_stream(conn, &data).await - } - Ok(_) => { - log::info!( - target: TARGET, - "Try new QUIC connection to {addr} in background" - ); - let result = quic.connect(&src, &dst, addr, &server_name).await; - if let Err(e) = result { - Err(error!( - "QUIC background connection to {addr} error: {e}" - )) - } else { - log::info!( - target: TARGET, - "QUIC connected to {addr} in background" - ); - continue; - } - } - Err(e) => Err(e), - }; - if let Err(e) = result { - log::warn!( - target: TARGET, - "QUIC send to {addr} in background error: {e}" - ); - } - break; - } - } - }); - break Ok(conn); - } else { - log::info!(target: TARGET, "Try new QUIC connection to {addr} in foreground"); - self.connect(&src, dst, addr, &server_name).await?; - log::info!(target: TARGET, "QUIC connected to {addr} in foreground"); - } - } - } - async fn handle_connection( incoming: quinn::Incoming, local_key_names: Arc>>, @@ -938,36 +909,76 @@ impl QuicNode { let peers = AdnlPeers::with_keys(local_key_id, peer_key_id); let conn_id = conn.stable_id(); // Limit concurrent in-flight streams per connection to bound memory usage. - // When the semaphore is full, accept_bi() stalls, applying QUIC-level backpressure. + // When the semaphore is full, accept stalls, applying QUIC-level backpressure. let stream_semaphore = Arc::new(tokio::sync::Semaphore::new(max_streams_per_connection)); - loop { - let (send, recv) = match conn.accept_bi().await { - Ok(streams) => streams, - Err(e) => { - log::warn!( - target: TARGET, - "QUIC accept stream from {addr}: {e}" - ); - break; - } - }; - let permit = match stream_semaphore.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => break, - }; - let subscribers = subscribers.clone(); - let peers = peers.clone(); - tokio::spawn(async move { - let _permit = permit; - if let Err(e) = - Self::process_incoming_stream(recv, send, &subscribers, &peers, addr).await - { - log::warn!( - target: TARGET, - "QUIC process stream from {addr}: {e}" - ); - } - }); + + // Accept both bi-directional streams (queries + legacy messages) and + // uni-directional streams (fire-and-forget messages from the new sender). + let conn_bi = conn.clone(); + let conn_uni = conn.clone(); + let sem_bi = stream_semaphore.clone(); + let sem_uni = stream_semaphore; + let subs_bi = subscribers.clone(); + let subs_uni = subscribers; + let peers_bi = peers.clone(); + let peers_uni = peers; + + let bi_loop = async { + loop { + let (send, recv) = match conn_bi.accept_bi().await { + Ok(streams) => streams, + Err(e) => { + log::warn!(target: TARGET, "QUIC accept bi-stream from {addr}: {e}"); + break; + } + }; + let permit = match sem_bi.clone().acquire_owned().await { + Ok(p) => p, + Err(_) => break, + }; + let subscribers = subs_bi.clone(); + let peers = peers_bi.clone(); + tokio::spawn(async move { + let _permit = permit; + if let Err(e) = + Self::process_incoming_stream(recv, send, &subscribers, &peers, addr).await + { + log::warn!(target: TARGET, "QUIC process bi-stream from {addr}: {e}"); + } + }); + } + }; + + let uni_loop = async { + loop { + let recv = match conn_uni.accept_uni().await { + Ok(stream) => stream, + Err(e) => { + log::warn!(target: TARGET, "QUIC accept uni-stream from {addr}: {e}"); + break; + } + }; + let permit = match sem_uni.clone().acquire_owned().await { + Ok(p) => p, + Err(_) => break, + }; + let subscribers = subs_uni.clone(); + let peers = peers_uni.clone(); + tokio::spawn(async move { + let _permit = permit; + if let Err(e) = + Self::process_incoming_uni_stream(recv, &subscribers, &peers, addr).await + { + log::warn!(target: TARGET, "QUIC process uni-stream from {addr}: {e}"); + } + }); + } + }; + + // Run both accept loops; when either exits (connection closed), both stop. + tokio::select! { + () = bi_loop => {} + () = uni_loop => {} } let is_current = inbound.map().get(&addr).map(|e| e.val().stable_id() == conn_id).unwrap_or(false); @@ -1018,24 +1029,42 @@ impl QuicNode { return Ok(()); } }; - log::debug!(target: TARGET, "process_incoming_stream from {addr}: read {} bytes", buf.len()); + log::debug!( + target: TARGET, + "process_incoming_stream from {addr}: read {} bytes", + buf.len() + ); if buf.is_empty() { return Ok(()); } let obj = deserialize_boxed(&buf) .map_err(|e| error!("Cannot deserialize QUIC message from {addr}: {e}"))?; - log::debug!(target: TARGET, "process_incoming_stream from {addr}: deserialized TL, about to downcast"); + log::debug!( + target: TARGET, + "process_incoming_stream from {addr}: deserialized TL, about to downcast" + ); match obj.downcast::() { Ok(Request::Quic_Message(msg)) => { - log::debug!(target: TARGET, "process_incoming_stream from {addr}: QUIC MESSAGE, dispatching to {} subscribers", subscribers.len()); + log::debug!( + target: TARGET, + "process_incoming_stream from {addr}: QUIC MESSAGE, \ + dispatching to {} subscribers", + subscribers.len() + ); for subscriber in subscribers { if subscriber.try_consume_custom(&msg.data, &peers).await? { - log::debug!(target: TARGET, "process_incoming_stream from {addr}: consumed by subscriber"); + log::debug!( + target: TARGET, + "process_incoming_stream from {addr}: consumed by subscriber" + ); break; } } let _ = send.finish(); - log::debug!(target: TARGET, "process_incoming_stream from {addr}: finished send side"); + log::debug!( + target: TARGET, + "process_incoming_stream from {addr}: finished send side" + ); } Ok(Request::Quic_Query(query)) => { log::debug!(target: TARGET, "process_incoming_stream from {addr}: QUIC QUERY"); @@ -1059,7 +1088,63 @@ impl QuicNode { let _ = send.finish(); } Err(_obj) => { - log::warn!(target: TARGET, "Unknown QUIC TL message from {addr}: failed to downcast to Request"); + log::warn!( + target: TARGET, + "Unknown QUIC TL message from {addr}: failed to downcast to Request" + ); + } + } + Ok(()) + } + + /// Process a fire-and-forget message received on a uni-directional QUIC stream. + /// Only `QuicMessage` is expected; queries arriving on uni streams are rejected + /// because there is no send side to write a response to. + async fn process_incoming_uni_stream( + mut recv: quinn::RecvStream, + subscribers: &[Arc], + peers: &AdnlPeers, + addr: SocketAddr, + ) -> Result<()> { + let buf = match tokio::time::timeout( + std::time::Duration::from_secs(5), + recv.read_to_end(16 * 1024 * 1024), + ) + .await + { + Ok(result) => result.map_err(|e| error!("QUIC uni read from {addr}: {e}"))?, + Err(_) => { + log::warn!( + target: TARGET, + "process_incoming_uni_stream from {addr}: read timed out after 5s" + ); + return Ok(()); + } + }; + if buf.is_empty() { + return Ok(()); + } + let obj = deserialize_boxed(&buf) + .map_err(|e| error!("Cannot deserialize QUIC uni-stream from {addr}: {e}"))?; + match obj.downcast::() { + Ok(Request::Quic_Message(msg)) => { + for subscriber in subscribers { + if subscriber.try_consume_custom(&msg.data, peers).await? { + break; + } + } + } + Ok(Request::Quic_Query(_)) => { + log::warn!( + target: TARGET, + "Received QUIC query on uni-directional stream from {addr} โ€” no response possible" + ); + } + Err(_) => { + log::warn!( + target: TARGET, + "Unknown QUIC TL message on uni-stream from {addr}" + ); } } Ok(()) @@ -1075,6 +1160,13 @@ impl QuicNode { dead_conn: &quinn::Connection, ) -> bool { let dead_id = dead_conn.stable_id(); + // Explicitly close the quinn connection so its internal ConnectionDriver + // task stops immediately. Without this, the driver continues processing + // keep-alive and retransmit timers until idle timeout (15s), causing the + // EndpointDriver to busy-loop on timer events and burn 100% CPU. + if dead_conn.close_reason().is_none() { + dead_conn.close(0u32.into(), b"dead connection cleanup"); + } match outbound.set_connection_state(addr, |found| { if let Some(ref conn) = found.conn { if conn.stable_id() == dead_id { @@ -1085,6 +1177,7 @@ impl QuicNode { return Ok(Some(QuicOutboundConnection { conn: None, send_queue: found.send_queue.clone(), + sender_state: found.sender_state.clone(), })); } } @@ -1144,6 +1237,85 @@ impl QuicNode { } } + /// Drain the send queue and exit. Spawned when `message()` has no live + /// connection and must enqueue data for later delivery. The task establishes + /// the connection, sends all queued messages, and terminates. + async fn run_sender_task( + quic: Arc, + src: Arc, + dst: Arc, + addr: SocketAddr, + server_name: String, + send_queue: Arc, + sender_state: Arc, + outbound: Arc>, + ) { + log::trace!(target: TARGET, "QUIC sender task started for {addr}"); + + loop { + // Drain the queue + while let Some(data) = send_queue.pop() { + if let Err(e) = + quic.send_message(&src, &dst, addr, &server_name, &outbound, &data).await + { + log::warn!(target: TARGET, "QUIC sender to {addr} error: {e}"); + } + } + + // Mark inactive, then re-check: a new message may have been enqueued + // between the last pop() returning None and the store below. + sender_state.active.store(false, Ordering::Release); + if send_queue.is_empty() { + break; + } + // Lost race โ€” reactivate if no other task took over + if sender_state + .active + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + break; // another task took over + } + } + + log::trace!(target: TARGET, "QUIC sender task for {addr} exited"); + } + + /// Send a single message to the peer, establishing the connection first if needed. + async fn send_message( + &self, + src: &Arc, + dst: &Arc, + addr: SocketAddr, + server_name: &str, + outbound: &Connections, + data: &[u8], + ) -> Result<()> { + let entry = Self::get_or_create_outbound_connection(outbound, addr)?; + match entry.conn { + Some(ref conn) => { + if let Err(e) = Self::send_via_stream(conn, data).await { + log::warn!( + target: TARGET, + "QUIC send to {addr} failed: {e}, removing dead connection" + ); + Self::remove_dead_connection(outbound, addr, conn); + return Err(e); + } + } + None => { + log::info!(target: TARGET, "QUIC sender: connecting to {addr}"); + self.connect(src, dst, addr, server_name).await?; + log::info!(target: TARGET, "QUIC sender: connected to {addr}"); + let entry = Self::get_or_create_outbound_connection(outbound, addr)?; + if let Some(ref conn) = entry.conn { + Self::send_via_stream(conn, data).await?; + } + } + } + Ok(()) + } + async fn send_query_raw( self: &Arc, data: Vec, @@ -1156,7 +1328,7 @@ impl QuicNode { let timeout = Duration::from_millis(timeout_ms); // First attempt - match self.get_outbound_connection(src, dst, false).await? { + match self.ensure_outbound_connection(src, dst).await? { QuicOutboundConnection { conn: Some(ref conn), .. } => { let result = tokio::time::timeout(timeout, Self::send_via_stream(conn, &data)).await; @@ -1182,7 +1354,7 @@ impl QuicNode { } // Retry once with a fresh connection - match self.get_outbound_connection(src, dst, false).await? { + match self.ensure_outbound_connection(src, dst).await? { QuicOutboundConnection { conn: Some(ref conn), .. } => { Self::send_via_stream(conn, &data).await } @@ -1304,16 +1476,21 @@ impl QuicNode { conn.close_reason() ); Self::remove_dead_connection(outbound, addr, conn); - // Fully remove entry if queue is drained - if let Some(entry) = outbound.map().get(&addr) { - let s = entry.val(); - if s.conn.is_none() && s.send_queue.is_inactive() { - outbound.map().remove(&addr); - } - } removed += 1; } } + // Fully remove entry only when connection is cleared, no sender + // task is running, and the queue is empty. Re-fetch from map + // because remove_dead_connection may have updated the entry. + if let Some(fresh) = outbound.map().get(&addr) { + let s = fresh.val(); + if s.conn.is_none() + && !s.sender_state.active.load(Ordering::Acquire) + && s.send_queue.is_empty() + { + outbound.map().remove(&addr); + } + } } } if removed > 0 { diff --git a/src/adnl/src/rldp/mod.rs b/src/adnl/src/rldp/mod.rs index c75c1e2..dd5a911 100644 --- a/src/adnl/src/rldp/mod.rs +++ b/src/adnl/src/rldp/mod.rs @@ -1206,16 +1206,16 @@ impl RldpNode { let start_ms = peer.stats.v1.timestamp_ms(); let mut last_warn_ms = start_ms; #[cfg(feature = "debug")] - let mut total_packets = 0; - #[cfg(feature = "debug")] let mut last_seqno = 0; + #[cfg(any(feature = "debug", feature = "telemetry"))] + let mut total_packets: u32 = 0; loop { let mut transfer_wave = transfer.start_next_part()?; if transfer_wave == 0 { #[cfg(feature = "debug")] Self::check_timestamp( &context.timestamp, - format!("Send transfer finished, packets {}", total_packets).as_str(), + format!("Send transfer finished, packets {total_packets}").as_str(), ); break; } @@ -1224,9 +1224,12 @@ impl RldpNode { let mut recv_seqno = 0; 'part: loop { for _ in 0..transfer_wave { - #[cfg(feature = "debug")] + #[cfg(any(feature = "debug", feature = "telemetry"))] { total_packets += 1; + } + #[cfg(feature = "debug")] + { last_seqno = transfer.state().seqno_send() } let (object, do_next) = transfer.prepare_chunk()?; @@ -1287,11 +1290,18 @@ impl RldpNode { peer.stats.v1.update(min_timeout_ms); recv_seqno = new_recv_seqno; } else if peer.stats.v1.try_timeout(start_ms) { + #[cfg(feature = "telemetry")] + log::info!( + target: TARGET, + "RLDPv1 send: packets sent {total_packets} (timeout) in {transfer_str}" + ); return Ok(false); } } peer.stats.v1.update(min_timeout_ms); } + #[cfg(feature = "telemetry")] + log::info!(target: TARGET, "RLDPv1 send: packets sent {total_packets} in {transfer_str}"); Ok(true) } @@ -1305,7 +1315,7 @@ impl RldpNode { let SendTransfer::V2(part_transfers) = &mut context.send_transfer else { fail!("Unexpected V1 send transfer in V2 send loop") }; - #[cfg(feature = "debug")] + #[cfg(any(feature = "debug", feature = "telemetry"))] let total_packets = Arc::new(AtomicU32::new(0)); let progress = Arc::new(AtomicU64::new(0)); let bbr_part_states = transfer_state.clone(); @@ -1356,7 +1366,7 @@ impl RldpNode { tag: context.tag, #[cfg(feature = "debug")] timestamp: context.timestamp.clone(), - #[cfg(feature = "debug")] + #[cfg(any(feature = "debug", feature = "telemetry"))] total_packets: total_packets.clone(), transfer_str: transfer_str.clone(), }; @@ -1393,6 +1403,13 @@ impl RldpNode { } } }?; + #[cfg(feature = "telemetry")] + log::info!( + target: TARGET, + "RLDPv2 send: packets sent {} ({}) in {transfer_str}", + total_packets.load(Ordering::Relaxed), + if ok { "ok" } else { "timeout" } + ); match bbr_task.await { Err(e) => Err(e.into()), Ok(Err(e)) => Err(e), @@ -1477,7 +1494,7 @@ impl RldpNode { continue; } }; - #[cfg(feature = "debug")] + #[cfg(any(feature = "debug", feature = "telemetry"))] context.total_packets.fetch_add(1, Ordering::Relaxed); let chunk = TaggedByteSlice { object, diff --git a/src/adnl/src/rldp/send.rs b/src/adnl/src/rldp/send.rs index 6b46335..08033de 100644 --- a/src/adnl/src/rldp/send.rs +++ b/src/adnl/src/rldp/send.rs @@ -624,7 +624,7 @@ pub(crate) struct SendPartContextV2 { pub(crate) tag: u32, #[cfg(feature = "debug")] pub(crate) timestamp: Arc>, - #[cfg(feature = "debug")] + #[cfg(any(feature = "debug", feature = "telemetry"))] pub(crate) total_packets: Arc, pub(crate) transfer_str: String, } diff --git a/src/adnl/tests/test_overlay.rs b/src/adnl/tests/test_overlay.rs index faab407..042da09 100644 --- a/src/adnl/tests/test_overlay.rs +++ b/src/adnl/tests/test_overlay.rs @@ -667,7 +667,9 @@ fn run_propagation( .await } Protocol::TwostepSimple | Protocol::TwostepFec => { - node_send.broadcast_two_step(&overlay_id_send, &data, None, 0).await + node_send + .broadcast_twostep(&overlay_id_send, &data, None, 0, Vec::new()) + .await } } .unwrap(); diff --git a/src/adnl/tests/test_quic.rs b/src/adnl/tests/test_quic.rs index 72e1c54..1fe7d06 100644 --- a/src/adnl/tests/test_quic.rs +++ b/src/adnl/tests/test_quic.rs @@ -13,6 +13,7 @@ use adnl::{ QuicNode, }; use std::{ + collections::HashSet, net::SocketAddr, sync::{ atomic::{AtomicUsize, Ordering}, @@ -129,7 +130,7 @@ fn test_quic_concurrent_accept() { let server_bind: SocketAddr = format!("127.0.0.1:{}", SERVER_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let server = QuicNode::new(vec![server_sub], server_token.clone()); + let server = QuicNode::new(vec![server_sub], server_token.clone(), None); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); // --- clients --- @@ -157,7 +158,7 @@ fn test_quic_concurrent_accept() { let bind: SocketAddr = format!("127.0.0.1:{}", port + QuicNode::OFFSET_PORT).parse().unwrap(); let token = CancellationToken::new(); - let quic = QuicNode::new(vec![sub], token.clone()); + let quic = QuicNode::new(vec![sub], token.clone(), None); quic.add_key(&key, &key_id, bind).unwrap(); quic.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(key_id.clone(), bind).unwrap(); @@ -253,10 +254,10 @@ fn test_quic_session() { let bind_a: SocketAddr = "127.0.0.1:5600".parse().unwrap(); let bind_b: SocketAddr = "127.0.0.1:5601".parse().unwrap(); - let quic_a = QuicNode::new(vec![sub_a], token_a.clone()); + let quic_a = QuicNode::new(vec![sub_a], token_a.clone(), None); quic_a.add_key(&key_bytes_a, &key_id_a, bind_a).unwrap(); - let quic_b = QuicNode::new(vec![sub_b], token_b.clone()); + let quic_b = QuicNode::new(vec![sub_b], token_b.clone(), None); quic_b.add_key(&key_bytes_b, &key_id_b, bind_b).unwrap(); // Register peer addresses @@ -334,7 +335,7 @@ fn test_quic_reconnect_after_server_restart() { let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) as Arc; - let client = QuicNode::new(vec![client_sub], client_token.clone()); + let client = QuicNode::new(vec![client_sub], client_token.clone(), None); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); // --- server B1 (will be shut down) --- @@ -353,7 +354,7 @@ fn test_quic_reconnect_after_server_restart() { Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx1 }) as Arc; - let server1 = QuicNode::new(vec![server_sub1], server_token1.clone()); + let server1 = QuicNode::new(vec![server_sub1], server_token1.clone(), None); server1.add_key(&server_key, &server_key_id, server_bind).unwrap(); // Register peer keys @@ -387,7 +388,7 @@ fn test_quic_reconnect_after_server_restart() { Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx2 }) as Arc; - let server2 = QuicNode::new(vec![server_sub2], server_token2.clone()); + let server2 = QuicNode::new(vec![server_sub2], server_token2.clone(), None); server2.add_key(&server_key, &server_key_id, server_bind).unwrap(); server2.add_peer_key(client_key_id.clone(), client_bind).unwrap(); println!("Step 3: server B2 started on same port with same key"); @@ -490,7 +491,7 @@ fn test_quic_stream_limit() { let server_bind: SocketAddr = format!("127.0.0.1:{}", SERVER_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); let server = - QuicNode::with_stream_limit(vec![server_sub], server_token.clone(), STREAM_LIMIT); + QuicNode::new(vec![server_sub], server_token.clone(), Some(STREAM_LIMIT)); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); // --- client (normal limits) --- @@ -509,7 +510,7 @@ fn test_quic_stream_limit() { let client_bind: SocketAddr = format!("127.0.0.1:{}", CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let client = QuicNode::new(vec![client_sub], client_token.clone()); + let client = QuicNode::new(vec![client_sub], client_token.clone(), None); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); // Register peers @@ -593,7 +594,7 @@ fn make_endpoint( let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let sub = Arc::new(TestSubscriber { key_id: key_id.clone(), msg_tx: tx }) as Arc; - let quic = QuicNode::new(vec![sub], token.clone()); + let quic = QuicNode::new(vec![sub], token.clone(), None); quic.add_key(&key, &key_id, bind).unwrap(); (quic, key, key_id, bind, token) } @@ -1263,7 +1264,7 @@ fn test_quic_connection_pool_exhaustion() { let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let sub = Arc::new(TestSubscriber { key_id: key_id.clone(), msg_tx: tx }) as Arc; - let quic = QuicNode::new(vec![sub], token.clone()); + let quic = QuicNode::new(vec![sub], token.clone(), None); quic.add_key(&key, &key_id, bind).unwrap(); quic.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(key_id.clone(), bind).unwrap(); @@ -1335,3 +1336,241 @@ fn test_quic_connection_pool_exhaustion() { server_token.cancel(); }); } + +/// Fire messages through a server restart cycle. Verifies the sender task +/// drains the queue after reconnection without hanging or losing messages. +/// In the old hot-loop design, the yield_now() spins would starve the runtime. +#[test] +fn test_quic_message_burst_reconnect() { + init_test_log(); + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(async { + const CLIENT_PORT: u16 = 8100; + const SERVER_PORT: u16 = 8101; + const BURST_SIZE: usize = 50; + + let client_bind: SocketAddr = format!("127.0.0.1:{CLIENT_PORT}").parse().unwrap(); + let server_bind: SocketAddr = format!("127.0.0.1:{SERVER_PORT}").parse().unwrap(); + + let client_token = CancellationToken::new(); + let client_key = ed25519_generate_private_key().unwrap().to_bytes(); + let (_, client_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{CLIENT_PORT}"), + vec![(client_key, KEY_TAG)], + ) + .unwrap(); + let client_key_id = client_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); + + let (cli_tx, _cli_rx) = tokio::sync::mpsc::unbounded_channel(); + let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) + as Arc; + let client = QuicNode::new(vec![client_sub], client_token.clone(), None); + client.add_key(&client_key, &client_key_id, client_bind).unwrap(); + + let server_key = ed25519_generate_private_key().unwrap().to_bytes(); + let (_, server_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{SERVER_PORT}"), + vec![(server_key, KEY_TAG)], + ) + .unwrap(); + let server_key_id = server_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); + + // --- Phase 1: first server instance --- + let srv_token1 = CancellationToken::new(); + let (srv_tx1, mut srv_rx1) = tokio::sync::mpsc::unbounded_channel(); + let srv_sub1 = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx1 }) + as Arc; + let server1 = QuicNode::new(vec![srv_sub1], srv_token1.clone(), None); + server1.add_key(&server_key, &server_key_id, server_bind).unwrap(); + + client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); + server1.add_peer_key(client_key_id.clone(), client_bind).unwrap(); + + for i in 0..BURST_SIZE { + let payload = format!("msg-phase1-{i}").into_bytes(); + client.message(payload, None, &client_key_id, &server_key_id).await.unwrap(); + } + + let expected_p1: HashSet> = + (0..BURST_SIZE).map(|i| format!("msg-phase1-{i}").into_bytes()).collect(); + let mut got_p1 = HashSet::new(); + let deadline = tokio::time::Instant::now() + Duration::from_secs(15); + while got_p1.len() < BURST_SIZE { + match tokio::time::timeout_at(deadline, srv_rx1.recv()).await { + Ok(Some(data)) => { + got_p1.insert(data); + } + _ => break, + } + } + println!("Phase 1: received {}/{BURST_SIZE} unique messages", got_p1.len()); + assert_eq!( + got_p1, expected_p1, + "Phase 1 must deliver every distinct message (at-least-once guarantee)" + ); + + // --- Phase 2: restart server, send another burst --- + server1.shutdown(); + srv_token1.cancel(); + drop(server1); + tokio::time::sleep(Duration::from_millis(1000)).await; + + let srv_token2 = CancellationToken::new(); + let (srv_tx2, mut srv_rx2) = tokio::sync::mpsc::unbounded_channel(); + let srv_sub2 = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx2 }) + as Arc; + let server2 = QuicNode::new(vec![srv_sub2], srv_token2.clone(), None); + server2.add_key(&server_key, &server_key_id, server_bind).unwrap(); + server2.add_peer_key(client_key_id.clone(), client_bind).unwrap(); + + for i in 0..BURST_SIZE { + let payload = format!("msg-phase2-{i}").into_bytes(); + client.message(payload, None, &client_key_id, &server_key_id).await.unwrap(); + } + + let expected_p2: HashSet> = + (0..BURST_SIZE).map(|i| format!("msg-phase2-{i}").into_bytes()).collect(); + let mut got_p2 = HashSet::new(); + let deadline = tokio::time::Instant::now() + Duration::from_secs(30); + while got_p2.len() < BURST_SIZE { + match tokio::time::timeout_at(deadline, srv_rx2.recv()).await { + Ok(Some(data)) => { + got_p2.insert(data); + } + _ => break, + } + } + println!( + "Phase 2: received {}/{BURST_SIZE} unique messages after server restart", + got_p2.len() + ); + assert_eq!( + got_p2, expected_p2, + "Phase 2 must deliver every distinct message after restart (at-least-once guarantee)" + ); + + client.shutdown(); + server2.shutdown(); + client_token.cancel(); + srv_token2.cancel(); + }); +} + +/// Concurrent message senders to the same peer must not deadlock or starve +/// the Tokio runtime. Uses only 2 worker threads to make thread starvation +/// from the old yield_now() hot loops detectable. +#[test] +fn test_quic_single_sender_invariant() { + init_test_log(); + let rt = + tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap(); + rt.block_on(async { + const CLIENT_PORT: u16 = 8200; + const SERVER_PORT: u16 = 8201; + const NUM_SENDERS: usize = 20; + const MSGS_PER_SENDER: usize = 5; + const TOTAL_MSGS: usize = NUM_SENDERS * MSGS_PER_SENDER; + const TIMEOUT: Duration = Duration::from_secs(20); + + let client_bind: SocketAddr = format!("127.0.0.1:{CLIENT_PORT}").parse().unwrap(); + let server_bind: SocketAddr = format!("127.0.0.1:{SERVER_PORT}").parse().unwrap(); + + let client_token = CancellationToken::new(); + let client_key = ed25519_generate_private_key().unwrap().to_bytes(); + let (_, client_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{CLIENT_PORT}"), + vec![(client_key, KEY_TAG)], + ) + .unwrap(); + let client_key_id = client_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); + + let (cli_tx, _cli_rx) = tokio::sync::mpsc::unbounded_channel(); + let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) + as Arc; + let client = QuicNode::new(vec![client_sub], client_token.clone(), None); + client.add_key(&client_key, &client_key_id, client_bind).unwrap(); + + let srv_token = CancellationToken::new(); + let server_key = ed25519_generate_private_key().unwrap().to_bytes(); + let (_, server_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{SERVER_PORT}"), + vec![(server_key, KEY_TAG)], + ) + .unwrap(); + let server_key_id = server_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); + + let (srv_tx, mut srv_rx) = tokio::sync::mpsc::unbounded_channel(); + let srv_sub = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx }) + as Arc; + let server = QuicNode::new(vec![srv_sub], srv_token.clone(), None); + server.add_key(&server_key, &server_key_id, server_bind).unwrap(); + + client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); + server.add_peer_key(client_key_id.clone(), client_bind).unwrap(); + + let expected: HashSet> = (0..NUM_SENDERS) + .flat_map(|s| { + (0..MSGS_PER_SENDER).map(move |m| format!("sender-{s}-msg-{m}").into_bytes()) + }) + .collect(); + let got = Arc::new(tokio::sync::Mutex::new(HashSet::new())); + let got_clone = got.clone(); + let drain_handle = tokio::spawn(async move { + while let Some(data) = srv_rx.recv().await { + got_clone.lock().await.insert(data); + } + }); + + let mut handles = Vec::with_capacity(NUM_SENDERS); + for sender_id in 0..NUM_SENDERS { + let quic = client.clone(); + let src = client_key_id.clone(); + let dst = server_key_id.clone(); + handles.push(tokio::spawn(async move { + for msg_id in 0..MSGS_PER_SENDER { + let payload = format!("sender-{sender_id}-msg-{msg_id}").into_bytes(); + if let Err(e) = quic.message(payload, None, &src, &dst).await { + eprintln!("sender {sender_id} msg {msg_id} failed: {e}"); + } + } + })); + } + + let send_result = tokio::time::timeout(TIMEOUT, async { + for h in handles { + h.await.expect("sender task panicked"); + } + }) + .await; + assert!(send_result.is_ok(), "Concurrent senders timed out โ€” possible hot-loop regression"); + + let recv_deadline = tokio::time::Instant::now() + Duration::from_secs(30); + loop { + let unique_count = got.lock().await.len(); + if unique_count >= TOTAL_MSGS { + break; + } + if tokio::time::Instant::now() >= recv_deadline { + break; + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + + let received = got.lock().await; + println!( + "Single-sender invariant: {}/{TOTAL_MSGS} unique messages delivered \ + by {NUM_SENDERS} concurrent senders on 2 Tokio threads", + received.len() + ); + assert_eq!( + *received, expected, + "All {TOTAL_MSGS} distinct messages must be delivered (at-least-once guarantee)" + ); + + client.shutdown(); + server.shutdown(); + client_token.cancel(); + srv_token.cancel(); + drain_handle.abort(); + }); +} diff --git a/src/block-json/src/deserialize.rs b/src/block-json/src/deserialize.rs index d10d88a..ff15b65 100644 --- a/src/block-json/src/deserialize.rs +++ b/src/block-json/src/deserialize.rs @@ -665,12 +665,45 @@ impl StateParser { } fn parse_simplex_config(p: &PathMap) -> Result { + let d = NoncriticalParams::default(); Ok(SimplexConfig { use_quic: p.get_num32("use_quic").unwrap_or(0) != 0, - target_rate_ms: p.get_num32("target_rate_ms")?, slots_per_leader_window: p.get_num32("slots_per_leader_window")?, - first_block_timeout_ms: p.get_num32("first_block_timeout_ms")?, - max_leader_window_desync: p.get_num32("max_leader_window_desync")?, + noncritical_params: NoncriticalParams { + target_rate_ms: p.get_num32("target_rate_ms")?, + first_block_timeout_ms: p.get_num32("first_block_timeout_ms")?, + first_block_timeout_multiplier_bits: p + .get_num32("first_block_timeout_multiplier_bits") + .unwrap_or(d.first_block_timeout_multiplier_bits), + first_block_timeout_cap_ms: p + .get_num32("first_block_timeout_cap_ms") + .unwrap_or(d.first_block_timeout_cap_ms), + candidate_resolve_timeout_ms: p + .get_num32("candidate_resolve_timeout_ms") + .unwrap_or(d.candidate_resolve_timeout_ms), + candidate_resolve_timeout_multiplier_bits: p + .get_num32("candidate_resolve_timeout_multiplier_bits") + .unwrap_or(d.candidate_resolve_timeout_multiplier_bits), + candidate_resolve_timeout_cap_ms: p + .get_num32("candidate_resolve_timeout_cap_ms") + .unwrap_or(d.candidate_resolve_timeout_cap_ms), + candidate_resolve_cooldown_ms: p + .get_num32("candidate_resolve_cooldown_ms") + .unwrap_or(d.candidate_resolve_cooldown_ms), + standstill_timeout_ms: p + .get_num32("standstill_timeout_ms") + .unwrap_or(d.standstill_timeout_ms), + standstill_max_egress_bytes_per_s: p + .get_num32("standstill_max_egress_bytes_per_s") + .unwrap_or(d.standstill_max_egress_bytes_per_s), + max_leader_window_desync: p.get_num32("max_leader_window_desync")?, + bad_signature_ban_duration_ms: p + .get_num32("bad_signature_ban_duration_ms") + .unwrap_or(d.bad_signature_ban_duration_ms), + candidate_resolve_rate_limit: p + .get_num32("candidate_resolve_rate_limit") + .unwrap_or(d.candidate_resolve_rate_limit), + }, }) } diff --git a/src/block-json/src/serialize.rs b/src/block-json/src/serialize.rs index 5faf3c8..1b8d70a 100644 --- a/src/block-json/src/serialize.rs +++ b/src/block-json/src/serialize.rs @@ -1074,10 +1074,37 @@ fn serialize_simplex_config(cfg: &SimplexConfig) -> Result { if cfg.use_quic { serialize_field(&mut map, "use_quic", 1u32); } - serialize_field(&mut map, "target_rate_ms", cfg.target_rate_ms); serialize_field(&mut map, "slots_per_leader_window", cfg.slots_per_leader_window); - serialize_field(&mut map, "first_block_timeout_ms", cfg.first_block_timeout_ms); - serialize_field(&mut map, "max_leader_window_desync", cfg.max_leader_window_desync); + let np = &cfg.noncritical_params; + serialize_field(&mut map, "target_rate_ms", np.target_rate_ms); + serialize_field(&mut map, "first_block_timeout_ms", np.first_block_timeout_ms); + serialize_field( + &mut map, + "first_block_timeout_multiplier_bits", + np.first_block_timeout_multiplier_bits, + ); + serialize_field(&mut map, "first_block_timeout_cap_ms", np.first_block_timeout_cap_ms); + serialize_field(&mut map, "candidate_resolve_timeout_ms", np.candidate_resolve_timeout_ms); + serialize_field( + &mut map, + "candidate_resolve_timeout_multiplier_bits", + np.candidate_resolve_timeout_multiplier_bits, + ); + serialize_field( + &mut map, + "candidate_resolve_timeout_cap_ms", + np.candidate_resolve_timeout_cap_ms, + ); + serialize_field(&mut map, "candidate_resolve_cooldown_ms", np.candidate_resolve_cooldown_ms); + serialize_field(&mut map, "standstill_timeout_ms", np.standstill_timeout_ms); + serialize_field( + &mut map, + "standstill_max_egress_bytes_per_s", + np.standstill_max_egress_bytes_per_s, + ); + serialize_field(&mut map, "max_leader_window_desync", np.max_leader_window_desync); + serialize_field(&mut map, "bad_signature_ban_duration_ms", np.bad_signature_ban_duration_ms); + serialize_field(&mut map, "candidate_resolve_rate_limit", np.candidate_resolve_rate_limit); Ok(map.into()) } diff --git a/src/block-json/src/tests/test_deserialize.rs b/src/block-json/src/tests/test_deserialize.rs index 9d2dbe5..5a1065c 100644 --- a/src/block-json/src/tests/test_deserialize.rs +++ b/src/block-json/src/tests/test_deserialize.rs @@ -10,10 +10,11 @@ */ use super::*; use crate::{serialize_config, serialize_config_param, SerializationMode}; +use std::fmt::Debug; use ton_block::{ - BuilderData, ConfigParam3, ConfigParam32, ConfigParam33, ConfigParam35, ConfigParam36, - ConfigParam37, ConfigParam39, ConfigParam4, ConfigParam6, ConfigVotingSetup, IBitstring, - Number16, SigPubKey, VarUInteger32, + write_boc, BuilderData, ConfigParam3, ConfigParam32, ConfigParam33, ConfigParam35, + ConfigParam36, ConfigParam37, ConfigParam39, ConfigParam4, ConfigParam6, ConfigParamEnum, + ConfigVotingSetup, IBitstring, NoncriticalParams, Number16, SigPubKey, VarUInteger32, }; include!("./test_common.rs"); @@ -27,7 +28,75 @@ fn test_parse_zerostate() { assert_json_eq(&json, ðalon, "zerostate"); } -fn check_err(result: Result, text: &str) { +#[test] +fn test_parse_zerostate_p30_use_quic_survives_into_v2_boc() { + let ethalon = std::fs::read_to_string("src/tests/data/zerostate-ethalon.json").unwrap(); + let mut map = serde_json::from_str::>(ðalon).unwrap(); + + let master = map.get_mut("master").unwrap().as_object_mut().unwrap(); + let config = master.get_mut("config").unwrap().as_object_mut().unwrap(); + config.get_mut("p8").unwrap().as_object_mut().unwrap().insert("version".to_string(), 13.into()); + config.insert( + "p30".to_string(), + serde_json::json!({ + "mc": { + "use_quic": 1, + "slots_per_leader_window": 8, + "target_rate_ms": 200, + "first_block_timeout_ms": 500, + "max_leader_window_desync": 2 + }, + "shard": { + "use_quic": 1, + "slots_per_leader_window": 16, + "target_rate_ms": 200, + "first_block_timeout_ms": 500, + "max_leader_window_desync": 2 + } + }), + ); + + let state = parse_state(&map).unwrap(); + let custom = state.read_custom().unwrap().unwrap(); + let config = custom.config(); + + let ConfigParamEnum::ConfigParam30(parsed_p30) = config.config(30).unwrap().unwrap() else { + panic!("expected ConfigParam30 in parsed zerostate"); + }; + + let mc = parsed_p30.mc.as_ref().expect("expected MC simplex config"); + assert!(mc.use_quic); + assert_eq!(mc.slots_per_leader_window, 8); + assert_eq!(mc.noncritical_params.target_rate_ms, 200); + assert_eq!(mc.noncritical_params.first_block_timeout_ms, 500); + assert_eq!(mc.noncritical_params.max_leader_window_desync, 2); + + let shard = parsed_p30.shard.as_ref().expect("expected shard simplex config"); + assert!(shard.use_quic); + assert_eq!(shard.slots_per_leader_window, 16); + assert_eq!(shard.noncritical_params.target_rate_ms, 200); + assert_eq!(shard.noncritical_params.first_block_timeout_ms, 500); + assert_eq!(shard.noncritical_params.max_leader_window_desync, 2); + + let key = 30u32.write_to_bitstring().unwrap(); + let p30_slice = config.config_params.get(key).unwrap().expect("expected raw p30 cell"); + let p30_cell = p30_slice.reference(0).unwrap(); + let p30_boc = write_boc(&p30_cell).unwrap(); + + let mc_v2_quic = [0x22, 0x01, 0x00, 0x00, 0x00, 0x08]; + assert!( + p30_boc.windows(mc_v2_quic.len()).any(|window| window == mc_v2_quic), + "serialized p30 BOC must contain MC simplex_config_v2#22 with use_quic=1" + ); + + let shard_v2_quic = [0x22, 0x01, 0x00, 0x00, 0x00, 0x10]; + assert!( + p30_boc.windows(shard_v2_quic.len()).any(|window| window == shard_v2_quic), + "serialized p30 BOC must contain shard simplex_config_v2#22 with use_quic=1" + ); +} + +fn check_err(result: Result, text: &str) { let len = text.len(); assert_eq!(&result.expect_err("must generate error").to_string()[0..len], text) } @@ -293,17 +362,23 @@ fn get_config_param63() -> AcceleratedConsensusConfig { fn get_config_param30() -> NewConsensusConfigAll { NewConsensusConfigAll { mc: Some(SimplexConfig { - target_rate_ms: 300, slots_per_leader_window: 4, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, + noncritical_params: NoncriticalParams { + target_rate_ms: 300, + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, + ..Default::default() + }, ..Default::default() }), shard: Some(SimplexConfig { - target_rate_ms: 200, slots_per_leader_window: 8, - first_block_timeout_ms: 500, - max_leader_window_desync: 50, + noncritical_params: NoncriticalParams { + target_rate_ms: 200, + first_block_timeout_ms: 500, + max_leader_window_desync: 50, + ..Default::default() + }, ..Default::default() }), } diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index 161b1f9..216ae35 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -24,6 +24,7 @@ use crate::{ BASE_WORKCHAIN_ID, MAX_SPLIT_DEPTH, }; use num::BigInt; +use std::collections::BTreeMap; #[cfg(test)] #[path = "tests/test_config_params.rs"] @@ -3768,56 +3769,198 @@ const NEW_CONSENSUS_CONFIG_ALL_TAG: u8 = 0x10; #[allow(dead_code)] // Used in deserialization logic - null consensus means fallback to catchain const NULL_CONSENSUS_CONFIG_TAG: u8 = 0x20; const SIMPLEX_CONFIG_TAG: u8 = 0x21; +const SIMPLEX_CONFIG_V2_TAG: u8 = 0x22; -/// SimplexConfig - simplex_config#21 from ConfigParam 30 -/// Enables Simplex (Alpenglow) consensus for the specified workchain. +/// Named noncritical consensus parameters, mirroring the C++ `NoncriticalParams` struct +/// defined via `ENUMERATE_NONCRITICAL_PARAMS` in `ton-types.h`. /// -/// TL-B: simplex_config#21 flags:(## 8) target_rate_ms:uint32 -/// slots_per_leader_window:uint32 first_block_timeout_ms:uint32 -/// max_leader_window_desync:uint32 = NewConsensusConfig; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +/// All fields have concrete default values matching C++. For v1 configs the three +/// on-chain fields (`target_rate_ms`, `first_block_timeout_ms`, `max_leader_window_desync`) +/// are populated from the TL-B, the rest keep their defaults. For v2, the on-chain hashmap +/// overrides any subset of the 13 parameters. +/// +/// "double" parameters (multipliers) are stored as raw `f32` bit patterns in a `u32`, +/// matching the C++ `store_double` / `read_double` convention. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NoncriticalParams { + pub target_rate_ms: u32, // idx 0, duration + pub first_block_timeout_ms: u32, // idx 1, duration + pub first_block_timeout_multiplier_bits: u32, // idx 2, double (f32 bits) + pub first_block_timeout_cap_ms: u32, // idx 3, duration + pub candidate_resolve_timeout_ms: u32, // idx 4, duration + pub candidate_resolve_timeout_multiplier_bits: u32, // idx 5, double (f32 bits) + pub candidate_resolve_timeout_cap_ms: u32, // idx 6, duration + pub candidate_resolve_cooldown_ms: u32, // idx 7, duration + pub standstill_timeout_ms: u32, // idx 8, duration + pub standstill_max_egress_bytes_per_s: u32, // idx 9, uint32 + pub max_leader_window_desync: u32, // idx 10, uint32 + pub bad_signature_ban_duration_ms: u32, // idx 11, duration + pub candidate_resolve_rate_limit: u32, // idx 12, uint32 +} + +impl Default for NoncriticalParams { + fn default() -> Self { + Self { + target_rate_ms: 2400, + first_block_timeout_ms: 1000, + first_block_timeout_multiplier_bits: (1.2f32).to_bits(), + first_block_timeout_cap_ms: 100_000, + candidate_resolve_timeout_ms: 1000, + candidate_resolve_timeout_multiplier_bits: (1.2f32).to_bits(), + candidate_resolve_timeout_cap_ms: 10_000, + candidate_resolve_cooldown_ms: 10, + standstill_timeout_ms: 10_000, + standstill_max_egress_bytes_per_s: 50 << 17, + max_leader_window_desync: 250, + bad_signature_ban_duration_ms: 5_000, + candidate_resolve_rate_limit: 10, + } + } +} + +impl NoncriticalParams { + /// Set a parameter by its on-chain hashmap index. + pub fn set(&mut self, idx: u8, value: u32) { + match idx { + 0 => self.target_rate_ms = value, + 1 => self.first_block_timeout_ms = value, + 2 => self.first_block_timeout_multiplier_bits = value, + 3 => self.first_block_timeout_cap_ms = value, + 4 => self.candidate_resolve_timeout_ms = value, + 5 => self.candidate_resolve_timeout_multiplier_bits = value, + 6 => self.candidate_resolve_timeout_cap_ms = value, + 7 => self.candidate_resolve_cooldown_ms = value, + 8 => self.standstill_timeout_ms = value, + 9 => self.standstill_max_egress_bytes_per_s = value, + 10 => self.max_leader_window_desync = value, + 11 => self.bad_signature_ban_duration_ms = value, + 12 => self.candidate_resolve_rate_limit = value, + _ => {} + } + } + + /// Construct from a raw hashmap (as stored on-chain in simplex_config_v2). + pub fn from_raw_map(map: &BTreeMap) -> Self { + let mut p = Self::default(); + for (&k, &v) in map { + p.set(k, v); + } + p + } + + /// Convert all fields to a raw hashmap for on-chain v2 serialization. + pub fn to_raw_map(&self) -> BTreeMap { + BTreeMap::from([ + (0, self.target_rate_ms), + (1, self.first_block_timeout_ms), + (2, self.first_block_timeout_multiplier_bits), + (3, self.first_block_timeout_cap_ms), + (4, self.candidate_resolve_timeout_ms), + (5, self.candidate_resolve_timeout_multiplier_bits), + (6, self.candidate_resolve_timeout_cap_ms), + (7, self.candidate_resolve_cooldown_ms), + (8, self.standstill_timeout_ms), + (9, self.standstill_max_egress_bytes_per_s), + (10, self.max_leader_window_desync), + (11, self.bad_signature_ban_duration_ms), + (12, self.candidate_resolve_rate_limit), + ]) + } +} + +/// Unified Simplex consensus config โ€” the single output type +/// produced by deserializing either `simplex_config#21` (v1) or +/// `simplex_config_v2#22` (v2) from ConfigParam 30. +/// +/// Mirrors C++ `NewConsensusConfig` in `ton-types.h`: critical fields +/// (`use_quic`, `slots_per_leader_window`) live at top level; all tunable +/// timing/rate parameters live inside `noncritical_params`. +#[derive(Clone, Debug, Eq, PartialEq)] pub struct SimplexConfig { pub use_quic: bool, - pub target_rate_ms: u32, pub slots_per_leader_window: u32, - pub first_block_timeout_ms: u32, - pub max_leader_window_desync: u32, + pub noncritical_params: NoncriticalParams, } -/// Byte layout: flags:(## 7) use_quic:Bool โ€” 7 flag bits (reserved) + 1 use_quic bit = 1 byte. -/// TLB writes MSB-first, so use_quic occupies the LSB: byte = (flags << 1) | use_quic. +impl Default for SimplexConfig { + fn default() -> Self { + Self { + use_quic: false, + slots_per_leader_window: 4, + noncritical_params: NoncriticalParams::default(), + } + } +} + +/// Maximum noncritical param key defined in the C++ reference (candidate_resolve_rate_limit). +const NONCRITICAL_PARAMS_MAX_KEY: u8 = 12; + +/// Always serializes as simplex_config_v2#22 (the current on-chain format). impl Serializable for SimplexConfig { fn write_to(&self, cell: &mut BuilderData) -> Result<()> { - cell.append_u8(SIMPLEX_CONFIG_TAG)?; + cell.append_u8(SIMPLEX_CONFIG_V2_TAG)?; let flags_byte = if self.use_quic { 1u8 } else { 0u8 }; cell.append_u8(flags_byte)?; - self.target_rate_ms.write_to(cell)?; self.slots_per_leader_window.write_to(cell)?; - self.first_block_timeout_ms.write_to(cell)?; - self.max_leader_window_desync.write_to(cell)?; + let raw_map = self.noncritical_params.to_raw_map(); + let mut params_hashmap = HashmapE::with_bit_len(8); + for (&key, &value) in &raw_map { + let key_slice = SliceData::from_raw(vec![key], 8); + let mut vb = BuilderData::new(); + value.write_to(&mut vb)?; + params_hashmap.set(key_slice, &SliceData::load_builder(vb)?)?; + } + params_hashmap.write_hashmap_data(cell)?; Ok(()) } } +/// Deserializes both simplex_config#21 (v1) and simplex_config_v2#22 (v2). impl Deserializable for SimplexConfig { fn construct_from(slice: &mut SliceData) -> Result { let tag = slice.get_next_byte()?; - if tag != SIMPLEX_CONFIG_TAG { - fail!(Self::invalid_tag(tag as u32)); + match tag { + SIMPLEX_CONFIG_TAG => { + let flags_byte = slice.get_next_byte()?; + let use_quic = (flags_byte & 1) != 0; + let target_rate_ms = u32::construct_from(slice)?; + let slots_per_leader_window = u32::construct_from(slice)?; + let first_block_timeout_ms = u32::construct_from(slice)?; + let max_leader_window_desync = u32::construct_from(slice)?; + Ok(Self { + use_quic, + slots_per_leader_window, + noncritical_params: NoncriticalParams { + target_rate_ms, + first_block_timeout_ms, + max_leader_window_desync, + ..Default::default() + }, + }) + } + SIMPLEX_CONFIG_V2_TAG => { + let flags_byte = slice.get_next_byte()?; + let use_quic = (flags_byte & 1) != 0; + let slots_per_leader_window = u32::construct_from(slice)?; + let has_params = slice.get_next_bit()?; + let params_cell = + if has_params { Some(slice.checked_drain_reference()?) } else { None }; + let params_map = HashmapE::with_hashmap(8, params_cell); + let mut raw = BTreeMap::new(); + for key_idx in 0..=NONCRITICAL_PARAMS_MAX_KEY { + let key = SliceData::from_raw(vec![key_idx], 8); + if let Some(mut vs) = params_map.get(key)? { + raw.insert(key_idx, u32::construct_from(&mut vs)?); + } + } + Ok(Self { + use_quic, + slots_per_leader_window, + noncritical_params: NoncriticalParams::from_raw_map(&raw), + }) + } + _ => fail!(Self::invalid_tag(tag as u32)), } - let flags_byte = slice.get_next_byte()?; - let use_quic = (flags_byte & 1) != 0; - let target_rate_ms = u32::construct_from(slice)?; - let slots_per_leader_window = u32::construct_from(slice)?; - let first_block_timeout_ms = u32::construct_from(slice)?; - let max_leader_window_desync = u32::construct_from(slice)?; - Ok(Self { - use_quic, - target_rate_ms, - slots_per_leader_window, - first_block_timeout_ms, - max_leader_window_desync, - }) } } @@ -3869,17 +4012,17 @@ impl Deserializable for NewConsensusConfigAll { let cell = slice.checked_drain_reference()?; let mut inner = SliceData::load_cell(cell)?; let inner_tag = inner.clone().get_next_byte()?; - if inner_tag == SIMPLEX_CONFIG_TAG { + if inner_tag == SIMPLEX_CONFIG_TAG || inner_tag == SIMPLEX_CONFIG_V2_TAG { result.mc = Some(SimplexConfig::construct_from(&mut inner)?); } - // else null_consensus_config#20 - leave as None (catchain fallback) + // else null_consensus_config#20 or unknown โ†’ None (catchain fallback) } // shard:(Maybe ^NewConsensusConfig) if slice.get_next_bit()? { let cell = slice.checked_drain_reference()?; let mut inner = SliceData::load_cell(cell)?; let inner_tag = inner.clone().get_next_byte()?; - if inner_tag == SIMPLEX_CONFIG_TAG { + if inner_tag == SIMPLEX_CONFIG_TAG || inner_tag == SIMPLEX_CONFIG_V2_TAG { result.shard = Some(SimplexConfig::construct_from(&mut inner)?); } } diff --git a/src/block/src/tests/test_config_params.rs b/src/block/src/tests/test_config_params.rs index c9eb765..4219ab8 100644 --- a/src/block/src/tests/test_config_params.rs +++ b/src/block/src/tests/test_config_params.rs @@ -933,10 +933,13 @@ fn test_accelerated_consensus_config() { #[test] fn test_simplex_config() { let config = SimplexConfig { - target_rate_ms: 300, slots_per_leader_window: 4, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, + noncritical_params: NoncriticalParams { + target_rate_ms: 300, + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, + ..Default::default() + }, ..Default::default() }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); @@ -948,10 +951,14 @@ fn test_simplex_config() { fn test_simplex_config_with_quic() { let config = SimplexConfig { use_quic: true, - target_rate_ms: 300, slots_per_leader_window: 4, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, + noncritical_params: NoncriticalParams { + target_rate_ms: 300, + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, + ..Default::default() + }, + ..Default::default() }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); @@ -962,17 +969,23 @@ fn test_simplex_config_with_quic() { #[test] fn test_new_consensus_config_all_both() { let mc_config = SimplexConfig { - target_rate_ms: 300, slots_per_leader_window: 4, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, + noncritical_params: NoncriticalParams { + target_rate_ms: 300, + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, + ..Default::default() + }, ..Default::default() }; let shard_config = SimplexConfig { - target_rate_ms: 200, slots_per_leader_window: 8, - first_block_timeout_ms: 500, - max_leader_window_desync: 50, + noncritical_params: NoncriticalParams { + target_rate_ms: 200, + first_block_timeout_ms: 500, + max_leader_window_desync: 50, + ..Default::default() + }, ..Default::default() }; let config = NewConsensusConfigAll { mc: Some(mc_config), shard: Some(shard_config) }; @@ -984,10 +997,13 @@ fn test_new_consensus_config_all_both() { #[test] fn test_new_consensus_config_all_shard_only() { let shard_config = SimplexConfig { - target_rate_ms: 200, slots_per_leader_window: 8, - first_block_timeout_ms: 500, - max_leader_window_desync: 50, + noncritical_params: NoncriticalParams { + target_rate_ms: 200, + first_block_timeout_ms: 500, + max_leader_window_desync: 50, + ..Default::default() + }, ..Default::default() }; let config = NewConsensusConfigAll { mc: None, shard: Some(shard_config) }; @@ -999,10 +1015,13 @@ fn test_new_consensus_config_all_shard_only() { #[test] fn test_new_consensus_config_all_mc_only() { let mc_config = SimplexConfig { - target_rate_ms: 300, slots_per_leader_window: 4, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, + noncritical_params: NoncriticalParams { + target_rate_ms: 300, + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, + ..Default::default() + }, ..Default::default() }; let config = NewConsensusConfigAll { mc: Some(mc_config), shard: None }; @@ -1018,3 +1037,145 @@ fn test_new_consensus_config_all_empty() { let config2 = NewConsensusConfigAll::construct_from_cell(cell).unwrap(); assert_eq!(config, config2); } + +// ===================== simplex_config v2 serialization tests ===================== + +#[test] +fn test_simplex_config_v2_round_trip() { + let config = SimplexConfig { + use_quic: true, + slots_per_leader_window: 8, + noncritical_params: NoncriticalParams { + target_rate_ms: 2400, + first_block_timeout_ms: 1000, + max_leader_window_desync: 250, + candidate_resolve_rate_limit: 10, + ..Default::default() + }, + ..Default::default() + }; + let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); + let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); + assert_eq!(config, config2); +} + +#[test] +fn test_simplex_config_default_round_trip() { + let config = SimplexConfig::default(); + let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); + let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); + assert_eq!(config, config2); +} + +#[test] +fn test_simplex_config_custom_noncritical_params() { + let config = SimplexConfig { + use_quic: true, + slots_per_leader_window: 6, + noncritical_params: NoncriticalParams { + target_rate_ms: 500, + first_block_timeout_ms: 2000, + max_leader_window_desync: 100, + first_block_timeout_multiplier_bits: (1.5f32).to_bits(), + bad_signature_ban_duration_ms: 10_000, + ..Default::default() + }, + ..Default::default() + }; + let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); + let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); + assert_eq!(config, config2); +} + +// ===================== v1 deserialization backward-compat tests ===================== + +/// Build a raw simplex_config#21 cell by hand (the legacy on-chain format). +fn build_v1_cell(use_quic: bool, target_rate_ms: u32, slots: u32, fbt_ms: u32, mld: u32) -> Cell { + let mut b = BuilderData::new(); + b.append_u8(0x21).unwrap(); + b.append_u8(if use_quic { 1 } else { 0 }).unwrap(); + target_rate_ms.write_to(&mut b).unwrap(); + slots.write_to(&mut b).unwrap(); + fbt_ms.write_to(&mut b).unwrap(); + mld.write_to(&mut b).unwrap(); + b.into_cell().unwrap() +} + +#[test] +fn test_deserialize_v1_cell() { + let cell = build_v1_cell(false, 300, 4, 1000, 100); + let config = SimplexConfig::construct_from_cell(cell).unwrap(); + assert!(!config.use_quic); + assert_eq!(config.slots_per_leader_window, 4); + assert_eq!(config.noncritical_params.target_rate_ms, 300); + assert_eq!(config.noncritical_params.first_block_timeout_ms, 1000); + assert_eq!(config.noncritical_params.max_leader_window_desync, 100); + let d = NoncriticalParams::default(); + assert_eq!( + config.noncritical_params.first_block_timeout_multiplier_bits, + d.first_block_timeout_multiplier_bits + ); +} + +#[test] +fn test_deserialize_v1_cell_with_quic() { + let cell = build_v1_cell(true, 300, 4, 1000, 100); + let config = SimplexConfig::construct_from_cell(cell).unwrap(); + assert!(config.use_quic); +} + +#[test] +fn test_new_consensus_config_all_with_v2() { + let config = SimplexConfig { + use_quic: true, + slots_per_leader_window: 8, + noncritical_params: NoncriticalParams { + target_rate_ms: 2400, + first_block_timeout_ms: 1000, + max_leader_window_desync: 250, + ..Default::default() + }, + ..Default::default() + }; + + let all = NewConsensusConfigAll { mc: Some(config.clone()), shard: Some(config.clone()) }; + let cell = all.write_to_new_cell().unwrap().into_cell().unwrap(); + let parsed = NewConsensusConfigAll::construct_from_cell(cell).unwrap(); + assert_eq!(parsed.mc.unwrap(), config); + assert_eq!(parsed.shard.unwrap(), config); +} + +#[test] +fn test_new_consensus_config_all_mixed_v1_mc_v2_shard() { + let mc_cell = build_v1_cell(false, 300, 4, 1000, 100); + let shard_config = SimplexConfig { + slots_per_leader_window: 6, + noncritical_params: NoncriticalParams { + target_rate_ms: 500, + first_block_timeout_ms: 2000, + ..Default::default() + }, + ..Default::default() + }; + let shard_cell = shard_config.write_to_new_cell().unwrap().into_cell().unwrap(); + + let mut builder = BuilderData::new(); + builder.append_u8(0x10).unwrap(); + builder.append_bit_one().unwrap(); + builder.checked_append_reference(mc_cell).unwrap(); + builder.append_bit_one().unwrap(); + builder.checked_append_reference(shard_cell).unwrap(); + let cell = builder.into_cell().unwrap(); + + let parsed = NewConsensusConfigAll::construct_from_cell(cell).unwrap(); + let mc = parsed.mc.unwrap(); + assert_eq!(mc.noncritical_params.target_rate_ms, 300); + + let shard = parsed.shard.unwrap(); + assert_eq!(shard.noncritical_params.target_rate_ms, 500); + assert_eq!(shard.noncritical_params.first_block_timeout_ms, 2000); + assert_eq!( + shard.noncritical_params.max_leader_window_desync, + NoncriticalParams::default().max_leader_window_desync + ); +} diff --git a/src/ci/sync-test/README.md b/src/ci/sync-test/README.md new file mode 100644 index 0000000..d6aedc8 --- /dev/null +++ b/src/ci/sync-test/README.md @@ -0,0 +1,224 @@ +# Sync Test + +Automated mainnet sync test for the TON Rust Node. Builds a node image from the current commit, deploys it to Kubernetes via the public [`ton-rust-node`](https://github.com/rsquad/ton-rust-node) Helm chart, and waits for the node to fully sync with the network. Reports the result as a GitHub commit status on the triggering commit. + +## How it works + +### Overview + +``` +GitHub Actions (manual trigger, ~5 min) + 1. Build node image โ†’ ghcr.io/rsquad/ton-node:sha- + 2. Set GitHub commit status โ†’ pending + 3. helm upgrade --install โ†’ deploys to ton-synctest namespace + 4. CI exits + +Kubernetes pod (runs for hours) + Container "ton-node": syncs with mainnet + Container "watcher": polls metrics, reports result to GitHub +``` + +### Watcher logic + +The watcher runs as a sidecar container alongside the node. Every 60 seconds it fetches the node's Prometheus metrics and checks sync progress. + +**Metrics used:** + +| Metric | Description | +|--------|-------------| +| `ton_node_engine_sync_status` | Sync state machine (see stages below) | +| `ton_node_engine_last_mc_block_seqno` | Latest applied masterchain block seqno | +| `ton_node_engine_timediff_seconds` | Seconds between now and last applied MC block | +| `ton_node_engine_shards_timediff_seconds` | Seconds between now and MC block last processed by shard client | + +**Sync stages** (`sync_status` values): + +| Value | Name | Description | +|-------|------|-------------| +| 0 | `not_set` | Initial state, node just started | +| 1 | `boot` | Downloading init block proof, key blocks | +| 2 | `load_states` | Downloading and applying persistent states (long phase, seqno does not advance) | +| 3 | `finish_boot` | Boot complete, preparing to sync | +| 4 | `sync_archives` | Syncing via archives (bulk download) | +| 5 | `sync_blocks` | Syncing block-by-block from peers | +| 6 | `synced` | Masterchain caught up, shard client within 16 MC blocks | +| 7 | `checking_db` | DB integrity check in progress | +| 8 | `db_broken` | DB corruption detected | + +**Terminal conditions:** + +| Condition | Trigger | GitHub status | Pod behavior | +|-----------|---------|---------------|--------------| +| Synced | `sync_status = 6` | `success` | Sleeps forever (replaced on next run) | +| DB broken | `sync_status = 8` | `failure` | Sleeps forever (stays for debugging) | +| Timeout | Elapsed > `SYNC_TIMEOUT` (default 24h) | `failure` | Sleeps forever (stays for debugging) | + +**Watcher log output:** + +``` +[watcher] stage=boot seqno=0 mc_timediff=0s shards_timediff=0s elapsed=0h0m +[watcher] stage=boot seqno=47554071 mc_timediff=200000s shards_timediff=200000s elapsed=0h1m +[watcher] stage=load_states seqno=58847563 mc_timediff=67000s shards_timediff=67000s elapsed=0h30m +[watcher] stage=sync_archives seqno=58850000 mc_timediff=10000s shards_timediff=10000s elapsed=1h00m +[watcher] stage=sync_blocks seqno=58870000 mc_timediff=500s shards_timediff=600s elapsed=3h00m +[watcher] stage=synced seqno=58881412 mc_timediff=2s shards_timediff=2s elapsed=3h34m +[watcher] SUCCESS โ€” node synced +``` + +### Debugging failures + +On failure (timeout or DB broken) the pod stays alive. Inspect logs: + +```bash +# Watcher log (sync progress) +kubectl logs synctest-mainnet-0 -c watcher -n ton-synctest + +# Node log (last 200 lines) +kubectl logs synctest-mainnet-0 -c ton-node -n ton-synctest --tail=200 + +# Full node log file (written by log4rs) +kubectl exec -it synctest-mainnet-0 -c ton-node -n ton-synctest -- tail -200 /logs/output.log + +# Useful greps for node log +kubectl exec synctest-mainnet-0 -c ton-node -n ton-synctest -- grep -e boot -e sync /logs/output.log | tail -20 +kubectl exec synctest-mainnet-0 -c ton-node -n ton-synctest -- grep Applied /logs/output.log | tail -20 +``` + +### Re-running + +Each workflow run **deletes the previous deployment** (helm uninstall + PVC cleanup) before deploying fresh. The old watcher catches SIGTERM and sets `failure "Cancelled"` on its commit so no commit is left stuck in `pending`. + +This means: if a sync test is still running and you trigger a new one, the old test is cancelled and its commit gets a red status. Inspect logs before re-running if you need to debug a failure. + +## How to run + +```bash +gh workflow run sync-test.yml -R RSquad/ton-node +``` + +Or: GitHub UI > Actions > Sync Test > Run workflow. + +Check current commit status: + +```bash +gh api repos/RSquad/ton-node/commits//status \ + --jq '.statuses[] | select(.context=="sync-test/mainnet") | {state, description}' +``` + +## Files + +| File | Purpose | +|------|---------| +| `.github/workflows/sync-test.yml` | CI workflow: build image, push to GHCR, helm deploy | +| `ci/sync-test/values.yaml` | Helm values override for the `ton-rust-node` chart | +| `ci/sync-test/watcher.sh` | Sidecar script: poll Prometheus metrics, set GitHub commit status | +| `ci/sync-test/gen-node-config.sh` | Generates node config with random ADNL keys for given IP | +| `ci/sync-test/README.md` | This file | + +## Cluster setup from scratch + +All commands target cluster `velia-sgp1`. The namespace is `ton-synctest`. + +### 1. Create namespace + +```bash +kubectl create ns ton-synctest +``` + +### 2. Image pull secret + +Required for pulling node images from GHCR. + +```bash +kubectl create secret docker-registry ghcr -n ton-synctest \ + --docker-server=ghcr.io \ + --docker-username= \ + --docker-password= +``` + +### 3. GitHub token for commit statuses + +The watcher needs a token to set commit statuses from inside the K8s pod. Create a fine-grained PAT: + +1. Go to https://github.com/settings/tokens?type=beta +2. Repository access: select `RSquad/ton-node` +3. Permissions: **Commit statuses > Read and write** (nothing else) + +```bash +kubectl create secret generic credentials -n ton-synctest \ + --from-literal=GITHUB_TOKEN=github_pat_... +``` + +### 4. Kubeconfig for CI + +The GitHub Actions runner needs kubectl/helm access to the cluster. Create a dedicated Rancher user with minimal permissions: + +1. **Rancher UI > Users & Authentication > Create**: username `synctest-ci`, global role `User-Base` +2. **Create a Project** in cluster `velia-sgp1` containing namespace `ton-synctest` +3. **Add `synctest-ci` as Project Member** to that project +4. **Download kubeconfig** for `synctest-ci` (login as that user in Rancher UI, or via Rancher API) + +Add to GitHub repo secrets (base64-encoded): + +```bash +cat kubeconfig.yaml | base64 | gh secret set SYNCTEST_VELIA_SGP1_KUBECONFIG -R RSquad/ton-node +``` + +### 5. Node IP + +The external IP for the ADNL LoadBalancer. Must match an IP available in the MetalLB pool. + +```bash +gh secret set SYNCTEST_NODE_IP -R RSquad/ton-node -b "" +``` + +CI uses this to generate the node config (ADNL address) and the MetalLB annotation. + +### Summary of resources + +**Kubernetes (ton-synctest namespace):** + +| Resource | Name | Purpose | +|----------|------|---------| +| Namespace | `ton-synctest` | Isolates sync test workloads | +| Secret | `ghcr` | Image pull credentials for GHCR | +| Secret | `credentials` | GitHub PAT for commit status API | +| ConfigMap | `synctest-watcher` | Watcher script (created by CI) | +| Secret | `*-node-configs` | Node config with ADNL keys (created by Helm) | + +**GitHub Secrets:** + +| Secret | Purpose | +|--------|---------| +| `SYNCTEST_VELIA_SGP1_KUBECONFIG` | Kubeconfig for CI to access cluster | +| `SYNCTEST_NODE_IP` | External IP for ADNL service | + +## Configuration reference + +### Sync timeout + +Environment variable `SYNC_TIMEOUT` in `values.yaml` (seconds). Default: `86400` (24 hours). If the node does not reach `sync_status=6` within this window, the test fails. + +### ADNL IP + +Set via GitHub Secret `SYNCTEST_NODE_IP`. CI uses it in both the node config (ADNL address) and MetalLB annotation (LoadBalancer IP). They must match. + +### Helm chart version + +In `.github/workflows/sync-test.yml`, env `HELM_CHART_VERSION`. Must match a published version of `oci://ghcr.io/rsquad/ton-rust-node/helm/node`. + +### Resources + +Inherited from the Helm chart defaults (8 CPU / 32Gi request, 16 CPU / 64Gi limit). Override in `values.yaml` under `resources` if needed. + +### Storage + +DB volume uses the chart default (1Ti, `local-path` storage class). All PVCs have `resourcePolicy: ""` โ€” they are deleted together with the Helm release on `helm uninstall`. + +### Global config + +Uses the mainnet `global.config.json` bundled in the Helm chart. No manual config needed. + +### Logs config + +Uses the default `logs.config.yml` bundled in the Helm chart. Node logs are written to `/logs/output.log` inside the pod. diff --git a/src/ci/sync-test/gen-node-config.sh b/src/ci/sync-test/gen-node-config.sh new file mode 100644 index 0000000..15718e5 --- /dev/null +++ b/src/ci/sync-test/gen-node-config.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# Generates a minimal node config for sync test with random ADNL keys. +# Usage: gen-node-config.sh > node-0.json +set -eu + +IP="${1:?Usage: gen-node-config.sh }" + +DHT_KEY=$(openssl rand -base64 32) +FN_KEY=$(openssl rand -base64 32) +CTL_KEY=$(openssl rand -base64 32) + +cat < + +storage: + main: + resourcePolicy: "" + db: + resourcePolicy: "" + keys: + resourcePolicy: "" + +# Watcher sidecar โ€” polls /healthz, sets GitHub commit status +extraVolumes: + - name: watcher-script + configMap: + name: synctest-watcher + defaultMode: 0755 + +extraContainers: + - name: watcher + image: alpine:3.21 + command: ["/bin/sh", "/scripts/watcher.sh"] + env: + - name: GITHUB_SHA + value: "{{ .Values.synctest.sha }}" + - name: GITHUB_REPO + value: "{{ .Values.synctest.repo }}" + - name: NETWORK + value: mainnet + - name: SYNC_TIMEOUT + value: "86400" + - name: METRICS_PORT + value: "9100" + envFrom: + - secretRef: + name: credentials + resources: + requests: { cpu: 50m, memory: 64Mi } + limits: { cpu: 200m, memory: 128Mi } + volumeMounts: + - name: watcher-script + mountPath: /scripts + readOnly: true diff --git a/src/ci/sync-test/watcher.sh b/src/ci/sync-test/watcher.sh new file mode 100644 index 0000000..b652a25 --- /dev/null +++ b/src/ci/sync-test/watcher.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# +# Sync test watcher +# +# Runs as a sidecar alongside the TON node. Polls Prometheus metrics +# every 60 seconds and decides whether the node has synced, failed, +# or timed out. Reports the result as a GitHub commit status. +# +# Metrics used (from /metrics on the node's metrics port): +# ton_node_engine_sync_status โ€” sync state machine (6 = synced, 8 = db broken) +# ton_node_engine_last_mc_block_seqno โ€” latest masterchain block applied +# ton_node_engine_timediff_seconds โ€” seconds between now and last MC block +# ton_node_engine_shards_timediff_seconds โ€” seconds between now and MC block +# last processed by shard client +# +# Sync stages (sync_status values, ordered by normal flow): +# 0 = not_set Initial state, node just started +# 1 = boot Downloading init block proof, key blocks +# 2 = load_states Downloading and applying persistent states (long, no seqno progress) +# 3 = finish_boot Boot complete, preparing to sync +# 4 = sync_archives Syncing via archives (bulk download) +# 5 = sync_blocks Syncing block-by-block from peers +# 6 = synced Masterchain caught up, shard client within 16 MC blocks +# 7 = checking_db DB integrity check in progress +# 8 = db_broken DB corruption detected +# +# Behavior on terminal states: +# synced โ†’ set GitHub commit status "success", then sleep forever +# db_broken โ†’ set GitHub commit status "failure", then sleep forever +# timeout โ†’ set GitHub commit status "failure", then sleep forever +# +# On failure the pod stays alive so engineers can inspect logs: +# kubectl exec -it -c ton-node -n ton-synctest -- tail -100 /logs/output.log +# +# The next workflow run replaces the pod via `helm upgrade`. +# +# Required env: METRICS_PORT, SYNC_TIMEOUT, NETWORK, GITHUB_TOKEN, GITHUB_SHA, GITHUB_REPO + +set -eu + +apk add --no-cache curl jq >/dev/null 2>&1 + +# --------------------------------------------------------------------------- +# Graceful shutdown: if pod is killed before sync completes, report failure. +# --------------------------------------------------------------------------- + +FINISHED=false + +cleanup() { + if [ "$FINISHED" = "false" ]; then + elapsed=$(( $(date +%s) - ${START:-0} )) + github_status failure "Cancelled after $(fmt $elapsed)" + echo "[watcher] CANCELLED โ€” pod terminated before sync completed" + fi + exit 0 +} + +trap cleanup TERM INT + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Set commit status on the GitHub commit that triggered this test. +github_status() { + local state="$1" description="$2" + curl -sf -X POST \ + "https://api.github.com/repos/${GITHUB_REPO}/statuses/${GITHUB_SHA}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -cn --arg s "$state" --arg d "$description" \ + --arg c "sync-test/${NETWORK}" \ + '{state:$s, description:$d, context:$c}')" >/dev/null 2>&1 || true +} + +# Format seconds as "Xh Ym". +fmt() { + local h=$(($1 / 3600)) m=$((($1 % 3600) / 60)) + if [ "$h" -gt 0 ]; then echo "${h}h${m}m"; else echo "${m}m"; fi +} + +# Human-readable name for sync_status value. +stage_name() { + case "$1" in + 0) echo "not_set" ;; 1) echo "boot" ;; 2) echo "load_states" ;; + 3) echo "finish_boot" ;; 4) echo "sync_archives" ;; 5) echo "sync_blocks" ;; + 6) echo "synced" ;; 7) echo "checking_db" ;; 8) echo "db_broken" ;; + *) echo "unknown($1)" ;; + esac +} + +# Extract a gauge value from Prometheus text output. +# Handles both "name value" and "name{labels} value" formats. +# Usage: metric "$prometheus_text" "metric_name" +metric() { + echo "$1" | awk -v m="$2" '$1 ~ "^" m "($|\\{)" && $1 !~ /^#/ { print int($2); exit }' +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +METRICS="http://localhost:${METRICS_PORT}/metrics" +HEALTHZ="http://localhost:${METRICS_PORT}/healthz" +POLL_INTERVAL=60 + +# Wait for the node's HTTP server to come up. +while ! curl -sf "$HEALTHZ" >/dev/null 2>&1; do sleep 5; done + +START=$(date +%s) + +while true; do + # Fetch all metrics in one request. + prom=$(curl -sf "$METRICS" 2>/dev/null) || { sleep "$POLL_INTERVAL"; continue; } + + status=$(metric "$prom" "ton_node_engine_sync_status") + seqno=$(metric "$prom" "ton_node_engine_last_mc_block_seqno") + timediff=$(metric "$prom" "ton_node_engine_timediff_seconds") + shards_td=$(metric "$prom" "ton_node_engine_shards_timediff_seconds") + status=${status:-0} + seqno=${seqno:-0} + timediff=${timediff:--} + shards_td=${shards_td:--} + elapsed=$(( $(date +%s) - START )) + + echo "[watcher] stage=$(stage_name "$status") seqno=$seqno mc_timediff=${timediff}s shards_timediff=${shards_td}s elapsed=$(fmt $elapsed)" + + # --- Terminal states --- + + # sync_status=6: node considers itself synced (MC timediff < 600s, + # shard client within 16 MC blocks of masterchain). + if [ "$status" = "6" ]; then + FINISHED=true + github_status success "Synced in $(fmt $elapsed) (seqno $seqno, mc_timediff ${timediff}s)" + echo "[watcher] SUCCESS โ€” node synced" + exec tail -f /dev/null + fi + + # sync_status=8: database corruption detected. + if [ "$status" = "8" ]; then + FINISHED=true + github_status failure "DB broken (seqno $seqno, $(fmt $elapsed))" + echo "[watcher] FAILURE โ€” DB broken, pod stays alive for debugging" + exec tail -f /dev/null + fi + + # Timeout: node did not sync within the allowed window. + if [ "$elapsed" -gt "$SYNC_TIMEOUT" ]; then + FINISHED=true + github_status failure "Timeout after $(fmt $elapsed): $(stage_name "$status"), mc_timediff=${timediff}s, shards=${shards_td}s" + echo "[watcher] FAILURE โ€” timeout after $(fmt $elapsed), pod stays alive for debugging" + exec tail -f /dev/null + fi + + sleep "$POLL_INTERVAL" & + wait $! +done diff --git a/src/node/catchain/src/receiver.rs b/src/node/catchain/src/receiver.rs index eb154a1..bc8e1b0 100644 --- a/src/node/catchain/src/receiver.rs +++ b/src/node/catchain/src/receiver.rs @@ -304,7 +304,7 @@ impl Receiver for ReceiverWrapper { self.out_bytes.increment(payload.data().len() as u64); // Send broadcast through overlay directly - self.overlay.send_broadcast_fec_ex(&self.local_adnl_id, &self.local_id, payload); + self.overlay.send_broadcast_fec_ex(&self.local_adnl_id, &self.local_id, payload, None); } /// Send query via RLDP @@ -3457,7 +3457,7 @@ impl ReceiverImpl { //overlay creation - log::debug!( + log::info!( "Receiver: starting up overlay for session {:x} with ID {:x}, short_id {}", session_id, overlay_id, @@ -3486,7 +3486,7 @@ impl ReceiverImpl { let overlay_replay_listener: Arc = overlay_listener.clone(); - log::debug!( + log::info!( "Receiver: starting up overlay for session {:x} with ID/incarnation {:x}, short_id {}", session_id, overlay_id, diff --git a/src/node/consensus-common/src/adnl_overlay.rs b/src/node/consensus-common/src/adnl_overlay.rs index 4029141..a19d058 100644 --- a/src/node/consensus-common/src/adnl_overlay.rs +++ b/src/node/consensus-common/src/adnl_overlay.rs @@ -36,15 +36,16 @@ use ton_api::{ deserialize_boxed, serialize_bare, serialize_boxed, serialize_boxed_append, ton::{ catchain::BroadcastWrapper, + consensus::simplex::{Certificate as SimplexCertificate, Vote as SimplexVote}, overlay::{ - broadcast::BroadcastTwostepSimple, + broadcast::BroadcastTwostepSimple, broadcast_twostep::id::Id as BroadcastTwostepId, broadcast_twostep_simple::tosign::ToSign as BroadcastTwostepSimpleToSign, Certificate as OverlayCertificate, }, }, - BoxedSerialize, IntoBoxed, TLObject, + BoxedSerialize, IntoBoxed, Serializer, TLObject, }; -use ton_block::{error, fail}; +use ton_block::{error, fail, sha256_digest, KeyId, KeyOption, UInt256}; const LOG_TARGET: &str = "consensus_adnl_overlay"; @@ -120,7 +121,8 @@ impl TaskProcessor { const TASK_AGE_WARNING_THRESHOLD: Duration = Duration::from_secs(5); const WARNING_THROTTLE_INTERVAL: Duration = Duration::from_secs(10); - let mut last_warning_time = Instant::now() - WARNING_THROTTLE_INTERVAL; // Allow first warning immediately + // Allow first warning immediately + let mut last_warning_time = Instant::now() - WARNING_THROTTLE_INTERVAL; log::debug!(target: LOG_TARGET, "TaskProcessor loop started: {}", name_clone); @@ -141,8 +143,14 @@ impl TaskProcessor { if let Ok(elapsed) = task_desc.creation_time.elapsed() { if elapsed > TASK_AGE_WARNING_THRESHOLD { let now = Instant::now(); - if now.duration_since(last_warning_time) >= WARNING_THROTTLE_INTERVAL { - log::warn!(target: LOG_TARGET, "TaskProcessor {}: Processing delayed task (age: {:?})", name_clone, elapsed); + if now.duration_since(last_warning_time) + >= WARNING_THROTTLE_INTERVAL + { + log::warn!( + target: LOG_TARGET, + "TaskProcessor {name_clone}: \ + Processing delayed task (age: {elapsed:?})" + ); last_warning_time = now; } } @@ -153,7 +161,10 @@ impl TaskProcessor { } Ok(None) => { // Channel closed - log::debug!(target: LOG_TARGET, "TaskProcessor channel closed: {}", name_clone); + log::debug!( + target: LOG_TARGET, + "TaskProcessor channel closed: {name_clone}" + ); break; } Err(_) => { @@ -178,7 +189,11 @@ impl TaskProcessor { F: FnOnce() -> Pin + Send + 'static>> + Send + 'static, { if self.stop_requested.load(Ordering::Relaxed) { - log::trace!(target: LOG_TARGET, "TaskProcessor {} stop requested, ignoring posted closure", self.name); + log::trace!( + target: LOG_TARGET, + "TaskProcessor {} stop requested, ignoring posted closure", + self.name + ); return; } @@ -211,14 +226,24 @@ impl TaskProcessor { while !self.is_stopped.load(Ordering::Relaxed) { if wait_count % 10 == 0 { // Log every second - log::info!(target: LOG_TARGET, "TaskProcessor {}: Waiting for stop completion... ({}ms)", self.name, wait_count * 100); + log::info!( + target: LOG_TARGET, + "TaskProcessor {}: Waiting for stop completion... ({}ms)", + self.name, + wait_count * 100 + ); } std::thread::sleep(STOP_WAIT_DELAY); wait_count += 1; } - log::debug!(target: LOG_TARGET, "TaskProcessor {}: Stopped after {}ms", self.name, wait_count * 100); + log::debug!( + target: LOG_TARGET, + "TaskProcessor {}: Stopped after {}ms", + self.name, + wait_count * 100 + ); } } @@ -251,7 +276,10 @@ impl TaskProcessorManager { num_tags: u32, runtime_handle: tokio::runtime::Handle, ) -> Self { - log::info!(target: LOG_TARGET, "Creating TaskProcessorManager {} with {} tags", name, num_tags); + log::info!( + target: LOG_TARGET, + "Creating TaskProcessorManager {name} with {num_tags} tags" + ); let metrics_handle = MetricsHandle::new(Some(Duration::from_secs(30))); let mut processors = HashMap::new(); @@ -272,7 +300,11 @@ impl TaskProcessorManager { processors.insert(tag, processor); } - log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Created and started {} TaskProcessors", name, processors.len()); + log::info!( + target: LOG_TARGET, + "TaskProcessorManager {name}: Created and started {} TaskProcessors", + processors.len() + ); let manager = Self { name: name.clone(), @@ -296,7 +328,10 @@ impl TaskProcessorManager { let is_stopped = self.is_stopped.clone(); let manager_name = self.name.clone(); - log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Starting metrics reporting", manager_name); + log::debug!( + target: LOG_TARGET, + "TaskProcessorManager {manager_name}: Starting metrics reporting" + ); let _handle = self.runtime_handle.spawn(async move { const METRICS_DUMP_PERIOD: Duration = Duration::from_secs(30); @@ -304,7 +339,10 @@ impl TaskProcessorManager { let mut next_metrics_dump_time = SystemTime::now() + METRICS_DUMP_PERIOD; - log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Metrics loop started", manager_name); + log::debug!( + target: LOG_TARGET, + "TaskProcessorManager {manager_name}: Metrics loop started" + ); while !stop_requested.load(Ordering::Relaxed) { tokio::time::sleep(SLEEP_PERIOD).await; @@ -316,15 +354,23 @@ impl TaskProcessorManager { let mut metrics_dumper = Self::create_metrics_dumper(&processor_names); metrics_dumper.update(&metrics_handle); - log::debug!(target: LOG_TARGET, "TaskProcessorManager {} metrics:", manager_name); - metrics_dumper.dump(|string| log::debug!(target: LOG_TARGET, "{}: {}", manager_name, string)); + log::debug!( + target: LOG_TARGET, + "TaskProcessorManager {manager_name} metrics:" + ); + metrics_dumper.dump( + |string| log::debug!(target: LOG_TARGET, "{manager_name}: {string}"), + ); } next_metrics_dump_time = SystemTime::now() + METRICS_DUMP_PERIOD; } } - log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Metrics loop finished", manager_name); + log::debug!( + target: LOG_TARGET, + "TaskProcessorManager {manager_name}: Metrics loop finished" + ); // Mark as actually stopped is_stopped.store(true, Ordering::Relaxed); @@ -360,20 +406,32 @@ impl TaskProcessorManager { F: FnOnce() -> Pin + Send + 'static>> + Send + 'static, { if self.stop_requested.load(Ordering::Relaxed) { - log::trace!(target: LOG_TARGET, "TaskProcessorManager {} is stopped, ignoring posted closure", self.name); + log::trace!( + target: LOG_TARGET, + "TaskProcessorManager {} is stopped, ignoring posted closure", + self.name + ); return; } if let Some(processor) = self.processors.get(&tag) { processor.post_closure(closure); } else { - log::warn!(target: LOG_TARGET, "TaskProcessorManager {}: No TaskProcessor found for tag {}", self.name, tag); + log::warn!( + target: LOG_TARGET, + "TaskProcessorManager {}: No TaskProcessor found for tag {tag}", + self.name + ); } } /// Stop all task processors asynchronously pub fn stop_async(&self) { - log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping asynchronously", self.name); + log::info!( + target: LOG_TARGET, + "TaskProcessorManager {}: Stopping asynchronously", + self.name + ); self.stop_requested.store(true, Ordering::Relaxed); @@ -385,7 +443,11 @@ impl TaskProcessorManager { /// Stop all task processors and wait for completion pub fn stop(&self) { - log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping synchronously", self.name); + log::info!( + target: LOG_TARGET, + "TaskProcessorManager {}: Stopping synchronously", + self.name + ); self.stop_async(); @@ -396,7 +458,12 @@ impl TaskProcessorManager { while !self.is_stopped.load(Ordering::Relaxed) { if wait_count % 10 == 0 { // Log every second - log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Waiting for stop completion... ({}ms)", self.name, wait_count * 100); + log::info!( + target: LOG_TARGET, + "TaskProcessorManager {}: Waiting for stop completion... ({}ms)", + self.name, + wait_count * 100 + ); } std::thread::sleep(STOP_WAIT_DELAY); @@ -404,20 +471,39 @@ impl TaskProcessorManager { } // Manually stop all task processors - log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping {} TaskProcessors", self.name, self.processors.len()); + log::debug!( + target: LOG_TARGET, + "TaskProcessorManager {}: Stopping {} TaskProcessors", + self.name, + self.processors.len() + ); for (tag, processor) in &self.processors { - log::trace!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping TaskProcessor for tag={}", self.name, tag); + log::trace!( + target: LOG_TARGET, + "TaskProcessorManager {}: Stopping TaskProcessor for tag={tag}", + self.name + ); processor.stop(); } - log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Stopped after {}ms", self.name, wait_count * 100); + log::info!( + target: LOG_TARGET, + "TaskProcessorManager {}: Stopped after {}ms", + self.name, + wait_count * 100 + ); } } impl Drop for TaskProcessorManager { fn drop(&mut self) { - log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Dropping with {} processors", self.name, self.processors.len()); + log::info!( + target: LOG_TARGET, + "TaskProcessorManager {}: Dropping with {} processors", + self.name, + self.processors.len() + ); self.stop(); log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Dropped", self.name); } @@ -428,8 +514,8 @@ impl Drop for TaskProcessorManager { */ struct Peer { - src_adnl_addr: Arc, - dst_adnl_addr: Arc, + src_adnl_addr: Arc, + dst_adnl_addr: Arc, overlay_node: Arc, dht_node: Arc, is_stop_requested: Arc, @@ -439,8 +525,8 @@ struct Peer { impl Peer { /// Create new peer and start address resolution loop fn new( - src_adnl_addr: Arc, - dst_adnl_addr: Arc, + src_adnl_addr: Arc, + dst_adnl_addr: Arc, overlay_node: Arc, dht_node: Arc, runtime_handle: tokio::runtime::Handle, @@ -497,7 +583,10 @@ impl Peer { &self.src_adnl_addr, &[self.dst_adnl_addr.clone()], ) { - log::warn!(target: LOG_TARGET, "Error deleting old peer address: {:?}", e); + log::warn!( + target: LOG_TARGET, + "Error deleting old peer address: {e:?}" + ); } } @@ -509,16 +598,28 @@ impl Peer { if let Err(e) = add_result { log::warn!(target: LOG_TARGET, "Error adding peer address: {:?}", e); } else { - log::debug!(target: LOG_TARGET, "Peer address updated: {:?}", self.dst_adnl_addr); + log::debug!( + target: LOG_TARGET, + "Peer address updated: {:?}", + self.dst_adnl_addr + ); current_addr = Some(()); // Mark that we have an address } } } Ok(None) => { - log::trace!(target: LOG_TARGET, "Peer address not found in DHT: {:?}", self.dst_adnl_addr); + log::trace!( + target: LOG_TARGET, + "Peer address not found in DHT: {:?}", + self.dst_adnl_addr + ); } Err(e) => { - log::warn!(target: LOG_TARGET, "DHT fetch error for peer {:?}: {:?}", self.dst_adnl_addr, e); + log::warn!( + target: LOG_TARGET, + "DHT fetch error for peer {:?}: {e:?}", + self.dst_adnl_addr + ); } } } @@ -542,7 +643,12 @@ impl Peer { /// Stop peer resolution loop synchronously and wait for completion fn stop(&self) { - log::trace!(target: LOG_TARGET, "Stopping Peer: {:?} -> {:?}", self.src_adnl_addr, self.dst_adnl_addr); + log::trace!( + target: LOG_TARGET, + "Stopping Peer: {:?} -> {:?}", + self.src_adnl_addr, + self.dst_adnl_addr + ); // Stop the resolution loop self.stop_async(); @@ -563,7 +669,12 @@ impl Peer { } } - log::trace!(target: LOG_TARGET, "Peer resolution loop finished: {:?} -> {:?}", self.src_adnl_addr, self.dst_adnl_addr); + log::trace!( + target: LOG_TARGET, + "Peer resolution loop finished: {:?} -> {:?}", + self.src_adnl_addr, + self.dst_adnl_addr + ); } } @@ -579,8 +690,7 @@ impl Drop for Peer { */ struct PeerStorage { - peers: - Arc, Arc), Weak>>>, + peers: Arc, Arc), Weak>>>, } impl PeerStorage { @@ -591,8 +701,8 @@ impl PeerStorage { /// Get or create peer for given src/dst ADNL addresses fn get_peer( self: &Arc, - src_adnl_addr: Arc, - dst_adnl_addr: Arc, + src_adnl_addr: Arc, + dst_adnl_addr: Arc, overlay_node: Arc, dht_node: Arc, runtime_handle: tokio::runtime::Handle, @@ -666,7 +776,10 @@ impl AdnlOverlayConsumer { overlay: Weak, stop_requested: Arc, ) -> Self { - log::debug!(target: LOG_TARGET, "Creating AdnlOverlayConsumer for overlay_id={}", overlay_id); + log::debug!( + target: LOG_TARGET, + "Creating AdnlOverlayConsumer for overlay_id={overlay_id}" + ); Self { overlay_id, overlay, stop_requested } } } @@ -691,9 +804,9 @@ impl Subscriber for AdnlOverlayConsumer { }; // Handle simplex direct messages (may come as custom messages in some paths) - let simplex_kind = if object.is::() { + let simplex_kind = if object.is::() { Some("vote") - } else if object.is::() { + } else if object.is::() { Some("certificate") } else { None @@ -720,7 +833,11 @@ impl Subscriber for AdnlOverlayConsumer { async fn try_consume_query(&self, query: TLObject, _peers: &AdnlPeers) -> Result { // Check if overlay is stopped if self.stop_requested.load(Ordering::Relaxed) { - log::warn!(target: LOG_TARGET, "AdnlOverlayConsumer: Overlay {} was stopped!", &self.overlay_id); + log::warn!( + target: LOG_TARGET, + "AdnlOverlayConsumer: Overlay {} was stopped!", + &self.overlay_id + ); fail!("Overlay {} was stopped!", &self.overlay_id); } @@ -728,7 +845,11 @@ impl Subscriber for AdnlOverlayConsumer { if let Some(overlay) = self.overlay.upgrade() { overlay.process_query(query, _peers).await } else { - log::warn!(target: LOG_TARGET, "AdnlOverlayConsumer: Overlay {} was dropped!", &self.overlay_id); + log::warn!( + target: LOG_TARGET, + "AdnlOverlayConsumer: Overlay {} was dropped!", + &self.overlay_id + ); fail!("Overlay {} was dropped!", &self.overlay_id); } } @@ -739,11 +860,11 @@ impl Subscriber for AdnlOverlayConsumer { */ struct AdnlOverlay { - stack: Arc, //ADNL network stack - overlay_id: Arc, //private overlay short identifier - local_id: PublicKeyHash, //local validator key hash - local_validator_key: Arc, //local validator key for signing broadcasts - local_adnl_key: Arc, //local ADNL key for two-step broadcast signing + stack: Arc, //ADNL network stack + overlay_id: Arc, //private overlay short identifier + local_id: PublicKeyHash, //local validator key hash + local_validator_key: Arc, //local validator key for signing broadcasts + local_adnl_key: Arc, //local ADNL key for two-step broadcast signing adnl_to_validator: HashMap, //ADNL key hash โ†’ validator key hash all_node_ids: Vec, //all node ADNL IDs in the overlay for multicast emulation of broadcast messages listener: ConsensusOverlayListenerPtr, //consensus overlay listener for incoming events @@ -785,7 +906,7 @@ impl AdnlOverlay { ); // Find local ADNL key from nodes by matching local_id - let mut local_adnl_key: Option> = None; + let mut local_adnl_key: Option> = None; let mut peers = Vec::new(); for node in nodes { @@ -975,7 +1096,11 @@ impl AdnlOverlay { .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) .is_err() { - log::trace!(target: LOG_TARGET, "AdnlOverlay already stopped: overlay_id={}", self.overlay_id); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay already stopped: overlay_id={}", + self.overlay_id + ); return; // Already stopped } @@ -993,7 +1118,11 @@ impl AdnlOverlay { } }); - log::trace!(target: LOG_TARGET, "AdnlOverlay: Will cleanup {} peers on drop", self.peers.len()); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay: Will cleanup {} peers on drop", + self.peers.len() + ); log::debug!( target: LOG_TARGET, @@ -1005,7 +1134,11 @@ impl AdnlOverlay { /// Process incoming query from consumer pub async fn process_query(&self, query: TLObject, peers: &AdnlPeers) -> Result { - log::trace!(target: LOG_TARGET, "AdnlOverlay::process_query: overlay_id={}", self.overlay_id); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay::process_query: overlay_id={}", + self.overlay_id + ); let now = Instant::now(); let data = serialize_boxed(&query).map_err(|e| { @@ -1049,16 +1182,18 @@ impl AdnlOverlay { Box::new(move |result| { // Check if stopped before responding if stop_requested_clone.load(Ordering::Relaxed) { - log::trace!(target: LOG_TARGET, "AdnlOverlay: Query response cancelled - overlay stopped"); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay: Query response cancelled - overlay stopped" + ); wait_for_response.respond(None); return; } // Transform BlockPayloadPtr result to Answer let answer_result = result.and_then(|payload| { - deserialize_boxed(payload.data()).map(|answer| { - Some(Answer::Object(answer.into())) - }) + deserialize_boxed(payload.data()) + .map(|answer| Some(Answer::Object(answer.into()))) }); wait_for_response.respond(Some(answer_result)); }), @@ -1066,16 +1201,25 @@ impl AdnlOverlay { } // Wait for response - let res = wait.wait(&mut queue_reader, true).await + let res = wait + .wait(&mut queue_reader, true) + .await .ok_or_else(|| { - log::warn!(target: LOG_TARGET, "AdnlOverlay: Waiting returned an internal error (query: {:?})", query); + log::warn!( + target: LOG_TARGET, + "AdnlOverlay: Waiting returned an internal error (query: {query:?})" + ); error!("Waiting returned an internal error!") })? .ok_or_else(|| error!("Answer was not set!"))?; // Log timing and metrics let elapsed = now.elapsed(); - log::trace!(target: LOG_TARGET, "AdnlOverlay: query elapsed: {}ms", elapsed.as_millis()); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay: query elapsed: {}ms", + elapsed.as_millis() + ); metrics::histogram!("ton_node_network_consensus_overlay_query_seconds").record(elapsed); Ok(TimedAnswer { @@ -1120,7 +1264,11 @@ impl AdnlOverlay { /// Start broadcast listeners (similar to CatchainClient::run_wait_broadcast) pub fn run_wait_broadcast(self: Arc) { - log::trace!(target: LOG_TARGET, "Starting broadcast listeners for overlay_id={}", self.overlay_id); + log::trace!( + target: LOG_TARGET, + "Starting broadcast listeners for overlay_id={}", + self.overlay_id + ); let overlay_id = self.overlay_id.clone(); let overlay = Arc::downgrade(&self); @@ -1185,59 +1333,86 @@ impl AdnlOverlay { // Spawn task for consensus broadcasts self.runtime_handle.spawn(async move { - log::trace!(target: LOG_TARGET, "AdnlOverlay::wait_consensus_broadcast started for overlay_id={}", overlay_id); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay::wait_consensus_broadcast started for overlay_id={overlay_id}" + ); let receiver = overlay_node.clone(); let consensus_listener = listener.clone(); loop { if stop_requested2.load(Ordering::Relaxed) { - log::trace!(target: LOG_TARGET, "AdnlOverlay::wait_consensus_broadcast stopping for overlay_id={}", overlay_id); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay::wait_consensus_broadcast stopping for overlay_id={overlay_id}" + ); break; } let message = receiver.wait_for_catchain(&overlay_id).await; match message { Ok(Some((catchain_block_update, inner_update, source_id))) => { - log::trace!(target: LOG_TARGET, "AdnlOverlay: catchain broadcast ValidatorSession_BlockUpdate received"); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay: catchain broadcast ValidatorSession_BlockUpdate received" + ); if let Some(listener) = consensus_listener.upgrade() { // Serialize catchain block update and inner update similar to reference let mut data: crate::RawBuffer = crate::RawBuffer::default(); - let mut serializer = ton_api::Serializer::new(&mut data); + let mut serializer = Serializer::new(&mut data); match serializer.write_boxed(&catchain_block_update.into_boxed()) { Ok(_) => { match inner_update { CatchainData::Catchain(upd) => { if let Err(e) = serializer.write_boxed(&upd.into_boxed()) { - log::error!(target: LOG_TARGET, "AdnlOverlay: Failed to serialize catchain update: {}", e); + log::error!( + target: LOG_TARGET, + "AdnlOverlay: Failed to serialize catchain update: {e}" + ); continue; } } CatchainData::ValidatorSession(upd) => { if let Err(e) = serializer.write_boxed(&upd.into_boxed()) { - log::error!(target: LOG_TARGET, "AdnlOverlay: Failed to serialize validator session update: {}", e); + log::error!( + target: LOG_TARGET, + "AdnlOverlay: Failed to serialize validator session update: {e}" + ); continue; } } } let data = crate::ConsensusCommonFactory::create_block_payload(data); - log::trace!(target: LOG_TARGET, "AdnlOverlay: routing consensus broadcast to listener via on_message"); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay: routing consensus broadcast to listener via on_message" + ); listener.on_message(source_id, &data); } Err(e) => { - log::error!(target: LOG_TARGET, "AdnlOverlay: Failed to serialize catchain block update: {}", e); + log::error!( + target: LOG_TARGET, + "AdnlOverlay: Failed to serialize catchain block update: {e}" + ); } } } } Ok(None) => { - log::trace!(target: LOG_TARGET, "AdnlOverlay::wait_consensus_broadcast finished for overlay_id={}", overlay_id); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay::wait_consensus_broadcast finished for overlay_id={overlay_id}" + ); break; } Err(e) => { - log::error!(target: LOG_TARGET, "AdnlOverlay: consensus broadcast error: {}", e); + log::error!( + target: LOG_TARGET, + "AdnlOverlay: consensus broadcast error: {e}" + ); } } } @@ -1283,7 +1458,11 @@ impl ConsensusOverlay for AdnlOverlay { _is_retransmission: bool, ) { if self.stop_requested.load(Ordering::Relaxed) { - log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {} was stopped!", &self.overlay_id); + log::warn!( + target: LOG_TARGET, + "AdnlOverlay: Overlay {} was stopped!", + &self.overlay_id + ); return; } @@ -1451,21 +1630,24 @@ impl ConsensusOverlay for AdnlOverlay { let mut query_data = overlay_node.get_query_prefix(&overlay_id)?; serialize_boxed_append(&mut query_data, &query_body)?; - let (data, _) = overlay_node.query_via_rldp( - &dst_adnl_id, - &TaggedByteSlice { - object: &query_data[..], - #[cfg(feature = "telemetry")] - tag: query_body.bare_object().constructor(), - }, - &overlay_id, - Some(max_answer_size), - v2, - None, - ).await?; + let (data, _) = overlay_node + .query_via_rldp( + &dst_adnl_id, + &TaggedByteSlice { + object: &query_data[..], + #[cfg(feature = "telemetry")] + tag: query_body.bare_object().constructor(), + }, + &overlay_id, + Some(max_answer_size), + v2, + None, + ) + .await?; let data = data.ok_or_else(|| error!("answer is None!"))?; Ok(crate::ConsensusCommonFactory::create_block_payload(data)) - }.await; + } + .await; log::info!(target: LOG_TARGET, "AdnlOverlay::send_query_via_rldp: {:?}", result); @@ -1473,7 +1655,10 @@ impl ConsensusOverlay for AdnlOverlay { if !stop_requested.load(Ordering::Relaxed) { response_callback(result); } else { - log::trace!(target: LOG_TARGET, "AdnlOverlay: Skipping RLDP query callback - overlay stopped"); + log::trace!( + target: LOG_TARGET, + "AdnlOverlay: Skipping RLDP query callback - overlay stopped" + ); } }); } @@ -1484,6 +1669,7 @@ impl ConsensusOverlay for AdnlOverlay { sender_id: &PublicKeyHash, _send_as: &PublicKeyHash, payload: BlockPayloadPtr, + extra: Option>, ) { let overlay_id = self.overlay_id.clone(); let stop_requested = self.stop_requested.clone(); @@ -1503,39 +1689,34 @@ impl ConsensusOverlay for AdnlOverlay { payload.data().len(), self.all_node_ids.len(), ); - // Build C++-compatible overlay.broadcastTwostepSimple instead of - // the Rust-only catchain.BroadcastWrapper. This uses the local ADNL - // key for signing (matching C++ Simplex behaviour) so that both Rust - // and C++ nodes can verify and deliver the broadcast. let result = (|| -> Result<()> { let data = payload.data().to_vec(); + let extra = extra.unwrap_or_default(); let date = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i32; let flags: i32 = 0; - // Compute broadcast_id for to_sign - let data_hash = ton_block::sha256_digest(&data); + let data_hash = sha256_digest(&data); let bcast_id = { - use ton_api::ton::overlay::broadcast_twostep::id::Id as BroadcastTwostepId; let id = BroadcastTwostepId { date, flags, - src: ton_block::UInt256::from_slice(self.local_adnl_key.id().data()), - src_adnl_id: ton_block::UInt256::from_slice( - self.local_adnl_key.id().data(), - ), - data_hash: ton_block::UInt256::with_array(data_hash), + src: UInt256::from_slice(self.local_adnl_key.id().data()), + src_adnl_id: UInt256::from_slice(self.local_adnl_key.id().data()), + data_hash: UInt256::with_array(data_hash), + // Broadcast simulation over TCP, no partitioning + data_size: data.len() as i32, part_size: data.len() as i32, + extra: extra.clone(), }; let id_bytes = serialize_bare(&id)?; - ton_block::sha256_digest(&id_bytes) + sha256_digest(&id_bytes) }; - // Sign: BroadcastTwostepSimpleToSign { id, data } let to_sign = BroadcastTwostepSimpleToSign { - id: ton_block::UInt256::with_array(bcast_id), + id: UInt256::with_array(bcast_id), data: data.clone(), }; let to_sign_bytes = serialize_bare(&to_sign)?; @@ -1546,9 +1727,10 @@ impl ConsensusOverlay for AdnlOverlay { date, flags, src: (&self.local_adnl_key).try_into()?, - src_adnl_id: ton_block::UInt256::from_slice(self.local_adnl_key.id().data()), + src_adnl_id: UInt256::from_slice(self.local_adnl_key.id().data()), certificate: OverlayCertificate::Overlay_EmptyCertificate, data, + extra, signature, } .into_boxed(); @@ -1590,7 +1772,10 @@ impl ConsensusOverlay for AdnlOverlay { runtime_handle.spawn(async move { if stop_requested.load(Ordering::Relaxed) { - log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {} was stopped!", &overlay_id); + log::warn!( + target: LOG_TARGET, + "AdnlOverlay: Overlay {overlay_id} was stopped!" + ); return; } @@ -1600,15 +1785,20 @@ impl ConsensusOverlay for AdnlOverlay { tag: 0x80000002, // Catchain broadcast }; - let result = overlay_node.broadcast( - &overlay_id, - &msg_tagged, - Some(&local_validator_key), - 0, - AdnlSendMethod::Fast, - ).await; + let result = overlay_node + .broadcast( + &overlay_id, + &msg_tagged, + Some(&local_validator_key), + 0, + AdnlSendMethod::Fast, + ) + .await; - log::debug!(target: LOG_TARGET, "AdnlOverlay::send_broadcast_fec_ex status: {:?}", result); + log::debug!( + target: LOG_TARGET, + "AdnlOverlay::send_broadcast_fec_ex status: {result:?}" + ); }); } } @@ -1717,7 +1907,10 @@ impl ConsensusOverlayManager for AdnlOverlayManager { // Add to managed overlays atomically overlays.insert(overlay_short_id.clone(), overlay.clone()); - log::trace!(target: LOG_TARGET, "Successfully started overlay: overlay_id={}", overlay_short_id); + log::trace!( + target: LOG_TARGET, + "Successfully started overlay: overlay_id={overlay_short_id}" + ); overlay }; @@ -1741,7 +1934,10 @@ impl ConsensusOverlayManager for AdnlOverlayManager { overlay_impl.stop(); self.overlays.lock().remove(overlay_short_id); } else { - log::warn!(target: LOG_TARGET, "Cannot downcast overlay to AdnlOverlay: overlay_id={}", overlay_short_id); + log::warn!( + target: LOG_TARGET, + "Cannot downcast overlay to AdnlOverlay: overlay_id={overlay_short_id}" + ); } } } diff --git a/src/node/consensus-common/src/dummy_catchain_overlay.rs b/src/node/consensus-common/src/dummy_catchain_overlay.rs index bdfabfe..85d184b 100644 --- a/src/node/consensus-common/src/dummy_catchain_overlay.rs +++ b/src/node/consensus-common/src/dummy_catchain_overlay.rs @@ -93,6 +93,7 @@ impl ConsensusOverlay for DummyConsensusOverlay { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, + _extra: Option>, ) { log::trace!( "DummyConsensusOverlay: send broadcast_fec_ex {:?}/{:?}: {:?}", diff --git a/src/node/consensus-common/src/in_process_overlay.rs b/src/node/consensus-common/src/in_process_overlay.rs index 99d57c0..ea2ca14 100644 --- a/src/node/consensus-common/src/in_process_overlay.rs +++ b/src/node/consensus-common/src/in_process_overlay.rs @@ -251,6 +251,7 @@ impl ConsensusOverlay for OverlayClientImpl { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, + _extra: Option>, ) { log::trace!( target: LOG_TARGET, diff --git a/src/node/consensus-common/src/lib.rs b/src/node/consensus-common/src/lib.rs index d6422ea..c8e36f2 100644 --- a/src/node/consensus-common/src/lib.rs +++ b/src/node/consensus-common/src/lib.rs @@ -616,12 +616,13 @@ pub trait ConsensusOverlay: Send + Sync { v2: bool, ); - /// Send broadcast + /// Send broadcast with optional extra metadata (e.g. consensus.broadcastExtra for slot info) fn send_broadcast_fec_ex( &self, sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, + extra: Option>, ); /// Implementation specific @@ -1032,6 +1033,18 @@ pub trait SessionListener: Send + Sync { /// This is the common interface for all consensus session implementations /// (both catchain-based validator-session and simplex). pub trait Session: fmt::Display + Send + Sync { + /// Signal the session to begin active consensus processing. + /// + /// For Simplex sessions, `initial_block_seqno` is the expected seqno of + /// the first block to be produced (derived from prev_block_ids). The + /// session overlay is created at `create()` time so it can warm up + /// connections to peers. The FSM timeout clock only starts after + /// `start()` is called, preventing premature skip-votes on an + /// unconnected overlay. + /// + /// For Catchain sessions, the parameter is ignored (no-op). + fn start(&self, initial_block_seqno: u32); + /// Stop the session (blocks until all threads have stopped) /// Database is preserved for potential restart/recovery. fn stop(&self); diff --git a/src/node/consensus-common/src/log_player.rs b/src/node/consensus-common/src/log_player.rs index 4454456..8ce88e2 100644 --- a/src/node/consensus-common/src/log_player.rs +++ b/src/node/consensus-common/src/log_player.rs @@ -932,6 +932,7 @@ impl ConsensusOverlay for OverlayImpl { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, + _extra: Option>, ) { log::debug!("LogReplay: send broadcast_fec_ex {}/{}: {:?}", sender_id, send_as, payload); } diff --git a/src/node/consensus-common/src/node_test_network.rs b/src/node/consensus-common/src/node_test_network.rs index fb20b78..f9aebf6 100644 --- a/src/node/consensus-common/src/node_test_network.rs +++ b/src/node/consensus-common/src/node_test_network.rs @@ -243,7 +243,8 @@ impl<'a> NodeTestNetwork<'a> { overlay.set_rldp(rldp.clone()).unwrap(); let quic = if is_quic_enabled { - let quic = QuicNode::new(vec![overlay.clone()], cancellation_token.clone()); + let quic = + QuicNode::new(vec![overlay.clone()], cancellation_token.clone(), None); overlay.set_quic(quic.clone()).unwrap(); Some(quic) } else { @@ -496,6 +497,7 @@ impl ConsensusOverlay for ToggleableOverlay { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, + extra: Option>, ) { if !self.enabled.load(Ordering::Relaxed) { log::trace!( @@ -507,7 +509,7 @@ impl ConsensusOverlay for ToggleableOverlay { let _ = payload; return; } - self.inner.send_broadcast_fec_ex(sender_id, send_as, payload); + self.inner.send_broadcast_fec_ex(sender_id, send_as, payload, extra); } fn get_impl(&self) -> &dyn std::any::Any { diff --git a/src/node/consensus-common/tests/test_adnl_overlay.rs b/src/node/consensus-common/tests/test_adnl_overlay.rs index 87de8dc..733c59a 100644 --- a/src/node/consensus-common/tests/test_adnl_overlay.rs +++ b/src/node/consensus-common/tests/test_adnl_overlay.rs @@ -370,6 +370,7 @@ fn run_overlay_test( &node1.adnl_id, &node1.public_key.id(), broadcast_payload.clone(), + None, ); } } @@ -778,6 +779,7 @@ fn run_adnl_overlay_performance_test( &node1.adnl_id, &node1.public_key.id(), make_broadcast_payload(), + None, ); broadcasts_sent += 1; thread::sleep(SLEEP_TIME); diff --git a/src/node/consensus-common/tests/test_in_process_overlay.rs b/src/node/consensus-common/tests/test_in_process_overlay.rs index 70b1437..ea3f795 100644 --- a/src/node/consensus-common/tests/test_in_process_overlay.rs +++ b/src/node/consensus-common/tests/test_in_process_overlay.rs @@ -253,7 +253,7 @@ fn run_overlay_test(manager: ConsensusOverlayManagerPtr) -> Result<()> { } } // Send a broadcast. This will be received by all, including the sender. - overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload()); + overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload(), None); } // Wait for all broadcasts to be delivered, with a timeout, instead of a fixed sleep @@ -470,7 +470,7 @@ fn run_overlay_performance_test(manager: ConsensusOverlayManagerPtr) -> Result<( ); queries_sent += 1; } - overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload()); + overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload(), None); broadcasts_sent += 1; thread::sleep(SLEEP_TIME); } diff --git a/src/node/simplex/src/lib.rs b/src/node/simplex/src/lib.rs index 995fb29..c93df7f 100644 --- a/src/node/simplex/src/lib.rs +++ b/src/node/simplex/src/lib.rs @@ -27,15 +27,13 @@ //! // 1. Create overlay manager (for production use ADNL, for tests use in-process) //! let overlay_manager = SessionFactory::create_in_process_overlay_manager(4); //! -//! // 2. Create session with options +//! // 2. Create session with options (overlay starts warming up immediately) //! let options = SessionOptions::default(); //! let shard = ton_block::ShardIdent::masterchain(); -//! let initial_block_seqno = 1; // Expected seqno for first block //! let session = SessionFactory::create_session( //! &options, //! &session_id, -//! &shard, // Shard identifier -//! initial_block_seqno, // First block will have this seqno +//! &shard, //! validator_nodes, //! &local_private_key, //! db_path, @@ -44,8 +42,12 @@ //! session_listener, // Weak //! )?; //! -//! // 3. Session runs in background threads, callbacks via SessionListener -//! // 4. Stop when done +//! // 3. Start consensus processing with initial block seqno +//! let initial_block_seqno = 1; +//! session.start(initial_block_seqno); +//! +//! // 4. Session runs in background threads, callbacks via SessionListener +//! // 5. Stop when done //! session.stop(); //! ``` //! @@ -489,6 +491,40 @@ pub struct SessionOptions { /// Default: 30s (warn), 120s (error) pub health_parent_aging_warning_secs: u64, pub health_parent_aging_error_secs: u64, + + // -- Noncritical params (from simplex_config_v2 HashmapE) -- + // + // These fields are deserialized from on-chain config and passed through, but not yet + // consumed by the Rust session logic. + + // TODO: replace `timeout_increase_factor` / `max_backoff_delay` with these two fields. + // C++ consensus.cpp applies multiplier+cap on window skip (exponential backoff of + // first_block_timeout_). Rust simplex_state.rs uses hardcoded values instead. + pub first_block_timeout_multiplier: f64, + pub first_block_timeout_cap: Duration, + + // TODO: wire into candidate resolver. C++ candidate-resolver.cpp uses these four params + // for exponential-backoff fetch retries with cooldown. + pub candidate_resolve_timeout: Duration, + pub candidate_resolve_timeout_multiplier: f64, + pub candidate_resolve_timeout_cap: Duration, + pub candidate_resolve_cooldown: Duration, + + // TODO: wire into standstill recovery egress shaping. C++ pool.cpp uses this to + // rate-limit bytes/s during standstill_resolution_task. + pub standstill_max_egress_bytes_per_s: u32, + + // TODO: wire into slot/vote acceptance bounds. C++ consensus.cpp and pool.cpp use this + // to reject candidates/votes from too-far-future windows. + pub max_leader_window_desync: u32, + + // TODO: wire into peer ban logic. C++ pool.cpp bans peers with bad vote/cert signatures + // for this duration. + pub bad_signature_ban_duration: Duration, + + // TODO: wire into candidate resolver rate limiting. C++ candidate-resolver.cpp uses a + // 1-second sliding window with this limit per peer for requestCandidate. + pub candidate_resolve_rate_limit: u32, } impl Default for SessionOptions { @@ -518,6 +554,16 @@ impl Default for SessionOptions { health_stall_error_secs: 60, health_parent_aging_warning_secs: 30, health_parent_aging_error_secs: 120, + first_block_timeout_multiplier: 1.2, + first_block_timeout_cap: Duration::from_secs(100), + candidate_resolve_timeout: Duration::from_secs(1), + candidate_resolve_timeout_multiplier: 1.2, + candidate_resolve_timeout_cap: Duration::from_secs(10), + candidate_resolve_cooldown: Duration::from_millis(10), + standstill_max_egress_bytes_per_s: 50 << 17, + max_leader_window_desync: 250, + bad_signature_ban_duration: Duration::from_secs(5), + candidate_resolve_rate_limit: 10, } } } @@ -588,6 +634,39 @@ impl SessionOptions { fail!("health_parent_aging_error_secs must be >= health_parent_aging_warning_secs") } + // Noncritical params from on-chain config + if !self.first_block_timeout_multiplier.is_finite() + || self.first_block_timeout_multiplier < 1.0 + { + fail!("first_block_timeout_multiplier must be finite and >= 1.0") + } + + if self.first_block_timeout_cap.is_zero() { + fail!("first_block_timeout_cap must be > 0") + } + + if self.candidate_resolve_timeout.is_zero() { + fail!("candidate_resolve_timeout must be > 0") + } + + if !self.candidate_resolve_timeout_multiplier.is_finite() + || self.candidate_resolve_timeout_multiplier < 1.0 + { + fail!("candidate_resolve_timeout_multiplier must be finite and >= 1.0") + } + + if self.candidate_resolve_timeout_cap.is_zero() { + fail!("candidate_resolve_timeout_cap must be > 0") + } + + if self.candidate_resolve_cooldown.is_zero() { + fail!("candidate_resolve_cooldown must be > 0") + } + + if self.bad_signature_ban_duration.is_zero() { + fail!("bad_signature_ban_duration must be > 0") + } + Ok(()) } @@ -722,19 +801,19 @@ impl SessionFactory { /// * `options` - Session configuration options /// * `session_id` - Unique session identifier /// * `shard` - Shard identifier for this session - /// * `initial_block_seqno` - Expected seqno for the first block produced by this session. - /// For merge scenarios, caller should pass max(prev1.seqno, prev2.seqno) + 1. /// * `ids` - List of validator nodes /// * `local_key` - Private key for signing /// * `db_path` - Full database path /// * `overlay_manager` - Network overlay manager /// * `listener` - Session event listener + /// + /// After creation, call `Session::start(initial_block_seqno)` to provide + /// the expected first block seqno and begin consensus processing. #[allow(clippy::too_many_arguments)] pub fn create_session( options: &SessionOptions, session_id: &SessionId, shard: &ShardIdent, - initial_block_seqno: u32, ids: Vec, local_key: &PrivateKey, db_path: String, @@ -745,7 +824,6 @@ impl SessionFactory { options, session_id, shard, - initial_block_seqno, ids, local_key, db_path, diff --git a/src/node/simplex/src/receiver.rs b/src/node/simplex/src/receiver.rs index 94e14fb..8844318 100644 --- a/src/node/simplex/src/receiver.rs +++ b/src/node/simplex/src/receiver.rs @@ -83,6 +83,7 @@ use ton_api::{ deserialize_boxed, serialize_boxed, tag_from_data, ton::{ consensus::{ + broadcastextra::BroadcastExtra, candidateid::CandidateId, overlayid::OverlayId, simplex::{ @@ -804,6 +805,9 @@ pub(crate) struct ReceiverImpl { shard: ShardIdent, /// Maximum block + collated data size for candidate verification max_candidate_size: usize, + /// Maximum answer size for candidate request queries (network budget). + /// Matches C++ PR #2195: max_block_size + max_collated_data_size + (1 << 20). + max_candidate_query_answer_size: u64, /// Protocol version from consensus config (determines BOC serialization flags) proto_version: u32, /// Metrics @@ -1728,15 +1732,12 @@ impl ReceiverImpl { let session_id = self.session_id.clone(); let task_queues = self.get_task_queues(); - // Send query via overlay - self.overlay.send_query( - &peer_adnl_id, - &self.local_adnl_id, - query_name, - CANDIDATE_REQUEST_TIMEOUT, - &payload, + // Send query via RLDP overlay with explicit response size budget (C++ PR #2195 parity) + let timeout_deadline = SystemTime::now() + CANDIDATE_REQUEST_TIMEOUT; + self.overlay.send_query_via_rldp( + peer_adnl_id, + query_name.to_string(), Box::new(move |result: Result| { - // Post response handling to receiver thread task_queues.post_closure(Box::new(move |receiver: &mut ReceiverImpl| { receiver.handle_candidate_response( slot_for_cb, @@ -1746,6 +1747,10 @@ impl ReceiverImpl { ); })); }), + timeout_deadline, + payload, + self.max_candidate_query_answer_size, + true, // RLDPv2 ); } @@ -2275,8 +2280,17 @@ impl ReceiverImpl { stats.last_send_time = Some(SystemTime::now()); } + // Build consensus.broadcastExtra with slot info (required for C++ interop) + let broadcast_extra = BroadcastExtra { slot: slot as i32 }; + let extra = consensus_common::serialize_tl_boxed_object!(&broadcast_extra.into_boxed()); + // Send via overlay FEC broadcast - self.overlay.send_broadcast_fec_ex(&self.local_adnl_id, self.local_key.id(), payload); + self.overlay.send_broadcast_fec_ex( + &self.local_adnl_id, + self.local_key.id(), + payload, + Some(extra), + ); } /// Shuffle send order for fairness @@ -3132,6 +3146,7 @@ impl ReceiverWrapper { session_id: SessionId, shard: &ShardIdent, max_candidate_size: usize, + max_candidate_query_answer_size: u64, proto_version: u32, ids: &[SessionNode], local_key: &PrivateKey, @@ -3315,6 +3330,7 @@ impl ReceiverWrapper { dedup_votes: HashMap::new(), shard: shard_clone, max_candidate_size, + max_candidate_query_answer_size, proto_version, in_messages_bytes: in_messages_bytes_clone, out_messages_bytes: out_messages_bytes_clone, diff --git a/src/node/simplex/src/session.rs b/src/node/simplex/src/session.rs index 7cd0601..3480a55 100644 --- a/src/node/simplex/src/session.rs +++ b/src/node/simplex/src/session.rs @@ -82,7 +82,7 @@ use std::{ collections::BTreeMap, fmt, panic, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU32, Ordering}, Arc, }, thread, @@ -344,6 +344,15 @@ pub(crate) struct SessionImpl { stop_flag: Arc, /// Indicates database should be destroyed on stop destroy_db_flag: Arc, + /// Atomic flag: main_loop should begin active FSM processing. + /// Set by `start(seqno)`. The overlay is created at `create()` time and + /// warms up while main_loop polls this flag, so peers are connected + /// before the first_block_timeout clock starts ticking. + start_flag: Arc, + /// Initial block seqno, provided by `start(seqno)`. + /// Read by main_loop after start_flag is set, before SessionDescription + /// creation. + deferred_initial_seqno: Arc, /// Atomic flag to indicate main processing thread has stopped main_processing_thread_stopped: Arc, /// Atomic flag to indicate callbacks processing thread has stopped @@ -365,6 +374,16 @@ pub(crate) struct SessionImpl { } impl ConsensusSession for SessionImpl { + fn start(&self, initial_block_seqno: u32) { + log::info!( + "SimplexSession {}: start(seqno={}) called โ€” storing seqno and unblocking main loop", + self.session_id.to_hex_string(), + initial_block_seqno + ); + self.deferred_initial_seqno.store(initial_block_seqno, Ordering::Release); + self.start_flag.store(true, Ordering::Release); + } + fn stop(&self) { self.stop_async(); self.stop_impl(false); @@ -464,19 +483,21 @@ impl SessionImpl { should_stop_flag: Arc, is_stopped_flag: Arc, destroy_db_flag: Arc, + start_flag: Arc, + deferred_initial_seqno: Arc, panicked_flag: Arc, task_queue: TaskQueuePtr, callbacks_task_queue: CallbackTaskQueuePtr, options: SessionOptions, session_id: SessionId, shard: ShardIdent, - initial_block_seqno: u32, ids: Vec, local_key: PrivateKey, listener: SessionListenerPtr, overlay_manager: ConsensusOverlayManagerPtr, receiver_listener: ReceiverListenerPtr, max_candidate_size: usize, + max_candidate_query_answer_size: u64, db_path: String, session_activity_node: ActivityNodePtr, session_creation_time: SystemTime, @@ -493,7 +514,7 @@ impl SessionImpl { // Signal thread start based on wait_for_db_init option: // - If false: send Ok(()) now (non-blocking for caller) // - If true: wait until full initialization completes - let mut init_signaled = false; + let init_signaled = Cell::new(false); if !options.wait_for_db_init { if init_result_sender.send(Ok(())).is_err() { log::warn!( @@ -503,7 +524,7 @@ impl SessionImpl { is_stopped_flag.store(true, Ordering::Release); return; } - init_signaled = true; + init_signaled.set(true); } // Configure metrics @@ -522,7 +543,7 @@ impl SessionImpl { let fail_startup = |err: Error, ctx: &str| { log::error!("Session {} {}: {:?}", session_id.to_hex_string(), ctx, err); startup_errors.set(startup_errors.get().saturating_add(1)); - if !init_signaled { + if !init_signaled.get() { let _ = init_result_sender.send(Err(err)); } is_stopped_flag.store(true, Ordering::Release); @@ -541,7 +562,7 @@ impl SessionImpl { // Check if we should stop before loading bootstrap if should_stop_flag.load(Ordering::Relaxed) { log::info!("Session {} stopping before bootstrap load", session_id.to_hex_string()); - if !init_signaled { + if !init_signaled.get() { let _ = init_result_sender.send(Err(error!("Session stopped before bootstrap load"))); } @@ -559,6 +580,7 @@ impl SessionImpl { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, options.proto_version, &ids, &local_key, @@ -585,7 +607,7 @@ impl SessionImpl { // Check if this was a cancellation if should_stop_flag.load(Ordering::Relaxed) { log::info!("Session {} bootstrap load cancelled", session_id.to_hex_string()); - if !init_signaled { + if !init_signaled.get() { let _ = init_result_sender .send(Err(error!("Session bootstrap load cancelled"))); } @@ -608,6 +630,49 @@ impl SessionImpl { bootstrap.notar_certs.len(), ); + // Signal init complete before the start gate โ€” the overlay and DB are + // fully ready. The caller (create()) can return and later call + // start(seqno) to unblock the FSM. + if !init_signaled.get() { + if init_result_sender.send(Ok(())).is_err() { + log::warn!( + "SimplexSession {} main loop: failed to send init result (receiver dropped)", + session_id.to_hex_string() + ); + } + init_signaled.set(true); + } + + // Wait for start(seqno) before creating SessionDescription. + // The overlay is already registered and warming up peer connections + // while we poll here, so by the time start() is called the overlay + // should have established connectivity -- preventing premature + // first_block_timeout skips that occur when the FSM starts before + // any peers are reachable. + if !start_flag.load(Ordering::Acquire) { + log::info!( + "SimplexSession {} waiting for start(seqno) signal (overlay warming up)...", + session_id.to_hex_string() + ); + while !start_flag.load(Ordering::Acquire) { + if should_stop_flag.load(Ordering::Relaxed) { + log::info!( + "SimplexSession {} stopped while waiting for start()", + session_id.to_hex_string() + ); + is_stopped_flag.store(true, Ordering::Release); + return; + } + thread::sleep(Duration::from_millis(10)); + } + } + let initial_block_seqno = deferred_initial_seqno.load(Ordering::Acquire); + log::info!( + "SimplexSession {} start(seqno={}) received, creating SessionDescription", + session_id.to_hex_string(), + initial_block_seqno + ); + // Phase 4a: Create session description (immutable session configuration) let description = match SessionDescription::new( &options, @@ -685,16 +750,6 @@ impl SessionImpl { } } - // Signal full initialization complete (if wait_for_db_init is true) - if !init_signaled { - if init_result_sender.send(Ok(())).is_err() { - log::warn!( - "SimplexSession {} main loop: failed to send init result after full init (receiver dropped)", - session_id.to_hex_string() - ); - } - } - // Create metrics dumper for computed/derivative metrics let mut metrics_dumper = Self::create_metrics_dumper(); @@ -1058,7 +1113,6 @@ impl SessionImpl { options: &SessionOptions, session_id: &SessionId, shard: &ShardIdent, - initial_block_seqno: u32, ids: Vec, local_key: &PrivateKey, db_path: String, @@ -1066,10 +1120,9 @@ impl SessionImpl { listener: SessionListenerPtr, ) -> Result { log::info!( - "Creating SimplexSession (session_id is {}, shard={}, initial_seqno={}, nodes_count={}, db_path={})", + "Creating SimplexSession (session_id is {}, shard={}, nodes_count={}, db_path={})", session_id.to_hex_string(), shard, - initial_block_seqno, ids.len(), db_path ); @@ -1094,6 +1147,8 @@ impl SessionImpl { // Create thread synchronization flags let stop_flag = Arc::new(AtomicBool::new(false)); let destroy_db_flag = Arc::new(AtomicBool::new(false)); + let start_flag = Arc::new(AtomicBool::new(false)); + let deferred_initial_seqno = Arc::new(AtomicU32::new(0)); let main_processing_thread_stopped = Arc::new(AtomicBool::new(false)); let callbacks_processing_thread_stopped = Arc::new(AtomicBool::new(false)); let panicked_flag = Arc::new(AtomicBool::new(false)); @@ -1104,13 +1159,18 @@ impl SessionImpl { ReceiverListenerImpl::create(main_task_queue.clone(), session_id.clone()); let receiver_listener_weak: ReceiverListenerPtr = Arc::downgrade(&receiver_listener); - // Compute max candidate size for receiver + // Compute max candidate size for receiver (local validation guard, +1KB slack) let max_candidate_size = options.max_block_size + options.max_collated_data_size + 1024; + // Network response budget for requestCandidate queries (C++ PR #2195 parity: +1MB) + let max_candidate_query_answer_size: u64 = + (options.max_block_size + options.max_collated_data_size) as u64 + (1 << 20); // Create session (receiver is created in main_loop after bootstrap loading) let session = SessionImpl { stop_flag: stop_flag.clone(), destroy_db_flag: destroy_db_flag.clone(), + start_flag: start_flag.clone(), + deferred_initial_seqno: deferred_initial_seqno.clone(), main_processing_thread_stopped: main_processing_thread_stopped.clone(), callbacks_processing_thread_stopped: callbacks_processing_thread_stopped.clone(), panicked_flag: panicked_flag.clone(), @@ -1153,19 +1213,21 @@ impl SessionImpl { stop_flag_for_main_loop, main_processing_thread_stopped, destroy_db_flag, + start_flag, + deferred_initial_seqno, panicked_flag_for_main_loop, main_task_queue, callbacks_task_queue, options_clone, session_id_clone, shard_clone, - initial_block_seqno, ids, local_key_clone, listener, overlay_manager, receiver_listener_weak, max_candidate_size, + max_candidate_query_answer_size, db_path, session_activity_node, session_creation_time, diff --git a/src/node/simplex/src/session_processor.rs b/src/node/simplex/src/session_processor.rs index b4025ea..6a3bd8f 100644 --- a/src/node/simplex/src/session_processor.rs +++ b/src/node/simplex/src/session_processor.rs @@ -749,6 +749,8 @@ pub(crate) struct SessionProcessor { batch_commit_counter: metrics::Counter, /// Histogram for batch commit sizes (number of blocks committed at once) batch_commit_size_histogram: metrics::Histogram, + /// Gauge for finalized-but-uncommitted journal size (commit lag indicator) + finalized_uncommitted_gauge: metrics::Gauge, /* Error tracking for SessionStats @@ -1284,6 +1286,9 @@ impl SessionProcessor { health_warnings_counter, ) = Self::init_metrics(&metrics_receiver, &description); + let finalized_uncommitted_gauge = + metrics_receiver.sink().register_gauge(&"simplex_finalized_uncommitted_count".into()); + let now = description.get_time(); let num_validators = description.get_total_nodes() as usize; @@ -1345,6 +1350,7 @@ impl SessionProcessor { errors_counter, batch_commit_counter, batch_commit_size_histogram, + finalized_uncommitted_gauge, // Error tracking (includes startup errors from before processor was created) session_errors_count: AtomicU32::new(initial_errors), // Slot stage tracking @@ -2689,15 +2695,18 @@ impl SessionProcessor { self.round_debug_at = now + ROUND_DEBUG_PERIOD; } - // Call SimplexState FSM check_all (processes timeouts, pending blocks) - self.simplex_state.check_all(&self.description); - // Check validation (process pending validations) self.check_validation(); - // Process validated candidates and feed to FSM + // Feed validated candidates to FSM BEFORE timeout processing so that + // the FSM has all available candidates before it evaluates timeouts + // (mirrors C++ where process_blocks() feeds candidates before the + // round timer is checked). self.process_validated_candidates(); + // Call SimplexState FSM check_all (processes timeouts, pending blocks) + self.simplex_state.check_all(&self.description); + // Process all events produced by FSM self.process_simplex_events(); @@ -2822,6 +2831,22 @@ impl SessionProcessor { // Reference: C++ block-producer.cpp collates on notarized chain. let current_slot = self.simplex_state.get_first_non_progressed_slot(); + // Stale window guard (C++ parity: consensus.cpp LeaderWindowObserved handler sets + // current_window_ BEFORE the leader check). Skip collation when the progress + // cursor still points at a slot in a window that has already been superseded. + let slot_window = self.description.get_window_idx(current_slot); + let current_window = self.simplex_state.get_current_leader_window_idx(); + if slot_window < current_window { + log::trace!( + "Session {} check_collation: skipping stale slot {} (window {} < current {})", + &self.session_id().to_hex_string()[..8], + current_slot, + slot_window, + current_window + ); + return; + } + // Don't generate if already generated or pending for this slot if self.slot_is_generated(current_slot) || self.slot_is_pending_generate(current_slot) { return; @@ -3629,6 +3654,23 @@ impl SessionProcessor { // Remove from precollated blocks self.remove_precollated_block(slot); + // Stale window guard (C++ parity: block-producer.cpp generation loop, + // consensus.cpp start_generation). Discard candidates whose leader window + // has already been superseded โ€” the collation callback arrived too late. + let slot_window = self.description.get_window_idx(slot); + let current_window = self.simplex_state.get_current_leader_window_idx(); + if slot_window != current_window { + log::warn!( + "Session {} generated_block: discarding stale candidate for slot {} \ + (window {} != current {})", + &self.session_id().to_hex_string()[..8], + slot, + slot_window, + current_window + ); + return; + } + // Use FSM's progress cursor to validate this is for the current slot. // Collation follows notarized/skipped progress, not finalization. let fsm_first_non_progressed_slot = self.simplex_state.get_first_non_progressed_slot(); @@ -6183,14 +6225,14 @@ impl SessionProcessor { .get(&candidate_id) .and_then(|p| p.raw_candidate.block.as_block().map(|b| b.id.seq_no)), ) { - log::warn!( - "Session {} candidate_decision_ok: slot={slot}, hash={:?}, \ - committed_seqno={committed_seqno}, cand_seqno={cand_seqno} (drop because new \ - block is already committed)", - self.session_id().to_hex_string(), - candidate_id, - ); if cand_seqno <= committed_seqno { + log::warn!( + "Session {} candidate_decision_ok: slot={slot}, hash={:?}, \ + committed_seqno={committed_seqno}, cand_seqno={cand_seqno} (drop because \ + new block is already committed)", + self.session_id().to_hex_string(), + candidate_id, + ); self.pending_approve.remove(&candidate_id); self.pending_validations.remove(&candidate_id); self.validation_attempt_map.remove(&candidate_id); @@ -6200,7 +6242,8 @@ impl SessionProcessor { self.candidate_decision_ok_internal(candidate_id, slot, receive_time); - self.set_next_awake_time(validity_start_time); + // Wake immediately so check_all() runs in the very next main-loop iteration + self.set_next_awake_time(self.now()); } /// Internal helper for successful validation (used by both normal and empty block paths) @@ -7806,14 +7849,21 @@ impl SessionProcessor { /// /// This function is idempotent and safe to call multiple times. fn try_commit_finalized_chains(&mut self) { - // Collect keys to process (avoid borrow conflicts) - let finalized_keys: Vec = + // Collect keys to process, sorted by (seqno, slot) for deterministic + // oldest-first commit ordering (avoid arbitrary HashMap iteration order). + let mut finalized_keys: Vec = self.finalized_journal_pending_commit.keys().cloned().collect(); if finalized_keys.is_empty() { return; } + finalized_keys.sort_unstable_by_key(|id| { + let seqno = + self.received_candidates.get(id).map(|r| r.block_id.seq_no).unwrap_or(u32::MAX); + (seqno, id.slot.0) + }); + log::trace!( "Session {} try_commit_finalized_chains: checking {} finalized blocks", &self.session_id().to_hex_string()[..8], @@ -7999,10 +8049,6 @@ impl SessionProcessor { finalized_id: inner_finalized_id, finalized_seqno: inner_finalized_seqno, } => { - // Invariant: commit_target was selected as the *next committable* non-empty - // masterchain block (seqno == expected_seqno) and we have its FinalCert. - // Therefore, collect_gapless_commit_chain(commit_target) must NOT return - // WaitingForFinalCert again. log::error!( "Session {} try_commit_finalized_chains: MC gap \ recovery invariant violated - \ @@ -8076,9 +8122,18 @@ impl SessionProcessor { } // Remove committed entries from journal + let did_commit = !committed_keys.is_empty(); for key in committed_keys { self.finalized_journal_pending_commit.remove(&key); } + + // If something was committed, newly-unblocked chains may now be ready. + // Reschedule check_all so the session loop re-enters this function. + if did_commit { + self.set_next_awake_time(self.now()); + } + + self.finalized_uncommitted_gauge.set(self.finalized_journal_pending_commit.len() as f64); } /// Commit a finalized chain that has been verified as commit-ready @@ -8373,6 +8428,39 @@ impl SessionProcessor { // Clean up candidate_data_cache in sync with received_candidates self.candidate_data_cache.retain(|id, _| id.slot >= up_to_slot); + // Remove stale finalized-journal entries for old slots. + { + let now = self.now(); + let session_id_hex = self.session_id().to_hex_string(); + let mut stale_count = 0u32; + self.finalized_journal_pending_commit.retain(|id, entry| { + if id.slot < up_to_slot { + let age_secs = now + .duration_since(entry.finalized_at) + .map(|d| d.as_secs_f64()) + .unwrap_or(0.0); + log::warn!( + "Session {} cleanup: removing stale finalized-journal entry slot={} \ + (finalized {:.1}s ago, never committed)", + &session_id_hex[..8], + id.slot, + age_secs, + ); + stale_count += 1; + false + } else { + true + } + }); + if stale_count > 0 { + self.session_errors_count + .fetch_add(stale_count, std::sync::atomic::Ordering::Relaxed); + self.errors_counter.increment(stale_count as u64); + self.finalized_uncommitted_gauge + .set(self.finalized_journal_pending_commit.len() as f64); + } + } + // Prune log-throttle set to prevent unbounded growth over long sessions self.missing_body_logged.retain(|&slot| slot >= up_to_slot.value()); diff --git a/src/node/simplex/src/simplex_state.rs b/src/node/simplex/src/simplex_state.rs index 07b91c7..008f963 100644 --- a/src/node/simplex/src/simplex_state.rs +++ b/src/node/simplex/src/simplex_state.rs @@ -2534,26 +2534,46 @@ impl SimplexState { self.ensure_window_exists(window_idx); - let is_voted = if let Some(window) = self.get_window(window_idx) { - window.slots[offset].is_voted + // C++ consensus.cpp CandidateReceived only gates on voted_notar (line 170), + // NOT voted_skip. A local skip vote must NOT prevent storing a candidate as + // pending โ€” the pending retry (`check_pending_blocks`) will notarize it once + // the parent base propagates through skip certs. + // + // Alpenglow uses the stricter `is_voted` (any local vote blocks storage). + let dominated = if let Some(window) = self.get_window(window_idx) { + if self.opts.enable_fallback_protocol { + window.slots[offset].is_voted + } else { + window.slots[offset].voted_notar.is_some() + } } else { false }; - if !is_voted { + if !dominated { log::trace!( "SimplexState::on_candidate: ({}/{}) try_notar=false, storing as pending block", window_idx, slot ); - // Alpenglow: pendingBlocks[s] โ† Block(s, hash, hashparent) + // C++ parity: first pending candidate wins. If a pending block already + // exists for this slot, reject any different candidate (equivocation). if let Some(window) = self.get_window_mut(window_idx) { + if let Some(ref existing) = window.slots[offset].pending_block { + if existing.id.hash != candidate.id.hash { + log::warn!( + "SimplexState::on_candidate: ({window_idx}/{slot}) \ + pending_block already set with different hash, ignoring" + ); + } + return Ok(()); + } window.slots[offset].pending_block = Some(candidate); self.pending_slots.push(PendingSlot(slot)); } } else { log::trace!( - "SimplexState::on_candidate: ({}/{}) already voted, ignoring candidate", + "SimplexState::on_candidate: ({}/{}) already notarized, ignoring candidate", window_idx, slot ); @@ -4361,8 +4381,11 @@ impl SimplexState { slot_state.is_voted } else { - // C++: only notarize/final blocks further notarize (skip is allowed) - slot_state.voted_notar.is_some() || slot_state.its_over + // C++ parity: only voted_notar gates notarization. C++ try_notarize() + // does NOT check voted_final/its_over โ€” a slot that was finalized on a + // previous run can still be re-notarized after restart (the later + // auto-finalize simply skips re-broadcasting). + slot_state.voted_notar.is_some() }; if already_voted { @@ -5817,6 +5840,14 @@ impl SimplexState { /// `if (auto base = slot.state->available_base) next_slot.state->add_available_base(*base);` /// Note: C++ uses `add_available_base` (max-merge), not a conditional assignment. /// + /// C++ also calls `maybe_resolve_requests()` (pool.cpp) after every certificate, + /// which does a backward walk to resolve pending parent-wait requests even if + /// `available_base` was not set on intermediate slots. Rust has no backward walk, + /// so instead we chain the base forward through all consecutive already-skipped + /// slots, ensuring every intermediate slot gets its `available_base` set. This + /// allows `check_pending_blocks` / `try_notar` to find the base for any pending + /// block regardless of skip-cert arrival order. + /// /// This is always called when a slot is skipped, regardless of mode. /// The tracked state is used for progress when `use_notarized_parent_chain` is enabled. fn propagate_base_after_skip_cert(&mut self, desc: &SessionDescription, slot: SlotIndex) { @@ -5830,20 +5861,37 @@ impl SimplexState { ); } - // Propagate base forward using max-merge: if slot has a base, merge it into - // the next non-skipped slot (C++ pool.cpp on_skip: add_available_base). - let next_slot = self.find_next_nonskipped_slot(desc, slot); - let current_base = self.get_slot_available_base(desc, slot); - - if let Some(base) = current_base { - if let Some(next_state) = self.get_slot_mut(desc, next_slot) { + // Chain base forward: propagate slot-by-slot through consecutive already-skipped + // slots. Unlike the previous `find_next_nonskipped_slot` approach which jumped + // directly to the first non-skipped slot (potentially hundreds of slots away), + // this ensures every intermediate skipped slot gets its `available_base` set. + // + // Without this chaining, skip certs arriving out-of-order leave gaps: + // cert(5) arrives first โ†’ slot 5 has no base โ†’ nothing propagates + // cert(0) arrives โ†’ base jumps from 0 to 388 (next non-skipped) โ†’ slots 1-387 have no base + // With chaining: + // cert(0) โ†’ base set on slot 1 โ†’ slot 1 already skipped โ†’ chain to slot 2 โ†’ ... โ†’ slot 388 + let mut current = slot; + loop { + let current_base = self.get_slot_available_base(desc, current); + let Some(base) = current_base else { + break; + }; + let next = current + 1; + self.ensure_window_exists(desc.get_window_idx(next)); + if let Some(next_state) = self.get_slot_mut(desc, next) { log::trace!( "SimplexState: propagating base from skipped slot {} -> slot {} (max-merge)", - slot, - next_slot + current, + next ); next_state.add_available_base_max(base); } + if self.is_slot_skipped_cert(desc, next) { + current = next; + } else { + break; + } } // C++ compatibility: advance skip timer when SkipCert arrives @@ -5953,12 +6001,21 @@ impl SimplexState { panic!("SimplexState::find_next_nonskipped_slot: exceeded scan limit from slot {}", slot); } - /// Advance leader window when progress cursor crosses window boundary + /// Advance leader window when progress cursor crosses window boundary. /// /// Reference: C++ pool.cpp maybe_publish_new_leader_windows() /// /// This triggers timeout scheduling for the new window and applies adaptive backoff. /// Only called when `SimplexStateOptions::use_notarized_parent_chain` is enabled. + /// + /// # Ordering guarantee (C++ parity: PR #2195) + /// + /// `current_leader_window_idx` is updated here, inside `check_all()` -> + /// notarization/skip handlers -> `advance_progress_cursor()` -> this method. + /// `SessionProcessor::check_collation()` runs strictly after `check_all()` + /// returns, so the leader-status check always sees the up-to-date window. + /// This mirrors C++ consensus.cpp where `current_window_` is set BEFORE + /// the leader check in the `LeaderWindowObserved` handler. fn advance_leader_window_on_progress_cursor(&mut self, desc: &SessionDescription) { let now_window = desc.get_window_idx(self.first_non_progressed_slot); if now_window <= self.current_leader_window_idx { diff --git a/src/node/simplex/src/tests/test_receiver.rs b/src/node/simplex/src/tests/test_receiver.rs index f923745..9b58db2 100644 --- a/src/node/simplex/src/tests/test_receiver.rs +++ b/src/node/simplex/src/tests/test_receiver.rs @@ -244,6 +244,7 @@ impl ReceiverInstance { // Use masterchain shard and default size limits for tests let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; // 8 MB + let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); let panicked_flag = Arc::new(AtomicBool::new(false)); let health_counters = Arc::new(crate::receiver::ReceiverHealthCounters::new()); @@ -251,6 +252,7 @@ impl ReceiverInstance { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, nodes, &private_key, @@ -671,6 +673,7 @@ fn test_receiver_candidate_resolver() { // Use masterchain shard and default size limits let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; // 8 MB + let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); // === Step 1: Create receiver 0 and broadcast a candidate === log::info!("Step 1: Creating receiver 0 and broadcasting candidate..."); @@ -683,6 +686,7 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[0], @@ -772,6 +776,7 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[1], @@ -792,6 +797,7 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[2], @@ -853,6 +859,153 @@ fn test_receiver_candidate_resolver() { println!("โœ“ Candidate resolver test passed: late-joining receivers successfully retrieved missed candidate"); } +/// Test that candidate resolver works with a large candidate payload (~1 MB) +/// that exercises the +1MB RLDP response budget (C++ PR #2195 parity). +/// +/// Scenario: +/// 1. Receiver 0 broadcasts a ~1 MB candidate +/// 2. Receiver 1 joins late and requests the candidate via the resolver +/// 3. Assert receiver 1 receives the full large candidate +#[test] +fn test_receiver_candidate_resolver_large_payload() { + let _ = env_logger::Builder::new().filter_level(log::LevelFilter::Trace).try_init(); + + let overlay_manager = SessionFactory::create_in_process_overlay_manager(2); + let session_id = UInt256::rand(); + + let keys: Vec<_> = + (0..2).map(|_| Ed25519KeyOption::generate().expect("Failed to generate key")).collect(); + let nodes: Vec = keys + .iter() + .map(|k| SessionNode { public_key: k.clone(), adnl_id: k.id().clone(), weight: 1 }) + .collect(); + + let shard = ShardIdent::masterchain(); + let max_candidate_size = 8 << 20; // 8 MB (validation guard) + let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); + + // === Step 1: Create receiver 0 and broadcast a ~1 MB candidate === + log::info!("Step 1: Creating receiver 0 and broadcasting large candidate..."); + + let (listener0, stats0) = TestReceiverListener::create(0); + let listener0_arc: Arc = listener0.clone(); + let receiver0 = crate::receiver::ReceiverWrapper::create( + session_id.clone(), + &shard, + max_candidate_size, + max_candidate_query_answer_size, + 0, + &nodes, + &keys[0], + overlay_manager.clone(), + Arc::downgrade(&listener0_arc), + Duration::from_secs(10), + Arc::new(AtomicBool::new(false)), + false, + Arc::new(crate::receiver::ReceiverHealthCounters::new()), + ) + .expect("Failed to create receiver 0"); + + thread::sleep(Duration::from_millis(500)); + + // Build a ~1 MB block payload + let slot = 7u32; + let block_data = vec![0xABu8; 1 << 20]; // 1 MiB of data + let collated_data: Vec = vec![]; + let root_hash = UInt256::from_slice(&sha256_digest(&block_data)); + let file_hash = UInt256::from_slice(&sha256_digest(&block_data)); + let collated_file_hash = UInt256::from_slice(&sha256_digest(&collated_data)); + + let tl_inner = TlCandidate { + src: UInt256::default(), + round: slot as i32, + root_hash: root_hash.clone(), + data: block_data.clone().into(), + collated_data: collated_data.clone().into(), + }; + let candidate_bytes = consensus_common::serialize_tl_boxed_object!(&tl_inner.into_boxed()); + + let block_id = BlockIdExt { + shard_id: shard.clone(), + seq_no: slot, + root_hash: root_hash.clone(), + file_hash: file_hash.clone(), + }; + + let candidate_hash = crate::utils::compute_candidate_id_hash_u32( + slot, + Some(&block_id), + Some(&collated_file_hash), + None, + ); + + let signature = crate::utils::sign_candidate_u32(&session_id, slot, &candidate_hash, &keys[0]) + .expect("Failed to sign candidate"); + + let broadcast = CandidateData::Consensus_Block(CandidateDataBlock { + slot: slot as i32, + candidate: candidate_bytes.into(), + parent: CandidateParent::Consensus_CandidateWithoutParents, + signature: signature.into(), + }); + + receiver0.send_block_broadcast(slot, candidate_hash.clone(), broadcast); + // requestCandidate currently asks for both candidate+notar. Seed notar in + // resolver cache so late joiners can complete merged CandidateAndCert. + receiver0.cache_notarization_cert(slot, candidate_hash.clone(), vec![0xAA, 0xBB, 0xCC]); + thread::sleep(Duration::from_millis(500)); + + // === Step 2: Create receiver 1 (late joiner) === + log::info!("Step 2: Creating late-joining receiver 1..."); + thread::sleep(Duration::from_secs(4)); + + let (listener1, stats1) = TestReceiverListener::create(1); + let listener1_arc: Arc = listener1.clone(); + let receiver1 = crate::receiver::ReceiverWrapper::create( + session_id.clone(), + &shard, + max_candidate_size, + max_candidate_query_answer_size, + 0, + &nodes, + &keys[1], + overlay_manager.clone(), + Arc::downgrade(&listener1_arc), + Duration::from_secs(10), + Arc::new(AtomicBool::new(false)), + false, + Arc::new(crate::receiver::ReceiverHealthCounters::new()), + ) + .expect("Failed to create receiver 1"); + + thread::sleep(Duration::from_millis(1000)); + + // === Step 3: Receiver 1 requests the large candidate === + log::info!("Step 3: Receiver 1 requesting large candidate via resolver..."); + receiver1.request_candidate(slot, candidate_hash.clone()); + + log::info!("Waiting for large candidate resolver request to complete..."); + thread::sleep(Duration::from_secs(10)); + + // === Step 4: Assert receiver 1 received the candidate === + let r0_broadcasts = stats0.broadcasts_received.load(Ordering::Relaxed); + let r1_broadcasts = stats1.broadcasts_received.load(Ordering::Relaxed); + log::info!("Receiver 0: broadcasts_received = {}", r0_broadcasts); + log::info!("Receiver 1: broadcasts_received = {}", r1_broadcasts); + + receiver0.stop(); + receiver1.stop(); + thread::sleep(Duration::from_millis(500)); + + assert!( + r1_broadcasts >= 1, + "Receiver 1 should have received the large candidate (~1 MB) via resolver, got {}", + r1_broadcasts + ); + + println!("โœ“ Large candidate resolver test passed: ~1 MB candidate successfully retrieved via RLDP path"); +} + // ============================================================================ // Certificate send + standstill re-broadcast tests // ============================================================================ @@ -927,6 +1080,7 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; + let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); // Create receivers with short standstill timeout to simulate retransmission let (listener0, _stats0) = TestReceiverListener::create(0); @@ -935,6 +1089,7 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[0], @@ -953,6 +1108,7 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[1], @@ -1031,6 +1187,7 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; + let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); // Create receivers with short standstill timeout to simulate retransmission let (listener0, _stats0) = TestReceiverListener::create(0); @@ -1039,6 +1196,7 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[0], @@ -1057,6 +1215,7 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[1], @@ -1113,6 +1272,7 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; + let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); let (listener0, _stats0) = TestReceiverListener::create(0); let listener0_arc: Arc = listener0.clone(); @@ -1120,6 +1280,7 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[0], @@ -1138,6 +1299,7 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { session_id.clone(), &shard, max_candidate_size, + max_candidate_query_answer_size, 0, &nodes, &keys[1], diff --git a/src/node/simplex/src/tests/test_session_processor.rs b/src/node/simplex/src/tests/test_session_processor.rs index 0600345..914b2de 100644 --- a/src/node/simplex/src/tests/test_session_processor.rs +++ b/src/node/simplex/src/tests/test_session_processor.rs @@ -2418,3 +2418,499 @@ fn test_recovery_drain_startup_events_drops_certificate_relay_events() { "drained startup certificate events must not be re-broadcast on first normal tick" ); } + +// ============================================================================ +// Gapless commit scheduler hardening tests +// ============================================================================ + +/// Verify that `cleanup_old_candidates` removes stale journal entries for old slots +/// and increments the session error counter accordingly. +#[test] +fn test_journal_cleanup_removes_stale_entries() { + let mut fixture = TestFixture::new(4); + + let old_slot = SlotIndex::new(5); + let current_slot = SlotIndex::new(20); + let old_hash = UInt256::rand(); + let current_hash = UInt256::rand(); + + let old_id = RawCandidateId { slot: old_slot, hash: old_hash.clone() }; + let current_id = RawCandidateId { slot: current_slot, hash: current_hash.clone() }; + + let dummy_cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { slot: old_slot, block_hash: old_hash.clone() }, + signatures: Vec::new(), + }); + + let dummy_cert2: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { + slot: current_slot, + block_hash: current_hash.clone(), + }, + signatures: Vec::new(), + }); + + let now = fixture.description.get_time(); + + fixture.processor.finalized_journal_pending_commit.insert( + old_id.clone(), + FinalizedEntry { + event: BlockFinalizedEvent { + slot: old_slot, + block_hash: old_hash, + block_id: None, + certificate: dummy_cert, + }, + finalized_at: now - Duration::from_secs(60), + }, + ); + + fixture.processor.finalized_journal_pending_commit.insert( + current_id.clone(), + FinalizedEntry { + event: BlockFinalizedEvent { + slot: current_slot, + block_hash: current_hash, + block_id: None, + certificate: dummy_cert2, + }, + finalized_at: now, + }, + ); + + assert_eq!(fixture.processor.finalized_journal_pending_commit.len(), 2); + + let errors_before = + fixture.processor.session_errors_count.load(std::sync::atomic::Ordering::Relaxed); + + // Cleanup slots < 10 โ€” old_slot(5) should be removed, current_slot(20) kept. + fixture.processor.cleanup_old_candidates(SlotIndex::new(10)); + + assert_eq!(fixture.processor.finalized_journal_pending_commit.len(), 1); + assert!(!fixture.processor.finalized_journal_pending_commit.contains_key(&old_id)); + assert!(fixture.processor.finalized_journal_pending_commit.contains_key(¤t_id)); + + let errors_after = + fixture.processor.session_errors_count.load(std::sync::atomic::Ordering::Relaxed); + assert_eq!(errors_after - errors_before, 1, "stale journal entry should increment error count"); +} + +/// Verify that the scheduler processes entries in seqno-ascending order, +/// not arbitrary HashMap order. +/// Both entries are WaitingForFinalCert on MC (seqno ahead of committed head) +/// so neither can commit. Both stay pending in the journal. +#[test] +fn test_try_commit_processes_in_seqno_order() { + let mut fixture = TestFixture::new(4); + + // TestFixture defaults to masterchain. Committed head seqno = 10. + let committed_block_id = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 10, + UInt256::rand(), + UInt256::rand(), + ); + fixture.processor.last_committed_seqno = Some(10); + fixture.processor.last_committed_block_id = Some(committed_block_id); + + // Both seqnos are ahead of expected (11), so MC fast-path returns + // WaitingForFinalCert and both entries remain in the journal. + let slot_a = SlotIndex::new(30); + let hash_a = UInt256::rand(); + let id_a = RawCandidateId { slot: slot_a, hash: hash_a.clone() }; + let block_id_a = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 13, // ahead: expected=11 + UInt256::rand(), + UInt256::rand(), + ); + + let slot_b = SlotIndex::new(25); + let hash_b = UInt256::rand(); + let id_b = RawCandidateId { slot: slot_b, hash: hash_b.clone() }; + let block_id_b = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 12, // ahead: expected=11 + UInt256::rand(), + UInt256::rand(), + ); + + for (id, slot, hash, block_id) in + [(&id_a, slot_a, &hash_a, &block_id_a), (&id_b, slot_b, &hash_b, &block_id_b)] + { + fixture.processor.received_candidates.insert( + id.clone(), + ReceivedCandidate { + slot, + source_idx: ValidatorIndex::new(0), + candidate_id_hash: hash.clone(), + candidate_hash_data_bytes: vec![1, 2, 3], + block_id: block_id.clone(), + root_hash: block_id.root_hash.clone(), + file_hash: block_id.file_hash.clone(), + data: consensus_common::ConsensusCommonFactory::create_block_payload( + vec![0xAA].into(), + ), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + vec![0xBB].into(), + ), + receive_time: fixture.description.get_time(), + is_empty: false, + parent_id: None, + is_fully_resolved: true, + }, + ); + + let cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { slot, block_hash: hash.clone() }, + signatures: vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), + ], + }); + fixture.processor.finalized_journal_pending_commit.insert( + id.clone(), + FinalizedEntry { + event: BlockFinalizedEvent { + slot, + block_hash: hash.clone(), + block_id: Some(block_id.clone()), + certificate: cert, + }, + finalized_at: fixture.description.get_time(), + }, + ); + } + + fixture.processor.try_commit_finalized_chains(); + + // Both entries remain pending โ€” MC blocks ahead of committed head wait for FinalCert. + assert!(fixture.processor.finalized_journal_pending_commit.contains_key(&id_a)); + assert!(fixture.processor.finalized_journal_pending_commit.contains_key(&id_b)); + assert_eq!(fixture.processor.last_committed_seqno, Some(10)); +} + +/// Verify the finalized_uncommitted_gauge is updated correctly. +#[test] +fn test_finalized_uncommitted_gauge_tracks_journal_size() { + let mut fixture = TestFixture::new(4); + + // Empty journal โ€” gauge should be 0 (function runs without panic). + fixture.processor.try_commit_finalized_chains(); + + // Add a journal entry that will become AlreadyCommitted + let slot = SlotIndex::new(5); + let hash = UInt256::rand(); + let id = RawCandidateId { slot, hash: hash.clone() }; + + fixture.processor.last_committed_seqno = Some(100); + let committed_block_id = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 100, + UInt256::rand(), + UInt256::rand(), + ); + fixture.processor.last_committed_block_id = Some(committed_block_id.clone()); + + // Insert a received candidate with seqno < committed so collect_gapless returns AlreadyCommitted + fixture.processor.received_candidates.insert( + id.clone(), + ReceivedCandidate { + slot, + source_idx: ValidatorIndex::new(0), + candidate_id_hash: hash.clone(), + candidate_hash_data_bytes: vec![1, 2, 3], + block_id: committed_block_id.clone(), + root_hash: committed_block_id.root_hash.clone(), + file_hash: committed_block_id.file_hash.clone(), + data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xAA].into()), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + vec![0xBB].into(), + ), + receive_time: fixture.description.get_time(), + is_empty: false, + parent_id: None, + is_fully_resolved: true, + }, + ); + + let cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { slot, block_hash: hash.clone() }, + signatures: Vec::new(), + }); + fixture.processor.finalized_journal_pending_commit.insert( + id.clone(), + FinalizedEntry { + event: BlockFinalizedEvent { + slot, + block_hash: hash, + block_id: Some(committed_block_id), + certificate: cert, + }, + finalized_at: fixture.description.get_time(), + }, + ); + + assert_eq!(fixture.processor.finalized_journal_pending_commit.len(), 1); + + fixture.processor.try_commit_finalized_chains(); + + // The AlreadyCommitted entry should be removed + assert!( + fixture.processor.finalized_journal_pending_commit.is_empty(), + "AlreadyCommitted entry should be removed from journal" + ); +} + +/// Verify that seqno-sorted iteration commits sequential chains in a single pass +/// and schedules an immediate re-check via set_next_awake_time(now). +#[test] +fn test_sorted_pass_commits_sequential_chains_and_reschedules() { + let mut fixture = TestFixture::new(4); + + // Committed head at seqno 10 + let committed_block_id = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 10, + UInt256::rand(), + UInt256::rand(), + ); + fixture.processor.last_committed_seqno = Some(10); + fixture.processor.last_committed_block_id = Some(committed_block_id.clone()); + + // Build chain: slot_a (seqno 11, parent=boundary) โ†’ slot_b (seqno 12, parent=slot_a) + // On MC with matching expected_seqno, the MC fast-path commits the single block. + // After committing slot_a (seqno=11), re-loop should pick up slot_b (seqno=12). + let slot_a = SlotIndex::new(20); + let hash_a = UInt256::rand(); + let id_a = RawCandidateId { slot: slot_a, hash: hash_a.clone() }; + let block_id_a = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 11, + UInt256::rand(), + UInt256::rand(), + ); + + let slot_b = SlotIndex::new(25); + let hash_b = UInt256::rand(); + let id_b = RawCandidateId { slot: slot_b, hash: hash_b.clone() }; + let block_id_b = ton_block::BlockIdExt::with_params( + ton_block::ShardIdent::masterchain(), + 12, + UInt256::rand(), + UInt256::rand(), + ); + + // slot_a: parent = None (session boundary โ†’ MC fast-path single-commit if seqno matches) + fixture.processor.received_candidates.insert( + id_a.clone(), + ReceivedCandidate { + slot: slot_a, + source_idx: ValidatorIndex::new(0), + candidate_id_hash: hash_a.clone(), + candidate_hash_data_bytes: vec![1, 2, 3], + block_id: block_id_a.clone(), + root_hash: block_id_a.root_hash.clone(), + file_hash: block_id_a.file_hash.clone(), + data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xAA].into()), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + vec![0xBB].into(), + ), + receive_time: fixture.description.get_time(), + is_empty: false, + parent_id: None, + is_fully_resolved: true, + }, + ); + + // slot_b: parent = slot_a (will be WaitingForFinalCert initially since expected=11, seqno=12) + fixture.processor.received_candidates.insert( + id_b.clone(), + ReceivedCandidate { + slot: slot_b, + source_idx: ValidatorIndex::new(0), + candidate_id_hash: hash_b.clone(), + candidate_hash_data_bytes: vec![4, 5, 6], + block_id: block_id_b.clone(), + root_hash: block_id_b.root_hash.clone(), + file_hash: block_id_b.file_hash.clone(), + data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xCC].into()), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + vec![0xDD].into(), + ), + receive_time: fixture.description.get_time(), + is_empty: false, + parent_id: Some(id_a.clone()), + is_fully_resolved: true, + }, + ); + + // Provide FinalCert for both (MC requires FinalCert for non-empty blocks) + for (slot, hash) in [(&slot_a, &hash_a), (&slot_b, &hash_b)] { + let final_cert = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { slot: *slot, block_hash: hash.clone() }, + signatures: vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), + ], + }); + fixture + .processor + .simplex_state + .set_finalize_certificate(&fixture.description, *slot, hash, final_cert) + .expect("store final cert"); + } + + // Journal entries for both + for (id, slot, hash, block_id) in + [(&id_a, slot_a, &hash_a, &block_id_a), (&id_b, slot_b, &hash_b, &block_id_b)] + { + let cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { + vote: crate::simplex_state::FinalizeVote { slot, block_hash: hash.clone() }, + signatures: vec![ + crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), + crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), + ], + }); + fixture.processor.finalized_journal_pending_commit.insert( + id.clone(), + FinalizedEntry { + event: BlockFinalizedEvent { + slot, + block_hash: hash.clone(), + block_id: Some(block_id.clone()), + certificate: cert, + }, + finalized_at: fixture.description.get_time(), + }, + ); + } + + // Push next_awake_time into the future so we can verify it gets pulled back after commit. + fixture.processor.reset_next_awake_time(); + assert!( + fixture.processor.get_next_awake_time() > fixture.description.get_time(), + "next_awake_time should be in the future before commit" + ); + + // Because finalized_keys are sorted by seqno, slot_a (seqno=11) is processed + // first. After it commits, last_committed_seqno advances to 11, so when the + // iteration reaches slot_b (seqno=12), expected_seqno matches and it commits too. + fixture.processor.try_commit_finalized_chains(); + + assert_eq!( + fixture.processor.last_committed_seqno, + Some(12), + "sorted iteration should commit both seqno 11 and 12 in one pass" + ); + assert!( + fixture.processor.finalized_journal_pending_commit.is_empty(), + "all journal entries should be committed and removed" + ); + // Commits happened, so an immediate re-check should be scheduled. + assert!( + fixture.processor.get_next_awake_time() <= fixture.description.get_time(), + "next_awake_time should be <= now after a successful commit" + ); +} + +/// Verify that the correct processing order (validated candidates BEFORE +/// FSM timeouts) allows a candidate to be notarized even when the clock +/// has advanced past the skip timeout. +/// +/// Without Fix A (processing-order), calling `simplex_state.check_all()` +/// first would fire the `first_block_timeout` and skip-vote the slot +/// before the already-validated candidate is fed to the FSM. +#[test] +fn test_process_validated_candidates_before_fsm_timeout() { + let mut fixture = TestFixture::new(4); + + let slot = SlotIndex::new(0); + let candidate_hash = UInt256::rand(); + let candidate_id = RawCandidateId { slot, hash: candidate_hash.clone() }; + + // Create a non-empty candidate for slot 0 with no parent (genesis). + let raw_candidate = make_test_non_empty_candidate(candidate_id.clone(), None, &fixture.nodes); + let time = fixture.description.get_time(); + + // Insert pending validation so candidate_decision_ok_internal can find it. + insert_pending_validation(&mut fixture.processor, &candidate_id, raw_candidate, time); + + // Simulate validation success: push the resolved candidate into the queue. + fixture.processor.candidate_decision_ok_internal(candidate_id.clone(), slot, time); + assert!( + !fixture.processor.validated_candidates.is_empty(), + "candidate must be in the validated_candidates queue" + ); + + // Advance time past first_block_timeout + target_rate (defaults: 3s + 1s = 4s). + fixture.advance_time(Duration::from_secs(5)); + + // --- Correct order (Fix A): feed candidates, THEN run FSM timeouts --- + fixture.processor.process_validated_candidates(); + fixture.processor.simplex_state.check_all(&fixture.description); + + // Collect FSM events produced by the two calls above. + let mut has_notarize = false; + while let Some(event) = fixture.processor.simplex_state.pull_event() { + if let crate::simplex_state::SimplexEvent::BroadcastVote( + crate::simplex_state::Vote::Notarize(ref v), + ) = event + { + if v.slot == slot { + has_notarize = true; + } + } + } + + // The critical invariant: the candidate was notarized because + // process_validated_candidates() ran before simplex_state.check_all(). + assert!( + has_notarize, + "slot 0 must be notarized (candidate was fed to FSM before timeout evaluation)" + ); + // In C++ mode (allow_skip_after_notarize=true) a skip vote may follow + // the notarize vote after the timeout fires -- that is harmless and + // expected. The key property is that the notarize vote was emitted. +} + +/// Verify that the `log::warn!` for "drop because new block is already +/// committed" only fires when `cand_seqno <= committed_seqno`, i.e. the +/// candidate is actually dropped. When `cand_seqno > committed_seqno` +/// the candidate must proceed to `validated_candidates`. +#[test] +fn test_candidate_decision_ok_does_not_drop_when_cand_seqno_greater_than_committed() { + let mut fixture = TestFixture::new(4); + + let slot = SlotIndex::new(0); + let candidate_hash = UInt256::rand(); + let candidate_id = RawCandidateId { slot, hash: candidate_hash.clone() }; + + let raw_candidate = make_test_non_empty_candidate(candidate_id.clone(), None, &fixture.nodes); + let time = fixture.description.get_time(); + insert_pending_validation(&mut fixture.processor, &candidate_id, raw_candidate, time); + + // Set last_committed_seqno to a value BELOW the candidate's seqno. + // make_test_non_empty_candidate uses slot.value()+1 as seq_no, so for + // slot 0 the candidate seqno = 1. Setting committed to 0 means + // cand_seqno (1) > committed_seqno (0) โ†’ candidate must NOT be dropped. + fixture.processor.last_committed_seqno = Some(0); + + // Call the public wrapper which contains the guard. + let validity_start = time; + fixture.processor.candidate_decision_ok(slot, candidate_id.clone(), validity_start, time); + + // The candidate must have been pushed to validated_candidates (not dropped). + assert!( + !fixture.processor.validated_candidates.is_empty(), + "candidate with cand_seqno > committed_seqno must NOT be dropped" + ); + // And it must have been removed from pending_validations (consumed, not leaked). + assert!( + !fixture.processor.pending_validations.contains_key(&candidate_id), + "pending_validations entry must be consumed" + ); +} diff --git a/src/node/simplex/src/tests/test_simplex_state.rs b/src/node/simplex/src/tests/test_simplex_state.rs index 52663ae..f5727b5 100644 --- a/src/node/simplex/src/tests/test_simplex_state.rs +++ b/src/node/simplex/src/tests/test_simplex_state.rs @@ -5168,3 +5168,537 @@ fn test_available_base_skip_propagates_max_merge() { "skip-propagation max-merge must upgrade to the higher-slot parent" ); } + +// ========================================================================== +// Stale window guard tests +// ========================================================================== + +#[test] +fn test_stale_window_guard_current_leader_window_idx_updated_before_collation_check() { + // Verifies the ordering guarantee for leader window state: + // `current_leader_window_idx` must be up-to-date after notarization advances + // the progress cursor across a window boundary, BEFORE any code can check + // leader status (the stale-window guard in SessionProcessor::check_collation + // compares slot_window vs current_leader_window_idx). + // + // Setup: 4 validators, 2 slots per window. + // Progress both slots in window 0 via notarization -> cursor crosses to window 1. + let desc = create_test_desc(4, 2); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(0)); + + // Notarize slot 0 (3 out of 4 validators -> quorum) + let h0 = UInt256::from([0xD0u8; 32]); + let vote0 = Vote::Notarize(NotarizeVote { slot: SlotIndex::new(0), block_hash: h0.clone() }); + state.on_vote_test(&desc, ValidatorIndex::new(0), vote0.clone(), vec![1]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), vote0.clone(), vec![2]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), vote0, vec![3]).unwrap(); + + // Slot 0 notarized -> cursor at slot 1, still in window 0 + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); + assert_eq!( + state.current_leader_window_idx, + WindowIndex::new(0), + "window must not advance until full window is progressed" + ); + + // Notarize slot 1 (3 out of 4) + let h1 = UInt256::from([0xD1u8; 32]); + let vote1 = Vote::Notarize(NotarizeVote { slot: SlotIndex::new(1), block_hash: h1.clone() }); + state.on_vote_test(&desc, ValidatorIndex::new(0), vote1.clone(), vec![4]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), vote1.clone(), vec![5]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), vote1, vec![6]).unwrap(); + + // Both slots in window 0 are notarized -> cursor crosses to slot 2 (window 1) + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); + assert_eq!( + state.current_leader_window_idx, + WindowIndex::new(1), + "current_leader_window_idx must advance when progress cursor crosses window boundary" + ); + + // The stale-window guard: slot 0 is in window 0, but current window is 1. + // SessionProcessor::check_collation would see slot_window(0) < current_window(1) -> skip. + let slot0_window = desc.get_window_idx(SlotIndex::new(0)); + assert!( + slot0_window < state.current_leader_window_idx, + "slot 0 (window {slot0_window}) must be stale relative to current window {}", + state.current_leader_window_idx + ); + + // Slot 2 is in the current window -> not stale + let slot2_window = desc.get_window_idx(SlotIndex::new(2)); + assert_eq!( + slot2_window, state.current_leader_window_idx, + "slot 2 must be in the current window" + ); +} + +#[test] +fn test_stale_window_guard_skip_also_advances_window() { + // Same as above but using skip votes instead of notarization. + // Window advancement via skips must also update current_leader_window_idx. + let desc = create_test_desc(4, 2); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + // Skip slot 0 (3 out of 4 validators) + let skip0 = Vote::Skip(SkipVote { slot: SlotIndex::new(0) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip0.clone(), vec![1]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip0.clone(), vec![2]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip0, vec![3]).unwrap(); + + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); + assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); + + // Skip slot 1 (3 out of 4) + let skip1 = Vote::Skip(SkipVote { slot: SlotIndex::new(1) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip1.clone(), vec![4]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip1.clone(), vec![5]).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip1, vec![6]).unwrap(); + + // Both slots in window 0 skipped -> cursor at slot 2, window must advance + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); + assert_eq!( + state.current_leader_window_idx, + WindowIndex::new(1), + "current_leader_window_idx must advance after full-window skip" + ); +} + +/* + ======================================================================== + C++ parity: candidate pending storage despite local skip vote + + Regression tests for the fix to on_candidate() where candidates were + permanently dropped after a local skip vote. C++ consensus.cpp only + gates on voted_notar โ€” a skip vote must NOT prevent storing a candidate + as pending_block for later retry via check_pending_blocks. + ======================================================================== +*/ + +#[test] +fn test_candidate_stored_as_pending_despite_skip_vote_cpp_mode() { + // A local skip vote must NOT prevent storing a candidate as pending_block + // when try_notar fails (base not propagated yet). + // Reference: C++ consensus.cpp CandidateReceived only checks voted_notar. + let desc = create_test_desc(4, 4); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + // Cast local skip for all of window 1 (slots 4-7). + state.try_skip_window(WindowIndex::new(1)); + drain_events(&mut state); + + let w1 = state.get_window(WindowIndex::new(1)).unwrap(); + assert!(w1.slots[0].voted_skip, "voted_skip must be set for slot 4"); + assert!(w1.slots[0].voted_notar.is_none(), "voted_notar must NOT be set"); + assert!( + w1.slots[0].available_base.is_none(), + "available_base for slot 4 must be None (not propagated)" + ); + + // Submit candidate for slot 4 with genesis parent + let hash4 = UInt256::from([0xAA; 32]); + let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate).unwrap(); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + !events.iter().any(|e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(_)))), + "must NOT broadcast NotarVote โ€” base not propagated yet, got: {:?}", + events + ); + + let w1 = state.get_window(WindowIndex::new(1)).unwrap(); + assert!( + w1.slots[0].pending_block.is_some(), + "candidate must be stored as pending_block despite local skip vote (C++ parity)" + ); +} + +#[test] +fn test_pending_block_notarized_after_base_propagates_via_skip_certs() { + // Full lifecycle: candidate stored as pending after skip vote, then notarized + // when skip certs propagate the genesis base through to the candidate's slot. + let desc = create_test_desc(4, 4); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + // Cast local skip for window 1 (slots 4-7) + state.try_skip_window(WindowIndex::new(1)); + drain_events(&mut state); + + // Store candidate at slot 4 (pending โ€” base not propagated) + let hash4 = UInt256::from([0xBB; 32]); + let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate).unwrap(); + drain_events(&mut state); + + assert!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), + "precondition: candidate stored as pending" + ); + + let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; + + // Issue skip certs for s0, s1, s2, s3 โ€” each propagates genesis base one hop forward + for s in 0..4u32 { + let skip_cert = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(s), skip_cert).unwrap(); + } + + // After all 4 skip certs, genesis base should have reached slot 4. + // check_pending_blocks (called by propagate_base_after_skip_cert) must + // retry the pending candidate โ†’ try_notar succeeds โ†’ NotarVote emitted. + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + events.iter().any( + |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) + ), + "must emit NotarVote for pending candidate at slot 4 after base propagates, got: {:?}", + events + ); + + // Pending block should be cleared after successful notarization + assert!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_none(), + "pending_block must be cleared after notarization" + ); +} + +#[test] +fn test_candidate_dropped_when_voted_notar_cpp_mode() { + // When voted_notar is already set for a slot, a second candidate with a different + // hash must be correctly dropped (not stored as pending). + let desc = create_test_desc(4, 1); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); + + // Slot 0 has genesis base โ†’ first candidate succeeds immediately + let h1 = UInt256::from([0x11; 32]); + let candidate1 = create_test_candidate(0, h1.clone(), BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate1).unwrap(); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + events.iter().any( + |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { block_hash, .. })) if *block_hash == h1) + ), + "first candidate must trigger NotarVote" + ); + + // Now send a second candidate with a different hash for the same slot + let h2 = UInt256::from([0x22; 32]); + let candidate2 = create_test_candidate(0, h2, BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate2).unwrap(); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + !events.iter().any(|e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(_)))), + "second candidate must NOT trigger NotarVote (voted_notar already set)" + ); + + // Candidate must NOT be stored as pending โ€” voted_notar gates it + let w0 = state.get_window(WindowIndex::new(0)).unwrap(); + assert!( + w0.slots[0].pending_block.is_none(), + "candidate must NOT be stored as pending when voted_notar is set" + ); +} + +#[test] +fn test_out_of_order_skip_certs_still_propagate_base_to_pending() { + // Out-of-order skip cert arrival: s3 arrives first but has no base, so + // nothing propagates. Later s0, s1, s2 arrive in order โ€” when s2 is + // processed, find_next_nonskipped_slot skips over s3 (already marked + // skipped) and propagates genesis base directly to s4. + let desc = create_test_desc(4, 4); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + // Cast local skip for window 1 (slots 4-7) + state.try_skip_window(WindowIndex::new(1)); + drain_events(&mut state); + + // Store candidate at slot 4 (pending โ€” no base) + let hash4 = UInt256::from([0xCC; 32]); + let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate).unwrap(); + drain_events(&mut state); + + assert!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), + "precondition: candidate stored as pending" + ); + + let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; + + // Issue skip cert for s3 FIRST (out of order) + let skip3 = create_test_skip_cert(&desc, SlotIndex::new(3), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(3), skip3).unwrap(); + + // s3 has no base โ†’ nothing propagates โ†’ no vote yet + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + !events.iter().any( + |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) + ), + "no NotarVote yet โ€” s3 had no base to propagate" + ); + + // Issue skip certs for s0, s1 + for s in 0..2u32 { + let skip = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(s), skip).unwrap(); + drain_events(&mut state); + } + + // Verify slot 4 still has no base (propagated to s2 only so far) + assert!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), + "candidate still pending after s0+s1 skip certs" + ); + + // Issue skip cert for s2 โ€” propagation chain: s2 skipped, find_next_nonskipped(s2) + // skips over s3 (already skipped) โ†’ lands on s4 โ†’ base arrives โ†’ pending block retried + let skip2 = create_test_skip_cert(&desc, SlotIndex::new(2), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(2), skip2).unwrap(); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + events.iter().any( + |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) + ), + "must emit NotarVote for slot 4 after out-of-order skip certs propagate base, got: {:?}", + events + ); + + assert!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_none(), + "pending_block must be cleared after successful notarization" + ); +} + +/* + ======================================================================== + Base propagation chaining through already-skipped slots + + When skip certs arrive out of order, `propagate_base_after_skip_cert` + must chain the base forward through all consecutive already-skipped + intermediate slots. Without this, the base jumps from the cert's slot + to the first non-skipped slot, leaving intermediate slots baseless + and pending blocks stuck forever (no backward-walk like C++ has). + ======================================================================== +*/ + +#[test] +fn test_base_chains_through_already_skipped_slots() { + // Scenario: skip certs for slots 1-6 arrive BEFORE slot 0's cert. + // When slot 0's cert is finally processed, the chaining loop must + // propagate the genesis base through slots 1โ†’2โ†’3โ†’4โ†’5โ†’6โ†’7. + let desc = create_test_desc(4, 8); // 4 validators, 8 slots/window + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; + + // Issue skip certs for slots 1-6 first (out of order โ€” slot 0 last) + for s in 1..=6u32 { + let cert = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(s), cert).unwrap(); + } + drain_events(&mut state); + + // Verify: slots 1-6 are skipped but have no available_base (no source yet) + for s in 1..=6u32 { + let base = state.get_slot_ref(&desc, SlotIndex::new(s)).unwrap().available_base.clone(); + assert!( + base.is_none(), + "slot {} should have no base before slot 0's cert chains through", + s + ); + } + + // Now issue skip cert for slot 0 โ€” triggers chaining through 1โ†’2โ†’3โ†’4โ†’5โ†’6โ†’7 + let cert0 = create_test_skip_cert(&desc, SlotIndex::new(0), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(0), cert0).unwrap(); + drain_events(&mut state); + + // Every intermediate skipped slot must now have the genesis base + for s in 1..=6u32 { + let base = state.get_slot_ref(&desc, SlotIndex::new(s)).unwrap().available_base.clone(); + assert_eq!( + base, + Some(None), // genesis + "slot {} must have genesis base after chaining from slot 0", + s + ); + } + + // Slot 7 (first non-skipped after the chain) must also have the base + let base7 = state.get_slot_ref(&desc, SlotIndex::new(7)).unwrap().available_base.clone(); + assert_eq!(base7, Some(None), "slot 7 (first non-skipped) must have genesis base"); +} + +#[test] +fn test_base_chaining_enables_pending_block_at_intermediate_skipped_slot() { + // Regression test for the real-network failure mode: + // A pending block sits at a slot whose skip cert arrived before the base + // propagated. The old code would never set `available_base` on that slot + // because `find_next_nonskipped_slot` jumped past it. The chaining fix + // ensures the base reaches it. + let desc = create_test_desc(4, 8); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; + + // Skip-vote slot 4 locally so candidate can be stored as pending + state.try_skip_window(WindowIndex::new(0)); + drain_events(&mut state); + + // Store a pending candidate at slot 4 (parent = genesis) + let hash4 = UInt256::from([0xDD; 32]); + let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate).unwrap(); + drain_events(&mut state); + + assert!( + state.get_window(WindowIndex::new(0)).unwrap().slots[4].pending_block.is_some(), + "precondition: candidate stored as pending at slot 4" + ); + + // Skip certs for slots 1-3 arrive BEFORE slot 0. + // Slot 4 doesn't get a skip cert (it only has a local skip vote). + for s in 1..=3u32 { + let cert = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(s), cert).unwrap(); + } + drain_events(&mut state); + + // Verify no notarize vote yet โ€” slot 4 still has no base + assert!( + state.get_window(WindowIndex::new(0)).unwrap().slots[4].pending_block.is_some(), + "candidate still pending โ€” base hasn't reached slot 4 yet" + ); + + // Now process slot 0's skip cert โ†’ chain: 0โ†’1โ†’2โ†’3โ†’4 (slot 4 not skipped-cert) + let cert0 = create_test_skip_cert(&desc, SlotIndex::new(0), &signers); + state.set_skip_certificate(&desc, SlotIndex::new(0), cert0).unwrap(); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + events.iter().any( + |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) + ), + "must emit NotarVote for pending slot 4 after base chains through, got: {:?}", + events + ); + + assert!( + state.get_window(WindowIndex::new(0)).unwrap().slots[4].pending_block.is_none(), + "pending_block must be cleared after notarization" + ); +} + +#[test] +fn test_pending_block_not_overwritten_by_second_candidate_cpp_mode() { + // C++ parity: first pending candidate wins. A second candidate with a different + // hash for the same slot must be rejected (equivocation), keeping the original. + let desc = create_test_desc(4, 4); + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + // Cast local skip for window 1 (slots 4-7) so candidates go to pending + state.try_skip_window(WindowIndex::new(1)); + drain_events(&mut state); + + // Store candidate A at slot 4 as pending (no base โ†’ try_notar fails) + let hash_a = UInt256::from([0xAA; 32]); + let candidate_a = create_test_candidate(4, hash_a.clone(), BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate_a).unwrap(); + drain_events(&mut state); + + assert!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), + "precondition: candidate A stored as pending" + ); + assert_eq!( + state.get_window(WindowIndex::new(1)).unwrap().slots[0] + .pending_block + .as_ref() + .unwrap() + .id + .hash, + hash_a, + "precondition: pending_block is candidate A" + ); + + let pending_count_before = state.pending_slots.len(); + + // Submit candidate B with a different hash for the same slot 4 + let hash_b = UInt256::from([0xBB; 32]); + let candidate_b = create_test_candidate(4, hash_b, BlockIdExt::default(), None, 1); + state.on_candidate(&desc, candidate_b).unwrap(); + drain_events(&mut state); + + // pending_block must still hold candidate A (not B) + let w1 = state.get_window(WindowIndex::new(1)).unwrap(); + assert_eq!( + w1.slots[0].pending_block.as_ref().unwrap().id.hash, + hash_a, + "pending_block must still be candidate A โ€” first candidate wins" + ); + + // No additional PendingSlot should have been pushed + assert_eq!( + state.pending_slots.len(), + pending_count_before, + "no additional PendingSlot should be pushed for duplicate/equivocating candidate" + ); +} + +#[test] +fn test_try_notar_not_blocked_by_its_over_after_finalize_restart_cpp_mode() { + // C++ parity: after restart with a persisted Finalize vote, its_over=true and + // voted_final=true are set, but voted_notar remains None. C++ try_notarize() + // does NOT check voted_final, so notarization must still proceed. + let desc = create_test_desc(4, 1); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); + + // Simulate restart recovery: mark slot 0 as having a persisted Finalize vote + let finalize_vote = Vote::Finalize(FinalizeVote { + slot: SlotIndex::new(0), + block_hash: UInt256::from([0xFF; 32]), + }); + state.mark_slot_voted_on_restart(&desc, &finalize_vote); + + // Verify preconditions + let w0 = state.get_window(WindowIndex::new(0)).unwrap(); + assert!(w0.slots[0].its_over, "precondition: its_over must be true after Finalize restart"); + assert!( + w0.slots[0].voted_final, + "precondition: voted_final must be true after Finalize restart" + ); + assert!( + w0.slots[0].voted_notar.is_none(), + "precondition: voted_notar must be None (Finalize does not set it)" + ); + + // Submit candidate for slot 0 (has genesis base โ†’ should succeed) + let hash = UInt256::from([0xCC; 32]); + let candidate = create_test_candidate(0, hash, BlockIdExt::default(), None, 0); + state.on_candidate(&desc, candidate).unwrap(); + + let events: Vec<_> = from_fn(|| state.pull_event()).collect(); + assert!( + events.iter().any( + |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(0)) + ), + "must emit NotarVote for slot 0 โ€” its_over must NOT block try_notar in C++ mode, got: {:?}", + events + ); +} diff --git a/src/node/simplex/tests/test_collation.rs b/src/node/simplex/tests/test_collation.rs index b785cc3..0be6bdd 100644 --- a/src/node/simplex/tests/test_collation.rs +++ b/src/node/simplex/tests/test_collation.rs @@ -350,7 +350,6 @@ fn run_collation_test() { &session_opts, &session_id, &shard, - initial_block_seqno, nodes, &private_key, db_path, @@ -358,6 +357,7 @@ fn run_collation_test() { Arc::downgrade(&session_listener), ) .expect("Failed to create session"); + session.start(initial_block_seqno); log::info!("Session created, waiting for collation callback..."); diff --git a/src/node/simplex/tests/test_consensus.rs b/src/node/simplex/tests/test_consensus.rs index dfc9432..8314bab 100644 --- a/src/node/simplex/tests/test_consensus.rs +++ b/src/node/simplex/tests/test_consensus.rs @@ -1292,7 +1292,6 @@ where &session_opts, &session_id, &shard, - initial_block_seqno, nodes.clone(), &local_key, db_path, @@ -1300,6 +1299,7 @@ where Arc::downgrade(&session_listener), ) .unwrap(); + session.start(initial_block_seqno); let session_instance = Arc::new(SpinMutex::new(SessionInstance { public_key: nodes[i].public_key.clone(), @@ -1625,12 +1625,10 @@ where let session_listener: Arc = new_listener.clone(); - // Recreate the session with the same DB path (recovery from persistent storage). let new_session = SessionFactory::create_session( &ctx.session_opts, &ctx.session_id, &ctx.shard, - ctx.initial_block_seqno, ctx.nodes.as_ref().clone(), &ctx.local_key, ctx.db_path.clone(), @@ -1640,6 +1638,7 @@ where match new_session { Ok(session) => { + session.start(ctx.initial_block_seqno); // Create a completely new SessionInstance with fresh state. // The seqno trackers are shared with the listener - they were already // updated by on_block_committed during recovery (before this point). @@ -2475,6 +2474,193 @@ fn test_collated_file_hash_consistency() { ); } +/// Verify the start gate: sessions create the overlay immediately but do NOT +/// begin FSM processing until `start(seqno)` is called. +/// +/// This tests the overlay-warmup fix for the mixed C++/Rust timing gap where +/// the FSM's `first_block_timeout` would fire before the overlay had established +/// peer connections, permanently stalling finalization. +#[test] +fn test_simplex_start_gate() { + let _test_lock = SIMPLEX_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + const DB_PATH: &str = "../../target/test"; + + if !is_test_logging_enabled() { + return; + } + + let _ = env_logger::builder().is_test(true).try_init(); + + let node_count = 7usize; + let shard = ShardIdent::masterchain(); + let initial_block_seqno = 1u32; + + let mut nodes = Vec::with_capacity(node_count); + for _ in 0..node_count { + let key = Ed25519KeyOption::generate().unwrap(); + let adnl_id = key.id().clone(); + nodes.push(SessionNode { public_key: key, adnl_id, weight: 1 }); + } + + let overlay_manager = SessionFactory::create_in_process_overlay_manager(node_count); + + let rand_name: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(7) + .map(char::from) + .collect(); + let db_path_base = format!("{}/simplex_start_gate_{}", DB_PATH, rand_name); + let mut rng = rand::thread_rng(); + let session_id: UInt256 = UInt256::from(rng.gen::<[u8; 32]>()); + + let session_opts = SessionOptions { + proto_version: 0, + target_rate: Duration::from_millis(200), + first_block_timeout: Duration::from_millis(1000), + slots_per_leader_window: 1, + wait_for_db_init: true, + ..Default::default() + }; + + let committed_blocks: CommittedBlocksMap = Arc::new(Mutex::new(HashMap::new())); + let commit_counters: Vec> = + (0..node_count).map(|_| Arc::new(AtomicU32::new(0))).collect(); + let mut sessions: Vec = Vec::new(); + // Keep instances alive so the listener Weak pointers remain valid. + let mut instances: Vec>> = Vec::new(); + + let config = TestConfig { + total_rounds: 10, + min_commit_percent: 0.5, + node_count, + generation_failure_probability: 0.0, + candidate_rejection_probability: 0.0, + max_collations: 10000, + target_rate: Duration::from_millis(200), + first_block_timeout: Duration::from_millis(1000), + test_name: "simplex_start_gate".to_string(), + test_timeout: Duration::from_secs(60), + expect_timeout: false, + shard: shard.clone(), + mc_notification_interval: None, + overlay_type: OverlayType::InProcess, + net_gremlin: None, + restart_gremlin: None, + lossy_overlay: None, + lossy_overlay_node_indices: None, + standstill_timeout: None, + }; + + for i in 0..node_count { + let local_key = nodes[i].public_key.clone(); + let db_path = format!("{}_node{}", db_path_base, i); + let approved_candidates: Arc< + Mutex>>, + > = Arc::new(Mutex::new(HashMap::new())); + let next_expected_commit_seqno = Arc::new(AtomicU32::new(initial_block_seqno)); + + let listener = Arc::new(SessionInstanceListener { + instance: SpinMutex::new(Weak::new()), + approved_candidates: approved_candidates.clone(), + next_expected_commit_seqno: next_expected_commit_seqno.clone(), + committed_blocks: committed_blocks.clone(), + }); + let session_listener: Arc = listener.clone(); + + let session = SessionFactory::create_session( + &session_opts, + &session_id, + &shard, + nodes.clone(), + &local_key, + db_path, + overlay_manager.clone(), + Arc::downgrade(&session_listener), + ) + .expect("Failed to create session"); + + let session_instance = Arc::new(SpinMutex::new(SessionInstance { + source_index: i as u32, + public_key: nodes[i].public_key.clone(), + batch_processed: Arc::new(AtomicBool::new(false)), + collation_requested: Arc::new(AtomicBool::new(false)), + collation_count: Arc::new(AtomicU32::new(0)), + on_candidate_count: Arc::new(AtomicU32::new(0)), + on_block_committed_count: commit_counters[i].clone(), + is_collator: Arc::new(AtomicBool::new(false)), + config: config.clone(), + current_round: Arc::new(AtomicU32::new(0)), + commit_latencies: Arc::new(Mutex::new(Vec::new())), + next_expected_commit_seqno, + session_errors_count: Arc::new(AtomicU32::new(0)), + approved_candidates, + committed_blocks: committed_blocks.clone(), + _session: session.clone(), + _listener: listener.clone(), + })); + + *listener.instance.lock() = Arc::downgrade(&session_instance); + + sessions.push(session); + instances.push(session_instance); + } + + // Phase 1: verify no commits while sessions are gated (overlay is warming up) + log::info!("[start_gate] Phase 1: verifying no commits for 2s without start()"); + thread::sleep(Duration::from_secs(2)); + for (i, counter) in commit_counters.iter().enumerate() { + let commits = counter.load(Ordering::Relaxed); + assert_eq!( + commits, 0, + "Node {} committed {} blocks before start() was called โ€” start gate failed", + i, commits + ); + } + log::info!("[start_gate] Phase 1 passed: zero commits before start()"); + + // Phase 2: call start(seqno) on all sessions, then wait for commits + log::info!( + "[start_gate] Phase 2: calling start(seqno={}) on all sessions", + initial_block_seqno + ); + for session in &sessions { + session.start(initial_block_seqno); + } + + let deadline = Instant::now() + Duration::from_secs(30); + let min_commits = 3u32; + loop { + thread::sleep(Duration::from_millis(200)); + let all_committed = + commit_counters.iter().all(|c| c.load(Ordering::Relaxed) >= min_commits); + if all_committed { + break; + } + if Instant::now() > deadline { + for (i, counter) in commit_counters.iter().enumerate() { + log::error!("[start_gate] Node {} commits: {}", i, counter.load(Ordering::Relaxed)); + } + panic!( + "Timed out waiting for {} commits after start() โ€” \ + sessions did not begin processing after start gate was released", + min_commits + ); + } + } + + log::info!( + "[start_gate] Phase 2 passed: all nodes committed >= {} blocks after start()", + min_commits + ); + + for session in &sessions { + session.stop(); + } + drop(instances); + log::info!("[start_gate] Test passed"); +} + /// Test that empty collated_data produces a valid (non-default) hash #[test] fn test_empty_collated_data_hash() { diff --git a/src/node/simplex/tests/test_restart.rs b/src/node/simplex/tests/test_restart.rs index 264ba47..1db5cc0 100644 --- a/src/node/simplex/tests/test_restart.rs +++ b/src/node/simplex/tests/test_restart.rs @@ -490,7 +490,6 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate &session_opts, &session_id, &shard, - initial_block_seqno, nodes.clone(), &private_key, db_path.clone(), @@ -498,6 +497,7 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate Arc::downgrade(&session_listener), ) .expect("Failed to create session (phase 1)"); + session_1.start(initial_block_seqno); let rounds_before_restart: u32 = 5; let start = Instant::now(); @@ -550,7 +550,6 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate &session_opts, &session_id, &shard, - restart_initial_seqno, nodes, &private_key, db_path, @@ -558,6 +557,7 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate Arc::downgrade(&session_listener), ) .expect("Failed to create session (phase 2)"); + session_2.start(restart_initial_seqno); // Wait for first post-restart slot generation (proof that current slot was seeded) let start = Instant::now(); diff --git a/src/node/simplex/tests/test_validation.rs b/src/node/simplex/tests/test_validation.rs index c980ce7..b5985cf 100644 --- a/src/node/simplex/tests/test_validation.rs +++ b/src/node/simplex/tests/test_validation.rs @@ -402,7 +402,6 @@ fn run_validation_test() { &session_opts, &session_id, &shard, - initial_block_seqno, nodes.clone(), &private_key_0, db_path_0, @@ -410,13 +409,12 @@ fn run_validation_test() { Arc::downgrade(&session_listener_0), ) .expect("Failed to create session 0"); + session_0.start(initial_block_seqno); - // Create session for node 1 let session_1 = SessionFactory::create_session( &session_opts, &session_id, &shard, - initial_block_seqno, nodes.clone(), &private_key_1, db_path_1, @@ -424,6 +422,7 @@ fn run_validation_test() { Arc::downgrade(&session_listener_1), ) .expect("Failed to create session 1"); + session_1.start(initial_block_seqno); log::info!("Sessions created, waiting for validation callback on node 1..."); diff --git a/src/node/src/config.rs b/src/node/src/config.rs index 5ae331c..4ca45ab 100644 --- a/src/node/src/config.rs +++ b/src/node/src/config.rs @@ -793,6 +793,7 @@ impl TonNodeConfig { Ok(()) } + #[allow(dead_code)] fn get_validator_key_info(&self, validator_key_id: &str) -> Result> { if let Some(validator_keys) = &self.validator_keys { for key_json in validator_keys { @@ -1021,10 +1022,11 @@ impl TonNodeConfig { election_id: i32, expire_at: i32, ) -> Result { + let new_key_id_b64 = base64_encode(key_id); let key_info = ValidatorKeysJson { expire_at, election_id, - validator_key_id: base64_encode(key_id), + validator_key_id: new_key_id_b64.clone(), validator_adnl_key_id: None, }; @@ -1034,7 +1036,16 @@ impl TonNodeConfig { let added_key_info = self.get_validator_key_info_by_election_id(&election_id)?; match &mut self.validator_keys { Some(validator_keys) => match added_key_info { - Some(_) => { + Some(existing) => { + if existing.validator_key_id != new_key_id_b64 { + log::warn!( + "add_validator_key: OVERWRITING validator key for election_id={}: \ + old_key={} -> new_key={} (adnl_key will be cleared)", + election_id, + existing.validator_key_id, + new_key_id_b64, + ); + } self.update_validator_key_info(key_info.clone())?; } None => { @@ -1054,12 +1065,46 @@ impl TonNodeConfig { validator_key_id: &[u8; 32], adnl_key_id: &[u8; 32], ) -> Result { - if let Some(mut key_info) = self.get_validator_key_info(&base64_encode(validator_key_id))? { - key_info.validator_adnl_key_id = Some(base64_encode(adnl_key_id)); - self.update_validator_key_info(key_info) - } else { + let key_id_b64 = base64_encode(validator_key_id); + let new_adnl_b64 = base64_encode(adnl_key_id); + + let matching: Vec = self + .validator_keys + .as_ref() + .map(|keys| keys.iter().filter(|k| k.validator_key_id == key_id_b64).cloned().collect()) + .unwrap_or_default(); + + if matching.is_empty() { fail!("Validator key have not been added!") } + + let mut last_updated = None; + for entry in &matching { + if let Some(existing_adnl) = &entry.validator_adnl_key_id { + if *existing_adnl != new_adnl_b64 { + log::warn!( + "add_validator_adnl_key: OVERWRITING adnl key for election_id={}: \ + old_adnl={} -> new_adnl={} (validator_key={})", + entry.election_id, + existing_adnl, + new_adnl_b64, + key_id_b64, + ); + } + } + let mut updated = entry.clone(); + updated.validator_adnl_key_id = Some(new_adnl_b64.clone()); + last_updated = Some(self.update_validator_key_info(updated)?); + } + if matching.len() > 1 { + log::info!( + "add_validator_adnl_key: updated adnl binding for {} elections sharing \ + validator_key={}", + matching.len(), + key_id_b64, + ); + } + Ok(last_updated.unwrap()) } async fn remove_validator_key( @@ -2154,7 +2199,27 @@ impl ValidatorKeys { // inserted in sorted order let mut first = false; - add_unbound_object_to_map_with_update(&self.values, key.election_id, |_| { + add_unbound_object_to_map_with_update(&self.values, key.election_id, |old| { + if let Some(existing) = old { + if existing.validator_key_id != key.validator_key_id { + log::warn!( + "ValidatorKeys: replacing validator key for election_id={}: \ + old_key={} -> new_key={}", + key.election_id, + existing.validator_key_id, + key.validator_key_id, + ); + } + if existing.validator_adnl_key_id != key.validator_adnl_key_id { + log::warn!( + "ValidatorKeys: adnl key changed for election_id={}: \ + old_adnl={:?} -> new_adnl={:?}", + key.election_id, + existing.validator_adnl_key_id, + key.validator_adnl_key_id, + ); + } + } if self .first .compare_exchange( diff --git a/src/node/src/engine.rs b/src/node/src/engine.rs index f734c37..862497d 100644 --- a/src/node/src/engine.rs +++ b/src/node/src/engine.rs @@ -430,14 +430,15 @@ impl Engine { pub const MASK_SERVICE_ARCHIVES_GC: u32 = 0x0800; pub const MASK_SERVICE_SS_CACHE_KEEPER: u32 = 0x1000; - // Sync status - pub const SYNC_STATUS_START_BOOT: u32 = 0x0001; - pub const SYNC_STATUS_LOAD_STATES: u32 = 0x0003; - pub const SYNC_STATUS_FINISH_BOOT: u32 = 0x0004; - pub const SYNC_STATUS_SYNC_BLOCKS: u32 = 0x0005; - pub const SYNC_STATUS_FINISH_SYNC: u32 = 0x0006; - pub const SYNC_STATUS_CHECKING_DB: u32 = 0x0007; - pub const SYNC_STATUS_DB_BROKEN: u32 = 0x0008; + // Sync status (ordered by normal flow: boot โ†’ states โ†’ finish boot โ†’ archives โ†’ blocks โ†’ synced) + pub const SYNC_STATUS_START_BOOT: u32 = 1; + pub const SYNC_STATUS_LOAD_STATES: u32 = 2; + pub const SYNC_STATUS_FINISH_BOOT: u32 = 3; + pub const SYNC_STATUS_SYNC_ARCHIVES: u32 = 4; + pub const SYNC_STATUS_SYNC_BLOCKS: u32 = 5; + pub const SYNC_STATUS_FINISH_SYNC: u32 = 6; + pub const SYNC_STATUS_CHECKING_DB: u32 = 7; + pub const SYNC_STATUS_DB_BROKEN: u32 = 8; const MASK_STOP: u32 = 0x80000000; const TIMEOUT_STOP_MS: u64 = 1000; @@ -2293,6 +2294,7 @@ pub async fn run( // Sync by archives if sync_by_archives && !engine.check_sync().await? { + engine.set_sync_status(Engine::SYNC_STATUS_SYNC_ARCHIVES); struct Checker; #[async_trait::async_trait] impl crate::sync::StopSyncChecker for Checker { @@ -2531,8 +2533,8 @@ pub fn init_prometheus_recorder( // -- engine metrics::describe_gauge!( "ton_node_engine_sync_status", - "Sync state (0=not_set, 1=boot, 3=load_states, 4=finish_boot, \ - 5=syncing, 6=synced, 7=checking_db, 8=db_broken)" + "Sync state (0=not_set, 1=boot, 2=load_states, 3=finish_boot, \ + 4=sync_archives, 5=sync_blocks, 6=synced, 7=checking_db, 8=db_broken)" ); metrics::describe_gauge!( "ton_node_engine_timediff_seconds", diff --git a/src/node/src/engine_operations.rs b/src/node/src/engine_operations.rs index 2d3fdaa..00d0fcc 100644 --- a/src/node/src/engine_operations.rs +++ b/src/node/src/engine_operations.rs @@ -13,13 +13,16 @@ use crate::{ block_proof::BlockProofStuff, config::{CollatorConfig, CollatorTestBundlesGeneralConfig}, engine::{Engine, EngineFlags, SplitQueues}, - engine_traits::{EngineAlloc, EngineOperations, PrivateOverlayOperations}, + engine_traits::{ + EngineAlloc, EngineOperations, PrivateOverlayOperations, ValidatorKeyBinding, + ValidatorListOutcome, + }, error::NodeError, ext_messages::{create_ext_message, EXT_MESSAGES_TRACE_TARGET}, full_node::shard_client::{process_block_broadcast, process_block_broadcast_v2}, internal_db::{ - BlockResult, INITIAL_MC_BLOCK, LAST_APPLIED_MC_BLOCK, LAST_ROTATION_MC_BLOCK, - SHARD_CLIENT_MC_BLOCK, + BlockResult, DESTROYED_VALIDATOR_SESSIONS, INITIAL_MC_BLOCK, LAST_APPLIED_MC_BLOCK, + LAST_ROTATION_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, }, shard_state::ShardStateStuff, shard_states_keeper::PinnedShardStateGuard, @@ -44,11 +47,44 @@ use ton_api::ton::{ }; use ton_block::{ error, fail, AccountIdPrefixFull, BlockIdExt, BlockSignaturesVariant, Cell, CellsFactory, - ConfigParams, CryptoSignaturePair, KeyId, KeyOption, Message, OutMsgQueue, Result, ShardIdent, - UInt256, + ConfigParams, CryptoSignaturePair, KeyId, Message, OutMsgQueue, Result, ShardIdent, UInt256, }; use validator_session::{BlockHash, SessionId, ValidatorBlockCandidate}; +fn serialize_destroyed_session_ids(ids: &HashSet) -> Vec { + let mut sorted_ids = ids.iter().cloned().collect::>(); + sorted_ids.sort_by(|left, right| left.as_slice().cmp(right.as_slice())); + + let mut data = Vec::with_capacity(4 + sorted_ids.len() * 32); + data.extend_from_slice(&(sorted_ids.len() as u32).to_le_bytes()); + for id in sorted_ids { + data.extend_from_slice(id.as_slice()); + } + data +} + +fn deserialize_destroyed_session_ids(data: &[u8]) -> Result> { + if data.len() < 4 { + fail!("Destroyed-session payload is too short: {}", data.len()); + } + + let count = u32::from_le_bytes(data[..4].try_into()?) as usize; + let expected_len = 4 + count * 32; + if data.len() != expected_len { + fail!( + "Destroyed-session payload has invalid length: expected {}, got {}", + expected_len, + data.len() + ); + } + + let mut ids = Vec::with_capacity(count); + for chunk in data[4..].chunks_exact(32) { + ids.push(UInt256::from_slice(chunk)); + } + Ok(ids) +} + #[async_trait::async_trait] impl EngineOperations for Engine { // Global node's state @@ -91,23 +127,28 @@ impl EngineOperations for Engine { Engine::validator_network(self) } + /// Register the local node's participation in a validator list and update network overlays. + /// + /// Delegates to [`PrivateOverlayOperations::set_validator_list`] for key matching and + /// ADNL setup, then refreshes private and custom overlays **only** when the network + /// layer is fully ready (`network_ready == true`). Overlay updates require the ADNL key + /// to be loaded into the ADNL stack first, which is why they happen here rather than + /// at the call site. async fn set_validator_list( &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result>> { - let key = + ) -> Result { + let outcome = self.validator_network().set_validator_list(validator_list_id, validators).await?; - // Private overlays updated here, because we don't have the needed keys in adnl - // before set_validator_list call. - let state = self.load_last_applied_mc_state().await?; - let config = state.config_params()?; - self.overlays_router()?.update_private_overlays(config).await?; - - // Update custom overlays as well, because some of them can become active with new keys - self.overlays_router()?.update_custom_overlays(None).await?; - Ok(key) + if matches!(&outcome, ValidatorListOutcome::Selected { network_ready: true, .. }) { + let state = self.load_last_applied_mc_state().await?; + let config = state.config_params()?; + self.overlays_router()?.update_private_overlays(config).await?; + self.overlays_router()?.update_custom_overlays(None).await?; + } + Ok(outcome) } fn activate_validator_list(&self, validator_list_id: UInt256) -> Result<()> { @@ -122,6 +163,19 @@ impl EngineOperations for Engine { self.validation_status() } + fn get_validator_key_bindings(&self) -> Result> { + let keys = self.network().config_handler().get_actual_validator_keys()?; + Ok(keys + .into_iter() + .map(|k| ValidatorKeyBinding { + election_id: k.election_id, + validator_key_id: k.validator_key_id, + validator_adnl_key_id: k.validator_adnl_key_id, + expire_at: k.expire_at, + }) + .collect()) + } + fn set_validation_status(&self, status: ValidationStatus) { self.set_validation_status(status) } @@ -339,6 +393,26 @@ impl EngineOperations for Engine { self.db().drop_validator_state(LAST_ROTATION_MC_BLOCK) } + fn load_destroyed_session_ids(&self) -> Result> { + match self.db().load_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS)? { + Some(data) => deserialize_destroyed_session_ids(&data), + None => Ok(Vec::new()), + } + } + + fn save_destroyed_session_ids(&self, ids: &HashSet) -> Result<()> { + if ids.is_empty() { + self.db().drop_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS) + } else { + let data = serialize_destroyed_session_ids(ids); + self.db().save_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS, &data) + } + } + + fn clear_destroyed_session_ids(&self) -> Result<()> { + self.db().drop_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS) + } + fn save_block_candidate( &self, session_id: &SessionId, @@ -1198,3 +1272,7 @@ async fn redirect_external_message( fail!("External message is not properly formatted: {}", message) } } + +#[cfg(test)] +#[path = "tests/test_engine_operations.rs"] +mod tests; diff --git a/src/node/src/engine_traits.rs b/src/node/src/engine_traits.rs index 40d2e85..b3158b0 100644 --- a/src/node/src/engine_traits.rs +++ b/src/node/src/engine_traits.rs @@ -76,13 +76,59 @@ pub struct EngineAlloc { pub validator_sets: Arc, } +/// Config-level binding of a validator key to an election. +/// +/// Each entry represents the `(election_id, validator_key, adnl_key)` tuple +/// stored in the node configuration. `election_id` is the primary key โ€” +/// at most one binding must exist per election. +#[derive(Debug, Clone)] +pub struct ValidatorKeyBinding { + pub election_id: i32, + pub validator_key_id: String, + pub validator_adnl_key_id: Option, + pub expire_at: i32, +} + +/// Outcome of [`PrivateOverlayOperations::set_validator_list`]. +/// +/// Models the result of checking whether the local node belongs to a given validator list. +/// +/// # C++ counterpart +/// +/// C++ uses `get_validator()` (`manager.cpp`) which returns a `PublicKeyHash` +/// (zero = not a validator). There is no explicit `network_ready` concept in C++ because +/// ADNL identity is always resolvable (falling back to the validator public key hash when +/// `addr` is zero, see `create_validator_group()` in `manager.cpp`). The `network_ready` flag is a +/// Rust-specific extension that decouples validator membership from ADNL/overlay readiness. +/// Rust still records validator membership immediately, while overlay activation is retried +/// until the network layer finishes loading the ADNL key. +/// +/// # Variants +/// +/// - `Selected { key, matching_keys, network_ready }` -- local node's public key is in the +/// validator set. `key` is the first selected local key used for network setup, while +/// `matching_keys` preserves all local matches in C++ `temp_keys_` order so shard subsets +/// can still choose the right local validator key. `network_ready` is `true` when the +/// corresponding ADNL key and overlay infrastructure are operational; `false` when the +/// pubkey matched but ADNL setup is still pending. +/// - `NotValidator` -- no local key matches the validator set. +#[derive(Debug)] +pub enum ValidatorListOutcome { + Selected { + key: Arc, + matching_keys: Vec>, + network_ready: bool, + }, + NotValidator, +} + #[async_trait::async_trait] pub trait PrivateOverlayOperations: Sync + Send { async fn set_validator_list( &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result>>; + ) -> Result; fn activate_validator_list(&self, validator_list_id: UInt256) -> Result<()>; @@ -151,6 +197,13 @@ pub trait EngineOperations: Sync + Send { unimplemented!() } + /// Return all `(election_id, validator_key, adnl_key)` bindings known to this node. + /// + /// Used by the validator manager to display and verify key uniqueness per election_id. + fn get_validator_key_bindings(&self) -> Result> { + unimplemented!() + } + fn set_validation_status(&self, status: ValidationStatus) { unimplemented!() } @@ -187,7 +240,7 @@ pub trait EngineOperations: Sync + Send { &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result>> { + ) -> Result { unimplemented!() } @@ -294,6 +347,15 @@ pub trait EngineOperations: Sync + Send { fn clear_last_rotation_block_id(&self) -> Result<()> { unimplemented!() } + fn load_destroyed_session_ids(&self) -> Result> { + unimplemented!() + } + fn save_destroyed_session_ids(&self, _ids: &HashSet) -> Result<()> { + unimplemented!() + } + fn clear_destroyed_session_ids(&self) -> Result<()> { + unimplemented!() + } fn save_block_candidate( &self, session_id: &SessionId, diff --git a/src/node/src/internal_db/mod.rs b/src/node/src/internal_db/mod.rs index 356ac72..e80c567 100644 --- a/src/node/src/internal_db/mod.rs +++ b/src/node/src/internal_db/mod.rs @@ -72,6 +72,7 @@ const CELLSCOUNTERS_CF_NAME: &str = "cells_db_v6_counters"; /// Validator state keys pub(crate) const LAST_ROTATION_MC_BLOCK: &str = "LastRotationBlockId"; +pub(crate) const DESTROYED_VALIDATOR_SESSIONS: &str = "DestroyedValidatorSessions"; #[derive(Clone, Debug)] pub enum DataStatus { @@ -1227,6 +1228,21 @@ impl InternalDb { self.block_handle_storage.save_validator_state(key.to_string(), block_id) } + pub fn drop_validator_state_raw(&self, key: &'static str) -> Result<()> { + let _tc = TimeChecker::new(format!("drop_validator_state_raw {}", key), 30); + self.block_handle_storage.drop_validator_state_raw(key) + } + + pub fn load_validator_state_raw(&self, key: &'static str) -> Result>> { + let _tc = TimeChecker::new(format!("load_validator_state_raw {}", key), 30); + self.block_handle_storage.load_validator_state_raw(key) + } + + pub fn save_validator_state_raw(&self, key: &'static str, data: &[u8]) -> Result<()> { + let _tc = TimeChecker::new(format!("save_validator_state_raw {}", key), 30); + self.block_handle_storage.save_validator_state_raw(key, data) + } + pub async fn get_archive_id(&self, mc_seq_no: u32, shard: &ShardIdent) -> Option { let _tc = TimeChecker::new(format!("get_archive_id {mc_seq_no} {shard}"), 30); self.archive_manager.get_archive_id(mc_seq_no, shard).await diff --git a/src/node/src/internal_db/restore.rs b/src/node/src/internal_db/restore.rs index 2f871cc..c85e77b 100644 --- a/src/node/src/internal_db/restore.rs +++ b/src/node/src/internal_db/restore.rs @@ -11,8 +11,8 @@ use crate::{ block::{BlockIdExtExtention, BlockStuff}, internal_db::{ - BlockHandle, InternalDb, ARCHIVES_GC_BLOCK, LAST_APPLIED_MC_BLOCK, LAST_ROTATION_MC_BLOCK, - PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, + BlockHandle, InternalDb, ARCHIVES_GC_BLOCK, DESTROYED_VALIDATOR_SESSIONS, + LAST_APPLIED_MC_BLOCK, LAST_ROTATION_MC_BLOCK, PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, }, shard_state::ShardStateStuff, }; @@ -673,6 +673,7 @@ async fn calc_min_mc_state_id( min_id = id; } else { db.drop_validator_state(LAST_ROTATION_MC_BLOCK)?; + db.drop_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS)?; } } } diff --git a/src/node/src/network/catchain_client.rs b/src/node/src/network/catchain_client.rs index e447011..c96dd7e 100644 --- a/src/node/src/network/catchain_client.rs +++ b/src/node/src/network/catchain_client.rs @@ -467,6 +467,7 @@ impl CatchainOverlay for CatchainClient { _sender_id: &PublicKeyHash, _send_as: &PublicKeyHash, payload: BlockPayloadPtr, + _extra: Option>, ) { let msg = payload.clone(); let overlay_id = self.overlay_id.clone(); diff --git a/src/node/src/network/control.rs b/src/node/src/network/control.rs index 78478a5..ab0511b 100644 --- a/src/node/src/network/control.rs +++ b/src/node/src/network/control.rs @@ -309,6 +309,7 @@ impl ControlQuerySubscriber { Engine::SYNC_STATUS_START_BOOT => "start_boot".to_string(), Engine::SYNC_STATUS_LOAD_STATES => "load_states".to_string(), Engine::SYNC_STATUS_FINISH_BOOT => "finish_boot".to_string(), + Engine::SYNC_STATUS_SYNC_ARCHIVES => "synchronization_by_archives".to_string(), Engine::SYNC_STATUS_SYNC_BLOCKS => "synchronization_by_blocks".to_string(), Engine::SYNC_STATUS_FINISH_SYNC => "synchronization_finished".to_string(), Engine::SYNC_STATUS_CHECKING_DB => "checking_db".to_string(), diff --git a/src/node/src/network/full_node_overlays.rs b/src/node/src/network/full_node_overlays.rs index f96fdfd..3e9ee83 100644 --- a/src/node/src/network/full_node_overlays.rs +++ b/src/node/src/network/full_node_overlays.rs @@ -540,6 +540,11 @@ impl FullNodeOverlaysRouter { Ok(()) } + /// Look up the local ADNL key for the given validator set. + /// + /// Returns `None` both when the node is not a validator and when it is a validator + /// but the ADNL/overlay context is not yet ready (the `network_ready == false` case + /// in [`ValidatorListOutcome`]). Callers must tolerate `None` gracefully. fn try_get_our_key( self: &Arc, validators: &ValidatorSet, @@ -549,7 +554,11 @@ impl FullNodeOverlaysRouter { match self.network.try_get_validator_adnl_key(&val_list_id) { None => { - log::info!("We are not a validator"); + log::info!( + "No local validator ADNL key for list {:x} (node is either not a validator \ + for this list yet, or validator network context is still not ready)", + val_list_id + ); return Ok(None); } Some(k) => Ok(Some(k)), diff --git a/src/node/src/network/node_network.rs b/src/node/src/network/node_network.rs index c3dbb62..0f6189b 100644 --- a/src/node/src/network/node_network.rs +++ b/src/node/src/network/node_network.rs @@ -10,7 +10,7 @@ */ use crate::{ config::{ConfigEvent, NodeConfigHandler, NodeConfigSubscriber, TonNodeConfig}, - engine_traits::{EngineAlloc, PrivateOverlayOperations}, + engine_traits::{EngineAlloc, PrivateOverlayOperations, ValidatorListOutcome}, network::catchain_client::CatchainClient, }; #[cfg(feature = "telemetry")] @@ -82,6 +82,38 @@ struct ValidatorContext { current_set: Arc>, // zero or one element [0] } +/// Select the local node's entry from the validator list by local-key order. +/// +/// Returns `(Some(node), adnl_missing)` where `adnl_missing` is true when the matched +/// validator's ADNL ID is not among the locally known ADNL keys. This mirrors the C++ +/// `get_validator()` function (`manager.cpp`) which iterates `temp_keys_` and returns the +/// first local key that belongs to the validator set. C++ does not consider ADNL readiness +/// at this layer; the `adnl_missing` flag is a Rust-specific diagnostic for the +/// network-readiness model. +fn select_local_validator_candidate<'a>( + validators: &'a [CatchainNode], + validator_key_ids: &[Arc], + validator_adnl_key_ids: &[Arc], +) -> (Option<&'a CatchainNode>, bool) { + for key_id in validator_key_ids { + if let Some(local_validator) = validators.iter().find(|val| val.public_key.id() == key_id) { + let adnl_missing = !validator_adnl_key_ids.contains(&local_validator.adnl_id); + return (Some(local_validator), adnl_missing); + } + } + (None, false) +} + +fn collect_local_validator_candidates<'a>( + validators: &'a [CatchainNode], + validator_key_ids: &[Arc], +) -> Vec<&'a CatchainNode> { + validator_key_ids + .iter() + .filter_map(|key_id| validators.iter().find(|val| val.public_key.id() == key_id)) + .collect() +} + declare_counted!( struct ValidatorSetContext { validator_peers: Vec>, @@ -129,7 +161,7 @@ impl NodeNetwork { // Initialize QUIC transport (lazy: no endpoint bound until add_key() is called). // Validator ADNL keys are registered when a validator set is activated. let quic = { - let quic = adnl::QuicNode::new(vec![overlay.clone()], cancellation_token.clone()); + let quic = adnl::QuicNode::new(vec![overlay.clone()], cancellation_token.clone(), None); overlay.set_quic(quic.clone())?; Some(quic) }; @@ -486,52 +518,151 @@ impl NodeNetwork { #[async_trait::async_trait] impl PrivateOverlayOperations for NodeNetwork { + /// Check local validator membership, set up ADNL keys, and prepare overlay peers. + /// + /// Flow: + /// 1. Match local public keys against the validator list (`select_local_validator_candidate`) + /// 2. If matched, load or store the ADNL key for the validator's overlay address + /// 3. Fetch peer addresses via DHT; queue missing peers for background resolution + /// 4. Create the `ValidatorSetContext` and register overlay peers + /// + /// Returns `Selected { network_ready: false }` when the pubkey matches but ADNL setup + /// fails -- the caller should retry next round without treating this as non-membership. async fn set_validator_list( &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result>> { + ) -> Result { log::trace!("start set_validator_list validator_list_id: {validator_list_id:x}"); let validator_adnl_key_ids = self.config_handler.get_actual_validator_adnl_key_ids()?; let validator_key_ids = self.config_handler.get_actual_validator_key_ids()?; - let local_validator = validators.iter().find_map(|val| { - if !validator_adnl_key_ids.contains(&val.adnl_id) { - return None; - } - if !validator_key_ids.contains(&val.public_key.id()) { - return None; - } - Some(val.clone()) - }); - let Some(local_validator) = local_validator else { - return Ok(None); + + let local_validators = collect_local_validator_candidates(validators, &validator_key_ids); + let (local_validator, pubkey_matched_but_adnl_missing) = select_local_validator_candidate( + validators, + &validator_key_ids, + &validator_adnl_key_ids, + ); + let Some(local_validator) = local_validator.cloned() else { + log::trace!( + target: "validator_manager", + "set_validator_list {:x}: no local key found among {} validators \ + (local key_ids: {}, adnl_ids: {})", + validator_list_id, + validators.len(), + validator_key_ids.len(), + validator_adnl_key_ids.len() + ); + return Ok(ValidatorListOutcome::NotValidator); }; - let local_validator_key_raw = - self.config_handler.get_validator_key(local_validator.public_key.id()).await; - let (local_validator_key, election_id) = - local_validator_key_raw.ok_or_else(|| error!("validator key not found!"))?; - let local_validator_adnl_key = - match self.network_context.stack.adnl.key_by_id(&local_validator.adnl_id) { - Ok(adnl_key) => adnl_key, - Err(e) => { - // adnl key isn`t stored in two cases: - // 1. First elections. Then make storing adnl key and repeat its load. - // 2. Internal error. In this case the error will be returned - log::warn!("error load adnl validator key (first attempt): {e}"); - if !self - .load_and_store_validator_adnl_key( - local_validator.adnl_id.clone(), - election_id, - ) - .await? - { + let mut matching_local_keys = Vec::with_capacity(local_validators.len()); + let mut election_id = None; + for validator in &local_validators { + let (validator_key, current_election_id) = self + .config_handler + .get_validator_key(validator.public_key.id()) + .await + .ok_or_else(|| error!("validator key not found!"))?; + if let Some(first_eid) = election_id { + if first_eid != current_election_id { + fail!( + "set_validator_list {:x}: election_id mismatch among matching local \ + keys: first key election_id={}, this key election_id={} (key_id={}). \ + Each election_id must map to exactly one (validator_key, adnl_id) tuple.", + validator_list_id, + first_eid, + current_election_id, + hex::encode(validator.public_key.id().data()), + ); + } + } else { + election_id = Some(current_election_id); + } + matching_local_keys.push(validator_key); + } + let local_validator_key = matching_local_keys + .first() + .cloned() + .ok_or_else(|| error!("validator key not found!"))?; + let election_id = election_id.ok_or_else(|| error!("validator election id not found!"))?; + + if pubkey_matched_but_adnl_missing { + log::warn!( + target: "validator_manager", + "set_validator_list {:x}: public key {} matches local key but ADNL id {} \ + is not in actual ADNL key set ({} keys). Possible config/key-binding issue.", + validator_list_id, + hex::encode(local_validator.public_key.id().data()), + hex::encode(local_validator.adnl_id.data()), + validator_adnl_key_ids.len() + ); + } + let local_validator_adnl_key = match self + .network_context + .stack + .adnl + .key_by_id(&local_validator.adnl_id) + { + Ok(adnl_key) => adnl_key, + Err(e) => { + log::warn!("error load adnl validator key (first attempt): {e}"); + match self + .load_and_store_validator_adnl_key(local_validator.adnl_id.clone(), election_id) + .await + { + Ok(true) => {} + Ok(false) if pubkey_matched_but_adnl_missing => { + log::warn!( + target: "validator_manager", + "set_validator_list {:x}: ADNL key {} not available yet \ + (pubkey matched, network not ready; will retry next round)", + validator_list_id, local_validator.adnl_id, + ); + return Ok(ValidatorListOutcome::Selected { + key: local_validator_key.clone(), + matching_keys: matching_local_keys.clone(), + network_ready: false, + }); + } + Ok(false) => { fail!("can't load and store adnl key (id: {})", &local_validator.adnl_id); } - self.network_context.stack.adnl.key_by_id(&local_validator.adnl_id)? + Err(e) if pubkey_matched_but_adnl_missing => { + log::warn!( + target: "validator_manager", + "set_validator_list {:x}: ADNL key {} load failed for matched \ + validator pubkey (network pending, will retry): {e}", + validator_list_id, local_validator.adnl_id, + ); + return Ok(ValidatorListOutcome::Selected { + key: local_validator_key.clone(), + matching_keys: matching_local_keys.clone(), + network_ready: false, + }); + } + Err(e) => return Err(e), } - }; + match self.network_context.stack.adnl.key_by_id(&local_validator.adnl_id) { + Ok(key) => key, + Err(e) if pubkey_matched_but_adnl_missing => { + log::warn!( + target: "validator_manager", + "set_validator_list {:x}: ADNL key {} still not loadable \ + after store (pubkey matched, network pending): {e}", + validator_list_id, local_validator.adnl_id, + ); + return Ok(ValidatorListOutcome::Selected { + key: local_validator_key.clone(), + matching_keys: matching_local_keys.clone(), + network_ready: false, + }); + } + Err(e) => return Err(e.into()), + } + } + }; let mut peers = Vec::new(); let mut lost_validators = Vec::new(); @@ -542,7 +673,6 @@ impl PrivateOverlayOperations for NodeNetwork { continue; } peers_ids.push(val.adnl_id.clone()); - lost_validators.push(val.clone()); match self.network_context.stack.dht.fetch_address(&val.adnl_id).await { Ok(Some((addr, key))) => { log::info!("addr: {:?}, key: {:x?}", &addr, &key); @@ -584,6 +714,17 @@ impl PrivateOverlayOperations for NodeNetwork { format!("vaidator set for validator list id {validator_list_id:x}"), )?; + log::info!( + target: "validator_manager", + "set_validator_list {:x}: binding confirmed โ€” election_id={} \ + validator_key={} adnl_key={} peers={}", + validator_list_id, + election_id, + hex::encode(context.validator_key.id().data()), + hex::encode(context.validator_adnl_key.id().data()), + context.validator_peers.len(), + ); + if !lost_validators.is_empty() { self.search_validator_keys_for_validator( local_validator_adnl_key.id().clone(), @@ -629,7 +770,11 @@ impl PrivateOverlayOperations for NodeNetwork { } } log::trace!("finish set_validator_list validator_list_id: {:x}", validator_list_id); - Ok(Some(context.validator_key.clone())) + Ok(ValidatorListOutcome::Selected { + key: context.validator_key.clone(), + matching_keys: matching_local_keys, + network_ready: true, + }) } fn activate_validator_list(&self, validator_list_id: UInt256) -> Result<()> { @@ -784,3 +929,7 @@ impl NodeConfigSubscriber for NodeNetwork { } } } + +#[cfg(test)] +#[path = "tests/test_node_network_validator_list.rs"] +mod tests; diff --git a/src/node/src/network/tests/test_node_network_validator_list.rs b/src/node/src/network/tests/test_node_network_validator_list.rs new file mode 100644 index 0000000..466d443 --- /dev/null +++ b/src/node/src/network/tests/test_node_network_validator_list.rs @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use super::*; +use ton_block::Ed25519KeyOption; + +fn make_test_key() -> Arc { + Ed25519KeyOption::generate().unwrap() +} + +fn make_validator_node( + public_key: Arc, + adnl_key: Arc, +) -> CatchainNode { + CatchainNode { public_key, adnl_id: adnl_key.id().clone() } +} + +#[test] +fn test_select_local_validator_candidate_matches_pubkey_and_adnl() { + let validator_key = make_test_key(); + let adnl_key = make_test_key(); + let validator = make_validator_node(validator_key.clone(), adnl_key.clone()); + let validator_key_ids = vec![validator_key.id().clone()]; + let validator_adnl_key_ids = vec![adnl_key.id().clone()]; + + let (local_validator, adnl_missing) = select_local_validator_candidate( + std::slice::from_ref(&validator), + &validator_key_ids, + &validator_adnl_key_ids, + ); + + let local_validator = local_validator.expect("local validator should be selected"); + assert_eq!(local_validator.public_key.id(), validator_key.id()); + assert_eq!(local_validator.adnl_id, adnl_key.id().clone()); + assert!(!adnl_missing); +} + +#[test] +fn test_select_local_validator_candidate_matches_pubkey_when_adnl_missing() { + let validator_key = make_test_key(); + let chain_adnl_key = make_test_key(); + let local_adnl_key = make_test_key(); + let validator = make_validator_node(validator_key.clone(), chain_adnl_key.clone()); + let validator_key_ids = vec![validator_key.id().clone()]; + let validator_adnl_key_ids = vec![local_adnl_key.id().clone()]; + + let (local_validator, adnl_missing) = select_local_validator_candidate( + std::slice::from_ref(&validator), + &validator_key_ids, + &validator_adnl_key_ids, + ); + + let local_validator = local_validator.expect("pubkey membership should select the validator"); + assert_eq!(local_validator.public_key.id(), validator_key.id()); + assert_eq!(local_validator.adnl_id, chain_adnl_key.id().clone()); + assert!(adnl_missing); +} + +#[test] +fn test_select_local_validator_candidate_returns_none_without_pubkey_match() { + let validator_key = make_test_key(); + let adnl_key = make_test_key(); + let other_validator_key = make_test_key(); + let validator = make_validator_node(validator_key, adnl_key.clone()); + let validator_key_ids = vec![other_validator_key.id().clone()]; + let validator_adnl_key_ids = vec![adnl_key.id().clone()]; + + let (local_validator, adnl_missing) = select_local_validator_candidate( + std::slice::from_ref(&validator), + &validator_key_ids, + &validator_adnl_key_ids, + ); + + assert!(local_validator.is_none()); + assert!(!adnl_missing); +} + +/// Verifies that selection follows local-key order, not validator-list order. +/// +/// This mirrors C++ `get_validator()` which iterates `temp_keys_` and returns the first +/// local key that is present in the validator set, without ADNL consideration. +#[test] +fn test_select_local_validator_candidate_uses_first_local_key_match() { + let key_a = make_test_key(); + let key_b = make_test_key(); + let adnl_a = make_test_key(); + let adnl_b = make_test_key(); + let local_adnl = make_test_key(); + + let val_a = make_validator_node(key_a.clone(), adnl_a.clone()); + let val_b = make_validator_node(key_b.clone(), adnl_b.clone()); + let validators = vec![val_a, val_b]; + + // Case 1: validator list order is [A, B], but local key order is [B, A]. + // Rust must follow local key order to match C++ temp_keys_ iteration. + let local_key_ids = vec![key_b.id().clone(), key_a.id().clone()]; + let local_adnl_ids = vec![adnl_b.id().clone(), local_adnl.id().clone()]; + + let (selected, adnl_missing) = + select_local_validator_candidate(&validators, &local_key_ids, &local_adnl_ids); + + let selected = selected.expect("should select first local-key match"); + assert_eq!(selected.public_key.id(), key_b.id(), "must follow local key order"); + assert!(!adnl_missing, "selected validator has a ready ADNL key"); + + // Case 2: ADNL readiness still must not change key selection. + let unrelated_adnl = make_test_key(); + let local_adnl_ids_none = vec![unrelated_adnl.id().clone(), adnl_a.id().clone()]; + + let (selected2, adnl_missing2) = + select_local_validator_candidate(&validators, &local_key_ids, &local_adnl_ids_none); + + let selected2 = selected2.expect("should still select first local-key match"); + assert_eq!(selected2.public_key.id(), key_b.id(), "ADNL readiness must not affect key order"); + assert!(adnl_missing2); +} diff --git a/src/node/src/tests/test_engine_operations.rs b/src/node/src/tests/test_engine_operations.rs new file mode 100644 index 0000000..e444efe --- /dev/null +++ b/src/node/src/tests/test_engine_operations.rs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use super::*; + +#[test] +fn test_destroyed_session_ids_roundtrip() { + let id_a = UInt256::from_slice(&[0x11; 32]); + let id_b = UInt256::from_slice(&[0x22; 32]); + let ids = HashSet::from([id_b.clone(), id_a.clone()]); + + let serialized = serialize_destroyed_session_ids(&ids); + let restored = deserialize_destroyed_session_ids(&serialized).unwrap(); + + assert_eq!(restored, vec![id_a, id_b], "session IDs must round-trip in sorted order"); +} + +#[test] +fn test_destroyed_session_ids_reject_invalid_length() { + let data = vec![1, 0, 0, 0, 0xaa]; + let err = deserialize_destroyed_session_ids(&data).unwrap_err(); + assert!(err.to_string().contains("invalid length"), "unexpected error: {err}"); +} diff --git a/src/node/src/tests/test_internal_db.rs b/src/node/src/tests/test_internal_db.rs index faebddb..7c83c41 100644 --- a/src/node/src/tests/test_internal_db.rs +++ b/src/node/src/tests/test_internal_db.rs @@ -551,6 +551,39 @@ async fn test_full_node_state_impl() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_validator_state_raw() { + clean_up(true, "test_validator_state_raw").await; + let r = test_validator_state_raw_impl().await; + clean_up(false, "test_validator_state_raw").await; + r.unwrap(); +} + +async fn test_validator_state_raw_impl() -> Result<()> { + let bytes = vec![1u8, 2, 3, 4, 5, 6]; + + { + let db = create_db("test_validator_state_raw", 0).await?; + + db.save_validator_state_raw("test_raw", &bytes)?; + assert_eq!(db.load_validator_state_raw("test_raw")?.unwrap(), bytes); + + db.drop_validator_state_raw("test_raw")?; + + tokio::time::sleep(Duration::from_millis(100)).await; + assert!(db.load_validator_state_raw("test_raw")?.is_none()); + stop_db(&db).await; + } + tokio::time::sleep(Duration::from_millis(100)).await; + { + let db = create_db("test_validator_state_raw", 0).await?; + assert!(db.load_validator_state_raw("test_raw")?.is_none()); + stop_db(&db).await; + } + + Ok(()) +} + const SHARD_PREFIX_LEN: u8 = 5; const THREADS: u64 = 10; const MC_BLOCKS: u32 = 500; diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index a502254..bf988c5 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -3897,7 +3897,7 @@ impl Collator { let account = shard_acc.account(); if let Some(storage_dict) = shard_acc.storage_dict() { if account.dict_hash().is_some() { - let size = account.storage_info().map_or(0, |info| info.used().cells()); + let size = account.storage_info().map(|info| info.used().cells()).unwrap_or(0); log::trace!( "{}: updated storage dict with hash {:x} for account {:x} of size {}", self.collated_block_descr, diff --git a/src/node/src/validator/consensus.rs b/src/node/src/validator/consensus.rs index 0908cc7..d0bb54b 100644 --- a/src/node/src/validator/consensus.rs +++ b/src/node/src/validator/consensus.rs @@ -59,23 +59,6 @@ pub(super) const ACCELERATED_CONSENSUS_VALIDATION_RETRY_TIMEOUT_MS: u64 = 500; pub(super) const ACCELERATED_CONSENSUS_BLOCK_CANDIDATE_SENDING_RETRY_TIMEOUT_MS: u64 = 2000; pub(super) const ACCELERATED_CONSENSUS_BLOCK_CANDIDATE_SENDING_RETRY_ATTEMPTS: u32 = 3; -// ============================================================================= -// Simplex testing constants - Override network config params during testing -// ============================================================================= -// These values are used instead of ConfigParam 30 when testing simplex consensus. -// Reference: p30.mc from testnet config -pub(super) const SIMPLEX_TARGET_RATE_MS: u64 = 500; -pub(super) const SIMPLEX_SLOTS_PER_LEADER_WINDOW: u32 = 4; -pub(super) const SIMPLEX_FIRST_BLOCK_TIMEOUT_MS: u64 = 1000; -pub(super) const SIMPLEX_MAX_LEADER_WINDOW_DESYNC: u32 = 2; - -// Additional simplex timing constants (matching accelerated consensus patterns) -pub(super) const SIMPLEX_VALIDATION_RETRY_ATTEMPTS: u32 = 8; -pub(super) const SIMPLEX_VALIDATION_RETRY_TIMEOUT_MS: u64 = 500; -pub(super) const SIMPLEX_COLLATION_RETRY_TIMEOUT_MS: u64 = 500; -pub(super) const SIMPLEX_COLLATION_RETRY_MAX_ATTEMPTS: u32 = 3; -pub(super) const SIMPLEX_STANDSTILL_TIMEOUT_MS: u64 = 10000; - // ============================================================================= // Common Types from consensus-common (preferred source) // ============================================================================= @@ -287,6 +270,10 @@ impl SessionHolder { // Implement consensus_common::Session for SessionHolder // Delegates to the common Session interface of the inner session impl consensus_common::Session for SessionHolder { + fn start(&self, initial_block_seqno: u32) { + self.inner.as_common_session().start(initial_block_seqno); + } + fn stop(&self) { self.inner.as_common_session().stop(); } @@ -431,7 +418,6 @@ impl ConsensusFactory { options: &SimplexSessionOptions, session_id: &SessionId, shard: &ShardIdent, - initial_block_seqno: u32, nodes: Vec, local_key: &PrivateKey, db_root: String, @@ -439,18 +425,15 @@ impl ConsensusFactory { overlay_manager: ConsensusOverlayManagerPtr, listener: SessionListenerPtr, ) -> consensus_common::Result { - // Disable callback thread - ValidatorSessionListener has its own let mut options = options.clone(); options.use_callback_thread = false; - // Construct full DB path let db_path = Self::make_simplex_db_path(&db_root, shard, catchain_seqno, session_id); let simplex_session = Self::create_simplex_session( &options, session_id, shard, - initial_block_seqno, nodes, local_key, db_path, @@ -458,49 +441,9 @@ impl ConsensusFactory { listener, )?; - // Wrap in SessionHolder and return as SessionHolderPtr Ok(Arc::new(SessionHolder::simplex(simplex_session))) } - /// Create simplex options with testing constants. - /// - /// Uses hardcoded testing values instead of network config params. - /// Reference values from p30.mc testnet config: - /// - target_rate_ms: 500 - /// - slots_per_leader_window: 4 - /// - first_block_timeout_ms: 1000 - /// - max_leader_window_desync: 2 - pub fn create_simplex_options( - max_block_size: usize, - max_collated_data_size: usize, - proto_version: u32, - ) -> SimplexSessionOptions { - use super::consensus::*; - - SimplexSessionOptions { - proto_version, - - // Core timing from testing constants (p30 reference) - target_rate: Duration::from_millis(SIMPLEX_TARGET_RATE_MS), - slots_per_leader_window: SIMPLEX_SLOTS_PER_LEADER_WINDOW, - first_block_timeout: Duration::from_millis(SIMPLEX_FIRST_BLOCK_TIMEOUT_MS), - - // Retry and timeout settings - validation_retry_attempts: SIMPLEX_VALIDATION_RETRY_ATTEMPTS, - validation_retry_timeout: Duration::from_millis(SIMPLEX_VALIDATION_RETRY_TIMEOUT_MS), - collation_retry_timeout: Duration::from_millis(SIMPLEX_COLLATION_RETRY_TIMEOUT_MS), - collation_retry_max_attempts: SIMPLEX_COLLATION_RETRY_MAX_ATTEMPTS, - standstill_timeout: Duration::from_millis(SIMPLEX_STANDSTILL_TIMEOUT_MS), - - // Block size limits from catchain config (ConfigParam 29) - max_block_size, - max_collated_data_size, - - // Other settings use defaults - ..Default::default() - } - } - /// Configure catchain-specific options for accelerated consensus pub fn configure_catchain_options( mut options: CatchainSessionOptions, @@ -617,7 +560,6 @@ impl ConsensusFactory { options: &SimplexSessionOptions, session_id: &SessionId, shard: &ShardIdent, - initial_block_seqno: u32, nodes: Vec, local_key: &PrivateKey, db_path: String, @@ -628,7 +570,6 @@ impl ConsensusFactory { options, session_id, shard, - initial_block_seqno, nodes, local_key, db_path, diff --git a/src/node/src/validator/tests/test_session_id.rs b/src/node/src/validator/tests/test_session_id.rs index 851c741..4e2883a 100644 --- a/src/node/src/validator/tests/test_session_id.rs +++ b/src/node/src/validator/tests/test_session_id.rs @@ -16,7 +16,7 @@ use std::{ sync::Arc, time::Duration, }; -use ton_block::{signature::SigPubKey, validators::ValidatorDescr}; +use ton_block::{signature::SigPubKey, validators::ValidatorDescr, Ed25519KeyOption}; fn parse_shard_ident(parser: &LogParser, name: &str) -> ShardIdent { ShardIdent::with_tagged_prefix( @@ -131,6 +131,7 @@ fn do_test_catchain_unsafe_rotate(s: &str) { p.general_session_info.clone(), p.val_set.list(), true, + true, Some(prev_block), &config, false, @@ -193,3 +194,391 @@ fn test_session_id_unsafe_v2() { assert_eq!(base64_encode(hash), "zGHYA323cMOmXestPYStZs5hVCDfOY2mdQm2l9zF4Bo="); } } + +#[test] +fn test_cxx_interop_session_options_hash_ignores_accelerated_fields() { + let base_opts = CatchainSessionOptions { + proto_version: 4, + round_candidates: 3, + max_round_attempts: 4, + max_block_size: 1024, + max_collated_data_size: 2048, + new_catchain_ids: true, + ..Default::default() + }; + let (base_hash, base_serialized) = get_validator_session_options_hash(base_opts.clone(), 100); + + let mut accelerated_opts = base_opts.clone(); + accelerated_opts.accelerated_consensus_enabled = true; + accelerated_opts.accelerated_consensus_collation_retry_timeout = Duration::from_millis(777); + accelerated_opts.accelerated_consensus_skip_rounds_count_for_collator_rotation = 9; + accelerated_opts.accelerated_consensus_max_precollated_blocks = 17; + + let (accelerated_hash, _) = get_validator_session_options_hash(accelerated_opts.clone(), 100); + let (interop_hash, interop_serialized) = + get_cxx_interop_session_options_hash(&accelerated_opts, 100); + + assert_eq!( + accelerated_hash, base_hash, + "validator-session config hashing must stay C++-compatible even if runtime accelerated fields differ" + ); + assert_eq!(interop_hash, base_hash, "interop hash must stay aligned with C++"); + assert_eq!(interop_serialized, base_serialized, "interop serialization must match C++ options"); +} + +fn make_test_consensus_config() -> ConsensusConfig { + ConsensusConfig { + new_catchain_ids: true, + round_candidates: 3, + next_candidate_delay_ms: 2000, + consensus_timeout_ms: 16000, + fast_attempts: 4, + attempt_duration: 8, + catchain_max_deps: 4, + max_block_bytes: 1024, + max_collated_bytes: 2048, + proto_version: 4, + catchain_max_blocks_coeff: 2500000, + } +} + +#[cfg(not(feature = "xp25"))] +#[test] +fn test_session_id_hashes_stay_shared_without_xp25() { + let consensus_config = make_test_consensus_config(); + let catchain_config = CatchainConfig::default(); + let mc_options = CatchainSessionOptions { + max_round_attempts: 5, + max_block_size: 1024, + max_collated_data_size: 2048, + new_catchain_ids: true, + proto_version: 4, + ..Default::default() + }; + let shard_options = CatchainSessionOptions { + max_round_attempts: 6, + max_block_size: 1024, + max_collated_data_size: 2048, + new_catchain_ids: true, + proto_version: 4, + ..Default::default() + }; + + let ((mc_hash, _), (shard_hash, _)) = get_session_id_hashes( + &consensus_config, + &catchain_config, + &mc_options, + &shard_options, + 100, + ); + + assert_eq!(mc_hash, shard_hash, "non-xp25 must keep one shared C++-compatible opts_hash"); +} + +#[cfg(feature = "xp25")] +#[test] +fn test_session_id_hashes_can_differ_with_xp25() { + let consensus_config = make_test_consensus_config(); + let catchain_config = CatchainConfig::default(); + let mc_options = CatchainSessionOptions { + max_round_attempts: 5, + max_block_size: 1024, + max_collated_data_size: 2048, + new_catchain_ids: true, + proto_version: 4, + ..Default::default() + }; + let shard_options = CatchainSessionOptions { + max_round_attempts: 6, + max_block_size: 1024, + max_collated_data_size: 2048, + new_catchain_ids: true, + proto_version: 4, + ..Default::default() + }; + + let ((mc_hash, _), (shard_hash, _)) = get_session_id_hashes( + &consensus_config, + &catchain_config, + &mc_options, + &shard_options, + 100, + ); + + assert_ne!(mc_hash, shard_hash, "xp25 must allow MC/shard opts_hash to diverge"); +} + +// --------------------------------------------------------------------------- +// ValidatorListStatus and validator-manager helper tests +// --------------------------------------------------------------------------- + +fn make_test_key() -> PublicKey { + Ed25519KeyOption::generate().unwrap() +} + +fn make_validator_descr_from_key(key: &PublicKey) -> ValidatorDescr { + ValidatorDescr::with_params(SigPubKey::from_bytes(key.pub_key().unwrap()).unwrap(), 1, None) +} + +#[test] +fn test_validator_list_status_get_local_key_for_list() { + let mut status = ValidatorListStatus::default(); + let key_a = make_test_key(); + let key_b = make_test_key(); + let list_curr = UInt256::from_slice(&[1u8; 32]); + let list_next = UInt256::from_slice(&[2u8; 32]); + + status.add_list(list_curr.clone(), vec![key_a.clone()], true); + status.add_list(list_next.clone(), vec![key_b.clone()], true); + status.curr = Some(list_curr.clone()); + status.next = Some(list_next.clone()); + + // get_local_keys returns only the curr list's keys + let local = status.get_local_keys().unwrap(); + assert_eq!(local[0].id(), key_a.id()); + + // get_local_keys_for_list returns the keys for the specified list + let local_curr = status.get_local_keys_for_list(&list_curr).unwrap(); + assert_eq!(local_curr[0].id(), key_a.id()); + + let local_next = status.get_local_keys_for_list(&list_next).unwrap(); + assert_eq!(local_next[0].id(), key_b.id()); + + // unknown list returns None + let unknown = UInt256::from_slice(&[3u8; 32]); + assert!(status.get_local_keys_for_list(&unknown).is_none()); +} + +#[test] +fn test_validator_list_status_get_local_key_curr_none() { + let mut status = ValidatorListStatus::default(); + let key = make_test_key(); + let list_next = UInt256::from_slice(&[2u8; 32]); + + status.add_list(list_next.clone(), vec![key.clone()], true); + status.next = Some(list_next.clone()); + // curr is None + + // get_local_keys returns None when curr is None + assert!(status.get_local_keys().is_none()); + + // but get_local_keys_for_list still finds the next list key + let found = status.get_local_keys_for_list(&list_next).unwrap(); + assert_eq!(found[0].id(), key.id()); +} + +#[test] +fn test_validator_list_status_actual_or_coming() { + let mut status = ValidatorListStatus::default(); + let key = make_test_key(); + let list_curr = UInt256::from_slice(&[1u8; 32]); + let list_next = UInt256::from_slice(&[2u8; 32]); + let list_old = UInt256::from_slice(&[3u8; 32]); + + status.add_list(list_curr.clone(), vec![key.clone()], true); + status.add_list(list_next.clone(), vec![key.clone()], true); + status.curr = Some(list_curr.clone()); + status.next = Some(list_next.clone()); + + assert!(status.actual_or_coming(&list_curr)); + assert!(status.actual_or_coming(&list_next)); + assert!(!status.actual_or_coming(&list_old)); +} + +#[test] +fn test_validator_list_status_network_readiness() { + let mut status = ValidatorListStatus::default(); + let key = make_test_key(); + let list_id = UInt256::from_slice(&[9u8; 32]); + + status.add_list(list_id.clone(), vec![key.clone()], false); + assert!(!status.is_list_network_ready(&list_id)); + assert_eq!(status.get_local_keys_for_list(&list_id).unwrap()[0].id(), key.id()); + + status.add_list(list_id.clone(), vec![key], true); + assert!(status.is_list_network_ready(&list_id)); +} + +#[test] +fn test_validator_list_status_ready_current_list_requires_network_readiness() { + let mut status = ValidatorListStatus::default(); + let key = make_test_key(); + let list_id = UInt256::from_slice(&[7u8; 32]); + + status.add_list(list_id.clone(), vec![key.clone()], false); + status.curr = Some(list_id.clone()); + assert!(status.get_ready_current_list().is_none()); + + status.add_list(list_id.clone(), vec![key], true); + assert_eq!(status.get_ready_current_list(), Some(&list_id)); +} + +#[test] +fn test_validator_list_status_ready_current_list_ignores_next_only_membership() { + let mut status = ValidatorListStatus::default(); + let key = make_test_key(); + let next_list = UInt256::from_slice(&[8u8; 32]); + + status.add_list(next_list.clone(), vec![key], true); + status.next = Some(next_list); + + assert!(status.get_ready_current_list().is_none()); +} + +#[test] +fn test_validator_list_status_next_only_ready_list_remains_usable_for_future_sessions() { + let mut status = ValidatorListStatus::default(); + let key = make_test_key(); + let next_list = UInt256::from_slice(&[10u8; 32]); + + status.add_list(next_list.clone(), vec![key], true); + status.next = Some(next_list.clone()); + + assert!(status.get_ready_current_list().is_none()); + assert!(status.is_list_network_ready(&next_list)); +} + +#[test] +fn test_find_local_validator_key_uses_local_key_order_per_subset() { + let key_a = make_test_key(); + let key_b = make_test_key(); + let validators = vec![make_validator_descr_from_key(&key_b)]; + let local_keys = vec![key_a, key_b.clone()]; + + let selected = find_local_validator_key(&validators, Some(local_keys.as_slice())) + .expect("second local key should match the subset"); + assert_eq!(selected.id(), key_b.id()); +} + +#[test] +fn test_session_id_new_catchain_ids_true_succeeds() { + let session_info = Arc::new(GeneralSessionInfo { + shard: ShardIdent::masterchain(), + opts_hash: UInt256::default(), + catchain_seqno: 1, + key_seqno: 0, + max_vertical_seqno: 0, + }); + let key = make_test_key(); + let val = make_validator_descr_from_key(&key); + + let result = get_session_id_serialize(session_info, &[val], true); + assert!(!result.is_empty()); +} + +#[test] +#[should_panic(expected = "Old catchain IDs format")] +fn test_session_id_new_catchain_ids_false_panics() { + let session_info = Arc::new(GeneralSessionInfo { + shard: ShardIdent::masterchain(), + opts_hash: UInt256::default(), + catchain_seqno: 1, + key_seqno: 0, + max_vertical_seqno: 0, + }); + let key = make_test_key(); + let val = make_validator_descr_from_key(&key); + + // This should panic with the assert message + let _ = get_session_id_serialize(session_info, &[val], false); +} + +#[test] +fn test_session_id_with_accelerated_consensus() { + let session_info = Arc::new(GeneralSessionInfo { + shard: ShardIdent::masterchain(), + opts_hash: UInt256::default(), + catchain_seqno: 1, + key_seqno: 0, + max_vertical_seqno: 0, + }); + let key = make_test_key(); + let val = make_validator_descr_from_key(&key); + + let id_without = get_session_id(session_info.clone(), &[val.clone()], true, false); + let id_with = get_session_id(session_info, &[val], true, true); + + // Accelerated consensus tag changes the session ID + assert_ne!(id_without, id_with); +} + +#[test] +fn test_find_local_validator_key_matches_subset() { + let key = make_test_key(); + let validator = make_validator_descr_from_key(&key); + let local_keys = vec![key.clone()]; + + let found = find_local_validator_key(&[validator], Some(local_keys.as_slice())) + .expect("local validator key should match the subset"); + + assert_eq!(found.id(), key.id()); +} + +#[test] +fn test_find_local_validator_key_returns_none_when_key_not_in_subset() { + let key = make_test_key(); + let other_key = make_test_key(); + let validator = make_validator_descr_from_key(&other_key); + let local_keys = vec![key]; + + assert!(find_local_validator_key(&[validator], Some(local_keys.as_slice())).is_none()); +} + +#[test] +fn test_should_skip_session_for_unsafe_rotation_matches_cpp_policy() { + let masterchain = ShardIdent::masterchain(); + let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + + assert!(!should_skip_session_for_unsafe_rotation(false, &masterchain)); + assert!(!should_skip_session_for_unsafe_rotation(false, &shard)); + assert!(!should_skip_session_for_unsafe_rotation(true, &masterchain)); + assert!(should_skip_session_for_unsafe_rotation(true, &shard)); +} + +#[test] +fn test_unsafe_rotation_block_seqno_uses_last_masterchain_block_only() { + let masterchain = ShardIdent::masterchain(); + let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + let last_masterchain_block = BlockIdExt::with_params( + ShardIdent::masterchain(), + 777, + UInt256::default(), + UInt256::default(), + ); + + assert_eq!(unsafe_rotation_block_seqno(&masterchain, &last_masterchain_block), Some(777)); + assert_eq!(unsafe_rotation_block_seqno(&shard, &last_masterchain_block), None); +} + +#[test] +fn test_get_session_unsafe_id_skips_patch_when_flag_false() { + let session_info = Arc::new(GeneralSessionInfo { + shard: ShardIdent::masterchain(), + opts_hash: UInt256::default(), + catchain_seqno: 42, + key_seqno: 0, + max_vertical_seqno: 0, + }); + let key = make_test_key(); + let val = make_validator_descr_from_key(&key); + + let mut config = ValidatorManagerConfig::default(); + config.unsafe_catchain_rotates.insert(42, (1, 99)); + + let plain_id = get_session_id(session_info.clone(), &[val.clone()], true, false); + + let id_with_flag_false = get_session_unsafe_id( + session_info.clone(), + &[val.clone()], + true, + false, + Some(100), + &config, + false, + ); + assert_eq!(id_with_flag_false, plain_id, "flag=false must return the plain session ID"); + + let id_with_flag_true = + get_session_unsafe_id(session_info, &[val], true, true, Some(100), &config, false); + assert_ne!(id_with_flag_true, plain_id, "flag=true must apply the unsafe rotation patch"); +} diff --git a/src/node/src/validator/validator_group.rs b/src/node/src/validator/validator_group.rs index f31bb4d..2d97c6e 100644 --- a/src/node/src/validator/validator_group.rs +++ b/src/node/src/validator/validator_group.rs @@ -68,6 +68,23 @@ fn is_simplex_roundless(round: u32) -> bool { round == SIMPLEX_ROUNDLESS } +/// Snapshot of session state for monitoring dumps. +/// Captured atomically from the inner mutex to avoid multiple async calls. +pub struct SessionSnapshot { + pub session_id: UInt256, + pub shard: ShardIdent, + pub cc_seqno: u32, + pub status: ValidatorGroupStatus, + pub consensus_type: ConsensusType, + pub round: u32, + pub mc_initial_seqno: u32, + pub has_engine: bool, + pub is_collator: bool, + pub created_at: SystemTime, + pub key_seqno: u32, + pub last_accepted_mc_seqno: Option, +} + /// When true, non-accelerated consensus (Catchain / Simplex) will block the /// validator-group message loop while a validation task runs. /// Set to false (default) to let those tasks run in the background, keeping @@ -116,6 +133,7 @@ fn should_reject_stale_mc_candidate( #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum ValidatorGroupStatus { Created, + EngineCreated, Countdown { start_at: tokio::time::Instant }, Sync, Active, @@ -127,6 +145,7 @@ impl Display for ValidatorGroupStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ValidatorGroupStatus::Created => write!(f, "created"), + ValidatorGroupStatus::EngineCreated => write!(f, "engine_created"), ValidatorGroupStatus::Countdown { start_at: at } => { let now = tokio::time::Instant::now(); write!(f, "cntdwn {}", at.saturating_duration_since(now).as_secs()) @@ -139,6 +158,20 @@ impl Display for ValidatorGroupStatus { } } +impl ValidatorGroupStatus { + pub fn metric_label(&self) -> &'static str { + match self { + Self::Created => "created", + Self::EngineCreated => "engine_created", + Self::Countdown { .. } => "countdown", + Self::Sync => "sync", + Self::Active => "active", + Self::Stopping => "stopping", + Self::Stopped => "stopped", + } + } +} + impl ValidatorGroupStatus { pub fn before(&self, of: &ValidatorGroupStatus) -> bool { match (&self, of) { @@ -244,6 +277,7 @@ pub struct ValidatorGroupImpl { replay_finished: bool, status: ValidatorGroupStatus, + start_pending: bool, /// Highest MC block seqno accepted (committed) in this session. /// Used for MC fork prevention: reject candidates building on stale heads. @@ -252,14 +286,48 @@ pub struct ValidatorGroupImpl { impl Drop for ValidatorGroupImpl { fn drop(&mut self) { - // Important: does not stop the session -- to avoid database deletion, - // which otherwise would happen each time the validator-manager crashes. - log::info!(target: "validator", "ValidatorGroupImpl: dropping session {}", self.info()); + // Does not stop the session to avoid database deletion on validator-manager crash. + log::info!(target: "validator", + "SESSION_LIFECYCLE: dropped shard={} cc_seqno={} session_id={:x} final_status={} \ + has_engine={}", + self.shard, self.cc_seqno, self.session_id, self.status, self.session.is_some()); } } impl ValidatorGroupImpl { - // Creates and starts session + /// Create the consensus engine (session) without starting the validation queue. + /// + /// Two-phase activation (C++ parity): `create_engine()` materializes the consensus + /// session so future groups have a pre-initialized engine. `start()` then spawns + /// the validation queue processor and calls `Session::start(initial_block_seqno)` + /// to begin consensus. If `create_engine()` was not called (e.g. the group was + /// promoted directly), `start()` creates the session inline as a fallback. + fn create_engine( + &mut self, + g: Arc, + session_listener: SessionListenerPtr, + ) -> Result<()> { + if self.session.is_some() { + log::debug!(target: "validator", + "create_engine: session already exists for shard={} cc_seqno={}, skipping", + self.shard, self.cc_seqno); + return Ok(()); + } + if self.status >= ValidatorGroupStatus::Stopping { + fail!("Inactive session cannot have engine created! {}", self.info()) + } + + log::info!(target: "validator", + "SESSION_LIFECYCLE: create_engine shard={} cc_seqno={} session_id={:x} \ + consensus={} status={} -> engine_created", + self.shard, self.cc_seqno, self.session_id, + self.consensus_type, self.status); + let session = self.create_consensus_session(g, session_listener)?; + self.session = Some(session); + self.status = ValidatorGroupStatus::EngineCreated; + Ok(()) + } + #[allow(clippy::too_many_arguments)] fn start( &mut self, @@ -274,21 +342,35 @@ impl ValidatorGroupImpl { fail!("Inactive session cannot be started! {}", self.info()) } - self.status = ValidatorGroupStatus::Sync; - - log::info!(target: "validator", "Starting session {}", self.info()); - self.prev_block_ids.update_prev(prev); self.min_masterchain_block_id = Some(min_masterchain_block_id.clone()); self.min_ts = min_ts; if self.shard.is_masterchain() { - // Seed stale-head guard baseline at session start so fork prevention - // is active before the first local on_block_committed callback. self.last_accepted_mc_seqno = Some(min_masterchain_block_id.seq_no); } - // Create session using unified factory - let session = self.create_consensus_session(g.clone(), session_listener)?; + if self.session.is_none() { + log::info!(target: "validator", + "SESSION_LIFECYCLE: create_session_at_start shard={} cc_seqno={} consensus={}", + self.shard, self.cc_seqno, self.consensus_type); + let session = self.create_consensus_session(g.clone(), session_listener)?; + self.session = Some(session); + } + + let initial_block_seqno = self.prev_block_ids.get_next_seqno().unwrap_or(1); + if let Some(session) = &self.session { + log::info!(target: "validator", + "SESSION_LIFECYCLE: session.start(seqno={}) shard={} cc_seqno={}", + initial_block_seqno, self.shard, self.cc_seqno); + session.start(initial_block_seqno); + } + + log::info!(target: "validator", + "SESSION_LIFECYCLE: start shard={} cc_seqno={} session_id={:x} consensus={} \ + mc_init_seqno={} status={} -> sync", + self.shard, self.cc_seqno, self.session_id, self.consensus_type, + self.min_masterchain_block_id.as_ref().map_or(0, |id| id.seq_no), + self.status); let g_clone = g.clone(); let receiver = g.receiver.lock().unwrap().take().ok_or_else(|| { @@ -298,14 +380,39 @@ impl ValidatorGroupImpl { process_validation_queue(receiver, g_clone.clone(), rt).await; }); - log::trace!(target: "validator", "Started session {}, options {:?}, ref.cnt = {}", - self.info(), g.consensus_options, Arc::strong_count(&session) - ); + log::debug!(target: "validator", + "Validation queue spawned for shard={} cc_seqno={}, options={:?}", + self.shard, self.cc_seqno, g.consensus_options); - self.session = Some(session); + self.status = ValidatorGroupStatus::Sync; Ok(()) } + fn prepare_start(&mut self, validation_start_status: ValidatorGroupStatus) -> bool { + if self.start_pending { + return false; + } + match self.status { + ValidatorGroupStatus::Created | ValidatorGroupStatus::EngineCreated => {} + _ => return false, + } + if let ValidatorGroupStatus::Countdown { .. } = validation_start_status { + self.status = validation_start_status; + } + self.start_pending = true; + true + } + + fn reset_after_start_failure(&mut self) { + log::warn!(target: "validator", + "SESSION_LIFECYCLE: reset_after_failure shard={} cc_seqno={} session_id={:x} \ + status={} -> created (session dropped, will retry)", + self.shard, self.cc_seqno, self.session_id, self.status); + self.session = None; + self.start_pending = false; + self.status = ValidatorGroupStatus::Created; + } + /// Get the consensus type for this validator group #[allow(dead_code)] pub fn get_consensus_type(&self) -> ConsensusType { @@ -369,14 +476,10 @@ impl ValidatorGroupImpl { ) } ConsensusOptions::Simplex(simplex_options) => { - //TODO: check initial seqno for simplex - let initial_block_seqno = self.prev_block_ids.get_next_seqno().unwrap_or(1); - ConsensusFactory::create_simplex_based_session( simplex_options, &g.session_id, &g.shard, - initial_block_seqno, nodes, &g.local_key, db_root, @@ -419,8 +522,9 @@ impl ValidatorGroupImpl { is_accelerated_consensus_enabled: bool, consensus_type: ConsensusType, ) -> ValidatorGroupImpl { - log::info!(target: "validator", "Initializing session {:x}, shard {}, consensus_type {}", - session_id, shard, consensus_type); + log::info!(target: "validator", + "SESSION_LIFECYCLE: created shard={} cc_seqno={} session_id={:x} consensus={} local_id={}", + shard, cc_seqno, session_id, consensus_type, local_id); let prev_block_ids = PrevBlockHistory::with_shard(&shard); ValidatorGroupImpl { @@ -430,6 +534,7 @@ impl ValidatorGroupImpl { cc_seqno, min_ts: SystemTime::now(), status: ValidatorGroupStatus::Created, + start_pending: false, expected_current_round: 0, expected_collation_round: 0, is_collator: false, @@ -547,10 +652,8 @@ pub struct ValidatorGroup { is_accelerated_consensus_enabled: bool, session_id: SessionId, shard: ShardIdent, - //catchain_seqno: u32, validator_list_id: ValidatorListHash, - //shard: ShardIdent, engine: Arc, validator_set: ValidatorSet, #[allow(dead_code)] @@ -563,6 +666,8 @@ pub struct ValidatorGroup { last_validation_time: Arc, last_collation_time: Arc, is_collating: Arc, + /// Set by the validation queue on prolonged inactivity, cleared on any action. + pub stalled: Arc, } impl ValidatorGroup { @@ -612,6 +717,7 @@ impl ValidatorGroup { last_validation_time: Arc::new(AtomicU64::new(0)), last_collation_time: Arc::new(AtomicU64::new(0)), is_collating: Arc::new(AtomicBool::new(false)), + stalled: Arc::new(AtomicBool::new(false)), } } @@ -626,10 +732,34 @@ impl ValidatorGroup { &self.general_session_info.shard } + pub fn cc_seqno(&self) -> u32 { + self.general_session_info.catchain_seqno + } + pub fn is_simplex(&self) -> bool { matches!(self.consensus_options, ConsensusOptions::Simplex(_)) } + pub async fn snapshot(&self) -> SessionSnapshot { + let key_seqno = self.general_session_info.key_seqno; + self.group_impl + .execute_sync(|g| SessionSnapshot { + session_id: g.session_id.clone(), + shard: g.shard.clone(), + cc_seqno: g.cc_seqno, + status: g.status, + consensus_type: g.consensus_type, + round: g.expected_current_round, + mc_initial_seqno: g.min_masterchain_block_id.as_ref().map_or(0, |id| id.seq_no), + has_engine: g.session.is_some(), + is_collator: g.is_collator, + created_at: g.min_ts, + key_seqno, + last_accepted_mc_seqno: g.last_accepted_mc_seqno, + }) + .await + } + /// Notify this session about masterchain finalization. /// /// For simplex shard sessions, this updates the MC finalization tracking which is @@ -656,6 +786,10 @@ impl ValidatorGroup { .await; } + pub fn is_collating(&self) -> bool { + self.is_collating.load(Ordering::Relaxed) + } + pub fn last_validation_time(&self) -> u64 { self.last_validation_time.load(Ordering::Relaxed) } @@ -668,6 +802,23 @@ impl ValidatorGroup { Arc::downgrade(&self.callback) } + /// Pre-create the consensus engine without starting the validation queue. + /// Called for future-validator groups to warm up the session ahead of time. + pub async fn pre_create_engine(self: Arc) -> Result<()> { + let callback = self.make_validator_session_callback(); + self.group_impl + .execute_sync(|group_impl| { + if let Err(e) = group_impl.create_engine(self.clone(), callback) { + log::error!(target: "validator", + "SESSION_LIFECYCLE: pre_create_engine failed shard={} cc_seqno={} \ + session_id={:x}: {}", + group_impl.shard, group_impl.cc_seqno, group_impl.session_id, e); + } + }) + .await; + Ok(()) + } + #[allow(clippy::too_many_arguments)] pub async fn start_with_status( self: Arc, @@ -677,33 +828,39 @@ impl ValidatorGroup { min_ts: SystemTime, rt: tokio::runtime::Handle, ) -> Result<()> { - self.set_status(validation_start_status).await?; - rt.clone().spawn (async move { + rt.clone().spawn(async move { if let ValidatorGroupStatus::Countdown { start_at } = validation_start_status { log::trace!(target: "validator", "Session delay started: {}", self.info().await); tokio::time::sleep_until(start_at).await; } let callback = self.make_validator_session_callback(); - self.group_impl.execute_sync(|group_impl| - { - if group_impl.status <= ValidatorGroupStatus::Active { - if let Err(e) = group_impl.start( - callback, - prev, - min_masterchain_block_id, - min_ts, - self.clone(), - rt - ) - { - log::error!(target: "validator", "Cannot start group: {}", e); + self.group_impl + .execute_sync(|group_impl| { + if group_impl.status <= ValidatorGroupStatus::Active { + if let Err(e) = group_impl.start( + callback, + prev, + min_masterchain_block_id, + min_ts, + self.clone(), + rt, + ) { + group_impl.reset_after_start_failure(); + log::error!( + target: "validator", + "Cannot start group: {}; resetting session to Created for retry", + e + ); + } else { + group_impl.start_pending = false; + } + } else { + group_impl.start_pending = false; + log::trace!(target: "validator", "Session deleted before countdown: {}", group_impl.info()); } - } - else { - log::trace!(target: "validator", "Session deleted before countdown: {}", group_impl.info()); - } - }).await; + }) + .await; }); Ok(()) } @@ -714,39 +871,40 @@ impl ValidatorGroup { destroy_database: bool, ) -> Result<()> { self.set_status(ValidatorGroupStatus::Stopping).await?; - log::info!(target: "validator", "Stopping group: {} (destroy database {})", self.info().await, destroy_database); + let consensus_label = if self.is_simplex() { "simplex" } else { "catchain" }; + metrics::counter!("ton_node_validator_session_stopped_total", "consensus" => consensus_label) + .increment(1); + let shard = self.shard.clone(); + let cc_seqno = self.cc_seqno(); + log::info!(target: "validator", + "SESSION_LIFECYCLE: stop_initiated shard={} cc_seqno={} destroy_db={}", + shard, cc_seqno, destroy_database); let group_impl = self.group_impl.clone(); let self_clone = self.clone(); rt.spawn({ async move { - log::debug!(target: "validator", "Stopping group (spawn): {}", self_clone.info().await); - let session_ptr = group_impl.execute_sync( - |group_impl| group_impl.session.clone()).await; + let session_ptr = + group_impl.execute_sync(|group_impl| group_impl.session.clone()).await; if let Some(s_ptr) = session_ptr { - log::debug!(target: "validator", "Stopping catchain: {}", self_clone.info().await); + log::debug!(target: "validator", + "Stopping consensus engine for shard={} cc_seqno={}", shard, cc_seqno); if destroy_database { - s_ptr.destroy(); // Blocking, destroys catchain DB + s_ptr.destroy(); } else { - s_ptr.stop(); // Blocking, preserves catchain DB + s_ptr.stop(); } } - log::debug!(target: "validator", "Group stopped: {}", self_clone.info().await); let _ = self_clone.set_status(ValidatorGroupStatus::Stopped).await; - log::info!(target: "validator", "Status set: {}", self_clone.info().await); if destroy_database { let _ = self_clone.destroy_db().await; - log::debug!(target: "validator", "Db destroyed: {}", self_clone.info().await); - } - else { - log::debug!( - target: "validator", - "Db destroy skipped (destroy_databse option set to false): {}", - self_clone.info().await - ); + log::debug!(target: "validator", + "DB destroyed for shard={} cc_seqno={}", shard, cc_seqno); } + log::info!(target: "validator", + "SESSION_LIFECYCLE: stopped shard={} cc_seqno={} destroy_db={}", + shard, cc_seqno, destroy_database); } }); - log::debug!(target: "validator", "Stopping group {}, stop spawned", self.info().await); Ok(()) } @@ -768,13 +926,35 @@ impl ValidatorGroup { self.group_impl.execute_sync(|group_impl| group_impl.status).await } + pub async fn is_start_pending(&self) -> bool { + self.group_impl.execute_sync(|group_impl| group_impl.start_pending).await + } + + pub async fn try_prepare_start( + &self, + validation_start_status: ValidatorGroupStatus, + ) -> Result { + self.group_impl + .execute_sync(|group_impl| Ok(group_impl.prepare_start(validation_start_status))) + .await + } + pub async fn set_status(&self, status: ValidatorGroupStatus) -> Result<()> { self.group_impl .execute_sync(|group_impl| { if group_impl.status.before(&status) { + let from = group_impl.status; group_impl.status = status; + log::info!(target: "validator", + "SESSION_LIFECYCLE: transition shard={} cc_seqno={} session_id={:x} {} -> {}", + group_impl.shard, group_impl.cc_seqno, group_impl.session_id, from, status); Ok(()) } else { + log::error!(target: "validator", + "SESSION_LIFECYCLE: invalid transition shard={} cc_seqno={} session_id={:x} \ + {} -> {} (monotonic violation)", + group_impl.shard, group_impl.cc_seqno, group_impl.session_id, + group_impl.status, status); fail!("Status cannot retreat, from {} to {}", group_impl.status, status) } }) @@ -1560,6 +1740,30 @@ impl ValidatorGroup { if !group_impl.prev_block_ids.same_prevs(&prev_block_history) { Err(error!("Sync error: two requests at a time, prevs have changed!!!")) } else { + // Block verified and accepted: transition Sync -> Active. + // Deferred to this point (after ensure_next_block_new + + // run_accept_block_query) so stale/duplicate commits don't + // produce false-positive activation. + if group_impl.status == ValidatorGroupStatus::Sync { + group_impl.status = ValidatorGroupStatus::Active; + let cl = if group_impl.consensus_type == ConsensusType::Simplex { + "simplex" + } else { + "catchain" + }; + metrics::counter!( + "ton_node_validator_session_activated_total", + "consensus" => cl + ) + .increment(1); + log::info!(target: "validator", + "SESSION_LIFECYCLE: transition shard={} cc_seqno={} \ + session_id={:x} sync -> active \ + (first committed block accepted)", + group_impl.shard, + group_impl.cc_seqno, + group_impl.session_id); + } Ok(()) } } @@ -1714,6 +1918,18 @@ impl Drop for ValidatorGroup { #[cfg(test)] mod tests { use super::*; + use ton_block::KeyId; + + fn make_group_impl_for_start_tests() -> ValidatorGroupImpl { + ValidatorGroupImpl::new( + &KeyId::from_data([0u8; 32]), + ShardIdent::masterchain(), + 1, + UInt256::default(), + false, + ConsensusType::Catchain, + ) + } #[test] fn test_mc_fork_prevention_none_allows() { @@ -1738,4 +1954,240 @@ mod tests { assert!(should_reject_stale_mc_candidate(Some(10), 0)); assert!(should_reject_stale_mc_candidate(Some(100), 50)); } + + #[test] + fn test_prepare_start_immediate_keeps_created_and_marks_pending() { + let mut group = make_group_impl_for_start_tests(); + + assert!(group.prepare_start(ValidatorGroupStatus::Sync)); + assert!(group.status == ValidatorGroupStatus::Created); + assert!(group.start_pending); + } + + #[test] + fn test_prepare_start_countdown_marks_pending_and_countdown() { + let mut group = make_group_impl_for_start_tests(); + let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; + + assert!(group.prepare_start(countdown)); + assert!(matches!(group.status, ValidatorGroupStatus::Countdown { .. })); + assert!(group.start_pending); + } + + #[test] + fn test_prepare_start_rejects_duplicate_pending_start() { + let mut group = make_group_impl_for_start_tests(); + + assert!(group.prepare_start(ValidatorGroupStatus::Sync)); + assert!(!group.prepare_start(ValidatorGroupStatus::Sync)); + assert!(group.status == ValidatorGroupStatus::Created); + assert!(group.start_pending); + } + + #[test] + fn test_reset_after_start_failure_restores_retryable_state() { + let mut group = make_group_impl_for_start_tests(); + let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; + + assert!(group.prepare_start(countdown)); + group.reset_after_start_failure(); + + assert!(group.status == ValidatorGroupStatus::Created); + assert!(!group.start_pending); + assert!(group.session.is_none()); + } + + // --- Status ordering / transition table tests (WS6) --- + + #[test] + fn test_status_ordering_is_monotonic() { + let states = [ + ValidatorGroupStatus::Created, + ValidatorGroupStatus::EngineCreated, + ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }, + ValidatorGroupStatus::Sync, + ValidatorGroupStatus::Active, + ValidatorGroupStatus::Stopping, + ValidatorGroupStatus::Stopped, + ]; + for i in 0..states.len() { + for j in i + 1..states.len() { + assert!(states[i] < states[j], "{} must be < {}", states[i], states[j]); + } + } + } + + #[test] + fn test_before_allows_forward_transitions() { + let created = ValidatorGroupStatus::Created; + let engine_created = ValidatorGroupStatus::EngineCreated; + let sync = ValidatorGroupStatus::Sync; + let active = ValidatorGroupStatus::Active; + let stopping = ValidatorGroupStatus::Stopping; + + assert!(created.before(&engine_created)); + assert!(engine_created.before(&sync)); + assert!(sync.before(&active)); + assert!(active.before(&stopping)); + } + + #[test] + fn test_before_rejects_backward_transitions() { + let sync = ValidatorGroupStatus::Sync; + let active = ValidatorGroupStatus::Active; + let created = ValidatorGroupStatus::Created; + + assert!(!active.before(&sync)); + assert!(!sync.before(&created)); + } + + #[test] + fn test_before_rejects_countdown_to_countdown() { + let c1 = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; + let c2 = ValidatorGroupStatus::Countdown { + start_at: tokio::time::Instant::now() + Duration::from_secs(10), + }; + assert!(!c1.before(&c2)); + assert!(!c2.before(&c1)); + } + + #[test] + fn test_engine_created_state_between_created_and_countdown() { + let created = ValidatorGroupStatus::Created; + let engine_created = ValidatorGroupStatus::EngineCreated; + let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; + + assert!(created < engine_created); + assert!(engine_created < countdown); + assert!(created.before(&engine_created)); + assert!(engine_created.before(&countdown)); + } + + #[test] + fn test_prepare_start_accepts_engine_created_state() { + let mut group = make_group_impl_for_start_tests(); + group.status = ValidatorGroupStatus::EngineCreated; + + let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; + assert!(group.prepare_start(countdown)); + assert!(matches!(group.status, ValidatorGroupStatus::Countdown { .. })); + assert!(group.start_pending); + } + + #[test] + fn test_prepare_start_rejects_sync_and_later_states() { + for status in [ + ValidatorGroupStatus::Sync, + ValidatorGroupStatus::Active, + ValidatorGroupStatus::Stopping, + ValidatorGroupStatus::Stopped, + ] { + let mut group = make_group_impl_for_start_tests(); + group.status = status; + assert!( + !group.prepare_start(ValidatorGroupStatus::Sync), + "prepare_start should reject status {}", + status + ); + } + } + + // --- Stale-future culling predicate tests (mirrors manager.cpp equal+related) --- + + /// Reproduces the stale-future culling predicate from validator_manager.rs + /// to verify correctness in isolation with various shard topologies. + fn should_cull_future( + active_shard: &ShardIdent, + active_cc: u32, + future_shard: &ShardIdent, + future_cc: u32, + ) -> bool { + let shards_equal = active_shard == future_shard; + let shards_related = active_shard.is_ancestor_for(future_shard) + || future_shard.is_ancestor_for(active_shard); + let equal_condition = shards_equal && active_cc >= future_cc; + let related_condition = shards_related && active_cc > future_cc; + equal_condition || related_condition + } + + #[test] + fn test_cull_same_shard_equal_seqno() { + let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + assert!(should_cull_future(&shard, 5, &shard, 5)); + } + + #[test] + fn test_cull_same_shard_higher_active_seqno() { + let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + assert!(should_cull_future(&shard, 6, &shard, 5)); + } + + #[test] + fn test_no_cull_same_shard_lower_active_seqno() { + let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + assert!(!should_cull_future(&shard, 4, &shard, 5)); + } + + #[test] + fn test_cull_ancestor_shard_higher_seqno() { + let parent = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + let child = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); + assert!(parent.is_ancestor_for(&child)); + assert!(should_cull_future(&parent, 6, &child, 5)); + } + + #[test] + fn test_cull_descendant_shard_higher_seqno() { + let parent = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + let child = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); + assert!(should_cull_future(&child, 6, &parent, 5)); + } + + #[test] + fn test_no_cull_related_shard_equal_seqno() { + let parent = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + let child = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); + // For related (non-equal) shards, the condition is strict > + assert!(!should_cull_future(&parent, 5, &child, 5)); + } + + #[test] + fn test_no_cull_unrelated_shards() { + let shard_a = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); + let shard_b = ShardIdent::with_tagged_prefix(0, 0xC000_0000_0000_0000).unwrap(); + assert!(!shard_a.is_ancestor_for(&shard_b)); + assert!(!shard_b.is_ancestor_for(&shard_a)); + assert!(!should_cull_future(&shard_a, 100, &shard_b, 1)); + } + + #[test] + fn test_no_cull_different_workchain() { + let shard_wc0 = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); + let shard_wc1 = ShardIdent::with_tagged_prefix(1, 0x8000_0000_0000_0000).unwrap(); + assert!(!should_cull_future(&shard_wc0, 10, &shard_wc1, 5)); + } + + #[test] + fn test_metric_label_covers_all_states() { + let states = vec![ + (ValidatorGroupStatus::Created, "created"), + (ValidatorGroupStatus::EngineCreated, "engine_created"), + ( + ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }, + "countdown", + ), + (ValidatorGroupStatus::Sync, "sync"), + (ValidatorGroupStatus::Active, "active"), + (ValidatorGroupStatus::Stopping, "stopping"), + (ValidatorGroupStatus::Stopped, "stopped"), + ]; + for (status, expected_label) in states { + assert_eq!( + status.metric_label(), + expected_label, + "metric_label mismatch for {}", + status + ); + } + } } diff --git a/src/node/src/validator/validator_manager.rs b/src/node/src/validator/validator_manager.rs index 49deb54..5bc5354 100644 --- a/src/node/src/validator/validator_manager.rs +++ b/src/node/src/validator/validator_manager.rs @@ -15,11 +15,11 @@ use super::consensus::{ use crate::{ config::ValidatorManagerConfig, engine::Engine, - engine_traits::EngineOperations, + engine_traits::{EngineOperations, ValidatorListOutcome}, shard_state::ShardStateStuff, validator::{ out_msg_queue::OutMsgQueueInfoStuff, - validator_group::{ValidatorGroup, ValidatorGroupStatus}, + validator_group::{SessionSnapshot, ValidatorGroup, ValidatorGroupStatus}, validator_utils::{ compute_validator_list_id, get_group_members_by_validator_descrs, get_masterchain_seqno, try_calc_subset_for_workchain, @@ -35,18 +35,15 @@ use std::{ convert::TryFrom, fs, ops::RangeInclusive, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, + sync::{atomic::Ordering, Arc}, time::{Duration, SystemTime}, }; use tokio::time::timeout; use ton_api::IntoBoxed; use ton_block::{ - error, fail, AcceleratedConsensusConfig, BlockIdExt, CatchainConfig, ConfigParamEnum, - ConsensusConfig, FutureSplitMerge, McStateExtra, Result, ShardDescr, ShardIdent, SimplexConfig, - UInt256, UnixTime, ValidatorDescr, ValidatorSet, + base64_encode, error, fail, AcceleratedConsensusConfig, BlockIdExt, CatchainConfig, + ConfigParamEnum, ConsensusConfig, FutureSplitMerge, McStateExtra, Result, ShardDescr, + ShardIdent, SimplexConfig, UInt256, UnixTime, ValidatorDescr, ValidatorSet, }; #[cfg(feature = "xp25")] @@ -54,19 +51,55 @@ const MC_ACCELERATED_CONSENSUS_ENABLED: bool = true; #[cfg(not(feature = "xp25"))] const MC_ACCELERATED_CONSENSUS_ENABLED: bool = false; -// When true, use hardcoded testing constants for simplex instead of ConfigParam 30. -// Set to false for production: reads ConfigParam 30 from masterchain state. -const SIMPLEX_USE_TESTING_CONSTANTS: bool = false; +fn format_shard_short(shard: &ShardIdent) -> String { + if shard.is_masterchain() { + "MC".to_string() + } else { + format!("{}:{:04X}..", shard.workchain_id(), shard.shard_prefix_with_tag() >> 48) + } +} + +fn format_time_ago(now_unix: u64, ts: u64) -> String { + if ts == 0 { + "-".to_string() + } else if now_unix >= ts { + format_duration_short(Duration::from_secs(now_unix - ts)) + } else { + "0s".to_string() + } +} + +fn format_duration_short(d: Duration) -> String { + let secs = d.as_secs(); + if secs < 60 { + format!("{}s", secs) + } else if secs < 3600 { + format!("{}m{}s", secs / 60, secs % 60) + } else { + format!("{}h{}m", secs / 3600, (secs % 3600) / 60) + } +} -// Magic tag for accelerated consensus session ID differentiation +/// Magic suffix appended to session-ID serialization when accelerated consensus is enabled. +/// +/// **Rust-specific extension**: the C++ reference (`get_validator_set_id()` in `manager.cpp`) +/// does not include this tag because C++ does not yet support accelerated consensus in the +/// validator manager (`bridge.cpp` has only a TODO). When accelerated consensus is +/// disabled (the default for C++ interop), session IDs are byte-identical to C++. const ACCELERATED_CONSENSUS_MAGIC_TAG: u32 = 0xACCE1E8A; #[derive(Clone)] pub struct SessionsOptions { pub mc_options: CatchainSessionOptions, - pub mc_hash: UInt256, pub shard_options: CatchainSessionOptions, - pub shard_hash: UInt256, + /// Session-options hash used inside `validator.groupNew` for masterchain sessions. + /// + /// Without `xp25`, both hashes intentionally collapse to one C++-compatible + /// `ValidatorSessionOptions` hash. With `xp25`, masterchain and shard hashes may + /// diverge if their runtime session options differ in hash-relevant fields. + pub mc_session_id_hash: UInt256, + /// Session-options hash used inside `validator.groupNew` for shard sessions. + pub shard_session_id_hash: UInt256, } impl SessionsOptions { @@ -77,8 +110,21 @@ impl SessionsOptions { &self.shard_options } } + + pub fn get_session_id_hash(&self, shard_id: &ShardIdent) -> &UInt256 { + if shard_id.is_masterchain() { + &self.mc_session_id_hash + } else { + &self.shard_session_id_hash + } + } } +/// Serialize the `validator.groupNew` TL object for session-ID hashing. +/// +/// Mirrors `get_validator_set_id()` in C++ (`manager.cpp`) for the +/// `new_catchain_ids == true` branch. Old catchain ID formats are intentionally +/// unsupported (see assertion). fn get_session_id_serialize( session_info: Arc, vals: &[ValidatorDescr], @@ -87,9 +133,11 @@ fn get_session_id_serialize( let mut members = Vec::new(); get_group_members_by_validator_descrs(vals, &mut members); - if !new_catchain_ids { - unimplemented!("Old catchain ids format is not supported") - } else { + assert!( + new_catchain_ids, + "Old catchain IDs format (new_catchain_ids=false) is not supported by the Rust implementation" + ); + { serialize_tl_boxed_object!(&ton_api::ton::validator::group::GroupNew { workchain: session_info.shard.workchain_id(), shard: session_info.shard.shard_prefix_with_tag() as i64, @@ -103,7 +151,11 @@ fn get_session_id_serialize( } } -/// serialize data and calc sha256 +/// Compute session ID by hashing the serialized `validator.groupNew` TL object. +/// +/// When `accelerated_consensus_enabled` is true, appends [`ACCELERATED_CONSENSUS_MAGIC_TAG`] +/// before hashing to differentiate accelerated sessions from standard ones. Without the +/// tag, the resulting hash is byte-identical to C++ `get_validator_set_id()`. fn get_session_id( session_info: Arc, val_set: &[ValidatorDescr], @@ -124,13 +176,57 @@ fn compute_session_unsafe_serialized(session_id: &UInt256, rotate_id: u32) -> Ve unsafe_id_serialized } +/// C++ parity: during unsafe rotation (`force_recover`), skip all non-masterchain shards. +/// +/// Mirrors the `force_recover` early-continue in C++ `update_shards()`: +/// ```cpp +/// if (force_recover && !desc.first.is_masterchain()) { continue; } +/// ``` +fn should_skip_session_for_unsafe_rotation( + do_unsafe_catchain_rotate: bool, + shard: &ShardIdent, +) -> bool { + do_unsafe_catchain_rotate && !shard.is_masterchain() +} + +fn unsafe_rotation_block_seqno( + shard: &ShardIdent, + last_masterchain_block: &BlockIdExt, +) -> Option { + shard.is_masterchain().then_some(last_masterchain_block.seq_no) +} + +/// Check whether any local key belongs to the given validator subset. +/// +/// Mirrors the inner loop of C++ `get_validator()` (`manager.cpp`) which +/// iterates `temp_keys_` and calls `val_set->is_validator(key)`. Returns the first local +/// key, in local-key order, that matches any validator descriptor's short ID. +fn find_local_validator_key( + validators: &[ValidatorDescr], + local_keys: Option<&[PublicKey]>, +) -> Option { + for local_key in local_keys? { + let local_keyhash = local_key.id().data(); + for val in validators { + let pkhash = val.compute_node_id_short(); + if pkhash.as_slice() == local_keyhash { + return Some(local_key.clone()); + } + } + } + None +} + /// Computes session_id and if unsafe rotation is taking place, /// replaces session_id with unsafe rotation session id. +/// The `do_unsafe_catchain_rotate` flag mirrors C++ `force_recover`: +/// the per-shard rotation check only runs when the global gate is set. fn get_session_unsafe_id( session_info: Arc, val_set: &[ValidatorDescr], new_catchain_ids: bool, - prev_block_opt: Option, + do_unsafe_catchain_rotate: bool, + rotation_block_seqno_opt: Option, vm_config: &ValidatorManagerConfig, accelerated_consensus_enabled: bool, ) -> UInt256 { @@ -141,18 +237,18 @@ fn get_session_unsafe_id( accelerated_consensus_enabled, ); - if session_info.shard.is_masterchain() { - if let Some(rotate_id) = - vm_config.check_unsafe_catchain_rotation(prev_block_opt, session_info.catchain_seqno) + if do_unsafe_catchain_rotate && session_info.shard.is_masterchain() { + if let Some(rotate_id) = vm_config + .check_unsafe_catchain_rotation(rotation_block_seqno_opt, session_info.catchain_seqno) { let unsafe_serialized = compute_session_unsafe_serialized(&session_id, rotate_id); let unsafe_id = UInt256::calc_file_hash(unsafe_serialized.as_slice()); log::warn!( - target: "validator", + target: "validator_manager", "Unsafe master session rotation: session {} at block={:?}, cc={} -> rotate_id={}, new session {}", session_id.to_hex_string(), - prev_block_opt, + rotation_block_seqno_opt, session_info.catchain_seqno, rotate_id, unsafe_id.to_hex_string() @@ -256,6 +352,70 @@ fn get_validator_session_options_hash( (UInt256::calc_file_hash(&serialized), serialized) } +#[cfg_attr(feature = "xp25", allow(dead_code))] +fn get_cxx_interop_session_options_hash( + opts: &CatchainSessionOptions, + last_masterchain_block_seqno: u32, +) -> (UInt256, RawBuffer) { + let mut interop_opts = opts.clone(); + let defaults = CatchainSessionOptions::default(); + + interop_opts.accelerated_consensus_enabled = false; + interop_opts.accelerated_consensus_collation_retry_timeout = + defaults.accelerated_consensus_collation_retry_timeout; + interop_opts.accelerated_consensus_skip_rounds_count_for_collator_rotation = + defaults.accelerated_consensus_skip_rounds_count_for_collator_rotation; + interop_opts.accelerated_consensus_max_precollated_blocks = + defaults.accelerated_consensus_max_precollated_blocks; + + get_validator_session_options_hash(interop_opts, last_masterchain_block_seqno) +} + +/// Build the session-options view used by C++ `ValidatorSessionOptions opts{config}`. +/// +/// This is intentionally derived only from config 29 / catchain config and never from +/// Rust's runtime accelerated-consensus toggles. It is used only in non-`xp25` builds +/// where Rust must preserve the C++ single-`opts_hash` behavior. +#[cfg_attr(feature = "xp25", allow(dead_code))] +fn get_cxx_interop_session_options( + opts: &ConsensusConfig, + catchain_config: &CatchainConfig, +) -> CatchainSessionOptions { + let no_accelerated_consensus_config = None; + get_session_options(opts, catchain_config, false, &no_accelerated_consensus_config) +} + +#[cfg(feature = "xp25")] +fn get_session_id_hashes( + _consensus_config: &ConsensusConfig, + _catchain_config: &CatchainConfig, + mc_options: &CatchainSessionOptions, + shard_options: &CatchainSessionOptions, + last_masterchain_block_seqno: u32, +) -> ((UInt256, RawBuffer), (UInt256, RawBuffer)) { + ( + get_validator_session_options_hash(mc_options.clone(), last_masterchain_block_seqno), + get_validator_session_options_hash(shard_options.clone(), last_masterchain_block_seqno), + ) +} + +#[cfg(not(feature = "xp25"))] +fn get_session_id_hashes( + consensus_config: &ConsensusConfig, + catchain_config: &CatchainConfig, + _mc_options: &CatchainSessionOptions, + _shard_options: &CatchainSessionOptions, + last_masterchain_block_seqno: u32, +) -> ((UInt256, RawBuffer), (UInt256, RawBuffer)) { + let session_id_options = get_cxx_interop_session_options(consensus_config, catchain_config); + let (session_id_hash, session_id_options_serialized) = + get_cxx_interop_session_options_hash(&session_id_options, last_masterchain_block_seqno); + ( + (session_id_hash.clone(), session_id_options_serialized.clone()), + (session_id_hash, session_id_options_serialized), + ) +} + fn get_session_options( opts: &ConsensusConfig, catchain_config: &CatchainConfig, @@ -380,9 +540,27 @@ impl ValidationStatus { } } +/// Local node's participation record for a single validator list. +/// +/// Pairs the node's local validator keys with a `network_ready` flag indicating whether the +/// ADNL/overlay infrastructure was successfully set up for this list. Keys are stored in the +/// same local-key order that C++ uses for `temp_keys_`, so per-shard selection can still pick +/// the first matching local key within a subset. +struct LocalValidatorListEntry { + keys: Vec, + network_ready: bool, +} + +/// Tracks which validator lists the local node belongs to and their readiness state. +/// +/// Maintains the current and next validator list IDs (mirroring the masterchain state's +/// current and next validator sets) along with the local node's keys for each. +/// +/// C++ does not have a direct equivalent structure; it relies on `temp_keys_` for +/// membership checks and `allow_validate_` for the enable/disable gate. #[derive(Default)] struct ValidatorListStatus { - known_lists: HashMap, + known_lists: HashMap, curr: Option, next: Option, curr_utime_since: Option, @@ -390,8 +568,8 @@ struct ValidatorListStatus { } impl ValidatorListStatus { - fn add_list(&mut self, list_id: ValidatorListHash, key: PublicKey) { - self.known_lists.insert(list_id, key); + fn add_list(&mut self, list_id: ValidatorListHash, keys: Vec, network_ready: bool) { + self.known_lists.insert(list_id, LocalValidatorListEntry { keys, network_ready }); } fn contains_list(&self, list_id: &ValidatorListHash) -> bool { @@ -402,18 +580,24 @@ impl ValidatorListStatus { self.known_lists.remove(list_id); } - fn get_list(&self, list_id: &ValidatorListHash) -> Option { - return match self.known_lists.get(list_id) { - None => None, - Some(ch) => Some(ch.clone()), - }; + fn get_list(&self, list_id: &ValidatorListHash) -> Option<&LocalValidatorListEntry> { + self.known_lists.get(list_id) } - fn get_local_key(&self) -> Option { - match &self.curr { - None => None, - Some(ch) => self.get_list(ch), - } + fn get_local_keys_for_list(&self, list_id: &ValidatorListHash) -> Option<&[PublicKey]> { + self.get_list(list_id).map(|entry| entry.keys.as_slice()) + } + + fn get_local_keys(&self) -> Option<&[PublicKey]> { + self.curr.as_ref().and_then(|current_list| self.get_local_keys_for_list(current_list)) + } + + fn get_ready_current_list(&self) -> Option<&ValidatorListHash> { + self.curr.as_ref().filter(|current_list| self.is_list_network_ready(current_list)) + } + + fn is_list_network_ready(&self, list_id: &ValidatorListHash) -> bool { + self.known_lists.get(list_id).map(|entry| entry.network_ready).unwrap_or(false) } fn actual_or_coming(&self, list_id: &ValidatorListHash) -> bool { @@ -437,12 +621,30 @@ fn rotate_all_shards(mc_state_extra: &McStateExtra) -> bool { mc_state_extra.validator_info.nx_cc_updated } +/// Core validator manager state. +/// +/// Mirrors `ValidatorManagerImpl` in C++ (`manager.cpp`). Tracks active and future +/// validator sessions, the local node's membership in validator lists, and a blacklist +/// of destroyed session IDs to prevent recreation during the same masterchain cycle. struct ValidatorManagerImpl { engine: Arc, rt: tokio::runtime::Handle, - validator_sessions: HashMap>, // Sessions: both actual (started) and future + /// Sessions for the current validator set (started or starting). + current_sessions: HashMap>, + /// Sessions for the next (future) validator set with pre-created engines. + future_sessions: HashMap>, validator_list_status: ValidatorListStatus, config: ValidatorManagerConfig, + /// Session IDs that have been destroyed and must not be recreated until the next + /// full shard rotation. Mirrors C++ `destroyed_validator_sessions_` in `manager.cpp`. + /// + /// Persisted in the validator-state DB and cleared when `rotate_all_shards()` returns + /// true, matching the C++ lifecycle around init-block updates. + destroyed_sessions: HashSet, + /// Set to `true` once `check_sync()` succeeds, enabling `Waiting -> Countdown`. + sync_complete: bool, + /// Wall-clock timestamp of the last full metrics dump. + last_metrics_dump: tokio::time::Instant, } impl ValidatorManagerImpl { @@ -455,29 +657,74 @@ impl ValidatorManagerImpl { ValidatorManagerImpl { engine: engine.clone(), rt: rt.clone(), - validator_sessions: HashMap::default(), + current_sessions: HashMap::default(), + future_sessions: HashMap::default(), validator_list_status: ValidatorListStatus::default(), config, + destroyed_sessions: HashSet::new(), + sync_complete: false, + last_metrics_dump: tokio::time::Instant::now(), } } - /// find own key in validator subset - fn find_us(&self, validators: &[ValidatorDescr]) -> Option { - if let Some(lk) = self.validator_list_status.get_local_key() { - let local_keyhash = lk.id().data(); - for val in validators { - let pkhash = val.compute_node_id_short(); - if pkhash.as_slice() == local_keyhash { - //log::info!(target: "validator_manager", "Comparing {} with {}", pkhash, local_keyhash); - //log::info!(target: "validator_manager", "({:?})", pk.pub_key().unwrap()); - //compute public key hash - return Some(lk); - } - } + fn load_destroyed_sessions(&mut self) -> Result<()> { + let persisted = self.engine.load_destroyed_session_ids()?; + self.destroyed_sessions = persisted.into_iter().collect(); + if !self.destroyed_sessions.is_empty() { + log::info!( + target: "validator_manager", + "Loaded {} destroyed session IDs from persistent storage", + self.destroyed_sessions.len() + ); + } + Ok(()) + } + + fn persist_destroyed_sessions(&self) -> Result<()> { + self.engine.save_destroyed_session_ids(&self.destroyed_sessions) + } + + fn clear_destroyed_sessions(&mut self) -> Result<()> { + if !self.destroyed_sessions.is_empty() { + log::debug!( + target: "validator_manager", + "Clearing {} destroyed session IDs", + self.destroyed_sessions.len() + ); + self.destroyed_sessions.clear(); } - None + self.engine.clear_destroyed_session_ids() + } + + /// Find the first matching local key for a subset using a specific validator list. + /// + /// Used for future-session creation where the validator list may differ from `curr`. + fn find_us_for_list( + &self, + validators: &[ValidatorDescr], + list_id: &ValidatorListHash, + ) -> Option { + find_local_validator_key( + validators, + self.validator_list_status.get_local_keys_for_list(list_id), + ) } + /// Find the first matching local key for a subset using the current validator list. + /// + /// Used for active-session creation in `start_sessions`. + fn find_us(&self, validators: &[ValidatorDescr]) -> Option { + find_local_validator_key(validators, self.validator_list_status.get_local_keys()) + } + + /// Register the local node in a validator list and return its hash if matched. + /// + /// Calls [`EngineOperations::set_validator_list`] which checks local keys against the + /// validator set and sets up ADNL/overlay infrastructure. The result is cached in + /// [`ValidatorListStatus`]: even when `network_ready` is `false`, the list hash is + /// returned so that the caller can track membership without ADNL being fully operational. + /// + /// Returns `Ok(None)` only when the local node is genuinely not in the validator set. async fn update_single_validator_list( &mut self, validator_list: &[ValidatorDescr], @@ -485,9 +732,13 @@ impl ValidatorManagerImpl { ) -> Result> { let list_id = match compute_validator_list_id(validator_list, None)? { None => return Ok(None), - Some(l) if self.validator_list_status.contains_list(&l) => return Ok(Some(l)), Some(l) => l, }; + if self.validator_list_status.contains_list(&list_id) + && self.validator_list_status.is_list_network_ready(&list_id) + { + return Ok(Some(list_id)); + } let nodes_res: Vec = validator_list .iter() @@ -496,7 +747,7 @@ impl ValidatorManagerImpl { log::info!(target: "validator_manager", "Updating {} validator list (id {:x}):", name, list_id); for x in &nodes_res { - log::debug!(target: "validator_manager", "pk: {}, pk_id: {}, andl_id: {}", + log::debug!(target: "validator_manager", "pk: {}, pk_id: {}, adnl_id: {}", hex::encode(x.public_key.pub_key().unwrap()), hex::encode(x.public_key.id().data()), hex::encode(x.adnl_id.data()) @@ -504,21 +755,44 @@ impl ValidatorManagerImpl { } match self.engine.set_validator_list(list_id.clone(), &nodes_res).await? { - Some(key) => { - self.validator_list_status.add_list(list_id.clone(), key.clone()); - log::info!(target: "validator_manager", "Local node: pk_id: {} id: {}", - hex::encode(key.pub_key().unwrap()), - hex::encode(key.id().data()) + ValidatorListOutcome::Selected { key, matching_keys, network_ready } => { + self.validator_list_status.add_list( + list_id.clone(), + matching_keys.clone(), + network_ready, ); + if network_ready { + log::info!(target: "validator_manager", "Local node: pk_id: {} id: {}", + hex::encode(key.pub_key().unwrap()), + hex::encode(key.id().data()) + ); + } else { + log::warn!( + target: "validator_manager", + "Local node is a {} validator by pubkey (id {:x}, key {}), but ADNL/network \ + context is not ready yet; will retry and keep validator membership", + name, + list_id, + hex::encode(key.id().data()) + ); + } Ok(Some(list_id)) } - None => { + ValidatorListOutcome::NotValidator => { log::info!(target: "validator_manager", "Local node is not a {} validator", name); Ok(None) } } } + /// Refresh the current and next validator lists from the masterchain state. + /// + /// Returns `true` if the local node belongs to at least one validator set (current or + /// next), `false` if it is not a validator at all. The caller uses `false` to disable + /// validation entirely. + /// + /// Mirrors the implicit list-management in C++ `update_shards()` where `get_validator()` + /// is called per-shard. In Rust, we pre-resolve membership once per update round. async fn update_validator_lists(&mut self, mc_state: &ShardStateStuff) -> Result { let (validator_set, next_validator_set) = match mc_state.state()?.read_custom()? { None => return Ok(false), @@ -529,35 +803,30 @@ impl ValidatorManagerImpl { self.update_single_validator_list(validator_set.list(), "current").await?; self.validator_list_status.curr_utime_since = Some(validator_set.utime_since()); if let Some(id) = self.validator_list_status.curr.as_ref() { - self.engine.activate_validator_list(id.clone())?; + if self.validator_list_status.is_list_network_ready(id) { + self.engine.activate_validator_list(id.clone())?; + } else { + log::warn!( + target: "validator_manager", + "Current validator list {:x} is matched by pubkey but network context \ + is not ready; keeping previous active validator list until ready", + id + ); + } } self.validator_list_status.next = self.update_single_validator_list(next_validator_set.list(), "next").await?; self.validator_list_status.next_utime_since = Some(next_validator_set.utime_since()); - metrics::gauge!("ton_node_validator_in_current_set").set(if self - .validator_list_status - .curr - .is_some() - { - 1 - } else { - 0 - } as f64); - metrics::gauge!("ton_node_validator_in_next_set").set(if self - .validator_list_status - .next - .is_some() - { - 1 - } else { - 0 - } as f64); + metrics::gauge!("ton_node_validator_in_current_set") + .set(self.validator_list_status.curr.is_some() as u8 as f64); + metrics::gauge!("ton_node_validator_in_next_set") + .set(self.validator_list_status.next.is_some() as u8 as f64); Ok(self.validator_list_status.curr.is_some() || self.validator_list_status.next.is_some()) } async fn is_active_shard(&self, shard: &ShardIdent) -> bool { - for group in self.validator_sessions.values() { + for group in self.current_sessions.values().chain(self.future_sessions.values()) { if group.shard() == shard { match group.get_status().await { ValidatorGroupStatus::Sync @@ -574,7 +843,7 @@ impl ValidatorManagerImpl { log::trace!(target: "validator_manager", "Garbage collect lists"); let mut lists_gc = self.validator_list_status.known_hashes(); - for id in self.validator_sessions.values() { + for id in self.current_sessions.values().chain(self.future_sessions.values()) { lists_gc.remove(&id.get_validator_list_id()); } @@ -614,7 +883,7 @@ impl ValidatorManagerImpl { fn notify_shard_sessions_mc_finalized(&self, mc_block_seqno: u32) { consensus_common::check_execution_time!(5000); // 5ms max - for (session_id, group) in self.validator_sessions.iter() { + for (session_id, group) in self.current_sessions.iter() { if group.shard().is_masterchain() || !group.is_simplex() { continue; } @@ -638,39 +907,98 @@ impl ValidatorManagerImpl { } } + /// Stop sessions that are no longer active or pending, and record their IDs in the + /// destroyed-session blacklist to prevent recreation within the same masterchain cycle. + /// Mirrors C++ group destruction logic in `update_shards()` (`manager.cpp`). async fn stop_and_remove_sessions( &mut self, sessions_to_remove: &HashSet, destroy_database: bool, ) { + if !sessions_to_remove.is_empty() { + log::debug!(target: "validator_manager", + "stop_and_remove_sessions: removing {} sessions, destroy_db={}", + sessions_to_remove.len(), destroy_database); + } for id in sessions_to_remove.iter() { - log::trace!(target: "validator_manager", "stop&remove: removing {:x}", id); - match self.validator_sessions.get(id) { + self.destroyed_sessions.insert(id.clone()); + + let future_group = self.future_sessions.remove(id); + + match self.current_sessions.get(id) { None => { - log::error!(target: "validator_manager", - "Session stopping error: {:x} already removed from hash", id - ) + if let Some(fg) = future_group { + // C++ parity: tentative groups are destroyed via + // IValidatorGroup::destroy in manager.cpp. + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: gc_future shard={} cc_seqno={} session_id={:x} \ + destroy_db={} (no longer needed)", + fg.shard(), fg.cc_seqno(), id, destroy_database); + let cl = if fg.is_simplex() { "simplex" } else { "catchain" }; + metrics::counter!( + "ton_node_validator_session_destroyed_total", + "consensus" => cl + ) + .increment(1); + if let Err(e) = fg.stop(self.rt.clone(), destroy_database).await { + log::error!(target: "validator_manager", + "SESSION_LIFECYCLE: gc_future_stop_failed session_id={:x}: {}", + id, e); + } + } else { + log::trace!(target: "validator_manager", + "Session {:x} not in current or future maps", id); + } } - Some(session) => match session.get_status().await { - ValidatorGroupStatus::Stopping => {} - ValidatorGroupStatus::Stopped => { - if let Some(group) = self.validator_sessions.remove(id) { - if !self.is_active_shard(group.shard()).await { - self.engine.remove_last_validation_time(group.shard()); - self.engine.remove_last_collation_time(group.shard()); + Some(session) => { + let status = session.get_status().await; + let shard = session.shard().clone(); + match status { + ValidatorGroupStatus::Stopping => { + log::debug!(target: "validator_manager", + "SESSION_LIFECYCLE: already stopping shard={} session_id={:x}", + shard, id); + } + ValidatorGroupStatus::Stopped => { + if let Some(group) = self.current_sessions.remove(id) { + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: gc_stopped shard={} session_id={:x}", + shard, id); + let cl = if group.is_simplex() { "simplex" } else { "catchain" }; + metrics::counter!( + "ton_node_validator_session_destroyed_total", + "consensus" => cl + ) + .increment(1); + if !self.is_active_shard(group.shard()).await { + self.engine.remove_last_validation_time(group.shard()); + self.engine.remove_last_collation_time(group.shard()); + } } } - } - _ => { - if let Err(e) = - session.clone().stop(self.rt.clone(), destroy_database).await - { - log::error!(target: "validator_manager", - "Could not stop session {:x}: `{}`", id, e); - self.validator_sessions.remove(id); + _ => { + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: gc_stop shard={shard} session_id={id:x} \ + status={status} destroy_db={}", + destroy_database + ); + let cl = if session.is_simplex() { "simplex" } else { "catchain" }; + metrics::counter!( + "ton_node_validator_session_destroyed_total", + "consensus" => cl + ) + .increment(1); + if let Err(e) = + session.clone().stop(self.rt.clone(), destroy_database).await + { + log::error!(target: "validator_manager", + "SESSION_LIFECYCLE: gc_stop_failed shard={} session_id={:x}: {}", + shard, id, e); + self.current_sessions.remove(id); + } } } - }, + } } } } @@ -685,6 +1013,8 @@ impl ValidatorManagerImpl { _ => fail!("no CatchainConfig in config_params"), }; let accelerated_consensus_config = self.get_accelerated_consensus_config(mc_state_extra); + let last_key_block_seqno = + mc_state_extra.last_key_block.as_ref().map(|x| x.seq_no).unwrap_or(0); // Compute session options for masterchain (accelerated consensus controlled by constant) let mc_accelerated_consensus_enabled = MC_ACCELERATED_CONSENSUS_ENABLED @@ -695,10 +1025,6 @@ impl ValidatorManagerImpl { mc_accelerated_consensus_enabled, &accelerated_consensus_config, ); - let (mc_hash, mc_session_options_serialized) = get_validator_session_options_hash( - mc_options.clone(), - mc_state_extra.last_key_block.as_ref().map(|x| x.seq_no).unwrap_or(0), - ); // Compute session options for shards (accelerated consensus may be enabled) let shard_accelerated_consensus_enabled = @@ -709,27 +1035,39 @@ impl ValidatorManagerImpl { shard_accelerated_consensus_enabled, &accelerated_consensus_config, ); - let (shard_hash, shard_session_options_serialized) = get_validator_session_options_hash( - shard_options.clone(), - mc_state_extra.last_key_block.as_ref().map(|x| x.seq_no).unwrap_or(0), + let ( + (mc_session_id_hash, mc_session_id_options_serialized), + (shard_session_id_hash, shard_session_id_options_serialized), + ) = get_session_id_hashes( + &consensus_config, + catchain_config, + &mc_options, + &shard_options, + last_key_block_seqno, ); - log::trace!(target: "validator_manager", "MC SessionOptions from config.29: {:?}", mc_options); log::trace!( target: "validator_manager", - "MC SessionOptions from config.29 serialized: {} hash: {:x}", - hex::encode(mc_session_options_serialized), - mc_hash + "MC SessionOptions from config.29: {mc_options:?}" + ); + log::trace!( + target: "validator_manager", + "MC Session-ID SessionOptions serialized: {} hash: {:x}", + hex::encode(mc_session_id_options_serialized), + mc_session_id_hash + ); + log::trace!( + target: "validator_manager", + "Shard Session-ID SessionOptions serialized: {} hash: {:x}", + hex::encode(shard_session_id_options_serialized), + shard_session_id_hash ); - log::trace!(target: "validator_manager", "Shard SessionOptions from config.29: {:?}", shard_options); log::trace!( target: "validator_manager", - "Shard SessionOptions from config.29 serialized: {} hash: {:x}", - hex::encode(shard_session_options_serialized), - shard_hash + "Shard SessionOptions from config.29: {shard_options:?}" ); - Ok(SessionsOptions { mc_options, mc_hash, shard_options, shard_hash }) + Ok(SessionsOptions { mc_options, shard_options, mc_session_id_hash, shard_session_id_hash }) } fn get_accelerated_consensus_config( @@ -773,54 +1111,31 @@ impl ValidatorManagerImpl { /// Select consensus options based on ConfigParam 30 (NewConsensusConfigAll). /// - /// If simplex feature is enabled and ConfigParam 30 contains a SimplexConfig for - /// the given shard (mc or shard), returns `ConsensusOptions::Simplex`. - /// Otherwise, returns `ConsensusOptions::Catchain` with the provided catchain options. + /// Returns `ConsensusOptions::Simplex` when ConfigParam 30 contains a valid + /// SimplexConfig (v1 or v2) for the given shard. Otherwise, returns + /// `ConsensusOptions::Catchain` โ€” the node must remain fully catchain-compatible + /// as long as the on-chain config can switch between the two at any time. /// - /// This follows the C++ pattern in `ValidatorManagerImpl::create_validator_group`: - /// - Get `new_consensus_config` from masterchain state - /// - If present, create bridge (simplex/null consensus) - /// - If absent, create catchain + /// C++ reference: `validator/manager.cpp` โ€” `ValidatorManagerImpl::create_validator_group` + /// - calls `last_masterchain_state_->get_new_consensus_config(shard.workchain)` + /// - if present โ†’ `IValidatorGroup::create_bridge` (simplex) + /// - if absent โ†’ `IValidatorGroup::create_catchain` + /// + /// Noncritical params override (not yet implemented): + /// C++ ref: `validator/validator-options.hpp` โ€” + /// `ValidatorManagerOptionsImpl::get_noncritical_params` fn select_consensus_options( &self, shard: &ShardIdent, mc_state: &ShardStateStuff, catchain_options: &CatchainSessionOptions, + _cc_seqno: u32, ) -> ConsensusOptions { - use super::consensus::{ConsensusFactory, SimplexSessionOptions}; - - // During testing period, use hardcoded constants instead of ConfigParam 30 - if SIMPLEX_USE_TESTING_CONSTANTS { - let mut options = ConsensusFactory::create_simplex_options( - catchain_options.max_block_size as usize, - catchain_options.max_collated_data_size as usize, - catchain_options.proto_version as u32, - ); - options.proto_version = catchain_options.proto_version; - static LAST_WARN: AtomicU64 = AtomicU64::new(0); - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - let last = LAST_WARN.load(Ordering::Relaxed); - if now >= last + 30 - && LAST_WARN - .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - log::warn!( - target: "validator_manager", - "Simplex TESTING MODE for {}: target_rate={}ms, slots_per_window={}, first_block_timeout={}ms", - shard, - options.target_rate.as_millis(), - options.slots_per_leader_window, - options.first_block_timeout.as_millis() - ); - } - return ConsensusOptions::Simplex(options); - } + use super::consensus::SimplexSessionOptions; - // Try to get ConfigParam 30 from masterchain state + // C++ ref: mc-config.cpp โ€” Config::get_new_consensus_config reads ConfigParam 30 + // directly without checking global_version. Absence of the param + // (or a parse error) falls through to the catchain path below. let config_params = match mc_state.config_params() { Ok(cfg) => cfg, Err(e) => { @@ -833,18 +1148,9 @@ impl ValidatorManagerImpl { } }; - // C++ compatibility: simplex requires global_version >= 13 - let global_ver = config_params.global_version(); - if global_ver < 13 { - log::trace!( - target: "validator_manager", - "global_version={global_ver} < 13 for {shard}, using catchain" - ); - return ConsensusOptions::Catchain(catchain_options.clone()); - } - - // Get simplex config for mc or shard based on workchain. - // Consensus type is selected by global_version >= 13 AND ConfigParam 30 presence. + // C++ ref: get_new_consensus_config(wc) selects mc or shard inner config, + // then tries simplex_config#21 / simplex_config_v2#22. + // Absence โ†’ catchain fallback (the node must stay catchain-compatible). let simplex_cfg: Option = if shard.is_masterchain() { config_params.get_mc_simplex_config().ok().flatten() } else { @@ -852,27 +1158,63 @@ impl ValidatorManagerImpl { }; if let Some(cfg) = simplex_cfg { - log::info!( + log::trace!( target: "validator_manager", "Simplex config found for {}: target_rate={}ms, slots_per_window={}, first_block_timeout={}ms", shard, - cfg.target_rate_ms, + cfg.noncritical_params.target_rate_ms, cfg.slots_per_leader_window, - cfg.first_block_timeout_ms + cfg.noncritical_params.first_block_timeout_ms ); - return ConsensusOptions::Simplex(SimplexSessionOptions { + + // C++ ref: mc-config.cpp maps noncritical params to + // NewConsensusConfig::NoncriticalParams fields via ENUMERATE_NONCRITICAL_PARAMS. + // Doubles are stored as f32 bits in the u32 values. + // + // TODO: C++ also applies per-shard/cc_seqno overrides here via + // get_noncritical_params() in validator-options.hpp. + let np = &cfg.noncritical_params; + let opts = SimplexSessionOptions { proto_version: catchain_options.proto_version as u32, slots_per_leader_window: cfg.slots_per_leader_window, - first_block_timeout: Duration::from_millis(cfg.first_block_timeout_ms as u64), - target_rate: Duration::from_millis(cfg.target_rate_ms as u64), - // max_block_size and max_collated_data_size come from ConfigParam 29 (via catchain_options) + target_rate: Duration::from_millis(np.target_rate_ms as u64), + first_block_timeout: Duration::from_millis(np.first_block_timeout_ms as u64), + first_block_timeout_multiplier: f32::from_bits( + np.first_block_timeout_multiplier_bits, + ) as f64, + first_block_timeout_cap: Duration::from_millis( + np.first_block_timeout_cap_ms as u64, + ), + candidate_resolve_timeout: Duration::from_millis( + np.candidate_resolve_timeout_ms as u64, + ), + candidate_resolve_timeout_multiplier: f32::from_bits( + np.candidate_resolve_timeout_multiplier_bits, + ) as f64, + candidate_resolve_timeout_cap: Duration::from_millis( + np.candidate_resolve_timeout_cap_ms as u64, + ), + candidate_resolve_cooldown: Duration::from_millis( + np.candidate_resolve_cooldown_ms as u64, + ), + standstill_timeout: Duration::from_millis(np.standstill_timeout_ms as u64), + standstill_max_egress_bytes_per_s: np.standstill_max_egress_bytes_per_s, + max_leader_window_desync: np.max_leader_window_desync, + bad_signature_ban_duration: Duration::from_millis( + np.bad_signature_ban_duration_ms as u64, + ), + candidate_resolve_rate_limit: np.candidate_resolve_rate_limit, max_block_size: catchain_options.max_block_size as usize, max_collated_data_size: catchain_options.max_collated_data_size as usize, use_quic: cfg.use_quic, ..Default::default() - }); + }; + return ConsensusOptions::Simplex(opts); } + // No simplex config โ†’ catchain fallback. + // This is the expected path when testnet has empty ConfigParam 30 or when + // ConfigParam 30 contains null_consensus_config#20. log::trace!( target: "validator_manager", "No simplex config for {}, using catchain", @@ -886,36 +1228,93 @@ impl ValidatorManagerImpl { mc_state: &ShardStateStuff, mc_state_extra: &McStateExtra, ) -> Result<()> { - match self.engine.validation_status() { + let prev_status = self.engine.validation_status(); + match prev_status { ValidationStatus::Waiting => { let last_masterchain_block = mc_state.block_id(); - if last_masterchain_block.seq_no == 0 || rotate_all_shards(mc_state_extra) { - let later_than_hardfork = self.engine.get_last_fork_masterchain_seqno() - <= last_masterchain_block.seq_no; - - if self.engine.check_sync().await? && later_than_hardfork { - if last_masterchain_block.seq_no == 0 - && self.config.no_countdown_for_zerostate - { - self.engine.set_validation_status(ValidationStatus::Active); - } else { - self.engine.set_validation_status(ValidationStatus::Countdown); + let later_than_hardfork = + self.engine.get_last_fork_masterchain_seqno() <= last_masterchain_block.seq_no; + + let synced = self.engine.check_sync().await?; + log::trace!(target: "validator_manager", + "update_validation_status: Waiting check: synced={} later_than_hardfork={} \ + mc_seqno={}", + synced, later_than_hardfork, last_masterchain_block.seq_no); + + // Phase 1: mark sync complete and backfill future engines. + // C++ parity: sync_complete() sets started_=true and sweeps both + // validator_groups_ and next_validator_groups_ calling + // create_session() on all groups (manager.cpp sync_complete). + if synced && later_than_hardfork && !self.sync_complete { + self.sync_complete = true; + if !self.future_sessions.is_empty() { + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: backfill_pre_create_engine for {} future sessions \ + after sync_complete", + self.future_sessions.len()); + for (id, group) in &self.future_sessions { + let g = group.clone(); + let session_id = id.clone(); + tokio::spawn(async move { + if let Err(e) = g.pre_create_engine().await { + log::error!(target: "validator_manager", + "SESSION_LIFECYCLE: backfill_pre_create_failed \ + session_id={:x}: {}", + session_id, e); + } + }); } } } + + // Phase 2: transition Waiting -> Countdown/Active. + // C++ parity: allow_validate_ is set purely from + // rotated_all_shards() || seqno==0 plus the fork check + // (manager.cpp update_shards). sync_complete (started_) + // only gates create_session() on existing groups (Phase 1 + // above), NOT the allow_validate_ / status transition. + let rotated = + rotate_all_shards(mc_state_extra) || last_masterchain_block.seq_no == 0; + if rotated && later_than_hardfork { + if last_masterchain_block.seq_no == 0 && self.config.no_countdown_for_zerostate + { + log::info!(target: "validator_manager", + "VALIDATION_STATUS: Waiting -> Active (zerostate, no countdown)"); + self.engine.set_validation_status(ValidationStatus::Active); + } else { + log::info!(target: "validator_manager", + "VALIDATION_STATUS: Waiting -> Countdown \ + (rotated_all_shards={}, mc_seqno={}, sync_complete={})", + rotate_all_shards(mc_state_extra), + last_masterchain_block.seq_no, + self.sync_complete); + self.engine.set_validation_status(ValidationStatus::Countdown); + } + } else if !rotated { + log::trace!(target: "validator_manager", + "update_validation_status: rotated_all_shards=false, \ + deferring Waiting -> Countdown (mc_seqno={})", + last_masterchain_block.seq_no); + } } ValidationStatus::Countdown => { - for (_, group) in self.validator_sessions.iter() { + // C++ parity: transition only after a session has genuinely accepted + // a committed block (ValidatorGroupStatus::Active), not merely + // entered Sync. Combined with the deferred Sync->Active in + // on_block_committed(), this prevents false-positive activation + // from stale or duplicate commit events. + for (_, group) in self.current_sessions.iter() { let status = group.get_status().await; - if status == ValidatorGroupStatus::Sync - || status == ValidatorGroupStatus::Active - { + if status == ValidatorGroupStatus::Active { let path_str: String = self.engine.db_root_dir()?.to_owned() + "/catchains"; tokio::spawn(async move { if let Err(err) = clear_catchains_cache(path_str).await { log::warn!("Error clearing catchains cache: {}", err); } }); + log::info!(target: "validator_manager", + "VALIDATION_STATUS: Countdown -> Active (session shard={} reached {})", + group.shard(), status); self.engine.set_validation_status(ValidationStatus::Active); break; } @@ -927,17 +1326,31 @@ impl ValidatorManagerImpl { } async fn disable_validation(&mut self, clear_rotation: bool) -> Result<()> { + let prev_status = self.engine.validation_status(); + let n_current = self.current_sessions.len(); + let n_future = self.future_sessions.len(); + log::info!(target: "validator_manager", + "VALIDATION_STATUS: {prev_status:?} -> Disabled (clear_rotation={clear_rotation}, \ + current_sessions={n_current}, future_sessions={n_future})" + ); + self.engine.set_validation_status(ValidationStatus::Disabled); + self.sync_complete = false; let existing_validator_sessions: HashSet = - self.validator_sessions.keys().cloned().collect(); + self.current_sessions.keys().chain(self.future_sessions.keys()).cloned().collect(); self.stop_and_remove_sessions(&existing_validator_sessions, clear_rotation).await; self.garbage_collect().await; self.engine.set_will_validate(false); if clear_rotation { + self.clear_destroyed_sessions()?; self.engine.clear_last_rotation_block_id()?; + } else { + self.persist_destroyed_sessions()?; } - log::info!(target: "validator_manager", "All sessions were removed, validation disabled"); + log::info!(target: "validator_manager", + "VALIDATION_STATUS: Disabled complete (stopped {} current + {} future sessions)", + n_current, n_future); Ok(()) } @@ -949,11 +1362,28 @@ impl ValidatorManagerImpl { fn enable_validation(&mut self) { self.engine.set_will_validate(true); - let validation_status = max(self.engine.validation_status(), ValidationStatus::Waiting); - self.engine.set_validation_status(validation_status); - log::debug!(target: "validator_manager", "Validation enabled: status {:?}", validation_status); + let current = self.engine.validation_status(); + // C++ parity: enable_validation() only ensures we are at least + // Waiting. The Waiting -> Countdown promotion is handled by + // update_validation_status() based on rotated_all_shards(), + // matching C++'s allow_validate_ which is independent of sync. + let target = max(current, ValidationStatus::Waiting); + if target != current { + log::info!(target: "validator_manager", + "VALIDATION_STATUS: {:?} -> {:?}", + current, target); + } + self.engine.set_validation_status(target); } + /// Create and start validator sessions for all currently active shards. + /// + /// Mirrors the `new_shards` loop in C++ `update_shards()` (`manager.cpp`): + /// - Skips non-masterchain shards during unsafe rotation (`force_recover`) + /// - Computes validator subset and session ID (with optional unsafe-rotation patch) + /// - Skips sessions in the [`destroyed_sessions`] blacklist + /// - Finds the local validator key in the subset + /// - Creates or reuses the `ValidatorGroup` and starts it if newly created #[allow(clippy::too_many_arguments)] async fn start_sessions( &mut self, @@ -968,9 +1398,19 @@ impl ValidatorManagerImpl { master_cc_range: &RangeInclusive, last_masterchain_block: &BlockIdExt, ) -> Result<()> { - let validator_list_id = match &self.validator_list_status.curr { - Some(list_id) => list_id, - None => return Ok(()), + let validator_list_id = match self.validator_list_status.get_ready_current_list() { + Some(list_id) => list_id.clone(), + None => { + if let Some(list_id) = self.validator_list_status.curr.as_ref() { + log::warn!( + target: "validator_manager", + "Skipping current-session start for validator list {:x}: \ + network context is not ready yet", + list_id + ); + } + return Ok(()); + } }; let full_validator_set = mc_state_extra.config.validator_set()?; @@ -1007,8 +1447,20 @@ impl ValidatorManagerImpl { let cc_seqno = cc_seqno_from_state; - log::trace!(target: "validator_manager", "Trying to start/update session for shard {}, cc_seqno {}", - ident, cc_seqno_from_state + // C++ parity: during unsafe rotation, skip all non-masterchain shards + // before any expensive subset/session-id computation. + if should_skip_session_for_unsafe_rotation(do_unsafe_catchain_rotate, &ident) { + log::trace!( + target: "validator_manager", + "Shard {}, cc_seqno {}: unsafe rotation skipping", + ident, cc_seqno + ); + continue; + } + + log::trace!( + target: "validator_manager", + "Trying to start/update session for shard {ident}, cc_seqno {cc_seqno_from_state}" ); let prev = PrevBlockHistory::with_prevs(&ident, prev_blocks); @@ -1036,33 +1488,37 @@ impl ValidatorManagerImpl { ValidatorSet::with_cc_seqno(0, 0, 0, cc_seqno, subset.validators.clone())?; let max_vertical_seqno = self.engine.hardforks().len() as u32; - // Select appropriate options hash based on shard type - let current_opts_hash = if ident.is_masterchain() { - &sessions_options.mc_hash - } else { - &sessions_options.shard_hash - }; - let general_session_info = Arc::new(GeneralSessionInfo { shard: ident.clone(), - opts_hash: current_opts_hash.clone(), + opts_hash: sessions_options.get_session_id_hash(&ident).clone(), catchain_seqno: cc_seqno, key_seqno: keyblock_seqno, max_vertical_seqno: max_vertical_seqno, }); - let prev_block_seqno_opt = prev.get_prevs().first().map(|x| x.seq_no); + let rotation_block_seqno_opt = + unsafe_rotation_block_seqno(&ident, last_masterchain_block); let accelerated_consensus_enabled = self.is_accelerated_consensus_enabled_for_shard(mc_state_extra, &ident); let session_id = get_session_unsafe_id( general_session_info.clone(), vsubset.list(), true, - prev_block_seqno_opt, + do_unsafe_catchain_rotate, + rotation_block_seqno_opt, &self.config, accelerated_consensus_enabled, ); + // C++ parity: skip sessions in the destroyed set + if self.destroyed_sessions.contains(&session_id) { + log::trace!( + target: "validator_manager", + "Skipping destroyed session {:x} for shard {}", session_id, ident + ); + continue; + } + let local_id_option = self.find_us(&subset.validators); if let Some(local_id) = &local_id_option { @@ -1078,48 +1534,63 @@ impl ValidatorManagerImpl { gc_validator_sessions.remove(&session_id); - // If blockchain works under unsafe_catchain_rotation, then do not change its status: - // 1. Do not start new sessions - // 2. Do not remove functioning old sessions - if do_unsafe_catchain_rotate && !ident.is_masterchain() && local_id_option.is_none() - { - log::trace!( - target: "validator", - "Current shard {}, session {:x}: unsafe rotation skipping", - ident, session_id - ); - continue; - } - let engine = self.engine.clone(); let allow_unsafe_self_blocks_resync = self.config.unsafe_resync_catchains.contains(&cc_seqno); let current_session_options = sessions_options.get_session_options(&ident); // Select consensus type based on ConfigParam 30 - let consensus_options = - self.select_consensus_options(&ident, mc_state, current_session_options); + let consensus_options = self.select_consensus_options( + &ident, + mc_state, + current_session_options, + cc_seqno, + ); - let session = self - .validator_sessions - .entry(session_id.clone()) - .or_insert_with(|| { - Arc::new(ValidatorGroup::new( - general_session_info.clone(), - local_id.clone(), - session_id.clone(), - validator_list_id.clone(), - vsubset.clone(), - consensus_options.clone(), - engine, - allow_unsafe_self_blocks_resync, - )) - }) - .clone(); + let session = if let Some(promoted) = self.future_sessions.remove(&session_id) { + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: promote shard={} cc_seqno={} session_id={:x} \ + future -> current", + ident, cc_seqno, session_id); + self.current_sessions.entry(session_id.clone()).or_insert(promoted).clone() + } else { + self.current_sessions + .entry(session_id.clone()) + .or_insert_with(|| { + let consensus_name = match &consensus_options { + ConsensusOptions::Simplex(_) => "simplex", + ConsensusOptions::Catchain(_) => "catchain", + }; + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: create_current shard={} cc_seqno={} \ + session_id={:x} consensus={} local_key={}", + ident, cc_seqno, session_id, consensus_name, + hex::encode(local_id.id().data())); + metrics::counter!( + "ton_node_validator_session_created_total", + "consensus" => consensus_name + ) + .increment(1); + Arc::new(ValidatorGroup::new( + general_session_info.clone(), + local_id.clone(), + session_id.clone(), + validator_list_id.clone(), + vsubset.clone(), + consensus_options.clone(), + engine, + allow_unsafe_self_blocks_resync, + )) + }) + .clone() + }; let session_status = session.get_status().await; - if session_status == ValidatorGroupStatus::Created { - log::trace!(target: "validator_manager", "Current shard {}, session {:x}: starting", ident, session_id); + if session.try_prepare_start(group_start_status).await? { + log::trace!( + target: "validator_manager", + "Current shard {ident}, session {session_id:x}: starting" + ); session .start_with_status( @@ -1130,10 +1601,24 @@ impl ValidatorManagerImpl { self.rt.clone(), ) .await?; + } else if session.is_start_pending().await { + log::trace!( + target: "validator_manager", + "Current shard {}, session {:x}: start pending", + ident, + session_id + ); } else if session_status >= ValidatorGroupStatus::Stopping { - log::error!(target: "validator_manager", "Cannot start stopped session {}", session.info().await); + log::error!( + target: "validator_manager", + "Cannot start stopped session {}", + session.info().await + ); } else { - log::trace!(target: "validator_manager", "Current shard {}, session {:x}: working", ident, session_id); + log::trace!( + target: "validator_manager", + "Current shard {ident}, session {session_id:x}: working" + ); } } else { log::trace!(target: "validator_manager", "We are not in subset for {}", ident); @@ -1144,6 +1629,16 @@ impl ValidatorManagerImpl { Ok(()) } + /// Main per-masterchain-block update loop. + /// + /// Mirrors `ValidatorManagerImpl::update_shards()` in C++ (`manager.cpp`). + /// Responsibilities: + /// 1. Refresh validator list membership (`update_validator_lists`) + /// 2. Collect current and future shards from the masterchain state + /// 3. Create/start sessions for current shards (`start_sessions`) + /// 4. Pre-create sessions for upcoming shards (future-sessions loop) + /// 5. GC sessions that are no longer needed (`stop_and_remove_sessions`) + /// 6. Clear the destroyed-sessions blacklist on full shard rotation async fn update_shards(&mut self, mc_state: Arc) -> Result<()> { let mc_state_extra = mc_state.shard_state_extra()?; let master_cc_seqno = get_masterchain_seqno(self.engine.clone(), &mc_state).await?; @@ -1151,8 +1646,14 @@ impl ValidatorManagerImpl { let sessions_options = self.compute_session_options(mc_state_extra, &catchain_config).await?; + log::trace!(target: "validator_manager", + "update_shards: mc_seqno={} mc_cc_seqno={} current_sessions={} future_sessions={}", + mc_state.block_id().seq_no, master_cc_seqno, + self.current_sessions.len(), self.future_sessions.len()); + if !self.update_validator_lists(&mc_state).await? { - log::info!(target: "validator_manager", "Current validator list is empty, validation is disabled."); + log::info!(target: "validator_manager", + "VALIDATION_STATUS: not a validator (not in current or next set), disabling"); self.disable_validation(true).await?; return Ok(()); } @@ -1177,7 +1678,7 @@ impl ValidatorManagerImpl { // Collect info about shards let mut gc_validator_sessions: HashSet = - self.validator_sessions.keys().cloned().collect(); + self.current_sessions.keys().chain(self.future_sessions.keys()).cloned().collect(); // Shards that are working or about to start (continue) in this masterstate: shard_ident -> prevs let mut new_shards = HashMap::new(); @@ -1202,14 +1703,14 @@ impl ValidatorManagerImpl { ident.clone(), descr.seq_no, descr.root_hash, - descr.file_hash + descr.file_hash, ); if descr.before_split { let lr_shards = ident.split(); match lr_shards { - Err(e) => log::error!(target: "validator_manager", "Cannot split shard: `{}`", e), - Ok((l,r)) => { + Err(e) => log::error!(target: "validator_manager", "Cannot split shard: `{e}`"), + Ok((l, r)) => { new_shards.insert(l, vec![top_block.clone()]); new_shards.insert(r, vec![top_block.clone()]); blocks_before_split.insert(top_block); @@ -1218,15 +1719,15 @@ impl ValidatorManagerImpl { } else if descr.before_merge { let parent_shard = ident.merge(); match parent_shard { - Err(e) => log::error!(target: "validator_manager", "Cannot merge shard: `{}`", e), + Err(e) => log::error!(target: "validator_manager", "Cannot merge shard: `{e}`"), Ok(p) => { let mut prev_blocks = match new_shards.get(&p) { Some(pb) => pb.clone(), - None => vec![BlockIdExt::default(), BlockIdExt::default()] + None => vec![BlockIdExt::default(), BlockIdExt::default()], }; // Add previous block for the shard: there are two parents for merge, so two prevs - let (_l,r) = p.split()?; + let (_l, r) = p.split()?; prev_blocks[(r == ident) as usize] = top_block; new_shards.insert(p, prev_blocks); } @@ -1242,26 +1743,32 @@ impl ValidatorManagerImpl { FutureSplitMerge::None => { future_shards.insert(ident); } - FutureSplitMerge::Split{split_utime: time, interval: _interval} => { + FutureSplitMerge::Split { split_utime: time, interval: _interval } => { if (time as u64) < cur_time + 60 { match ident.split() { - Ok((l,r)) => { + Ok((l, r)) => { future_shards.insert(l); future_shards.insert(r); } - Err(e) => log::error!(target: "validator_manager", "Cannot split shard {}: `{}`", ident, e) + Err(e) => log::error!( + target: "validator_manager", + "Cannot split shard {ident}: `{e}`" + ), } } else { future_shards.insert(ident); } } - FutureSplitMerge::Merge{merge_utime: time, interval: _interval} => { + FutureSplitMerge::Merge { merge_utime: time, interval: _interval } => { if (time as u64) < cur_time + 60 { match ident.merge() { Ok(p) => { future_shards.insert(p); } - Err(e) => log::error!(target: "validator_manager", "Cannot merge shard {}: `{}`", ident, e) + Err(e) => log::error!( + target: "validator_manager", + "Cannot merge shard {ident}: `{e}`" + ), } } else { future_shards.insert(ident); @@ -1367,18 +1874,22 @@ impl ValidatorManagerImpl { mc_validators.append(&mut wc.validators.clone()); } - if let Some(local_id) = self.find_us(&wc.validators) { - let max_vertical_seqno = self.engine.hardforks().len() as u32; - // Select appropriate options and hash based on shard type - let current_opts_hash = if ident.is_masterchain() { - &sessions_options.mc_hash - } else { - &sessions_options.shard_hash - }; + if !self.validator_list_status.is_list_network_ready(next_val_list_id) { + log::trace!( + target: "validator_manager", + "Skipping future-session precreation for shard {}: validator list {:x} \ + network context is not ready yet", + ident, + next_val_list_id + ); + continue; + } + if let Some(local_id) = self.find_us_for_list(&wc.validators, next_val_list_id) { + let max_vertical_seqno = self.engine.hardforks().len() as u32; let new_session_info = Arc::new(GeneralSessionInfo { shard: ident.clone(), - opts_hash: current_opts_hash.clone(), + opts_hash: sessions_options.get_session_id_hash(&ident).clone(), catchain_seqno: *next_cc_seqno, key_seqno: keyblock_seqno, max_vertical_seqno: max_vertical_seqno, @@ -1392,6 +1903,16 @@ impl ValidatorManagerImpl { true, accelerated_consensus_enabled, ); + + // C++ parity: skip sessions in the destroyed set + if self.destroyed_sessions.contains(&session_id) { + log::trace!( + target: "validator_manager", + "Skipping destroyed future session {:x} for shard {}", session_id, ident + ); + continue; + } + let vsubset = wc.compute_validator_set(*next_cc_seqno)?; let current_session_options = sessions_options.get_session_options(&ident); gc_validator_sessions.remove(&session_id); @@ -1401,24 +1922,111 @@ impl ValidatorManagerImpl { &ident, mc_state.as_ref(), current_session_options, + *next_cc_seqno, ); - self.validator_sessions.entry(session_id.clone()).or_insert_with(|| { - Arc::new(ValidatorGroup::new( - new_session_info, - local_id, - session_id.clone(), - next_val_list_id.clone(), - vsubset.clone(), - consensus_options.clone(), - self.engine.clone(), - self.config.unsafe_resync_catchains.contains(next_cc_seqno), - )) - }); + if self.current_sessions.contains_key(&session_id) { + log::trace!( + target: "validator_manager", + "Future session {:x} for shard {} already in current_sessions, skipping", + session_id, ident + ); + continue; + } + + let is_new = !self.future_sessions.contains_key(&session_id); + let group = self + .future_sessions + .entry(session_id.clone()) + .or_insert_with(|| { + let consensus_name = match &consensus_options { + ConsensusOptions::Simplex(_) => "simplex", + ConsensusOptions::Catchain(_) => "catchain", + }; + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: create_future shard={} cc_seqno={} \ + session_id={:x} consensus={}", + ident, next_cc_seqno, session_id, consensus_name); + metrics::counter!( + "ton_node_validator_session_created_total", + "consensus" => consensus_name + ) + .increment(1); + Arc::new(ValidatorGroup::new( + new_session_info, + local_id, + session_id.clone(), + next_val_list_id.clone(), + vsubset.clone(), + consensus_options.clone(), + self.engine.clone(), + self.config.unsafe_resync_catchains.contains(next_cc_seqno), + )) + }) + .clone(); + + if is_new && self.sync_complete { + log::debug!(target: "validator_manager", + "Pre-creating engine for future session shard={} cc_seqno={} session_id={:x}", + ident, next_cc_seqno, session_id); + let g = group.clone(); + let sid = session_id.clone(); + tokio::spawn(async move { + if let Err(e) = g.pre_create_engine().await { + log::error!(target: "validator_manager", + "SESSION_LIFECYCLE: pre_create_engine_failed session_id={:x} error={}", sid, e); + } + }); + } } } + + // Stale-future culling: remove future entries whose shard already has a current + // group with equal or higher cc_seqno, or whose shard is an ancestor/descendant + // of a current shard with strictly higher cc_seqno. + // C++ parity: equal + related conditions from manager.cpp update_shards(). + { + let stale_ids: Vec = self + .future_sessions + .iter() + .filter(|(_, fg)| { + self.current_sessions.values().any(|cg| { + let shards_equal = cg.shard() == fg.shard(); + let shards_related = cg.shard().is_ancestor_for(fg.shard()) + || fg.shard().is_ancestor_for(cg.shard()); + let equal_condition = shards_equal && cg.cc_seqno() >= fg.cc_seqno(); + let related_condition = shards_related && cg.cc_seqno() > fg.cc_seqno(); + equal_condition || related_condition + }) + }) + .map(|(id, _)| id.clone()) + .collect(); + for id in stale_ids { + if let Some(fg) = self.future_sessions.remove(&id) { + // C++ parity: destroyed_validator_sessions_.insert(id) + self.destroyed_sessions.insert(id.clone()); + log::info!(target: "validator_manager", + "SESSION_LIFECYCLE: cull_stale_future shard={} cc_seqno={} session_id={:x} \ + (superseded by active current session)", + fg.shard(), fg.cc_seqno(), id); + let cl = if fg.is_simplex() { "simplex" } else { "catchain" }; + metrics::counter!( + "ton_node_validator_session_destroyed_total", + "consensus" => cl + ) + .increment(1); + // C++ parity: IValidatorGroup::destroy + if let Err(e) = fg.stop(self.rt.clone(), true).await { + log::error!(target: "validator_manager", + "SESSION_LIFECYCLE: cull_stale_future_stop_failed session_id={:x}: {}", + id, e); + } + } + } + } + let mut precalc_split_queues_for: HashSet = HashSet::new(); - for session in self.validator_sessions.values() { + for session in self.current_sessions.values().chain(self.future_sessions.values()) { for id in &blocks_before_split { if id.shard().is_parent_for(session.shard()) { log::trace!( @@ -1448,11 +2056,6 @@ impl ValidatorManagerImpl { }); } - if rotate_all_shards(mc_state_extra) { - log::info!(target: "validator_manager", "New last rotation block: {}", last_masterchain_block); - self.engine.save_last_rotation_block_id(last_masterchain_block)?; - } - // Notify shard simplex sessions about MC finalization // This is needed for empty block generation (finalization recovery) self.notify_shard_sessions_mc_finalized(last_masterchain_block.seq_no); @@ -1460,20 +2063,43 @@ impl ValidatorManagerImpl { log::trace!(target: "validator_manager", "starting stop&remove"); self.stop_and_remove_sessions(&gc_validator_sessions, true).await; + if rotate_all_shards(mc_state_extra) { + log::info!(target: "validator_manager", "New last rotation block: {}", last_masterchain_block); + self.engine.save_last_rotation_block_id(last_masterchain_block)?; + self.clear_destroyed_sessions()?; + } else { + self.persist_destroyed_sessions()?; + } + log::trace!(target: "validator_manager", "starting garbage collect"); self.garbage_collect().await; log::trace!(target: "validator_manager", "exiting"); Ok(()) } + /// Light per-iteration update: Prometheus gauges, engine timing, health warnings. + /// Called on every wait-loop iteration (every few seconds). async fn stats(&mut self) { - log::info!(target: "validator_manager", "{:32} {}", "session id", "st round shard"); - log::info!(target: "validator_manager", "{:-64}", ""); + let validation_status = self.engine.validation_status(); + let in_current_set = self.validator_list_status.curr.is_some(); - // Validation shards statistics - for (_, group) in self.validator_sessions.iter() { - log::info!(target: "validator_manager", "{}", group.info().await); + let mut state_counts: HashMap<&'static str, u64> = HashMap::new(); + let mut stalled_count: u64 = 0; + let mut simplex_count: u64 = 0; + let mut catchain_count: u64 = 0; + + for group in self.current_sessions.values() { let status = group.get_status().await; + let is_stalled = group.stalled.load(Ordering::Relaxed); + *state_counts.entry(status.metric_label()).or_default() += 1; + if is_stalled { + stalled_count += 1; + } + if group.is_simplex() { + simplex_count += 1; + } else { + catchain_count += 1; + } if status == ValidatorGroupStatus::Sync || status == ValidatorGroupStatus::Active || status == ValidatorGroupStatus::Stopping @@ -1484,7 +2110,272 @@ impl ValidatorManagerImpl { .set_last_collation_time(group.shard().clone(), group.last_collation_time()); } } - log::trace!(target: "validator_manager", "======= sessions stats over ======="); + for group in self.future_sessions.values() { + let status = group.get_status().await; + *state_counts.entry(status.metric_label()).or_default() += 1; + if group.is_simplex() { + simplex_count += 1; + } else { + catchain_count += 1; + } + } + + // Health warnings for operator attention + if stalled_count > 0 { + log::warn!(target: "validator_manager", + "HEALTH_CHECK: {} session(s) stalled (validation queue inactive)", stalled_count); + } + if in_current_set + && self.current_sessions.is_empty() + && validation_status >= ValidationStatus::Countdown + { + log::warn!(target: "validator_manager", + "HEALTH_CHECK: node is in current validator set but has no current sessions \ + (validation_status={:?})", validation_status); + } + if validation_status == ValidationStatus::Countdown { + let has_active_or_sync = state_counts.get("active").copied().unwrap_or(0) > 0 + || state_counts.get("sync").copied().unwrap_or(0) > 0; + if !has_active_or_sync && !self.current_sessions.is_empty() { + log::warn!(target: "validator_manager", + "HEALTH_CHECK: validation_status=Countdown but no session reached sync/active \ + (possible TN-1050 regression)"); + } + } + + // Prometheus metrics + metrics::gauge!("ton_node_validator_sessions_total", "role" => "current") + .set(self.current_sessions.len() as f64); + metrics::gauge!("ton_node_validator_sessions_total", "role" => "future") + .set(self.future_sessions.len() as f64); + metrics::gauge!("ton_node_validator_sessions_by_consensus", "type" => "simplex") + .set(simplex_count as f64); + metrics::gauge!("ton_node_validator_sessions_by_consensus", "type" => "catchain") + .set(catchain_count as f64); + for (state, count) in &state_counts { + metrics::gauge!("ton_node_validator_sessions_by_state", "state" => *state) + .set(*count as f64); + } + metrics::gauge!("ton_node_validator_group_stalled").set(stalled_count as f64); + metrics::gauge!("ton_node_validator_sync_complete").set(self.sync_complete as u8 as f64); + + // Full metrics dump once per minute + if self.last_metrics_dump.elapsed() >= Duration::from_secs(60) { + self.last_metrics_dump = tokio::time::Instant::now(); + self.dump_metrics().await; + } + } + + /// Comprehensive metrics dump emitted once per minute. + /// Structured for easy grep and readable operator dashboards. + async fn dump_metrics(&self) { + let now = SystemTime::now(); + let now_unix = now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + let validation_status = self.engine.validation_status(); + let in_current_set = self.validator_list_status.curr.is_some(); + let in_next_set = self.validator_list_status.next.is_some(); + // โ”€โ”€ Header: overall manager state โ”€โ”€ + let mut simplex_count = 0u32; + let mut catchain_count = 0u32; + let mut stalled_count = 0u32; + let mut state_counts: HashMap<&'static str, u32> = HashMap::new(); + + // Collect current session snapshots + let mut current_snapshots: Vec<(SessionSnapshot, bool, bool, u64, u64)> = Vec::new(); + for group in self.current_sessions.values() { + let snap = group.snapshot().await; + let is_stalled = group.stalled.load(Ordering::Relaxed); + let is_collating = group.is_collating(); + let last_val = group.last_validation_time(); + let last_col = group.last_collation_time(); + *state_counts.entry(snap.status.metric_label()).or_default() += 1; + if is_stalled { + stalled_count += 1; + } + if snap.consensus_type == super::consensus::ConsensusType::Simplex { + simplex_count += 1; + } else { + catchain_count += 1; + } + current_snapshots.push((snap, is_stalled, is_collating, last_val, last_col)); + } + + // Collect future session snapshots + let mut future_snapshots: Vec = Vec::new(); + for group in self.future_sessions.values() { + let snap = group.snapshot().await; + *state_counts.entry(snap.status.metric_label()).or_default() += 1; + if snap.consensus_type == super::consensus::ConsensusType::Simplex { + simplex_count += 1; + } else { + catchain_count += 1; + } + future_snapshots.push(snap); + } + + let state_str: String = + state_counts.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(" "); + + let mut lines = Vec::::new(); + lines.push(format!( + "=== VALIDATOR MANAGER METRICS (once/min) ===\n\ + \x20 validation_status={:?} sync_complete={}\n\ + \x20 in_current_set={} in_next_set={}\n\ + \x20 sessions: current={} future={} total={} (simplex={} catchain={})\n\ + \x20 by_state: [{}] stalled={}", + validation_status, + self.sync_complete, + in_current_set, + in_next_set, + self.current_sessions.len(), + self.future_sessions.len(), + self.current_sessions.len() + self.future_sessions.len(), + simplex_count, + catchain_count, + state_str, + stalled_count, + )); + + // โ”€โ”€ Validator keys โ”€โ”€ + lines.push(String::from(" VALIDATOR KEYS:")); + for (role, list_id_opt, utime_opt) in [ + ( + "current", + &self.validator_list_status.curr, + self.validator_list_status.curr_utime_since, + ), + ("next", &self.validator_list_status.next, self.validator_list_status.next_utime_since), + ] { + if let Some(list_id) = list_id_opt { + let entry = self.validator_list_status.get_list(list_id); + let net_ready = entry.map_or(false, |e| e.network_ready); + let key_strs: Vec = entry + .map(|e| e.keys.iter().map(|k| base64_encode(k.id().data())).collect()) + .unwrap_or_default(); + lines.push(format!( + " [{}] list_id={:x} election_utime={} net_ready={} keys=[{}]", + role, + list_id, + utime_opt.map_or("-".to_string(), |u| u.to_string()), + net_ready, + key_strs.join(", "), + )); + } else { + lines.push(format!(" [{}] not in set", role)); + } + } + + // โ”€โ”€ Config-level key bindings (election_id โ†’ validator_key, adnl_key) โ”€โ”€ + match self.engine.get_validator_key_bindings() { + Ok(bindings) => { + lines.push(format!(" KEY BINDINGS ({}):", bindings.len())); + let mut seen_elections: HashMap = HashMap::new(); + for (idx, b) in bindings.iter().enumerate() { + let adnl_str = b.validator_adnl_key_id.as_deref().unwrap_or("(none)"); + lines.push(format!( + " election_id={:<12} key={} adnl={} expire_at={}", + b.election_id, b.validator_key_id, adnl_str, b.expire_at, + )); + if let Some(prev_idx) = seen_elections.insert(b.election_id, idx) { + log::error!( + target: "validator_manager", + "KEY BINDING INVARIANT VIOLATION: duplicate election_id={}: \ + binding[{}] and binding[{}] share the same election_id", + b.election_id, prev_idx, idx, + ); + } + if b.validator_adnl_key_id.is_none() { + log::warn!( + target: "validator_manager", + "KEY BINDING: election_id={} has validator_key={} but no ADNL key bound", + b.election_id, b.validator_key_id, + ); + } + } + } + Err(e) => { + lines.push(format!(" KEY BINDINGS: error retrieving: {e}")); + } + } + + // โ”€โ”€ Current sessions detail โ”€โ”€ + if current_snapshots.is_empty() { + lines.push(String::from(" CURRENT SESSIONS: (none)")); + } else { + lines.push(format!(" CURRENT SESSIONS ({}):", current_snapshots.len())); + for (snap, is_stalled, is_collating, last_val, last_col) in ¤t_snapshots { + let shard_str = format_shard_short(&snap.shard); + let consensus_str = + if snap.consensus_type == super::consensus::ConsensusType::Simplex { + "splx" + } else { + "cch" + }; + let age = now.duration_since(snap.created_at).unwrap_or_default(); + let val_ago = format_time_ago(now_unix, *last_val); + let col_ago = format_time_ago(now_unix, *last_col); + let countdown_str = match snap.status { + ValidatorGroupStatus::Countdown { start_at } => { + let remaining = + start_at.saturating_duration_since(tokio::time::Instant::now()); + format!("countdown({}s)", remaining.as_secs()) + } + _ => format!("{}", snap.status), + }; + let last_mc = + snap.last_accepted_mc_seqno.map_or("-".to_string(), |s| s.to_string()); + lines.push(format!( + " {:<8} cc={:<4} {:<4} {:<14} rnd={:<4} collator={:<3} collating={:<3} \ + stall={:<3} val_ago={:<6} col_ago={:<6} mc_init={:<6} mc_last={:<6} \ + age={} id={:x}", + shard_str, + snap.cc_seqno, + consensus_str, + countdown_str, + snap.round, + if snap.is_collator { "yes" } else { "no" }, + if *is_collating { "yes" } else { "no" }, + if *is_stalled { "yes" } else { "no" }, + val_ago, + col_ago, + snap.mc_initial_seqno, + last_mc, + format_duration_short(age), + snap.session_id, + )); + } + } + + // โ”€โ”€ Future sessions detail โ”€โ”€ + if future_snapshots.is_empty() { + lines.push(String::from(" FUTURE SESSIONS: (none)")); + } else { + lines.push(format!(" FUTURE SESSIONS ({}):", future_snapshots.len())); + for snap in &future_snapshots { + let shard_str = format_shard_short(&snap.shard); + let consensus_str = + if snap.consensus_type == super::consensus::ConsensusType::Simplex { + "splx" + } else { + "cch" + }; + let age = now.duration_since(snap.created_at).unwrap_or_default(); + lines.push(format!( + " {:<8} cc={:<4} {:<4} {:<14} engine={:<3} key_seq={} age={} id={:x}", + shard_str, + snap.cc_seqno, + consensus_str, + format!("{}", snap.status), + if snap.has_engine { "yes" } else { "no" }, + snap.key_seqno, + format_duration_short(age), + snap.session_id, + )); + } + } + + lines.push(String::from("=== END VALIDATOR MANAGER METRICS ===")); + log::info!(target: "validator_manager", "{}", lines.join("\n")); } fn read_catchain_config(&self, state: &ShardStateStuff) -> Result { @@ -1494,6 +2385,7 @@ impl ValidatorManagerImpl { /// infinite loop with possible error cancellation async fn invoke(&mut self) -> Result<()> { + self.load_destroyed_sessions()?; let last_applied_block_id = self.engine.load_last_applied_mc_block_id()?.ok_or_else(|| { error!("Cannot run validator_manager if no last applied block is present") @@ -1512,8 +2404,10 @@ impl ValidatorManagerImpl { .load_block_handle(&id)? .ok_or_else(|| error!("Cannot load handle for master block {}", id))? } else { - log::info!(target: "validator_manager", - "Validator manager initialization: no last rotation block, using last applied block: {}", last_applied_block_id + log::info!( + target: "validator_manager", + "Validator manager initialization: no last rotation block, \ + using last applied block: {last_applied_block_id}" ); last_applied_block_handle.clone() }; @@ -1521,15 +2415,29 @@ impl ValidatorManagerImpl { //let block_observer = self.initialize_block_observer(&last_applied_block_handle).await?; while !self.engine.check_stop() { - log::trace!(target: "validator_manager", "Trying to load state for masterblock {}", mc_handle.id().seq_no); + log::trace!( + target: "validator_manager", + "Trying to load state for masterblock {}", + mc_handle.id().seq_no + ); match self.engine.load_state(mc_handle.id()).await { Ok(mc_state) => { - log::info!(target: "validator_manager", "Processing masterblock {}", mc_handle.id().seq_no); - log::trace!(target: "validator_manager", "Processing messages from masterblock {}", mc_handle.id().seq_no); - log::trace!(target: "validator_manager", "Updating shards according to masterblock {}", mc_handle.id().seq_no); + let seqno = mc_handle.id().seq_no; + log::info!(target: "validator_manager", "Processing masterblock {seqno}"); + log::trace!( + target: "validator_manager", + "Processing messages from masterblock {seqno}" + ); + log::trace!( + target: "validator_manager", + "Updating shards according to masterblock {seqno}" + ); self.update_shards(mc_state).await?; - log::trace!(target: "validator_manager", "Shards for masterblock {} updated", mc_handle.id().seq_no); + log::trace!( + target: "validator_manager", + "Shards for masterblock {seqno} updated" + ); } Err(e) => { if self.engine.validation_status().allows_validate() { @@ -1540,19 +2448,30 @@ impl ValidatorManagerImpl { e ) } - log::info!(target: "validator_manager", "Processing masterblock {}: state not available, going forward", mc_handle.id().seq_no); + log::info!( + target: "validator_manager", + "Processing masterblock {}: state not available, going forward", + mc_handle.id().seq_no + ); } } mc_handle = loop { log::trace!(target: "validator_manager", "Checking stop engine"); if self.engine.check_stop() { - log::trace!(target: "validator_manager", "Engine is stoped. Exiting from invocation loop (while loading block)"); + log::trace!( + target: "validator_manager", + "Engine is stopped. Exiting from invocation loop (while loading block)" + ); return Ok(()); } log::trace!(target: "validator_manager", "Checked stop engine: going on"); self.stats().await; - log::trace!(target: "validator_manager", "Waiting next applied masterblock after {}", mc_handle.id().seq_no); + log::trace!( + target: "validator_manager", + "Waiting next applied masterblock after {}", + mc_handle.id().seq_no + ); match timeout( self.config.update_interval, self.engine.wait_next_applied_mc_block(&mc_handle, None), @@ -1560,7 +2479,9 @@ impl ValidatorManagerImpl { .await { Ok(r_res) => { - log::trace!(target: "validator_manager", "Got next applied master block (result): {}", + log::trace!( + target: "validator_manager", + "Got next applied master block (result): {}", match &r_res { Err(e) => format!("Err({})", e), Ok((h, _bs)) => format!("Ok({})", h.id()) @@ -1579,7 +2500,10 @@ impl ValidatorManagerImpl { } } - log::info!(target: "validator_manager", "Engine is stopped. Exiting from invocation loop (while applying state)"); + log::info!( + target: "validator_manager", + "Engine is stopped. Exiting from invocation loop (while applying state)" + ); Ok(()) } } @@ -1592,11 +2516,15 @@ pub fn start_validator_manager( ) { const CHECK_VALIDATOR_TIMEOUT: u64 = 60; //secs runtime.clone().spawn(async move { - log::info!(target: "validator_manager", "checking if current node is a validator during {CHECK_VALIDATOR_TIMEOUT} secs"); + log::info!( + target: "validator_manager", + "checking if current node is a validator during {CHECK_VALIDATOR_TIMEOUT} secs" + ); engine.acquire_stop(Engine::MASK_SERVICE_VALIDATOR_MANAGER); while !engine.get_validator_status() { log::trace!(target: "validator_manager", "Not a validator, waiting..."); let _ = engine.clear_last_rotation_block_id(); + let _ = engine.clear_destroyed_session_ids(); for _ in 0..CHECK_VALIDATOR_TIMEOUT { tokio::time::sleep(Duration::from_secs(1)).await; if engine.check_stop() { @@ -1611,7 +2539,10 @@ pub fn start_validator_manager( let mut manager = ValidatorManagerImpl::create(engine.clone(), runtime.clone(), config); if let Err(e) = manager.invoke().await { - log::error!(target: "validator_manager", "FATAL!!! Unexpected error in validator manager: {}", e); + log::error!( + target: "validator_manager", + "FATAL!!! Unexpected error in validator manager: {e}" + ); } manager.stop_validation().await; diff --git a/src/node/src/validator/validator_session_listener.rs b/src/node/src/validator/validator_session_listener.rs index 257cbc7..1fece8b 100644 --- a/src/node/src/validator/validator_session_listener.rs +++ b/src/node/src/validator/validator_session_listener.rs @@ -17,7 +17,7 @@ use super::consensus::{ use crate::validator::validator_group::{ValidatorGroup, ValidatorGroupStatus}; use std::{ fmt, - sync::Arc, + sync::{atomic::Ordering, Arc}, time::{Duration, SystemTime, SystemTimeError}, }; use ton_block::{BlockIdExt, BlockSignaturesVariant, ShardIdent}; @@ -399,23 +399,22 @@ pub async fn process_validation_queue( ); break 'queue_loop; } - Err(_elapsed) => { - // Timeout occurred, queue is empty - match (last_action + VALIDATION_QUEUE_EMPTY_TOO_LONG).elapsed() { - Ok(_) => { - log::info!( - target: "validator", - "({}): Session {}: validation action queue empty", - next_block_descr, - g_info - ); - last_action = SystemTime::now(); - } - Err(SystemTimeError { .. }) => (), + Err(_elapsed) => match (last_action + VALIDATION_QUEUE_EMPTY_TOO_LONG).elapsed() { + Ok(_) => { + g.stalled.store(true, Ordering::Relaxed); + log::info!( + target: "validator", + "({}): Session {}: validation action queue empty (stalled=true)", + next_block_descr, + g_info + ); + last_action = SystemTime::now(); } - } + Err(SystemTimeError { .. }) => (), + }, Ok(Some(action)) => { last_action = SystemTime::now(); + g.stalled.store(false, Ordering::Relaxed); let action_str = action.to_string(); log::info!( diff --git a/src/node/storage/src/block_handle_db.rs b/src/node/storage/src/block_handle_db.rs index ffce6cd..ecc9837 100644 --- a/src/node/storage/src/block_handle_db.rs +++ b/src/node/storage/src/block_handle_db.rs @@ -635,6 +635,10 @@ impl BlockHandleStorage { self.load_state(key, &self.validator_state_db) } + pub fn load_validator_state_raw(&self, key: &str) -> Result>> { + Ok(self.validator_state_db.try_get_raw(key.as_bytes())?.map(|value| value.to_vec())) + } + pub fn save_handle( &self, handle: &Arc, @@ -659,6 +663,16 @@ impl BlockHandleStorage { .map_err(|_| error!("Cannot store validator state {}: storer thread dropped", id)) } + pub fn save_validator_state_raw(&self, key: &str, data: &[u8]) -> Result<()> { + self.delete_state(key)?; + self.validator_state_db.put_raw(key.as_bytes(), data) + } + + pub fn drop_validator_state_raw(&self, key: &str) -> Result<()> { + self.delete_state(key)?; + self.validator_state_db.delete_raw(key.as_bytes()) + } + pub fn drop_handle(&self, id: BlockIdExt, callback: Option>) -> Result<()> { let _ = self.handle_cache.remove(id.root_hash()); self.storer diff --git a/src/node/tests/compat_test/src/test_helpers.rs b/src/node/tests/compat_test/src/test_helpers.rs index daa8efd..58c2d43 100644 --- a/src/node/tests/compat_test/src/test_helpers.rs +++ b/src/node/tests/compat_test/src/test_helpers.rs @@ -281,13 +281,13 @@ impl RustTestNode { } /// Send two-step FEC broadcast via overlay (requires RLDP) - pub fn send_broadcast_two_step(&self, overlay_id: &Arc, data: &[u8]) { + pub fn send_broadcast_twostep(&self, overlay_id: &Arc, data: &[u8]) { self.rt.block_on(async { let tagged = TaggedByteSlice::with_object(data); self.overlay - .broadcast_two_step(overlay_id, &tagged, None, 0) + .broadcast_twostep(overlay_id, &tagged, None, 0, Vec::new()) .await - .expect("broadcast_two_step failed"); + .expect("broadcast_twostep failed"); }); } @@ -517,7 +517,7 @@ impl RustQuicTestNode { let _guard = rt.enter(); let quic_subscribers: Vec> = vec![test_sub as Arc, overlay.clone()]; - let quic = QuicNode::new(quic_subscribers, cancellation_token.clone()); + let quic = QuicNode::new(quic_subscribers, cancellation_token.clone(), None); let bind_addr = SocketAddr::new( Ipv4Addr::from(adnl.ip_address().ip()).into(), adnl.ip_address().port() + QuicNode::OFFSET_PORT, diff --git a/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs b/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs index 373a297..70b50a0 100644 --- a/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs +++ b/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs @@ -321,7 +321,7 @@ fn test_twostep_rust_sender_cpp_leaf() { // Send TwostepFec from Rust sender if let Node::Rust(ref sender) = topo.sender { - sender.send_broadcast_two_step(&topo.overlay_short_id, &test_data); + sender.send_broadcast_twostep(&topo.overlay_short_id, &test_data); } // Wait for redistribution and delivery @@ -379,7 +379,7 @@ fn test_twostep_mixed_bridges_rust_leaf() { // Send TwostepFec from Rust sender if let Node::Rust(ref sender) = topo.sender { - sender.send_broadcast_two_step(&topo.overlay_short_id, &test_data); + sender.send_broadcast_twostep(&topo.overlay_short_id, &test_data); } // Check Rust leaf diff --git a/src/node/tests/test_sync/.dockerignore b/src/node/tests/test_sync/.dockerignore deleted file mode 100644 index 251ef5d..0000000 --- a/src/node/tests/test_sync/.dockerignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -npm-debug.log -*.log -.git -.gitignore -README.md -.env -.DS_Store -db/ -*.pid diff --git a/src/node/tests/test_sync/.gitignore b/src/node/tests/test_sync/.gitignore deleted file mode 100644 index ba802bb..0000000 --- a/src/node/tests/test_sync/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Node modules -node_modules/ - -# Runtime files -watcher.pid -watcher.log -server.log -db/ - -# Editor directories -.vscode/ -.idea/ diff --git a/src/node/tests/test_sync/Dockerfile b/src/node/tests/test_sync/Dockerfile deleted file mode 100644 index c312003..0000000 --- a/src/node/tests/test_sync/Dockerfile +++ /dev/null @@ -1,89 +0,0 @@ -# Multi-stage Dockerfile for test_sync (located at node/tests/test_sync/) -# Based on the main TON Node Dockerfile -# Adds Node.js-based watcher server for node management and testing - -# =================================================================== -# Stage 1: Build TON Node -# =================================================================== -FROM rust:slim AS builder - -RUN apt-get update && apt-get install -y \ - pkg-config \ - make \ - clang \ - libssl-dev \ - libzstd-dev \ - libgoogle-perftools-dev \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Copy ton-node source (from repository root, 3 levels up) -COPY . /ton-node -WORKDIR /ton-node - -# Accept build arguments for git metadata -ARG GIT_BRANCH -ARG GIT_COMMIT -ARG GIT_COMMIT_DATE - -# Pass them as environment variables to build.rs -ENV GIT_BRANCH=${GIT_BRANCH} -ENV GIT_COMMIT=${GIT_COMMIT} -ENV GIT_COMMIT_DATE=${GIT_COMMIT_DATE} - -# Build TON node binary -RUN cargo build --release --bin node - -# =================================================================== -# Stage 2: Runtime with Node.js and Watcher Server -# =================================================================== -FROM debian:stable-slim - -# Install runtime dependencies for TON node and Node.js -RUN apt-get update && apt-get install -y \ - openssl \ - libzstd1 \ - libgoogle-perftools4 \ - curl \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Install Node.js (using NodeSource repository for latest LTS) -RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ - && apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* - -# Copy TON node binary from builder stage (ะฟะตั€ะตะธะผะตะฝะพะฒะฐะฝ ะฒ ton-node) -COPY --from=builder /ton-node/target/release/node /usr/local/bin/ton-node - -# Create working directory structure -WORKDIR /watcher - - -# Copy watcher server files -COPY node/tests/test_sync/package.json ./ -COPY node/tests/test_sync/server.js ./ - -# Install Node.js dependencies (form-data for Slack upload) -RUN npm install --production - -# Create necessary directories -RUN mkdir -p /main /main/static /db /logs - -# Environment variables -ENV NODE_WATCHER_HTTP=0.0.0.0:32080 -ENV SERVER_IP=127.0.0.1 - -# Expose watcher HTTP port -EXPOSE 32080 - -# Expose TON node ports (if needed) -EXPOSE 9100 -EXPOSE 30303 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:32080/status || exit 1 - -# Start watcher server (which will manage node and run tests) -CMD ["/usr/bin/node", "server.js"] diff --git a/src/node/tests/test_sync/README.md b/src/node/tests/test_sync/README.md deleted file mode 100644 index c562f24..0000000 --- a/src/node/tests/test_sync/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# TON Node Sync Test Watcher - -This directory contains an automated sync test watcher for `ton-node`. - -The watcher is a Node.js HTTP server (`server.js`) that: -- Starts on `NODE_WATCHER_HTTP` -- Runs sync test cases automatically on startup -- Manages `ton-node` lifecycle (start/stop) -- Optionally wipes `/db` between test cases -- Produces JSON/HTML status and report endpoints -- Optionally sends a summary/report to Slack - -## Files - -- `server.js`: watcher and test orchestration logic -- `package.json`: Node.js dependencies and start script -- `Dockerfile`: containerized build/runtime for the sync test watcher - -## Test Cases - -The watcher executes tests sequentially: - -1. `Stop -> Wipe DB -> Start -> Wait for Sync` -2. `Stop -> Start -> Wait for Sync` - -Sync is considered complete when both of these log-derived ages are `< 10s`: -- `Applied master block ... Ns old` -- `Applied block ... Ns old` - -After all tests complete, the process exits with code `0`. - -## HTTP API - -Only `GET` is supported. - -- `/status` - - Returns watcher/node status, PID, uptime, sync flags, and config values. -- `/getlogs?last=N` - - Returns the last `N` lines from `/logs/node-watcher.log`. - - Default `N=100`, maximum `N=3000`. -- `/report` - - Returns an HTML report for current/last test execution. - -## Environment Variables - -Required: -- `NODE_WATCHER_HTTP` - - Listen address in `:` format (example: `0.0.0.0:32080`). - -Optional: -- `SERVER_IP` (default: `127.0.0.1`) -- `NODE_RUN_ARGS` (default: `-c /main`) -- `SYNC_TEST_NETWORK` (label in report/slack) -- `SYNC_TEST_NODE_ID` (label in report/slack) - -Slack (optional): -- `SLACK_WEBHOOK_URL` -- `SLACK_BOT_TOKEN` -- `SLACK_CHANNEL_ID` - -## Logs and Data Paths - -Inside the runtime/container, watcher expects: -- Node watcher log: `/logs/node-watcher.log` -- Node log: `/logs/output.log` -- Node DB: `/db` -- Node config base path usually under `/main` (via `NODE_RUN_ARGS`) - -## Local Run (without Docker) - -From this directory: - -```bash -npm install -NODE_WATCHER_HTTP=127.0.0.1:32080 npm start -``` - -Notes: -- `ton-node` must be available in `PATH`. -- Ensure `/db`, `/logs`, and config path referenced by `NODE_RUN_ARGS` are valid for your environment. - -## Dockerfile Usage - -This project includes a dedicated `Dockerfile` at: -- `node/tests/test_sync/Dockerfile` - -Build from repository root: - -```bash -docker build -f node/tests/test_sync/Dockerfile -t ton-sync-test:local . -``` - -Run example: - -```bash -docker run --rm \ - -p 32080:32080 \ - -e NODE_WATCHER_HTTP=0.0.0.0:32080 \ - -e SERVER_IP=127.0.0.1 \ - -e NODE_RUN_ARGS='-c /main' \ - -v $(pwd)/node/tests/test_sync/main:/main \ - -v $(pwd)/node/tests/test_sync/db:/db \ - -v $(pwd)/node/tests/test_sync/logs:/logs \ - ton-sync-test:local -``` - -Then query: - -```bash -curl http://127.0.0.1:32080/status -curl 'http://127.0.0.1:32080/getlogs?last=200' -``` - -## Behavior Notes - -- The watcher rotates `/logs/output.log` before each test case. -- `ton-node` is started detached and monitored by PID. -- On shutdown signals (`SIGINT`, `SIGTERM`), watcher tries graceful node stop and server close. diff --git a/src/node/tests/test_sync/package.json b/src/node/tests/test_sync/package.json deleted file mode 100644 index 6b56ec6..0000000 --- a/src/node/tests/test_sync/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "node-watcher", - "version": "1.0.0", - "description": "Simple HTTP server for node watcher endpoints", - "main": "server.js", - "scripts": { - "start": "node server.js" - }, - "keywords": ["http", "server", "watcher"], - "author": "", - "license": "ISC", - "dependencies": { - "form-data": "^4.0.0" - } -} diff --git a/src/node/tests/test_sync/server.js b/src/node/tests/test_sync/server.js deleted file mode 100644 index f150881..0000000 --- a/src/node/tests/test_sync/server.js +++ /dev/null @@ -1,1353 +0,0 @@ -/** - * Node Watcher HTTP Server - * - * A monitoring server for automated testing of node synchronization. - * - * Features: - * - Automatically runs test sequences on startup - * - Monitor sync status via metrics endpoint - * - View server logs - * - Generate test reports - * - Health status endpoint - * - * Environment Variables: - * - NODE_WATCHER_HTTP: Server address (e.g., 127.0.0.1:3000) - * - SERVER_IP: Metrics server IP (default: 127.0.0.1) - */ - -const http = require('http'); -const https = require('https'); -const fs = require('fs').promises; -const path = require('path'); -const { spawn } = require('child_process'); -const util = require('util'); -const execPromise = util.promisify(require('child_process').exec); - -// Simple HTML escaping helper to safely render text in HTML contexts. -// Escapes only the characters that are significant in HTML, preserving -// whitespace and newlines for use inside
 blocks.
-function escapeHtml(str) {
-  if (str === null || str === undefined) return '';
-  return String(str)
-    .replace(/&/g, '&')
-    .replace(//g, '>')
-    .replace(/"/g, '"')
-    .replace(/'/g, ''');
-}
-
-// ===================================================================
-// CONFIGURATION
-// ===================================================================
-
-const LOG_FILE = '/logs/node-watcher.log';
-const NODE_LOG_FILE = '/logs/output.log';
-const DB_PATH = '/db';
-const NODE_STOP_TIMEOUT = 60000; // 1 minute in milliseconds
-const NODE_STABILITY_CHECK_DELAY = 5000; // 5 seconds
-const SYNC_WAIT_TIMEOUT = 6 * 60 * 60 * 1000; // 6 hours in milliseconds
-const NODE_RUN_ARGS = process.env.NODE_RUN_ARGS && process.env.NODE_RUN_ARGS.trim() !== '' ? process.env.NODE_RUN_ARGS.trim().split(/\s+/) : ['-c', '/main']; // Base arguments to run the node
-
-// Get SERVER_IP from environment variable
-const SERVER_IP = process.env.SERVER_IP || '127.0.0.1';
-
-// Slack webhook URL (set via environment variables)
-const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL || '';
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || '';
-const SLACK_CHANNEL_ID = process.env.SLACK_CHANNEL_ID || '';
-
-// ===================================================================
-// GLOBAL STATE
-let FormData;
-let nodePid = null;
-let nodeStartTime = null;
-let syncCheckRunning = false;
-let syncWaiters = [];
-let isSynced = false;
-let testResults = null;
-
-class SyncTimeoutError extends Error {
-  constructor(message) {
-    super(message);
-    this.name = 'SyncTimeoutError';
-  }
-}
-
-// Global config values for network and node_id
-let GLOBAL_NETWORK = process.env.SYNC_TEST_NETWORK && process.env.SYNC_TEST_NETWORK !== '' ? process.env.SYNC_TEST_NETWORK : 'unknown-net';
-let GLOBAL_NODE_ID = process.env.SYNC_TEST_NODE_ID && process.env.SYNC_TEST_NODE_ID !== '' ? process.env.SYNC_TEST_NODE_ID : 'unknown-node';
-
-// ===================================================================
-// UTILITY FUNCTIONS
-// ===================================================================
-
-function normalizePid(pid) {
-  const pidNum = typeof pid === 'number' ? pid : Number(pid);
-  if (!Number.isInteger(pidNum) || pidNum <= 0) {
-    return null;
-  }
-  return pidNum;
-}
-
-function isProcessRunning(pid) {
-  const pidNum = normalizePid(pid);
-  if (pidNum === null) {
-    return false;
-  }
-  try {
-    process.kill(pidNum, 0);
-    return true;
-  } catch (error) {
-    return false;
-  }
-}
-
-async function stopProcessWithTimeout(pid) {
-  const pidNum = normalizePid(pid);
-  if (pidNum === null) {
-    await log(`Invalid PID '${pid}', cannot stop process`);
-    return false;
-  }
-
-  try {
-    process.kill(pidNum, 'SIGTERM');
-    await log(`Sent SIGTERM to process ${pidNum}`);
-  } catch (error) {
-    if (error && error.code === 'ESRCH') {
-      await log(`Process ${pidNum} is already stopped`);
-      return true;
-    }
-    await log(`Failed to send SIGTERM to process ${pidNum}: ${error.message}`);
-    return false;
-  }
-
-  const stopStartTime = Date.now();
-  while (Date.now() - stopStartTime < NODE_STOP_TIMEOUT) {
-    if (!isProcessRunning(pidNum)) {
-      await log(`Process ${pidNum} has stopped successfully`);
-      return true;
-    }
-    await new Promise(resolve => setTimeout(resolve, 1000));
-  }
-
-  await log(`Process ${pidNum} did not stop after ${NODE_STOP_TIMEOUT / 1000} seconds, sending SIGKILL`);
-  try {
-    process.kill(pidNum, 'SIGKILL');
-    await log(`Sent SIGKILL to process ${pidNum}`);
-  } catch (error) {
-    if (error && error.code === 'ESRCH') {
-      await log(`Process ${pidNum} exited before SIGKILL was delivered`);
-      return true;
-    }
-    await log(`Error sending SIGKILL to process ${pidNum}: ${error.message}`);
-    return false;
-  }
-
-  // Give the OS a short window to reap the process after SIGKILL.
-  const killConfirmDeadline = Date.now() + 5000;
-  while (Date.now() < killConfirmDeadline) {
-    if (!isProcessRunning(pidNum)) {
-      await log(`Process ${pidNum} has stopped after SIGKILL`);
-      return true;
-    }
-    await new Promise(resolve => setTimeout(resolve, 200));
-  }
-
-  await log(`WARNING: Process ${pidNum} is still running after SIGKILL attempt`);
-  return false;
-}
-
-// Stop all running node processes (by name)
-async function stopAllNodeProcesses() {
-  try {
-    const { stdout } = await execPromise('pgrep -f "ton-node" || true');
-    const pids = stdout.trim().split(/\s+/).filter(pid => pid);
-    if (pids.length > 0) {
-      await log(`Found ${pids.length} running node process(es). Stopping all...`);
-      for (const pidStr of pids) {
-        const pidNumber = parseInt(pidStr, 10);
-        if (!Number.isInteger(pidNumber) || pidNumber <= 0) {
-          await log(`Skipping invalid PID from pgrep output: "${pidStr}"`);
-          continue;
-        }
-        await stopProcessWithTimeout(pidNumber);
-      }
-    } else {
-      await log('No running node processes found.');
-    }
-  } catch (e) {
-    await log('Error checking/stopping node processes: ' + e.message);
-  }
-}
-
-// Analyzes logs and returns {blocksAccepted, syncSpeed}
-async function analyzeSyncLog(syncDurationSeconds) {
-  let blocksAccepted = null;
-  try {
-    const { stdout: blocksStdout } = await execPromise(`cat "${NODE_LOG_FILE}" | grep "Applied master block" | wc -l`);
-    blocksAccepted = parseInt(blocksStdout.trim(), 10);
-    if (isNaN(blocksAccepted)) blocksAccepted = 0;
-  } catch (err) {
-    await log(`Could not count applied master blocks: ${err.message}`);
-    blocksAccepted = null;
-  }
-  const syncSpeed = (blocksAccepted !== null && syncDurationSeconds > 0) ? Number((blocksAccepted / syncDurationSeconds).toFixed(2)) : null;
-  return { blocksAccepted, syncSpeed };
-}
-
-// Logging function
-async function log(message) {
-  const timestamp = new Date().toISOString();
-  const logMessage = `${timestamp} - ${message}\n`;
-  
-  // Write to file
-  try {
-    await fs.appendFile(LOG_FILE, logMessage);
-  } catch (err) {
-    console.error('Failed to write to log file:', err);
-  }
-  
-  // Also output to console
-  console.log(logMessage.trim());
-}
-
-// Helper function to send JSON response
-function sendJsonResponse(res, statusCode, data) {
-  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
-  res.end(JSON.stringify(data));
-}
-
-// Helper function to get uptime in seconds
-function getUptime() {
-  if (!nodeStartTime) return null;
-  return Math.round((Date.now() - nodeStartTime) / 1000);
-}
-
-// ===================================================================
-// SERVER CONFIGURATION
-// ===================================================================
-
-// Parse the NODE_WATCHER_HTTP environment variable
-function parseAddress() {
-  const address = process.env.NODE_WATCHER_HTTP;
-  
-  if (!address) {
-    throw new Error('NODE_WATCHER_HTTP environment variable is not set. Example: NODE_WATCHER_HTTP=127.0.0.1:3000');
-  }
-  
-  const [host, port] = address.split(':');
-  
-  if (!host || !port) {
-    throw new Error('Invalid NODE_WATCHER_HTTP format. Expected: :. Example: NODE_WATCHER_HTTP=127.0.0.1:3000');
-  }
-  
-  const portNum = parseInt(port, 10);
-  
-  if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
-    throw new Error('Invalid port number in NODE_WATCHER_HTTP');
-  }
-  
-  return { host, port: portNum };
-}
-
-// ===================================================================
-// HTTP ENDPOINT HANDLERS
-// ===================================================================
-
-// Handler functions for each endpoint
-// IMPORTANT: These handlers provide read-only access to monitoring data
-// The server runs tests automatically on startup
-
-async function handleLogs(req, res, queryParams) {
-  await log(`Received /getlogs request${queryParams.size > 0 ? ` with params: ${JSON.stringify(Object.fromEntries(queryParams))}` : ''}`);
-  
-  try {
-    // Get the 'last' parameter, default to 100, maximum 3000
-    const lastParam = queryParams.get('last');
-    let last = lastParam ? parseInt(lastParam, 10) : 100;
-    
-    // Validate the parameter
-    if (isNaN(last) || last < 1) {
-      sendJsonResponse(res, 400, { 
-        status: 'error', 
-        message: 'Invalid "last" parameter. Must be a positive number.',
-        endpoint: '/getlogs'
-      });
-      return;
-    }
-    
-    // Limit to maximum 3000 lines
-    if (last > 3000) {
-      last = 3000;
-    }
-    
-    // Check if log file exists
-    try {
-      await fs.access(LOG_FILE);
-    } catch (err) {
-      sendJsonResponse(res, 200, { 
-        status: 'success', 
-        message: 'No logs available',
-        lines: [],
-        count: 0,
-        endpoint: '/getlogs'
-      });
-      return;
-    }
-    
-    // Use tail command to read log file efficiently
-    let lines;
-    let totalLines = null;
-    
-    try {
-      // Return last N lines using tail (max 3000), with a 5s timeout
-      const { stdout } = await execPromise(`timeout 5 tail -n ${last} "${LOG_FILE}"`);
-      lines = stdout.split('\n').filter(line => line.trim() !== '');
-      // Get total line count efficiently for reporting (timeout 5s)
-      const { stdout: wcOutput } = await execPromise(`timeout 5 wc -l < "${LOG_FILE}"`);
-      totalLines = parseInt(wcOutput.trim(), 10);
-      if (isNaN(totalLines)) totalLines = lines.length;
-    } catch (error) {
-      await log(`Error reading log file with tail: ${error.message}`);
-      sendJsonResponse(res, 500, { 
-        status: 'error', 
-        message: `Failed to read log file: ${error.message}`,
-        endpoint: '/getlogs'
-      });
-      return;
-    }
-    
-    sendJsonResponse(res, 200, { 
-      status: 'success', 
-      message: `Retrieved ${lines.length} log lines`,
-      lines: lines,
-      count: lines.length,
-      total: totalLines,
-      endpoint: '/getlogs'
-    });
-  } catch (error) {
-    await log(`Error in /getlogs handler: ${error.message}`);
-    sendJsonResponse(res, 500, { 
-      status: 'error', 
-      message: error.message,
-      endpoint: '/getlogs'
-    });
-  }
-}
-
-async function handleReport(req, res, queryParams) {
-  await log('Received /report request');
-  
-  try {
-    if (!testResults) {
-      sendJsonResponse(res, 200, {
-        status: 'success',
-        message: 'No test results available. Run tests first.',
-        endpoint: '/report'
-      });
-      return;
-    }
-    
-    // Generate HTML report
-    const html = generateHtmlReport(testResults);
-    
-    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
-    res.end(html);
-  } catch (error) {
-    await log(`Error in /report handler: ${error.message}`);
-    sendJsonResponse(res, 500, { 
-      status: 'error', 
-      message: error.message,
-      endpoint: '/report'
-    });
-  }
-}
-
-function generateHtmlReport(results) {
-  const isRunning = results.endTime === null;
-  const totalDuration = isRunning ? Math.round((Date.now() - results.startTime) / 1000) : Math.round((results.endTime - results.startTime) / 1000);
-  const successCount = results.cases.filter(c => c.status === 'SUCCESS').length;
-  const failedCount = results.cases.filter(c => c.status === 'FAILED').length;
-  const completedCount = successCount + failedCount;
-  const passRate = completedCount > 0 ? ((successCount / completedCount) * 100).toFixed(1) : '0.0';
-  
-  let statusColor, statusText;
-  if (isRunning) {
-    statusColor = '#f59e0b'; // Orange/amber color for in progress
-    statusText = 'IN PROGRESS';
-  } else if (results.cases.length === 0) {
-    statusColor = '#64748b'; // Gray for no tests
-    statusText = 'NO TESTS';
-  } else if (failedCount === 0) {
-    statusColor = '#10b981'; // Green for all passed
-    statusText = 'ALL PASSED';
-  } else {
-    statusColor = '#ef4444'; // Red for failures
-    statusText = `${failedCount} FAILED`;
-  }
-  
-  return `
-
-
-  
-  
-  Test Report - Node Watcher
-  
-
-
-  
-
-

๐Ÿงช Test Execution Report

-

Node Watcher Automated Tests

-
- Network: ${encodeURIComponent(GLOBAL_NETWORK)} - Node ID: ${encodeURIComponent(GLOBAL_NODE_ID)} -
-
-
-
-

Status

-
${statusText}
-
-
-

Pass Rate

-
${passRate}%
-
-
-

Total Tests

-
${isRunning ? `${completedCount}/${results.cases.length}` : results.cases.length}
-
-
-

Duration

-
${totalDuration}s
-
-
-
- ${results.cases.map((testCase, index) => { - const isTestRunning = testCase.endTime === null || testCase.status === 'RUNNING'; - const displayStatus = isTestRunning ? 'RUNNING' : testCase.status; - const currentDuration = isTestRunning ? Math.round((Date.now() - testCase.startTime) / 1000) : testCase.duration; - return ` -
-
-
Test Case ${index + 1}: ${encodeURIComponent(testCase.name)}
- ${displayStatus} -
-
-
- Duration - ${currentDuration}s${isTestRunning ? ' (ongoing)' : ''} -
-
- Started - ${new Date(testCase.startTime).toLocaleTimeString()} -
-
- Ended - ${isTestRunning ? 'In progress...' : new Date(testCase.endTime).toLocaleTimeString()} -
-
- Blocks Accepted - ${typeof testCase.blocksAccepted === 'number' && testCase.blocksAccepted !== null ? testCase.blocksAccepted : 'N/A'} -
-
- Sync Speed - ${typeof testCase.syncSpeed === 'number' && testCase.syncSpeed !== null ? testCase.syncSpeed + ' blocks/s' : 'N/A'} -
-
- ${testCase.error ? ` -
- โŒ Error: -
${escapeHtml(String(testCase.error))}
-
- ` : ''} -
- `; - }).join('')} -
-
- Generated at ${new Date(isRunning ? Date.now() : results.endTime).toLocaleString()}${isRunning ? ' (report refreshes on reload)' : ''} -
-
- -`; -} - -async function handleStatus(req, res, queryParams) { - await log('Received /status request'); - - try { - const status = { - status: 'success', - node: { - pid: nodePid, - running: nodePid !== null, - startTime: nodeStartTime, - uptime: getUptime(), - synced: isSynced, - syncChecking: syncCheckRunning - }, - server: { - serverIp: SERVER_IP, - dbPath: DB_PATH, - stopTimeout: NODE_STOP_TIMEOUT / 1000, - stabilityCheckDelay: NODE_STABILITY_CHECK_DELAY / 1000 - }, - endpoint: '/status' - }; - - sendJsonResponse(res, 200, status); - } catch (error) { - await log(`Error in /status handler: ${error.message}`); - sendJsonResponse(res, 500, { - status: 'error', - message: error.message, - endpoint: '/status' - }); - } -} - -async function handleNotFound(req, res) { - sendJsonResponse(res, 404, { - status: 'error', - message: 'Endpoint not found', - path: req.url - }); -} - -async function handleMethodNotAllowed(req, res) { - res.writeHead(405, { - 'Content-Type': 'application/json', - 'Allow': 'GET' - }); - res.end(JSON.stringify({ - status: 'error', - message: 'Method not allowed. Only GET requests are supported.', - method: req.method - })); -} - -// =================================================================== -// REQUEST ROUTING -// =================================================================== - -// Request handler with async/await -async function handleRequest(req, res) { - try { - // Check if the method is GET - if (req.method !== 'GET') { - await handleMethodNotAllowed(req, res); - return; - } - - // Parse URL to extract pathname and query parameters - const parsedUrl = new URL(req.url, `http://${req.headers.host}`); - const pathname = parsedUrl.pathname; - const queryParams = parsedUrl.searchParams; - - // Route the request based on the pathname - switch (pathname) { - case '/getlogs': - await handleLogs(req, res, queryParams); - break; - case '/status': - await handleStatus(req, res, queryParams); - break; - case '/report': - await handleReport(req, res, queryParams); - break; - default: - await handleNotFound(req, res); - break; - } - } catch (error) { - await log(`Error handling request: ${error && error.stack ? error.stack : error}`); - console.error('Error handling request:', error); - if (!res.headersSent) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - status: 'error', - message: 'Internal server error' - })); - } - } -} - -// Create the HTTP server -const server = http.createServer((req, res) => { - handleRequest(req, res); -}); - -// Start the server with async/await -async function startServer() { - const { host, port } = parseAddress(); - // Load global labels before starting server - await log(`Loaded global labels: network='${GLOBAL_NETWORK}', node_id='${GLOBAL_NODE_ID}'`); - return new Promise((resolve, reject) => { - server.listen(port, host, async () => { - await log(`Server is running on http://${host}:${port}`); - console.log('Available endpoints:'); - console.log(` - GET http://${host}:${port}/getlogs?last=N`); - console.log(` - GET http://${host}:${port}/status`); - console.log(` - GET http://${host}:${port}/report`); - // Start automated tests - await log('Starting automated tests...'); - runAllTests().catch(async (error) => { - await log(`Automated test execution failed: ${error.message}`); - }); - resolve(); - }); - server.on('error', (err) => { - if (err.code === 'EADDRINUSE') { - console.error(`ERROR: Port ${port} is already in use`); - } else if (err.code === 'EADDRNOTAVAIL') { - console.error(`ERROR: Address ${host} is not available`); - } else { - console.error('ERROR:', err.message); - } - reject(err); - }); - }); -} - -// Graceful shutdown handler for HTTP server and node process -async function shutdown(signal) { - await log(`${signal} received, shutting down HTTP server and node process gracefully`); - - // Stop the node process if running - await stopNode(); - - return new Promise((resolve) => { - server.close(async () => { - await log('HTTP server closed'); - resolve(); - }); - }); -} - - -// =================================================================== -// NODE LOG ROTATION -// =================================================================== - -// Rotate node log file: if exists, move to _N where N is next available -async function rotateNodeLog() { - try { - // Check if log file exists - await fs.access(NODE_LOG_FILE); - } catch (e) { - await log(`Node log file not found, nothing to rotate: ${NODE_LOG_FILE}`); - return; - } - // Find next available suffix N, insert before extension - const parsed = path.parse(NODE_LOG_FILE); - let n = 1; - let nextLog; - while (true) { - nextLog = path.join(parsed.dir, `${parsed.name}_${n}${parsed.ext}`); - try { - await fs.access(nextLog); - n++; - } catch (e) { - break; - } - } - // Move the log file - await fs.rename(NODE_LOG_FILE, nextLog); - await log(`Node log rotated: ${NODE_LOG_FILE} -> ${nextLog}`); -} - -// =================================================================== -// NODE PROCESS MANAGEMENT -// =================================================================== - -// Function to start the node process if not already running -async function startNode() { - try { - await log('Starting node process check...'); - - // Check if node process is already running - const { stdout } = await execPromise('pgrep -f "ton-node" || true'); - const pids = stdout.trim().split(/\s+/).filter(pid => pid); - - if (pids.length > 0) { - // Found existing node process(es) - const pid = pids[0]; // Use the first one - await log(`Found existing node process with PID: ${pid}`); - - // Save PID to global variable - nodePid = parseInt(pid, 10); - nodeStartTime = Date.now(); - await log(`PID ${pid} saved to global variable`); - return; - } - - // No existing node process found - await log('No existing node process found, starting new one...'); - - - - // No existing node process, start a new one - const nodeProcess = spawn('ton-node', NODE_RUN_ARGS, { - detached: true - }); - nodeStartTime = Date.now(); - const pid = nodeProcess.pid; - // Set nodePid BEFORE exit handler to avoid race condition - nodePid = pid; - nodeProcess.on('exit', (code, signal) => { - log(`Node process exited with code ${code}, signal ${signal}`); - nodePid = null; - nodeStartTime = null; - syncCheckRunning = false; - isSynced = false; - }); - // Detach the process so it continues running independently - nodeProcess.unref(); - await log(`Started new node process with PID: ${pid}`); - await log(`Waiting ${NODE_STABILITY_CHECK_DELAY / 1000} seconds to verify process stability...`); - await new Promise(resolve => setTimeout(resolve, NODE_STABILITY_CHECK_DELAY)); - try { - process.kill(pid, 0); - await log(`Process ${pid} is still running after ${NODE_STABILITY_CHECK_DELAY / 1000} seconds`); - await log(`PID ${pid} confirmed in global variable`); - } catch (error) { - await log(`ERROR: Process ${pid} is no longer running after ${NODE_STABILITY_CHECK_DELAY / 1000} seconds`); - await log('Node process failed to start or crashed immediately'); - try { - const { stdout: nodeLogs } = await execPromise(`timeout 5 tail -n 100 "${NODE_LOG_FILE}"`); - if (nodeLogs.trim()) { - await log('=== Last 100 lines of node log ==='); - const logLines = nodeLogs.trim().split('\n'); - for (const line of logLines) { - await log(`[NODE] ${line}`); - } - await log('=== End of node log ==='); - } else { - await log('Node log file is empty'); - } - } catch (logError) { - await log(`Could not read node logs: ${logError.message}`); - } - nodePid = null; - nodeStartTime = null; - throw new Error(`Node process failed to start - exited within ${NODE_STABILITY_CHECK_DELAY / 1000} seconds`); - } - - } catch (error) { - await log(`Error in startNode: ${error.message}`); - throw error; - } -} - - -// Wait for node to sync by polling the log -async function waitForSync() { - if (!nodePid) { - syncCheckRunning = false; - isSynced = false; - await log('WARNING: No node process running, cannot wait for sync'); - return; - } - - syncCheckRunning = true; - isSynced = false; - await log(`Waiting for node to sync (log polling, timeout ${Math.round(SYNC_WAIT_TIMEOUT / 3600000)}h)...`); - - try { - let prevMasterAge = null; - let prevShardAge = null; - let lastProgressAt = Date.now(); - while (true) { - // Read last "Applied master block" and "Applied block" lines - let masterLine = null, shardLine = null; - try { - const { stdout: masterStdout } = await execPromise(`grep 'Applied master block' "${NODE_LOG_FILE}" | tail -n 1`); - masterLine = masterStdout.trim(); - } catch {} - try { - const { stdout: shardStdout } = await execPromise(`grep 'Applied block' "${NODE_LOG_FILE}" | tail -n 1`); - shardLine = shardStdout.trim(); - } catch {} - - // Parse "Ns old" from both lines - function parseAge(line) { - if (!line) return null; - const m = line.match(/(\d+)s old/); - return m ? parseInt(m[1], 10) : null; - } - const masterAge = parseAge(masterLine); - const shardAge = parseAge(shardLine); - - // If both values < 10, consider sync complete - if (masterAge !== null && shardAge !== null && masterAge < 10 && shardAge < 10) { - isSynced = true; - await log(`Sync complete: masterAge=${masterAge}, shardAge=${shardAge}`); - return; - } - - // Check progress: if at least one value decreased, reset lastProgressAt - if ( - (prevMasterAge !== null && masterAge !== null && masterAge < prevMasterAge) || - (prevShardAge !== null && shardAge !== null && shardAge < prevShardAge) - ) { - lastProgressAt = Date.now(); - } - - // Save current values for the next iteration - prevMasterAge = masterAge; - prevShardAge = shardAge; - - // Timeout only if there is no progress - const elapsed = Date.now() - lastProgressAt; - if (elapsed >= SYNC_WAIT_TIMEOUT) { - const timeoutSeconds = Math.round(elapsed / 1000); - const timeoutError = new SyncTimeoutError(`Sync timeout exceeded after ${timeoutSeconds} seconds (no progress)`); - await log(`ERROR: ${timeoutError.message}`); - await stopNode(); - throw timeoutError; - } - - await new Promise(r => setTimeout(r, 2000)); // Wait before next attempt - } - } finally { - syncCheckRunning = false; - } -} - -// =================================================================== -// TEST CASES -// =================================================================== - -// Test Case 1: Stop node, wipe DB, start node, wait for sync -async function testCase1() { - await log('========================================'); - await log('=== TEST CASE 1 START ==='); - await log('=== Stop -> Wipe DB -> Start -> Wait for Sync ==='); - await log('========================================'); - const caseStartTime = Date.now(); - let caseEndTime = null; - let caseDuration = null; - let blocksAccepted = null, syncSpeed = null; - let error = null; - - try { - if(nodePid) { - await stopNode(); - await log('Test Case 1: Node stopped'); - } - // Rotate node log before starting node - await rotateNodeLog(); - await cleanDb(); - await log('Test Case 1: Database wiped'); - await startNode(); - await log('Test Case 1: Node started, waiting for sync...'); - await waitForSync(); - caseEndTime = Date.now(); - caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); - try { - const logStats = await analyzeSyncLog(caseDuration); - blocksAccepted = logStats.blocksAccepted; - syncSpeed = logStats.syncSpeed; - await log(`Test Case 1: Blocks accepted=${blocksAccepted}, speed=${syncSpeed} blocks/s`); - } catch (e) { - await log(`Test Case 1: Failed to analyze log: ${e.message}`); - } - await log('========================================'); - await log(`=== TEST CASE 1 STOP ===`); - await log(`=== Duration: ${caseDuration} seconds ===`); - await log(`=== Status: SUCCESS ===`); - await log('========================================'); - } catch (err) { - error = err; - if (err instanceof SyncTimeoutError) { - await log(`Test Case 1: Timeout exceeded: ${err.message}`); - } - caseEndTime = Date.now(); - caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); - await log('========================================'); - await log(`=== TEST CASE 1 STOP ===`); - await log(`=== Status: FAILED - ${err.message}`); - await log('========================================'); - } - - return { - name: 'Stop -> Wipe DB -> Start -> Wait for Sync', - startTime: caseStartTime, - endTime: caseEndTime, - duration: caseDuration, - status: error ? 'FAILED' : 'SUCCESS', - error: error ? error.message : null, - blocksAccepted, - syncSpeed - }; -} - -// Test Case 2: Stop node, start node, wait for sync -async function testCase2() { - await log('========================================'); - await log('=== TEST CASE 2 START ==='); - await log('=== Stop -> Start -> Wait for Sync ==='); - await log('========================================'); - const caseStartTime = Date.now(); - let caseEndTime = null; - let caseDuration = null; - let blocksAccepted = null, syncSpeed = null; - let error = null; - - try { - if (nodePid) { - await stopNode(); - await log('Test Case 2: Node stopped'); - } - // Rotate node log before starting node - await rotateNodeLog(); - await startNode(); - await log('Test Case 2: Node started, waiting for sync...'); - await waitForSync(); - caseEndTime = Date.now(); - caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); - try { - const logStats = await analyzeSyncLog(caseDuration); - blocksAccepted = logStats.blocksAccepted; - syncSpeed = logStats.syncSpeed; - await log(`Test Case 2: Blocks accepted=${blocksAccepted}, speed=${syncSpeed} blocks/s`); - } catch (e) { - await log(`Test Case 2: Failed to analyze log: ${e.message}`); - } - await log('========================================'); - await log(`=== TEST CASE 2 STOP ===`); - await log(`=== Duration: ${caseDuration} seconds ===`); - await log(`=== Status: SUCCESS ===`); - await log('========================================'); - } catch (err) { - error = err; - if (err instanceof SyncTimeoutError) { - await log(`Test Case 2: Timeout exceeded: ${err.message}`); - } - caseEndTime = Date.now(); - caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); - await log('========================================'); - await log(`=== TEST CASE 2 STOP ===`); - await log(`=== Status: FAILED - ${err.message}`); - await log('========================================'); - } - - // Stop node after test case completes - await stopNode(); - await log('Test Case 2: Node stopped'); - - return { - name: 'Stop -> Start -> Wait for Sync', - startTime: caseStartTime, - endTime: caseEndTime, - duration: caseDuration, - status: error ? 'FAILED' : 'SUCCESS', - error: error ? error.message : null, - blocksAccepted, - syncSpeed - }; -} - -// Run all test cases in sequence (one-by-one) -async function runAllTests() { - await log('====== Starting All Test Cases (Sequential Execution) ======'); - const overallStartTime = Date.now(); - - // Reset test results for new run - testResults = { - startTime: overallStartTime, - endTime: null, - cases: [] - }; - - try { - // Add placeholder for Case 1 - testResults.cases.push({ - name: 'Stop -> Wipe DB -> Start -> Wait for Sync', - startTime: Date.now(), - endTime: null, - duration: 0, - status: 'RUNNING', - error: null - }); - - // Execute Case 1 - const result1 = await testCase1(); - testResults.cases[0] = result1; // Update with actual result - await log('>>> Proceeding to Test Case 2...'); - - // Add placeholder for Case 2 - testResults.cases.push({ - name: 'Stop -> Start -> Wait for Sync', - startTime: Date.now(), - endTime: null, - duration: 0, - status: 'RUNNING', - error: null - }); - - // Execute Case 2 - const result2 = await testCase2(); - testResults.cases[1] = result2; // Update with actual result - - const totalTime = Math.round((Date.now() - overallStartTime) / 1000); - testResults.endTime = Date.now(); - - await log(`====== All Test Cases Completed in ${totalTime} seconds ======`); - await log(`Report available at: /report`); - // Send report to Slack - try { - await sendSlackReport(testResults); - await log('Slack report sent'); - } catch (e) { - await log('Failed to send Slack report: ' + e.message); - } - } catch (error) { - testResults.endTime = Date.now(); - await log(`====== Test execution failed: ${error.message} ======`); - } - // Only exit if all tests succeeded - const allPassed = testResults.cases.every(c => c.status === 'SUCCESS'); - if (allPassed) { - await log('All tests complete, exiting...'); - process.exit(0); - } else { - await log('All tests complete, but some tests failed. Server will remain running for investigation.'); - // Ensure all node processes are stopped - await stopAllNodeProcesses(); - // Do not exit, keep server running - } -} - -// Builds and sends a report to Slack -async function sendSlackReport(results) { - // Short summary message - const case1 = results.cases[0]; - const case2 = results.cases[1]; - const msg = `Node: ${GLOBAL_NODE_ID}\nNetwork: ${GLOBAL_NETWORK}\nCase 1: ${case1.status === 'SUCCESS' ? 'success' : 'fail'} in ${case1.duration} seconds\nCase 2: ${case2.status === 'SUCCESS' ? 'success' : 'fail'} in ${case2.duration} seconds`; - - let fileSent = false; - let fileError = null; - // Try to send HTML report as file if both token and channel are set - if (SLACK_BOT_TOKEN && SLACK_CHANNEL_ID) { - try { - if (!FormData) FormData = require('form-data'); - const html = generateHtmlReport(results); - const tmpPath = `/tmp/node_report_${Date.now()}.html`; - await fs.writeFile(tmpPath, html, 'utf8'); - await uploadFileToSlack(tmpPath, 'Node Test Report', msg); - await fs.unlink(tmpPath).catch(() => {}); - fileSent = true; - } catch (e) { - fileError = e; - await log('Slack file upload failed: ' + e.message); - } - } - - // If file not sent, send message to channel (webhook or chat.postMessage) - if (!fileSent) { - // Prefer webhook if set - if (SLACK_WEBHOOK_URL) { - const payload = { text: msg }; - await postToSlack(payload); - } else if (SLACK_BOT_TOKEN && SLACK_CHANNEL_ID) { - // Fallback: use chat.postMessage - await sendSlackTextMessage(SLACK_CHANNEL_ID, msg); - } else { - await log('No Slack credentials for sending message'); - } - } -} -// Send a plain text message to a Slack channel using chat.postMessage -async function sendSlackTextMessage(channel, text) { - const payload = JSON.stringify({ channel, text }); - const options = { - method: 'POST', - hostname: 'slack.com', - path: '/api/chat.postMessage', - headers: { - 'Authorization': `Bearer ${SLACK_BOT_TOKEN}`, - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload) - } - }; - await new Promise((resolve, reject) => { - const req = https.request(options, (res) => { - let data = ''; - res.on('data', chunk => { data += chunk; }); - res.on('end', async () => { - try { - const json = JSON.parse(data); - await log(`Slack chat.postMessage response: ${data}`); - if (json.ok) resolve(); - else reject(new Error('Slack chat.postMessage error: ' + (json.error || data))); - } catch (e) { reject(e); } - }); - }); - req.on('error', reject); - req.write(payload); - req.end(); - }); -} - -// Upload file to Slack using files.upload API -async function uploadFileToSlack(filePath, title, initialComment) { - // Step 1: Get upload URL and file_id - const stat = require('fs').statSync(filePath); - const fileSize = stat.size; - const fileName = 'report.html'; - const getUrlForm = new (require('form-data'))(); - getUrlForm.append('filename', fileName); - getUrlForm.append('length', fileSize.toString()); - const getUrlOptions = { - method: 'POST', - headers: { - 'Authorization': `Bearer ${SLACK_BOT_TOKEN}`, - ...getUrlForm.getHeaders() - } - }; - const uploadUrlResp = await new Promise((resolve, reject) => { - const req = https.request('https://slack.com/api/files.getUploadURLExternal', getUrlOptions, (res) => { - let data = ''; - res.on('data', chunk => { data += chunk; }); - res.on('end', () => { - try { - const json = JSON.parse(data); - if (json.ok && json.upload_url && json.file_id) resolve(json); - else reject(new Error('Slack getUploadURLExternal error: ' + (json.error || data))); - } catch (e) { reject(e); } - }); - }); - req.on('error', reject); - getUrlForm.pipe(req); - }); - const { upload_url, file_id } = uploadUrlResp; - - // Step 2: Upload file binary to upload_url - const fileBuffer = require('fs').readFileSync(filePath); - await new Promise((resolve, reject) => { - const url = new URL(upload_url); - const options = { - method: 'POST', - hostname: url.hostname, - path: url.pathname + url.search, - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Length': fileBuffer.length - } - }; - const req = https.request(options, (res) => { - let data = ''; - res.on('data', chunk => { data += chunk; }); - res.on('end', async () => { - if (res.statusCode >= 200 && res.statusCode < 300) resolve(); - else reject(new Error('Slack upload_url HTTP error: ' + res.statusCode + ' ' + data)); - }); - }); - req.on('error', reject); - req.write(fileBuffer); - req.end(); - }); - - // Step 3: Complete upload and share in channel - // Find a default channel from env or fallback - const channel = SLACK_CHANNEL_ID || null; - if (!channel) { - await log('No SLACK_CHANNEL_ID set, file will not be shared in a channel'); - return; - } - const completeForm = new (require('form-data'))(); - completeForm.append('files', JSON.stringify([{ id: file_id, title: title || fileName }])); - completeForm.append('channel_id', channel); - if (initialComment) completeForm.append('initial_comment', initialComment); - const completeOptions = { - method: 'POST', - headers: { - 'Authorization': `Bearer ${SLACK_BOT_TOKEN}`, - ...completeForm.getHeaders() - } - }; - await new Promise((resolve, reject) => { - const req = https.request('https://slack.com/api/files.completeUploadExternal', completeOptions, (res) => { - let data = ''; - res.on('data', chunk => { data += chunk; }); - res.on('end', () => { - try { - const json = JSON.parse(data); - if (json.ok) resolve(); - else reject(new Error('Slack completeUploadExternal error: ' + (json.error || data))); - } catch (e) { reject(e); } - }); - }); - req.on('error', reject); - completeForm.pipe(req); - }); -} - -function postToSlack(payload) { - return new Promise((resolve, reject) => { - const url = new URL(SLACK_WEBHOOK_URL); - const data = JSON.stringify(payload); - const options = { - hostname: url.hostname, - path: url.pathname + url.search, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(data) - } - }; - const req = https.request(options, (res) => { - let body = ''; - res.on('data', (chunk) => { body += chunk; }); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) resolve(); - else reject(new Error('Slack error: ' + res.statusCode + ' ' + body)); - }); - }); - req.on('error', reject); - req.write(data); - req.end(); - }); -} - -// =================================================================== -// NODE LIFECYCLE FUNCTIONS -// =================================================================== - -// Stop node process if it's running -async function stopNode() { - if (!nodePid) { - await log('No node process is currently running'); - return; - } - - try { - const savedPid = nodePid; - await log(`Attempting to stop node process with PID: ${savedPid}`); - - // Stop sync checking and reject waiting promises - syncCheckRunning = false; - isSynced = false; - - // Reject all waiting promises before clearing - const waitersToReject = [...syncWaiters]; - syncWaiters = []; - waitersToReject.forEach(resolve => { - // Resolve instead of reject to avoid unhandled rejections - // The waiters will just get unblocked when node stops - resolve(); - }); - - await stopProcessWithTimeout(savedPid); - nodePid = null; - nodeStartTime = null; - } catch (error) { - await log(`Error stopping node process: ${error.message}`); - nodePid = null; - nodeStartTime = null; - } -} - -// =================================================================== -// DATABASE MANAGEMENT -// =================================================================== - -// Clean the database folder contents (keep the folder itself) -async function cleanDb() { - try { - await log(`Cleaning database contents at path: ${DB_PATH}`); - - // Check if the db directory exists - try { - await fs.access(DB_PATH); - } catch (err) { - await log(`Database directory does not exist, nothing to clean`); - return; - } - - // Get all items in the directory - const items = await fs.readdir(DB_PATH); - - if (items.length === 0) { - await log(`Database directory is already empty`); - return; - } - - await log(`Found ${items.length} items to delete`); - - // Delete each item in the directory (but keep the directory itself) - for (const item of items) { - const itemPath = path.join(DB_PATH, item); - await fs.rm(itemPath, { recursive: true, force: true }); - } - - await log(`Database directory contents cleaned successfully`); - } catch (error) { - await log(`Error cleaning database: ${error.message}`); - } -} - -// =================================================================== -// SIGNAL HANDLERS & STARTUP -// =================================================================== - -// Signal handlers for graceful shutdown -process.on('SIGTERM', async () => { - await shutdown('SIGTERM'); - process.exit(0); -}); - -process.on('SIGINT', async () => { - await shutdown('SIGINT'); - process.exit(0); -}); - -// Handle uncaught errors -process.on('uncaughtException', async (error) => { - await log(`Uncaught exception: ${error && error.stack ? error.stack : error}`); - console.error('Uncaught exception:', error); - await shutdown('UNCAUGHT_EXCEPTION'); - process.exit(0); -}); - -process.on('unhandledRejection', async (reason, promise) => { - await log(`Unhandled rejection at: ${promise}, reason: ${reason}`); - console.error('Unhandled rejection:', reason); -}); - -// Start the server -startServer().catch(async (err) => { - await log(`Failed to start server: ${err && err.stack ? err.stack : err}`); - process.exit(0); -}); diff --git a/src/node/validator-session/src/session.rs b/src/node/validator-session/src/session.rs index a70caa9..e1ad2c0 100644 --- a/src/node/validator-session/src/session.rs +++ b/src/node/validator-session/src/session.rs @@ -358,6 +358,7 @@ impl CatchainOverlay for LoopbackOverlay { _sender_id: &PublicKeyHash, _send_as: &PublicKeyHash, _payload: BlockPayloadPtr, + _extra: Option>, ) { // no need to send broadcast to itself /*if let Some(listener) = self.listener.upgrade() { @@ -439,6 +440,10 @@ pub(crate) struct SessionImpl { */ impl consensus_common::Session for SessionImpl { + fn start(&self, _initial_block_seqno: u32) { + log::trace!("CatchainSession::start() called (no-op for catchain)"); + } + fn stop(&self) { self.stop_impl(false); // Stop without destroying DB (preserve for recovery) } diff --git a/src/tl/ton_api/tl/ton_api.tl b/src/tl/ton_api/tl/ton_api.tl index b60f263..e6f0d6d 100644 --- a/src/tl/ton_api/tl/ton_api.tl +++ b/src/tl/ton_api/tl/ton_api.tl @@ -269,11 +269,11 @@ overlay.broadcastFecShort src:PublicKey certificate:overlay.Certificate broadcas overlay.broadcastNotFound = overlay.Broadcast; overlay.broadcastStream data:overlay.broadcast trace:(vector int256) = overlay.Broadcast; -overlay.broadcastTwostepSimple flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data:bytes signature:bytes = overlay.Broadcast; -overlay.broadcastTwostepFec flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data_hash:int256 data_size:int seqno:int part:bytes signature:bytes = overlay.Broadcast; -overlay.broadcastTwostep.id flags:int date:int src:int256 src_adnl_id:int256 data_hash:int256 part_size:int = overlay.broadcastTwostep.Id; +overlay.broadcastTwostepSimple flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data:bytes extra:bytes signature:bytes = overlay.Broadcast; +overlay.broadcastTwostepFec flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data_hash:int256 data_size:int seqno:int part:bytes extra:bytes signature:bytes = overlay.Broadcast; +overlay.broadcastTwostep.id flags:int date:int src:int256 src_adnl_id:int256 data_hash:int256 data_size:int part_size:int extra:bytes = overlay.broadcastTwostep.Id; overlay.broadcastTwostepSimple.toSign id:int256 data:bytes = overlay.broadcastTwostepSimple.ToSign; -overlay.broadcastTwostepFec.toSign id:int256 data_size:int seqno:int part:bytes = overlay.broadcastTwostepFec.ToSign; +overlay.broadcastTwostepFec.toSign id:int256 seqno:int part:bytes = overlay.broadcastTwostepFec.ToSign; ---functions--- @@ -1091,6 +1091,8 @@ consensus.candidateHashDataEmpty block:tonNode.blockIdExt parent:consensus.candi consensus.block slot:int parent:consensus.CandidateParent candidate:bytes signature:bytes = consensus.CandidateData; consensus.empty slot:int parent:consensus.CandidateId block:tonNode.blockIdExt signature:bytes = consensus.CandidateData; +consensus.broadcastExtra slot:int = consensus.BroadcastExtra; + // Simplex consensus votes consensus.requestError = consensus.RequestError; From 2ad771df62b0d698a3ff2d308b7ff2eb99455ac3 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Mon, 30 Mar 2026 22:31:02 +0300 Subject: [PATCH 16/48] Fix formatting --- src/node/src/validator/collator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index bf988c5..a502254 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -3897,7 +3897,7 @@ impl Collator { let account = shard_acc.account(); if let Some(storage_dict) = shard_acc.storage_dict() { if account.dict_hash().is_some() { - let size = account.storage_info().map(|info| info.used().cells()).unwrap_or(0); + let size = account.storage_info().map_or(0, |info| info.used().cells()); log::trace!( "{}: updated storage dict with hash {:x} for account {:x} of size {}", self.collated_block_descr, From f0454ee866897944efeafb3981d3e3d2b0b73226 Mon Sep 17 00:00:00 2001 From: Slava Date: Wed, 1 Apr 2026 14:52:48 +0300 Subject: [PATCH 17/48] Additional updates on Simplex --- src/node/simplex/src/session_processor.rs | 88 ++++++++- src/node/simplex/src/simplex_state.rs | 59 +++++-- .../src/tests/test_session_processor.rs | 167 ++++++++++++++++-- .../simplex/src/tests/test_simplex_state.rs | 122 +++++++++++++ 4 files changed, 394 insertions(+), 42 deletions(-) diff --git a/src/node/simplex/src/session_processor.rs b/src/node/simplex/src/session_processor.rs index 6a3bd8f..83a8b4c 100644 --- a/src/node/simplex/src/session_processor.rs +++ b/src/node/simplex/src/session_processor.rs @@ -5996,7 +5996,9 @@ impl SessionProcessor { } } - // Empty blocks don't need validation (C++ skips validation for empty blocks) + // Empty blocks skip ValidatorGroup validation but still need FSM-tip reference + // check (performed in try_approve_block). C++ block-validator.cpp rejects unless + // block == event->state->as_normal(). if pending.raw_candidate.block.is_empty() { to_validate.push(( candidate_id.clone(), @@ -6035,6 +6037,36 @@ impl SessionProcessor { } } + /// Resolve the expected referenced BlockIdExt for an empty candidate. + /// + /// Walks the parent chain through `received_candidates` until a non-empty + /// ancestor is found. Returns its `block_id`, which is the C++ equivalent + /// of `event->state->as_normal()` in `block-validator.cpp`. + /// + /// Returns `None` if the parent chain is broken, missing, or contains + /// only empty ancestors (no normal tip exists). + fn resolve_expected_empty_block(&self, raw_candidate: &RawCandidate) -> Option { + let parent_id = raw_candidate.parent_id.as_ref()?; + let parent = self.received_candidates.get(parent_id)?; + if !parent.is_empty { + return Some(parent.block_id.clone()); + } + let mut current_parent = parent.parent_id.clone(); + let mut depth = 0u32; + while let Some(pid) = current_parent { + depth += 1; + if depth > MAX_CHAIN_DEPTH { + return None; + } + let ancestor = self.received_candidates.get(&pid)?; + if !ancestor.is_empty { + return Some(ancestor.block_id.clone()); + } + current_parent = ancestor.parent_id.clone(); + } + None + } + /// Try to approve a block candidate by sending to higher layer /// /// Reference: validator-session/src/session_processor.rs try_approve_block() @@ -6070,15 +6102,53 @@ impl SessionProcessor { return; }; - // Handle empty blocks (no validation needed) + // Handle empty blocks: C++ block-validator.cpp rejects unless the referenced + // block equals event->state->as_normal(). We resolve the expected block from + // the parent chain and compare before approving. if pending.raw_candidate.block.is_empty() { - log::trace!( - "Session {} try_approve_block: empty block, auto-approving {:?}", - self.session_id().to_hex_string(), - candidate_id, - ); - // Empty blocks are auto-approved - directly push to validated_candidates - self.candidate_decision_ok_internal(candidate_id.clone(), slot, receive_time); + let referenced_block = pending.raw_candidate.block.block_id().clone(); + let expected = self.resolve_expected_empty_block(&pending.raw_candidate); + let cid = candidate_id.clone(); + + match expected { + Some(expected_block) if referenced_block == expected_block => { + log::trace!( + "Session {} try_approve_block: empty block matches parent normal tip, \ + approving {:?}", + self.session_id().to_hex_string(), + cid, + ); + self.candidate_decision_ok_internal(cid, slot, receive_time); + } + Some(expected_block) => { + log::warn!( + "Session {} try_approve_block: empty block REJECTED โ€” wrong referenced \ + block (got seqno={}, expected seqno={}) for {:?}", + self.session_id().to_hex_string(), + referenced_block.seq_no, + expected_block.seq_no, + cid, + ); + self.candidate_decision_fail( + slot, + cid, + error!("Wrong referenced block in empty candidate"), + ); + } + None => { + log::warn!( + "Session {} try_approve_block: empty block REJECTED โ€” cannot resolve \ + parent normal tip for {:?}", + self.session_id().to_hex_string(), + cid, + ); + self.candidate_decision_fail( + slot, + cid, + error!("Cannot resolve parent normal tip for empty candidate"), + ); + } + } return; } diff --git a/src/node/simplex/src/simplex_state.rs b/src/node/simplex/src/simplex_state.rs index 008f963..72c768c 100644 --- a/src/node/simplex/src/simplex_state.rs +++ b/src/node/simplex/src/simplex_state.rs @@ -6034,25 +6034,29 @@ impl SimplexState { self.first_non_progressed_slot, ); - // C++ CHECK(base.has_value()) before publishing LeaderWindowObserved(now_, base) - // Enforce the same invariant for window-start slots - if desc.is_first_in_window(self.first_non_progressed_slot) { - let base_known = - self.get_slot_available_base(desc, self.first_non_progressed_slot).is_some(); - if !base_known { - log::error!( - "SimplexState: notarized-parent chain invariant violated - \ - base unknown for window start slot {} (now_window={})", - self.first_non_progressed_slot, - now_window - ); - } - debug_assert!( - base_known, - "notarized-parent chain: base must be known for window start slot {}", - self.first_non_progressed_slot + // C++ parity: read available_base from the progress cursor slot. + // Reference: pool.cpp advance_present(): + // ParentId base = {}; + // if (now_ != 0) { base = slot_at(now_)->state->available_base.value(); } + // publish(now_, base); + // + // For genesis (slot 0), base is None (matches C++ ParentId{} = std::nullopt). + // For later slots, base comes from the per-slot available_base propagated + // by notarization/skip handlers. + let base: CandidateParent = if self.first_non_progressed_slot.value() == 0 { + None + } else { + let slot_base = self.get_slot_available_base(desc, self.first_non_progressed_slot); + assert!( + slot_base.is_some(), + "SimplexState: notarized-parent chain invariant violated โ€” \ + base unknown for progress cursor slot {} (now_window={}). \ + C++ CHECK(maybe_base.has_value()) in pool.cpp advance_present()", + self.first_non_progressed_slot, + now_window ); - } + slot_base.unwrap() + }; // Apply adaptive timeout backoff (reuse existing logic) self.apply_adaptive_timeout_backoff( @@ -6065,9 +6069,26 @@ impl SimplexState { self.current_leader_window_idx = now_window; self.set_timeouts(desc); + // C++ parity: populate new window's available_bases and first slot base. + // In C++ this happens via LeaderWindowObserved -> consensus.cpp handler which + // calls start_generation(event->base, ...). In Rust the FSM handles this + // directly: the base is inserted into the window's available_bases set so + // that check_collation() -> has_available_parent() sees it. + self.ensure_window_exists(now_window); + if let Some(window) = self.get_window_mut(now_window) { + window.available_bases.insert(base.clone()); + } + let first_slot = now_window.window_start(self.slots_per_leader_window); + if let Some(slot) = self.get_slot_mut(desc, first_slot) { + if slot.available_base.is_none() { + slot.available_base = Some(base.clone()); + } + } + log::trace!( - "SimplexState: advanced to window {}, scheduling timeouts from slot {}", + "SimplexState: advanced to window {}, base={}, scheduling timeouts from slot {}", now_window, + Self::format_parent(base.as_ref()), self.skip_slot ); } diff --git a/src/node/simplex/src/tests/test_session_processor.rs b/src/node/simplex/src/tests/test_session_processor.rs index 914b2de..01d0829 100644 --- a/src/node/simplex/src/tests/test_session_processor.rs +++ b/src/node/simplex/src/tests/test_session_processor.rs @@ -1455,26 +1455,51 @@ fn make_test_non_empty_candidate( crate::block::RawCandidate::new(candidate_id, parent_id, ValidatorIndex::new(0), block, vec![]) } -/// Helper: create an empty RawCandidate for check_validation tests. -fn make_test_empty_candidate( +/// Helper: create an empty RawCandidate with a specific referenced BlockIdExt. +fn make_test_empty_candidate_with_block( candidate_id: RawCandidateId, parent_id: RawCandidateId, + referenced_block: BlockIdExt, ) -> crate::block::RawCandidate { - let block_id = BlockIdExt::with_params( - ShardIdent::masterchain(), - parent_id.slot.value() + 1, - UInt256::rand(), - UInt256::rand(), - ); crate::block::RawCandidate::new_empty( candidate_id, parent_id, ValidatorIndex::new(0), - block_id, + referenced_block, vec![], ) } +/// Helper: insert a minimal ReceivedCandidate into the processor's received_candidates map. +fn insert_received_candidate( + processor: &mut SessionProcessor, + candidate_id: &RawCandidateId, + block_id: BlockIdExt, + is_empty: bool, + parent_id: Option, +) { + processor.received_candidates.insert( + candidate_id.clone(), + ReceivedCandidate { + slot: candidate_id.slot, + source_idx: ValidatorIndex::new(0), + candidate_id_hash: candidate_id.hash.clone(), + candidate_hash_data_bytes: Vec::new(), + block_id: block_id.clone(), + root_hash: block_id.root_hash.clone(), + file_hash: block_id.file_hash.clone(), + data: consensus_common::ConsensusCommonFactory::create_block_payload(Vec::new()), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + Vec::new(), + ), + receive_time: SystemTime::now(), + is_empty, + parent_id, + is_fully_resolved: true, + }, + ); +} + /// Helper: insert a PendingValidation into the processor. fn insert_pending_validation( processor: &mut SessionProcessor, @@ -1601,20 +1626,134 @@ fn test_check_validation_auto_approves_empty_blocks() { let parent_slot = SlotIndex::new(0); let parent_id = RawCandidateId { slot: parent_slot, hash: UInt256::rand() }; + let parent_block_id = + BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); + + insert_received_candidate( + &mut fixture.processor, + &parent_id, + parent_block_id.clone(), + false, + None, + ); + let child_slot = SlotIndex::new(1); let child_id = RawCandidateId { slot: child_slot, hash: UInt256::rand() }; - let raw_candidate = make_test_empty_candidate(child_id.clone(), parent_id.clone()); + let raw_candidate = make_test_empty_candidate_with_block( + child_id.clone(), + parent_id.clone(), + parent_block_id.clone(), + ); + insert_received_candidate( + &mut fixture.processor, + &child_id, + parent_block_id, + true, + Some(parent_id), + ); let time = fixture.description.get_time(); insert_pending_validation(&mut fixture.processor, &child_id, raw_candidate, time); - // Empty blocks should be auto-approved even without parent notarization. - // candidate_decision_ok_internal removes from both pending_validations and pending_approve, - // so we verify the candidate was processed by checking it left pending_validations. + // Empty blocks with a matching referenced block should be auto-approved. + // C++ block-validator.cpp accepts when block == event->state->as_normal(). fixture.processor.check_validation(); assert!( !fixture.processor.pending_validations.contains_key(&child_id), - "empty block must be processed (removed from pending_validations) regardless of parent notarization" + "empty block must be approved when referenced block matches parent normal tip" + ); +} + +#[test] +fn test_empty_block_accepted_when_referenced_block_matches_parent() { + let mut fixture = TestFixture::new(4); + + let parent_slot = SlotIndex::new(0); + let parent_id = RawCandidateId { slot: parent_slot, hash: UInt256::rand() }; + + let parent_block_id = + BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); + + insert_received_candidate( + &mut fixture.processor, + &parent_id, + parent_block_id.clone(), + false, + None, + ); + + let child_slot = SlotIndex::new(1); + let child_id = RawCandidateId { slot: child_slot, hash: UInt256::rand() }; + + let raw_candidate = make_test_empty_candidate_with_block( + child_id.clone(), + parent_id.clone(), + parent_block_id.clone(), + ); + insert_received_candidate( + &mut fixture.processor, + &child_id, + parent_block_id, + true, + Some(parent_id), + ); + let time = fixture.description.get_time(); + insert_pending_validation(&mut fixture.processor, &child_id, raw_candidate, time); + + fixture.processor.check_validation(); + assert!( + !fixture.processor.pending_validations.contains_key(&child_id), + "empty block must be approved when referenced block matches parent normal tip" + ); + assert!( + fixture.processor.approved.contains_key(&child_id), + "empty block must appear in approved set after matching reference check" + ); +} + +#[test] +fn test_empty_block_rejected_when_referenced_block_differs() { + let mut fixture = TestFixture::new(4); + + let parent_slot = SlotIndex::new(0); + let parent_id = RawCandidateId { slot: parent_slot, hash: UInt256::rand() }; + + let parent_block_id = + BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); + + insert_received_candidate(&mut fixture.processor, &parent_id, parent_block_id, false, None); + + let child_slot = SlotIndex::new(1); + let child_id = RawCandidateId { slot: child_slot, hash: UInt256::rand() }; + + let wrong_block_id = + BlockIdExt::with_params(ShardIdent::masterchain(), 99, UInt256::rand(), UInt256::rand()); + + let raw_candidate = make_test_empty_candidate_with_block( + child_id.clone(), + parent_id.clone(), + wrong_block_id.clone(), + ); + insert_received_candidate( + &mut fixture.processor, + &child_id, + wrong_block_id, + true, + Some(parent_id), + ); + let time = fixture.description.get_time(); + insert_pending_validation(&mut fixture.processor, &child_id, raw_candidate, time); + + // C++ block-validator.cpp rejects empty candidates whose referenced block + // does not match event->state->as_normal(). Rust must do the same. + fixture.processor.check_validation(); + assert!( + fixture.processor.rejected.contains(&child_id), + "empty block must be rejected when referenced block differs from parent normal tip" + ); + assert!( + !fixture.processor.approved.contains_key(&child_id), + "rejected empty block must not appear in approved set" ); } diff --git a/src/node/simplex/src/tests/test_simplex_state.rs b/src/node/simplex/src/tests/test_simplex_state.rs index f5727b5..1dea13b 100644 --- a/src/node/simplex/src/tests/test_simplex_state.rs +++ b/src/node/simplex/src/tests/test_simplex_state.rs @@ -5702,3 +5702,125 @@ fn test_try_notar_not_blocked_by_its_over_after_finalize_restart_cpp_mode() { events ); } + +#[test] +fn test_notarized_parent_chain_genesis_base_propagates_across_skipped_windows() { + // Regression test for bootstrap deadlock: when use_notarized_parent_chain=true (default + // C++ compat mode), skipping an entire window must propagate the available base to the + // next window via advance_leader_window_on_progress_cursor(). + // + // Without the fix, advance_leader_window_on_progress_cursor() only advanced the window + // index and set timeouts but never populated the new window's available_bases, causing + // has_available_parent() to return false and blocking all collation permanently. + // + // Reference: C++ pool.cpp advance_present() reads slot_at(now_)->state->available_base + // and publishes it via LeaderWindowObserved(now_, base). + let desc = create_test_desc(4, 2); // 2 slots per window + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + // Window 0 starts with genesis base + assert!(state.has_available_parent(&desc, SlotIndex::new(0))); + assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); + + // Skip slot 0 (need 3 out of 4 for threshold_66) + let skip_vote_0 = Vote::Skip(SkipVote { slot: SlotIndex::new(0) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip_vote_0.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip_vote_0.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip_vote_0, Vec::new()).unwrap(); + while state.pull_event().is_some() {} + + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); + + // Skip slot 1 (last slot in window 0) -> should trigger window advancement + let skip_vote_1 = Vote::Skip(SkipVote { slot: SlotIndex::new(1) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip_vote_1.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip_vote_1.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip_vote_1, Vec::new()).unwrap(); + while state.pull_event().is_some() {} + + // Progress cursor should be at slot 2 (start of window 1) + assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); + + // Window must have advanced to window 1 + assert_eq!( + state.current_leader_window_idx, + WindowIndex::new(1), + "leader window must advance to window 1 after all window 0 slots skipped" + ); + + // Window 1's available_bases must contain the genesis base (None) + let w1 = state.get_window(WindowIndex::new(1)); + assert!(w1.is_some(), "window 1 must exist"); + assert!( + w1.unwrap().available_bases.contains(&None), + "window 1 must have genesis (None) base propagated from window 0 via \ + advance_leader_window_on_progress_cursor(). Got: {:?}", + w1.unwrap().available_bases + ); + + // Slot 2 (first slot of window 1) must have available_base set + let slot2_base = state.get_slot_available_base(&desc, SlotIndex::new(2)); + assert_eq!(slot2_base, Some(None), "slot 2 available_base must be genesis (Some(None))"); + + // has_available_parent must return true for collation to proceed + assert!( + state.has_available_parent(&desc, SlotIndex::new(2)), + "has_available_parent must be true for slot 2 after genesis base propagated" + ); + + // get_available_parent must return None (genesis = no parent info) + let parent = state.get_available_parent(&desc, SlotIndex::new(2)); + assert_eq!(parent, None, "genesis parent should return None (no parent id)"); +} + +#[test] +fn test_notarized_parent_chain_base_propagates_across_multiple_skipped_windows() { + // Verify that base propagation works across multiple consecutive skipped windows. + // This is the sustained stall scenario: window 0 -> 1 -> 2 all skip without finalization. + let desc = create_test_desc(4, 1); // 1 slot per window for simplicity + let mut state = + SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); + + assert!(state.has_available_parent(&desc, SlotIndex::new(0))); + assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); + + // Skip window 0 (slot 0) + let skip = Vote::Skip(SkipVote { slot: SlotIndex::new(0) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip, Vec::new()).unwrap(); + while state.pull_event().is_some() {} + + assert_eq!(state.current_leader_window_idx, WindowIndex::new(1)); + assert!( + state.has_available_parent(&desc, SlotIndex::new(1)), + "window 1 must have available parent after window 0 skipped" + ); + + // Skip window 1 (slot 1) + let skip = Vote::Skip(SkipVote { slot: SlotIndex::new(1) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip, Vec::new()).unwrap(); + while state.pull_event().is_some() {} + + assert_eq!(state.current_leader_window_idx, WindowIndex::new(2)); + assert!( + state.has_available_parent(&desc, SlotIndex::new(2)), + "window 2 must have available parent after windows 0+1 skipped" + ); + + // Skip window 2 (slot 2) + let skip = Vote::Skip(SkipVote { slot: SlotIndex::new(2) }); + state.on_vote_test(&desc, ValidatorIndex::new(0), skip.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(1), skip.clone(), Vec::new()).unwrap(); + state.on_vote_test(&desc, ValidatorIndex::new(2), skip, Vec::new()).unwrap(); + while state.pull_event().is_some() {} + + assert_eq!(state.current_leader_window_idx, WindowIndex::new(3)); + assert!( + state.has_available_parent(&desc, SlotIndex::new(3)), + "window 3 must have available parent after windows 0+1+2 all skipped" + ); +} From 8bd0d74ce93bd3c434a7bee9bf42e04787694b7f Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Wed, 1 Apr 2026 22:00:34 +0700 Subject: [PATCH 18/48] chore(helm): add quic and simplex logger targets --- helm/ton-rust-node/docs/logging.md | 2 ++ helm/ton-rust-node/files/logs.config.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/helm/ton-rust-node/docs/logging.md b/helm/ton-rust-node/docs/logging.md index 2582ed8..6131382 100644 --- a/helm/ton-rust-node/docs/logging.md +++ b/helm/ton-rust-node/docs/logging.md @@ -238,6 +238,8 @@ These are the targets you can configure in the `loggers` section: | `storage` | Data storage | | `index` | Data indexing | | `ext_messages` | External message handling | +| `quic` | QUIC transport protocol | +| `simplex` | Simplex consensus protocol | | `telemetry` | Telemetry and metrics | > **Note:** HTTP requests (JSON-RPC, metrics endpoints) are not logged by the node. There is no logger target for HTTP request tracing. diff --git a/helm/ton-rust-node/files/logs.config.yml b/helm/ton-rust-node/files/logs.config.yml index b205229..e079df1 100644 --- a/helm/ton-rust-node/files/logs.config.yml +++ b/helm/ton-rust-node/files/logs.config.yml @@ -74,3 +74,7 @@ loggers: level: warn ext_messages: level: info + quic: + level: info + simplex: + level: info From 228770bf71be3b6f0e7126e62e8bd63b8194a8aa Mon Sep 17 00:00:00 2001 From: R Date: Wed, 1 Apr 2026 23:48:30 +0700 Subject: [PATCH 19/48] feat(helm): add terminationGracePeriodSeconds parameter (#45) --- helm/ton-rust-node/CHANGELOG.md | 8 ++++++++ helm/ton-rust-node/Chart.yaml | 2 +- helm/ton-rust-node/templates/statefulset.yaml | 1 + helm/ton-rust-node/values.yaml | 4 ++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/helm/ton-rust-node/CHANGELOG.md b/helm/ton-rust-node/CHANGELOG.md index 8c8609c..87f30ab 100644 --- a/helm/ton-rust-node/CHANGELOG.md +++ b/helm/ton-rust-node/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to the Helm chart will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). Versions follow the Helm chart release tags (e.g. `helm/v0.3.0`). +## [0.4.3] - 2026-04-01 + +appVersion: `v0.3.0` + +### Added + +- `terminationGracePeriodSeconds` โ€” configurable grace period before SIGKILL on pod termination. Defaults to 300s (5 minutes). The Kubernetes default of 30s is too short for a TON node โ€” an unclean kill may corrupt the database and forces a cold boot + ## [0.4.2] - 2026-03-18 appVersion: `v0.3.0` diff --git a/helm/ton-rust-node/Chart.yaml b/helm/ton-rust-node/Chart.yaml index 12d65dc..e5d4546 100644 --- a/helm/ton-rust-node/Chart.yaml +++ b/helm/ton-rust-node/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: node description: TON Rust Node deployment type: application -version: 0.4.2 +version: 0.4.3 appVersion: "v0.3.0" sources: diff --git a/helm/ton-rust-node/templates/statefulset.yaml b/helm/ton-rust-node/templates/statefulset.yaml index 34b35be..ffca504 100644 --- a/helm/ton-rust-node/templates/statefulset.yaml +++ b/helm/ton-rust-node/templates/statefulset.yaml @@ -46,6 +46,7 @@ spec: affinity: {{- toYaml . | nindent 8 }} {{- end }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} {{- if .Values.hostNetwork }} hostNetwork: true dnsPolicy: ClusterFirstWithHostDNS diff --git a/helm/ton-rust-node/values.yaml b/helm/ton-rust-node/values.yaml index 9d00a04..8d4d719 100644 --- a/helm/ton-rust-node/values.yaml +++ b/helm/ton-rust-node/values.yaml @@ -407,6 +407,10 @@ serviceAccount: name: "" annotations: {} +## @param terminationGracePeriodSeconds Time (in seconds) given to the node process to shut down gracefully before SIGKILL. The default Kubernetes value (30s) is too short for a TON node โ€” an unclean kill may corrupt the database and forces a cold boot. Set this to at least 300s. +## +terminationGracePeriodSeconds: 300 + ## @section Scheduling parameters ## @param nodeSelector [object] Node selector for pod scheduling From 7f09052a77f51d6290b02eb043a016480edd3d5a Mon Sep 17 00:00:00 2001 From: Slava Date: Wed, 1 Apr 2026 20:01:18 +0300 Subject: [PATCH 20/48] Prevent sync by archives to be stalled when shards split/merge --- src/node/src/sync.rs | 63 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/src/node/src/sync.rs b/src/node/src/sync.rs index b1fa501..2737980 100644 --- a/src/node/src/sync.rs +++ b/src/node/src/sync.rs @@ -57,6 +57,9 @@ impl ArchiveContext { master + self.shards_count } } + fn has_retryable_shards(&self) -> bool { + self.absent_shards.values().any(|&retries| retries > 0) + } } #[derive(Default, Debug)] @@ -361,8 +364,7 @@ pub(crate) async fn start_sync( Some(Some((seq_no, Err(e)))) => { log::error!( target: TARGET, - "Error while downloading package seq_no {}: {}", - seq_no, e + "Error while downloading package seq_no {seq_no}: {e}" ); download(&mut sync_context, seq_no, None) } @@ -374,8 +376,8 @@ pub(crate) async fn start_sync( fail!("INTERNAL ERROR: sync queue broken") }; sync_context.downloads -= update; - let incomplete = archive_context.master.is_none() - || !archive_context.absent_shards.is_empty(); + let incomplete = + archive_context.master.is_none() || archive_context.has_retryable_shards(); let archive_context = if incomplete { Some(archive_context) } else if seq_no_recv <= last_mc_block_id.seq_no() + 1 { @@ -401,8 +403,7 @@ pub(crate) async fn start_sync( Err(e) => { log::error!( target: TARGET, - "Cannot apply downloaded package for MC seq_no = {}: {}", - seq_no_recv, e + "Cannot apply downloaded package for MC seq_no = {seq_no_recv}: {e}" ); download(&mut sync_context, seq_no_recv, None); None @@ -415,6 +416,21 @@ pub(crate) async fn start_sync( None }; if let Some(mut archive_context) = archive_context { + if !archive_context.has_retryable_shards() + && archive_context.master.is_some() + { + // All absent shard retries exhausted โ€” skip this archive + // and move on rather than looping forever + log::warn!( + target: TARGET, + "Giving up on MC seq_no {seq_no_recv}: \ + shard retries exhausted for {:?}", + archive_context.absent_shards.keys().collect::>() + ); + queue.remove(index); + sync_context.concurrency = max_concurrency; + break; + } let mut msg = format!( "{}, need shards {}", if archive_context.master.is_none() { @@ -424,8 +440,10 @@ pub(crate) async fn start_sync( }, archive_context.need_shards, ); - for shard in archive_context.absent_shards.keys() { - msg.push_str(format!(", shard {shard} absent").as_str()); + for (shard, retries) in &archive_context.absent_shards { + msg.push_str( + format!(", shard {shard} absent (retries={retries})").as_str(), + ); } for shard in archive_context.loaded_shards.keys() { msg.push_str(format!(", shard {shard} loaded").as_str()); @@ -435,7 +453,7 @@ pub(crate) async fn start_sync( "Incomplete archive detected for MC seq_no {seq_no_recv}: {msg}" ); let (_, status) = &mut queue[index]; - if !archive_context.absent_shards.is_empty() { + if archive_context.has_retryable_shards() { archive_context.need_shards = true; } *status = ArchiveStatus::Incomplete(archive_context); @@ -494,20 +512,21 @@ async fn download_archives( tasks.push(task); } if archive_context.need_shards { + let mut scheduled_shards = HashSet::new(); for i in 0..archive_context.shards_count { let shard = ShardIdent::with_tagged_prefix( BASE_WORKCHAIN_ID, ((i as u64) * 2 + 1) << (63 - sync_context.engine.get_monitor_min_split()), )?; + scheduled_shards.insert(shard.clone()); if archive_context.loaded_shards.get(&shard).is_some() { continue; } let retry = archive_context.absent_shards.entry(shard.clone()).or_insert(SHARD_RETRIES); - if *retry == 1 { - archive_context.absent_shards.remove(&shard); - } else { - *retry -= 1; + if *retry == 0 { + continue; } + *retry -= 1; let context = sync_context.clone(); let task = tokio::spawn(async move { let shard = Some(shard); @@ -515,6 +534,20 @@ async fn download_archives( }); tasks.push(task); } + // Decrement retries for absent shards not covered by min_split + // (e.g. shards from a different split depth in the MC block) + for (shard, retries) in archive_context.absent_shards.iter_mut() { + if scheduled_shards.contains(shard) || *retries == 0 { + continue; + } + log::warn!( + target: TARGET, + "Shard {shard} absent but not in min_split layout, \ + decrementing retries ({retries} -> {})", + *retries - 1 + ); + *retries -= 1; + } } if tasks.is_empty() { return Ok((archive_context, 0)); @@ -754,7 +787,9 @@ async fn import_shard_blocks( if !bad_shards.is_empty() { for shard in bad_shards { archive_context.loaded_shards.remove(&shard); - archive_context.absent_shards.insert(shard, SHARD_RETRIES); + // Use or_insert to avoid resetting retry counter for + // shards that already exhausted their download attempts + archive_context.absent_shards.entry(shard).or_insert(SHARD_RETRIES); } let Ok(maps) = Arc::try_unwrap(maps) else { fail!("INTERNAL ERROR: archive master maps are locked") From 93e41eab3d951d0530f32687bd8564434bc7ffa4 Mon Sep 17 00:00:00 2001 From: Slava Date: Wed, 1 Apr 2026 23:22:40 +0300 Subject: [PATCH 21/48] Archival node functionality --- src/Cargo.lock | 4 + src/block/src/shard.rs | 19 + src/node/Cargo.toml | 6 + src/node/bin/archive_import.rs | 102 +++ src/node/src/archive_import/ingester.rs | 830 ++++++++++++++++++ src/node/src/archive_import/mod.rs | 412 +++++++++ src/node/src/archive_import/scanner.rs | 155 ++++ src/node/src/archive_import/validator.rs | 81 ++ src/node/src/config.rs | 12 +- src/node/src/engine.rs | 7 +- src/node/src/engine_operations.rs | 14 + src/node/src/engine_traits.rs | 11 + src/node/src/full_node/apply_block.rs | 130 +-- src/node/src/internal_db/mod.rs | 367 ++++++-- src/node/src/internal_db/restore.rs | 5 +- src/node/src/lib.rs | 1 + src/node/src/network/liteserver.rs | 2 +- src/node/src/shard_blocks.rs | 7 +- src/node/src/sync.rs | 4 +- ...4B7173205F740A39CD56F537DEFD28B48A0F6E.boc | Bin 0 -> 8529 bytes ...18EA720CAD1A0E7B4D2ED673C488E72E910342.boc | Bin 0 -> 105 bytes .../archive.00000.0:8000000000000000.pack | Bin 0 -> 341182 bytes .../tests/static/archives/archive.00000.pack | Bin 0 -> 1185911 bytes .../archive.00100.0:8000000000000000.pack | Bin 0 -> 693410 bytes .../tests/static/archives/archive.00100.pack | Bin 0 -> 1250896 bytes src/node/src/tests/test_archive_import.rs | 270 ++++++ src/node/src/tests/test_sync.rs | 11 +- src/node/src/types/awaiters_pool.rs | 12 + src/node/src/validator/accept_block.rs | 5 +- src/node/storage/Cargo.toml | 2 + src/node/storage/src/archive_shardstate_db.rs | 127 +++ .../storage/src/archives/archive_manager.rs | 217 ++++- .../storage/src/archives/archive_slice.rs | 224 +++-- .../storage/src/archives/block_index_db.rs | 31 +- src/node/storage/src/archives/db_provider.rs | 63 ++ src/node/storage/src/archives/epoch.rs | 276 ++++++ src/node/storage/src/archives/file_maps.rs | 98 ++- src/node/storage/src/archives/mod.rs | 4 +- src/node/storage/src/archives/package.rs | 83 +- .../storage/src/archives/package_entry.rs | 7 + src/node/storage/src/archives/package_id.rs | 4 +- src/node/storage/src/block_handle_db.rs | 119 ++- src/node/storage/src/block_info_db.rs | 5 + src/node/storage/src/cell_db.rs | 439 +++++++++ src/node/storage/src/db/rocksdb.rs | 4 +- .../storage/src/dynamic_boc_archive_db.rs | 219 +++++ src/node/storage/src/dynamic_boc_rc_db.rs | 496 ++--------- src/node/storage/src/lib.rs | 3 + src/node/storage/src/shard_top_blocks_db.rs | 2 + src/node/storage/src/shardstate_db_async.rs | 11 +- src/node/storage/src/tests/mod.rs | 1 + .../storage/src/tests/test_archive_manager.rs | 221 ++++- .../storage/src/tests/test_archive_slice.rs | 60 +- .../src/tests/test_dynamic_boc_archive_db.rs | 261 ++++++ .../src/tests/test_dynamic_boc_rc_db.rs | 11 +- src/node/storage/src/tests/test_epoch.rs | 156 ++++ src/node/storage/src/types/block_meta.rs | 29 + src/node/storage/src/types/storage_cell.rs | 60 +- .../src/types/tests/test_storage_cell.rs | 23 +- 59 files changed, 4926 insertions(+), 797 deletions(-) create mode 100644 src/node/bin/archive_import.rs create mode 100644 src/node/src/archive_import/ingester.rs create mode 100644 src/node/src/archive_import/mod.rs create mode 100644 src/node/src/archive_import/scanner.rs create mode 100644 src/node/src/archive_import/validator.rs create mode 100644 src/node/src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc create mode 100644 src/node/src/tests/static/EE0BEDFE4B32761FB35E9E1D8818EA720CAD1A0E7B4D2ED673C488E72E910342.boc create mode 100644 src/node/src/tests/static/archives/archive.00000.0:8000000000000000.pack create mode 100644 src/node/src/tests/static/archives/archive.00000.pack create mode 100644 src/node/src/tests/static/archives/archive.00100.0:8000000000000000.pack create mode 100644 src/node/src/tests/static/archives/archive.00100.pack create mode 100644 src/node/src/tests/test_archive_import.rs create mode 100644 src/node/storage/src/archive_shardstate_db.rs create mode 100644 src/node/storage/src/archives/db_provider.rs create mode 100644 src/node/storage/src/archives/epoch.rs create mode 100644 src/node/storage/src/cell_db.rs create mode 100644 src/node/storage/src/dynamic_boc_archive_db.rs create mode 100644 src/node/storage/src/tests/test_dynamic_boc_archive_db.rs create mode 100644 src/node/storage/src/tests/test_epoch.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index aee58d4..8590ed8 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -3310,6 +3310,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "rand 0.8.5", + "rayon", "regex", "reqwest", "secrets-vault", @@ -3323,6 +3324,7 @@ dependencies = [ "storage", "stream-cancel", "string-builder", + "tempfile", "thiserror 1.0.69", "tokio", "tokio-util", @@ -5206,9 +5208,11 @@ dependencies = [ "serde", "serde_cbor", "serde_derive", + "serde_json", "smallvec", "strum 0.18.0", "strum_macros 0.18.0", + "tempfile", "thiserror 1.0.69", "tokio", "tokio-util", diff --git a/src/block/src/shard.rs b/src/block/src/shard.rs index 3e6ab62..9d528b1 100644 --- a/src/block/src/shard.rs +++ b/src/block/src/shard.rs @@ -31,6 +31,7 @@ use crate::{ use std::{ any::type_name, fmt::{self, Display, Formatter}, + str::FromStr, }; #[cfg(test)] @@ -644,6 +645,24 @@ impl fmt::Debug for ShardIdent { } } +impl FromStr for ShardIdent { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + let (workchain_part, shard_part_with_maybe_extra) = + s.split_once(':').ok_or_else(|| error!("Can't read shard ident from {}", s))?; + + let workchain_id: i32 = workchain_part + .trim() + .parse() + .map_err(|e| error!("Can't read workchain_id from {}: {}", s, e))?; + let prefix = u64::from_str_radix(shard_part_with_maybe_extra.trim(), 16) + .map_err(|e| error!("Can't read shard from {}: {}", s, e))?; + + Ok(Self { workchain_id, prefix }) + } +} + impl Deserializable for ShardIdent { fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { let constructor_and_pfx = cell.get_next_byte()?; diff --git a/src/node/Cargo.toml b/src/node/Cargo.toml index afd08d6..8052da5 100644 --- a/src/node/Cargo.toml +++ b/src/node/Cargo.toml @@ -37,6 +37,10 @@ path = 'bin/print.rs' name = 'zerostate' path = 'bin/zerostate.rs' +[[bin]] +name = 'archive_import' +path = 'bin/archive_import.rs' + [[bin]] name = 'hardfork' path = 'bin/hardfork.rs' @@ -73,6 +77,7 @@ num_cpus = '1.13' openssl = '0.10' parking_lot = '0.12' rand = '0.8' +rayon = '1' regex = '1.10' serde = '1.0' serde_derive = '1.0' @@ -110,6 +115,7 @@ harness = false [dev-dependencies] criterion = { version = "0.5", features = ["html_reports", "async_tokio"] } +tempfile = '3' difference = '2.0' external-ip = '6.0' http-body-util = "0.1" diff --git a/src/node/bin/archive_import.rs b/src/node/bin/archive_import.rs new file mode 100644 index 0000000..0def3cd --- /dev/null +++ b/src/node/bin/archive_import.rs @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use clap::{Arg, ArgAction, Command}; +use node::archive_import::{run_import, ImportConfig}; +use std::path::PathBuf; + +fn main() { + env_logger::Builder::from_default_env().format_timestamp_millis().init(); + + let matches = Command::new("archive_import") + .about("Import raw .pack archive files into epoch-based storage") + .arg( + Arg::new("archives-path") + .long("archives-path") + .required(true) + .help("Path to directory with source .pack files"), + ) + .arg( + Arg::new("epochs-path") + .long("epochs-path") + .required(true) + .help("Path where epoch directories will be created"), + ) + .arg( + Arg::new("epoch-size") + .long("epoch-size") + .default_value("10000000") + .help("Number of MC blocks per epoch (must be multiple of 20000)"), + ) + .arg( + Arg::new("node-db-path") + .long("node-db-path") + .required(true) + .help("Path to node database directory"), + ) + .arg( + Arg::new("mc-zerostate") + .long("mc-zerostate") + .required(true) + .help("Path to masterchain zerostate .boc file"), + ) + .arg( + Arg::new("wc-zerostate") + .long("wc-zerostate") + .action(ArgAction::Append) + .required(true) + .help("Path to workchain zerostate .boc file (one per workchain)"), + ) + .arg( + Arg::new("global-config") + .long("global-config") + .required(true) + .help("Path to global config JSON file (describes zerostate and hard forks)"), + ) + .arg( + Arg::new("skip-validation") + .long("skip-validation") + .action(ArgAction::SetTrue) + .help("Skip block proof validation (for re-importing already validated archives)"), + ) + .arg(Arg::new("copy").long("copy").action(ArgAction::SetTrue).help( + "Copy source .pack files instead of moving them. Use for keeping original \ + files or when source and destination are on different filesystems.", + )) + .get_matches(); + + let config = ImportConfig { + archives_path: PathBuf::from(matches.get_one::("archives-path").unwrap()), + epochs_path: PathBuf::from(matches.get_one::("epochs-path").unwrap()), + epoch_size: matches + .get_one::("epoch-size") + .unwrap() + .parse() + .expect("epoch-size must be a number"), + node_db_path: PathBuf::from(matches.get_one::("node-db-path").unwrap()), + mc_zerostate_path: PathBuf::from(matches.get_one::("mc-zerostate").unwrap()), + wc_zerostate_paths: matches + .get_many::("wc-zerostate") + .unwrap() + .map(|s| PathBuf::from(s)) + .collect(), + global_config_path: PathBuf::from(matches.get_one::("global-config").unwrap()), + skip_validation: matches.get_flag("skip-validation"), + move_files: !matches.get_flag("copy"), + }; + + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create tokio runtime"); + + if let Err(e) = rt.block_on(run_import(config)) { + log::error!("Import failed: {}", e); + std::process::exit(1); + } +} diff --git a/src/node/src/archive_import/ingester.rs b/src/node/src/archive_import/ingester.rs new file mode 100644 index 0000000..8eaccc2 --- /dev/null +++ b/src/node/src/archive_import/ingester.rs @@ -0,0 +1,830 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use crate::{ + archive_import::{scanner::PackageGroup, validator::ValidatorState}, + block::BlockIdExtExtention, + block_proof::BlockProofStuff, + internal_db::{ + ARCHIVES_GC_BLOCK, LAST_APPLIED_MC_BLOCK, PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, + }, + shard_state::ShardHashesStuff, +}; +use futures::future::try_join_all; +use rayon::prelude::*; +use std::{ + collections::{HashMap, HashSet}, + path::Path, + sync::Arc, +}; +use storage::{ + archives::{ + archive_manager::{ArchiveManager, ImportBlockMeta, ImportEntry}, + package::read_package_from_file, + package_entry_id::PackageEntryId, + }, + block_handle_db::BlockHandleStorage, + block_info_db::BlockInfoDb, + traits::Serializable, + types::BlockMeta, +}; +use ton_block::{ + error, fail, Block, BlockIdExt, Cell, Deserializable, Result, ShardIdent, UInt256, +}; + +const TARGET: &str = "archive_import"; + +struct RawEntry { + block_data: Vec, + block_offset: u64, + proof_data: Vec, + proof_offset: u64, +} + +async fn read_raw_package(path: &Path) -> Result> { + let mut reader = read_package_from_file(path).await?; + let mut entries = HashMap::::new(); + let mut offset: u64 = 0; + while let Some(entry) = reader.next().await? { + let entry_size = entry.serialized_size(); + let entry_id = PackageEntryId::::from_filename(entry.filename())?; + let (block_id, is_proof) = match entry_id { + PackageEntryId::Block(id) => (id, false), + PackageEntryId::Proof(id) if id.is_masterchain() => (id, true), + PackageEntryId::ProofLink(id) if !id.is_masterchain() => (id, true), + entry_id => { + log::warn!("Unexpected entry type {} in {}", entry_id, path.display()); + offset += entry_size; + continue; + } + }; + let mut data = entry.take_data(); + entries + .entry(block_id) + .and_modify(|e| { + if is_proof { + e.proof_data = std::mem::take(&mut data); + e.proof_offset = offset; + } else { + e.block_data = std::mem::take(&mut data); + e.block_offset = offset; + } + }) + .or_insert_with(|| { + if is_proof { + RawEntry { + block_data: vec![], + block_offset: 0, + proof_data: data, + proof_offset: offset, + } + } else { + RawEntry { + block_data: data, + block_offset: offset, + proof_data: vec![], + proof_offset: 0, + } + } + }); + offset += entry_size; + } + Ok(entries) +} + +struct McEntry { + block_id: BlockIdExt, + prev_block_id: BlockIdExt, + proof: BlockProofStuff, + is_key: bool, + gen_utime: u32, + end_lt: u64, + shard_tops: Vec, + state_update_new: Cell, + proof_data: Vec, + proof_offset: u64, + block_data: Vec, + block_offset: u64, +} + +struct ProcessedEntry { + block_id: BlockIdExt, + gen_utime: u32, + end_lt: u64, + mc_ref_seq_no: u32, + is_key_block: bool, + proof_offset: u64, + block_offset: u64, + prevs: Vec, + state_update_new: Cell, +} + +impl ProcessedEntry { + fn to_import_entries(&self) -> [ImportEntry; 2] { + let proof_entry_id = if self.block_id.is_masterchain() { + PackageEntryId::Proof(self.block_id.clone()) + } else { + PackageEntryId::ProofLink(self.block_id.clone()) + }; + [ + ImportEntry { entry_id: proof_entry_id, offset: self.proof_offset, block_meta: None }, + ImportEntry { + entry_id: PackageEntryId::Block(self.block_id.clone()), + offset: self.block_offset, + block_meta: Some(ImportBlockMeta { + seq_no: self.block_id.seq_no(), + shard: self.block_id.shard_id.clone(), + gen_utime: self.gen_utime, + end_lt: self.end_lt, + mc_ref_seq_no: self.mc_ref_seq_no, + }), + }, + ] + } +} + +struct KeyBlockData { + block_id: BlockIdExt, + proof_data: Vec, + block_data: Vec, +} + +pub struct LastGroupState { + pub mc_block_id: BlockIdExt, + pub shard_tops: Vec, +} + +fn parse_and_verify_block(data: &[u8], declared_id: &BlockIdExt) -> Result { + let file_hash = UInt256::calc_file_hash(data); + let root_cell = ton_block::read_single_root_boc(data)?; + let root_hash = root_cell.repr_hash(); + let block = Block::construct_from_cell(root_cell)?; + let info = block.read_info()?; + let actual_id = + BlockIdExt::with_params(info.shard().clone(), info.seq_no(), root_hash, file_hash); + if actual_id != *declared_id { + return Err(error!("Block declared as {} but data contains {}", declared_id, actual_id)); + } + Ok(block) +} + +fn deserialize_mc_entry(block_id: BlockIdExt, raw: RawEntry) -> Result { + if raw.proof_data.is_empty() { + return Err(error!("MC block {} has no proof in the package", block_id)); + } + if raw.block_data.is_empty() { + return Err(error!("MC block {} has no block data in the package", block_id)); + } + + let proof = BlockProofStuff::deserialize(&block_id, raw.proof_data.clone(), false)?; + let (virt_block, _) = proof.virtualize_block()?; + let is_key = virt_block.read_info()?.key_block(); + + let block = parse_and_verify_block(&raw.block_data, &block_id)?; + let block_info = block.read_info()?; + let gen_utime = block_info.gen_utime(); + let end_lt = block_info.end_lt(); + let mut prev_ids = block_info.read_prev_ids()?; + if prev_ids.len() != 1 { + return Err(error!("MC block {} has {} prev refs, expected 1", block_id, prev_ids.len())); + } + let prev_block_id = prev_ids.pop().unwrap(); + let extra = block + .read_extra()? + .read_custom()? + .ok_or_else(|| error!("No McExtra in master block {}", block_id))?; + let shard_tops = ShardHashesStuff::from(extra.shards().clone()).top_blocks_all()?; + let state_update_new = block.read_state_update()?.new; + + Ok(McEntry { + block_id, + prev_block_id, + proof, + is_key, + gen_utime, + end_lt, + shard_tops, + state_update_new, + proof_data: raw.proof_data, + proof_offset: raw.proof_offset, + block_data: raw.block_data, + block_offset: raw.block_offset, + }) +} + +fn validate_mc_range( + entries: &[McEntry], + key_proof: &Option, + zerostate: &Arc, +) -> Result<()> { + entries.par_iter().try_for_each(|e| match key_proof { + None => e.proof.check_with_master_state(zerostate), + Some(kb) => e.proof.check_with_prev_key_block_proof(kb), + }) +} + +fn check_mc_chain(entries: &[McEntry], expected_first_prev: &BlockIdExt) -> Result<()> { + if let Some(first) = entries.first() { + if first.prev_block_id != *expected_first_prev { + fail!( + "MC chain gap between packages: block {} prev_ref = {} but expected {}", + first.block_id, + first.prev_block_id, + expected_first_prev, + ); + } + } + for w in entries.windows(2) { + if w[1].prev_block_id != w[0].block_id { + fail!( + "MC chain gap: block {} prev_ref = {} but expected {}", + w[1].block_id, + w[1].prev_block_id, + w[0].block_id, + ); + } + } + Ok(()) +} + +fn parse_mc_entries( + raw: HashMap, + validator: &mut ValidatorState, + skip: bool, + expected_first_prev: BlockIdExt, +) -> Result<(Vec, Option, Vec<(u32, Vec)>, LastGroupState)> +{ + let mut entries: Vec = + raw.into_par_iter().map(|(id, r)| deserialize_mc_entry(id, r)).collect::>()?; + entries.sort_by_key(|e| e.block_id.seq_no()); + check_mc_chain(&entries, &expected_first_prev)?; + + let rest_start = if entries.first().map(|e| e.is_key).unwrap_or(false) { + let block_id = &entries[0].block_id; + if !skip { + // Skip re-validation if this key block is already the current validation root (resume). + let already_done = validator + .current_key_block_proof() + .map(|kp| kp.id().seq_no() >= block_id.seq_no()) + .unwrap_or(false); + if !already_done { + let is_hardfork = validator.is_hardfork(block_id); + if is_hardfork { + log::info!( + target: TARGET, + "Hard fork block {} accepted as new validation root", + block_id, + ); + } else { + let key_proof = validator.current_key_block_proof().cloned(); + let zerostate = Arc::clone(validator.zerostate()); + validate_mc_range(&entries[..1], &key_proof, &zerostate)?; + } + } + } + validator.set_key_block_proof(entries[0].proof.clone()); + 1 + } else { + 0 + }; + + if !skip { + let key_proof = validator.current_key_block_proof().cloned(); + let zerostate = Arc::clone(validator.zerostate()); + validate_mc_range(&entries[rest_start..], &key_proof, &zerostate)?; + } + + let mut processed = Vec::with_capacity(entries.len()); + let mut key_block: Option = None; + let mut mc_shard_tops: Vec<(u32, Vec)> = Vec::new(); + + for entry in entries { + if entry.is_key { + if Some(&entry.block_id) != validator.current_key_block_proof().map(|kp| kp.id()) { + fail!("Second key block {} in package", entry.block_id); + } + key_block = Some(KeyBlockData { + block_id: entry.block_id.clone(), + proof_data: entry.proof_data, + block_data: entry.block_data.clone(), + }); + } + mc_shard_tops.push((entry.block_id.seq_no(), entry.shard_tops)); + processed.push(ProcessedEntry { + mc_ref_seq_no: entry.block_id.seq_no(), + block_id: entry.block_id, + gen_utime: entry.gen_utime, + end_lt: entry.end_lt, + is_key_block: entry.is_key, + proof_offset: entry.proof_offset, + block_offset: entry.block_offset, + prevs: vec![entry.prev_block_id], + state_update_new: entry.state_update_new, + }); + } + + let last_group_state = + processed.last().ok_or_else(|| error!("MC package is empty")).map(|e| LastGroupState { + mc_block_id: e.block_id.clone(), + shard_tops: mc_shard_tops.last().map(|(_, tops)| tops.clone()).unwrap_or_default(), + })?; + Ok((processed, key_block, mc_shard_tops, last_group_state)) +} + +fn deserialize_shard_entry( + block_id: BlockIdExt, + raw: RawEntry, + skip: bool, +) -> Result { + if raw.proof_data.is_empty() { + return Err(error!("Shard block {} has no proof link in the package", block_id)); + } + if raw.block_data.is_empty() { + return Err(error!("Shard block {} has no block data in the package", block_id)); + } + + if !skip { + let proof = BlockProofStuff::deserialize(&block_id, raw.proof_data.clone(), true)?; + proof.check_proof_link()?; + } + + let block = parse_and_verify_block(&raw.block_data, &block_id)?; + let info = block.read_info()?; + let prevs = info.read_prev_ids()?; + let state_update_new = block.read_state_update()?.new; + + Ok(ProcessedEntry { + gen_utime: info.gen_utime(), + end_lt: info.end_lt(), + mc_ref_seq_no: 0, + is_key_block: false, + proof_offset: raw.proof_offset, + block_offset: raw.block_offset, + block_id, + prevs, + state_update_new, + }) +} + +fn parse_shard_entries( + raw: HashMap, + archive_id: u32, + shard: ShardIdent, + mc_shard_tops: Vec<(u32, Vec)>, + prev_shard_tops: Vec, + skip: bool, +) -> Result> { + let now = std::time::Instant::now(); + let results: HashMap = raw + .into_par_iter() + .map(|(id, r)| deserialize_shard_entry(id.clone(), r, skip).map(|res| (id, res))) + .collect::>()?; + log::debug!(target: TARGET, "Deserialized shard entries after {:#?}", now.elapsed()); + + let entries = if !skip { + let prev_committed: HashSet = + prev_shard_tops.into_iter().filter(|id| id.shard_id.intersect_with(&shard)).collect(); + validate_shard_and_assign_mc_refs(&shard, mc_shard_tops, results, prev_committed)? + } else { + // mc_ref_seq_no must be >= archive_id for choose_package() to find the right file. + let mut entries: Vec = results + .into_iter() + .map(|(_, mut entry)| { + entry.mc_ref_seq_no = archive_id; + entry + }) + .collect(); + entries.sort_by_key(|e| e.block_id.seq_no()); + entries + }; + + Ok(entries) +} + +fn validate_shard_and_assign_mc_refs( + shard: &ShardIdent, + mut mc_shard_tops: Vec<(u32, Vec)>, + mut blocks: HashMap, + prev_committed: HashSet, +) -> Result> { + if blocks.len() == 0 { + return Ok(vec![]); + } + + let mut known: HashSet = prev_committed; + mc_shard_tops.sort_by_key(|(seqno, _)| *seqno); + + let mut entries = Vec::with_capacity(blocks.len()); + for (mc_seqno, tops) in mc_shard_tops { + for top in tops { + if !top.shard_id.intersect_with(shard) { + continue; + } + let mut current = top; + loop { + if known.contains(¤t) { + break; + } + if let Some(mut entry) = blocks.remove(¤t) { + entry.mc_ref_seq_no = mc_seqno; + let mut prevs = entry.prevs.clone(); + entries.push(entry); + // blocks before merge are always committed by MC block + if prevs.len() > 1 + && (blocks.contains_key(&prevs[0]) || blocks.contains_key(&prevs[1])) + { + fail!("Block {} parents are not committed by MC blocks", current); + } + let prev = + prevs.pop().ok_or_else(|| error!("Block {} has no parents", current))?; + known.insert(current); + current = prev; + } else { + fail!( + "Shard chain break: block {} is not in current package \ + and was not committed by previous archive group", + current, + ); + } + } + } + } + + if !blocks.is_empty() { + fail!("Some blocks in shard {} are not reachable from MC shard_hashes", shard); + } + + // Sort by seqno ascending: prev block handles must exist when setting next links. + // This also handles cross-shard deps (parent shard blocks have lower seqno than children after split). + entries.sort_by_key(|e| e.block_id.seq_no()); + Ok(entries) +} + +pub struct Ingester { + archive_manager: Arc, + block_handle_storage: Arc, + archive_state_db: Arc, + prev1_block_db: BlockInfoDb, + prev2_block_db: BlockInfoDb, + next1_block_db: BlockInfoDb, + next2_block_db: BlockInfoDb, + move_files: bool, + skip_validation: bool, +} + +impl Ingester { + pub fn new( + archive_manager: Arc, + block_handle_storage: Arc, + archive_state_db: Arc, + prev1_block_db: BlockInfoDb, + prev2_block_db: BlockInfoDb, + next1_block_db: BlockInfoDb, + next2_block_db: BlockInfoDb, + move_files: bool, + skip_validation: bool, + ) -> Self { + Self { + archive_manager, + block_handle_storage, + archive_state_db, + prev1_block_db, + prev2_block_db, + next1_block_db, + next2_block_db, + move_files, + skip_validation, + } + } + + pub async fn run_groups( + &self, + groups: &[PackageGroup], + start_idx: usize, + total: usize, + mut validator: ValidatorState, + mut last_group_state: LastGroupState, + ) -> Result { + let mut prefetch: Option< + tokio::task::JoinHandle< + Result<(HashMap, Vec>)>, + >, + > = None; + let start = std::time::Instant::now(); + + for (local_idx, group) in groups.iter().enumerate() { + let global_idx = start_idx + local_idx; + let elapsed = start.elapsed(); + let eta = (elapsed * total as u32 / (global_idx + 1) as u32).saturating_sub(elapsed); + log::info!( + target: TARGET, + "Processing group {}/{}: archive_id={}, {} shard packages. ETA {:#?}", + global_idx + 1, + total, + group.archive_id, + group.shard_packages.len(), + eta, + ); + + let next_prefetch = groups.get(local_idx + 1).map(|next| { + let mc_path = next.mc_package.path.clone(); + let shard_paths: Vec<_> = + next.shard_packages.iter().map(|p| p.path.clone()).collect(); + tokio::spawn(async move { + let (mc_raw, shard_raws) = tokio::try_join!( + read_raw_package(&mc_path), + try_join_all(shard_paths.iter().map(|p| read_raw_package(p))), + )?; + Ok::<_, ton_block::Error>((mc_raw, shard_raws)) + }) + }); + + let (mc_raw, shard_raws) = match prefetch.take() { + Some(handle) => { + handle.await.map_err(|e| error!("Prefetch task panicked: {}", e))?? + } + None => tokio::try_join!( + read_raw_package(&group.mc_package.path), + try_join_all(group.shard_packages.iter().map(|p| read_raw_package(&p.path))), + )?, + }; + + let (new_validator, new_state) = self + .ingest_group_from_raw(group, mc_raw, shard_raws, validator, last_group_state) + .await?; + validator = new_validator; + last_group_state = new_state; + prefetch = next_prefetch; + } + + self.block_handle_storage.save_full_node_state( + LAST_APPLIED_MC_BLOCK.to_string(), + &last_group_state.mc_block_id, + )?; + self.block_handle_storage.save_full_node_state( + SHARD_CLIENT_MC_BLOCK.to_string(), + &last_group_state.mc_block_id, + )?; + self.block_handle_storage + .save_full_node_state(ARCHIVES_GC_BLOCK.to_string(), &last_group_state.mc_block_id)?; + self.block_handle_storage + .save_full_node_state(PSS_KEEPER_MC_BLOCK.to_string(), &last_group_state.mc_block_id)?; + + Ok(validator) + } + + async fn ingest_group_from_raw( + &self, + group: &PackageGroup, + mc_raw: HashMap, + shard_raws: Vec>, + validator: ValidatorState, + prev_group_state: LastGroupState, + ) -> Result<(ValidatorState, LastGroupState)> { + let skip = self.skip_validation; + let expected_first_mc_prev = prev_group_state.mc_block_id; + let prev_shard_tops = prev_group_state.shard_tops; + let mc_block_count = mc_raw.len(); + let group_start = std::time::Instant::now(); + + let t = std::time::Instant::now(); + let (mc_entries, key_block, mc_shard_tops, last_group_state, validator) = + tokio::task::spawn_blocking(move || -> Result<_> { + let mut v = validator; + let (entries, key_block, shard_tops, last_state) = + parse_mc_entries(mc_raw, &mut v, skip, expected_first_mc_prev)?; + Ok((entries, key_block, shard_tops, last_state, v)) + }) + .await + .map_err(|e| error!("MC parse task panicked: {}", e))??; + let parse_mc_ms = t.elapsed().as_millis(); + + let t = std::time::Instant::now(); + for entry in &mc_entries { + self.update_block_handles(entry)?; + } + let mc_handles_ms = t.elapsed().as_millis(); + + let mc_import_entries: Vec = + mc_entries.iter().flat_map(|e| e.to_import_entries()).collect(); + + let archive_id = group.archive_id; + let shard_parse_handles: Vec<_> = shard_raws + .into_iter() + .zip(group.shard_packages.iter()) + .map(|(raw, pkg)| { + let shard = pkg.shard.clone(); + let tops = mc_shard_tops.clone(); + let prev = prev_shard_tops.clone(); + tokio::task::spawn_blocking(move || { + parse_shard_entries(raw, archive_id, shard, tops, prev, skip) + }) + }) + .collect(); + let archive_state_db = Arc::clone(&self.archive_state_db); + let fill_mc_states_db = tokio::task::spawn_blocking(move || -> Result<()> { + for entry in &mc_entries { + archive_state_db.put_update(&entry.block_id, entry.state_update_new.clone())?; + } + Ok(()) + }); + + let mc_shard = ShardIdent::masterchain(); + + // Run mc_import, mc_states, and the full shard pipeline concurrently. + let t = std::time::Instant::now(); + let mut shard_block_count = 0usize; + let (_, _, shard_pipeline_ms) = tokio::try_join!( + // Task 1: import MC package into archive + self.archive_manager.import_package( + &group.mc_package.path, + group.mc_package.archive_id, + &mc_shard, + &mc_import_entries, + false, + key_block.is_some(), + ), + // Task 2: save MC state cells + async { + fill_mc_states_db.await.map_err(|e| error!("MC states db task panicked: {}", e))? + }, + // Task 3: shard pipeline โ€” parse โ†’ handles+states โ†’ import + async { + let t_pipeline = std::time::Instant::now(); + + // 3a: await shard parse (already spawned above) + let shard_parse_results: Vec> = + try_join_all(shard_parse_handles.into_iter().map(|h| async move { + h.await.map_err(|e| error!("Shard parse task panicked: {}", e))? + })) + .await?; + + // 3b: update block handles + save shard state cells + for shard_entries in &shard_parse_results { + shard_block_count += shard_entries.len(); + for entry in shard_entries { + self.update_block_handles(entry)?; + self.archive_state_db + .put_update(&entry.block_id, entry.state_update_new.clone())?; + } + } + + // 3c: import shard packages into archive + let shard_import_entries: Vec> = shard_parse_results + .iter() + .map(|entries| entries.iter().flat_map(|e| e.to_import_entries()).collect()) + .collect(); + + try_join_all( + group + .shard_packages + .iter() + .zip(shard_import_entries.iter()) + .filter(|(_, entries)| !entries.is_empty()) + .map(|(pkg, import_entries)| { + self.archive_manager.import_package( + &pkg.path, + pkg.archive_id, + &pkg.shard, + import_entries, + self.move_files, + false, + ) + }), + ) + .await?; + + Ok(t_pipeline.elapsed().as_millis()) + }, + )?; + let parallel_ms = t.elapsed().as_millis(); + + let t = std::time::Instant::now(); + if let Some(kb) = key_block { + self.archive_key_block(&kb.block_id, kb.proof_data, kb.block_data).await?; + } + let key_block_ms = t.elapsed().as_millis(); + + if self.move_files { + if let Err(e) = tokio::fs::remove_file(&group.mc_package.path).await { + log::warn!( + target: TARGET, + "Failed to remove MC pack {} after import: {}", + group.mc_package.path.display(), + e, + ); + } + } + + log::info!( + target: TARGET, + "Imported archive {} ({} MC, {} shard blocks, {} shard pkgs) total {:#?}: \ + parse_mc {}ms, mc_handles {}ms, \ + parallel {}ms (shard_pipeline {}ms), key_block {}ms", + group.archive_id, + mc_block_count, + shard_block_count, + group.shard_packages.len(), + group_start.elapsed(), + parse_mc_ms, + mc_handles_ms, + parallel_ms, + shard_pipeline_ms, + key_block_ms, + ); + + Ok((validator, last_group_state)) + } + + async fn archive_key_block( + &self, + block_id: &BlockIdExt, + proof_data: Vec, + block_data: Vec, + ) -> Result<()> { + let handle = self.block_handle_storage.load_handle_by_id(block_id)?.ok_or_else(|| { + error!("Block handle not found for key block {} during key archive creation", block_id) + })?; + self.archive_manager + .add_block_data_to_package( + proof_data, + &handle, + &PackageEntryId::Proof(block_id.clone()), + true, + ) + .await?; + self.archive_manager + .add_block_data_to_package( + block_data, + &handle, + &PackageEntryId::Block(block_id.clone()), + true, + ) + .await?; + Ok(()) + } + + fn update_block_handles(&self, entry: &ProcessedEntry) -> Result<()> { + let meta = BlockMeta::for_import( + entry.gen_utime, + entry.end_lt, + entry.mc_ref_seq_no, + entry.is_key_block, + entry.block_id.is_masterchain(), + entry.prevs.len() > 1, + ); + + if let Some(handle) = + self.block_handle_storage.create_handle(entry.block_id.clone(), meta, None)? + { + log::trace!( + target: TARGET, + "Created block handle for {} (key={})", + entry.block_id, + entry.is_key_block, + ); + let _ = handle; + } + + let prev1 = entry + .prevs + .first() + .ok_or_else(|| error!("Block {} has no prev refs", entry.block_id))?; + + self.prev1_block_db.put(&entry.block_id, &prev1.serialize())?; + self.store_next_link(&entry.block_id, prev1)?; + + if let Some(prev2) = entry.prevs.get(1) { + self.prev2_block_db.put(&entry.block_id, &prev2.serialize())?; + self.store_next_link(&entry.block_id, prev2)?; + } + + Ok(()) + } + + fn store_next_link(&self, block_id: &BlockIdExt, prev_id: &BlockIdExt) -> Result<()> { + let prev_handle = + self.block_handle_storage.load_handle_by_id(prev_id)?.ok_or_else(|| { + error!("Block handle not found for prev block {} of {}", prev_id, block_id) + })?; + + let prev_shard = prev_id.shard(); + let shard = block_id.shard(); + if prev_shard != shard && prev_shard.split()?.1 == *shard { + // After split: right child โ†’ next2 + self.next2_block_db.put(prev_id, &block_id.serialize())?; + prev_handle.set_next2(); + } else { + // Simple chain or after merge or left child โ†’ next1 + self.next1_block_db.put(prev_id, &block_id.serialize())?; + prev_handle.set_next1(); + } + self.block_handle_storage.save_handle(&prev_handle, None)?; + Ok(()) + } +} diff --git a/src/node/src/archive_import/mod.rs b/src/node/src/archive_import/mod.rs new file mode 100644 index 0000000..722e52e --- /dev/null +++ b/src/node/src/archive_import/mod.rs @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +pub mod ingester; +pub mod scanner; +pub mod validator; + +use crate::{ + block_proof::BlockProofStuff, + collator_test_bundle::create_engine_allocated, + config::TonNodeGlobalConfig, + engine_traits::EngineAlloc, + internal_db::{ + ARCHIVE_CELLS_CF_NAME, ARCHIVE_SHARDSTATE_CF_NAME, CURRENT_DB_VERSION, DB_VERSION, + }, + shard_state::ShardStateStuff, +}; +#[cfg(feature = "telemetry")] +use crate::{collator_test_bundle::create_engine_telemetry, engine_traits::EngineTelemetry}; +use ingester::{Ingester, LastGroupState}; +use std::{ + collections::HashMap, + path::PathBuf, + sync::{atomic::AtomicU8, Arc}, +}; +use storage::{ + archive_shardstate_db::ArchiveShardStateDb, + archives::{ + archive_manager::ArchiveManager, + db_provider::EpochDbProvider, + epoch::{ArchivalModeConfig, EpochRouter}, + ARCHIVE_PACKAGE_SIZE, + }, + block_handle_db::{ + BlockHandleDb, BlockHandleStorage, NodeStateDb, BLOCK_HANDLE_DB_NAME, + VALIDATOR_STATE_DB_NAME, + }, + block_info_db::{ + BlockInfoDb, NEXT1_BLOCK_DB_NAME, NEXT2_BLOCK_DB_NAME, PREV1_BLOCK_DB_NAME, + PREV2_BLOCK_DB_NAME, + }, + db::rocksdb::{AccessType, RocksDb, NODE_DB_NAME}, + shardstate_db_async::CellsDbConfig, + traits::Serializable, + types::BlockMeta, +}; +use ton_block::{ + error, AccountIdPrefixFull, Block, BlockIdExt, Deserializable, Result, ShardIdent, UInt256, + WorkchainDescr, MASTERCHAIN_ID, SHARD_FULL, +}; +use validator::ValidatorState; + +const TARGET: &str = "archive_import"; + +pub struct ImportConfig { + pub archives_path: PathBuf, + pub epochs_path: PathBuf, + pub epoch_size: u32, + pub node_db_path: PathBuf, + pub mc_zerostate_path: PathBuf, + pub wc_zerostate_paths: Vec, + pub global_config_path: PathBuf, + pub skip_validation: bool, + pub move_files: bool, +} + +fn read_wc_zerostates_from_config(mc_zerostate: &ShardStateStuff) -> Result> { + // shard_hashes is empty at genesis; workchain zerostates are in ConfigParams::workchains() + let mut shards = Vec::new(); + mc_zerostate.config_params()?.workchains()?.iterate_with_keys( + |wc_id: i32, descr: WorkchainDescr| { + let shard = ShardIdent::with_tagged_prefix(wc_id, SHARD_FULL)?; + shards.push(BlockIdExt::with_params( + shard, + 0, + descr.zerostate_root_hash, + descr.zerostate_file_hash, + )); + Ok(true) + }, + )?; + Ok(shards) +} + +async fn build_initial_group_state( + zerostate: &ShardStateStuff, + archive_manager: &ArchiveManager, + last_imported: u32, +) -> Result { + if last_imported == 0 { + let shard_tops = read_wc_zerostates_from_config(zerostate)?; + log::info!( + target: TARGET, + "Initial state from zerostate {}, {} workchain shard tops", + zerostate.block_id(), + shard_tops.len(), + ); + return Ok(LastGroupState { mc_block_id: zerostate.block_id().clone(), shard_tops }); + } + + let mc_prefix = AccountIdPrefixFull { workchain_id: MASTERCHAIN_ID, prefix: 0 }; + let (block_id, block_data) = archive_manager + .lookup_block_by_seqno(&mc_prefix, last_imported) + .await? + .ok_or_else(|| error!("Cannot find MC block at seqno {}", last_imported))?; + let block = Block::construct_from_bytes(&block_data)?; + let extra = block + .read_extra()? + .read_custom()? + .ok_or_else(|| error!("No McExtra in MC block {}", block_id))?; + let shard_tops = + crate::shard_state::ShardHashesStuff::from(extra.shards().clone()).top_blocks_all()?; + log::info!( + target: TARGET, + "Resuming from MC block {} (seqno {}), {} shard tops", + block_id, + last_imported, + shard_tops.len(), + ); + Ok(LastGroupState { mc_block_id: block_id, shard_tops }) +} + +fn process_zerostates( + config: &ImportConfig, + global_config: &TonNodeGlobalConfig, + archive_state_db: &ArchiveShardStateDb, + block_handle_storage: &BlockHandleStorage, + #[cfg(feature = "telemetry")] engine_telemetry: Arc, + engine_allocated: Arc, +) -> Result> { + log::info!(target: TARGET, "Loading MC zerostate from {}", config.mc_zerostate_path.display()); + let zerostate_bytes = std::fs::read(&config.mc_zerostate_path).map_err(|e| { + error!("Cannot read MC zerostate file {}: {}", config.mc_zerostate_path.display(), e) + })?; + let expected_mc_zerostate_id = global_config.zero_state()?; + let mc_zerostate = ShardStateStuff::deserialize_zerostate( + expected_mc_zerostate_id.clone(), + &zerostate_bytes, + #[cfg(feature = "telemetry")] + &engine_telemetry, + &engine_allocated, + )?; + log::info!(target: TARGET, "MC zerostate loaded successfully"); + + // Load and validate workchain zerostates + let mut expected_wc_zerostates: HashMap = HashMap::from_iter( + read_wc_zerostates_from_config(&mc_zerostate)? + .into_iter() + .map(|id| (id.file_hash.clone(), id)), + ); + + let mut wc_zerostates = Vec::new(); + for path in &config.wc_zerostate_paths { + log::info!(target: TARGET, "Loading workchain zerostate from {}", path.display()); + let zerostate_bytes = std::fs::read(path) + .map_err(|e| error!("Cannot read WC zerostate file {}: {}", path.display(), e))?; + let id = expected_wc_zerostates.remove(&UInt256::calc_file_hash(&zerostate_bytes)).ok_or_else(|| { + error!( + "Workchain zerostate file {} does not match any expected file hash from MC zerostate", + path.display(), + ) + })?; + let state = ShardStateStuff::deserialize_zerostate( + id.clone(), + &zerostate_bytes, + #[cfg(feature = "telemetry")] + &engine_telemetry, + &engine_allocated, + )?; + wc_zerostates.push((id, state.root_cell().clone())); + } + + if !expected_wc_zerostates.is_empty() { + let missing: Vec<_> = expected_wc_zerostates.into_iter().collect(); + return Err(error!("Missing workchain zerostates: {:?}", missing,)); + } + + let save_handle = |id: &BlockIdExt| -> Result<()> { + let handle = if let Some(handle) = + block_handle_storage.create_handle(id.clone(), BlockMeta::default(), None)? + { + handle + } else { + block_handle_storage + .load_handle_by_id(&id)? + .ok_or_else(|| error!("Failed to create or load block handle for MC zerostate"))? + }; + if handle.set_state() | handle.set_state_saved() | handle.set_block_applied() { + block_handle_storage.save_handle(&handle, None)?; + } + Ok(()) + }; + + archive_state_db.put(&expected_mc_zerostate_id, mc_zerostate.root_cell().clone())?; + save_handle(&expected_mc_zerostate_id)?; + log::info!(target: TARGET, "MC zerostate saved to archive state DB"); + + for (wc_id, wc_root) in wc_zerostates { + archive_state_db.put(&wc_id, wc_root)?; + save_handle(&wc_id)?; + log::info!(target: TARGET, "Workchain zerostate {} saved to archive state DB", wc_id); + } + + Ok(mc_zerostate) +} + +/// Returns the node_db Arc so the caller can wait for all background tasks to release it. +pub async fn run_import(config: ImportConfig) -> Result> { + log::info!( + target: TARGET, + "Loading global config from {}", + config.global_config_path.display() + ); + let global_config = TonNodeGlobalConfig::from_json_file(&config.global_config_path) + .map_err(|e| error!("Cannot load global config: {}", e))?; + let expected_zerostate_id = global_config.zero_state()?; + let mut hardforks = global_config.hardforks()?; + hardforks.sort_by_key(|hf| hf.seq_no()); + log::info!( + target: TARGET, + "Global config: zerostate={}, {} hard fork(s)", + expected_zerostate_id, + hardforks.len(), + ); + + #[cfg(feature = "telemetry")] + let engine_telemetry = create_engine_telemetry(); + let engine_allocated = create_engine_allocated(); + + let epoch_config = ArchivalModeConfig { + epoch_size: config.epoch_size, + new_epochs_path: config.epochs_path.clone(), + existing_epochs: vec![], + }; + let router = Arc::new(EpochRouter::new(&epoch_config).await?); + let db_provider = Arc::new(EpochDbProvider::new(router)); + + std::fs::create_dir_all(&config.node_db_path).map_err(|e| { + error!("Cannot create node_db_path {}: {}", config.node_db_path.display(), e) + })?; + let node_db = RocksDb::new(&config.node_db_path, NODE_DB_NAME, None, AccessType::ReadWrite)?; + + let handle_db = Arc::new(BlockHandleDb::with_db(node_db.clone(), BLOCK_HANDLE_DB_NAME, true)?); + let full_node_state_db = Arc::new(NodeStateDb::with_db( + node_db.clone(), + storage::db::rocksdb::NODE_STATE_DB_NAME, + true, + )?); + full_node_state_db.put(&DB_VERSION, &CURRENT_DB_VERSION.serialize())?; + let validator_state_db = + Arc::new(NodeStateDb::with_db(node_db.clone(), VALIDATOR_STATE_DB_NAME, true)?); + + let prev1_block_db = BlockInfoDb::with_db(node_db.clone(), PREV1_BLOCK_DB_NAME, true)?; + let prev2_block_db = BlockInfoDb::with_db(node_db.clone(), PREV2_BLOCK_DB_NAME, true)?; + let next1_block_db = BlockInfoDb::with_db(node_db.clone(), NEXT1_BLOCK_DB_NAME, true)?; + let next2_block_db = BlockInfoDb::with_db(node_db.clone(), NEXT2_BLOCK_DB_NAME, true)?; + + #[cfg(feature = "telemetry")] + let storage_telemetry = engine_telemetry.storage.clone(); + let storage_alloc = engine_allocated.storage.clone(); + + let mut block_handle_storage = BlockHandleStorage::with_dbs( + handle_db, + full_node_state_db, + validator_state_db, + #[cfg(feature = "telemetry")] + storage_telemetry.clone(), + storage_alloc.clone(), + ); + block_handle_storage.set_no_cache(); + let block_handle_storage = Arc::new(block_handle_storage); + + let db_root_path = Arc::new(config.node_db_path.clone()); + let shard_split_depth = Arc::new(AtomicU8::new(0)); + + let archive_manager = Arc::new( + ArchiveManager::with_data( + node_db.clone(), + db_root_path, + db_provider, + 0, // last_unneeded_key_block + shard_split_depth, + #[cfg(feature = "telemetry")] + storage_telemetry, + storage_alloc, + ) + .await?, + ); + + let cells_db_config = CellsDbConfig::default(); + let archive_states_db = RocksDb::new( + &config.node_db_path, + crate::internal_db::ARCHIVE_STATES_DB_NAME, + std::collections::HashMap::from([( + ARCHIVE_CELLS_CF_NAME.to_string(), + storage::cell_db::CellDb::build_cf_options(cells_db_config.cells_cache_size_bytes), + )]), + AccessType::ReadWrite, + )?; + let archive_state_db = Arc::new(ArchiveShardStateDb::new( + archive_states_db, + ARCHIVE_SHARDSTATE_CF_NAME, + ARCHIVE_CELLS_CF_NAME, + &config.node_db_path, + &cells_db_config, + #[cfg(feature = "telemetry")] + engine_telemetry.storage.clone(), + engine_allocated.storage.clone(), + )?); + + let mc_zerostate = process_zerostates( + &config, + &global_config, + &archive_state_db, + &block_handle_storage, + #[cfg(feature = "telemetry")] + engine_telemetry.clone(), + engine_allocated.clone(), + )?; + + log::info!(target: TARGET, "Scanning packages in {}", config.archives_path.display()); + let packages = scanner::scan_packages(&config.archives_path)?; + log::info!(target: TARGET, "Found {} package files", packages.len()); + + if packages.is_empty() { + log::warn!(target: TARGET, "No packages found, nothing to import"); + return Ok(node_db); + } + + let groups = scanner::group_by_archive_id(packages)?; + log::info!(target: TARGET, "Grouped into {} archive groups", groups.len()); + + let mut validator_state = ValidatorState::new(mc_zerostate.clone(), hardforks); + let mut skip_count = 0; + + let last_imported = if let Some(max_seqno) = archive_manager.get_max_mc_seqno().await { + if max_seqno > groups.last().unwrap().archive_id + ARCHIVE_PACKAGE_SIZE as u32 { + log::warn!(target: TARGET, + "Existing import detected with max MC seqno {}, which is beyond the last archive group ({}), skipping all groups", + max_seqno, groups.last().unwrap().archive_id); + return Ok(node_db); + } + skip_count = + groups.iter().take_while(|g| g.archive_id < max_seqno).count().saturating_sub(1); + + log::info!( + target: TARGET, + "Detected existing import (max MC seqno = {}), skipping {} groups", + max_seqno, + skip_count, + ); + + // Restore key block proof regardless of skip_count: files may have been moved + // and the scanned list may start mid-chain. + if !config.skip_validation { + if let Some(key_seqno) = archive_manager.get_max_key_block_seqno().await { + let mc_prefix = AccountIdPrefixFull { workchain_id: MASTERCHAIN_ID, prefix: 0 }; + let (block_id, proof_data) = archive_manager + .lookup_proof_by_seqno(&mc_prefix, key_seqno) + .await? + .ok_or_else(|| { + error!( + "Key block seqno {} found in index but proof not readable", + key_seqno, + ) + })?; + let proof = BlockProofStuff::deserialize(&block_id, proof_data, false)?; + log::info!( + target: TARGET, + "Restored key block proof: {}", + block_id, + ); + validator_state.set_key_block_proof(proof); + } + } + groups[skip_count].archive_id.saturating_sub(1) + } else { + 0 + }; + + let initial_group_state = + build_initial_group_state(&mc_zerostate, &archive_manager, last_imported).await?; + + let ingester = Ingester::new( + archive_manager, + block_handle_storage, + archive_state_db, + prev1_block_db, + prev2_block_db, + next1_block_db, + next2_block_db, + config.move_files, + config.skip_validation, + ); + + let total = groups.len(); + ingester + .run_groups(&groups[skip_count..], skip_count, total, validator_state, initial_group_state) + .await?; + + log::info!(target: TARGET, "Import complete! Processed {} archive groups", total); + Ok(node_db) +} + +#[cfg(test)] +#[path = "../tests/test_archive_import.rs"] +mod tests; diff --git a/src/node/src/archive_import/scanner.rs b/src/node/src/archive_import/scanner.rs new file mode 100644 index 0000000..077e793 --- /dev/null +++ b/src/node/src/archive_import/scanner.rs @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + str::FromStr, +}; +use ton_block::{error, fail, Result, ShardIdent}; + +pub struct PackageFile { + pub path: PathBuf, + pub archive_id: u32, + pub shard: ShardIdent, +} + +pub struct PackageGroup { + pub archive_id: u32, + pub mc_package: PackageFile, + pub shard_packages: Vec, +} + +/// Parse a .pack filename into (archive_id, shard). +fn parse_pack_filename(filename: &str) -> Result> { + if !filename.ends_with(".pack") { + return Ok(None); + } + let stem = &filename[..filename.len() - 5]; + + if stem.starts_with("key.") { + return Ok(None); + } + + if !stem.starts_with("archive.") { + return Ok(None); + } + let rest = &stem[8..]; + + if let Some(dot_pos) = rest.find('.') { + // archive.NNNNN.WC:HHHHHHHHHHHHHHHH - shards + let id_str = &rest[..dot_pos]; + let shard_str = &rest[dot_pos + 1..]; + + let archive_id: u32 = + id_str.parse().map_err(|_| error!("Invalid archive id in filename: {}", filename))?; + + let shard = ShardIdent::from_str(shard_str)?; + Ok(Some((archive_id, shard))) + } else { + // archive.NNNNN โ€” masterchain + let archive_id: u32 = + rest.parse().map_err(|_| error!("Invalid archive id in filename: {}", filename))?; + Ok(Some((archive_id, ShardIdent::masterchain()))) + } +} + +/// Scan the source directory for .pack files, parse filenames, sort by archive_id. +pub fn scan_packages(archives_path: &Path) -> Result> { + let entries = std::fs::read_dir(archives_path) + .map_err(|e| error!("Cannot read archives directory {}: {}", archives_path.display(), e))?; + + let mut packages = Vec::new(); + + for entry in entries { + let entry = entry.map_err(|e| error!("Error reading directory entry: {}", e))?; + let path = entry.path(); + + if !path.is_file() { + continue; + } + + let filename = match path.file_name().and_then(|n| n.to_str()) { + Some(name) => name.to_string(), + None => continue, + }; + + if let Some((archive_id, shard)) = parse_pack_filename(&filename)? { + packages.push(PackageFile { path, archive_id, shard }); + } + } + + // Sort by archive_id, then MC before shards + packages.sort_by(|a, b| { + a.archive_id.cmp(&b.archive_id).then_with(|| { + let a_mc = a.shard.is_masterchain() as u8; + let b_mc = b.shard.is_masterchain() as u8; + b_mc.cmp(&a_mc) // MC first + }) + }); + + Ok(packages) +} + +/// Group packages by archive_id: each group has one MC package and zero or more shard packages. +pub fn group_by_archive_id(packages: Vec) -> Result> { + let mut map: BTreeMap, Vec)> = BTreeMap::new(); + + for pkg in packages { + let entry = map.entry(pkg.archive_id).or_insert_with(|| (None, Vec::new())); + if pkg.shard.is_masterchain() { + if entry.0.is_some() { + fail!("Duplicate MC package for archive_id {}", pkg.archive_id); + } + entry.0 = Some(pkg); + } else { + entry.1.push(pkg); + } + } + + let mut groups = Vec::with_capacity(map.len()); + for (archive_id, (mc_package, shard_packages)) in map { + let mc_package = mc_package + .ok_or_else(|| error!("No MC package found for archive_id {}", archive_id))?; + groups.push(PackageGroup { archive_id, mc_package, shard_packages }); + } + + Ok(groups) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_mc_filename() { + let (id, shard) = parse_pack_filename("archive.00100.pack").unwrap().unwrap(); + assert_eq!(id, 100); + assert!(shard.is_masterchain()); + } + + #[test] + fn test_parse_shard_filename_with_wc() { + let (id, shard) = + parse_pack_filename("archive.00100.0:8000000000000000.pack").unwrap().unwrap(); + assert_eq!(id, 100); + assert!(!shard.is_masterchain()); + assert_eq!(shard.workchain_id(), 0); + assert_eq!(shard.shard_prefix_with_tag(), 0x8000000000000000); + } + + #[test] + fn test_parse_key_filename_skipped() { + assert!(parse_pack_filename("key.archive.000000.pack").unwrap().is_none()); + } + + #[test] + fn test_parse_non_pack_file() { + assert!(parse_pack_filename("readme.txt").unwrap().is_none()); + } +} diff --git a/src/node/src/archive_import/validator.rs b/src/node/src/archive_import/validator.rs new file mode 100644 index 0000000..9ff74fe --- /dev/null +++ b/src/node/src/archive_import/validator.rs @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use crate::{block_proof::BlockProofStuff, shard_state::ShardStateStuff}; +use std::sync::Arc; +use ton_block::{BlockIdExt, BlockInfo, Result}; + +pub struct ValidatorState { + zerostate: Arc, + current_key_block_proof: Option, + hardforks: Vec, +} + +impl ValidatorState { + pub fn new(zerostate: Arc, hardforks: Vec) -> Self { + Self { zerostate, current_key_block_proof: None, hardforks } + } + + pub(crate) fn is_hardfork(&self, block_id: &BlockIdExt) -> bool { + self.hardforks.iter().any(|hf| hf == block_id) + } + + pub(crate) fn zerostate(&self) -> &Arc { + &self.zerostate + } + + pub(crate) fn current_key_block_proof(&self) -> Option<&BlockProofStuff> { + self.current_key_block_proof.as_ref() + } + + pub fn set_key_block_proof(&mut self, proof: BlockProofStuff) { + self.current_key_block_proof = Some(proof); + } + + pub fn validate_mc_proof(&mut self, proof: &BlockProofStuff) -> Result { + let (virt_block, _virt_root) = proof.virtualize_block()?; + let info = virt_block.read_info()?; + + let prev_key_block_seqno = info.prev_key_block_seqno(); + + if prev_key_block_seqno == 0 { + proof.check_with_master_state(&self.zerostate)?; + } else { + let prev_key_proof = self.current_key_block_proof.as_ref().ok_or_else(|| { + ton_block::error!( + "No key block proof available for validation of block {} \ + (prev_key_block_seqno = {})", + proof.id(), + prev_key_block_seqno + ) + })?; + proof.check_with_prev_key_block_proof(prev_key_proof)?; + } + + if info.key_block() { + self.current_key_block_proof = Some(proof.clone()); + } + + Ok(info) + } + + pub fn extract_mc_info(&mut self, proof: &BlockProofStuff) -> Result { + let (virt_block, _virt_root) = proof.virtualize_block()?; + let info = virt_block.read_info()?; + + if info.key_block() { + self.current_key_block_proof = Some(proof.clone()); + } + + Ok(info) + } + + pub fn validate_shard_proof_link(&self, proof: &BlockProofStuff) -> Result<()> { + proof.check_proof_link() + } +} diff --git a/src/node/src/config.rs b/src/node/src/config.rs index 4ca45ab..db4bd8e 100644 --- a/src/node/src/config.rs +++ b/src/node/src/config.rs @@ -44,7 +44,7 @@ use std::{ }, time::Duration, }; -use storage::shardstate_db_async::CellsDbConfig; +use storage::{archives::epoch::ArchivalModeConfig, shardstate_db_async::CellsDbConfig}; use ton_api::{ ton::{ self, @@ -408,6 +408,8 @@ pub struct TonNodeConfig { sync_by_archives: bool, #[serde(default)] accelerated_consensus_disabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + archival_mode: Option, #[serde(skip)] custom_overlays: CustomOverlaysConfigBoxed, #[serde(default)] @@ -619,6 +621,10 @@ impl TonNodeConfig { } pub fn gc_archives_life_time_hours(&self) -> Option { + // GC disabled in archival mode + if self.archival_mode.is_some() { + return None; + } if let Some(gc) = &self.gc { if gc.enable_for_archives { return gc.archives_life_time_hours.or(Some(0)); @@ -627,6 +633,10 @@ impl TonNodeConfig { None } + pub fn archival_mode(&self) -> Option<&ArchivalModeConfig> { + self.archival_mode.as_ref() + } + pub fn internal_db_path(&self) -> &str { self.internal_db_path.as_deref().unwrap_or(Self::DEFAULT_DB_ROOT) } diff --git a/src/node/src/engine.rs b/src/node/src/engine.rs index 862497d..9bb2c63 100644 --- a/src/node/src/engine.rs +++ b/src/node/src/engine.rs @@ -518,7 +518,11 @@ impl Engine { }); let archives_life_time_hours = general_config.gc_archives_life_time_hours(); - let cells_lifetime_sec = general_config.cells_gc_config().cells_lifetime_sec; + let cells_lifetime_sec = if general_config.archival_mode().is_none() { + general_config.cells_gc_config().cells_lifetime_sec + } else { + u64::MAX + }; let enable_shard_state_persistent_gc = general_config.enable_shard_state_persistent_gc(); let skip_saving_persistent_states = general_config.skip_saving_persistent_states(); let states_cache_mode = general_config.states_cache_mode(); @@ -529,6 +533,7 @@ impl Engine { db_directory: general_config.internal_db_path().to_string(), cells_gc_interval_sec: general_config.cells_gc_config().gc_interval_sec, cells_db_config: cells_db_config.clone(), + archival_mode: general_config.archival_mode().cloned(), }; let control_config = general_config.control_server()?; let collator_config = general_config.collator_config().clone(); diff --git a/src/node/src/engine_operations.rs b/src/node/src/engine_operations.rs index 00d0fcc..668eaed 100644 --- a/src/node/src/engine_operations.rs +++ b/src/node/src/engine_operations.rs @@ -747,6 +747,16 @@ impl EngineOperations for Engine { Ok(state) } + async fn store_state_update( + &self, + handle: &Arc, + state_update: Cell, + ) -> Result<()> { + self.db().store_state_update(handle, state_update).await?; + self.shard_states_awaiters().shunt_async(handle.id(), self.load_state(handle.id())).await?; + Ok(()) + } + async fn store_zerostate( &self, state: Arc, @@ -1248,6 +1258,10 @@ impl EngineOperations for Engine { ) -> Result<()> { self.update_public_overlays(keyblock_id, config).await } + + fn is_archival_mode(&self) -> bool { + self.db().is_archival_mode() + } } async fn redirect_external_message( diff --git a/src/node/src/engine_traits.rs b/src/node/src/engine_traits.rs index b3158b0..6f6ca39 100644 --- a/src/node/src/engine_traits.rs +++ b/src/node/src/engine_traits.rs @@ -636,6 +636,13 @@ pub trait EngineOperations: Sync + Send { ) -> Result> { unimplemented!() } + async fn store_state_update( + &self, + handle: &Arc, + state_update: Cell, + ) -> Result<()> { + unimplemented!() + } async fn store_zerostate( &self, state: Arc, @@ -991,6 +998,10 @@ pub trait EngineOperations: Sync + Send { ) -> Result<()> { Ok(()) } + + fn is_archival_mode(&self) -> bool { + false + } } #[async_trait::async_trait] diff --git a/src/node/src/full_node/apply_block.rs b/src/node/src/full_node/apply_block.rs index f55e74e..8a58f03 100644 --- a/src/node/src/full_node/apply_block.rs +++ b/src/node/src/full_node/apply_block.rs @@ -37,7 +37,7 @@ pub async fn apply_block( check_prev_blocks(&prev_ids, engine, mc_seq_no, pre_apply, recursion_depth).await?; if !handle.has_state() { - calc_shard_state(handle, block, &prev_ids, engine).await?; + store_state_update(handle, block, &prev_ids, engine).await?; } set_prev_ids(handle, &prev_ids, engine.deref())?; if !pre_apply { @@ -92,75 +92,87 @@ async fn check_prev_blocks( Ok(()) } -// Gets prev block(s) state and applies merkle update from block to calculate new state -pub async fn calc_shard_state( +// Normal mode - gets prev block(s) state and applies merkle update from block to calculate new state +// Archival mode - just saves state update from block, without applying it +pub async fn store_state_update( handle: &Arc, block: &BlockStuff, prev_ids: &(BlockIdExt, Option), engine: &Arc, -) -> Result<(Arc, (Arc, Option>))> { +) -> Result<()> { let block_descr = fmt_block_id_short(block.id()); - log::debug!("({}): calc_shard_state: block: {}", block_descr, block.id()); - - let (prev_ss_root, prev_ss) = match prev_ids { - (prev1, Some(prev2)) => { - let ss1 = engine.clone().wait_state(prev1, None, true).await?; - let ss2 = engine.clone().wait_state(prev2, None, true).await?; - let root = ShardStateStuff::construct_split_root( - ss1.root_cell().clone(), - ss2.root_cell().clone(), - )?; - (root, (ss1, Some(ss2))) - } - (prev, None) => { - let ss = engine.clone().wait_state(prev, None, true).await?; - (ss.root_cell().clone(), (ss, None)) - } - }; + log::debug!("({}): store_state_update: block: {}", block_descr, block.id()); - let merkle_update = block.block()?.read_state_update()?; - let block_id = block.id().clone(); - let engine_cloned = engine.clone(); - - let block_descr_clone = block_descr.clone(); - let ss = tokio::task::spawn_blocking(move || -> Result> { - let now = std::time::Instant::now(); - let cf = engine_cloned.db_cells_factory()?; - let cl = engine_cloned.db_cells_loader()?; - let (ss_root, _metrics) = - merkle_update.apply_for_ex(&prev_ss_root, &cf, cl.deref()).map_err(|e| { - error!( - "Error applying Merkle update for block {}: {}\ - prev_ss_root: {:#.2}\ - merkle_update: {}", - block_id, e, prev_ss_root, merkle_update - ) - })?; - let elapsed = now.elapsed(); + if engine.is_archival_mode() { + log::debug!("({}): store_state_update: store_state_update: {}", block_descr, handle.id()); + engine.store_state_update(handle, block.block()?.read_state_update()?.new).await?; log::debug!( - "({}): TIME: calc_shard_state: applied Merkle update {}ms {}", - block_descr_clone, - elapsed.as_millis(), - block_id + "({}): store_state_update: store_state_update: {} done", + block_descr, + handle.id() ); - #[cfg(feature = "telemetry")] - log::debug!(target: "telemetry", "({}): applying Merkle update: \n{}", block_descr_clone, _metrics); - metrics::histogram!("ton_node_db_calc_merkle_update_seconds").record(elapsed); - ShardStateStuff::from_root_cell( - block_id.clone(), - ss_root, + } else { + let prev_ss_root = match prev_ids { + (prev1, Some(prev2)) => { + let ss1 = engine.clone().wait_state(prev1, None, true).await?; + let ss2 = engine.clone().wait_state(prev2, None, true).await?; + let root = ShardStateStuff::construct_split_root( + ss1.root_cell().clone(), + ss2.root_cell().clone(), + )?; + root + } + (prev, None) => { + let ss = engine.clone().wait_state(prev, None, true).await?; + ss.root_cell().clone() + } + }; + + let merkle_update = block.block()?.read_state_update()?; + let block_id = block.id().clone(); + let engine_cloned = engine.clone(); + + let block_descr_clone = block_descr.clone(); + let ss = tokio::task::spawn_blocking(move || -> Result> { + let now = std::time::Instant::now(); + let cf = engine_cloned.db_cells_factory()?; + let cl = engine_cloned.db_cells_loader()?; + let (ss_root, _metrics) = + merkle_update.apply_for_ex(&prev_ss_root, &cf, cl.deref()).map_err(|e| { + error!( + "Error applying Merkle update for block {}: {}\ + prev_ss_root: {:#.2}\ + merkle_update: {}", + block_id, e, prev_ss_root, merkle_update + ) + })?; + let elapsed = now.elapsed(); + log::debug!( + "({}): TIME: store_state_update: applied Merkle update {}ms {}", + block_descr_clone, + elapsed.as_millis(), + block_id + ); #[cfg(feature = "telemetry")] - engine_cloned.engine_telemetry(), - engine_cloned.engine_allocated(), - ) - }) - .await??; + log::debug!(target: "telemetry", "({}): applying Merkle update: \n{}", block_descr_clone, _metrics); + metrics::histogram!("ton_node_db_calc_merkle_update_seconds").record(elapsed); + ShardStateStuff::from_root_cell( + block_id.clone(), + ss_root, + #[cfg(feature = "telemetry")] + engine_cloned.engine_telemetry(), + engine_cloned.engine_allocated(), + ) + }) + .await??; - log::debug!("({}): calc_shard_state: store_state: {}", block_descr, handle.id()); - let ss = engine.store_state(handle, ss).await?; - log::debug!("({}): calc_shard_state: store_state: {} done", block_descr, handle.id()); - Ok((ss, prev_ss)) + log::debug!("({}): store_state_update: store_state: {}", block_descr, handle.id()); + engine.store_state(handle, ss).await?; + log::debug!("({}): store_state_update: store_state: {} done", block_descr, handle.id()); + } + + Ok(()) } // set next block ids for prev blocks diff --git a/src/node/src/internal_db/mod.rs b/src/node/src/internal_db/mod.rs index e80c567..6c0d0c9 100644 --- a/src/node/src/internal_db/mod.rs +++ b/src/node/src/internal_db/mod.rs @@ -34,16 +34,28 @@ use std::{ #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - archives::{archive_manager::ArchiveManager, package_entry_id::PackageEntryId}, - block_handle_db::{self, BlockHandle, BlockHandleDb, BlockHandleStorage, NodeStateDb}, - block_info_db::BlockInfoDb, + archive_shardstate_db::ArchiveShardStateDb, + archives::{ + archive_manager::ArchiveManager, + db_provider::{ArchiveDbProvider, EpochDbProvider, SingleDbProvider}, + epoch::{ArchivalModeConfig, EpochRouter}, + package_entry_id::PackageEntryId, + }, + block_handle_db::{ + self, BlockHandle, BlockHandleDb, BlockHandleStorage, NodeStateDb, BLOCK_HANDLE_DB_NAME, + VALIDATOR_STATE_DB_NAME, + }, + block_info_db::{ + BlockInfoDb, NEXT1_BLOCK_DB_NAME, NEXT2_BLOCK_DB_NAME, PREV1_BLOCK_DB_NAME, + PREV2_BLOCK_DB_NAME, + }, db::{ filedb::FileDb, - rocksdb::{AccessType, RocksDb}, + rocksdb::{AccessType, RocksDb, CATCHAINS_DB_NAME, NODE_DB_NAME}, }, dynamic_boc_rc_db::{AsyncCellsStorageAdapter, DynamicBocDb}, - shard_top_blocks_db::ShardTopBlocksDb, - shardstate_db_async::{AllowStateGcResolver, CellsDbConfig, ShardStateDb}, + shard_top_blocks_db::{ShardTopBlocksDb, SHARD_TOP_BLOCKS_DB_NAME}, + shardstate_db_async::{AllowStateGcResolver, CellsDbConfig, Job, ShardStateDb}, traits::Serializable, types::{BlockMeta, PersistentStatePartId, PersistentStatePartKey}, StorageAlloc, TimeChecker, @@ -67,8 +79,13 @@ pub const DB_VERSION: &str = "DbVersion"; pub const DB_VERSION_7: u32 = 7; // with block indexes pub const CURRENT_DB_VERSION: u32 = DB_VERSION_7; +pub const SHARDSTATE_DB_NAME: &str = "shardstate_db"; const CELLS_CF_NAME: &str = "cells_db_v6"; const CELLSCOUNTERS_CF_NAME: &str = "cells_db_v6_counters"; +const SHARD_STATE_PERSISTENT_DB_NAME: &str = "shard_state_persistent_db"; +pub const ARCHIVE_STATES_DB_NAME: &str = "archive_states"; +pub const ARCHIVE_CELLS_CF_NAME: &str = "archive_cells_db"; +pub const ARCHIVE_SHARDSTATE_CF_NAME: &str = "archive_shardstate_db"; /// Validator state keys pub(crate) const LAST_ROTATION_MC_BLOCK: &str = "LastRotationBlockId"; @@ -176,6 +193,59 @@ pub struct InternalDbConfig { pub db_directory: String, pub cells_gc_interval_sec: u32, pub cells_db_config: CellsDbConfig, + pub archival_mode: Option, +} + +pub enum StateDb { + Dynamic(Arc), + Archive(Arc), +} + +impl StateDb { + pub fn get(&self, id: &BlockIdExt) -> Result { + match self { + StateDb::Dynamic(db) => db.get(id), + StateDb::Archive(db) => db.get(id), + } + } + + pub fn get_cell(&self, id: &UInt256) -> Result { + match self { + StateDb::Dynamic(db) => db.get_cell(id), + StateDb::Archive(db) => db.get_cell(id), + } + } + + pub fn cells_factory(&self) -> Result> { + match self { + StateDb::Dynamic(db) => db.cells_factory(), + StateDb::Archive(db) => Ok(db.cells_factory()), + } + } + + pub fn create_hashed_cell_storage( + &self, + root: Option<&Cell>, + max_inmemory_cells: usize, + ) -> Result> { + match self { + StateDb::Dynamic(db) => { + Ok(Arc::new(db.create_hashed_cell_storage(root, max_inmemory_cells)?)) + } + StateDb::Archive(db) => { + Ok(Arc::new(db.create_hashed_cell_storage(root, max_inmemory_cells)?)) + } + } + } +} + +impl Clone for StateDb { + fn clone(&self) -> Self { + match self { + StateDb::Dynamic(db) => StateDb::Dynamic(db.clone()), + StateDb::Archive(db) => StateDb::Archive(db.clone()), + } + } } pub struct InternalDb { @@ -186,7 +256,7 @@ pub struct InternalDb { next1_block_db: BlockInfoDb, next2_block_db: BlockInfoDb, shard_state_persistent_db: Arc, - shard_state_dynamic_db: Arc, + state_db: StateDb, archive_manager: Arc, shard_top_blocks_db: ShardTopBlocksDb, full_node_state_db: Arc, @@ -258,28 +328,39 @@ impl InternalDb { allocated: Arc, ) -> Result { let mut cfs_opts = HashMap::new(); - cfs_opts.insert( - CELLS_CF_NAME.to_string(), - DynamicBocDb::build_cells_cf_options(&config.cells_db_config), - ); - cfs_opts.insert( - CELLSCOUNTERS_CF_NAME.to_string(), - DynamicBocDb::build_counters_cf_options(&config.cells_db_config), - ); + if config.archival_mode.is_none() { + cfs_opts.insert( + CELLS_CF_NAME.to_string(), + DynamicBocDb::build_cells_cf_options(&config.cells_db_config), + ); + cfs_opts.insert( + CELLSCOUNTERS_CF_NAME.to_string(), + DynamicBocDb::build_counters_cf_options(&config.cells_db_config), + ); + } let access_type = access_type.unwrap_or(AccessType::ReadWrite); let can_create_db = access_type == AccessType::ReadWrite; - let db = RocksDb::new(config.db_directory.as_str(), "db", cfs_opts, access_type.clone())?; - let db_catchain = - RocksDb::new(config.db_directory.as_str(), "catchains", None, access_type)?; + let db = RocksDb::new( + config.db_directory.as_str(), + NODE_DB_NAME, + cfs_opts, + access_type.clone(), + )?; + let db_catchain = RocksDb::new( + config.db_directory.as_str(), + CATCHAINS_DB_NAME, + None, + access_type.clone(), + )?; let block_handle_db = - Arc::new(BlockHandleDb::with_db(db.clone(), "block_handle_db", can_create_db)?); + Arc::new(BlockHandleDb::with_db(db.clone(), BLOCK_HANDLE_DB_NAME, can_create_db)?); let full_node_state_db = Arc::new(NodeStateDb::with_db( db.clone(), storage::db::rocksdb::NODE_STATE_DB_NAME, can_create_db, )?); let validator_state_db = - Arc::new(NodeStateDb::with_db(db_catchain, "validator_state_db", can_create_db)?); + Arc::new(NodeStateDb::with_db(db_catchain, VALIDATOR_STATE_DB_NAME, can_create_db)?); let block_handle_storage = Arc::new(BlockHandleStorage::with_dbs( block_handle_db.clone(), full_node_state_db.clone(), @@ -289,19 +370,52 @@ impl InternalDb { allocated.storage.clone(), )); - let shard_state_dynamic_db = Self::create_shard_state_dynamic_db( - db.clone(), - &config, - #[cfg(feature = "telemetry")] - telemetry.storage.clone(), - allocated.storage.clone(), - )?; + let state_db = if config.archival_mode.is_some() { + let states_db = RocksDb::new( + &config.db_directory, + ARCHIVE_STATES_DB_NAME, + std::collections::HashMap::from([( + ARCHIVE_CELLS_CF_NAME.to_string(), + storage::cell_db::CellDb::build_cf_options( + config.cells_db_config.cells_cache_size_bytes, + ), + )]), + access_type.clone(), + )?; + StateDb::Archive(Arc::new(ArchiveShardStateDb::new( + states_db, + ARCHIVE_SHARDSTATE_CF_NAME, + ARCHIVE_CELLS_CF_NAME, + &config.db_directory, + &config.cells_db_config, + #[cfg(feature = "telemetry")] + telemetry.storage.clone(), + allocated.storage.clone(), + )?)) + } else { + StateDb::Dynamic(Self::create_shard_state_dynamic_db( + db.clone(), + &config, + #[cfg(feature = "telemetry")] + telemetry.storage.clone(), + allocated.storage.clone(), + )?) + }; let last_unneeded_key_block_id = block_handle_storage.load_full_node_state(LAST_UNNEEDED_KEY_BLOCK)?.unwrap_or_default(); + let db_root_path = Arc::new(PathBuf::from(&config.db_directory)); + let db_provider: Arc = + if let Some(ref archival_config) = config.archival_mode { + let router = Arc::new(EpochRouter::new(archival_config).await?); + Arc::new(EpochDbProvider::new(router)) + } else { + Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())) + }; let archive_manager = Arc::new( ArchiveManager::with_data( db.clone(), - Arc::new(PathBuf::from(&config.db_directory)), + db_root_path, + db_provider, last_unneeded_key_block_id.seq_no(), monitor_min_split, #[cfg(feature = "telemetry")] @@ -314,18 +428,18 @@ impl InternalDb { let db = Self { db: db.clone(), block_handle_storage, - prev1_block_db: BlockInfoDb::with_db(db.clone(), "prev1_block_db", can_create_db)?, - prev2_block_db: BlockInfoDb::with_db(db.clone(), "prev2_block_db", can_create_db)?, - next1_block_db: BlockInfoDb::with_db(db.clone(), "next1_block_db", can_create_db)?, - next2_block_db: BlockInfoDb::with_db(db.clone(), "next2_block_db", can_create_db)?, + prev1_block_db: BlockInfoDb::with_db(db.clone(), PREV1_BLOCK_DB_NAME, can_create_db)?, + prev2_block_db: BlockInfoDb::with_db(db.clone(), PREV2_BLOCK_DB_NAME, can_create_db)?, + next1_block_db: BlockInfoDb::with_db(db.clone(), NEXT1_BLOCK_DB_NAME, can_create_db)?, + next2_block_db: BlockInfoDb::with_db(db.clone(), NEXT2_BLOCK_DB_NAME, can_create_db)?, shard_state_persistent_db: Arc::new(FileDb::with_path( - Path::new(config.db_directory.as_str()).join("shard_state_persistent_db"), + Path::new(config.db_directory.as_str()).join(SHARD_STATE_PERSISTENT_DB_NAME), )), - shard_state_dynamic_db, + state_db, archive_manager, shard_top_blocks_db: ShardTopBlocksDb::with_db( db.clone(), - "shard_top_blocks_db", + SHARD_TOP_BLOCKS_DB_NAME, can_create_db, )?, full_node_state_db, @@ -365,7 +479,7 @@ impl InternalDb { ) -> Result> { ShardStateDb::new( db, - "shardstate_db", + SHARDSTATE_DB_NAME, CELLS_CF_NAME, CELLSCOUNTERS_CF_NAME, &config.db_directory, @@ -377,36 +491,49 @@ impl InternalDb { } pub fn clean_shard_state_dynamic_db(&mut self) -> Result<()> { - if self.shard_state_dynamic_db.is_gc_run() { - fail!("It is forbidden to clear shard_state_dynamic_db while cells GC is running") - } + match &self.state_db { + StateDb::Dynamic(db) => { + if db.is_gc_run() { + fail!( + "It is forbidden to clear shard_state_dynamic_db while cells GC is running" + ) + } - if let Err(e) = self.db.drop_table_force("shardstate_db") { - log::warn!("Can't drop table \"shardstate_db\": {}", e); - } - if let Err(e) = self.db.drop_table_force(CELLS_CF_NAME) { - log::warn!("Can't drop table \"cells_db\": {}", e); - } - let _ = self.db.drop_table_force("cells_db1"); // depricated table, used in db versions 1 & 2 - self.full_node_state_db.put(&ASSUME_OLD_FORMAT_CELLS, &[0])?; + if let Err(e) = self.db.drop_table_force(SHARDSTATE_DB_NAME) { + log::warn!("Can't drop table \"shardstate_db\": {}", e); + } + if let Err(e) = self.db.drop_table_force(CELLS_CF_NAME) { + log::warn!("Can't drop table \"cells_db\": {}", e); + } + let _ = self.db.drop_table_force("cells_db1"); + self.full_node_state_db.put(&ASSUME_OLD_FORMAT_CELLS, &[0])?; - self.shard_state_dynamic_db = Self::create_shard_state_dynamic_db( - self.db.clone(), - &self.config, - #[cfg(feature = "telemetry")] - self.telemetry.storage.clone(), - self.allocated.storage.clone(), - )?; + self.state_db = StateDb::Dynamic(Self::create_shard_state_dynamic_db( + self.db.clone(), + &self.config, + #[cfg(feature = "telemetry")] + self.telemetry.storage.clone(), + self.allocated.storage.clone(), + )?); - Ok(()) + Ok(()) + } + StateDb::Archive(_) => { + fail!("clean_shard_state_dynamic_db is not supported in archival mode") + } + } } pub fn start_states_gc(&self, resolver: Arc) { - self.shard_state_dynamic_db.clone().start_gc(resolver, self.cells_gc_interval.clone()) + if let StateDb::Dynamic(db) = &self.state_db { + db.clone().start_gc(resolver, self.cells_gc_interval.clone()) + } } pub async fn stop_states_db(&self) { - self.shard_state_dynamic_db.stop().await + if let StateDb::Dynamic(db) = &self.state_db { + db.stop().await + } } fn store_block_handle( @@ -679,15 +806,35 @@ impl InternalDb { } let _lock = handle.saving_state_lock().lock().await; if force || !handle.has_saved_state() { - let callback = - SsCallback::new(handle.clone(), self.block_handle_storage.clone(), callback_ss); - let callback = - Some(Arc::new(callback) as Arc); - self.shard_state_dynamic_db - .put(state.block_id(), state.root_cell().clone(), callback) - .await?; - if handle.set_state() { - self.store_block_handle(handle, callback_handle)?; + match &self.state_db { + StateDb::Archive(db) => { + let state = state.clone(); + let db = db.clone(); + let state_root = state.root_cell().clone(); + tokio::task::spawn_blocking(move || { + db.put(state.block_id(), state.root_cell().clone()) + }) + .await??; + if let Some(callback) = callback_ss { + callback.invoke(Job::PutState(state_root, handle.id().clone()), true).await; + } + if handle.set_state() | handle.set_state_saved() { + self.store_block_handle(handle, callback_handle)?; + } + } + StateDb::Dynamic(db) => { + let callback = SsCallback::new( + handle.clone(), + self.block_handle_storage.clone(), + callback_ss, + ); + let callback = + Some(Arc::new(callback) as Arc); + db.put(state.block_id(), state.root_cell().clone(), callback).await?; + if handle.set_state() { + self.store_block_handle(handle, callback_handle)?; + } + } } Ok((state.clone(), true)) } else { @@ -702,17 +849,62 @@ impl InternalDb { callback_ss: Option>, ) -> Result { let timeout = 30; - let callback = - SsCallback::new(handle.clone(), self.block_handle_storage.clone(), callback_ss); - let callback = Some(Arc::new(callback) as Arc); let _tc = TimeChecker::new( format!("store_shard_state_dynamic_raw_force {}", handle.id()), timeout, ); let _lock = handle.saving_state_lock().lock().await; - self.shard_state_dynamic_db.put(handle.id(), state_root.clone(), callback).await?; - Ok(state_root) + match &self.state_db { + StateDb::Archive(db) => { + let db = db.clone(); + let id = handle.id().clone(); + let saved = tokio::task::spawn_blocking(move || db.put(&id, state_root)).await??; + if let Some(callback) = callback_ss { + callback.invoke(Job::PutState(saved.clone(), handle.id().clone()), true).await; + } + if handle.set_state() | handle.set_state_saved() { + self.store_block_handle(handle, None)?; + } + Ok(saved) + } + StateDb::Dynamic(db) => { + let callback = + SsCallback::new(handle.clone(), self.block_handle_storage.clone(), callback_ss); + let callback = + Some(Arc::new(callback) as Arc); + db.put(handle.id(), state_root.clone(), callback).await?; + Ok(state_root) + } + } + } + + pub async fn store_state_update( + &self, + handle: &Arc, + state_update: Cell, + ) -> Result<()> { + let timeout = 30; + let _tc = TimeChecker::new(format!("store_state_update {}", handle.id()), timeout); + + let _lock = handle.saving_state_lock().lock().await; + if !handle.has_saved_state() { + match &self.state_db { + StateDb::Archive(db) => { + let db = db.clone(); + let id = handle.id().clone(); + tokio::task::spawn_blocking(move || db.put_update(&id, state_update)).await??; + if handle.set_state() | handle.set_state_saved() { + self.store_block_handle(handle, None)?; + } + } + _ => { + fail!("store_state_update is only supported in archival mode") + } + } + } + + Ok(()) } pub fn load_shard_state_dynamic(&self, id: &BlockIdExt) -> Result> { @@ -726,7 +918,7 @@ impl InternalDb { fail!("ShardState is not saved for {}", id); } - let root_cell = self.shard_state_dynamic_db.get(handle.id())?; + let root_cell = self.state_db.get(handle.id())?; ShardStateStuff::from_root_cell( handle.id().clone(), @@ -739,7 +931,7 @@ impl InternalDb { pub fn load_cell(&self, id: &UInt256) -> Result { let _tc = TimeChecker::new(format!("load_cell {}", id), 30); - self.shard_state_dynamic_db.get_cell(id) + self.state_db.get_cell(id) } pub fn shard_state_persistent_write_obj( @@ -788,7 +980,7 @@ impl InternalDb { log::info!("store_shard_state_persistent {:x}: already saved", root_hash); } else { let id = handle.id().clone(); - let shard_state_dynamic_db = self.shard_state_dynamic_db.clone(); + let state_db = self.state_db.clone(); let shard_state_persistent_db = self.shard_state_persistent_db.clone(); tokio::task::spawn_blocking(move || -> Result<()> { let root_cell = state.root_cell().clone(); @@ -803,13 +995,13 @@ impl InternalDb { // in memory cells, as we do it while storing part (see store_shard_state_persistent_part). // It means we don't need to pass root cell into the adapter // and can set a zero limit for in-memory cells. - let cells_storage = shard_state_dynamic_db.create_hashed_cell_storage(None, 0)?; + let cells_storage = state_db.create_hashed_cell_storage(None, 0)?; let writer = BigBocWriter::with_params( [root_cell], MAX_SAFE_DEPTH, BocFlags::all(), abort.deref(), - Arc::new(cells_storage), + cells_storage, )?; let arrange_time = now.elapsed(); let cells_count = writer.cells_count(); @@ -855,7 +1047,7 @@ impl InternalDb { log::info!("store_shard_state_persistent_part {}: already saved", id); } else { tokio::task::spawn_blocking({ - let shard_state_dynamic_db = self.shard_state_dynamic_db.clone(); + let state_db = self.state_db.clone(); let shard_state_persistent_db = self.shard_state_persistent_db.clone(); let db_key: PersistentStatePartKey = id.into(); let id = id.clone(); @@ -889,14 +1081,14 @@ impl InternalDb { // and remembers their data in memory. // The adapter does not store the cell (don't keep references), only data. // The maximum number of cells to store in memory is limited - let cells_storage = shard_state_dynamic_db - .create_hashed_cell_storage(Some(&part), MAX_INMEMORY_CELLS)?; + let cells_storage = + state_db.create_hashed_cell_storage(Some(&part), MAX_INMEMORY_CELLS)?; let writer = BigBocWriter::with_params( [part], MAX_SAFE_DEPTH, BocFlags::all(), abort.deref(), - Arc::new(cells_storage), + cells_storage, )?; let arrange_time = now.elapsed(); let cells_count = writer.cells_count(); @@ -1453,7 +1645,12 @@ impl InternalDb { &self, index: Vec<(UInt256, u16)>, ) -> Result { - self.shard_state_dynamic_db.create_fast_cell_storage(index) + match &self.state_db { + StateDb::Dynamic(db) => db.create_fast_cell_storage(index), + StateDb::Archive(_) => { + fail!("create_fast_cell_storage is not supported in archival mode") + } + } } pub fn find_full_block_id(&self, root_hash: &UInt256) -> Result> { @@ -1461,13 +1658,17 @@ impl InternalDb { } pub fn cells_factory(&self) -> Result> { - self.shard_state_dynamic_db.cells_factory() + self.state_db.cells_factory() } pub fn cells_loader(&self) -> Result Result + Send + Sync>> { - let cs = self.shard_state_dynamic_db.create_hashed_cell_storage(None, 0)?; + let cs = self.state_db.create_hashed_cell_storage(None, 0)?; Ok(Arc::new(move |hash| cs.load_cell(hash))) } + + pub fn is_archival_mode(&self) -> bool { + matches!(self.state_db, StateDb::Archive(_)) + } } #[cfg(test)] diff --git a/src/node/src/internal_db/restore.rs b/src/node/src/internal_db/restore.rs index c85e77b..27c2d82 100644 --- a/src/node/src/internal_db/restore.rs +++ b/src/node/src/internal_db/restore.rs @@ -29,7 +29,7 @@ use std::{ time::Duration, }; use storage::{ - dynamic_boc_rc_db::BROKEN_CELL_BEACON_FILE, shardstate_db_async::SsNotificationCallback, + cell_db::BROKEN_CELL_BEACON_FILE, shardstate_db_async::SsNotificationCallback, traits::Serializable, }; use ton_block::{ @@ -350,6 +350,9 @@ async fn restore( log::info!("Fast restore successfully finished"); return Ok(db); } + if db.config.archival_mode.is_some() { + fail!("Refilling cells db is not supported in archival mode"); + } // If there was broken cell or special flag set - check blocks and restore cells db log::info!("Checking blocks..."); diff --git a/src/node/src/lib.rs b/src/node/src/lib.rs index 8cb3936..a82818e 100644 --- a/src/node/src/lib.rs +++ b/src/node/src/lib.rs @@ -8,6 +8,7 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ +pub mod archive_import; pub mod block; pub mod block_proof; pub mod boot; diff --git a/src/node/src/network/liteserver.rs b/src/node/src/network/liteserver.rs index 8ddecdb..212af10 100644 --- a/src/node/src/network/liteserver.rs +++ b/src/node/src/network/liteserver.rs @@ -1656,7 +1656,7 @@ impl LiteServerQuerySubscriber { let result = BlockState { id: block_id, root_hash: state.root_cell().repr_hash(), - file_hash: state.block_id().file_hash.clone(), + file_hash: UInt256::calc_file_hash(&data), data, }; Ok(result) diff --git a/src/node/src/shard_blocks.rs b/src/node/src/shard_blocks.rs index ef10ed0..ff2ca81 100644 --- a/src/node/src/shard_blocks.rs +++ b/src/node/src/shard_blocks.rs @@ -259,7 +259,11 @@ impl ShardBlocksPool { } if last_mc_seq_no != mc_seqno { - log::debug!("get_shard_blocks: Given last_mc_seq_no {} is not actual", last_mc_seq_no); + log::debug!( + "get_shard_blocks: Given last_mc_seq_no {} is not actual {}", + last_mc_seq_no, + mc_seqno + ); fail!("Given last_mc_seq_no {} is not actual {}", last_mc_seq_no, mc_seqno); } else { let mut returned_list = string_builder::Builder::default(); @@ -340,6 +344,7 @@ async fn resend_top_shard_blocks(engine: &dyn EngineOperations) -> Result<()> { Err(e) => { if actual_last_mc_seqno != mc_state.block_id().seq_no { log::trace!("resend_top_shard_blocks: goto next attempt"); + futures_timer::Delay::new(Duration::from_millis(100)).await; continue; } fail!("resend_top_shard_blocks: {:?}", e); diff --git a/src/node/src/sync.rs b/src/node/src/sync.rs index 2737980..9a57a22 100644 --- a/src/node/src/sync.rs +++ b/src/node/src/sync.rs @@ -760,7 +760,7 @@ async fn import_shard_blocks( } }; if let Some(block) = block { - engine.apply_block(&handle, &block, mc_seq_no, false).await?; + engine.apply_block(&handle, &block, mc_handle.id().seq_no(), false).await?; return Ok(id); } } @@ -770,7 +770,7 @@ async fn import_shard_blocks( unapplied blocks. Will try to download it directly" ); absent_blocks.fetch_add(1, Ordering::Relaxed); - engine.download_and_apply_block(&id, mc_seq_no, false).await?; + engine.download_and_apply_block(&id, mc_handle.id().seq_no(), false).await?; Ok(id) }); tasks.push(task) diff --git a/src/node/src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc b/src/node/src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc new file mode 100644 index 0000000000000000000000000000000000000000..2e614928ed7e9795793b0e582e3a63a8285a01d8 GIT binary patch literal 8529 zcma($30xCL_ir}|1PQQ&aHA}TC>Sw>fFJ>aqN1X5X|3226)jphM66a3R}7vM74W7S z6)(K8wTOs8v7pd`K($q?5ii=>8Zr7s1oEE=(9eFq@2}q^**80R^XARFclO)Aiqcz2 z!U_lgXIlVBgEjDka3yMqo1_<6Ms~9XutHeVSQV_B>@1E4r`U*Xw8PlOc$@Kl?s#sy zNt#Kn$c5W-R8BKEilV4+hC?M=bP`c z@VCge*lcl|Vk|u@i!D`rbG{Sbi_h@$_(l8@{%tFvRhCtrRgqPRl}2DEIAlG*daCu$ z)><2x%_N%{HnVKU*lw}CZTsBztsQAc*$uE0+KsS_u$yWpw_9VMXTQh(dwZ>egTr74 ze}}ma>l~UK+8x2s+|kL=%Q41rkK;{8wbPUSZT(*lU=PT49_*~3h4ctIf}Tnz(&;qg z;^oS9{lYcNHP>~QYm4hWw`cCt-H!|uczAe>@EGF}vy)E3kp z)F~Y)T_@cpRZ2^w-$-u<)4^WB0l{N}bAz`A?+zy`~{Van2K*qK$sHddt{nL6U5Z}O7AB4Qp7IpiW+`}wwkT^4Va zi>8Ah+j82YrqA1M0|h38Z{O5DsF)fk+126Cz+m9;r!0T=AlYr+#}92D{}g-3_B$d# zJkRb&{n!_4sN3V%#nBhRwBEz3@iXVrh~%?N19IkkMj;-zEMTg*5I#+bjA zO?GTKobn7TF}YE%{D&fP!$3ru!NoZ?0}w59upstcn*V;Om2k*7(?@+;`5=@5g=j|B zzrS(XS`Jblq}7KY11oZ2!>cLpZ!wy@eoWsK1PR$z@(oxwBCF(E3UVwwtrXA0EXK5? z2tFPpxjon9$g-BX(ZOREy%`o8EinPE9<;?)T4j5!DST}iF(A%lY}^ftFzcl=&Yxdz z(tNXiPS)bqQkMg62@lJ0o%6Lni7O>N5p92a_8In4F8YSRC7Q!!;|CV;k{BTOS7>gfg&0G-uqCCJWS;Hjhhvkv(D2 zB#yhE?^mmRW*J?bxMb6YNptNug9@*^e~?xmf(!;BVRhQbOP9`7KFpoCDE0IbPHxdP znP8e`V(Gf&C4085w`!j?Alp&ZHw8grbW{IwEG&C)AO!E|&=Df=-;;yW&yW6B*Z8+d z6V3&1ouJt0*sj~NYeVfco8ZR12eic`L4yYdtQzx*5;1%UIFTf7oXbjC&XF|LYUjK&23j4KxHoA^M3<%%o*C^IWEe_7YK{P(jzEE@B2=O;VQaezz1a>yiE%akw%#*#z|jE4y%F&{bP zI2d_Mh)hk(K`|;mO|4;39tlRPjkV^f`wBWhlrKta1-G)SmWdX!7^C0Zj7=t*rJd;> z4C~8J_Kg|cvp^0qM>F^5$-374gGDRHKIyRO;Bz0O4O5!=yzKAO6^cGKn$a#snt|Z2 z@f>)-oObl_p3Sm@MW+ugkCp@z5h!>$E|o&H$@Z$hrlgIryGc2jRKA6$Fp2>SFC~OYCNN?zv6@N*1z<4-{SPcB@`9 z?Ukwi=iSlVI{cEF%gcET2y)c?bEwc6;0_4(m4>Vt{Od`^YwsY0TU7o4)Ol zFy^zIO`D(Q=pVfsbee>@E)DvQ9kN&}G0K{rjjPvuA%flXYQv!E6`aXB+WkG#$D22kKTURJMN5|7W6t z)Z*CjV<*t>2*Kt{;3Hs9U5a-$9pW+g92Q_O13tlE4$Q^iQ%JyIHY5@V$ic;+o?s%F z2w@~OTD?|Ib{bO6!qfQ)ER93TiGYD2&Jb=1^DU5Q{Pdy z#3h>=J14`vyueChsYW;0Tgvn7<6^=!{B1pMldo`YO`{||&VzCO`eO2gazqHh)bbeW z%m%f{mxh@z3wVesp|Jq>)dNoNRyhAX>p=nwJjI-gzzGKSMSv`(w5DC(zI+PGyb{EJ zO*a$51QQYledKtfpFt7^^I$#<5p&vWuBO+tKsUo=F&9D1ppP8-)2wcuR+q~BJw*R(g(rd@heTJtD!X{jtrMS~-)2-q}nxy_sqhBw$fGN2S~nL$HH9fOA5PK zaArH@m%vHW7^~UMdRz%L>syFMR2SJ3H?pZYkx?0_UWt*Rp9uZrb@~_@<)+4H>o=C$ zsiXw%LXGhQm(>>M4L4sPGR7K~52u?p%cg>MU59;iiK*N99aKdE&nF%I8||uu4Cszt zadA6!Jb||eue+VvonVSNh@IUNqUgP%4)|C}G>7^$>qRll#VtOq2BZv(M4+|MgvF>& zw;$Jm&aHM@2U+JVsB^Zd)7#bQWy2AfQEAjil!S7lJkm7mS}hO_HmHW93(_=PwZYbx zfWh*=gk|uh6W94+t_g}Z&W<=Xxvhn7^xB5{ENl~VcyWT|+YNoxfi7AM@Q5Q~F=_nko4Bh#;qp|4d3|%XEbTL`r~3 zT@B1Nll@g#oAOM0vib-l$&q8qxxw}*^r5hq(NLeEuU~<@4cjR%BSORsWSdD@b0XQ? zPF8?I2?uygE+@+koZ<6Hq*I2~?>9oQ;Y0)s>ifOvWMi-z z6mU=qVclC`o-iT}gmbErtix5AP$p0*xxvT^XRp^bX!TtN?#VVWC%vJ%UEhV>5IIyK zarP=B=UQ8C38o(Nh$L95FpG-n1SR~!i^xJW2i4PxP;}~0#5QBAk<*m*#vTDrUDbu; zGh1O>^&c1$S0S~fBTwh705I%^R)oagz1HWUzE=woGm6sN3KRz#+T~OTa+-NadZ^OA zRpk^QK|-v>`+-vn5^t&Q1|Weg=4MrKeN^t-t2|`O<5Zbp$_gf6uNqiRG?v0zScgGA z6o8)-98jos*RG4NhTYZUyu7YdpU7q(u?ghyG+X#ITijT9&Lk~) zW!@N1WU`s5`%?GOc_Oj{dK?iwiLegTTGy=?lquSun;XzLkAe0OQG2S0a^S7sp0a=23BT zj5LB+vZ6pNcFngEwH8E4M6I#@l|-V%RRuAl&hyV~Phj_OLU+@y2QeS1R4SDg3(1Hz ziA3TbGo`%!%qR%~U$MK5Fmdy_D5-l{O=ikq+#g$4K8 zI=w?(XcVV|w7U*62OKLlw7PBB(8`89*aVwl3s9!iQfdqfWe#rSYE3^did-EJHpN~k zR6O;OH6`I=RTdH_q{vMxdEGLBDh1ij-2m=^jV;&c9=l(wt*MW|`LSL=cvqav;p-_)G;kQ(};u{rkcSkEsk2Ev8Gi(pB-X^ zm>y3BPW)BFcFPl>XIj*1`C90Ui5ja18je`!Mko(ed!92fm?vz7MuxiOa?8{rCJkAX zm6G8j!QvcH=WJJJVOwW`WN4*@Ylt=c*c)r7F)`7y(@ft$RHD*TiHy{~&Y54Qx7I=w ztAkAIAZ?JeB@C?{Rg32Rd-;`6?1DynjX#uZ4sF)Q+Lq@Hm9VsIywAbM$mpH>q7Y5% zx=BbvwZB1J7E_FyP*i|x$(u@?hce? z$snani8hysgQCH^WfuuSBnq~;&cyC7r~GOcU)HOUUTZUrXp<1$O{tM^w&E_1k{3~w z?1?E-PQA0vR&|$#0x2Xs+_WC4v@iI>H#PMk+`n3`4qM=+h@8TSma=jNTlZJ|r3*JGz2F%DZg!ArxA-$mpjnSAX z6s1s5k`j?T<{P>nW)__cw1l^P`A3t=KPzA$=KS(bXLB9YgXklyxS!nv-K1 zXW<(R&OQeEgvM(8hrU4CuK!N|xAy|xGQZBdpur54$tant!Cs7u>C&EmlrY*xHDke`bs@6I2)Uv3j7%K<6gg3C?_FjIa=H^d ztP+|_n+WPxIG}h}&As_Bk1OH&HKnb8xFMtJ)#Vyh8asL)B*ET{})n?JwnPilz{oP?2s)!O+vxUnY zX|pjypXHxc#kL%2@mnjHMq_vjz@+*zhi!}r90Avi2P$q>k7Zjv}d}LRVf_{2R~Xiz73xsJcB``_Zy%QpGY=i1G<4PO(1*!(fpJtAp5}SjW5XH1C1b5@Dpa> zCfow+N&r^H+0V?XrbL1DB~%brTy!YhkKR5IEpQuS-GRH{Q2h=~5_A14{_8%V2OkyO zgYOz+y`N5M0D?*bub=mxTWh|?A@PB!@prW~;}%$V{`K_x7wL%you3!AmAEb8yw(rg z8D#OxsmbQmyv2b=Q@=6}KD7Pwz}E0Z+ap%(!z)=JK6JfHvax6$;7cnAJ|8nBlw#w@ zAE43eGSnFKqYaRBvc#{{K529h0S<6}fJg8cbNK|GLVQzEhKV$$sZyJHb@7FoSFgMd zce}rMY8BBKHTly4Hlr8(F_af%ra^RY%f-{$z;LwsJNyChf``bP%SA&QuB$5=xB6%< z6-g#peZA2B=FsM)u4m*^zD(4A+IMKimiOXsdw6%VD8GLGsL(>yea)U@1MV$ZFQ6l) zcTZqqSVz5AFnB3|zws+mlI^-`%kJ+RKDM|+#rhJiRtg=;M%PU^IZ-jOL2K57{%`zs z)AMec*2P{Ne_~72UHMq;ibJXN>LFBaXLQp;7H1fGbM5*Ukc{+$_UOya7~K(9i{4nEqRQ>4d@?(9;qPz+$BA9_|dHdPV^a7O( zXw20Wv@t~3Y8_@4J;)IRL89#u9Kr}qCRq5whLOJ;mhR=YcUL{@6AWNa??dTjs+U>3 z5I`>qCTmP^31S*yipiSbOFwX}LMT|&=x3N_3?px%TNZ$EBR~k#(>HL>1{&R=AM#a# whuJkF%rRXHf`XBk4!!@a+FW4WKaI{l2}^zZva-0_9LIR;kF)d@HM_z55q^547CQu(QP>V?s{B)^3U3miU^_G~luDGGC`rRhC z?@7OGAMxcw5LLwfa(rY%9CLbK2K6JbdS) zi<%yz!PSUBQ{B$A#_L|YN=iy2 z0lT};*#SSZf2??ej;?x**zAxm)bTyhntJ-pmumJp=seE2fuZ(OiC4X3Z7Jy)KV`yG!@`ULH&8yIRV z=_Gnz+&@-BT5saUx@UEIGDa!pu1mT^?3H-j2=Ax$K31zoe}n#g9xh+I@7VNjr()bo zQX`$7jH;`zm!Fb+c5j5&rbR*Reb#b^kIdyTa!wL;jpd}FK3XBA%uf{YL z=r--3rpDExK&?XuHFeHDR2OInYy@iRK~|B12cRRv#!m38-um%ay#@^YZh@e_V3#0O zt+(29^@SP@HCk%4(^#tUMpIYQRP(l02Q4?PKAPoHtfsD^StU{o%J4&qb#(QsMvC?H zt4NAXOilkbDb^Se+Gzh1b}A}UrSh9%T2Y#M0$8=RF)%bzEB{kBV-Rms!Os(^ntFpv zUhq7771<@iqqX)h%P~uJW*cZTdux7r-y)_ZZnFe)hmkMdA7?FxO zFs1(VV(WSi%hxrGXrN6umJc(~QEwTaNUuA$zGKw5?srE@_lMm+ziL6w^h{hm`Jvi^ ziS7Mv4o+-tP&e_}mt_IBUFMf!ilY%OOu=OK(*?3}Lh)3qw8XM|5;vx}rchU{1f9^4 zv-jU3Z!eqGx6#w%TFX}6Ni$lW;jFfE`m_md0`(u5+8ExpcI}n}++wnddKJf~Z|~l< z;GX-vdy!&?t~E6pWls2X{IKVThFN~|A6SLu3yiWgYJWU1`mseThvQfc-1(~bU&EBu ze1-OKEe*KV7pj=z>OFxvizyy`$QM)9ML((tQ-3DKs+i&$2OcA!{vA`5mg0X4Q~#k9 z{~A+@5mcIbl3?oLhbay9HtWww+MAK44;Slm;EX<-r1aVe)Itch1zXqL&ZO+<2#X z@W{4ete%tO?e*RXJMH%iPj>pUGNQ<9f4<$0DG$}}4{&l!n0U!nfEk@(6qw>_gbPy| z3KA(lfULwRes8N+`gB`8HEv9CDX`kpFAWX!1NPd7_cR?Ed%AzOZh5vd1~S{fowd~+ zd9~guWlSXyyZxQqr-z0<^|RiG07J-GN`AEtEF`;BZ}tuQsQX5&D9m=ZU0 z6j8XGp&1l*>o5Y9qYNX&Fd-vhV2_WKpkf@6i^L=aKoU|EgGywSn4}3YDv^l9Qc_Gg zQlyBKF*t*W7}(hpky3(|FtEJ`e%z4=K=?V;~V;*Qw)WVxRAu7AaTqw8I=mpG{Ng*H)hHNK{|S(;H~b(gUC zrQY*Z3x3mR-a3lreSWDVmY=?omlL{*+-OFHE|( zsbi;mPW93Rx7?(0wxiqhZQZKtyXHsb)WiVmYwy>^1sd)lMFs6$gAHrO-Tl`v1;B!ieOYy&jssB)le~l@{2r5lI z1SlFmOqr-pldP&%m`aeMyZK>C(##Q)<6=ZcBXXf!LegOJU}89^BE`5w2!uijp_D*y zjA8$ZVGu+@WAHRX!xkVZ!Nmk%iWU;2kj5!Oh>;?>T!@H-I3+?Ql!%dw5QY@W5Jo6L zP==OKz(TPc%rp_nNQ5%Ej3F5gYf5YU6uqmDpT-ZsDBr`r7RJpjqo;nGV)91orqAoE z^pPohF4aAF>wN8hUYQDKqXV6@Ob&(?xV64>v3A$6!i4UTQ3(o6aW%q)DL5wi=>l0P z-ToaA;;(pVK#20$l)^e!J)7cE;De2~UOd%!UEbL)zT>lInaao_?!k z4sSyHW`*9`HY+8(?2MqZE2}+~USFtUimUfZF~y?~e_-k%4AigBrmBm6lwxYn|2mtx ztJO)XhgQG;c{cTDQml$8u5sWo0_B*hv=slq)L$dTzs8he1eK{$hUexWf}X3>PF{PBEQ|t{fCC zvwK$XCAhyp^7dSYiM#s|Jg?`<*LqsZWJWVb#K}fhJe*?JrUHQpDgU1*Q}E*V!>Jyl z-!$jPDGYY~B)ABtq%uZ=VKj|P2r&(|5)O_Kz$H}7fZ-!y;J^x%FqDWAW8f(P29*dG zvmB!(5=Xg|#Btd9BjhB4;37EIqG$<_30@?DP$E)-h$J$xL`o168HtE-nHa%nQjUR@ zg;O#)2dA{k^=l#HUYr4IkrODfF%QA`^*nf3mJTAK8atotg&yLOAx$l6En7rgx zU^F1K@Q6k~`O=sTnloI+of^4#cBFz+T#ayXN?S1o$`2qcX^P)RUr8*xD<4k%f>T@? z{2*fKwa+VtM!cD}`jR{?+u5V0nTdgT-bj55rwjcW_t#M7)UoZ-yH9TjqmT8tmv<7q z&|h6AyoM=HI*P~{X?@0D_jM<4R#RCUphD&o|i zNwF%YxW<9U2$XZG(o+0yaq2&m;$L%0F@j1{PZB5^KTbKT`yaYGxLR?_ma!Sbk5hOv zM*=5l5kLt8w7~8hF2leeVnnbJ2nR?al)(`hDTG}=Dd30@i{zL{2%C4{06~gnQc@~% zlnLP-n3MzuJ_^A|N-CF&a1le}V1dac1R^8g9E(9@7?6nqrwll(5(5nxxs0M=ZxQF< zl+N~ep}$ALvOeLa&TZccM6G=`)Ze!F*w%KQ<_#JIbqt!fVyJdd`fTmEWm%Jpr@1_h zUcYy_-uRJ;2fAeU@0NF|!&ZdcpTqLtUPo=ur2sp zVph@T!&g3@`F3o6yRM6`elWO^prKcP(`qlbT}^TZ%)GjL#<|Dyi2QoH%IvApS6KC_ z^a4YbQ(V1Q$|)Xw$d^;qML(*DQ-3DKs+{5)2Oc9(&Z$aE@efY@HB$U*PANuEY3gBZ z7!6+b)LeD{s}U)soYIQDod|CfY92l^c#Ca%am#_}+6L>>>$GSQtv~YOMq}r0yE~Tz z&&{7sWj_ls@riHq23c{wPVZj&oBh0w?bScFG15|Ob@0TFpX@1rXj9NC?KEF|O8?17 zPkx+|Hgl99ut!G|h*(O1mxN3zC&b{A$Iz_9I4(lLyH5%R5e|PQ0hVDXCPFZwOemGo zI0=uWBTi5fp_m{LLI`_>C`r?Da5YCs@Vda{QUaGS7%rEi7>UZ@oJ$N`L`WGSLr`d4 zC>IkPoYHkmX%*dJ(~-5$T@Dr>zB9Syx&<1(hHf6iJhkgshRm`Int8hBE`5vcEgrm# zYT;V@+4dnLUNt@x=GtkN!TQi9s8uHgr??v7;*>5s1Qjk2mh*}ymbkl=-Ib506#K}k zoZ`}8PvZ#-9_1c1-Zo>)t>*P>x+Z)!`Fz6o$-PlsqzU;aPCQcPR99D<7zcNkGsB9P z^shJO>!vwl=HJp2#W{9gFlBI`?WY2M!;50{m5cBlG#6V8+Af)@JNa!!&sM`$h23vG zVc{N(QTKQ~H7Y{8p|je{Q(Y_;8(es6l``(`sjvUo`}O~4{eszbuvOlddbgd1q?BFTgXZ>;WRUz_e9mtgw>d$9ZU1+>r8-Lf5hlEOwYV>3K&O*WKPk7OifI2cngv!BRk3tDTMnO0f>m7*5 z5n2RR6DpNEN&r3Jgbv46G6E6F2ow&kWKtX>XjCQ#*K|0K0*7~+q6r4#rU+6dgTN^< zCWAmOQYH~`xl!oNJ5?A!X;^LD>wU|opu?f~Q1s=avwDXvDiI92A+2ZQ_l0%5tV_<3!(f7xC6 zd`e-Ut8$7p@r`B{Eypeg> zymp^fpMpaRTZfxB^8VPnpqtg+{##%DgWY=f8|PC?+&jZ}(2lI(net`ekdVOHM^A3c znro7HxBfwVcl&hL)@}Xro^2CESjZz98OiJPyJ@+BOp8|2tdZ48&H7$e)H^?Qx1$r< zmvL%|JF7mGUSOzlimUfZImM$7`Eshd=tmWC>d&NDl~Y{fz+(i;IaO&X{=uogMv8yU zDa8mXO+5ju+VbI)T8Vmqp){nFQ@X3^M0n$<$F-fDkDUH^yUB$yC&uG%z8Y=1U_SHk z9Qsh31DT@4h->=#s-NM+-~|D{*v<;#0DcK9B4Tyo;t+#;6xdS)=7@+WDIsFO$PzIOM!^}B6ym19e-OMVFao?KP^p9_;Z3NJKxtYg zgt%X+SOotD9E8ajio>AN-@3cGyQ|ZKaXn@=AAh;%iypb_sGUcLE#9Va zc1ytu=lAChEK_TJ85v#Y$%1?F!|JxWQ>)i}bQ{_4s{&M9jc|cVAI7!f1=4a&@yvFG z5iJmGzpZ>grLfRdLB*xQ`6hjzj94{oReiZXwIOMpm+7n--nu^cG4uB>Lk?^WZmSHc z&EFggZk|i2U3;NEYIZ->dGndD^VYZP>uA66nUi|j>-ukiYHc{X@u#ywL*I6NotpjJ zV|qVoo)o^^^bMDIfH21iJTC41ZSZdSR-|mjy zTAX@$n@1U_)`qkCQ|T3kDyX=6uM|`~`tUobq6GijkwREq6r_qs^=D$NN-C~_;4uW{ zq^h(S{~*<0BgVfbm0|>y=AI;0Z~2f)14z|e>uo8i^p=cFgg4GGMK;&jv_ihRQ?K#r zH@3}g)1gy~+w~ov5XhW@57W1L*~EEJS4Deo8Ez~Y>*adEv>(~id@Q->%FAuLy4~;m z+*|i2Qu#xh;Q!3qlOj@2r#4SgK{^TsNCi-lLNt?zkVzR#CI_zvDay!5n#4t5OG#;o z3_#F%l9&pyVQem`EfPGLDFZ5DGD=n8axk00r?>3?W4*v77=R$$((MJb)KG zAwdv1DH97B1Wvoak%FQqoaEa2Gbq}9S!$HBbNQ&JO9MZ}A8ThA(yzykWi!*>9jl*p zAWUejwW;>}WB1IyeE#g8`l4^gf{m|rUvzadK2E$jwqx(PP1nZXXki)_OM)VtM*NO&1DWpf5VlcYE>MV4P8>{C59@g zxO%UYR6P2SH>s+Nf>aTy{!ENjNyRk~Jcgj0RFxLve~VQAp&0*~REiN)ntRkh(|D0e z2T0ZaMxSa$D%+v^6L^w}jkuQ+BG%830lP{Fr%fmVM@=*dIV#|2NhCoeQXE_+V22QA zC@BtZdJ;%eL5sm#5B&Eiu?RwaMTi8WQBoooq2OsD#&9798-{=~@Ee2#NkUu($5$wX zWr>9tI9yN&jtk*Mk&uu`aIVBjhOf4CAwRpA_ z&Dm4}();?6tVZU{L{EnUYfb8^&9Lb4uBi5GL!F6w^Xm4Rt{@dxBV431ETxgsBS_0l z#jk0DT*~gshg6E)WL2ArONDPVPYGw1;u=mpCMO_Sfm=I-n6A9Bx0}s+Nf>aTy{!ENjNyRk~Jcgj0RFxLv zAEf$g#Q4{wQjDO|+(WQ>%g3fN0aAHI$CZ*we@<{Bym4uE-Pr=`6;0D~XYK6o6D7c- z?HrQ#Pb)6Ip%xreBV}e_khr-)(YVuj3+BXc9`NSI)NjExTK9bxtUjM+B8KlV`pG-J zG(@lG3opK>RCc0P9z00}LHBYT#V9c(>l1_9AEZGN%Lx<_$q9)Fl2d>YL?dvj1p9&% zm_{;0B9n_K1Z*Z5SVjz_u)rM=A%s~$R4)l$6=0S@ES6jfzUzb#8UY$%A~=)6Xt5lE zup}77YoQ`&ivnr^!BDXn;n@8%%59t)s9CT)tm|ft=RGkWLj}y?e`(*H@qK z>@syh+a4{Cdp)}_@0FoxTR#VTs+Nf>aTy{!ENjNyRk~Jcgj0RFxLv zAEf$g#Q4{wQjDO|+!Mg+Egw=@1F7bXw=E@=!NdcJ@W$10vkURf25wE&8QC+V;f3(Q z!LjWgC28Dx@}#Gc*N&~I`M{0l&;5sbHa}|<_uceTFmmL4pbi2FaL&3_(G}m4wTtGS=TiO}i9&e$BNmP4EHN25vNV6J6S;XZFqa zq)( z7f8!}#S__&6U*+(=Tr(SU6oW^D%_va*`t0wpmsBlR&8Ae%kv_H`8~5vrjG8G<`ooj zHSV@Dsd{M4@xA!CIG{;!Novf3d7tZywZhgOUSvG!(6jlo14i`ujdQAus9^XG1`lIn zg}CWn|) z8*|yGMv-*Ij+m%TWu(f8V)dueOAJ*~arIs)sd)4uZ&Fnk1*sxZ{h1i6l8S2}cnm=~ zsVXhTKS=f0i1DvUr5Hh_xhIL$TRx<622yQmv#^v@M*Th}!W-lBgXWq0jBY+5ebTAB zhvJ$#nJw=0zQ?3!or$+hCatRR_+rD%ckTwuJDmKdmCv#vS2sAgP0jDqe_*bfHMOMf z(te*UE0RjfAKDa*zH*u`sUq6A4&X^D2)&0LJBbK_n@Gs?$6!*7U?2?=C1NA0*zhVD zo1+5$LIRHK6y%`=cXJYle6|i}oD&iX6T9B9#d{ z1{E%lmWPTbitI|CFS1iUr&3tys-)sl;WGuE$4$HMep`Ru<=G*Z{@I!yQLG*)85KCF zLxyvX2x+cNs#z%(Ui%+^hU8MYN%n%q>9WB&EAsmfIo{-%%bceVj!yXvQe{8B2w%dm z=(6;=#?dPWvTFptIox5571_4cxVL8?T%5hhAu;^ShYvSUjq_{Wu9BncS};x}OSZe6 zYSCj-gKmz=ne$?|?_O_KMyl+`to~GbiJ?j=uHGvp6^}mTO{(gmAXP-FKNDkBQgICg zk0B^0Ri(xF2dVxVG5$5F6eFlK_o%UY%ZF5Rfm8{)`K6>X?pi+)-gu>R*0Sg}W4{;^ zmRAhuB@?q7UB8*xGUWc5^JXvf#?GBPM`Yu)bAgd&<4v2UcwYIq-q+yCktVk3g?n0k zY_oLc@l8!DlBzVKe3H*ozNG3J(y%vAQbF*&P%OtV@F|DAJvn4amy!%a5RhzE0(*i4 zq&O#0iHL$E72s4Mr-Uf%{}GT?R)Uc52XGiAbcDnf5Q{}Ia&RBSVGB_v$7F2qFCl{D zeqgByagjvGU?c_`iwyh~WL|>$A7b!vM@cxxqB(Z|Ol#(hoccOaM>pZ~j`h7$cYHb2 zGr8Mpha{VR`zN{`8S8NHY((^xY}Y9Tk)yQZwis@_V9`UR=DRJi8YxKq2EJ=ou*qu0}@wdO;vPJD7< zLyGaU4_^udFTK{-DwAs1#5T>}y*)d>!`rj}Y|uhAdPHxoXJgsi>p?sB$>h-JNx$Jy zQMD=#%!VGR^b$jrR9wARN-7?G$eUEvMM0{FRDUMMs-)r?2p&UFPO3_a@xMi?|4@v7 zO)A9*D$P9vXc{k4l>n*k-*l^1q*~Z(T`iuZg7ACT!9yX99|mp`m>hg37|5e8goF~X zj|ba)2&9;R$S4#-t}qnR%|cQ?L`unV3WJS6Ou`UiM+o=@drB;l$VrlcG9RcEgA!H} zF)f6YwjxLr2(}c&W}$NMx`4t~kf8}2GUPHG{#p#xMMw@()d)WHM%&gH4Ln`g|94$e($oik+XF0~%i;FBK*Hyt*|{hHn62~Rf{kU7^wj`+oQU30Q1 z`HO;7T#ay%s=Uw(3nj$`(vrpgpk}tizwEAjPNmpSR<)_PRCs98xHi{^I&EJep0jYt zfTa!hS>3aC7~U>oa7Ig`H_xY@R3??U!S(EU&DZ)ZzqD%2nw%D4{r8O;@*#4&lhszo z^hHf}h5d#{MUfp#R-?l6uHAQ>Fd9s)-x53L>$;?S7j?%ji`4#dep3CJwfvHc9ru^? z_TQJYw)4nyA8)ohIHCU34;CMHu6}5~y}`swOAkIQwW)yJto~GbiJ?j=uHGvp6^}mT zO{(gmAXP-FKNDkBQgICgk0B^0Ri(xF2dVxVG5$5F6eFlK_XMzd%fqHpV@dUS%aKx2 znUF4t@WxE1tw-txMQ3EocC}fQd2VBaxWt8L^Bzrz+irViQ=b>DZ|o|0H&XxAsQGEZ zQ6ZlBwF}Nnm^gRoxUI(NM522>yE_@C}4GVKI;j!$eT)1J0>Xp#&#o zU^Ph@DCYqif20@!sc0!8L1d)myEGOfKudtci2IX-6O+xx@rF`I5{8BJawnUue* z?KTwu9B5N-r@~NyXKB zrKIA~hrCHuT@<8>NcCr8tV$}bf#5L&<)o^#7?+dk?-Ao)lS(myN^?&VtG7Hzr2(W0 zw47MClvJkJ@I-jyOQ%tX_U#YSzO2_%x^_U+_6>z^CN#g&>2TLUTime5&TBMvw`9_X z&gJ%fs++pP{N9E>uN&{EIag4avM+yX<^}uIpE6V^B69=*|Jd^-m5tXLDNj;C2tL#_ zgk3pGA{K#dB$bg2q@9H#z)*|>gSx+vkQSn%p!NsCKu{H!Ti_xAp68GN5K^rZ7%34(beg;EMf1A*kg4Sq~dCXi&W+?t`#qkmgnpec>--Iqvc|EGS+Hr36yI>yH$GVN=xJ=*^M zhGsp}?u{KX_-#M)OShf`ulSf8$^V=RXnPU9gvq3&-eGPg`}EW$^wkfOwPPPUFYER& z%6ZhyW5@zgz3ZmO?q;jE^VS>P+;qlf_n7mqg(U;G8R~oGhIOjNcCr8tV$}bf#5L&<)o^#82=#EUn9o9CY53YmF6Bb zR&RNbN{1yC)^2Vosm!eQB*Gg%w5&BsBKZ6$w2^xklWEA9W#%8Vi}MQKM~s=Q+fx{C zYcOY@OQRf*@Xeca*4uX)xUbOq-Fc71ZG&fsHzgb#&}!sQHkFP)v?&OE=f#&)8TCWs z_>l^Q5PaC-lYrki)aU>riD4rTYR-#rMouu`O@Wb!2$9R7(ua_yA^8NrO3na;VE<1j zl!AK&C3ciDkb?<|FR}@hP`0iWu!*f|&oELP+yNOmWV~e{yShXM^{Bvq5W;~`DR2*p zTM6Zeh->$+=25M-mrdu_RO>$#|F-`Va{zgjOFTHRWYFu`!MQaSj!0c@wX3aqn(gRj z=X<>^S)@7lRi8JL``vuAV*bkI{WcPh+9*iH)d&}zjN^91SwW{Exp9z{9cks*9hC^x$5bFw21N1%A~5*d}`S8Mg8CT zH#t^g*D>4u)9)kaw(k10_q<<7dO~Qv|8J1WWddtc)o8S~$3&~Jt%Icxy6*Yt({0uj}6T=(4_zUhYe`G&TNo?6`J>Wa`d6f%}H_PO?irow!WfYkL`~ zfZ5PmrI#40q~hwmQd05gL%yV{HVRTjr1~>4RwWhJK=2rXa#B@VjDL{ouMy*4lS(my zN^?&DtG7HzWx|rm-5|S^R5i7W5@G5xlPzdcBhOKu6_Wn^$|BACPDZ@>(4gNu^(&Jx#d9JN8bH^%BH024L6&0a^6_i8- zJ4!^0!JSlHXBw; z(_^95#2XuAERf`>i3KvMrEA|Ps!G(!s zcjeu`UyzDRg-vVb4~s`mAGbKQqSKt3k26m_YT@sF~v!wUAw zid51SP51F66@=j9ko25}b14yImIY^XQUpE}V#rz{CE!5AfQbHmJ ziwg3pLrfKlVO#|iYtMLl)yUCq;25zn)n%#tTz<+v37a6r|#6go{+QSr}EgKw1jfC)CDcis#|3 zy!)rH(p7CLE)@uFC`YHbW{5OSG2YvLsQl8h{txT%h`pl5L zWtp2MH{2iL+gs+naE9ysrG3t#D=U+%0PZgeS$8*B@PMvdSk?*3|Uwh(V^eqU(+^9w({) ztVed4O*OcX)t^c)F;q##)qAC+;?ak^NmX4Gq>4!OXJV{MDz1UxF$CqLsM}?k0 zd>Fz08Bh7zR3;f;&haD_gy2KfC_;$JFfjy0Nhk^dKXMVIRcDJ-0ktsL?;|1i1h`v3 zd@r0lK_M#EToSXTJus=9lsLjq;7%cAHwnS}0#XLzD3r32q2S;rf+~s-UxhP59LHz| zde z`-Iw@=`Lk=La8FX!b(>q6_*O@t?nY#?F*;lKOVb^j~E?6+o|EL zl}WXBD&E3pcwgI42EBs`!=};TZVRv3GF!OiVcxqSjDdUG;4fUS4e0Fkx7oG5F*{~ItGWR;)bX-{1 zU7H?qeO_p+=x74RwWhJK=2rXa#B@V zjDL{ouMy*4lS(myN^=jv>Maja&1FfocZzu_scL6=B*N6ysiB35-I_%%b?#1ZUNN&? zqXSH<$lKkUs^?vJzNf{bmpMIbA~#!2wP*V}7AXtk*VJ}f6#xYV(!TU(Aq92wf0kp|kFXBtC8L~z^KI&H2OOy4ZBDc+c zar%LO$0p>(0%!9{Z=L4~ueDIK$+;b}*1gurX;Wq=lsJgQQ%_wnKdK-VS0n#}RP#X= z1eWYWg$tzR6Z?eP21TJ4_O879r&#-_l8Q@(Kh^oi)uR8Z9L?A%sEvI;=e>brq#fro z{|t$$7bhGUUx8G8<|NrQy<=7Me0qRezqO-B&5muA+I~jp?6;D4w|#Un5`N>HD)>Y& ze3ja@#@k6WFJq}2XKyTcINChC{lcVCBRWoK7h*fNfn6W>1qmrHgC3+V!LuYcQVXAN z#-5MalYc1er284a6{}LZ#yOUeYR+_4e=5DiP$d;t@0F5@M<4PgRdrF2%9E-RVysFk zu7Th&1m&cvv>5*&)n6mVzb2JpB2t=rl32avK`MZUdf)|OdMT+aFYHK!scUHOCzM;~ ztg#)>oV(H>XOnFB^s%+Wr-Teg;oqw(8LTt3;X z&5_R2^6GRJ=cnwbNGe5M9zoh5IbTwZi`o{+lT;9b4-VrLY*A{`;@3W9+tk`hzk)DC-vP=g9m zu}dUuIV(zrvFA}34*vr9!V6IpN(jpUXly1y$ot1&TzUWM#I%g<{UoHtr=uHdt? z9tu)%HNr)zIv@}gFOZh6>@#Y+ofR*@U3vHK7o_4+VFSUGM}526ZO(BRYhLp7Qq6f` z%c*H|1d-dtwbryYmp4+jsd9=Jz0!>(zwK^)FmI-NKbt+5W;kLY*1Ps?Yqw%dpIur1 z8L5ET&?S{#VyKdetM^Jt#iI{-ld8HXNEMOl&%{`jR9pkWV+hJgRcSH)w@CFLit(>W zr5Hh_xkn8&jR&b9oC-*lXZ)aAk!sV3LmwuUiQuS<0q{W$D+UFi zA#lP41{v)5K{5+e238mYJBw0=0Y-tVpcE3+3c1dy>Sp#iHGa<0wH-#*6K(Xp9CKys zsEe}gfw>V;asAgiCEu0X`o?w~8D=-6<%u?LJGx~VP2Se^V6&BT#nO@)HZ22bYXzye z8sQ>UT?LJlA3$2ZvA?UG2yiL8EARd(taMeIic5u0JlWpxQLgp1rg^i1%udvCR6kWC za=4di!-GrZ=IwIT&M1>=7Cx+XhcWR>uFTDzX?ws)=WDH^i$32rO>7xuwY#9>#8toH zQL*7GdroC>yt!V$+|<4Ox14w0{B!_iI&%D!&TBq@TvB`CiJWLr<6Rfh$7>d*4Xd?g z{@mNKF1gwlUz@5gzEtl^RBDTnGfdJG%4{lNHndjhC59@gxO%UYR6P2SH>s+Nf>aTy z{!ENjNyRk~Jcgj0RFxLvAEf$g#Q4{wQjDO|+(WQ>%WYFZI8}-IJnj2rDXHotTug+i zYu3u>wH9AHW$bd=^4X!+VWIZIT>9MH{fk$O9J6n<20D6AJoBiVlecZZ>FtYy<{SHr zy<8w@8hkC`_*e4iqR?X3pEA@M_``=0T>9q4*QS!t(RFy*R1ks>`OKjJqL4-ynn9uP zApuFzAxj^Wq=z&Ukf>Hl!l@IEK<)~Nxq=NoT8Kf3DS`&?2)PJS_c3nSzvG=Q$Fj43EqPwTZg@vxFzr%b+lX?rb9eweRYFaPt z#t^s1xBRrE3Q}=3!bPgG8dU5^lwE@^N3-*>K6}VorK^&PONH%9EQfR$ z?AF-kLiOW@_~k@eg)M5-X3C-jWl~L9GvC_!jmFkQVV5l%Mvht6^6v9( z2k%{%@9Ar=)u7?k9{h7ufIY~bQ(3O*LcTq0*s|tU2(!+BC_f zdhdqjb*Zy^K{!pMzcXwrHEi&5>E17MrJp}`AR3>EEXq3~L&`{XB7oJON-r@~NyXKB zrKIA~hrCHuT@<8>NcCr8tV$}bf#5L&<)o^#82=#EUn9o9CY53YmFAuRR&TjU1>sac zD!ZNAOG#DtpErpxbuF2a-nxZLa+0etGx~C!+&bgeHZ5*=fAGiy7Lx~FOXzL2>e~9) ze#7Yo-P}@c_$NKQy({xr;qv|S=bMe4@w)4Vo?*HbcmLQqL?FnBTELf7Yn_TUc#;Z2 z@Nw9zlj9KXB!}8lup0-36{V0|9qRl-zFBbgBY`YpMQ&NBd?+FzXjdo%M|LoeU?&j5 zd7%Nwla34JQtW{CMuiZx1&P$b;~h2`MFe16XIIOTXkjWBfX0{bJr51Uvm3q5MyG;qS_ zgD!zfj)tAJZJW}2eA^a|b8BTBGEk6;s}U|z)mIFH@&ibVCi}bE9j(N&yYe}e!b(>q z6_*OPC>YZ5AmY8;Z_+$BSy90w^}OEApY<+mwe)(%siC7LrYVyuS|Dd< zd?;KJbbhsVyG+<&bvcqI+w!g zPoaP*wUz18Pf=Y8w z605h|q=IlNAQd^yu#{BwcGOISscZGMfR!7!9-HuLWW94o4m~OUq~Y}OtOGS~_y|G4 zmXXI%{n*H{EhITz3j9`QEgsswrf$uTvfFp+6q-&vV%%lX^P0DRB9%XU7{LRV7`~)B z-9MXWjtUe)@Nw{@0Q9icsYI~lhf_EU9vP%Shg$M-$VCBxy;5-EgX1T#y1>!`2MVaG zh=8*>R6-tepe}L!e|ZBgCNUFT4#}hZ8KR5b9UKsg)3g#I|xV z#1F&XBJ4TJIO3@qO!ulIE|_m+|y<=DP0AZ63uujydktWv=bU zclv%>1FnB67~A*h9HNr)z2J9GAxIkJI*+bNxbx=GH zcja>`#o9-eR9q^2ZiVH;Z}oRgZW%lX^#~dEV9b|;Gir@9Hr;qPIbrUNrY_2)+KN~+ zhY}}upjvi26WkH;ec9S~^U3xf7PW7g`JwTp)b77=PIX6%-TkY#rsl%Fp0_T0FYP+R z^+_7(P^0n8{M0wycg4Kj+4^y+EN)t8ExX6A+Qq5J_RZ^zFSkpH+_tGiFHa*##8Of<2pyORq&hyagV(~gvvWpn&0Te>!wwA*O2BbztyKUoSC84qif9r6Yf*`Gn<9J$a*1rKHBg3{5|uM3 zDv?2)F2wvY;93s(Wg!<5LSV4_2m6L%839#;DJc01F;-9`m@R50#&O^j_)nlDWU3%# zGRSL7$RVVdkup&33S;vw;ZQ+@gH%?BWfpz^DawhY?A1qDKPn7fbL!CRhW#cD>iE@Y z*2y4!4?%Iqt*w)ViN|7%-P%uoimp67_ju&&ec{f_FKsj2JIhBwD$YhU)$L4cyzaFN zI=P01Cg|qE4QF;PVtZcUG-bv9RN(??QB+G*`>@o7eF5&u=TyHS6_*O%4;#E@!!yZ~ zb6MlQ)`XVBo&0<%P^?Wfx`YA9998> zAX^{gQj*FE67nfQWhq*MLW(5tF=q=P%79y76Gb!~ zZ~!D=Gm*`6OF|$pA;H1oLfKtF_#3F#$ev%Z+(QUjCWGuuxQvTb*5PB4ot6(xy!{IQ z`25uVjct3_j+o}rd*d3jv)bmd8FIqcs9Wb3>Mc9(%1sSByeY=Fi%noiH+;JJEQdFH z*XgbaRgj9S5iXm`nuSq?3#3J{FRQM7(Vu+*?#ky>3M*aJrs7iJVUHGWIHI+&OL4OO z1;4#oPR{-AoLXn*d}7z@E*Z=a+nLIwGDv>a>O=Uvg$B>CZda{h2D!}`von8fMi3IT z!7bHI^K}0{50|gqcWnB%Q!(x(sgX`kM%C5V%TGx@yEnpX)1slLT#Ss3%dDVh9oX+u zKkwb1f)67vwd-8#$(O!QB#*s{yM$s+Nf>aTy{!ENjNyRk~ zJcgj0RFxLvAEf$g#Q4{wQjDO|+!Mg+Ew@cIS3#-`nQcl*Wi{VF5f;~%qxxU5o*H~3 z``)u}slA;BONNEM*?qjO733I;uWK$`!atn&=wBR56pI4%wQ~}o}wVlM1R1kvC?#+o&h;9-Ra-m2jLJ(5O zh{0PAB789^1pR`)ISJ{|;e1L4-hng)b?G507>>1A2MU641aAuz{sfB71D>F|2jsax zg#-#7?hNEl0@Df3tJoq}P)QN01Vi0l2*QHI>Oe#Y90o6T4x7rRjuScN@jmk~!NH>8 zIkJ)6uk<=1NvN|l;+%D#)z8L-?eal;9ctTANTwaCYa3tK#v{q)P|u9rmhom6V_#)0 ziwjqfimMSWQrUn&RJ=f16eFWv+c=SZ1@6k{R0=Cyl~i0Ryrko5t&&}vPip6k`1*E) z_k#4A4cw%sCeXTt;R|+7S}Rv3)z0pA^&8sup1*G5-6h_=0wtjXhYXCV{Vu=b9#8L( z%k5A8hDXJRrR+IXgZo3WI_X)=`fzw&P)yOeeQJxM=|i!JixWF7_7AkQ>2}282dy&qm$RM-B0`M@A$6swNmb}%!bw~y~I!@6<6<-l8Q$k@+MVv zQIIMkl?{wb#fy&^tCEUqAb1QxIjJfw#y?2)*NE}2NoB(hpVFWu!LJRtNyUOgectgs z(WRuap1w5^NcG_KH*xfeX$iABNKXvuy#A5pOikAo!MeekI~MEO25Np;fjwP8p{2G#{?Ww z!Q}|Wr!b0wlP3hyE`dQNr{H`F4z!>kqntz_eo77jzZev$VjG8wjZoGe4yd3yh!6+A zLJ3zcbz9B+;WZD+dPEDaf0-Wec!GVHtZftNr2Fl22W3v!Jz(9s*0nEYzuDIge=&B* zy3hL#G-`L@egB5h`1rgdm+udqez3oSR9ua4k;;}Gg9;Z&iz2t5x_x)W^Ke%_r&3ty zs-)sl;r#hQKIbx?6rap=zUH=56jS1>;q>Cmz(*^aHPE*WkhW7M)xD3e>UGoar~f4J z!8Kiz(8p7Z`#mQPFMfIc#K05zBm1o5?@JX8+Pt?{6(}? zrv1Jf-4;a;v1alFH%GX+>zSWPrrvix-Z5YIwoSBnjNi4J`fGzToeK1dYx-X=BNZ?k zTC4ODLzPrqy;n*q9(~B0RMka6s)$s7CdR6y;u;7ZLr_ktN{ewhss0`@{xzu-Bd9d@ zsIhv>Nh&s+3P^QprdBDbY{s5V1XAVQJsM_W`Ozb$M_YBf)Gv|e)n2~s|5E&MrdC1G z_)sHnUoDpvPP6TthhJC~c_1WEUxPXJvVq6W#g_`Nm_}XeVAQlCsTB1ip!ABIFR4s1 zn|nM-1tIv5_DCqCNk~U4f~x@xhgWGoto=n6gq(Uwzq-r&*4fN4JriRu28f ztUd3NrL*_OqaZ!)4X@jFJHFy^%1F`L#yZy&q~dDie~@Y`$bz6D`%vKmX;DP?s*A&1 z*cafgd`@MY|(V^vad4Fr!NC?{2=#rOxQ z{u(jnlhFC?8HWl}b*3KA+w;FwB6NhJUq@Sz}4s8WR!lp|#C1J8Ci z%mTM|1Y&-1L?V^YP&)(>!Fd$8P(YG(LIe=Qpq?Veh!F&e+skEeB8Ea53*u*+ik;*H z{_I2P)#$^}QiZagqY?X1;iG}HD0cYNhxKP)6Tn?< zao<^2%KlFAhVP^zpe(eeo__PCnp`TpA@hT8WQTF4J6nGkpLSv1q{1~D7AH6jN?!1( zq(P4cHC&WQb%B2LZ1x=XXf0sD~xn{K+m~i@ip1^UbON1*`wanPuh9--VmE*0nO6u_RXm) z9+UYlSo6?CXPaE{_=gk9NCnJ>)+)WkC@0l4$trOL z2X|HifhZ1Ah{5hSf{G#eB`zaGgrk@cL#j-QU~nNE^TIDOu^5A5+6?$JF|eJ1Lkbuf z36*qUxhMwzCWJlnr5Mml3huI>lVd6*5&HQ1)o5?`e0s>fZ3g$94h4)ed^5Olg!_w_ z=z@B+2m9N#YQNsG6W!wchx-0oJJKUY7spIhn{}#FWU!^2G`tZAw zLO6euMy)P z%ee|*{ywiL0a`KFaBe)Xla`u#px)9E{)#o1y>NCSa9!Bj;Oau8*RTX!??BwN2b;Ue zvM!~!zh5x-Vv|891g}S=MX6n^yP?aGx4O^FB8Zq}L$@&TC4CcJo0z{fp3J<=VZ-A+ z*tScr8vbxsYZmmB?{ZG-#-LL{RzE3W^`%smD`8o`u}2=9EctyU1NIG0LVj~t%|ZP`87_tTys+ViBT}59 z2!x_&ITWCR;vcZgW1vVq!$1`)j9`zbAcL(Ob%fIM5CKey#FPY5PQVct>>RRH79j@{ z)cB<-DVwVTieSMbK|!<@jX?w!IB-DLbprBT{G6lW=lRjDR_@^w^J)!Q6I^562i>{n z4KL-G#Ubj&!%F1dJJ&@fT6%9x?s|VolUiO)>Rz04)64bo(xtW6 zDwc~}jr@=0qCYzncI-oij|S4xn0-<`)F!d)p0wchLkIRL_+Rmws!heU-f#N$!a8qU zcUsfA&u!lK-IX0zZwO}HYm4!=DaExrwSJVSyj)!6^lb0QXw(PO<>^ffWbGz<4 zJet27oEF)9B9a^2>Nj@({2#LCRE^i&-PrhsorX!lwZnOJk&Ybg>7Gru8y*}L9Y07cUuAQe&3E5PfduMoj*LEAqY$~uWp|wgI!>TqF zSMQbDR6P3d`*M+&O$ACqg4rZR2|_4@ZE(oeB7logtQvKx;O5{Z z!v3tRBr^TE8`-De|6eEx7w2kpuKiMTMTAA!D`+9c0!26(!-aW4VlBRVCr- zy;3FN(TCra#uipM-vzMQM{T>oP^_pgXC*PbQU z#;2+nbM>A;{ns{?g4RlaPGaX8UN)76dZ6XRx}{s>O^u!KFl4^x&2NaHdS?@RFx|stCH-+hb5{?$Tb`CmFHh<&FFJ<5~4v3E74<)T88RBb9Q6&@5nq5J1K z9lw=q@b9v!-?ZbCVsamt_Zb6HKWzT3?efkEVd?(fHRAj`JdFKp6kBFfh1#(CQ|WcSDrdQR zuXMS{qYu9?7ylodN_EM|DhWhx$VCvMA%-NGVu^@i;{-_@V&>tj3MWJen|K)t-N*@< z5Mn;SDM^e&;Gi5MrI2(C7deWch#Ny9GzQ`GQ2$Mc%AwW(MT06(QV|^U;-DA^=o@%2 ziQqgKPIFPnRVKt43AodOx%K}ki9d95*=28FofVPVt*GZ|U)RYi0!D1npaPAbuSm6@ z)gC`JX8D52ZSH+B@>)X?4!!o=)Ho~NJ?*ZhbKvBDS6uAvE#9@0=_r(hs}cT6;$QZ; z{y!yA+IaYdl5j0HpXNSz@vwcT#%+e{bjE!D~huxpsZ1=ar|dByrPE zEUs~CI3BX#Lx81DRO|eeHw)ctF3exj{NuOFl6k*#PW7)S3D;&1*N%~@l5q82sgm&M z!|zJ+lTB59DN;qtxj&ohRhM&IvlWkds(d+D>AC*La_(OdW3KHOu00)9G3M$$6`Sh& zTnKl?2rA7zEV!HSvZ-{`1Do`@Sb9#?Ox-XMxIW9W#lXeU1%+L9Trjv;@-aJp?F9|} zbkEq3JGWo$Iqeac_j31=)+P^_J$RkNU7qaEx?ObPQ`-*OL+hQ@49Q;}eQz1dTtBl!RSCIfLmu;Bxe`{I zCI7M#{ueCy$)@sG42|NU+9{#OkI(koN;q9GDTb#_1qnDM;4@Cc`4V`IBP1n-wV)Um z!$!HBg5xT%j-W;&&JbV~LD3Gdu7q+3n3BN}6Zi$f=@UUZ5;9miiZKd>@F<4OO$*5< zByi9Lm8{qlwh(zGfn>E{apB;&4hfVXunK-az@dZSITpOWUaR-n}#D@Yp6nZzp^T?f0QaE&HRN2h{wgGxP4P z%}w?i;U#spDwc~}jc_d&6+7xb+f=RCkEy;d#-;2|aNAT|9QFB}XnSyK!d(x_xzVLM z@zX}Nn17iTUQIR=G_WsP`?i8j<#YYze3ylT`qq+|{^Qo{p*C4}{qNze>d(~ zhyMM4!}qUhISZ2=^hl+RVO7p@^C@mlb*H}tMN`(vt3P9tS z2#hE20Ti>F266<>W7#q`;NHq+ULnC0f`XzVu>|%cB%~t+Is&OK!SxlKY}tb?QUtaZ z6fkAP1eDYfG6blcRIE@L250lT$e{!rq~Ii_kkf?`{eMd0&x&2?)%feWfcfWQt;epv zazB-;gYj<4kOoWXonsUci%JM@|X{4MQ^Si2$J@3De085;`5S`WG(v-Z93&N zos+-lP@zIexEkTFB>rWe3pm)C{ZUy-lsMRweNA=afs1qWiX5-AU1A;&A9-A?CySTi zzT=RaZHL7yJQTU^={(}9vQ2fMiC^-gFXK?$t6=wC>SfgWnTc~(#7}#2{oRw1w=bP< z^c&|?QJ!olN7LeSmjAR{HvHv{6+4zJpLxw%{CxV$D_Zwg#!g1h_x3!Rkp3~dN%uXb z(YdEvo4O5jjfsuFU|hCJrMawV)ZOMWQfU&E50 zl<@mt!I*tl!tV45JKkThh*me7|`)>xF zYq3bwGRC{jnBs2BNZ-0i`o^<+6liJvGjM94n@91%m`PsDwAbMUPyE;%6&~;b%C4I1 zU9Hwk{*a{AG@Tn4#jVVhnmguH)HSrMTj#jPBQ&_gy`NXGRBei({b$ssK} zBnFg92`CSYiKH|o#30d<7zzr@xX!7X^xw6y4ifqH#_`Pes98UknO=Sl7jg`RnA8WHT>3sm!`;*da>ccMUQW4>ba(h3`MWQM)t^cm!>Y?guHGwMF7oKZ@5@DAHWerdc<2d9l%h!q zDkb57Ng|d)@=Fx<2`GXsHULMkDAcw_X{iVz1u>9&NFyd;M6mN9#o-Ls5tN0&z~2=E zV~mkNbiGInMF+r35TfW|p8}GNL8WbIl}4Z_90mS`Vn#|ype_y#smkD#m&2xNGEVK# zr6Wy`>~UYL?eKrxT?JSaU-u^L#zX~0F)*3!9S~u5cUHwNY{3Q$Y!nq112GUR6ct+x zOl&Mb5nF6g>`rX4zjt<7*dGJm41$k5&h!15b@ey@;oke+_ndRjc~9X}j@8~OM#o)m z8tztRPr>(V_Kh#U$Z?pt=e3U&MztwewvB6z`yCsMX`a}+iT8)p_+Gt)9a6L);d%&v zkZ3Uf%mp44Fn2?caQjr8Jh#N&^m^$Vr(GwFT41s4?Z@%{!Grc+k`+2TLH2O(ARALd zkZc}2e*U9Z<8M^=JiqRz@HC;iW>LG?ELFo7ZC}ql@S~a6U-PNbK*Ht6ayi3nkZ_Kk zN%Q8$CPv(2s0RtBPnBO-B#+Fw-`nfi<{Z~<#bcjJH|KJ_*Jqe>|IA*W$(&>MnY2qg zgV=lY1nrOS>+v$@s+&xvYYTMeRP~>Ciow=3H#%A25?7?luG3@bHj2hW&fIId?QqCjs<76${Fv0GoN6x%-I9jl53R50$mG)w| zmDk{hahDG3_?}a(FcZDw;Zx-omdbMwGULTue7J-SLeAZgdml^(VXnC(1B7|&Q~gCQ z$pk`;cB-^L^dQt501OEE`Ba7=td~E0s^C}dHd*>qtOH{))AfTwH+gj6&-%~pId>5GQHF1v&{@!-hBI_p?y8OC%!tdK;&AiCb13C_0ILXPl`@M6uiyu+a z&j#M9@={`9<^{5SDlSuI=!n$PUsVrH z)yrS6Qc0)oxgR=ehivBg4ZGHVO}Kox#SKGqvBehOFna8{&0)#E`d)dV82 zFR(dpzo(pgg{xz8a87mT2~$snzRE|J-QKeMl~u2;2jWUjHZHv0Z@|-^A2wR~M3?NY zyngYAYDlz0mpaN3y(3RHf7o-F>Cf{!Ki=?+*fw1nP&WFYUZdjB69zxIw)5HMBG>5Y zd@3Gen2ouZe*uz5O!a%qn9WpN6TxE&(wQpP%Q%Cn{+VT*JyU6C&|vS0WpK;OTy!*< z+^P6OT?oFx?Qt>Kx>hY&MxK3sv-ibEq>lzP?@8D=B(Gml97?i`ieg zHP+wyLFMVisiCb}bzWF>p67fmQ*k|ni>b81yv$4`VP?i8c?ff3?uJQI4VdrPOvMG^ zUiWWFr?j%K>w4$nSI5#@%Y7TUGiIgYcDskoTuN_Vcst&ZscMsr#LhixEpendrs5hs9aHfbLp@Vv5&dH&;WFl&Osr5M@(lwFmKbD`&`y*@7>`5(k`jMMOO-Mu z5-T8wDN!J#t(J==a_BJ=a;X^jA|-a{p)DX)lo;xoBxIinS}BtQJrI0WkrV~+F>=lL z5735GN>a#Y7pWP2CRBom1cv#J{y{I1|jln%sH;G$ne+;rrw)Nw~c+@Gq6R?Hf}4IU0J!;yzsE|anoL} zPU-7q2$J^uulGOFxvcH1@Of>l9^7nuKX&m2)5?{4980V-ICylVO%6cvM|hCySc2

E>Ln_xcQT?w{G~vp46o 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∋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

=T-N@$j}nl`hLG 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 literal 0 HcmV?d00001 diff --git a/src/node/src/tests/static/archives/archive.00000.pack b/src/node/src/tests/static/archives/archive.00000.pack new file mode 100644 index 0000000000000000000000000000000000000000..c1c06f65270105cae003bfc505babff24e1859cb GIT binary patch literal 1185911 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%0Du2-bXP)>pWzV-G7%zZ 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

f(lsi?!f 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

Z4g}#Am@#f1pCm( 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

sgv5~k!2Z8-+5Ks(T8pLDfX%(t5`ww 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

eKa!H^=&4ij_cF6hQHn5fNXfV@c^=6yq=#( ziC-Z+fu)#WVJ9I&j0c3_%^yewAZ7w{ zq@U%AZM$PAe2`A*OH%)it+i)5QWfKmPD<}KxviO6Ii|yQYcc&0uERNKK7aWNGy5D& zKg7;tiJgff7d?_kz5pK9Z}YzfJcvG+34;kIF4G3YOaOm04>q!#C43A2F5Jgn`ux}b z;aItB9h97*+y#?Y^f5EN5>Y}Nt|C`o+%URx+-EL=YrK|&JtLNa=YID!MA-V70stSs z-MhCrOUfzAs-u59$ttQTOKCy|0J;en7%JloRrS>rRrE19WweHdqM;r}Lq$XgMq7{vxrE}WTdKMs9~gMsESrL(udRm zT18DoOyJG=MCBlA`m}c^ zM>S_aNh$>aJRnxePlE>q_&xJG@SqI&lHSd`a$&w4JitryXvo-0hfaL8V#yghO&|}5 zDJp}<9~N$Bf6$YhlZ}!5x@kk+=t^9}P;JAhW785-L86bAlLs^=eud23vuRmzwa1PW zWZJ%bb8dYU=jn!#N*=k}0h}J6%dT&k5g81P>=jHL1&l-FT(d9$~& zlLxEZ5$)jL#y<$;0ipxMlLvWY`jx*S4}?s0&X$se9aeL%BpE#*7P708QEn8sUVfX_ zLeEg_$Z9TmMO}rR{HVfhexP|K`{~0)^nh4sZ?>}9S@j+)ND=GoKOW1*(z@p5jot@+ z0ta6yi#w2wNi3oV8k49`))RK`M@%qV9u>Z!^y4ivKacIDj*z%V9?o`NkboZC^W^bq zq0Tc;NU+#DGNIor?9JwuVI(rX_k67RmGzs~5bOj#YoSA{xwnkSA6W&`5E+} zM>g&0z*8KJ2w5D5C~I$((hHi5=|>y*nY+;K9r?^I4$;B>)xMKs(_?2}TC6YfnLj*s z->TN+xNEN=&0Vaz2nluqpxmsQ;)#2c+Zkzd)UmKz7iJ#_^FIInXwo_7P`tNS$t(|- z*SQl;9Pzy4d*0FtjMZMg&;5ODfLDb-mmQyIQHm50J|1-HwcL*nHhUUhzU$ogQ~SU9M>)zX1nFZ`Q)(~&)%S7asZIcky_ zo_D_CJlj=obLe`efVX(xWM`>-Srz$~5ZY)q;&uYhLaR1ks}IV{+smQ1#wf6bLE9DNI)7I~k{*LL`E$K*|(m12IqnT2Y>4RAgCXL*z8%+~jC-QwkhK2}KKKAY~<0 z2~{gKj`}W*Ce3M@_p~_LLfS8Md~|MfjdbJm91uOQp|7DIVDMsOV=QG-Wr}5HVcx_- z!4kpJ%*w|)%tpm#!{);l%a+g9!ZyXO$L`J^%0AA)z;T*Wg)^M3!)6*Gbyg06z0f@wmgLMcLnLgPa7 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

jqcHb^97;pREV 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

v7hwXa{AtL@#$Kma&=Vl15F4)zql3^1z8 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 literal 0 HcmV?d00001 diff --git a/src/node/src/tests/test_archive_import.rs b/src/node/src/tests/test_archive_import.rs new file mode 100644 index 0000000..0620be0 --- /dev/null +++ b/src/node/src/tests/test_archive_import.rs @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +#[cfg(feature = "telemetry")] +use crate::collator_test_bundle::create_engine_telemetry; +use crate::{ + archive_import::{run_import, ImportConfig}, + block::{BlockIdExtExtention, BlockStuff}, + collator_test_bundle::create_engine_allocated, + internal_db::{ + InternalDb, InternalDbConfig, ARCHIVES_GC_BLOCK, LAST_APPLIED_MC_BLOCK, + PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, + }, + test_helper::init_test_log, +}; +use std::{ + path::{Path, PathBuf}, + sync::{atomic::AtomicU8, Arc}, +}; +use storage::{archives::epoch::ArchivalModeConfig, db::rocksdb::RocksDb}; +use ton_block::{ + read_single_root_boc, write_boc, AccountIdPrefixFull, BlockIdExt, Result, SHARD_FULL, +}; + +async fn wait_for_db_release(db: Arc) { + while Arc::strong_count(&db) > 1 { + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + drop(db); +} + +const ARCHIVES_PATH: &str = "src/tests/static/archives"; +const MC_ZEROSTATE_PATH: &str = + "src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc"; +const WC_ZEROSTATE_PATH: &str = + "src/tests/static/EE0BEDFE4B32761FB35E9E1D8818EA720CAD1A0E7B4D2ED673C488E72E910342.boc"; +const GLOBAL_CONFIG_PATH: &str = "src/tests/config/mainnet.json"; + +fn import_config(dir: &Path) -> ImportConfig { + ImportConfig { + archives_path: PathBuf::from(ARCHIVES_PATH), + epochs_path: dir.join("epochs"), + epoch_size: 20_000, + node_db_path: dir.join("node_db"), + mc_zerostate_path: PathBuf::from(MC_ZEROSTATE_PATH), + wc_zerostate_paths: vec![PathBuf::from(WC_ZEROSTATE_PATH)], + global_config_path: PathBuf::from(GLOBAL_CONFIG_PATH), + skip_validation: false, + move_files: false, + } +} + +async fn open_db(dir: &Path) -> Result { + let db_dir = dir.join("node_db"); + let epochs_path = dir.join("epochs"); + InternalDb::with_update( + InternalDbConfig { + db_directory: db_dir.to_string_lossy().to_string(), + archival_mode: Some(ArchivalModeConfig { + epoch_size: 20_000, + new_epochs_path: epochs_path, + existing_epochs: vec![], + }), + ..Default::default() + }, + false, + false, + false, + &|| Ok(()), + None, + Arc::new(AtomicU8::new(0)), + None, + #[cfg(feature = "telemetry")] + create_engine_telemetry(), + create_engine_allocated(), + ) + .await +} + +async fn check_imported_block( + db: &InternalDb, + block_id: &BlockIdExt, +) -> Result> { + let handle = + db.load_block_handle(block_id)?.expect("Block handle must exist for imported block"); + assert!(handle.has_state(), "Imported block must have state"); + assert!(handle.has_saved_state(), "Imported block must have saved state"); + assert!(handle.is_applied(), "Imported block must be applied"); + + let mut block_stuff = None; + if block_id.seq_no() > 0 { + assert!(handle.has_data(), "Imported block must have data"); + assert!(handle.has_prev1(), "Imported block must have prev1"); + if block_id.is_masterchain() { + assert!(handle.has_proof(), "Imported MC block must have proof"); + } else { + assert!(handle.has_proof_link(), "Imported shard block must have proof link"); + } + + let prev1 = db.load_block_prev1(&block_id)?; + assert_eq!(prev1.seq_no(), block_id.seq_no() - 1); + let prev_handle = db.load_block_handle(&prev1)?.expect("Prev block handle must exist"); + assert!(prev_handle.has_next1(), "Imported block must have next1"); + let next1 = db.load_block_next1(prev_handle.id())?; + assert_eq!(&next1, block_id); + + block_stuff = Some(db.load_block_data(&handle).await?); + let _ = db.load_block_proof(&handle, !block_id.is_masterchain()).await?; + } + + let loaded_state = db.load_shard_state_dynamic(block_id)?; + let boc = write_boc(loaded_state.root_cell())?; + let deserialized_state = read_single_root_boc(&boc)?; + assert_eq!(loaded_state.root_cell().repr_hash(), deserialized_state.repr_hash()); + if block_id.seq_no() > 0 { + assert_eq!( + deserialized_state.repr_hash(), + block_stuff.as_ref().unwrap().block()?.read_state_update()?.new_hash + ); + } else { + assert_eq!(&deserialized_state.repr_hash(), block_id.root_hash()); + } + + Ok(block_stuff) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_import_and_verify() -> Result<()> { + init_test_log(); + let dir = tempfile::tempdir().unwrap(); + let config = import_config(dir.path()); + + run_import(config).await?; + + let db = open_db(dir.path()).await?; + + let last_mc = + db.load_full_node_state(LAST_APPLIED_MC_BLOCK)?.expect("LAST_APPLIED_MC_BLOCK must be set"); + assert_eq!(last_mc.seq_no(), 199); + assert!(last_mc.shard().is_masterchain()); + + let gc_block = db.load_full_node_state(ARCHIVES_GC_BLOCK)?; + assert_eq!(last_mc, gc_block.unwrap()); + + let pss_block = db.load_full_node_state(PSS_KEEPER_MC_BLOCK)?; + assert_eq!(last_mc, pss_block.unwrap()); + + let shard_client = db.load_full_node_state(SHARD_CLIENT_MC_BLOCK)?; + assert_eq!(last_mc, shard_client.unwrap()); + + let last_mc_block = check_imported_block(&db, &last_mc).await?.unwrap(); + + for shard_block in last_mc_block.top_blocks_all()? { + check_imported_block(&db, &shard_block).await?; + } + + let first_mc = + db.lookup_block_by_seqno(&AccountIdPrefixFull::any_masterchain(), 1).await?.unwrap(); + let first_mc_block = check_imported_block(&db, &first_mc.0).await?.unwrap(); + // MC zerostate + check_imported_block(&db, &first_mc_block.construct_prev_id()?.0).await?; + + let first_wc = + db.lookup_block_by_seqno(&AccountIdPrefixFull::workchain(0, SHARD_FULL), 1).await?.unwrap(); + let first_wc_block = check_imported_block(&db, &first_wc.0).await?.unwrap(); + // WC zerostate + check_imported_block(&db, &first_wc_block.construct_prev_id()?.0).await?; + + db.stop_states_db().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_import_resume() -> Result<()> { + init_test_log(); + let dir = tempfile::tempdir().unwrap(); + let partial_archives = dir.path().join("partial"); + std::fs::create_dir_all(&partial_archives)?; + + // Copy only the first group (archive.00000.*) + for entry in std::fs::read_dir(ARCHIVES_PATH)? { + let entry = entry?; + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("archive.00000.") { + std::fs::copy(entry.path(), partial_archives.join(&name))?; + } + } + + // First import โ€” only first group + let config1 = ImportConfig { + archives_path: partial_archives.clone(), + epochs_path: dir.path().join("epochs"), + epoch_size: 20_000, + node_db_path: dir.path().join("node_db"), + mc_zerostate_path: PathBuf::from(MC_ZEROSTATE_PATH), + wc_zerostate_paths: vec![PathBuf::from(WC_ZEROSTATE_PATH)], + global_config_path: PathBuf::from(GLOBAL_CONFIG_PATH), + skip_validation: false, + move_files: true, + }; + let node_db = run_import(config1).await?; + wait_for_db_release(node_db).await; + + let db1 = open_db(dir.path()).await?; + let last_mc_1 = db1 + .load_full_node_state(LAST_APPLIED_MC_BLOCK)? + .expect("After first import, LAST_APPLIED_MC_BLOCK must be set"); + assert_eq!(last_mc_1.seq_no(), 99); + drop(db1); + + // Copy remaining files for second import + for entry in std::fs::read_dir(ARCHIVES_PATH)? { + let entry = entry?; + let name = entry.file_name().to_string_lossy().to_string(); + if !name.starts_with("archive.00000.") { + std::fs::copy(entry.path(), partial_archives.join(&name))?; + } + } + + // Second import โ€” should resume and process remaining groups + let config2 = ImportConfig { + archives_path: partial_archives, + epochs_path: dir.path().join("epochs"), + epoch_size: 20_000, + node_db_path: dir.path().join("node_db"), + mc_zerostate_path: PathBuf::from(MC_ZEROSTATE_PATH), + wc_zerostate_paths: vec![PathBuf::from(WC_ZEROSTATE_PATH)], + global_config_path: PathBuf::from(GLOBAL_CONFIG_PATH), + skip_validation: false, + move_files: false, + }; + run_import(config2).await?; + + let db2 = open_db(dir.path()).await?; + let last_mc_2 = db2 + .load_full_node_state(LAST_APPLIED_MC_BLOCK)? + .expect("After second import, LAST_APPLIED_MC_BLOCK must be set"); + assert_eq!(last_mc_2.seq_no(), 199); + db2.stop_states_db().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_import_skip_validation() -> Result<()> { + init_test_log(); + let dir = tempfile::tempdir().unwrap(); + let mut config = import_config(dir.path()); + config.skip_validation = true; + + run_import(config).await?; + + let db = open_db(dir.path()).await?; + let last_mc = db.load_full_node_state(LAST_APPLIED_MC_BLOCK)?; + assert!(last_mc.is_some(), "Even with skip_validation, last MC must be set"); + + let last_mc = last_mc.unwrap(); + let handle = db + .load_block_handle(&last_mc)? + .expect("Block handle must exist after skip_validation import"); + assert!(handle.has_data()); + + db.stop_states_db().await; + Ok(()) +} diff --git a/src/node/src/tests/test_sync.rs b/src/node/src/tests/test_sync.rs index ba0b3fc..2a37923 100644 --- a/src/node/src/tests/test_sync.rs +++ b/src/node/src/tests/test_sync.rs @@ -40,7 +40,10 @@ use std::{ }, }; use storage::{ - archives::archive_manager::ArchiveManager, + archives::{ + archive_manager::ArchiveManager, + db_provider::{ArchiveDbProvider, SingleDbProvider}, + }, block_handle_db::BlockHandleStorage, db::rocksdb::{AccessType, RocksDb}, types::{BlockMeta, PersistentStatePartId}, @@ -119,9 +122,13 @@ async fn test_sync() -> Result<()> { let allocated = create_engine_allocated(); #[cfg(feature = "telemetry")] let telemetry = create_engine_telemetry(); + let db_root_path = Arc::new(PathBuf::from(DB_PATH)); + let db_provider: Arc = + Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())); let archive_manager = ArchiveManager::with_data( db.clone(), - Arc::new(PathBuf::from(DB_PATH)), + db_root_path, + db_provider, init_mc_block_id.seq_no(), monitor_min_split.clone(), #[cfg(feature = "telemetry")] diff --git a/src/node/src/types/awaiters_pool.rs b/src/node/src/types/awaiters_pool.rs index d27cbe3..894cdc4 100644 --- a/src/node/src/types/awaiters_pool.rs +++ b/src/node/src/types/awaiters_pool.rs @@ -161,6 +161,18 @@ where Ok(()) } + pub async fn shunt_async( + &self, + id: &I, + operation: impl futures::Future>, + ) -> Result<()> { + if let Some(op_awaiters) = self.ops_awaiters.get(id) { + let r = operation.await?; + let _ = op_awaiters.1.tx.send(Some(Ok(r))); + } + Ok(()) + } + async fn wait_operation( &self, id: &I, diff --git a/src/node/src/validator/accept_block.rs b/src/node/src/validator/accept_block.rs index e80dde3..4951a46 100644 --- a/src/node/src/validator/accept_block.rs +++ b/src/node/src/validator/accept_block.rs @@ -12,7 +12,7 @@ use crate::{ block::{construct_and_check_prev_stuff, BlockStuff}, block_proof::BlockProofStuff, engine_traits::EngineOperations, - full_node::apply_block::calc_shard_state, + full_node::apply_block::store_state_update, shard_state::ShardStateStuff, types::top_block_descr::TopBlockDescrStuff, validating_utils::{fmt_block_id_short, simplex_to_sign_checked, UNREGISTERED_CHAIN_MAX_LEN}, @@ -295,8 +295,7 @@ pub async fn accept_block_routine( } log::debug!(target: "validator", "({}): accept_block: calculating shard state", block_descr); - let _ss = - calc_shard_state(&handle, &block, &(prev[0].clone(), prev.get(1).cloned()), engine).await?; + store_state_update(&handle, &block, &(prev[0].clone(), prev.get(1).cloned()), engine).await?; // Create proof using variant-aware function if Simplex variant provided let (proof, signatures_out) = match signatures_variant { diff --git a/src/node/storage/Cargo.toml b/src/node/storage/Cargo.toml index 5b263b6..8119d15 100644 --- a/src/node/storage/Cargo.toml +++ b/src/node/storage/Cargo.toml @@ -23,6 +23,7 @@ rocksdb = '0.23' serde = '1.0' serde_cbor = '0.11' serde_derive = '1.0' +serde_json = '1.0' smallvec = { features = [ 'const_new', 'union', 'write' ], version = '1.10' } strum = '0.18' strum_macros = '0.18' @@ -38,6 +39,7 @@ ton_api = { path = '../../tl/ton_api' } cc = { features = [ 'parallel' ], version = '1.0.61' } [dev-dependencies] +tempfile = '3' zip = '2.2' [features] diff --git a/src/node/storage/src/archive_shardstate_db.rs b/src/node/storage/src/archive_shardstate_db.rs new file mode 100644 index 0000000..c1362ac --- /dev/null +++ b/src/node/storage/src/archive_shardstate_db.rs @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +#[cfg(feature = "telemetry")] +use crate::StorageTelemetry; +use crate::{ + cell_db::CellByHashStorageAdapter, + db::rocksdb::{RocksDb, RocksDbTable}, + dynamic_boc_archive_db::DynamicBocArchiveDb, + shardstate_db_async::{CellsDbConfig, DbEntry}, + traits::Serializable, + StorageAlloc, TARGET, +}; +use std::{path::Path, sync::Arc}; +use ton_block::{BlockIdExt, Cell, CellsFactory, CellsStorage, Result, UInt256, UnixTime}; + +pub struct ArchiveShardStateDb { + index: Arc>, + boc_db: Arc, +} + +impl ArchiveShardStateDb { + #[allow(clippy::too_many_arguments)] + pub fn new( + db: Arc, + index_cf: &str, + cells_cf: &str, + db_root_path: impl AsRef, + config: &CellsDbConfig, + #[cfg(feature = "telemetry")] telemetry: Arc, + allocated: Arc, + ) -> Result { + let boc_db = Arc::new(DynamicBocArchiveDb::with_db( + db.clone(), + cells_cf, + db_root_path.as_ref(), + config, + #[cfg(feature = "telemetry")] + telemetry, + allocated, + )?); + let index = Arc::new(RocksDbTable::with_db(db, index_cf, true)?); + Ok(Self { index, boc_db }) + } + + pub fn put(&self, id: &BlockIdExt, state_root: Cell) -> Result { + let cell_id = state_root.repr_hash(); + log::debug!( + target: TARGET, + "ArchiveShardStateDb::put id {} root_cell_id {:x}", id, cell_id + ); + + if self.index.contains(id)? { + log::debug!( + target: TARGET, + "ArchiveShardStateDb::put ALREADY EXISTS id {}", id + ); + let data = self.index.get(id)?; + let db_entry = DbEntry::deserialize(&data)?; + return self.boc_db.cell_db().load_cell(&db_entry.cell_id, false); + } + + let saved = self.boc_db.save_boc(state_root, &|| Ok(()))?; + let save_utime = UnixTime::now(); + let db_entry = DbEntry::with_params(id.clone(), cell_id, save_utime); + self.index.put(id, &db_entry.serialize())?; + Ok(saved) + } + + pub fn put_update(&self, id: &BlockIdExt, state_root: Cell) -> Result<()> { + let state_root = state_root.virtualize(1); + let cell_id = state_root.repr_hash(); + log::debug!( + target: TARGET, + "ArchiveShardStateDb::put_update id {} root_cell_id {:x}", id, cell_id + ); + + if self.index.contains(id)? { + log::info!( + target: TARGET, + "ArchiveShardStateDb::put_update ALREADY EXISTS id {}", id + ); + return Ok(()); + } + + self.boc_db.save_update(state_root)?; + let save_utime = UnixTime::now(); + let db_entry = DbEntry::with_params(id.clone(), cell_id, save_utime); + self.index.put(id, &db_entry.serialize())?; + Ok(()) + } + + pub fn get(&self, id: &BlockIdExt) -> Result { + let data = self.index.get(id)?; + let db_entry = DbEntry::deserialize(&data)?; + log::debug!( + target: TARGET, + "ArchiveShardStateDb::get id {} cell_id {:x}", id, db_entry.cell_id + ); + self.boc_db.cell_db().load_cell(&db_entry.cell_id, false) + } + + pub fn get_cell(&self, id: &UInt256) -> Result { + self.boc_db.cell_db().load_cell(id, false) + } + + pub fn contains(&self, id: &BlockIdExt) -> Result { + self.index.contains(id) + } + + pub fn cells_factory(&self) -> Arc { + self.boc_db.cell_db().clone() as Arc + } + + pub fn create_hashed_cell_storage( + &self, + root: Option<&Cell>, + max_inmemory_cells: usize, + ) -> Result { + CellByHashStorageAdapter::new(self.boc_db.cell_db().clone(), root, max_inmemory_cells) + } +} diff --git a/src/node/storage/src/archives/archive_manager.rs b/src/node/storage/src/archives/archive_manager.rs index c54ed3a..1910fa4 100644 --- a/src/node/storage/src/archives/archive_manager.rs +++ b/src/node/storage/src/archives/archive_manager.rs @@ -13,11 +13,13 @@ use crate::StorageTelemetry; use crate::{ archives::{ archive_slice::ArchiveSlice, + db_provider::ArchiveDbProvider, file_maps::{BlockRanges, FileDescription, FileMaps}, get_mc_seq_no, + package::PKG_HEADER_SIZE, package_entry::PackageEntry, package_entry_id::{parse_short_filename, GetFileName, PackageEntryId}, - package_id::PackageId, + package_id::{PackageId, PackageType}, ARCHIVE_SLICE_SIZE, KEY_ARCHIVE_PACKAGE_SIZE, }, block_handle_db::BlockHandle, @@ -28,7 +30,7 @@ use std::{ borrow::Borrow, hash::Hash, io::ErrorKind, - path::PathBuf, + path::{Path, PathBuf}, sync::{ atomic::{AtomicU8, Ordering}, Arc, @@ -38,12 +40,32 @@ use std::{ use tokio::io::AsyncWriteExt; use ton_block::{error, fail, AccountIdPrefixFull, BlockIdExt, Result, ShardIdent, MASTERCHAIN_ID}; +/// Metadata about a block being imported into the archive. +pub struct ImportBlockMeta { + pub seq_no: u32, + pub shard: ShardIdent, + pub gen_utime: u32, + pub end_lt: u64, + pub mc_ref_seq_no: u32, +} + +/// A single entry from a .pack file being imported. +pub struct ImportEntry { + pub entry_id: PackageEntryId, + pub offset: u64, + /// Metadata for Block entries. Must be Some for PackageEntryId::Block, + /// None for Proof/ProofLink. + pub block_meta: Option, +} + pub struct ArchiveManager { db: Arc, db_root_path: Arc, + db_provider: Arc, file_maps: FileMaps, shard_split_depth: Arc, unapplied_files_path: PathBuf, + create_slice_mutex: tokio::sync::Mutex<()>, #[cfg(feature = "telemetry")] telemetry: Arc, allocated: Arc, @@ -55,6 +77,7 @@ impl ArchiveManager { pub async fn with_data( db: Arc, db_root_path: Arc, + db_provider: Arc, last_unneeded_key_block: u32, shard_split_depth: Arc, #[cfg(feature = "telemetry")] telemetry: Arc, @@ -63,6 +86,7 @@ impl ArchiveManager { let file_maps = FileMaps::new( db.clone(), &db_root_path, + &db_provider, last_unneeded_key_block, #[cfg(feature = "telemetry")] &telemetry, @@ -76,9 +100,11 @@ impl ArchiveManager { let ret = Self { db, db_root_path, + db_provider, file_maps, shard_split_depth, unapplied_files_path, + create_slice_mutex: tokio::sync::Mutex::new(()), #[cfg(feature = "telemetry")] telemetry, allocated, @@ -373,14 +399,14 @@ impl ArchiveManager { } Ok(read) => read, }; - let data = self.move_file_to_archive(data, handle, entry_id, false).await?; + let data = self.add_block_data_to_package(data, handle, entry_id, false).await?; if handle.is_key_block()? { - self.move_file_to_archive(data, handle, entry_id, true).await?; + self.add_block_data_to_package(data, handle, entry_id, true).await?; } Ok(Some(filename)) } - async fn move_file_to_archive + Hash>( + pub async fn add_block_data_to_package + Hash>( &self, data: Vec, handle: &BlockHandle, @@ -404,8 +430,10 @@ impl ArchiveManager { .get_file_desc(&package_id, true) .await? .ok_or_else(|| error!("Expected some value for {package_id:?}"))?; - if !key_archive && fd.update_block_ranges(handle) { - self.file_maps.files().update(fd.id().id(), &fd).await?; + if fd.update_block_ranges(handle) { + let file_map = + if key_archive { self.file_maps.key_files() } else { self.file_maps.files() }; + file_map.update(fd.id().id(), &fd).await?; } fd.archive_slice().add_file(handle, entry_id, data).await } @@ -436,7 +464,6 @@ impl ArchiveManager { id: &PackageId, force_create: bool, ) -> Result>> { - // TODO: Rewrite logics in order to handle multithreaded adding of packages if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { if fd.deleted() { return Ok(None); @@ -444,6 +471,13 @@ impl ArchiveManager { return Ok(Some(fd)); } if force_create { + let _guard = self.create_slice_mutex.lock().await; + if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { + if fd.deleted() { + return Ok(None); + } + return Ok(Some(fd)); + } Ok(Some(self.add_file_desc(id).await?)) } else { Ok(None) @@ -451,14 +485,17 @@ impl ArchiveManager { } async fn add_file_desc(&self, id: &PackageId) -> Result> { - // TODO: Rewrite logics in order to handle multithreaded adding of packages let file_map = self.file_maps.get(id.package_type()); assert!(file_map.get(id.id()).await.is_none()); - let dir = self.db_root_path.join(id.path()); + let (slice_db, slice_root_path) = match id.package_type() { + PackageType::KeyBlocks => (self.db.clone(), Arc::clone(&self.db_root_path)), + PackageType::Blocks => self.db_provider.db_for_archive(id.id()).await?, + }; + let dir = slice_root_path.join(id.path()); tokio::fs::create_dir_all(&dir).await?; let archive_slice = ArchiveSlice::new_empty( - self.db.clone(), - Arc::clone(&self.db_root_path), + slice_db, + slice_root_path, id.id(), id.package_type(), self.shard_split_depth.load(Ordering::Relaxed), @@ -522,6 +559,137 @@ impl ArchiveManager { } } + pub async fn import_package( + &self, + source_path: &Path, + archive_id: u32, + shard: &ShardIdent, + entries: &[ImportEntry], + move_file: bool, + contains_key_block: bool, + ) -> Result<()> { + let slice_id = self.get_package_id_force(archive_id, false, contains_key_block).await; + let fd = self.get_or_create_import_desc(&slice_id, shard.prefix_len()).await?; + + let pkg_id = PackageId::for_block(archive_id); + let target_path = pkg_id.full_path(fd.archive_slice().db_root_path(), shard)?; + + if target_path.exists() { + tokio::fs::remove_file(&target_path).await.map_err(|e| { + error!("Failed to remove existing file {}: {}", target_path.display(), e) + })?; + } else { + if let Some(parent) = target_path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + } + + if move_file { + tokio::fs::rename(source_path, &target_path).await.map_err(|e| { + error!( + "Failed to move {} to {}: {}", + source_path.display(), + target_path.display(), + e + ) + })?; + } else { + tokio::fs::copy(source_path, &target_path).await.map_err(|e| { + error!( + "Failed to copy {} to {}: {}", + source_path.display(), + target_path.display(), + e + ) + })?; + } + + let file_len = tokio::fs::metadata(&target_path).await?.len(); + let file_size = file_len.checked_sub(PKG_HEADER_SIZE as u64).ok_or_else(|| { + error!("Package file {} is too short ({} bytes)", target_path.display(), file_len) + })?; + + fd.archive_slice().import_package_entries(archive_id, &shard, file_size, entries).await?; + + let file_map = self.file_maps.get(PackageType::Blocks); + let mut ranges_updated = false; + for entry in entries { + if let Some(meta) = &entry.block_meta { + ranges_updated |= fd.update_block_ranges_raw( + &meta.shard, + meta.seq_no, + meta.gen_utime, + meta.end_lt, + ); + } + } + if ranges_updated { + file_map.update(fd.id().id(), &fd).await?; + } + + Ok(()) + } + + async fn get_or_create_import_desc( + &self, + id: &PackageId, + shard_split_depth: u8, + ) -> Result> { + if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { + if !fd.deleted() { + return Ok(fd); + } + } + let _guard = self.create_slice_mutex.lock().await; + if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { + if !fd.deleted() { + return Ok(fd); + } + } + self.add_file_desc_for_import(id, shard_split_depth).await + } + + async fn add_file_desc_for_import( + &self, + id: &PackageId, + shard_split_depth: u8, + ) -> Result> { + let file_map = self.file_maps.get(id.package_type()); + let (slice_db, slice_root_path) = match id.package_type() { + PackageType::KeyBlocks => (self.db.clone(), Arc::clone(&self.db_root_path)), + PackageType::Blocks => self.db_provider.db_for_archive(id.id()).await?, + }; + let dir = slice_root_path.join(id.path()); + tokio::fs::create_dir_all(&dir).await?; + let archive_slice = ArchiveSlice::new_for_import( + slice_db, + slice_root_path, + id.id(), + id.package_type(), + shard_split_depth, + #[cfg(feature = "telemetry")] + self.telemetry.clone(), + self.allocated.clone(), + ) + .await?; + let fd = Arc::new(FileDescription::with_data( + id.clone(), + archive_slice, + false, + lockfree::map::Map::new(), + )); + file_map + .put( + id.id(), + Arc::clone(&fd), + #[cfg(feature = "telemetry")] + &self.telemetry, + &self.allocated, + ) + .await?; + Ok(fd) + } + pub async fn trunc bool>( &self, block_id: &BlockIdExt, @@ -568,6 +736,31 @@ impl ArchiveManager { Ok(()) } + pub async fn get_max_mc_seqno(&self) -> Option { + let fd = self.file_maps.files().get_closest(u32::MAX).await?; + let guard = fd.blocks_ranges().get(&ShardIdent::masterchain())?; + Some(guard.val().max_seqno.load(Ordering::Relaxed)) + } + + pub async fn get_max_key_block_seqno(&self) -> Option { + let fd = self.file_maps.key_files().get_closest(u32::MAX).await?; + let guard = fd.blocks_ranges().get(&ShardIdent::masterchain())?; + Some(guard.val().max_seqno.load(Ordering::Relaxed)) + } + + pub async fn lookup_proof_by_seqno( + &self, + prefix: &AccountIdPrefixFull, + seqno: u32, + ) -> Result)>> { + if let Some(fd) = + self.lookup_file_descr_by(prefix, &mut |br| br.compare_seqno(&seqno)).await + { + return fd.archive_slice().lookup_proof_by_seqno(prefix, seqno).await; + } + Ok(None) + } + async fn lookup_file_descr_by( &self, prefix: &AccountIdPrefixFull, diff --git a/src/node/storage/src/archives/archive_slice.rs b/src/node/storage/src/archives/archive_slice.rs index 6bb346e..f49b21e 100644 --- a/src/node/storage/src/archives/archive_slice.rs +++ b/src/node/storage/src/archives/archive_slice.rs @@ -12,7 +12,7 @@ use crate::StorageTelemetry; use crate::{ archives::{ - archive_manager::ArchiveManager, + archive_manager::{ArchiveManager, ImportEntry}, block_index_db::{BlockIndexDb, LookupResult}, get_mc_seq_no, package::{read_package_from, Package}, @@ -161,6 +161,86 @@ impl ArchiveSlice { Ok(ret) } + /// Create a new archive slice for importing existing .pack files. + /// Unlike `new_empty()`, this does not create an initial package file. + /// Packages are registered later via `import_package_entries()`. + pub async fn new_for_import( + db: Arc, + db_root_path: Arc, + archive_id: u32, + package_type: PackageType, + shard_split_depth: u8, + #[cfg(feature = "telemetry")] telemetry: Arc, + allocated: Arc, + ) -> Result { + let mut ret = Self::create( + db, + db_root_path, + archive_id, + package_type, + true, // finalized: prevents truncation when opening packages + true, // create_if_not_exist + shard_split_depth, + #[cfg(feature = "telemetry")] + telemetry, + allocated, + ) + .await?; + let mut transaction = ret.package_status_db.begin_transaction()?; + if ret.sliced_mode { + ret.shard_separated = true; + transaction.put(&PackageStatusKey::SlicedMode, &true.serialize())?; + transaction.put(&PackageStatusKey::TotalSlices, &0u32.serialize())?; + transaction.put(&PackageStatusKey::SliceSize, &ret.slice_size.serialize())?; + transaction + .put(&PackageStatusKey::ShardSplitDepth, &ret.shard_split_depth.serialize())?; + } else { + transaction.put(&PackageStatusKey::SlicedMode, &false.serialize())?; + transaction.put(&PackageStatusKey::NonSlicedSize, &0u64.serialize())?; + } + transaction.commit()?; + Ok(ret) + } + + pub async fn import_package_entries( + &self, + package_archive_id: u32, + shard: &ShardIdent, + file_size: u64, + entries: &[ImportEntry], + ) -> Result<()> { + let entry = PackageEntryInfo { seqno: package_archive_id, shard: shard.clone() }; + + if self.package_store.get(&entry).is_none() { + self.add_package(entry, file_size).await?; + } + + for import_entry in entries { + let offset_key = (&import_entry.entry_id).into(); + self.offsets_db.put_value(&offset_key, &import_entry.offset)?; + if let (PackageEntryId::Block(_), Some(bm)) = + (&import_entry.entry_id, &import_entry.block_meta) + { + self.block_index_db.put_raw( + &bm.shard, + bm.seq_no, + bm.end_lt, + bm.gen_utime, + bm.mc_ref_seq_no, + u32::try_from(import_entry.offset).map_err(|_| { + error!("entry offset {} exceeds u32 range", import_entry.offset) + })?, + )?; + } + } + + Ok(()) + } + + pub fn db_root_path(&self) -> &std::path::Path { + self.db_root_path.as_path() + } + #[allow(clippy::too_many_arguments)] pub async fn with_data( db: Arc, @@ -344,6 +424,58 @@ impl ArchiveSlice { } } + async fn add_package(&self, entry: PackageEntryInfo, size: u64) -> Result<()> { + let try_add_package = async |package_count, entry: &PackageEntryInfo| { + if self + .new_package(entry.clone(), Some(package_count), size, DEFAULT_PKG_VERSION) + .await? + { + let info = if self.shard_separated { Some(entry) } else { None }; + self.entry_db.put_value( + &package_count.into(), + &PackageEntryMeta::with_data(size, DEFAULT_PKG_VERSION, info), + )?; + self.package_status_db + .put_value(&PackageStatusKey::TotalSlices, &(package_count + 1))?; + Ok(true) + } else { + Ok(false) + } + }; + loop { + const BUSY: u32 = 0x80000000; + let package_count = self.package_count.fetch_or(BUSY, Ordering::Relaxed); + if (package_count & BUSY) != 0 { + tokio::task::yield_now().await; + continue; + } + let result = try_add_package(package_count, &entry).await; + let new_count = match &result { + Err(_) | Ok(false) => package_count, + Ok(true) => package_count + 1, + }; + if self + .package_count + .compare_exchange( + package_count | BUSY, + new_count, + Ordering::Relaxed, + Ordering::Relaxed, + ) + .is_err() + && result.is_ok() + { + tokio::task::yield_now().await; + continue; + } + if let Err(e) = result { + break Err(e); + } else { + break Ok(()); + } + } + } + pub async fn add_file + Hash>( &self, block_handle: &BlockHandle, @@ -372,55 +504,7 @@ impl ArchiveSlice { mc_seq_no - (mc_seq_no - self.archive_id) / self.slice_size ) } - let try_add_package = async |package_count, entry: &PackageEntryInfo| { - if self - .new_package(entry.clone(), Some(package_count), 0, DEFAULT_PKG_VERSION) - .await? - { - let info = if self.shard_separated { Some(entry) } else { None }; - self.entry_db.put_value( - &package_count.into(), - &PackageEntryMeta::with_data(0, DEFAULT_PKG_VERSION, info), - )?; - self.package_status_db - .put_value(&PackageStatusKey::TotalSlices, &(package_count + 1))?; - Ok(true) - } else { - Ok(false) - } - }; - loop { - const BUSY: u32 = 0x80000000; - let package_count = self.package_count.fetch_or(BUSY, Ordering::Relaxed); - if (package_count & BUSY) != 0 { - tokio::task::yield_now().await; - continue; - } - let result = try_add_package(package_count, &entry).await; - let new_count = match &result { - Err(_) | Ok(false) => package_count, - Ok(true) => package_count + 1, - }; - if self - .package_count - .compare_exchange( - package_count | BUSY, - new_count, - Ordering::Relaxed, - Ordering::Relaxed, - ) - .is_err() - && result.is_ok() - { - tokio::task::yield_now().await; - continue; - } - if let Err(e) = result { - return Err(e); - } else { - break; - } - } + self.add_package(entry, 0).await?; } } }; @@ -451,14 +535,23 @@ impl ArchiveSlice { &self, block_handle: &BlockHandle, entry_id: &PackageEntryId, + ) -> Result> { + let mc_seq_no = get_mc_seq_no(block_handle); + let shard = block_handle.id().shard(); + self.get_file_raw(mc_seq_no, &shard, entry_id).await + } + + async fn get_file_raw + Hash>( + &self, + mc_seq_no: u32, + shard: &ShardIdent, + entry_id: &PackageEntryId, ) -> Result> { let offset_key = entry_id.into(); let offset = match self.offsets_db.try_get_value(&offset_key)? { Some(offset) => offset, None => return Ok(None), }; - let mc_seq_no = get_mc_seq_no(block_handle); - let shard = block_handle.id().shard(); let package_info = match self.choose_package(mc_seq_no, shard).await? { ChosenPackage::Info(info) => info, ChosenPackage::Slot(_) => { @@ -569,6 +662,31 @@ impl ArchiveSlice { self.get_block_by_lookup_result(lr).await } + pub async fn lookup_proof_by_seqno( + &self, + prefix: &AccountIdPrefixFull, + seqno: u32, + ) -> Result)>> { + let Some(lr) = self.block_index_db.lookup_by_seqno(prefix, seqno)? else { + return Ok(None); + }; + let mc_seq_no = lr.mc_ref; + let Some((block_id, _)) = self.get_block_by_lookup_result(lr).await? else { + return Ok(None); + }; + + // Masterchain blocks store proofs under `Proof`, shard blocks under `ProofLink`. + let entry_id = if block_id.shard().is_masterchain() { + PackageEntryId::Proof(block_id.clone()) + } else { + PackageEntryId::ProofLink(block_id.clone()) + }; + + self.get_file_raw(mc_seq_no, block_id.shard(), &entry_id) + .await + .map(|opt_entry| opt_entry.map(|entry| (block_id, entry.take_data()))) + } + pub async fn lookup_block_by_lt( &self, prefix: &AccountIdPrefixFull, @@ -928,7 +1046,7 @@ impl ArchiveSlice { .map_err(|e| error!("Cannot create directory {} : {e}", parent.display()))?; if add_unbound_object_to_map(&self.package_store, entry.clone(), || Ok(OnceLock::new()))? { let create_package = async || { - let package = match Package::open(path.clone(), false, true).await { + let package = match Package::open(path.clone(), self.finalized, true).await { Ok(p) => p, Err(e) => match tokio::fs::remove_file(path.as_path()).await { Ok(_) => fail!( diff --git a/src/node/storage/src/archives/block_index_db.rs b/src/node/storage/src/archives/block_index_db.rs index e87fcee..6493868 100644 --- a/src/node/storage/src/archives/block_index_db.rs +++ b/src/node/storage/src/archives/block_index_db.rs @@ -164,25 +164,40 @@ impl BlockIndexDb { offset, block.masterchain_ref_seq_no() ); + self.put_raw( + block.id().shard(), + block.id().seq_no(), + block.end_lt(), + block.gen_utime(), + block.masterchain_ref_seq_no(), + offset, + ) + } + /// Write block index entries from raw values (for archive import). + pub fn put_raw( + &self, + shard: &ShardIdent, + seq_no: u32, + end_lt: u64, + gen_utime: u32, + mc_ref_seq_no: u32, + offset: u32, + ) -> Result<()> { let cf = self.cf()?; - let value = Self::serialize_value(block.masterchain_ref_seq_no(), offset); + let value = Self::serialize_value(mc_ref_seq_no, offset); let mut transaction = rocksdb::WriteBatch::default(); - let key = BlocksIndexKey::key_with_lt(block.id().shard(), block.end_lt()); + let key = BlocksIndexKey::key_with_lt(shard, end_lt); log::trace!("Putting key: {}", key); transaction.put_cf(&cf, &key, value); - let key = BlocksIndexKey::key_with_seqno(block.id().shard(), block.id().seq_no()); + let key = BlocksIndexKey::key_with_seqno(shard, seq_no); log::trace!("Putting key: {}", key); transaction.put_cf(&cf, &key, value); - let key = BlocksIndexKey::key_with_utime( - block.id().shard(), - block.gen_utime(), - block.id().seq_no(), - ); + let key = BlocksIndexKey::key_with_utime(shard, gen_utime, seq_no); log::trace!("Putting key: {}", key); transaction.put_cf(&cf, &key, value); diff --git a/src/node/storage/src/archives/db_provider.rs b/src/node/storage/src/archives/db_provider.rs new file mode 100644 index 0000000..b574192 --- /dev/null +++ b/src/node/storage/src/archives/db_provider.rs @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use super::epoch::EpochRouter; +use crate::db::rocksdb::RocksDb; +use std::{path::PathBuf, sync::Arc}; +use ton_block::Result; + +/// Abstracts over single-db and epoch-based db selection for archive slices. +/// Provides the correct RocksDb instance and root path for a given archive_id. +#[async_trait::async_trait] +pub trait ArchiveDbProvider: Send + Sync { + /// Get the root path and RocksDb instance for the archive slice + async fn db_for_archive(&self, archive_id: u32) -> Result<(Arc, Arc)>; +} + +/// Single shared RocksDb, single root path. +/// Used when archival_mode is not configured. +pub struct SingleDbProvider { + db: Arc, + db_root_path: Arc, +} + +impl SingleDbProvider { + pub fn new(db: Arc, db_root_path: Arc) -> Self { + Self { db, db_root_path } + } +} + +#[async_trait::async_trait] +impl ArchiveDbProvider for SingleDbProvider { + async fn db_for_archive(&self, _archive_id: u32) -> Result<(Arc, Arc)> { + Ok((self.db.clone(), self.db_root_path.clone())) + } +} + +/// Epoch-based provider: routes archive requests to the correct epoch's RocksDb and path. +pub struct EpochDbProvider { + router: Arc, +} + +impl EpochDbProvider { + pub fn new(router: Arc) -> Self { + Self { router } + } + + pub fn router(&self) -> &Arc { + &self.router + } +} + +#[async_trait::async_trait] +impl ArchiveDbProvider for EpochDbProvider { + async fn db_for_archive(&self, archive_id: u32) -> Result<(Arc, Arc)> { + let epoch_db = self.router.resolve_or_create(archive_id).await?; + Ok((epoch_db.db().clone(), epoch_db.path().clone())) + } +} diff --git a/src/node/storage/src/archives/epoch.rs b/src/node/storage/src/archives/epoch.rs new file mode 100644 index 0000000..5251d28 --- /dev/null +++ b/src/node/storage/src/archives/epoch.rs @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use crate::{ + archives::ARCHIVE_SLICE_SIZE, + db::rocksdb::{AccessType, RocksDb}, + TARGET, +}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use ton_block::{error, fail, Result}; + +const EPOCH_META_FILENAME: &str = "epoch_meta.json"; + +/// Persisted metadata for an epoch directory +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub(crate) struct EpochMeta { + pub mc_seq_no_start: u32, + pub mc_seq_no_end: u32, +} + +async fn read_epoch_meta(epoch_path: &Path) -> Result { + let meta_path = epoch_path.join(EPOCH_META_FILENAME); + let data = tokio::fs::read_to_string(&meta_path) + .await + .map_err(|e| error!("Cannot read {}: {}", meta_path.display(), e))?; + serde_json::from_str(&data).map_err(|e| error!("Cannot parse {}: {}", meta_path.display(), e)) +} + +pub(crate) async fn write_epoch_meta(epoch_path: &Path, meta: &EpochMeta) -> Result<()> { + let meta_path = epoch_path.join(EPOCH_META_FILENAME); + let data = serde_json::to_string_pretty(meta) + .map_err(|e| error!("Cannot serialize epoch meta: {}", e))?; + tokio::fs::write(&meta_path, data.as_bytes()) + .await + .map_err(|e| error!("Cannot write {}: {}", meta_path.display(), e)) +} + +/// Configuration for a single existing epoch directory +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct EpochEntry { + pub path: PathBuf, +} + +/// Archival mode configuration. +/// When present, archives are split into epochs and GC is disabled. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ArchivalModeConfig { + /// Number of MC blocks per epoch. Must be a positive multiple of ARCHIVE_SLICE_SIZE (20_000). + pub epoch_size: u32, + /// Path where new epoch directories will be created + pub new_epochs_path: PathBuf, + /// List of existing epoch directories, ordered by ascending MC seq_no. + #[serde(default)] + pub existing_epochs: Vec, +} + +/// Runtime state for a single epoch +pub struct Epoch { + mc_seq_no_start: u32, + mc_seq_no_end: u32, + path: Arc, + db: Arc, +} + +impl Epoch { + pub fn mc_seq_no_start(&self) -> u32 { + self.mc_seq_no_start + } + + pub fn mc_seq_no_end(&self) -> u32 { + self.mc_seq_no_end + } + + pub fn path(&self) -> &Arc { + &self.path + } + + pub fn db(&self) -> &Arc { + &self.db + } +} + +/// Routes mc_seq_no to the appropriate epoch's RocksDb and filesystem path. +/// +/// All epochs must have the same size (`epoch_size`), which allows O(1) arithmetic lookup +/// without any map search. +pub struct EpochRouter { + epochs: lockfree::map::Map>, + epoch_size: u32, + new_epochs_path: PathBuf, + creation_mutex: tokio::sync::Mutex<()>, +} + +impl EpochRouter { + pub async fn new(config: &ArchivalModeConfig) -> Result { + if config.epoch_size == 0 || config.epoch_size % ARCHIVE_SLICE_SIZE != 0 { + fail!( + "epoch_size must be a positive multiple of ARCHIVE_SLICE_SIZE ({}), got {}", + ARCHIVE_SLICE_SIZE, + config.epoch_size + ); + } + + let epochs = lockfree::map::Map::new(); + + for (i, entry) in config.existing_epochs.iter().enumerate() { + if !entry.path.exists() { + fail!("Epoch {} path does not exist: {}", i, entry.path.display()); + } + + let meta = read_epoch_meta(&entry.path).await?; + Self::validate_epoch_meta(&meta, config.epoch_size, &entry.path)?; + + let db = RocksDb::new(&entry.path, "archive_db", None, AccessType::ReadWrite)?; + + log::info!( + target: TARGET, + "Opened epoch {}: mc_seq_no [{}, {}], path: {}", + i, meta.mc_seq_no_start, meta.mc_seq_no_end, entry.path.display() + ); + + epochs.insert( + meta.mc_seq_no_start, + Arc::new(Epoch { + mc_seq_no_start: meta.mc_seq_no_start, + mc_seq_no_end: meta.mc_seq_no_end, + path: Arc::new(entry.path.clone()), + db, + }), + ); + } + + tokio::fs::create_dir_all(&config.new_epochs_path).await.map_err(|e| { + error!("Cannot create new_epochs_path {}: {}", config.new_epochs_path.display(), e) + })?; + + // Discover epochs previously created in new_epochs_path (survive restarts) + let mut read_dir = tokio::fs::read_dir(&config.new_epochs_path).await.map_err(|e| { + error!("Cannot read new_epochs_path {}: {}", config.new_epochs_path.display(), e) + })?; + let mut discovered = Vec::new(); + while let Some(entry) = read_dir + .next_entry() + .await + .map_err(|e| error!("Error reading new_epochs_path: {}", e))? + { + let epoch_path = entry.path(); + if epoch_path.is_dir() && epoch_path.join(EPOCH_META_FILENAME).exists() { + discovered.push(epoch_path); + } + } + + for epoch_path in discovered { + let meta = read_epoch_meta(&epoch_path).await?; + Self::validate_epoch_meta(&meta, config.epoch_size, &epoch_path)?; + + // Skip if already loaded from existing_epochs + if epochs.get(&meta.mc_seq_no_start).is_some() { + continue; + } + + let db = RocksDb::new(&epoch_path, "archive_db", None, AccessType::ReadWrite)?; + + log::info!( + target: TARGET, + "Discovered epoch: mc_seq_no [{}, {}], path: {}", + meta.mc_seq_no_start, meta.mc_seq_no_end, epoch_path.display() + ); + + epochs.insert( + meta.mc_seq_no_start, + Arc::new(Epoch { + mc_seq_no_start: meta.mc_seq_no_start, + mc_seq_no_end: meta.mc_seq_no_end, + path: Arc::new(epoch_path), + db, + }), + ); + } + + Ok(Self { + epochs, + epoch_size: config.epoch_size, + new_epochs_path: config.new_epochs_path.clone(), + creation_mutex: tokio::sync::Mutex::new(()), + }) + } + + pub fn resolve(&self, mc_seq_no: u32) -> Option> { + let start = (mc_seq_no / self.epoch_size) * self.epoch_size; + self.epochs.get(&start).map(|g| Arc::clone(g.val())) + } + + /// Resolve the epoch for a given mc_seq_no, creating a new one if needed. + pub async fn resolve_or_create(&self, mc_seq_no: u32) -> Result> { + if let Some(epoch) = self.resolve(mc_seq_no) { + return Ok(epoch); + } + + // Serialize creation to prevent concurrent RocksDb::new() on the same path + let _creation_guard = self.creation_mutex.lock().await; + + // Double-check after acquiring the mutex โ€” another caller may have created the epoch + if let Some(epoch) = self.resolve(mc_seq_no) { + return Ok(epoch); + } + + let epoch_index = mc_seq_no / self.epoch_size; + let start = epoch_index * self.epoch_size; + let end = start + self.epoch_size - 1; + + let epoch_dir = self.new_epochs_path.join(format!("epoch_{}", epoch_index)); + tokio::fs::create_dir_all(&epoch_dir) + .await + .map_err(|e| error!("Cannot create epoch directory {}: {}", epoch_dir.display(), e))?; + + let meta = EpochMeta { mc_seq_no_start: start, mc_seq_no_end: end }; + write_epoch_meta(&epoch_dir, &meta).await?; + + let db = RocksDb::new(&epoch_dir, "archive_db", None, AccessType::ReadWrite)?; + + log::info!( + target: TARGET, + "Created new epoch {}: mc_seq_no [{}, {}], path: {}", + epoch_index, start, end, epoch_dir.display() + ); + + let epoch = Arc::new(Epoch { + mc_seq_no_start: start, + mc_seq_no_end: end, + path: Arc::new(epoch_dir), + db, + }); + self.epochs.insert(start, Arc::clone(&epoch)); + + Ok(epoch) + } + + pub fn epoch_size(&self) -> u32 { + self.epoch_size + } + + fn validate_epoch_meta(meta: &EpochMeta, epoch_size: u32, path: &Path) -> Result<()> { + if meta.mc_seq_no_start % epoch_size != 0 { + fail!( + "Epoch at {} has mc_seq_no_start={} which is not aligned to epoch_size={}", + path.display(), + meta.mc_seq_no_start, + epoch_size + ); + } + let expected_end = meta.mc_seq_no_start + epoch_size - 1; + if meta.mc_seq_no_end != expected_end { + fail!( + "Epoch at {} has mc_seq_no_end={} but expected {} for epoch_size={}", + path.display(), + meta.mc_seq_no_end, + expected_end, + epoch_size + ); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "../tests/test_epoch.rs"] +mod tests; diff --git a/src/node/storage/src/archives/file_maps.rs b/src/node/storage/src/archives/file_maps.rs index fd66f4f..0a0d4ad 100644 --- a/src/node/storage/src/archives/file_maps.rs +++ b/src/node/storage/src/archives/file_maps.rs @@ -14,6 +14,7 @@ use crate::StorageTelemetry; use crate::{ archives::{ archive_slice::ArchiveSlice, + db_provider::{ArchiveDbProvider, SingleDbProvider}, package_id::{PackageId, PackageType}, package_index_db::{PackageIndexDb, PackageIndexEntry}, }, @@ -35,6 +36,9 @@ use std::{ }; use ton_block::{error, fail, BlockIdExt, Result, ShardIdent, LT_ALIGN}; +pub const FILES_DB_NAME: &str = "files"; +pub const KEY_FILES_DB_NAME: &str = "key_files"; + #[derive(serde::Serialize, serde::Deserialize)] pub struct BlockRanges { pub min_seqno: AtomicU32, @@ -71,17 +75,6 @@ impl Clone for BlockRanges { } } impl BlockRanges { - pub fn new(handle: &BlockHandle) -> Self { - Self { - min_seqno: AtomicU32::new(handle.id().seq_no()), - max_seqno: AtomicU32::new(handle.id().seq_no()), - min_utime: AtomicU32::new(handle.gen_utime()), - max_utime: AtomicU32::new(handle.gen_utime()), - min_lt: AtomicU64::new(handle.end_lt()), - max_lt: AtomicU64::new(handle.end_lt()), - } - } - pub fn compare_seqno(&self, seqno: &u32) -> std::cmp::Ordering { let min_sn = self.min_seqno.load(Ordering::Relaxed); let max_sn = self.max_seqno.load(Ordering::Relaxed); @@ -150,6 +143,21 @@ impl FileDescription { } pub fn update_block_ranges(&self, handle: &BlockHandle) -> bool { + self.update_block_ranges_raw( + handle.id().shard(), + handle.id().seq_no(), + handle.gen_utime(), + handle.end_lt(), + ) + } + + pub fn update_block_ranges_raw( + &self, + shard: &ShardIdent, + seq_no: u32, + gen_utime: u32, + end_lt: u64, + ) -> bool { macro_rules! update_atomic { ($atomic:expr, $new:expr, $cmp_fn:expr) => {{ let mut prev = $atomic.load(Ordering::Relaxed); @@ -181,26 +189,27 @@ impl FileDescription { } let mut updated = false; - let _ = add_unbound_object_to_map_with_update( - &self.blocks_ranges, - handle.id().shard().clone(), - |prev| { - if let Some(prev) = prev { - let sn = handle.id().seq_no(); - updated |= update_min_32(&prev.min_seqno, sn); - updated |= update_max_32(&prev.max_seqno, sn); - let ut = handle.gen_utime(); - updated |= update_min_32(&prev.min_utime, ut); - updated |= update_max_32(&prev.max_utime, ut); - let lt = handle.end_lt(); - updated |= update_min_64(&prev.min_lt, lt - lt % LT_ALIGN); - updated |= update_max_64(&prev.max_lt, lt); - Ok(None) - } else { - Ok(Some(BlockRanges::new(handle))) - } - }, - ); + let _ = add_unbound_object_to_map_with_update(&self.blocks_ranges, shard.clone(), |prev| { + if let Some(prev) = prev { + updated |= update_min_32(&prev.min_seqno, seq_no); + updated |= update_max_32(&prev.max_seqno, seq_no); + updated |= update_min_32(&prev.min_utime, gen_utime); + updated |= update_max_32(&prev.max_utime, gen_utime); + updated |= update_min_64(&prev.min_lt, end_lt - end_lt % LT_ALIGN); + updated |= update_max_64(&prev.max_lt, end_lt); + Ok(None) + } else { + updated = true; + Ok(Some(BlockRanges { + min_seqno: AtomicU32::new(seq_no), + max_seqno: AtomicU32::new(seq_no), + min_utime: AtomicU32::new(gen_utime), + max_utime: AtomicU32::new(gen_utime), + min_lt: AtomicU64::new(end_lt - end_lt % LT_ALIGN), + max_lt: AtomicU64::new(end_lt), + })) + } + }); updated } @@ -241,15 +250,15 @@ pub struct FileMap { impl FileMap { pub async fn new( - db: Arc, - db_root_path: &Arc, + index_db: Arc, + db_provider: &Arc, path: impl ToString, package_type: PackageType, last_unneeded_key_block: u32, #[cfg(feature = "telemetry")] telemetry: &Arc, allocated: &Arc, ) -> Result { - let storage = PackageIndexDb::with_db(db.clone(), path, true)?; + let storage = PackageIndexDb::with_db(index_db, path, true)?; let mut index_pairs = Vec::new(); storage.for_each_deserialized(|key, value| { @@ -258,19 +267,21 @@ impl FileMap { })?; index_pairs.sort_by_key(|pair| pair.0); + let last = index_pairs.last().map(|pair| pair.0); let mut elements = Vec::new(); for (key, value) in index_pairs { let unneeded = key < last_unneeded_key_block; - let finalized = value.finalized(); + let finalized = value.finalized() && Some(key) != last; log::info!( target: TARGET, "Opening archive slice {}, finalized {}, unneeded {}", key, finalized, unneeded ); + let (slice_db, slice_root_path) = db_provider.db_for_archive(key).await?; let archive_slice = match ArchiveSlice::with_data( - db.clone(), - db_root_path.clone(), + slice_db, + slice_root_path, key, package_type, finalized, @@ -509,15 +520,18 @@ impl FileMaps { pub async fn new( db: Arc, db_root_path: &Arc, + db_provider: &Arc, last_unneeded_key_block: u32, #[cfg(feature = "telemetry")] telemetry: &Arc, allocated: &Arc, ) -> Result { + let key_db_provider: Arc = + Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())); Ok(Self { files: FileMap::new( db.clone(), - db_root_path, - "files", + db_provider, + FILES_DB_NAME, PackageType::Blocks, last_unneeded_key_block, #[cfg(feature = "telemetry")] @@ -527,15 +541,15 @@ impl FileMaps { .await?, key_files: FileMap::new( db.clone(), - db_root_path, - "key_files", + &key_db_provider, + KEY_FILES_DB_NAME, PackageType::KeyBlocks, 0, #[cfg(feature = "telemetry")] telemetry, allocated, ) - .await?, // temp_files: FileMap::new(db_root_path, path.join("temp_files"), PackageType::Temp).await?, + .await?, }) } diff --git a/src/node/storage/src/archives/mod.rs b/src/node/storage/src/archives/mod.rs index 20420b4..89d96d2 100644 --- a/src/node/storage/src/archives/mod.rs +++ b/src/node/storage/src/archives/mod.rs @@ -13,6 +13,8 @@ use crate::block_handle_db::BlockHandle; mod package_index_db; pub mod archive_manager; +pub mod db_provider; +pub mod epoch; pub mod package; pub mod package_entry; pub mod package_entry_id; @@ -21,7 +23,7 @@ mod archive_slice; mod block_index_db; mod file_maps; mod package_entry_meta_db; -mod package_id; +pub mod package_id; mod package_info; mod package_offsets_db; mod package_status_db; diff --git a/src/node/storage/src/archives/package.rs b/src/node/storage/src/archives/package.rs index 48eceda..97d523b 100644 --- a/src/node/storage/src/archives/package.rs +++ b/src/node/storage/src/archives/package.rs @@ -45,27 +45,36 @@ async fn read_header(reader: &mut R) -> Resu impl Package { pub async fn open(path: PathBuf, read_only: bool, create: bool) -> Result { - let mut file = Self::open_file_ext(read_only, create, path.as_path()).await?; - let mut size = file.metadata().await?.len(); - - file.seek(SeekFrom::Start(0)).await?; - if size < PKG_HEADER_SIZE as u64 { - if !create { + let (file, size) = if read_only { + let size = tokio::fs::metadata(&path).await?.len(); + if size < PKG_HEADER_SIZE as u64 { fail!("Package file is too short") } - file.write_all(&PKG_HEADER_MAGIC.to_le_bytes()).await?; - file.flush().await?; - size = PKG_HEADER_SIZE as u64; + (None, size) } else { - read_header(&mut file).await?; - file.seek(SeekFrom::End(0)).await?; - } + let mut file = Self::open_file_ext(read_only, create, path.as_path()).await?; + let mut size = file.metadata().await?.len(); + + file.seek(SeekFrom::Start(0)).await?; + if size < PKG_HEADER_SIZE as u64 { + if !create { + fail!("Package file is too short") + } + file.write_all(&PKG_HEADER_MAGIC.to_le_bytes()).await?; + file.flush().await?; + size = PKG_HEADER_SIZE as u64; + } else { + read_header(&mut file).await?; + file.seek(SeekFrom::End(0)).await?; + } + (Some(file), size) + }; Ok(Self { path, read_only, size: AtomicU64::new(size), - write_mutex: tokio::sync::Mutex::new(Some(file)), + write_mutex: tokio::sync::Mutex::new(file), }) } @@ -102,22 +111,21 @@ impl Package { pub async fn truncate(&self, size: u64) -> Result<()> { let new_size = PKG_HEADER_SIZE as u64 + size; - // let md = tokio::fs::metadata(self.path()).await?; - // if md.len() == new_size { - // return Ok(()) - // } - log::debug!( - target: TARGET, - "Truncating package {}, new size: {new_size} bytes", - self.path.display() - ); - self.size.store(new_size, Ordering::SeqCst); let Some(file) = &*self.write_mutex.lock().await else { fail!( "Cannot truncate package file {}, because it was not opened", self.path().display() ) }; + let old_raw = self.size.load(Ordering::SeqCst); + let old_file_len = file.metadata().await?.len(); + log::warn!( + target: TARGET, + "Truncating package {}: raw_size {old_raw} -> {new_size}, \ + file_len {old_file_len} -> {new_size}", + self.path.display() + ); + self.size.store(new_size, Ordering::SeqCst); file.set_len(new_size).await?; Ok(()) } @@ -164,23 +172,34 @@ impl Package { self.path().display() ) }; - let actual = file.metadata().await?.len(); - let entry_offset = self.size(); - if entry_offset + PKG_HEADER_SIZE as u64 != actual { + let actual_before = file.metadata().await?.len(); + let raw_size = self.size.load(Ordering::SeqCst); + let entry_offset = raw_size - PKG_HEADER_SIZE as u64; + if raw_size != actual_before { log::error!( target: TARGET, - "Package entry {} offset mismatch: expected {entry_offset} vs {actual}", - entry.filename() + "Package {} entry {} offset mismatch BEFORE write: \ + raw_size={raw_size}, file_len={actual_before}, \ + diff={}, entry_data_len={}, entry_filename={}", + self.path.display(), + entry.filename(), + actual_before as i64 - raw_size as i64, + entry.data().len(), + entry.filename(), ) } let entry_size = entry.write_to(file).await?; let total_size = self.size.fetch_add(entry_size, Ordering::SeqCst) + entry_size; - let actual = file.metadata().await?.len(); - if total_size != actual { + let actual_after = file.metadata().await?.len(); + if total_size != actual_after { log::error!( target: TARGET, - "Package entry {} size mismatch: expected {total_size} vs {actual}", - entry.filename() + "Package {} entry {} size mismatch AFTER write: \ + expected_total={total_size}, file_len={actual_after}, \ + diff={}, entry_size={entry_size}, raw_size_before={raw_size}", + self.path.display(), + entry.filename(), + actual_after as i64 - total_size as i64, ) } after_append(entry_offset, entry_offset + entry_size) diff --git a/src/node/storage/src/archives/package_entry.rs b/src/node/storage/src/archives/package_entry.rs index 2fcfdf8..1cb705e 100644 --- a/src/node/storage/src/archives/package_entry.rs +++ b/src/node/storage/src/archives/package_entry.rs @@ -109,4 +109,11 @@ impl PackageEntry { pub fn take_data(self) -> Vec { self.data } + + /// Returns the serialized size of this entry (header + filename + data). + pub fn serialized_size(&self) -> u64 { + PKG_ENTRY_HEADER_SIZE as u64 + + self.filename.as_bytes().len() as u64 + + self.data.len() as u64 + } } diff --git a/src/node/storage/src/archives/package_id.rs b/src/node/storage/src/archives/package_id.rs index 21e2988..7d59352 100644 --- a/src/node/storage/src/archives/package_id.rs +++ b/src/node/storage/src/archives/package_id.rs @@ -13,13 +13,13 @@ use std::path::{Path, PathBuf}; use ton_block::{fail, Result, ShardIdent}; #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub(crate) enum PackageType { +pub enum PackageType { Blocks, KeyBlocks, //Temp } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub(crate) struct PackageId { +pub struct PackageId { id: u32, package_type: PackageType, } diff --git a/src/node/storage/src/block_handle_db.rs b/src/node/storage/src/block_handle_db.rs index ecc9837..f0f6037 100644 --- a/src/node/storage/src/block_handle_db.rs +++ b/src/node/storage/src/block_handle_db.rs @@ -29,25 +29,27 @@ use ton_block::{error, fail, BlockIdExt, Result, ShardIdent, UInt256}; #[path = "tests/test_block_handle_db.rs"] mod tests; -const FLAG_DATA: u32 = 0x00000001; -const FLAG_PROOF: u32 = 0x00000002; -const FLAG_PROOF_LINK: u32 = 0x00000004; +pub(crate) const FLAG_DATA: u32 = 0x00000001; +pub(crate) const FLAG_PROOF: u32 = 0x00000002; +pub(crate) const FLAG_PROOF_LINK: u32 = 0x00000004; //const FLAG_EXT_DB: u32 = 0x00000008; -const FLAG_STATE: u32 = 0x00000010; +pub(crate) const FLAG_STATE: u32 = 0x00000010; const FLAG_PERSISTENT_STATE: u32 = 0x00000020; const FLAG_NEXT_1: u32 = 0x00000040; const FLAG_NEXT_2: u32 = 0x00000080; -const FLAG_PREV_1: u32 = 0x00000100; -const FLAG_PREV_2: u32 = 0x00000200; -const FLAG_APPLIED: u32 = 0x00000400; +pub(crate) const FLAG_PREV_1: u32 = 0x00000100; +pub(crate) const FLAG_PREV_2: u32 = 0x00000200; +pub(crate) const FLAG_APPLIED: u32 = 0x00000400; pub(crate) const FLAG_KEY_BLOCK: u32 = 0x00000800; -const FLAG_MOVED_TO_ARCHIVE: u32 = 0x00002000; -const FLAG_STATE_SAVED: u32 = 0x00010000; +pub(crate) const FLAG_MOVED_TO_ARCHIVE: u32 = 0x00002000; +pub(crate) const FLAG_STATE_SAVED: u32 = 0x00010000; const FLAG_HAS_FULL_ID: u32 = 0x00020000; // not serializing flags (possible flags - 1, 2, 4, 8) const FLAG_ARCHIVING: u32 = 0x80000000; +pub const VALIDATOR_STATE_DB_NAME: &str = "validator_state_db"; + db_impl_base!(NodeStateDb, &'static str); /// Meta information related to block @@ -436,6 +438,8 @@ impl Drop for BlockHandle { // Real value is // - BlockMeta if FLAG_HAS_FULL_ID is not set // - BlockMeta + wc (i32) + shard (u64) + seqno (u32) + file_hash (UInt256) if FLAG_HAS_FULL_ID is set +pub const BLOCK_HANDLE_DB_NAME: &str = "block_handle_db"; + db_impl_base!(BlockHandleDb, BlockIdExt); declare_counted!( @@ -464,6 +468,7 @@ pub trait Callback: Sync + Send { pub struct BlockHandleStorage { handle_db: Arc, handle_cache: Arc, + no_cache: bool, full_node_state_db: Arc, validator_state_db: Arc, state_cache: lockfree::map::Map>, @@ -485,6 +490,7 @@ impl BlockHandleStorage { let ret = Self { handle_db: handle_db.clone(), handle_cache: Arc::new(lockfree::map::Map::new()), + no_cache: false, full_node_state_db: full_node_state_db.clone(), validator_state_db: validator_state_db.clone(), state_cache: lockfree::map::Map::new(), @@ -578,6 +584,10 @@ impl BlockHandleStorage { ret } + pub fn set_no_cache(&mut self) { + self.no_cache = true; + } + pub fn create_handle( &self, id: BlockIdExt, @@ -613,10 +623,13 @@ impl BlockHandleStorage { pub fn load_full_block_id(&self, root_hash: &UInt256) -> Result> { log::trace!(target: TARGET, "load_full_block_id {:x}", root_hash); - let weak = self.handle_cache.get(root_hash); - if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { - Ok(Some(handle.id.clone())) - } else if let Some(data) = self.handle_db.try_get_raw(root_hash.as_slice())? { + if !self.no_cache { + let weak = self.handle_cache.get(root_hash); + if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { + return Ok(Some(handle.id.clone())); + } + } + if let Some(data) = self.handle_db.try_get_raw(root_hash.as_slice())? { Ok(BlockHandle::deserialize_full_id(root_hash, &data)?) } else { Ok(None) @@ -642,8 +655,11 @@ impl BlockHandleStorage { pub fn save_handle( &self, handle: &Arc, - callback: Option>, + callback: Option>, // not invoked in no-cache mode ) -> Result<()> { + if self.no_cache { + return self.handle_db.put_raw(handle.id().root_hash().as_slice(), &handle.serialize()); + } self.storer .send((StoreJob::SaveHandle(handle.clone()), callback)) .map_err(|_| error!("Cannot store handle {}: storer thread dropped", handle.id())) @@ -705,23 +721,35 @@ impl BlockHandleStorage { ) -> Result>> { let rh = id.root_hash().clone(); let ret = Arc::new(BlockHandle::with_values(id, meta, self.handle_cache.clone())); - let added = add_counted_object_to_map(&self.handle_cache, rh, || { - let ret = HandleObject { - object: Arc::downgrade(&ret), - counter: self.allocated.handles.clone().into(), - }; - #[cfg(feature = "telemetry")] - self.telemetry.handles.update(self.allocated.handles.load(Ordering::Relaxed)); - Ok(ret) - })?; - if added { - if store { - self.save_handle(&ret, callback)? + let ret = if self.no_cache { + if self.handle_db.try_get_raw(rh.as_slice())?.is_some() { + None + } else { + if store { + self.save_handle(&ret, callback)? + } + Some(ret) } - Ok(Some(ret)) } else { - Ok(None) - } + let added = add_counted_object_to_map(&self.handle_cache, rh, || { + let ret = HandleObject { + object: Arc::downgrade(&ret), + counter: self.allocated.handles.clone().into(), + }; + #[cfg(feature = "telemetry")] + self.telemetry.handles.update(self.allocated.handles.load(Ordering::Relaxed)); + Ok(ret) + })?; + if added { + if store { + self.save_handle(&ret, callback)? + } + Some(ret) + } else { + None + } + }; + Ok(ret) } fn create_state(&self, key: String, id: &BlockIdExt) -> Result> { @@ -745,11 +773,7 @@ impl BlockHandleStorage { } else { log::trace!(target: TARGET, "load block handle by id {id}") } - let ret = loop { - let weak = self.handle_cache.get(id.root_hash()); - if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { - break Some(handle); - } + let ret = if self.no_cache { if let Some(data) = self.handle_db.try_get_raw(id.root_hash().as_slice())? { let meta = if rh_only { BlockHandle::deserialize_nonchecked(&mut id, &data)? @@ -758,12 +782,31 @@ impl BlockHandleStorage { meta.set_flags(FLAG_HAS_FULL_ID); meta }; - let handle = self.create_handle_and_store(id.clone(), meta, None, false)?; - if let Some(handle) = handle { + Some(Arc::new(BlockHandle::with_values(id, meta, self.handle_cache.clone()))) + } else { + None + } + } else { + loop { + let weak = self.handle_cache.get(id.root_hash()); + if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { break Some(handle); } - } else { - break None; + if let Some(data) = self.handle_db.try_get_raw(id.root_hash().as_slice())? { + let meta = if rh_only { + BlockHandle::deserialize_nonchecked(&mut id, &data)? + } else { + let meta = BlockHandle::deserialize(&id, &data)?; + meta.set_flags(FLAG_HAS_FULL_ID); + meta + }; + let handle = self.create_handle_and_store(id.clone(), meta, None, false)?; + if let Some(handle) = handle { + break Some(handle); + } + } else { + break None; + } } }; Ok(ret) diff --git a/src/node/storage/src/block_info_db.rs b/src/node/storage/src/block_info_db.rs index 376c68d..de5f575 100644 --- a/src/node/storage/src/block_info_db.rs +++ b/src/node/storage/src/block_info_db.rs @@ -11,4 +11,9 @@ use crate::db_impl_base; use ton_block::BlockIdExt; +pub const PREV1_BLOCK_DB_NAME: &str = "prev1_block_db"; +pub const PREV2_BLOCK_DB_NAME: &str = "prev2_block_db"; +pub const NEXT1_BLOCK_DB_NAME: &str = "next1_block_db"; +pub const NEXT2_BLOCK_DB_NAME: &str = "next2_block_db"; + db_impl_base!(BlockInfoDb, BlockIdExt); diff --git a/src/node/storage/src/cell_db.rs b/src/node/storage/src/cell_db.rs new file mode 100644 index 0000000..1ad3c4f --- /dev/null +++ b/src/node/storage/src/cell_db.rs @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +#[cfg(feature = "telemetry")] +use crate::StorageTelemetry; +use crate::{ + db::rocksdb::RocksDb, + shardstate_db_async::CellsDbConfig, + types::{StoredCell, StoringCell}, + StorageAlloc, TARGET, +}; +#[cfg(feature = "telemetry")] +use std::sync::atomic::{AtomicU64, Ordering}; +use std::{ + fs::write, + io::Write, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; +use ton_block::{ + error, fail, merkle_update::CellsFactory, BuilderData, Cell, CellsStorage, Result, UInt256, +}; + +pub const BROKEN_CELL_BEACON_FILE: &str = "ton_node.broken_cell"; + +pub struct CellDb { + db: Arc, + cells_cf_name: String, + db_root_path: PathBuf, + storing_cells: Arc>, + #[cfg(feature = "telemetry")] + storing_cells_count: AtomicU64, + cell_cache: quick_cache::sync::Cache, + #[cfg(feature = "telemetry")] + telemetry: Arc, + allocated: Arc, +} + +impl CellDb { + pub fn with_db( + db: Arc, + cell_db_cf: &str, + db_root_path: impl AsRef, + config: &CellsDbConfig, + #[cfg(feature = "telemetry")] telemetry: Arc, + allocated: Arc, + ) -> Result { + if db.cf_handle(cell_db_cf).is_none() { + db.create_cf(cell_db_cf, &Self::build_cf_options(config.cells_cache_size_bytes))?; + } + Ok(Self { + db, + cells_cf_name: cell_db_cf.to_string(), + db_root_path: db_root_path.as_ref().to_path_buf(), + storing_cells: Arc::new(lockfree::map::Map::new()), + #[cfg(feature = "telemetry")] + storing_cells_count: AtomicU64::new(0), + cell_cache: quick_cache::sync::Cache::new(config.cells_lru_cache_capacity), + #[cfg(feature = "telemetry")] + telemetry, + allocated, + }) + } + + pub fn build_cf_options(cache_size: u64) -> rocksdb::Options { + let mut options = rocksdb::Options::default(); + let mut block_opts = rocksdb::BlockBasedOptions::default(); + + // specified cache for blocks. + let cache = rocksdb::Cache::new_lru_cache(cache_size as usize); + block_opts.set_block_cache(&cache); + + // save in LRU block cache also indexes and bloom filters + block_opts.set_cache_index_and_filter_blocks(true); + + // keep indexes and filters in block cache until tablereader freed + block_opts.set_pin_l0_filter_and_index_blocks_in_cache(true); + + // Setup bloom filter with length of 10 bits per key. + // This length provides less than 1% false positive rate. + block_opts.set_bloom_filter(10.0, false); + + options.set_block_based_table_factory(&block_opts); + + // Enable whole key bloom filter in memtable. + options.set_memtable_whole_key_filtering(true); + + // Amount of data to build up in memory (backed by an unsorted log + // on disk) before converting to a sorted on-disk file. + // + // Larger values increase performance, especially during bulk loads. + // Up to max_write_buffer_number write buffers may be held in memory + // at the same time, + // so you may wish to adjust this parameter to control memory usage. + // Also, a larger write buffer will result in a longer recovery time + // the next time the database is opened. + options.set_write_buffer_size(1024 * 1024 * 1024); + + // The maximum number of write buffers that are built up in memory. + // The default and the minimum number is 2, so that when 1 write buffer + // is being flushed to storage, new writes can continue to the other + // write buffer. + // If max_write_buffer_number > 3, writing will be slowed down to + // options.delayed_write_rate if we are writing to the last write buffer + // allowed. + options.set_max_write_buffer_number(4); + + // if prefix_extractor is set and memtable_prefix_bloom_size_ratio is not 0, + // create prefix bloom for memtable with the size of + // write_buffer_size * memtable_prefix_bloom_size_ratio. + // If it is larger than 0.25, it is sanitized to 0.25. + let transform = rocksdb::SliceTransform::create_fixed_prefix(32); + options.set_prefix_extractor(transform); + options.set_memtable_prefix_bloom_ratio(0.1); + + options + } + + pub fn db(&self) -> &Arc { + &self.db + } + + pub fn allocated(&self) -> &StorageAlloc { + &self.allocated + } + + pub fn cells_cf(&self) -> Result>> { + self.db + .cf_handle(&self.cells_cf_name) + .ok_or_else(|| error!("Can't get `{}` cf handle", self.cells_cf_name)) + } + + pub fn storing_cells(&self) -> &Arc> { + &self.storing_cells + } + + #[cfg(feature = "telemetry")] + pub fn telemetry(&self) -> &Arc { + &self.telemetry + } + + /// If root cell already exists in DB, load and return it. Otherwise return None. + pub fn try_load_existing_root( + self: &Arc, + root_id: &UInt256, + cells_cf: &impl rocksdb::AsColumnFamilyRef, + ) -> Result> { + #[cfg(feature = "telemetry")] + let now = std::time::Instant::now(); + if let Some(val) = self.db.get_pinned_cf(cells_cf, root_id.as_slice())? { + let cell = StoredCell::deserialize(self, root_id, &val)?; + #[cfg(feature = "telemetry")] + { + self.telemetry + .stored_cells + .update(self.allocated.storage_cells.load(Ordering::Relaxed)); + self.telemetry.loaded_cells_from_db.update(1); + self.telemetry.load_cell_from_db_time_nanos.update(now.elapsed().as_nanos() as u64); + } + Ok(Some(Cell::with_cell_impl(cell))) + } else { + Ok(None) + } + } + + /// Remove saved cell hashes from the storing_cells in-memory cache. + pub fn cleanup_storing_cells<'a>(&self, saved_ids: impl Iterator) { + for id in saved_ids { + let mut stack = vec![id.clone()]; + while let Some(id) = stack.pop() { + if let Some(removed) = self.storing_cells.remove(&id) { + log::trace!( + target: TARGET, + "CellDb::cleanup_storing_cells {:x} removed from storing_cells", id + ); + #[cfg(feature = "telemetry")] + { + let _count = self.storing_cells_count.fetch_sub(1, Ordering::Relaxed); + self.telemetry.storing_cells.update(_count - 1); + } + + for i in 0..removed.val().references_count() { + if let Ok(ref_hash) = removed.val().reference_repr_hash(i) { + stack.push(ref_hash); + } + } + } + } + } + } + + #[cfg(test)] + pub fn count(&self) -> usize { + if let Ok(cf) = self.cells_cf() { + self.db.iterator_cf(&cf, rocksdb::IteratorMode::Start).count() + } else { + 0 + } + } + + pub(crate) fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { + #[cfg(feature = "telemetry")] + let now = std::time::Instant::now(); + if let Some(cell) = self.cell_cache.get(cell_id) { + #[cfg(feature = "telemetry")] + { + self.telemetry.cell_cache_hits.update(1); + self.telemetry + .load_cell_from_cache_time_nanos + .update(now.elapsed().as_nanos() as u64); + } + return Ok(cell); + } + #[cfg(feature = "telemetry")] + self.telemetry.cell_cache_misses.update(1); + let cell = self.load_cell_uncached(cell_id, panic)?; + #[cfg(feature = "telemetry")] + let now_insert = std::time::Instant::now(); + self.cell_cache.insert(cell_id.clone(), cell.clone()); + #[cfg(feature = "telemetry")] + { + self.telemetry + .store_cell_to_cache_time_nanos + .update(now_insert.elapsed().as_nanos() as u64); + self.telemetry.cell_cache_len.update(self.cell_cache.len() as u64); + } + Ok(cell) + } + + fn load_cell_uncached(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { + #[cfg(feature = "telemetry")] + let now = std::time::Instant::now(); + let storage_cell_data = match self.db.get_pinned_cf(&self.cells_cf()?, cell_id.as_slice()) { + Ok(Some(data)) => data, + _ => { + if let Some(guard) = self.storing_cells.get(cell_id) { + log::trace!( + target: TARGET, + "CellDb::load_cell from storing_cells by id {cell_id:x}", + ); + return Ok(guard.val().clone()); + } + + if !panic { + fail!("Can't load cell {:x} from db", cell_id); + } + + log::error!("FATAL!"); + log::error!("FATAL! Can't load cell {:x} from db", cell_id); + log::error!("FATAL!"); + + let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); + write(path, "")?; + + std::thread::sleep(Duration::from_millis(100)); + std::process::exit(0xFF); + } + }; + + #[cfg(feature = "telemetry")] + let load_cell_from_db_time_nanos = now.elapsed().as_nanos() as u64; + + let storage_cell = match StoredCell::deserialize(self, cell_id, &storage_cell_data) { + Ok(cell) => Arc::new(cell), + Err(e) => { + if !panic { + fail!("Can't deserialize cell {:x} from db, error: {:?}", cell_id, e); + } + + log::error!("FATAL!"); + log::error!( + "FATAL! Can't deserialize cell {:x} from db, data: {}, error: {:?}", + cell_id, + hex::encode(&storage_cell_data), + e + ); + log::error!("FATAL!"); + + let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); + write(path, "")?; + + std::thread::sleep(Duration::from_millis(100)); + std::process::exit(0xFF); + } + }; + + #[cfg(feature = "telemetry")] + { + self.telemetry + .stored_cells + .update(self.allocated.storage_cells.load(Ordering::Relaxed)); + self.telemetry.load_cell_from_db_time_nanos.update(load_cell_from_db_time_nanos); + self.telemetry.loaded_cells_from_db.update(1); + } + + log::trace!( + target: TARGET, + "CellDb::load_cell from DB id {cell_id:x}" + ); + + Ok(Cell::with_cell_impl_arc(storage_cell)) + } +} + +impl CellsFactory for CellDb { + fn create_cell(self: Arc, builder: BuilderData) -> Result { + let cell = StoringCell::with_cell(&*builder.into_cell()?, &self)?; + let cell = Cell::with_cell_impl(cell); + let repr_hash = cell.repr_hash(); + + let mut result_cell = None; + + let result = self.storing_cells.insert_with(repr_hash, |_, inserted, found| { + if let Some((_, found)) = found { + result_cell = Some(found.clone()); + lockfree::map::Preview::Discard + } else if let Some(inserted) = inserted { + result_cell = Some(inserted.clone()); + lockfree::map::Preview::Keep + } else { + result_cell = Some(cell.clone()); + lockfree::map::Preview::New(cell.clone()) + } + }); + + let result_cell = result_cell + .ok_or_else(|| error!("INTERNAL ERROR: result_cell {:x} is None", cell.repr_hash()))?; + + match result { + lockfree::map::Insertion::Created => { + log::trace!(target: TARGET, "CellDb::create_cell {:x} - created new", cell.repr_hash()); + #[cfg(feature = "telemetry")] + { + let storing_cells_count = + self.storing_cells_count.fetch_add(1, Ordering::Relaxed); + self.telemetry.storing_cells.update(storing_cells_count + 1); + } + } + lockfree::map::Insertion::Failed(_) => { + log::trace!(target: TARGET, "CellDb::create_cell {:x} - already exists", cell.repr_hash()); + } + lockfree::map::Insertion::Updated(old) => { + fail!( + "INTERNAL ERROR: storing_cells.insert_with {:x} returned Updated({:?})", + cell.repr_hash(), + old + ) + } + } + + Ok(result_cell) + } +} + +// This wrapper-struct is added because it is impossible +// to implement foreign trait (CellByHashStorage) for foreign type (Arc) +pub struct CellByHashStorageAdapter { + db: Arc, + root_cells_data: ahash::HashMap>, +} + +impl CellByHashStorageAdapter { + pub fn new( + db: Arc, + root_cell: Option<&Cell>, + max_inmemory_cells: usize, + ) -> Result { + let mut root_cells_data = ahash::HashMap::default(); + if let Some(root_cell) = root_cell { + if db.load_cell(&root_cell.repr_hash(), false).is_err() { + let mut stack = vec![root_cell.clone()]; + while let Some(cell) = stack.pop() { + if root_cells_data.len() >= max_inmemory_cells { + fail!( + "Too many cells in boc to store in memory: {}, max_inmemory_cells: {}", + root_cells_data.len(), + max_inmemory_cells + ); + } + let cell_data = StoredCell::serialize(cell.cell_impl().deref())?; + let cell_hash = cell.repr_hash(); + root_cells_data.insert(cell_hash, cell_data); + + for i in 0..cell.references_count() { + if db.load_cell(&cell.reference_repr_hash(i)?, false).is_err() { + stack.push(cell.reference(i)?); + } + } + } + } + } + Ok(Self { db, root_cells_data }) + } +} + +impl CellsStorage for CellByHashStorageAdapter { + fn load_cell(&self, hash: &UInt256) -> Result { + if let Ok(c) = self.db.clone().load_cell_uncached(hash, false) { + Ok(c) + } else if let Some(data) = self.root_cells_data.get(hash) { + StoredCell::deserialize(&self.db, hash, data).map(Cell::with_cell_impl) + } else { + fail!("Can't load cell {:x} from db", hash); + } + } + + fn load_cell_data( + &self, + hash: &UInt256, + write_hashes: bool, + dest: &mut dyn Write, + ) -> Result<()> { + #[cfg(feature = "telemetry")] + let now = std::time::Instant::now(); + if let Ok(Some(data)) = self.db.db.get_pinned_cf(&self.db.cells_cf()?, hash.as_slice()) { + #[cfg(feature = "telemetry")] + { + self.db + .telemetry + .load_cell_from_db_time_nanos + .update(now.elapsed().as_nanos() as u64); + self.db.telemetry.loaded_cells_from_db.update(1); + } + + StoredCell::write_cell_data(&data, hash, write_hashes, dest) + } else if let Some(data) = self.root_cells_data.get(hash) { + StoredCell::write_cell_data(data, hash, write_hashes, dest) + } else { + fail!("Can't load cell {:x} from db", hash); + } + } +} diff --git a/src/node/storage/src/db/rocksdb.rs b/src/node/storage/src/db/rocksdb.rs index 41596ca..5a487ca 100644 --- a/src/node/storage/src/db/rocksdb.rs +++ b/src/node/storage/src/db/rocksdb.rs @@ -35,6 +35,8 @@ pub enum AccessType { pub const LAST_UNNEEDED_KEY_BLOCK: &str = "LastUnneededKeyBlockId"; // Latest key block we can delete in archives GC pub const NODE_STATE_DB_NAME: &str = "node_state_db"; +pub const NODE_DB_NAME: &str = "db"; +pub const CATCHAINS_DB_NAME: &str = "catchains"; pub type DbPredicateMut<'a> = &'a mut dyn FnMut(&[u8], &[u8]) -> Result; @@ -340,7 +342,7 @@ impl RocksDbTable { /// Returns true, if collection is empty; false otherwise pub fn is_empty(&self) -> Result { - Ok(self.len()? == 0) + Ok(self.db.iterator_cf(&self.cf()?, IteratorMode::Start).next().is_none()) } pub fn destroy(&mut self) -> Result { diff --git a/src/node/storage/src/dynamic_boc_archive_db.rs b/src/node/storage/src/dynamic_boc_archive_db.rs new file mode 100644 index 0000000..2588bc7 --- /dev/null +++ b/src/node/storage/src/dynamic_boc_archive_db.rs @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +#[cfg(feature = "telemetry")] +use crate::StorageTelemetry; +use crate::{ + cell_db::CellDb, db::rocksdb::RocksDb, shardstate_db_async::CellsDbConfig, types::StoredCell, + StorageAlloc, TARGET, +}; +use std::{ops::Deref, path::Path, sync::Arc}; +use ton_block::{Cell, Result, UInt256, MAX_LEVEL}; + +pub struct DynamicBocArchiveDb { + cell_db: Arc, +} + +impl DynamicBocArchiveDb { + pub fn with_db( + db: Arc, + cell_db_cf: &str, + db_root_path: impl AsRef, + config: &CellsDbConfig, + #[cfg(feature = "telemetry")] telemetry: Arc, + allocated: Arc, + ) -> Result { + let cell_db = Arc::new(CellDb::with_db( + db, + cell_db_cf, + db_root_path, + config, + #[cfg(feature = "telemetry")] + telemetry, + allocated, + )?); + Ok(Self { cell_db }) + } + + pub fn cell_db(&self) -> &Arc { + &self.cell_db + } + + /// Thread-safe append-only save. + pub fn save_boc( + &self, + root_cell: Cell, + check_stop: &(dyn Fn() -> Result<()> + Sync), + ) -> Result { + let root_id = root_cell.hash(MAX_LEVEL); + let cells_cf = self.cell_db.cells_cf()?; + + log::debug!(target: TARGET, "DynamicBocArchiveDb::save_boc {:x}", root_id); + + if let Some(existing) = self.cell_db.try_load_existing_root(&root_id, &cells_cf)? { + log::info!(target: TARGET, "DynamicBocArchiveDb::save_boc ALREADY EXISTS {:x}", root_id); + return Ok(existing); + } + + let start = std::time::Instant::now(); + + // Traverse cell tree, collect new cells + let mut new_cells = fnv::FnvHashMap::default(); + let mut visited = fnv::FnvHashSet::default(); + self.collect_new_cells(&root_cell, &mut new_cells, &mut visited, &cells_cf, check_stop)?; + let cells_traverse_time = start.elapsed().as_micros(); + + // Batch write all new cells + let wrote_cells = new_cells.len(); + let write_start = std::time::Instant::now(); + if !new_cells.is_empty() { + let mut batch = rocksdb::WriteBatch::default(); + for (id, data) in &new_cells { + batch.put_cf(&cells_cf, id.as_slice(), data); + } + self.cell_db.db().write(batch)?; + } + #[cfg(feature = "telemetry")] + if wrote_cells > 0 { + self.cell_db + .telemetry() + .boc_db_element_write_nanos + .update(write_start.elapsed().as_nanos() as u64 / wrote_cells as u64); + } + let write_time = write_start.elapsed().as_micros(); + + let now4 = std::time::Instant::now(); + self.cell_db.cleanup_storing_cells(new_cells.keys()); + let storing_cells_cleanup_time = now4.elapsed().as_micros(); + + let total_time = start.elapsed().as_micros() as u64; + #[cfg(feature = "telemetry")] + { + self.cell_db.telemetry().stored_new_cells.update(wrote_cells as u64); + self.cell_db.telemetry().save_boc_total_micros.update(total_time); + self.cell_db.telemetry().save_boc_traverse_micros.update(cells_traverse_time as u64); + self.cell_db.telemetry().save_boc_commit_micros.update(write_time as u64); + self.cell_db + .telemetry() + .save_boc_cleanup_micros + .update(storing_cells_cleanup_time as u64); + } + + log::debug!( + target: TARGET, + "DynamicBocArchiveDb::save_boc {:x} wrote {}, visited {} TIME: {} (tr:{}|cmt:{}|scc:{})", + root_id, wrote_cells, visited.len(), total_time, cells_traverse_time, write_time, + storing_cells_cleanup_time + ); + + self.cell_db.load_cell(&root_id, true) + } + + fn collect_new_cells( + &self, + cell: &Cell, + new_cells: &mut fnv::FnvHashMap>, + visited: &mut fnv::FnvHashSet, + cells_cf: &impl rocksdb::AsColumnFamilyRef, + check_stop: &(dyn Fn() -> Result<()> + Sync), + ) -> Result<()> { + check_stop()?; + let cell_id = cell.repr_hash(); + + // Already visited in this traversal (new or existing) โ€” skip + if !visited.insert(cell_id.clone()) { + return Ok(()); + } + + // Already a StoredCell (loaded from DB) + if cell.is::() { + return Ok(()); + } + + // Recurse into children first + for i in 0..cell.references_count() { + let reference = cell.reference(i)?; + self.collect_new_cells(&reference, new_cells, visited, cells_cf, check_stop)?; + } + + // Check if cell exists in DB + if self.cell_db.db().get_pinned_cf(cells_cf, cell_id.as_slice())?.is_some() { + return Ok(()); + } + + // Serialize and add to batch + let data = StoredCell::serialize(cell.deref())?; + new_cells.insert(cell_id, data); + Ok(()) + } + + /// Fast import-only save: writes all non-pruned cells from state update unconditionally, + /// without checking the DB. + pub fn save_update(&self, root_cell: Cell) -> Result<()> { + let root_id = root_cell.hash(MAX_LEVEL); + let cells_cf = self.cell_db.cells_cf()?; + + log::debug!(target: TARGET, "DynamicBocArchiveDb::save_update {:x}", root_id); + + let start = std::time::Instant::now(); + + let mut new_cells = fnv::FnvHashMap::default(); + Self::collect_cells_from_update(&root_cell, &mut new_cells)?; + let cells_traverse_time = start.elapsed().as_micros(); + + let wrote_cells = new_cells.len(); + let write_start = std::time::Instant::now(); + if !new_cells.is_empty() { + let mut batch = rocksdb::WriteBatch::default(); + for (id, data) in &new_cells { + batch.put_cf(&cells_cf, id.as_slice(), data); + } + self.cell_db.db().write(batch)?; + } + let write_time = write_start.elapsed().as_micros(); + + log::debug!( + target: TARGET, + "DynamicBocArchiveDb::save_update {:x} wrote {} TIME: {} (tr:{}|cmt:{})", + root_id, wrote_cells, start.elapsed().as_micros(), cells_traverse_time, write_time, + ); + + Ok(()) + } + + /// Collect all non-pruned cells from the tree. No DB lookups โ€” pruned branches + /// are the boundary (they represent unchanged subtrees already in the DB). + fn collect_cells_from_update( + cell: &Cell, + new_cells: &mut fnv::FnvHashMap>, + ) -> Result<()> { + let cell_id = cell.repr_hash(); + + if new_cells.contains_key(&cell_id) { + return Ok(()); + } + + // PrunedBranch = unchanged subtree, already in DB + if cell.is_pruned() && cell.level() == 0 { + return Ok(()); + } + + for i in 0..cell.references_count() { + let reference = cell.reference(i)?; + Self::collect_cells_from_update(&reference, new_cells)?; + } + + let data = StoredCell::serialize_virtual(cell.deref())?; + new_cells.insert(cell_id, data); + Ok(()) + } + + pub fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { + self.cell_db.load_cell(cell_id, panic) + } +} diff --git a/src/node/storage/src/dynamic_boc_rc_db.rs b/src/node/storage/src/dynamic_boc_rc_db.rs index 64a63d0..1dc497b 100644 --- a/src/node/storage/src/dynamic_boc_rc_db.rs +++ b/src/node/storage/src/dynamic_boc_rc_db.rs @@ -11,29 +11,15 @@ #[cfg(feature = "telemetry")] use crate::StorageTelemetry; use crate::{ - db::rocksdb::RocksDb, - shardstate_db_async::CellsDbConfig, - types::{StoredCell, StoringCell}, + cell_db::CellDb, db::rocksdb::RocksDb, shardstate_db_async::CellsDbConfig, types::StoredCell, StorageAlloc, TARGET, }; -use std::{ - fs::write, - io::{Cursor, Write}, - ops::Deref, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, - time::{Duration, Instant}, -}; +use std::{io::Cursor, ops::Deref, path::Path, sync::Arc, time::Instant}; use ton_block::{ - error, fail, merkle_update::CellsFactory, BuilderData, ByteOrderRead, Cell, CellData, - CellsStorage, CellsTempStorage, Result, UInt256, MAX_LEVEL, MAX_REFERENCES_COUNT, + error, fail, ByteOrderRead, Cell, CellData, CellsFactory, CellsTempStorage, Result, UInt256, + MAX_LEVEL, MAX_REFERENCES_COUNT, }; -pub const BROKEN_CELL_BEACON_FILE: &str = "ton_node.broken_cell"; - // FnvHashMap is a standard HashMap with FNV hasher. This hasher is bit faster than default one. pub type CellsCounters = fnv::FnvHashMap; @@ -111,17 +97,9 @@ impl VisitedCell { } pub struct DynamicBocDb { - db: Arc, - cells_cf_name: String, + cell_db: Arc, counters_cf_name: String, - db_root_path: PathBuf, - storing_cells: Arc>, - storing_cells_count: AtomicU64, cells_counters: Option>>, - cell_cache: quick_cache::sync::Cache, - #[cfg(feature = "telemetry")] - telemetry: Arc, - allocated: Arc, } impl DynamicBocDb { @@ -134,11 +112,17 @@ impl DynamicBocDb { #[cfg(feature = "telemetry")] telemetry: Arc, allocated: Arc, ) -> Result { - if db.cf_handle(cell_db_cf).is_none() { - db.create_cf(cell_db_cf, &Self::build_cells_cf_options(config))?; - } + let cell_db = CellDb::with_db( + db.clone(), + cell_db_cf, + db_root_path.as_ref(), + config, + #[cfg(feature = "telemetry")] + telemetry, + allocated, + )?; if db.cf_handle(counters_cf_name).is_none() { - db.create_cf(counters_cf_name, &Self::build_cells_cf_options(config))?; + db.create_cf(counters_cf_name, &Self::build_counters_cf_options(config))?; } let cells_counters = if config.prefill_cells_counters { let counters = CellsCounters::default(); @@ -147,86 +131,41 @@ impl DynamicBocDb { None }; Ok(Self { - db, - cells_cf_name: cell_db_cf.to_string(), + cell_db: Arc::new(cell_db), counters_cf_name: counters_cf_name.to_string(), - db_root_path: db_root_path.as_ref().to_path_buf(), - storing_cells: Arc::new(lockfree::map::Map::new()), - storing_cells_count: AtomicU64::new(0), cells_counters, - cell_cache: quick_cache::sync::Cache::new(config.cells_lru_cache_capacity), - #[cfg(feature = "telemetry")] - telemetry, - allocated, }) } + pub fn cell_db(&self) -> &Arc { + &self.cell_db + } + pub fn build_cells_cf_options(config: &CellsDbConfig) -> rocksdb::Options { - Self::build_cf_options(config.cells_cache_size_bytes) + CellDb::build_cf_options(config.cells_cache_size_bytes) } pub fn build_counters_cf_options(config: &CellsDbConfig) -> rocksdb::Options { - Self::build_cf_options(config.counters_cache_size_bytes) + CellDb::build_cf_options(config.counters_cache_size_bytes) + } + + pub(crate) fn load_cell(&self, cell_id: &UInt256, panic: bool) -> Result { + self.cell_db.load_cell(cell_id, panic) + } + + #[allow(dead_code)] + fn allocated(&self) -> &StorageAlloc { + self.cell_db.allocated() } - fn build_cf_options(cache_size: u64) -> rocksdb::Options { - let mut options = rocksdb::Options::default(); - let mut block_opts = rocksdb::BlockBasedOptions::default(); - - // specified cache for blocks. - let cache = rocksdb::Cache::new_lru_cache(cache_size as usize); - block_opts.set_block_cache(&cache); - - // save in LRU block cache also indexes and bloom filters - block_opts.set_cache_index_and_filter_blocks(true); - - // keep indexes and filters in block cache until tablereader freed - block_opts.set_pin_l0_filter_and_index_blocks_in_cache(true); - - // Setup bloom filter with length of 10 bits per key. - // This length provides less than 1% false positive rate. - block_opts.set_bloom_filter(10.0, false); - - options.set_block_based_table_factory(&block_opts); - - // Enable whole key bloom filter in memtable. - options.set_memtable_whole_key_filtering(true); - - // Amount of data to build up in memory (backed by an unsorted log - // on disk) before converting to a sorted on-disk file. - // - // Larger values increase performance, especially during bulk loads. - // Up to max_write_buffer_number write buffers may be held in memory - // at the same time, - // so you may wish to adjust this parameter to control memory usage. - // Also, a larger write buffer will result in a longer recovery time - // the next time the database is opened. - options.set_write_buffer_size(1024 * 1024 * 1024); - - // The maximum number of write buffers that are built up in memory. - // The default and the minimum number is 2, so that when 1 write buffer - // is being flushed to storage, new writes can continue to the other - // write buffer. - // If max_write_buffer_number > 3, writing will be slowed down to - // options.delayed_write_rate if we are writing to the last write buffer - // allowed. - options.set_max_write_buffer_number(4); - - // if prefix_extractor is set and memtable_prefix_bloom_size_ratio is not 0, - // create prefix bloom for memtable with the size of - // write_buffer_size * memtable_prefix_bloom_size_ratio. - // If it is larger than 0.25, it is sanitized to 0.25. - let transform = rocksdb::SliceTransform::create_fixed_prefix(32); - options.set_prefix_extractor(transform); - options.set_memtable_prefix_bloom_ratio(0.1); - - options + pub fn cells_factory(&self) -> Arc { + self.cell_db.clone() as Arc } #[cfg(test)] pub fn count(&self) -> usize { if let Ok(cf) = self.counters_cf() { - self.db.iterator_cf(&cf, rocksdb::IteratorMode::Start).count() + self.cell_db.db().iterator_cf(&cf, rocksdb::IteratorMode::Start).count() } else { 0 } @@ -241,28 +180,18 @@ impl DynamicBocDb { let root_id = root_cell.hash(MAX_LEVEL); log::debug!(target: TARGET, "DynamicBocDb::save_boc {:x}", root_id); - let cells_cf = self.cells_cf()?; + let cells_cf = self.cell_db.cells_cf()?; - #[cfg(feature = "telemetry")] - let now = Instant::now(); - if let Some(val) = self.db.get_pinned_cf(&cells_cf, root_id.as_slice())? { + if let Some(existing) = self.cell_db.try_load_existing_root(&root_id, &cells_cf)? { log::info!(target: TARGET, "DynamicBocDb::save_boc ALREADY EXISTS {:x}", root_id); - let cell = StoredCell::deserialize(self, &root_id, &val)?; - #[cfg(feature = "telemetry")] - { - self.telemetry - .stored_cells - .update(self.allocated.storage_cells.load(Ordering::Relaxed)); - self.telemetry.loaded_cells_from_db.update(1); - self.telemetry.load_cell_from_db_time_nanos.update(now.elapsed().as_nanos() as u64); - } - return Ok(Cell::with_cell_impl(cell)); + return Ok(existing); } let mut guard = self.cells_counters.as_ref().map(|m| m.lock()); let mut cells_counters: Option<&mut CellsCounters> = guard.as_deref_mut(); #[cfg(feature = "telemetry")] - self.telemetry + self.cell_db + .telemetry() .cached_cells_counters .update(cells_counters.as_ref().map(|c| c.len()).unwrap_or_default() as u64); @@ -297,55 +226,40 @@ impl DynamicBocDb { let tr_build_time = now2.elapsed().as_micros(); let now3 = Instant::now(); - self.db.write(transaction)?; + self.cell_db.db().write(transaction)?; #[cfg(feature = "telemetry")] if !visited.is_empty() { - self.telemetry.boc_db_element_write_nanos.update( + self.cell_db.telemetry().boc_db_element_write_nanos.update( now3.elapsed().as_nanos() as u64 / (wrote_cells as u64 + wrote_counters as u64), ); } let tr_commit_time = now3.elapsed().as_micros(); let now4 = Instant::now(); - for (id, _) in visited.iter() { - let mut stack = vec![id.clone()]; - while let Some(id) = stack.pop() { - if let Some(removed) = self.storing_cells.remove(&id) { - log::trace!( - target: TARGET, - "DynamicBocDb::save_boc {:x} cell removed from storing_cells", id - ); - let _storing_cells_count = - self.storing_cells_count.fetch_sub(1, Ordering::Relaxed); - #[cfg(feature = "telemetry")] - self.telemetry.storing_cells.update(_storing_cells_count - 1); - - for i in 0..removed.val().references_count() { - stack.push(removed.val().reference_repr_hash(i)?); - } - } - } - } + self.cell_db.cleanup_storing_cells(visited.keys()); let storing_cells_cleanup_time = now4.elapsed().as_micros(); let saved_root = if let Some(c) = visited.get(&root_id).and_then(|vc| vc.cell()) { c.clone() } else { // only if the root cell was already saved (just updated counter) - we need to load it here - self.load_cell(&root_id, true)? + self.cell_db.load_cell(&root_id, true)? }; let updated = visited.len() - wrote_cells; let total_time = now.elapsed().as_micros() as u64; #[cfg(feature = "telemetry")] { - self.telemetry.stored_new_cells.update(wrote_cells as u64); - self.telemetry.updated_counters.update((wrote_counters - wrote_cells) as u64); - self.telemetry.save_boc_total_micros.update(total_time); - self.telemetry.save_boc_traverse_micros.update(cells_traverse_time as u64); - self.telemetry.save_boc_tr_build_micros.update(tr_build_time as u64); - self.telemetry.save_boc_commit_micros.update(tr_commit_time as u64); - self.telemetry.save_boc_cleanup_micros.update(storing_cells_cleanup_time as u64); + self.cell_db.telemetry().stored_new_cells.update(wrote_cells as u64); + self.cell_db.telemetry().updated_counters.update((wrote_counters - wrote_cells) as u64); + self.cell_db.telemetry().save_boc_total_micros.update(total_time); + self.cell_db.telemetry().save_boc_traverse_micros.update(cells_traverse_time as u64); + self.cell_db.telemetry().save_boc_tr_build_micros.update(tr_build_time as u64); + self.cell_db.telemetry().save_boc_commit_micros.update(tr_commit_time as u64); + self.cell_db + .telemetry() + .save_boc_cleanup_micros + .update(storing_cells_cleanup_time as u64); } log::debug!( @@ -368,7 +282,7 @@ impl DynamicBocDb { fail!("INTERNAL ERROR: fill_counters called with already filled counters cache"); } let counters_cf = self.counters_cf()?; - for kv in self.db.iterator_cf(&counters_cf, rocksdb::IteratorMode::Start) { + for kv in self.cell_db.db().iterator_cf(&counters_cf, rocksdb::IteratorMode::Start) { let (key, value) = kv?; let cell_id = UInt256::from_slice(key.as_ref()); let counter = Cursor::new(value).read_le_u32()?; @@ -412,7 +326,8 @@ impl DynamicBocDb { let mut guard = self.cells_counters.as_ref().map(|m| m.lock()); let cells_counters: Option<&mut CellsCounters> = guard.as_deref_mut(); #[cfg(feature = "telemetry")] - self.telemetry + self.cell_db + .telemetry() .cached_cells_counters .update(cells_counters.as_ref().map(|c| c.len()).unwrap_or_default() as u64); self.delete_cells_recursive( @@ -427,7 +342,7 @@ impl DynamicBocDb { #[cfg(feature = "telemetry")] let now2 = std::time::Instant::now(); - let cells_cf = self.cells_cf()?; + let cells_cf = self.cell_db.cells_cf()?; let counters_cf = self.counters_cf()?; let mut deleted = 0; let mut transaction = rocksdb::WriteBatch::default(); @@ -452,7 +367,7 @@ impl DynamicBocDb { #[cfg(feature = "telemetry")] let now3 = Instant::now(); - self.db.write(transaction)?; + self.cell_db.db().write(transaction)?; #[cfg(feature = "telemetry")] let tr_commit_time = now3.elapsed().as_micros(); @@ -462,15 +377,16 @@ impl DynamicBocDb { let updated = visited.len() - deleted; #[cfg(feature = "telemetry")] if !visited.is_empty() { - self.telemetry + self.cell_db + .telemetry() .boc_db_element_write_nanos .update(now3.elapsed().as_nanos() as u64 / (visited.len() as u64 + deleted as u64)); - self.telemetry.deleted_cells.update(deleted as u64); - self.telemetry.updated_counters.update(updated as u64); - self.telemetry.delete_boc_total_micros.update(total_time); - self.telemetry.delete_boc_traverse_micros.update(traverse_time as u64); - self.telemetry.delete_boc_tr_build_micros.update(tr_build_time as u64); - self.telemetry.delete_boc_commit_micros.update(tr_commit_time as u64); + self.cell_db.telemetry().deleted_cells.update(deleted as u64); + self.cell_db.telemetry().updated_counters.update(updated as u64); + self.cell_db.telemetry().delete_boc_total_micros.update(total_time); + self.cell_db.telemetry().delete_boc_traverse_micros.update(traverse_time as u64); + self.cell_db.telemetry().delete_boc_tr_build_micros.update(tr_build_time as u64); + self.cell_db.telemetry().delete_boc_commit_micros.update(tr_commit_time as u64); } #[cfg(feature = "telemetry")] @@ -488,125 +404,9 @@ impl DynamicBocDb { Ok(()) } - pub(crate) fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { - #[cfg(feature = "telemetry")] - let now = Instant::now(); - if let Some(cell) = self.cell_cache.get(cell_id) { - #[cfg(feature = "telemetry")] - { - self.telemetry.cell_cache_hits.update(1); - self.telemetry - .load_cell_from_cache_time_nanos - .update(now.elapsed().as_nanos() as u64); - } - return Ok(cell); - } - #[cfg(feature = "telemetry")] - self.telemetry.cell_cache_misses.update(1); - let cell = self.load_cell_uncached(cell_id, panic)?; - #[cfg(feature = "telemetry")] - let now_insert = Instant::now(); - self.cell_cache.insert(cell_id.clone(), cell.clone()); - #[cfg(feature = "telemetry")] - { - self.telemetry - .store_cell_to_cache_time_nanos - .update(now_insert.elapsed().as_nanos() as u64); - self.telemetry.cell_cache_len.update(self.cell_cache.len() as u64); - } - Ok(cell) - } - - pub(crate) fn load_cell_uncached( - self: &Arc, - cell_id: &UInt256, - panic: bool, - ) -> Result { - #[cfg(feature = "telemetry")] - let now = Instant::now(); - let storage_cell_data = match self.db.get_pinned_cf(&self.cells_cf()?, cell_id.as_slice()) { - Ok(Some(data)) => data, - _ => { - if let Some(guard) = self.storing_cells.get(cell_id) { - log::trace!( - target: TARGET, - "DynamicBocDb::load_cell from storing_cells by id {cell_id:x}", - ); - return Ok(guard.val().clone()); - } - - if !panic { - fail!("Can't load cell {:x} from db", cell_id); - } - - log::error!("FATAL!"); - log::error!("FATAL! Can't load cell {:x} from db", cell_id); - log::error!("FATAL!"); - - let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); - write(path, "")?; - - std::thread::sleep(Duration::from_millis(100)); - std::process::exit(0xFF); - } - }; - - #[cfg(feature = "telemetry")] - let load_cell_from_db_time_nanos = now.elapsed().as_nanos() as u64; - - let storage_cell = match StoredCell::deserialize(self, cell_id, &storage_cell_data) { - Ok(cell) => Arc::new(cell), - Err(e) => { - if !panic { - fail!("Can't deserialize cell {:x} from db, error: {:?}", cell_id, e); - } - - log::error!("FATAL!"); - log::error!( - "FATAL! Can't deserialize cell {:x} from db, data: {}, error: {:?}", - cell_id, - hex::encode(&storage_cell_data), - e - ); - log::error!("FATAL!"); - - let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); - write(path, "")?; - - std::thread::sleep(Duration::from_millis(100)); - std::process::exit(0xFF); - } - }; - - #[cfg(feature = "telemetry")] - { - self.telemetry - .stored_cells - .update(self.allocated.storage_cells.load(Ordering::Relaxed)); - self.telemetry.load_cell_from_db_time_nanos.update(load_cell_from_db_time_nanos); - self.telemetry.loaded_cells_from_db.update(1); - } - - log::trace!( - target: TARGET, - "DynamicBocDb::load_cell from DB id {cell_id:x}" - ); - - Ok(Cell::with_cell_impl_arc(storage_cell)) - } - - pub(crate) fn allocated(&self) -> &StorageAlloc { - &self.allocated - } - - fn cells_cf(&self) -> Result>> { - self.db - .cf_handle(&self.cells_cf_name) - .ok_or_else(|| error!("Can't get `{}` cf handle", self.cells_cf_name)) - } - fn counters_cf(&self) -> Result>> { - self.db + self.cell_db + .db() .cf_handle(&self.counters_cf_name) .ok_or_else(|| error!("Can't get `{}` cf handle", self.counters_cf_name)) } @@ -666,12 +466,15 @@ impl DynamicBocDb { } #[cfg(feature = "telemetry")] let now = Instant::now(); - if let Some(raw) = self.db.get_pinned_cf(counters_cf, cell_id.as_slice())? { + if let Some(raw) = self.cell_db.db().get_pinned_cf(counters_cf, cell_id.as_slice())? { // Cell is existing #[cfg(feature = "telemetry")] { - self.telemetry.load_counter_time_nanos.update(now.elapsed().as_nanos() as u64); - self.telemetry.loaded_counters.update(1); + self.cell_db + .telemetry() + .load_counter_time_nanos + .update(now.elapsed().as_nanos() as u64); + self.cell_db.telemetry().loaded_counters.update(1); } let mut reader = Cursor::new(raw); return Ok((false, Some(reader.read_le_u32()?))); @@ -821,7 +624,7 @@ impl DynamicBocDb { let cell = if let Some(c) = cell { c } else { - match self.load_cell(&cell_id, true) { + match self.cell_db.load_cell(&cell_id, true) { Ok(cell) => cell, Err(e) => { log::warn!("DynamicBocDb::delete_cells_recursive {:?}", e); @@ -893,13 +696,18 @@ impl DynamicBocDb { if cells_counters.is_none() { #[cfg(feature = "telemetry")] let now = Instant::now(); - if let Some(counter_raw) = self.db.get_pinned_cf(counters_cf, cell_id.as_slice())? { + if let Some(counter_raw) = + self.cell_db.db().get_pinned_cf(counters_cf, cell_id.as_slice())? + { // Cell's counter is in DB - load it and update #[cfg(feature = "telemetry")] { - self.telemetry.load_counter_time_nanos.update(now.elapsed().as_nanos() as u64); - self.telemetry.loaded_counters.update(1); + self.cell_db + .telemetry() + .load_counter_time_nanos + .update(now.elapsed().as_nanos() as u64); + self.cell_db.telemetry().loaded_counters.update(1); } let mut visited_cell = VisitedCell::with_raw_counter(&counter_raw)?; @@ -922,135 +730,6 @@ impl DynamicBocDb { } } -impl CellsFactory for DynamicBocDb { - fn create_cell(self: Arc, builder: BuilderData) -> Result { - let cell = StoringCell::with_cell(&*builder.into_cell()?, &self)?; - let cell = Cell::with_cell_impl(cell); - let repr_hash = cell.repr_hash(); - - let mut result_cell = None; - - let result = self.storing_cells.insert_with(repr_hash, |_, inserted, found| { - if let Some((_, found)) = found { - result_cell = Some(found.clone()); - lockfree::map::Preview::Discard - } else if let Some(inserted) = inserted { - result_cell = Some(inserted.clone()); - lockfree::map::Preview::Keep - } else { - result_cell = Some(cell.clone()); - lockfree::map::Preview::New(cell.clone()) - } - }); - - let result_cell = result_cell - .ok_or_else(|| error!("INTERNAL ERROR: result_cell {:x} is None", cell.repr_hash()))?; - - match result { - lockfree::map::Insertion::Created => { - log::trace!(target: TARGET, "DynamicBocDb::create_cell {:x} - created new", cell.repr_hash()); - #[cfg(feature = "telemetry")] - { - let storing_cells_count = - self.storing_cells_count.fetch_add(1, Ordering::Relaxed); - self.telemetry.storing_cells.update(storing_cells_count + 1); - } - } - lockfree::map::Insertion::Failed(_) => { - log::trace!(target: TARGET, "DynamicBocDb::create_cell {:x} - already exists", cell.repr_hash()); - } - lockfree::map::Insertion::Updated(old) => { - fail!( - "INTERNAL ERROR: storing_cells.insert_with {:x} returned Updated({:?})", - cell.repr_hash(), - old - ) - } - } - - Ok(result_cell) - } -} - -// This wrapper-struct is added because it is impossible -// to implement foreign trait (CellByHashStorage) for foreign type (Arc) -pub struct CellByHashStorageAdapter { - db: Arc, - root_cells_data: ahash::HashMap>, -} - -impl CellByHashStorageAdapter { - pub fn new( - db: Arc, - root_cell: Option<&Cell>, - max_inmemory_cells: usize, - ) -> Result { - let mut root_cells_data = ahash::HashMap::default(); - if let Some(root_cell) = root_cell { - if db.load_cell(&root_cell.repr_hash(), false).is_err() { - let mut stack = vec![root_cell.clone()]; - while let Some(cell) = stack.pop() { - if root_cells_data.len() >= max_inmemory_cells { - fail!( - "Too many cells in boc to store in memory: {}, max_inmemory_cells: {}", - root_cells_data.len(), - max_inmemory_cells - ); - } - let cell_data = StoredCell::serialize(cell.cell_impl().deref())?; - let cell_hash = cell.repr_hash(); - root_cells_data.insert(cell_hash, cell_data); - - for i in 0..cell.references_count() { - if db.load_cell(&cell.reference_repr_hash(i)?, false).is_err() { - stack.push(cell.reference(i)?); - } - } - } - } - } - Ok(Self { db, root_cells_data }) - } -} - -impl CellsStorage for CellByHashStorageAdapter { - fn load_cell(&self, hash: &UInt256) -> Result { - if let Ok(c) = self.db.clone().load_cell_uncached(hash, false) { - Ok(c) - } else if let Some(data) = self.root_cells_data.get(hash) { - StoredCell::deserialize(&self.db, hash, data).map(Cell::with_cell_impl) - } else { - fail!("Can't load cell {:x} from db", hash); - } - } - - fn load_cell_data( - &self, - hash: &UInt256, - write_hashes: bool, - dest: &mut dyn Write, - ) -> Result<()> { - #[cfg(feature = "telemetry")] - let now = std::time::Instant::now(); - if let Ok(Some(data)) = self.db.db.get_pinned_cf(&self.db.cells_cf()?, hash.as_slice()) { - #[cfg(feature = "telemetry")] - { - self.db - .telemetry - .load_cell_from_db_time_nanos - .update(now.elapsed().as_nanos() as u64); - self.db.telemetry.loaded_cells_from_db.update(1); - } - - StoredCell::write_cell_data(&data, hash, write_hashes, dest) - } else if let Some(data) = self.root_cells_data.get(hash) { - StoredCell::write_cell_data(data, hash, write_hashes, dest) - } else { - fail!("Can't load cell {:x} from db", hash); - } - } -} - pub struct AsyncCellsStorageAdapter { boc_db: Arc, index: Vec<(UInt256, u16)>, // hash & depth. @@ -1072,7 +751,7 @@ impl AsyncCellsStorageAdapter { let mut guard = boc_db_clone.cells_counters.as_ref().map(|m| m.lock()); let mut cells_counters: Option<&mut CellsCounters> = guard.as_deref_mut(); - let cells_cf = boc_db_clone.cells_cf()?; + let cells_cf = boc_db_clone.cell_db.cells_cf()?; let counters_cf = boc_db_clone.counters_cf()?; let mut visited = fnv::FnvHashMap::::default(); @@ -1088,7 +767,7 @@ impl AsyncCellsStorageAdapter { // counter transaction.put_cf(&counters_cf, id.as_slice(), vc.serialize_counter()); } - boc_db_clone.db.write(transaction)?; + boc_db_clone.cell_db.db().write(transaction)?; visited.clear(); Ok(()) }; @@ -1143,7 +822,7 @@ impl CellsTempStorage for AsyncCellsStorageAdapter { Ok(guard.val().clone()) } else { let (hash, _) = self.load_hash_and_depth(index)?; - let cell = self.boc_db.clone().load_cell(&hash, false)?; + let cell = self.boc_db.cell_db.load_cell(&hash, false)?; self.cache.insert(index, cell.clone()); Ok(cell) } @@ -1162,7 +841,8 @@ impl CellsTempStorage for AsyncCellsStorageAdapter { fail!("AsyncCellsStorageAdapter::store_simple_cell supports only zero level cells"); } self.index[index as usize] = (data.hash(0), data.depth(0)); - let cell = Cell::with_cell_impl(StoredCell::with_cell_data(data, refs, &self.boc_db)?); + let cell = + Cell::with_cell_impl(StoredCell::with_cell_data(data, refs, &self.boc_db.cell_db)?); self.cache.insert(index, cell.clone()); self.sender.blocking_send((index, cell))?; Ok(()) diff --git a/src/node/storage/src/lib.rs b/src/node/storage/src/lib.rs index 4059f99..dd82204 100644 --- a/src/node/storage/src/lib.rs +++ b/src/node/storage/src/lib.rs @@ -8,11 +8,14 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ +pub mod archive_shardstate_db; pub mod archives; pub mod block_handle_db; pub mod block_info_db; pub mod catchain_persistent_db; +pub mod cell_db; pub mod db; +pub mod dynamic_boc_archive_db; pub mod dynamic_boc_rc_db; pub mod error; mod macros; diff --git a/src/node/storage/src/shard_top_blocks_db.rs b/src/node/storage/src/shard_top_blocks_db.rs index 23d2dbf..01619cd 100644 --- a/src/node/storage/src/shard_top_blocks_db.rs +++ b/src/node/storage/src/shard_top_blocks_db.rs @@ -10,4 +10,6 @@ */ use crate::db_impl_base; +pub const SHARD_TOP_BLOCKS_DB_NAME: &str = "shard_top_blocks_db"; + db_impl_base!(ShardTopBlocksDb, Vec); diff --git a/src/node/storage/src/shardstate_db_async.rs b/src/node/storage/src/shardstate_db_async.rs index a3eac99..78f8599 100644 --- a/src/node/storage/src/shardstate_db_async.rs +++ b/src/node/storage/src/shardstate_db_async.rs @@ -11,11 +11,12 @@ #[cfg(feature = "telemetry")] use crate::StorageTelemetry; use crate::{ + cell_db::CellByHashStorageAdapter, db::{ rocksdb::{RocksDb, RocksDbTable}, DbKey, }, - dynamic_boc_rc_db::{AsyncCellsStorageAdapter, CellByHashStorageAdapter, DynamicBocDb}, + dynamic_boc_rc_db::{AsyncCellsStorageAdapter, DynamicBocDb}, error::StorageError, traits::Serializable, StorageAlloc, TARGET, @@ -489,7 +490,11 @@ impl ShardStateDb { root: Option<&Cell>, max_inmemory_cells: usize, ) -> Result { - CellByHashStorageAdapter::new(self.dynamic_boc_db.clone(), root, max_inmemory_cells) + CellByHashStorageAdapter::new( + self.dynamic_boc_db.cell_db().clone(), + root, + max_inmemory_cells, + ) } pub fn create_fast_cell_storage( @@ -500,7 +505,7 @@ impl ShardStateDb { } pub fn cells_factory(&self) -> Result> { - Ok(self.dynamic_boc_db.clone() as Arc) + Ok(self.dynamic_boc_db.cells_factory()) } pub fn enumerate_ids( diff --git a/src/node/storage/src/tests/mod.rs b/src/node/storage/src/tests/mod.rs index 2abde60..1d864c8 100644 --- a/src/node/storage/src/tests/mod.rs +++ b/src/node/storage/src/tests/mod.rs @@ -9,6 +9,7 @@ * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ mod test_catchain_persistent_db; +mod test_dynamic_boc_archive_db; mod test_dynamic_boc_rc_db; mod test_shardstate_db_async; diff --git a/src/node/storage/src/tests/test_archive_manager.rs b/src/node/storage/src/tests/test_archive_manager.rs index 2d07c52..763f7d1 100644 --- a/src/node/storage/src/tests/test_archive_manager.rs +++ b/src/node/storage/src/tests/test_archive_manager.rs @@ -11,8 +11,10 @@ use crate::StorageTelemetry; use crate::{ archives::{ archive_manager::ArchiveManager, + db_provider::{ArchiveDbProvider, EpochDbProvider, SingleDbProvider}, + epoch::{ArchivalModeConfig, EpochRouter}, package_entry_id::{GetFileName, PackageEntryId}, - ARCHIVE_PACKAGE_SIZE, + ARCHIVE_PACKAGE_SIZE, ARCHIVE_SLICE_SIZE, }, block_handle_db::{BlockHandleStorage, FLAG_KEY_BLOCK}, db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, @@ -44,9 +46,12 @@ async fn create_manager( std::fs::remove_dir_all(&path).ok(); } let db = RocksDb::new(root, name, None, AccessType::ReadWrite)?; + let db_root_path = Arc::new(path); + let db_provider = Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())); let manager = ArchiveManager::with_data( db.clone(), - Arc::new(path), + db_root_path, + db_provider, 0, Arc::new(AtomicU8::new(0)), #[cfg(feature = "telemetry")] @@ -363,6 +368,11 @@ async fn test_block_index() -> Result<()> { data.extend_from_slice(&id.seq_no().to_le_bytes()); data } + fn make_proof(id: &BlockIdExt) -> Vec { + let mut data = id.shard().shard_prefix_with_tag().to_be_bytes().to_vec(); + data.extend_from_slice(&id.seq_no().to_be_bytes()); + data + } const DB_NAME: &str = "test_block_index"; @@ -382,7 +392,7 @@ async fn test_block_index() -> Result<()> { for mc_seqno in 1..total_mc_blocks { let id = generate_block_id(-1, 0x8000_0000_0000_0000, mc_seqno); manager.add_file(&PackageEntryId::Block(&id), &make_data(&id)).await?; - manager.add_file(&PackageEntryId::Proof(&id), &[1, 2, 3]).await?; + manager.add_file(&PackageEntryId::Proof(&id), &make_proof(&id)).await?; let flags = if rand::random::() % 12345 == 0 { FLAG_KEY_BLOCK } else { 0 }; let block_meta = BlockMeta::with_data(flags, gen_utime, lt, mc_seqno, 0); let handle = block_handle_storage.create_handle(id.clone(), block_meta, None)?.unwrap(); @@ -401,7 +411,7 @@ async fn test_block_index() -> Result<()> { *seqno, ); manager.add_file(&PackageEntryId::Block(&id), &make_data(&id)).await?; - manager.add_file(&PackageEntryId::ProofLink(&id), &[1, 2, 3]).await?; + manager.add_file(&PackageEntryId::ProofLink(&id), &make_proof(&id)).await?; let block_meta = BlockMeta::with_data(0, gen_utime, lt + i as u64 * 1_000_000, mc_seqno, 0); let handle = @@ -443,8 +453,11 @@ async fn test_block_index() -> Result<()> { let prefix = AccountIdPrefixFull { workchain_id: -1, prefix: rand::random::() }; let (id, data) = manager.lookup_block_by_seqno(&prefix, seqno).await?.unwrap(); assert_eq!(data, make_data(&id)); + let (id, data) = manager.lookup_proof_by_seqno(&prefix, seqno).await?.unwrap(); + assert_eq!(data, make_proof(&id)); let mut found = 0; + let mut ids = vec![]; let utime = init_utime + (rand::random::() % (gen_utime - init_utime)) - 100; log::info!("lookup by utime {}", utime); let prefix = AccountIdPrefixFull { workchain_id: 0, prefix: rand::random::() }; @@ -455,12 +468,21 @@ async fn test_block_index() -> Result<()> { Box::new(|id, data| { assert_eq!(data, make_data(&id)); found += 1; + ids.push(id); Ok(true) }), ) .await?; assert!(found > 0); + for id in ids { + let (id, data) = manager + .lookup_proof_by_seqno(&id.shard().account_id_prefix(), id.seq_no()) + .await? + .unwrap(); + assert_eq!(data, make_proof(&id)); + } } + assert_eq!(manager.get_max_mc_seqno().await, Some(total_mc_blocks - 1)); drop(block_handle_storage); drop(manager); @@ -484,6 +506,8 @@ async fn test_block_index() -> Result<()> { assert_eq!(data, make_data(&id)); assert_eq!(id.seq_no(), 20_000); + assert_eq!(manager.get_max_mc_seqno().await, Some(total_mc_blocks - 1)); + for _ in 0..20_000 { let lt = rand::random::() % lt; log::info!("lookup by lt {}", lt); @@ -497,8 +521,12 @@ async fn test_block_index() -> Result<()> { if let Some((id, data)) = manager.lookup_block_by_seqno(&prefix, seqno).await? { assert_eq!(data, make_data(&id)); } + if let Some((id, data)) = manager.lookup_proof_by_seqno(&prefix, seqno).await? { + assert_eq!(data, make_proof(&id)); + } let mut found = 0; + let mut ids = vec![]; let utime = init_utime + rand::random::() % (gen_utime - init_utime) - 100; log::info!("lookup by utime {}", utime); let prefix = AccountIdPrefixFull { workchain_id: -1, prefix: rand::random::() }; @@ -509,11 +537,19 @@ async fn test_block_index() -> Result<()> { Box::new(|id, data| { assert_eq!(data, make_data(&id)); found += 1; + ids.push(id); Ok(true) }), ) .await?; assert!(found > 0); + for id in ids { + let (id, data) = manager + .lookup_proof_by_seqno(&id.shard().account_id_prefix(), id.seq_no()) + .await? + .unwrap(); + assert_eq!(data, make_proof(&id)); + } } drop(manager); @@ -521,3 +557,180 @@ async fn test_block_index() -> Result<()> { destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); Ok(()) } + +// --- Archival mode (epoch-based) tests --- + +fn mc_block_id(mc_seq_no: u32) -> BlockIdExt { + BlockIdExt::with_params( + ShardIdent::masterchain(), + mc_seq_no, + UInt256::from_le_bytes(&mc_seq_no.to_le_bytes()), + UInt256::default(), + ) +} + +async fn write_blocks( + manager: &ArchiveManager, + bhs: &BlockHandleStorage, + range: std::ops::Range, + data: &[u8], +) -> Result<()> { + for mc_seq_no in range { + let block_id = mc_block_id(mc_seq_no); + let meta = BlockMeta::with_data(0, 0, 0, 0, 0); + let handle = bhs + .create_handle(block_id.clone(), meta, None)? + .ok_or_else(|| error!("Cannot create handle for block {}", block_id))?; + manager.add_file(&PackageEntryId::Proof(&block_id), data).await?; + handle.set_proof(); + handle.set_block_applied(); + manager.move_to_archive(&handle, || Ok(())).await?; + handle.set_archived(); + bhs.save_handle(&handle, None)?; + } + Ok(()) +} + +async fn read_block( + manager: &ArchiveManager, + bhs: &BlockHandleStorage, + mc_seq_no: u32, +) -> Result> { + let block_id = mc_block_id(mc_seq_no); + let handle = bhs.load_handle_by_id(&block_id)?.unwrap(); + manager.get_file(&handle, &PackageEntryId::Proof(&block_id)).await +} + +async fn create_epoch_manager( + dir: &Path, +) -> Result<(ArchiveManager, Arc, Arc)> { + let db_root = dir.join("main_db"); + let new_epochs_path = dir.join("new_epochs"); + + let config = ArchivalModeConfig { + epoch_size: ARCHIVE_SLICE_SIZE, + new_epochs_path, + existing_epochs: vec![], + }; + + let db = RocksDb::new(&db_root, "db", None, AccessType::ReadWrite)?; + let db_root_path = Arc::new(db_root); + + let router = Arc::new(EpochRouter::new(&config).await?); + let db_provider: Arc = Arc::new(EpochDbProvider::new(router.clone())); + + let manager = ArchiveManager::with_data( + db.clone(), + db_root_path, + db_provider, + 0, + Arc::new(AtomicU8::new(0)), + #[cfg(feature = "telemetry")] + Arc::new(StorageTelemetry::default()), + Arc::new(StorageAlloc::default()), + ) + .await?; + + Ok((manager, db, router)) +} + +#[tokio::test] +async fn test_archival_mode_minimal() -> Result<()> { + let dir = tempfile::tempdir().unwrap(); + let (manager, db, router) = create_epoch_manager(dir.path()).await?; + let (bhs, _) = create_block_handle_storage(db.clone()); + router.resolve_or_create(0).await?; + + write_blocks(&manager, &bhs, 50..51, &[1, 2, 3]).await?; + + let result = read_block(&manager, &bhs, 50).await?; + assert_eq!(result, vec![1, 2, 3]); + Ok(()) +} + +#[tokio::test] +async fn test_archival_mode_write_and_read() -> Result<()> { + let dir = tempfile::tempdir().unwrap(); + let (manager, db, router) = create_epoch_manager(dir.path()).await?; + let (bhs, _) = create_block_handle_storage(db.clone()); + router.resolve_or_create(0).await?; + + let data = vec![1, 2, 3, 4, 5]; + write_blocks(&manager, &bhs, 0..150, &data).await?; + + for mc_seq_no in 0..150 { + assert_eq!(read_block(&manager, &bhs, mc_seq_no).await?, data); + } + + // Verify .pack files are in epoch directory, not main db + let epoch_dir = dir.path().join("new_epochs").join("epoch_0"); + assert!(epoch_dir.exists(), "Epoch directory should exist"); + assert!( + epoch_dir.join("archive").join("packages").exists(), + "Pack files should be in epoch directory" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_archival_mode_multiple_epochs() -> Result<()> { + let dir = tempfile::tempdir().unwrap(); + let (manager, db, router) = create_epoch_manager(dir.path()).await?; + let (bhs, _) = create_block_handle_storage(db.clone()); + + router.resolve_or_create(0).await?; + router.resolve_or_create(20_000).await?; + + let data_epoch0 = vec![10, 20, 30]; + let data_epoch1 = vec![40, 50, 60]; + + write_blocks(&manager, &bhs, 0..100, &data_epoch0).await?; + assert_eq!(read_block(&manager, &bhs, 50).await?, data_epoch0); + + write_blocks(&manager, &bhs, 20_000..20_100, &data_epoch1).await?; + + assert_eq!(read_block(&manager, &bhs, 50).await?, data_epoch0); + assert_eq!(read_block(&manager, &bhs, 20_050).await?, data_epoch1); + + assert!(dir.path().join("new_epochs").join("epoch_0").exists()); + assert!(dir.path().join("new_epochs").join("epoch_1").exists()); + + Ok(()) +} + +#[tokio::test] +async fn test_archival_mode_restart_preserves_data() -> Result<()> { + let dir = tempfile::tempdir().unwrap(); + let data = vec![7, 8, 9]; + + // First "run": write some blocks + let db = { + let (manager, db, router) = create_epoch_manager(dir.path()).await?; + let (bhs, _bh_db) = create_block_handle_storage(db.clone()); + router.resolve_or_create(0).await?; + write_blocks(&manager, &bhs, 0..50, &data).await?; + db + }; + + // BlockHandleStorage has background task which holds RocksDB instance + while Arc::strong_count(&db) > 1 { + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + drop(db); + + // Second "run": recreate manager, verify data is accessible + let (manager, db, _router) = create_epoch_manager(dir.path()).await?; + let (bhs, _) = create_block_handle_storage(db.clone()); + + for mc_seq_no in 0..50 { + assert_eq!( + read_block(&manager, &bhs, mc_seq_no).await?, + data, + "Block {} data mismatch after restart", + mc_seq_no + ); + } + + Ok(()) +} diff --git a/src/node/storage/src/tests/test_archive_slice.rs b/src/node/storage/src/tests/test_archive_slice.rs index 313042d..53cbaf1 100644 --- a/src/node/storage/src/tests/test_archive_slice.rs +++ b/src/node/storage/src/tests/test_archive_slice.rs @@ -23,7 +23,7 @@ use crate::{ StorageAlloc, }; use std::{future::Future, path::Path, pin::Pin, sync::Arc}; -use ton_block::{error, BlockIdExt, Result, ShardIdent, UInt256}; +use ton_block::{error, AccountIdPrefixFull, BlockIdExt, Result, ShardIdent, UInt256}; const DB_PATH: &str = "../../target/test"; @@ -41,6 +41,7 @@ async fn prepare_test( name: &str, package_type: PackageType, shard_split_depth: u8, + archive_id: u32, ) -> Result<(Arc, TestContext)> { let db_root = Path::new(DB_PATH).join(name); let _ = std::fs::remove_dir_all(&db_root); @@ -48,7 +49,7 @@ async fn prepare_test( let archive_slice = ArchiveSlice::new_empty( db.clone(), Arc::new(db_root), - 0, + archive_id, package_type, shard_split_depth, #[cfg(feature = "telemetry")] @@ -72,9 +73,11 @@ async fn run_test( name: &str, package_type: PackageType, shard_split_depth: u8, + archive_id: u32, scenario: impl Fn(TestContext) -> Pinned, ) -> Result<()> { - let (db, test_context) = prepare_test(name, package_type, shard_split_depth).await?; + let (db, test_context) = + prepare_test(name, package_type, shard_split_depth, archive_id).await?; scenario(test_context).await?; destroy_db(db, name).await; Ok(()) @@ -147,7 +150,7 @@ async fn test_scenario_gold() -> Result<()> { Ok(()) } - run_test("test_archive_slice_scenario_gold", PackageType::Blocks, 0, |ctx| { + run_test("test_archive_slice_scenario_gold", PackageType::Blocks, 0, 0, |ctx| { Box::pin(scenario(ctx)) }) .await @@ -184,6 +187,53 @@ async fn test_key_blocks_slice() -> Result<()> { Ok(()) } - run_test("test_key_blocks_slice", PackageType::KeyBlocks, 0, |ctx| Box::pin(scenario(ctx))) + run_test("test_key_blocks_slice", PackageType::KeyBlocks, 0, 0, |ctx| Box::pin(scenario(ctx))) .await } + +#[tokio::test] +async fn test_lookup_proof_by_seqno() -> Result<()> { + async fn scenario(test_context: TestContext) -> Result<()> { + let proof_data = vec![7u8, 8, 9]; + let mc_seqno = 55u32; + + let block_id = BlockIdExt::with_params( + ShardIdent::masterchain(), + mc_seqno, + UInt256::with_array([mc_seqno as u8; 32]), + UInt256::default(), + ); + let meta = BlockMeta::with_data(0, 1000, 100_000, mc_seqno, 0); + let handle = test_context + .block_handle_storage + .create_handle(block_id.clone(), meta, None)? + .ok_or_else(|| error!("Cannot create handle"))?; + + test_context + .archive_slice + .add_file(&handle, &PackageEntryId::Block(&block_id), vec![1, 2, 3]) + .await?; + test_context + .archive_slice + .add_file(&handle, &PackageEntryId::Proof(&block_id), proof_data.clone()) + .await?; + + let prefix = AccountIdPrefixFull { workchain_id: -1, prefix: 0 }; + + let result = test_context.archive_slice.lookup_proof_by_seqno(&prefix, mc_seqno).await?; + let (found_id, found_data) = result.expect("proof should be found"); + assert_eq!(found_id, block_id); + assert_eq!(found_data, proof_data); + + let result = test_context.archive_slice.lookup_proof_by_seqno(&prefix, 999).await?; + assert!(result.is_none(), "lookup of non-existent seqno should return None"); + + drop(test_context); + Ok(()) + } + + run_test("test_lookup_proof_by_seqno", PackageType::Blocks, 0, 50, |ctx| { + Box::pin(scenario(ctx)) + }) + .await +} diff --git a/src/node/storage/src/tests/test_dynamic_boc_archive_db.rs b/src/node/storage/src/tests/test_dynamic_boc_archive_db.rs new file mode 100644 index 0000000..5f01ee9 --- /dev/null +++ b/src/node/storage/src/tests/test_dynamic_boc_archive_db.rs @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +#[cfg(feature = "telemetry")] +use crate::StorageTelemetry; +use crate::{ + archive_shardstate_db::ArchiveShardStateDb, + cell_db::CellByHashStorageAdapter, + db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, + dynamic_boc_archive_db::DynamicBocArchiveDb, + shardstate_db_async::CellsDbConfig, + tests::utils::{count_tree_unique_cells, get_test_tree_of_cells, init_test_log}, + StorageAlloc, +}; +use std::sync::Arc; +use ton_block::{ + read_single_root_boc, BigBocWriter, BlockIdExt, BocFlags, BuilderData, CellsFactory, + IBitstring, Result, ShardIdent, UInt256, MAX_SAFE_DEPTH, SHARD_FULL, +}; + +const DB_PATH: &str = "../../target/test"; + +fn make_block_id(seq_no: u32) -> BlockIdExt { + BlockIdExt::with_params( + ShardIdent::with_tagged_prefix(-1, SHARD_FULL).unwrap(), + seq_no, + UInt256::from([seq_no as u8; 32]), + UInt256::from([(seq_no + 100) as u8; 32]), + ) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_dynamic_boc_archive_db() -> Result<()> { + init_test_log(); + + const DB_NAME: &str = "test_dynamic_boc_archive_db"; + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + + let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; + let boc_db = Arc::new(DynamicBocArchiveDb::with_db( + db.clone(), + "cells", + "", + &CellsDbConfig::default(), + #[cfg(feature = "telemetry")] + Arc::new(StorageTelemetry::default()), + Arc::new(StorageAlloc::default()), + )?); + + let root_cell = get_test_tree_of_cells(); + let initial_count = count_tree_unique_cells(root_cell.clone()); + + // Save and verify + boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; + assert_eq!(boc_db.cell_db().count(), initial_count); + + // Load and verify + let loaded = boc_db.cell_db().load_cell(&root_cell.repr_hash(), false)?; + assert_eq!(count_tree_unique_cells(loaded), initial_count); + + drop(boc_db); + drop(db); + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_archive_save_idempotent() -> Result<()> { + init_test_log(); + + const DB_NAME: &str = "test_archive_save_idempotent"; + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + + let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; + let boc_db = Arc::new(DynamicBocArchiveDb::with_db( + db.clone(), + "cells", + "", + &CellsDbConfig::default(), + #[cfg(feature = "telemetry")] + Arc::new(StorageTelemetry::default()), + Arc::new(StorageAlloc::default()), + )?); + + let root_cell = get_test_tree_of_cells(); + let initial_count = count_tree_unique_cells(root_cell.clone()); + + // Save twice + boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; + boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; + + // Count should not change + assert_eq!(boc_db.cell_db().count(), initial_count); + + drop(boc_db); + drop(db); + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_archive_shared_cells() -> Result<()> { + init_test_log(); + + const DB_NAME: &str = "test_archive_shared_cells"; + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + + let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; + let boc_db = Arc::new(DynamicBocArchiveDb::with_db( + db.clone(), + "cells", + "", + &CellsDbConfig::default(), + #[cfg(feature = "telemetry")] + Arc::new(StorageTelemetry::default()), + Arc::new(StorageAlloc::default()), + )?); + + // Create shared cells via CellsFactory + let cells_factory = boc_db.cell_db().clone() as Arc; + let create_chain = |data_values: Vec<&str>| -> ton_block::Cell { + let mut child = None; + let mut cell = ton_block::Cell::default(); + for data in data_values.iter().rev() { + let mut builder = BuilderData::new(); + let mut data = data.as_bytes().to_vec(); + data.push(0x80); + builder.append_bitstring(&data).unwrap(); + if let Some(child) = child { + builder.checked_append_reference(child).unwrap(); + } + cell = cells_factory.clone().create_cell(builder).unwrap(); + child = Some(cell.clone()); + } + cell + }; + + let r1 = create_chain(vec!["r1", "shared", "leaf"]); + boc_db.save_boc(r1.clone(), &|| Ok(()))?; + let count_after_r1 = boc_db.cell_db().count(); + + let r2 = create_chain(vec!["r2", "shared", "leaf"]); + boc_db.save_boc(r2.clone(), &|| Ok(()))?; + let count_after_r2 = boc_db.cell_db().count(); + + // r2 shares "shared" and "leaf" with r1, so only 1 new cell ("r2") should be added + assert_eq!(count_after_r2, count_after_r1 + 1); + + // Both roots should be loadable + let _ = boc_db.cell_db().load_cell(&r1.repr_hash(), false)?; + let _ = boc_db.cell_db().load_cell(&r2.repr_hash(), false)?; + + drop(cells_factory); + drop(boc_db); + drop(db); + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_archive_shardstate_db() -> Result<()> { + init_test_log(); + + const DB_NAME: &str = "test_archive_shardstate_db"; + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + + let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; + let ss_db = ArchiveShardStateDb::new( + db.clone(), + "shardstate_idx", + "cells", + "", + &CellsDbConfig::default(), + #[cfg(feature = "telemetry")] + Arc::new(StorageTelemetry::default()), + Arc::new(StorageAlloc::default()), + )?; + + let root_cell = get_test_tree_of_cells(); + let block_id = make_block_id(1); + + // Put + assert!(!ss_db.contains(&block_id)?); + ss_db.put(&block_id, root_cell.clone())?; + assert!(ss_db.contains(&block_id)?); + + // Get + let loaded = ss_db.get(&block_id)?; + assert_eq!(count_tree_unique_cells(loaded), count_tree_unique_cells(root_cell)); + + // Put idempotent + ss_db.put(&block_id, ton_block::Cell::default())?; // should return existing, not overwrite + let loaded2 = ss_db.get(&block_id)?; + assert_eq!(loaded2.repr_hash(), ss_db.get(&block_id)?.repr_hash()); + + drop(ss_db); + drop(db); + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_archive_cell_by_hash_storage() -> Result<()> { + init_test_log(); + + const DB_NAME: &str = "test_archive_cell_by_hash_storage"; + destroy_rocks_db(DB_PATH, DB_NAME).await?; + + let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; + let boc_db = Arc::new(DynamicBocArchiveDb::with_db( + db.clone(), + "cells", + "", + &CellsDbConfig::default(), + #[cfg(feature = "telemetry")] + Arc::new(StorageTelemetry::default()), + Arc::new(StorageAlloc::default()), + )?); + + let data = std::fs::read( + "../../block/src/tests/data/6A3BD5B96ABEA186BFEE202B70D510C29F85E126A522B08C1DCAD39F92CF5C51.boc", + )?; + let root_cell = read_single_root_boc(&data)?; + + // Repack without hashes (same as test_cell_by_hash_storage in test_dynamic_boc_rc_db.rs) + fn repack(cell: ton_block::Cell) -> Result { + let mut builder = BuilderData::with_raw(cell.data(), cell.bit_length())?; + builder.set_type(cell.cell_type()); + for r in cell.clone_references() { + builder.checked_append_reference(repack(r)?)?; + } + builder.finalize(MAX_SAFE_DEPTH) + } + let root_cell = repack(root_cell)?; + + boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; + + let writer = BigBocWriter::with_params( + [root_cell.clone()], + MAX_SAFE_DEPTH, + BocFlags::all(), + &|| false, + Arc::new(CellByHashStorageAdapter::new(boc_db.cell_db().clone(), None, 0)?), + )?; + + let mut boc = Vec::new(); + writer.write(&mut boc)?; + + assert_eq!(boc.len(), data.len()); + assert_eq!(boc, data); + + drop(boc_db); + drop(db); + destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); + Ok(()) +} diff --git a/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs b/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs index d543db0..bdda250 100644 --- a/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs +++ b/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs @@ -11,8 +11,9 @@ #[cfg(feature = "telemetry")] use crate::StorageTelemetry; use crate::{ + cell_db::CellByHashStorageAdapter, db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, - dynamic_boc_rc_db::{CellByHashStorageAdapter, DynamicBocDb}, + dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::CellsDbConfig, tests::utils::{ count_tree_unique_cells, get_another_test_tree_of_cells, get_test_tree_of_cells, @@ -22,8 +23,8 @@ use crate::{ }; use std::sync::Arc; use ton_block::{ - read_single_root_boc, BigBocWriter, BocFlags, BuilderData, Cell, CellsFactory, IBitstring, - Result, MAX_SAFE_DEPTH, + read_single_root_boc, BigBocWriter, BocFlags, BuilderData, Cell, IBitstring, Result, + MAX_SAFE_DEPTH, }; const DB_PATH: &str = "../../target/test"; @@ -94,7 +95,7 @@ async fn test_dynamic_boc_rc_db_2() -> Result<()> { Arc::new(StorageAlloc::default()), )?); - let cells_factory = boc_db.clone() as Arc; + let cells_factory = boc_db.cells_factory(); let create_ss = |cells_chain: Vec<&str>| -> Cell { let mut child = None; let mut cell = Cell::default(); @@ -180,7 +181,7 @@ async fn test_cell_by_hash_storage() -> Result<()> { MAX_SAFE_DEPTH, BocFlags::all(), &|| false, - Arc::new(CellByHashStorageAdapter::new(boc_db.clone(), None, 0)?), + Arc::new(CellByHashStorageAdapter::new(boc_db.cell_db().clone(), None, 0)?), )?; let mut boc = Vec::new(); diff --git a/src/node/storage/src/tests/test_epoch.rs b/src/node/storage/src/tests/test_epoch.rs new file mode 100644 index 0000000..e65e734 --- /dev/null +++ b/src/node/storage/src/tests/test_epoch.rs @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +use super::*; + +#[tokio::test] +async fn test_epoch_router_validation() { + let dir = tempfile::tempdir().unwrap(); + + let config = ArchivalModeConfig { + epoch_size: 0, + new_epochs_path: dir.path().to_path_buf(), + existing_epochs: vec![], + }; + assert!(EpochRouter::new(&config).await.is_err()); + + let config = ArchivalModeConfig { + epoch_size: 10_000, // not a multiple of ARCHIVE_SLICE_SIZE + new_epochs_path: dir.path().to_path_buf(), + existing_epochs: vec![], + }; + assert!(EpochRouter::new(&config).await.is_err()); +} + +#[tokio::test] +async fn test_epoch_router_resolve_and_create() { + let dir = tempfile::tempdir().unwrap(); + let new_epochs_path = dir.path().join("new_epochs"); + + let config = ArchivalModeConfig { + epoch_size: 40_000, + new_epochs_path: new_epochs_path.clone(), + existing_epochs: vec![], + }; + + let router = EpochRouter::new(&config).await.unwrap(); + + // No epochs exist yet + assert!(router.resolve(0).is_none()); + assert!(router.resolve(39_999).is_none()); + + // Create epoch for mc_seq_no 0 + let epoch = router.resolve_or_create(0).await.unwrap(); + assert_eq!(epoch.mc_seq_no_start(), 0); + assert_eq!(epoch.mc_seq_no_end(), 39_999); + assert!(epoch.path().starts_with(&new_epochs_path)); + + // Resolve same epoch + let epoch2 = router.resolve(20_000).unwrap(); + assert_eq!(epoch2.mc_seq_no_start(), 0); + + // Create second epoch + let epoch3 = router.resolve_or_create(50_000).await.unwrap(); + assert_eq!(epoch3.mc_seq_no_start(), 40_000); + assert_eq!(epoch3.mc_seq_no_end(), 79_999); + + // Verify both exist + assert!(router.resolve(0).is_some()); + assert!(router.resolve(50_000).is_some()); + assert!(router.resolve(80_000).is_none()); +} + +#[tokio::test] +async fn test_epoch_router_with_existing_epochs() { + let dir = tempfile::tempdir().unwrap(); + let epoch0_path = dir.path().join("epoch_0"); + let epoch1_path = dir.path().join("epoch_1"); + let new_epochs_path = dir.path().join("new_epochs"); + + std::fs::create_dir_all(&epoch0_path).unwrap(); + std::fs::create_dir_all(&epoch1_path).unwrap(); + + // Write metadata for existing epochs + let meta0 = EpochMeta { mc_seq_no_start: 0, mc_seq_no_end: 39_999 }; + let meta1 = EpochMeta { mc_seq_no_start: 40_000, mc_seq_no_end: 79_999 }; + write_epoch_meta(&epoch0_path, &meta0).await.unwrap(); + write_epoch_meta(&epoch1_path, &meta1).await.unwrap(); + + let config = ArchivalModeConfig { + epoch_size: 40_000, + new_epochs_path, + existing_epochs: vec![EpochEntry { path: epoch0_path }, EpochEntry { path: epoch1_path }], + }; + + let router = EpochRouter::new(&config).await.unwrap(); + + let e0 = router.resolve(0).unwrap(); + assert_eq!(e0.mc_seq_no_start(), 0); + assert_eq!(e0.mc_seq_no_end(), 39_999); + + let e1 = router.resolve(40_000).unwrap(); + assert_eq!(e1.mc_seq_no_start(), 40_000); + assert_eq!(e1.mc_seq_no_end(), 79_999); + + assert!(router.resolve(80_000).is_none()); +} + +#[tokio::test] +async fn test_epoch_router_rejects_misaligned_existing() { + let dir = tempfile::tempdir().unwrap(); + let epoch_path = dir.path().join("bad_epoch"); + std::fs::create_dir_all(&epoch_path).unwrap(); + + // Epoch with wrong size (60_000 != 40_000) + let meta = EpochMeta { mc_seq_no_start: 0, mc_seq_no_end: 59_999 }; + write_epoch_meta(&epoch_path, &meta).await.unwrap(); + + let config = ArchivalModeConfig { + epoch_size: 40_000, + new_epochs_path: dir.path().join("new_epochs"), + existing_epochs: vec![EpochEntry { path: epoch_path }], + }; + + assert!(EpochRouter::new(&config).await.is_err()); +} + +#[tokio::test] +async fn test_epoch_router_discovers_on_restart() { + let dir = tempfile::tempdir().unwrap(); + let new_epochs_path = dir.path().join("new_epochs"); + + // First "run": create epochs dynamically + let config = ArchivalModeConfig { + epoch_size: 40_000, + new_epochs_path: new_epochs_path.clone(), + existing_epochs: vec![], + }; + let router = EpochRouter::new(&config).await.unwrap(); + router.resolve_or_create(0).await.unwrap(); + router.resolve_or_create(50_000).await.unwrap(); + assert!(router.resolve(0).is_some()); + assert!(router.resolve(50_000).is_some()); + drop(router); + + // Second "run": new router should discover epochs from new_epochs_path + let config2 = ArchivalModeConfig { + epoch_size: 40_000, + new_epochs_path: new_epochs_path.clone(), + existing_epochs: vec![], + }; + let router2 = EpochRouter::new(&config2).await.unwrap(); + let e0 = router2.resolve(0).unwrap(); + assert_eq!(e0.mc_seq_no_start(), 0); + assert_eq!(e0.mc_seq_no_end(), 39_999); + + let e1 = router2.resolve(50_000).unwrap(); + assert_eq!(e1.mc_seq_no_start(), 40_000); + assert_eq!(e1.mc_seq_no_end(), 79_999); + + assert!(router2.resolve(80_000).is_none()); +} diff --git a/src/node/storage/src/types/block_meta.rs b/src/node/storage/src/types/block_meta.rs index 945eb61..e95aece 100644 --- a/src/node/storage/src/types/block_meta.rs +++ b/src/node/storage/src/types/block_meta.rs @@ -25,6 +25,35 @@ pub struct BlockMeta { } impl BlockMeta { + /// Create BlockMeta for archive import with all necessary flags pre-set. + pub fn for_import( + gen_utime: u32, + end_lt: u64, + masterchain_ref_seq_no: u32, + is_key_block: bool, + is_masterchain: bool, + has_prev2: bool, + ) -> Self { + let mut flags = block_handle_db::FLAG_DATA + | block_handle_db::FLAG_APPLIED + | block_handle_db::FLAG_STATE + | block_handle_db::FLAG_STATE_SAVED + | block_handle_db::FLAG_MOVED_TO_ARCHIVE + | block_handle_db::FLAG_PREV_1; + if has_prev2 { + flags |= block_handle_db::FLAG_PREV_2; + } + if is_masterchain { + flags |= block_handle_db::FLAG_PROOF; + } else { + flags |= block_handle_db::FLAG_PROOF_LINK; + } + if is_key_block { + flags |= block_handle_db::FLAG_KEY_BLOCK; + } + Self::with_data(flags, gen_utime, end_lt, masterchain_ref_seq_no, 0) + } + pub fn from_block(block: &Block) -> Result { let info = block.read_info()?; let flags = if info.key_block() { block_handle_db::FLAG_KEY_BLOCK } else { 0 }; diff --git a/src/node/storage/src/types/storage_cell.rs b/src/node/storage/src/types/storage_cell.rs index 9264a8b..7dbd352 100644 --- a/src/node/storage/src/types/storage_cell.rs +++ b/src/node/storage/src/types/storage_cell.rs @@ -8,7 +8,8 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -use crate::{dynamic_boc_rc_db::DynamicBocDb, TARGET}; +use crate::{cell_db::CellDb, TARGET}; +use smallvec::SmallVec; use std::{ io::Write, sync::{ @@ -17,9 +18,9 @@ use std::{ }, }; use ton_block::{ - calc_d1, cell_type, error, fail, full_len, hashes_count, level, level_mask, refs_count, - store_hashes, Cell, CellData, CellImpl, CellType, LevelMask, Result, UInt256, DEPTH_SIZE, - MAX_LEVEL, SHA256_SIZE, + append_tag, calc_d1, cell_type, error, fail, full_len, hashes_count, level, level_mask, + refs_count, store_hashes, Cell, CellData, CellImpl, CellType, LevelMask, Result, UInt256, + DEPTH_SIZE, MAX_LEVEL, SHA256_SIZE, }; #[cfg(test)] @@ -28,6 +29,9 @@ mod tests; const NOT_INITIALIZED_DEPTH: u16 = u16::MAX; +// Max raw data: d1(1) + d2(1) + hashes(32*3) + depths(2*4) + data(128) + ref_hashes(32*4) + ref_depths(2*4) +pub const STORED_CELL_MAX_RAW_LEN: usize = 1 + 1 + 32 * 3 + 2 * 4 + 128 + 32 * 4 + 2 * 4; + struct Reference { hash: UInt256, depth: u16, @@ -37,7 +41,7 @@ struct Reference { pub struct StoredCell { cell_data: CellData, references: parking_lot::RwLock>, - boc_db: Weak, + boc_db: Weak, } static STORED_CELL_COUNT: AtomicU64 = AtomicU64::new(0); @@ -62,11 +66,7 @@ impl<'a> SliceReader<'a> { /// Represents Cell for storing in persistent storage impl StoredCell { - pub fn deserialize( - boc_db: &Arc, - repr_hash: &UInt256, - data: &[u8], - ) -> Result { + pub fn deserialize(boc_db: &Arc, repr_hash: &UInt256, data: &[u8]) -> Result { if data.len() < 2 { fail!("Buffer is too small to read description bytes"); } @@ -221,9 +221,35 @@ impl StoredCell { } pub fn serialize(cell: &dyn CellImpl) -> Result> { - let store_hashes = cell.store_hashes(); + Self::serialize_internal(cell, cell.raw_data()?, cell.store_hashes()) + } + + pub fn serialize_virtual(cell: &dyn CellImpl) -> Result> { + if cell.is_pruned() && cell.level() == 0 { + fail!("Virtual pruned cell can't be serialized"); + } + + let mut data = SmallVec::from_slice(cell.data()); + if cell.bit_length() % 8 == 0 { + append_tag(&mut data, cell.bit_length()); + }; + let data = CellData::with_params( + cell.cell_type(), + data.as_slice(), + cell.level_mask().mask(), + cell.references_count() as u8, + )?; + + Self::serialize_internal(cell, data.raw_data(), false) + } + + fn serialize_internal( + cell: &dyn CellImpl, + raw_data: &[u8], + store_hashes: bool, + ) -> Result> { let data_size = Self::calc_serialized_size( - cell.raw_data()?.len(), + raw_data.len(), store_hashes, cell.level(), cell.references_count(), @@ -231,7 +257,7 @@ impl StoredCell { ); let mut data = Vec::with_capacity(data_size); - data.extend_from_slice(cell.raw_data()?); + data.extend_from_slice(raw_data); if !store_hashes { if cell.cell_type() != CellType::PrunedBranch { @@ -258,7 +284,7 @@ impl StoredCell { pub fn with_cell_data( cell_data: CellData, refs: &[(UInt256, u16)], - boc_db: &Arc, + boc_db: &Arc, ) -> Result { if cell_data.references_count() != refs.len() { fail!("References count mismatch: {} != {}", cell_data.references_count(), refs.len()); @@ -298,7 +324,7 @@ impl PartialEq for StoredCell { pub struct StoringCell { cell_data: CellData, references: parking_lot::RwLock>, - boc_db: Weak, + boc_db: Weak, } impl PartialEq for StoringCell { @@ -308,7 +334,7 @@ impl PartialEq for StoringCell { } impl StoringCell { - pub fn with_cell(cell: &dyn CellImpl, boc_db: &Arc) -> Result { + pub fn with_cell(cell: &dyn CellImpl, boc_db: &Arc) -> Result { let references_count = cell.references_count(); let mut references = Vec::with_capacity(references_count); for i in 0..references_count { @@ -436,7 +462,7 @@ define_CellImpl!(StoringCell); fn reference( index: usize, references: &parking_lot::RwLock>, - boc_db: &Weak, + boc_db: &Weak, repr_hash: &dyn Fn() -> UInt256, ) -> Result> { let hash = { diff --git a/src/node/storage/src/types/tests/test_storage_cell.rs b/src/node/storage/src/types/tests/test_storage_cell.rs index 8767dfa..bb2c531 100644 --- a/src/node/storage/src/types/tests/test_storage_cell.rs +++ b/src/node/storage/src/types/tests/test_storage_cell.rs @@ -21,13 +21,12 @@ use ton_block::{create_cell, BuilderData, IBitstring}; const DB_PATH: &str = "../../target/test"; -async fn init_boc_db(db_name: &str) -> Result> { +async fn init_cell_db(db_name: &str) -> Result> { destroy_rocks_db(DB_PATH, db_name).await?; let db = RocksDb::new(DB_PATH, db_name, None, AccessType::ReadWrite)?; - Ok(Arc::new(DynamicBocDb::with_db( + Ok(Arc::new(CellDb::with_db( db.clone(), "cells", - "counters", DB_PATH, &CellsDbConfig::default(), #[cfg(feature = "telemetry")] @@ -38,7 +37,7 @@ async fn init_boc_db(db_name: &str) -> Result> { #[tokio::test] async fn test_storage_cell_serde() -> Result<()> { - let boc_db = init_boc_db("test_storage_cell_serde").await?; + let cell_db = init_cell_db("test_storage_cell_serde").await?; let c1 = create_cell(vec![], &[1, 2, 45, 76, 200])?; let c2 = create_cell(vec![], &[10, 200, 45, 7, 20])?; @@ -52,20 +51,20 @@ async fn test_storage_cell_serde() -> Result<()> { b.append_u16(47)?; let c4 = b.into_cell()?; - let s1 = StoringCell::with_cell(c1.cell_impl().deref(), &boc_db)?; - let s2 = StoringCell::with_cell(c2.cell_impl().deref(), &boc_db)?; - let s3 = StoringCell::with_cell(c3.cell_impl().deref(), &boc_db)?; - let s4 = StoringCell::with_cell(c4.cell_impl().deref(), &boc_db)?; + let s1 = StoringCell::with_cell(c1.cell_impl().deref(), &cell_db)?; + let s2 = StoringCell::with_cell(c2.cell_impl().deref(), &cell_db)?; + let s3 = StoringCell::with_cell(c3.cell_impl().deref(), &cell_db)?; + let s4 = StoringCell::with_cell(c4.cell_impl().deref(), &cell_db)?; let d1 = StoredCell::serialize(&s1)?; let d2 = StoredCell::serialize(&s2)?; let d3 = StoredCell::serialize(&s3)?; let d4 = StoredCell::serialize(&s4)?; - assert!(s1.cell_data == StoredCell::deserialize(&boc_db, &c1.repr_hash(), &d1)?.cell_data); - assert!(s2.cell_data == StoredCell::deserialize(&boc_db, &c2.repr_hash(), &d2)?.cell_data); - assert!(s3.cell_data == StoredCell::deserialize(&boc_db, &c3.repr_hash(), &d3)?.cell_data); - assert!(s4.cell_data == StoredCell::deserialize(&boc_db, &c4.repr_hash(), &d4)?.cell_data); + assert!(s1.cell_data == StoredCell::deserialize(&cell_db, &c1.repr_hash(), &d1)?.cell_data); + assert!(s2.cell_data == StoredCell::deserialize(&cell_db, &c2.repr_hash(), &d2)?.cell_data); + assert!(s3.cell_data == StoredCell::deserialize(&cell_db, &c3.repr_hash(), &d3)?.cell_data); + assert!(s4.cell_data == StoredCell::deserialize(&cell_db, &c4.repr_hash(), &d4)?.cell_data); Ok(()) } From 24f56aa2de51c35c9d94fbe820cf06b7ee093968 Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Thu, 2 Apr 2026 16:30:56 +0700 Subject: [PATCH 22/48] fix(helm): fix invalid dnsPolicy and make it configurable --- helm/ton-rust-node/CHANGELOG.md | 12 ++++++++++++ helm/ton-rust-node/Chart.yaml | 2 +- helm/ton-rust-node/README.md | 12 +++++++----- helm/ton-rust-node/docs/networking.md | 7 +++++++ helm/ton-rust-node/templates/statefulset.yaml | 2 +- helm/ton-rust-node/values.yaml | 5 +++++ 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/helm/ton-rust-node/CHANGELOG.md b/helm/ton-rust-node/CHANGELOG.md index 87f30ab..94b8cc3 100644 --- a/helm/ton-rust-node/CHANGELOG.md +++ b/helm/ton-rust-node/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to the Helm chart will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). Versions follow the Helm chart release tags (e.g. `helm/v0.3.0`). +## [0.4.4] - 2026-04-02 + +appVersion: `v0.3.0` + +### Added + +- `dnsPolicy` โ€” override pod DNS policy when `hostNetwork` is enabled (default: `ClusterFirstWithHostNet`) + +### Fixed + +- `hostNetwork: true` set invalid `dnsPolicy: ClusterFirstWithHostDNS` (typo โ€” correct value is `ClusterFirstWithHostNet`), causing StatefulSet creation to fail + ## [0.4.3] - 2026-04-01 appVersion: `v0.3.0` diff --git a/helm/ton-rust-node/Chart.yaml b/helm/ton-rust-node/Chart.yaml index e5d4546..e129a4d 100644 --- a/helm/ton-rust-node/Chart.yaml +++ b/helm/ton-rust-node/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: node description: TON Rust Node deployment type: application -version: 0.4.3 +version: 0.4.4 appVersion: "v0.3.0" sources: diff --git a/helm/ton-rust-node/README.md b/helm/ton-rust-node/README.md index 3510724..2da0a12 100644 --- a/helm/ton-rust-node/README.md +++ b/helm/ton-rust-node/README.md @@ -318,6 +318,7 @@ When an `existing*Name` is set, the chart does not create that resource โ€” it o | Name | Description | Value | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | | `hostNetwork` | Bind pods directly to the host network. The pod gets the node's IP with zero NAT overhead. Requires one pod per node โ€” use nodeSelector or podAntiAffinity to spread replicas. See docs/networking.md. | `false` | +| `dnsPolicy` | Pod DNS policy (only applies when hostNetwork is true). Defaults to ClusterFirstWithHostNet. Supported values: ClusterFirstWithHostNet, ClusterFirst, Default, None. | `""` | | `hostPort.adnl` | Expose the ADNL port on the host IP via hostPort | `false` | | `hostPort.simplex` | Expose the simplex port on the host IP via hostPort | `false` | | `hostPort.control` | Expose the control port on the host IP via hostPort | `false` | @@ -344,11 +345,12 @@ When an `existing*Name` is set, the chart does not create that resource โ€” it o ### ServiceAccount parameters -| Name | Description | Value | -| ---------------------------- | ----------------------------------------------------------------------------- | ------- | -| `serviceAccount.enabled` | Create a ServiceAccount for the pods | `false` | -| `serviceAccount.name` | ServiceAccount name. Defaults to the release fullname if not set. | `""` | -| `serviceAccount.annotations` | Annotations for the ServiceAccount (e.g. for Vault or cloud IAM role binding) | `{}` | +| Name | Description | Value | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `serviceAccount.enabled` | Create a ServiceAccount for the pods | `false` | +| `serviceAccount.name` | ServiceAccount name. Defaults to the release fullname if not set. | `""` | +| `serviceAccount.annotations` | Annotations for the ServiceAccount (e.g. for Vault or cloud IAM role binding) | `{}` | +| `terminationGracePeriodSeconds` | Time (in seconds) given to the node process to shut down gracefully before SIGKILL. The default Kubernetes value (30s) is too short for a TON node โ€” an unclean kill may corrupt the database and forces a cold boot. Set this to at least 300s. | `300` | ### Scheduling parameters diff --git a/helm/ton-rust-node/docs/networking.md b/helm/ton-rust-node/docs/networking.md index 20bd738..6f272f8 100644 --- a/helm/ton-rust-node/docs/networking.md +++ b/helm/ton-rust-node/docs/networking.md @@ -190,6 +190,13 @@ The pod uses the host's network stack directly. All container ports bind on the hostNetwork: true ``` +The chart automatically sets `dnsPolicy: ClusterFirstWithHostNet` when `hostNetwork` is enabled. This ensures pods can still resolve cluster DNS names. Override with `dnsPolicy` if needed: + +```yaml +hostNetwork: true +dnsPolicy: Default +``` + **When to use:** bare-metal deployments where you need zero NAT overhead and accept the security trade-off. **Trade-offs:** diff --git a/helm/ton-rust-node/templates/statefulset.yaml b/helm/ton-rust-node/templates/statefulset.yaml index ffca504..b37e8e4 100644 --- a/helm/ton-rust-node/templates/statefulset.yaml +++ b/helm/ton-rust-node/templates/statefulset.yaml @@ -49,7 +49,7 @@ spec: terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} {{- if .Values.hostNetwork }} hostNetwork: true - dnsPolicy: ClusterFirstWithHostDNS + dnsPolicy: {{ .Values.dnsPolicy | default "ClusterFirstWithHostNet" }} {{- end }} initContainers: - name: init-bootstrap diff --git a/helm/ton-rust-node/values.yaml b/helm/ton-rust-node/values.yaml index 8d4d719..e9ac5f5 100644 --- a/helm/ton-rust-node/values.yaml +++ b/helm/ton-rust-node/values.yaml @@ -319,6 +319,11 @@ vault: ## hostNetwork: false +## @param dnsPolicy Pod DNS policy (only applies when hostNetwork is true). Defaults to ClusterFirstWithHostNet. Supported values: ClusterFirstWithHostNet, ClusterFirst, Default, None. +## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy +## +dnsPolicy: "" + ## @param hostPort.adnl Expose the ADNL port on the host IP via hostPort ## @param hostPort.simplex Expose the simplex port on the host IP via hostPort ## @param hostPort.control Expose the control port on the host IP via hostPort From 83a2d74ece48c1ad7b2e66449d91bc7b697ebea2 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Thu, 2 Apr 2026 12:34:25 +0300 Subject: [PATCH 23/48] Fix storage phase for special accounts - pay for due --- src/emulator/src/lib.rs | 9 ++++---- src/executor/src/transaction_executor.rs | 26 +++++++++++------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/emulator/src/lib.rs b/src/emulator/src/lib.rs index b8d8fdc..26e1901 100644 --- a/src/emulator/src/lib.rs +++ b/src/emulator/src/lib.rs @@ -233,12 +233,11 @@ pub extern "C" fn transaction_emulator_set_prev_blocks_info( match SliceData::load_cell(info_cell).and_then(|mut slice| read_stack_item(&mut slice)) { Ok(info) => { - if info.is_tuple() { - transaction_emulator.prev_blocks_info = PrevBlocksInfo::Tuple(info); + transaction_emulator.prev_blocks_info = if info.is_tuple() { + PrevBlocksInfo::Tuple(info) } else { - transaction_emulator.prev_blocks_info = - PrevBlocksInfo::Tuple(StackItem::tuple(Vec::new())); - } + PrevBlocksInfo::Tuple(StackItem::tuple(Vec::new())) + }; } Err(err) => { log::error!("Failed to parse info_cell: {err}"); diff --git a/src/executor/src/transaction_executor.rs b/src/executor/src/transaction_executor.rs index 88fda3a..595d2e4 100644 --- a/src/executor/src/transaction_executor.rs +++ b/src/executor/src/transaction_executor.rs @@ -147,6 +147,7 @@ pub trait TransactionExecutor { /// If account does not exist - phase skipped. /// Calculates storage fees and substracts them from account balance. /// If account balance becomes negative after that, then account is frozen. + /// is_special - flag indicating that account is in list of special smart contracts, for which storage fees are not applied fn storage_phase( &self, acc: &mut Account, @@ -159,23 +160,11 @@ pub trait TransactionExecutor { if tr.now() < acc.last_paid() { fail!("transaction timestamp must be greater then account timestamp") } - - if is_special { - log::debug!(target: "executor", "Special account: AccStatusChange::Unchanged"); - return Ok(TrStoragePhase::with_params( - Coins::zero(), - acc.due_payment().cloned(), - AccStatusChange::Unchanged, - )); - } let mut fee = match acc.storage_info() { - Some(storage_info) => { + Some(storage_info) if !is_special => { self.config().calc_storage_fees(storage_info, is_masterchain, tr.now())? } - None => { - log::debug!(target: "executor", "Account::None"); - return Ok(Default::default()); - } + _ => Default::default(), }; if let Some(due_payment) = acc.due_payment() { fee.add(due_payment)?; @@ -192,6 +181,15 @@ pub trait TransactionExecutor { let storage_fees_collected = std::mem::take(&mut acc_balance.coins); tr.add_fee_coins(&storage_fees_collected)?; fee.sub(&storage_fees_collected)?; + if is_special { + log::debug!(target: "executor", "special account, due payment {fee} still active"); + acc.set_due_payment(Some(fee)); + return Ok(TrStoragePhase::with_params( + storage_fees_collected, + Some(fee), + AccStatusChange::Unchanged, + )); + } let need_freeze = acc.is_active() && fee > self.config().get_gas_config(is_masterchain).freeze_due_limit; let need_delete = (acc.is_uninit() || acc.is_frozen()) From 6cb5d3f35d78d4822991d4263a1a20e7b235c268 Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 2 Apr 2026 16:04:40 +0300 Subject: [PATCH 24/48] More changes on Simplex/QUIC --- src/adnl/src/adnl/common.rs | 31 + src/adnl/src/overlay/broadcast.rs | 8 +- src/adnl/src/overlay/mod.rs | 25 +- src/adnl/src/quic/mod.rs | 704 +++++++++++++++--- src/adnl/tests/test_overlay.rs | 8 +- src/adnl/tests/test_quic.rs | 140 ++-- src/node/consensus-common/src/adnl_overlay.rs | 106 +-- src/node/simplex/README.md | 1 - src/node/simplex/src/lib.rs | 12 - src/node/simplex/src/session.rs | 4 + src/node/simplex/src/session_processor.rs | 251 ++++++- src/node/simplex/src/simplex_state.rs | 23 +- .../src/tests/test_session_processor.rs | 174 ++++- .../simplex/src/tests/test_simplex_state.rs | 129 +++- src/node/simplex/tests/test_consensus.rs | 79 +- src/node/src/engine.rs | 2 +- src/node/src/network/custom_overlay_client.rs | 8 +- src/node/src/tests/test_control.rs | 3 +- src/node/src/validator/validator_group.rs | 91 +-- src/node/src/validator/validator_manager.rs | 154 ++-- .../tests/compat_test/src/test_helpers.rs | 18 +- 21 files changed, 1485 insertions(+), 486 deletions(-) diff --git a/src/adnl/src/adnl/common.rs b/src/adnl/src/adnl/common.rs index 378bfc2..d0802ff 100644 --- a/src/adnl/src/adnl/common.rs +++ b/src/adnl/src/adnl/common.rs @@ -470,19 +470,40 @@ pub enum Answer { /// Asynchronous data receiver pub(crate) struct AsyncReceiver { + id: u64, data: lockfree::queue::Queue>, subscribers: lockfree::queue::Queue>, sync: AtomicU32, started_receiving: AtomicBool, + alive_tasks: AtomicU32, + total_spawned: AtomicU64, +} + +impl Drop for AsyncReceiver { + fn drop(&mut self) { + log::info!( + target: TARGET, + "AsyncReceiver #{} dropped (alive_tasks={}, total_spawned={})", + self.id, + self.alive_tasks.load(Ordering::Relaxed), + self.total_spawned.load(Ordering::Relaxed), + ); + } } impl AsyncReceiver { pub(crate) fn new() -> Arc { + static INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0); + let id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + log::info!(target: TARGET, "AsyncReceiver #{id} created"); Arc::new(Self { + id, data: lockfree::queue::Queue::new(), subscribers: lockfree::queue::Queue::new(), sync: AtomicU32::new(0), started_receiving: AtomicBool::new(false), + alive_tasks: AtomicU32::new(0), + total_spawned: AtomicU64::new(0), }) } @@ -515,6 +536,15 @@ impl AsyncReceiver { if self.sync.load(Ordering::Relaxed) == 0 { return; } + let alive = self.alive_tasks.fetch_add(1, Ordering::Relaxed) + 1; + let total = self.total_spawned.fetch_add(1, Ordering::Relaxed) + 1; + if total % 1000 == 0 || alive > 10 { + log::warn!( + target: TARGET, + "AsyncReceiver #{}: alive_tasks={alive}, total_spawned={total}, sync={}", + self.id, self.sync.load(Ordering::Relaxed) + ); + } let receiver = self.clone(); tokio::spawn(async move { while receiver.sync.load(Ordering::Relaxed) > 0 { @@ -525,6 +555,7 @@ impl AsyncReceiver { tokio::task::yield_now().await; } } + receiver.alive_tasks.fetch_sub(1, Ordering::Relaxed); }); } } diff --git a/src/adnl/src/overlay/broadcast.rs b/src/adnl/src/overlay/broadcast.rs index a8210ec..fd7637d 100644 --- a/src/adnl/src/overlay/broadcast.rs +++ b/src/adnl/src/overlay/broadcast.rs @@ -121,7 +121,7 @@ pub(crate) struct BroadcastSendContext<'a> { pub(crate) enum BroadcastSendMethod { Fast, - Rldp, + QuicOrRldp, Safe, } @@ -129,7 +129,7 @@ impl Display for BroadcastSendMethod { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { let msg = match self { Self::Fast => "datagram ADNL", - Self::Rldp => "RLDP", + Self::QuicOrRldp => "QUIC/RLDP", Self::Safe => "stream ADNL", }; write!(f, "{msg}") @@ -1722,7 +1722,7 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { } fn send_method(&self) -> BroadcastSendMethod { - BroadcastSendMethod::Rldp + BroadcastSendMethod::QuicOrRldp } // Receive side @@ -1994,7 +1994,7 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco fn send_method(&self) -> BroadcastSendMethod { if self.big_data { - BroadcastSendMethod::Rldp + BroadcastSendMethod::QuicOrRldp } else { BroadcastSendMethod::Fast } diff --git a/src/adnl/src/overlay/mod.rs b/src/adnl/src/overlay/mod.rs index b4192ab..049415e 100644 --- a/src/adnl/src/overlay/mod.rs +++ b/src/adnl/src/overlay/mod.rs @@ -466,6 +466,7 @@ declare_counted!( declare_counted!( struct Overlay { adnl: Arc, + quic: Option>, rldp: Option>, options: Arc, overlay_type: OverlayType, @@ -697,11 +698,18 @@ impl Overlay { BroadcastSendMethod::Fast => { self.adnl.send_custom_get_status(data, peers, AdnlSendMethod::Fast).await.err() } - BroadcastSendMethod::Rldp => { - let Some(rldp) = self.rldp.as_ref() else { - fail!("No RLDP sender is set in overlay {}", self.overlay_id); - }; - rldp.message(data, peers, true, None).await.err() + BroadcastSendMethod::QuicOrRldp => { + if let Some(quic) = self.quic.as_ref() { + quic.message(data.object.to_vec(), Some(&self.adnl), peers).await.err() + } else { + let Some(rldp) = self.rldp.as_ref() else { + fail!( + "Neither QUIC nor RLDP sender is set in overlay {}", + self.overlay_id + ); + }; + rldp.message(data, peers, true, None).await.err() + } } BroadcastSendMethod::Safe => { self.adnl.send_custom_get_status(data, peers, AdnlSendMethod::Safe).await.err() @@ -1769,7 +1777,7 @@ impl OverlayNode { "Sending QUIC message to unknown overlay", ) .await?; - quic.message(data, Some(&self.adnl), peers.local(), peers.other()).await?; + quic.message(data, Some(&self.adnl), &peers).await?; Ok(()) } @@ -1850,7 +1858,7 @@ impl OverlayNode { .await?; let mut data = overlay.query_prefix.clone(); serialize_boxed_append(&mut data, &query.object)?; - match quic.query(data, Some(&self.adnl), peers.local(), peers.other(), timeout_ms).await? { + match quic.query(data, Some(&self.adnl), &peers, timeout_ms).await? { Some(raw) => Ok(Some(deserialize_boxed(&raw)?)), None => Ok(None), } @@ -1966,6 +1974,7 @@ impl OverlayNode { let overlay = Overlay { adnl: self.adnl.clone(), rldp: self.rldp.get().cloned(), + quic: self.quic.get().cloned(), flags: params.flags, hops: params.hops, known_peers: AddressCacheWithBads::with_params(Self::MAX_PEERS, policy), @@ -2140,7 +2149,7 @@ impl OverlayNode { fn delete_overlay(&self, overlay_id: &Arc, is_private: bool) -> Result { let type_of = if is_private { "private" } else { "public" }; - log::debug!(target: TARGET, "Delete {} overlay {}", type_of, overlay_id); + log::info!(target: TARGET, "Delete {} overlay {}", type_of, overlay_id); if let Some(overlay) = self.overlays.get(overlay_id) { let overlay = overlay.val(); if is_private { diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index 98477a2..784f541 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -15,16 +15,17 @@ use crate::{ transport::{Connections, SendQueue}, }; use std::{ + collections::{HashMap, HashSet}, fmt, net::SocketAddr, sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Once, + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, Mutex, Once, Weak, }, time::Duration, }; use ton_api::{ - deserialize_boxed, serialize_boxed, + deserialize_boxed, deserialize_boxed_with_suffix, serialize_boxed, ton::quic::{ answer::Answer as QuicAnswer, request::{Message as QuicMessage, Query as QuicQuery}, @@ -171,13 +172,13 @@ struct QuicServerCertResolver { /// Most recently registered identity name. Used as SNI fallback when the client /// (e.g. C++ ngtcp2) doesn't send SNI, matching C++ SO_REUSEADDR behavior where /// the last-bound socket receives packets. - last_added_name: Arc>>, + last_added_name: Arc>>, } impl QuicServerCertResolver { fn new( keys: Arc>>, - last_added_name: Arc>>, + last_added_name: Arc>>, ) -> Arc { Arc::new(Self { keys, last_added_name }) } @@ -331,7 +332,7 @@ struct EndpointState { server_cert_keys: Arc>>, local_key_names: Arc>>, /// Tracks the most recently added identity name for SNI fallback. - last_added_name: Arc>>, + last_added_name: Arc>>, } pub struct QuicNode { @@ -339,19 +340,25 @@ pub struct QuicNode { /// One entry per local identity; each carries its own client config and outbound pool. local_keys: lockfree::map::Map, Arc>, /// One endpoint per unique bind port. Endpoints are created lazily by `add_key()`. - endpoints: std::sync::Mutex>>, + endpoints: Mutex>>, /// Shared subscriber list for all accept loops. subscribers: Arc>>, peer_keys: lockfree::map::Map, SocketAddr>, /// Max concurrent in-flight streams per inbound connection. max_streams_per_connection: usize, + /// Inbound connection maps, one per endpoint/accept-loop. Used by the stats dumper. + inbound_pools: Mutex>>>, + /// Per-TL-tag message counters for the stats dumper. + msg_stats: Arc, } impl QuicNode { pub const OFFSET_PORT: u16 = 1000; /// How often the background checker scans outbound connections for dead ones. - const CONNECTION_CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); + const CONNECTION_CHECK_INTERVAL: Duration = Duration::from_secs(5); + /// How often the stats dumper logs connection statistics. + const STATS_DUMP_INTERVAL: Duration = Duration::from_secs(60); const DEFAULT_MAX_STREAMS_PER_CONNECTION: usize = 256; const DEFAULT_QUERY_TIMEOUT_MS: u64 = 5000; /// Maximum number of messages buffered per outbound peer @@ -375,12 +382,15 @@ impl QuicNode { let transport = Arc::new(Self { cancellation_token: cancellation_token.clone(), local_keys: lockfree::map::Map::new(), - endpoints: std::sync::Mutex::new(std::collections::HashMap::new()), + endpoints: Mutex::new(HashMap::new()), subscribers: Arc::new(subscribers), peer_keys: lockfree::map::Map::new(), max_streams_per_connection, + inbound_pools: Mutex::new(Vec::new()), + msg_stats: MsgStats::new(), }); - Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token); + Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token.clone()); + Self::spawn_stats_dumper(Arc::downgrade(&transport), cancellation_token); transport } @@ -417,10 +427,9 @@ impl QuicNode { // Match server-side timeouts so both ends agree on connection liveness. let mut client_transport = quinn::TransportConfig::default(); client_transport.max_idle_timeout(Some( - quinn::IdleTimeout::try_from(std::time::Duration::from_secs(15)) - .expect("15s fits in IdleTimeout"), + quinn::IdleTimeout::try_from(Duration::from_secs(15)).expect("15s fits in IdleTimeout"), )); - client_transport.keep_alive_interval(Some(std::time::Duration::from_secs(5))); + client_transport.keep_alive_interval(Some(Duration::from_secs(5))); quinn_client_config.transport_config(Arc::new(client_transport)); let local_key_state = Arc::new(LocalKeyState { @@ -481,24 +490,29 @@ impl QuicNode { self: &Arc, data: Vec, adnl: Option<&AdnlNode>, - src: &Arc, - dst: &Arc, + peers: &AdnlPeers, ) -> Result> { - self.ensure_peer_registered(adnl, src, dst)?; + self.ensure_peer_registered(adnl, peers)?; + let tag = extract_inner_tag(&data); + let size = data.len(); let data = serialize_boxed(&QuicMessage { data: data.into() }.into_boxed())?; - let addr = self.addr_by_key(dst)?; - let state = self.local_key_state(src)?; + let addr = self.addr_by_key(peers.other())?; + let state = self.local_key_state(peers.local())?; let outbound = Self::get_or_create_outbound_connection(&state.outbound, addr)?; // Fast path: if connection is alive, send directly without queue overhead if let Some(ref conn) = outbound.conn { match Self::send_via_stream(conn, &data).await { - Ok(_) => return Ok(Some(data.len())), + Ok(_) => { + self.msg_stats.record(tag, size, addr, true, false); + return Ok(Some(data.len())); + } Err(e) => { log::warn!( target: TARGET, - "QUIC direct send to {dst} failed: {e}, removing dead connection, \ - falling back to queue" + "QUIC direct send to {} failed: {e}, removing dead connection, \ + falling back to queue", + peers.other() ); Self::remove_dead_connection(&state.outbound, addr, conn); } @@ -508,8 +522,9 @@ impl QuicNode { // Slow path: no connection (or it just died) โ€” enqueue for the sender task // which will establish the connection and deliver if !outbound.send_queue.try_push(data) { - fail!("QUIC send queue full for peer {dst}"); + fail!("QUIC send queue full for peer {}", peers.other()); } + self.msg_stats.record(tag, size, addr, true, false); // Spawn sender task if not already running (CAS guarantees at most one per peer) if outbound @@ -519,19 +534,16 @@ impl QuicNode { .is_ok() { let quic = self.clone(); - let src = src.clone(); - let dst = dst.clone(); let send_queue = outbound.send_queue.clone(); let sender_state = outbound.sender_state.clone(); let outbound_conns = state.outbound.clone(); - let server_name = Self::key_id_to_server_name(&dst); + let server_name = Self::key_id_to_server_name(peers.other()); spawn_cancelable( self.cancellation_token.clone(), Self::run_sender_task( quic, - src, - dst, + peers.clone(), addr, server_name, send_queue, @@ -548,14 +560,17 @@ impl QuicNode { self: &Arc, data: Vec, adnl: Option<&AdnlNode>, - src: &Arc, - dst: &Arc, + peers: &AdnlPeers, timeout_ms: Option, ) -> Result>> { - self.ensure_peer_registered(adnl, src, dst)?; + self.ensure_peer_registered(adnl, peers)?; + let addr = self.addr_by_key(peers.other())?; + let tag = extract_inner_tag(&data); + let size = data.len(); let timeout_ms = timeout_ms.unwrap_or(Self::DEFAULT_QUERY_TIMEOUT_MS); let wire = serialize_boxed(&QuicQuery { data: data.into() }.into_boxed())?; - let response = self.send_query_raw(wire, src, dst, timeout_ms).await?; + self.msg_stats.record(tag, size, addr, true, true); + let response = self.send_query_raw(wire, peers, timeout_ms).await?; if response.is_empty() { return Ok(None); } @@ -598,14 +613,9 @@ impl QuicNode { } } - async fn connect( - &self, - src: &Arc, - dst: &Arc, - addr: SocketAddr, - server_name: &str, - ) -> Result<()> { - let state = self.local_key_state(src)?; + async fn connect(&self, peers: &AdnlPeers, addr: SocketAddr, server_name: &str) -> Result<()> { + let dst = peers.other(); + let state = self.local_key_state(peers.local())?; let endpoint = { let endpoints = self.endpoints.lock().map_err(|e| error!("Endpoints lock: {e}"))?; endpoints @@ -648,29 +658,24 @@ impl QuicNode { /// Used by the query path where a live connection is required synchronously. async fn ensure_outbound_connection( self: &Arc, - src: &Arc, - dst: &Arc, + peers: &AdnlPeers, ) -> Result { - let addr = self.addr_by_key(dst)?; - let server_name = Self::key_id_to_server_name(dst); - let state = self.local_key_state(src)?; + let addr = self.addr_by_key(peers.other())?; + let server_name = Self::key_id_to_server_name(peers.other()); + let state = self.local_key_state(peers.local())?; loop { let conn = Self::get_or_create_outbound_connection(&state.outbound, addr)?; if conn.conn.is_some() { break Ok(conn); } log::info!(target: TARGET, "Try new QUIC connection to {addr} in foreground"); - self.connect(src, dst, addr, &server_name).await?; + self.connect(peers, addr, &server_name).await?; log::info!(target: TARGET, "QUIC connected to {addr} in foreground"); } } - fn ensure_peer_registered( - &self, - adnl: Option<&AdnlNode>, - src: &Arc, - dst: &Arc, - ) -> Result<()> { + fn ensure_peer_registered(&self, adnl: Option<&AdnlNode>, peers: &AdnlPeers) -> Result<()> { + let dst = peers.other(); if self.has_peer_key(dst) { return Ok(()); } @@ -678,7 +683,7 @@ impl QuicNode { fail!("QUIC peer {dst} is not registered and no ADNL node provided"); }; let mut addr = adnl - .peer_ip_address(src, dst)? + .peer_ip_address(peers.local(), dst)? .ok_or_else(|| error!("QUIC peer {dst} IP is not known in ADNL"))?; let quic_port = addr.port().checked_add(Self::OFFSET_PORT).ok_or_else(|| { error!("QUIC port overflow for peer {dst}: ADNL port {}", addr.port()) @@ -699,8 +704,7 @@ impl QuicNode { // Create per-endpoint TLS state let server_cert_keys: Arc>> = Arc::new(lockfree::map::Map::new()); - let last_added_name: Arc>> = - Arc::new(std::sync::Mutex::new(None)); + let last_added_name: Arc>> = Arc::new(Mutex::new(None)); let verifier = QuicClientCertVerifier::new(); let server_cert_resolver = QuicServerCertResolver::new(server_cert_keys.clone(), last_added_name.clone()); @@ -724,12 +728,11 @@ impl QuicNode { // connections for the default 30s, overloading the internal event loop and // making endpoint.accept() slow (the "HoL blocking" symptom). transport_config.max_idle_timeout(Some( - quinn::IdleTimeout::try_from(std::time::Duration::from_secs(15)) - .expect("15s fits in IdleTimeout"), + quinn::IdleTimeout::try_from(Duration::from_secs(15)).expect("15s fits in IdleTimeout"), )); // Keep established connections alive so the idle timeout only fires on // truly dead peers, not on connections that are just quiet between rounds. - transport_config.keep_alive_interval(Some(std::time::Duration::from_secs(5))); + transport_config.keep_alive_interval(Some(Duration::from_secs(5))); quinn_server_config.transport_config(Arc::new(transport_config)); // Create UDP socket with SO_REUSEADDR so the port can be reused immediately @@ -759,6 +762,15 @@ impl QuicNode { let local_key_names: Arc>> = Arc::new(lockfree::map::Map::new()); + let inbound: Arc> = Connections::new(); + match self.inbound_pools.lock() { + Ok(mut pools) => pools.push(inbound.clone()), + Err(e) => log::warn!( + target: TARGET, + "inbound_pools lock poisoned, inbound stats will be incomplete: {e}" + ), + } + Self::spawn_accept_loop( endpoint.clone(), local_key_names.clone(), @@ -767,6 +779,8 @@ impl QuicNode { bind_addr, self.max_streams_per_connection, self.cancellation_token.clone(), + inbound, + self.msg_stats.clone(), ); let state = Arc::new(EndpointState { @@ -830,13 +844,14 @@ impl QuicNode { subscribers: Arc>>, bind_addr: SocketAddr, max_streams_per_connection: usize, + msg_stats: Arc, ) { let addr = incoming.remote_address(); // Bound handshake time: C++ ngtcp2 clients abandon after ~3-5s and retry, // so a handshake still in progress after 5s is almost certainly stale. // Without this, stale Connecting futures accumulate inside quinn's endpoint, // slowing its internal event loop and delaying endpoint.accept() for new peers. - let conn = match tokio::time::timeout(std::time::Duration::from_secs(5), incoming).await { + let conn = match tokio::time::timeout(Duration::from_secs(5), incoming).await { Ok(Ok(conn)) => conn, Ok(Err(e)) => { log::warn!(target: TARGET, "QUIC handshake from {addr} on {bind_addr} failed: {e}"); @@ -922,6 +937,8 @@ impl QuicNode { let subs_uni = subscribers; let peers_bi = peers.clone(); let peers_uni = peers; + let stats_bi = msg_stats.clone(); + let stats_uni = msg_stats; let bi_loop = async { loop { @@ -938,10 +955,18 @@ impl QuicNode { }; let subscribers = subs_bi.clone(); let peers = peers_bi.clone(); + let stats = stats_bi.clone(); tokio::spawn(async move { let _permit = permit; - if let Err(e) = - Self::process_incoming_stream(recv, send, &subscribers, &peers, addr).await + if let Err(e) = Self::process_incoming_stream( + recv, + send, + &subscribers, + &peers, + addr, + &stats, + ) + .await { log::warn!(target: TARGET, "QUIC process bi-stream from {addr}: {e}"); } @@ -964,10 +989,12 @@ impl QuicNode { }; let subscribers = subs_uni.clone(); let peers = peers_uni.clone(); + let stats = stats_uni.clone(); tokio::spawn(async move { let _permit = permit; if let Err(e) = - Self::process_incoming_uni_stream(recv, &subscribers, &peers, addr).await + Self::process_incoming_uni_stream(recv, &subscribers, &peers, addr, &stats) + .await { log::warn!(target: TARGET, "QUIC process uni-stream from {addr}: {e}"); } @@ -1011,10 +1038,11 @@ impl QuicNode { subscribers: &[Arc], peers: &AdnlPeers, addr: SocketAddr, + msg_stats: &MsgStats, ) -> Result<()> { log::debug!(target: TARGET, "process_incoming_stream from {addr}: reading data..."); let buf = match tokio::time::timeout( - std::time::Duration::from_secs(5), + Duration::from_secs(5), recv.read_to_end(16 * 1024 * 1024), // 16MB limit ) .await @@ -1045,6 +1073,7 @@ impl QuicNode { ); match obj.downcast::() { Ok(Request::Quic_Message(msg)) => { + msg_stats.record(extract_inner_tag(&msg.data), msg.data.len(), addr, false, false); log::debug!( target: TARGET, "process_incoming_stream from {addr}: QUIC MESSAGE, \ @@ -1067,6 +1096,13 @@ impl QuicNode { ); } Ok(Request::Quic_Query(query)) => { + msg_stats.record( + extract_inner_tag(&query.data), + query.data.len(), + addr, + false, + true, + ); log::debug!(target: TARGET, "process_incoming_stream from {addr}: QUIC QUERY"); let answer = Query::process(subscribers, &query.data, &peers).await?; if let Some(answer) = answer { @@ -1105,22 +1141,21 @@ impl QuicNode { subscribers: &[Arc], peers: &AdnlPeers, addr: SocketAddr, + msg_stats: &MsgStats, ) -> Result<()> { - let buf = match tokio::time::timeout( - std::time::Duration::from_secs(5), - recv.read_to_end(16 * 1024 * 1024), - ) - .await - { - Ok(result) => result.map_err(|e| error!("QUIC uni read from {addr}: {e}"))?, - Err(_) => { - log::warn!( - target: TARGET, - "process_incoming_uni_stream from {addr}: read timed out after 5s" - ); - return Ok(()); - } - }; + let buf = + match tokio::time::timeout(Duration::from_secs(5), recv.read_to_end(16 * 1024 * 1024)) + .await + { + Ok(result) => result.map_err(|e| error!("QUIC uni read from {addr}: {e}"))?, + Err(_) => { + log::warn!( + target: TARGET, + "process_incoming_uni_stream from {addr}: read timed out after 5s" + ); + return Ok(()); + } + }; if buf.is_empty() { return Ok(()); } @@ -1128,6 +1163,7 @@ impl QuicNode { .map_err(|e| error!("Cannot deserialize QUIC uni-stream from {addr}: {e}"))?; match obj.downcast::() { Ok(Request::Quic_Message(msg)) => { + msg_stats.record(extract_inner_tag(&msg.data), msg.data.len(), addr, false, false); for subscriber in subscribers { if subscriber.try_consume_custom(&msg.data, peers).await? { break; @@ -1201,7 +1237,7 @@ impl QuicNode { ) { use rand::Rng; let delay_ms = rand::thread_rng().gen_range(500..=2500); - tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; + tokio::time::sleep(Duration::from_millis(delay_ms)).await; let old_alive = inbound.map().get(&addr).map(|e| e.val().close_reason().is_none()).unwrap_or(false); @@ -1242,8 +1278,7 @@ impl QuicNode { /// the connection, sends all queued messages, and terminates. async fn run_sender_task( quic: Arc, - src: Arc, - dst: Arc, + peers: AdnlPeers, addr: SocketAddr, server_name: String, send_queue: Arc, @@ -1256,7 +1291,7 @@ impl QuicNode { // Drain the queue while let Some(data) = send_queue.pop() { if let Err(e) = - quic.send_message(&src, &dst, addr, &server_name, &outbound, &data).await + quic.send_message(&peers, addr, &server_name, &outbound, &data).await { log::warn!(target: TARGET, "QUIC sender to {addr} error: {e}"); } @@ -1284,8 +1319,7 @@ impl QuicNode { /// Send a single message to the peer, establishing the connection first if needed. async fn send_message( &self, - src: &Arc, - dst: &Arc, + peers: &AdnlPeers, addr: SocketAddr, server_name: &str, outbound: &Connections, @@ -1305,7 +1339,7 @@ impl QuicNode { } None => { log::info!(target: TARGET, "QUIC sender: connecting to {addr}"); - self.connect(src, dst, addr, server_name).await?; + self.connect(peers, addr, server_name).await?; log::info!(target: TARGET, "QUIC sender: connected to {addr}"); let entry = Self::get_or_create_outbound_connection(outbound, addr)?; if let Some(ref conn) = entry.conn { @@ -1319,16 +1353,15 @@ impl QuicNode { async fn send_query_raw( self: &Arc, data: Vec, - src: &Arc, - dst: &Arc, + peers: &AdnlPeers, timeout_ms: u64, ) -> Result> { - let addr = self.addr_by_key(dst)?; - let state = self.local_key_state(src)?; + let addr = self.addr_by_key(peers.other())?; + let state = self.local_key_state(peers.local())?; let timeout = Duration::from_millis(timeout_ms); // First attempt - match self.ensure_outbound_connection(src, dst).await? { + match self.ensure_outbound_connection(peers).await? { QuicOutboundConnection { conn: Some(ref conn), .. } => { let result = tokio::time::timeout(timeout, Self::send_via_stream(conn, &data)).await; @@ -1337,28 +1370,31 @@ impl QuicNode { Ok(Err(e)) => { log::warn!( target: TARGET, - "QUIC query to {dst} failed: {e}, removing dead connection and retrying" + "QUIC query to {} failed: {e}, removing dead connection and retrying", + peers.other() ); Self::remove_dead_connection(&state.outbound, addr, conn); } Err(_) => { log::warn!( target: TARGET, - "QUIC query to {dst} timed out ({timeout_ms}ms), removing dead connection and retrying" + "QUIC query to {} timed out ({timeout_ms}ms), \ + removing dead connection and retrying", + peers.other() ); Self::remove_dead_connection(&state.outbound, addr, conn); } } } - _ => fail!("Cannot create QUIC connection to {dst} in foreground"), + _ => fail!("Cannot create QUIC connection to {} in foreground", peers.other()), } // Retry once with a fresh connection - match self.ensure_outbound_connection(src, dst).await? { + match self.ensure_outbound_connection(peers).await? { QuicOutboundConnection { conn: Some(ref conn), .. } => { Self::send_via_stream(conn, &data).await } - _ => fail!("Cannot create QUIC connection to {dst} in foreground (retry)"), + _ => fail!("Cannot create QUIC connection to {} in foreground (retry)", peers.other()), } } @@ -1373,7 +1409,7 @@ impl QuicNode { send.write_all(data).await.map_err(|e| error!("QUIC stream write: {e}"))?; send.finish().map_err(|e| error!("QUIC stream finish: {e}"))?; let response = match tokio::time::timeout( - std::time::Duration::from_secs(30), + Duration::from_secs(30), recv.read_to_end(16 * 1024 * 1024), // 16MB limit ) .await @@ -1404,9 +1440,10 @@ impl QuicNode { bind_addr: SocketAddr, max_streams_per_connection: usize, cancellation_token: tokio_util::sync::CancellationToken, + inbound: Arc>, + msg_stats: Arc, ) { tokio::spawn(async move { - let inbound: Arc> = Connections::new(); loop { log::trace!(target: TARGET, "Loop QUIC server on {bind_addr}"); tokio::select! { @@ -1427,6 +1464,7 @@ impl QuicNode { let scr = server_cert_resolver.clone(); let ib = inbound.clone(); let subs = subscribers.clone(); + let stats = msg_stats.clone(); tokio::spawn(async move { tokio::select! { _ = token.cancelled() => { @@ -1434,7 +1472,7 @@ impl QuicNode { } _ = Self::handle_connection( incoming, lkn, scr, ib, subs, bind_addr, - max_streams_per_connection, + max_streams_per_connection, stats, ) => {} } }); @@ -1449,7 +1487,7 @@ impl QuicNode { /// idle timeouts before the next send attempt, avoiding the 10-15s hang on /// first use of a dead connection. fn spawn_connection_checker( - weak: std::sync::Weak, + weak: Weak, cancellation_token: tokio_util::sync::CancellationToken, ) { spawn_cancelable(cancellation_token, async move { @@ -1507,4 +1545,474 @@ impl QuicNode { } }); } + + /// Background task that periodically logs statistics for all active QUIC connections. + /// Shows deltas (bytes/dgrams/lost since last dump) plus instantaneous path metrics. + fn spawn_stats_dumper( + weak: Weak, + cancellation_token: tokio_util::sync::CancellationToken, + ) { + spawn_cancelable(cancellation_token, async move { + // Key: (stable_id, is_outbound) โ†’ previous snapshot of cumulative counters + let mut prev: HashMap<(usize, bool), ConnSnapshot> = HashMap::new(); + + loop { + tokio::time::sleep(Self::STATS_DUMP_INTERVAL).await; + let Some(transport) = weak.upgrade() else { + log::trace!(target: TARGET, "Stats dumper: transport dropped, exiting"); + break; + }; + + let mut seen = HashSet::new(); + let mut total = 0u32; + let mut dump = String::from("QUIC STATS dump:\n"); + + // Outbound connections + for key_entry in transport.local_keys.iter() { + let key_id = key_entry.key(); + for conn_entry in key_entry.val().outbound.map().iter() { + let addr = *conn_entry.key(); + if let Some(ref conn) = conn_entry.val().conn { + let s = conn.stats(); + let id = (conn.stable_id(), true); + seen.insert(id); + total += 1; + let snap = ConnSnapshot::from_stats(&s); + let delta = prev.get(&id).map(|p| snap.delta(p)).unwrap_or(snap); + prev.insert(id, snap); + fmt::Write::write_fmt( + &mut dump, + format_args!( + " outbound peer={addr} \ + dtx={} bytes/{} dgrams drx={} bytes/{} dgrams \ + dlost={} pkts rtt={:?} cwnd={} mtu={} key={key_id:.8}\n", + delta.tx_bytes, + delta.tx_dgrams, + delta.rx_bytes, + delta.rx_dgrams, + delta.lost_pkts, + s.path.rtt, + s.path.cwnd, + s.path.current_mtu, + ), + ) + .ok(); + } + } + } + + // Inbound connections + let pools = match transport.inbound_pools.lock() { + Ok(g) => g.clone(), + Err(e) => { + log::warn!( + target: TARGET, + "inbound_pools lock poisoned, skipping inbound stats: {e}" + ); + Vec::new() + } + }; + for pool in &pools { + for conn_entry in pool.map().iter() { + let addr = *conn_entry.key(); + let conn = conn_entry.val(); + let s = conn.stats(); + let id = (conn.stable_id(), false); + seen.insert(id); + total += 1; + let snap = ConnSnapshot::from_stats(&s); + let delta = prev.get(&id).map(|p| snap.delta(p)).unwrap_or(snap); + prev.insert(id, snap); + fmt::Write::write_fmt( + &mut dump, + format_args!( + " inbound peer={addr} \ + dtx={} bytes/{} dgrams drx={} bytes/{} dgrams \ + dlost={} pkts rtt={:?} cwnd={} mtu={}\n", + delta.tx_bytes, + delta.tx_dgrams, + delta.rx_bytes, + delta.rx_dgrams, + delta.lost_pkts, + s.path.rtt, + s.path.cwnd, + s.path.current_mtu, + ), + ) + .ok(); + } + } + + // Evict snapshots for connections that no longer exist + prev.retain(|id, _| seen.contains(id)); + + // Per-peer, per-message-kind stats (deltas since last dump) + let msg_entries = transport.msg_stats.drain(); + let mut current_peer = None; + for (key, count, bytes) in &msg_entries { + if current_peer != Some(key.addr) { + current_peer = Some(key.addr); + fmt::Write::write_fmt(&mut dump, format_args!(" peer {}:\n", key.addr,)) + .ok(); + } + let dir = if key.is_outbound { "out" } else { " in" }; + let kind = if key.is_query { "query" } else { "msg " }; + fmt::Write::write_fmt( + &mut dump, + format_args!( + " {dir}/{kind} {:#010x}({}) count={count} bytes={bytes}\n", + key.tag, + tl_tag_name(key.tag), + ), + ) + .ok(); + } + + fmt::Write::write_fmt(&mut dump, format_args!( + " total: {total} connections, {} msg entries", + msg_entries.len(), + )).ok(); + + log::info!(target: TARGET, "{dump}"); + } + }); + } +} + +/// Extract the "inner" TL constructor tag from message data. +/// +/// QUIC message payloads are typically wrapped in an overlay prefix +/// (`overlay.message` or `overlay.query`). The outer tag is not useful +/// for diagnostics. This function skips past the overlay wrapper and +/// returns the constructor tag of the actual inner payload. +/// +/// `overlay.message` and `overlay.query` have a fixed layout: +/// constructor(4 bytes) + int256(32 bytes) = 36 bytes prefix. +/// `WithExtra` variants have a variable-length extra field, so we +/// fall back to `deserialize_boxed_with_suffix` for those. +fn extract_inner_tag(data: &[u8]) -> u32 { + if data.len() < 4 { + return 0; + } + let outer = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); + // overlay.message / overlay.query: fixed 36-byte prefix (constructor + int256) + const FIXED_PREFIX: usize = 4 + 32; + match outer { + 0x75252420 | 0xccfd8443 => { + // overlay.message, overlay.query + if data.len() >= FIXED_PREFIX + 4 { + let s = &data[FIXED_PREFIX..]; + return u32::from_le_bytes([s[0], s[1], s[2], s[3]]); + } + outer + } + 0xa232233d | 0x94ffc3e9 => { + // overlay.messageWithExtra, overlay.queryWithExtra + if let Ok((_obj, suffix_offset)) = deserialize_boxed_with_suffix(data) { + if suffix_offset + 4 <= data.len() { + let s = &data[suffix_offset..]; + return u32::from_le_bytes([s[0], s[1], s[2], s[3]]); + } + } + outer + } + _ => outer, + } +} + +/// Map well-known TL constructor tags to short human-readable names for log output. +fn tl_tag_name(tag: u32) -> &'static str { + match tag { + 0x75252420 => "overlay.message", + 0xa232233d => "overlay.messageWithExtra", + 0xccfd8443 => "overlay.query", + 0x94ffc3e9 => "overlay.queryWithExtra", + 0xb15a2b6b => "overlay.broadcast", + 0xbad7c36a => "overlay.broadcastFec", + 0xf1881342 => "overlay.broadcastFecShort", + 0x46efae62 => "overlay.broadcastStream", + 0xf99fd63d => "overlay.broadcastTwostepFec", + 0x80b859b0 => "overlay.broadcastTwostepSimple", + 0x33534e24 => "overlay.unicast", + 0xd55c14ec => "overlay.fec.received", + 0x09d76914 => "overlay.fec.completed", + 0x48ee64ab => "overlay.getRandomPeers", + 0xa58e7ecc => "overlay.getRandomPeersV2", + 0x690cb481 => "overlay.ping", + 0x236758c4 => "catchain.blockUpdate", + 0x9283ce37 => "validatorSession.blockUpdate", + 0xbe7b573a => "consensus.simplex.certificate", + 0xc37ef4f3 => "consensus.simplex.vote", + _ => "unknown", + } +} + +/// Snapshot of cumulative counters from a single connection, used to compute deltas. +#[derive(Clone, Copy)] +struct ConnSnapshot { + tx_bytes: u64, + tx_dgrams: u64, + rx_bytes: u64, + rx_dgrams: u64, + lost_pkts: u64, +} + +impl ConnSnapshot { + fn from_stats(s: &quinn::ConnectionStats) -> Self { + Self { + tx_bytes: s.udp_tx.bytes, + tx_dgrams: s.udp_tx.datagrams, + rx_bytes: s.udp_rx.bytes, + rx_dgrams: s.udp_rx.datagrams, + lost_pkts: s.path.lost_packets, + } + } + + fn delta(&self, prev: &Self) -> Self { + Self { + tx_bytes: self.tx_bytes.saturating_sub(prev.tx_bytes), + tx_dgrams: self.tx_dgrams.saturating_sub(prev.tx_dgrams), + rx_bytes: self.rx_bytes.saturating_sub(prev.rx_bytes), + rx_dgrams: self.rx_dgrams.saturating_sub(prev.rx_dgrams), + lost_pkts: self.lost_pkts.saturating_sub(prev.lost_pkts), + } + } +} + +/// Per-TL-tag message counters (lock-free atomics, collected per dump interval). +struct MsgTagCounters { + count: AtomicU64, + bytes: AtomicU64, +} + +impl MsgTagCounters { + fn new() -> Self { + Self { count: AtomicU64::new(0), bytes: AtomicU64::new(0) } + } + + fn record(&self, size: usize) { + self.count.fetch_add(1, Ordering::Relaxed); + self.bytes.fetch_add(size as u64, Ordering::Relaxed); + } + + /// Take current values and reset to zero. + fn take(&self) -> (u64, u64) { + (self.count.swap(0, Ordering::Relaxed), self.bytes.swap(0, Ordering::Relaxed)) + } +} + +/// Per-peer, per-TL-tag message statistics key. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct MsgStatsKey { + addr: SocketAddr, + tag: u32, + is_outbound: bool, + is_query: bool, +} + +/// Tracks per-peer, per-message-kind statistics for QUIC traffic. +struct MsgStats { + counters: lockfree::map::Map, +} + +impl MsgStats { + fn new() -> Arc { + Arc::new(Self { counters: lockfree::map::Map::new() }) + } + + fn record(&self, tag: u32, size: usize, addr: SocketAddr, is_outbound: bool, is_query: bool) { + let key = MsgStatsKey { addr, tag, is_outbound, is_query }; + if let Some(entry) = self.counters.get(&key) { + entry.val().record(size); + return; + } + let _ = add_unbound_object_to_map(&self.counters, key, || Ok(MsgTagCounters::new())); + if let Some(entry) = self.counters.get(&key) { + entry.val().record(size); + } + } + + /// Drain all counters and return entries sorted by peer then bytes desc. + /// Entries with zero activity since the last drain are removed + fn drain(&self) -> Vec<(MsgStatsKey, u64, u64)> { + let mut result = Vec::new(); + let mut stale = Vec::new(); + for entry in self.counters.iter() { + let (count, bytes) = entry.val().take(); + if count > 0 { + result.push((*entry.key(), count, bytes)); + } else { + stale.push(*entry.key()); + } + } + for key in stale { + self.counters.remove(&key); + } + result.sort_by(|a, b| a.0.addr.cmp(&b.0.addr).then(b.2.cmp(&a.2))); + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // --- extract_inner_tag --- + + /// Helper: build an overlay.message (0x75252420) wrapping the given inner tag. + fn make_overlay_message(inner_tag: u32) -> Vec { + let mut buf = Vec::new(); + buf.extend_from_slice(&0x75252420u32.to_le_bytes()); // outer tag + buf.extend_from_slice(&[0u8; 32]); // overlay int256 + buf.extend_from_slice(&inner_tag.to_le_bytes()); // inner payload tag + buf + } + + /// Helper: build an overlay.query (0xccfd8443) wrapping the given inner tag. + fn make_overlay_query(inner_tag: u32) -> Vec { + let mut buf = Vec::new(); + buf.extend_from_slice(&0xccfd8443u32.to_le_bytes()); + buf.extend_from_slice(&[0u8; 32]); + buf.extend_from_slice(&inner_tag.to_le_bytes()); + buf + } + + #[test] + fn test_extract_inner_tag_empty() { + assert_eq!(extract_inner_tag(&[]), 0); + assert_eq!(extract_inner_tag(&[1, 2, 3]), 0); + } + + #[test] + fn test_extract_inner_tag_unknown_outer() { + let data = 0xDEADBEEFu32.to_le_bytes(); + assert_eq!(extract_inner_tag(&data), 0xDEADBEEF); + } + + #[test] + fn test_extract_inner_tag_overlay_message() { + let data = make_overlay_message(0x236758c4); // catchain.blockUpdate + assert_eq!(extract_inner_tag(&data), 0x236758c4); + } + + #[test] + fn test_extract_inner_tag_overlay_query() { + let data = make_overlay_query(0x48ee64ab); // overlay.getRandomPeers + assert_eq!(extract_inner_tag(&data), 0x48ee64ab); + } + + #[test] + fn test_extract_inner_tag_overlay_message_too_short() { + // outer tag + partial overlay id (not enough for inner tag) + let mut data = Vec::new(); + data.extend_from_slice(&0x75252420u32.to_le_bytes()); + data.extend_from_slice(&[0u8; 30]); // only 30 bytes, need 32 + 4 + assert_eq!(extract_inner_tag(&data), 0x75252420); // falls back to outer + } + + // --- MsgStats --- + + fn test_addr(port: u16) -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], port)) + } + + #[test] + fn test_msg_stats_record_and_drain() { + let stats = MsgStats::new(); + let addr = test_addr(1000); + + stats.record(0xAA, 100, addr, true, false); + stats.record(0xAA, 200, addr, true, false); + stats.record(0xBB, 50, addr, true, true); + + let entries = stats.drain(); + assert_eq!(entries.len(), 2); + + // Sorted by addr (same), then bytes desc: AA(300) before BB(50) + assert_eq!(entries[0].0.tag, 0xAA); + assert_eq!(entries[0].1, 2); // count + assert_eq!(entries[0].2, 300); // bytes + + assert_eq!(entries[1].0.tag, 0xBB); + assert_eq!(entries[1].1, 1); + assert_eq!(entries[1].2, 50); + } + + #[test] + fn test_msg_stats_drain_sorts_by_addr_then_bytes() { + let stats = MsgStats::new(); + let addr_a = test_addr(1000); + let addr_b = test_addr(2000); + + stats.record(0xAA, 10, addr_b, true, false); + stats.record(0xBB, 500, addr_a, true, false); + stats.record(0xCC, 100, addr_a, true, false); + + let entries = stats.drain(); + assert_eq!(entries.len(), 3); + + // addr_a (port 1000) first, sorted by bytes desc + assert_eq!(entries[0].0.addr, addr_a); + assert_eq!(entries[0].0.tag, 0xBB); // 500 bytes + assert_eq!(entries[1].0.addr, addr_a); + assert_eq!(entries[1].0.tag, 0xCC); // 100 bytes + + // addr_b (port 2000) last + assert_eq!(entries[2].0.addr, addr_b); + assert_eq!(entries[2].0.tag, 0xAA); + } + + #[test] + fn test_msg_stats_drain_resets_counters() { + let stats = MsgStats::new(); + let addr = test_addr(1000); + + stats.record(0xAA, 100, addr, true, false); + let entries = stats.drain(); + assert_eq!(entries.len(), 1); + + // Second drain: no new activity, should return empty + let entries = stats.drain(); + assert!(entries.is_empty()); + } + + #[test] + fn test_msg_stats_drain_evicts_stale_keys() { + let stats = MsgStats::new(); + let addr = test_addr(1000); + + stats.record(0xAA, 100, addr, true, false); + stats.record(0xBB, 50, addr, false, false); + + // First drain: both active, counters reset + let _ = stats.drain(); + + // Only record on 0xAA + stats.record(0xAA, 200, addr, true, false); + + // Second drain: 0xBB was idle โ†’ evicted + let _ = stats.drain(); + + // Record on 0xBB again โ€” must re-insert (was evicted) + stats.record(0xBB, 30, addr, false, false); + let entries = stats.drain(); + + // 0xAA was idle since last drain (evicted), only 0xBB with activity is returned + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].0.tag, 0xBB); + assert_eq!(entries[0].2, 30); + } + + #[test] + fn test_msg_stats_distinguishes_direction_and_kind() { + let stats = MsgStats::new(); + let addr = test_addr(1000); + + stats.record(0xAA, 100, addr, true, false); // outbound msg + stats.record(0xAA, 200, addr, false, false); // inbound msg + stats.record(0xAA, 300, addr, true, true); // outbound query + + let entries = stats.drain(); + assert_eq!(entries.len(), 3); + } } diff --git a/src/adnl/tests/test_overlay.rs b/src/adnl/tests/test_overlay.rs index 042da09..09326f3 100644 --- a/src/adnl/tests/test_overlay.rs +++ b/src/adnl/tests/test_overlay.rs @@ -98,6 +98,9 @@ pub fn build_dht_node_info_ex( } */ +/// Base port for test_broadcast nodes (range 4210..4219, does not overlap with other tests). +const BROADCAST_TEST_BASE_PORT: u16 = 4210; + fn init_overlay_simple_compatibility_test( local_ip_template: &str, #[cfg(feature = "dump")] dump_path: Option<&str>, @@ -817,12 +820,11 @@ fn test_broadcast( test: impl Fn(&[LocalNode], &[Arc>>], Protocol) -> RunResult, protocol: Protocol, ) { - const FIRST_PORT: usize = 4181; - init_test(); let mut nodes = Vec::new(); for i in 0..n { - let ip = format!("127.0.0.1:{}", FIRST_PORT + i); + let port = BROADCAST_TEST_BASE_PORT + i as u16; + let ip = format!("127.0.0.1:{port}"); nodes.push(init_local_node(ip, 100 / n as u8)); } diff --git a/src/adnl/tests/test_quic.rs b/src/adnl/tests/test_quic.rs index 1fe7d06..c8b4ac1 100644 --- a/src/adnl/tests/test_quic.rs +++ b/src/adnl/tests/test_quic.rs @@ -171,12 +171,11 @@ fn test_quic_concurrent_accept() { let mut handles = Vec::with_capacity(NUM_CLIENTS); for (i, client) in clients.iter().enumerate() { let quic = client.quic.clone(); - let local = client.key_id.clone(); - let remote = server_key_id.clone(); + let peers = AdnlPeers::with_keys(client.key_id.clone(), server_key_id.clone()); let value = i as i64; handles.push(tokio::spawn(async move { let resp = quic - .query(make_ping_data(value), None, &local, &remote, None) + .query(make_ping_data(value), None, &peers, None) .await .unwrap_or_else(|e| panic!("client {i} query failed: {e}")); let pong = parse_pong(resp.unwrap()); @@ -264,27 +263,23 @@ fn test_quic_session() { quic_a.add_peer_key(key_id_b.clone(), "127.0.0.1:5601".parse().unwrap()).unwrap(); quic_b.add_peer_key(key_id_a.clone(), "127.0.0.1:5600".parse().unwrap()).unwrap(); + let peers_ab = AdnlPeers::with_keys(key_id_a.clone(), key_id_b.clone()); + let peers_ba = AdnlPeers::with_keys(key_id_b.clone(), key_id_a.clone()); for i in 0..ITERATIONS { let value = i as i64; // A โ†’ B: query - let resp = quic_a - .query(make_ping_data(value), None, &key_id_a, &key_id_b, None) - .await - .unwrap() - .unwrap(); + let resp = + quic_a.query(make_ping_data(value), None, &peers_ab, None).await.unwrap().unwrap(); assert_eq!(parse_pong(resp), value, "Aโ†’B query iter {i}: pong mismatch"); // B โ†’ A: query - let resp = quic_b - .query(make_ping_data(value), None, &key_id_b, &key_id_a, None) - .await - .unwrap() - .unwrap(); + let resp = + quic_b.query(make_ping_data(value), None, &peers_ba, None).await.unwrap().unwrap(); assert_eq!(parse_pong(resp), value, "Bโ†’A query iter {i}: pong mismatch"); // A โ†’ B: message - quic_a.message(MSG_PAYLOAD.to_vec(), None, &key_id_a, &key_id_b).await.unwrap(); + quic_a.message(MSG_PAYLOAD.to_vec(), None, &peers_ab).await.unwrap(); assert_eq!( recv_with_timeout(&mut rx_b).await, MSG_PAYLOAD, @@ -292,7 +287,7 @@ fn test_quic_session() { ); // B โ†’ A: message - quic_b.message(MSG_PAYLOAD.to_vec(), None, &key_id_b, &key_id_a).await.unwrap(); + quic_b.message(MSG_PAYLOAD.to_vec(), None, &peers_ba).await.unwrap(); assert_eq!( recv_with_timeout(&mut rx_a).await, MSG_PAYLOAD, @@ -360,15 +355,14 @@ fn test_quic_reconnect_after_server_restart() { // Register peer keys client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server1.add_peer_key(client_key_id.clone(), client_bind).unwrap(); + let peers = AdnlPeers::with_keys(client_key_id.clone(), server_key_id.clone()); // Step 1: successful ping/pong through B1 - let resp = tokio::time::timeout( - TIMEOUT, - client.query(make_ping_data(1), None, &client_key_id, &server_key_id, None), - ) - .await - .expect("initial query timed out") - .expect("initial query failed"); + let resp = + tokio::time::timeout(TIMEOUT, client.query(make_ping_data(1), None, &peers, None)) + .await + .expect("initial query timed out") + .expect("initial query failed"); assert_eq!(parse_pong(resp.unwrap()), 1, "initial pong mismatch"); println!("Step 1: initial ping/pong succeeded"); @@ -394,13 +388,11 @@ fn test_quic_reconnect_after_server_restart() { println!("Step 3: server B2 started on same port with same key"); // Step 4: client sends another query โ€” should remove dead conn, reconnect, and succeed - let resp = tokio::time::timeout( - TIMEOUT, - client.query(make_ping_data(2), None, &client_key_id, &server_key_id, None), - ) - .await - .expect("reconnect query timed out โ€” dead connection removal may be broken") - .expect("reconnect query failed"); + let resp = + tokio::time::timeout(TIMEOUT, client.query(make_ping_data(2), None, &peers, None)) + .await + .expect("reconnect query timed out โ€” dead connection removal may be broken") + .expect("reconnect query failed"); assert_eq!(parse_pong(resp.unwrap()), 2, "reconnect pong mismatch"); println!("Step 4: reconnect ping/pong succeeded"); @@ -516,11 +508,12 @@ fn test_quic_stream_limit() { // Register peers client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(client_key_id.clone(), client_bind).unwrap(); + let peers = AdnlPeers::with_keys(client_key_id.clone(), server_key_id.clone()); // Establish the connection with a ping/pong first let resp = tokio::time::timeout( Duration::from_secs(10), - client.query(make_ping_data(42), None, &client_key_id, &server_key_id, None), + client.query(make_ping_data(42), None, &peers, None), ) .await .expect("initial query timed out") @@ -531,11 +524,10 @@ fn test_quic_stream_limit() { let mut handles = Vec::with_capacity(NUM_MESSAGES); for i in 0..NUM_MESSAGES { let quic = client.clone(); - let local = client_key_id.clone(); - let remote = server_key_id.clone(); + let peers = peers.clone(); handles.push(tokio::spawn(async move { let payload = format!("msg-{i}"); - quic.message(payload.as_bytes().to_vec(), None, &local, &remote) + quic.message(payload.as_bytes().to_vec(), None, &peers) .await .unwrap_or_else(|e| panic!("message {i} failed: {e}")); })); @@ -716,22 +708,19 @@ fn test_quic_duplicate_inbound_resolution() { server.add_peer_key(c1_key_id.clone(), c1_bind).unwrap(); server.add_peer_key(c2_key_id.clone(), c2_bind).unwrap(); + let peers1 = AdnlPeers::with_keys(c1_key_id.clone(), server_key_id.clone()); + let peers2 = AdnlPeers::with_keys(c2_key_id.clone(), server_key_id.clone()); + // Step 1: both clients connect concurrently let h1 = { let q = client1.clone(); - let local = c1_key_id.clone(); - let remote = server_key_id.clone(); - tokio::spawn( - async move { q.query(make_ping_data(1), None, &local, &remote, None).await }, - ) + let peers = peers1.clone(); + tokio::spawn(async move { q.query(make_ping_data(1), None, &peers, None).await }) }; let h2 = { let q = client2.clone(); - let local = c2_key_id.clone(); - let remote = server_key_id.clone(); - tokio::spawn( - async move { q.query(make_ping_data(2), None, &local, &remote, None).await }, - ) + let peers = peers2.clone(); + tokio::spawn(async move { q.query(make_ping_data(2), None, &peers, None).await }) }; let (r1, r2) = tokio::time::timeout(TIMEOUT, async { tokio::join!(h1, h2) }) @@ -747,22 +736,18 @@ fn test_quic_duplicate_inbound_resolution() { tokio::time::sleep(Duration::from_secs(3)).await; // Step 3: both clients should still be able to query (through surviving connections) - let resp1 = tokio::time::timeout( - TIMEOUT, - client1.query(make_ping_data(10), None, &c1_key_id, &server_key_id, None), - ) - .await - .expect("post-resolution query1 timed out") - .expect("post-resolution query1 failed"); + let resp1 = + tokio::time::timeout(TIMEOUT, client1.query(make_ping_data(10), None, &peers1, None)) + .await + .expect("post-resolution query1 timed out") + .expect("post-resolution query1 failed"); assert_eq!(parse_pong(resp1.unwrap()), 10); - let resp2 = tokio::time::timeout( - TIMEOUT, - client2.query(make_ping_data(20), None, &c2_key_id, &server_key_id, None), - ) - .await - .expect("post-resolution query2 timed out") - .expect("post-resolution query2 failed"); + let resp2 = + tokio::time::timeout(TIMEOUT, client2.query(make_ping_data(20), None, &peers2, None)) + .await + .expect("post-resolution query2 timed out") + .expect("post-resolution query2 failed"); assert_eq!(parse_pong(resp2.unwrap()), 20); println!("Step 3: both clients still functional after duplicate resolution"); @@ -975,7 +960,12 @@ fn test_quic_stream_read_timeout() { let resp = tokio::time::timeout( Duration::from_secs(10), - normal_client.query(make_ping_data(777), None, &nc_key_id, &server_key_id, None), + normal_client.query( + make_ping_data(777), + None, + &AdnlPeers::with_keys(nc_key_id.clone(), server_key_id.clone()), + None, + ), ) .await .expect("normal client query timed out") @@ -1139,7 +1129,12 @@ fn test_quic_reject_non_rpk_client() { let resp = tokio::time::timeout( Duration::from_secs(10), - legit.query(make_ping_data(42), None, &lk_id, &server_key_id, None), + legit.query( + make_ping_data(42), + None, + &AdnlPeers::with_keys(lk_id.clone(), server_key_id.clone()), + None, + ), ) .await .expect("legit query timed out after rogue attempt") @@ -1191,7 +1186,12 @@ fn test_quic_rpk_identity_mismatch() { // peer_key_id == expected dst, and they won't match. let result = tokio::time::timeout( Duration::from_secs(10), - client.query(make_ping_data(1), None, &client_key_id, &fake_key_id, None), + client.query( + make_ping_data(1), + None, + &AdnlPeers::with_keys(client_key_id.clone(), fake_key_id.clone()), + None, + ), ) .await; @@ -1276,12 +1276,11 @@ fn test_quic_connection_pool_exhaustion() { let mut handles = Vec::with_capacity(NUM_CLIENTS); for (i, client) in clients.iter().enumerate() { let quic = client.quic.clone(); - let local = client.key_id.clone(); - let remote = server_key_id.clone(); + let peers = AdnlPeers::with_keys(client.key_id.clone(), server_key_id.clone()); let value = i as i64; handles.push(tokio::spawn(async move { let resp = quic - .query(make_ping_data(value), None, &local, &remote, None) + .query(make_ping_data(value), None, &peers, None) .await .unwrap_or_else(|e| panic!("client {i} query failed: {e}")); assert_eq!(parse_pong(resp.unwrap()), value, "client {i}: pong mismatch"); @@ -1320,7 +1319,12 @@ fn test_quic_connection_pool_exhaustion() { let resp = tokio::time::timeout( Duration::from_secs(10), - fresh.query(make_ping_data(12345), None, &fk_id, &server_key_id, None), + fresh.query( + make_ping_data(12345), + None, + &AdnlPeers::with_keys(fk_id.clone(), server_key_id.clone()), + None, + ), ) .await .expect("fresh client query timed out after pool exhaust") @@ -1385,10 +1389,11 @@ fn test_quic_message_burst_reconnect() { client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server1.add_peer_key(client_key_id.clone(), client_bind).unwrap(); + let peers = AdnlPeers::with_keys(client_key_id.clone(), server_key_id.clone()); for i in 0..BURST_SIZE { let payload = format!("msg-phase1-{i}").into_bytes(); - client.message(payload, None, &client_key_id, &server_key_id).await.unwrap(); + client.message(payload, None, &peers).await.unwrap(); } let expected_p1: HashSet> = @@ -1425,7 +1430,7 @@ fn test_quic_message_burst_reconnect() { for i in 0..BURST_SIZE { let payload = format!("msg-phase2-{i}").into_bytes(); - client.message(payload, None, &client_key_id, &server_key_id).await.unwrap(); + client.message(payload, None, &peers).await.unwrap(); } let expected_p2: HashSet> = @@ -1529,7 +1534,8 @@ fn test_quic_single_sender_invariant() { handles.push(tokio::spawn(async move { for msg_id in 0..MSGS_PER_SENDER { let payload = format!("sender-{sender_id}-msg-{msg_id}").into_bytes(); - if let Err(e) = quic.message(payload, None, &src, &dst).await { + let peers = AdnlPeers::with_keys(src.clone(), dst.clone()); + if let Err(e) = quic.message(payload, None, &peers).await { eprintln!("sender {sender_id} msg {msg_id} failed: {e}"); } } diff --git a/src/node/consensus-common/src/adnl_overlay.rs b/src/node/consensus-common/src/adnl_overlay.rs index a19d058..4b9da5f 100644 --- a/src/node/consensus-common/src/adnl_overlay.rs +++ b/src/node/consensus-common/src/adnl_overlay.rs @@ -934,8 +934,8 @@ impl AdnlOverlay { peers.len() ); - // Register QUIC keys and create transport if QUIC is enabled - let transport = if use_quic { + // Register QUIC keys if QUIC is enabled + let quic_enabled = if use_quic { if let Some(quic) = &stack.quic { // Register local validator's ADNL key as a TLS identity on a per-port endpoint let key_bytes: [u8; 32] = *local_adnl_key.pvt_key()?; @@ -1022,7 +1022,7 @@ impl AdnlOverlay { // Point-to-point multicast is used for broadcasts when TCP or QUIC // transport is available (FEC/UDP broadcast has no peers in private overlays). let is_tcp_available = - (stack.is_tcp_available() && allow_tcp_communication) || transport; + (stack.is_tcp_available() && allow_tcp_communication) || quic_enabled; if is_tcp_available { log::debug!( @@ -1069,7 +1069,7 @@ impl AdnlOverlay { peers: peer_objects, peers_storage: peer_storage.clone(), is_tcp_available: is_tcp_available, - is_quic_available: transport, + is_quic_available: quic_enabled, all_node_ids: all_node_ids, task_processor_manager, } @@ -1675,11 +1675,65 @@ impl ConsensusOverlay for AdnlOverlay { let stop_requested = self.stop_requested.clone(); if stop_requested.load(Ordering::Relaxed) { - log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {} was stopped!", &overlay_id); + log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {overlay_id} was stopped!"); return; } - if self.is_tcp_available { + if self.is_quic_available || !self.is_tcp_available { + // QUIC or UDP path + // If extra given, use two-step broadcast via QUIC/RLDP + // Otherwise use canonic broadcast via ADNL + let msg = payload.clone(); + let overlay_node = self.stack.overlay.clone(); + let local_validator_key = self.local_validator_key.clone(); + let transport = if self.is_quic_available { "QUIC" } else { "UDP" }; + + self.runtime_handle.spawn(async move { + if stop_requested.load(Ordering::Relaxed) { + log::warn!( + target: LOG_TARGET, + "AdnlOverlay: Overlay {overlay_id} was stopped!" + ); + return; + } + + let msg_tagged = TaggedByteSlice { + object: msg.data(), + #[cfg(feature = "telemetry")] + tag: 0x80000002, // Consensus broadcast + }; + + let result = if let Some(extra) = extra { + // Twostep broadcast with extra + overlay_node + .broadcast_twostep( + &overlay_id, + &msg_tagged, + Some(&local_validator_key), + 0, + extra, + ) + .await + } else { + // Canonic broadcast + overlay_node + .broadcast( + &overlay_id, + &msg_tagged, + Some(&local_validator_key), + 0, + AdnlSendMethod::Fast, + ) + .await + }; + + log::debug!( + target: LOG_TARGET, + "AdnlOverlay::send_broadcast_fec_ex ({transport}) status: {result:?}" + ); + }); + } else { + // TCP path: manually build BroadcastTwostepSimple and multicast const IS_RETRANSMISSION: bool = false; log::trace!( @@ -1744,7 +1798,7 @@ impl ConsensusOverlay for AdnlOverlay { log::trace!( target: LOG_TARGET, "AdnlOverlay::send_broadcast_fec_ex: sending BroadcastTwostepSimple \ - ({} bytes payload) via multicast to {} peers", + ({} bytes payload) via TCP multicast to {} peers", broadcast_payload.data().len(), self.all_node_ids.len(), ); @@ -1761,45 +1815,9 @@ impl ConsensusOverlay for AdnlOverlay { if let Err(err) = result { log::error!( target: LOG_TARGET, - "AdnlOverlay::send_broadcast_fec_ex: failed to build/send two-step broadcast: {err}" + "AdnlOverlay::send_broadcast_fec_ex: failed to build/send TCP broadcast: {err}" ); } - } else { - let msg = payload.clone(); - let overlay_node = self.stack.overlay.clone(); - let local_validator_key = self.local_validator_key.clone(); - let runtime_handle = self.runtime_handle.clone(); - - runtime_handle.spawn(async move { - if stop_requested.load(Ordering::Relaxed) { - log::warn!( - target: LOG_TARGET, - "AdnlOverlay: Overlay {overlay_id} was stopped!" - ); - return; - } - - let msg_tagged = TaggedByteSlice { - object: msg.data(), - #[cfg(feature = "telemetry")] - tag: 0x80000002, // Catchain broadcast - }; - - let result = overlay_node - .broadcast( - &overlay_id, - &msg_tagged, - Some(&local_validator_key), - 0, - AdnlSendMethod::Fast, - ) - .await; - - log::debug!( - target: LOG_TARGET, - "AdnlOverlay::send_broadcast_fec_ex status: {result:?}" - ); - }); } } } diff --git a/src/node/simplex/README.md b/src/node/simplex/README.md index da80006..8fced04 100644 --- a/src/node/simplex/README.md +++ b/src/node/simplex/README.md @@ -544,7 +544,6 @@ Cryptographic and utility functions: | `max_collated_data_size` | `usize` | 4 MB | Max collated data | | `collation_retry_timeout` | `Duration` | 1s | Collation retry timeout | | `collation_retry_max_attempts` | `u32` | 3 | Max collation retries | -| `max_precollated_blocks` | `u32` | 10 | Max precollated blocks | | `use_callback_thread` | `bool` | true | Use separate callback thread | ## Integration diff --git a/src/node/simplex/src/lib.rs b/src/node/simplex/src/lib.rs index c93df7f..6bb2aae 100644 --- a/src/node/simplex/src/lib.rs +++ b/src/node/simplex/src/lib.rs @@ -435,9 +435,6 @@ pub struct SessionOptions { /// Collation retry max attempts pub collation_retry_max_attempts: u32, - /// Maximum number of precollated blocks to keep in pipeline - pub max_precollated_blocks: u32, - /// Standstill timeout - if no finalization occurs within this period, /// re-broadcast all our votes for tracked slots /// Default: 10 seconds (matches C++ standstill_timeout_s) @@ -543,7 +540,6 @@ impl Default for SessionOptions { validation_retry_timeout: Duration::from_secs(1), collation_retry_timeout: Duration::from_millis(500), collation_retry_max_attempts: 3, - max_precollated_blocks: 0, // Precollation disabled until pipeline reset is implemented standstill_timeout: Duration::from_secs(10), empty_block_mc_lag_threshold: None, wait_for_db_init: false, @@ -604,14 +600,6 @@ impl SessionOptions { fail!("collation_retry_timeout must be > 0") } - // Precollation is temporarily disabled until pipeline reset triggering is implemented - // TODO: Remove this check when precollation pipeline reset is implemented - if self.max_precollated_blocks != 0 { - fail!( - "max_precollated_blocks must be 0 (precollation disabled until pipeline reset is implemented)" - ) - } - // collation_retry_max_attempts = 0 is valid (no retries) if self.health_alert_cooldown.is_zero() { diff --git a/src/node/simplex/src/session.rs b/src/node/simplex/src/session.rs index 3480a55..004789e 100644 --- a/src/node/simplex/src/session.rs +++ b/src/node/simplex/src/session.rs @@ -762,6 +762,10 @@ impl SessionImpl { let mut next_profiling_dump_time = next_metrics_dump_time; let mut next_health_check_time = next_metrics_dump_time; + // Arm FSM skip timeouts now that overlay warmup and bootstrap + // recovery are complete. Matches C++ Start event timing. + processor.start(); + loop { { session_activity_node.tick(); diff --git a/src/node/simplex/src/session_processor.rs b/src/node/simplex/src/session_processor.rs index 83a8b4c..45d7df7 100644 --- a/src/node/simplex/src/session_processor.rs +++ b/src/node/simplex/src/session_processor.rs @@ -315,6 +315,29 @@ struct PrecollatedBlock { /// Map of slot -> precollated block type PrecollatedBlockMap = HashMap; +/// Window-local chain head for candidate chaining (C++ block-producer.cpp parity). +/// +/// In C++, `generate_candidates()` carries mutable `parent` and `state` across slots +/// within a single leader window, so slot N+1 chains off slot N's locally generated +/// candidate without waiting for notarization. This struct tracks the same chain head +/// on the Rust side: after `generated_block()` completes for a slot, we record the +/// produced candidate's identity here so that `precollate_block()` for the next slot +/// in the same window can use it immediately as an explicit parent. +/// +/// Reset when the leader window changes or the progress cursor jumps. +#[derive(Clone, Debug)] +#[allow(dead_code)] +struct LocalChainHead { + /// Window this chain head belongs to + window: WindowIndex, + /// Slot of the last locally generated candidate + slot: SlotIndex, + /// Candidate parent info (slot + candidate-id hash) for the next slot + parent_info: crate::block::CandidateParentInfo, + /// Resolved BlockIdExt of the generated candidate (for seqno derivation and explicit parent hint) + block_id: BlockIdExt, +} + /* Collation result @@ -674,6 +697,16 @@ pub(crate) struct SessionProcessor { /// precollated block is consumed). Checked at the top of `check_collation`. /// Reference: C++ block-producer.cpp coro_sleep(target_time) earliest_collation_time: Option, + /// Window-local chain head for candidate chaining across slots in the same + /// leader window (C++ block-producer.cpp parity). Updated synchronously in + /// `generated_block()` so that `precollate_block()` can chain the next slot + /// without waiting for the async `on_candidate_received` self-loop. + local_chain_head: Option, + /// Synchronous cache of locally generated parent metadata, keyed by + /// `RawCandidateId`. Populated in `generated_block()` *before* the async + /// `on_candidate_received` self-loop, so `resolve_parent_block_id()` can + /// find the parent immediately for chained precollation. + generated_parent_cache: HashMap, /* Validation state @@ -1321,6 +1354,8 @@ impl SessionProcessor { precollated_blocks_next_request_id: 0, precollated_blocks_max_slot: None, earliest_collation_time: None, + local_chain_head: None, + generated_parent_cache: HashMap::new(), // Validation state pending_validations: HashMap::new(), pending_approve: HashSet::new(), @@ -2672,6 +2707,24 @@ impl SessionProcessor { Core consensus operations */ + /// Arm FSM timeouts and prepare the processor for the main loop. + /// + /// Must be called exactly once, after overlay warmup and bootstrap + /// recovery, right before the main loop begins. The FSM is created + /// with unarmed timeouts (`skip_timestamp = None`) so that no skip + /// cascade fires during the startup delay. + /// + /// Reference: C++ `Start` event -> `advance_present()` -> + /// `LeaderWindowObserved` -> `alarm_timestamp()`. + pub(crate) fn start(&mut self) { + self.simplex_state.set_timeouts(&self.description); + + log::info!( + "Session {} started: skip timeouts armed", + &self.session_id().to_hex_string()[..8], + ); + } + /// Check all pending operations /// /// Called periodically from main loop when awake time is reached. @@ -2794,7 +2847,13 @@ impl SessionProcessor { parent: &crate::block::CandidateParentInfo, ) -> Option { let parent_id = RawCandidateId { slot: parent.slot, hash: parent.hash.clone() }; - self.received_candidates.get(&parent_id).map(|c| c.block_id.clone()) + // Check synchronous generated-parent cache first (populated in generated_block() + // before the async on_candidate_received self-loop), then fall back to the + // received_candidates map which is populated asynchronously. + self.generated_parent_cache + .get(&parent_id) + .cloned() + .or_else(|| self.received_candidates.get(&parent_id).map(|c| c.block_id.clone())) } /// Advance `earliest_collation_time` by `target_rate` from now. @@ -2847,6 +2906,22 @@ impl SessionProcessor { return; } + // Invalidate the local chain head and cancel stale precollations when the + // leader window has changed since the last generation (C++ parity: the + // OurLeaderWindowStarted handler cancels the previous generation coroutine). + if let Some(ref head) = self.local_chain_head { + if head.window != current_window { + log::debug!( + "Session {} check_collation: leader window changed ({} -> {}), \ + resetting precollation pipeline", + &self.session_id().to_hex_string()[..8], + head.window, + current_window, + ); + self.reset_precollations(); + } + } + // Don't generate if already generated or pending for this slot if self.slot_is_generated(current_slot) || self.slot_is_pending_generate(current_slot) { return; @@ -3226,6 +3301,10 @@ impl SessionProcessor { self.collates_expire_counter.failure(); self.generated_block(slot, result); + + // C++ parity: after generating a candidate, start precollation for the next + // slot in the same leader window (block-producer.cpp `++slot; parent = id;`). + self.precollate_block(slot + 1); } else if slot > fsm_first_non_progressed_slot { // Store as precollated for future slot // Note: Empty blocks are not precollated - they are generated on-demand @@ -3668,6 +3747,7 @@ impl SessionProcessor { slot_window, current_window ); + self.invalidate_local_chain_head(); return; } @@ -3732,6 +3812,31 @@ impl SessionProcessor { self.persist_generated_candidate_info_to_db(slot, &prepared, &parent, is_empty); + // --- C++ candidate-chaining parity (block-producer.cpp `parent = id`) --- + // Synchronously seed the generated-parent cache and local chain head BEFORE + // the async on_candidate_received self-loop, so that precollate_block() for + // the next slot in the same window can resolve the parent immediately. + let candidate_parent_info = + crate::block::CandidateParentInfo { slot, hash: prepared.candidate_hash.clone() }; + let raw_id = RawCandidateId { slot, hash: prepared.candidate_hash.clone() }; + self.generated_parent_cache.insert(raw_id, prepared.block_id_ext.clone()); + + let slot_window = self.description.get_window_idx(slot); + self.local_chain_head = Some(LocalChainHead { + window: slot_window, + slot, + parent_info: candidate_parent_info, + block_id: prepared.block_id_ext.clone(), + }); + + log::trace!( + "Session {} generated_block: updated local_chain_head: window={}, slot={}, hash={}", + &self.session_id().to_hex_string()[..8], + slot_window, + slot, + &prepared.candidate_hash.to_hex_string()[..8], + ); + // Clone TL candidate data before broadcasting (needed for on_candidate_received) let tl_candidate_data_for_self = prepared.tl_candidate_data.clone(); @@ -4028,44 +4133,41 @@ impl SessionProcessor { } /* - Precollation Pipeline - Reference: validator-session/src/session_processor.rs precollation + Precollation Pipeline โ€” C++ Candidate Chaining Parity + Reference: C++ block-producer.cpp generate_candidates() loop - NOTE: Precollation is currently DISABLED (max_precollated_blocks=0) + The precollation pipeline chains candidates across slots within a single leader + window. After `generated_block()` completes slot N, it updates `local_chain_head` + and calls `precollate_block(N+1)`, which uses the local chain head as the explicit + parent instead of waiting for FSM `available_base` propagation via notarization. - TODO(precollation): Before enabling precollation, fix these issues: - 1. Implement precollation pipeline reset triggering. - See reset_precollations() for details on what needs to be implemented. - 2. NOTE: Round mapping now uses slot directly (round = slot.value()). - - This eliminates the "precollation round mismatch" issue since round is derived - from the slot being collated, not from a separate counter. - - With optimistic validation on notarized parents, precollation slot - advancement is driven by the FSM progress cursor. + Pipeline reset (`reset_precollations()`) is triggered by: + - Session stop + - Leader window changes (via `invalidate_local_chain_head()`) + - Progress cursor jumping past queued slots โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Precollation Pipeline โ”‚ โ”‚ โ”‚ - โ”‚ 1. precollate_block(slot): โ”‚ - โ”‚ โ”œโ”€โ”€ Check max_precollated_blocks limit โ”‚ - โ”‚ โ”œโ”€โ”€ If slot already in pipeline, advance to max_slot + 1 โ”‚ - โ”‚ โ””โ”€โ”€ Call invoke_collation(slot) โ”‚ + โ”‚ 1. check_collation() โ€” first slot in window: โ”‚ + โ”‚ โ”œโ”€โ”€ Use FSM available_base as parent โ”‚ + โ”‚ โ””โ”€โ”€ invoke_collation(slot, parent) โ”‚ โ”‚ โ”‚ - โ”‚ 2. invoke_collation(slot): โ”‚ - โ”‚ โ”œโ”€โ”€ Skip if already pending for slot โ”‚ - โ”‚ โ”œโ”€โ”€ Check priority (is leader for slot?) โ”‚ - โ”‚ โ”œโ”€โ”€ Update precollated_blocks_max_slot โ”‚ - โ”‚ โ”œโ”€โ”€ Create AsyncRequest and PrecollatedBlock entry โ”‚ - โ”‚ โ””โ”€โ”€ notify_generate_slot(source_info, request, callback) โ”‚ + โ”‚ 2. generated_block() โ€” after broadcast: โ”‚ + โ”‚ โ”œโ”€โ”€ Update local_chain_head + generated_parent_cache โ”‚ + โ”‚ โ””โ”€โ”€ precollate_block(slot + 1) โ€” chain next slot โ”‚ โ”‚ โ”‚ - โ”‚ 3. Collation callback (on_collation_complete): โ”‚ - โ”‚ โ”œโ”€โ”€ Store candidate in PrecollatedBlock โ”‚ - โ”‚ โ””โ”€โ”€ precollate_block(slot + 1) to keep pipeline full โ”‚ + โ”‚ 3. precollate_block(slot): โ”‚ + โ”‚ โ”œโ”€โ”€ Prefer local_chain_head for parent (same window) โ”‚ + โ”‚ โ”œโ”€โ”€ Fall back to FSM available_base โ”‚ + โ”‚ โ””โ”€โ”€ invoke_collation(slot, parent) โ”‚ โ”‚ โ”‚ โ”‚ 4. check_collation() finds precollated block: โ”‚ โ”‚ โ”œโ”€โ”€ Use precollated candidate directly โ”‚ โ”‚ โ””โ”€โ”€ Remove from pipeline, start next precollation โ”‚ โ”‚ โ”‚ - โ”‚ 5. Slot skip: reset_precollations() cancels all pending โ”‚ + โ”‚ 5. Window change / progress jump: โ”‚ + โ”‚ โ””โ”€โ”€ invalidate_local_chain_head() + reset_precollations() โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ */ @@ -4075,7 +4177,8 @@ impl SessionProcessor { /// Reference: validator-session/src/session_processor.rs precollate_block fn precollate_block(&mut self, slot: SlotIndex) { // Check max precollated blocks limit - let max_precollated = self.description.opts().max_precollated_blocks as usize; + let max_precollated = + self.description.opts().slots_per_leader_window.saturating_sub(1) as usize; if self.precollated_blocks.len() >= max_precollated { log::trace!( "Session {} precollate_block: max precollated blocks limit {} reached", @@ -4097,19 +4200,69 @@ impl SessionProcessor { } } - // Precollation should only start when the parent is available (genesis or resolved). - // Note: `get_available_parent()` returns None for both genesis and "base unknown", - // so use `has_available_parent()` to disambiguate. - if !self.simplex_state.has_available_parent(&self.description, target_slot) { + // Must still be in the same leader window as the current window + let target_window = self.description.get_window_idx(target_slot); + let current_window = self.simplex_state.get_current_leader_window_idx(); + if target_window != current_window { + log::trace!( + "Session {} precollate_block: slot {} is in window {} (current={}), skipping", + self.session_id().to_hex_string(), + target_slot, + target_window, + current_window + ); + return; + } + + // Must be leader for the target slot + let self_idx = self.description.get_self_idx(); + let leader = self.description.get_leader(target_slot); + if leader != self_idx { log::trace!( - "Session {} precollate_block: parent is not available for slot {}", + "Session {} precollate_block: not leader for slot {} (leader={})", self.session_id().to_hex_string(), - target_slot + target_slot, + leader ); return; } - let parent = self.simplex_state.get_available_parent(&self.description, target_slot); + // C++ parity: prefer local chain head for parent when the previous slot in + // the same window was just generated locally (block-producer.cpp `parent = id`). + // Fall back to FSM available_base for the first slot in a window or if the + // local chain head is stale. + let parent = if let Some(ref head) = self.local_chain_head { + if head.window == target_window && head.slot + 1 == target_slot { + log::trace!( + "Session {} precollate_block: using local_chain_head for slot {} \ + (parent=s{}:{})", + &self.session_id().to_hex_string()[..8], + target_slot, + head.parent_info.slot, + &head.parent_info.hash.to_hex_string()[..8], + ); + Some(head.parent_info.clone()) + } else { + None + } + } else { + None + }; + + let parent = if parent.is_some() { + parent + } else { + // Fall back to FSM available_base + if !self.simplex_state.has_available_parent(&self.description, target_slot) { + log::trace!( + "Session {} precollate_block: parent is not available for slot {}", + self.session_id().to_hex_string(), + target_slot + ); + return; + } + self.simplex_state.get_available_parent(&self.description, target_slot) + }; if let Some(ref parent_info) = parent { if self.resolve_parent_block_id(parent_info).is_none() { @@ -4137,10 +4290,16 @@ impl SessionProcessor { } } - /// Reset all precollations (on slot skip or session stop) + /// Reset all precollations and invalidate the local chain head. + /// + /// Called when the precollation pipeline must be flushed: + /// - Session stop + /// - Leader window change (progress cursor moved to a new window) + /// - Progress cursor jumped past queued precollation slots fn reset_precollations(&mut self) { log::debug!( - "Session {} reset_precollations: cancelling {} pending precollations", + "Session {} reset_precollations: cancelling {} pending precollations, \ + clearing local_chain_head", self.session_id().to_hex_string(), self.precollated_blocks.len() ); @@ -4152,6 +4311,26 @@ impl SessionProcessor { self.precollated_blocks.clear(); self.precollated_blocks_max_slot = None; + self.invalidate_local_chain_head(); + } + + /// Invalidate the window-local chain head. + /// + /// Called when the leader window changes, the progress cursor jumps, or a + /// consensus event (skip/notarize) invalidates the locally generated parent + /// chain. After invalidation, the next collation will start fresh from the + /// FSM `available_base`. + fn invalidate_local_chain_head(&mut self) { + if let Some(ref head) = self.local_chain_head { + log::trace!( + "Session {} invalidate_local_chain_head: clearing (was window={}, slot={})", + &self.session_id().to_hex_string()[..8], + head.window, + head.slot, + ); + } + self.local_chain_head = None; + self.generated_parent_cache.clear(); } /* diff --git a/src/node/simplex/src/simplex_state.rs b/src/node/simplex/src/simplex_state.rs index 72c768c..9cc05a3 100644 --- a/src/node/simplex/src/simplex_state.rs +++ b/src/node/simplex/src/simplex_state.rs @@ -1493,9 +1493,11 @@ impl SimplexState { // (available_base is optional-of-optional; RawParentId{} = nullopt = genesis) window.slots[0].available_base = Some(None); - // Set initial timeouts - // Reference: C++ start_up() โ†’ set_timeouts(window) - state.set_timeouts(desc); + // Timeouts are NOT armed here. The FSM starts with skip_timestamp=None + // so that no skip cascade fires before the session is actually started. + // SessionProcessor::start() calls set_timeouts() at the correct moment + // (after overlay warmup and bootstrap recovery), matching C++ where + // timeouts are only armed after the Start event. Ok(state) } @@ -1911,6 +1913,11 @@ impl SimplexState { /// /// Used in unit tests to satisfy SessionProcessor assertions when /// injecting events without running full FSM vote accumulation. + #[cfg(test)] + pub fn try_skip_window_for_test(&mut self, window_idx: WindowIndex) { + self.try_skip_window(window_idx); + } + #[cfg(test)] pub fn set_first_non_finalized_slot_for_test(&mut self, slot: SlotIndex) { self.first_non_finalized_slot = slot; @@ -2187,7 +2194,7 @@ impl SimplexState { /// for i โˆˆ windowSlots(s) do // set timeouts for all slots /// schedule event Timeout(i) at time clock()+ฮ”timeout+(iโˆ’s+1)ยทฮ”block /// ``` - fn set_timeouts(&mut self, desc: &SessionDescription) { + pub(crate) fn set_timeouts(&mut self, desc: &SessionDescription) { let window_start = self.current_leader_window_idx * self.slots_per_leader_window; self.skip_slot = window_start; @@ -4741,7 +4748,13 @@ impl SimplexState { window.slots[offset].is_voted = true; window.slots[offset].voted_skip = true; window.slots[offset].is_bad_window = true; - window.slots[offset].pending_block = None; + // C++ alarm() only sets voted_skip โ€” it does NOT clear pending_block. + // The async try_notarize() coroutine can still complete after a skip + // vote, producing both Skip and Notar votes for the same slot. + // Only clear pending_block in Alpenglow mode (strict Voted gate). + if enable_fallback { + window.slots[offset].pending_block = None; + } } } } diff --git a/src/node/simplex/src/tests/test_session_processor.rs b/src/node/simplex/src/tests/test_session_processor.rs index 01d0829..5f799b5 100644 --- a/src/node/simplex/src/tests/test_session_processor.rs +++ b/src/node/simplex/src/tests/test_session_processor.rs @@ -84,26 +84,12 @@ fn create_test_validators(count: u32) -> Vec { } /// Create test SessionDescription with default options +#[allow(dead_code)] fn create_test_desc( nodes: &[SessionNode], local_idx: usize, ) -> Arc { - let local_key = nodes[local_idx].public_key.clone(); - let shard = ShardIdent::masterchain(); - let opts = SessionOptions::default(); - Arc::new( - crate::session_description::SessionDescription::new( - &opts, - SessionId::default(), - 1, // initial_block_seqno - nodes, - local_key, - &shard, - SystemTime::now(), - None, - ) - .unwrap(), - ) + create_test_desc_with_opts(nodes, local_idx, &SessionOptions::default()) } // ============================================================================ @@ -519,11 +505,39 @@ struct TestFixture { task_queue: Arc, } +/// Create test SessionDescription with custom options +fn create_test_desc_with_opts( + nodes: &[SessionNode], + local_idx: usize, + opts: &SessionOptions, +) -> Arc { + let local_key = nodes[local_idx].public_key.clone(); + let shard = ShardIdent::masterchain(); + Arc::new( + crate::session_description::SessionDescription::new( + opts, + SessionId::default(), + 1, // initial_block_seqno + nodes, + local_key, + &shard, + SystemTime::now(), + None, // metrics + ) + .unwrap(), + ) +} + impl TestFixture { /// Create a test fixture with N validators (local is validator 0) fn new(validator_count: u32) -> Self { + Self::new_with_opts(validator_count, SessionOptions::default()) + } + + /// Create a test fixture with N validators and custom session options + fn new_with_opts(validator_count: u32, opts: SessionOptions) -> Self { let nodes = create_test_validators(validator_count); - let description = create_test_desc(&nodes, 0); + let description = create_test_desc_with_opts(&nodes, 0, &opts); let listener: Arc = Arc::new(MockListener); @@ -3053,3 +3067,129 @@ fn test_candidate_decision_ok_does_not_drop_when_cand_seqno_greater_than_committ "pending_validations entry must be consumed" ); } + +// ============================================================================ +// Candidate Chaining Tests (C++ parity) +// ============================================================================ + +/// Test that local_chain_head and generated_parent_cache start empty. +#[test] +fn test_local_chain_head_initial_state() { + let fixture = TestFixture::new(4); + assert!(fixture.processor.local_chain_head.is_none()); + assert!(fixture.processor.generated_parent_cache.is_empty()); +} + +/// Test that invalidate_local_chain_head clears both the chain head and cache. +#[test] +fn test_invalidate_local_chain_head_clears_state() { + let mut fixture = TestFixture::new(4); + + let hash = UInt256::from([0xAA; 32]); + let block_id = BlockIdExt::with_params( + ShardIdent::masterchain(), + 1, + UInt256::from([0xBB; 32]), + UInt256::from([0xCC; 32]), + ); + let parent_info = + crate::block::CandidateParentInfo { slot: SlotIndex::new(0), hash: hash.clone() }; + let raw_id = RawCandidateId { slot: SlotIndex::new(0), hash: hash.clone() }; + + fixture.processor.local_chain_head = Some(LocalChainHead { + window: WindowIndex::new(0), + slot: SlotIndex::new(0), + parent_info, + block_id: block_id.clone(), + }); + fixture.processor.generated_parent_cache.insert(raw_id.clone(), block_id); + + assert!(fixture.processor.local_chain_head.is_some()); + assert!(!fixture.processor.generated_parent_cache.is_empty()); + + fixture.processor.invalidate_local_chain_head(); + + assert!(fixture.processor.local_chain_head.is_none()); + assert!(fixture.processor.generated_parent_cache.is_empty()); +} + +/// Test that resolve_parent_block_id finds parents in generated_parent_cache +/// even before the async on_candidate_received self-loop populates received_candidates. +#[test] +fn test_resolve_parent_from_generated_cache() { + let mut fixture = TestFixture::new(4); + + let hash = UInt256::from([0xDD; 32]); + let block_id = BlockIdExt::with_params( + ShardIdent::masterchain(), + 1, + UInt256::from([0xEE; 32]), + UInt256::from([0xFF; 32]), + ); + let parent_info = + crate::block::CandidateParentInfo { slot: SlotIndex::new(5), hash: hash.clone() }; + let raw_id = RawCandidateId { slot: SlotIndex::new(5), hash: hash.clone() }; + + // Not in received_candidates yet + assert!(fixture.processor.resolve_parent_block_id(&parent_info).is_none()); + + // Seed the generated_parent_cache (as generated_block would) + fixture.processor.generated_parent_cache.insert(raw_id, block_id.clone()); + + // Now resolvable + let resolved = fixture.processor.resolve_parent_block_id(&parent_info); + assert_eq!(resolved, Some(block_id)); +} + +/// Test that reset_precollations clears the local chain head. +#[test] +fn test_reset_precollations_clears_chain_head() { + let mut fixture = TestFixture::new(4); + + let hash = UInt256::from([0x11; 32]); + let block_id = BlockIdExt::with_params( + ShardIdent::masterchain(), + 1, + UInt256::from([0x22; 32]), + UInt256::from([0x33; 32]), + ); + let parent_info = + crate::block::CandidateParentInfo { slot: SlotIndex::new(0), hash: hash.clone() }; + + fixture.processor.local_chain_head = Some(LocalChainHead { + window: WindowIndex::new(0), + slot: SlotIndex::new(0), + parent_info, + block_id, + }); + + fixture.processor.reset_precollations(); + + assert!(fixture.processor.local_chain_head.is_none()); + assert!(fixture.processor.generated_parent_cache.is_empty()); +} + +/// Test that multi-slot window options produce correct precollation depth. +#[test] +fn test_slots_per_leader_window_precollation_depth() { + // Single-slot window: no precollation + let opts1 = SessionOptions { slots_per_leader_window: 1, ..Default::default() }; + assert_eq!(opts1.slots_per_leader_window.saturating_sub(1), 0); + + // 4-slot window: up to 3 precollated + let opts4 = SessionOptions { slots_per_leader_window: 4, ..Default::default() }; + assert_eq!(opts4.slots_per_leader_window.saturating_sub(1), 3); + + // 8-slot window: up to 7 precollated + let opts8 = SessionOptions { slots_per_leader_window: 8, ..Default::default() }; + assert_eq!(opts8.slots_per_leader_window.saturating_sub(1), 7); +} + +/// Test that creating a SessionProcessor with multi-slot window succeeds. +#[test] +fn test_multi_slot_window_session_creation() { + let opts = SessionOptions { slots_per_leader_window: 4, ..Default::default() }; + let fixture = TestFixture::new_with_opts(4, opts); + assert_eq!(fixture.description.opts().slots_per_leader_window, 4); + assert!(fixture.processor.local_chain_head.is_none()); +} diff --git a/src/node/simplex/src/tests/test_simplex_state.rs b/src/node/simplex/src/tests/test_simplex_state.rs index 1dea13b..3ac3276 100644 --- a/src/node/simplex/src/tests/test_simplex_state.rs +++ b/src/node/simplex/src/tests/test_simplex_state.rs @@ -194,7 +194,8 @@ fn test_new_creates_initial_state() { assert_eq!(state.first_non_finalized_slot, SlotIndex::new(0)); assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); assert!(!state.has_pending_events()); - assert!(state.get_next_timeout().is_some()); + // Timeouts start unarmed; SessionProcessor::start() calls set_timeouts(). + assert!(state.get_next_timeout().is_none()); // Window 0 should have None (genesis) as available base assert!(state.get_window(WindowIndex::new(0)).unwrap().available_bases.contains(&None)); @@ -1639,11 +1640,133 @@ fn test_timeout_management() { let desc = create_test_desc(4, 2); let state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - // Should have initial timeout - assert!(state.get_next_timeout().is_some()); + // FSM is created with unarmed timeouts (skip_timestamp = None). + // SessionProcessor::start() is responsible for calling set_timeouts(). + assert!(state.get_next_timeout().is_none(), "FSM must start with unarmed timeouts"); assert_eq!(state.skip_slot, SlotIndex::new(0)); } +#[test] +fn test_unarmed_fsm_no_skip_cascade_after_delay() { + // Regression: the FSM must NOT fire skip votes when check_all() runs + // with unarmed timeouts, even after an arbitrary delay. + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); + + // Simulate 60 s overlay warmup delay + let future = desc.get_time() + std::time::Duration::from_secs(60); + desc.set_time(future); + + state.check_all(&desc); + + let mut skip_count = 0; + while let Some(event) = state.pull_event() { + if matches!(event, SimplexEvent::BroadcastVote(Vote::Skip(_))) { + skip_count += 1; + } + } + assert_eq!(skip_count, 0, "unarmed FSM must produce NO skip votes regardless of clock delay"); +} + +#[test] +fn test_set_timeouts_enables_skip_after_expiry() { + // After set_timeouts() the skip timer fires once the deadline elapses. + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); + + let t0 = desc.get_time(); + + // Arm at t0 + state.set_timeouts(&desc); + assert!(state.get_next_timeout().is_some(), "set_timeouts must set skip_timestamp"); + + // Immediately after arming, check_all should produce no skips + state.check_all(&desc); + let mut skip_count = 0; + while let Some(event) = state.pull_event() { + if matches!(event, SimplexEvent::BroadcastVote(Vote::Skip(_))) { + skip_count += 1; + } + } + assert_eq!(skip_count, 0, "no skip votes before timeout expires"); + + // Advance past first_block_timeout + target_rate (defaults: 3s + 1s = 4s) + desc.set_time(t0 + std::time::Duration::from_secs(5)); + state.check_all(&desc); + + let mut skip_count = 0; + while let Some(event) = state.pull_event() { + if matches!(event, SimplexEvent::BroadcastVote(Vote::Skip(_))) { + skip_count += 1; + } + } + assert!(skip_count > 0, "skip votes must fire after timeout expires"); +} + +#[test] +fn test_try_skip_window_preserves_pending_block_cpp_mode() { + // In C++ mode, alarm() only sets voted_skip=true and does NOT clear + // pending_block. The async try_notarize() coroutine can still complete + // after a skip vote, producing both Skip and Notar for the same slot. + let desc = create_test_desc(4, 2); + let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); + + // Store a candidate as pending at slot 0 + let hash = UInt256::from([1u8; 32]); + let block_id = BlockIdExt::default(); + let candidate = create_test_candidate(0, hash.clone(), block_id, None, 0); + let _ = state.on_candidate(&desc, candidate); + // Drain events from on_candidate + while state.pull_event().is_some() {} + + // Verify pending_block is set + let pending_before = state + .get_window(WindowIndex::new(0)) + .and_then(|w| w.slots[0].pending_block.as_ref()) + .is_some(); + + // Fire skip for the entire window (simulates timeout -> try_skip_window) + state.try_skip_window_for_test(WindowIndex::new(0)); + + // voted_skip must be set + let voted_skip = + state.get_window(WindowIndex::new(0)).map(|w| w.slots[0].voted_skip).unwrap_or(false); + assert!(voted_skip, "slot 0 must have voted_skip after try_skip_window"); + + // In C++ mode, pending_block must be PRESERVED (not cleared) + let pending_after = state + .get_window(WindowIndex::new(0)) + .and_then(|w| w.slots[0].pending_block.as_ref()) + .is_some(); + assert_eq!( + pending_before, pending_after, + "C++ mode: pending_block must be preserved after skip (was={}, now={})", + pending_before, pending_after + ); +} + +#[test] +fn test_try_skip_window_clears_pending_block_alpenglow_mode() { + // Alpenglow mode: pendingBlocks[k] <- bottom after skip + let desc = create_test_desc(4, 2); + let mut state = + SimplexState::new(&desc, opts_alpenglow()).expect("Failed to create SimplexState"); + + let hash = UInt256::from([1u8; 32]); + let block_id = BlockIdExt::default(); + let candidate = create_test_candidate(0, hash.clone(), block_id, None, 0); + let _ = state.on_candidate(&desc, candidate); + while state.pull_event().is_some() {} + + state.try_skip_window_for_test(WindowIndex::new(0)); + + let pending_after = state + .get_window(WindowIndex::new(0)) + .and_then(|w| w.slots[0].pending_block.as_ref()) + .is_some(); + assert!(!pending_after, "Alpenglow mode: pending_block must be cleared after skip"); +} + /* ======================================================================== Available Parent Tests diff --git a/src/node/simplex/tests/test_consensus.rs b/src/node/simplex/tests/test_consensus.rs index 8314bab..aff2eb5 100644 --- a/src/node/simplex/tests/test_consensus.rs +++ b/src/node/simplex/tests/test_consensus.rs @@ -176,6 +176,10 @@ struct TestConfig { /// Restart tests benefit from a shorter interval so recovered nodes /// receive cached certificates faster than the skip timeout. standstill_timeout: Option, + /// Override slots_per_leader_window (default: 1). + /// Set > 1 to test candidate chaining within a leader window. + /// Precollation depth is derived automatically as (window - 1). + slots_per_leader_window: Option, } /// Network gremlin configuration (net-gremlin). @@ -233,6 +237,7 @@ impl Default for TestConfig { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, } } } @@ -1232,11 +1237,12 @@ where let session_id: UInt256 = UInt256::from(rng.gen::<[u8; 32]>()); // Session options + let slots_per_window = config.slots_per_leader_window.unwrap_or(1); let mut session_opts = SessionOptions { proto_version: 0, target_rate: config.target_rate, first_block_timeout: config.first_block_timeout, - slots_per_leader_window: 1, + slots_per_leader_window: slots_per_window, empty_block_mc_lag_threshold: if config.shard.is_masterchain() { None // MC uses internal finalization tracking } else { @@ -1993,6 +1999,7 @@ fn test_simplex_consensus_basic() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2048,6 +2055,7 @@ fn test_simplex_consensus_with_failures() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2117,6 +2125,7 @@ fn test_simplex_consensus_finalcert_recovery() { }), lossy_overlay_node_indices: Some(vec![0]), standstill_timeout: None, + slots_per_leader_window: None, }, |instances| { let config = &instances[0].lock().config.clone(); @@ -2190,6 +2199,7 @@ fn test_simplex_consensus_shard_with_mc_notifications() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2246,6 +2256,7 @@ fn test_simplex_consensus_adnl_overlay() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2306,6 +2317,7 @@ fn test_simplex_consensus_adnl_net_gremlin() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement (best-effort under partitions). @@ -2370,6 +2382,7 @@ fn test_simplex_consensus_restart_gremlin() { // 1s rebroadcast cadence can flood restart-gremlin runs (large [begin,end) ranges), // causing receiver queues to explode and the test to stall intermittently. standstill_timeout: Some(Duration::from_secs(5)), + slots_per_leader_window: None, }, |instances| { let config = &instances[0].lock().config.clone(); @@ -2550,6 +2563,7 @@ fn test_simplex_start_gate() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, + slots_per_leader_window: None, }; for i in 0..node_count { @@ -2661,6 +2675,69 @@ fn test_simplex_start_gate() { log::info!("[start_gate] Test passed"); } +/// Test candidate chaining within a multi-slot leader window (C++ parity). +/// +/// Configures `slots_per_leader_window = 4`. Precollation depth is derived +/// automatically from `slots_per_leader_window`, so the leader can chain +/// candidates across slots within a single window. With +/// chaining, the leader generates slot N+1 with slot N's candidate as parent, +/// which causes seqnos to increment (1, 2, 3, 4) instead of repeating seqno=1. +/// +/// Acceptance: the test reaches the commit threshold (at least 30% of rounds +/// committed), proving that chained candidates are notarized and finalized. +#[test] +fn test_simplex_consensus_candidate_chaining() { + run_simplex_consensus_test( + TestConfig { + total_rounds: 20, + min_commit_percent: 0.3, + node_count: 4, + generation_failure_probability: 0.0, + candidate_rejection_probability: 0.0, + max_collations: 2000, + target_rate: Duration::from_millis(300), + first_block_timeout: Duration::from_millis(3000), + test_name: "simplex_candidate_chaining".to_string(), + test_timeout: Duration::from_secs(120), + expect_timeout: false, + shard: ShardIdent::masterchain(), + mc_notification_interval: None, + overlay_type: OverlayType::InProcess, + net_gremlin: None, + restart_gremlin: None, + lossy_overlay: None, + lossy_overlay_node_indices: None, + standstill_timeout: None, + slots_per_leader_window: Some(4), + }, + |instances| { + let config = &instances[0].lock().config.clone(); + let min_commits = (config.total_rounds as f64 * config.min_commit_percent) as u32; + + for (idx, instance) in instances.iter().enumerate() { + let commits = instance.lock().on_block_committed_count(); + log::info!( + "[chaining] Instance {}: {} commits out of {} total_rounds (min required: {})", + idx, + commits, + config.total_rounds, + min_commits + ); + assert!( + commits >= min_commits, + "Instance {} has {} commits but requires at least {} ({}% of {} rounds). \ + Candidate chaining may not be working correctly.", + idx, + commits, + min_commits, + config.min_commit_percent * 100.0, + config.total_rounds + ); + } + }, + ); +} + /// Test that empty collated_data produces a valid (non-default) hash #[test] fn test_empty_collated_data_hash() { diff --git a/src/node/src/engine.rs b/src/node/src/engine.rs index 9bb2c63..e517d3d 100644 --- a/src/node/src/engine.rs +++ b/src/node/src/engine.rs @@ -2568,7 +2568,7 @@ pub fn init_prometheus_recorder( // -- validator metrics::describe_gauge!( "ton_node_validator_status", - "Validation state (0=Disabled, 1=Waiting, 2=Countdown, 3=Active)" + "Validation state (0=Disabled, 1=Waiting, 2=Active)" ); metrics::describe_gauge!( "ton_node_validator_in_current_set", diff --git a/src/node/src/network/custom_overlay_client.rs b/src/node/src/network/custom_overlay_client.rs index 77cd4b8..907c7bb 100644 --- a/src/node/src/network/custom_overlay_client.rs +++ b/src/node/src/network/custom_overlay_client.rs @@ -128,11 +128,9 @@ impl CustomOverlayClient { let Some(key) = key else { return Ok(false); }; - if let Err(e) = self.overlay_node.add_private_overlay( - OverlayParams::with_id_only(&self.id), - &key, - &peers, - ) { + let params = + OverlayParams { flags: 0, hops: None, overlay_id: &self.id, runtime: None }; + if let Err(e) = self.overlay_node.add_private_overlay(params, &key, &peers) { attempt += 1; if attempt >= 10 { fail!("Error while adding custom overlay \"{}\": {}", self.config.name, e); diff --git a/src/node/src/tests/test_control.rs b/src/node/src/tests/test_control.rs index 32dd515..83e2bce 100644 --- a/src/node/src/tests/test_control.rs +++ b/src/node/src/tests/test_control.rs @@ -638,8 +638,7 @@ fn test_convert_for_stats() { assert_eq!("Disabled", &format!("{:?}", ValidationStatus::from_u8(5))); assert_eq!("Disabled", &format!("{:?}", ValidationStatus::from_u8(0))); assert_eq!("Waiting", &format!("{:?}", ValidationStatus::from_u8(1))); - assert_eq!("Countdown", &format!("{:?}", ValidationStatus::from_u8(2))); - assert_eq!("Active", &format!("{:?}", ValidationStatus::from_u8(3))); + assert_eq!("Active", &format!("{:?}", ValidationStatus::from_u8(2))); let shard_id = ShardIdent::with_tagged_prefix(15, 0xABCD_0000_0000_0000u64).unwrap(); let root_hash = diff --git a/src/node/src/validator/validator_group.rs b/src/node/src/validator/validator_group.rs index 2d97c6e..ed8f10c 100644 --- a/src/node/src/validator/validator_group.rs +++ b/src/node/src/validator/validator_group.rs @@ -134,7 +134,6 @@ fn should_reject_stale_mc_candidate( pub enum ValidatorGroupStatus { Created, EngineCreated, - Countdown { start_at: tokio::time::Instant }, Sync, Active, Stopping, @@ -146,10 +145,6 @@ impl Display for ValidatorGroupStatus { match self { ValidatorGroupStatus::Created => write!(f, "created"), ValidatorGroupStatus::EngineCreated => write!(f, "engine_created"), - ValidatorGroupStatus::Countdown { start_at: at } => { - let now = tokio::time::Instant::now(); - write!(f, "cntdwn {}", at.saturating_duration_since(now).as_secs()) - } ValidatorGroupStatus::Sync => write!(f, "sync"), ValidatorGroupStatus::Active => write!(f, "active"), ValidatorGroupStatus::Stopping => write!(f, "stopping"), @@ -163,7 +158,6 @@ impl ValidatorGroupStatus { match self { Self::Created => "created", Self::EngineCreated => "engine_created", - Self::Countdown { .. } => "countdown", Self::Sync => "sync", Self::Active => "active", Self::Stopping => "stopping", @@ -174,12 +168,7 @@ impl ValidatorGroupStatus { impl ValidatorGroupStatus { pub fn before(&self, of: &ValidatorGroupStatus) -> bool { - match (&self, of) { - (ValidatorGroupStatus::Countdown { .. }, ValidatorGroupStatus::Countdown { .. }) => { - false - } - _ => self <= of, - } + self <= of } } @@ -388,7 +377,7 @@ impl ValidatorGroupImpl { Ok(()) } - fn prepare_start(&mut self, validation_start_status: ValidatorGroupStatus) -> bool { + fn prepare_start(&mut self) -> bool { if self.start_pending { return false; } @@ -396,9 +385,6 @@ impl ValidatorGroupImpl { ValidatorGroupStatus::Created | ValidatorGroupStatus::EngineCreated => {} _ => return false, } - if let ValidatorGroupStatus::Countdown { .. } = validation_start_status { - self.status = validation_start_status; - } self.start_pending = true; true } @@ -820,20 +806,14 @@ impl ValidatorGroup { } #[allow(clippy::too_many_arguments)] - pub async fn start_with_status( + pub async fn start_session( self: Arc, - validation_start_status: ValidatorGroupStatus, prev: Vec, min_masterchain_block_id: BlockIdExt, min_ts: SystemTime, rt: tokio::runtime::Handle, ) -> Result<()> { rt.clone().spawn(async move { - if let ValidatorGroupStatus::Countdown { start_at } = validation_start_status { - log::trace!(target: "validator", "Session delay started: {}", self.info().await); - tokio::time::sleep_until(start_at).await; - } - let callback = self.make_validator_session_callback(); self.group_impl .execute_sync(|group_impl| { @@ -857,7 +837,7 @@ impl ValidatorGroup { } } else { group_impl.start_pending = false; - log::trace!(target: "validator", "Session deleted before countdown: {}", group_impl.info()); + log::trace!(target: "validator", "Session deleted before start: {}", group_impl.info()); } }) .await; @@ -930,13 +910,8 @@ impl ValidatorGroup { self.group_impl.execute_sync(|group_impl| group_impl.start_pending).await } - pub async fn try_prepare_start( - &self, - validation_start_status: ValidatorGroupStatus, - ) -> Result { - self.group_impl - .execute_sync(|group_impl| Ok(group_impl.prepare_start(validation_start_status))) - .await + pub async fn try_prepare_start(&self) -> Result { + self.group_impl.execute_sync(|group_impl| Ok(group_impl.prepare_start())).await } pub async fn set_status(&self, status: ValidatorGroupStatus) -> Result<()> { @@ -1147,7 +1122,9 @@ impl ValidatorGroup { ConsensusOptions::Catchain(opts) => { opts.accelerated_consensus_max_precollated_blocks as usize } - ConsensusOptions::Simplex(opts) => opts.max_precollated_blocks as usize, + ConsensusOptions::Simplex(opts) => { + opts.slots_per_leader_window.saturating_sub(1) as usize + } }; let request_clone = request.clone(); let cc_seqno = self.general_session_info.catchain_seqno; @@ -1959,18 +1936,17 @@ mod tests { fn test_prepare_start_immediate_keeps_created_and_marks_pending() { let mut group = make_group_impl_for_start_tests(); - assert!(group.prepare_start(ValidatorGroupStatus::Sync)); + assert!(group.prepare_start()); assert!(group.status == ValidatorGroupStatus::Created); assert!(group.start_pending); } #[test] - fn test_prepare_start_countdown_marks_pending_and_countdown() { + fn test_prepare_start_keeps_created_status() { let mut group = make_group_impl_for_start_tests(); - let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; - assert!(group.prepare_start(countdown)); - assert!(matches!(group.status, ValidatorGroupStatus::Countdown { .. })); + assert!(group.prepare_start()); + assert!(group.status == ValidatorGroupStatus::Created); assert!(group.start_pending); } @@ -1978,8 +1954,8 @@ mod tests { fn test_prepare_start_rejects_duplicate_pending_start() { let mut group = make_group_impl_for_start_tests(); - assert!(group.prepare_start(ValidatorGroupStatus::Sync)); - assert!(!group.prepare_start(ValidatorGroupStatus::Sync)); + assert!(group.prepare_start()); + assert!(!group.prepare_start()); assert!(group.status == ValidatorGroupStatus::Created); assert!(group.start_pending); } @@ -1987,9 +1963,8 @@ mod tests { #[test] fn test_reset_after_start_failure_restores_retryable_state() { let mut group = make_group_impl_for_start_tests(); - let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; - assert!(group.prepare_start(countdown)); + assert!(group.prepare_start()); group.reset_after_start_failure(); assert!(group.status == ValidatorGroupStatus::Created); @@ -2004,7 +1979,6 @@ mod tests { let states = [ ValidatorGroupStatus::Created, ValidatorGroupStatus::EngineCreated, - ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }, ValidatorGroupStatus::Sync, ValidatorGroupStatus::Active, ValidatorGroupStatus::Stopping, @@ -2042,25 +2016,15 @@ mod tests { } #[test] - fn test_before_rejects_countdown_to_countdown() { - let c1 = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; - let c2 = ValidatorGroupStatus::Countdown { - start_at: tokio::time::Instant::now() + Duration::from_secs(10), - }; - assert!(!c1.before(&c2)); - assert!(!c2.before(&c1)); - } - - #[test] - fn test_engine_created_state_between_created_and_countdown() { + fn test_engine_created_state_between_created_and_sync() { let created = ValidatorGroupStatus::Created; let engine_created = ValidatorGroupStatus::EngineCreated; - let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; + let sync = ValidatorGroupStatus::Sync; assert!(created < engine_created); - assert!(engine_created < countdown); + assert!(engine_created < sync); assert!(created.before(&engine_created)); - assert!(engine_created.before(&countdown)); + assert!(engine_created.before(&sync)); } #[test] @@ -2068,9 +2032,8 @@ mod tests { let mut group = make_group_impl_for_start_tests(); group.status = ValidatorGroupStatus::EngineCreated; - let countdown = ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }; - assert!(group.prepare_start(countdown)); - assert!(matches!(group.status, ValidatorGroupStatus::Countdown { .. })); + assert!(group.prepare_start()); + assert!(group.status == ValidatorGroupStatus::EngineCreated); assert!(group.start_pending); } @@ -2084,11 +2047,7 @@ mod tests { ] { let mut group = make_group_impl_for_start_tests(); group.status = status; - assert!( - !group.prepare_start(ValidatorGroupStatus::Sync), - "prepare_start should reject status {}", - status - ); + assert!(!group.prepare_start(), "prepare_start should reject status {}", status); } } @@ -2172,10 +2131,6 @@ mod tests { let states = vec![ (ValidatorGroupStatus::Created, "created"), (ValidatorGroupStatus::EngineCreated, "engine_created"), - ( - ValidatorGroupStatus::Countdown { start_at: tokio::time::Instant::now() }, - "countdown", - ), (ValidatorGroupStatus::Sync, "sync"), (ValidatorGroupStatus::Active, "active"), (ValidatorGroupStatus::Stopping, "stopping"), diff --git a/src/node/src/validator/validator_manager.rs b/src/node/src/validator/validator_manager.rs index 5bc5354..ca61cfe 100644 --- a/src/node/src/validator/validator_manager.rs +++ b/src/node/src/validator/validator_manager.rs @@ -30,10 +30,9 @@ use crate::{ }, }; use std::{ - cmp::{max, min}, + cmp::max, collections::{HashMap, HashSet}, convert::TryFrom, - fs, ops::RangeInclusive, sync::{atomic::Ordering, Arc}, time::{Duration, SystemTime}, @@ -80,6 +79,16 @@ fn format_duration_short(d: Duration) -> String { } } +fn validation_state_phase_label(status: ValidatorGroupStatus) -> &'static str { + match status { + ValidatorGroupStatus::Created | ValidatorGroupStatus::EngineCreated => "pre-start", + ValidatorGroupStatus::Sync => "pre-commit", + ValidatorGroupStatus::Active => "post-commit", + ValidatorGroupStatus::Stopping => "stopping", + ValidatorGroupStatus::Stopped => "stopped", + } +} + /// Magic suffix appended to session-ID serialization when accelerated consensus is enabled. /// /// **Rust-specific extension**: the C++ reference (`get_validator_set_id()` in `manager.cpp`) @@ -492,49 +501,25 @@ fn get_session_options( result } -async fn clear_catchains_cache(path_str: String) -> Result<()> { - log::info!(target: "validator_manager", "Clearing catchains cache..."); - let removed = tokio::task::spawn_blocking(move || -> Result { - let entries = fs::read_dir(path_str)?; - let mut removed = 0; - for entry in entries { - let path = entry?.path(); - if path.is_dir() { - if let Err(err) = fs::remove_dir_all(path.clone()) { - log::warn!("Error clearing catchains cache {}: {}", path.display(), err); - } else { - removed += 1; - } - } - } - Ok(removed) - }) - .await??; - log::info!(target: "validator_manager", "Cleared catchains cache, removed {} entries", removed); - Ok(()) -} - #[repr(u8)] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] pub enum ValidationStatus { Disabled = 0, Waiting = 1, - Countdown = 2, - Active = 3, + Active = 2, } impl ValidationStatus { fn allows_validate(&self) -> bool { match self { Self::Disabled | Self::Waiting => false, - Self::Countdown | Self::Active => true, + Self::Active => true, } } pub fn from_u8(value: u8) -> Self { match value { 1 => ValidationStatus::Waiting, - 2 => ValidationStatus::Countdown, - 3 => ValidationStatus::Active, + 2 => ValidationStatus::Active, _ => ValidationStatus::Disabled, } } @@ -641,7 +626,7 @@ struct ValidatorManagerImpl { /// Persisted in the validator-state DB and cleared when `rotate_all_shards()` returns /// true, matching the C++ lifecycle around init-block updates. destroyed_sessions: HashSet, - /// Set to `true` once `check_sync()` succeeds, enabling `Waiting -> Countdown`. + /// Set to `true` once `check_sync()` succeeds, enabling `Waiting -> Active`. sync_complete: bool, /// Wall-clock timestamp of the last full metrics dump. last_metrics_dump: tokio::time::Instant, @@ -829,9 +814,7 @@ impl ValidatorManagerImpl { for group in self.current_sessions.values().chain(self.future_sessions.values()) { if group.shard() == shard { match group.get_status().await { - ValidatorGroupStatus::Sync - | ValidatorGroupStatus::Active - | ValidatorGroupStatus::Countdown { .. } => return true, + ValidatorGroupStatus::Sync | ValidatorGroupStatus::Active => return true, _ => (), } } @@ -1267,7 +1250,7 @@ impl ValidatorManagerImpl { } } - // Phase 2: transition Waiting -> Countdown/Active. + // Phase 2: transition Waiting -> Active. // C++ parity: allow_validate_ is set purely from // rotated_all_shards() || seqno==0 plus the fork check // (manager.cpp update_shards). sync_complete (started_) @@ -1276,50 +1259,24 @@ impl ValidatorManagerImpl { let rotated = rotate_all_shards(mc_state_extra) || last_masterchain_block.seq_no == 0; if rotated && later_than_hardfork { - if last_masterchain_block.seq_no == 0 && self.config.no_countdown_for_zerostate - { - log::info!(target: "validator_manager", - "VALIDATION_STATUS: Waiting -> Active (zerostate, no countdown)"); - self.engine.set_validation_status(ValidationStatus::Active); - } else { - log::info!(target: "validator_manager", - "VALIDATION_STATUS: Waiting -> Countdown \ - (rotated_all_shards={}, mc_seqno={}, sync_complete={})", - rotate_all_shards(mc_state_extra), - last_masterchain_block.seq_no, - self.sync_complete); - self.engine.set_validation_status(ValidationStatus::Countdown); - } + let bootstrap = last_masterchain_block.seq_no == 0; + log::info!(target: "validator_manager", + "VALIDATION_STATUS: Waiting -> Active \ + (rotated_all_shards={}, mc_seqno={}, sync_complete={}, bootstrap={}, \ + no_countdown_for_zerostate={})", + rotate_all_shards(mc_state_extra), + last_masterchain_block.seq_no, + self.sync_complete, + bootstrap, + self.config.no_countdown_for_zerostate); + self.engine.set_validation_status(ValidationStatus::Active); } else if !rotated { log::trace!(target: "validator_manager", "update_validation_status: rotated_all_shards=false, \ - deferring Waiting -> Countdown (mc_seqno={})", + deferring Waiting -> Active (mc_seqno={})", last_masterchain_block.seq_no); } } - ValidationStatus::Countdown => { - // C++ parity: transition only after a session has genuinely accepted - // a committed block (ValidatorGroupStatus::Active), not merely - // entered Sync. Combined with the deferred Sync->Active in - // on_block_committed(), this prevents false-positive activation - // from stale or duplicate commit events. - for (_, group) in self.current_sessions.iter() { - let status = group.get_status().await; - if status == ValidatorGroupStatus::Active { - let path_str: String = self.engine.db_root_dir()?.to_owned() + "/catchains"; - tokio::spawn(async move { - if let Err(err) = clear_catchains_cache(path_str).await { - log::warn!("Error clearing catchains cache: {}", err); - } - }); - log::info!(target: "validator_manager", - "VALIDATION_STATUS: Countdown -> Active (session shard={} reached {})", - group.shard(), status); - self.engine.set_validation_status(ValidationStatus::Active); - break; - } - } - } ValidationStatus::Disabled | ValidationStatus::Active => {} } Ok(()) @@ -1364,7 +1321,7 @@ impl ValidatorManagerImpl { self.engine.set_will_validate(true); let current = self.engine.validation_status(); // C++ parity: enable_validation() only ensures we are at least - // Waiting. The Waiting -> Countdown promotion is handled by + // Waiting. The Waiting -> Active promotion is handled by // update_validation_status() based on rotated_all_shards(), // matching C++'s allow_validate_ which is independent of sync. let target = max(current, ValidationStatus::Waiting); @@ -1414,17 +1371,7 @@ impl ValidatorManagerImpl { }; let full_validator_set = mc_state_extra.config.validator_set()?; - let validation_status = self.engine.validation_status(); let catchain_config = self.read_catchain_config(mc_state)?; - let group_start_status = if validation_status == ValidationStatus::Countdown { - let session_lifetime = - min(catchain_config.mc_catchain_lifetime, catchain_config.shard_catchain_lifetime); - let start_at = - tokio::time::Instant::now() + Duration::from_secs((session_lifetime / 2).into()); - ValidatorGroupStatus::Countdown { start_at } - } else { - ValidatorGroupStatus::Sync - }; let do_unsafe_catchain_rotate = self .config @@ -1586,15 +1533,14 @@ impl ValidatorManagerImpl { }; let session_status = session.get_status().await; - if session.try_prepare_start(group_start_status).await? { + if session.try_prepare_start().await? { log::trace!( target: "validator_manager", "Current shard {ident}, session {session_id:x}: starting" ); session - .start_with_status( - group_start_status, + .start_session( prev.get_prevs().to_vec(), last_masterchain_block.clone(), SystemTime::UNIX_EPOCH + Duration::from_secs(mc_now as u64), @@ -2125,21 +2071,20 @@ impl ValidatorManagerImpl { log::warn!(target: "validator_manager", "HEALTH_CHECK: {} session(s) stalled (validation queue inactive)", stalled_count); } - if in_current_set - && self.current_sessions.is_empty() - && validation_status >= ValidationStatus::Countdown + if in_current_set && self.current_sessions.is_empty() && validation_status.allows_validate() { log::warn!(target: "validator_manager", "HEALTH_CHECK: node is in current validator set but has no current sessions \ (validation_status={:?})", validation_status); } - if validation_status == ValidationStatus::Countdown { - let has_active_or_sync = state_counts.get("active").copied().unwrap_or(0) > 0 - || state_counts.get("sync").copied().unwrap_or(0) > 0; - if !has_active_or_sync && !self.current_sessions.is_empty() { + if validation_status.allows_validate() { + let sync_count = state_counts.get("sync").copied().unwrap_or(0); + let active_count = state_counts.get("active").copied().unwrap_or(0); + if sync_count == 0 && active_count == 0 && !self.current_sessions.is_empty() { log::warn!(target: "validator_manager", - "HEALTH_CHECK: validation_status=Countdown but no session reached sync/active \ - (possible TN-1050 regression)"); + "HEALTH_CHECK: validation enabled but no current session reached sync yet \ + (possible startup/session-start regression, validation_status={:?})", + validation_status); } } @@ -2314,24 +2259,19 @@ impl ValidatorManagerImpl { let age = now.duration_since(snap.created_at).unwrap_or_default(); let val_ago = format_time_ago(now_unix, *last_val); let col_ago = format_time_ago(now_unix, *last_col); - let countdown_str = match snap.status { - ValidatorGroupStatus::Countdown { start_at } => { - let remaining = - start_at.saturating_duration_since(tokio::time::Instant::now()); - format!("countdown({}s)", remaining.as_secs()) - } - _ => format!("{}", snap.status), - }; + let status_str = format!("{}", snap.status); let last_mc = snap.last_accepted_mc_seqno.map_or("-".to_string(), |s| s.to_string()); + let phase_str = validation_state_phase_label(snap.status); lines.push(format!( - " {:<8} cc={:<4} {:<4} {:<14} rnd={:<4} collator={:<3} collating={:<3} \ - stall={:<3} val_ago={:<6} col_ago={:<6} mc_init={:<6} mc_last={:<6} \ - age={} id={:x}", + " {:<8} cc={:<4} {:<4} {:<14} phase={:<13} rnd={:<4} collator={:<3} \ + collating={:<3} stall={:<3} val_ago={:<6} col_ago={:<6} \ + mc_init={:<6} mc_last={:<6} age={} id={:x}", shard_str, snap.cc_seqno, consensus_str, - countdown_str, + status_str, + phase_str, snap.round, if snap.is_collator { "yes" } else { "no" }, if *is_collating { "yes" } else { "no" }, diff --git a/src/node/tests/compat_test/src/test_helpers.rs b/src/node/tests/compat_test/src/test_helpers.rs index 58c2d43..8b1aefb 100644 --- a/src/node/tests/compat_test/src/test_helpers.rs +++ b/src/node/tests/compat_test/src/test_helpers.rs @@ -636,7 +636,7 @@ impl RustQuicTestNode { let src = self.adnl_key_id(); self.rt.block_on(async { self.quic - .message(data.to_vec(), Some(&*self.adnl), &src, dst) + .message(data.to_vec(), Some(&*self.adnl), &AdnlPeers::with_keys(src, dst.clone())) .await .expect("QUIC message failed"); }); @@ -660,7 +660,7 @@ impl RustQuicTestNode { self.rt.block_on(async { self.quic - .message(overlay_data, Some(&*self.adnl), &src, dst) + .message(overlay_data, Some(&*self.adnl), &AdnlPeers::with_keys(src, dst.clone())) .await .expect("QUIC message failed"); }); @@ -673,7 +673,12 @@ impl RustQuicTestNode { let src = self.adnl_key_id(); self.rt.block_on(async { self.quic - .query(data.to_vec(), Some(&*self.adnl), &src, dst, None) + .query( + data.to_vec(), + Some(&*self.adnl), + &AdnlPeers::with_keys(src, dst.clone()), + None, + ) .await .expect("QUIC query failed") .expect("empty QUIC query answer") @@ -699,7 +704,12 @@ impl RustQuicTestNode { self.rt.block_on(async { match tokio::time::timeout( Duration::from_secs(timeout_secs), - self.quic.query(overlay_data, Some(&*self.adnl), &src, dst, None), + self.quic.query( + overlay_data, + Some(&*self.adnl), + &AdnlPeers::with_keys(src, dst.clone()), + None, + ), ) .await { From 35b8421768cd7cc1d3e372bd1515da0a0d210758 Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 2 Apr 2026 17:43:01 +0300 Subject: [PATCH 25/48] Cancellation safety for AsyncReceiver --- src/adnl/src/adnl/common.rs | 22 ++++++++++++++++++++-- src/adnl/src/quic/mod.rs | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/adnl/src/adnl/common.rs b/src/adnl/src/adnl/common.rs index d0802ff..efc40de 100644 --- a/src/adnl/src/adnl/common.rs +++ b/src/adnl/src/adnl/common.rs @@ -468,6 +468,24 @@ pub enum Answer { Raw(TaggedByteVec), } +/// Cancellation-safety guard over AtomicU32 counter +struct SyncGuard<'a> { + counter: &'a AtomicU32, +} + +impl<'a> SyncGuard<'a> { + fn new(counter: &'a AtomicU32) -> Self { + counter.fetch_add(1, Ordering::Relaxed); + Self { counter } + } +} + +impl Drop for SyncGuard<'_> { + fn drop(&mut self) { + self.counter.fetch_sub(1, Ordering::Relaxed); + } +} + /// Asynchronous data receiver pub(crate) struct AsyncReceiver { id: u64, @@ -515,10 +533,10 @@ impl AsyncReceiver { pub(crate) async fn pop(&self) -> Result> { self.started_receiving.store(true, Ordering::Relaxed); - self.sync.fetch_add(1, Ordering::Relaxed); + // Protect counter by guard + let _guard = SyncGuard::new(&self.sync); loop { if let Some(data) = self.data.pop() { - self.sync.fetch_sub(1, Ordering::Relaxed); break Ok(data); } let subscriber = Arc::new(tokio::sync::Barrier::new(2)); diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index 784f541..f9782f2 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -569,8 +569,8 @@ impl QuicNode { let size = data.len(); let timeout_ms = timeout_ms.unwrap_or(Self::DEFAULT_QUERY_TIMEOUT_MS); let wire = serialize_boxed(&QuicQuery { data: data.into() }.into_boxed())?; - self.msg_stats.record(tag, size, addr, true, true); let response = self.send_query_raw(wire, peers, timeout_ms).await?; + self.msg_stats.record(tag, size, addr, true, true); if response.is_empty() { return Ok(None); } From be2422b3a648dda3b2f8257f6f0e3db4a88d4e3d Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Thu, 2 Apr 2026 18:34:43 +0700 Subject: [PATCH 26/48] ci(node): add node release workflow --- .github/workflows/node-release.yml | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 .github/workflows/node-release.yml diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml new file mode 100644 index 0000000..3aaeee2 --- /dev/null +++ b/.github/workflows/node-release.yml @@ -0,0 +1,94 @@ +name: Publish node + +on: + push: + tags: + - "node/v*" + +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + +env: + WORKING_DIR: src + +jobs: + container: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v5 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/rsquad/ton-rust-node/node + tags: | + type=match,pattern=node/(v.*),group=1 + type=raw,value=latest,enable=${{ !contains(github.ref_name, 'rc') && !contains(github.ref_name, 'alpha') && !contains(github.ref_name, 'beta') }} + type=sha + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./src + file: ./src/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + GIT_BRANCH=${{ github.ref_name }} + GIT_COMMIT=${{ github.sha }} + GIT_COMMIT_DATE=${{ github.event.head_commit.timestamp }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release: + needs: [container] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - name: Extract version from tag + id: version + run: | + VERSION="${GITHUB_REF_NAME#node/}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + if echo "$VERSION" | grep -qE '(rc|alpha|beta)'; then + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: "node/${{ steps.version.outputs.version }}" + body: | + node ${{ steps.version.outputs.version }} + + Image: `ghcr.io/rsquad/ton-rust-node/node:${{ steps.version.outputs.version }}` + prerelease: ${{ steps.version.outputs.prerelease }} + make_latest: ${{ steps.version.outputs.prerelease != 'true' }} + draft: ${{ steps.version.outputs.prerelease }} + + - name: Publish pre-release + if: steps.version.outputs.prerelease == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release edit "$GITHUB_REF_NAME" --draft=false From 007a5e34acd85dc44a6a7ceedea3851c7f7ad558 Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Thu, 2 Apr 2026 22:16:02 +0700 Subject: [PATCH 27/48] ci(node): fix commit timestamp for tag push events --- .github/workflows/node-release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index 3aaeee2..ec8d7e6 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -21,6 +21,10 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Get commit timestamp + id: commit + run: echo "date=$(git log -1 --format=%cI)" >> "$GITHUB_OUTPUT" + - uses: docker/setup-buildx-action@v3 - name: Log in to GHCR @@ -51,7 +55,7 @@ jobs: build-args: | GIT_BRANCH=${{ github.ref_name }} GIT_COMMIT=${{ github.sha }} - GIT_COMMIT_DATE=${{ github.event.head_commit.timestamp }} + GIT_COMMIT_DATE=${{ steps.commit.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max From 837dd2366e08041da53ba2dc45551dbdea93aee0 Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Thu, 2 Apr 2026 22:25:39 +0700 Subject: [PATCH 28/48] ci(node): use ton-large runner for container build --- .github/workflows/node-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index ec8d7e6..a2f8047 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -14,7 +14,7 @@ env: jobs: container: - runs-on: ubuntu-latest + runs-on: ton-large permissions: contents: read packages: write From a7fe8ed84b4372aa4aeef21d3477db271d51090a Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 2 Apr 2026 18:36:00 +0300 Subject: [PATCH 29/48] Add release workflow --- .github/workflows/node-release.yml | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/node-release.yml diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml new file mode 100644 index 0000000..a2f8047 --- /dev/null +++ b/.github/workflows/node-release.yml @@ -0,0 +1,98 @@ +name: Publish node + +on: + push: + tags: + - "node/v*" + +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + +env: + WORKING_DIR: src + +jobs: + container: + runs-on: ton-large + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v5 + + - name: Get commit timestamp + id: commit + run: echo "date=$(git log -1 --format=%cI)" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/rsquad/ton-rust-node/node + tags: | + type=match,pattern=node/(v.*),group=1 + type=raw,value=latest,enable=${{ !contains(github.ref_name, 'rc') && !contains(github.ref_name, 'alpha') && !contains(github.ref_name, 'beta') }} + type=sha + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./src + file: ./src/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + GIT_BRANCH=${{ github.ref_name }} + GIT_COMMIT=${{ github.sha }} + GIT_COMMIT_DATE=${{ steps.commit.outputs.date }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release: + needs: [container] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - name: Extract version from tag + id: version + run: | + VERSION="${GITHUB_REF_NAME#node/}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + if echo "$VERSION" | grep -qE '(rc|alpha|beta)'; then + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: "node/${{ steps.version.outputs.version }}" + body: | + node ${{ steps.version.outputs.version }} + + Image: `ghcr.io/rsquad/ton-rust-node/node:${{ steps.version.outputs.version }}` + prerelease: ${{ steps.version.outputs.prerelease }} + make_latest: ${{ steps.version.outputs.prerelease != 'true' }} + draft: ${{ steps.version.outputs.prerelease }} + + - name: Publish pre-release + if: steps.version.outputs.prerelease == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release edit "$GITHUB_REF_NAME" --draft=false From 1e888dea4702f6b39b86ca8788d1005ca42cae4d Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Thu, 2 Apr 2026 23:00:00 +0700 Subject: [PATCH 30/48] ci(node): fix runs-on to use runner group syntax --- .github/workflows/node-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index a2f8047..84cbb0e 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -14,7 +14,8 @@ env: jobs: container: - runs-on: ton-large + runs-on: + group: ton-large permissions: contents: read packages: write From 14d7a71257cd2cef5c342fa7c9ea37e5e672fd6d Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Thu, 2 Apr 2026 23:00:00 +0700 Subject: [PATCH 31/48] ci(node): fix runs-on to use runner group syntax --- .github/workflows/node-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index a2f8047..84cbb0e 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -14,7 +14,8 @@ env: jobs: container: - runs-on: ton-large + runs-on: + group: ton-large permissions: contents: read packages: write From 39d6b569c0e9c598c5d6dc53b73fd34ce54b7a37 Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 2 Apr 2026 23:57:51 +0300 Subject: [PATCH 32/48] QUIC connection deduplication fix --- src/adnl/src/quic/mod.rs | 75 ++++++---- src/adnl/tests/test_quic.rs | 274 ++++++++++++++++++++++++++++++++++++ src/node/src/engine.rs | 2 +- 3 files changed, 319 insertions(+), 32 deletions(-) diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index f9782f2..0b07157 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -39,6 +39,14 @@ use ton_block::{ const TARGET: &str = "quic"; +/// Key for the QUIC inbound connection map: (local_key_id, peer_key_id). +/// Matches the C++ `AdnlPath{local_id, peer_id}` semantics so that two +/// connections from the same peer address but different key pairs (e.g. +/// current + next validator keys) coexist instead of evicting each other. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +struct QuicInboundKey(Arc, Arc); + +type QuicInboundMap = lockfree::map::Map; type QuicSendQueue = SendQueue>; /// Extract a `KeyId` from an Ed25519 SubjectPublicKeyInfo (SPKI) DER blob. @@ -347,7 +355,7 @@ pub struct QuicNode { /// Max concurrent in-flight streams per inbound connection. max_streams_per_connection: usize, /// Inbound connection maps, one per endpoint/accept-loop. Used by the stats dumper. - inbound_pools: Mutex>>>, + inbound_pools: Mutex>>, /// Per-TL-tag message counters for the stats dumper. msg_stats: Arc, } @@ -762,7 +770,7 @@ impl QuicNode { let local_key_names: Arc>> = Arc::new(lockfree::map::Map::new()); - let inbound: Arc> = Connections::new(); + let inbound: Arc = Arc::new(lockfree::map::Map::new()); match self.inbound_pools.lock() { Ok(mut pools) => pools.push(inbound.clone()), Err(e) => log::warn!( @@ -840,7 +848,7 @@ impl QuicNode { incoming: quinn::Incoming, local_key_names: Arc>>, server_cert_resolver: Arc, - inbound: Arc>, + inbound: Arc, subscribers: Arc>>, bind_addr: SocketAddr, max_streams_per_connection: usize, @@ -900,17 +908,19 @@ impl QuicNode { } }; + let inbound_key = QuicInboundKey(local_key_id.clone(), peer_key_id.clone()); let had_existing = { let mut found_existing = false; - let result = add_unbound_object_to_map_with_update(inbound.map(), addr, |existing| { - if existing.is_some() { - found_existing = true; - // Keep existing entry; resolver task will handle replacement - Ok(None) - } else { - Ok(Some(conn.clone())) - } - }); + let result = + add_unbound_object_to_map_with_update(&inbound, inbound_key.clone(), |existing| { + if existing.is_some() { + found_existing = true; + // Keep existing entry; resolver task will handle replacement + Ok(None) + } else { + Ok(Some(conn.clone())) + } + }); if let Err(e) = result { log::warn!(target: TARGET, "Store QUIC inbound for {addr}: {e}"); return; @@ -918,7 +928,12 @@ impl QuicNode { found_existing }; if had_existing { - tokio::spawn(Self::resolve_duplicate_connection(inbound.clone(), conn.clone(), addr)); + tokio::spawn(Self::resolve_duplicate_connection( + inbound.clone(), + conn.clone(), + inbound_key.clone(), + addr, + )); } let peers = AdnlPeers::with_keys(local_key_id, peer_key_id); @@ -1008,9 +1023,9 @@ impl QuicNode { () = uni_loop => {} } let is_current = - inbound.map().get(&addr).map(|e| e.val().stable_id() == conn_id).unwrap_or(false); + inbound.get(&inbound_key).map(|e| e.val().stable_id() == conn_id).unwrap_or(false); if is_current { - inbound.map().remove(&addr); + inbound.remove(&inbound_key); } log::info!( target: TARGET, @@ -1231,8 +1246,9 @@ impl QuicNode { } async fn resolve_duplicate_connection( - inbound: Arc>, + inbound: Arc, new_conn: quinn::Connection, + key: QuicInboundKey, addr: SocketAddr, ) { use rand::Rng; @@ -1240,11 +1256,11 @@ impl QuicNode { tokio::time::sleep(Duration::from_millis(delay_ms)).await; let old_alive = - inbound.map().get(&addr).map(|e| e.val().close_reason().is_none()).unwrap_or(false); + inbound.get(&key).map(|e| e.val().close_reason().is_none()).unwrap_or(false); let new_alive = new_conn.close_reason().is_none(); if old_alive && new_alive { - if let Some(old) = inbound.map().remove(&addr) { + if let Some(old) = inbound.remove(&key) { log::info!( target: TARGET, "Closing old duplicate inbound from {addr} (both alive after {delay_ms}ms)" @@ -1252,15 +1268,11 @@ impl QuicNode { old.val().close(0u32.into(), b"Replaced by new inbound"); } let nc = new_conn.clone(); - let _ = add_unbound_object_to_map_with_update(inbound.map(), addr, |_| { - Ok(Some(nc.clone())) - }); + let _ = add_unbound_object_to_map_with_update(&inbound, key, |_| Ok(Some(nc.clone()))); } else if new_alive { - inbound.map().remove(&addr); + inbound.remove(&key); let nc = new_conn.clone(); - let _ = add_unbound_object_to_map_with_update(inbound.map(), addr, |_| { - Ok(Some(nc.clone())) - }); + let _ = add_unbound_object_to_map_with_update(&inbound, key, |_| Ok(Some(nc.clone()))); log::debug!( target: TARGET, "Old inbound from {addr} already closed, keeping new" @@ -1440,7 +1452,7 @@ impl QuicNode { bind_addr: SocketAddr, max_streams_per_connection: usize, cancellation_token: tokio_util::sync::CancellationToken, - inbound: Arc>, + inbound: Arc, msg_stats: Arc, ) { tokio::spawn(async move { @@ -1613,8 +1625,9 @@ impl QuicNode { } }; for pool in &pools { - for conn_entry in pool.map().iter() { - let addr = *conn_entry.key(); + for conn_entry in pool.iter() { + let QuicInboundKey(ref local_id, ref peer_id) = *conn_entry.key(); + let addr = conn_entry.val().remote_address(); let conn = conn_entry.val(); let s = conn.stats(); let id = (conn.stable_id(), false); @@ -1626,9 +1639,9 @@ impl QuicNode { fmt::Write::write_fmt( &mut dump, format_args!( - " inbound peer={addr} \ - dtx={} bytes/{} dgrams drx={} bytes/{} dgrams \ - dlost={} pkts rtt={:?} cwnd={} mtu={}\n", + " inbound peer={addr} local={local_id} remote={peer_id} \ + dtx={} bytes/{} dgrams drx={} bytes/{} dgrams \ + dlost={} pkts rtt={:?} cwnd={} mtu={}\n", delta.tx_bytes, delta.tx_dgrams, delta.rx_bytes, diff --git a/src/adnl/tests/test_quic.rs b/src/adnl/tests/test_quic.rs index c8b4ac1..d41d8b6 100644 --- a/src/adnl/tests/test_quic.rs +++ b/src/adnl/tests/test_quic.rs @@ -871,6 +871,280 @@ fn test_quic_duplicate_inbound_same_address() { }); } +// =========================================================================== +// Test 1b: Multiple keys from same address must coexist +// =========================================================================== + +/// A TON node may have multiple connections to each peer โ€” one for the +/// current validator key and one for the next key. Both connections originate +/// from the same source address but use different client Ed25519 keys. +/// They must coexist: the server must NOT close one when the other arrives. +/// +/// This test opens two raw quinn connections from the same UDP endpoint with +/// different Ed25519 RPK identities. After waiting past the duplicate-resolution +/// window, BOTH connections must still be alive and answer queries. +#[test] +fn test_quic_multi_key_same_address() { + init_test_log(); + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(async { + const SERVER_PORT: u16 = 5915; + const RAW_CLIENT_PORT: u16 = 5916; + + // --- server --- + let (server, _server_key, server_key_id, server_bind, server_token) = + make_endpoint(SERVER_PORT); + + // --- two different client keys (simulating current + next validator keys) --- + let key1 = ed25519_generate_private_key().unwrap().to_bytes(); + let key2 = ed25519_generate_private_key().unwrap().to_bytes(); + + let (_, cfg1) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{RAW_CLIENT_PORT}"), + vec![(key1, KEY_TAG)], + ) + .unwrap(); + let key1_id = cfg1.key_by_tag(KEY_TAG).unwrap().id().clone(); + + let (_, cfg2) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{RAW_CLIENT_PORT}"), + vec![(key2, KEY_TAG)], + ) + .unwrap(); + let key2_id = cfg2.key_by_tag(KEY_TAG).unwrap().id().clone(); + + server + .add_peer_key( + key1_id.clone(), + format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(), + ) + .unwrap(); + server + .add_peer_key( + key2_id.clone(), + format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(), + ) + .unwrap(); + + // Build two different quinn client configs (different RPK identities) + let client_config1 = build_raw_quinn_client(&key1); + let client_config2 = build_raw_quinn_client(&key2); + + // Create a single raw quinn endpoint (both connections share the same source addr) + let raw_bind: SocketAddr = + format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); + let sock = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + ) + .unwrap(); + sock.set_reuse_address(true).unwrap(); + sock.bind(&raw_bind.into()).unwrap(); + sock.set_nonblocking(true).unwrap(); + let udp = std::net::UdpSocket::from(sock); + let runtime: Arc = Arc::new(quinn::TokioRuntime); + let endpoint = + quinn::Endpoint::new(quinn::EndpointConfig::default(), None, udp, runtime).unwrap(); + + // SNI name matching QuicNode's key_id_to_server_name + let hex = hex::encode(server_key_id.data()); + let sni = format!("{}.{}", &hex[..32], &hex[32..]); + + // Open connection 1 with key1 + let conn1 = endpoint + .connect_with(client_config1, server_bind, &sni) + .unwrap() + .await + .expect("conn1 (key1) handshake failed"); + + // Open connection 2 with key2 (same source address, different identity) + let conn2 = endpoint + .connect_with(client_config2, server_bind, &sni) + .unwrap() + .await + .expect("conn2 (key2) handshake failed"); + + println!("Two connections established from same address with different keys"); + + // Verify both work immediately + let ping1 = make_ping_wire(101); + let (mut s1, mut r1) = conn1.open_bi().await.unwrap(); + s1.write_all(&ping1).await.unwrap(); + s1.finish().unwrap(); + let resp1 = tokio::time::timeout(Duration::from_secs(10), r1.read_to_end(1 << 20)) + .await + .expect("conn1 response timed out") + .expect("conn1 read failed"); + assert_eq!(parse_pong_wire(&resp1), 101); + println!("conn1 (key1) ping/pong OK"); + + let ping2 = make_ping_wire(102); + let (mut s2, mut r2) = conn2.open_bi().await.unwrap(); + s2.write_all(&ping2).await.unwrap(); + s2.finish().unwrap(); + let resp2 = tokio::time::timeout(Duration::from_secs(10), r2.read_to_end(1 << 20)) + .await + .expect("conn2 response timed out") + .expect("conn2 read failed"); + assert_eq!(parse_pong_wire(&resp2), 102); + println!("conn2 (key2) ping/pong OK"); + + // Wait past the maximum duplicate-resolution window (2500ms + margin) + tokio::time::sleep(Duration::from_secs(4)).await; + + // BOTH connections must still be alive โ€” this is the key assertion. + // With the old SocketAddr-based keying, one would have been killed. + assert!( + conn1.close_reason().is_none(), + "conn1 (key1) was closed โ€” multi-key coexistence broken!" + ); + assert!( + conn2.close_reason().is_none(), + "conn2 (key2) was closed โ€” multi-key coexistence broken!" + ); + + // Both must still answer queries + let ping3 = make_ping_wire(201); + let (mut s3, mut r3) = conn1.open_bi().await.expect("conn1 should still accept streams"); + s3.write_all(&ping3).await.unwrap(); + s3.finish().unwrap(); + let resp3 = tokio::time::timeout(Duration::from_secs(10), r3.read_to_end(1 << 20)) + .await + .expect("conn1 post-wait response timed out") + .expect("conn1 post-wait read failed"); + assert_eq!(parse_pong_wire(&resp3), 201); + + let ping4 = make_ping_wire(202); + let (mut s4, mut r4) = conn2.open_bi().await.expect("conn2 should still accept streams"); + s4.write_all(&ping4).await.unwrap(); + s4.finish().unwrap(); + let resp4 = tokio::time::timeout(Duration::from_secs(10), r4.read_to_end(1 << 20)) + .await + .expect("conn2 post-wait response timed out") + .expect("conn2 post-wait read failed"); + assert_eq!(parse_pong_wire(&resp4), 202); + + println!("PASS: both connections survived duplicate-resolution window"); + + // --- cleanup --- + conn1.close(0u32.into(), b"done"); + conn2.close(0u32.into(), b"done"); + endpoint.close(0u32.into(), b"done"); + server.shutdown(); + server_token.cancel(); + }); +} + +// =========================================================================== +// Test 1c: Same-key duplicate connections must be deduplicated +// =========================================================================== + +/// When two connections arrive from the same client key pair (genuine duplicate), +/// the server must close the old one after the resolution window. This verifies +/// that deduplication still works correctly with the AdnlPath-based keying. +#[test] +fn test_quic_same_key_deduplication() { + init_test_log(); + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(async { + const SERVER_PORT: u16 = 5917; + const RAW_CLIENT_PORT: u16 = 5918; + + // --- server --- + let (server, _server_key, server_key_id, server_bind, server_token) = + make_endpoint(SERVER_PORT); + + // --- single client key (both connections use the same identity) --- + let key = ed25519_generate_private_key().unwrap().to_bytes(); + let (_, cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( + &format!("127.0.0.1:{RAW_CLIENT_PORT}"), + vec![(key, KEY_TAG)], + ) + .unwrap(); + let key_id = cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); + let raw_bind: SocketAddr = + format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); + + server.add_peer_key(key_id.clone(), raw_bind).unwrap(); + + let client_config = build_raw_quinn_client(&key); + + // Create raw quinn endpoint + let sock = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + ) + .unwrap(); + sock.set_reuse_address(true).unwrap(); + sock.bind(&raw_bind.into()).unwrap(); + sock.set_nonblocking(true).unwrap(); + let udp = std::net::UdpSocket::from(sock); + let runtime: Arc = Arc::new(quinn::TokioRuntime); + let mut endpoint = + quinn::Endpoint::new(quinn::EndpointConfig::default(), None, udp, runtime).unwrap(); + endpoint.set_default_client_config(client_config); + + let hex = hex::encode(server_key_id.data()); + let sni = format!("{}.{}", &hex[..32], &hex[32..]); + + // Open first connection, verify it works + let conn1 = + endpoint.connect(server_bind, &sni).unwrap().await.expect("conn1 handshake failed"); + + let ping1 = make_ping_wire(301); + let (mut s1, mut r1) = conn1.open_bi().await.unwrap(); + s1.write_all(&ping1).await.unwrap(); + s1.finish().unwrap(); + let resp1 = tokio::time::timeout(Duration::from_secs(10), r1.read_to_end(1 << 20)) + .await + .expect("conn1 response timed out") + .expect("conn1 read failed"); + assert_eq!(parse_pong_wire(&resp1), 301); + println!("conn1 ping/pong OK"); + + // Open second connection with the SAME key (genuine duplicate) + let conn2 = + endpoint.connect(server_bind, &sni).unwrap().await.expect("conn2 handshake failed"); + + let ping2 = make_ping_wire(302); + let (mut s2, mut r2) = conn2.open_bi().await.unwrap(); + s2.write_all(&ping2).await.unwrap(); + s2.finish().unwrap(); + let resp2 = tokio::time::timeout(Duration::from_secs(10), r2.read_to_end(1 << 20)) + .await + .expect("conn2 response timed out") + .expect("conn2 read failed"); + assert_eq!(parse_pong_wire(&resp2), 302); + println!("conn2 ping/pong OK"); + + // Wait past the duplicate-resolution window (max 2500ms + margin) + tokio::time::sleep(Duration::from_secs(4)).await; + + // The old connection (conn1) should have been closed by duplicate resolution. + // Check by trying to open a stream โ€” if the connection was closed, this fails. + let conn1_alive = conn1.close_reason().is_none() && conn1.open_bi().await.is_ok(); + let conn2_alive = conn2.close_reason().is_none() && conn2.open_bi().await.is_ok(); + + println!("After dedup: conn1_alive={conn1_alive}, conn2_alive={conn2_alive}"); + + // Exactly one should have been closed (the old one). + // The new connection (conn2) must survive. + assert!(conn2_alive, "conn2 (newer) should survive deduplication"); + assert!(!conn1_alive, "conn1 (older) should have been closed by deduplication"); + + println!("PASS: same-key duplicate was correctly deduplicated"); + + // --- cleanup --- + conn1.close(0u32.into(), b"done"); + conn2.close(0u32.into(), b"done"); + endpoint.close(0u32.into(), b"done"); + server.shutdown(); + server_token.cancel(); + }); +} + // =========================================================================== // Test 2: Stream timeout handling // =========================================================================== diff --git a/src/node/src/engine.rs b/src/node/src/engine.rs index e517d3d..ec97f02 100644 --- a/src/node/src/engine.rs +++ b/src/node/src/engine.rs @@ -2115,7 +2115,7 @@ async fn boot( let (last_applied_mc_block, cold) = match result { Ok(block_id) => (block_id.clone(), false), Err(err) => { - log::debug!("before cold boot: {}", err); + log::warn!("Before cold boot: {err}"); engine.acquire_stop(Engine::MASK_SERVICE_BOOT); let result = boot::cold_boot(engine.clone(), pss_downloading_threads).await; engine.release_stop(Engine::MASK_SERVICE_BOOT); From 9c670a2df09a70023a1a3ea99a22035faa856301 Mon Sep 17 00:00:00 2001 From: Alexey Vavilin Date: Fri, 3 Apr 2026 13:21:33 +0400 Subject: [PATCH 33/48] Rename colons in archives --- src/node/src/archive_import/mod.rs | 1 + ... => archive.00000.0_8000000000000000.pack} | Bin ... => archive.00100.0_8000000000000000.pack} | Bin src/node/src/tests/test_archive_import.rs | 32 ++++++++++++++---- 4 files changed, 27 insertions(+), 6 deletions(-) rename src/node/src/tests/static/archives/{archive.00000.0:8000000000000000.pack => archive.00000.0_8000000000000000.pack} (100%) rename src/node/src/tests/static/archives/{archive.00100.0:8000000000000000.pack => archive.00100.0_8000000000000000.pack} (100%) diff --git a/src/node/src/archive_import/mod.rs b/src/node/src/archive_import/mod.rs index 722e52e..c1bfffa 100644 --- a/src/node/src/archive_import/mod.rs +++ b/src/node/src/archive_import/mod.rs @@ -407,6 +407,7 @@ pub async fn run_import(config: ImportConfig) -> Result> { Ok(node_db) } +#[cfg(not(target_os = "windows"))] #[cfg(test)] #[path = "../tests/test_archive_import.rs"] mod tests; diff --git a/src/node/src/tests/static/archives/archive.00000.0:8000000000000000.pack b/src/node/src/tests/static/archives/archive.00000.0_8000000000000000.pack similarity index 100% rename from src/node/src/tests/static/archives/archive.00000.0:8000000000000000.pack rename to src/node/src/tests/static/archives/archive.00000.0_8000000000000000.pack diff --git a/src/node/src/tests/static/archives/archive.00100.0:8000000000000000.pack b/src/node/src/tests/static/archives/archive.00100.0_8000000000000000.pack similarity index 100% rename from src/node/src/tests/static/archives/archive.00100.0:8000000000000000.pack rename to src/node/src/tests/static/archives/archive.00100.0_8000000000000000.pack diff --git a/src/node/src/tests/test_archive_import.rs b/src/node/src/tests/test_archive_import.rs index 0620be0..49fb1cc 100644 --- a/src/node/src/tests/test_archive_import.rs +++ b/src/node/src/tests/test_archive_import.rs @@ -41,9 +41,22 @@ const WC_ZEROSTATE_PATH: &str = "src/tests/static/EE0BEDFE4B32761FB35E9E1D8818EA720CAD1A0E7B4D2ED673C488E72E910342.boc"; const GLOBAL_CONFIG_PATH: &str = "src/tests/config/mainnet.json"; -fn import_config(dir: &Path) -> ImportConfig { +/// Copy archive files to a temporary directory, restoring colons in filenames +/// (files are stored with underscores to avoid issues on Windows). +fn prepare_archives(dest: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dest)?; + for entry in std::fs::read_dir(ARCHIVES_PATH)? { + let entry = entry?; + let name = entry.file_name().to_string_lossy().to_string(); + let restored_name = name.replacen("0_8000000000000000", "0:8000000000000000", 1); + std::fs::copy(entry.path(), dest.join(restored_name))?; + } + Ok(()) +} + +fn import_config(dir: &Path, archives_path: PathBuf) -> ImportConfig { ImportConfig { - archives_path: PathBuf::from(ARCHIVES_PATH), + archives_path, epochs_path: dir.join("epochs"), epoch_size: 20_000, node_db_path: dir.join("node_db"), @@ -133,7 +146,9 @@ async fn check_imported_block( async fn test_import_and_verify() -> Result<()> { init_test_log(); let dir = tempfile::tempdir().unwrap(); - let config = import_config(dir.path()); + let archives = dir.path().join("archives"); + prepare_archives(&archives).unwrap(); + let config = import_config(dir.path(), archives); run_import(config).await?; @@ -180,11 +195,14 @@ async fn test_import_and_verify() -> Result<()> { async fn test_import_resume() -> Result<()> { init_test_log(); let dir = tempfile::tempdir().unwrap(); + let all_archives = dir.path().join("archives"); + prepare_archives(&all_archives).unwrap(); + let partial_archives = dir.path().join("partial"); std::fs::create_dir_all(&partial_archives)?; // Copy only the first group (archive.00000.*) - for entry in std::fs::read_dir(ARCHIVES_PATH)? { + for entry in std::fs::read_dir(&all_archives)? { let entry = entry?; let name = entry.file_name().to_string_lossy().to_string(); if name.starts_with("archive.00000.") { @@ -215,7 +233,7 @@ async fn test_import_resume() -> Result<()> { drop(db1); // Copy remaining files for second import - for entry in std::fs::read_dir(ARCHIVES_PATH)? { + for entry in std::fs::read_dir(&all_archives)? { let entry = entry?; let name = entry.file_name().to_string_lossy().to_string(); if !name.starts_with("archive.00000.") { @@ -250,7 +268,9 @@ async fn test_import_resume() -> Result<()> { async fn test_import_skip_validation() -> Result<()> { init_test_log(); let dir = tempfile::tempdir().unwrap(); - let mut config = import_config(dir.path()); + let archives = dir.path().join("archives"); + prepare_archives(&archives).unwrap(); + let mut config = import_config(dir.path(), archives); config.skip_validation = true; run_import(config).await?; From 57c2a08c663e165a1b2b3dd67640fc758d539ded Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Fri, 3 Apr 2026 18:39:26 +0700 Subject: [PATCH 34/48] docs(helm): mark ports.simplex as required for validators --- helm/ton-rust-node/README.md | 20 ++++++++++++-------- helm/ton-rust-node/docs/networking.md | 2 +- helm/ton-rust-node/values.yaml | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/helm/ton-rust-node/README.md b/helm/ton-rust-node/README.md index 2da0a12..98960b6 100644 --- a/helm/ton-rust-node/README.md +++ b/helm/ton-rust-node/README.md @@ -21,6 +21,7 @@ A TON node can run in two roles โ€” **validator** or **fullnode** โ€” using the **Validator** participates in network consensus: it validates blocks, votes in elections, and earns rewards. Validator is currently supported on **mainnet** only โ€” testnet validator support is not yet available. A validator is a critical infrastructure component, so: +- **Enable `ports.simplex: true`** โ€” the network uses simplex consensus and validators will not work without it. - Never expose `liteserver` or `jsonRpc` ports on a validator. Every open port is an attack surface and adds unnecessary load to a machine that must stay performant and stable. - Allocate more resources (see [docs/resources.md](docs/resources.md) for recommended values). @@ -54,6 +55,9 @@ Minimal deployment: # values.yaml replicas: 2 +ports: + simplex: true + services: perReplica: - annotations: @@ -240,14 +244,14 @@ When an `existing*Name` is set, the chart does not create that resource โ€” it o ### Port parameters -| Name | Description | Value | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `ports.adnl` | ADNL port (UDP) | `30303` | -| `ports.simplex` | Simplex consensus port (UDP). Only needed for validators after switching to simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. | `false` | -| `ports.control` | Control port (TCP). Set to null to disable. | `50000` | -| `ports.liteserver` | Liteserver port (TCP). Set to enable. | `nil` | -| `ports.jsonRpc` | JSON-RPC port (TCP). Set to enable. | `nil` | -| `ports.metrics` | Metrics/probes HTTP port (TCP). Serves /metrics, /healthz, /readyz. Set to enable. | `nil` | +| Name | Description | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `ports.adnl` | ADNL port (UDP) | `30303` | +| `ports.simplex` | Simplex consensus port (UDP). Required for validators โ€” the network uses simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. | `false` | +| `ports.control` | Control port (TCP). Set to null to disable. | `50000` | +| `ports.liteserver` | Liteserver port (TCP). Set to enable. | `nil` | +| `ports.jsonRpc` | JSON-RPC port (TCP). Set to enable. | `nil` | +| `ports.metrics` | Metrics/probes HTTP port (TCP). Serves /metrics, /healthz, /readyz. Set to enable. | `nil` | ### Service parameters diff --git a/helm/ton-rust-node/docs/networking.md b/helm/ton-rust-node/docs/networking.md index 6f272f8..6e89e9e 100644 --- a/helm/ton-rust-node/docs/networking.md +++ b/helm/ton-rust-node/docs/networking.md @@ -21,7 +21,7 @@ The chart manages six ports. Each port is optional (set to `null` to disable) ex | Port | Protocol | Default | Purpose | |------|----------|---------|---------| | `ports.adnl` | UDP | `30303` | Peer-to-peer protocol. Must be publicly reachable. | -| `ports.simplex` | UDP | `false` | Simplex consensus protocol. Only needed for validators after switching to simplex consensus. `true` = adnl + 1000, or set an explicit port number. | +| `ports.simplex` | UDP | `false` | Simplex consensus protocol. **Required for validators** โ€” the network uses simplex consensus. `true` = adnl + 1000, or set an explicit port number. | | `ports.control` | TCP | `50000` | Node management (stop, restart, elections). Recommended to keep internal. | | `ports.liteserver` | TCP | `null` | Liteserver API for external consumers. | | `ports.jsonRpc` | TCP | `null` | JSON-RPC API for external consumers. | diff --git a/helm/ton-rust-node/values.yaml b/helm/ton-rust-node/values.yaml index e9ac5f5..78f968d 100644 --- a/helm/ton-rust-node/values.yaml +++ b/helm/ton-rust-node/values.yaml @@ -133,7 +133,7 @@ storage: ## @section Port parameters ## @param ports.adnl ADNL port (UDP) -## @param ports.simplex [nullable] Simplex consensus port (UDP). Only needed for validators after switching to simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. +## @param ports.simplex [nullable] Simplex consensus port (UDP). Required for validators โ€” the network uses simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. ## @param ports.control Control port (TCP). Set to null to disable. ## @param ports.liteserver [nullable] Liteserver port (TCP). Set to enable. ## @param ports.jsonRpc [nullable] JSON-RPC port (TCP). Set to enable. From 549f15d81777806482db54ca42192942223377b2 Mon Sep 17 00:00:00 2001 From: Alexey Vavilin Date: Fri, 3 Apr 2026 15:57:53 +0400 Subject: [PATCH 35/48] Review fix --- src/node/src/tests/test_archive_import.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/src/tests/test_archive_import.rs b/src/node/src/tests/test_archive_import.rs index 49fb1cc..82b7939 100644 --- a/src/node/src/tests/test_archive_import.rs +++ b/src/node/src/tests/test_archive_import.rs @@ -48,7 +48,7 @@ fn prepare_archives(dest: &Path) -> std::io::Result<()> { for entry in std::fs::read_dir(ARCHIVES_PATH)? { let entry = entry?; let name = entry.file_name().to_string_lossy().to_string(); - let restored_name = name.replacen("0_8000000000000000", "0:8000000000000000", 1); + let restored_name = name.replace("_", ":"); std::fs::copy(entry.path(), dest.join(restored_name))?; } Ok(()) From 40687974417344007daf506307c0a395ba8bb946 Mon Sep 17 00:00:00 2001 From: Lapo4kaKek Date: Sat, 4 Apr 2026 00:04:28 +0300 Subject: [PATCH 36/48] fix masterchain ValueFlow burned fees and blackhole accounting --- src/block/src/config_params.rs | 15 ++- src/block/src/transactions.rs | 12 +++ src/executor/src/ordinary_transaction.rs | 26 ++++- src/executor/src/tests/common/mod.rs | 2 + .../src/tests/test_ordinary_transaction.rs | 81 +++++++++++++++- src/node/src/validator/collator.rs | 38 ++++++-- src/node/src/validator/validate_query.rs | 96 +++++++++++++++++-- 7 files changed, 249 insertions(+), 21 deletions(-) diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index 6c08015..0bf905b 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -19,9 +19,9 @@ use crate::{ ChildCell, Coins, ExtraCurrencyCollection, Number12, Number13, Number16, Number32, Number8, }, validators::{ValidatorDescr, ValidatorSet}, - AccountId, BlockError, BuilderData, Cell, Deserializable, HashmapE, HashmapIterator, - HashmapType, IBitstring, MsgAddressInt, Result, Serializable, SliceData, UInt256, - BASE_WORKCHAIN_ID, MAX_SPLIT_DEPTH, + AccountId, BlockError, BuilderData, Cell, CurrencyCollection, Deserializable, HashmapE, + HashmapIterator, HashmapType, IBitstring, MsgAddressInt, Result, Serializable, SliceData, + UInt256, BASE_WORKCHAIN_ID, MAX_SPLIT_DEPTH, }; use num::BigInt; use std::collections::BTreeMap; @@ -938,6 +938,15 @@ impl BurningConfig { } Ok(()) } + + pub fn calculate_burned_fees(&self, value: &CurrencyCollection) -> Result { + if self.fee_burn_num == 0 || value.coins.is_zero() { + return Ok(CurrencyCollection::default()); + } + let burned = + value.coins.as_u128() * u128::from(self.fee_burn_num) / u128::from(self.fee_burn_denom); + Ok(CurrencyCollection::from_coins(Coins::try_from(burned)?)) + } } impl Deserializable for BurningConfig { diff --git a/src/block/src/transactions.rs b/src/block/src/transactions.rs index 7937c77..e7c6ab2 100644 --- a/src/block/src/transactions.rs +++ b/src/block/src/transactions.rs @@ -1265,6 +1265,7 @@ pub struct Transaction { pub in_msg: ChildCell, pub out_msgs: OutMessages, total_fees: CurrencyCollection, + blackhole_burned: CurrencyCollection, state_update: ChildCell, description: ChildCell, } @@ -1285,6 +1286,7 @@ impl Transaction { in_msg: ChildCell::default(), out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), + blackhole_burned: CurrencyCollection::default(), state_update: ChildCell::default(), description: ChildCell::default(), } @@ -1308,6 +1310,7 @@ impl Transaction { in_msg: ChildCell::with_struct(msg)?, out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), + blackhole_burned: CurrencyCollection::default(), state_update: ChildCell::default(), description: ChildCell::default(), }) @@ -1371,6 +1374,14 @@ impl Transaction { &mut self.total_fees } + pub fn blackhole_burned(&self) -> &CurrencyCollection { + &self.blackhole_burned + } + + pub fn set_blackhole_burned(&mut self, burned: CurrencyCollection) { + self.blackhole_burned = burned; + } + pub fn read_in_msg(&self) -> Result> { match self.in_msg.is_empty() { true => Ok(None), @@ -1557,6 +1568,7 @@ impl Default for Transaction { in_msg: ChildCell::default(), out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), + blackhole_burned: CurrencyCollection::default(), state_update: ChildCell::default(), description: ChildCell::default(), } diff --git a/src/executor/src/ordinary_transaction.rs b/src/executor/src/ordinary_transaction.rs index 3f050f6..485bc9e 100644 --- a/src/executor/src/ordinary_transaction.rs +++ b/src/executor/src/ordinary_transaction.rs @@ -18,9 +18,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; use ton_block::{ error, fail, AccStatusChange, Account, AddSub, Cell, Coins, CommonMsgInfo, ComputeSkipReason, - Deserializable, Message, MsgAddressInt, Result, Serializable, StorageUsageCalc, TrBouncePhase, - TrComputePhase, Transaction, TransactionDescr, TransactionDescrOrdinary, MASTERCHAIN_ID, - MAX_MSG_MERKLE_DEPTH, + ConfigParamEnum, CurrencyCollection, Deserializable, Message, MsgAddressInt, Result, + Serializable, StorageUsageCalc, TrBouncePhase, TrComputePhase, Transaction, TransactionDescr, + TransactionDescrOrdinary, MASTERCHAIN_ID, MAX_MSG_MERKLE_DEPTH, }; use ton_vm::{ boolean, int, @@ -178,6 +178,26 @@ impl TransactionExecutor for OrdinaryTransactionExecutor { tr.add_fee_coins(&in_fwd_fee)?; } + if is_masterchain { + if let Some(ConfigParamEnum::ConfigParam5(burning)) = + self.config.raw_config().config(5)? + { + if burning.blackhole_addr.as_ref() == Some(&account_id) + && !msg_balance.coins.is_zero() + { + let burned = + CurrencyCollection::from_coins(std::mem::take(&mut msg_balance.coins)); + log::debug!( + target: "executor", + "Burning {} nanoton for blackhole account {:x}", + burned.coins, + account_id + ); + tr.set_blackhole_burned(burned); + } + } + } + if description.credit_first && !is_ext_msg { description.credit_ph = match self.credit_phase(&msg_balance, &mut acc_balance) { Ok(credit_ph) => Some(credit_ph), diff --git a/src/executor/src/tests/common/mod.rs b/src/executor/src/tests/common/mod.rs index d424dee..cb9ddc8 100644 --- a/src/executor/src/tests/common/mod.rs +++ b/src/executor/src/tests/common/mod.rs @@ -320,6 +320,7 @@ pub fn check_account_and_transaction_balances( let mut right = acc_after.balance().cloned().unwrap_or_default(); right.add(trans.total_fees()).unwrap(); + right.add(trans.blackhole_burned()).unwrap(); trans .iterate_out_msgs(|out_msg| { if let Some(header) = out_msg.int_header() { @@ -881,6 +882,7 @@ pub fn replay_transaction( let mut right = account.balance().cloned().unwrap_or_default().coins; right.add(&our_transaction.total_fees().coins).unwrap(); + right.add(&our_transaction.blackhole_burned().coins).unwrap(); our_transaction .iterate_out_msgs(|out_msg| { if let Some(header) = out_msg.int_header() { diff --git a/src/executor/src/tests/test_ordinary_transaction.rs b/src/executor/src/tests/test_ordinary_transaction.rs index 9682482..dc7b1b5 100644 --- a/src/executor/src/tests/test_ordinary_transaction.rs +++ b/src/executor/src/tests/test_ordinary_transaction.rs @@ -27,7 +27,7 @@ use ton_block::{ AccStatusChange, TrActionPhase, TrComputePhase, TrComputePhaseVm, TrCreditPhase, TrStoragePhase, Transaction, TransactionDescr, }, - AccountId, AccountStatus, AnycastInfo, BouncedByPhase, BuilderData, Cell, Coins, + AccountId, AccountStatus, AnycastInfo, BouncedByPhase, BuilderData, BurningConfig, Cell, Coins, ComputeSkipReason, ConfigParam8, ConfigParamEnum, ConfigParams, CurrencyCollection, Deserializable, ExceptionCode, GetRepresentationHash, GlobalVersion, IBitstring, InternalMessageHeader, MerkleProof, NewBounceBody, NewBounceComputePhaseInfo, @@ -4067,3 +4067,82 @@ fn test_new_bounce() { .unwrap() ); } + +#[test] +fn test_masterchain_blackhole_burns_inbound_value() { + let acc_id = AccountId::from([0x44; 32]); + let code = compile_code_to_cell("ACCEPT").unwrap(); + let start_balance = 200_000_000; + let msg_value = 14_200_000; + let msg = create_int_msg_workchain( + -1, + SENDER_ACCOUNT.clone(), + acc_id.clone(), + msg_value, + false, + PREV_BLOCK_LT, + ); + let params = execute_params_none(); + + let mut raw_config = BLOCKCHAIN_CONFIG.raw_config().clone(); + raw_config + .set_config(ConfigParamEnum::ConfigParam5(BurningConfig { + blackhole_addr: None, + fee_burn_num: 0, + fee_burn_denom: 1, + })) + .unwrap(); + let regular_config = BlockchainConfig::with_config(raw_config.clone()).unwrap(); + + raw_config + .set_config(ConfigParamEnum::ConfigParam5(BurningConfig { + blackhole_addr: Some(acc_id.clone()), + fee_burn_num: 0, + fee_burn_denom: 1, + })) + .unwrap(); + let blackhole_config = BlockchainConfig::with_config(raw_config).unwrap(); + + let mut regular_acc = create_test_account_workchain( + start_balance, + -1, + acc_id.clone(), + code.clone(), + Cell::default(), + ); + let regular_before = regular_acc.clone(); + let regular_tr = + try_replay_transaction(&mut regular_acc, Some(&msg), regular_config, ¶ms).unwrap(); + check_account_and_transaction_balances(®ular_before, ®ular_acc, &msg, Some(®ular_tr)); + + let mut blackhole_acc = + create_test_account_workchain(start_balance, -1, acc_id.clone(), code, Cell::default()); + let blackhole_before = blackhole_acc.clone(); + let blackhole_tr = + try_replay_transaction(&mut blackhole_acc, Some(&msg), blackhole_config, ¶ms).unwrap(); + check_account_and_transaction_balances( + &blackhole_before, + &blackhole_acc, + &msg, + Some(&blackhole_tr), + ); + + assert_eq!(*regular_tr.blackhole_burned(), CurrencyCollection::default()); + assert_eq!(*blackhole_tr.blackhole_burned(), CurrencyCollection::with_coins(msg_value)); + assert_eq!( + regular_acc.balance().unwrap().coins.as_u128() + - blackhole_acc.balance().unwrap().coins.as_u128(), + msg_value as u128 + - (regular_tr.total_fees().coins.as_u128() - blackhole_tr.total_fees().coins.as_u128()) + ); + + let TransactionDescr::Ordinary(regular_descr) = regular_tr.read_description().unwrap() else { + panic!("ordinary description expected"); + }; + let TransactionDescr::Ordinary(blackhole_descr) = blackhole_tr.read_description().unwrap() + else { + panic!("ordinary description expected"); + }; + assert_eq!(regular_descr.credit_ph.unwrap().credit, CurrencyCollection::with_coins(msg_value)); + assert_eq!(blackhole_descr.credit_ph.unwrap().credit, CurrencyCollection::default()); +} diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index a502254..f958b41 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -1142,6 +1142,7 @@ impl ExecutionManager { msg_metadata, is_special, )?; + collator_data.value_flow.burned.add(tr.blackhole_burned())?; collator_data.update_lt(self.max_lt.load(Ordering::Relaxed)); @@ -2717,9 +2718,27 @@ impl Collator { } let shard_fees = collator_data.shard_fees().root_extra().clone(); + collator_data.value_flow.fees_imported = shard_fees.fees.clone(); - collator_data.value_flow.fees_collected.add(&shard_fees.fees)?; - collator_data.value_flow.fees_imported = shard_fees.fees; + let mut burned_imported = CurrencyCollection::default(); + if let Some(ConfigParamEnum::ConfigParam5(burning)) = mc_data.config().config(5)? { + if shard_fees.fees.coins.as_u128() < shard_fees.create.coins.as_u128() { + fail!( + "fees_imported is smaller than imported created fees: {} < {}", + shard_fees.fees.coins, + shard_fees.create.coins + ); + } + let imported_base = CurrencyCollection::from_coins(Coins::try_from( + shard_fees.fees.coins.as_u128() - shard_fees.create.coins.as_u128(), + )?); + burned_imported = burning.calculate_burned_fees(&imported_base)?; + } + collator_data.value_flow.burned.add(&burned_imported)?; + + let mut net_imported = collator_data.value_flow.fees_imported.clone(); + net_imported.sub(&burned_imported)?; + collator_data.value_flow.fees_collected.add(&net_imported)?; Ok(()) } @@ -3924,12 +3943,17 @@ impl Collator { let mut value_flow = collator_data.value_flow.clone(); value_flow.imported = collator_data.in_msgs.root_extra().value_imported.clone(); value_flow.exported = collator_data.out_msgs.root_extra().clone(); - value_flow.fees_collected = accounts.root_extra().clone(); - value_flow.fees_collected.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; + let mut total_fees = accounts.root_extra().clone(); + total_fees.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; - // value_flow.fees_collected.coins.add(&out_msg_dscr.root_extra().coins)?; // TODO: Why only coins? - - value_flow.fees_collected.add(&value_flow.fees_imported)?; + value_flow.fees_collected.add(&total_fees)?; + if self.shard.is_masterchain() { + if let Some(ConfigParamEnum::ConfigParam5(burning)) = mc_data.config().config(5)? { + let burned_master = burning.calculate_burned_fees(&total_fees)?; + value_flow.fees_collected.sub(&burned_master)?; + value_flow.burned.add(&burned_master)?; + } + } value_flow.fees_collected.add(&value_flow.created)?; value_flow.to_next_blk = new_accounts.full_balance().clone(); //value_flow.to_next_blk.add(&value_flow.recovered)?; diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 671ca8a..69b5239 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -41,16 +41,14 @@ use std::{ mem, sync::{ atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}, - Arc, + Arc, Mutex, }, time::Instant, }; #[cfg(test)] use ton_block::{base64_encode, write_boc, UsageTree}; -#[cfg(feature = "xp25")] -use ton_block::{fail, ShardDescr, SHARD_FULL}; use ton_block::{ - read_boc, Account, AccountBlock, AccountDispatchQueue, AccountId, AccountIdPrefixFull, + fail, read_boc, Account, AccountBlock, AccountDispatchQueue, AccountId, AccountIdPrefixFull, AccountStatus, AccountStorageDictProof, AddSub, Block, BlockCreateStats, BlockError, BlockExtra, BlockIdExt, BlockInfo, BlockLimits, Cell, CellType, Coins, ConfigParamEnum, ConfigParams, ConsensusExtraData, Counters, CreatorStats, CurrencyCollection, DepthBalanceInfo, @@ -63,6 +61,8 @@ use ton_block::{ TransactionDescr, UInt15, UInt256, ValidatorSet, ValueFlow, WorkchainDescr, INVALID_WORKCHAIN_ID, MASTERCHAIN_ID, MAX_SPLIT_DEPTH, }; +#[cfg(feature = "xp25")] +use ton_block::{ShardDescr, SHARD_FULL}; use ton_executor::{ BlockchainConfig, ExecuteParams, OrdinaryTransactionExecutor, TickTockTransactionExecutor, TransactionExecutor, @@ -115,6 +115,7 @@ struct ValidateResult { removed_dispatch_queue_messages: lockfree::map::Map<(AccountId, u64), Cell>, new_dispatch_queue_messages: lockfree::map::Map<(AccountId, u64), Cell>, account_expected_defer_all_messages: lockfree::set::Set, + blackhole_burned: Mutex, } impl Default for ValidateResult { @@ -135,6 +136,7 @@ impl Default for ValidateResult { removed_dispatch_queue_messages: lockfree::map::Map::new(), new_dispatch_queue_messages: lockfree::map::Map::new(), account_expected_defer_all_messages: lockfree::set::Set::new(), + blackhole_burned: Mutex::new(CurrencyCollection::default()), } } } @@ -2343,28 +2345,96 @@ impl ValidateQuery { ) } let transaction_fees = base.account_blocks.full_transaction_fees(); + let expected_fee_burned = Self::expected_fee_burned(&base, transaction_fees, &fees_import)?; + if !base.shard().is_masterchain() && !base.value_flow.burned.is_zero()? { + reject_query!( + "ValueFlow of block {} is invalid (non-zero burned value in a non-masterchain block)", + base.block_id() + ) + } + let mut expected_fees = transaction_fees.clone(); expected_fees.add(&base.value_flow.fees_imported)?; expected_fees.add(&base.value_flow.created)?; expected_fees.add(&fees_import)?; + expected_fees.sub(&expected_fee_burned)?; if base.value_flow.fees_collected != expected_fees { reject_query!( "ValueFlow for {} declares fees_collected={} but \ the total message import fees are {}, the total transaction fees are {}, \ creation fee for this block is {} and the total imported fees from shards \ - are {} with a total of {}", + are {}, the burned fees are {} with a total of {}", base.block_id(), base.value_flow.fees_collected.coins, fees_import, transaction_fees.coins, base.value_flow.created.coins, base.value_flow.fees_imported.coins, + expected_fee_burned.coins, expected_fees.coins ) } Ok(()) } + fn expected_fee_burned( + base: &ValidateBase, + transaction_fees: &CurrencyCollection, + fees_import: &CurrencyCollection, + ) -> Result { + if !base.shard().is_masterchain() { + return Ok(CurrencyCollection::default()); + } + let Some(ConfigParamEnum::ConfigParam5(burning)) = base.config_params.config(5)? else { + return Ok(CurrencyCollection::default()); + }; + + let mut total_fees = transaction_fees.clone(); + total_fees.add(fees_import)?; + let mut burned = burning.calculate_burned_fees(&total_fees)?; + + let mut imported_base = base.value_flow.fees_imported.clone(); + if !imported_base.sub(&base.mc_extra.fees().root_extra().create)? { + fail!( + "fees_imported ({}) is smaller than imported created fees ({})", + base.value_flow.fees_imported, + base.mc_extra.fees().root_extra().create + ); + } + let burned_imported = burning.calculate_burned_fees(&imported_base)?; + burned.add(&burned_imported)?; + Ok(burned) + } + + fn check_burned_value_flow(base: &ValidateBase) -> Result<()> { + if !base.shard().is_masterchain() { + return Ok(()); + } + let fees_import = + CurrencyCollection::from_coins(base.in_msg_descr.full_import_fees().fees_collected); + let mut expected_burned = Self::expected_fee_burned( + base, + base.account_blocks.full_transaction_fees(), + &fees_import, + )?; + let blackhole_burned = base + .result + .blackhole_burned + .lock() + .map_err(|_| error!("blackhole burned accumulator is poisoned"))? + .clone(); + expected_burned.add(&blackhole_burned)?; + if base.value_flow.burned != expected_burned { + reject_query!( + "ValueFlow of block {} declares burned fees {}, but the expected value is {}", + base.block_id(), + base.value_flow.burned.coins, + expected_burned.coins + ) + } + Ok(()) + } + // similar to Collator::compute_minted_amount() fn compute_minted_amount(base: &ValidateBase) -> Result { let mut to_mint = CurrencyCollection::default(); @@ -5395,6 +5465,7 @@ impl ValidateQuery { let old_account_root = account_root.clone(); #[cfg(test)] let mut our_trans = None; + let mut blackhole_burned = CurrencyCollection::default(); let mut error = None; match executor.execute_with_params(in_msg_cell, account, params) { Ok(mut trans_execute) => { @@ -5432,6 +5503,14 @@ impl ValidateQuery { base.gas_used.fetch_add(compute_ph.gas_used.as_u64(), Ordering::Relaxed); } base.transactions_executed.fetch_add(1, Ordering::Relaxed); + blackhole_burned = trans_execute.blackhole_burned().clone(); + if !blackhole_burned.is_zero()? { + base.result + .blackhole_burned + .lock() + .map_err(|_| error!("blackhole burned accumulator is poisoned"))? + .add(&blackhole_burned)?; + } // we cannot know prev transaction in executor trans_execute.set_prev_trans_hash(trans.prev_trans_hash().clone()); @@ -5457,18 +5536,20 @@ impl ValidateQuery { let mut right_balance = new_balance.clone(); right_balance.add(&money_exported)?; right_balance.add(trans.total_fees())?; + right_balance.add(&blackhole_burned)?; if left_balance != right_balance { error = Some(error!( "transaction {} of {:x} violates the currency flow condition: \ old balance={} + imported={} does not equal new balance={} + exported=\ - {} + total_fees={}", + {} + total_fees={} + burned={}", lt, account_addr, old_balance.coins, money_imported.coins, new_balance.coins, money_exported.coins, - trans.total_fees().coins + trans.total_fees().coins, + blackhole_burned.coins )); } } @@ -6801,6 +6882,7 @@ impl ValidateQuery { // Self::check_delivered_dequeued(&base, &manager)?; Self::check_all_ticktock_processed(&base)?; Self::check_message_processing_order(&mut base)?; + Self::check_burned_value_flow(&base)?; Self::check_new_state(&mut base, &mc_data, &manager)?; Self::check_mc_block_extra(&base, &mc_data)?; self.check_mc_state_extra(&base, &mc_data)?; From aee43addc6191513c1f71cd0c37badc63843ce20 Mon Sep 17 00:00:00 2001 From: Lapo4kaKek Date: Sat, 4 Apr 2026 11:14:18 +0300 Subject: [PATCH 37/48] fix: enforce mcStateExtra flags <=1 and remove ValidatorsStat --- src/block-json/src/serialize.rs | 15 --- src/block/src/master.rs | 31 +----- src/block/src/tests/test_master.rs | 6 -- src/block/src/tests/test_validators.rs | 13 --- src/block/src/validators.rs | 126 +------------------------ src/node/src/validator/collator.rs | 5 +- 6 files changed, 6 insertions(+), 190 deletions(-) diff --git a/src/block-json/src/serialize.rs b/src/block-json/src/serialize.rs index 1b8d70a..51ed955 100644 --- a/src/block-json/src/serialize.rs +++ b/src/block-json/src/serialize.rs @@ -763,14 +763,6 @@ fn serialize_out_msg(msg: &OutMsg, mode: SerializationMode) -> Result { Ok(map.into()) } -fn serialize_validators_stat(stat: &ValidatorsStat) -> Result { - let mut map = Map::new(); - for i in 0..stat.len() as u16 { - serialize_field(&mut map, &i.to_string(), stat.get(i)?); - } - Ok(map.into()) -} - fn serialize_shard_descr(descr: &ShardDescr, mode: SerializationMode) -> Result { let mut map = Map::new(); serialize_field(&mut map, "seq_no", descr.seq_no); @@ -1644,13 +1636,6 @@ fn serialize_mc_state_extra( serialize_block_create_stats(&mut extra_map, "block_create_stats", stats, mode)?; } serialize_cc(&mut extra_map, "global_balance", &extra.global_balance, mode)?; - if !extra.validators_stat.is_empty() { - serialize_field( - &mut extra_map, - "validators_unreliability", - serialize_validators_stat(&extra.validators_stat)?, - ); - } map.insert(id_str.to_string(), extra_map.into()); Ok(()) } diff --git a/src/block/src/master.rs b/src/block/src/master.rs index 2cbc4e9..0506bf2 100644 --- a/src/block/src/master.rs +++ b/src/block/src/master.rs @@ -21,7 +21,7 @@ use crate::{ shard::{AccountIdPrefixFull, ShardIdent, SHARD_FULL}, signature::CryptoSignaturePair, types::{ChildCell, CurrencyCollection, InRefValue}, - validators::{ValidatorInfo, ValidatorsStat}, + validators::ValidatorInfo, AccountId, Augmentation, BuilderData, Cell, Deserializable, IBitstring, Result, Serializable, SliceData, UInt256, }; @@ -395,7 +395,6 @@ pub struct McBlockExtra { recover_create_msg: Option>, mint_msg: Option>, config: Option, - validators_stat: ValidatorsStat, } impl McBlockExtra { @@ -479,18 +478,6 @@ impl McBlockExtra { pub fn mint_msg_cell(&self) -> Option { self.mint_msg.as_ref().map(|mr| mr.cell()) } - - pub fn validators_stat(&self) -> &ValidatorsStat { - &self.validators_stat - } - - pub fn validators_stat_mut(&mut self) -> &mut ValidatorsStat { - &mut self.validators_stat - } - - pub fn set_validators_stat(&mut self, stat: ValidatorsStat) { - self.validators_stat = stat; - } } const MC_BLOCK_EXTRA_TAG: u16 = 0xCCA5; // Original struct. @@ -1047,12 +1034,10 @@ pub struct McStateExtra { pub last_key_block: Option, pub block_create_stats: Option, pub global_balance: CurrencyCollection, - pub validators_stat: ValidatorsStat, } const MC_STATE_EXTRA_TAG: u16 = 0xcc26; const MC_STATE_CREATE_STATS_FLAG: u16 = 0b0001; -const MC_STATE_VAL_STAT_FLAG: u16 = 0b1000; impl McStateExtra { /// Adds new workchain @@ -1115,9 +1100,9 @@ impl Deserializable for McStateExtra { let cell1 = &mut SliceData::load_cell(cell.checked_drain_reference()?)?; let mut flags = 0u16; flags.read_from(cell1)?; // 16 + 0 - if flags > 15 { + if flags > 1 { fail!(BlockError::InvalidData(format!( - "Invalid flags value ({}). Must be <= 7.", + "Invalid flags value ({}). Must be <= 1.", flags ))) } @@ -1130,10 +1115,6 @@ impl Deserializable for McStateExtra { } else { Some(BlockCreateStats::construct_from(cell1)?) // 1 + 1 }; - let flag_val_stat = flags & MC_STATE_VAL_STAT_FLAG != 0; - if flag_val_stat { - self.validators_stat.read_from(cell1)?; - } self.global_balance.read_from(cell)?; Ok(()) } @@ -1150,9 +1131,6 @@ impl Serializable for McStateExtra { if self.block_create_stats.is_some() { flags |= MC_STATE_CREATE_STATS_FLAG; } - if !self.validators_stat.is_empty() { - flags |= MC_STATE_VAL_STAT_FLAG; - } flags.write_to(&mut builder1)?; self.validator_info.write_to(&mut builder1)?; self.prev_blocks.write_to(&mut builder1)?; @@ -1161,9 +1139,6 @@ impl Serializable for McStateExtra { if let Some(ref block_create_stats) = self.block_create_stats { block_create_stats.write_to(&mut builder1)?; } - if !self.validators_stat.is_empty() { - self.validators_stat.write_to(&mut builder1)?; - } builder.checked_append_reference(builder1.into_cell()?)?; self.global_balance.write_to(builder)?; diff --git a/src/block/src/tests/test_master.rs b/src/block/src/tests/test_master.rs index 150dd8d..a7b44bd 100644 --- a/src/block/src/tests/test_master.rs +++ b/src/block/src/tests/test_master.rs @@ -138,12 +138,6 @@ fn test_mc_state_extra() { .unwrap(); write_read_and_assert(extra.clone()); - - extra.validators_stat = ValidatorsStat::new(3); - extra.validators_stat.update(1, |_| 123).unwrap(); - extra.validators_stat.update(2, |_| 456).unwrap(); - - write_read_and_assert(extra.clone()); } fn build_mc_block_extra() -> McBlockExtra { diff --git a/src/block/src/tests/test_validators.rs b/src/block/src/tests/test_validators.rs index bc755e5..d5544a2 100644 --- a/src/block/src/tests/test_validators.rs +++ b/src/block/src/tests/test_validators.rs @@ -213,16 +213,3 @@ fn test_isolate_mc_validators() { } } } - -#[test] -fn test_validator_shard_stat() { - // std::env::set_var("RUST_BACKTRACE", "full"); - for len in 0..1024 { - println!("len = {}", len); - let mut stat = ValidatorsStat::default(); - for i in 0..len { - stat.values.push(i as u16); - } - write_read_and_assert(stat.clone()); - } -} diff --git a/src/block/src/validators.rs b/src/block/src/validators.rs index d42882b..d60ffb8 100644 --- a/src/block/src/validators.rs +++ b/src/block/src/validators.rs @@ -10,14 +10,14 @@ */ use crate::{ config_params::CatchainConfig, - define_HashmapE, error, + define_HashmapE, error::{BlockError, Result}, fail, sha512_digest, shard::{MASTERCHAIN_ID, SHARD_FULL}, signature::{CryptoSignature, SigPubKey}, types::Number16, BuilderData, ByteOrderRead, Cell, Crc32, Deserializable, IBitstring, KeyId, Serializable, - SliceData, UInt256, MAX_DATA_BITS, + SliceData, UInt256, }; use std::{ borrow::Cow, @@ -664,128 +664,6 @@ impl ValidatorSetPRNG { } } -const VALIDATORS_SHARD_STAT_TAG: u8 = 0x1; // 4 bits -const VALIDATORS_STAT_EXPECTED_MAX: usize = 256; - -#[derive(Clone, Debug, Eq, PartialEq, Default)] -pub struct ValidatorsStat { - // Single u16 value for each validator. - // VALIDATORS_STAT_EXPECTED_MAX values are stored inplace, - // if more - SmallVec just reallocates memory using heap. - values: smallvec::SmallVec<[u16; VALIDATORS_STAT_EXPECTED_MAX]>, -} - -impl ValidatorsStat { - pub fn new(validators_count: u16) -> Self { - ValidatorsStat { values: smallvec::smallvec![0; validators_count as usize] } - } - - pub fn is_empty(&self) -> bool { - self.values.is_empty() - } - - pub fn update(&mut self, validator_index: u16, updater: F) -> Result<()> - where - F: FnOnce(u16) -> u16, - { - if self.values.len() <= validator_index as usize { - fail!("Invalid validator index: {} (max is {})", validator_index, self.values.len() - 1) - } - self.values[validator_index as usize] = updater(self.values[validator_index as usize]); - Ok(()) - } - - pub fn get(&self, validator_index: u16) -> Result { - if self.values.is_empty() { - fail!("ValidatorsStat is empty") - } - self.values.get(validator_index as usize).copied().ok_or_else(|| { - error!( - "Invalid validator index: {} (max is {})", - validator_index, - self.values.len() - 1 - ) - }) - } - - pub fn len(&self) -> usize { - self.values.len() - } -} - -impl Serializable for ValidatorsStat { - fn write_to(&self, builder: &mut BuilderData) -> Result<()> { - builder.append_bits(VALIDATORS_SHARD_STAT_TAG as usize, 4)?; - - // Items are wrote one by one into cell. If the cell is full, - // the rest of the items are wrote into the child cell, etc. - - let mut remaining_len = self.values.len(); - if remaining_len == 0 { - return Ok(()); - } - let mut stack = vec![builder.clone()]; - loop { - // Calculate how many items can be written to the current builder - let builder_fits = - stack.last().ok_or_else(|| error!("INTERNAL ERROR: stack is empty"))?.bits_free() - / 16; - - // If the current builder can fit all remaining items - finish - if builder_fits >= remaining_len { - break; - } else { - // If not - create one more builder and push it to the stack - remaining_len = remaining_len.saturating_sub(builder_fits); - stack.push(BuilderData::new()); - } - } - - // Write items to the builders from the last (deeper) cell to the first. - let mut start = self.values.len().saturating_sub(remaining_len); - while let Some(mut last_builder) = stack.pop() { - // Fill a builder - let builder_fits = last_builder.bits_free() / 16; - for i in start..min(start + builder_fits, self.values.len()) { - last_builder.append_u16(self.values[i])?; - } - - // Move start to the start of the next builder (minus one cell of items) - start = start.saturating_sub(MAX_DATA_BITS / 16); - - if let Some(prev_builder) = stack.last_mut() { - prev_builder.checked_append_reference(last_builder.into_cell()?)?; - } else { - *builder = last_builder; - } - } - - Ok(()) - } -} - -impl Deserializable for ValidatorsStat { - fn read_from(&mut self, slice: &mut SliceData) -> Result<()> { - let tag = slice.get_next_int(4)? as u8; - if tag != VALIDATORS_SHARD_STAT_TAG { - fail!("Invalid tag for ValidatorsShardStat: {}", tag) - } - self.values.clear(); - while slice.remaining_bits() > 0 { - self.values.push(u16::construct_from(slice)?); - } - let mut next_cell_opt = slice.checked_drain_reference().ok(); - while let Some(next_cell) = next_cell_opt { - let mut slice = SliceData::load_cell(next_cell)?; - while slice.remaining_bits() > 0 { - self.values.push(u16::construct_from(&mut slice)?); - } - next_cell_opt = slice.checked_drain_reference().ok(); - } - Ok(()) - } -} - #[cfg(test)] #[path = "tests/test_validators.rs"] mod tests; diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index a502254..b637dae 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -59,7 +59,7 @@ use ton_block::{ Serializable, ShardAccount, ShardAccountBlocks, ShardAccounts, ShardDescr, ShardFees, ShardHashes, ShardIdent, ShardStateSplit, ShardStateUnsplit, SliceData, StorageStatDict, TopBlockDescrSet, Transaction, TransactionTickTock, UInt256, UsageTree, ValidatorSet, - ValidatorsStat, ValueFlow, WorkchainDescr, Workchains, MASTERCHAIN_ID, + ValueFlow, WorkchainDescr, Workchains, MASTERCHAIN_ID, }; #[cfg(feature = "xp25")] use ton_block::{RefShardBlocks, ShardBlockRef, WcExtra}; @@ -4408,8 +4408,6 @@ impl Collator { None }; - let validators_stat = ValidatorsStat::default(); - Ok(( McStateExtra { shards: collator_data.shards()?.clone(), @@ -4420,7 +4418,6 @@ impl Collator { last_key_block, block_create_stats, global_balance, - validators_stat, }, min_ref_mc_seqno, )) From e4fbe0b4d81d78c7ab824826affaaaa566bc3d6a Mon Sep 17 00:00:00 2001 From: Lapo4kaKek Date: Sat, 4 Apr 2026 13:55:14 +0300 Subject: [PATCH 38/48] fix(executor): preserve original due_payment for special accounts in partial storage phase --- .../src/tests/test_ordinary_transaction.rs | 98 +++++++++++++++++++ src/executor/src/transaction_executor.rs | 3 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/executor/src/tests/test_ordinary_transaction.rs b/src/executor/src/tests/test_ordinary_transaction.rs index 9682482..54b9dff 100644 --- a/src/executor/src/tests/test_ordinary_transaction.rs +++ b/src/executor/src/tests/test_ordinary_transaction.rs @@ -4067,3 +4067,101 @@ fn test_new_bounce() { .unwrap() ); } + +#[test] +fn special_account_due_payment_fully_covered() { + // Special active account with due_payment > 0 and sufficient balance. + // C++ collects due_payment from balance and clears it. + let code = compile_code_to_cell("ACCEPT").unwrap(); + let acc_id = THIRD_ACCOUNT.clone(); + + let start_balance = 1_000_000_000u64; + let due = 100_000_000u64; + let msg_income = 14_200_000u64; + + let mut acc = create_test_account(start_balance, acc_id.clone(), code, Cell::default()); + acc.set_due_payment(Some(due.into())); + let addr = acc.get_addr().unwrap(); + assert!(BLOCKCHAIN_CONFIG.is_special_account(addr.is_masterchain(), addr.address()).unwrap()); + + let msg = create_int_msg(THIRD_ACCOUNT.clone(), acc_id, msg_income, true, BLOCK_LT - 2); + let params = execute_params(BLOCK_LT + 1); + let trans = execute_with_params( + SIMPLE_MC_STATE.to_owned(), + Some(msg.serialize().unwrap()), + &mut acc, + ¶ms, + ) + .unwrap(); + + let storage = get_tr_descr(&trans).storage_ph.unwrap(); + assert_eq!(storage.storage_fees_collected, Coins::from(due)); + assert_eq!(storage.storage_fees_due, None); + assert_eq!(storage.status_change, AccStatusChange::Unchanged); + assert_eq!(acc.due_payment(), None); + assert_eq!(acc.last_paid(), 0); +} + +#[test] +fn special_account_due_payment_partial() { + // Special active account where balance < due_payment. + // C++ collects the full balance but keeps the ORIGINAL due_payment on the account + // (Transaction::due_payment is never updated for special in the partial path). + let code = compile_code_to_cell("ACCEPT").unwrap(); + let acc_id = THIRD_ACCOUNT.clone(); + + let start_balance = 50_000_000u64; + let due = 100_000_000u64; + let msg_income = 14_200_000u64; + + let mut acc = create_test_account(start_balance, acc_id.clone(), code, Cell::default()); + acc.set_due_payment(Some(due.into())); + + let msg = create_int_msg(THIRD_ACCOUNT.clone(), acc_id, msg_income, true, BLOCK_LT - 2); + let params = execute_params(BLOCK_LT + 1); + let trans = execute_with_params( + SIMPLE_MC_STATE.to_owned(), + Some(msg.serialize().unwrap()), + &mut acc, + ¶ms, + ) + .unwrap(); + + let storage = get_tr_descr(&trans).storage_ph.unwrap(); + assert_eq!(storage.storage_fees_collected, Coins::from(start_balance)); + assert_eq!(storage.storage_fees_due, Some(Coins::from(due - start_balance))); + assert_eq!(storage.status_change, AccStatusChange::Unchanged); + assert_eq!(acc.due_payment(), Some(&Coins::from(due))); + assert_eq!(acc.last_paid(), 0); +} + +#[test] +fn special_account_due_payment_zero_balance() { + // Special active account with due_payment > 0 and balance = 0. + // Nothing to collect, due_payment stays at original. + let code = compile_code_to_cell("ACCEPT").unwrap(); + let acc_id = THIRD_ACCOUNT.clone(); + + let due = 100_000_000u64; + let msg_income = 14_200_000u64; + + let mut acc = create_test_account(0u64, acc_id.clone(), code, Cell::default()); + acc.set_due_payment(Some(due.into())); + + let msg = create_int_msg(THIRD_ACCOUNT.clone(), acc_id, msg_income, true, BLOCK_LT - 2); + let params = execute_params(BLOCK_LT + 1); + let trans = execute_with_params( + SIMPLE_MC_STATE.to_owned(), + Some(msg.serialize().unwrap()), + &mut acc, + ¶ms, + ) + .unwrap(); + + let storage = get_tr_descr(&trans).storage_ph.unwrap(); + assert_eq!(storage.storage_fees_collected, Coins::zero()); + assert_eq!(storage.storage_fees_due, Some(Coins::from(due))); + assert_eq!(storage.status_change, AccStatusChange::Unchanged); + assert_eq!(acc.due_payment(), Some(&Coins::from(due))); + assert_eq!(acc.last_paid(), 0); +} diff --git a/src/executor/src/transaction_executor.rs b/src/executor/src/transaction_executor.rs index 595d2e4..1c7071d 100644 --- a/src/executor/src/transaction_executor.rs +++ b/src/executor/src/transaction_executor.rs @@ -160,6 +160,7 @@ pub trait TransactionExecutor { if tr.now() < acc.last_paid() { fail!("transaction timestamp must be greater then account timestamp") } + let original_due_payment = acc.due_payment().cloned(); let mut fee = match acc.storage_info() { Some(storage_info) if !is_special => { self.config().calc_storage_fees(storage_info, is_masterchain, tr.now())? @@ -183,7 +184,7 @@ pub trait TransactionExecutor { fee.sub(&storage_fees_collected)?; if is_special { log::debug!(target: "executor", "special account, due payment {fee} still active"); - acc.set_due_payment(Some(fee)); + acc.set_due_payment(original_due_payment); return Ok(TrStoragePhase::with_params( storage_fees_collected, Some(fee), From 8fb327f9204db0891966edde96e4a5d287f6e7c2 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Sat, 4 Apr 2026 15:58:16 +0300 Subject: [PATCH 39/48] Implement burning coins --- src/block/src/config_params.rs | 16 ++- src/block/src/transactions.rs | 12 +- src/executor/src/blockchain_config.rs | 18 ++- src/executor/src/ordinary_transaction.rs | 32 +++-- src/executor/src/tests/common/mod.rs | 4 +- .../src/tests/test_ordinary_transaction.rs | 4 +- src/node/src/validator/collator.rs | 109 ++++++++---------- src/node/src/validator/validate_query.rs | 43 ++++--- 8 files changed, 114 insertions(+), 124 deletions(-) diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index 0bf905b..b292be9 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -19,9 +19,9 @@ use crate::{ ChildCell, Coins, ExtraCurrencyCollection, Number12, Number13, Number16, Number32, Number8, }, validators::{ValidatorDescr, ValidatorSet}, - AccountId, BlockError, BuilderData, Cell, CurrencyCollection, Deserializable, HashmapE, - HashmapIterator, HashmapType, IBitstring, MsgAddressInt, Result, Serializable, SliceData, - UInt256, BASE_WORKCHAIN_ID, MAX_SPLIT_DEPTH, + AccountId, BlockError, BuilderData, Cell, Deserializable, HashmapE, HashmapIterator, + HashmapType, IBitstring, MsgAddressInt, Result, Serializable, SliceData, UInt256, + BASE_WORKCHAIN_ID, MAX_SPLIT_DEPTH, }; use num::BigInt; use std::collections::BTreeMap; @@ -939,13 +939,11 @@ impl BurningConfig { Ok(()) } - pub fn calculate_burned_fees(&self, value: &CurrencyCollection) -> Result { - if self.fee_burn_num == 0 || value.coins.is_zero() { - return Ok(CurrencyCollection::default()); + pub fn calculate_burned_fees(&self, value: u128) -> Result { + if self.fee_burn_num == 0 || value == 0 { + return Ok(Coins::default()); } - let burned = - value.coins.as_u128() * u128::from(self.fee_burn_num) / u128::from(self.fee_burn_denom); - Ok(CurrencyCollection::from_coins(Coins::try_from(burned)?)) + (value * self.fee_burn_num as u128 / self.fee_burn_denom as u128).try_into() } } diff --git a/src/block/src/transactions.rs b/src/block/src/transactions.rs index e7c6ab2..2d6883b 100644 --- a/src/block/src/transactions.rs +++ b/src/block/src/transactions.rs @@ -1265,7 +1265,7 @@ pub struct Transaction { pub in_msg: ChildCell, pub out_msgs: OutMessages, total_fees: CurrencyCollection, - blackhole_burned: CurrencyCollection, + blackhole_burned: Coins, state_update: ChildCell, description: ChildCell, } @@ -1286,7 +1286,7 @@ impl Transaction { in_msg: ChildCell::default(), out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), - blackhole_burned: CurrencyCollection::default(), + blackhole_burned: Coins::default(), state_update: ChildCell::default(), description: ChildCell::default(), } @@ -1310,7 +1310,7 @@ impl Transaction { in_msg: ChildCell::with_struct(msg)?, out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), - blackhole_burned: CurrencyCollection::default(), + blackhole_burned: Coins::default(), state_update: ChildCell::default(), description: ChildCell::default(), }) @@ -1374,11 +1374,11 @@ impl Transaction { &mut self.total_fees } - pub fn blackhole_burned(&self) -> &CurrencyCollection { + pub fn blackhole_burned(&self) -> &Coins { &self.blackhole_burned } - pub fn set_blackhole_burned(&mut self, burned: CurrencyCollection) { + pub fn set_blackhole_burned(&mut self, burned: Coins) { self.blackhole_burned = burned; } @@ -1568,7 +1568,7 @@ impl Default for Transaction { in_msg: ChildCell::default(), out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), - blackhole_burned: CurrencyCollection::default(), + blackhole_burned: Coins::default(), state_update: ChildCell::default(), description: ChildCell::default(), } diff --git a/src/executor/src/blockchain_config.rs b/src/executor/src/blockchain_config.rs index 9b85324..e593b0e 100644 --- a/src/executor/src/blockchain_config.rs +++ b/src/executor/src/blockchain_config.rs @@ -10,9 +10,10 @@ */ use num::BigInt; use ton_block::{ - fail, AccountId, Coins, ConfigParam18, ConfigParams, FundamentalSmcAddresses, GasLimitsPrices, - GlobalCapabilities, Mask, MsgAddressInt, MsgForwardPrices, Result, SizeLimitsConfig, - StorageInfo, StoragePrices, UInt256, SUPPORTED_VERSION, + fail, AccountId, BurningConfig, Coins, ConfigParam18, ConfigParamEnum, ConfigParams, + FundamentalSmcAddresses, GasLimitsPrices, GlobalCapabilities, Mask, MsgAddressInt, + MsgForwardPrices, Result, SizeLimitsConfig, StorageInfo, StoragePrices, UInt256, + SUPPORTED_VERSION, }; pub(crate) trait DefaultConfig { @@ -147,6 +148,7 @@ pub struct BlockchainConfig { fwd_prices_mc: MsgForwardPrices, fwd_prices_wc: MsgForwardPrices, storage_prices: AccStoragePrices, + burning_cfg: Option, special_contracts: FundamentalSmcAddresses, limits: SizeLimitsConfig, capabilities: u64, @@ -164,6 +166,7 @@ impl Default for BlockchainConfig { fwd_prices_mc: MsgForwardPrices::default_mc(), fwd_prices_wc: MsgForwardPrices::default_wc(), storage_prices: AccStoragePrices::default(), + burning_cfg: None, special_contracts: Self::get_default_special_contracts(), limits: Default::default(), raw_config: Self::get_defult_raw_config(), @@ -206,12 +209,17 @@ impl BlockchainConfig { log::debug!( "Creating BlockchainConfig: capabilities={capabilities:#x}, block_version={global_version}" ); + let burning_cfg = match config.config(5)? { + Some(ConfigParamEnum::ConfigParam5(burning_cfg)) => Some(burning_cfg), + _ => None, + }; Ok(BlockchainConfig { gas_prices_mc: config.gas_prices(true)?, gas_prices_wc: config.gas_prices(false)?, fwd_prices_mc: config.fwd_prices(true)?, fwd_prices_wc: config.fwd_prices(false)?, storage_prices: AccStoragePrices::with_config(&config.storage_prices()?)?, + burning_cfg, limits: config.size_limits_config()?, special_contracts: config.fundamental_smc_addr()?, capabilities, @@ -234,6 +242,10 @@ impl BlockchainConfig { &self.limits } + pub fn burning_config(&self) -> Option<&BurningConfig> { + self.burning_cfg.as_ref() + } + /// Calculate gas fee for account pub fn calc_gas_fee(&self, gas_used: u64, address: &MsgAddressInt) -> u128 { self.get_gas_config(address.is_masterchain()).calc_gas_fee(gas_used) diff --git a/src/executor/src/ordinary_transaction.rs b/src/executor/src/ordinary_transaction.rs index 485bc9e..c80c2bb 100644 --- a/src/executor/src/ordinary_transaction.rs +++ b/src/executor/src/ordinary_transaction.rs @@ -18,9 +18,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; use ton_block::{ error, fail, AccStatusChange, Account, AddSub, Cell, Coins, CommonMsgInfo, ComputeSkipReason, - ConfigParamEnum, CurrencyCollection, Deserializable, Message, MsgAddressInt, Result, - Serializable, StorageUsageCalc, TrBouncePhase, TrComputePhase, Transaction, TransactionDescr, - TransactionDescrOrdinary, MASTERCHAIN_ID, MAX_MSG_MERKLE_DEPTH, + Deserializable, Message, MsgAddressInt, Result, Serializable, StorageUsageCalc, TrBouncePhase, + TrComputePhase, Transaction, TransactionDescr, TransactionDescrOrdinary, MASTERCHAIN_ID, + MAX_MSG_MERKLE_DEPTH, }; use ton_vm::{ boolean, int, @@ -178,23 +178,17 @@ impl TransactionExecutor for OrdinaryTransactionExecutor { tr.add_fee_coins(&in_fwd_fee)?; } - if is_masterchain { - if let Some(ConfigParamEnum::ConfigParam5(burning)) = - self.config.raw_config().config(5)? + if let Some(burning_cfg) = self.config.burning_config() { + if is_masterchain + && !msg_balance.coins.is_zero() + && burning_cfg.blackhole_addr.as_ref() == Some(&account_id) { - if burning.blackhole_addr.as_ref() == Some(&account_id) - && !msg_balance.coins.is_zero() - { - let burned = - CurrencyCollection::from_coins(std::mem::take(&mut msg_balance.coins)); - log::debug!( - target: "executor", - "Burning {} nanoton for blackhole account {:x}", - burned.coins, - account_id - ); - tr.set_blackhole_burned(burned); - } + let burned = std::mem::take(&mut msg_balance.coins); + log::debug!( + target: "executor", + "Burning {burned} nanocoins for blackhole account {account_id:x}", + ); + tr.set_blackhole_burned(burned); } } diff --git a/src/executor/src/tests/common/mod.rs b/src/executor/src/tests/common/mod.rs index cb9ddc8..320b637 100644 --- a/src/executor/src/tests/common/mod.rs +++ b/src/executor/src/tests/common/mod.rs @@ -320,7 +320,7 @@ pub fn check_account_and_transaction_balances( let mut right = acc_after.balance().cloned().unwrap_or_default(); right.add(trans.total_fees()).unwrap(); - right.add(trans.blackhole_burned()).unwrap(); + right.coins.add(trans.blackhole_burned()).unwrap(); trans .iterate_out_msgs(|out_msg| { if let Some(header) = out_msg.int_header() { @@ -882,7 +882,7 @@ pub fn replay_transaction( let mut right = account.balance().cloned().unwrap_or_default().coins; right.add(&our_transaction.total_fees().coins).unwrap(); - right.add(&our_transaction.blackhole_burned().coins).unwrap(); + right.add(&our_transaction.blackhole_burned()).unwrap(); our_transaction .iterate_out_msgs(|out_msg| { if let Some(header) = out_msg.int_header() { diff --git a/src/executor/src/tests/test_ordinary_transaction.rs b/src/executor/src/tests/test_ordinary_transaction.rs index dc7b1b5..5cdc6a8 100644 --- a/src/executor/src/tests/test_ordinary_transaction.rs +++ b/src/executor/src/tests/test_ordinary_transaction.rs @@ -4127,8 +4127,8 @@ fn test_masterchain_blackhole_burns_inbound_value() { Some(&blackhole_tr), ); - assert_eq!(*regular_tr.blackhole_burned(), CurrencyCollection::default()); - assert_eq!(*blackhole_tr.blackhole_burned(), CurrencyCollection::with_coins(msg_value)); + assert_eq!(*regular_tr.blackhole_burned(), Coins::default()); + assert_eq!(*blackhole_tr.blackhole_burned(), msg_value); assert_eq!( regular_acc.balance().unwrap().coins.as_u128() - blackhole_acc.balance().unwrap().coins.as_u128(), diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index f958b41..b330919 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -1142,7 +1142,9 @@ impl ExecutionManager { msg_metadata, is_special, )?; - collator_data.value_flow.burned.add(tr.blackhole_burned())?; + if !tr.blackhole_burned().is_zero() { + collator_data.value_flow.burned.coins.add(tr.blackhole_burned())?; + } collator_data.update_lt(self.max_lt.load(Ordering::Relaxed)); @@ -1624,7 +1626,7 @@ impl Collator { self.after_split, false, &self.prev_blocks_ids, - mc_data.config(), + collator_data.config.raw_config(), mc_data.mc_state_extra(), false, now, @@ -1644,7 +1646,7 @@ impl Collator { self.collator_settings.is_fake, )?; - self.check_utime(&mc_data, &prev_data, &mut collator_data)?; + self.check_utime(&prev_data, &mut collator_data)?; if is_masterchain { self.adjust_shard_config(&mc_data, &mut collator_data)?; @@ -1768,16 +1770,9 @@ impl Collator { // tick & special transactions if self.shard.is_masterchain() { - self.create_ticktock_transactions( - false, - mc_data, - prev_data, - collator_data, - &mut exec_manager, - ) - .await?; - self.create_special_transactions(mc_data, prev_data, collator_data, &mut exec_manager) + self.create_ticktock_transactions(false, prev_data, collator_data, &mut exec_manager) .await?; + self.create_special_transactions(prev_data, collator_data, &mut exec_manager).await?; } // merge prepare / merge install @@ -1892,14 +1887,8 @@ impl Collator { // tock transactions if self.shard.is_masterchain() { - self.create_ticktock_transactions( - true, - mc_data, - prev_data, - collator_data, - &mut exec_manager, - ) - .await?; + self.create_ticktock_transactions(true, prev_data, collator_data, &mut exec_manager) + .await?; } // process newly-generated messages (only by including them into output queue) @@ -2246,12 +2235,7 @@ impl Collator { (gen_utime, gen_utime_ms) } - fn check_utime( - &self, - mc_data: &McData, - prev_data: &PrevData, - collator_data: &mut CollatorData, - ) -> Result<()> { + fn check_utime(&self, prev_data: &PrevData, collator_data: &mut CollatorData) -> Result<()> { let now = collator_data.gen_utime; if now > collator_data.now_upper_limit() { fail!( @@ -2262,7 +2246,7 @@ impl Collator { // check whether masterchain catchain rotation is overdue let prev_now = prev_data.prev_state_utime(); - let ccvc = mc_data.config().catchain_config()?; + let ccvc = collator_data.config.raw_config().catchain_config()?; let lifetime = ccvc.mc_catchain_lifetime; if self.shard.is_masterchain() && now / lifetime > prev_now / lifetime @@ -2391,7 +2375,7 @@ impl Collator { log::trace!("{}: adjust_shard_config", self.collated_block_descr); CHECK!(self.shard.is_masterchain()); collator_data.set_shards(mc_data.state().shards()?.clone())?; - let wc_set = mc_data.config().workchains()?; + let wc_set = collator_data.config.raw_config().workchains()?; wc_set.iterate_with_keys(|wc_id: i32, wc_info| { log::trace!( " @@ -2446,7 +2430,8 @@ impl Collator { let mut cancelled = false; let mut new_shard_descrs = collator_data.shards()?.clone(); - let lt_limit = prev_data.prev_state_lt() + mc_data.config().get_max_lt_growth(); + let lt_limit = + prev_data.prev_state_lt() + collator_data.config.raw_config().get_max_lt_growth(); shard_top_blocks.sort_by(|a, b| cmp_shard_block_descr(a, b)); let mut shards_updated = HashSet::new(); let mut tb_act = 0; @@ -2718,27 +2703,25 @@ impl Collator { } let shard_fees = collator_data.shard_fees().root_extra().clone(); - collator_data.value_flow.fees_imported = shard_fees.fees.clone(); - let mut burned_imported = CurrencyCollection::default(); - if let Some(ConfigParamEnum::ConfigParam5(burning)) = mc_data.config().config(5)? { - if shard_fees.fees.coins.as_u128() < shard_fees.create.coins.as_u128() { + collator_data.value_flow.fees_collected.add(&shard_fees.fees)?; + if let Some(burning_cfg) = collator_data.config.burning_config() { + let Some(imported_base) = + shard_fees.fees.coins.as_u128().checked_sub(shard_fees.create.coins.as_u128()) + else { fail!( "fees_imported is smaller than imported created fees: {} < {}", shard_fees.fees.coins, shard_fees.create.coins - ); + ) + }; + let burned = burning_cfg.calculate_burned_fees(imported_base)?; + if !burned.is_zero() { + collator_data.value_flow.burned.coins.add(&burned)?; + collator_data.value_flow.fees_collected.coins.add(&burned)?; } - let imported_base = CurrencyCollection::from_coins(Coins::try_from( - shard_fees.fees.coins.as_u128() - shard_fees.create.coins.as_u128(), - )?); - burned_imported = burning.calculate_burned_fees(&imported_base)?; } - collator_data.value_flow.burned.add(&burned_imported)?; - - let mut net_imported = collator_data.value_flow.fees_imported.clone(); - net_imported.sub(&burned_imported)?; - collator_data.value_flow.fees_collected.add(&net_imported)?; + collator_data.value_flow.fees_imported = shard_fees.fees; Ok(()) } @@ -2969,7 +2952,8 @@ impl Collator { log::trace!("{}: update_value_flow", self.collated_block_descr); if self.shard.is_masterchain() { - collator_data.value_flow.created.coins = mc_data.config().block_create_fees(true)?; + collator_data.value_flow.created.coins = + collator_data.config.raw_config().block_create_fees(true)?; collator_data.value_flow.recovered = collator_data.value_flow.created.clone(); collator_data.value_flow.recovered.add(&collator_data.value_flow.fees_collected)?; @@ -2978,7 +2962,7 @@ impl Collator { .recovered .add(mc_data.state().state()?.total_validator_fees())?; - match mc_data.config().fee_collector_address() { + match collator_data.config.raw_config().fee_collector_address() { Err(_) => { log::debug!( "{}: fee recovery disabled \ @@ -2999,10 +2983,10 @@ impl Collator { } }; - collator_data.value_flow.minted = self.compute_minted_amount(mc_data)?; + collator_data.value_flow.minted = self.compute_minted_amount(mc_data, collator_data)?; if !collator_data.value_flow.minted.is_zero()? - && mc_data.config().minter_address().is_err() + && collator_data.config.raw_config().minter_address().is_err() { log::warn!( "{}: minting of {} disabled: no minting smart contract defined", @@ -3012,20 +2996,25 @@ impl Collator { collator_data.value_flow.minted = CurrencyCollection::default(); } } else { - collator_data.value_flow.created.coins = mc_data.config().block_create_fees(false)?; + collator_data.value_flow.created.coins = + collator_data.config.raw_config().block_create_fees(false)?; collator_data.value_flow.created.coins >>= self.shard.prefix_len(); } collator_data.value_flow.from_prev_blk = prev_data.total_balance().clone(); Ok(()) } - fn compute_minted_amount(&self, mc_data: &McData) -> Result { + fn compute_minted_amount( + &self, + mc_data: &McData, + collator_data: &CollatorData, + ) -> Result { log::trace!("{}: compute_minted_amount", self.collated_block_descr); CHECK!(self.shard.is_masterchain()); let mut to_mint = CurrencyCollection::default(); - let to_mint_cp = match mc_data.config().to_mint() { + let to_mint_cp = match collator_data.config.raw_config().to_mint() { Err(e) => { log::warn!( "{}: Can't get config param 7 (to_mint): {}", @@ -3285,13 +3274,12 @@ impl Collator { async fn create_ticktock_transactions( &self, tock: bool, - mc_data: &McData, prev_data: &PrevData, collator_data: &mut CollatorData, exec_manager: &mut ExecutionManager, ) -> Result<()> { log::trace!("{}: create_ticktock_transactions", self.collated_block_descr); - let fundamental_dict = mc_data.config().fundamental_smc_addr()?; + let fundamental_dict = collator_data.config.raw_config().fundamental_smc_addr()?; for res in &fundamental_dict { let account_id = SliceData::load_bitstring(res?.0)?; self.create_ticktock_transaction( @@ -3304,7 +3292,7 @@ impl Collator { .await?; self.check_stop_flag()?; } - let account_id = mc_data.config().config_addr.clone(); + let account_id = collator_data.config.raw_config().config_addr.clone(); self.create_ticktock_transaction(account_id, tock, prev_data, collator_data, exec_manager) .await?; exec_manager.wait_transactions(collator_data).await?; @@ -3353,7 +3341,6 @@ impl Collator { async fn create_special_transactions( &self, - mc_data: &McData, prev_data: &PrevData, collator_data: &mut CollatorData, exec_manager: &mut ExecutionManager, @@ -3363,7 +3350,7 @@ impl Collator { } log::debug!("{}: create_special_transactions", self.collated_block_descr); - let account_id = mc_data.config().fee_collector_address()?; + let account_id = collator_data.config.raw_config().fee_collector_address()?; self.create_special_transaction( account_id, collator_data.value_flow.recovered.clone(), @@ -3375,7 +3362,7 @@ impl Collator { .await?; self.check_stop_flag()?; - let account_id = mc_data.config().minter_address()?; + let account_id = collator_data.config.raw_config().minter_address()?; self.create_special_transaction( account_id, collator_data.value_flow.minted.clone(), @@ -3948,10 +3935,10 @@ impl Collator { value_flow.fees_collected.add(&total_fees)?; if self.shard.is_masterchain() { - if let Some(ConfigParamEnum::ConfigParam5(burning)) = mc_data.config().config(5)? { - let burned_master = burning.calculate_burned_fees(&total_fees)?; - value_flow.fees_collected.sub(&burned_master)?; - value_flow.burned.add(&burned_master)?; + if let Some(burning_cfg) = collator_data.config.burning_config() { + let burned = burning_cfg.calculate_burned_fees(total_fees.coins.as_u128())?; + value_flow.fees_collected.coins.sub(&burned)?; + value_flow.burned.coins.add(&burned)?; } } value_flow.fees_collected.add(&value_flow.created)?; @@ -4001,7 +3988,7 @@ impl Collator { info.set_prev_key_block_seqno(mc_data.prev_key_block_seqno()); info.write_master_ref(master_ref.as_ref())?; - if mc_data.config().has_capability(GlobalCapabilities::CapReportVersion) { + if collator_data.config.raw_config().has_capability(GlobalCapabilities::CapReportVersion) { info.set_gen_software(Some(GlobalVersion { version: supported_version(), capabilities: supported_capabilities(), diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 69b5239..4c76662 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -115,7 +115,7 @@ struct ValidateResult { removed_dispatch_queue_messages: lockfree::map::Map<(AccountId, u64), Cell>, new_dispatch_queue_messages: lockfree::map::Map<(AccountId, u64), Cell>, account_expected_defer_all_messages: lockfree::set::Set, - blackhole_burned: Mutex, + blackhole_burned: Mutex, } impl Default for ValidateResult { @@ -136,7 +136,7 @@ impl Default for ValidateResult { removed_dispatch_queue_messages: lockfree::map::Map::new(), new_dispatch_queue_messages: lockfree::map::Map::new(), account_expected_defer_all_messages: lockfree::set::Set::new(), - blackhole_burned: Mutex::new(CurrencyCollection::default()), + blackhole_burned: Mutex::new(Coins::default()), } } } @@ -2383,15 +2383,14 @@ impl ValidateQuery { fees_import: &CurrencyCollection, ) -> Result { if !base.shard().is_masterchain() { - return Ok(CurrencyCollection::default()); + return Ok(Default::default()); } - let Some(ConfigParamEnum::ConfigParam5(burning)) = base.config_params.config(5)? else { - return Ok(CurrencyCollection::default()); + let Some(ConfigParamEnum::ConfigParam5(burning_cfg)) = base.config_params.config(5)? else { + return Ok(Default::default()); }; - let mut total_fees = transaction_fees.clone(); - total_fees.add(fees_import)?; - let mut burned = burning.calculate_burned_fees(&total_fees)?; + let total_fees = transaction_fees.coins.as_u128() + fees_import.coins.as_u128(); + let mut burned = burning_cfg.calculate_burned_fees(total_fees)?; let mut imported_base = base.value_flow.fees_imported.clone(); if !imported_base.sub(&base.mc_extra.fees().root_extra().create)? { @@ -2401,9 +2400,9 @@ impl ValidateQuery { base.mc_extra.fees().root_extra().create ); } - let burned_imported = burning.calculate_burned_fees(&imported_base)?; + let burned_imported = burning_cfg.calculate_burned_fees(imported_base.coins.as_u128())?; burned.add(&burned_imported)?; - Ok(burned) + Ok(CurrencyCollection::from_coins(burned)) } fn check_burned_value_flow(base: &ValidateBase) -> Result<()> { @@ -2417,19 +2416,19 @@ impl ValidateQuery { base.account_blocks.full_transaction_fees(), &fees_import, )?; - let blackhole_burned = base - .result - .blackhole_burned - .lock() - .map_err(|_| error!("blackhole burned accumulator is poisoned"))? - .clone(); - expected_burned.add(&blackhole_burned)?; + expected_burned.coins.add( + &*base + .result + .blackhole_burned + .lock() + .map_err(|_| error!("blackhole burned accumulator is poisoned"))?, + )?; if base.value_flow.burned != expected_burned { reject_query!( "ValueFlow of block {} declares burned fees {}, but the expected value is {}", base.block_id(), base.value_flow.burned.coins, - expected_burned.coins + expected_burned ) } Ok(()) @@ -5465,7 +5464,7 @@ impl ValidateQuery { let old_account_root = account_root.clone(); #[cfg(test)] let mut our_trans = None; - let mut blackhole_burned = CurrencyCollection::default(); + let mut blackhole_burned = Coins::default(); let mut error = None; match executor.execute_with_params(in_msg_cell, account, params) { Ok(mut trans_execute) => { @@ -5504,7 +5503,7 @@ impl ValidateQuery { } base.transactions_executed.fetch_add(1, Ordering::Relaxed); blackhole_burned = trans_execute.blackhole_burned().clone(); - if !blackhole_burned.is_zero()? { + if !blackhole_burned.is_zero() { base.result .blackhole_burned .lock() @@ -5536,7 +5535,7 @@ impl ValidateQuery { let mut right_balance = new_balance.clone(); right_balance.add(&money_exported)?; right_balance.add(trans.total_fees())?; - right_balance.add(&blackhole_burned)?; + right_balance.coins.add(&blackhole_burned)?; if left_balance != right_balance { error = Some(error!( "transaction {} of {:x} violates the currency flow condition: \ @@ -5549,7 +5548,7 @@ impl ValidateQuery { new_balance.coins, money_exported.coins, trans.total_fees().coins, - blackhole_burned.coins + blackhole_burned )); } } From 428afab2841bb5e1d78a5229c2644d85ef540871 Mon Sep 17 00:00:00 2001 From: yaroslavser Date: Sat, 4 Apr 2026 16:32:14 +0300 Subject: [PATCH 40/48] Implement burning coins --- src/node/src/validator/collator.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index b330919..a6f610d 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -2718,7 +2718,7 @@ impl Collator { let burned = burning_cfg.calculate_burned_fees(imported_base)?; if !burned.is_zero() { collator_data.value_flow.burned.coins.add(&burned)?; - collator_data.value_flow.fees_collected.coins.add(&burned)?; + collator_data.value_flow.fees_collected.coins.sub(&burned)?; } } collator_data.value_flow.fees_imported = shard_fees.fees; @@ -3930,17 +3930,17 @@ impl Collator { let mut value_flow = collator_data.value_flow.clone(); value_flow.imported = collator_data.in_msgs.root_extra().value_imported.clone(); value_flow.exported = collator_data.out_msgs.root_extra().clone(); - let mut total_fees = accounts.root_extra().clone(); - total_fees.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; - - value_flow.fees_collected.add(&total_fees)?; + value_flow.fees_collected = accounts.root_extra().clone(); + value_flow.fees_collected.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; if self.shard.is_masterchain() { if let Some(burning_cfg) = collator_data.config.burning_config() { - let burned = burning_cfg.calculate_burned_fees(total_fees.coins.as_u128())?; + let burned = + burning_cfg.calculate_burned_fees(value_flow.fees_collected.coins.as_u128())?; value_flow.fees_collected.coins.sub(&burned)?; value_flow.burned.coins.add(&burned)?; } } + value_flow.fees_collected.add(&value_flow.fees_imported)?; value_flow.fees_collected.add(&value_flow.created)?; value_flow.to_next_blk = new_accounts.full_balance().clone(); //value_flow.to_next_blk.add(&value_flow.recovered)?; From a76466affa3291e4677c05b0fb50e2a7c2f289a6 Mon Sep 17 00:00:00 2001 From: Lapo4kaKek Date: Sat, 4 Apr 2026 16:55:21 +0300 Subject: [PATCH 41/48] accumulate fees_collected instead of overwriting to preserve shard burn --- src/node/src/validator/collator.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index a6f610d..2e4f303 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -3930,17 +3930,17 @@ impl Collator { let mut value_flow = collator_data.value_flow.clone(); value_flow.imported = collator_data.in_msgs.root_extra().value_imported.clone(); value_flow.exported = collator_data.out_msgs.root_extra().clone(); - value_flow.fees_collected = accounts.root_extra().clone(); - value_flow.fees_collected.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; + let mut total_fees = accounts.root_extra().clone(); + total_fees.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; + + value_flow.fees_collected.add(&total_fees)?; if self.shard.is_masterchain() { if let Some(burning_cfg) = collator_data.config.burning_config() { - let burned = - burning_cfg.calculate_burned_fees(value_flow.fees_collected.coins.as_u128())?; + let burned = burning_cfg.calculate_burned_fees(total_fees.coins.as_u128())?; value_flow.fees_collected.coins.sub(&burned)?; value_flow.burned.coins.add(&burned)?; } } - value_flow.fees_collected.add(&value_flow.fees_imported)?; value_flow.fees_collected.add(&value_flow.created)?; value_flow.to_next_blk = new_accounts.full_balance().clone(); //value_flow.to_next_blk.add(&value_flow.recovered)?; From 875d3cd3a8dbcd84084bf6be86fd7c1cf11e487c Mon Sep 17 00:00:00 2001 From: Lapo4kaKek Date: Sat, 4 Apr 2026 18:51:13 +0300 Subject: [PATCH 42/48] Merge release/node/v0.4.0 (fix-storage-phase updates) --- src/emulator/src/lib.rs | 9 ++++---- src/executor/src/transaction_executor.rs | 27 ++++++++++++------------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/emulator/src/lib.rs b/src/emulator/src/lib.rs index b8d8fdc..26e1901 100644 --- a/src/emulator/src/lib.rs +++ b/src/emulator/src/lib.rs @@ -233,12 +233,11 @@ pub extern "C" fn transaction_emulator_set_prev_blocks_info( match SliceData::load_cell(info_cell).and_then(|mut slice| read_stack_item(&mut slice)) { Ok(info) => { - if info.is_tuple() { - transaction_emulator.prev_blocks_info = PrevBlocksInfo::Tuple(info); + transaction_emulator.prev_blocks_info = if info.is_tuple() { + PrevBlocksInfo::Tuple(info) } else { - transaction_emulator.prev_blocks_info = - PrevBlocksInfo::Tuple(StackItem::tuple(Vec::new())); - } + PrevBlocksInfo::Tuple(StackItem::tuple(Vec::new())) + }; } Err(err) => { log::error!("Failed to parse info_cell: {err}"); diff --git a/src/executor/src/transaction_executor.rs b/src/executor/src/transaction_executor.rs index 88fda3a..1c7071d 100644 --- a/src/executor/src/transaction_executor.rs +++ b/src/executor/src/transaction_executor.rs @@ -147,6 +147,7 @@ pub trait TransactionExecutor { /// If account does not exist - phase skipped. /// Calculates storage fees and substracts them from account balance. /// If account balance becomes negative after that, then account is frozen. + /// is_special - flag indicating that account is in list of special smart contracts, for which storage fees are not applied fn storage_phase( &self, acc: &mut Account, @@ -159,23 +160,12 @@ pub trait TransactionExecutor { if tr.now() < acc.last_paid() { fail!("transaction timestamp must be greater then account timestamp") } - - if is_special { - log::debug!(target: "executor", "Special account: AccStatusChange::Unchanged"); - return Ok(TrStoragePhase::with_params( - Coins::zero(), - acc.due_payment().cloned(), - AccStatusChange::Unchanged, - )); - } + let original_due_payment = acc.due_payment().cloned(); let mut fee = match acc.storage_info() { - Some(storage_info) => { + Some(storage_info) if !is_special => { self.config().calc_storage_fees(storage_info, is_masterchain, tr.now())? } - None => { - log::debug!(target: "executor", "Account::None"); - return Ok(Default::default()); - } + _ => Default::default(), }; if let Some(due_payment) = acc.due_payment() { fee.add(due_payment)?; @@ -192,6 +182,15 @@ pub trait TransactionExecutor { let storage_fees_collected = std::mem::take(&mut acc_balance.coins); tr.add_fee_coins(&storage_fees_collected)?; fee.sub(&storage_fees_collected)?; + if is_special { + log::debug!(target: "executor", "special account, due payment {fee} still active"); + acc.set_due_payment(original_due_payment); + return Ok(TrStoragePhase::with_params( + storage_fees_collected, + Some(fee), + AccStatusChange::Unchanged, + )); + } let need_freeze = acc.is_active() && fee > self.config().get_gas_config(is_masterchain).freeze_due_limit; let need_delete = (acc.is_uninit() || acc.is_frozen()) From 67c636460a40631d7f97fea5cd3d7268dc6bf34f Mon Sep 17 00:00:00 2001 From: Slava Date: Sun, 5 Apr 2026 00:48:41 +0300 Subject: [PATCH 43/48] Updates for Simplex. Support of separate address for QUIC --- src/Makefile | 1 + src/adnl/benches/bench_rldp.rs | 4 +- src/adnl/src/adnl/node.rs | 226 +++++++--- src/adnl/src/dht/mod.rs | 46 +- src/adnl/src/overlay/broadcast.rs | 8 +- src/adnl/src/overlay/mod.rs | 49 ++- src/adnl/src/quic/mod.rs | 85 +++- src/adnl/src/rldp/mod.rs | 10 +- src/adnl/tests/test_adnl.rs | 6 +- src/adnl/tests/test_overlay.rs | 22 +- src/adnl/tests/test_quic.rs | 395 +++++++++++++++++- src/adnl/tests/test_rldp.rs | 10 +- src/adnl/tests/test_udp.rs | 7 +- src/adnl/tests/test_utils.rs | 4 +- src/node/bin/adnl_ping.rs | 2 +- src/node/bin/adnl_resolve.rs | 4 +- src/node/bin/dhtscan.rs | 134 ++++-- src/node/consensus-common/src/adnl_overlay.rs | 24 +- src/node/consensus-common/src/lib.rs | 2 +- .../consensus-common/src/node_test_network.rs | 8 +- src/node/src/config.rs | 31 ++ src/node/src/network/catchain_client.rs | 2 +- src/node/src/network/custom_overlay_client.rs | 13 +- src/node/src/network/node_network.rs | 77 +++- .../network/tests/test_full_node_overlays.rs | 1 + .../compat_test/cpp_src/compat_test_node.cpp | 8 +- src/node/tests/compat_test/src/lib.rs | 15 + .../tests/compat_test/src/test_helpers.rs | 87 +++- .../compat_test/tests/test_overlay_message.rs | 2 +- .../compat_test/tests/test_quic_address.rs | 153 +++++++ .../tests/test_quic_private_overlay.rs | 10 +- .../tests/test_run_net_py/test_run_net.py | 55 ++- src/tl/ton_api/tl/ton_api.tl | 1 + 33 files changed, 1243 insertions(+), 259 deletions(-) create mode 100644 src/node/tests/compat_test/tests/test_quic_address.rs diff --git a/src/Makefile b/src/Makefile index ab93dbe..bf68a05 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,6 +44,7 @@ fmt: @cargo +nightly fmt $(check) --manifest-path ./node/consensus-common/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node/simplex/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node/storage/Cargo.toml + @cargo +nightly fmt $(check) --manifest-path ./node/tests/compat_test/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node/validator-session/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node-control/commands/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node-control/common/Cargo.toml diff --git a/src/adnl/benches/bench_rldp.rs b/src/adnl/benches/bench_rldp.rs index c49f367..ed3a046 100644 --- a/src/adnl/benches/bench_rldp.rs +++ b/src/adnl/benches/bench_rldp.rs @@ -197,8 +197,8 @@ fn bench_rldp(loopback: bool, v2: bool, #[cfg(feature = "debug")] loss_percentag #[cfg(feature = "debug")] loss_percentage, ); - adnl1.add_peer(key1.id(), adnl2.ip_address(), &key2).unwrap(); - adnl2.add_peer(key2.id(), adnl1.ip_address(), &key1).unwrap(); + adnl1.add_peer(key1.id(), adnl2.ip_address_adnl(), None, &key2).unwrap(); + adnl2.add_peer(key2.id(), adnl1.ip_address_adnl(), None, &key1).unwrap(); if let Some(rt2) = &rt2 { rt1.block_on(adnl1.start_over_udp(vec![rldp1.clone()])).unwrap(); rt2.block_on(adnl2.start_over_udp(vec![rldp2.clone()])).unwrap(); diff --git a/src/adnl/src/adnl/node.rs b/src/adnl/src/adnl/node.rs index 15a13c1..f0cb3fd 100644 --- a/src/adnl/src/adnl/node.rs +++ b/src/adnl/src/adnl/node.rs @@ -30,7 +30,7 @@ use std::{ cmp::{max, min}, convert::TryInto, fmt::{self, Debug, Display, Formatter}, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{ atomic::{AtomicI32, AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering}, Arc, Condvar, Mutex, @@ -50,7 +50,7 @@ use ton_api::{ deserialize_boxed, deserialize_typed, serialize_boxed, ton::{ adnl::{ - address::address::Udp, + address::address::{Quic, Udp}, addresslist::AddressList, id::short::Short as AdnlIdShort, message::message::{ @@ -75,15 +75,15 @@ macro_rules! adnl_node_test_key { format!( "{{ \"tag\": {}, - \"data\": {{ + \"data\": {{ \"type_id\": 1209251014, \"pvt_key\": \"{}\" }} }}", - $tag, - $key - ).as_str() - } + $tag, $key + ) + .as_str() + }; } #[macro_export] @@ -92,28 +92,27 @@ macro_rules! adnl_node_test_config { format!( "{{ \"ip_address\": \"{}\", - \"keys\": [ - {} + \"keys\": [ + {} ] }}", - $ip, - $key - ).as_str() + $ip, $key + ) + .as_str() }; ($ip: expr, $key1:expr, $key2:expr) => { format!( "{{ \"ip_address\": \"{}\", - \"keys\": [ - {}, - {} + \"keys\": [ + {}, + {} ] }}", - $ip, - $key1, - $key2 - ).as_str() - } + $ip, $key1, $key2 + ) + .as_str() + }; } /// ADNL addresses cache iterator @@ -630,24 +629,43 @@ impl AdnlChannel { struct AdnlNodeAddress { channel_key: Arc, - ip_version_address: AtomicPair, + ip_version_address_adnl: AtomicPair, + ip_version_address_quic: AtomicPair, key: Arc, } impl AdnlNodeAddress { - fn from_ip_address_and_key(ip_address: &IpAddress, key: &Arc) -> Result { + fn from_ip_addresses_and_key( + ip_address_adnl: &IpAddress, + ip_address_quic: Option<&IpAddress>, + key: &Arc, + ) -> Result { let ret = Self { channel_key: Ed25519KeyOption::generate()?, - ip_version_address: AtomicPair::new(ip_address.version as u64, ip_address.address), + ip_version_address_adnl: AtomicPair::new( + ip_address_adnl.version as u64, + ip_address_adnl.address, + ), + ip_version_address_quic: match ip_address_quic { + Some(q) => AtomicPair::new(q.version as u64, q.address), + None => AtomicPair::new(0, 0), + }, key: key.clone(), }; Ok(ret) } - fn update(&self, ip_address: &IpAddress) -> bool { - self.ip_version_address.update( - ip_address.version as u64, - ip_address.address, + fn update(&self, ip_address_adnl: &IpAddress, ip_address_quic: Option<&IpAddress>) -> bool { + if let Some(q) = ip_address_quic { + self.ip_version_address_quic.update( + q.version as u64, + q.address, + |old_version, new_version| old_version < new_version, + ); + } + self.ip_version_address_adnl.update( + ip_address_adnl.version as u64, + ip_address_adnl.address, |old_version, new_version| old_version < new_version, ) } @@ -656,6 +674,7 @@ impl AdnlNodeAddress { /// ADNL node configuration pub struct AdnlNodeConfig { ip_address: IpAddress, + ip_address_quic: Option, keys: lockfree::map::Map, Arc>, tags: lockfree::map::Map>, recv_pipeline_pool: Option, // %% of cpu cores to assign for recv workers @@ -677,6 +696,8 @@ struct AdnlNodeKeyJson { #[derive(serde::Deserialize, serde::Serialize)] pub struct AdnlNodeConfigJson { ip_address: String, + #[serde(default)] + ip_address_quic: Option, keys: Vec, recv_pipeline_pool: Option, // %% of cpu cores to assign for recv workers recv_priority_pool: Option, // %% of workers to assign for priority recv @@ -694,6 +715,14 @@ impl AdnlNodeConfigJson { IpAddress::from_versioned_string(&self.ip_address, None) } + /// Get QUIC IP address + pub fn ip_address_quic(&self) -> Result> { + self.ip_address_quic + .as_deref() + .map(|s| IpAddress::from_versioned_string(s, None)) + .transpose() + } + /// Get key by tag pub fn key_by_tag(&self, tag: usize, as_src: bool) -> Result> { for key in self.keys.iter() { @@ -718,10 +747,14 @@ impl AdnlNodeConfig { /// Construct from IP address and key data pub fn from_ip_address_and_keys( ip_address: &str, + ip_address_quic: Option<&str>, keys: Vec<(Arc, usize)>, ) -> Result { + let ip_address_quic = + ip_address_quic.map(|s| IpAddress::from_versioned_string(s, None)).transpose()?; let ret = AdnlNodeConfig { ip_address: IpAddress::from_versioned_string(ip_address, None)?, + ip_address_quic, keys: lockfree::map::Map::new(), tags: lockfree::map::Map::new(), recv_pipeline_pool: None, @@ -762,6 +795,7 @@ impl AdnlNodeConfig { pub fn from_json_config(json_config: &AdnlNodeConfigJson) -> Result { let ret = AdnlNodeConfig { ip_address: json_config.ip_address()?, + ip_address_quic: json_config.ip_address_quic()?, keys: lockfree::map::Map::new(), tags: lockfree::map::Map::new(), recv_pipeline_pool: json_config.recv_pipeline_pool, @@ -825,6 +859,16 @@ impl AdnlNodeConfig { self.ip_address.set_port(port) } + /// Set QUIC address + pub fn set_ip_address_quic(&mut self, addr: SocketAddr) { + if let IpAddr::V4(ipv4) = addr.ip() { + self.ip_address_quic = + Some(IpAddress::from_versioned_parts(u32::from(ipv4), addr.port(), None)); + } else { + log::warn!(target: TARGET, "IPv6 QUIC address {} ignored, only IPv4 is supported", addr); + } + } + /// Set worker pools pub fn set_recv_worker_pools( &mut self, @@ -873,6 +917,7 @@ impl AdnlNodeConfig { } let json = AdnlNodeConfigJson { ip_address: ip_address.to_string(), + ip_address_quic: None, keys: json_keys, recv_pipeline_pool: None, recv_priority_pool: None, @@ -883,7 +928,7 @@ impl AdnlNodeConfig { timeout_check_packet_processing_mcs: None, timeout_expire_queued_packet_sec: None, }; - Ok((json, Self::from_ip_address_and_keys(ip_address, tags_keys)?)) + Ok((json, Self::from_ip_address_and_keys(ip_address, None, tags_keys)?)) } fn delete_key(&self, key: &Arc, tag: usize) -> Result { @@ -2799,6 +2844,7 @@ impl AdnlNode { &self, local_key: &Arc, peer_ip_address: &IpAddress, + peer_ip_address_quic: Option<&IpAddress>, peer_key: &Arc, ) -> Result>> { if peer_key.id() == local_key { @@ -2810,14 +2856,17 @@ impl AdnlNode { self.peers(local_key)?.map_of.insert_with(ret.clone(), |key, inserted, found| { if let Some((_, found)) = found { ret = key.clone(); - found.address.update(peer_ip_address); + found.address.update(peer_ip_address, peer_ip_address_quic); lockfree::map::Preview::Discard } else if inserted.is_some() { ret = key.clone(); lockfree::map::Preview::Keep } else { - let address = - AdnlNodeAddress::from_ip_address_and_key(peer_ip_address, peer_key); + let address = AdnlNodeAddress::from_ip_addresses_and_key( + peer_ip_address, + peer_ip_address_quic, + peer_key, + ); match address { Ok(address) => { #[cfg(feature = "telemetry")] @@ -2873,11 +2922,17 @@ impl AdnlNode { Ok(Some(ret)) } - /// Build address list for given node + /// Build address list for given node. pub fn build_address_list(&self, expire_at: Option) -> Result { let version = Version::get(); + let mut addrs: Vec

= vec![self.config.ip_address.to_udp().into_boxed()]; + if let Some(quic_addr) = self.ip_address_quic() { + addrs.push( + Quic { ip: quic_addr.ip() as i32, port: quic_addr.port() as i32 }.into_boxed(), + ); + } let ret = AddressList { - addrs: vec![self.config.ip_address.to_udp().into_boxed()].into(), + addrs: addrs.into(), version, reinit_date: self.start_timestamp, priority: 0, @@ -2941,9 +2996,14 @@ impl AdnlNode { (self.options.load(Ordering::Relaxed) & Self::OPTION_MASK_TIMEOUT_CHANNEL_RESET_SEC) as u64 } - /// Node IP address - pub fn ip_address(&self) -> &IpAddress { - self.config.ip_address() + /// Node ADNL IP address + pub fn ip_address_adnl(&self) -> &IpAddress { + &self.config.ip_address + } + + /// Node QUIC IP address (None = not configured). + pub fn ip_address_quic(&self) -> Option<&IpAddress> { + self.config.ip_address_quic.as_ref() } /// Node key by ID @@ -2957,7 +3017,35 @@ impl AdnlNode { } /// Parse other's address list - pub fn parse_address_list(list: &AddressList) -> Result> { + pub fn parse_address_list( + list: &AddressList, + ) -> Result)>> { + fn parse_addr( + out: &mut Option, + kind: &str, + ip: i32, + port: i32, + version: i32, + ) -> bool { + if out.is_some() { + log::warn!(target: TARGET, "Duplicate {kind} address in address list"); + return false; + } + if ip == 0 { + log::warn!(target: TARGET, "{kind} address with zero IP in address list"); + return false; + } + if (port <= 0) || (port > 65535) { + log::warn!( + target: TARGET, + "{kind} address with invalid port {port} in address list" + ); + return false; + } + *out = Some(IpAddress::from_versioned_parts(ip as u32, port as u16, Some(version))); + true + } + if list.addrs.is_empty() { log::warn!(target: TARGET, "Address list is empty"); return Ok(None); @@ -2978,37 +3066,58 @@ impl AdnlNode { log::warn!(target: TARGET, "Address list is expired"); return Ok(None); } - match &list.addrs[0] { - Address::Adnl_Address_Udp(x) => { - let ret = - IpAddress::from_versioned_parts(x.ip as u32, x.port as u16, Some(list.version)); - Ok(Some(ret)) + let mut adnl_addr = None; + let mut quic_addr = None; + for addr in list.addrs.iter() { + match addr { + Address::Adnl_Address_Udp(x) => { + if !parse_addr(&mut adnl_addr, "ADNL", x.ip, x.port, list.version) { + return Ok(None); + } + } + Address::Adnl_Address_Quic(x) => { + if !parse_addr(&mut quic_addr, "QUIC", x.ip, x.port, list.version) { + return Ok(None); + } + } + _ => {} } - _ => { - log::warn!(target: TARGET, "Only IPv4 address format is supported"); + } + match adnl_addr { + Some(ip) => Ok(Some((ip, quic_addr))), + None => { + log::warn!(target: TARGET, "No IPv4 ADNL address in address list"); Ok(None) } } } - /// Get peer's socket address if known. + /// Get peer's ADNL and QUIC socket addresses if known pub fn peer_ip_address( &self, local_key: &Arc, peer_key: &Arc, - ) -> Result> { + ) -> Result)>> { let peers = self.peers(local_key)?; let Some(peer) = peers.map_of.get(peer_key) else { return Ok(None); }; - let (_, address) = peer.val().address.ip_version_address.get(); - if address == 0 { - Ok(None) - } else { - let ip = (address >> 16) as u32; - let port = address as u16; - Ok(Some(SocketAddr::new(IpAddr::V4(ip.into()), port))) + let (_, adnl_address) = peer.val().address.ip_version_address_adnl.get(); + if adnl_address == 0 { + return Ok(None); } + let adnl_ip = (adnl_address >> 16) as u32; + let adnl_port = adnl_address as u16; + let adnl_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::from(adnl_ip)), adnl_port); + let (_, quic_address) = peer.val().address.ip_version_address_quic.get(); + let quic_addr = if quic_address != 0 { + let quic_ip = (quic_address >> 16) as u32; + let quic_port = quic_address as u16; + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(quic_ip)), quic_port)) + } else { + None + }; + Ok(Some((adnl_addr, quic_addr))) } /// Send query @@ -3177,9 +3286,10 @@ impl AdnlNode { })?; log::warn!(target: TARGET, "Resetting peer pair {} -> {}", local_key, other_key); let peer = peer.val(); - let (_, address) = peer.address.ip_version_address.get(); - let address = AdnlNodeAddress::from_ip_address_and_key( + let (_, address) = peer.address.ip_version_address_adnl.get(); + let address = AdnlNodeAddress::from_ip_addresses_and_key( &IpAddress { address, version: Version::get() }, + None, &peer.address.key, )?; peers @@ -3449,8 +3559,8 @@ impl AdnlNode { } else { Some(address.reinit_date) }; - if let Some(ip_address) = Self::parse_address_list(address)? { - self.add_peer(local_key, &ip_address, &key)?; + if let Some((adnl_addr, quic_addr)) = Self::parse_address_list(address)? { + self.add_peer(local_key, &adnl_addr, quic_addr.as_ref(), &key)?; (other_key, Cow::Borrowed("from-with-ip-address"), address_reinit_date, false) } else { let disposition = Cow::Owned(format!("from-without-ip-address {address:?}")); @@ -4478,7 +4588,7 @@ impl AdnlNode { .send(LoopbackData::Packet(data)) .map_err(|e| error!("Error when sending loopback ADNL packet: {e}"))? } else { - let (_, address) = peer.address.ip_version_address.get(); + let (_, address) = peer.address.ip_version_address_adnl.get(); let job = SendData { destination: address, data, method: method.clone() }; if method == AdnlSendMethodDetailed::FastUrgent { self.send_pipeline.put_urgent(SendJob::Data(job)) diff --git a/src/adnl/src/dht/mod.rs b/src/adnl/src/dht/mod.rs index 4adce73..d42882f 100644 --- a/src/adnl/src/dht/mod.rs +++ b/src/adnl/src/dht/mod.rs @@ -234,7 +234,7 @@ impl OverlayNodeResolveContext { } pub async fn resolve(&mut self, dht: &Arc) -> Result> { - Ok(dht.find_address(&mut self.search).await?.map(|(ip, _)| ip)) + Ok(dht.find_address(&mut self.search).await?.map(|(ip, _, _)| ip)) } } @@ -417,7 +417,7 @@ impl DhtNode { pub async fn fetch_address( &self, key_id: &Arc, - ) -> Result)>> { + ) -> Result, Arc)>> { let key = Self::dht_key_from_key_id(key_id, "address"); let value = self.network.search_dht_key(&hash(key)?); if let Some(value) = value { @@ -432,7 +432,7 @@ impl DhtNode { pub async fn find_address( self: &Arc, ctx_search: &mut AddressSearchContext, - ) -> Result)>> { + ) -> Result, Arc)>> { let mut addr_list = self .find_value( &ctx_search.key_id, @@ -641,7 +641,7 @@ impl DhtNode { /// Node IP address pub fn ip_address(&self) -> &IpAddress { - self.adnl.ip_address() + self.adnl.ip_address_adnl() } /// Node key @@ -666,7 +666,7 @@ impl DhtNode { pub async fn store_ip_address(self: &Arc, key: &Arc) -> Result { log::debug!(target: TARGET, "Storing key ID {}", key.id()); let addr_list = self.adnl.build_address_list(None)?; - let addr = AdnlNode::parse_address_list(&addr_list)? + let addrs = AdnlNode::parse_address_list(&addr_list)? .ok_or_else(|| error!("INTERNAL ERROR: cannot parse generated address list"))?; let value = serialize_boxed(&addr_list.into_boxed())?; let value = Self::sign_value("address", value, key)?; @@ -683,17 +683,14 @@ impl DhtNode { while let Some((_, object)) = objects.pop() { if let Ok(addr_list) = object.downcast::() { let addr_list = addr_list.only(); - if let Some(ip) = AdnlNode::parse_address_list(&addr_list)? { - if ip == addr { - //dht.adnl.ip_address() { - log::debug!(target: TARGET, "Checked stored address {:?}", ip); + if let Some(stored) = AdnlNode::parse_address_list(&addr_list)? { + if stored == addrs { + log::debug!(target: TARGET, "Checked stored address {stored:?}"); return Ok(true); } else { log::warn!( target: TARGET, - "Found another stored address {:?}, expected {:?}", - ip, - self.adnl.ip_address() + "Found another stored address {stored:?}, expected {addrs:?}" ) } } else { @@ -770,13 +767,16 @@ impl DhtNode { log::warn!(target: TARGET, "Error when verifying DHT peer: {}", e); return Ok(None); } - let addr = if let Some(addr) = AdnlNode::parse_address_list(&peer.addr_list)? { - addr - } else { - log::warn!(target: TARGET, "Wrong DHT peer address {:?}", peer.addr_list); - return Ok(None); - }; - let ret = self.adnl.add_peer(network.node_key.id(), &addr, &((&peer.id).try_into()?))?; + let (adnl_addr, quic_addr) = + if let Some(addrs) = AdnlNode::parse_address_list(&peer.addr_list)? { + addrs + } else { + log::warn!(target: TARGET, "Wrong DHT peer address {:?}", peer.addr_list); + return Ok(None); + }; + let peer_key: Arc = (&peer.id).try_into()?; + let ret = + self.adnl.add_peer(network.node_key.id(), &adnl_addr, quic_addr.as_ref(), &peer_key)?; let ret = if let Some(ret) = ret { ret } else { return Ok(None) }; if network.known_peers.all().put(ret.clone())? { let key1 = network.node_key.id().data(); @@ -964,11 +964,13 @@ impl DhtNode { fn parse_value_as_address( key: DhtKeyDescription, value: TLObject, - ) -> Result<(IpAddress, Arc)> { + ) -> Result<(IpAddress, Option, Arc)> { if let Ok(addr_list) = value.downcast::() { - let ip_address = AdnlNode::parse_address_list(&addr_list.only())? + let addr_list = addr_list.only(); + let (adnl_addr, quic_addr) = AdnlNode::parse_address_list(&addr_list)? .ok_or_else(|| error!("Wrong address list in DHT search"))?; - Ok((ip_address, (&key.id).try_into()?)) + let peer_key: Arc = (&key.id).try_into()?; + Ok((adnl_addr, quic_addr, peer_key)) } else { fail!("Address list type mismatch in DHT search") } diff --git a/src/adnl/src/overlay/broadcast.rs b/src/adnl/src/overlay/broadcast.rs index fd7637d..d754556 100644 --- a/src/adnl/src/overlay/broadcast.rs +++ b/src/adnl/src/overlay/broadcast.rs @@ -343,12 +343,16 @@ pub(crate) trait BroadcastProtocol: return Ok(()); } let info = self.check_broadcast(&bcast, &ctx)?; + let bcast_id = base64_encode(&info.bcast_id); if info.dup { - let bcast_id = base64_encode(&info.bcast_id); log::info!(target: TARGET, "Received duplicated {bcast_type} broadcast {bcast_id}"); return Ok(()); }; - log::trace!(target: TARGET, "Received {bcast_type} broadcast, {} bytes", info.data_len); + log::trace!( + target: TARGET, + "Received {bcast_type} broadcast {bcast_id}, {} bytes", + info.data_len + ); #[cfg(feature = "telemetry")] let tag = info.maybe_tag.unwrap_or(bcast.default_tag()); #[cfg(feature = "telemetry")] diff --git a/src/adnl/src/overlay/mod.rs b/src/adnl/src/overlay/mod.rs index 049415e..e1d855f 100644 --- a/src/adnl/src/overlay/mod.rs +++ b/src/adnl/src/overlay/mod.rs @@ -353,7 +353,10 @@ struct SlaveInfo { enum OverlayType { Public, // Overlay with fixed members set - Private(Arc), + Private { + key: Arc, + use_quic: bool, + }, // Overlay with externally certified members CertifiedMembers { key: Option>, @@ -369,15 +372,13 @@ enum OverlayType { impl OverlayType { fn bcast_prefix(&self) -> Option> { match self { - OverlayType::Public => None, - OverlayType::Private(_) => None, OverlayType::CertifiedMembers { bcast_prefix, .. } => Some(bcast_prefix.clone()), + OverlayType::Public | OverlayType::Private { .. } => None, } } fn calc_message_prefix(&self, overlay_id: &OverlayShortId) -> Result> { match self { - Self::Public | Self::Private(_) => OverlayUtils::calc_message_prefix(overlay_id), Self::CertifiedMembers { certificate, .. } => serialize_boxed( &OverlayMessageWithExtra { overlay: UInt256::with_array(*overlay_id.data()), @@ -387,27 +388,30 @@ impl OverlayType { } .into_boxed(), ), + OverlayType::Public | OverlayType::Private { .. } => { + OverlayUtils::calc_message_prefix(overlay_id) + } } } fn calc_query_prefix(&self, overlay_id: &OverlayShortId) -> Result> { match self { - Self::Public | Self::Private(_) => { - serialize_boxed(&OverlayQuery { overlay: UInt256::with_array(*overlay_id.data()) }) - } Self::CertifiedMembers { certificate, .. } => serialize_boxed(&OverlayQueryWithExtra { overlay: UInt256::with_array(*overlay_id.data()), extra: MessageExtra { certificate: certificate.as_ref().map(|cert| cert.clone().into_boxed()), }, }), + OverlayType::Public | OverlayType::Private { .. } => { + serialize_boxed(&OverlayQuery { overlay: UInt256::with_array(*overlay_id.data()) }) + } } } fn certificate(&self) -> Option<&MemberCertificate> { match self { - OverlayType::Public | OverlayType::Private(_) => None, OverlayType::CertifiedMembers { certificate, .. } => certificate.as_ref(), + OverlayType::Public | OverlayType::Private { .. } => None, } } @@ -611,8 +615,8 @@ impl Overlay { fn check_peer(&self, peer: &Arc, certificate: Option<&MemberCertificate>) -> Result<()> { match &self.overlay_type { OverlayType::Public => Ok(()), - OverlayType::Private(own_key) => { - if !(peer == own_key.id() || self.known_peers.all().contains(peer)) { + OverlayType::Private { key, .. } => { + if !(peer == key.id() || self.known_peers.all().contains(peer)) { fail!("Peer {peer} is not a member of the private overlay {}", self.overlay_id) } Ok(()) @@ -702,13 +706,14 @@ impl Overlay { if let Some(quic) = self.quic.as_ref() { quic.message(data.object.to_vec(), Some(&self.adnl), peers).await.err() } else { - let Some(rldp) = self.rldp.as_ref() else { + let Some(_rldp) = self.rldp.as_ref() else { fail!( "Neither QUIC nor RLDP sender is set in overlay {}", self.overlay_id ); }; - rldp.message(data, peers, true, None).await.err() + //rldp.message(data, peers, true, None).await.err() + None } } BroadcastSendMethod::Safe => { @@ -821,7 +826,7 @@ impl Overlay { fn overlay_key(&self) -> Option<&Arc> { match &self.overlay_type { - OverlayType::Private(key) => Some(key), + OverlayType::Private { key, .. } => Some(key), OverlayType::CertifiedMembers { key, .. } => key.as_ref(), _ => None, } @@ -1428,8 +1433,9 @@ impl OverlayNode { params: OverlayParams, overlay_key: &Arc, peers: &[Arc], + use_quic: bool, ) -> Result { - let overlay_type = OverlayType::Private(overlay_key.clone()); + let overlay_type = OverlayType::Private { key: overlay_key.clone(), use_quic }; self.add_typed_private_overlay(overlay_type, params, peers) } @@ -1460,11 +1466,11 @@ impl OverlayNode { pub fn add_private_peers( &self, local_adnl_key: &Arc, - peers: Vec<(IpAddress, Arc)>, + peers: Vec<(IpAddress, Option, Arc)>, ) -> Result>> { let mut ret = Vec::new(); - for (ip, key) in peers { - if let Some(peer) = self.adnl.add_peer(local_adnl_key, &ip, &key)? { + for (ip, quic_ip, key) in peers { + if let Some(peer) = self.adnl.add_peer(local_adnl_key, &ip, quic_ip.as_ref(), &key)? { ret.push(peer) } } @@ -1511,7 +1517,7 @@ impl OverlayNode { log::warn!(target: TARGET, "Error when verifying overlay peer {}: {e}", key.id()); return Ok(None); } - let Some(ret) = self.adnl.add_peer(self.node_key.id(), peer_ip_address, &key)? else { + let Some(ret) = self.adnl.add_peer(self.node_key.id(), peer_ip_address, None, &key)? else { return Ok(None); }; overlay.known_peers.add(&ret)?; @@ -1971,10 +1977,15 @@ impl OverlayNode { penalty: 1, to_block: Self::MAX_FAIL_COUNT, }; + let quic = if let OverlayType::Private { use_quic: true, .. } = &overlay_type { + self.quic.get().cloned() + } else { + None + }; let overlay = Overlay { adnl: self.adnl.clone(), rldp: self.rldp.get().cloned(), - quic: self.quic.get().cloned(), + quic, flags: params.flags, hops: params.hops, known_peers: AddressCacheWithBads::with_params(Self::MAX_PEERS, policy), diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index 0b07157..28f3a09 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -343,6 +343,18 @@ struct EndpointState { last_added_name: Arc>>, } +/// Command sent to the background Tokio task that manages QUIC key operations. +/// Quinn endpoint creation requires a Tokio runtime context, but callers may run +/// on bare OS threads (e.g. Simplex SXMAIN). The channel decouples the two. +enum KeyCommand { + AddKey { + key: [u8; Ed25519KeyOption::PVT_KEY_SIZE], + key_id: Arc, + bind_addr: SocketAddr, + reply: tokio::sync::oneshot::Sender>, + }, +} + pub struct QuicNode { cancellation_token: tokio_util::sync::CancellationToken, /// One entry per local identity; each carries its own client config and outbound pool. @@ -358,6 +370,8 @@ pub struct QuicNode { inbound_pools: Mutex>>, /// Per-TL-tag message counters for the stats dumper. msg_stats: Arc, + /// Channel for dispatching key operations to a Tokio-hosted background task. + key_cmd_tx: tokio::sync::mpsc::UnboundedSender, } impl QuicNode { @@ -374,10 +388,14 @@ impl QuicNode { /// Create a new QuicNode. No endpoints are bound โ€” they are created lazily /// by `add_key()` when the first identity for a given port is registered. + /// + /// `runtime_handle` is used to spawn a background task that processes key + /// operations requiring Tokio context (Quinn endpoint creation). pub fn new( subscribers: Vec>, cancellation_token: tokio_util::sync::CancellationToken, max_streams_per_connection: Option, + runtime_handle: tokio::runtime::Handle, ) -> Arc { let max_streams_per_connection = max_streams_per_connection.unwrap_or(Self::DEFAULT_MAX_STREAMS_PER_CONNECTION); @@ -387,6 +405,7 @@ impl QuicNode { .install_default() .expect("Failed to install default Rustls CryptoProvider"); }); + let (key_cmd_tx, mut key_cmd_rx) = tokio::sync::mpsc::unbounded_channel::(); let transport = Arc::new(Self { cancellation_token: cancellation_token.clone(), local_keys: lockfree::map::Map::new(), @@ -396,6 +415,30 @@ impl QuicNode { max_streams_per_connection, inbound_pools: Mutex::new(Vec::new()), msg_stats: MsgStats::new(), + key_cmd_tx, + }); + // Spawn background task that processes key commands inside the Tokio runtime. + let weak = Arc::downgrade(&transport); + let token = cancellation_token.clone(); + runtime_handle.spawn(async move { + loop { + tokio::select! { + cmd = key_cmd_rx.recv() => { + let Some(cmd) = cmd else { break }; + match cmd { + KeyCommand::AddKey { key, key_id, bind_addr, reply } => { + let result = if let Some(this) = weak.upgrade() { + this.add_key_inner(&key, &key_id, bind_addr) + } else { + Err(error!("QuicNode dropped")) + }; + let _ = reply.send(result); + } + } + } + _ = token.cancelled() => break, + } + } }); Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token.clone()); Self::spawn_stats_dumper(Arc::downgrade(&transport), cancellation_token); @@ -404,11 +447,44 @@ impl QuicNode { /// Register a local identity on a specific bind address. /// Creates a new endpoint if one doesn't exist for this port yet. + /// + /// Safe to call from any thread โ€” the actual work is dispatched to a + /// Tokio-hosted background task via an internal channel. pub fn add_key( &self, key: &[u8; Ed25519KeyOption::PVT_KEY_SIZE], key_id: &Arc, bind_addr: SocketAddr, + ) -> Result<()> { + let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); + self.key_cmd_tx + .send(KeyCommand::AddKey { + key: *key, + key_id: key_id.clone(), + bind_addr, + reply: reply_tx, + }) + .map_err(|_| error!("QuicNode key command channel closed"))?; + // Use blocking_recv on bare OS threads, tokio::block_in_place on Tokio workers. + match tokio::runtime::Handle::try_current() { + Ok(_) => tokio::task::block_in_place(|| { + reply_rx + .blocking_recv() + .map_err(|_| error!("QuicNode key command reply channel dropped"))? + }), + Err(_) => reply_rx + .blocking_recv() + .map_err(|_| error!("QuicNode key command reply channel dropped"))?, + } + } + + /// Internal implementation of add_key โ€” always runs inside the Tokio runtime + /// (called by the background key-command task). + fn add_key_inner( + &self, + key: &[u8; Ed25519KeyOption::PVT_KEY_SIZE], + key_id: &Arc, + bind_addr: SocketAddr, ) -> Result<()> { let key_bytes = ed25519_encode_private_key_to_pkcs8(key)?; let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_bytes) @@ -690,9 +766,16 @@ impl QuicNode { let Some(adnl) = adnl else { fail!("QUIC peer {dst} is not registered and no ADNL node provided"); }; - let mut addr = adnl + // Get peer's ADNL and optional QUIC addresses + let (adnl_addr, quic_addr) = adnl .peer_ip_address(peers.local(), dst)? .ok_or_else(|| error!("QUIC peer {dst} IP is not known in ADNL"))?; + // Prefer explicit QUIC address from peer's address list (adnl.address.quic) + if let Some(quic_addr) = quic_addr { + return self.add_peer_key(dst.clone(), quic_addr); + } + // Fallback: derive QUIC port from ADNL port + offset + let mut addr = adnl_addr; let quic_port = addr.port().checked_add(Self::OFFSET_PORT).ok_or_else(|| { error!("QUIC port overflow for peer {dst}: ADNL port {}", addr.port()) })?; diff --git a/src/adnl/src/rldp/mod.rs b/src/adnl/src/rldp/mod.rs index dd5a911..b0cc302 100644 --- a/src/adnl/src/rldp/mod.rs +++ b/src/adnl/src/rldp/mod.rs @@ -208,6 +208,7 @@ impl RldpNode { const MAX_OUTBOUNDS_PER_PEER: u32 = 3; const SIZE_TRANSFER_WAVE: u32 = 10; const SPINNER_MS: u64 = 1; + const SPINNER_V1_SEND_MS: u64 = 10; const TIMEOUT_MAX_MS: u64 = 10000; const TIMEOUT_MIN_MS: u64 = 500; const TIMEOUT_WARN_MS: u64 = 5000; @@ -1261,9 +1262,12 @@ impl RldpNode { break 'part; } } - tokio::time::timeout(Duration::from_millis(Self::SPINNER_MS), context.pong.recv()) - .await - .ok(); + tokio::time::timeout( + Duration::from_millis(Self::SPINNER_V1_SEND_MS), + context.pong.recv(), + ) + .await + .ok(); if transfer.state().is_transfer_finished_or_next_part(part)? { #[cfg(feature = "debug")] Self::check_timestamp( diff --git a/src/adnl/tests/test_adnl.rs b/src/adnl/tests/test_adnl.rs index 514a0ec..03d1d28 100644 --- a/src/adnl/tests/test_adnl.rs +++ b/src/adnl/tests/test_adnl.rs @@ -432,7 +432,8 @@ fn run_adnl_session(mode: SendMode, ip1: &str, ip2: &str) { let peer1 = node2 .add_peer( node2.key_by_tag(KEY_TAG).unwrap().id(), - node1.ip_address(), + node1.ip_address_adnl(), + None, &node1.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() @@ -440,7 +441,8 @@ fn run_adnl_session(mode: SendMode, ip1: &str, ip2: &str) { let peer2 = node1 .add_peer( node1.key_by_tag(KEY_TAG).unwrap().id(), - node2.ip_address(), + node2.ip_address_adnl(), + None, &node2.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() diff --git a/src/adnl/tests/test_overlay.rs b/src/adnl/tests/test_overlay.rs index 09326f3..95117e7 100644 --- a/src/adnl/tests/test_overlay.rs +++ b/src/adnl/tests/test_overlay.rs @@ -301,7 +301,7 @@ fn test_random_peers(ctx_test: &TestContext) { AddressSearchContext::with_params(key.id(), DhtSearchPolicy::FastSearch(5)) .unwrap(); match ctx_test.dht.find_address(&mut ctx_search).await { - Ok(Some((ip, _))) => println!("IP {}", ip), + Ok(Some((ip, _, _))) => println!("IP {}", ip), Ok(None) => println!("Address not found"), Err(err) => println!("Error {}", err), } @@ -373,7 +373,7 @@ fn test_overlay_broadcast_receive(ctx_test: &TestContext) { let mut ctx_search = AddressSearchContext::with_params(key_id, DhtSearchPolicy::FastSearch(5)) .unwrap(); - if let Ok(Some((ip, _))) = ctx_test.dht.find_address(&mut ctx_search).await { + if let Ok(Some((ip, _, _))) = ctx_test.dht.find_address(&mut ctx_search).await { println!("RECEIVED new overlay node {}", key_id); ctx_test.overlay.add_public_peer(&ip, &node, &ctx_test.overlay_id).unwrap(); known_nodes.insert(key_id.clone()); @@ -682,7 +682,7 @@ fn run_propagation( "Broadcasting {}->{} packets by {adnl_id_send}/{}, step {j}\n", info.packets, info.send_to, - adnl.ip_address(), + adnl.ip_address_adnl(), ); bcast_totally.fetch_add(1, Ordering::Relaxed); } @@ -820,6 +820,13 @@ fn test_broadcast( test: impl Fn(&[LocalNode], &[Arc>>], Protocol) -> RunResult, protocol: Protocol, ) { + let min_neighbours = match protocol { + Protocol::StreamSimple | Protocol::TwostepFec => return, /* Not ready yet */ + //Protocol::TwostepFec => 4, + Protocol::TwostepSimple => 3, + _ => 1, + }; + init_test(); let mut nodes = Vec::new(); for i in 0..n { @@ -828,12 +835,6 @@ fn test_broadcast( nodes.push(init_local_node(ip, 100 / n as u8)); } - let min_neighbours = match protocol { - Protocol::StreamSimple => return, /* Not ready yet */ - Protocol::TwostepFec => 4, - Protocol::TwostepSimple => 3, - _ => 1, - }; let mut neighbours = Vec::new(); for i in 0..n { let overlay_id = &nodes[i].overlay_id; @@ -979,7 +980,7 @@ fn test_overlay_ping() { AddressSearchContext::with_params(key.id(), DhtSearchPolicy::FastSearch(5)) .unwrap(); match ctx_test.dht.find_address(&mut ctx_search).await { - Ok(Some((ip, _))) => { + Ok(Some((ip, _, _))) => { println!("IP {}", ip); let node = node.into_boxed(); let node_encoded = base64_encode(&serialize_boxed(&node).unwrap()); @@ -1243,6 +1244,7 @@ fn test_stop() { params, &ctx_test.adnl.key_by_tag(KEY_TAG_OVERLAY).unwrap(), &Vec::new(), + false, ) .unwrap(); assert!(added); diff --git a/src/adnl/tests/test_quic.rs b/src/adnl/tests/test_quic.rs index d41d8b6..cab30a6 100644 --- a/src/adnl/tests/test_quic.rs +++ b/src/adnl/tests/test_quic.rs @@ -8,13 +8,13 @@ */ use adnl::{ - common::{AdnlPeers, QueryResult, Subscriber}, - node::AdnlNodeConfig, - QuicNode, + common::{AdnlPeers, QueryResult, Subscriber, Version}, + node::{AdnlNode, IpAddress}, + DhtNode, OverlayNode, QuicNode, }; use std::{ collections::HashSet, - net::SocketAddr, + net::Ipv4Addr, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -25,7 +25,12 @@ use tokio_util::sync::CancellationToken; use ton_api::{ deserialize_boxed, serialize_boxed, ton::{ - adnl::{pong::Pong as AdnlPong, Pong as AdnlPongBoxed}, + adnl::{ + address::address::{Quic, Udp}, + addresslist::AddressList, + pong::Pong as AdnlPong, + Address, Pong as AdnlPongBoxed, + }, quic::{request::Query as QuicQuery, Response as QuicResponse}, rpc::adnl::Ping as AdnlPing, }, @@ -33,15 +38,33 @@ use ton_api::{ }; use ton_block::{ ed25519_encode_private_key_to_pkcs8, ed25519_generate_private_key, Ed25519KeyOption, KeyId, - Result, }; +include!("../../common/src/config.rs"); include!("../../common/src/test.rs"); const KEY_TAG: usize = 0; const ITERATIONS: usize = 3; const MSG_PAYLOAD: &[u8] = b"quic test payload"; +fn ip_address_to_socket_addr(ip: &IpAddress) -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip.ip())), ip.port()) +} + +/// Helper: build an AddressList with the given addresses and current version. +fn make_address_list(addrs: Vec
) -> AddressList { + let version = Version::get(); + AddressList { addrs: addrs.into(), version, reinit_date: version, priority: 0, expire_at: 0 } +} + +fn udp_addr(ip: u32, port: u16) -> Address { + Address::Adnl_Address_Udp(Udp { ip: ip as i32, port: port as i32 }) +} + +fn quic_addr(ip: u32, port: u16) -> Address { + Address::Adnl_Address_Quic(Quic { ip: ip as i32, port: port as i32 }) +} + /// Routes messages and queries to a channel only when addressed to `key_id`. struct TestSubscriber { key_id: Arc, @@ -130,7 +153,12 @@ fn test_quic_concurrent_accept() { let server_bind: SocketAddr = format!("127.0.0.1:{}", SERVER_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let server = QuicNode::new(vec![server_sub], server_token.clone(), None); + let server = QuicNode::new( + vec![server_sub], + server_token.clone(), + None, + tokio::runtime::Handle::current(), + ); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); // --- clients --- @@ -158,7 +186,8 @@ fn test_quic_concurrent_accept() { let bind: SocketAddr = format!("127.0.0.1:{}", port + QuicNode::OFFSET_PORT).parse().unwrap(); let token = CancellationToken::new(); - let quic = QuicNode::new(vec![sub], token.clone(), None); + let quic = + QuicNode::new(vec![sub], token.clone(), None, tokio::runtime::Handle::current()); quic.add_key(&key, &key_id, bind).unwrap(); quic.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(key_id.clone(), bind).unwrap(); @@ -217,7 +246,7 @@ fn test_quic_concurrent_accept() { #[test] fn test_quic_session() { init_test_log(); - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); rt.block_on(async { let token_a = CancellationToken::new(); let token_b = CancellationToken::new(); @@ -253,10 +282,12 @@ fn test_quic_session() { let bind_a: SocketAddr = "127.0.0.1:5600".parse().unwrap(); let bind_b: SocketAddr = "127.0.0.1:5601".parse().unwrap(); - let quic_a = QuicNode::new(vec![sub_a], token_a.clone(), None); + let quic_a = + QuicNode::new(vec![sub_a], token_a.clone(), None, tokio::runtime::Handle::current()); quic_a.add_key(&key_bytes_a, &key_id_a, bind_a).unwrap(); - let quic_b = QuicNode::new(vec![sub_b], token_b.clone(), None); + let quic_b = + QuicNode::new(vec![sub_b], token_b.clone(), None, tokio::runtime::Handle::current()); quic_b.add_key(&key_bytes_b, &key_id_b, bind_b).unwrap(); // Register peer addresses @@ -330,7 +361,12 @@ fn test_quic_reconnect_after_server_restart() { let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) as Arc; - let client = QuicNode::new(vec![client_sub], client_token.clone(), None); + let client = QuicNode::new( + vec![client_sub], + client_token.clone(), + None, + tokio::runtime::Handle::current(), + ); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); // --- server B1 (will be shut down) --- @@ -349,7 +385,12 @@ fn test_quic_reconnect_after_server_restart() { Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx1 }) as Arc; - let server1 = QuicNode::new(vec![server_sub1], server_token1.clone(), None); + let server1 = QuicNode::new( + vec![server_sub1], + server_token1.clone(), + None, + tokio::runtime::Handle::current(), + ); server1.add_key(&server_key, &server_key_id, server_bind).unwrap(); // Register peer keys @@ -382,7 +423,12 @@ fn test_quic_reconnect_after_server_restart() { Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx2 }) as Arc; - let server2 = QuicNode::new(vec![server_sub2], server_token2.clone(), None); + let server2 = QuicNode::new( + vec![server_sub2], + server_token2.clone(), + None, + tokio::runtime::Handle::current(), + ); server2.add_key(&server_key, &server_key_id, server_bind).unwrap(); server2.add_peer_key(client_key_id.clone(), client_bind).unwrap(); println!("Step 3: server B2 started on same port with same key"); @@ -483,7 +529,7 @@ fn test_quic_stream_limit() { let server_bind: SocketAddr = format!("127.0.0.1:{}", SERVER_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); let server = - QuicNode::new(vec![server_sub], server_token.clone(), Some(STREAM_LIMIT)); + QuicNode::new(vec![server_sub], server_token.clone(), Some(STREAM_LIMIT), tokio::runtime::Handle::current()); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); // --- client (normal limits) --- @@ -502,7 +548,7 @@ fn test_quic_stream_limit() { let client_bind: SocketAddr = format!("127.0.0.1:{}", CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let client = QuicNode::new(vec![client_sub], client_token.clone(), None); + let client = QuicNode::new(vec![client_sub], client_token.clone(), None, tokio::runtime::Handle::current()); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); // Register peers @@ -586,7 +632,7 @@ fn make_endpoint( let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let sub = Arc::new(TestSubscriber { key_id: key_id.clone(), msg_tx: tx }) as Arc; - let quic = QuicNode::new(vec![sub], token.clone(), None); + let quic = QuicNode::new(vec![sub], token.clone(), None, tokio::runtime::Handle::current()); quic.add_key(&key, &key_id, bind).unwrap(); (quic, key, key_id, bind, token) } @@ -1538,7 +1584,8 @@ fn test_quic_connection_pool_exhaustion() { let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let sub = Arc::new(TestSubscriber { key_id: key_id.clone(), msg_tx: tx }) as Arc; - let quic = QuicNode::new(vec![sub], token.clone(), None); + let quic = + QuicNode::new(vec![sub], token.clone(), None, tokio::runtime::Handle::current()); quic.add_key(&key, &key_id, bind).unwrap(); quic.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(key_id.clone(), bind).unwrap(); @@ -1642,7 +1689,12 @@ fn test_quic_message_burst_reconnect() { let (cli_tx, _cli_rx) = tokio::sync::mpsc::unbounded_channel(); let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) as Arc; - let client = QuicNode::new(vec![client_sub], client_token.clone(), None); + let client = QuicNode::new( + vec![client_sub], + client_token.clone(), + None, + tokio::runtime::Handle::current(), + ); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); let server_key = ed25519_generate_private_key().unwrap().to_bytes(); @@ -1658,7 +1710,12 @@ fn test_quic_message_burst_reconnect() { let (srv_tx1, mut srv_rx1) = tokio::sync::mpsc::unbounded_channel(); let srv_sub1 = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx1 }) as Arc; - let server1 = QuicNode::new(vec![srv_sub1], srv_token1.clone(), None); + let server1 = QuicNode::new( + vec![srv_sub1], + srv_token1.clone(), + None, + tokio::runtime::Handle::current(), + ); server1.add_key(&server_key, &server_key_id, server_bind).unwrap(); client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); @@ -1698,7 +1755,12 @@ fn test_quic_message_burst_reconnect() { let (srv_tx2, mut srv_rx2) = tokio::sync::mpsc::unbounded_channel(); let srv_sub2 = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx2 }) as Arc; - let server2 = QuicNode::new(vec![srv_sub2], srv_token2.clone(), None); + let server2 = QuicNode::new( + vec![srv_sub2], + srv_token2.clone(), + None, + tokio::runtime::Handle::current(), + ); server2.add_key(&server_key, &server_key_id, server_bind).unwrap(); server2.add_peer_key(client_key_id.clone(), client_bind).unwrap(); @@ -1766,7 +1828,12 @@ fn test_quic_single_sender_invariant() { let (cli_tx, _cli_rx) = tokio::sync::mpsc::unbounded_channel(); let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) as Arc; - let client = QuicNode::new(vec![client_sub], client_token.clone(), None); + let client = QuicNode::new( + vec![client_sub], + client_token.clone(), + None, + tokio::runtime::Handle::current(), + ); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); let srv_token = CancellationToken::new(); @@ -1781,7 +1848,12 @@ fn test_quic_single_sender_invariant() { let (srv_tx, mut srv_rx) = tokio::sync::mpsc::unbounded_channel(); let srv_sub = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx }) as Arc; - let server = QuicNode::new(vec![srv_sub], srv_token.clone(), None); + let server = QuicNode::new( + vec![srv_sub], + srv_token.clone(), + None, + tokio::runtime::Handle::current(), + ); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); @@ -1854,3 +1926,280 @@ fn test_quic_single_sender_invariant() { drain_handle.abort(); }); } + +// --- TL serialization round-trip --- + +#[test] +fn test_quic_address_tl_roundtrip() { + let ip: u32 = u32::from(Ipv4Addr::new(1, 2, 3, 4)); + let port: u16 = 12345; + let list = make_address_list(vec![udp_addr(ip, 30000), quic_addr(ip, port)]); + + let bytes = serialize_boxed(&list.into_boxed()).unwrap(); + let restored = deserialize_boxed(&bytes) + .unwrap() + .downcast::() + .unwrap() + .only(); + + assert_eq!(restored.addrs.len(), 2); + match &restored.addrs[0] { + Address::Adnl_Address_Udp(u) => { + assert_eq!(u.ip as u32, ip); + assert_eq!(u.port, 30000); + } + other => panic!("Expected Udp, got {:?}", other), + } + match &restored.addrs[1] { + Address::Adnl_Address_Quic(q) => { + assert_eq!(q.ip as u32, ip); + assert_eq!(q.port, port as i32); + } + other => panic!("Expected Quic, got {:?}", other), + } +} + +#[test] +fn test_quic_address_tl_roundtrip_no_quic() { + let ip: u32 = u32::from(Ipv4Addr::new(10, 0, 0, 1)); + let list = make_address_list(vec![udp_addr(ip, 30000)]); + + let bytes = serialize_boxed(&list.into_boxed()).unwrap(); + let restored = deserialize_boxed(&bytes) + .unwrap() + .downcast::() + .unwrap() + .only(); + + assert_eq!(restored.addrs.len(), 1); + assert!(matches!(&restored.addrs[0], Address::Adnl_Address_Udp(_))); +} + +// --- parse_quic_address --- + +#[test] +fn test_parse_quic_address_present() { + let ip: u32 = u32::from(Ipv4Addr::new(192, 168, 1, 1)); + let list = make_address_list(vec![udp_addr(ip, 30000), quic_addr(ip, 31000)]); + + let (_, result) = AdnlNode::parse_address_list(&list).unwrap().unwrap(); + assert_eq!( + result.map(|q| ip_address_to_socket_addr(&q)), + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 31000)) + ); +} + +#[test] +fn test_parse_quic_address_absent() { + let ip: u32 = u32::from(Ipv4Addr::new(192, 168, 1, 1)); + let list = make_address_list(vec![udp_addr(ip, 30000)]); + + let (_, result) = AdnlNode::parse_address_list(&list).unwrap().unwrap(); + assert_eq!(result, None); +} + +#[test] +fn test_parse_quic_address_only_quic() { + let ip: u32 = u32::from(Ipv4Addr::new(10, 0, 0, 5)); + let list = make_address_list(vec![quic_addr(ip, 9999)]); + + // With parse_address_list, quic-only list returns None (no UDP address found) + let result = AdnlNode::parse_address_list(&list).unwrap(); + assert!(result.is_none()); +} + +#[test] +fn test_parse_quic_address_picks_first() { + let ip1: u32 = u32::from(Ipv4Addr::new(1, 1, 1, 1)); + let ip2: u32 = u32::from(Ipv4Addr::new(2, 2, 2, 2)); + let list = + make_address_list(vec![udp_addr(ip1, 30000), quic_addr(ip1, 31000), quic_addr(ip2, 32000)]); + + let (_, result) = AdnlNode::parse_address_list(&list).unwrap().unwrap(); + // Should return the first quic address + assert_eq!( + result.map(|q| ip_address_to_socket_addr(&q)), + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 31000)) + ); +} + +// --- parse_address_list still works (not broken by new variant) --- + +#[test] +fn test_parse_address_list_with_quic_and_udp() { + let ip: u32 = u32::from(Ipv4Addr::new(172, 16, 0, 1)); + let list = make_address_list(vec![udp_addr(ip, 30000), quic_addr(ip, 31000)]); + + let result = AdnlNode::parse_address_list(&list).unwrap(); + assert!(result.is_some()); + let (adnl_addr, _) = result.unwrap(); + assert_eq!(adnl_addr.ip(), ip); + assert_eq!(adnl_addr.port(), 30000); +} + +#[test] +fn test_parse_address_list_quic_only_returns_none() { + let ip: u32 = u32::from(Ipv4Addr::new(172, 16, 0, 1)); + let list = make_address_list(vec![quic_addr(ip, 31000)]); + + // parse_address_list looks at addrs[0] and expects UDP โ€” quic-only should return None + let result = AdnlNode::parse_address_list(&list).unwrap(); + assert!(result.is_none()); +} + +// --- TL wire compatibility: deserialize a quic address from raw bytes --- + +#[test] +fn test_quic_address_deserialize_from_bytes() { + // Build a known address list with quic, serialize, then deserialize + let ip: u32 = u32::from(Ipv4Addr::new(93, 174, 52, 11)); + let port: u16 = 40001; + let list = make_address_list(vec![udp_addr(ip, 30303), quic_addr(ip, port)]); + let bytes = serialize_boxed(&list.into_boxed()).unwrap(); + + // Deserialize from raw bytes (simulating reception from a C++ node) + let obj = deserialize_boxed(&bytes).unwrap(); + let restored = obj + .downcast::() + .expect("should deserialize as AddressList") + .only(); + + let (_, quic) = AdnlNode::parse_address_list(&restored).unwrap().unwrap(); + assert_eq!( + quic.map(|q| ip_address_to_socket_addr(&q)), + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(93, 174, 52, 11)), 40001)) + ); +} + +// --- DHT distribution tests --- + +fn init_local_dht_pair( + port1: u16, + port2: u16, +) -> ( + tokio::runtime::Runtime, + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, +) { + let rt = init_test(); + let mut config1 = rt + .block_on(get_adnl_config("quic_addr", &format!("127.0.0.1:{port1}"), vec![KEY_TAG], true)) + .unwrap(); + config1.set_ip_address_quic(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + port1 + 1000, + )); + let config2 = rt + .block_on(get_adnl_config("quic_addr", &format!("127.0.0.1:{port2}"), vec![KEY_TAG], true)) + .unwrap(); + let adnl1 = rt.block_on(AdnlNode::with_config(config1)).unwrap(); + let dht1 = DhtNode::with_adnl_node(adnl1.clone(), KEY_TAG).unwrap(); + let overlay1 = OverlayNode::with_params(adnl1.clone(), &[1u8; 32], KEY_TAG).unwrap(); + rt.block_on(adnl1.start_over_udp(vec![dht1.clone(), overlay1.clone()])).unwrap(); + let adnl2 = rt.block_on(AdnlNode::with_config(config2)).unwrap(); + let dht2 = DhtNode::with_adnl_node(adnl2.clone(), KEY_TAG).unwrap(); + let overlay2 = OverlayNode::with_params(adnl2.clone(), &[1u8; 32], KEY_TAG).unwrap(); + rt.block_on(adnl2.start_over_udp(vec![dht2.clone(), overlay2.clone()])).unwrap(); + (rt, adnl1, dht1, overlay1, adnl2, dht2, overlay2) +} + +/// Test: adnl.address.quic is stored in DHT and retrieved by another node. +/// +/// Node1 sets its QUIC port and stores its address via DHT (store_ip_address). +/// Node2 fetches node1's address from DHT (fetch_address). +/// Verify that node2's ADNL layer has the correct QUIC address for node1. +#[test] +fn test_quic_address_dht_distribution() { + let (rt, adnl1, dht1, _overlay1, adnl2, dht2, _overlay2) = init_local_dht_pair(4291, 4292); + + rt.block_on(async { + // Connect the two DHT nodes + let peer1 = dht2.add_peer(&dht1.get_signed_node().unwrap()).unwrap().unwrap(); + let peer2 = dht1.add_peer(&dht2.get_signed_node().unwrap()).unwrap().unwrap(); + assert!(dht1.ping(&peer2).await.unwrap()); + assert!(dht2.ping(&peer1).await.unwrap()); + + // Node1: QUIC address was set in config. + // build_address_list will include adnl.address.quic automatically. + let quic_addr_expected = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5291); + + // Verify build_address_list includes the quic address + let addr_list = adnl1.build_address_list(None).unwrap(); + let (_, parsed_quic) = AdnlNode::parse_address_list(&addr_list).unwrap().unwrap(); + assert!(parsed_quic.is_some(), "build_address_list should include adnl.address.quic"); + assert_eq!(ip_address_to_socket_addr(&parsed_quic.unwrap()), quic_addr_expected); + + // Store in DHT + assert!(dht1.store_ip_address(&dht1.key()).await.unwrap()); + + // Node2: fetch node1's address from DHT + let key1_id = dht1.key().id().clone(); + let fetched = dht2.fetch_address(&key1_id).await.unwrap(); + assert!(fetched.is_some(), "Node2 should find node1's address in DHT"); + + let (adnl_addr, _, _key) = fetched.unwrap(); + // Verify the UDP address was parsed correctly + assert_eq!(adnl_addr.port(), 4291, "UDP port should match node1"); + + // Verify the QUIC address was extracted and stored in the ADNL layer + let local_key2 = adnl2.key_by_tag(KEY_TAG).unwrap().id().clone(); + let peer_addrs = adnl2.peer_ip_address(&local_key2, &key1_id).unwrap(); + assert!(peer_addrs.is_some(), "Node2 should have address for node1 after DHT fetch"); + let (_, quic_addr) = peer_addrs.unwrap(); + assert!(quic_addr.is_some(), "Node2 should have QUIC address for node1 after DHT fetch"); + let quic_addr = quic_addr.unwrap(); + assert_eq!( + quic_addr, quic_addr_expected, + "QUIC address should match what node1 advertised" + ); + + adnl1.stop().await; + adnl2.stop().await; + }); +} + +/// Test: address list without adnl.address.quic does NOT set peer_quic_address. +/// +/// Node1 does NOT set a QUIC port, stores its address via DHT. +/// Node2 fetches it and verifies no QUIC address is stored. +#[test] +fn test_no_quic_address_dht_distribution() { + let (rt, adnl1, dht1, _overlay1, adnl2, dht2, _overlay2) = init_local_dht_pair(4293, 4294); + + rt.block_on(async { + let peer1 = dht2.add_peer(&dht1.get_signed_node().unwrap()).unwrap().unwrap(); + let peer2 = dht1.add_peer(&dht2.get_signed_node().unwrap()).unwrap().unwrap(); + assert!(dht1.ping(&peer2).await.unwrap()); + assert!(dht2.ping(&peer1).await.unwrap()); + + // Node1: no QUIC port set โ€” build_address_list should only have UDP + let addr_list = adnl1.build_address_list(None).unwrap(); + let (_, quic_addr) = AdnlNode::parse_address_list(&addr_list).unwrap().unwrap(); + assert!( + quic_addr.is_none(), + "Without set_quic_address, address list should not contain adnl.address.quic" + ); + + // Store and fetch + assert!(dht1.store_ip_address(&dht1.key()).await.unwrap()); + let key1_id = dht1.key().id().clone(); + let fetched = dht2.fetch_address(&key1_id).await.unwrap(); + assert!(fetched.is_some()); + + // Verify no QUIC address was stored + let local_key2 = adnl2.key_by_tag(KEY_TAG).unwrap().id().clone(); + let peer_addrs = adnl2.peer_ip_address(&local_key2, &key1_id).unwrap(); + let quic_addr = peer_addrs.and_then(|(_, q)| q); + assert!( + quic_addr.is_none(), + "No QUIC address should be stored when peer doesn't advertise one" + ); + + adnl1.stop().await; + adnl2.stop().await; + }); +} diff --git a/src/adnl/tests/test_rldp.rs b/src/adnl/tests/test_rldp.rs index 138d4ed..6abf51d 100644 --- a/src/adnl/tests/test_rldp.rs +++ b/src/adnl/tests/test_rldp.rs @@ -68,9 +68,9 @@ fn init_rldp_compatibility_test( #[cfg(feature = "dump")] None, ); - let ours_ip = format!("{}", ctx_test.adnl.ip_address()); + let ours_ip = format!("{}", ctx_test.adnl.ip_address_adnl()); let pos = ours_ip.find(":").unwrap(); - let ours_ip = format!("{}:{}", &ours_ip[..pos], ctx_test.adnl.ip_address().port() + 1); + let ours_ip = format!("{}:{}", &ours_ip[..pos], ctx_test.adnl.ip_address_adnl().port() + 1); let config = ctx_test.rt.block_on(get_adnl_config("rldp", &ours_ip, vec![KEY_TAG], true)).unwrap(); let adnl = ctx_test.rt.block_on(AdnlNode::with_config(config)).unwrap(); @@ -97,7 +97,7 @@ fn find_rldp_peer( let (peer_ip, peer_node) = find_overlay_peer(overlay_peers, ctx_search, ctx_test, TARGET); let ours_id = adnl.key_by_tag(KEY_TAG).unwrap().id().clone(); let peer_key: Arc = (&peer_node.id).try_into().unwrap(); - let peer_id = adnl.add_peer(&ours_id, &peer_ip, &peer_key).unwrap().unwrap(); + let peer_id = adnl.add_peer(&ours_id, &peer_ip, None, &peer_key).unwrap().unwrap(); (peer_id, ours_id) } @@ -193,8 +193,8 @@ fn init_local_test( rt.block_on(adnl2.start_over_udp(vec![rldp2.clone()])).unwrap(); let peer1 = adnl1.key_by_tag(KEY_TAG).unwrap(); let peer2 = adnl2.key_by_tag(KEY_TAG).unwrap(); - adnl1.add_peer(peer1.id(), adnl2.ip_address(), &peer2).unwrap(); - adnl2.add_peer(peer2.id(), adnl1.ip_address(), &peer1).unwrap(); + adnl1.add_peer(peer1.id(), adnl2.ip_address_adnl(), None, &peer2).unwrap(); + adnl2.add_peer(peer2.id(), adnl1.ip_address_adnl(), None, &peer1).unwrap(); let ctx1 = RldpContext { adnl: adnl1, peer: peer1.id().clone(), rldp: rldp1 }; let ctx2 = RldpContext { adnl: adnl2, peer: peer2.id().clone(), rldp: rldp2 }; (rt, ctx1, ctx2) diff --git a/src/adnl/tests/test_udp.rs b/src/adnl/tests/test_udp.rs index e3d765d..00d8864 100644 --- a/src/adnl/tests/test_udp.rs +++ b/src/adnl/tests/test_udp.rs @@ -161,6 +161,7 @@ async fn test_address( .add_peer( src.id(), &IpAddress::from_versioned_string(ip, Some(version)).unwrap(), + None, &node2.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() @@ -320,7 +321,8 @@ fn node_async_query() { let peer1 = node2 .add_peer( node2.key_by_tag(KEY_TAG).unwrap().id(), - node1.ip_address(), + node1.ip_address_adnl(), + None, &node1.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() @@ -328,7 +330,8 @@ fn node_async_query() { let peer2 = node1 .add_peer( node1.key_by_tag(KEY_TAG).unwrap().id(), - node2.ip_address(), + node2.ip_address_adnl(), + None, &node2.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() diff --git a/src/adnl/tests/test_utils.rs b/src/adnl/tests/test_utils.rs index 81a0945..5fe4592 100644 --- a/src/adnl/tests/test_utils.rs +++ b/src/adnl/tests/test_utils.rs @@ -279,13 +279,13 @@ pub fn find_overlay_peer( } } let (ip, node) = peers.pop().unwrap(); - if ip.to_udp() == ctx_test.adnl.ip_address().to_udp() { + if ip.to_udp() == ctx_test.adnl.ip_address_adnl().to_udp() { continue; } log::info!( target: log_target, "---- Try overlay peer {} {}, own address {}", - ip, node, ctx_test.adnl.ip_address() + ip, node, ctx_test.adnl.ip_address_adnl() ); let peer = ctx_test .overlay diff --git a/src/node/bin/adnl_ping.rs b/src/node/bin/adnl_ping.rs index fe8bf30..1012852 100644 --- a/src/node/bin/adnl_ping.rs +++ b/src/node/bin/adnl_ping.rs @@ -58,7 +58,7 @@ fn ping( let local_key = adnl.key_by_tag(KEY_TAG)?; let other_key = Arc::new(Ed25519KeyOption::from_public_key((&base64_decode(pub_key)?[..]).try_into()?)); - let other_id = adnl.add_peer(local_key.id(), &ip, &other_key)?; + let other_id = adnl.add_peer(local_key.id(), &ip, None, &other_key)?; let other_id = if let Some(other_id) = other_id { other_id } else { fail!("Cannot add peer to ADNL") }; diff --git a/src/node/bin/adnl_resolve.rs b/src/node/bin/adnl_resolve.rs index e213062..b087f62 100644 --- a/src/node/bin/adnl_resolve.rs +++ b/src/node/bin/adnl_resolve.rs @@ -45,8 +45,8 @@ async fn scan(adnlid: &str, cfgfile: &str) -> Result<()> { let mut index = 0; println!("Searching DHT for {}...", keyid); loop { - if let Ok(Some((ip, key))) = dht.find_address(&mut context).await { - println!("Found {} / {}", ip, key.id()); + if let Ok(Some((adnl_addr, quic_addr, key))) = dht.find_address(&mut context).await { + println!("Found ADNL={} QUIC={:?} / {}", adnl_addr, quic_addr, key.id()); return Ok(()); } if index >= nodes.len() { diff --git a/src/node/bin/dhtscan.rs b/src/node/bin/dhtscan.rs index b080808..f1e54ea 100644 --- a/src/node/bin/dhtscan.rs +++ b/src/node/bin/dhtscan.rs @@ -8,18 +8,32 @@ */ use adnl::{ node::{AdnlNode, AdnlNodeConfig}, - DhtNode, DhtSearchPolicy, OverlayNode, OverlayNodesSearchContext, + AddressSearchContext, DhtNode, DhtSearchPolicy, OverlayNode, OverlayNodesSearchContext, }; use node::config::TonNodeGlobalConfigJson; -use std::{collections::HashMap, env, fs::File, io::BufReader, ops::Deref, sync::Arc}; +use std::{ + collections::HashMap, + env, + fs::File, + io::BufReader, + net::{IpAddr, Ipv4Addr, SocketAddr}, + ops::Deref, + sync::Arc, +}; use ton_block::{base64_encode, error, fail, KeyOption, Result}; include!("../../common/src/log.rs"); -const IP: &str = "0.0.0.0:4191"; +const DEFAULT_IP: &str = "0.0.0.0:4191"; const KEY_TAG: usize = 1; -fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) -> Result<()> { +fn scan( + cfgfile: &str, + bind_addr: &str, + jsonl: bool, + search_overlay: bool, + use_workchain0: bool, +) -> Result<()> { let file = File::open(cfgfile)?; let reader = BufReader::new(file); let config: TonNodeGlobalConfigJson = serde_json::from_reader(reader) @@ -29,7 +43,8 @@ fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) let dht_nodes = config.get_dht_nodes_configs()?; let mut rt = tokio::runtime::Runtime::new()?; - let (_, config) = AdnlNodeConfig::with_ip_address_and_private_key_tags(IP, vec![KEY_TAG])?; + let (_, config) = + AdnlNodeConfig::with_ip_address_and_private_key_tags(bind_addr, vec![KEY_TAG])?; let adnl = rt.block_on(AdnlNode::with_config(config))?; let dht = DhtNode::with_adnl_node(adnl.clone(), KEY_TAG)?; let overlay = OverlayNode::with_params(adnl.clone(), zero_state.as_slice(), KEY_TAG)?; @@ -45,6 +60,16 @@ fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) } } + // Fetch signed address lists from preset nodes (picks up QUIC addresses) + println!("Querying preset DHT nodes for signed address lists..."); + for node in preset_nodes.iter() { + match rt.block_on(dht.get_signed_address_list(node)) { + Ok(true) => println!(" {} - OK", node), + Ok(false) => println!(" {} - no response", node), + Err(e) => println!(" {} - error: {}", node, e), + } + } + println!("Scanning DHT..."); for node in preset_nodes.iter() { rt.block_on(dht.find_dht_nodes(node))?; @@ -59,28 +84,47 @@ fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) } let mut count = 0; + let mut quic_count = 0; let nodes = dht.get_known_nodes(5000)?; - if nodes.len() > dht_nodes.len() { + if !nodes.is_empty() { println!("---- Found DHT nodes:"); for node in nodes { - let mut skip = false; - for dht_node in dht_nodes.iter() { - if dht_node.id == node.id { - skip = true; - break; - } - } - if skip { - continue; - } let key: Arc = (&node.id).try_into()?; match rt.block_on(dht.ping(key.id())) { Ok(true) => (), _ => continue, } - let adr = AdnlNode::parse_address_list(&node.addr_list)? - .ok_or_else(|| error!("Cannot parse address list {:?}", node.addr_list))? - .to_udp(); + let (adnl_addr, quic_addr) = AdnlNode::parse_address_list(&node.addr_list)? + .ok_or_else(|| error!("Cannot parse address list {:?}", node.addr_list))?; + let adr = adnl_addr.to_udp(); + + // If not in the node record, try DHT value lookup for stored address + // If no QUIC address from the node record, try DHT value lookup + let quic_socket_addr = if let Some(q) = &quic_addr { + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(q.ip())), q.port())) + } else { + let mut ctx = + AddressSearchContext::with_params(key.id(), DhtSearchPolicy::FastSearch(3))?; + if let Ok(Some((_, quic_addr, _))) = rt.block_on(dht.find_address(&mut ctx)) { + quic_addr.map(|q| SocketAddr::new(IpAddr::V4(Ipv4Addr::from(q.ip())), q.port())) + } else { + None + } + }; + + let mut addrs = vec![serde_json::json!({ + "@type": "adnl.address.udp", + "ip": adr.ip, + "port": adr.port + })]; + if let Some(q) = quic_socket_addr { + quic_count += 1; + addrs.push(serde_json::json!({ + "@type": "adnl.address.quic", + "ip": q.ip().to_string(), + "port": q.port() + })); + } let json = serde_json::json!( { "@type": "dht.node", @@ -90,13 +134,7 @@ fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) }, "addr_list": { "@type": "adnl.addressList", - "addrs": [ - { - "@type": "adnl.address.udp", - "ip": adr.ip, - "port": adr.port - } - ], + "addrs": addrs, "version": node.addr_list.version, "reinit_date": node.addr_list.reinit_date, "priority": node.addr_list.priority, @@ -116,10 +154,23 @@ fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) } ); } - println!("Total: {} DHT nodes", count); + println!("Total: {} DHT nodes ({} with QUIC)", count, quic_count); } else { - println!("---- No DHT nodes found"); + println!("---- No DHT nodes found via routing table"); + } + + // Also show QUIC addresses discovered from preset nodes via getSignedAddressList + println!("\n---- Preset node QUIC addresses (from getSignedAddressList):"); + let local_key = adnl.key_by_tag(KEY_TAG)?.id().clone(); + for node in preset_nodes.iter() { + let addrs = adnl.peer_ip_address(&local_key, node).ok().flatten(); + let (adnl_addr, quic_addr) = match addrs { + Some((a, q)) => (Some(a), q), + None => (None, None), + }; + println!(" {} ADNL={:?} QUIC={:?}", node, adnl_addr, quic_addr); } + Ok(()) } @@ -175,24 +226,29 @@ fn main() { let mut jsonl = false; let mut overlay = false; let mut workchain0 = false; - for arg in env::args().skip(1) { - if arg == "--jsonl" { - jsonl = true - } else if arg == "--overlay" { - overlay = true - } else if arg == "--workchain0" { - workchain0 = true - } else { - config = Some(arg) + let mut bind_addr = DEFAULT_IP.to_string(); + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--jsonl" => jsonl = true, + "--overlay" => overlay = true, + "--workchain0" => workchain0 = true, + "--bind" => { + bind_addr = args.next().unwrap_or_else(|| { + eprintln!("--bind requires an argument (e.g. 127.0.0.1:4191)"); + std::process::exit(1); + }); + } + _ => config = Some(arg), } } let config = if let Some(config) = config { config } else { - println!("Usage: dhtscan [--jsonl] [--overlay] [--workchain0] "); + println!("Usage: dhtscan [--jsonl] [--overlay] [--workchain0] [--bind ip:port] "); return; }; init_log("./common/config/log_cfg.yml"); - scan(&config, jsonl, overlay, workchain0) + scan(&config, &bind_addr, jsonl, overlay, workchain0) .unwrap_or_else(|e| println!("DHT scanning error: {}", e)) } diff --git a/src/node/consensus-common/src/adnl_overlay.rs b/src/node/consensus-common/src/adnl_overlay.rs index 4b9da5f..aad8d94 100644 --- a/src/node/consensus-common/src/adnl_overlay.rs +++ b/src/node/consensus-common/src/adnl_overlay.rs @@ -572,7 +572,7 @@ impl Peer { // Try to fetch address from DHT match self.dht_node.fetch_address(&self.dst_adnl_addr).await { - Ok(Some((addr, key))) => { + Ok(Some((adnl_addr, quic_addr, key))) => { // Check if address changed (first time or address different) let addr_changed = current_addr.is_none(); @@ -591,9 +591,10 @@ impl Peer { } // Add new address - let add_result = self - .overlay_node - .add_private_peers(&self.src_adnl_addr, vec![(addr, key)]); + let add_result = self.overlay_node.add_private_peers( + &self.src_adnl_addr, + vec![(adnl_addr, quic_addr, key)], + ); if let Err(e) = add_result { log::warn!(target: LOG_TARGET, "Error adding peer address: {:?}", e); @@ -939,7 +940,7 @@ impl AdnlOverlay { if let Some(quic) = &stack.quic { // Register local validator's ADNL key as a TLS identity on a per-port endpoint let key_bytes: [u8; 32] = *local_adnl_key.pvt_key()?; - let ip_addr = stack.adnl.ip_address(); + let ip_addr = stack.adnl.ip_address_adnl(); let quic_port = ip_addr.port().checked_add(adnl::QuicNode::OFFSET_PORT).ok_or_else(|| { error!( @@ -982,7 +983,7 @@ impl AdnlOverlay { overlay_id: &overlay_id, runtime: Some(runtime_handle.clone()), }; - stack.overlay.add_private_overlay(params, &local_adnl_key, &peers)?; + stack.overlay.add_private_overlay(params, &local_adnl_key, &peers, use_quic)?; let stop_requested = Arc::new(AtomicBool::new(false)); @@ -1019,11 +1020,8 @@ impl AdnlOverlay { stop_requested.clone(), )); - // Point-to-point multicast is used for broadcasts when TCP or QUIC - // transport is available (FEC/UDP broadcast has no peers in private overlays). - let is_tcp_available = - (stack.is_tcp_available() && allow_tcp_communication) || quic_enabled; - + // Point-to-point multicast is used for broadcasts when TCP transport is available + let is_tcp_available = stack.is_tcp_available() && allow_tcp_communication; if is_tcp_available { log::debug!( target: LOG_TARGET, @@ -1680,13 +1678,13 @@ impl ConsensusOverlay for AdnlOverlay { } if self.is_quic_available || !self.is_tcp_available { - // QUIC or UDP path + // QUIC or ADNL/UDP path // If extra given, use two-step broadcast via QUIC/RLDP // Otherwise use canonic broadcast via ADNL let msg = payload.clone(); let overlay_node = self.stack.overlay.clone(); let local_validator_key = self.local_validator_key.clone(); - let transport = if self.is_quic_available { "QUIC" } else { "UDP" }; + let transport = if self.is_quic_available { "QUIC" } else { "ADNL/UDP" }; self.runtime_handle.spawn(async move { if stop_requested.load(Ordering::Relaxed) { diff --git a/src/node/consensus-common/src/lib.rs b/src/node/consensus-common/src/lib.rs index c8e36f2..769c176 100644 --- a/src/node/consensus-common/src/lib.rs +++ b/src/node/consensus-common/src/lib.rs @@ -644,7 +644,7 @@ pub enum OverlayTransportType { impl OverlayTransportType { pub fn allow_tcp(&self) -> bool { - matches!(self, Self::CatchainTcp | Self::Simplex) + matches!(self, Self::CatchainTcp) } pub fn use_quic(&self) -> bool { diff --git a/src/node/consensus-common/src/node_test_network.rs b/src/node/consensus-common/src/node_test_network.rs index f9aebf6..6d16f58 100644 --- a/src/node/consensus-common/src/node_test_network.rs +++ b/src/node/consensus-common/src/node_test_network.rs @@ -243,8 +243,12 @@ impl<'a> NodeTestNetwork<'a> { overlay.set_rldp(rldp.clone()).unwrap(); let quic = if is_quic_enabled { - let quic = - QuicNode::new(vec![overlay.clone()], cancellation_token.clone(), None); + let quic = QuicNode::new( + vec![overlay.clone()], + cancellation_token.clone(), + None, + tokio::runtime::Handle::current(), + ); overlay.set_quic(quic.clone()).unwrap(); Some(quic) } else { diff --git a/src/node/src/config.rs b/src/node/src/config.rs index db4bd8e..d48bd12 100644 --- a/src/node/src/config.rs +++ b/src/node/src/config.rs @@ -366,6 +366,9 @@ pub struct TonNodeConfig { unsafe_catchain_patches_path: Option, #[serde(skip_serializing)] ip_address: Option, + /// Explicit QUIC address (ip:port). If absent, derived as same_ip:adnl_port+1000. + #[serde(skip_serializing_if = "Option::is_none")] + ip_address_quic: Option, adnl_node: Option, json_rpc_server: Option, metrics: Option, @@ -567,6 +570,9 @@ impl TonNodeConfig { if let Some(port) = self.port { ret.set_port(port) } + if let Some(quic_addr) = self.quic_address() { + ret.set_ip_address_quic(quic_addr); + } Ok(ret) } @@ -687,6 +693,31 @@ impl TonNodeConfig { self.accelerated_consensus_disabled } + pub fn quic_address(&self) -> Option { + self.ip_address_quic.as_ref().and_then(|s| match s.parse::() { + Ok(addr) => { + if !addr.ip().is_ipv4() { + log::warn!( + "ip_address_quic \"{s}\" is not an IPv4 address. \ + ADNL/TL address lists only support IPv4, so this QUIC address \ + cannot be advertised. QUIC address will not be used, \ + node will fall back to derived port." + ); + None + } else { + Some(addr) + } + } + Err(e) => { + log::warn!( + "Failed to parse ip_address_quic \"{s}\": {e}. \ + QUIC address will not be used, node will fall back to derived port." + ); + None + } + }) + } + #[cfg(test)] pub fn set_port(&mut self, port: u16) { self.port.replace(port); diff --git a/src/node/src/network/catchain_client.rs b/src/node/src/network/catchain_client.rs index c96dd7e..51d7146 100644 --- a/src/node/src/network/catchain_client.rs +++ b/src/node/src/network/catchain_client.rs @@ -89,7 +89,7 @@ impl CatchainClient { overlay_id, runtime: Some(runtime_handle.clone()), }; - network_context.stack.overlay.add_private_overlay(params, local_adnl_key, &peers)?; + network_context.stack.overlay.add_private_overlay(params, local_adnl_key, &peers, false)?; let consumer = Arc::new(CatchainClientConsumer::new(overlay_id.clone(), catchain_listener)); network_context.stack.overlay.add_consumer(overlay_id, consumer.clone())?; diff --git a/src/node/src/network/custom_overlay_client.rs b/src/node/src/network/custom_overlay_client.rs index 907c7bb..1e8023f 100644 --- a/src/node/src/network/custom_overlay_client.rs +++ b/src/node/src/network/custom_overlay_client.rs @@ -130,7 +130,7 @@ impl CustomOverlayClient { }; let params = OverlayParams { flags: 0, hops: None, overlay_id: &self.id, runtime: None }; - if let Err(e) = self.overlay_node.add_private_overlay(params, &key, &peers) { + if let Err(e) = self.overlay_node.add_private_overlay(params, &key, &peers, false) { attempt += 1; if attempt >= 10 { fail!("Error while adding custom overlay \"{}\": {}", self.config.name, e); @@ -439,9 +439,14 @@ impl CustomOverlayClient { )?) .await { - Ok(Some((ip, key))) => { - log::debug!("{}: peer {}: found ip: {ip:?}, key: {key:x?}", self.id, peer); - self.overlay_node.add_private_peers(&local_key, vec![(ip, key)])?; + Ok(Some((adnl_addr, quic_addr, key))) => { + log::debug!( + "{}: peer {}: found ip: {adnl_addr:?}, key: {key:x?}", + self.id, + peer + ); + self.overlay_node + .add_private_peers(&local_key, vec![(adnl_addr, quic_addr, key)])?; } Ok(None) => { log::warn!("{}: find address for {} failed", self.id, &peer); diff --git a/src/node/src/network/node_network.rs b/src/node/src/network/node_network.rs index 0f6189b..682ae38 100644 --- a/src/node/src/network/node_network.rs +++ b/src/node/src/network/node_network.rs @@ -28,7 +28,7 @@ use catchain::{ }; use std::{ hash::Hash, - net::{Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{ atomic::{AtomicI32, Ordering}, Arc, @@ -43,6 +43,8 @@ pub struct NetworkContext { pub stack: Arc, pub catchain_overlay_manager: CatchainOverlayManagerPtr, pub broadcast_hops: Option, + /// Explicit QUIC address from config (None = derive as same_ip:adnl_port+1000) + pub quic_address: Option, #[cfg(feature = "telemetry")] pub telemetry: FullNodeNetworkTelemetry, #[cfg(feature = "telemetry")] @@ -161,7 +163,12 @@ impl NodeNetwork { // Initialize QUIC transport (lazy: no endpoint bound until add_key() is called). // Validator ADNL keys are registered when a validator set is activated. let quic = { - let quic = adnl::QuicNode::new(vec![overlay.clone()], cancellation_token.clone(), None); + let quic = adnl::QuicNode::new( + vec![overlay.clone()], + cancellation_token.clone(), + None, + tokio::runtime::Handle::current(), + ); overlay.set_quic(quic.clone())?; Some(quic) }; @@ -171,6 +178,13 @@ impl NodeNetwork { dht.add_peer(peer)?; } + let default_rldp_roundtrip = config.default_rldp_roundtrip(); + let quic_address = config.quic_address(); + + if quic_address.is_some() { + log::info!("QUIC address set for advertising: {:?}", adnl.ip_address_quic()); + } + let dht_key = adnl.key_by_tag(Self::TAG_DHT_KEY)?; NodeNetwork::periodic_store_ip_addr(dht.clone(), dht_key, None, cancellation_token.clone()); @@ -182,8 +196,6 @@ impl NodeNetwork { cancellation_token.clone(), ); - let default_rldp_roundtrip = config.default_rldp_roundtrip(); - NodeNetwork::find_dht_nodes(dht.clone(), cancellation_token.clone()); let (config_handler, config_handler_context) = @@ -213,6 +225,7 @@ impl NodeNetwork { stack, catchain_overlay_manager, broadcast_hops, + quic_address, #[cfg(feature = "telemetry")] telemetry: FullNodeNetworkTelemetry::new_client(), #[cfg(feature = "telemetry")] @@ -242,7 +255,7 @@ impl NodeNetwork { pub async fn start(&self) -> Result<()> { log::info!( "start network: ip: {}, adnl_id: {}", - self.network_context.stack.adnl.ip_address(), + self.network_context.stack.adnl.ip_address_adnl(), self.network_context.stack.adnl.key_by_tag(Self::TAG_OVERLAY_KEY)?.id() ); self.network_context.stack.start_over_udp_tcp().await?; @@ -414,15 +427,23 @@ impl NodeNetwork { )?) .await { - Ok(Some((ip, key))) => { - log::info!("peer {}: found ip: {ip:?}, key: {key:x?}", val.adnl_id); + Ok(Some((adnl_addr, quic_addr, key))) => { + log::info!("peer {}: found ip: {adnl_addr:?}, key: {key:x?}", val.adnl_id); match full_node_callback { Some(ref callback) => { - adnl.add_peer(&local_adnl_id, &ip, &Arc::new(key))?; + adnl.add_peer( + &local_adnl_id, + &adnl_addr, + quic_addr.as_ref(), + &Arc::new(key), + )?; callback(val.adnl_id.clone()); } None => { - overlay.add_private_peers(&local_adnl_id, vec![(ip, key)])?; + overlay.add_private_peers( + &local_adnl_id, + vec![(adnl_addr, quic_addr, key)], + )?; } } } @@ -484,15 +505,33 @@ impl NodeNetwork { |e| error!("Cannot add validator ADNL key {key_id} for election {election_id}: {e}"), )?; if let Some(quic) = &self.network_context.stack.quic { - let ip_addr = self.network_context.stack.adnl.ip_address(); - let Some(quic_port) = ip_addr.port().checked_add(adnl::QuicNode::OFFSET_PORT) else { + let adnl_ip = self.network_context.stack.adnl.ip_address_adnl(); + let quic_addr = if let Some(addr) = self.network_context.quic_address { + addr + } else { + let Some(quic_port) = adnl_ip.port().checked_add(adnl::QuicNode::OFFSET_PORT) + else { + log::warn!( + "QUIC port overflow for ADNL port {}, skipping QUIC key {key_id}", + adnl_ip.port() + ); + return Ok(true); + }; + SocketAddr::new(Ipv4Addr::from(adnl_ip.ip()).into(), quic_port) + }; + let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), quic_addr.port()); + if quic_addr.ip() != IpAddr::from(Ipv4Addr::UNSPECIFIED) + && quic_addr.ip() != IpAddr::from(Ipv4Addr::from(adnl_ip.ip())) + { log::warn!( - "QUIC port overflow for ADNL port {}, skipping QUIC key {key_id}", - ip_addr.port() + "QUIC configured address {} differs from ADNL IP {}; \ + binding to {} but advertising {}", + quic_addr, + adnl_ip, + bind_addr, + quic_addr ); - return Ok(true); - }; - let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), quic_port); + } match adnl_key.pvt_key() { Ok(pvt_key) => { if let Err(e) = quic.add_key(pvt_key, &key_id, bind_addr) { @@ -674,9 +713,9 @@ impl PrivateOverlayOperations for NodeNetwork { } peers_ids.push(val.adnl_id.clone()); match self.network_context.stack.dht.fetch_address(&val.adnl_id).await { - Ok(Some((addr, key))) => { - log::info!("addr: {:?}, key: {:x?}", &addr, &key); - peers.push((addr, key)); + Ok(Some((adnl_addr, quic_addr, key))) => { + log::info!("addr: {:?}, key: {:x?}", &adnl_addr, &key); + peers.push((adnl_addr, quic_addr, key)); } Ok(None) => { log::info!("addr: {:?} skipped.", &val.adnl_id); diff --git a/src/node/src/network/tests/test_full_node_overlays.rs b/src/node/src/network/tests/test_full_node_overlays.rs index a954832..6c958b9 100644 --- a/src/node/src/network/tests/test_full_node_overlays.rs +++ b/src/node/src/network/tests/test_full_node_overlays.rs @@ -232,6 +232,7 @@ async fn test_overlay_client() { .add_peer( this_key.id(), &IpAddress::from_versioned_string("127.0.0.1:5000", None).unwrap(), + None, &peer_key, ) .unwrap() diff --git a/src/node/tests/compat_test/cpp_src/compat_test_node.cpp b/src/node/tests/compat_test/cpp_src/compat_test_node.cpp index 87e6af6..896ac78 100644 --- a/src/node/tests/compat_test/cpp_src/compat_test_node.cpp +++ b/src/node/tests/compat_test/cpp_src/compat_test_node.cpp @@ -8,7 +8,7 @@ * {"cmd": "ping"} * {"cmd": "get_info"} * {"cmd": "compute_overlay_id", "name": "BASE64_BYTES"} - * {"cmd": "add_peer", "pubkey": "BASE64_TL_PUBKEY", "ip": "127.0.0.1", "port": 14001} + * {"cmd": "add_peer", "pubkey": "BASE64_TL_PUBKEY", "ip": "127.0.0.1", "port": 14001, "quic_port": 0} * {"cmd": "create_overlay", "type": "public|private|semiprivate", * "overlay_name": "BASE64_TL_BYTES", * "peers": ["ADNL_ID_HEX", ...], @@ -366,6 +366,12 @@ void CompatTestNode::cmd_add_peer(td::JsonObject &obj) { // Build address list for the peer ton::adnl::AdnlAddressList peer_addr_list; peer_addr_list.add_udp_adnl_address(addr).ensure(); + auto quic_port = static_cast(get_int(obj, "quic_port")); + if (quic_port != 0) { + td::IPAddress quic_addr; + quic_addr.init_ipv4_port(ip, quic_port).ensure(); + peer_addr_list.add_quic_addr(quic_addr).ensure(); + } peer_addr_list.set_version(static_cast(td::Clocks::system())); peer_addr_list.set_reinit_date(ton::adnl::Adnl::adnl_start_time()); diff --git a/src/node/tests/compat_test/src/lib.rs b/src/node/tests/compat_test/src/lib.rs index 27984f7..30828ef 100644 --- a/src/node/tests/compat_test/src/lib.rs +++ b/src/node/tests/compat_test/src/lib.rs @@ -135,6 +135,9 @@ pub enum CppCommand { pubkey: String, ip: String, port: u16, + /// Optional explicit QUIC port (included as adnl.address.quic in address list) + #[serde(skip_serializing_if = "Option::is_none")] + quic_port: Option, }, #[serde(rename = "create_overlay")] @@ -516,10 +519,22 @@ impl CppTestNode { /// Add a peer to the ADNL peer table pub fn add_peer(&mut self, pubkey_tl_b64: &str, ip: &str, port: u16) -> Result { + self.add_peer_with_quic(pubkey_tl_b64, ip, port, None) + } + + /// Add a peer with an optional explicit QUIC address (adnl.address.quic) + pub fn add_peer_with_quic( + &mut self, + pubkey_tl_b64: &str, + ip: &str, + port: u16, + quic_port: Option, + ) -> Result { let result = self.expect_result(&CppCommand::AddPeer { pubkey: pubkey_tl_b64.to_string(), ip: ip.to_string(), port, + quic_port, })?; result["peer_id"] .as_str() diff --git a/src/node/tests/compat_test/src/test_helpers.rs b/src/node/tests/compat_test/src/test_helpers.rs index 8b1aefb..c774c99 100644 --- a/src/node/tests/compat_test/src/test_helpers.rs +++ b/src/node/tests/compat_test/src/test_helpers.rs @@ -28,6 +28,11 @@ use std::{ use ton_api::{ deserialize_boxed, serialize_boxed, serialize_boxed_append, ton::{ + adnl::{ + address::address::{Quic as AdnlAddrQuic, Udp as AdnlAddrUdp}, + addresslist::AddressList, + Address as AdnlAddress, + }, overlay::{ message::Message as OverlayMessage, node::Node as OverlayNodeV1, nodev2::NodeV2 as OverlayNodeV2, Node as OverlayNodeBoxed, @@ -173,7 +178,12 @@ impl RustTestNode { /// Unlike `add_private_overlay` (which creates a public overlay as shortcut), /// this creates a real private overlay where `try_consume_custom` is dispatched /// to the registered consumer. - pub fn add_true_private_overlay(&self, overlay_id: &Arc, peers: &[Arc]) { + pub fn add_true_private_overlay( + &self, + overlay_id: &Arc, + peers: &[Arc], + use_quic: bool, + ) { let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); let params = OverlayParams { flags: 0, @@ -182,7 +192,7 @@ impl RustTestNode { runtime: Some(self.rt.handle().clone()), }; self.overlay - .add_private_overlay(params, &local_key, peers) + .add_private_overlay(params, &local_key, peers, use_quic) .expect("add_private_overlay failed"); } @@ -204,7 +214,7 @@ impl RustTestNode { .expect("parse IP"); let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); } /// Add the C++ node as an ADNL peer AND to a specific public overlay via signed node. @@ -215,7 +225,7 @@ impl RustTestNode { .expect("parse IP"); let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); let signed_node = Self::get_cpp_signed_node(cpp, overlay_id); self.overlay.add_public_peer(&ip, &signed_node, overlay_id).expect("add_public_peer"); @@ -231,7 +241,7 @@ impl RustTestNode { let other_ip = IpAddress::from_versioned_string(&other.addr, None).expect("parse other IP"); let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &other_ip, &other_pubkey).expect("add_peer"); + self.adnl.add_peer(local_key.id(), &other_ip, None, &other_pubkey).expect("add_peer"); let signed_node = other.overlay.get_signed_node(overlay_id, false).expect("get_signed_node"); @@ -517,10 +527,15 @@ impl RustQuicTestNode { let _guard = rt.enter(); let quic_subscribers: Vec> = vec![test_sub as Arc, overlay.clone()]; - let quic = QuicNode::new(quic_subscribers, cancellation_token.clone(), None); + let quic = QuicNode::new( + quic_subscribers, + cancellation_token.clone(), + None, + rt.handle().clone(), + ); let bind_addr = SocketAddr::new( - Ipv4Addr::from(adnl.ip_address().ip()).into(), - adnl.ip_address().port() + QuicNode::OFFSET_PORT, + Ipv4Addr::from(adnl.ip_address_adnl().ip()).into(), + adnl.ip_address_adnl().port() + QuicNode::OFFSET_PORT, ); quic.add_key(&key_data, &key_id, bind_addr).expect("QUIC add_key failed"); quic @@ -573,7 +588,7 @@ impl RustQuicTestNode { let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) .expect("parse IP"); let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); } /// Add the C++ node as a QUIC peer (registers its QUIC address = udp_port + 1000) @@ -585,6 +600,49 @@ impl RustQuicTestNode { self.quic.add_peer_key(pubkey.id().clone(), quic_addr).expect("add_quic_peer"); } + /// Add the C++ node as both ADNL and QUIC peer by simulating reception of an + /// AddressList that contains adnl.address.quic (the new address type from C++ PR #2184). + /// The QUIC address is discovered via `parse_quic_address` โ€” no hardcoded offset. + pub fn add_cpp_peer_via_address_list(&self, cpp: &CppTestNode, quic_port: u16) { + let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); + let pubkey = Ed25519KeyOption::from_public_key(&raw_key); + + // Build an AddressList as a C++ node with PR #2184 would advertise: + // both adnl.address.udp and adnl.address.quic + let ip: u32 = u32::from(std::net::Ipv4Addr::new(127, 0, 0, 1)); + let addr_list = AddressList { + addrs: vec![ + AdnlAddress::Adnl_Address_Udp(AdnlAddrUdp { + ip: ip as i32, + port: cpp.udp_port() as i32, + }), + AdnlAddress::Adnl_Address_Quic(AdnlAddrQuic { + ip: ip as i32, + port: quic_port as i32, + }), + ] + .into(), + version: adnl::common::Version::get(), + reinit_date: adnl::common::Version::get(), + priority: 0, + expire_at: 0, + }; + + // Parse ADNL and QUIC addresses from the address list + let (adnl_addr, quic_addr) = + AdnlNode::parse_address_list(&addr_list).expect("parse").expect("has ADNL addr"); + + // Add ADNL peer using the UDP address, passing QUIC address too + let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); + let quic_addr = quic_addr.expect("AddressList should contain adnl.address.quic"); + self.adnl + .add_peer(local_key.id(), &adnl_addr, Some(&quic_addr), &pubkey) + .expect("add_peer"); + + // Do NOT call quic.add_peer_key โ€” let ensure_peer_registered discover + // the QUIC address via adnl.peer_ip_address() at connection time. + } + /// Add C++ node as both ADNL peer (UDP) and QUIC peer, and to overlay pub fn add_cpp_peer_full(&self, cpp: &mut CppTestNode, overlay_id: &Arc) { let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); @@ -592,7 +650,7 @@ impl RustQuicTestNode { let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) .expect("parse IP"); let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, &pubkey).expect("add_peer"); + self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); // Add to overlay via signed node let signed_node = RustTestNode::get_cpp_signed_node(cpp, overlay_id); @@ -761,7 +819,12 @@ impl RustQuicTestNode { } /// Create a private overlay with signing key and peer list. - pub fn add_private_overlay(&self, overlay_id: &Arc, peers: &[Arc]) { + pub fn add_private_overlay( + &self, + overlay_id: &Arc, + peers: &[Arc], + use_quic: bool, + ) { let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); let params = OverlayParams { flags: 0, @@ -770,7 +833,7 @@ impl RustQuicTestNode { runtime: Some(self.rt.handle().clone()), }; self.overlay - .add_private_overlay(params, &local_key, peers) + .add_private_overlay(params, &local_key, peers, use_quic) .expect("add_private_overlay failed"); } diff --git a/src/node/tests/compat_test/tests/test_overlay_message.rs b/src/node/tests/compat_test/tests/test_overlay_message.rs index a1b4c2f..837964d 100644 --- a/src/node/tests/compat_test/tests/test_overlay_message.rs +++ b/src/node/tests/compat_test/tests/test_overlay_message.rs @@ -58,7 +58,7 @@ fn test_overlay_message_cpp_to_rust() { // Must be private overlay โ€” the overlay dispatcher only calls try_consume_custom // on the consumer for private overlays. let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - rust_node.add_true_private_overlay(&overlay_short_id, &[cpp_key_id]); + rust_node.add_true_private_overlay(&overlay_short_id, &[cpp_key_id], false); let collector = MessageCollector::new(); rust_node.overlay.add_consumer(&overlay_short_id, collector.clone()).expect("add consumer"); diff --git a/src/node/tests/compat_test/tests/test_quic_address.rs b/src/node/tests/compat_test/tests/test_quic_address.rs new file mode 100644 index 0000000..6dfae77 --- /dev/null +++ b/src/node/tests/compat_test/tests/test_quic_address.rs @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2025-2026 RSquad Blockchain Lab. + * + * Licensed under the GNU General Public License v3.0. + * See the LICENSE file in the root of this repository. + * + * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ +//! QUIC address (adnl.address.quic) compatibility tests. +//! +//! Verifies that Rust and C++ nodes can establish QUIC connections when the +//! peer's QUIC address is discovered via `adnl.address.quic` in the address +//! list, rather than derived from the ADNL UDP port + 1000 offset. +//! +//! This tests the changes from C++ PR ton-blockchain/ton#2184 +//! ("Store ip:port for quic in AdnlAddressList"). + +use adnl::common::AdnlPeers; +use compat_test::{skip_if_no_cpp, test_helpers::RustQuicTestNode, CppTestNode, TestTimeout}; +use std::{thread::sleep, time::Duration}; +use ton_api::{serialize_boxed, ton::ton_node::data::Data as TonNodeData, IntoBoxed}; + +/// Port base for QUIC address tests (unique range to avoid conflicts) +const PORT_BASE: u16 = 18300; + +/// Test: Rust discovers C++ QUIC port via adnl.address.quic and sends a QUIC query. +/// +/// Instead of hardcoding the QUIC port as `udp_port + 1000`, the Rust node receives +/// an AddressList containing `adnl.address.quic` with an explicit port. The QUIC +/// connection is established using that address, and a query echo roundtrip verifies +/// it works end-to-end. +#[test] +fn test_quic_query_via_address_list_rust_to_cpp() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE; + let rust_port = PORT_BASE + 1; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + let cpp_quic_port = cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + + // Rust adds C++ peer via an AddressList containing adnl.address.quic. + // This exercises the new parse_quic_address โ†’ set_peer_quic_address โ†’ + // ensure_peer_registered path (no hardcoded port offset). + rust_node.add_cpp_peer_via_address_list(&cpp, cpp_quic_port); + + // C++ adds Rust as a peer (standard way) + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); + + sleep(Duration::from_millis(500)); + + // Send a QUIC query from Rust to C++. + // The query must be a valid TL-serialized object for the C++ side to process. + let payload = b"QUIC query via adnl.address.quic"; + let tl_query = TonNodeData { data: payload.to_vec().into() }; + let query_data = serialize_boxed(&tl_query.into_boxed()).expect("serialize query TL"); + + let src = rust_node.adnl_key_id(); + let dst = RustQuicTestNode::cpp_key_id(&cpp); + + println!( + "Sending QUIC query from Rust to C++ ({} bytes), QUIC addr discovered via adnl.address.quic", + query_data.len() + ); + + let result = rust_node.rt.block_on(async { + tokio::time::timeout( + Duration::from_secs(10), + rust_node.quic.query( + query_data.clone(), + Some(&*rust_node.adnl), + &AdnlPeers::with_keys(src, dst.clone()), + None, + ), + ) + .await + }); + + match result { + Ok(Ok(Some(answer))) => { + println!("SUCCESS: Got QUIC echo answer ({} bytes)", answer.len()); + assert_eq!(answer, query_data, "Echo data mismatch"); + } + Ok(Ok(None)) => panic!("QUIC query returned empty answer"), + Ok(Err(e)) => panic!("QUIC query failed: {}", e), + Err(_) => panic!("QUIC query timed out"), + } + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} + +/// Test: C++ discovers Rust QUIC port via adnl.address.quic and sends a QUIC query. +/// +/// The C++ node receives the Rust peer with an explicit `quic_port` in the +/// `add_peer` command, which includes `adnl.address.quic` in the address list. +/// C++ then sends a QUIC query to Rust using that address. +#[test] +fn test_quic_query_via_address_list_cpp_to_rust() { + skip_if_no_cpp!(); + let _ = env_logger::try_init(); + let _timeout = TestTimeout::new(0); + + let cpp_port = PORT_BASE + 10; + let rust_port = PORT_BASE + 11; + let rust_quic_port = rust_port + adnl::QuicNode::OFFSET_PORT; + + let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); + cpp.enable_quic().expect("enable QUIC on C++"); + + let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); + let rust_id = rust_node.adnl_id_hex(); + + // Rust adds C++ as a standard peer + rust_node.add_cpp_peer(&cpp); + + // C++ adds Rust with explicit quic_port in the address list. + // This makes C++ include adnl.address.quic in the peer's AddressList, + // so QuicSender::get_ip_address() uses it instead of the UDP+offset fallback. + let rust_pubkey = rust_node.pubkey_tl_b64(); + cpp.add_peer_with_quic(&rust_pubkey, "127.0.0.1", rust_port, Some(rust_quic_port)) + .expect("C++ add peer with quic"); + + sleep(Duration::from_millis(500)); + + // C++ sends QUIC query to Rust + let payload = b"QUIC query via adnl.address.quic from C++"; + let tl_query = TonNodeData { data: payload.to_vec().into() }; + let query_data = serialize_boxed(&tl_query.into_boxed()).expect("serialize query TL"); + + println!( + "C++ sending QUIC query to Rust ({} bytes), QUIC addr via adnl.address.quic", + query_data.len() + ); + + let result = cpp.send_quic_query(&rust_id, &query_data, 5000); + + match result { + Ok(answer) => { + println!("SUCCESS: C++ got QUIC echo answer ({} bytes)", answer.len()); + assert_eq!(answer, query_data, "Echo data mismatch"); + } + Err(e) => panic!("C++โ†’Rust QUIC query failed: {}", e), + } + + rust_node.stop(); + cpp.shutdown().expect("shutdown"); +} diff --git a/src/node/tests/compat_test/tests/test_quic_private_overlay.rs b/src/node/tests/compat_test/tests/test_quic_private_overlay.rs index 8c85a61..a752121 100644 --- a/src/node/tests/compat_test/tests/test_quic_private_overlay.rs +++ b/src/node/tests/compat_test/tests/test_quic_private_overlay.rs @@ -63,7 +63,7 @@ fn test_private_overlay_adnl_message_rust_to_cpp() { // Rust creates TRUE private overlay (not public shortcut) with ADNL transport let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); // Exchange ADNL peers rust_node.add_cpp_peer(&cpp); @@ -127,7 +127,7 @@ fn test_private_overlay_quic_message_rust_to_cpp() { // Rust creates private overlay with QUIC transport let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); // Exchange ADNL peers (needed for peer identity resolution) rust_node.add_cpp_peer(&cpp); @@ -191,7 +191,7 @@ fn test_private_overlay_quic_message_burst() { .expect("C++ create private overlay"); let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); rust_node.add_cpp_peer(&cpp); let rust_pubkey = rust_node.pubkey_tl_b64(); @@ -266,7 +266,7 @@ fn test_private_overlay_quic_query_rust_to_cpp() { // Rust creates private overlay with QUIC transport let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); rust_node.add_cpp_peer(&cpp); let rust_pubkey = rust_node.pubkey_tl_b64(); @@ -330,7 +330,7 @@ fn test_private_overlay_message_cpp_to_rust() { // Rust creates private overlay with ADNL (inbound transport doesn't matter โ€” // incoming messages arrive via ADNL subscriber regardless) let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()]); + rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); // Register message collector to verify receipt on Rust side let collector = MessageCollector::new(); diff --git a/src/node/tests/test_run_net_py/test_run_net.py b/src/node/tests/test_run_net_py/test_run_net.py index 68aca88..6d05466 100644 --- a/src/node/tests/test_run_net_py/test_run_net.py +++ b/src/node/tests/test_run_net_py/test_run_net.py @@ -258,7 +258,10 @@ def build_node_work_path(node_index: int) -> Path: return work_dirs_path / f"node_{node_index}" -def prepare_default_config(node_index: int, config_blank: str, log_config_blank: str): +def prepare_default_config( + node_index: int, config_blank: str, log_config_blank: str, + use_quic: bool = False, quic_port_offset: int = 1000, +): node_work_path = build_node_work_path(node_index) node_work_path.mkdir(parents=True, exist_ok=True) @@ -281,7 +284,11 @@ def prepare_default_config(node_index: int, config_blank: str, log_config_blank: config["log_config_name"] = str(node_work_path / "log_cfg.yml") config["ton_global_config_name"] = str(common_config_path / "global_config.json") config["internal_db_path"] = str(node_work_path) - config["ip_address"] = f"{ip_address}:{main_port_base + node_index}" + adnl_port = main_port_base + node_index + config["ip_address"] = f"{ip_address}:{adnl_port}" + if use_quic: + quic_port = adnl_port + quic_port_offset + config["ip_address_quic"] = f"{ip_address}:{quic_port}" config["control_server_port"] = control_port_base + node_index config["lite_server_port"] = liteserver_port_base + node_index config["json_rpc_server"] = {"address": f"0.0.0.0:{jsonrpc_port_base + node_index}"} @@ -411,14 +418,18 @@ def export_validator_pubkey( def prepare_node( - node_index: int, config_blank: str, log_config_blank: str + node_index: int, config_blank: str, log_config_blank: str, + use_quic: bool = False, quic_port_offset: int = 1000, ) -> str | None: # Prepare console key keygen_result = run_command([str(bins_path / "crypto"), "gen", "key"], cwd=bins_path) console_key_json = json.loads(keygen_result.stdout) - prepare_default_config(node_index, config_blank, log_config_blank) + prepare_default_config( + node_index, config_blank, log_config_blank, + use_quic=use_quic, quic_port_offset=quic_port_offset, + ) # Run node console_public = {"type_id": 1209251014, "pub_key": console_key_json["pubkey"]} @@ -507,7 +518,7 @@ def extract_keys_from_rust_config(rust_config: dict): return dht_pvt_key, fullnode_pvt_key -def transform_configs_for_cpp(node_index: int): +def transform_configs_for_cpp(node_index: int, use_quic: bool = False, quic_port_offset: int = 1000): print(f"Transforming configs for C++ node {node_index}...", end="") node_work_path = build_node_work_path(node_index) @@ -625,6 +636,20 @@ def transform_configs_for_cpp(node_index: int): add_to_cpp_keyring(node_index, console_srv_secret_b64, base64.b64decode(console_srv_id)) add_to_cpp_keyring(node_index, liteserver_pvt_key, base64.b64decode(liteserver_key_id_b64)) + # add QUIC address if enabled + if use_quic: + import ipaddress + adnl_port = main_port_base + node_index + quic_port = adnl_port + quic_port_offset + ip_int = int(ipaddress.IPv4Address(ip_address)) + cpp_config.setdefault("addrs", []).append({ + "@type": "engine.quicAddr", + "ip": ip_int, + "port": quic_port, + "categories": [0, 1, 2, 3], + "priority_categories": [], + }) + # save modified cpp config with open(node_work_path / "config.json", "w") as f: json.dump(cpp_config, f, indent=2) @@ -872,8 +897,18 @@ def main(): action="store_true", help="Enable QUIC overlay transport in ConfigParam 30 (use_quic flag). Implies --simplex.", ) + parser.add_argument( + "--quic_custom_port", + action="store_true", + help="Use QUIC port offset 2000 (instead of 1000) to verify DHT announces. " + "Nodes bind QUIC on adnl_port+2000 but the auto-derive fallback is adnl_port+1000, " + "so QUIC connections only work if advertised addresses are used. Implies --quic.", + ) args = parser.parse_args() + # --quic_custom_port implies --quic + if args.quic_custom_port: + args.quic = True # --quic implies --simplex if args.quic: args.simplex = True @@ -919,8 +954,12 @@ def main(): test_root_path / "global_config_blank.json", common_config_path / "global_config.json", ) + quic_port_offset = 2000 if args.quic_custom_port else 1000 for n in range(0 if run_fullnode else 1, nodes_count + 1): - vk = prepare_node(n, node_config_blank, log_config_blank) + vk = prepare_node( + n, node_config_blank, log_config_blank, + use_quic=args.quic, quic_port_offset=quic_port_offset, + ) if n != 0: validator_pub_keys.append(vk) @@ -965,7 +1004,9 @@ def main(): # Transform configs for C++ nodes for node_index in range(rust_nodes_count + 1, nodes_count + 1): - transform_configs_for_cpp(node_index) + transform_configs_for_cpp( + node_index, use_quic=args.quic, quic_port_offset=quic_port_offset, + ) if start: # Start nodes diff --git a/src/tl/ton_api/tl/ton_api.tl b/src/tl/ton_api/tl/ton_api.tl index e6f0d6d..fe5e706 100644 --- a/src/tl/ton_api/tl/ton_api.tl +++ b/src/tl/ton_api/tl/ton_api.tl @@ -69,6 +69,7 @@ adnl.address.udp6 ip:int128 port:int = adnl.Address; adnl.address.tunnel to:int256 pubkey:PublicKey = adnl.Address; adnl.address.reverse = adnl.Address; +adnl.address.quic ip:int port:int = adnl.Address; adnl.addressList addrs:(vector adnl.Address) version:int reinit_date:int priority:int expire_at:int = adnl.AddressList; From a1caff39ce9fde989707a56ad399c1abfe53e4dc Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Sun, 5 Apr 2026 22:57:45 +0700 Subject: [PATCH 44/48] chore(node): changelog and archival node docs for v0.4.0 --- CHANGELOG.md | 27 ++++ helm/ton-rust-node/docs/archival-node.md | 176 +++++++++++++++++++++++ helm/ton-rust-node/docs/maintaining.md | 1 + helm/ton-rust-node/docs/node-config.md | 4 +- 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 helm/ton-rust-node/docs/archival-node.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2626eaa..0dae690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,33 @@ For Helm chart changes, see [helm/ton-rust-node/CHANGELOG.md](helm/ton-rust-node The format is based on [Keep a Changelog](https://keepachangelog.com/). Versions follow the node release tags (e.g. `v0.1.2-mainnet`). +## [v0.4.0] - 2026-04-05 + +Image: `ghcr.io/rsquad/ton-rust-node/node:v0.4.0` + +This release brings support for the Simplex consensus protocol and QUIC transport โ€” key protocol upgrades rolling out across the TON network. It also introduces archival node functionality and a range of fixes to fee accounting, storage phase handling, and sync stability. + +### Added + +- Simplex consensus updates and QUIC integration +- QUIC transport with separate address support and connection deduplication +- Archival node functionality with split/merge resilience +- CellsDB cells cache + +### Changed + +- Validate query uses capabilities from blockchain config instead of candidate block +- Enforce mcStateExtra flags <=1, remove ValidatorsStat + +### Fixed + +- Storage phase: preserve original due_payment for special accounts in partial storage phase +- Masterchain ValueFlow burned fees and blackhole accounting +- Fee accumulation: accumulate fees_collected instead of overwriting to preserve shard burn +- Untouched account change detection +- Fast sync overlay creation reliability +- Archive sync stalling on shard split/merge + ## [v0.3.0] - 2026-03-12 Image: `ghcr.io/rsquad/ton-rust-node/node:v0.3.0` diff --git a/helm/ton-rust-node/docs/archival-node.md b/helm/ton-rust-node/docs/archival-node.md new file mode 100644 index 0000000..1158eda --- /dev/null +++ b/helm/ton-rust-node/docs/archival-node.md @@ -0,0 +1,176 @@ +# Archival node + +An archival node keeps the full blockchain history and serves historical queries via liteserver. Setting one up involves two steps: importing existing archives into epoch-based storage, then starting the node in archival mode. + +## Table of contents + +- [System requirements](#system-requirements) +- [Source data](#source-data) +- [Archive import](#archive-import) + - [Parameters](#parameters) + - [What the import does](#what-the-import-does) + - [Output structure](#output-structure) +- [Archival mode config](#archival-mode-config) + - [Simple setup](#simple-setup) + - [Distributed setup](#distributed-setup) +- [Cloning an existing archival node](#cloning-an-existing-archival-node) +- [Helm integration](#helm-integration) + +## System requirements + +| Resource | Minimum | +|----------|---------| +| Disk | 20 TB (block archives + cells database) | +| RAM | 32 GB | +| Import time | Several days for full blockchain history | + +## Source data + +TON block archives are stored as pairs of `.pack` files per archive group (masterchain + shard blocks): + +``` +archive.00000.pack # masterchain blocks 0-99 +archive.00000.0:8000000000000000.pack # workchain 0 shard blocks for the same range +archive.00100.pack # masterchain blocks 100-199 +archive.00100.0:8000000000000000.pack # workchain 0 shard blocks +... +``` + +You also need: + +- Masterchain zerostate `.boc` file +- Workchain zerostate(s) `.boc` file(s) (one per workchain) +- Global config JSON (contains zerostate hashes and hard fork list) + +## Archive import + +The `archive_import` tool converts raw `.pack` files into epoch-based storage used by the archival node. + +```bash +RUST_LOG=info archive_import \ + --archives-path /path/to/archives \ + --epochs-path /data/epochs \ + --node-db-path /data/node_db \ + --mc-zerostate /path/to/mc_zerostate.boc \ + --wc-zerostate /path/to/wc0_zerostate.boc \ + --global-config /path/to/global-config.json +``` + +### Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `--archives-path` | yes | Directory containing source `.pack` files | +| `--epochs-path` | yes | Directory where epoch subdirectories will be created | +| `--node-db-path` | yes | Path to node database | +| `--mc-zerostate` | yes | Path to masterchain zerostate `.boc` file | +| `--wc-zerostate` | yes | Path to workchain zerostate `.boc` file (repeat for each workchain) | +| `--global-config` | yes | Path to global config JSON | +| `--epoch-size` | no | MC blocks per epoch, default `10000000` (must be a multiple of 20000) | +| `--copy` | no | Copy `.pack` files instead of moving them | + +### What the import does + +1. Validates zerostate hashes against the global config +2. Scans the archives directory and groups `.pack` files by archive ID +3. For each group: deserializes blocks, validates proofs, imports packages into epoch storage +4. Populates the node database: block handles, prev/next block links, block index, shard states + +The import supports resume โ€” if interrupted, re-run with the same parameters to continue from the last imported group. + +### Output structure + +``` +/data/node_db/ + db/ # main RocksDB (block handles, indexes, state keys) + archive_states/ # shard state cell storage + +/data/epochs/ + epoch_0/ # archive packages for MC blocks 0..epoch_size-1 + archive_db/ # RocksDB with package metadata + archive/packages/ # .pack files + epoch_1/ + ... +``` + +--- + +## Archival mode config + +Add the `archival_mode` section to the node config to enable epoch-based archival storage. Set `internal_db_path` to point to the database created by the import. + +### Simple setup + +All epochs in one directory. The node auto-discovers existing epochs on startup and creates new ones in the same location. + +```json +{ + "internal_db_path": "/data/node_db", + "archival_mode": { + "epoch_size": 10000000, + "new_epochs_path": "/data/epochs", + "existing_epochs": [] + } +} +``` + +The `epoch_size` must match the value used during import. + +### Distributed setup + +Imported epochs on separate (slower) storage, new epochs on fast storage. List imported epoch directories explicitly in `existing_epochs`. + +```json +{ + "internal_db_path": "/data/node_db", + "archival_mode": { + "epoch_size": 10000000, + "new_epochs_path": "/fast_ssd/new_epochs", + "existing_epochs": [ + { "path": "/nfs/imported/epoch_0" }, + { "path": "/nfs/imported/epoch_1" }, + { "path": "/fast_ssd/imported/epoch_2" } + ] + } +} +``` + +> **Note:** The last imported epoch is likely incomplete โ€” its range covers blocks still being created. It will continue to receive new blocks during sync, so place it on fast storage alongside `new_epochs_path`. + +### Behavior + +When `archival_mode` is set: + +- Archive GC is disabled โ€” all historical data is preserved +- Shard states are stored in a separate cell database (`archive_states/`) +- New blocks arriving via sync are appended to the latest epoch + +> **See also:** For a simpler setup that keeps full history without epoch-based storage, see the [archival node section in node-config.md](node-config.md#archival-node). That approach disables GC and works without the import step, but requires the node to sync all history from scratch. + +--- + +## Cloning an existing archival node + +Instead of importing from scratch, you can copy data from a running archival node: + +1. Stop the source node +2. Copy epoch directories and the node database (`rsync` or similar) +3. On the new machine, generate a fresh node config with new ADNL keys +4. Set `internal_db_path` and `archival_mode` pointing to the copied data +5. Start the new node + +This works because the database contains only blockchain data (blocks, states, indexes). Node identity (ADNL keys, validator keys) is stored in the config file and secrets vault, not in the database. + +> **Important:** Do not copy the node config file โ€” it contains the ADNL private keys of the source node. Always generate fresh keys for the new node. + +--- + +## Helm integration + +The archive import runs outside of Kubernetes as a one-time migration step. After import, configure the Helm chart to start the node in archival mode: + +1. Mount the epoch storage and node database into the pod using `extraVolumes` and `extraVolumeMounts` +2. Set `archival_mode` in the node config (`nodeConfigs`) with paths matching the mount points +3. Size `storage.db.size` for the node database (the epoch data lives on external volumes) + +> **See also:** [node-config.md](node-config.md#archival-node) covers the GC-based approach to keeping full history, which does not require the import step but uses more disk on the primary volume. diff --git a/helm/ton-rust-node/docs/maintaining.md b/helm/ton-rust-node/docs/maintaining.md index 6d13199..0396a6c 100644 --- a/helm/ton-rust-node/docs/maintaining.md +++ b/helm/ton-rust-node/docs/maintaining.md @@ -59,6 +59,7 @@ All documentation lives in the `docs/` directory: | File | Content | |------|---------| | `node-config.md` | `config.json` field reference, JSON examples, archival setup | +| `archival-node.md` | Archive import tool, epoch-based archival mode, cloning | | `logging.md` | log4rs config (appenders, levels, rotation) | | `global-config.md` | Global network config overview | | `networking.md` | Networking modes (LoadBalancer, NodePort, hostPort, hostNetwork), NetworkPolicy | diff --git a/helm/ton-rust-node/docs/node-config.md b/helm/ton-rust-node/docs/node-config.md index e02173b..48ffecd 100644 --- a/helm/ton-rust-node/docs/node-config.md +++ b/helm/ton-rust-node/docs/node-config.md @@ -234,7 +234,9 @@ For a validator, add `control-server` and `control-client` to the loop. ## Archival node -By default the node prunes old archives and state snapshots via the `gc` section. To keep the **full** blockchain history, override the GC settings in your node config: +By default the node prunes old archives and state snapshots via the `gc` section. To keep the **full** blockchain history, override the GC settings in your node config. + +> **See also:** For epoch-based archival storage with historical archive import, see [archival-node.md](archival-node.md). That approach is needed when you want to serve the full blockchain history from imported block archives. ```json { From 9a9bcc6506324ce1659bd516860b0546adb8c867 Mon Sep 17 00:00:00 2001 From: R Date: Mon, 6 Apr 2026 00:18:09 +0700 Subject: [PATCH 45/48] chore(helm): bump appVersion and image tag to v0.4.0 (#64) --- helm/ton-rust-node/CHANGELOG.md | 8 ++++++++ helm/ton-rust-node/Chart.yaml | 4 ++-- helm/ton-rust-node/values.yaml | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/helm/ton-rust-node/CHANGELOG.md b/helm/ton-rust-node/CHANGELOG.md index 94b8cc3..301c0c6 100644 --- a/helm/ton-rust-node/CHANGELOG.md +++ b/helm/ton-rust-node/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to the Helm chart will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). Versions follow the Helm chart release tags (e.g. `helm/v0.3.0`). +## [0.4.5] - 2026-04-05 + +appVersion: `v0.4.0` + +### Changed + +- Default image tag and appVersion updated to `v0.4.0` + ## [0.4.4] - 2026-04-02 appVersion: `v0.3.0` diff --git a/helm/ton-rust-node/Chart.yaml b/helm/ton-rust-node/Chart.yaml index e129a4d..9b4308e 100644 --- a/helm/ton-rust-node/Chart.yaml +++ b/helm/ton-rust-node/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: node description: TON Rust Node deployment type: application -version: 0.4.4 -appVersion: "v0.3.0" +version: 0.4.5 +appVersion: "v0.4.0" sources: - https://github.com/rsquad/ton-rust-node diff --git a/helm/ton-rust-node/values.yaml b/helm/ton-rust-node/values.yaml index 78f968d..a1eddea 100644 --- a/helm/ton-rust-node/values.yaml +++ b/helm/ton-rust-node/values.yaml @@ -16,7 +16,7 @@ command: [] ## image: repository: ghcr.io/rsquad/ton-rust-node/node - tag: v0.3.0 + tag: v0.4.0 pullPolicy: IfNotPresent ## @param imagePullSecrets [array] Image pull secrets for private registries From a80207c6926b01975bb31f31c979d5d159fc83f8 Mon Sep 17 00:00:00 2001 From: inyellowbus Date: Tue, 7 Apr 2026 16:47:17 +0700 Subject: [PATCH 46/48] feat(node): switch to ubuntu base image and add console binary --- src/Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Dockerfile b/src/Dockerfile index 4fed586..584f14b 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -19,16 +19,18 @@ ENV GIT_BRANCH=${GIT_BRANCH} ENV GIT_COMMIT=${GIT_COMMIT} ENV GIT_COMMIT_DATE=${GIT_COMMIT_DATE} -RUN cargo build --release --bin node +RUN cargo build --release --bin node --bin console -FROM debian:stable-slim +FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y \ - openssl \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + libssl3t64 \ libzstd1 \ - libgoogle-perftools4 \ + libgoogle-perftools4t64 \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /node/target/release/node /usr/local/bin/ +COPY --from=builder /node/target/release/console /usr/local/bin/ WORKDIR /node From 4ef0fcf232f5f1cbe187a4e10b858e95ef88a6c3 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Wed, 8 Apr 2026 13:29:54 +0300 Subject: [PATCH 47/48] fix: max factor load from config --- src/node-control/README.md | 6 +-- .../commands/nodectl/config_elections_cmd.rs | 22 +++++++-- .../src/commands/nodectl/config_wallet_cmd.rs | 21 +++++++-- src/node-control/common/src/app_config.rs | 47 +++++++++++++++++-- src/node-control/common/src/ton_utils.rs | 9 ++++ src/node-control/elections/src/runner.rs | 30 ++++++++---- .../elections/src/runner_tests.rs | 6 +-- .../service/src/runtime_config.rs | 18 +++++++ .../src/v2/client_json_rpc.rs | 14 ++++++ 9 files changed, 145 insertions(+), 28 deletions(-) diff --git a/src/node-control/README.md b/src/node-control/README.md index 364d127..c596b8c 100644 --- a/src/node-control/README.md +++ b/src/node-control/README.md @@ -518,7 +518,7 @@ nodectl config elections tick-interval 60 ##### `config elections max-factor` -Set the maximum factor for elections. Must be in the range [1.0..3.0]. +Set the maximum stake factor for elections. The value must be between **1.0** and the networkโ€™s **maximum stake factor** from masterchain **config param 17** (`max_stake_factor`). nodectl does not use a hardcoded upper bound (e.g. 3.0): the CLI reads the current limit from the chain when validating and saving. | Argument | Description | |----------|-------------| @@ -1403,7 +1403,7 @@ Automatic elections task configuration: - `"minimum"` โ€” use minimum required stake - `{ "fixed": }` โ€” fixed stake amount in nanoTON - `policy_overrides` โ€” per-node stake policy overrides (node name -> policy). When a node has an entry here, it takes precedence over the default `policy`. Example: `{ "node0": { "fixed": 500000000000 } }` -- `max_factor` โ€” max factor for elections (default: 3.0, must be in range [1.0..3.0]) +- `max_factor` โ€” maximum stake factor (default `3.0` in generated configs). Valid values lie in `[1.0, network_max]`, where **`network_max` comes from masterchain config param 17** (`max_stake_factor`); the CLI and stake command validate against the live network when TON HTTP API is available - `tick_interval` โ€” interval between election checks in seconds (default: `40`) #### `voting` (optional) @@ -1720,7 +1720,7 @@ nodectl config wallet stake -b -a [-m ] |------|------|----------|---------|-------------| | `-b` | `--binding` | Yes | โ€” | Binding name (node-wallet-pool triple) | | `-a` | `--amount` | Yes | โ€” | Stake amount in TON | -| `-m` | `--max-factor` | No | `3.0` | Max factor (`1.0`โ€“`3.0`) | +| `-m` | `--max-factor` | No | `3.0` | Max factor: from `1.0` up to the network limit (**config param 17**), validated against the chain | Example: diff --git a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs index 4dae1cd..49d355d 100644 --- a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs @@ -6,7 +6,11 @@ * * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -use crate::commands::nodectl::{output_format::OutputFormat, utils::save_config}; +use crate::commands::nodectl::{ + output_format::OutputFormat, + utils::{load_config_vault_rpc_client, save_config}, +}; +use anyhow::Context; use colored::Colorize; use common::{ app_config::{AppConfig, BindingStatus, ElectionsConfig, StakePolicy}, @@ -71,7 +75,9 @@ pub struct TickIntervalCmd { #[derive(clap::Args, Clone)] pub struct MaxFactorCmd { - #[arg(help = "Max factor (1.0..3.0)")] + #[arg( + help = "Max factor: from 1.0 up to the network limit (config param 17 max_stake_factor)" + )] value: f32, } @@ -220,8 +226,16 @@ impl TickIntervalCmd { impl MaxFactorCmd { pub async fn run(&self, path: &Path) -> anyhow::Result<()> { - if !(1.0..=3.0).contains(&self.value) { - anyhow::bail!("max-factor must be in range [1.0..3.0]"); + let (_app, _vault, rpc_client) = load_config_vault_rpc_client(path).await?; + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor from chain (config param 17)")?; + if !(1.0..=network_max).contains(&self.value) { + anyhow::bail!( + "max-factor must be in range [1.0..{}] (network max_stake_factor from config param 17)", + network_max + ); } let mut config = AppConfig::load(path)?; config diff --git a/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs index 2d57bc2..5619613 100644 --- a/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs @@ -120,7 +120,12 @@ pub struct WalletStakeCmd { binding: String, #[arg(short = 'a', long = "amount", help = "Stake amount in TONs")] amount: f64, - #[arg(short = 'm', long = "max-factor", default_value = "3.0", help = "Max factor (1.0..3.0)")] + #[arg( + short = 'm', + long = "max-factor", + default_value = "3.0", + help = "Max factor from 1.0 up to the network limit (config param 17)" + )] max_factor: f32, } @@ -430,11 +435,17 @@ impl WalletSendCmd { impl WalletStakeCmd { pub async fn run(&self, path: &Path, cancellation_ctx: CancellationCtx) -> anyhow::Result<()> { - if !(1.0..=3.0).contains(&self.max_factor) { - anyhow::bail!("max-factor must be between 1.0 and 3.0"); - } - let (config, vault, rpc_client) = load_config_vault_rpc_client(path).await?; + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor from chain (config param 17)")?; + if !(1.0..=network_max).contains(&self.max_factor) { + anyhow::bail!( + "max-factor must be in range [1.0..{}] (network max_stake_factor from config param 17)", + network_max + ); + } // Resolve binding โ†’ wallet, pool, node let binding = config diff --git a/src/node-control/common/src/app_config.rs b/src/node-control/common/src/app_config.rs index 21d38ba..080db4c 100644 --- a/src/node-control/common/src/app_config.rs +++ b/src/node-control/common/src/app_config.rs @@ -459,7 +459,7 @@ fn default_workchain() -> i32 { -1 } -fn default_max_factor() -> f32 { +pub fn default_max_factor() -> f32 { 3.0 } @@ -506,10 +506,28 @@ impl ElectionsConfig { self.policy_overrides.get(node_id).unwrap_or(&self.policy) } - pub fn validate(&self) -> anyhow::Result<()> { - if !(1.0..=3.0).contains(&self.max_factor) { - anyhow::bail!("max_factor must be in range [1.0..3.0]"); + /// Validates elections settings. + /// + /// - `None`: only checks `max_factor >= 1.0` (e.g. [`AppConfig::load`] without RPC). No upper bound. + /// - `Some(m)`: `max_factor` must be in `[1.0, m]` where `m` is from config param 17 (service startup). + pub fn validate(&self, max_stake_factor_upper_bound: Option) -> anyhow::Result<()> { + self.validate_timing_fields()?; + match max_stake_factor_upper_bound { + None => { + if self.max_factor < 1.0 { + anyhow::bail!("max_factor must be >= 1.0"); + } + } + Some(m) => { + if !(1.0..=m).contains(&self.max_factor) { + anyhow::bail!("max_factor must be in range [1.0..{}]", m); + } + } } + Ok(()) + } + + fn validate_timing_fields(&self) -> anyhow::Result<()> { if !(0.0..=1.0).contains(&self.sleep_period_pct) { anyhow::bail!("sleep_period_pct must be in range [0.0..1.0]"); } @@ -714,7 +732,7 @@ impl AppConfig { } fn validate(&self) -> anyhow::Result<()> { - self.elections.as_ref().map(|e| e.validate()).transpose()?; + self.elections.as_ref().map(|e| e.validate(None)).transpose()?; Ok(()) } } @@ -756,6 +774,25 @@ mod tests { assert_eq!(stake, 10); } + #[test] + fn test_elections_validate_max_factor_respects_network_cap() { + let mut c = ElectionsConfig::default(); + c.max_factor = 5.0; + assert!(c.validate(Some(default_max_factor())).is_err()); + assert!(c.validate(Some(5.0)).is_ok()); + c.max_factor = 2.0; + assert!(c.validate(Some(default_max_factor())).is_ok()); + } + + #[test] + fn test_elections_validate_none_allows_max_factor_above_default_cap() { + let mut c = ElectionsConfig::default(); + c.max_factor = 25.0; + assert!(c.validate(None).is_ok()); + assert!(c.validate(Some(3.0)).is_err()); + assert!(c.validate(Some(30.0)).is_ok()); + } + #[test] fn test_calculate_stake_split50_ok() { let policy = StakePolicy::Split50; diff --git a/src/node-control/common/src/ton_utils.rs b/src/node-control/common/src/ton_utils.rs index 8837072..ebff8c8 100644 --- a/src/node-control/common/src/ton_utils.rs +++ b/src/node-control/common/src/ton_utils.rs @@ -18,6 +18,15 @@ pub fn nanotons_to_tons_f64(nanotons: u64) -> f64 { nanotons as f64 / 1_000_000_000.0 } +/// Elector uses fixed-point `max_stake_factor`: raw value is multiplier ร— 65536 (e.g. 3ร— โ†’ `3 * 65536`). +pub const MAX_STAKE_FACTOR_SCALE: u32 = 65536; + +/// Converts chain `max_stake_factor` (raw) to float multiplier (e.g. `196608` โ†’ `3.0`). +#[inline] +pub fn max_stake_factor_raw_to_multiplier(raw: u32) -> f32 { + raw as f32 / MAX_STAKE_FACTOR_SCALE as f32 +} + pub fn display_tons(nanotons: u64) -> String { format!("{:.4}", nanotons_to_tons_f64(nanotons)) .trim_end_matches('0') diff --git a/src/node-control/elections/src/runner.rs b/src/node-control/elections/src/runner.rs index a8394a4..e57d1a0 100644 --- a/src/node-control/elections/src/runner.rs +++ b/src/node-control/elections/src/runner.rs @@ -549,7 +549,8 @@ impl ElectionRunner { election_id: u64, params: &ConfigParams<'_>, ) -> anyhow::Result<()> { - let max_factor = (self.calc_max_factor() * 65536.0) as u32; + let max_factor = ((self.calc_max_factor() * 65536.0) as u32) + .clamp(common::ton_utils::MAX_STAKE_FACTOR_SCALE, params.cfg17.max_stake_factor); let stake_ctx = StakeContext { past_elections: &self.past_elections, our_max_factor: max_factor, @@ -691,7 +692,7 @@ impl ElectionRunner { max_factor, }); node.key_id = key_id; - Self::send_stake(node_id, node, stake).await?; + Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor).await?; Ok(()) } Some(entry) => { @@ -717,7 +718,8 @@ impl ElectionRunner { nanotons_to_tons_f64(old_stake + stake), nanotons_to_tons_f64(stake), ); - Self::send_stake(node_id, node, stake).await?; + Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor) + .await?; node.participant.as_mut().map(|p| p.stake += stake); } } @@ -726,7 +728,8 @@ impl ElectionRunner { if let Some(p) = node.participant.as_mut() { p.stake = stake; } - Self::send_stake(node_id, node, stake).await?; + Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor) + .await?; } } Ok(()) @@ -734,9 +737,15 @@ impl ElectionRunner { } } - async fn send_stake(node_id: &str, node: &mut Node, stake: u64) -> anyhow::Result<()> { + async fn send_stake( + node_id: &str, + node: &mut Node, + stake: u64, + network_max_stake_factor: u32, + ) -> anyhow::Result<()> { tracing::info!("node [{}] build stake message", node_id); - let payload = Self::build_new_stake_payload(node_id, node, stake).await?; + let payload = + Self::build_new_stake_payload(node_id, node, stake, network_max_stake_factor).await?; // For simplicity we always assume that the node has nominator pool. let fee = ELECTOR_STAKE_FEE + NPOOL_COMPUTE_FEE; let stake_balance = node.stake_balance(fee).await?; @@ -782,6 +791,7 @@ impl ElectionRunner { node_id: &str, node: &mut Node, stake: u64, + network_max_stake_factor: u32, ) -> anyhow::Result { let Some(participant) = &mut node.participant else { anyhow::bail!("node [{}] no participant info", node_id); @@ -798,8 +808,12 @@ impl ElectionRunner { participant.adnl_addr.as_slice() ) ); - if !(1.0..=3.0).contains(&(participant.max_factor as f32 / 65536.0)) { - anyhow::bail!(" must be a real number 1..3"); + let scale = common::ton_utils::MAX_STAKE_FACTOR_SCALE; + if participant.max_factor < scale || participant.max_factor > network_max_stake_factor { + anyhow::bail!( + " must be between 1.0 and {} (network max_stake_factor from config param 17)", + common::ton_utils::max_stake_factor_raw_to_multiplier(network_max_stake_factor) + ); } // todo: move to ElectorWrapper // validator-elect-req.fif diff --git a/src/node-control/elections/src/runner_tests.rs b/src/node-control/elections/src/runner_tests.rs index 702534e..13bd9d7 100644 --- a/src/node-control/elections/src/runner_tests.rs +++ b/src/node-control/elections/src/runner_tests.rs @@ -2382,7 +2382,7 @@ fn test_elections_config_validate_sleep_gt_waiting() { waiting_period_pct: 0.3, // sleep > waiting โ†’ invalid ..ElectionsConfig::default() }; - assert!(config.validate().is_err()); + assert!(config.validate(None).is_err()); } #[test] @@ -2391,7 +2391,7 @@ fn test_elections_config_validate_sleep_out_of_range() { sleep_period_pct: 1.5, // > 1.0 โ†’ invalid ..ElectionsConfig::default() }; - assert!(config.validate().is_err()); + assert!(config.validate(None).is_err()); } #[test] @@ -2401,7 +2401,7 @@ fn test_elections_config_validate_valid() { waiting_period_pct: 0.5, ..ElectionsConfig::default() }; - assert!(config.validate().is_ok()); + assert!(config.validate(None).is_ok()); } #[test] diff --git a/src/node-control/service/src/runtime_config.rs b/src/node-control/service/src/runtime_config.rs index 45be384..1f77613 100644 --- a/src/node-control/service/src/runtime_config.rs +++ b/src/node-control/service/src/runtime_config.rs @@ -93,6 +93,15 @@ impl RuntimeConfigStore { let vault = Some(SecretVaultBuilder::from_env().await?); let rpc_client = Self::load_rpc_client(&app_cfg).await?; + if let Some(elections) = app_cfg.elections.as_ref() { + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor for elections config validation")?; + elections + .validate(Some(network_max)) + .context("elections max_factor vs chain (config param 17)")?; + } let master_wallet = Self::load_master_wallet(&app_cfg, rpc_client.clone(), vault.clone()).await?; let wallets = Self::load_wallets(&app_cfg, rpc_client.clone(), vault.clone()).await?; @@ -116,6 +125,15 @@ impl RuntimeConfigStore { async fn reload(&self, new_config: AppConfig) -> anyhow::Result<()> { let vault = SecretVaultBuilder::from_env().await.context("failed to reopen vault")?; let rpc_client = Self::load_rpc_client(&new_config).await?; + if let Some(elections) = new_config.elections.as_ref() { + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor for elections config validation")?; + elections + .validate(Some(network_max)) + .context("elections max_factor vs chain (config param 17)")?; + } let master_wallet = Self::load_master_wallet(&new_config, rpc_client.clone(), Some(vault.clone())).await?; let wallets = diff --git a/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs b/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs index 246eb48..1031819 100644 --- a/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs +++ b/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs @@ -177,6 +177,20 @@ impl ClientJsonRpc { Ok(config_param) } + /// Global `max_stake_factor` from config param 17 (raw fixed-point, multiplier ร—65536). + pub async fn network_max_stake_factor_raw(&self) -> anyhow::Result { + match self.get_config_param(17).await? { + ConfigParamEnum::ConfigParam17(c) => Ok(c.max_stake_factor), + _ => anyhow::bail!("expected config param 17 (stakes config)"), + } + } + + /// Same as [`Self::network_max_stake_factor_raw`], as float multiplier (e.g. `3.0`). + pub async fn network_max_stake_factor_multiplier(&self) -> anyhow::Result { + let raw = self.network_max_stake_factor_raw().await?; + Ok(common::ton_utils::max_stake_factor_raw_to_multiplier(raw)) + } + pub async fn run_get_method( &self, args: &RunGetMethodParams, From e4302c8ceff1300aca79c8689019558b92c97900 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Wed, 8 Apr 2026 13:49:17 +0300 Subject: [PATCH 48/48] Revert "Merge branch 'master' into feature/sma-54-max-factor-upper-bound-is-hardcoded" This reverts commit fa35c528d28a1a1dd2186c5d6292a855898ba0bf, reversing changes made to 4ef0fcf232f5f1cbe187a4e10b858e95ef88a6c3. --- .github/workflows/node-release.yml | 99 - CHANGELOG.md | 27 - helm/ton-rust-node/CHANGELOG.md | 28 - helm/ton-rust-node/Chart.yaml | 4 +- helm/ton-rust-node/README.md | 32 +- helm/ton-rust-node/docs/archival-node.md | 176 -- helm/ton-rust-node/docs/logging.md | 2 - helm/ton-rust-node/docs/maintaining.md | 1 - helm/ton-rust-node/docs/networking.md | 9 +- helm/ton-rust-node/docs/node-config.md | 4 +- helm/ton-rust-node/files/logs.config.yml | 4 - helm/ton-rust-node/templates/statefulset.yaml | 3 +- helm/ton-rust-node/values.yaml | 13 +- src/.cargo/audit.toml | 13 - src/.gitignore | 3 +- src/Cargo.lock | 54 +- src/Cargo.toml | 3 +- src/Dockerfile | 12 +- src/Makefile | 1 - src/adnl/Cargo.toml | 1 - src/adnl/benches/bench_rldp.rs | 20 +- src/adnl/src/adnl/common.rs | 53 +- src/adnl/src/adnl/node.rs | 226 +-- src/adnl/src/adnl/transport.rs | 31 +- src/adnl/src/dht/mod.rs | 46 +- src/adnl/src/overlay/broadcast.rs | 90 +- src/adnl/src/overlay/mod.rs | 84 +- src/adnl/src/quic/mod.rs | 1400 +++----------- src/adnl/src/rldp/mod.rs | 41 +- src/adnl/src/rldp/send.rs | 2 +- src/adnl/tests/test_adnl.rs | 6 +- src/adnl/tests/test_overlay.rs | 32 +- src/adnl/tests/test_quic.rs | 1036 +---------- src/adnl/tests/test_rldp.rs | 10 +- src/adnl/tests/test_udp.rs | 7 +- src/adnl/tests/test_utils.rs | 4 +- src/assembler/src/complex.rs | 2 +- src/audit.toml | 7 + src/block-json/src/deserialize.rs | 40 +- src/block-json/src/serialize.rs | 51 +- src/block-json/src/tests/test_deserialize.rs | 97 +- src/block/src/accounts.rs | 2 +- src/block/src/blocks.rs | 2 +- src/block/src/config_params.rs | 248 +-- src/block/src/dictionary/hashmap.rs | 10 +- src/block/src/dictionary/mod.rs | 4 +- src/block/src/dictionary/pfxhashmap.rs | 6 +- .../src/dictionary/tests/test_hashmap.rs | 6 +- src/block/src/error.rs | 2 - src/block/src/master.rs | 31 +- src/block/src/out_actions.rs | 71 +- src/block/src/shard.rs | 19 - src/block/src/storage_stat.rs | 37 - src/block/src/tests/test_config_params.rs | 211 +-- src/block/src/tests/test_master.rs | 6 + src/block/src/tests/test_out_actions.rs | 35 +- src/block/src/tests/test_validators.rs | 13 + src/block/src/transactions.rs | 14 +- src/block/src/validators.rs | 126 +- src/ci/sync-test/README.md | 224 --- src/ci/sync-test/gen-node-config.sh | 47 - src/ci/sync-test/values.yaml | 56 - src/ci/sync-test/watcher.sh | 157 -- src/common/config/log_cfg_debug.yml | 2 - src/emulator/src/lib.rs | 19 +- src/executor/benches/benchmarks.rs | 9 +- ...ad_action_with_ignore_flag_account_new.boc | Bin 756 -> 0 bytes ...ad_action_with_ignore_flag_account_old.boc | Bin 756 -> 0 bytes ...ad_action_with_ignore_flag_transaction.boc | Bin 532 -> 0 bytes src/executor/src/blockchain_config.rs | 18 +- src/executor/src/ordinary_transaction.rs | 14 - src/executor/src/tests/common/mod.rs | 57 +- .../src/tests/test_ordinary_libs_and_code.rs | 4 +- .../src/tests/test_ordinary_transaction.rs | 179 +- ...est_transaction_executor_with_real_data.rs | 10 - src/executor/src/transaction_executor.rs | 90 +- src/node/Cargo.toml | 11 +- src/node/bin/adnl_ping.rs | 2 +- src/node/bin/adnl_resolve.rs | 4 +- src/node/bin/archive_import.rs | 102 -- src/node/bin/dhtscan.rs | 134 +- src/node/bin/print.rs | 2 +- src/node/catchain/src/receiver.rs | 6 +- src/node/consensus-common/src/adnl_overlay.rs | 514 ++---- .../src/dummy_catchain_overlay.rs | 1 - .../src/in_process_overlay.rs | 1 - src/node/consensus-common/src/lib.rs | 17 +- src/node/consensus-common/src/log_player.rs | 1 - .../consensus-common/src/node_test_network.rs | 10 +- .../tests/test_adnl_overlay.rs | 4 - .../tests/test_in_process_overlay.rs | 4 +- src/node/simplex/CHANGELOG.md | 27 +- src/node/simplex/Cargo.toml | 2 +- src/node/simplex/README.md | 49 +- src/node/simplex/src/block.rs | 17 +- src/node/simplex/src/database.rs | 336 +--- src/node/simplex/src/lib.rs | 116 +- src/node/simplex/src/receiver.rs | 368 +--- src/node/simplex/src/session.rs | 141 +- src/node/simplex/src/session_processor.rs | 1239 +++---------- src/node/simplex/src/simplex_state.rs | 784 +++----- src/node/simplex/src/startup_recovery.rs | 411 ++++- src/node/simplex/src/tests/test_block.rs | 7 +- .../src/tests/test_candidate_resolver.rs | 90 +- src/node/simplex/src/tests/test_receiver.rs | 201 +- src/node/simplex/src/tests/test_restart.rs | 193 +- .../src/tests/test_session_processor.rs | 1353 +------------- .../simplex/src/tests/test_simplex_state.rs | 993 +--------- src/node/simplex/src/utils.rs | 17 +- src/node/simplex/tests/test_collation.rs | 17 +- src/node/simplex/tests/test_consensus.rs | 321 +--- src/node/simplex/tests/test_restart.rs | 28 +- src/node/simplex/tests/test_validation.rs | 18 +- src/node/src/archive_import/ingester.rs | 830 --------- src/node/src/archive_import/mod.rs | 413 ----- src/node/src/archive_import/scanner.rs | 155 -- src/node/src/archive_import/validator.rs | 81 - src/node/src/collator_test_bundle.rs | 6 - src/node/src/config.rs | 122 +- src/node/src/engine.rs | 76 +- src/node/src/engine_operations.rs | 124 +- src/node/src/engine_traits.rs | 77 +- src/node/src/full_node/apply_block.rs | 130 +- src/node/src/internal_db/mod.rs | 383 +--- src/node/src/internal_db/restore.rs | 10 +- src/node/src/lib.rs | 1 - src/node/src/network/catchain_client.rs | 3 +- src/node/src/network/control.rs | 1 - src/node/src/network/custom_overlay_client.rs | 19 +- .../src/network/fast_sync_overlay_client.rs | 2 +- src/node/src/network/full_node_overlays.rs | 32 +- src/node/src/network/liteserver.rs | 2 +- src/node/src/network/node_network.rs | 302 +--- src/node/src/network/overlay_client.rs | 9 +- .../network/tests/test_full_node_overlays.rs | 1 - .../tests/test_node_network_validator_list.rs | 121 -- src/node/src/shard_blocks.rs | 7 +- src/node/src/sync.rs | 67 +- ...4B7173205F740A39CD56F537DEFD28B48A0F6E.boc | Bin 8529 -> 0 bytes ...18EA720CAD1A0E7B4D2ED673C488E72E910342.boc | Bin 105 -> 0 bytes .../archive.00000.0_8000000000000000.pack | Bin 341182 -> 0 bytes .../tests/static/archives/archive.00000.pack | Bin 1185911 -> 0 bytes .../archive.00100.0_8000000000000000.pack | Bin 693410 -> 0 bytes .../tests/static/archives/archive.00100.pack | Bin 1250896 -> 0 bytes src/node/src/tests/test_archive_import.rs | 290 --- src/node/src/tests/test_control.rs | 3 +- src/node/src/tests/test_engine_operations.rs | 28 - src/node/src/tests/test_helper.rs | 7 - src/node/src/tests/test_internal_db.rs | 33 - src/node/src/tests/test_sync.rs | 11 +- src/node/src/types/accounts.rs | 8 +- src/node/src/types/awaiters_pool.rs | 12 - src/node/src/validator/accept_block.rs | 5 +- src/node/src/validator/collator.rs | 239 ++- src/node/src/validator/consensus.rs | 70 +- src/node/src/validator/consensus_overlay.rs | 3 +- src/node/src/validator/fabric.rs | 10 +- src/node/src/validator/mod.rs | 10 +- src/node/src/validator/tests/test_collator.rs | 51 +- .../src/validator/tests/test_session_id.rs | 391 +--- src/node/src/validator/validate_query.rs | 240 +-- src/node/src/validator/validator_group.rs | 584 +----- src/node/src/validator/validator_manager.rs | 1609 ++++------------- .../validator/validator_session_listener.rs | 29 +- src/node/storage/Cargo.toml | 2 - src/node/storage/benches/shardstate_db1.rs | 4 +- src/node/storage/benches/shardstate_db2.rs | 4 +- src/node/storage/benches/shardstate_db3.rs | 4 +- src/node/storage/src/archive_shardstate_db.rs | 127 -- .../storage/src/archives/archive_manager.rs | 217 +-- .../storage/src/archives/archive_slice.rs | 224 +-- .../storage/src/archives/block_index_db.rs | 31 +- src/node/storage/src/archives/db_provider.rs | 63 - src/node/storage/src/archives/epoch.rs | 276 --- src/node/storage/src/archives/file_maps.rs | 98 +- src/node/storage/src/archives/mod.rs | 4 +- src/node/storage/src/archives/package.rs | 83 +- .../storage/src/archives/package_entry.rs | 7 - src/node/storage/src/archives/package_id.rs | 4 +- src/node/storage/src/block_handle_db.rs | 133 +- src/node/storage/src/block_info_db.rs | 5 - src/node/storage/src/cell_db.rs | 439 ----- src/node/storage/src/db/rocksdb.rs | 4 +- .../storage/src/dynamic_boc_archive_db.rs | 219 --- src/node/storage/src/dynamic_boc_rc_db.rs | 458 ++++- src/node/storage/src/lib.rs | 28 +- src/node/storage/src/shard_top_blocks_db.rs | 2 - src/node/storage/src/shardstate_db_async.rs | 24 +- src/node/storage/src/tests/mod.rs | 1 - .../storage/src/tests/test_archive_manager.rs | 221 +-- .../storage/src/tests/test_archive_slice.rs | 60 +- .../src/tests/test_dynamic_boc_archive_db.rs | 261 --- .../src/tests/test_dynamic_boc_rc_db.rs | 11 +- src/node/storage/src/tests/test_epoch.rs | 156 -- src/node/storage/src/types/block_meta.rs | 29 - src/node/storage/src/types/storage_cell.rs | 60 +- .../src/types/tests/test_storage_cell.rs | 23 +- src/node/tests/compat_test/.gitignore | 7 - src/node/tests/compat_test/Cargo.toml | 79 - src/node/tests/compat_test/Makefile | 112 -- src/node/tests/compat_test/README.md | 281 --- .../tests/compat_test/cpp_src/CMakeLists.txt | 258 --- .../compat_test/cpp_src/compat_test_node.cpp | 1302 ------------- .../compat_test/cpp_src/compat_test_node.hpp | 222 --- .../tests/compat_test/incompatibilities.md | 62 - src/node/tests/compat_test/src/lib.rs | 945 ---------- src/node/tests/compat_test/src/overlay_id.rs | 67 - .../tests/compat_test/src/test_helpers.rs | 851 --------- .../compat_test/tests/test_boc_compression.rs | 343 ---- .../tests/compat_test/tests/test_broadcast.rs | 188 -- .../tests/test_broadcast_validation.rs | 171 -- .../tests/test_candidate_id_to_sign.rs | 85 - .../tests/compat_test/tests/test_fec_relay.rs | 358 ---- .../compat_test/tests/test_overlay_id.rs | 126 -- .../compat_test/tests/test_overlay_message.rs | 293 --- .../compat_test/tests/test_public_overlay.rs | 137 -- .../compat_test/tests/test_quic_address.rs | 153 -- .../compat_test/tests/test_quic_overlay.rs | 290 --- .../tests/test_quic_private_overlay.rs | 360 ---- .../compat_test/tests/test_quic_transport.rs | 181 -- .../compat_test/tests/test_rldp_query.rs | 576 ------ .../tests/test_twostep_fec_relay.rs | 428 ----- .../tests/test_run_net_py/log_cfg_blank.yml | 5 - .../tests/test_run_net_py/simplex_config.json | 2 +- .../tests/test_run_net_py/test_run_net.py | 170 +- src/node/tests/test_sync/.dockerignore | 10 + src/node/tests/test_sync/.gitignore | 12 + src/node/tests/test_sync/Dockerfile | 89 + src/node/tests/test_sync/README.md | 118 ++ src/node/tests/test_sync/package.json | 15 + src/node/tests/test_sync/server.js | 1353 ++++++++++++++ src/node/validator-session/src/session.rs | 5 - src/tl/ton_api/tl/ton_api.tl | 15 +- src/vm/benches/benchmarks.rs | 2 +- src/vm/src/executor/dictionary.rs | 64 +- src/vm/src/executor/engine/core.rs | 25 +- src/vm/src/tests/test_executor.rs | 22 +- src/vm/tests/test_config.rs | 10 +- src/vm/tests/test_library.rs | 24 +- 239 files changed, 5971 insertions(+), 26778 deletions(-) delete mode 100644 .github/workflows/node-release.yml delete mode 100644 helm/ton-rust-node/docs/archival-node.md delete mode 100644 src/.cargo/audit.toml create mode 100644 src/audit.toml delete mode 100644 src/ci/sync-test/README.md delete mode 100644 src/ci/sync-test/gen-node-config.sh delete mode 100644 src/ci/sync-test/values.yaml delete mode 100644 src/ci/sync-test/watcher.sh delete mode 100644 src/executor/real_boc/bad_action_with_ignore_flag_account_new.boc delete mode 100644 src/executor/real_boc/bad_action_with_ignore_flag_account_old.boc delete mode 100644 src/executor/real_boc/bad_action_with_ignore_flag_transaction.boc delete mode 100644 src/node/bin/archive_import.rs delete mode 100644 src/node/src/archive_import/ingester.rs delete mode 100644 src/node/src/archive_import/mod.rs delete mode 100644 src/node/src/archive_import/scanner.rs delete mode 100644 src/node/src/archive_import/validator.rs delete mode 100644 src/node/src/network/tests/test_node_network_validator_list.rs delete mode 100644 src/node/src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc delete mode 100644 src/node/src/tests/static/EE0BEDFE4B32761FB35E9E1D8818EA720CAD1A0E7B4D2ED673C488E72E910342.boc delete mode 100644 src/node/src/tests/static/archives/archive.00000.0_8000000000000000.pack delete mode 100644 src/node/src/tests/static/archives/archive.00000.pack delete mode 100644 src/node/src/tests/static/archives/archive.00100.0_8000000000000000.pack delete mode 100644 src/node/src/tests/static/archives/archive.00100.pack delete mode 100644 src/node/src/tests/test_archive_import.rs delete mode 100644 src/node/src/tests/test_engine_operations.rs delete mode 100644 src/node/storage/src/archive_shardstate_db.rs delete mode 100644 src/node/storage/src/archives/db_provider.rs delete mode 100644 src/node/storage/src/archives/epoch.rs delete mode 100644 src/node/storage/src/cell_db.rs delete mode 100644 src/node/storage/src/dynamic_boc_archive_db.rs delete mode 100644 src/node/storage/src/tests/test_dynamic_boc_archive_db.rs delete mode 100644 src/node/storage/src/tests/test_epoch.rs delete mode 100644 src/node/tests/compat_test/.gitignore delete mode 100644 src/node/tests/compat_test/Cargo.toml delete mode 100644 src/node/tests/compat_test/Makefile delete mode 100644 src/node/tests/compat_test/README.md delete mode 100644 src/node/tests/compat_test/cpp_src/CMakeLists.txt delete mode 100644 src/node/tests/compat_test/cpp_src/compat_test_node.cpp delete mode 100644 src/node/tests/compat_test/cpp_src/compat_test_node.hpp delete mode 100644 src/node/tests/compat_test/incompatibilities.md delete mode 100644 src/node/tests/compat_test/src/lib.rs delete mode 100644 src/node/tests/compat_test/src/overlay_id.rs delete mode 100644 src/node/tests/compat_test/src/test_helpers.rs delete mode 100644 src/node/tests/compat_test/tests/test_boc_compression.rs delete mode 100644 src/node/tests/compat_test/tests/test_broadcast.rs delete mode 100644 src/node/tests/compat_test/tests/test_broadcast_validation.rs delete mode 100644 src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs delete mode 100644 src/node/tests/compat_test/tests/test_fec_relay.rs delete mode 100644 src/node/tests/compat_test/tests/test_overlay_id.rs delete mode 100644 src/node/tests/compat_test/tests/test_overlay_message.rs delete mode 100644 src/node/tests/compat_test/tests/test_public_overlay.rs delete mode 100644 src/node/tests/compat_test/tests/test_quic_address.rs delete mode 100644 src/node/tests/compat_test/tests/test_quic_overlay.rs delete mode 100644 src/node/tests/compat_test/tests/test_quic_private_overlay.rs delete mode 100644 src/node/tests/compat_test/tests/test_quic_transport.rs delete mode 100644 src/node/tests/compat_test/tests/test_rldp_query.rs delete mode 100644 src/node/tests/compat_test/tests/test_twostep_fec_relay.rs create mode 100644 src/node/tests/test_sync/.dockerignore create mode 100644 src/node/tests/test_sync/.gitignore create mode 100644 src/node/tests/test_sync/Dockerfile create mode 100644 src/node/tests/test_sync/README.md create mode 100644 src/node/tests/test_sync/package.json create mode 100644 src/node/tests/test_sync/server.js diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml deleted file mode 100644 index 84cbb0e..0000000 --- a/.github/workflows/node-release.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Publish node - -on: - push: - tags: - - "node/v*" - -concurrency: - group: ${{ github.workflow }}-${{ github.sha }} - cancel-in-progress: true - -env: - WORKING_DIR: src - -jobs: - container: - runs-on: - group: ton-large - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v5 - - - name: Get commit timestamp - id: commit - run: echo "date=$(git log -1 --format=%cI)" >> "$GITHUB_OUTPUT" - - - uses: docker/setup-buildx-action@v3 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/rsquad/ton-rust-node/node - tags: | - type=match,pattern=node/(v.*),group=1 - type=raw,value=latest,enable=${{ !contains(github.ref_name, 'rc') && !contains(github.ref_name, 'alpha') && !contains(github.ref_name, 'beta') }} - type=sha - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: ./src - file: ./src/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - GIT_BRANCH=${{ github.ref_name }} - GIT_COMMIT=${{ github.sha }} - GIT_COMMIT_DATE=${{ steps.commit.outputs.date }} - cache-from: type=gha - cache-to: type=gha,mode=max - - release: - needs: [container] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - - - name: Extract version from tag - id: version - run: | - VERSION="${GITHUB_REF_NAME#node/}" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - - if echo "$VERSION" | grep -qE '(rc|alpha|beta)'; then - echo "prerelease=true" >> "$GITHUB_OUTPUT" - else - echo "prerelease=false" >> "$GITHUB_OUTPUT" - fi - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - name: "node/${{ steps.version.outputs.version }}" - body: | - node ${{ steps.version.outputs.version }} - - Image: `ghcr.io/rsquad/ton-rust-node/node:${{ steps.version.outputs.version }}` - prerelease: ${{ steps.version.outputs.prerelease }} - make_latest: ${{ steps.version.outputs.prerelease != 'true' }} - draft: ${{ steps.version.outputs.prerelease }} - - - name: Publish pre-release - if: steps.version.outputs.prerelease == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release edit "$GITHUB_REF_NAME" --draft=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dae690..2626eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,33 +6,6 @@ For Helm chart changes, see [helm/ton-rust-node/CHANGELOG.md](helm/ton-rust-node The format is based on [Keep a Changelog](https://keepachangelog.com/). Versions follow the node release tags (e.g. `v0.1.2-mainnet`). -## [v0.4.0] - 2026-04-05 - -Image: `ghcr.io/rsquad/ton-rust-node/node:v0.4.0` - -This release brings support for the Simplex consensus protocol and QUIC transport โ€” key protocol upgrades rolling out across the TON network. It also introduces archival node functionality and a range of fixes to fee accounting, storage phase handling, and sync stability. - -### Added - -- Simplex consensus updates and QUIC integration -- QUIC transport with separate address support and connection deduplication -- Archival node functionality with split/merge resilience -- CellsDB cells cache - -### Changed - -- Validate query uses capabilities from blockchain config instead of candidate block -- Enforce mcStateExtra flags <=1, remove ValidatorsStat - -### Fixed - -- Storage phase: preserve original due_payment for special accounts in partial storage phase -- Masterchain ValueFlow burned fees and blackhole accounting -- Fee accumulation: accumulate fees_collected instead of overwriting to preserve shard burn -- Untouched account change detection -- Fast sync overlay creation reliability -- Archive sync stalling on shard split/merge - ## [v0.3.0] - 2026-03-12 Image: `ghcr.io/rsquad/ton-rust-node/node:v0.3.0` diff --git a/helm/ton-rust-node/CHANGELOG.md b/helm/ton-rust-node/CHANGELOG.md index 301c0c6..8c8609c 100644 --- a/helm/ton-rust-node/CHANGELOG.md +++ b/helm/ton-rust-node/CHANGELOG.md @@ -5,34 +5,6 @@ All notable changes to the Helm chart will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). Versions follow the Helm chart release tags (e.g. `helm/v0.3.0`). -## [0.4.5] - 2026-04-05 - -appVersion: `v0.4.0` - -### Changed - -- Default image tag and appVersion updated to `v0.4.0` - -## [0.4.4] - 2026-04-02 - -appVersion: `v0.3.0` - -### Added - -- `dnsPolicy` โ€” override pod DNS policy when `hostNetwork` is enabled (default: `ClusterFirstWithHostNet`) - -### Fixed - -- `hostNetwork: true` set invalid `dnsPolicy: ClusterFirstWithHostDNS` (typo โ€” correct value is `ClusterFirstWithHostNet`), causing StatefulSet creation to fail - -## [0.4.3] - 2026-04-01 - -appVersion: `v0.3.0` - -### Added - -- `terminationGracePeriodSeconds` โ€” configurable grace period before SIGKILL on pod termination. Defaults to 300s (5 minutes). The Kubernetes default of 30s is too short for a TON node โ€” an unclean kill may corrupt the database and forces a cold boot - ## [0.4.2] - 2026-03-18 appVersion: `v0.3.0` diff --git a/helm/ton-rust-node/Chart.yaml b/helm/ton-rust-node/Chart.yaml index 9b4308e..12d65dc 100644 --- a/helm/ton-rust-node/Chart.yaml +++ b/helm/ton-rust-node/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: node description: TON Rust Node deployment type: application -version: 0.4.5 -appVersion: "v0.4.0" +version: 0.4.2 +appVersion: "v0.3.0" sources: - https://github.com/rsquad/ton-rust-node diff --git a/helm/ton-rust-node/README.md b/helm/ton-rust-node/README.md index 98960b6..3510724 100644 --- a/helm/ton-rust-node/README.md +++ b/helm/ton-rust-node/README.md @@ -21,7 +21,6 @@ A TON node can run in two roles โ€” **validator** or **fullnode** โ€” using the **Validator** participates in network consensus: it validates blocks, votes in elections, and earns rewards. Validator is currently supported on **mainnet** only โ€” testnet validator support is not yet available. A validator is a critical infrastructure component, so: -- **Enable `ports.simplex: true`** โ€” the network uses simplex consensus and validators will not work without it. - Never expose `liteserver` or `jsonRpc` ports on a validator. Every open port is an attack surface and adds unnecessary load to a machine that must stay performant and stable. - Allocate more resources (see [docs/resources.md](docs/resources.md) for recommended values). @@ -55,9 +54,6 @@ Minimal deployment: # values.yaml replicas: 2 -ports: - simplex: true - services: perReplica: - annotations: @@ -244,14 +240,14 @@ When an `existing*Name` is set, the chart does not create that resource โ€” it o ### Port parameters -| Name | Description | Value | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | -| `ports.adnl` | ADNL port (UDP) | `30303` | -| `ports.simplex` | Simplex consensus port (UDP). Required for validators โ€” the network uses simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. | `false` | -| `ports.control` | Control port (TCP). Set to null to disable. | `50000` | -| `ports.liteserver` | Liteserver port (TCP). Set to enable. | `nil` | -| `ports.jsonRpc` | JSON-RPC port (TCP). Set to enable. | `nil` | -| `ports.metrics` | Metrics/probes HTTP port (TCP). Serves /metrics, /healthz, /readyz. Set to enable. | `nil` | +| Name | Description | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `ports.adnl` | ADNL port (UDP) | `30303` | +| `ports.simplex` | Simplex consensus port (UDP). Only needed for validators after switching to simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. | `false` | +| `ports.control` | Control port (TCP). Set to null to disable. | `50000` | +| `ports.liteserver` | Liteserver port (TCP). Set to enable. | `nil` | +| `ports.jsonRpc` | JSON-RPC port (TCP). Set to enable. | `nil` | +| `ports.metrics` | Metrics/probes HTTP port (TCP). Serves /metrics, /healthz, /readyz. Set to enable. | `nil` | ### Service parameters @@ -322,7 +318,6 @@ When an `existing*Name` is set, the chart does not create that resource โ€” it o | Name | Description | Value | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | | `hostNetwork` | Bind pods directly to the host network. The pod gets the node's IP with zero NAT overhead. Requires one pod per node โ€” use nodeSelector or podAntiAffinity to spread replicas. See docs/networking.md. | `false` | -| `dnsPolicy` | Pod DNS policy (only applies when hostNetwork is true). Defaults to ClusterFirstWithHostNet. Supported values: ClusterFirstWithHostNet, ClusterFirst, Default, None. | `""` | | `hostPort.adnl` | Expose the ADNL port on the host IP via hostPort | `false` | | `hostPort.simplex` | Expose the simplex port on the host IP via hostPort | `false` | | `hostPort.control` | Expose the control port on the host IP via hostPort | `false` | @@ -349,12 +344,11 @@ When an `existing*Name` is set, the chart does not create that resource โ€” it o ### ServiceAccount parameters -| Name | Description | Value | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | -| `serviceAccount.enabled` | Create a ServiceAccount for the pods | `false` | -| `serviceAccount.name` | ServiceAccount name. Defaults to the release fullname if not set. | `""` | -| `serviceAccount.annotations` | Annotations for the ServiceAccount (e.g. for Vault or cloud IAM role binding) | `{}` | -| `terminationGracePeriodSeconds` | Time (in seconds) given to the node process to shut down gracefully before SIGKILL. The default Kubernetes value (30s) is too short for a TON node โ€” an unclean kill may corrupt the database and forces a cold boot. Set this to at least 300s. | `300` | +| Name | Description | Value | +| ---------------------------- | ----------------------------------------------------------------------------- | ------- | +| `serviceAccount.enabled` | Create a ServiceAccount for the pods | `false` | +| `serviceAccount.name` | ServiceAccount name. Defaults to the release fullname if not set. | `""` | +| `serviceAccount.annotations` | Annotations for the ServiceAccount (e.g. for Vault or cloud IAM role binding) | `{}` | ### Scheduling parameters diff --git a/helm/ton-rust-node/docs/archival-node.md b/helm/ton-rust-node/docs/archival-node.md deleted file mode 100644 index 1158eda..0000000 --- a/helm/ton-rust-node/docs/archival-node.md +++ /dev/null @@ -1,176 +0,0 @@ -# Archival node - -An archival node keeps the full blockchain history and serves historical queries via liteserver. Setting one up involves two steps: importing existing archives into epoch-based storage, then starting the node in archival mode. - -## Table of contents - -- [System requirements](#system-requirements) -- [Source data](#source-data) -- [Archive import](#archive-import) - - [Parameters](#parameters) - - [What the import does](#what-the-import-does) - - [Output structure](#output-structure) -- [Archival mode config](#archival-mode-config) - - [Simple setup](#simple-setup) - - [Distributed setup](#distributed-setup) -- [Cloning an existing archival node](#cloning-an-existing-archival-node) -- [Helm integration](#helm-integration) - -## System requirements - -| Resource | Minimum | -|----------|---------| -| Disk | 20 TB (block archives + cells database) | -| RAM | 32 GB | -| Import time | Several days for full blockchain history | - -## Source data - -TON block archives are stored as pairs of `.pack` files per archive group (masterchain + shard blocks): - -``` -archive.00000.pack # masterchain blocks 0-99 -archive.00000.0:8000000000000000.pack # workchain 0 shard blocks for the same range -archive.00100.pack # masterchain blocks 100-199 -archive.00100.0:8000000000000000.pack # workchain 0 shard blocks -... -``` - -You also need: - -- Masterchain zerostate `.boc` file -- Workchain zerostate(s) `.boc` file(s) (one per workchain) -- Global config JSON (contains zerostate hashes and hard fork list) - -## Archive import - -The `archive_import` tool converts raw `.pack` files into epoch-based storage used by the archival node. - -```bash -RUST_LOG=info archive_import \ - --archives-path /path/to/archives \ - --epochs-path /data/epochs \ - --node-db-path /data/node_db \ - --mc-zerostate /path/to/mc_zerostate.boc \ - --wc-zerostate /path/to/wc0_zerostate.boc \ - --global-config /path/to/global-config.json -``` - -### Parameters - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `--archives-path` | yes | Directory containing source `.pack` files | -| `--epochs-path` | yes | Directory where epoch subdirectories will be created | -| `--node-db-path` | yes | Path to node database | -| `--mc-zerostate` | yes | Path to masterchain zerostate `.boc` file | -| `--wc-zerostate` | yes | Path to workchain zerostate `.boc` file (repeat for each workchain) | -| `--global-config` | yes | Path to global config JSON | -| `--epoch-size` | no | MC blocks per epoch, default `10000000` (must be a multiple of 20000) | -| `--copy` | no | Copy `.pack` files instead of moving them | - -### What the import does - -1. Validates zerostate hashes against the global config -2. Scans the archives directory and groups `.pack` files by archive ID -3. For each group: deserializes blocks, validates proofs, imports packages into epoch storage -4. Populates the node database: block handles, prev/next block links, block index, shard states - -The import supports resume โ€” if interrupted, re-run with the same parameters to continue from the last imported group. - -### Output structure - -``` -/data/node_db/ - db/ # main RocksDB (block handles, indexes, state keys) - archive_states/ # shard state cell storage - -/data/epochs/ - epoch_0/ # archive packages for MC blocks 0..epoch_size-1 - archive_db/ # RocksDB with package metadata - archive/packages/ # .pack files - epoch_1/ - ... -``` - ---- - -## Archival mode config - -Add the `archival_mode` section to the node config to enable epoch-based archival storage. Set `internal_db_path` to point to the database created by the import. - -### Simple setup - -All epochs in one directory. The node auto-discovers existing epochs on startup and creates new ones in the same location. - -```json -{ - "internal_db_path": "/data/node_db", - "archival_mode": { - "epoch_size": 10000000, - "new_epochs_path": "/data/epochs", - "existing_epochs": [] - } -} -``` - -The `epoch_size` must match the value used during import. - -### Distributed setup - -Imported epochs on separate (slower) storage, new epochs on fast storage. List imported epoch directories explicitly in `existing_epochs`. - -```json -{ - "internal_db_path": "/data/node_db", - "archival_mode": { - "epoch_size": 10000000, - "new_epochs_path": "/fast_ssd/new_epochs", - "existing_epochs": [ - { "path": "/nfs/imported/epoch_0" }, - { "path": "/nfs/imported/epoch_1" }, - { "path": "/fast_ssd/imported/epoch_2" } - ] - } -} -``` - -> **Note:** The last imported epoch is likely incomplete โ€” its range covers blocks still being created. It will continue to receive new blocks during sync, so place it on fast storage alongside `new_epochs_path`. - -### Behavior - -When `archival_mode` is set: - -- Archive GC is disabled โ€” all historical data is preserved -- Shard states are stored in a separate cell database (`archive_states/`) -- New blocks arriving via sync are appended to the latest epoch - -> **See also:** For a simpler setup that keeps full history without epoch-based storage, see the [archival node section in node-config.md](node-config.md#archival-node). That approach disables GC and works without the import step, but requires the node to sync all history from scratch. - ---- - -## Cloning an existing archival node - -Instead of importing from scratch, you can copy data from a running archival node: - -1. Stop the source node -2. Copy epoch directories and the node database (`rsync` or similar) -3. On the new machine, generate a fresh node config with new ADNL keys -4. Set `internal_db_path` and `archival_mode` pointing to the copied data -5. Start the new node - -This works because the database contains only blockchain data (blocks, states, indexes). Node identity (ADNL keys, validator keys) is stored in the config file and secrets vault, not in the database. - -> **Important:** Do not copy the node config file โ€” it contains the ADNL private keys of the source node. Always generate fresh keys for the new node. - ---- - -## Helm integration - -The archive import runs outside of Kubernetes as a one-time migration step. After import, configure the Helm chart to start the node in archival mode: - -1. Mount the epoch storage and node database into the pod using `extraVolumes` and `extraVolumeMounts` -2. Set `archival_mode` in the node config (`nodeConfigs`) with paths matching the mount points -3. Size `storage.db.size` for the node database (the epoch data lives on external volumes) - -> **See also:** [node-config.md](node-config.md#archival-node) covers the GC-based approach to keeping full history, which does not require the import step but uses more disk on the primary volume. diff --git a/helm/ton-rust-node/docs/logging.md b/helm/ton-rust-node/docs/logging.md index 6131382..2582ed8 100644 --- a/helm/ton-rust-node/docs/logging.md +++ b/helm/ton-rust-node/docs/logging.md @@ -238,8 +238,6 @@ These are the targets you can configure in the `loggers` section: | `storage` | Data storage | | `index` | Data indexing | | `ext_messages` | External message handling | -| `quic` | QUIC transport protocol | -| `simplex` | Simplex consensus protocol | | `telemetry` | Telemetry and metrics | > **Note:** HTTP requests (JSON-RPC, metrics endpoints) are not logged by the node. There is no logger target for HTTP request tracing. diff --git a/helm/ton-rust-node/docs/maintaining.md b/helm/ton-rust-node/docs/maintaining.md index 0396a6c..6d13199 100644 --- a/helm/ton-rust-node/docs/maintaining.md +++ b/helm/ton-rust-node/docs/maintaining.md @@ -59,7 +59,6 @@ All documentation lives in the `docs/` directory: | File | Content | |------|---------| | `node-config.md` | `config.json` field reference, JSON examples, archival setup | -| `archival-node.md` | Archive import tool, epoch-based archival mode, cloning | | `logging.md` | log4rs config (appenders, levels, rotation) | | `global-config.md` | Global network config overview | | `networking.md` | Networking modes (LoadBalancer, NodePort, hostPort, hostNetwork), NetworkPolicy | diff --git a/helm/ton-rust-node/docs/networking.md b/helm/ton-rust-node/docs/networking.md index 6e89e9e..20bd738 100644 --- a/helm/ton-rust-node/docs/networking.md +++ b/helm/ton-rust-node/docs/networking.md @@ -21,7 +21,7 @@ The chart manages six ports. Each port is optional (set to `null` to disable) ex | Port | Protocol | Default | Purpose | |------|----------|---------|---------| | `ports.adnl` | UDP | `30303` | Peer-to-peer protocol. Must be publicly reachable. | -| `ports.simplex` | UDP | `false` | Simplex consensus protocol. **Required for validators** โ€” the network uses simplex consensus. `true` = adnl + 1000, or set an explicit port number. | +| `ports.simplex` | UDP | `false` | Simplex consensus protocol. Only needed for validators after switching to simplex consensus. `true` = adnl + 1000, or set an explicit port number. | | `ports.control` | TCP | `50000` | Node management (stop, restart, elections). Recommended to keep internal. | | `ports.liteserver` | TCP | `null` | Liteserver API for external consumers. | | `ports.jsonRpc` | TCP | `null` | JSON-RPC API for external consumers. | @@ -190,13 +190,6 @@ The pod uses the host's network stack directly. All container ports bind on the hostNetwork: true ``` -The chart automatically sets `dnsPolicy: ClusterFirstWithHostNet` when `hostNetwork` is enabled. This ensures pods can still resolve cluster DNS names. Override with `dnsPolicy` if needed: - -```yaml -hostNetwork: true -dnsPolicy: Default -``` - **When to use:** bare-metal deployments where you need zero NAT overhead and accept the security trade-off. **Trade-offs:** diff --git a/helm/ton-rust-node/docs/node-config.md b/helm/ton-rust-node/docs/node-config.md index 48ffecd..e02173b 100644 --- a/helm/ton-rust-node/docs/node-config.md +++ b/helm/ton-rust-node/docs/node-config.md @@ -234,9 +234,7 @@ For a validator, add `control-server` and `control-client` to the loop. ## Archival node -By default the node prunes old archives and state snapshots via the `gc` section. To keep the **full** blockchain history, override the GC settings in your node config. - -> **See also:** For epoch-based archival storage with historical archive import, see [archival-node.md](archival-node.md). That approach is needed when you want to serve the full blockchain history from imported block archives. +By default the node prunes old archives and state snapshots via the `gc` section. To keep the **full** blockchain history, override the GC settings in your node config: ```json { diff --git a/helm/ton-rust-node/files/logs.config.yml b/helm/ton-rust-node/files/logs.config.yml index e079df1..b205229 100644 --- a/helm/ton-rust-node/files/logs.config.yml +++ b/helm/ton-rust-node/files/logs.config.yml @@ -74,7 +74,3 @@ loggers: level: warn ext_messages: level: info - quic: - level: info - simplex: - level: info diff --git a/helm/ton-rust-node/templates/statefulset.yaml b/helm/ton-rust-node/templates/statefulset.yaml index b37e8e4..34b35be 100644 --- a/helm/ton-rust-node/templates/statefulset.yaml +++ b/helm/ton-rust-node/templates/statefulset.yaml @@ -46,10 +46,9 @@ spec: affinity: {{- toYaml . | nindent 8 }} {{- end }} - terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} {{- if .Values.hostNetwork }} hostNetwork: true - dnsPolicy: {{ .Values.dnsPolicy | default "ClusterFirstWithHostNet" }} + dnsPolicy: ClusterFirstWithHostDNS {{- end }} initContainers: - name: init-bootstrap diff --git a/helm/ton-rust-node/values.yaml b/helm/ton-rust-node/values.yaml index a1eddea..9d00a04 100644 --- a/helm/ton-rust-node/values.yaml +++ b/helm/ton-rust-node/values.yaml @@ -16,7 +16,7 @@ command: [] ## image: repository: ghcr.io/rsquad/ton-rust-node/node - tag: v0.4.0 + tag: v0.3.0 pullPolicy: IfNotPresent ## @param imagePullSecrets [array] Image pull secrets for private registries @@ -133,7 +133,7 @@ storage: ## @section Port parameters ## @param ports.adnl ADNL port (UDP) -## @param ports.simplex [nullable] Simplex consensus port (UDP). Required for validators โ€” the network uses simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. +## @param ports.simplex [nullable] Simplex consensus port (UDP). Only needed for validators after switching to simplex consensus. false/null = disabled (default), true = adnl + 1000, number = explicit port. ## @param ports.control Control port (TCP). Set to null to disable. ## @param ports.liteserver [nullable] Liteserver port (TCP). Set to enable. ## @param ports.jsonRpc [nullable] JSON-RPC port (TCP). Set to enable. @@ -319,11 +319,6 @@ vault: ## hostNetwork: false -## @param dnsPolicy Pod DNS policy (only applies when hostNetwork is true). Defaults to ClusterFirstWithHostNet. Supported values: ClusterFirstWithHostNet, ClusterFirst, Default, None. -## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy -## -dnsPolicy: "" - ## @param hostPort.adnl Expose the ADNL port on the host IP via hostPort ## @param hostPort.simplex Expose the simplex port on the host IP via hostPort ## @param hostPort.control Expose the control port on the host IP via hostPort @@ -412,10 +407,6 @@ serviceAccount: name: "" annotations: {} -## @param terminationGracePeriodSeconds Time (in seconds) given to the node process to shut down gracefully before SIGKILL. The default Kubernetes value (30s) is too short for a TON node โ€” an unclean kill may corrupt the database and forces a cold boot. Set this to at least 300s. -## -terminationGracePeriodSeconds: 300 - ## @section Scheduling parameters ## @param nodeSelector [object] Node selector for pod scheduling diff --git a/src/.cargo/audit.toml b/src/.cargo/audit.toml deleted file mode 100644 index 9f16dff..0000000 --- a/src/.cargo/audit.toml +++ /dev/null @@ -1,13 +0,0 @@ -[advisories] -ignore = [ - # RUSTSEC-2023-0071: Marvin Attack timing side-channel in RSA decryption. - # Not exploitable here: rsa is only used for encryption (RSA-OAEP wrapping of - # an AES key with a public key). No decryption is performed. - # Tracked: NODE-31 - "RUSTSEC-2023-0071", - # RUSTSEC-2026-0049: rustls-webpki CRL matching logic bug. - # Not exploitable here: QUIC transport uses Raw Public Keys (RPK), not X.509 - # certificates with CRLs. Waiting for upstream rustls fix. - # Tracked: NODE-47 - "RUSTSEC-2026-0049", -] diff --git a/src/.gitignore b/src/.gitignore index a340d68..04cb361 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -9,6 +9,5 @@ node/tests/test_run_net_py/tmp node/tests/test_run_net_py/test_run_net.json node/tests/test_run_net_py/.ruff_cache .claude -node/tests/compat_test/cpp_src/build node/tests/mirrornet/mirrornet.json -node/tests/mirrornet/mirrornet_global_config.json +node/tests/mirrornet/mirrornet_global_config.json \ No newline at end of file diff --git a/src/Cargo.lock b/src/Cargo.lock index 693cdc3..0b18404 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -27,7 +27,6 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" name = "adnl" version = "0.11.38" dependencies = [ - "adnl", "anyhow", "async-trait", "chrono", @@ -247,9 +246,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" dependencies = [ "rustversion", ] @@ -917,28 +916,6 @@ dependencies = [ "utoipa", ] -[[package]] -name = "compat_test" -version = "0.1.0" -dependencies = [ - "adnl", - "anyhow", - "async-trait", - "base64 0.22.1", - "env_logger", - "hex", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-test", - "tokio-util", - "ton_api", - "ton_block", -] - [[package]] name = "consensus-common" version = "0.2.0" @@ -2707,9 +2684,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" @@ -3246,7 +3223,7 @@ dependencies = [ [[package]] name = "node" -version = "0.4.0" +version = "0.3.0" dependencies = [ "adnl", "ahash", @@ -3288,7 +3265,6 @@ dependencies = [ "parking_lot", "pretty_assertions", "rand 0.8.5", - "rayon", "regex", "reqwest", "secrets-vault", @@ -3302,7 +3278,6 @@ dependencies = [ "storage", "stream-cancel", "string-builder", - "tempfile", "thiserror 1.0.69", "tokio", "tokio-util", @@ -4606,9 +4581,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -5033,7 +5008,7 @@ dependencies = [ [[package]] name = "simplex" -version = "0.5.0" +version = "0.4.0" dependencies = [ "adnl", "anyhow", @@ -5167,11 +5142,9 @@ dependencies = [ "serde", "serde_cbor", "serde_derive", - "serde_json", "smallvec", "strum", "strum_macros", - "tempfile", "thiserror 1.0.69", "tokio", "tokio-util", @@ -5571,17 +5544,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-test" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" -dependencies = [ - "futures-core", - "tokio", - "tokio-stream", -] - [[package]] name = "tokio-util" version = "0.7.18" diff --git a/src/Cargo.toml b/src/Cargo.toml index cf32110..7bfa8d6 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -7,13 +7,12 @@ members = [ 'executor', 'emulator', 'lockfree', - 'node', 'node/catchain', 'node/consensus-common', 'node/simplex', 'node/storage', - 'node/tests/compat_test', 'node/validator-session', + 'node', 'node-control/commands', 'node-control/common', 'node-control/contracts', diff --git a/src/Dockerfile b/src/Dockerfile index 584f14b..4fed586 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -19,18 +19,16 @@ ENV GIT_BRANCH=${GIT_BRANCH} ENV GIT_COMMIT=${GIT_COMMIT} ENV GIT_COMMIT_DATE=${GIT_COMMIT_DATE} -RUN cargo build --release --bin node --bin console +RUN cargo build --release --bin node -FROM ubuntu:24.04 +FROM debian:stable-slim -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - libssl3t64 \ +RUN apt-get update && apt-get install -y \ + openssl \ libzstd1 \ - libgoogle-perftools4t64 \ + libgoogle-perftools4 \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /node/target/release/node /usr/local/bin/ -COPY --from=builder /node/target/release/console /usr/local/bin/ WORKDIR /node diff --git a/src/Makefile b/src/Makefile index bf68a05..ab93dbe 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,7 +44,6 @@ fmt: @cargo +nightly fmt $(check) --manifest-path ./node/consensus-common/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node/simplex/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node/storage/Cargo.toml - @cargo +nightly fmt $(check) --manifest-path ./node/tests/compat_test/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node/validator-session/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node-control/commands/Cargo.toml @cargo +nightly fmt $(check) --manifest-path ./node-control/common/Cargo.toml diff --git a/src/adnl/Cargo.toml b/src/adnl/Cargo.toml index 4da6d41..fedb3de 100644 --- a/src/adnl/Cargo.toml +++ b/src/adnl/Cargo.toml @@ -39,7 +39,6 @@ ton_api = { path = '../tl/ton_api' } [dev-dependencies] log4rs = '1.2' -adnl = { path = ".", features = [ 'server' ] } [features] client = [ ] diff --git a/src/adnl/benches/bench_rldp.rs b/src/adnl/benches/bench_rldp.rs index ed3a046..9c958e4 100644 --- a/src/adnl/benches/bench_rldp.rs +++ b/src/adnl/benches/bench_rldp.rs @@ -13,12 +13,13 @@ use adnl::{ AdnlPeers, Answer, QueryAnswer, QueryResult, Subscriber, TaggedByteSlice, TaggedByteVec, }, node::AdnlNode, - RldpNode, + rldp::RldpNode, }; use rand::Rng; -#[cfg(feature = "debug")] -use std::sync::atomic::{AtomicU32, AtomicU8, Ordering}; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicU32, AtomicU8, Ordering}, + Arc, +}; #[cfg(not(feature = "debug"))] use std::time::Instant; use ton_api::{ @@ -97,7 +98,12 @@ async fn bench_scenario( #[cfg(feature = "telemetry")] tag: 0, }; - let res = rldp1.query(&data, Some(size as u64 + 1024), &peers, v2, None).await.unwrap(); + let res = if v2 { + rldp1.query_v2(&data, Some(size as u64 + 1024), &peers, None).await + } else { + rldp1.query(&data, Some(size as u64 + 1024), &peers, None).await + } + .unwrap(); let (Some(reply), _) = res else { println!(" failed: empty response"); break; @@ -197,8 +203,8 @@ fn bench_rldp(loopback: bool, v2: bool, #[cfg(feature = "debug")] loss_percentag #[cfg(feature = "debug")] loss_percentage, ); - adnl1.add_peer(key1.id(), adnl2.ip_address_adnl(), None, &key2).unwrap(); - adnl2.add_peer(key2.id(), adnl1.ip_address_adnl(), None, &key1).unwrap(); + adnl1.add_peer(key1.id(), adnl2.ip_address(), &key2).unwrap(); + adnl2.add_peer(key2.id(), adnl1.ip_address(), &key1).unwrap(); if let Some(rt2) = &rt2 { rt1.block_on(adnl1.start_over_udp(vec![rldp1.clone()])).unwrap(); rt2.block_on(adnl2.start_over_udp(vec![rldp2.clone()])).unwrap(); diff --git a/src/adnl/src/adnl/common.rs b/src/adnl/src/adnl/common.rs index efc40de..378bfc2 100644 --- a/src/adnl/src/adnl/common.rs +++ b/src/adnl/src/adnl/common.rs @@ -468,60 +468,21 @@ pub enum Answer { Raw(TaggedByteVec), } -/// Cancellation-safety guard over AtomicU32 counter -struct SyncGuard<'a> { - counter: &'a AtomicU32, -} - -impl<'a> SyncGuard<'a> { - fn new(counter: &'a AtomicU32) -> Self { - counter.fetch_add(1, Ordering::Relaxed); - Self { counter } - } -} - -impl Drop for SyncGuard<'_> { - fn drop(&mut self) { - self.counter.fetch_sub(1, Ordering::Relaxed); - } -} - /// Asynchronous data receiver pub(crate) struct AsyncReceiver { - id: u64, data: lockfree::queue::Queue>, subscribers: lockfree::queue::Queue>, sync: AtomicU32, started_receiving: AtomicBool, - alive_tasks: AtomicU32, - total_spawned: AtomicU64, -} - -impl Drop for AsyncReceiver { - fn drop(&mut self) { - log::info!( - target: TARGET, - "AsyncReceiver #{} dropped (alive_tasks={}, total_spawned={})", - self.id, - self.alive_tasks.load(Ordering::Relaxed), - self.total_spawned.load(Ordering::Relaxed), - ); - } } impl AsyncReceiver { pub(crate) fn new() -> Arc { - static INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0); - let id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); - log::info!(target: TARGET, "AsyncReceiver #{id} created"); Arc::new(Self { - id, data: lockfree::queue::Queue::new(), subscribers: lockfree::queue::Queue::new(), sync: AtomicU32::new(0), started_receiving: AtomicBool::new(false), - alive_tasks: AtomicU32::new(0), - total_spawned: AtomicU64::new(0), }) } @@ -533,10 +494,10 @@ impl AsyncReceiver { pub(crate) async fn pop(&self) -> Result> { self.started_receiving.store(true, Ordering::Relaxed); - // Protect counter by guard - let _guard = SyncGuard::new(&self.sync); + self.sync.fetch_add(1, Ordering::Relaxed); loop { if let Some(data) = self.data.pop() { + self.sync.fetch_sub(1, Ordering::Relaxed); break Ok(data); } let subscriber = Arc::new(tokio::sync::Barrier::new(2)); @@ -554,15 +515,6 @@ impl AsyncReceiver { if self.sync.load(Ordering::Relaxed) == 0 { return; } - let alive = self.alive_tasks.fetch_add(1, Ordering::Relaxed) + 1; - let total = self.total_spawned.fetch_add(1, Ordering::Relaxed) + 1; - if total % 1000 == 0 || alive > 10 { - log::warn!( - target: TARGET, - "AsyncReceiver #{}: alive_tasks={alive}, total_spawned={total}, sync={}", - self.id, self.sync.load(Ordering::Relaxed) - ); - } let receiver = self.clone(); tokio::spawn(async move { while receiver.sync.load(Ordering::Relaxed) > 0 { @@ -573,7 +525,6 @@ impl AsyncReceiver { tokio::task::yield_now().await; } } - receiver.alive_tasks.fetch_sub(1, Ordering::Relaxed); }); } } diff --git a/src/adnl/src/adnl/node.rs b/src/adnl/src/adnl/node.rs index f0cb3fd..15a13c1 100644 --- a/src/adnl/src/adnl/node.rs +++ b/src/adnl/src/adnl/node.rs @@ -30,7 +30,7 @@ use std::{ cmp::{max, min}, convert::TryInto, fmt::{self, Debug, Display, Formatter}, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, SocketAddr}, sync::{ atomic::{AtomicI32, AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering}, Arc, Condvar, Mutex, @@ -50,7 +50,7 @@ use ton_api::{ deserialize_boxed, deserialize_typed, serialize_boxed, ton::{ adnl::{ - address::address::{Quic, Udp}, + address::address::Udp, addresslist::AddressList, id::short::Short as AdnlIdShort, message::message::{ @@ -75,15 +75,15 @@ macro_rules! adnl_node_test_key { format!( "{{ \"tag\": {}, - \"data\": {{ + \"data\": {{ \"type_id\": 1209251014, \"pvt_key\": \"{}\" }} }}", - $tag, $key - ) - .as_str() - }; + $tag, + $key + ).as_str() + } } #[macro_export] @@ -92,27 +92,28 @@ macro_rules! adnl_node_test_config { format!( "{{ \"ip_address\": \"{}\", - \"keys\": [ - {} + \"keys\": [ + {} ] }}", - $ip, $key - ) - .as_str() + $ip, + $key + ).as_str() }; ($ip: expr, $key1:expr, $key2:expr) => { format!( "{{ \"ip_address\": \"{}\", - \"keys\": [ - {}, - {} + \"keys\": [ + {}, + {} ] }}", - $ip, $key1, $key2 - ) - .as_str() - }; + $ip, + $key1, + $key2 + ).as_str() + } } /// ADNL addresses cache iterator @@ -629,43 +630,24 @@ impl AdnlChannel { struct AdnlNodeAddress { channel_key: Arc, - ip_version_address_adnl: AtomicPair, - ip_version_address_quic: AtomicPair, + ip_version_address: AtomicPair, key: Arc, } impl AdnlNodeAddress { - fn from_ip_addresses_and_key( - ip_address_adnl: &IpAddress, - ip_address_quic: Option<&IpAddress>, - key: &Arc, - ) -> Result { + fn from_ip_address_and_key(ip_address: &IpAddress, key: &Arc) -> Result { let ret = Self { channel_key: Ed25519KeyOption::generate()?, - ip_version_address_adnl: AtomicPair::new( - ip_address_adnl.version as u64, - ip_address_adnl.address, - ), - ip_version_address_quic: match ip_address_quic { - Some(q) => AtomicPair::new(q.version as u64, q.address), - None => AtomicPair::new(0, 0), - }, + ip_version_address: AtomicPair::new(ip_address.version as u64, ip_address.address), key: key.clone(), }; Ok(ret) } - fn update(&self, ip_address_adnl: &IpAddress, ip_address_quic: Option<&IpAddress>) -> bool { - if let Some(q) = ip_address_quic { - self.ip_version_address_quic.update( - q.version as u64, - q.address, - |old_version, new_version| old_version < new_version, - ); - } - self.ip_version_address_adnl.update( - ip_address_adnl.version as u64, - ip_address_adnl.address, + fn update(&self, ip_address: &IpAddress) -> bool { + self.ip_version_address.update( + ip_address.version as u64, + ip_address.address, |old_version, new_version| old_version < new_version, ) } @@ -674,7 +656,6 @@ impl AdnlNodeAddress { /// ADNL node configuration pub struct AdnlNodeConfig { ip_address: IpAddress, - ip_address_quic: Option, keys: lockfree::map::Map, Arc>, tags: lockfree::map::Map>, recv_pipeline_pool: Option, // %% of cpu cores to assign for recv workers @@ -696,8 +677,6 @@ struct AdnlNodeKeyJson { #[derive(serde::Deserialize, serde::Serialize)] pub struct AdnlNodeConfigJson { ip_address: String, - #[serde(default)] - ip_address_quic: Option, keys: Vec, recv_pipeline_pool: Option, // %% of cpu cores to assign for recv workers recv_priority_pool: Option, // %% of workers to assign for priority recv @@ -715,14 +694,6 @@ impl AdnlNodeConfigJson { IpAddress::from_versioned_string(&self.ip_address, None) } - /// Get QUIC IP address - pub fn ip_address_quic(&self) -> Result> { - self.ip_address_quic - .as_deref() - .map(|s| IpAddress::from_versioned_string(s, None)) - .transpose() - } - /// Get key by tag pub fn key_by_tag(&self, tag: usize, as_src: bool) -> Result> { for key in self.keys.iter() { @@ -747,14 +718,10 @@ impl AdnlNodeConfig { /// Construct from IP address and key data pub fn from_ip_address_and_keys( ip_address: &str, - ip_address_quic: Option<&str>, keys: Vec<(Arc, usize)>, ) -> Result { - let ip_address_quic = - ip_address_quic.map(|s| IpAddress::from_versioned_string(s, None)).transpose()?; let ret = AdnlNodeConfig { ip_address: IpAddress::from_versioned_string(ip_address, None)?, - ip_address_quic, keys: lockfree::map::Map::new(), tags: lockfree::map::Map::new(), recv_pipeline_pool: None, @@ -795,7 +762,6 @@ impl AdnlNodeConfig { pub fn from_json_config(json_config: &AdnlNodeConfigJson) -> Result { let ret = AdnlNodeConfig { ip_address: json_config.ip_address()?, - ip_address_quic: json_config.ip_address_quic()?, keys: lockfree::map::Map::new(), tags: lockfree::map::Map::new(), recv_pipeline_pool: json_config.recv_pipeline_pool, @@ -859,16 +825,6 @@ impl AdnlNodeConfig { self.ip_address.set_port(port) } - /// Set QUIC address - pub fn set_ip_address_quic(&mut self, addr: SocketAddr) { - if let IpAddr::V4(ipv4) = addr.ip() { - self.ip_address_quic = - Some(IpAddress::from_versioned_parts(u32::from(ipv4), addr.port(), None)); - } else { - log::warn!(target: TARGET, "IPv6 QUIC address {} ignored, only IPv4 is supported", addr); - } - } - /// Set worker pools pub fn set_recv_worker_pools( &mut self, @@ -917,7 +873,6 @@ impl AdnlNodeConfig { } let json = AdnlNodeConfigJson { ip_address: ip_address.to_string(), - ip_address_quic: None, keys: json_keys, recv_pipeline_pool: None, recv_priority_pool: None, @@ -928,7 +883,7 @@ impl AdnlNodeConfig { timeout_check_packet_processing_mcs: None, timeout_expire_queued_packet_sec: None, }; - Ok((json, Self::from_ip_address_and_keys(ip_address, None, tags_keys)?)) + Ok((json, Self::from_ip_address_and_keys(ip_address, tags_keys)?)) } fn delete_key(&self, key: &Arc, tag: usize) -> Result { @@ -2844,7 +2799,6 @@ impl AdnlNode { &self, local_key: &Arc, peer_ip_address: &IpAddress, - peer_ip_address_quic: Option<&IpAddress>, peer_key: &Arc, ) -> Result>> { if peer_key.id() == local_key { @@ -2856,17 +2810,14 @@ impl AdnlNode { self.peers(local_key)?.map_of.insert_with(ret.clone(), |key, inserted, found| { if let Some((_, found)) = found { ret = key.clone(); - found.address.update(peer_ip_address, peer_ip_address_quic); + found.address.update(peer_ip_address); lockfree::map::Preview::Discard } else if inserted.is_some() { ret = key.clone(); lockfree::map::Preview::Keep } else { - let address = AdnlNodeAddress::from_ip_addresses_and_key( - peer_ip_address, - peer_ip_address_quic, - peer_key, - ); + let address = + AdnlNodeAddress::from_ip_address_and_key(peer_ip_address, peer_key); match address { Ok(address) => { #[cfg(feature = "telemetry")] @@ -2922,17 +2873,11 @@ impl AdnlNode { Ok(Some(ret)) } - /// Build address list for given node. + /// Build address list for given node pub fn build_address_list(&self, expire_at: Option) -> Result { let version = Version::get(); - let mut addrs: Vec
= vec![self.config.ip_address.to_udp().into_boxed()]; - if let Some(quic_addr) = self.ip_address_quic() { - addrs.push( - Quic { ip: quic_addr.ip() as i32, port: quic_addr.port() as i32 }.into_boxed(), - ); - } let ret = AddressList { - addrs: addrs.into(), + addrs: vec![self.config.ip_address.to_udp().into_boxed()].into(), version, reinit_date: self.start_timestamp, priority: 0, @@ -2996,14 +2941,9 @@ impl AdnlNode { (self.options.load(Ordering::Relaxed) & Self::OPTION_MASK_TIMEOUT_CHANNEL_RESET_SEC) as u64 } - /// Node ADNL IP address - pub fn ip_address_adnl(&self) -> &IpAddress { - &self.config.ip_address - } - - /// Node QUIC IP address (None = not configured). - pub fn ip_address_quic(&self) -> Option<&IpAddress> { - self.config.ip_address_quic.as_ref() + /// Node IP address + pub fn ip_address(&self) -> &IpAddress { + self.config.ip_address() } /// Node key by ID @@ -3017,35 +2957,7 @@ impl AdnlNode { } /// Parse other's address list - pub fn parse_address_list( - list: &AddressList, - ) -> Result)>> { - fn parse_addr( - out: &mut Option, - kind: &str, - ip: i32, - port: i32, - version: i32, - ) -> bool { - if out.is_some() { - log::warn!(target: TARGET, "Duplicate {kind} address in address list"); - return false; - } - if ip == 0 { - log::warn!(target: TARGET, "{kind} address with zero IP in address list"); - return false; - } - if (port <= 0) || (port > 65535) { - log::warn!( - target: TARGET, - "{kind} address with invalid port {port} in address list" - ); - return false; - } - *out = Some(IpAddress::from_versioned_parts(ip as u32, port as u16, Some(version))); - true - } - + pub fn parse_address_list(list: &AddressList) -> Result> { if list.addrs.is_empty() { log::warn!(target: TARGET, "Address list is empty"); return Ok(None); @@ -3066,58 +2978,37 @@ impl AdnlNode { log::warn!(target: TARGET, "Address list is expired"); return Ok(None); } - let mut adnl_addr = None; - let mut quic_addr = None; - for addr in list.addrs.iter() { - match addr { - Address::Adnl_Address_Udp(x) => { - if !parse_addr(&mut adnl_addr, "ADNL", x.ip, x.port, list.version) { - return Ok(None); - } - } - Address::Adnl_Address_Quic(x) => { - if !parse_addr(&mut quic_addr, "QUIC", x.ip, x.port, list.version) { - return Ok(None); - } - } - _ => {} + match &list.addrs[0] { + Address::Adnl_Address_Udp(x) => { + let ret = + IpAddress::from_versioned_parts(x.ip as u32, x.port as u16, Some(list.version)); + Ok(Some(ret)) } - } - match adnl_addr { - Some(ip) => Ok(Some((ip, quic_addr))), - None => { - log::warn!(target: TARGET, "No IPv4 ADNL address in address list"); + _ => { + log::warn!(target: TARGET, "Only IPv4 address format is supported"); Ok(None) } } } - /// Get peer's ADNL and QUIC socket addresses if known + /// Get peer's socket address if known. pub fn peer_ip_address( &self, local_key: &Arc, peer_key: &Arc, - ) -> Result)>> { + ) -> Result> { let peers = self.peers(local_key)?; let Some(peer) = peers.map_of.get(peer_key) else { return Ok(None); }; - let (_, adnl_address) = peer.val().address.ip_version_address_adnl.get(); - if adnl_address == 0 { - return Ok(None); - } - let adnl_ip = (adnl_address >> 16) as u32; - let adnl_port = adnl_address as u16; - let adnl_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::from(adnl_ip)), adnl_port); - let (_, quic_address) = peer.val().address.ip_version_address_quic.get(); - let quic_addr = if quic_address != 0 { - let quic_ip = (quic_address >> 16) as u32; - let quic_port = quic_address as u16; - Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(quic_ip)), quic_port)) + let (_, address) = peer.val().address.ip_version_address.get(); + if address == 0 { + Ok(None) } else { - None - }; - Ok(Some((adnl_addr, quic_addr))) + let ip = (address >> 16) as u32; + let port = address as u16; + Ok(Some(SocketAddr::new(IpAddr::V4(ip.into()), port))) + } } /// Send query @@ -3286,10 +3177,9 @@ impl AdnlNode { })?; log::warn!(target: TARGET, "Resetting peer pair {} -> {}", local_key, other_key); let peer = peer.val(); - let (_, address) = peer.address.ip_version_address_adnl.get(); - let address = AdnlNodeAddress::from_ip_addresses_and_key( + let (_, address) = peer.address.ip_version_address.get(); + let address = AdnlNodeAddress::from_ip_address_and_key( &IpAddress { address, version: Version::get() }, - None, &peer.address.key, )?; peers @@ -3559,8 +3449,8 @@ impl AdnlNode { } else { Some(address.reinit_date) }; - if let Some((adnl_addr, quic_addr)) = Self::parse_address_list(address)? { - self.add_peer(local_key, &adnl_addr, quic_addr.as_ref(), &key)?; + if let Some(ip_address) = Self::parse_address_list(address)? { + self.add_peer(local_key, &ip_address, &key)?; (other_key, Cow::Borrowed("from-with-ip-address"), address_reinit_date, false) } else { let disposition = Cow::Owned(format!("from-without-ip-address {address:?}")); @@ -4588,7 +4478,7 @@ impl AdnlNode { .send(LoopbackData::Packet(data)) .map_err(|e| error!("Error when sending loopback ADNL packet: {e}"))? } else { - let (_, address) = peer.address.ip_version_address_adnl.get(); + let (_, address) = peer.address.ip_version_address.get(); let job = SendData { destination: address, data, method: method.clone() }; if method == AdnlSendMethodDetailed::FastUrgent { self.send_pipeline.put_urgent(SendJob::Data(job)) diff --git a/src/adnl/src/adnl/transport.rs b/src/adnl/src/adnl/transport.rs index d8cd3bf..909d15f 100644 --- a/src/adnl/src/adnl/transport.rs +++ b/src/adnl/src/adnl/transport.rs @@ -16,7 +16,7 @@ use std::{ io::{ErrorKind, Read}, net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{ - atomic::{AtomicU8, AtomicUsize, Ordering}, + atomic::{AtomicU8, Ordering}, mpsc::{channel, Receiver, Sender, TryRecvError}, Arc, }, @@ -114,8 +114,6 @@ impl Connections { pub(crate) struct SendQueue { buffer: lockfree::queue::Queue, sync: AtomicU8, - len: AtomicUsize, - capacity: usize, } impl SendQueue { @@ -124,15 +122,9 @@ impl SendQueue { const SYNC_CHECKING: u8 = 2; pub(crate) fn new() -> Arc { - Self::with_capacity(usize::MAX) - } - - pub(crate) fn with_capacity(capacity: usize) -> Arc { Arc::new(SendQueue { buffer: lockfree::queue::Queue::new(), sync: AtomicU8::new(Self::SYNC_INACTIVE), - len: AtomicUsize::new(0), - capacity, }) } @@ -155,32 +147,13 @@ impl SendQueue { } pub(crate) fn pop(&self) -> Option { - let item = self.buffer.pop(); - if item.is_some() { - self.len.fetch_sub(1, Ordering::Relaxed); - } - item + self.buffer.pop() } pub(crate) fn push(&self, data: Q) { - self.len.fetch_add(1, Ordering::Relaxed); self.buffer.push(data); } - /// Push data only if the queue has not reached its capacity. - /// Returns `true` if the data was enqueued, `false` if the queue is full. - pub(crate) fn try_push(&self, data: Q) -> bool { - if self.len.load(Ordering::Relaxed) >= self.capacity { - return false; - } - self.push(data); - true - } - - pub(crate) fn is_empty(&self) -> bool { - self.len.load(Ordering::Relaxed) == 0 - } - fn switch(&self, from: u8, to: u8) -> bool { self.sync.compare_exchange(from, to, Ordering::Relaxed, Ordering::Relaxed).is_ok() } diff --git a/src/adnl/src/dht/mod.rs b/src/adnl/src/dht/mod.rs index d42882f..4adce73 100644 --- a/src/adnl/src/dht/mod.rs +++ b/src/adnl/src/dht/mod.rs @@ -234,7 +234,7 @@ impl OverlayNodeResolveContext { } pub async fn resolve(&mut self, dht: &Arc) -> Result> { - Ok(dht.find_address(&mut self.search).await?.map(|(ip, _, _)| ip)) + Ok(dht.find_address(&mut self.search).await?.map(|(ip, _)| ip)) } } @@ -417,7 +417,7 @@ impl DhtNode { pub async fn fetch_address( &self, key_id: &Arc, - ) -> Result, Arc)>> { + ) -> Result)>> { let key = Self::dht_key_from_key_id(key_id, "address"); let value = self.network.search_dht_key(&hash(key)?); if let Some(value) = value { @@ -432,7 +432,7 @@ impl DhtNode { pub async fn find_address( self: &Arc, ctx_search: &mut AddressSearchContext, - ) -> Result, Arc)>> { + ) -> Result)>> { let mut addr_list = self .find_value( &ctx_search.key_id, @@ -641,7 +641,7 @@ impl DhtNode { /// Node IP address pub fn ip_address(&self) -> &IpAddress { - self.adnl.ip_address_adnl() + self.adnl.ip_address() } /// Node key @@ -666,7 +666,7 @@ impl DhtNode { pub async fn store_ip_address(self: &Arc, key: &Arc) -> Result { log::debug!(target: TARGET, "Storing key ID {}", key.id()); let addr_list = self.adnl.build_address_list(None)?; - let addrs = AdnlNode::parse_address_list(&addr_list)? + let addr = AdnlNode::parse_address_list(&addr_list)? .ok_or_else(|| error!("INTERNAL ERROR: cannot parse generated address list"))?; let value = serialize_boxed(&addr_list.into_boxed())?; let value = Self::sign_value("address", value, key)?; @@ -683,14 +683,17 @@ impl DhtNode { while let Some((_, object)) = objects.pop() { if let Ok(addr_list) = object.downcast::() { let addr_list = addr_list.only(); - if let Some(stored) = AdnlNode::parse_address_list(&addr_list)? { - if stored == addrs { - log::debug!(target: TARGET, "Checked stored address {stored:?}"); + if let Some(ip) = AdnlNode::parse_address_list(&addr_list)? { + if ip == addr { + //dht.adnl.ip_address() { + log::debug!(target: TARGET, "Checked stored address {:?}", ip); return Ok(true); } else { log::warn!( target: TARGET, - "Found another stored address {stored:?}, expected {addrs:?}" + "Found another stored address {:?}, expected {:?}", + ip, + self.adnl.ip_address() ) } } else { @@ -767,16 +770,13 @@ impl DhtNode { log::warn!(target: TARGET, "Error when verifying DHT peer: {}", e); return Ok(None); } - let (adnl_addr, quic_addr) = - if let Some(addrs) = AdnlNode::parse_address_list(&peer.addr_list)? { - addrs - } else { - log::warn!(target: TARGET, "Wrong DHT peer address {:?}", peer.addr_list); - return Ok(None); - }; - let peer_key: Arc = (&peer.id).try_into()?; - let ret = - self.adnl.add_peer(network.node_key.id(), &adnl_addr, quic_addr.as_ref(), &peer_key)?; + let addr = if let Some(addr) = AdnlNode::parse_address_list(&peer.addr_list)? { + addr + } else { + log::warn!(target: TARGET, "Wrong DHT peer address {:?}", peer.addr_list); + return Ok(None); + }; + let ret = self.adnl.add_peer(network.node_key.id(), &addr, &((&peer.id).try_into()?))?; let ret = if let Some(ret) = ret { ret } else { return Ok(None) }; if network.known_peers.all().put(ret.clone())? { let key1 = network.node_key.id().data(); @@ -964,13 +964,11 @@ impl DhtNode { fn parse_value_as_address( key: DhtKeyDescription, value: TLObject, - ) -> Result<(IpAddress, Option, Arc)> { + ) -> Result<(IpAddress, Arc)> { if let Ok(addr_list) = value.downcast::() { - let addr_list = addr_list.only(); - let (adnl_addr, quic_addr) = AdnlNode::parse_address_list(&addr_list)? + let ip_address = AdnlNode::parse_address_list(&addr_list.only())? .ok_or_else(|| error!("Wrong address list in DHT search"))?; - let peer_key: Arc = (&key.id).try_into()?; - Ok((adnl_addr, quic_addr, peer_key)) + Ok((ip_address, (&key.id).try_into()?)) } else { fail!("Address list type mismatch in DHT search") } diff --git a/src/adnl/src/overlay/broadcast.rs b/src/adnl/src/overlay/broadcast.rs index d754556..db622fb 100644 --- a/src/adnl/src/overlay/broadcast.rs +++ b/src/adnl/src/overlay/broadcast.rs @@ -94,7 +94,6 @@ impl BroadcastNeighbours { pub struct BroadcastRecvInfo { pub packets: u32, pub data: Vec, - pub extra: Option>, pub recv_from: Arc, } @@ -121,7 +120,7 @@ pub(crate) struct BroadcastSendContext<'a> { pub(crate) enum BroadcastSendMethod { Fast, - QuicOrRldp, + Rldp, Safe, } @@ -129,7 +128,7 @@ impl Display for BroadcastSendMethod { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { let msg = match self { Self::Fast => "datagram ADNL", - Self::QuicOrRldp => "QUIC/RLDP", + Self::Rldp => "RLDP", Self::Safe => "stream ADNL", }; write!(f, "{msg}") @@ -213,7 +212,6 @@ declare_counted!( data_hash: [u8; 32], date: i32, encoder: RaptorqEncoder, - extra: Vec, flags: u32, seqno: u32, src_key: Arc, @@ -343,16 +341,12 @@ pub(crate) trait BroadcastProtocol: return Ok(()); } let info = self.check_broadcast(&bcast, &ctx)?; - let bcast_id = base64_encode(&info.bcast_id); if info.dup { + let bcast_id = base64_encode(&info.bcast_id); log::info!(target: TARGET, "Received duplicated {bcast_type} broadcast {bcast_id}"); return Ok(()); }; - log::trace!( - target: TARGET, - "Received {bcast_type} broadcast {bcast_id}, {} bytes", - info.data_len - ); + log::trace!(target: TARGET, "Received {bcast_type} broadcast, {} bytes", info.data_len); #[cfg(feature = "telemetry")] let tag = info.maybe_tag.unwrap_or(bcast.default_tag()); #[cfg(feature = "telemetry")] @@ -534,9 +528,6 @@ pub(crate) trait BroadcastProtocol: pub(crate) trait FecBroadcastParsed: BroadcastParsed { fn data_hash(&self) -> &[u8; 32]; fn data_size(&self) -> usize; - fn extra(&self) -> Option<&[u8]> { - None - } fn fec_type(&self) -> Option; fn part_data(&self) -> &[u8]; fn seqno(&self) -> u32; @@ -578,7 +569,6 @@ trait FecProtocol: BroadcastProtocol tokio::spawn(async move { let mut received = false; let mut packets = 0; - let mut extra: Option> = None; #[cfg(feature = "telemetry")] let mut flags = RecvTransferFecTelemetry::FLAG_RECEIVE_STARTED; #[cfg(feature = "telemetry")] @@ -589,9 +579,6 @@ trait FecProtocol: BroadcastProtocol Some(bcast) => bcast, None => break, }; - if extra.is_none() { - extra = bcast.extra().map(|extra| extra.to_vec()); - } packets += 1; let Some(fec_type) = bcast.fec_type() else { log::warn!( @@ -635,7 +622,6 @@ trait FecProtocol: BroadcastProtocol overlay.received_rawbytes.push(BroadcastRecvInfo { packets, data, - extra, recv_from: src_key_id.clone(), }); received = true; @@ -943,12 +929,10 @@ trait BroadcastTwostep { fn calc_broadcast_id( data_hash: [u8; 32], date: i32, - data_size: usize, part_size: usize, src_key: &Arc, src_adnl_key_id: &Arc, flags: u32, - extra: &[u8], ) -> Result { let bcast_id = BroadcastTwostepId { date, @@ -956,9 +940,7 @@ trait BroadcastTwostep { src: UInt256::from_slice(src_key.id().data()), src_adnl_id: UInt256::from_slice(src_adnl_key_id.data()), data_hash: UInt256::with_array(data_hash), - data_size: data_size as i32, part_size: part_size as i32, - extra: extra.to_vec(), }; hash(bcast_id) } @@ -966,19 +948,15 @@ trait BroadcastTwostep { fn calc_broadcast_id_when_send( ctx: &BroadcastSendContext, date: i32, - data_size: usize, part_size: usize, - extra: &[u8], ) -> Result<(BroadcastId, bool)> { let bcast_id = Self::calc_broadcast_id( sha256_digest(ctx.data.object), date, - data_size, part_size, &ctx.src_key, &ctx.src_adnl_key_id, ctx.flags, - extra, )?; Ok((bcast_id, true)) } @@ -1179,7 +1157,6 @@ impl BroadcastProtocol for BroadcastFecProtocol { data_hash: sha256_digest(ctx.data.object), date, encoder, - extra: Vec::new(), flags: ctx.flags, seqno: 0, src_key: ctx.src_key.clone(), @@ -1417,7 +1394,6 @@ impl BroadcastSimpleProtocol { let info = BroadcastRecvInfo { packets: 1, data: bcast.data.into(), - extra: None, recv_from: src_key.id().clone(), }; Ok((Some(info), true)) @@ -1658,7 +1634,6 @@ impl FecBroadcastParsed for BroadcastTwostepFec { symbol_size, }) } - fn extra(&self) -> Option<&[u8]> { Some(&self.extra) } fn part_data(&self) -> &[u8] { &self.part } fn seqno(&self) -> u32 { self.seqno as u32 } fn signature(&self) -> &[u8] { &self.signature } @@ -1670,7 +1645,6 @@ struct BroadcastTwostepSendContext { } pub(crate) struct BroadcastTwostepFecProtocol { - extra: Option>, send_ctx: Option, } @@ -1678,10 +1652,10 @@ impl BroadcastTwostepFecProtocol { const MAX_PART_SIZE: usize = 65536; pub(crate) fn for_recv() -> Self { - Self { extra: None, send_ctx: None } + Self { send_ctx: None } } - pub(crate) fn for_send(data: &[u8], neighbours: u32, extra: Vec) -> Result { + pub(crate) fn for_send(data: &[u8], neighbours: u32) -> Result { if neighbours <= 3 { fail!("Not enough neighbours to build {} broadcast", Self::broadcast_type()); } @@ -1694,7 +1668,7 @@ impl BroadcastTwostepFecProtocol { fail!("Too big part size {part_size} in {} broadcast", Self::broadcast_type()); } let ctx = BroadcastTwostepSendContext { neighbours, part_size }; - Ok(Self { extra: Some(extra), send_ctx: Some(ctx) }) + Ok(Self { send_ctx: Some(ctx) }) } } @@ -1726,7 +1700,7 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { } fn send_method(&self) -> BroadcastSendMethod { - BroadcastSendMethod::QuicOrRldp + BroadcastSendMethod::Rldp } // Receive side @@ -1742,12 +1716,10 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { bcast_id: ::calc_broadcast_id( *bcast.data_hash.as_slice(), bcast.date, - bcast.data_size as usize, bcast.part.len(), &src_key, &src_adnl_key_id, bcast.flags as u32, - &bcast.extra, )?, dup: false, data_len: bcast.part.len(), @@ -1776,13 +1748,7 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { let Some(send_ctx) = self.send_ctx.as_ref() else { fail!("No send context set for {} broadcast", Self::broadcast_type()); }; - ::calc_broadcast_id_when_send( - ctx, - date, - ctx.data.object.len(), - send_ctx.part_size, - self.extra.as_deref().unwrap_or_default(), - ) + ::calc_broadcast_id_when_send(ctx, date, send_ctx.part_size) } fn build_broadcast( @@ -1820,7 +1786,6 @@ impl BroadcastProtocol for BroadcastTwostepFecProtocol { data_hash: sha256_digest(ctx.data.object), date, encoder: RaptorqEncoder::with_data(ctx.data.object, Some(send_ctx.part_size as u16)), - extra: self.extra.take().unwrap_or_default(), flags: ctx.flags, seqno: 0, src_key: ctx.src_key.clone(), @@ -1905,7 +1870,6 @@ impl FecProtocol for BroadcastTwostepFecProtocol { data_size: transfer.encoder.params().data_size as i32, seqno: transfer.seqno as i32, part: data, - extra: transfer.extra.clone(), signature, } .into_boxed()) @@ -1913,13 +1877,14 @@ impl FecProtocol for BroadcastTwostepFecProtocol { fn calc_to_sign( bcast_id: &BroadcastId, - _data_size: usize, + data_size: usize, part_data: &[u8], seqno: u32, _date: i32, ) -> Result> { let to_sign = BroadcastTwostepFecToSign { id: UInt256::from_slice(bcast_id), + data_size: data_size as i32, seqno: seqno as i32, part: part_data.to_vec(), }; @@ -1952,15 +1917,11 @@ impl BroadcastParsed for BroadcastTwostepSimple { pub(crate) struct BroadcastTwostepSimpleProtocol { big_data: bool, - extra: Option>, } impl BroadcastTwostepSimpleProtocol { - pub(crate) fn for_recv(big_data: bool) -> Self { - Self { big_data, extra: None } - } - pub(crate) fn for_send(big_data: bool, extra: Vec) -> Self { - Self { big_data, extra: Some(extra) } + pub(crate) fn new(big_data: bool) -> Self { + Self { big_data } } fn calc_to_sign(bcast_id: BroadcastId, data: &[u8]) -> Result> { let to_sign = @@ -1998,7 +1959,7 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco fn send_method(&self) -> BroadcastSendMethod { if self.big_data { - BroadcastSendMethod::QuicOrRldp + BroadcastSendMethod::Rldp } else { BroadcastSendMethod::Fast } @@ -2012,18 +1973,15 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco ctx: &BroadcastRecvContext, ) -> Result { let data_hash = sha256_digest(&bcast.data); - let data_size = bcast.data.len(); let src_key: Arc = (&bcast.src).try_into()?; let src_adnl_key_id = KeyId::from_data(*bcast.src_adnl_id.as_slice()); let bcast_id = ::calc_broadcast_id( data_hash, bcast.date, - data_size, - data_size, + bcast.data.len(), &src_key, &src_adnl_key_id, bcast.flags as u32, - &bcast.extra, )?; let dup = if add_unbound_object_to_map(&ctx.overlay.owned_broadcasts, bcast_id, || { Ok(OwnedBroadcast::Send) @@ -2058,12 +2016,8 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco ) -> Result<(Option, bool)> { let src_adnl_key_id = KeyId::from_data(*bcast.src_adnl_id.as_slice()); let resend = ctx.peers.other() == &src_adnl_key_id; - let info = BroadcastRecvInfo { - packets: 1, - data: bcast.data.into(), - extra: Some(bcast.extra), - recv_from: src_adnl_key_id, - }; + let info = + BroadcastRecvInfo { packets: 1, data: bcast.data.into(), recv_from: src_adnl_key_id }; Ok((Some(info), resend)) } @@ -2074,14 +2028,7 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco ctx: &BroadcastSendContext, date: i32, ) -> Result<(BroadcastId, bool)> { - let data_size = ctx.data.object.len(); - ::calc_broadcast_id_when_send( - ctx, - date, - data_size, - data_size, - self.extra.as_deref().unwrap_or_default(), - ) + ::calc_broadcast_id_when_send(ctx, date, ctx.data.object.len()) } fn build_broadcast( @@ -2102,7 +2049,6 @@ impl BroadcastProtocol for BroadcastTwostepSimpleProtoco src_adnl_id: UInt256::from_slice(ctx.src_adnl_key_id.data()), certificate: OverlayCertificate::Overlay_EmptyCertificate, data, - extra: self.extra.take().unwrap_or_default(), signature, } .into_boxed(), diff --git a/src/adnl/src/overlay/mod.rs b/src/adnl/src/overlay/mod.rs index e1d855f..0707b31 100644 --- a/src/adnl/src/overlay/mod.rs +++ b/src/adnl/src/overlay/mod.rs @@ -35,8 +35,8 @@ use std::{ time::{Duration, Instant}, }; use ton_api::{ - deserialize_boxed, deserialize_boxed_bundle_with_suffix, deserialize_boxed_with_suffix, - serialize_boxed, serialize_boxed_append, + deserialize_boxed_bundle_with_suffix, deserialize_boxed_with_suffix, serialize_boxed, + serialize_boxed_append, ton::{ adnl::id::short::Short as AdnlShortId, catchain::{ @@ -353,10 +353,7 @@ struct SlaveInfo { enum OverlayType { Public, // Overlay with fixed members set - Private { - key: Arc, - use_quic: bool, - }, + Private(Arc), // Overlay with externally certified members CertifiedMembers { key: Option>, @@ -372,13 +369,15 @@ enum OverlayType { impl OverlayType { fn bcast_prefix(&self) -> Option> { match self { + OverlayType::Public => None, + OverlayType::Private(_) => None, OverlayType::CertifiedMembers { bcast_prefix, .. } => Some(bcast_prefix.clone()), - OverlayType::Public | OverlayType::Private { .. } => None, } } fn calc_message_prefix(&self, overlay_id: &OverlayShortId) -> Result> { match self { + Self::Public | Self::Private(_) => OverlayUtils::calc_message_prefix(overlay_id), Self::CertifiedMembers { certificate, .. } => serialize_boxed( &OverlayMessageWithExtra { overlay: UInt256::with_array(*overlay_id.data()), @@ -388,30 +387,27 @@ impl OverlayType { } .into_boxed(), ), - OverlayType::Public | OverlayType::Private { .. } => { - OverlayUtils::calc_message_prefix(overlay_id) - } } } fn calc_query_prefix(&self, overlay_id: &OverlayShortId) -> Result> { match self { + Self::Public | Self::Private(_) => { + serialize_boxed(&OverlayQuery { overlay: UInt256::with_array(*overlay_id.data()) }) + } Self::CertifiedMembers { certificate, .. } => serialize_boxed(&OverlayQueryWithExtra { overlay: UInt256::with_array(*overlay_id.data()), extra: MessageExtra { certificate: certificate.as_ref().map(|cert| cert.clone().into_boxed()), }, }), - OverlayType::Public | OverlayType::Private { .. } => { - serialize_boxed(&OverlayQuery { overlay: UInt256::with_array(*overlay_id.data()) }) - } } } fn certificate(&self) -> Option<&MemberCertificate> { match self { + OverlayType::Public | OverlayType::Private(_) => None, OverlayType::CertifiedMembers { certificate, .. } => certificate.as_ref(), - OverlayType::Public | OverlayType::Private { .. } => None, } } @@ -470,7 +466,6 @@ declare_counted!( declare_counted!( struct Overlay { adnl: Arc, - quic: Option>, rldp: Option>, options: Arc, overlay_type: OverlayType, @@ -615,8 +610,8 @@ impl Overlay { fn check_peer(&self, peer: &Arc, certificate: Option<&MemberCertificate>) -> Result<()> { match &self.overlay_type { OverlayType::Public => Ok(()), - OverlayType::Private { key, .. } => { - if !(peer == key.id() || self.known_peers.all().contains(peer)) { + OverlayType::Private(own_key) => { + if !(peer == own_key.id() || self.known_peers.all().contains(peer)) { fail!("Peer {peer} is not a member of the private overlay {}", self.overlay_id) } Ok(()) @@ -702,19 +697,11 @@ impl Overlay { BroadcastSendMethod::Fast => { self.adnl.send_custom_get_status(data, peers, AdnlSendMethod::Fast).await.err() } - BroadcastSendMethod::QuicOrRldp => { - if let Some(quic) = self.quic.as_ref() { - quic.message(data.object.to_vec(), Some(&self.adnl), peers).await.err() - } else { - let Some(_rldp) = self.rldp.as_ref() else { - fail!( - "Neither QUIC nor RLDP sender is set in overlay {}", - self.overlay_id - ); - }; - //rldp.message(data, peers, true, None).await.err() - None - } + BroadcastSendMethod::Rldp => { + let Some(rldp) = self.rldp.as_ref() else { + fail!("No RLDP sender is set in overlay {}", self.overlay_id); + }; + rldp.message(data, peers, true, None).await.err() } BroadcastSendMethod::Safe => { self.adnl.send_custom_get_status(data, peers, AdnlSendMethod::Safe).await.err() @@ -826,7 +813,7 @@ impl Overlay { fn overlay_key(&self) -> Option<&Arc> { match &self.overlay_type { - OverlayType::Private { key, .. } => Some(key), + OverlayType::Private(key) => Some(key), OverlayType::CertifiedMembers { key, .. } => key.as_ref(), _ => None, } @@ -1433,9 +1420,8 @@ impl OverlayNode { params: OverlayParams, overlay_key: &Arc, peers: &[Arc], - use_quic: bool, ) -> Result { - let overlay_type = OverlayType::Private { key: overlay_key.clone(), use_quic }; + let overlay_type = OverlayType::Private(overlay_key.clone()); self.add_typed_private_overlay(overlay_type, params, peers) } @@ -1466,11 +1452,11 @@ impl OverlayNode { pub fn add_private_peers( &self, local_adnl_key: &Arc, - peers: Vec<(IpAddress, Option, Arc)>, + peers: Vec<(IpAddress, Arc)>, ) -> Result>> { let mut ret = Vec::new(); - for (ip, quic_ip, key) in peers { - if let Some(peer) = self.adnl.add_peer(local_adnl_key, &ip, quic_ip.as_ref(), &key)? { + for (ip, key) in peers { + if let Some(peer) = self.adnl.add_peer(local_adnl_key, &ip, &key)? { ret.push(peer) } } @@ -1517,7 +1503,7 @@ impl OverlayNode { log::warn!(target: TARGET, "Error when verifying overlay peer {}: {e}", key.id()); return Ok(None); } - let Some(ret) = self.adnl.add_peer(self.node_key.id(), peer_ip_address, None, &key)? else { + let Some(ret) = self.adnl.add_peer(self.node_key.id(), peer_ip_address, &key)? else { return Ok(None); }; overlay.known_peers.add(&ret)?; @@ -1608,13 +1594,12 @@ impl OverlayNode { } /// Two-step message broadcast - pub async fn broadcast_twostep( + pub async fn broadcast_two_step( &self, overlay_id: &Arc, data: &TaggedByteSlice<'_>, src_key: Option<&Arc>, flags: u32, - extra: Vec, ) -> Result { log::trace!(target: TARGET, "Two-step broadcast {} bytes", data.object.len()); let overlay = @@ -1629,9 +1614,9 @@ impl OverlayNode { let neighbours = overlay.calc_broadcast_twostep_neighbours(); let big_data = data.object.len() >= Self::MIN_BYTES_FEC_TWO_STEPS_BROADCAST; if big_data && (neighbours >= Self::MIN_NODES_FEC_TWO_STEPS_BROADCAST) { - BroadcastTwostepFecProtocol::for_send(data.object, neighbours, extra)?.send(ctx).await + BroadcastTwostepFecProtocol::for_send(data.object, neighbours)?.send(ctx).await } else { - BroadcastTwostepSimpleProtocol::for_send(big_data, extra).send(ctx).await + BroadcastTwostepSimpleProtocol::new(big_data).send(ctx).await } } @@ -1783,7 +1768,7 @@ impl OverlayNode { "Sending QUIC message to unknown overlay", ) .await?; - quic.message(data, Some(&self.adnl), &peers).await?; + quic.message(data, Some(&self.adnl), peers.local(), peers.other()).await?; Ok(()) } @@ -1864,10 +1849,7 @@ impl OverlayNode { .await?; let mut data = overlay.query_prefix.clone(); serialize_boxed_append(&mut data, &query.object)?; - match quic.query(data, Some(&self.adnl), &peers, timeout_ms).await? { - Some(raw) => Ok(Some(deserialize_boxed(&raw)?)), - None => Ok(None), - } + quic.query(data, Some(&self.adnl), peers.local(), peers.other(), timeout_ms).await } /// Send query via RLDP @@ -1977,15 +1959,9 @@ impl OverlayNode { penalty: 1, to_block: Self::MAX_FAIL_COUNT, }; - let quic = if let OverlayType::Private { use_quic: true, .. } = &overlay_type { - self.quic.get().cloned() - } else { - None - }; let overlay = Overlay { adnl: self.adnl.clone(), rldp: self.rldp.get().cloned(), - quic, flags: params.flags, hops: params.hops, known_peers: AddressCacheWithBads::with_params(Self::MAX_PEERS, policy), @@ -2160,7 +2136,7 @@ impl OverlayNode { fn delete_overlay(&self, overlay_id: &Arc, is_private: bool) -> Result { let type_of = if is_private { "private" } else { "public" }; - log::info!(target: TARGET, "Delete {} overlay {}", type_of, overlay_id); + log::debug!(target: TARGET, "Delete {} overlay {}", type_of, overlay_id); if let Some(overlay) = self.overlays.get(overlay_id) { let overlay = overlay.val(); if is_private { @@ -2407,7 +2383,7 @@ impl Subscriber for OverlayNode { } Ok(Broadcast::Overlay_BroadcastTwostepSimple(bcast)) => { let big_data = bcast.data.len() >= Self::MIN_BYTES_FEC_TWO_STEPS_BROADCAST; - BroadcastTwostepSimpleProtocol::for_recv(big_data).recv(bcast, ctx).await?; + BroadcastTwostepSimpleProtocol::new(big_data).recv(bcast, ctx).await?; return Ok(true); } Ok(bcast) => fail!("Unsupported overlay broadcast message {:?}", bcast), diff --git a/src/adnl/src/quic/mod.rs b/src/adnl/src/quic/mod.rs index 28f3a09..470f513 100644 --- a/src/adnl/src/quic/mod.rs +++ b/src/adnl/src/quic/mod.rs @@ -15,23 +15,19 @@ use crate::{ transport::{Connections, SendQueue}, }; use std::{ - collections::{HashMap, HashSet}, fmt, net::SocketAddr, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, Mutex, Once, Weak, - }, + sync::{Arc, Once}, time::Duration, }; use ton_api::{ - deserialize_boxed, deserialize_boxed_with_suffix, serialize_boxed, + deserialize_boxed, serialize_boxed, ton::quic::{ answer::Answer as QuicAnswer, request::{Message as QuicMessage, Query as QuicQuery}, Request, Response as QuicResponse, }, - IntoBoxed, + IntoBoxed, TLObject, }; use ton_block::{ ed25519_encode_private_key_to_pkcs8, error, fail, Ed25519KeyOption, KeyId, Result, @@ -39,14 +35,6 @@ use ton_block::{ const TARGET: &str = "quic"; -/// Key for the QUIC inbound connection map: (local_key_id, peer_key_id). -/// Matches the C++ `AdnlPath{local_id, peer_id}` semantics so that two -/// connections from the same peer address but different key pairs (e.g. -/// current + next validator keys) coexist instead of evicting each other. -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -struct QuicInboundKey(Arc, Arc); - -type QuicInboundMap = lockfree::map::Map; type QuicSendQueue = SendQueue>; /// Extract a `KeyId` from an Ed25519 SubjectPublicKeyInfo (SPKI) DER blob. @@ -68,19 +56,6 @@ fn key_id_from_spki(spki: &[u8]) -> Result> { struct QuicOutboundConnection { conn: Option, send_queue: Arc, - sender_state: Arc, -} - -/// Per-peer sender lifecycle guard. Uses an atomic flag to ensure exactly -/// one sender task runs per outbound peer. -struct SenderState { - active: AtomicBool, -} - -impl SenderState { - fn new() -> Arc { - Arc::new(Self { active: AtomicBool::new(false) }) - } } /// Presents a single fixed Ed25519 SPKI (RPK) as the client certificate. @@ -180,13 +155,13 @@ struct QuicServerCertResolver { /// Most recently registered identity name. Used as SNI fallback when the client /// (e.g. C++ ngtcp2) doesn't send SNI, matching C++ SO_REUSEADDR behavior where /// the last-bound socket receives packets. - last_added_name: Arc>>, + last_added_name: Arc>>, } impl QuicServerCertResolver { fn new( keys: Arc>>, - last_added_name: Arc>>, + last_added_name: Arc>>, ) -> Arc { Arc::new(Self { keys, last_added_name }) } @@ -340,19 +315,7 @@ struct EndpointState { server_cert_keys: Arc>>, local_key_names: Arc>>, /// Tracks the most recently added identity name for SNI fallback. - last_added_name: Arc>>, -} - -/// Command sent to the background Tokio task that manages QUIC key operations. -/// Quinn endpoint creation requires a Tokio runtime context, but callers may run -/// on bare OS threads (e.g. Simplex SXMAIN). The channel decouples the two. -enum KeyCommand { - AddKey { - key: [u8; Ed25519KeyOption::PVT_KEY_SIZE], - key_id: Arc, - bind_addr: SocketAddr, - reply: tokio::sync::oneshot::Sender>, - }, + last_added_name: Arc>>, } pub struct QuicNode { @@ -360,131 +323,30 @@ pub struct QuicNode { /// One entry per local identity; each carries its own client config and outbound pool. local_keys: lockfree::map::Map, Arc>, /// One endpoint per unique bind port. Endpoints are created lazily by `add_key()`. - endpoints: Mutex>>, + endpoints: std::sync::Mutex>>, /// Shared subscriber list for all accept loops. subscribers: Arc>>, peer_keys: lockfree::map::Map, SocketAddr>, /// Max concurrent in-flight streams per inbound connection. max_streams_per_connection: usize, - /// Inbound connection maps, one per endpoint/accept-loop. Used by the stats dumper. - inbound_pools: Mutex>>, - /// Per-TL-tag message counters for the stats dumper. - msg_stats: Arc, - /// Channel for dispatching key operations to a Tokio-hosted background task. - key_cmd_tx: tokio::sync::mpsc::UnboundedSender, } impl QuicNode { pub const OFFSET_PORT: u16 = 1000; + const DEFAULT_QUERY_TIMEOUT_MS: u64 = 5000; /// How often the background checker scans outbound connections for dead ones. - const CONNECTION_CHECK_INTERVAL: Duration = Duration::from_secs(5); - /// How often the stats dumper logs connection statistics. - const STATS_DUMP_INTERVAL: Duration = Duration::from_secs(60); - const DEFAULT_MAX_STREAMS_PER_CONNECTION: usize = 256; - const DEFAULT_QUERY_TIMEOUT_MS: u64 = 5000; - /// Maximum number of messages buffered per outbound peer - const SEND_QUEUE_CAPACITY: usize = 1024; + const CONNECTION_CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); - /// Create a new QuicNode. No endpoints are bound โ€” they are created lazily - /// by `add_key()` when the first identity for a given port is registered. - /// - /// `runtime_handle` is used to spawn a background task that processes key - /// operations requiring Tokio context (Quinn endpoint creation). - pub fn new( - subscribers: Vec>, - cancellation_token: tokio_util::sync::CancellationToken, - max_streams_per_connection: Option, - runtime_handle: tokio::runtime::Handle, - ) -> Arc { - let max_streams_per_connection = - max_streams_per_connection.unwrap_or(Self::DEFAULT_MAX_STREAMS_PER_CONNECTION); - static CRYPTO_INIT: Once = Once::new(); - CRYPTO_INIT.call_once(|| { - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install default Rustls CryptoProvider"); - }); - let (key_cmd_tx, mut key_cmd_rx) = tokio::sync::mpsc::unbounded_channel::(); - let transport = Arc::new(Self { - cancellation_token: cancellation_token.clone(), - local_keys: lockfree::map::Map::new(), - endpoints: Mutex::new(HashMap::new()), - subscribers: Arc::new(subscribers), - peer_keys: lockfree::map::Map::new(), - max_streams_per_connection, - inbound_pools: Mutex::new(Vec::new()), - msg_stats: MsgStats::new(), - key_cmd_tx, - }); - // Spawn background task that processes key commands inside the Tokio runtime. - let weak = Arc::downgrade(&transport); - let token = cancellation_token.clone(); - runtime_handle.spawn(async move { - loop { - tokio::select! { - cmd = key_cmd_rx.recv() => { - let Some(cmd) = cmd else { break }; - match cmd { - KeyCommand::AddKey { key, key_id, bind_addr, reply } => { - let result = if let Some(this) = weak.upgrade() { - this.add_key_inner(&key, &key_id, bind_addr) - } else { - Err(error!("QuicNode dropped")) - }; - let _ = reply.send(result); - } - } - } - _ = token.cancelled() => break, - } - } - }); - Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token.clone()); - Self::spawn_stats_dumper(Arc::downgrade(&transport), cancellation_token); - transport - } + const DEFAULT_MAX_STREAMS_PER_CONNECTION: usize = 256; /// Register a local identity on a specific bind address. /// Creates a new endpoint if one doesn't exist for this port yet. - /// - /// Safe to call from any thread โ€” the actual work is dispatched to a - /// Tokio-hosted background task via an internal channel. pub fn add_key( &self, key: &[u8; Ed25519KeyOption::PVT_KEY_SIZE], key_id: &Arc, bind_addr: SocketAddr, - ) -> Result<()> { - let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); - self.key_cmd_tx - .send(KeyCommand::AddKey { - key: *key, - key_id: key_id.clone(), - bind_addr, - reply: reply_tx, - }) - .map_err(|_| error!("QuicNode key command channel closed"))?; - // Use blocking_recv on bare OS threads, tokio::block_in_place on Tokio workers. - match tokio::runtime::Handle::try_current() { - Ok(_) => tokio::task::block_in_place(|| { - reply_rx - .blocking_recv() - .map_err(|_| error!("QuicNode key command reply channel dropped"))? - }), - Err(_) => reply_rx - .blocking_recv() - .map_err(|_| error!("QuicNode key command reply channel dropped"))?, - } - } - - /// Internal implementation of add_key โ€” always runs inside the Tokio runtime - /// (called by the background key-command task). - fn add_key_inner( - &self, - key: &[u8; Ed25519KeyOption::PVT_KEY_SIZE], - key_id: &Arc, - bind_addr: SocketAddr, ) -> Result<()> { let key_bytes = ed25519_encode_private_key_to_pkcs8(key)?; let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_bytes) @@ -511,9 +373,10 @@ impl QuicNode { // Match server-side timeouts so both ends agree on connection liveness. let mut client_transport = quinn::TransportConfig::default(); client_transport.max_idle_timeout(Some( - quinn::IdleTimeout::try_from(Duration::from_secs(15)).expect("15s fits in IdleTimeout"), + quinn::IdleTimeout::try_from(std::time::Duration::from_secs(15)) + .expect("15s fits in IdleTimeout"), )); - client_transport.keep_alive_interval(Some(Duration::from_secs(5))); + client_transport.keep_alive_interval(Some(std::time::Duration::from_secs(5))); quinn_client_config.transport_config(Arc::new(client_transport)); let local_key_state = Arc::new(LocalKeyState { @@ -574,114 +437,79 @@ impl QuicNode { self: &Arc, data: Vec, adnl: Option<&AdnlNode>, - peers: &AdnlPeers, + src: &Arc, + dst: &Arc, ) -> Result> { - self.ensure_peer_registered(adnl, peers)?; - let tag = extract_inner_tag(&data); - let size = data.len(); + self.ensure_peer_registered(adnl, src, dst)?; let data = serialize_boxed(&QuicMessage { data: data.into() }.into_boxed())?; - let addr = self.addr_by_key(peers.other())?; - let state = self.local_key_state(peers.local())?; - let outbound = Self::get_or_create_outbound_connection(&state.outbound, addr)?; - - // Fast path: if connection is alive, send directly without queue overhead - if let Some(ref conn) = outbound.conn { - match Self::send_via_stream(conn, &data).await { - Ok(_) => { - self.msg_stats.record(tag, size, addr, true, false); - return Ok(Some(data.len())); - } - Err(e) => { - log::warn!( - target: TARGET, - "QUIC direct send to {} failed: {e}, removing dead connection, \ - falling back to queue", - peers.other() - ); - Self::remove_dead_connection(&state.outbound, addr, conn); + match self.get_outbound_connection(src, dst, true).await? { + QuicOutboundConnection { conn: Some(ref conn), ref send_queue } => { + if send_queue.check(true) { + send_queue.push(data); + while !send_queue.check(false) { + tokio::task::yield_now().await; + } + } else { + let len = data.len(); + if let Err(e) = Self::send_via_stream(conn, &data).await { + log::warn!( + target: TARGET, + "QUIC send_message to {dst} failed: {e}, removing dead connection" + ); + let addr = self.addr_by_key(dst)?; + let state = self.local_key_state(src)?; + Self::remove_dead_connection(&state.outbound, addr, conn); + return Err(e); + } + return Ok(Some(len)); } } + QuicOutboundConnection { conn: None, ref send_queue } => send_queue.push(data), } - - // Slow path: no connection (or it just died) โ€” enqueue for the sender task - // which will establish the connection and deliver - if !outbound.send_queue.try_push(data) { - fail!("QUIC send queue full for peer {}", peers.other()); - } - self.msg_stats.record(tag, size, addr, true, false); - - // Spawn sender task if not already running (CAS guarantees at most one per peer) - if outbound - .sender_state - .active - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - let quic = self.clone(); - let send_queue = outbound.send_queue.clone(); - let sender_state = outbound.sender_state.clone(); - let outbound_conns = state.outbound.clone(); - let server_name = Self::key_id_to_server_name(peers.other()); - - spawn_cancelable( - self.cancellation_token.clone(), - Self::run_sender_task( - quic, - peers.clone(), - addr, - server_name, - send_queue, - sender_state, - outbound_conns, - ), - ); - } - Ok(None) } + /// Create a new QuicNode. No endpoints are bound โ€” they are created lazily + /// by `add_key()` when the first identity for a given port is registered. + pub fn new( + subscribers: Vec>, + cancellation_token: tokio_util::sync::CancellationToken, + ) -> Arc { + Self::with_stream_limit( + subscribers, + cancellation_token, + Self::DEFAULT_MAX_STREAMS_PER_CONNECTION, + ) + } + pub async fn query( self: &Arc, data: Vec, adnl: Option<&AdnlNode>, - peers: &AdnlPeers, + src: &Arc, + dst: &Arc, timeout_ms: Option, - ) -> Result>> { - self.ensure_peer_registered(adnl, peers)?; - let addr = self.addr_by_key(peers.other())?; - let tag = extract_inner_tag(&data); - let size = data.len(); + ) -> Result> { + self.ensure_peer_registered(adnl, src, dst)?; let timeout_ms = timeout_ms.unwrap_or(Self::DEFAULT_QUERY_TIMEOUT_MS); let wire = serialize_boxed(&QuicQuery { data: data.into() }.into_boxed())?; - let response = self.send_query_raw(wire, peers, timeout_ms).await?; - self.msg_stats.record(tag, size, addr, true, true); + let response = self.send_query_raw(wire, src, dst, timeout_ms).await?; if response.is_empty() { return Ok(None); } let obj = deserialize_boxed(&response) .map_err(|e| error!("Cannot deserialise QUIC answer: {e}"))?; match obj.downcast::() { - Ok(QuicResponse::Quic_Answer(answer)) => Ok(Some(answer.data.to_vec())), + Ok(QuicResponse::Quic_Answer(answer)) => Ok(Some( + deserialize_boxed(&answer.data) + .map_err(|e| error!("Cannot deserialise QUIC answer payload: {e}"))?, + )), Err(x) => fail!("Unexpected QUIC response type {x:?}"), } } - /// Shut down all QUIC endpoints, cancel background tasks, and release resources. + /// Shut down all QUIC endpoints. pub fn shutdown(&self) { - // Cancel all background tasks (accept loops, connection checkers, drain tasks) - self.cancellation_token.cancel(); - - // Close all outbound connections in every local key state - for entry in self.local_keys.iter() { - let outbound = &entry.val().outbound; - for conn_entry in outbound.map().iter() { - if let Some(ref conn) = conn_entry.val().conn { - conn.close(0u32.into(), b"shutdown"); - } - } - } - - // Close all endpoints if let Ok(endpoints) = self.endpoints.lock() { for (port, state) in endpoints.iter() { log::info!(target: TARGET, "Shutting down QUIC endpoint on port {port}"); @@ -690,6 +518,30 @@ impl QuicNode { } } + /// Like `new`, but with a custom per-connection stream concurrency limit. + pub fn with_stream_limit( + subscribers: Vec>, + cancellation_token: tokio_util::sync::CancellationToken, + max_streams_per_connection: usize, + ) -> Arc { + static CRYPTO_INIT: Once = Once::new(); + CRYPTO_INIT.call_once(|| { + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install default Rustls CryptoProvider"); + }); + let transport = Arc::new(Self { + cancellation_token: cancellation_token.clone(), + local_keys: lockfree::map::Map::new(), + endpoints: std::sync::Mutex::new(std::collections::HashMap::new()), + subscribers: Arc::new(subscribers), + peer_keys: lockfree::map::Map::new(), + max_streams_per_connection, + }); + Self::spawn_connection_checker(Arc::downgrade(&transport), cancellation_token); + transport + } + fn addr_by_key(&self, key_id: &Arc) -> Result { match self.peer_keys.get(key_id) { Some(entry) => Ok(*entry.val()), @@ -697,16 +549,15 @@ impl QuicNode { } } - async fn connect(&self, peers: &AdnlPeers, addr: SocketAddr, server_name: &str) -> Result<()> { - let dst = peers.other(); - let state = self.local_key_state(peers.local())?; - let endpoint = { - let endpoints = self.endpoints.lock().map_err(|e| error!("Endpoints lock: {e}"))?; - endpoints - .get(&state.bound_port) - .map(|s| s.endpoint.clone()) - .ok_or_else(|| error!("No QUIC endpoint for port {}", state.bound_port))? - }; + async fn connect( + &self, + src: &Arc, + dst: &Arc, + addr: SocketAddr, + server_name: &str, + ) -> Result<()> { + let state = self.local_key_state(src)?; + let endpoint = self.endpoint_for_port(state.bound_port)?; let conn = endpoint .connect_with(state.client_config.clone(), addr, server_name) .map_err(|e| error!("QUIC connect to {addr} (SNI={server_name}): {e}"))? @@ -727,7 +578,6 @@ impl QuicNode { Ok(Some(QuicOutboundConnection { conn: Some(conn.clone()), send_queue: found.send_queue.clone(), - sender_state: found.sender_state.clone(), })) } else { Ok(None) @@ -738,44 +588,30 @@ impl QuicNode { Ok(()) } - /// Obtain (or create) an outbound connection and connect in the foreground. - /// Used by the query path where a live connection is required synchronously. - async fn ensure_outbound_connection( - self: &Arc, - peers: &AdnlPeers, - ) -> Result { - let addr = self.addr_by_key(peers.other())?; - let server_name = Self::key_id_to_server_name(peers.other()); - let state = self.local_key_state(peers.local())?; - loop { - let conn = Self::get_or_create_outbound_connection(&state.outbound, addr)?; - if conn.conn.is_some() { - break Ok(conn); - } - log::info!(target: TARGET, "Try new QUIC connection to {addr} in foreground"); - self.connect(peers, addr, &server_name).await?; - log::info!(target: TARGET, "QUIC connected to {addr} in foreground"); - } + /// Get the endpoint for the given port. + fn endpoint_for_port(&self, port: u16) -> Result { + let endpoints = self.endpoints.lock().map_err(|e| error!("Endpoints lock: {e}"))?; + endpoints + .get(&port) + .map(|s| s.endpoint.clone()) + .ok_or_else(|| error!("No QUIC endpoint for port {port}")) } - fn ensure_peer_registered(&self, adnl: Option<&AdnlNode>, peers: &AdnlPeers) -> Result<()> { - let dst = peers.other(); + fn ensure_peer_registered( + &self, + adnl: Option<&AdnlNode>, + src: &Arc, + dst: &Arc, + ) -> Result<()> { if self.has_peer_key(dst) { return Ok(()); } let Some(adnl) = adnl else { fail!("QUIC peer {dst} is not registered and no ADNL node provided"); }; - // Get peer's ADNL and optional QUIC addresses - let (adnl_addr, quic_addr) = adnl - .peer_ip_address(peers.local(), dst)? + let mut addr = adnl + .peer_ip_address(src, dst)? .ok_or_else(|| error!("QUIC peer {dst} IP is not known in ADNL"))?; - // Prefer explicit QUIC address from peer's address list (adnl.address.quic) - if let Some(quic_addr) = quic_addr { - return self.add_peer_key(dst.clone(), quic_addr); - } - // Fallback: derive QUIC port from ADNL port + offset - let mut addr = adnl_addr; let quic_port = addr.port().checked_add(Self::OFFSET_PORT).ok_or_else(|| { error!("QUIC port overflow for peer {dst}: ADNL port {}", addr.port()) })?; @@ -795,7 +631,8 @@ impl QuicNode { // Create per-endpoint TLS state let server_cert_keys: Arc>> = Arc::new(lockfree::map::Map::new()); - let last_added_name: Arc>> = Arc::new(Mutex::new(None)); + let last_added_name: Arc>> = + Arc::new(std::sync::Mutex::new(None)); let verifier = QuicClientCertVerifier::new(); let server_cert_resolver = QuicServerCertResolver::new(server_cert_keys.clone(), last_added_name.clone()); @@ -819,11 +656,12 @@ impl QuicNode { // connections for the default 30s, overloading the internal event loop and // making endpoint.accept() slow (the "HoL blocking" symptom). transport_config.max_idle_timeout(Some( - quinn::IdleTimeout::try_from(Duration::from_secs(15)).expect("15s fits in IdleTimeout"), + quinn::IdleTimeout::try_from(std::time::Duration::from_secs(15)) + .expect("15s fits in IdleTimeout"), )); // Keep established connections alive so the idle timeout only fires on // truly dead peers, not on connections that are just quiet between rounds. - transport_config.keep_alive_interval(Some(Duration::from_secs(5))); + transport_config.keep_alive_interval(Some(std::time::Duration::from_secs(5))); quinn_server_config.transport_config(Arc::new(transport_config)); // Create UDP socket with SO_REUSEADDR so the port can be reused immediately @@ -853,15 +691,6 @@ impl QuicNode { let local_key_names: Arc>> = Arc::new(lockfree::map::Map::new()); - let inbound: Arc = Arc::new(lockfree::map::Map::new()); - match self.inbound_pools.lock() { - Ok(mut pools) => pools.push(inbound.clone()), - Err(e) => log::warn!( - target: TARGET, - "inbound_pools lock poisoned, inbound stats will be incomplete: {e}" - ), - } - Self::spawn_accept_loop( endpoint.clone(), local_key_names.clone(), @@ -869,9 +698,6 @@ impl QuicNode { self.subscribers.clone(), bind_addr, self.max_streams_per_connection, - self.cancellation_token.clone(), - inbound, - self.msg_stats.clone(), ); let state = Arc::new(EndpointState { @@ -894,6 +720,9 @@ impl QuicNode { match outbound.map().get(&addr) { Some(entry) => { let found = entry.val(); + // Proactive liveness check: if the connection is dead, remove it + // and loop again โ€” the next iteration will see conn: None and + // trigger a reconnect. if let Some(ref c) = found.conn { if c.close_reason().is_some() { log::info!( @@ -909,40 +738,112 @@ impl QuicNode { break Ok(QuicOutboundConnection { conn: found.conn.clone(), send_queue: found.send_queue.clone(), - sender_state: found.sender_state.clone(), }); } None => { - let queue = QuicSendQueue::with_capacity(Self::SEND_QUEUE_CAPACITY); - let sender_state = SenderState::new(); + let queue = QuicSendQueue::new(); add_unbound_object_to_map(outbound.map(), addr, || { - Ok(QuicOutboundConnection { - conn: None, - send_queue: queue.clone(), - sender_state: sender_state.clone(), - }) + Ok(QuicOutboundConnection { conn: None, send_queue: queue.clone() }) })?; } } } } + async fn get_outbound_connection( + self: &Arc, + src: &Arc, + dst: &Arc, + create_async: bool, + ) -> Result { + let addr = self.addr_by_key(dst)?; + let server_name = Self::key_id_to_server_name(dst); + let state = self.local_key_state(src)?; + loop { + let conn = Self::get_or_create_outbound_connection(&state.outbound, addr)?; + if let QuicOutboundConnection { conn: Some(_), .. } = &conn { + break Ok(conn); + } + if create_async { + let queue = conn.send_queue.clone(); + let quic = self.clone(); + let src = src.clone(); + let dst = dst.clone(); + let server_name = server_name.clone(); + spawn_cancelable(self.cancellation_token.clone(), async move { + while !queue.activate(true) { + tokio::task::yield_now().await; + } + loop { + let Some(data) = queue.pop() else { + if queue.activate(false) { + break; + } + tokio::task::yield_now().await; + continue; + }; + loop { + let result = quic.local_key_state(&src).and_then(|s| { + Self::get_or_create_outbound_connection(&s.outbound, addr) + }); + let result = match result { + Ok(QuicOutboundConnection { conn: Some(ref conn), .. }) => { + Self::send_via_stream(conn, &data).await + } + Ok(_) => { + log::info!( + target: TARGET, + "Try new QUIC connection to {addr} in background" + ); + let result = quic.connect(&src, &dst, addr, &server_name).await; + if let Err(e) = result { + Err(error!( + "QUIC background connection to {addr} error: {e}" + )) + } else { + log::info!( + target: TARGET, + "QUIC connected to {addr} in background" + ); + continue; + } + } + Err(e) => Err(e), + }; + if let Err(e) = result { + log::warn!( + target: TARGET, + "QUIC send to {addr} in background error: {e}" + ); + } + break; + } + } + }); + break Ok(conn); + } else { + log::info!(target: TARGET, "Try new QUIC connection to {addr} in foreground"); + self.connect(&src, dst, addr, &server_name).await?; + log::info!(target: TARGET, "QUIC connected to {addr} in foreground"); + } + } + } + async fn handle_connection( incoming: quinn::Incoming, local_key_names: Arc>>, server_cert_resolver: Arc, - inbound: Arc, + inbound: Arc>, subscribers: Arc>>, bind_addr: SocketAddr, max_streams_per_connection: usize, - msg_stats: Arc, ) { let addr = incoming.remote_address(); // Bound handshake time: C++ ngtcp2 clients abandon after ~3-5s and retry, // so a handshake still in progress after 5s is almost certainly stale. // Without this, stale Connecting futures accumulate inside quinn's endpoint, // slowing its internal event loop and delaying endpoint.accept() for new peers. - let conn = match tokio::time::timeout(Duration::from_secs(5), incoming).await { + let conn = match tokio::time::timeout(std::time::Duration::from_secs(5), incoming).await { Ok(Ok(conn)) => conn, Ok(Err(e)) => { log::warn!(target: TARGET, "QUIC handshake from {addr} on {bind_addr} failed: {e}"); @@ -991,19 +892,17 @@ impl QuicNode { } }; - let inbound_key = QuicInboundKey(local_key_id.clone(), peer_key_id.clone()); let had_existing = { let mut found_existing = false; - let result = - add_unbound_object_to_map_with_update(&inbound, inbound_key.clone(), |existing| { - if existing.is_some() { - found_existing = true; - // Keep existing entry; resolver task will handle replacement - Ok(None) - } else { - Ok(Some(conn.clone())) - } - }); + let result = add_unbound_object_to_map_with_update(inbound.map(), addr, |existing| { + if existing.is_some() { + found_existing = true; + // Keep existing entry; resolver task will handle replacement + Ok(None) + } else { + Ok(Some(conn.clone())) + } + }); if let Err(e) = result { log::warn!(target: TARGET, "Store QUIC inbound for {addr}: {e}"); return; @@ -1011,104 +910,47 @@ impl QuicNode { found_existing }; if had_existing { - tokio::spawn(Self::resolve_duplicate_connection( - inbound.clone(), - conn.clone(), - inbound_key.clone(), - addr, - )); + tokio::spawn(Self::resolve_duplicate_connection(inbound.clone(), conn.clone(), addr)); } let peers = AdnlPeers::with_keys(local_key_id, peer_key_id); let conn_id = conn.stable_id(); // Limit concurrent in-flight streams per connection to bound memory usage. - // When the semaphore is full, accept stalls, applying QUIC-level backpressure. + // When the semaphore is full, accept_bi() stalls, applying QUIC-level backpressure. let stream_semaphore = Arc::new(tokio::sync::Semaphore::new(max_streams_per_connection)); - - // Accept both bi-directional streams (queries + legacy messages) and - // uni-directional streams (fire-and-forget messages from the new sender). - let conn_bi = conn.clone(); - let conn_uni = conn.clone(); - let sem_bi = stream_semaphore.clone(); - let sem_uni = stream_semaphore; - let subs_bi = subscribers.clone(); - let subs_uni = subscribers; - let peers_bi = peers.clone(); - let peers_uni = peers; - let stats_bi = msg_stats.clone(); - let stats_uni = msg_stats; - - let bi_loop = async { - loop { - let (send, recv) = match conn_bi.accept_bi().await { - Ok(streams) => streams, - Err(e) => { - log::warn!(target: TARGET, "QUIC accept bi-stream from {addr}: {e}"); - break; - } - }; - let permit = match sem_bi.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => break, - }; - let subscribers = subs_bi.clone(); - let peers = peers_bi.clone(); - let stats = stats_bi.clone(); - tokio::spawn(async move { - let _permit = permit; - if let Err(e) = Self::process_incoming_stream( - recv, - send, - &subscribers, - &peers, - addr, - &stats, - ) - .await - { - log::warn!(target: TARGET, "QUIC process bi-stream from {addr}: {e}"); - } - }); - } - }; - - let uni_loop = async { - loop { - let recv = match conn_uni.accept_uni().await { - Ok(stream) => stream, - Err(e) => { - log::warn!(target: TARGET, "QUIC accept uni-stream from {addr}: {e}"); - break; - } - }; - let permit = match sem_uni.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => break, - }; - let subscribers = subs_uni.clone(); - let peers = peers_uni.clone(); - let stats = stats_uni.clone(); - tokio::spawn(async move { - let _permit = permit; - if let Err(e) = - Self::process_incoming_uni_stream(recv, &subscribers, &peers, addr, &stats) - .await - { - log::warn!(target: TARGET, "QUIC process uni-stream from {addr}: {e}"); - } - }); - } - }; - - // Run both accept loops; when either exits (connection closed), both stop. - tokio::select! { - () = bi_loop => {} - () = uni_loop => {} + loop { + let (send, recv) = match conn.accept_bi().await { + Ok(streams) => streams, + Err(e) => { + log::warn!( + target: TARGET, + "QUIC accept stream from {addr}: {e}" + ); + break; + } + }; + let permit = match stream_semaphore.clone().acquire_owned().await { + Ok(p) => p, + Err(_) => break, + }; + let subscribers = subscribers.clone(); + let peers = peers.clone(); + tokio::spawn(async move { + let _permit = permit; + if let Err(e) = + Self::process_incoming_stream(recv, send, &subscribers, &peers, addr).await + { + log::warn!( + target: TARGET, + "QUIC process stream from {addr}: {e}" + ); + } + }); } let is_current = - inbound.get(&inbound_key).map(|e| e.val().stable_id() == conn_id).unwrap_or(false); + inbound.map().get(&addr).map(|e| e.val().stable_id() == conn_id).unwrap_or(false); if is_current { - inbound.remove(&inbound_key); + inbound.map().remove(&addr); } log::info!( target: TARGET, @@ -1136,11 +978,10 @@ impl QuicNode { subscribers: &[Arc], peers: &AdnlPeers, addr: SocketAddr, - msg_stats: &MsgStats, ) -> Result<()> { log::debug!(target: TARGET, "process_incoming_stream from {addr}: reading data..."); let buf = match tokio::time::timeout( - Duration::from_secs(5), + std::time::Duration::from_secs(5), recv.read_to_end(16 * 1024 * 1024), // 16MB limit ) .await @@ -1155,52 +996,26 @@ impl QuicNode { return Ok(()); } }; - log::debug!( - target: TARGET, - "process_incoming_stream from {addr}: read {} bytes", - buf.len() - ); + log::debug!(target: TARGET, "process_incoming_stream from {addr}: read {} bytes", buf.len()); if buf.is_empty() { return Ok(()); } let obj = deserialize_boxed(&buf) .map_err(|e| error!("Cannot deserialize QUIC message from {addr}: {e}"))?; - log::debug!( - target: TARGET, - "process_incoming_stream from {addr}: deserialized TL, about to downcast" - ); + log::debug!(target: TARGET, "process_incoming_stream from {addr}: deserialized TL, about to downcast"); match obj.downcast::() { Ok(Request::Quic_Message(msg)) => { - msg_stats.record(extract_inner_tag(&msg.data), msg.data.len(), addr, false, false); - log::debug!( - target: TARGET, - "process_incoming_stream from {addr}: QUIC MESSAGE, \ - dispatching to {} subscribers", - subscribers.len() - ); + log::debug!(target: TARGET, "process_incoming_stream from {addr}: QUIC MESSAGE, dispatching to {} subscribers", subscribers.len()); for subscriber in subscribers { if subscriber.try_consume_custom(&msg.data, &peers).await? { - log::debug!( - target: TARGET, - "process_incoming_stream from {addr}: consumed by subscriber" - ); + log::debug!(target: TARGET, "process_incoming_stream from {addr}: consumed by subscriber"); break; } } let _ = send.finish(); - log::debug!( - target: TARGET, - "process_incoming_stream from {addr}: finished send side" - ); + log::debug!(target: TARGET, "process_incoming_stream from {addr}: finished send side"); } Ok(Request::Quic_Query(query)) => { - msg_stats.record( - extract_inner_tag(&query.data), - query.data.len(), - addr, - false, - true, - ); log::debug!(target: TARGET, "process_incoming_stream from {addr}: QUIC QUERY"); let answer = Query::process(subscribers, &query.data, &peers).await?; if let Some(answer) = answer { @@ -1222,63 +1037,7 @@ impl QuicNode { let _ = send.finish(); } Err(_obj) => { - log::warn!( - target: TARGET, - "Unknown QUIC TL message from {addr}: failed to downcast to Request" - ); - } - } - Ok(()) - } - - /// Process a fire-and-forget message received on a uni-directional QUIC stream. - /// Only `QuicMessage` is expected; queries arriving on uni streams are rejected - /// because there is no send side to write a response to. - async fn process_incoming_uni_stream( - mut recv: quinn::RecvStream, - subscribers: &[Arc], - peers: &AdnlPeers, - addr: SocketAddr, - msg_stats: &MsgStats, - ) -> Result<()> { - let buf = - match tokio::time::timeout(Duration::from_secs(5), recv.read_to_end(16 * 1024 * 1024)) - .await - { - Ok(result) => result.map_err(|e| error!("QUIC uni read from {addr}: {e}"))?, - Err(_) => { - log::warn!( - target: TARGET, - "process_incoming_uni_stream from {addr}: read timed out after 5s" - ); - return Ok(()); - } - }; - if buf.is_empty() { - return Ok(()); - } - let obj = deserialize_boxed(&buf) - .map_err(|e| error!("Cannot deserialize QUIC uni-stream from {addr}: {e}"))?; - match obj.downcast::() { - Ok(Request::Quic_Message(msg)) => { - msg_stats.record(extract_inner_tag(&msg.data), msg.data.len(), addr, false, false); - for subscriber in subscribers { - if subscriber.try_consume_custom(&msg.data, peers).await? { - break; - } - } - } - Ok(Request::Quic_Query(_)) => { - log::warn!( - target: TARGET, - "Received QUIC query on uni-directional stream from {addr} โ€” no response possible" - ); - } - Err(_) => { - log::warn!( - target: TARGET, - "Unknown QUIC TL message on uni-stream from {addr}" - ); + log::warn!(target: TARGET, "Unknown QUIC TL message from {addr}: failed to downcast to Request"); } } Ok(()) @@ -1294,13 +1053,6 @@ impl QuicNode { dead_conn: &quinn::Connection, ) -> bool { let dead_id = dead_conn.stable_id(); - // Explicitly close the quinn connection so its internal ConnectionDriver - // task stops immediately. Without this, the driver continues processing - // keep-alive and retransmit timers until idle timeout (15s), causing the - // EndpointDriver to busy-loop on timer events and burn 100% CPU. - if dead_conn.close_reason().is_none() { - dead_conn.close(0u32.into(), b"dead connection cleanup"); - } match outbound.set_connection_state(addr, |found| { if let Some(ref conn) = found.conn { if conn.stable_id() == dead_id { @@ -1311,7 +1063,6 @@ impl QuicNode { return Ok(Some(QuicOutboundConnection { conn: None, send_queue: found.send_queue.clone(), - sender_state: found.sender_state.clone(), })); } } @@ -1329,21 +1080,20 @@ impl QuicNode { } async fn resolve_duplicate_connection( - inbound: Arc, + inbound: Arc>, new_conn: quinn::Connection, - key: QuicInboundKey, addr: SocketAddr, ) { use rand::Rng; let delay_ms = rand::thread_rng().gen_range(500..=2500); - tokio::time::sleep(Duration::from_millis(delay_ms)).await; + tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; let old_alive = - inbound.get(&key).map(|e| e.val().close_reason().is_none()).unwrap_or(false); + inbound.map().get(&addr).map(|e| e.val().close_reason().is_none()).unwrap_or(false); let new_alive = new_conn.close_reason().is_none(); if old_alive && new_alive { - if let Some(old) = inbound.remove(&key) { + if let Some(old) = inbound.map().remove(&addr) { log::info!( target: TARGET, "Closing old duplicate inbound from {addr} (both alive after {delay_ms}ms)" @@ -1351,11 +1101,15 @@ impl QuicNode { old.val().close(0u32.into(), b"Replaced by new inbound"); } let nc = new_conn.clone(); - let _ = add_unbound_object_to_map_with_update(&inbound, key, |_| Ok(Some(nc.clone()))); + let _ = add_unbound_object_to_map_with_update(inbound.map(), addr, |_| { + Ok(Some(nc.clone())) + }); } else if new_alive { - inbound.remove(&key); + inbound.map().remove(&addr); let nc = new_conn.clone(); - let _ = add_unbound_object_to_map_with_update(&inbound, key, |_| Ok(Some(nc.clone()))); + let _ = add_unbound_object_to_map_with_update(inbound.map(), addr, |_| { + Ok(Some(nc.clone())) + }); log::debug!( target: TARGET, "Old inbound from {addr} already closed, keeping new" @@ -1368,95 +1122,19 @@ impl QuicNode { } } - /// Drain the send queue and exit. Spawned when `message()` has no live - /// connection and must enqueue data for later delivery. The task establishes - /// the connection, sends all queued messages, and terminates. - async fn run_sender_task( - quic: Arc, - peers: AdnlPeers, - addr: SocketAddr, - server_name: String, - send_queue: Arc, - sender_state: Arc, - outbound: Arc>, - ) { - log::trace!(target: TARGET, "QUIC sender task started for {addr}"); - - loop { - // Drain the queue - while let Some(data) = send_queue.pop() { - if let Err(e) = - quic.send_message(&peers, addr, &server_name, &outbound, &data).await - { - log::warn!(target: TARGET, "QUIC sender to {addr} error: {e}"); - } - } - - // Mark inactive, then re-check: a new message may have been enqueued - // between the last pop() returning None and the store below. - sender_state.active.store(false, Ordering::Release); - if send_queue.is_empty() { - break; - } - // Lost race โ€” reactivate if no other task took over - if sender_state - .active - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_err() - { - break; // another task took over - } - } - - log::trace!(target: TARGET, "QUIC sender task for {addr} exited"); - } - - /// Send a single message to the peer, establishing the connection first if needed. - async fn send_message( - &self, - peers: &AdnlPeers, - addr: SocketAddr, - server_name: &str, - outbound: &Connections, - data: &[u8], - ) -> Result<()> { - let entry = Self::get_or_create_outbound_connection(outbound, addr)?; - match entry.conn { - Some(ref conn) => { - if let Err(e) = Self::send_via_stream(conn, data).await { - log::warn!( - target: TARGET, - "QUIC send to {addr} failed: {e}, removing dead connection" - ); - Self::remove_dead_connection(outbound, addr, conn); - return Err(e); - } - } - None => { - log::info!(target: TARGET, "QUIC sender: connecting to {addr}"); - self.connect(peers, addr, server_name).await?; - log::info!(target: TARGET, "QUIC sender: connected to {addr}"); - let entry = Self::get_or_create_outbound_connection(outbound, addr)?; - if let Some(ref conn) = entry.conn { - Self::send_via_stream(conn, data).await?; - } - } - } - Ok(()) - } - async fn send_query_raw( self: &Arc, data: Vec, - peers: &AdnlPeers, + src: &Arc, + dst: &Arc, timeout_ms: u64, ) -> Result> { - let addr = self.addr_by_key(peers.other())?; - let state = self.local_key_state(peers.local())?; + let addr = self.addr_by_key(dst)?; + let state = self.local_key_state(src)?; let timeout = Duration::from_millis(timeout_ms); // First attempt - match self.ensure_outbound_connection(peers).await? { + match self.get_outbound_connection(src, dst, false).await? { QuicOutboundConnection { conn: Some(ref conn), .. } => { let result = tokio::time::timeout(timeout, Self::send_via_stream(conn, &data)).await; @@ -1465,31 +1143,28 @@ impl QuicNode { Ok(Err(e)) => { log::warn!( target: TARGET, - "QUIC query to {} failed: {e}, removing dead connection and retrying", - peers.other() + "QUIC query to {dst} failed: {e}, removing dead connection and retrying" ); Self::remove_dead_connection(&state.outbound, addr, conn); } Err(_) => { log::warn!( target: TARGET, - "QUIC query to {} timed out ({timeout_ms}ms), \ - removing dead connection and retrying", - peers.other() + "QUIC query to {dst} timed out ({timeout_ms}ms), removing dead connection and retrying" ); Self::remove_dead_connection(&state.outbound, addr, conn); } } } - _ => fail!("Cannot create QUIC connection to {} in foreground", peers.other()), + _ => fail!("Cannot create QUIC connection to {dst} in foreground"), } // Retry once with a fresh connection - match self.ensure_outbound_connection(peers).await? { + match self.get_outbound_connection(src, dst, false).await? { QuicOutboundConnection { conn: Some(ref conn), .. } => { Self::send_via_stream(conn, &data).await } - _ => fail!("Cannot create QUIC connection to {} in foreground (retry)", peers.other()), + _ => fail!("Cannot create QUIC connection to {dst} in foreground (retry)"), } } @@ -1504,7 +1179,7 @@ impl QuicNode { send.write_all(data).await.map_err(|e| error!("QUIC stream write: {e}"))?; send.finish().map_err(|e| error!("QUIC stream finish: {e}"))?; let response = match tokio::time::timeout( - Duration::from_secs(30), + std::time::Duration::from_secs(30), recv.read_to_end(16 * 1024 * 1024), // 16MB limit ) .await @@ -1534,45 +1209,27 @@ impl QuicNode { subscribers: Arc>>, bind_addr: SocketAddr, max_streams_per_connection: usize, - cancellation_token: tokio_util::sync::CancellationToken, - inbound: Arc, - msg_stats: Arc, ) { tokio::spawn(async move { + let inbound: Arc> = Connections::new(); loop { log::trace!(target: TARGET, "Loop QUIC server on {bind_addr}"); - tokio::select! { - _ = cancellation_token.cancelled() => { - log::info!(target: TARGET, "QUIC accept loop on {bind_addr} cancelled"); - break; - } - incoming = endpoint.accept() => { - let Some(incoming) = incoming else { - log::info!(target: TARGET, "QUIC endpoint on {bind_addr} closed"); - break; - }; - let addr = incoming.remote_address(); - log::debug!(target: TARGET, "Accept in QUIC server on {bind_addr} from {addr}"); - - let token = cancellation_token.clone(); - let lkn = local_key_names.clone(); - let scr = server_cert_resolver.clone(); - let ib = inbound.clone(); - let subs = subscribers.clone(); - let stats = msg_stats.clone(); - tokio::spawn(async move { - tokio::select! { - _ = token.cancelled() => { - log::debug!(target: TARGET, "QUIC connection handler for {addr} cancelled"); - } - _ = Self::handle_connection( - incoming, lkn, scr, ib, subs, bind_addr, - max_streams_per_connection, stats, - ) => {} - } - }); - } - } + let Some(incoming) = endpoint.accept().await else { + log::info!(target: TARGET, "QUIC endpoint on {bind_addr} closed"); + break; + }; + let addr = incoming.remote_address(); + log::debug!(target: TARGET, "Accept in QUIC server on {bind_addr} from {addr}"); + + tokio::spawn(Self::handle_connection( + incoming, + local_key_names.clone(), + server_cert_resolver.clone(), + inbound.clone(), + subscribers.clone(), + bind_addr, + max_streams_per_connection, + )); } }); } @@ -1582,7 +1239,7 @@ impl QuicNode { /// idle timeouts before the next send attempt, avoiding the 10-15s hang on /// first use of a dead connection. fn spawn_connection_checker( - weak: Weak, + weak: std::sync::Weak, cancellation_token: tokio_util::sync::CancellationToken, ) { spawn_cancelable(cancellation_token, async move { @@ -1612,18 +1269,6 @@ impl QuicNode { removed += 1; } } - // Fully remove entry only when connection is cleared, no sender - // task is running, and the queue is empty. Re-fetch from map - // because remove_dead_connection may have updated the entry. - if let Some(fresh) = outbound.map().get(&addr) { - let s = fresh.val(); - if s.conn.is_none() - && !s.sender_state.active.load(Ordering::Acquire) - && s.send_queue.is_empty() - { - outbound.map().remove(&addr); - } - } } } if removed > 0 { @@ -1640,475 +1285,4 @@ impl QuicNode { } }); } - - /// Background task that periodically logs statistics for all active QUIC connections. - /// Shows deltas (bytes/dgrams/lost since last dump) plus instantaneous path metrics. - fn spawn_stats_dumper( - weak: Weak, - cancellation_token: tokio_util::sync::CancellationToken, - ) { - spawn_cancelable(cancellation_token, async move { - // Key: (stable_id, is_outbound) โ†’ previous snapshot of cumulative counters - let mut prev: HashMap<(usize, bool), ConnSnapshot> = HashMap::new(); - - loop { - tokio::time::sleep(Self::STATS_DUMP_INTERVAL).await; - let Some(transport) = weak.upgrade() else { - log::trace!(target: TARGET, "Stats dumper: transport dropped, exiting"); - break; - }; - - let mut seen = HashSet::new(); - let mut total = 0u32; - let mut dump = String::from("QUIC STATS dump:\n"); - - // Outbound connections - for key_entry in transport.local_keys.iter() { - let key_id = key_entry.key(); - for conn_entry in key_entry.val().outbound.map().iter() { - let addr = *conn_entry.key(); - if let Some(ref conn) = conn_entry.val().conn { - let s = conn.stats(); - let id = (conn.stable_id(), true); - seen.insert(id); - total += 1; - let snap = ConnSnapshot::from_stats(&s); - let delta = prev.get(&id).map(|p| snap.delta(p)).unwrap_or(snap); - prev.insert(id, snap); - fmt::Write::write_fmt( - &mut dump, - format_args!( - " outbound peer={addr} \ - dtx={} bytes/{} dgrams drx={} bytes/{} dgrams \ - dlost={} pkts rtt={:?} cwnd={} mtu={} key={key_id:.8}\n", - delta.tx_bytes, - delta.tx_dgrams, - delta.rx_bytes, - delta.rx_dgrams, - delta.lost_pkts, - s.path.rtt, - s.path.cwnd, - s.path.current_mtu, - ), - ) - .ok(); - } - } - } - - // Inbound connections - let pools = match transport.inbound_pools.lock() { - Ok(g) => g.clone(), - Err(e) => { - log::warn!( - target: TARGET, - "inbound_pools lock poisoned, skipping inbound stats: {e}" - ); - Vec::new() - } - }; - for pool in &pools { - for conn_entry in pool.iter() { - let QuicInboundKey(ref local_id, ref peer_id) = *conn_entry.key(); - let addr = conn_entry.val().remote_address(); - let conn = conn_entry.val(); - let s = conn.stats(); - let id = (conn.stable_id(), false); - seen.insert(id); - total += 1; - let snap = ConnSnapshot::from_stats(&s); - let delta = prev.get(&id).map(|p| snap.delta(p)).unwrap_or(snap); - prev.insert(id, snap); - fmt::Write::write_fmt( - &mut dump, - format_args!( - " inbound peer={addr} local={local_id} remote={peer_id} \ - dtx={} bytes/{} dgrams drx={} bytes/{} dgrams \ - dlost={} pkts rtt={:?} cwnd={} mtu={}\n", - delta.tx_bytes, - delta.tx_dgrams, - delta.rx_bytes, - delta.rx_dgrams, - delta.lost_pkts, - s.path.rtt, - s.path.cwnd, - s.path.current_mtu, - ), - ) - .ok(); - } - } - - // Evict snapshots for connections that no longer exist - prev.retain(|id, _| seen.contains(id)); - - // Per-peer, per-message-kind stats (deltas since last dump) - let msg_entries = transport.msg_stats.drain(); - let mut current_peer = None; - for (key, count, bytes) in &msg_entries { - if current_peer != Some(key.addr) { - current_peer = Some(key.addr); - fmt::Write::write_fmt(&mut dump, format_args!(" peer {}:\n", key.addr,)) - .ok(); - } - let dir = if key.is_outbound { "out" } else { " in" }; - let kind = if key.is_query { "query" } else { "msg " }; - fmt::Write::write_fmt( - &mut dump, - format_args!( - " {dir}/{kind} {:#010x}({}) count={count} bytes={bytes}\n", - key.tag, - tl_tag_name(key.tag), - ), - ) - .ok(); - } - - fmt::Write::write_fmt(&mut dump, format_args!( - " total: {total} connections, {} msg entries", - msg_entries.len(), - )).ok(); - - log::info!(target: TARGET, "{dump}"); - } - }); - } -} - -/// Extract the "inner" TL constructor tag from message data. -/// -/// QUIC message payloads are typically wrapped in an overlay prefix -/// (`overlay.message` or `overlay.query`). The outer tag is not useful -/// for diagnostics. This function skips past the overlay wrapper and -/// returns the constructor tag of the actual inner payload. -/// -/// `overlay.message` and `overlay.query` have a fixed layout: -/// constructor(4 bytes) + int256(32 bytes) = 36 bytes prefix. -/// `WithExtra` variants have a variable-length extra field, so we -/// fall back to `deserialize_boxed_with_suffix` for those. -fn extract_inner_tag(data: &[u8]) -> u32 { - if data.len() < 4 { - return 0; - } - let outer = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); - // overlay.message / overlay.query: fixed 36-byte prefix (constructor + int256) - const FIXED_PREFIX: usize = 4 + 32; - match outer { - 0x75252420 | 0xccfd8443 => { - // overlay.message, overlay.query - if data.len() >= FIXED_PREFIX + 4 { - let s = &data[FIXED_PREFIX..]; - return u32::from_le_bytes([s[0], s[1], s[2], s[3]]); - } - outer - } - 0xa232233d | 0x94ffc3e9 => { - // overlay.messageWithExtra, overlay.queryWithExtra - if let Ok((_obj, suffix_offset)) = deserialize_boxed_with_suffix(data) { - if suffix_offset + 4 <= data.len() { - let s = &data[suffix_offset..]; - return u32::from_le_bytes([s[0], s[1], s[2], s[3]]); - } - } - outer - } - _ => outer, - } -} - -/// Map well-known TL constructor tags to short human-readable names for log output. -fn tl_tag_name(tag: u32) -> &'static str { - match tag { - 0x75252420 => "overlay.message", - 0xa232233d => "overlay.messageWithExtra", - 0xccfd8443 => "overlay.query", - 0x94ffc3e9 => "overlay.queryWithExtra", - 0xb15a2b6b => "overlay.broadcast", - 0xbad7c36a => "overlay.broadcastFec", - 0xf1881342 => "overlay.broadcastFecShort", - 0x46efae62 => "overlay.broadcastStream", - 0xf99fd63d => "overlay.broadcastTwostepFec", - 0x80b859b0 => "overlay.broadcastTwostepSimple", - 0x33534e24 => "overlay.unicast", - 0xd55c14ec => "overlay.fec.received", - 0x09d76914 => "overlay.fec.completed", - 0x48ee64ab => "overlay.getRandomPeers", - 0xa58e7ecc => "overlay.getRandomPeersV2", - 0x690cb481 => "overlay.ping", - 0x236758c4 => "catchain.blockUpdate", - 0x9283ce37 => "validatorSession.blockUpdate", - 0xbe7b573a => "consensus.simplex.certificate", - 0xc37ef4f3 => "consensus.simplex.vote", - _ => "unknown", - } -} - -/// Snapshot of cumulative counters from a single connection, used to compute deltas. -#[derive(Clone, Copy)] -struct ConnSnapshot { - tx_bytes: u64, - tx_dgrams: u64, - rx_bytes: u64, - rx_dgrams: u64, - lost_pkts: u64, -} - -impl ConnSnapshot { - fn from_stats(s: &quinn::ConnectionStats) -> Self { - Self { - tx_bytes: s.udp_tx.bytes, - tx_dgrams: s.udp_tx.datagrams, - rx_bytes: s.udp_rx.bytes, - rx_dgrams: s.udp_rx.datagrams, - lost_pkts: s.path.lost_packets, - } - } - - fn delta(&self, prev: &Self) -> Self { - Self { - tx_bytes: self.tx_bytes.saturating_sub(prev.tx_bytes), - tx_dgrams: self.tx_dgrams.saturating_sub(prev.tx_dgrams), - rx_bytes: self.rx_bytes.saturating_sub(prev.rx_bytes), - rx_dgrams: self.rx_dgrams.saturating_sub(prev.rx_dgrams), - lost_pkts: self.lost_pkts.saturating_sub(prev.lost_pkts), - } - } -} - -/// Per-TL-tag message counters (lock-free atomics, collected per dump interval). -struct MsgTagCounters { - count: AtomicU64, - bytes: AtomicU64, -} - -impl MsgTagCounters { - fn new() -> Self { - Self { count: AtomicU64::new(0), bytes: AtomicU64::new(0) } - } - - fn record(&self, size: usize) { - self.count.fetch_add(1, Ordering::Relaxed); - self.bytes.fetch_add(size as u64, Ordering::Relaxed); - } - - /// Take current values and reset to zero. - fn take(&self) -> (u64, u64) { - (self.count.swap(0, Ordering::Relaxed), self.bytes.swap(0, Ordering::Relaxed)) - } -} - -/// Per-peer, per-TL-tag message statistics key. -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -struct MsgStatsKey { - addr: SocketAddr, - tag: u32, - is_outbound: bool, - is_query: bool, -} - -/// Tracks per-peer, per-message-kind statistics for QUIC traffic. -struct MsgStats { - counters: lockfree::map::Map, -} - -impl MsgStats { - fn new() -> Arc { - Arc::new(Self { counters: lockfree::map::Map::new() }) - } - - fn record(&self, tag: u32, size: usize, addr: SocketAddr, is_outbound: bool, is_query: bool) { - let key = MsgStatsKey { addr, tag, is_outbound, is_query }; - if let Some(entry) = self.counters.get(&key) { - entry.val().record(size); - return; - } - let _ = add_unbound_object_to_map(&self.counters, key, || Ok(MsgTagCounters::new())); - if let Some(entry) = self.counters.get(&key) { - entry.val().record(size); - } - } - - /// Drain all counters and return entries sorted by peer then bytes desc. - /// Entries with zero activity since the last drain are removed - fn drain(&self) -> Vec<(MsgStatsKey, u64, u64)> { - let mut result = Vec::new(); - let mut stale = Vec::new(); - for entry in self.counters.iter() { - let (count, bytes) = entry.val().take(); - if count > 0 { - result.push((*entry.key(), count, bytes)); - } else { - stale.push(*entry.key()); - } - } - for key in stale { - self.counters.remove(&key); - } - result.sort_by(|a, b| a.0.addr.cmp(&b.0.addr).then(b.2.cmp(&a.2))); - result - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // --- extract_inner_tag --- - - /// Helper: build an overlay.message (0x75252420) wrapping the given inner tag. - fn make_overlay_message(inner_tag: u32) -> Vec { - let mut buf = Vec::new(); - buf.extend_from_slice(&0x75252420u32.to_le_bytes()); // outer tag - buf.extend_from_slice(&[0u8; 32]); // overlay int256 - buf.extend_from_slice(&inner_tag.to_le_bytes()); // inner payload tag - buf - } - - /// Helper: build an overlay.query (0xccfd8443) wrapping the given inner tag. - fn make_overlay_query(inner_tag: u32) -> Vec { - let mut buf = Vec::new(); - buf.extend_from_slice(&0xccfd8443u32.to_le_bytes()); - buf.extend_from_slice(&[0u8; 32]); - buf.extend_from_slice(&inner_tag.to_le_bytes()); - buf - } - - #[test] - fn test_extract_inner_tag_empty() { - assert_eq!(extract_inner_tag(&[]), 0); - assert_eq!(extract_inner_tag(&[1, 2, 3]), 0); - } - - #[test] - fn test_extract_inner_tag_unknown_outer() { - let data = 0xDEADBEEFu32.to_le_bytes(); - assert_eq!(extract_inner_tag(&data), 0xDEADBEEF); - } - - #[test] - fn test_extract_inner_tag_overlay_message() { - let data = make_overlay_message(0x236758c4); // catchain.blockUpdate - assert_eq!(extract_inner_tag(&data), 0x236758c4); - } - - #[test] - fn test_extract_inner_tag_overlay_query() { - let data = make_overlay_query(0x48ee64ab); // overlay.getRandomPeers - assert_eq!(extract_inner_tag(&data), 0x48ee64ab); - } - - #[test] - fn test_extract_inner_tag_overlay_message_too_short() { - // outer tag + partial overlay id (not enough for inner tag) - let mut data = Vec::new(); - data.extend_from_slice(&0x75252420u32.to_le_bytes()); - data.extend_from_slice(&[0u8; 30]); // only 30 bytes, need 32 + 4 - assert_eq!(extract_inner_tag(&data), 0x75252420); // falls back to outer - } - - // --- MsgStats --- - - fn test_addr(port: u16) -> SocketAddr { - SocketAddr::from(([127, 0, 0, 1], port)) - } - - #[test] - fn test_msg_stats_record_and_drain() { - let stats = MsgStats::new(); - let addr = test_addr(1000); - - stats.record(0xAA, 100, addr, true, false); - stats.record(0xAA, 200, addr, true, false); - stats.record(0xBB, 50, addr, true, true); - - let entries = stats.drain(); - assert_eq!(entries.len(), 2); - - // Sorted by addr (same), then bytes desc: AA(300) before BB(50) - assert_eq!(entries[0].0.tag, 0xAA); - assert_eq!(entries[0].1, 2); // count - assert_eq!(entries[0].2, 300); // bytes - - assert_eq!(entries[1].0.tag, 0xBB); - assert_eq!(entries[1].1, 1); - assert_eq!(entries[1].2, 50); - } - - #[test] - fn test_msg_stats_drain_sorts_by_addr_then_bytes() { - let stats = MsgStats::new(); - let addr_a = test_addr(1000); - let addr_b = test_addr(2000); - - stats.record(0xAA, 10, addr_b, true, false); - stats.record(0xBB, 500, addr_a, true, false); - stats.record(0xCC, 100, addr_a, true, false); - - let entries = stats.drain(); - assert_eq!(entries.len(), 3); - - // addr_a (port 1000) first, sorted by bytes desc - assert_eq!(entries[0].0.addr, addr_a); - assert_eq!(entries[0].0.tag, 0xBB); // 500 bytes - assert_eq!(entries[1].0.addr, addr_a); - assert_eq!(entries[1].0.tag, 0xCC); // 100 bytes - - // addr_b (port 2000) last - assert_eq!(entries[2].0.addr, addr_b); - assert_eq!(entries[2].0.tag, 0xAA); - } - - #[test] - fn test_msg_stats_drain_resets_counters() { - let stats = MsgStats::new(); - let addr = test_addr(1000); - - stats.record(0xAA, 100, addr, true, false); - let entries = stats.drain(); - assert_eq!(entries.len(), 1); - - // Second drain: no new activity, should return empty - let entries = stats.drain(); - assert!(entries.is_empty()); - } - - #[test] - fn test_msg_stats_drain_evicts_stale_keys() { - let stats = MsgStats::new(); - let addr = test_addr(1000); - - stats.record(0xAA, 100, addr, true, false); - stats.record(0xBB, 50, addr, false, false); - - // First drain: both active, counters reset - let _ = stats.drain(); - - // Only record on 0xAA - stats.record(0xAA, 200, addr, true, false); - - // Second drain: 0xBB was idle โ†’ evicted - let _ = stats.drain(); - - // Record on 0xBB again โ€” must re-insert (was evicted) - stats.record(0xBB, 30, addr, false, false); - let entries = stats.drain(); - - // 0xAA was idle since last drain (evicted), only 0xBB with activity is returned - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].0.tag, 0xBB); - assert_eq!(entries[0].2, 30); - } - - #[test] - fn test_msg_stats_distinguishes_direction_and_kind() { - let stats = MsgStats::new(); - let addr = test_addr(1000); - - stats.record(0xAA, 100, addr, true, false); // outbound msg - stats.record(0xAA, 200, addr, false, false); // inbound msg - stats.record(0xAA, 300, addr, true, true); // outbound query - - let entries = stats.drain(); - assert_eq!(entries.len(), 3); - } } diff --git a/src/adnl/src/rldp/mod.rs b/src/adnl/src/rldp/mod.rs index b0cc302..c75c1e2 100644 --- a/src/adnl/src/rldp/mod.rs +++ b/src/adnl/src/rldp/mod.rs @@ -208,7 +208,6 @@ impl RldpNode { const MAX_OUTBOUNDS_PER_PEER: u32 = 3; const SIZE_TRANSFER_WAVE: u32 = 10; const SPINNER_MS: u64 = 1; - const SPINNER_V1_SEND_MS: u64 = 10; const TIMEOUT_MAX_MS: u64 = 10000; const TIMEOUT_MIN_MS: u64 = 500; const TIMEOUT_WARN_MS: u64 = 5000; @@ -1207,16 +1206,16 @@ impl RldpNode { let start_ms = peer.stats.v1.timestamp_ms(); let mut last_warn_ms = start_ms; #[cfg(feature = "debug")] + let mut total_packets = 0; + #[cfg(feature = "debug")] let mut last_seqno = 0; - #[cfg(any(feature = "debug", feature = "telemetry"))] - let mut total_packets: u32 = 0; loop { let mut transfer_wave = transfer.start_next_part()?; if transfer_wave == 0 { #[cfg(feature = "debug")] Self::check_timestamp( &context.timestamp, - format!("Send transfer finished, packets {total_packets}").as_str(), + format!("Send transfer finished, packets {}", total_packets).as_str(), ); break; } @@ -1225,12 +1224,9 @@ impl RldpNode { let mut recv_seqno = 0; 'part: loop { for _ in 0..transfer_wave { - #[cfg(any(feature = "debug", feature = "telemetry"))] - { - total_packets += 1; - } #[cfg(feature = "debug")] { + total_packets += 1; last_seqno = transfer.state().seqno_send() } let (object, do_next) = transfer.prepare_chunk()?; @@ -1262,12 +1258,9 @@ impl RldpNode { break 'part; } } - tokio::time::timeout( - Duration::from_millis(Self::SPINNER_V1_SEND_MS), - context.pong.recv(), - ) - .await - .ok(); + tokio::time::timeout(Duration::from_millis(Self::SPINNER_MS), context.pong.recv()) + .await + .ok(); if transfer.state().is_transfer_finished_or_next_part(part)? { #[cfg(feature = "debug")] Self::check_timestamp( @@ -1294,18 +1287,11 @@ impl RldpNode { peer.stats.v1.update(min_timeout_ms); recv_seqno = new_recv_seqno; } else if peer.stats.v1.try_timeout(start_ms) { - #[cfg(feature = "telemetry")] - log::info!( - target: TARGET, - "RLDPv1 send: packets sent {total_packets} (timeout) in {transfer_str}" - ); return Ok(false); } } peer.stats.v1.update(min_timeout_ms); } - #[cfg(feature = "telemetry")] - log::info!(target: TARGET, "RLDPv1 send: packets sent {total_packets} in {transfer_str}"); Ok(true) } @@ -1319,7 +1305,7 @@ impl RldpNode { let SendTransfer::V2(part_transfers) = &mut context.send_transfer else { fail!("Unexpected V1 send transfer in V2 send loop") }; - #[cfg(any(feature = "debug", feature = "telemetry"))] + #[cfg(feature = "debug")] let total_packets = Arc::new(AtomicU32::new(0)); let progress = Arc::new(AtomicU64::new(0)); let bbr_part_states = transfer_state.clone(); @@ -1370,7 +1356,7 @@ impl RldpNode { tag: context.tag, #[cfg(feature = "debug")] timestamp: context.timestamp.clone(), - #[cfg(any(feature = "debug", feature = "telemetry"))] + #[cfg(feature = "debug")] total_packets: total_packets.clone(), transfer_str: transfer_str.clone(), }; @@ -1407,13 +1393,6 @@ impl RldpNode { } } }?; - #[cfg(feature = "telemetry")] - log::info!( - target: TARGET, - "RLDPv2 send: packets sent {} ({}) in {transfer_str}", - total_packets.load(Ordering::Relaxed), - if ok { "ok" } else { "timeout" } - ); match bbr_task.await { Err(e) => Err(e.into()), Ok(Err(e)) => Err(e), @@ -1498,7 +1477,7 @@ impl RldpNode { continue; } }; - #[cfg(any(feature = "debug", feature = "telemetry"))] + #[cfg(feature = "debug")] context.total_packets.fetch_add(1, Ordering::Relaxed); let chunk = TaggedByteSlice { object, diff --git a/src/adnl/src/rldp/send.rs b/src/adnl/src/rldp/send.rs index 08033de..6b46335 100644 --- a/src/adnl/src/rldp/send.rs +++ b/src/adnl/src/rldp/send.rs @@ -624,7 +624,7 @@ pub(crate) struct SendPartContextV2 { pub(crate) tag: u32, #[cfg(feature = "debug")] pub(crate) timestamp: Arc>, - #[cfg(any(feature = "debug", feature = "telemetry"))] + #[cfg(feature = "debug")] pub(crate) total_packets: Arc, pub(crate) transfer_str: String, } diff --git a/src/adnl/tests/test_adnl.rs b/src/adnl/tests/test_adnl.rs index 03d1d28..514a0ec 100644 --- a/src/adnl/tests/test_adnl.rs +++ b/src/adnl/tests/test_adnl.rs @@ -432,8 +432,7 @@ fn run_adnl_session(mode: SendMode, ip1: &str, ip2: &str) { let peer1 = node2 .add_peer( node2.key_by_tag(KEY_TAG).unwrap().id(), - node1.ip_address_adnl(), - None, + node1.ip_address(), &node1.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() @@ -441,8 +440,7 @@ fn run_adnl_session(mode: SendMode, ip1: &str, ip2: &str) { let peer2 = node1 .add_peer( node1.key_by_tag(KEY_TAG).unwrap().id(), - node2.ip_address_adnl(), - None, + node2.ip_address(), &node2.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() diff --git a/src/adnl/tests/test_overlay.rs b/src/adnl/tests/test_overlay.rs index 95117e7..faab407 100644 --- a/src/adnl/tests/test_overlay.rs +++ b/src/adnl/tests/test_overlay.rs @@ -98,9 +98,6 @@ pub fn build_dht_node_info_ex( } */ -/// Base port for test_broadcast nodes (range 4210..4219, does not overlap with other tests). -const BROADCAST_TEST_BASE_PORT: u16 = 4210; - fn init_overlay_simple_compatibility_test( local_ip_template: &str, #[cfg(feature = "dump")] dump_path: Option<&str>, @@ -301,7 +298,7 @@ fn test_random_peers(ctx_test: &TestContext) { AddressSearchContext::with_params(key.id(), DhtSearchPolicy::FastSearch(5)) .unwrap(); match ctx_test.dht.find_address(&mut ctx_search).await { - Ok(Some((ip, _, _))) => println!("IP {}", ip), + Ok(Some((ip, _))) => println!("IP {}", ip), Ok(None) => println!("Address not found"), Err(err) => println!("Error {}", err), } @@ -373,7 +370,7 @@ fn test_overlay_broadcast_receive(ctx_test: &TestContext) { let mut ctx_search = AddressSearchContext::with_params(key_id, DhtSearchPolicy::FastSearch(5)) .unwrap(); - if let Ok(Some((ip, _, _))) = ctx_test.dht.find_address(&mut ctx_search).await { + if let Ok(Some((ip, _))) = ctx_test.dht.find_address(&mut ctx_search).await { println!("RECEIVED new overlay node {}", key_id); ctx_test.overlay.add_public_peer(&ip, &node, &ctx_test.overlay_id).unwrap(); known_nodes.insert(key_id.clone()); @@ -670,9 +667,7 @@ fn run_propagation( .await } Protocol::TwostepSimple | Protocol::TwostepFec => { - node_send - .broadcast_twostep(&overlay_id_send, &data, None, 0, Vec::new()) - .await + node_send.broadcast_two_step(&overlay_id_send, &data, None, 0).await } } .unwrap(); @@ -682,7 +677,7 @@ fn run_propagation( "Broadcasting {}->{} packets by {adnl_id_send}/{}, step {j}\n", info.packets, info.send_to, - adnl.ip_address_adnl(), + adnl.ip_address(), ); bcast_totally.fetch_add(1, Ordering::Relaxed); } @@ -820,21 +815,21 @@ fn test_broadcast( test: impl Fn(&[LocalNode], &[Arc>>], Protocol) -> RunResult, protocol: Protocol, ) { - let min_neighbours = match protocol { - Protocol::StreamSimple | Protocol::TwostepFec => return, /* Not ready yet */ - //Protocol::TwostepFec => 4, - Protocol::TwostepSimple => 3, - _ => 1, - }; + const FIRST_PORT: usize = 4181; init_test(); let mut nodes = Vec::new(); for i in 0..n { - let port = BROADCAST_TEST_BASE_PORT + i as u16; - let ip = format!("127.0.0.1:{port}"); + let ip = format!("127.0.0.1:{}", FIRST_PORT + i); nodes.push(init_local_node(ip, 100 / n as u8)); } + let min_neighbours = match protocol { + Protocol::StreamSimple => return, /* Not ready yet */ + Protocol::TwostepFec => 4, + Protocol::TwostepSimple => 3, + _ => 1, + }; let mut neighbours = Vec::new(); for i in 0..n { let overlay_id = &nodes[i].overlay_id; @@ -980,7 +975,7 @@ fn test_overlay_ping() { AddressSearchContext::with_params(key.id(), DhtSearchPolicy::FastSearch(5)) .unwrap(); match ctx_test.dht.find_address(&mut ctx_search).await { - Ok(Some((ip, _, _))) => { + Ok(Some((ip, _))) => { println!("IP {}", ip); let node = node.into_boxed(); let node_encoded = base64_encode(&serialize_boxed(&node).unwrap()); @@ -1244,7 +1239,6 @@ fn test_stop() { params, &ctx_test.adnl.key_by_tag(KEY_TAG_OVERLAY).unwrap(), &Vec::new(), - false, ) .unwrap(); assert!(added); diff --git a/src/adnl/tests/test_quic.rs b/src/adnl/tests/test_quic.rs index cab30a6..308365e 100644 --- a/src/adnl/tests/test_quic.rs +++ b/src/adnl/tests/test_quic.rs @@ -8,13 +8,12 @@ */ use adnl::{ - common::{AdnlPeers, QueryResult, Subscriber, Version}, - node::{AdnlNode, IpAddress}, - DhtNode, OverlayNode, QuicNode, + common::{AdnlPeers, QueryResult, Subscriber}, + node::AdnlNodeConfig, + QuicNode, }; use std::{ - collections::HashSet, - net::Ipv4Addr, + net::SocketAddr, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -25,12 +24,7 @@ use tokio_util::sync::CancellationToken; use ton_api::{ deserialize_boxed, serialize_boxed, ton::{ - adnl::{ - address::address::{Quic, Udp}, - addresslist::AddressList, - pong::Pong as AdnlPong, - Address, Pong as AdnlPongBoxed, - }, + adnl::{pong::Pong as AdnlPong, Pong as AdnlPongBoxed}, quic::{request::Query as QuicQuery, Response as QuicResponse}, rpc::adnl::Ping as AdnlPing, }, @@ -38,33 +32,15 @@ use ton_api::{ }; use ton_block::{ ed25519_encode_private_key_to_pkcs8, ed25519_generate_private_key, Ed25519KeyOption, KeyId, + Result, }; -include!("../../common/src/config.rs"); include!("../../common/src/test.rs"); const KEY_TAG: usize = 0; const ITERATIONS: usize = 3; const MSG_PAYLOAD: &[u8] = b"quic test payload"; -fn ip_address_to_socket_addr(ip: &IpAddress) -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip.ip())), ip.port()) -} - -/// Helper: build an AddressList with the given addresses and current version. -fn make_address_list(addrs: Vec
) -> AddressList { - let version = Version::get(); - AddressList { addrs: addrs.into(), version, reinit_date: version, priority: 0, expire_at: 0 } -} - -fn udp_addr(ip: u32, port: u16) -> Address { - Address::Adnl_Address_Udp(Udp { ip: ip as i32, port: port as i32 }) -} - -fn quic_addr(ip: u32, port: u16) -> Address { - Address::Adnl_Address_Quic(Quic { ip: ip as i32, port: port as i32 }) -} - /// Routes messages and queries to a channel only when addressed to `key_id`. struct TestSubscriber { key_id: Arc, @@ -112,8 +88,8 @@ fn make_ping_wire(value: i64) -> Vec { serialize_boxed(&QuicQuery { data: make_ping_data(value).into() }.into_boxed()).unwrap() } -fn parse_pong(data: Vec) -> i64 { - deserialize_boxed(&data).unwrap().downcast::().unwrap().only().value +fn parse_pong(obj: TLObject) -> i64 { + obj.downcast::().unwrap().only().value } /// Parse pong from raw wire bytes (for low-level stream tests) @@ -153,12 +129,7 @@ fn test_quic_concurrent_accept() { let server_bind: SocketAddr = format!("127.0.0.1:{}", SERVER_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let server = QuicNode::new( - vec![server_sub], - server_token.clone(), - None, - tokio::runtime::Handle::current(), - ); + let server = QuicNode::new(vec![server_sub], server_token.clone()); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); // --- clients --- @@ -186,8 +157,7 @@ fn test_quic_concurrent_accept() { let bind: SocketAddr = format!("127.0.0.1:{}", port + QuicNode::OFFSET_PORT).parse().unwrap(); let token = CancellationToken::new(); - let quic = - QuicNode::new(vec![sub], token.clone(), None, tokio::runtime::Handle::current()); + let quic = QuicNode::new(vec![sub], token.clone()); quic.add_key(&key, &key_id, bind).unwrap(); quic.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(key_id.clone(), bind).unwrap(); @@ -200,11 +170,12 @@ fn test_quic_concurrent_accept() { let mut handles = Vec::with_capacity(NUM_CLIENTS); for (i, client) in clients.iter().enumerate() { let quic = client.quic.clone(); - let peers = AdnlPeers::with_keys(client.key_id.clone(), server_key_id.clone()); + let local = client.key_id.clone(); + let remote = server_key_id.clone(); let value = i as i64; handles.push(tokio::spawn(async move { let resp = quic - .query(make_ping_data(value), None, &peers, None) + .query(make_ping_data(value), None, &local, &remote, None) .await .unwrap_or_else(|e| panic!("client {i} query failed: {e}")); let pong = parse_pong(resp.unwrap()); @@ -246,7 +217,7 @@ fn test_quic_concurrent_accept() { #[test] fn test_quic_session() { init_test_log(); - let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); rt.block_on(async { let token_a = CancellationToken::new(); let token_b = CancellationToken::new(); @@ -282,35 +253,37 @@ fn test_quic_session() { let bind_a: SocketAddr = "127.0.0.1:5600".parse().unwrap(); let bind_b: SocketAddr = "127.0.0.1:5601".parse().unwrap(); - let quic_a = - QuicNode::new(vec![sub_a], token_a.clone(), None, tokio::runtime::Handle::current()); + let quic_a = QuicNode::new(vec![sub_a], token_a.clone()); quic_a.add_key(&key_bytes_a, &key_id_a, bind_a).unwrap(); - let quic_b = - QuicNode::new(vec![sub_b], token_b.clone(), None, tokio::runtime::Handle::current()); + let quic_b = QuicNode::new(vec![sub_b], token_b.clone()); quic_b.add_key(&key_bytes_b, &key_id_b, bind_b).unwrap(); // Register peer addresses quic_a.add_peer_key(key_id_b.clone(), "127.0.0.1:5601".parse().unwrap()).unwrap(); quic_b.add_peer_key(key_id_a.clone(), "127.0.0.1:5600".parse().unwrap()).unwrap(); - let peers_ab = AdnlPeers::with_keys(key_id_a.clone(), key_id_b.clone()); - let peers_ba = AdnlPeers::with_keys(key_id_b.clone(), key_id_a.clone()); for i in 0..ITERATIONS { let value = i as i64; // A โ†’ B: query - let resp = - quic_a.query(make_ping_data(value), None, &peers_ab, None).await.unwrap().unwrap(); + let resp = quic_a + .query(make_ping_data(value), None, &key_id_a, &key_id_b, None) + .await + .unwrap() + .unwrap(); assert_eq!(parse_pong(resp), value, "Aโ†’B query iter {i}: pong mismatch"); // B โ†’ A: query - let resp = - quic_b.query(make_ping_data(value), None, &peers_ba, None).await.unwrap().unwrap(); + let resp = quic_b + .query(make_ping_data(value), None, &key_id_b, &key_id_a, None) + .await + .unwrap() + .unwrap(); assert_eq!(parse_pong(resp), value, "Bโ†’A query iter {i}: pong mismatch"); // A โ†’ B: message - quic_a.message(MSG_PAYLOAD.to_vec(), None, &peers_ab).await.unwrap(); + quic_a.message(MSG_PAYLOAD.to_vec(), None, &key_id_a, &key_id_b).await.unwrap(); assert_eq!( recv_with_timeout(&mut rx_b).await, MSG_PAYLOAD, @@ -318,7 +291,7 @@ fn test_quic_session() { ); // B โ†’ A: message - quic_b.message(MSG_PAYLOAD.to_vec(), None, &peers_ba).await.unwrap(); + quic_b.message(MSG_PAYLOAD.to_vec(), None, &key_id_b, &key_id_a).await.unwrap(); assert_eq!( recv_with_timeout(&mut rx_a).await, MSG_PAYLOAD, @@ -361,12 +334,7 @@ fn test_quic_reconnect_after_server_restart() { let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) as Arc; - let client = QuicNode::new( - vec![client_sub], - client_token.clone(), - None, - tokio::runtime::Handle::current(), - ); + let client = QuicNode::new(vec![client_sub], client_token.clone()); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); // --- server B1 (will be shut down) --- @@ -385,25 +353,21 @@ fn test_quic_reconnect_after_server_restart() { Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx1 }) as Arc; - let server1 = QuicNode::new( - vec![server_sub1], - server_token1.clone(), - None, - tokio::runtime::Handle::current(), - ); + let server1 = QuicNode::new(vec![server_sub1], server_token1.clone()); server1.add_key(&server_key, &server_key_id, server_bind).unwrap(); // Register peer keys client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server1.add_peer_key(client_key_id.clone(), client_bind).unwrap(); - let peers = AdnlPeers::with_keys(client_key_id.clone(), server_key_id.clone()); // Step 1: successful ping/pong through B1 - let resp = - tokio::time::timeout(TIMEOUT, client.query(make_ping_data(1), None, &peers, None)) - .await - .expect("initial query timed out") - .expect("initial query failed"); + let resp = tokio::time::timeout( + TIMEOUT, + client.query(make_ping_data(1), None, &client_key_id, &server_key_id, None), + ) + .await + .expect("initial query timed out") + .expect("initial query failed"); assert_eq!(parse_pong(resp.unwrap()), 1, "initial pong mismatch"); println!("Step 1: initial ping/pong succeeded"); @@ -423,22 +387,19 @@ fn test_quic_reconnect_after_server_restart() { Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx2 }) as Arc; - let server2 = QuicNode::new( - vec![server_sub2], - server_token2.clone(), - None, - tokio::runtime::Handle::current(), - ); + let server2 = QuicNode::new(vec![server_sub2], server_token2.clone()); server2.add_key(&server_key, &server_key_id, server_bind).unwrap(); server2.add_peer_key(client_key_id.clone(), client_bind).unwrap(); println!("Step 3: server B2 started on same port with same key"); // Step 4: client sends another query โ€” should remove dead conn, reconnect, and succeed - let resp = - tokio::time::timeout(TIMEOUT, client.query(make_ping_data(2), None, &peers, None)) - .await - .expect("reconnect query timed out โ€” dead connection removal may be broken") - .expect("reconnect query failed"); + let resp = tokio::time::timeout( + TIMEOUT, + client.query(make_ping_data(2), None, &client_key_id, &server_key_id, None), + ) + .await + .expect("reconnect query timed out โ€” dead connection removal may be broken") + .expect("reconnect query failed"); assert_eq!(parse_pong(resp.unwrap()), 2, "reconnect pong mismatch"); println!("Step 4: reconnect ping/pong succeeded"); @@ -529,7 +490,7 @@ fn test_quic_stream_limit() { let server_bind: SocketAddr = format!("127.0.0.1:{}", SERVER_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); let server = - QuicNode::new(vec![server_sub], server_token.clone(), Some(STREAM_LIMIT), tokio::runtime::Handle::current()); + QuicNode::with_stream_limit(vec![server_sub], server_token.clone(), STREAM_LIMIT); server.add_key(&server_key, &server_key_id, server_bind).unwrap(); // --- client (normal limits) --- @@ -548,18 +509,17 @@ fn test_quic_stream_limit() { let client_bind: SocketAddr = format!("127.0.0.1:{}", CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let client = QuicNode::new(vec![client_sub], client_token.clone(), None, tokio::runtime::Handle::current()); + let client = QuicNode::new(vec![client_sub], client_token.clone()); client.add_key(&client_key, &client_key_id, client_bind).unwrap(); // Register peers client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(client_key_id.clone(), client_bind).unwrap(); - let peers = AdnlPeers::with_keys(client_key_id.clone(), server_key_id.clone()); // Establish the connection with a ping/pong first let resp = tokio::time::timeout( Duration::from_secs(10), - client.query(make_ping_data(42), None, &peers, None), + client.query(make_ping_data(42), None, &client_key_id, &server_key_id, None), ) .await .expect("initial query timed out") @@ -570,10 +530,11 @@ fn test_quic_stream_limit() { let mut handles = Vec::with_capacity(NUM_MESSAGES); for i in 0..NUM_MESSAGES { let quic = client.clone(); - let peers = peers.clone(); + let local = client_key_id.clone(); + let remote = server_key_id.clone(); handles.push(tokio::spawn(async move { let payload = format!("msg-{i}"); - quic.message(payload.as_bytes().to_vec(), None, &peers) + quic.message(payload.as_bytes().to_vec(), None, &local, &remote) .await .unwrap_or_else(|e| panic!("message {i} failed: {e}")); })); @@ -632,7 +593,7 @@ fn make_endpoint( let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let sub = Arc::new(TestSubscriber { key_id: key_id.clone(), msg_tx: tx }) as Arc; - let quic = QuicNode::new(vec![sub], token.clone(), None, tokio::runtime::Handle::current()); + let quic = QuicNode::new(vec![sub], token.clone()); quic.add_key(&key, &key_id, bind).unwrap(); (quic, key, key_id, bind, token) } @@ -754,19 +715,22 @@ fn test_quic_duplicate_inbound_resolution() { server.add_peer_key(c1_key_id.clone(), c1_bind).unwrap(); server.add_peer_key(c2_key_id.clone(), c2_bind).unwrap(); - let peers1 = AdnlPeers::with_keys(c1_key_id.clone(), server_key_id.clone()); - let peers2 = AdnlPeers::with_keys(c2_key_id.clone(), server_key_id.clone()); - // Step 1: both clients connect concurrently let h1 = { let q = client1.clone(); - let peers = peers1.clone(); - tokio::spawn(async move { q.query(make_ping_data(1), None, &peers, None).await }) + let local = c1_key_id.clone(); + let remote = server_key_id.clone(); + tokio::spawn( + async move { q.query(make_ping_data(1), None, &local, &remote, None).await }, + ) }; let h2 = { let q = client2.clone(); - let peers = peers2.clone(); - tokio::spawn(async move { q.query(make_ping_data(2), None, &peers, None).await }) + let local = c2_key_id.clone(); + let remote = server_key_id.clone(); + tokio::spawn( + async move { q.query(make_ping_data(2), None, &local, &remote, None).await }, + ) }; let (r1, r2) = tokio::time::timeout(TIMEOUT, async { tokio::join!(h1, h2) }) @@ -782,18 +746,22 @@ fn test_quic_duplicate_inbound_resolution() { tokio::time::sleep(Duration::from_secs(3)).await; // Step 3: both clients should still be able to query (through surviving connections) - let resp1 = - tokio::time::timeout(TIMEOUT, client1.query(make_ping_data(10), None, &peers1, None)) - .await - .expect("post-resolution query1 timed out") - .expect("post-resolution query1 failed"); + let resp1 = tokio::time::timeout( + TIMEOUT, + client1.query(make_ping_data(10), None, &c1_key_id, &server_key_id, None), + ) + .await + .expect("post-resolution query1 timed out") + .expect("post-resolution query1 failed"); assert_eq!(parse_pong(resp1.unwrap()), 10); - let resp2 = - tokio::time::timeout(TIMEOUT, client2.query(make_ping_data(20), None, &peers2, None)) - .await - .expect("post-resolution query2 timed out") - .expect("post-resolution query2 failed"); + let resp2 = tokio::time::timeout( + TIMEOUT, + client2.query(make_ping_data(20), None, &c2_key_id, &server_key_id, None), + ) + .await + .expect("post-resolution query2 timed out") + .expect("post-resolution query2 failed"); assert_eq!(parse_pong(resp2.unwrap()), 20); println!("Step 3: both clients still functional after duplicate resolution"); @@ -917,280 +885,6 @@ fn test_quic_duplicate_inbound_same_address() { }); } -// =========================================================================== -// Test 1b: Multiple keys from same address must coexist -// =========================================================================== - -/// A TON node may have multiple connections to each peer โ€” one for the -/// current validator key and one for the next key. Both connections originate -/// from the same source address but use different client Ed25519 keys. -/// They must coexist: the server must NOT close one when the other arrives. -/// -/// This test opens two raw quinn connections from the same UDP endpoint with -/// different Ed25519 RPK identities. After waiting past the duplicate-resolution -/// window, BOTH connections must still be alive and answer queries. -#[test] -fn test_quic_multi_key_same_address() { - init_test_log(); - let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); - rt.block_on(async { - const SERVER_PORT: u16 = 5915; - const RAW_CLIENT_PORT: u16 = 5916; - - // --- server --- - let (server, _server_key, server_key_id, server_bind, server_token) = - make_endpoint(SERVER_PORT); - - // --- two different client keys (simulating current + next validator keys) --- - let key1 = ed25519_generate_private_key().unwrap().to_bytes(); - let key2 = ed25519_generate_private_key().unwrap().to_bytes(); - - let (_, cfg1) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{RAW_CLIENT_PORT}"), - vec![(key1, KEY_TAG)], - ) - .unwrap(); - let key1_id = cfg1.key_by_tag(KEY_TAG).unwrap().id().clone(); - - let (_, cfg2) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{RAW_CLIENT_PORT}"), - vec![(key2, KEY_TAG)], - ) - .unwrap(); - let key2_id = cfg2.key_by_tag(KEY_TAG).unwrap().id().clone(); - - server - .add_peer_key( - key1_id.clone(), - format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(), - ) - .unwrap(); - server - .add_peer_key( - key2_id.clone(), - format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(), - ) - .unwrap(); - - // Build two different quinn client configs (different RPK identities) - let client_config1 = build_raw_quinn_client(&key1); - let client_config2 = build_raw_quinn_client(&key2); - - // Create a single raw quinn endpoint (both connections share the same source addr) - let raw_bind: SocketAddr = - format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - let sock = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::DGRAM, - Some(socket2::Protocol::UDP), - ) - .unwrap(); - sock.set_reuse_address(true).unwrap(); - sock.bind(&raw_bind.into()).unwrap(); - sock.set_nonblocking(true).unwrap(); - let udp = std::net::UdpSocket::from(sock); - let runtime: Arc = Arc::new(quinn::TokioRuntime); - let endpoint = - quinn::Endpoint::new(quinn::EndpointConfig::default(), None, udp, runtime).unwrap(); - - // SNI name matching QuicNode's key_id_to_server_name - let hex = hex::encode(server_key_id.data()); - let sni = format!("{}.{}", &hex[..32], &hex[32..]); - - // Open connection 1 with key1 - let conn1 = endpoint - .connect_with(client_config1, server_bind, &sni) - .unwrap() - .await - .expect("conn1 (key1) handshake failed"); - - // Open connection 2 with key2 (same source address, different identity) - let conn2 = endpoint - .connect_with(client_config2, server_bind, &sni) - .unwrap() - .await - .expect("conn2 (key2) handshake failed"); - - println!("Two connections established from same address with different keys"); - - // Verify both work immediately - let ping1 = make_ping_wire(101); - let (mut s1, mut r1) = conn1.open_bi().await.unwrap(); - s1.write_all(&ping1).await.unwrap(); - s1.finish().unwrap(); - let resp1 = tokio::time::timeout(Duration::from_secs(10), r1.read_to_end(1 << 20)) - .await - .expect("conn1 response timed out") - .expect("conn1 read failed"); - assert_eq!(parse_pong_wire(&resp1), 101); - println!("conn1 (key1) ping/pong OK"); - - let ping2 = make_ping_wire(102); - let (mut s2, mut r2) = conn2.open_bi().await.unwrap(); - s2.write_all(&ping2).await.unwrap(); - s2.finish().unwrap(); - let resp2 = tokio::time::timeout(Duration::from_secs(10), r2.read_to_end(1 << 20)) - .await - .expect("conn2 response timed out") - .expect("conn2 read failed"); - assert_eq!(parse_pong_wire(&resp2), 102); - println!("conn2 (key2) ping/pong OK"); - - // Wait past the maximum duplicate-resolution window (2500ms + margin) - tokio::time::sleep(Duration::from_secs(4)).await; - - // BOTH connections must still be alive โ€” this is the key assertion. - // With the old SocketAddr-based keying, one would have been killed. - assert!( - conn1.close_reason().is_none(), - "conn1 (key1) was closed โ€” multi-key coexistence broken!" - ); - assert!( - conn2.close_reason().is_none(), - "conn2 (key2) was closed โ€” multi-key coexistence broken!" - ); - - // Both must still answer queries - let ping3 = make_ping_wire(201); - let (mut s3, mut r3) = conn1.open_bi().await.expect("conn1 should still accept streams"); - s3.write_all(&ping3).await.unwrap(); - s3.finish().unwrap(); - let resp3 = tokio::time::timeout(Duration::from_secs(10), r3.read_to_end(1 << 20)) - .await - .expect("conn1 post-wait response timed out") - .expect("conn1 post-wait read failed"); - assert_eq!(parse_pong_wire(&resp3), 201); - - let ping4 = make_ping_wire(202); - let (mut s4, mut r4) = conn2.open_bi().await.expect("conn2 should still accept streams"); - s4.write_all(&ping4).await.unwrap(); - s4.finish().unwrap(); - let resp4 = tokio::time::timeout(Duration::from_secs(10), r4.read_to_end(1 << 20)) - .await - .expect("conn2 post-wait response timed out") - .expect("conn2 post-wait read failed"); - assert_eq!(parse_pong_wire(&resp4), 202); - - println!("PASS: both connections survived duplicate-resolution window"); - - // --- cleanup --- - conn1.close(0u32.into(), b"done"); - conn2.close(0u32.into(), b"done"); - endpoint.close(0u32.into(), b"done"); - server.shutdown(); - server_token.cancel(); - }); -} - -// =========================================================================== -// Test 1c: Same-key duplicate connections must be deduplicated -// =========================================================================== - -/// When two connections arrive from the same client key pair (genuine duplicate), -/// the server must close the old one after the resolution window. This verifies -/// that deduplication still works correctly with the AdnlPath-based keying. -#[test] -fn test_quic_same_key_deduplication() { - init_test_log(); - let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); - rt.block_on(async { - const SERVER_PORT: u16 = 5917; - const RAW_CLIENT_PORT: u16 = 5918; - - // --- server --- - let (server, _server_key, server_key_id, server_bind, server_token) = - make_endpoint(SERVER_PORT); - - // --- single client key (both connections use the same identity) --- - let key = ed25519_generate_private_key().unwrap().to_bytes(); - let (_, cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{RAW_CLIENT_PORT}"), - vec![(key, KEY_TAG)], - ) - .unwrap(); - let key_id = cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); - let raw_bind: SocketAddr = - format!("127.0.0.1:{}", RAW_CLIENT_PORT + QuicNode::OFFSET_PORT).parse().unwrap(); - - server.add_peer_key(key_id.clone(), raw_bind).unwrap(); - - let client_config = build_raw_quinn_client(&key); - - // Create raw quinn endpoint - let sock = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::DGRAM, - Some(socket2::Protocol::UDP), - ) - .unwrap(); - sock.set_reuse_address(true).unwrap(); - sock.bind(&raw_bind.into()).unwrap(); - sock.set_nonblocking(true).unwrap(); - let udp = std::net::UdpSocket::from(sock); - let runtime: Arc = Arc::new(quinn::TokioRuntime); - let mut endpoint = - quinn::Endpoint::new(quinn::EndpointConfig::default(), None, udp, runtime).unwrap(); - endpoint.set_default_client_config(client_config); - - let hex = hex::encode(server_key_id.data()); - let sni = format!("{}.{}", &hex[..32], &hex[32..]); - - // Open first connection, verify it works - let conn1 = - endpoint.connect(server_bind, &sni).unwrap().await.expect("conn1 handshake failed"); - - let ping1 = make_ping_wire(301); - let (mut s1, mut r1) = conn1.open_bi().await.unwrap(); - s1.write_all(&ping1).await.unwrap(); - s1.finish().unwrap(); - let resp1 = tokio::time::timeout(Duration::from_secs(10), r1.read_to_end(1 << 20)) - .await - .expect("conn1 response timed out") - .expect("conn1 read failed"); - assert_eq!(parse_pong_wire(&resp1), 301); - println!("conn1 ping/pong OK"); - - // Open second connection with the SAME key (genuine duplicate) - let conn2 = - endpoint.connect(server_bind, &sni).unwrap().await.expect("conn2 handshake failed"); - - let ping2 = make_ping_wire(302); - let (mut s2, mut r2) = conn2.open_bi().await.unwrap(); - s2.write_all(&ping2).await.unwrap(); - s2.finish().unwrap(); - let resp2 = tokio::time::timeout(Duration::from_secs(10), r2.read_to_end(1 << 20)) - .await - .expect("conn2 response timed out") - .expect("conn2 read failed"); - assert_eq!(parse_pong_wire(&resp2), 302); - println!("conn2 ping/pong OK"); - - // Wait past the duplicate-resolution window (max 2500ms + margin) - tokio::time::sleep(Duration::from_secs(4)).await; - - // The old connection (conn1) should have been closed by duplicate resolution. - // Check by trying to open a stream โ€” if the connection was closed, this fails. - let conn1_alive = conn1.close_reason().is_none() && conn1.open_bi().await.is_ok(); - let conn2_alive = conn2.close_reason().is_none() && conn2.open_bi().await.is_ok(); - - println!("After dedup: conn1_alive={conn1_alive}, conn2_alive={conn2_alive}"); - - // Exactly one should have been closed (the old one). - // The new connection (conn2) must survive. - assert!(conn2_alive, "conn2 (newer) should survive deduplication"); - assert!(!conn1_alive, "conn1 (older) should have been closed by deduplication"); - - println!("PASS: same-key duplicate was correctly deduplicated"); - - // --- cleanup --- - conn1.close(0u32.into(), b"done"); - conn2.close(0u32.into(), b"done"); - endpoint.close(0u32.into(), b"done"); - server.shutdown(); - server_token.cancel(); - }); -} - // =========================================================================== // Test 2: Stream timeout handling // =========================================================================== @@ -1280,12 +974,7 @@ fn test_quic_stream_read_timeout() { let resp = tokio::time::timeout( Duration::from_secs(10), - normal_client.query( - make_ping_data(777), - None, - &AdnlPeers::with_keys(nc_key_id.clone(), server_key_id.clone()), - None, - ), + normal_client.query(make_ping_data(777), None, &nc_key_id, &server_key_id, None), ) .await .expect("normal client query timed out") @@ -1449,12 +1138,7 @@ fn test_quic_reject_non_rpk_client() { let resp = tokio::time::timeout( Duration::from_secs(10), - legit.query( - make_ping_data(42), - None, - &AdnlPeers::with_keys(lk_id.clone(), server_key_id.clone()), - None, - ), + legit.query(make_ping_data(42), None, &lk_id, &server_key_id, None), ) .await .expect("legit query timed out after rogue attempt") @@ -1506,12 +1190,7 @@ fn test_quic_rpk_identity_mismatch() { // peer_key_id == expected dst, and they won't match. let result = tokio::time::timeout( Duration::from_secs(10), - client.query( - make_ping_data(1), - None, - &AdnlPeers::with_keys(client_key_id.clone(), fake_key_id.clone()), - None, - ), + client.query(make_ping_data(1), None, &client_key_id, &fake_key_id, None), ) .await; @@ -1584,8 +1263,7 @@ fn test_quic_connection_pool_exhaustion() { let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let sub = Arc::new(TestSubscriber { key_id: key_id.clone(), msg_tx: tx }) as Arc; - let quic = - QuicNode::new(vec![sub], token.clone(), None, tokio::runtime::Handle::current()); + let quic = QuicNode::new(vec![sub], token.clone()); quic.add_key(&key, &key_id, bind).unwrap(); quic.add_peer_key(server_key_id.clone(), server_bind).unwrap(); server.add_peer_key(key_id.clone(), bind).unwrap(); @@ -1597,11 +1275,12 @@ fn test_quic_connection_pool_exhaustion() { let mut handles = Vec::with_capacity(NUM_CLIENTS); for (i, client) in clients.iter().enumerate() { let quic = client.quic.clone(); - let peers = AdnlPeers::with_keys(client.key_id.clone(), server_key_id.clone()); + let local = client.key_id.clone(); + let remote = server_key_id.clone(); let value = i as i64; handles.push(tokio::spawn(async move { let resp = quic - .query(make_ping_data(value), None, &peers, None) + .query(make_ping_data(value), None, &local, &remote, None) .await .unwrap_or_else(|e| panic!("client {i} query failed: {e}")); assert_eq!(parse_pong(resp.unwrap()), value, "client {i}: pong mismatch"); @@ -1640,12 +1319,7 @@ fn test_quic_connection_pool_exhaustion() { let resp = tokio::time::timeout( Duration::from_secs(10), - fresh.query( - make_ping_data(12345), - None, - &AdnlPeers::with_keys(fk_id.clone(), server_key_id.clone()), - None, - ), + fresh.query(make_ping_data(12345), None, &fk_id, &server_key_id, None), ) .await .expect("fresh client query timed out after pool exhaust") @@ -1661,545 +1335,3 @@ fn test_quic_connection_pool_exhaustion() { server_token.cancel(); }); } - -/// Fire messages through a server restart cycle. Verifies the sender task -/// drains the queue after reconnection without hanging or losing messages. -/// In the old hot-loop design, the yield_now() spins would starve the runtime. -#[test] -fn test_quic_message_burst_reconnect() { - init_test_log(); - let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); - rt.block_on(async { - const CLIENT_PORT: u16 = 8100; - const SERVER_PORT: u16 = 8101; - const BURST_SIZE: usize = 50; - - let client_bind: SocketAddr = format!("127.0.0.1:{CLIENT_PORT}").parse().unwrap(); - let server_bind: SocketAddr = format!("127.0.0.1:{SERVER_PORT}").parse().unwrap(); - - let client_token = CancellationToken::new(); - let client_key = ed25519_generate_private_key().unwrap().to_bytes(); - let (_, client_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{CLIENT_PORT}"), - vec![(client_key, KEY_TAG)], - ) - .unwrap(); - let client_key_id = client_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); - - let (cli_tx, _cli_rx) = tokio::sync::mpsc::unbounded_channel(); - let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) - as Arc; - let client = QuicNode::new( - vec![client_sub], - client_token.clone(), - None, - tokio::runtime::Handle::current(), - ); - client.add_key(&client_key, &client_key_id, client_bind).unwrap(); - - let server_key = ed25519_generate_private_key().unwrap().to_bytes(); - let (_, server_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{SERVER_PORT}"), - vec![(server_key, KEY_TAG)], - ) - .unwrap(); - let server_key_id = server_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); - - // --- Phase 1: first server instance --- - let srv_token1 = CancellationToken::new(); - let (srv_tx1, mut srv_rx1) = tokio::sync::mpsc::unbounded_channel(); - let srv_sub1 = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx1 }) - as Arc; - let server1 = QuicNode::new( - vec![srv_sub1], - srv_token1.clone(), - None, - tokio::runtime::Handle::current(), - ); - server1.add_key(&server_key, &server_key_id, server_bind).unwrap(); - - client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); - server1.add_peer_key(client_key_id.clone(), client_bind).unwrap(); - let peers = AdnlPeers::with_keys(client_key_id.clone(), server_key_id.clone()); - - for i in 0..BURST_SIZE { - let payload = format!("msg-phase1-{i}").into_bytes(); - client.message(payload, None, &peers).await.unwrap(); - } - - let expected_p1: HashSet> = - (0..BURST_SIZE).map(|i| format!("msg-phase1-{i}").into_bytes()).collect(); - let mut got_p1 = HashSet::new(); - let deadline = tokio::time::Instant::now() + Duration::from_secs(15); - while got_p1.len() < BURST_SIZE { - match tokio::time::timeout_at(deadline, srv_rx1.recv()).await { - Ok(Some(data)) => { - got_p1.insert(data); - } - _ => break, - } - } - println!("Phase 1: received {}/{BURST_SIZE} unique messages", got_p1.len()); - assert_eq!( - got_p1, expected_p1, - "Phase 1 must deliver every distinct message (at-least-once guarantee)" - ); - - // --- Phase 2: restart server, send another burst --- - server1.shutdown(); - srv_token1.cancel(); - drop(server1); - tokio::time::sleep(Duration::from_millis(1000)).await; - - let srv_token2 = CancellationToken::new(); - let (srv_tx2, mut srv_rx2) = tokio::sync::mpsc::unbounded_channel(); - let srv_sub2 = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx2 }) - as Arc; - let server2 = QuicNode::new( - vec![srv_sub2], - srv_token2.clone(), - None, - tokio::runtime::Handle::current(), - ); - server2.add_key(&server_key, &server_key_id, server_bind).unwrap(); - server2.add_peer_key(client_key_id.clone(), client_bind).unwrap(); - - for i in 0..BURST_SIZE { - let payload = format!("msg-phase2-{i}").into_bytes(); - client.message(payload, None, &peers).await.unwrap(); - } - - let expected_p2: HashSet> = - (0..BURST_SIZE).map(|i| format!("msg-phase2-{i}").into_bytes()).collect(); - let mut got_p2 = HashSet::new(); - let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - while got_p2.len() < BURST_SIZE { - match tokio::time::timeout_at(deadline, srv_rx2.recv()).await { - Ok(Some(data)) => { - got_p2.insert(data); - } - _ => break, - } - } - println!( - "Phase 2: received {}/{BURST_SIZE} unique messages after server restart", - got_p2.len() - ); - assert_eq!( - got_p2, expected_p2, - "Phase 2 must deliver every distinct message after restart (at-least-once guarantee)" - ); - - client.shutdown(); - server2.shutdown(); - client_token.cancel(); - srv_token2.cancel(); - }); -} - -/// Concurrent message senders to the same peer must not deadlock or starve -/// the Tokio runtime. Uses only 2 worker threads to make thread starvation -/// from the old yield_now() hot loops detectable. -#[test] -fn test_quic_single_sender_invariant() { - init_test_log(); - let rt = - tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap(); - rt.block_on(async { - const CLIENT_PORT: u16 = 8200; - const SERVER_PORT: u16 = 8201; - const NUM_SENDERS: usize = 20; - const MSGS_PER_SENDER: usize = 5; - const TOTAL_MSGS: usize = NUM_SENDERS * MSGS_PER_SENDER; - const TIMEOUT: Duration = Duration::from_secs(20); - - let client_bind: SocketAddr = format!("127.0.0.1:{CLIENT_PORT}").parse().unwrap(); - let server_bind: SocketAddr = format!("127.0.0.1:{SERVER_PORT}").parse().unwrap(); - - let client_token = CancellationToken::new(); - let client_key = ed25519_generate_private_key().unwrap().to_bytes(); - let (_, client_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{CLIENT_PORT}"), - vec![(client_key, KEY_TAG)], - ) - .unwrap(); - let client_key_id = client_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); - - let (cli_tx, _cli_rx) = tokio::sync::mpsc::unbounded_channel(); - let client_sub = Arc::new(TestSubscriber { key_id: client_key_id.clone(), msg_tx: cli_tx }) - as Arc; - let client = QuicNode::new( - vec![client_sub], - client_token.clone(), - None, - tokio::runtime::Handle::current(), - ); - client.add_key(&client_key, &client_key_id, client_bind).unwrap(); - - let srv_token = CancellationToken::new(); - let server_key = ed25519_generate_private_key().unwrap().to_bytes(); - let (_, server_cfg) = AdnlNodeConfig::from_ip_address_and_private_keys( - &format!("127.0.0.1:{SERVER_PORT}"), - vec![(server_key, KEY_TAG)], - ) - .unwrap(); - let server_key_id = server_cfg.key_by_tag(KEY_TAG).unwrap().id().clone(); - - let (srv_tx, mut srv_rx) = tokio::sync::mpsc::unbounded_channel(); - let srv_sub = Arc::new(TestSubscriber { key_id: server_key_id.clone(), msg_tx: srv_tx }) - as Arc; - let server = QuicNode::new( - vec![srv_sub], - srv_token.clone(), - None, - tokio::runtime::Handle::current(), - ); - server.add_key(&server_key, &server_key_id, server_bind).unwrap(); - - client.add_peer_key(server_key_id.clone(), server_bind).unwrap(); - server.add_peer_key(client_key_id.clone(), client_bind).unwrap(); - - let expected: HashSet> = (0..NUM_SENDERS) - .flat_map(|s| { - (0..MSGS_PER_SENDER).map(move |m| format!("sender-{s}-msg-{m}").into_bytes()) - }) - .collect(); - let got = Arc::new(tokio::sync::Mutex::new(HashSet::new())); - let got_clone = got.clone(); - let drain_handle = tokio::spawn(async move { - while let Some(data) = srv_rx.recv().await { - got_clone.lock().await.insert(data); - } - }); - - let mut handles = Vec::with_capacity(NUM_SENDERS); - for sender_id in 0..NUM_SENDERS { - let quic = client.clone(); - let src = client_key_id.clone(); - let dst = server_key_id.clone(); - handles.push(tokio::spawn(async move { - for msg_id in 0..MSGS_PER_SENDER { - let payload = format!("sender-{sender_id}-msg-{msg_id}").into_bytes(); - let peers = AdnlPeers::with_keys(src.clone(), dst.clone()); - if let Err(e) = quic.message(payload, None, &peers).await { - eprintln!("sender {sender_id} msg {msg_id} failed: {e}"); - } - } - })); - } - - let send_result = tokio::time::timeout(TIMEOUT, async { - for h in handles { - h.await.expect("sender task panicked"); - } - }) - .await; - assert!(send_result.is_ok(), "Concurrent senders timed out โ€” possible hot-loop regression"); - - let recv_deadline = tokio::time::Instant::now() + Duration::from_secs(30); - loop { - let unique_count = got.lock().await.len(); - if unique_count >= TOTAL_MSGS { - break; - } - if tokio::time::Instant::now() >= recv_deadline { - break; - } - tokio::time::sleep(Duration::from_millis(200)).await; - } - - let received = got.lock().await; - println!( - "Single-sender invariant: {}/{TOTAL_MSGS} unique messages delivered \ - by {NUM_SENDERS} concurrent senders on 2 Tokio threads", - received.len() - ); - assert_eq!( - *received, expected, - "All {TOTAL_MSGS} distinct messages must be delivered (at-least-once guarantee)" - ); - - client.shutdown(); - server.shutdown(); - client_token.cancel(); - srv_token.cancel(); - drain_handle.abort(); - }); -} - -// --- TL serialization round-trip --- - -#[test] -fn test_quic_address_tl_roundtrip() { - let ip: u32 = u32::from(Ipv4Addr::new(1, 2, 3, 4)); - let port: u16 = 12345; - let list = make_address_list(vec![udp_addr(ip, 30000), quic_addr(ip, port)]); - - let bytes = serialize_boxed(&list.into_boxed()).unwrap(); - let restored = deserialize_boxed(&bytes) - .unwrap() - .downcast::() - .unwrap() - .only(); - - assert_eq!(restored.addrs.len(), 2); - match &restored.addrs[0] { - Address::Adnl_Address_Udp(u) => { - assert_eq!(u.ip as u32, ip); - assert_eq!(u.port, 30000); - } - other => panic!("Expected Udp, got {:?}", other), - } - match &restored.addrs[1] { - Address::Adnl_Address_Quic(q) => { - assert_eq!(q.ip as u32, ip); - assert_eq!(q.port, port as i32); - } - other => panic!("Expected Quic, got {:?}", other), - } -} - -#[test] -fn test_quic_address_tl_roundtrip_no_quic() { - let ip: u32 = u32::from(Ipv4Addr::new(10, 0, 0, 1)); - let list = make_address_list(vec![udp_addr(ip, 30000)]); - - let bytes = serialize_boxed(&list.into_boxed()).unwrap(); - let restored = deserialize_boxed(&bytes) - .unwrap() - .downcast::() - .unwrap() - .only(); - - assert_eq!(restored.addrs.len(), 1); - assert!(matches!(&restored.addrs[0], Address::Adnl_Address_Udp(_))); -} - -// --- parse_quic_address --- - -#[test] -fn test_parse_quic_address_present() { - let ip: u32 = u32::from(Ipv4Addr::new(192, 168, 1, 1)); - let list = make_address_list(vec![udp_addr(ip, 30000), quic_addr(ip, 31000)]); - - let (_, result) = AdnlNode::parse_address_list(&list).unwrap().unwrap(); - assert_eq!( - result.map(|q| ip_address_to_socket_addr(&q)), - Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 31000)) - ); -} - -#[test] -fn test_parse_quic_address_absent() { - let ip: u32 = u32::from(Ipv4Addr::new(192, 168, 1, 1)); - let list = make_address_list(vec![udp_addr(ip, 30000)]); - - let (_, result) = AdnlNode::parse_address_list(&list).unwrap().unwrap(); - assert_eq!(result, None); -} - -#[test] -fn test_parse_quic_address_only_quic() { - let ip: u32 = u32::from(Ipv4Addr::new(10, 0, 0, 5)); - let list = make_address_list(vec![quic_addr(ip, 9999)]); - - // With parse_address_list, quic-only list returns None (no UDP address found) - let result = AdnlNode::parse_address_list(&list).unwrap(); - assert!(result.is_none()); -} - -#[test] -fn test_parse_quic_address_picks_first() { - let ip1: u32 = u32::from(Ipv4Addr::new(1, 1, 1, 1)); - let ip2: u32 = u32::from(Ipv4Addr::new(2, 2, 2, 2)); - let list = - make_address_list(vec![udp_addr(ip1, 30000), quic_addr(ip1, 31000), quic_addr(ip2, 32000)]); - - let (_, result) = AdnlNode::parse_address_list(&list).unwrap().unwrap(); - // Should return the first quic address - assert_eq!( - result.map(|q| ip_address_to_socket_addr(&q)), - Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 31000)) - ); -} - -// --- parse_address_list still works (not broken by new variant) --- - -#[test] -fn test_parse_address_list_with_quic_and_udp() { - let ip: u32 = u32::from(Ipv4Addr::new(172, 16, 0, 1)); - let list = make_address_list(vec![udp_addr(ip, 30000), quic_addr(ip, 31000)]); - - let result = AdnlNode::parse_address_list(&list).unwrap(); - assert!(result.is_some()); - let (adnl_addr, _) = result.unwrap(); - assert_eq!(adnl_addr.ip(), ip); - assert_eq!(adnl_addr.port(), 30000); -} - -#[test] -fn test_parse_address_list_quic_only_returns_none() { - let ip: u32 = u32::from(Ipv4Addr::new(172, 16, 0, 1)); - let list = make_address_list(vec![quic_addr(ip, 31000)]); - - // parse_address_list looks at addrs[0] and expects UDP โ€” quic-only should return None - let result = AdnlNode::parse_address_list(&list).unwrap(); - assert!(result.is_none()); -} - -// --- TL wire compatibility: deserialize a quic address from raw bytes --- - -#[test] -fn test_quic_address_deserialize_from_bytes() { - // Build a known address list with quic, serialize, then deserialize - let ip: u32 = u32::from(Ipv4Addr::new(93, 174, 52, 11)); - let port: u16 = 40001; - let list = make_address_list(vec![udp_addr(ip, 30303), quic_addr(ip, port)]); - let bytes = serialize_boxed(&list.into_boxed()).unwrap(); - - // Deserialize from raw bytes (simulating reception from a C++ node) - let obj = deserialize_boxed(&bytes).unwrap(); - let restored = obj - .downcast::() - .expect("should deserialize as AddressList") - .only(); - - let (_, quic) = AdnlNode::parse_address_list(&restored).unwrap().unwrap(); - assert_eq!( - quic.map(|q| ip_address_to_socket_addr(&q)), - Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(93, 174, 52, 11)), 40001)) - ); -} - -// --- DHT distribution tests --- - -fn init_local_dht_pair( - port1: u16, - port2: u16, -) -> ( - tokio::runtime::Runtime, - Arc, - Arc, - Arc, - Arc, - Arc, - Arc, -) { - let rt = init_test(); - let mut config1 = rt - .block_on(get_adnl_config("quic_addr", &format!("127.0.0.1:{port1}"), vec![KEY_TAG], true)) - .unwrap(); - config1.set_ip_address_quic(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - port1 + 1000, - )); - let config2 = rt - .block_on(get_adnl_config("quic_addr", &format!("127.0.0.1:{port2}"), vec![KEY_TAG], true)) - .unwrap(); - let adnl1 = rt.block_on(AdnlNode::with_config(config1)).unwrap(); - let dht1 = DhtNode::with_adnl_node(adnl1.clone(), KEY_TAG).unwrap(); - let overlay1 = OverlayNode::with_params(adnl1.clone(), &[1u8; 32], KEY_TAG).unwrap(); - rt.block_on(adnl1.start_over_udp(vec![dht1.clone(), overlay1.clone()])).unwrap(); - let adnl2 = rt.block_on(AdnlNode::with_config(config2)).unwrap(); - let dht2 = DhtNode::with_adnl_node(adnl2.clone(), KEY_TAG).unwrap(); - let overlay2 = OverlayNode::with_params(adnl2.clone(), &[1u8; 32], KEY_TAG).unwrap(); - rt.block_on(adnl2.start_over_udp(vec![dht2.clone(), overlay2.clone()])).unwrap(); - (rt, adnl1, dht1, overlay1, adnl2, dht2, overlay2) -} - -/// Test: adnl.address.quic is stored in DHT and retrieved by another node. -/// -/// Node1 sets its QUIC port and stores its address via DHT (store_ip_address). -/// Node2 fetches node1's address from DHT (fetch_address). -/// Verify that node2's ADNL layer has the correct QUIC address for node1. -#[test] -fn test_quic_address_dht_distribution() { - let (rt, adnl1, dht1, _overlay1, adnl2, dht2, _overlay2) = init_local_dht_pair(4291, 4292); - - rt.block_on(async { - // Connect the two DHT nodes - let peer1 = dht2.add_peer(&dht1.get_signed_node().unwrap()).unwrap().unwrap(); - let peer2 = dht1.add_peer(&dht2.get_signed_node().unwrap()).unwrap().unwrap(); - assert!(dht1.ping(&peer2).await.unwrap()); - assert!(dht2.ping(&peer1).await.unwrap()); - - // Node1: QUIC address was set in config. - // build_address_list will include adnl.address.quic automatically. - let quic_addr_expected = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5291); - - // Verify build_address_list includes the quic address - let addr_list = adnl1.build_address_list(None).unwrap(); - let (_, parsed_quic) = AdnlNode::parse_address_list(&addr_list).unwrap().unwrap(); - assert!(parsed_quic.is_some(), "build_address_list should include adnl.address.quic"); - assert_eq!(ip_address_to_socket_addr(&parsed_quic.unwrap()), quic_addr_expected); - - // Store in DHT - assert!(dht1.store_ip_address(&dht1.key()).await.unwrap()); - - // Node2: fetch node1's address from DHT - let key1_id = dht1.key().id().clone(); - let fetched = dht2.fetch_address(&key1_id).await.unwrap(); - assert!(fetched.is_some(), "Node2 should find node1's address in DHT"); - - let (adnl_addr, _, _key) = fetched.unwrap(); - // Verify the UDP address was parsed correctly - assert_eq!(adnl_addr.port(), 4291, "UDP port should match node1"); - - // Verify the QUIC address was extracted and stored in the ADNL layer - let local_key2 = adnl2.key_by_tag(KEY_TAG).unwrap().id().clone(); - let peer_addrs = adnl2.peer_ip_address(&local_key2, &key1_id).unwrap(); - assert!(peer_addrs.is_some(), "Node2 should have address for node1 after DHT fetch"); - let (_, quic_addr) = peer_addrs.unwrap(); - assert!(quic_addr.is_some(), "Node2 should have QUIC address for node1 after DHT fetch"); - let quic_addr = quic_addr.unwrap(); - assert_eq!( - quic_addr, quic_addr_expected, - "QUIC address should match what node1 advertised" - ); - - adnl1.stop().await; - adnl2.stop().await; - }); -} - -/// Test: address list without adnl.address.quic does NOT set peer_quic_address. -/// -/// Node1 does NOT set a QUIC port, stores its address via DHT. -/// Node2 fetches it and verifies no QUIC address is stored. -#[test] -fn test_no_quic_address_dht_distribution() { - let (rt, adnl1, dht1, _overlay1, adnl2, dht2, _overlay2) = init_local_dht_pair(4293, 4294); - - rt.block_on(async { - let peer1 = dht2.add_peer(&dht1.get_signed_node().unwrap()).unwrap().unwrap(); - let peer2 = dht1.add_peer(&dht2.get_signed_node().unwrap()).unwrap().unwrap(); - assert!(dht1.ping(&peer2).await.unwrap()); - assert!(dht2.ping(&peer1).await.unwrap()); - - // Node1: no QUIC port set โ€” build_address_list should only have UDP - let addr_list = adnl1.build_address_list(None).unwrap(); - let (_, quic_addr) = AdnlNode::parse_address_list(&addr_list).unwrap().unwrap(); - assert!( - quic_addr.is_none(), - "Without set_quic_address, address list should not contain adnl.address.quic" - ); - - // Store and fetch - assert!(dht1.store_ip_address(&dht1.key()).await.unwrap()); - let key1_id = dht1.key().id().clone(); - let fetched = dht2.fetch_address(&key1_id).await.unwrap(); - assert!(fetched.is_some()); - - // Verify no QUIC address was stored - let local_key2 = adnl2.key_by_tag(KEY_TAG).unwrap().id().clone(); - let peer_addrs = adnl2.peer_ip_address(&local_key2, &key1_id).unwrap(); - let quic_addr = peer_addrs.and_then(|(_, q)| q); - assert!( - quic_addr.is_none(), - "No QUIC address should be stored when peer doesn't advertise one" - ); - - adnl1.stop().await; - adnl2.stop().await; - }); -} diff --git a/src/adnl/tests/test_rldp.rs b/src/adnl/tests/test_rldp.rs index 6abf51d..138d4ed 100644 --- a/src/adnl/tests/test_rldp.rs +++ b/src/adnl/tests/test_rldp.rs @@ -68,9 +68,9 @@ fn init_rldp_compatibility_test( #[cfg(feature = "dump")] None, ); - let ours_ip = format!("{}", ctx_test.adnl.ip_address_adnl()); + let ours_ip = format!("{}", ctx_test.adnl.ip_address()); let pos = ours_ip.find(":").unwrap(); - let ours_ip = format!("{}:{}", &ours_ip[..pos], ctx_test.adnl.ip_address_adnl().port() + 1); + let ours_ip = format!("{}:{}", &ours_ip[..pos], ctx_test.adnl.ip_address().port() + 1); let config = ctx_test.rt.block_on(get_adnl_config("rldp", &ours_ip, vec![KEY_TAG], true)).unwrap(); let adnl = ctx_test.rt.block_on(AdnlNode::with_config(config)).unwrap(); @@ -97,7 +97,7 @@ fn find_rldp_peer( let (peer_ip, peer_node) = find_overlay_peer(overlay_peers, ctx_search, ctx_test, TARGET); let ours_id = adnl.key_by_tag(KEY_TAG).unwrap().id().clone(); let peer_key: Arc = (&peer_node.id).try_into().unwrap(); - let peer_id = adnl.add_peer(&ours_id, &peer_ip, None, &peer_key).unwrap().unwrap(); + let peer_id = adnl.add_peer(&ours_id, &peer_ip, &peer_key).unwrap().unwrap(); (peer_id, ours_id) } @@ -193,8 +193,8 @@ fn init_local_test( rt.block_on(adnl2.start_over_udp(vec![rldp2.clone()])).unwrap(); let peer1 = adnl1.key_by_tag(KEY_TAG).unwrap(); let peer2 = adnl2.key_by_tag(KEY_TAG).unwrap(); - adnl1.add_peer(peer1.id(), adnl2.ip_address_adnl(), None, &peer2).unwrap(); - adnl2.add_peer(peer2.id(), adnl1.ip_address_adnl(), None, &peer1).unwrap(); + adnl1.add_peer(peer1.id(), adnl2.ip_address(), &peer2).unwrap(); + adnl2.add_peer(peer2.id(), adnl1.ip_address(), &peer1).unwrap(); let ctx1 = RldpContext { adnl: adnl1, peer: peer1.id().clone(), rldp: rldp1 }; let ctx2 = RldpContext { adnl: adnl2, peer: peer2.id().clone(), rldp: rldp2 }; (rt, ctx1, ctx2) diff --git a/src/adnl/tests/test_udp.rs b/src/adnl/tests/test_udp.rs index 00d8864..e3d765d 100644 --- a/src/adnl/tests/test_udp.rs +++ b/src/adnl/tests/test_udp.rs @@ -161,7 +161,6 @@ async fn test_address( .add_peer( src.id(), &IpAddress::from_versioned_string(ip, Some(version)).unwrap(), - None, &node2.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() @@ -321,8 +320,7 @@ fn node_async_query() { let peer1 = node2 .add_peer( node2.key_by_tag(KEY_TAG).unwrap().id(), - node1.ip_address_adnl(), - None, + node1.ip_address(), &node1.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() @@ -330,8 +328,7 @@ fn node_async_query() { let peer2 = node1 .add_peer( node1.key_by_tag(KEY_TAG).unwrap().id(), - node2.ip_address_adnl(), - None, + node2.ip_address(), &node2.key_by_tag(KEY_TAG).unwrap(), ) .unwrap() diff --git a/src/adnl/tests/test_utils.rs b/src/adnl/tests/test_utils.rs index 5fe4592..81a0945 100644 --- a/src/adnl/tests/test_utils.rs +++ b/src/adnl/tests/test_utils.rs @@ -279,13 +279,13 @@ pub fn find_overlay_peer( } } let (ip, node) = peers.pop().unwrap(); - if ip.to_udp() == ctx_test.adnl.ip_address_adnl().to_udp() { + if ip.to_udp() == ctx_test.adnl.ip_address().to_udp() { continue; } log::info!( target: log_target, "---- Try overlay peer {} {}, own address {}", - ip, node, ctx_test.adnl.ip_address_adnl() + ip, node, ctx_test.adnl.ip_address() ); let peer = ctx_test .overlay diff --git a/src/assembler/src/complex.rs b/src/assembler/src/complex.rs index 6cbedf7..023e0c2 100644 --- a/src/assembler/src/complex.rs +++ b/src/assembler/src/complex.rs @@ -265,7 +265,7 @@ fn build_code_dict( } else { let value_cell = value_slice.clone().into_cell()?; info.append(&mut value_dbg); - dict.setref(key_slice.clone(), value_cell) + dict.setref(key_slice.clone(), &value_cell) .map_err(|e| OperationError::CodeDictConstruction(e.to_string()))?; } } diff --git a/src/audit.toml b/src/audit.toml new file mode 100644 index 0000000..bf3a76d --- /dev/null +++ b/src/audit.toml @@ -0,0 +1,7 @@ +[advisories] +ignore = [ + # RUSTSEC-2023-0071: Marvin Attack timing side-channel in RSA decryption. + # Not exploitable here: rsa is only used for encryption (RSA-OAEP wrapping of + # an AES key with a public key). No decryption is performed. + "RUSTSEC-2023-0071", +] diff --git a/src/block-json/src/deserialize.rs b/src/block-json/src/deserialize.rs index ff15b65..7af350a 100644 --- a/src/block-json/src/deserialize.rs +++ b/src/block-json/src/deserialize.rs @@ -665,45 +665,11 @@ impl StateParser { } fn parse_simplex_config(p: &PathMap) -> Result { - let d = NoncriticalParams::default(); Ok(SimplexConfig { - use_quic: p.get_num32("use_quic").unwrap_or(0) != 0, + target_rate_ms: p.get_num32("target_rate_ms")?, slots_per_leader_window: p.get_num32("slots_per_leader_window")?, - noncritical_params: NoncriticalParams { - target_rate_ms: p.get_num32("target_rate_ms")?, - first_block_timeout_ms: p.get_num32("first_block_timeout_ms")?, - first_block_timeout_multiplier_bits: p - .get_num32("first_block_timeout_multiplier_bits") - .unwrap_or(d.first_block_timeout_multiplier_bits), - first_block_timeout_cap_ms: p - .get_num32("first_block_timeout_cap_ms") - .unwrap_or(d.first_block_timeout_cap_ms), - candidate_resolve_timeout_ms: p - .get_num32("candidate_resolve_timeout_ms") - .unwrap_or(d.candidate_resolve_timeout_ms), - candidate_resolve_timeout_multiplier_bits: p - .get_num32("candidate_resolve_timeout_multiplier_bits") - .unwrap_or(d.candidate_resolve_timeout_multiplier_bits), - candidate_resolve_timeout_cap_ms: p - .get_num32("candidate_resolve_timeout_cap_ms") - .unwrap_or(d.candidate_resolve_timeout_cap_ms), - candidate_resolve_cooldown_ms: p - .get_num32("candidate_resolve_cooldown_ms") - .unwrap_or(d.candidate_resolve_cooldown_ms), - standstill_timeout_ms: p - .get_num32("standstill_timeout_ms") - .unwrap_or(d.standstill_timeout_ms), - standstill_max_egress_bytes_per_s: p - .get_num32("standstill_max_egress_bytes_per_s") - .unwrap_or(d.standstill_max_egress_bytes_per_s), - max_leader_window_desync: p.get_num32("max_leader_window_desync")?, - bad_signature_ban_duration_ms: p - .get_num32("bad_signature_ban_duration_ms") - .unwrap_or(d.bad_signature_ban_duration_ms), - candidate_resolve_rate_limit: p - .get_num32("candidate_resolve_rate_limit") - .unwrap_or(d.candidate_resolve_rate_limit), - }, + first_block_timeout_ms: p.get_num32("first_block_timeout_ms")?, + max_leader_window_desync: p.get_num32("max_leader_window_desync")?, }) } diff --git a/src/block-json/src/serialize.rs b/src/block-json/src/serialize.rs index 51ed955..f51ee3c 100644 --- a/src/block-json/src/serialize.rs +++ b/src/block-json/src/serialize.rs @@ -763,6 +763,14 @@ fn serialize_out_msg(msg: &OutMsg, mode: SerializationMode) -> Result { Ok(map.into()) } +fn serialize_validators_stat(stat: &ValidatorsStat) -> Result { + let mut map = Map::new(); + for i in 0..stat.len() as u16 { + serialize_field(&mut map, &i.to_string(), stat.get(i)?); + } + Ok(map.into()) +} + fn serialize_shard_descr(descr: &ShardDescr, mode: SerializationMode) -> Result { let mut map = Map::new(); serialize_field(&mut map, "seq_no", descr.seq_no); @@ -1063,40 +1071,10 @@ fn serialize_accelerator(acc: &AcceleratedConsensusConfig) -> Result { fn serialize_simplex_config(cfg: &SimplexConfig) -> Result { let mut map = Map::new(); - if cfg.use_quic { - serialize_field(&mut map, "use_quic", 1u32); - } + serialize_field(&mut map, "target_rate_ms", cfg.target_rate_ms); serialize_field(&mut map, "slots_per_leader_window", cfg.slots_per_leader_window); - let np = &cfg.noncritical_params; - serialize_field(&mut map, "target_rate_ms", np.target_rate_ms); - serialize_field(&mut map, "first_block_timeout_ms", np.first_block_timeout_ms); - serialize_field( - &mut map, - "first_block_timeout_multiplier_bits", - np.first_block_timeout_multiplier_bits, - ); - serialize_field(&mut map, "first_block_timeout_cap_ms", np.first_block_timeout_cap_ms); - serialize_field(&mut map, "candidate_resolve_timeout_ms", np.candidate_resolve_timeout_ms); - serialize_field( - &mut map, - "candidate_resolve_timeout_multiplier_bits", - np.candidate_resolve_timeout_multiplier_bits, - ); - serialize_field( - &mut map, - "candidate_resolve_timeout_cap_ms", - np.candidate_resolve_timeout_cap_ms, - ); - serialize_field(&mut map, "candidate_resolve_cooldown_ms", np.candidate_resolve_cooldown_ms); - serialize_field(&mut map, "standstill_timeout_ms", np.standstill_timeout_ms); - serialize_field( - &mut map, - "standstill_max_egress_bytes_per_s", - np.standstill_max_egress_bytes_per_s, - ); - serialize_field(&mut map, "max_leader_window_desync", np.max_leader_window_desync); - serialize_field(&mut map, "bad_signature_ban_duration_ms", np.bad_signature_ban_duration_ms); - serialize_field(&mut map, "candidate_resolve_rate_limit", np.candidate_resolve_rate_limit); + serialize_field(&mut map, "first_block_timeout_ms", cfg.first_block_timeout_ms); + serialize_field(&mut map, "max_leader_window_desync", cfg.max_leader_window_desync); Ok(map.into()) } @@ -1636,6 +1614,13 @@ fn serialize_mc_state_extra( serialize_block_create_stats(&mut extra_map, "block_create_stats", stats, mode)?; } serialize_cc(&mut extra_map, "global_balance", &extra.global_balance, mode)?; + if !extra.validators_stat.is_empty() { + serialize_field( + &mut extra_map, + "validators_unreliability", + serialize_validators_stat(&extra.validators_stat)?, + ); + } map.insert(id_str.to_string(), extra_map.into()); Ok(()) } diff --git a/src/block-json/src/tests/test_deserialize.rs b/src/block-json/src/tests/test_deserialize.rs index 5a1065c..e16edc9 100644 --- a/src/block-json/src/tests/test_deserialize.rs +++ b/src/block-json/src/tests/test_deserialize.rs @@ -10,11 +10,10 @@ */ use super::*; use crate::{serialize_config, serialize_config_param, SerializationMode}; -use std::fmt::Debug; use ton_block::{ - write_boc, BuilderData, ConfigParam3, ConfigParam32, ConfigParam33, ConfigParam35, - ConfigParam36, ConfigParam37, ConfigParam39, ConfigParam4, ConfigParam6, ConfigParamEnum, - ConfigVotingSetup, IBitstring, NoncriticalParams, Number16, SigPubKey, VarUInteger32, + BuilderData, ConfigParam3, ConfigParam32, ConfigParam33, ConfigParam35, ConfigParam36, + ConfigParam37, ConfigParam39, ConfigParam4, ConfigParam6, ConfigVotingSetup, IBitstring, + Number16, SigPubKey, VarUInteger32, }; include!("./test_common.rs"); @@ -28,75 +27,7 @@ fn test_parse_zerostate() { assert_json_eq(&json, ðalon, "zerostate"); } -#[test] -fn test_parse_zerostate_p30_use_quic_survives_into_v2_boc() { - let ethalon = std::fs::read_to_string("src/tests/data/zerostate-ethalon.json").unwrap(); - let mut map = serde_json::from_str::>(ðalon).unwrap(); - - let master = map.get_mut("master").unwrap().as_object_mut().unwrap(); - let config = master.get_mut("config").unwrap().as_object_mut().unwrap(); - config.get_mut("p8").unwrap().as_object_mut().unwrap().insert("version".to_string(), 13.into()); - config.insert( - "p30".to_string(), - serde_json::json!({ - "mc": { - "use_quic": 1, - "slots_per_leader_window": 8, - "target_rate_ms": 200, - "first_block_timeout_ms": 500, - "max_leader_window_desync": 2 - }, - "shard": { - "use_quic": 1, - "slots_per_leader_window": 16, - "target_rate_ms": 200, - "first_block_timeout_ms": 500, - "max_leader_window_desync": 2 - } - }), - ); - - let state = parse_state(&map).unwrap(); - let custom = state.read_custom().unwrap().unwrap(); - let config = custom.config(); - - let ConfigParamEnum::ConfigParam30(parsed_p30) = config.config(30).unwrap().unwrap() else { - panic!("expected ConfigParam30 in parsed zerostate"); - }; - - let mc = parsed_p30.mc.as_ref().expect("expected MC simplex config"); - assert!(mc.use_quic); - assert_eq!(mc.slots_per_leader_window, 8); - assert_eq!(mc.noncritical_params.target_rate_ms, 200); - assert_eq!(mc.noncritical_params.first_block_timeout_ms, 500); - assert_eq!(mc.noncritical_params.max_leader_window_desync, 2); - - let shard = parsed_p30.shard.as_ref().expect("expected shard simplex config"); - assert!(shard.use_quic); - assert_eq!(shard.slots_per_leader_window, 16); - assert_eq!(shard.noncritical_params.target_rate_ms, 200); - assert_eq!(shard.noncritical_params.first_block_timeout_ms, 500); - assert_eq!(shard.noncritical_params.max_leader_window_desync, 2); - - let key = 30u32.write_to_bitstring().unwrap(); - let p30_slice = config.config_params.get(key).unwrap().expect("expected raw p30 cell"); - let p30_cell = p30_slice.reference(0).unwrap(); - let p30_boc = write_boc(&p30_cell).unwrap(); - - let mc_v2_quic = [0x22, 0x01, 0x00, 0x00, 0x00, 0x08]; - assert!( - p30_boc.windows(mc_v2_quic.len()).any(|window| window == mc_v2_quic), - "serialized p30 BOC must contain MC simplex_config_v2#22 with use_quic=1" - ); - - let shard_v2_quic = [0x22, 0x01, 0x00, 0x00, 0x00, 0x10]; - assert!( - p30_boc.windows(shard_v2_quic.len()).any(|window| window == shard_v2_quic), - "serialized p30 BOC must contain shard simplex_config_v2#22 with use_quic=1" - ); -} - -fn check_err(result: Result, text: &str) { +fn check_err(result: Result, text: &str) { let len = text.len(); assert_eq!(&result.expect_err("must generate error").to_string()[0..len], text) } @@ -362,24 +293,16 @@ fn get_config_param63() -> AcceleratedConsensusConfig { fn get_config_param30() -> NewConsensusConfigAll { NewConsensusConfigAll { mc: Some(SimplexConfig { + target_rate_ms: 300, slots_per_leader_window: 4, - noncritical_params: NoncriticalParams { - target_rate_ms: 300, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, }), shard: Some(SimplexConfig { + target_rate_ms: 200, slots_per_leader_window: 8, - noncritical_params: NoncriticalParams { - target_rate_ms: 200, - first_block_timeout_ms: 500, - max_leader_window_desync: 50, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 500, + max_leader_window_desync: 50, }), } } diff --git a/src/block/src/accounts.rs b/src/block/src/accounts.rs index ef98957..01f7ad3 100644 --- a/src/block/src/accounts.rs +++ b/src/block/src/accounts.rs @@ -1239,7 +1239,7 @@ impl Account { let config_cell = data .checked_drain_reference() .map_err(|_| error!("config SMC data doesn't contain reference with config"))?; - ConfigParams::with_root(config_cell) + Ok(ConfigParams::with_root(config_cell)) } } diff --git a/src/block/src/blocks.rs b/src/block/src/blocks.rs index b3b1f6e..e27ae57 100644 --- a/src/block/src/blocks.rs +++ b/src/block/src/blocks.rs @@ -1464,7 +1464,7 @@ impl TopBlockDescrSet { pub fn insert(&mut self, shard: &ShardIdent, descr: &TopBlockDescr) -> Result<()> { let key = shard.full_key_with_tag()?; let value = descr.serialize()?; - self.collection.0.setref(key, value)?; + self.collection.0.setref(key, &value)?; Ok(()) } pub fn is_empty(&self) -> bool { diff --git a/src/block/src/config_params.rs b/src/block/src/config_params.rs index b292be9..2446ffe 100644 --- a/src/block/src/config_params.rs +++ b/src/block/src/config_params.rs @@ -24,7 +24,6 @@ use crate::{ BASE_WORKCHAIN_ID, MAX_SPLIT_DEPTH, }; use num::BigInt; -use std::collections::BTreeMap; #[cfg(test)] #[path = "tests/test_config_params.rs"] @@ -48,17 +47,15 @@ impl Default for ConfigParams { } impl ConfigParams { - pub fn with_root(data: Cell) -> Result { - let config_params = HashmapE::with_hashmap(32, Some(data)); - let cell = config_params - .get(0u32.write_to_bitstring()?)? - .ok_or_else(|| error!("config param 0 is missing"))? - .reference(0)?; - let result = ConfigParamEnum::construct_from_cell_and_number(cell, 0)?; - let ConfigParamEnum::ConfigParam0(ConfigParam0 { config_addr }) = result else { - fail!("config param 0 has invalid format"); - }; - Ok(Self { config_addr, config_params }) + pub const fn with_root(data: Cell) -> Self { + Self { + config_addr: AccountId::ZERO_ID, + config_params: HashmapE::with_hashmap(32, Some(data)), + } + } + + pub const fn with_address_and_root(config_addr: AccountId, data: Cell) -> Self { + Self { config_addr, config_params: HashmapE::with_hashmap(32, Some(data)) } } pub const fn with_address_and_params(config_addr: AccountId, data: Option) -> Self { @@ -432,7 +429,8 @@ pub enum GlobalCapabilities { CapResolveMerkleCell = 0x0000_0200_0000, } -pub const SUPPORTED_VERSION: u32 = 13; +//TODO: LK: enable after change block version to 13 +pub const SUPPORTED_VERSION: u32 = 12; pub const LT_ALIGN: u64 = 1_000_000; impl ConfigParams { @@ -568,11 +566,10 @@ impl ConfigParams { } impl Deserializable for ConfigParams { - fn construct_from(slice: &mut SliceData) -> Result { - let config_addr = slice.get_next_slice(256)?; - let data = slice.checked_drain_reference()?; - let config_params = HashmapE::with_hashmap(32, Some(data)); - Ok(Self { config_addr, config_params }) + fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { + self.config_addr.read_from(cell)?; + *self.config_params.data_mut() = Some(cell.checked_drain_reference()?); + Ok(()) } } @@ -938,13 +935,6 @@ impl BurningConfig { } Ok(()) } - - pub fn calculate_burned_fees(&self, value: u128) -> Result { - if self.fee_burn_num == 0 || value == 0 { - return Ok(Coins::default()); - } - (value * self.fee_burn_num as u128 / self.fee_burn_denom as u128).try_into() - } } impl Deserializable for BurningConfig { @@ -3779,198 +3769,50 @@ const NEW_CONSENSUS_CONFIG_ALL_TAG: u8 = 0x10; #[allow(dead_code)] // Used in deserialization logic - null consensus means fallback to catchain const NULL_CONSENSUS_CONFIG_TAG: u8 = 0x20; const SIMPLEX_CONFIG_TAG: u8 = 0x21; -const SIMPLEX_CONFIG_V2_TAG: u8 = 0x22; - -/// Named noncritical consensus parameters, mirroring the C++ `NoncriticalParams` struct -/// defined via `ENUMERATE_NONCRITICAL_PARAMS` in `ton-types.h`. -/// -/// All fields have concrete default values matching C++. For v1 configs the three -/// on-chain fields (`target_rate_ms`, `first_block_timeout_ms`, `max_leader_window_desync`) -/// are populated from the TL-B, the rest keep their defaults. For v2, the on-chain hashmap -/// overrides any subset of the 13 parameters. -/// -/// "double" parameters (multipliers) are stored as raw `f32` bit patterns in a `u32`, -/// matching the C++ `store_double` / `read_double` convention. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct NoncriticalParams { - pub target_rate_ms: u32, // idx 0, duration - pub first_block_timeout_ms: u32, // idx 1, duration - pub first_block_timeout_multiplier_bits: u32, // idx 2, double (f32 bits) - pub first_block_timeout_cap_ms: u32, // idx 3, duration - pub candidate_resolve_timeout_ms: u32, // idx 4, duration - pub candidate_resolve_timeout_multiplier_bits: u32, // idx 5, double (f32 bits) - pub candidate_resolve_timeout_cap_ms: u32, // idx 6, duration - pub candidate_resolve_cooldown_ms: u32, // idx 7, duration - pub standstill_timeout_ms: u32, // idx 8, duration - pub standstill_max_egress_bytes_per_s: u32, // idx 9, uint32 - pub max_leader_window_desync: u32, // idx 10, uint32 - pub bad_signature_ban_duration_ms: u32, // idx 11, duration - pub candidate_resolve_rate_limit: u32, // idx 12, uint32 -} - -impl Default for NoncriticalParams { - fn default() -> Self { - Self { - target_rate_ms: 2400, - first_block_timeout_ms: 1000, - first_block_timeout_multiplier_bits: (1.2f32).to_bits(), - first_block_timeout_cap_ms: 100_000, - candidate_resolve_timeout_ms: 1000, - candidate_resolve_timeout_multiplier_bits: (1.2f32).to_bits(), - candidate_resolve_timeout_cap_ms: 10_000, - candidate_resolve_cooldown_ms: 10, - standstill_timeout_ms: 10_000, - standstill_max_egress_bytes_per_s: 50 << 17, - max_leader_window_desync: 250, - bad_signature_ban_duration_ms: 5_000, - candidate_resolve_rate_limit: 10, - } - } -} -impl NoncriticalParams { - /// Set a parameter by its on-chain hashmap index. - pub fn set(&mut self, idx: u8, value: u32) { - match idx { - 0 => self.target_rate_ms = value, - 1 => self.first_block_timeout_ms = value, - 2 => self.first_block_timeout_multiplier_bits = value, - 3 => self.first_block_timeout_cap_ms = value, - 4 => self.candidate_resolve_timeout_ms = value, - 5 => self.candidate_resolve_timeout_multiplier_bits = value, - 6 => self.candidate_resolve_timeout_cap_ms = value, - 7 => self.candidate_resolve_cooldown_ms = value, - 8 => self.standstill_timeout_ms = value, - 9 => self.standstill_max_egress_bytes_per_s = value, - 10 => self.max_leader_window_desync = value, - 11 => self.bad_signature_ban_duration_ms = value, - 12 => self.candidate_resolve_rate_limit = value, - _ => {} - } - } - - /// Construct from a raw hashmap (as stored on-chain in simplex_config_v2). - pub fn from_raw_map(map: &BTreeMap) -> Self { - let mut p = Self::default(); - for (&k, &v) in map { - p.set(k, v); - } - p - } - - /// Convert all fields to a raw hashmap for on-chain v2 serialization. - pub fn to_raw_map(&self) -> BTreeMap { - BTreeMap::from([ - (0, self.target_rate_ms), - (1, self.first_block_timeout_ms), - (2, self.first_block_timeout_multiplier_bits), - (3, self.first_block_timeout_cap_ms), - (4, self.candidate_resolve_timeout_ms), - (5, self.candidate_resolve_timeout_multiplier_bits), - (6, self.candidate_resolve_timeout_cap_ms), - (7, self.candidate_resolve_cooldown_ms), - (8, self.standstill_timeout_ms), - (9, self.standstill_max_egress_bytes_per_s), - (10, self.max_leader_window_desync), - (11, self.bad_signature_ban_duration_ms), - (12, self.candidate_resolve_rate_limit), - ]) - } -} - -/// Unified Simplex consensus config โ€” the single output type -/// produced by deserializing either `simplex_config#21` (v1) or -/// `simplex_config_v2#22` (v2) from ConfigParam 30. +/// SimplexConfig - simplex_config#21 from ConfigParam 30 +/// Enables Simplex (Alpenglow) consensus for the specified workchain. /// -/// Mirrors C++ `NewConsensusConfig` in `ton-types.h`: critical fields -/// (`use_quic`, `slots_per_leader_window`) live at top level; all tunable -/// timing/rate parameters live inside `noncritical_params`. -#[derive(Clone, Debug, Eq, PartialEq)] +/// TL-B: simplex_config#21 flags:(## 8) target_rate_ms:uint32 +/// slots_per_leader_window:uint32 first_block_timeout_ms:uint32 +/// max_leader_window_desync:uint32 = NewConsensusConfig; +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SimplexConfig { - pub use_quic: bool, + pub target_rate_ms: u32, pub slots_per_leader_window: u32, - pub noncritical_params: NoncriticalParams, + pub first_block_timeout_ms: u32, + pub max_leader_window_desync: u32, } -impl Default for SimplexConfig { - fn default() -> Self { - Self { - use_quic: false, - slots_per_leader_window: 4, - noncritical_params: NoncriticalParams::default(), - } - } -} - -/// Maximum noncritical param key defined in the C++ reference (candidate_resolve_rate_limit). -const NONCRITICAL_PARAMS_MAX_KEY: u8 = 12; - -/// Always serializes as simplex_config_v2#22 (the current on-chain format). impl Serializable for SimplexConfig { fn write_to(&self, cell: &mut BuilderData) -> Result<()> { - cell.append_u8(SIMPLEX_CONFIG_V2_TAG)?; - let flags_byte = if self.use_quic { 1u8 } else { 0u8 }; - cell.append_u8(flags_byte)?; + cell.append_u8(SIMPLEX_CONFIG_TAG)?; + cell.append_u8(0)?; // flags - reserved for future use + self.target_rate_ms.write_to(cell)?; self.slots_per_leader_window.write_to(cell)?; - let raw_map = self.noncritical_params.to_raw_map(); - let mut params_hashmap = HashmapE::with_bit_len(8); - for (&key, &value) in &raw_map { - let key_slice = SliceData::from_raw(vec![key], 8); - let mut vb = BuilderData::new(); - value.write_to(&mut vb)?; - params_hashmap.set(key_slice, &SliceData::load_builder(vb)?)?; - } - params_hashmap.write_hashmap_data(cell)?; + self.first_block_timeout_ms.write_to(cell)?; + self.max_leader_window_desync.write_to(cell)?; Ok(()) } } -/// Deserializes both simplex_config#21 (v1) and simplex_config_v2#22 (v2). impl Deserializable for SimplexConfig { fn construct_from(slice: &mut SliceData) -> Result { let tag = slice.get_next_byte()?; - match tag { - SIMPLEX_CONFIG_TAG => { - let flags_byte = slice.get_next_byte()?; - let use_quic = (flags_byte & 1) != 0; - let target_rate_ms = u32::construct_from(slice)?; - let slots_per_leader_window = u32::construct_from(slice)?; - let first_block_timeout_ms = u32::construct_from(slice)?; - let max_leader_window_desync = u32::construct_from(slice)?; - Ok(Self { - use_quic, - slots_per_leader_window, - noncritical_params: NoncriticalParams { - target_rate_ms, - first_block_timeout_ms, - max_leader_window_desync, - ..Default::default() - }, - }) - } - SIMPLEX_CONFIG_V2_TAG => { - let flags_byte = slice.get_next_byte()?; - let use_quic = (flags_byte & 1) != 0; - let slots_per_leader_window = u32::construct_from(slice)?; - let has_params = slice.get_next_bit()?; - let params_cell = - if has_params { Some(slice.checked_drain_reference()?) } else { None }; - let params_map = HashmapE::with_hashmap(8, params_cell); - let mut raw = BTreeMap::new(); - for key_idx in 0..=NONCRITICAL_PARAMS_MAX_KEY { - let key = SliceData::from_raw(vec![key_idx], 8); - if let Some(mut vs) = params_map.get(key)? { - raw.insert(key_idx, u32::construct_from(&mut vs)?); - } - } - Ok(Self { - use_quic, - slots_per_leader_window, - noncritical_params: NoncriticalParams::from_raw_map(&raw), - }) - } - _ => fail!(Self::invalid_tag(tag as u32)), + if tag != SIMPLEX_CONFIG_TAG { + fail!(Self::invalid_tag(tag as u32)); } + let _flags = slice.get_next_byte()?; // Reserved, ignore + let target_rate_ms = u32::construct_from(slice)?; + let slots_per_leader_window = u32::construct_from(slice)?; + let first_block_timeout_ms = u32::construct_from(slice)?; + let max_leader_window_desync = u32::construct_from(slice)?; + Ok(Self { + target_rate_ms, + slots_per_leader_window, + first_block_timeout_ms, + max_leader_window_desync, + }) } } @@ -4022,17 +3864,17 @@ impl Deserializable for NewConsensusConfigAll { let cell = slice.checked_drain_reference()?; let mut inner = SliceData::load_cell(cell)?; let inner_tag = inner.clone().get_next_byte()?; - if inner_tag == SIMPLEX_CONFIG_TAG || inner_tag == SIMPLEX_CONFIG_V2_TAG { + if inner_tag == SIMPLEX_CONFIG_TAG { result.mc = Some(SimplexConfig::construct_from(&mut inner)?); } - // else null_consensus_config#20 or unknown โ†’ None (catchain fallback) + // else null_consensus_config#20 - leave as None (catchain fallback) } // shard:(Maybe ^NewConsensusConfig) if slice.get_next_bit()? { let cell = slice.checked_drain_reference()?; let mut inner = SliceData::load_cell(cell)?; let inner_tag = inner.clone().get_next_byte()?; - if inner_tag == SIMPLEX_CONFIG_TAG || inner_tag == SIMPLEX_CONFIG_V2_TAG { + if inner_tag == SIMPLEX_CONFIG_TAG { result.shard = Some(SimplexConfig::construct_from(&mut inner)?); } } diff --git a/src/block/src/dictionary/hashmap.rs b/src/block/src/dictionary/hashmap.rs index e2b01c4..e6d2d20 100644 --- a/src/block/src/dictionary/hashmap.rs +++ b/src/block/src/dictionary/hashmap.rs @@ -136,13 +136,13 @@ impl HashmapE { self.hashmap_set_with_mode(key, value, gas_consumer, ADD) } /// sets value as reference - pub fn setref(&mut self, key: SliceData, value: Cell) -> Leaf { + pub fn setref(&mut self, key: SliceData, value: &Cell) -> Leaf { self.hashmap_setref_with_mode(key, value, &mut 0, ADD | REPLACE) } pub fn setref_with_gas( &mut self, key: SliceData, - value: Cell, + value: &Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, ADD | REPLACE) @@ -150,7 +150,7 @@ impl HashmapE { pub fn replaceref_with_gas( &mut self, key: SliceData, - value: Cell, + value: &Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, REPLACE) @@ -158,7 +158,7 @@ impl HashmapE { pub fn addref_with_gas( &mut self, key: SliceData, - value: Cell, + value: &Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, ADD) @@ -478,7 +478,7 @@ macro_rules! define_HashmapE { let value = value.write_to_new_cell()?; self.0.set_builder(key, &value) } - pub fn setref(&mut self, key: &K, value: Cell) -> Result<()> { + pub fn setref(&mut self, key: &K, value: &Cell) -> Result<()> { let key = key.write_to_bitstring()?; self.0.setref(key, value)?; Ok(()) diff --git a/src/block/src/dictionary/mod.rs b/src/block/src/dictionary/mod.rs index 4ba15c3..be312e4 100644 --- a/src/block/src/dictionary/mod.rs +++ b/src/block/src/dictionary/mod.rs @@ -558,12 +558,12 @@ pub trait HashmapType { fn hashmap_setref_with_mode( &mut self, key: SliceData, - value: Cell, + value: &Cell, gas_consumer: &mut dyn GasConsumer, mode: u8, ) -> Leaf { let mut builder = BuilderData::default(); - builder.checked_append_reference(value)?; + builder.checked_append_reference(value.clone())?; self.hashmap_set_with_mode(key, &builder, gas_consumer, mode) } diff --git a/src/block/src/dictionary/pfxhashmap.rs b/src/block/src/dictionary/pfxhashmap.rs index 8589565..8436b35 100644 --- a/src/block/src/dictionary/pfxhashmap.rs +++ b/src/block/src/dictionary/pfxhashmap.rs @@ -86,13 +86,13 @@ impl PfxHashmapE { self.hashmap_set_with_mode(key, value, gas_consumer, REPLACE) } /// sets value as reference in empty SliceData - pub fn setref(&mut self, key: SliceData, value: Cell) -> Leaf { + pub fn setref(&mut self, key: SliceData, value: &Cell) -> Leaf { self.hashmap_setref_with_mode(key, value, &mut 0, ADD | REPLACE) } pub fn setref_with_gas( &mut self, key: SliceData, - value: Cell, + value: &Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, ADD | REPLACE) @@ -100,7 +100,7 @@ impl PfxHashmapE { pub fn replaceref_with_gas( &mut self, key: SliceData, - value: Cell, + value: &Cell, gas_consumer: &mut dyn GasConsumer, ) -> Leaf { self.hashmap_setref_with_mode(key, value, gas_consumer, REPLACE) diff --git a/src/block/src/dictionary/tests/test_hashmap.rs b/src/block/src/dictionary/tests/test_hashmap.rs index 87ac963..e16a362 100644 --- a/src/block/src/dictionary/tests/test_hashmap.rs +++ b/src/block/src/dictionary/tests/test_hashmap.rs @@ -35,7 +35,7 @@ fn setref_and_get() { assert_eq!( tree.setref( SliceData::from_raw(vec![0b11111111], 8), - BuilderData::with_raw(vec![0b11111111], 8).unwrap().into_cell().unwrap() + &BuilderData::with_raw(vec![0b11111111], 8).unwrap().into_cell().unwrap() ) .unwrap(), None @@ -464,14 +464,14 @@ fn test_dictionary_of_dictionaries() { let mut root = HashmapE::with_bit_len(3); let key1 = SliceData::from_raw(vec![0xFF], 3); - assert_eq!(root.setref(key1.clone(), tree1.data().unwrap().clone()).unwrap(), None); + assert_eq!(root.setref(key1.clone(), tree1.data().unwrap()).unwrap(), None); assert_eq!( root.get(key1.clone()).unwrap().unwrap().reference(0).as_ref().unwrap(), tree1.data().unwrap() ); let key2 = SliceData::from_raw(vec![0xC0], 3); - assert_eq!(root.setref(key2.clone(), tree2.data().unwrap().clone()).unwrap(), None); + assert_eq!(root.setref(key2.clone(), tree2.data().unwrap()).unwrap(), None); assert_eq!( root.get(key2.clone()).unwrap().unwrap().reference(0).as_ref().unwrap(), tree2.data().unwrap() diff --git a/src/block/src/error.rs b/src/block/src/error.rs index c121ed0..26b0c1c 100644 --- a/src/block/src/error.rs +++ b/src/block/src/error.rs @@ -54,8 +54,6 @@ pub enum BlockError { UnexpectedStructVariant(String, String), #[error("Mismatched serde options: {0} exp={1} real={2}")] MismatchedSerdeOptions(String, usize, usize), - #[error("OutAction deserialize error {0}, mode {1}")] - OutActionError(#[source] crate::Error, u8), } // Exception codes ***************************************************************** diff --git a/src/block/src/master.rs b/src/block/src/master.rs index 0506bf2..2cbc4e9 100644 --- a/src/block/src/master.rs +++ b/src/block/src/master.rs @@ -21,7 +21,7 @@ use crate::{ shard::{AccountIdPrefixFull, ShardIdent, SHARD_FULL}, signature::CryptoSignaturePair, types::{ChildCell, CurrencyCollection, InRefValue}, - validators::ValidatorInfo, + validators::{ValidatorInfo, ValidatorsStat}, AccountId, Augmentation, BuilderData, Cell, Deserializable, IBitstring, Result, Serializable, SliceData, UInt256, }; @@ -395,6 +395,7 @@ pub struct McBlockExtra { recover_create_msg: Option>, mint_msg: Option>, config: Option, + validators_stat: ValidatorsStat, } impl McBlockExtra { @@ -478,6 +479,18 @@ impl McBlockExtra { pub fn mint_msg_cell(&self) -> Option { self.mint_msg.as_ref().map(|mr| mr.cell()) } + + pub fn validators_stat(&self) -> &ValidatorsStat { + &self.validators_stat + } + + pub fn validators_stat_mut(&mut self) -> &mut ValidatorsStat { + &mut self.validators_stat + } + + pub fn set_validators_stat(&mut self, stat: ValidatorsStat) { + self.validators_stat = stat; + } } const MC_BLOCK_EXTRA_TAG: u16 = 0xCCA5; // Original struct. @@ -1034,10 +1047,12 @@ pub struct McStateExtra { pub last_key_block: Option, pub block_create_stats: Option, pub global_balance: CurrencyCollection, + pub validators_stat: ValidatorsStat, } const MC_STATE_EXTRA_TAG: u16 = 0xcc26; const MC_STATE_CREATE_STATS_FLAG: u16 = 0b0001; +const MC_STATE_VAL_STAT_FLAG: u16 = 0b1000; impl McStateExtra { /// Adds new workchain @@ -1100,9 +1115,9 @@ impl Deserializable for McStateExtra { let cell1 = &mut SliceData::load_cell(cell.checked_drain_reference()?)?; let mut flags = 0u16; flags.read_from(cell1)?; // 16 + 0 - if flags > 1 { + if flags > 15 { fail!(BlockError::InvalidData(format!( - "Invalid flags value ({}). Must be <= 1.", + "Invalid flags value ({}). Must be <= 7.", flags ))) } @@ -1115,6 +1130,10 @@ impl Deserializable for McStateExtra { } else { Some(BlockCreateStats::construct_from(cell1)?) // 1 + 1 }; + let flag_val_stat = flags & MC_STATE_VAL_STAT_FLAG != 0; + if flag_val_stat { + self.validators_stat.read_from(cell1)?; + } self.global_balance.read_from(cell)?; Ok(()) } @@ -1131,6 +1150,9 @@ impl Serializable for McStateExtra { if self.block_create_stats.is_some() { flags |= MC_STATE_CREATE_STATS_FLAG; } + if !self.validators_stat.is_empty() { + flags |= MC_STATE_VAL_STAT_FLAG; + } flags.write_to(&mut builder1)?; self.validator_info.write_to(&mut builder1)?; self.prev_blocks.write_to(&mut builder1)?; @@ -1139,6 +1161,9 @@ impl Serializable for McStateExtra { if let Some(ref block_create_stats) = self.block_create_stats { block_create_stats.write_to(&mut builder1)?; } + if !self.validators_stat.is_empty() { + self.validators_stat.write_to(&mut builder1)?; + } builder.checked_append_reference(builder1.into_cell()?)?; self.global_balance.write_to(builder)?; diff --git a/src/block/src/out_actions.rs b/src/block/src/out_actions.rs index adb4cd3..105b995 100644 --- a/src/block/src/out_actions.rs +++ b/src/block/src/out_actions.rs @@ -9,8 +9,11 @@ * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ use crate::{ - error::BlockError, fail, messages::Message, types::CurrencyCollection, BuilderData, Cell, - Deserializable, IBitstring, Result, Serializable, SliceData, UInt256, + error::{BlockError, Error}, + fail, + messages::Message, + types::CurrencyCollection, + BuilderData, Cell, Deserializable, IBitstring, Result, Serializable, SliceData, UInt256, }; use std::collections::LinkedList; @@ -53,6 +56,19 @@ pub fn unpack_out_action_slices(mut cell: SliceData) -> Result> { Ok(slices_rev) } +pub fn deserialize_out_action_slices( + action_slices: Vec, +) -> std::result::Result, (usize, Error)> { + let mut parsed_actions = Vec::with_capacity(action_slices.len()); + for (i, mut action_slice) in action_slices.into_iter().enumerate() { + match OutAction::construct_from(&mut action_slice) { + Ok(action) => parsed_actions.push(action), + Err(err) => return Err((i, err)), + } + } + Ok(parsed_actions) +} + /// /// Implementation of Serializable for OutActions /// @@ -79,10 +95,11 @@ impl Serializable for OutActions { /// impl Deserializable for OutActions { fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { - let action_slices = unpack_out_action_slices(cell.clone())?; - for mut action_slice in action_slices { - self.push_back(OutAction::construct_from(&mut action_slice)?); - } + let actions = match deserialize_out_action_slices(unpack_out_action_slices(cell.clone())?) { + Ok(actions) => actions, + Err((_, err)) => return Err(err), + }; + self.extend(actions); Ok(()) } } @@ -106,7 +123,7 @@ pub enum OutAction { /// /// Action for reserving some account balance. /// It is roughly equivalent to creating an output - /// message carrying x nanocoins to oneself, so that + /// message carrying x nanocoins to oneself,so that /// the subsequent output actions would not be able /// to spend more money than the remainder. /// @@ -231,46 +248,42 @@ impl Serializable for OutAction { } impl Deserializable for OutAction { - fn construct_from(slice: &mut SliceData) -> Result { - if slice.remaining_bits() < std::mem::size_of::() * 8 { + fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { + if cell.remaining_bits() < std::mem::size_of::() * 8 { fail!(BlockError::InvalidArg("cell can't be shorter than 32 bits".to_string())) } - let tag = slice.get_next_u32()?; - let action = match tag { + let tag = cell.get_next_u32()?; + match tag { ACTION_SEND_MSG => { - let mode = slice.get_next_byte()?; - match Message::construct_from_reference(slice) { - Ok(msg) => OutAction::new_send(mode, msg), - Err(err) => fail!(BlockError::OutActionError(err, mode)), - } + let mode = cell.get_next_byte()?; + let msg = Message::construct_from_reference(cell)?; + *self = OutAction::new_send(mode, msg); } - ACTION_SET_CODE => OutAction::new_set(slice.checked_drain_reference()?), + ACTION_SET_CODE => *self = OutAction::new_set(cell.checked_drain_reference()?), ACTION_RESERVE => { - let mode = slice.get_next_byte()?; - match Deserializable::construct_from(slice) { - Ok(value) => OutAction::new_reserve(mode, value), - Err(err) => fail!(BlockError::OutActionError(err, mode)), - } + let mode = cell.get_next_byte()?; + let value = Deserializable::construct_from(cell)?; + *self = OutAction::new_reserve(mode, value); } ACTION_CHANGE_LIB => { - let mode = slice.get_next_byte()?; + let mode = cell.get_next_byte()?; let flags = (mode >> 1) & SET_LIB_CODE_ADD_PRIVATE_OR_PUBLIC_MASK; match (mode & CHANGE_SET_LIB_MASK, flags) { (CHANGE_LIB_MODE, 0) => { - let hash = UInt256::construct_from(slice)?; - OutAction::new_change_library(mode, None, Some(hash)) + let hash = UInt256::construct_from(cell)?; + *self = OutAction::new_change_library(mode, None, Some(hash)); } (SET_LIB_CODE_MODE, SET_LIB_CODE_REMOVE) | (SET_LIB_CODE_MODE, SET_LIB_CODE_ADD_PRIVATE) | (SET_LIB_CODE_MODE, SET_LIB_CODE_ADD_PUBLIC) => { - let code = slice.checked_drain_reference()?; - OutAction::new_change_library(mode, Some(code), None) + let code = cell.checked_drain_reference()?; + *self = OutAction::new_change_library(mode, Some(code), None); } _ => fail!("wrong mode for ChangeLibrary action: {mode}"), } } tag => fail!(BlockError::InvalidConstructorTag { t: tag, s: "OutAction".to_string() }), - }; - Ok(action) + } + Ok(()) } } diff --git a/src/block/src/shard.rs b/src/block/src/shard.rs index 9d528b1..3e6ab62 100644 --- a/src/block/src/shard.rs +++ b/src/block/src/shard.rs @@ -31,7 +31,6 @@ use crate::{ use std::{ any::type_name, fmt::{self, Display, Formatter}, - str::FromStr, }; #[cfg(test)] @@ -645,24 +644,6 @@ impl fmt::Debug for ShardIdent { } } -impl FromStr for ShardIdent { - type Err = crate::Error; - - fn from_str(s: &str) -> Result { - let (workchain_part, shard_part_with_maybe_extra) = - s.split_once(':').ok_or_else(|| error!("Can't read shard ident from {}", s))?; - - let workchain_id: i32 = workchain_part - .trim() - .parse() - .map_err(|e| error!("Can't read workchain_id from {}: {}", s, e))?; - let prefix = u64::from_str_radix(shard_part_with_maybe_extra.trim(), 16) - .map_err(|e| error!("Can't read shard from {}: {}", s, e))?; - - Ok(Self { workchain_id, prefix }) - } -} - impl Deserializable for ShardIdent { fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { let constructor_and_pfx = cell.get_next_byte()?; diff --git a/src/block/src/storage_stat.rs b/src/block/src/storage_stat.rs index b96b152..2dc6e16 100644 --- a/src/block/src/storage_stat.rs +++ b/src/block/src/storage_stat.rs @@ -17,7 +17,6 @@ use std::{collections::BTreeMap, ops::Not}; mod tests; const DICT_PROOF_TAG: u32 = 0x37c1e3fc; -const CONSENSUS_EXTRA_DATA_TAG: u32 = 0x638eb292; #[derive(Debug, Default, Clone, PartialEq)] pub struct StorageStatCellInfo { @@ -309,39 +308,3 @@ impl Deserializable for AccountStorageDictProof { Ok(()) } } - -/// consensus_extra_data#638eb292 flags:# gen_utime_ms:uint64 = ConsensusExtraData; -#[derive(Debug, Default, Clone)] -pub struct ConsensusExtraData { - pub flags: u32, - pub gen_utime_ms: u64, -} - -impl ConsensusExtraData { - pub const TAG: u32 = CONSENSUS_EXTRA_DATA_TAG; -} - -impl Serializable for ConsensusExtraData { - fn write_to(&self, cell: &mut BuilderData) -> Result<()> { - cell.append_u32(CONSENSUS_EXTRA_DATA_TAG)?; - cell.append_u32(self.flags)?; - cell.append_u64(self.gen_utime_ms)?; - Ok(()) - } -} - -impl Deserializable for ConsensusExtraData { - fn read_from(&mut self, cell: &mut SliceData) -> Result<()> { - let tag = cell.get_next_u32()?; - if tag != CONSENSUS_EXTRA_DATA_TAG { - fail!( - "Invalid ConsensusExtraData tag: expected {:#x}, found {:#x}", - CONSENSUS_EXTRA_DATA_TAG, - tag - ); - } - self.flags = cell.get_next_u32()?; - self.gen_utime_ms = cell.get_next_u64()?; - Ok(()) - } -} diff --git a/src/block/src/tests/test_config_params.rs b/src/block/src/tests/test_config_params.rs index 4219ab8..2c9992f 100644 --- a/src/block/src/tests/test_config_params.rs +++ b/src/block/src/tests/test_config_params.rs @@ -933,60 +933,29 @@ fn test_accelerated_consensus_config() { #[test] fn test_simplex_config() { let config = SimplexConfig { + target_rate_ms: 300, slots_per_leader_window: 4, - noncritical_params: NoncriticalParams { - target_rate_ms: 300, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); assert_eq!(config, config2); } -#[test] -fn test_simplex_config_with_quic() { - let config = SimplexConfig { - use_quic: true, - slots_per_leader_window: 4, - noncritical_params: NoncriticalParams { - target_rate_ms: 300, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, - ..Default::default() - }, - ..Default::default() - }; - let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); - let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); - assert_eq!(config, config2); - assert!(config2.use_quic); -} - #[test] fn test_new_consensus_config_all_both() { let mc_config = SimplexConfig { + target_rate_ms: 300, slots_per_leader_window: 4, - noncritical_params: NoncriticalParams { - target_rate_ms: 300, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, }; let shard_config = SimplexConfig { + target_rate_ms: 200, slots_per_leader_window: 8, - noncritical_params: NoncriticalParams { - target_rate_ms: 200, - first_block_timeout_ms: 500, - max_leader_window_desync: 50, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 500, + max_leader_window_desync: 50, }; let config = NewConsensusConfigAll { mc: Some(mc_config), shard: Some(shard_config) }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); @@ -997,14 +966,10 @@ fn test_new_consensus_config_all_both() { #[test] fn test_new_consensus_config_all_shard_only() { let shard_config = SimplexConfig { + target_rate_ms: 200, slots_per_leader_window: 8, - noncritical_params: NoncriticalParams { - target_rate_ms: 200, - first_block_timeout_ms: 500, - max_leader_window_desync: 50, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 500, + max_leader_window_desync: 50, }; let config = NewConsensusConfigAll { mc: None, shard: Some(shard_config) }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); @@ -1015,14 +980,10 @@ fn test_new_consensus_config_all_shard_only() { #[test] fn test_new_consensus_config_all_mc_only() { let mc_config = SimplexConfig { + target_rate_ms: 300, slots_per_leader_window: 4, - noncritical_params: NoncriticalParams { - target_rate_ms: 300, - first_block_timeout_ms: 1000, - max_leader_window_desync: 100, - ..Default::default() - }, - ..Default::default() + first_block_timeout_ms: 1000, + max_leader_window_desync: 100, }; let config = NewConsensusConfigAll { mc: Some(mc_config), shard: None }; let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); @@ -1037,145 +998,3 @@ fn test_new_consensus_config_all_empty() { let config2 = NewConsensusConfigAll::construct_from_cell(cell).unwrap(); assert_eq!(config, config2); } - -// ===================== simplex_config v2 serialization tests ===================== - -#[test] -fn test_simplex_config_v2_round_trip() { - let config = SimplexConfig { - use_quic: true, - slots_per_leader_window: 8, - noncritical_params: NoncriticalParams { - target_rate_ms: 2400, - first_block_timeout_ms: 1000, - max_leader_window_desync: 250, - candidate_resolve_rate_limit: 10, - ..Default::default() - }, - ..Default::default() - }; - let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); - let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); - assert_eq!(config, config2); -} - -#[test] -fn test_simplex_config_default_round_trip() { - let config = SimplexConfig::default(); - let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); - let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); - assert_eq!(config, config2); -} - -#[test] -fn test_simplex_config_custom_noncritical_params() { - let config = SimplexConfig { - use_quic: true, - slots_per_leader_window: 6, - noncritical_params: NoncriticalParams { - target_rate_ms: 500, - first_block_timeout_ms: 2000, - max_leader_window_desync: 100, - first_block_timeout_multiplier_bits: (1.5f32).to_bits(), - bad_signature_ban_duration_ms: 10_000, - ..Default::default() - }, - ..Default::default() - }; - let cell = config.write_to_new_cell().unwrap().into_cell().unwrap(); - let config2 = SimplexConfig::construct_from_cell(cell).unwrap(); - assert_eq!(config, config2); -} - -// ===================== v1 deserialization backward-compat tests ===================== - -/// Build a raw simplex_config#21 cell by hand (the legacy on-chain format). -fn build_v1_cell(use_quic: bool, target_rate_ms: u32, slots: u32, fbt_ms: u32, mld: u32) -> Cell { - let mut b = BuilderData::new(); - b.append_u8(0x21).unwrap(); - b.append_u8(if use_quic { 1 } else { 0 }).unwrap(); - target_rate_ms.write_to(&mut b).unwrap(); - slots.write_to(&mut b).unwrap(); - fbt_ms.write_to(&mut b).unwrap(); - mld.write_to(&mut b).unwrap(); - b.into_cell().unwrap() -} - -#[test] -fn test_deserialize_v1_cell() { - let cell = build_v1_cell(false, 300, 4, 1000, 100); - let config = SimplexConfig::construct_from_cell(cell).unwrap(); - assert!(!config.use_quic); - assert_eq!(config.slots_per_leader_window, 4); - assert_eq!(config.noncritical_params.target_rate_ms, 300); - assert_eq!(config.noncritical_params.first_block_timeout_ms, 1000); - assert_eq!(config.noncritical_params.max_leader_window_desync, 100); - let d = NoncriticalParams::default(); - assert_eq!( - config.noncritical_params.first_block_timeout_multiplier_bits, - d.first_block_timeout_multiplier_bits - ); -} - -#[test] -fn test_deserialize_v1_cell_with_quic() { - let cell = build_v1_cell(true, 300, 4, 1000, 100); - let config = SimplexConfig::construct_from_cell(cell).unwrap(); - assert!(config.use_quic); -} - -#[test] -fn test_new_consensus_config_all_with_v2() { - let config = SimplexConfig { - use_quic: true, - slots_per_leader_window: 8, - noncritical_params: NoncriticalParams { - target_rate_ms: 2400, - first_block_timeout_ms: 1000, - max_leader_window_desync: 250, - ..Default::default() - }, - ..Default::default() - }; - - let all = NewConsensusConfigAll { mc: Some(config.clone()), shard: Some(config.clone()) }; - let cell = all.write_to_new_cell().unwrap().into_cell().unwrap(); - let parsed = NewConsensusConfigAll::construct_from_cell(cell).unwrap(); - assert_eq!(parsed.mc.unwrap(), config); - assert_eq!(parsed.shard.unwrap(), config); -} - -#[test] -fn test_new_consensus_config_all_mixed_v1_mc_v2_shard() { - let mc_cell = build_v1_cell(false, 300, 4, 1000, 100); - let shard_config = SimplexConfig { - slots_per_leader_window: 6, - noncritical_params: NoncriticalParams { - target_rate_ms: 500, - first_block_timeout_ms: 2000, - ..Default::default() - }, - ..Default::default() - }; - let shard_cell = shard_config.write_to_new_cell().unwrap().into_cell().unwrap(); - - let mut builder = BuilderData::new(); - builder.append_u8(0x10).unwrap(); - builder.append_bit_one().unwrap(); - builder.checked_append_reference(mc_cell).unwrap(); - builder.append_bit_one().unwrap(); - builder.checked_append_reference(shard_cell).unwrap(); - let cell = builder.into_cell().unwrap(); - - let parsed = NewConsensusConfigAll::construct_from_cell(cell).unwrap(); - let mc = parsed.mc.unwrap(); - assert_eq!(mc.noncritical_params.target_rate_ms, 300); - - let shard = parsed.shard.unwrap(); - assert_eq!(shard.noncritical_params.target_rate_ms, 500); - assert_eq!(shard.noncritical_params.first_block_timeout_ms, 2000); - assert_eq!( - shard.noncritical_params.max_leader_window_desync, - NoncriticalParams::default().max_leader_window_desync - ); -} diff --git a/src/block/src/tests/test_master.rs b/src/block/src/tests/test_master.rs index a7b44bd..150dd8d 100644 --- a/src/block/src/tests/test_master.rs +++ b/src/block/src/tests/test_master.rs @@ -138,6 +138,12 @@ fn test_mc_state_extra() { .unwrap(); write_read_and_assert(extra.clone()); + + extra.validators_stat = ValidatorsStat::new(3); + extra.validators_stat.update(1, |_| 123).unwrap(); + extra.validators_stat.update(2, |_| 456).unwrap(); + + write_read_and_assert(extra.clone()); } fn build_mc_block_extra() -> McBlockExtra { diff --git a/src/block/src/tests/test_out_actions.rs b/src/block/src/tests/test_out_actions.rs index f236c21..fe4fd7b 100644 --- a/src/block/src/tests/test_out_actions.rs +++ b/src/block/src/tests/test_out_actions.rs @@ -129,24 +129,39 @@ fn test_unpack_out_action_slices_rejects_non_empty_tail() { #[test] fn test_deserialize_out_action_slices_valid_list() { let actions = get_out_actions(); - let slice = SliceData::load_cell(actions.serialize().unwrap()).unwrap(); - let slices = unpack_out_action_slices(slice).unwrap(); - assert_eq!(slices.len(), actions.len()); - for (expected, mut slice) in actions.into_iter().zip(slices.into_iter()) { - let actual = OutAction::construct_from(&mut slice).unwrap(); + let slices = + unpack_out_action_slices(SliceData::load_cell(actions.serialize().unwrap()).unwrap()) + .unwrap(); + let parsed = deserialize_out_action_slices(slices).unwrap(); + assert_eq!(parsed.len(), actions.len()); + for (expected, actual) in actions.into_iter().zip(parsed.into_iter()) { assert_eq!(expected, actual); } } #[test] -fn test_deserialize_bad_out_action() { +fn test_deserialize_out_action_slices_returns_indexed_error() { let valid_cell = OutAction::new_set(Cell::default()).serialize().unwrap(); - let mut valid_slice = SliceData::load_cell(valid_cell).unwrap(); - OutAction::construct_from(&mut valid_slice).unwrap(); // sanity check that the valid slice is indeed valid + let valid_slice = SliceData::load_cell(valid_cell).unwrap(); let mut invalid_builder = BuilderData::new(); 0xffff_ffffu32.write_to(&mut invalid_builder).unwrap(); - let mut invalid_slice = SliceData::load_cell(invalid_builder.into_cell().unwrap()).unwrap(); + let invalid_slice = SliceData::load_cell(invalid_builder.into_cell().unwrap()).unwrap(); - OutAction::construct_from(&mut invalid_slice).unwrap_err(); // sanity check that the invalid slice is indeed invalid + let err = deserialize_out_action_slices(vec![valid_slice, invalid_slice]).unwrap_err(); + assert_eq!(err.0, 1); } + +// TODO: move to anythere +// #[test] +// fn test_tvm_serialize_currency_collection() { +// let coins = 1u64<<63; +// let coins1 = int!(coins).as_coins().unwrap(); +// let coins1 = serialize_currency_collection(coins1, None).unwrap(); +// let coins1: CurrencyCollection = CurrencyCollection::construct_from(&mut coins1.into()).unwrap(); +// let coins2 = CurrencyCollection::with_coins(coins); +// assert_eq!(coins1, coins2); + +// assert_eq!(int!(1u128<<120).as_coins().expect_err("Expect range check error").code, +// ExceptionCode::RangeCheckError); +// } diff --git a/src/block/src/tests/test_validators.rs b/src/block/src/tests/test_validators.rs index d5544a2..bc755e5 100644 --- a/src/block/src/tests/test_validators.rs +++ b/src/block/src/tests/test_validators.rs @@ -213,3 +213,16 @@ fn test_isolate_mc_validators() { } } } + +#[test] +fn test_validator_shard_stat() { + // std::env::set_var("RUST_BACKTRACE", "full"); + for len in 0..1024 { + println!("len = {}", len); + let mut stat = ValidatorsStat::default(); + for i in 0..len { + stat.values.push(i as u16); + } + write_read_and_assert(stat.clone()); + } +} diff --git a/src/block/src/transactions.rs b/src/block/src/transactions.rs index 2d6883b..f200424 100644 --- a/src/block/src/transactions.rs +++ b/src/block/src/transactions.rs @@ -1265,7 +1265,6 @@ pub struct Transaction { pub in_msg: ChildCell, pub out_msgs: OutMessages, total_fees: CurrencyCollection, - blackhole_burned: Coins, state_update: ChildCell, description: ChildCell, } @@ -1286,7 +1285,6 @@ impl Transaction { in_msg: ChildCell::default(), out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), - blackhole_burned: Coins::default(), state_update: ChildCell::default(), description: ChildCell::default(), } @@ -1310,7 +1308,6 @@ impl Transaction { in_msg: ChildCell::with_struct(msg)?, out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), - blackhole_burned: Coins::default(), state_update: ChildCell::default(), description: ChildCell::default(), }) @@ -1374,14 +1371,6 @@ impl Transaction { &mut self.total_fees } - pub fn blackhole_burned(&self) -> &Coins { - &self.blackhole_burned - } - - pub fn set_blackhole_burned(&mut self, burned: Coins) { - self.blackhole_burned = burned; - } - pub fn read_in_msg(&self) -> Result> { match self.in_msg.is_empty() { true => Ok(None), @@ -1425,7 +1414,7 @@ impl Transaction { /// add output message to Hashmap pub fn add_out_message(&mut self, msg: &Message) -> Result<()> { - self.out_msgs.setref(&UInt15(self.outmsg_cnt), msg.serialize()?)?; + self.out_msgs.setref(&UInt15(self.outmsg_cnt), &msg.serialize()?)?; self.outmsg_cnt += 1; Ok(()) } @@ -1568,7 +1557,6 @@ impl Default for Transaction { in_msg: ChildCell::default(), out_msgs: OutMessages::default(), total_fees: CurrencyCollection::default(), - blackhole_burned: Coins::default(), state_update: ChildCell::default(), description: ChildCell::default(), } diff --git a/src/block/src/validators.rs b/src/block/src/validators.rs index d60ffb8..d42882b 100644 --- a/src/block/src/validators.rs +++ b/src/block/src/validators.rs @@ -10,14 +10,14 @@ */ use crate::{ config_params::CatchainConfig, - define_HashmapE, + define_HashmapE, error, error::{BlockError, Result}, fail, sha512_digest, shard::{MASTERCHAIN_ID, SHARD_FULL}, signature::{CryptoSignature, SigPubKey}, types::Number16, BuilderData, ByteOrderRead, Cell, Crc32, Deserializable, IBitstring, KeyId, Serializable, - SliceData, UInt256, + SliceData, UInt256, MAX_DATA_BITS, }; use std::{ borrow::Cow, @@ -664,6 +664,128 @@ impl ValidatorSetPRNG { } } +const VALIDATORS_SHARD_STAT_TAG: u8 = 0x1; // 4 bits +const VALIDATORS_STAT_EXPECTED_MAX: usize = 256; + +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct ValidatorsStat { + // Single u16 value for each validator. + // VALIDATORS_STAT_EXPECTED_MAX values are stored inplace, + // if more - SmallVec just reallocates memory using heap. + values: smallvec::SmallVec<[u16; VALIDATORS_STAT_EXPECTED_MAX]>, +} + +impl ValidatorsStat { + pub fn new(validators_count: u16) -> Self { + ValidatorsStat { values: smallvec::smallvec![0; validators_count as usize] } + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn update(&mut self, validator_index: u16, updater: F) -> Result<()> + where + F: FnOnce(u16) -> u16, + { + if self.values.len() <= validator_index as usize { + fail!("Invalid validator index: {} (max is {})", validator_index, self.values.len() - 1) + } + self.values[validator_index as usize] = updater(self.values[validator_index as usize]); + Ok(()) + } + + pub fn get(&self, validator_index: u16) -> Result { + if self.values.is_empty() { + fail!("ValidatorsStat is empty") + } + self.values.get(validator_index as usize).copied().ok_or_else(|| { + error!( + "Invalid validator index: {} (max is {})", + validator_index, + self.values.len() - 1 + ) + }) + } + + pub fn len(&self) -> usize { + self.values.len() + } +} + +impl Serializable for ValidatorsStat { + fn write_to(&self, builder: &mut BuilderData) -> Result<()> { + builder.append_bits(VALIDATORS_SHARD_STAT_TAG as usize, 4)?; + + // Items are wrote one by one into cell. If the cell is full, + // the rest of the items are wrote into the child cell, etc. + + let mut remaining_len = self.values.len(); + if remaining_len == 0 { + return Ok(()); + } + let mut stack = vec![builder.clone()]; + loop { + // Calculate how many items can be written to the current builder + let builder_fits = + stack.last().ok_or_else(|| error!("INTERNAL ERROR: stack is empty"))?.bits_free() + / 16; + + // If the current builder can fit all remaining items - finish + if builder_fits >= remaining_len { + break; + } else { + // If not - create one more builder and push it to the stack + remaining_len = remaining_len.saturating_sub(builder_fits); + stack.push(BuilderData::new()); + } + } + + // Write items to the builders from the last (deeper) cell to the first. + let mut start = self.values.len().saturating_sub(remaining_len); + while let Some(mut last_builder) = stack.pop() { + // Fill a builder + let builder_fits = last_builder.bits_free() / 16; + for i in start..min(start + builder_fits, self.values.len()) { + last_builder.append_u16(self.values[i])?; + } + + // Move start to the start of the next builder (minus one cell of items) + start = start.saturating_sub(MAX_DATA_BITS / 16); + + if let Some(prev_builder) = stack.last_mut() { + prev_builder.checked_append_reference(last_builder.into_cell()?)?; + } else { + *builder = last_builder; + } + } + + Ok(()) + } +} + +impl Deserializable for ValidatorsStat { + fn read_from(&mut self, slice: &mut SliceData) -> Result<()> { + let tag = slice.get_next_int(4)? as u8; + if tag != VALIDATORS_SHARD_STAT_TAG { + fail!("Invalid tag for ValidatorsShardStat: {}", tag) + } + self.values.clear(); + while slice.remaining_bits() > 0 { + self.values.push(u16::construct_from(slice)?); + } + let mut next_cell_opt = slice.checked_drain_reference().ok(); + while let Some(next_cell) = next_cell_opt { + let mut slice = SliceData::load_cell(next_cell)?; + while slice.remaining_bits() > 0 { + self.values.push(u16::construct_from(&mut slice)?); + } + next_cell_opt = slice.checked_drain_reference().ok(); + } + Ok(()) + } +} + #[cfg(test)] #[path = "tests/test_validators.rs"] mod tests; diff --git a/src/ci/sync-test/README.md b/src/ci/sync-test/README.md deleted file mode 100644 index d6aedc8..0000000 --- a/src/ci/sync-test/README.md +++ /dev/null @@ -1,224 +0,0 @@ -# Sync Test - -Automated mainnet sync test for the TON Rust Node. Builds a node image from the current commit, deploys it to Kubernetes via the public [`ton-rust-node`](https://github.com/rsquad/ton-rust-node) Helm chart, and waits for the node to fully sync with the network. Reports the result as a GitHub commit status on the triggering commit. - -## How it works - -### Overview - -``` -GitHub Actions (manual trigger, ~5 min) - 1. Build node image โ†’ ghcr.io/rsquad/ton-node:sha- - 2. Set GitHub commit status โ†’ pending - 3. helm upgrade --install โ†’ deploys to ton-synctest namespace - 4. CI exits - -Kubernetes pod (runs for hours) - Container "ton-node": syncs with mainnet - Container "watcher": polls metrics, reports result to GitHub -``` - -### Watcher logic - -The watcher runs as a sidecar container alongside the node. Every 60 seconds it fetches the node's Prometheus metrics and checks sync progress. - -**Metrics used:** - -| Metric | Description | -|--------|-------------| -| `ton_node_engine_sync_status` | Sync state machine (see stages below) | -| `ton_node_engine_last_mc_block_seqno` | Latest applied masterchain block seqno | -| `ton_node_engine_timediff_seconds` | Seconds between now and last applied MC block | -| `ton_node_engine_shards_timediff_seconds` | Seconds between now and MC block last processed by shard client | - -**Sync stages** (`sync_status` values): - -| Value | Name | Description | -|-------|------|-------------| -| 0 | `not_set` | Initial state, node just started | -| 1 | `boot` | Downloading init block proof, key blocks | -| 2 | `load_states` | Downloading and applying persistent states (long phase, seqno does not advance) | -| 3 | `finish_boot` | Boot complete, preparing to sync | -| 4 | `sync_archives` | Syncing via archives (bulk download) | -| 5 | `sync_blocks` | Syncing block-by-block from peers | -| 6 | `synced` | Masterchain caught up, shard client within 16 MC blocks | -| 7 | `checking_db` | DB integrity check in progress | -| 8 | `db_broken` | DB corruption detected | - -**Terminal conditions:** - -| Condition | Trigger | GitHub status | Pod behavior | -|-----------|---------|---------------|--------------| -| Synced | `sync_status = 6` | `success` | Sleeps forever (replaced on next run) | -| DB broken | `sync_status = 8` | `failure` | Sleeps forever (stays for debugging) | -| Timeout | Elapsed > `SYNC_TIMEOUT` (default 24h) | `failure` | Sleeps forever (stays for debugging) | - -**Watcher log output:** - -``` -[watcher] stage=boot seqno=0 mc_timediff=0s shards_timediff=0s elapsed=0h0m -[watcher] stage=boot seqno=47554071 mc_timediff=200000s shards_timediff=200000s elapsed=0h1m -[watcher] stage=load_states seqno=58847563 mc_timediff=67000s shards_timediff=67000s elapsed=0h30m -[watcher] stage=sync_archives seqno=58850000 mc_timediff=10000s shards_timediff=10000s elapsed=1h00m -[watcher] stage=sync_blocks seqno=58870000 mc_timediff=500s shards_timediff=600s elapsed=3h00m -[watcher] stage=synced seqno=58881412 mc_timediff=2s shards_timediff=2s elapsed=3h34m -[watcher] SUCCESS โ€” node synced -``` - -### Debugging failures - -On failure (timeout or DB broken) the pod stays alive. Inspect logs: - -```bash -# Watcher log (sync progress) -kubectl logs synctest-mainnet-0 -c watcher -n ton-synctest - -# Node log (last 200 lines) -kubectl logs synctest-mainnet-0 -c ton-node -n ton-synctest --tail=200 - -# Full node log file (written by log4rs) -kubectl exec -it synctest-mainnet-0 -c ton-node -n ton-synctest -- tail -200 /logs/output.log - -# Useful greps for node log -kubectl exec synctest-mainnet-0 -c ton-node -n ton-synctest -- grep -e boot -e sync /logs/output.log | tail -20 -kubectl exec synctest-mainnet-0 -c ton-node -n ton-synctest -- grep Applied /logs/output.log | tail -20 -``` - -### Re-running - -Each workflow run **deletes the previous deployment** (helm uninstall + PVC cleanup) before deploying fresh. The old watcher catches SIGTERM and sets `failure "Cancelled"` on its commit so no commit is left stuck in `pending`. - -This means: if a sync test is still running and you trigger a new one, the old test is cancelled and its commit gets a red status. Inspect logs before re-running if you need to debug a failure. - -## How to run - -```bash -gh workflow run sync-test.yml -R RSquad/ton-node -``` - -Or: GitHub UI > Actions > Sync Test > Run workflow. - -Check current commit status: - -```bash -gh api repos/RSquad/ton-node/commits//status \ - --jq '.statuses[] | select(.context=="sync-test/mainnet") | {state, description}' -``` - -## Files - -| File | Purpose | -|------|---------| -| `.github/workflows/sync-test.yml` | CI workflow: build image, push to GHCR, helm deploy | -| `ci/sync-test/values.yaml` | Helm values override for the `ton-rust-node` chart | -| `ci/sync-test/watcher.sh` | Sidecar script: poll Prometheus metrics, set GitHub commit status | -| `ci/sync-test/gen-node-config.sh` | Generates node config with random ADNL keys for given IP | -| `ci/sync-test/README.md` | This file | - -## Cluster setup from scratch - -All commands target cluster `velia-sgp1`. The namespace is `ton-synctest`. - -### 1. Create namespace - -```bash -kubectl create ns ton-synctest -``` - -### 2. Image pull secret - -Required for pulling node images from GHCR. - -```bash -kubectl create secret docker-registry ghcr -n ton-synctest \ - --docker-server=ghcr.io \ - --docker-username= \ - --docker-password= -``` - -### 3. GitHub token for commit statuses - -The watcher needs a token to set commit statuses from inside the K8s pod. Create a fine-grained PAT: - -1. Go to https://github.com/settings/tokens?type=beta -2. Repository access: select `RSquad/ton-node` -3. Permissions: **Commit statuses > Read and write** (nothing else) - -```bash -kubectl create secret generic credentials -n ton-synctest \ - --from-literal=GITHUB_TOKEN=github_pat_... -``` - -### 4. Kubeconfig for CI - -The GitHub Actions runner needs kubectl/helm access to the cluster. Create a dedicated Rancher user with minimal permissions: - -1. **Rancher UI > Users & Authentication > Create**: username `synctest-ci`, global role `User-Base` -2. **Create a Project** in cluster `velia-sgp1` containing namespace `ton-synctest` -3. **Add `synctest-ci` as Project Member** to that project -4. **Download kubeconfig** for `synctest-ci` (login as that user in Rancher UI, or via Rancher API) - -Add to GitHub repo secrets (base64-encoded): - -```bash -cat kubeconfig.yaml | base64 | gh secret set SYNCTEST_VELIA_SGP1_KUBECONFIG -R RSquad/ton-node -``` - -### 5. Node IP - -The external IP for the ADNL LoadBalancer. Must match an IP available in the MetalLB pool. - -```bash -gh secret set SYNCTEST_NODE_IP -R RSquad/ton-node -b "" -``` - -CI uses this to generate the node config (ADNL address) and the MetalLB annotation. - -### Summary of resources - -**Kubernetes (ton-synctest namespace):** - -| Resource | Name | Purpose | -|----------|------|---------| -| Namespace | `ton-synctest` | Isolates sync test workloads | -| Secret | `ghcr` | Image pull credentials for GHCR | -| Secret | `credentials` | GitHub PAT for commit status API | -| ConfigMap | `synctest-watcher` | Watcher script (created by CI) | -| Secret | `*-node-configs` | Node config with ADNL keys (created by Helm) | - -**GitHub Secrets:** - -| Secret | Purpose | -|--------|---------| -| `SYNCTEST_VELIA_SGP1_KUBECONFIG` | Kubeconfig for CI to access cluster | -| `SYNCTEST_NODE_IP` | External IP for ADNL service | - -## Configuration reference - -### Sync timeout - -Environment variable `SYNC_TIMEOUT` in `values.yaml` (seconds). Default: `86400` (24 hours). If the node does not reach `sync_status=6` within this window, the test fails. - -### ADNL IP - -Set via GitHub Secret `SYNCTEST_NODE_IP`. CI uses it in both the node config (ADNL address) and MetalLB annotation (LoadBalancer IP). They must match. - -### Helm chart version - -In `.github/workflows/sync-test.yml`, env `HELM_CHART_VERSION`. Must match a published version of `oci://ghcr.io/rsquad/ton-rust-node/helm/node`. - -### Resources - -Inherited from the Helm chart defaults (8 CPU / 32Gi request, 16 CPU / 64Gi limit). Override in `values.yaml` under `resources` if needed. - -### Storage - -DB volume uses the chart default (1Ti, `local-path` storage class). All PVCs have `resourcePolicy: ""` โ€” they are deleted together with the Helm release on `helm uninstall`. - -### Global config - -Uses the mainnet `global.config.json` bundled in the Helm chart. No manual config needed. - -### Logs config - -Uses the default `logs.config.yml` bundled in the Helm chart. Node logs are written to `/logs/output.log` inside the pod. diff --git a/src/ci/sync-test/gen-node-config.sh b/src/ci/sync-test/gen-node-config.sh deleted file mode 100644 index 15718e5..0000000 --- a/src/ci/sync-test/gen-node-config.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -# Generates a minimal node config for sync test with random ADNL keys. -# Usage: gen-node-config.sh > node-0.json -set -eu - -IP="${1:?Usage: gen-node-config.sh }" - -DHT_KEY=$(openssl rand -base64 32) -FN_KEY=$(openssl rand -base64 32) -CTL_KEY=$(openssl rand -base64 32) - -cat < - -storage: - main: - resourcePolicy: "" - db: - resourcePolicy: "" - keys: - resourcePolicy: "" - -# Watcher sidecar โ€” polls /healthz, sets GitHub commit status -extraVolumes: - - name: watcher-script - configMap: - name: synctest-watcher - defaultMode: 0755 - -extraContainers: - - name: watcher - image: alpine:3.21 - command: ["/bin/sh", "/scripts/watcher.sh"] - env: - - name: GITHUB_SHA - value: "{{ .Values.synctest.sha }}" - - name: GITHUB_REPO - value: "{{ .Values.synctest.repo }}" - - name: NETWORK - value: mainnet - - name: SYNC_TIMEOUT - value: "86400" - - name: METRICS_PORT - value: "9100" - envFrom: - - secretRef: - name: credentials - resources: - requests: { cpu: 50m, memory: 64Mi } - limits: { cpu: 200m, memory: 128Mi } - volumeMounts: - - name: watcher-script - mountPath: /scripts - readOnly: true diff --git a/src/ci/sync-test/watcher.sh b/src/ci/sync-test/watcher.sh deleted file mode 100644 index b652a25..0000000 --- a/src/ci/sync-test/watcher.sh +++ /dev/null @@ -1,157 +0,0 @@ -#!/bin/sh -# -# Sync test watcher -# -# Runs as a sidecar alongside the TON node. Polls Prometheus metrics -# every 60 seconds and decides whether the node has synced, failed, -# or timed out. Reports the result as a GitHub commit status. -# -# Metrics used (from /metrics on the node's metrics port): -# ton_node_engine_sync_status โ€” sync state machine (6 = synced, 8 = db broken) -# ton_node_engine_last_mc_block_seqno โ€” latest masterchain block applied -# ton_node_engine_timediff_seconds โ€” seconds between now and last MC block -# ton_node_engine_shards_timediff_seconds โ€” seconds between now and MC block -# last processed by shard client -# -# Sync stages (sync_status values, ordered by normal flow): -# 0 = not_set Initial state, node just started -# 1 = boot Downloading init block proof, key blocks -# 2 = load_states Downloading and applying persistent states (long, no seqno progress) -# 3 = finish_boot Boot complete, preparing to sync -# 4 = sync_archives Syncing via archives (bulk download) -# 5 = sync_blocks Syncing block-by-block from peers -# 6 = synced Masterchain caught up, shard client within 16 MC blocks -# 7 = checking_db DB integrity check in progress -# 8 = db_broken DB corruption detected -# -# Behavior on terminal states: -# synced โ†’ set GitHub commit status "success", then sleep forever -# db_broken โ†’ set GitHub commit status "failure", then sleep forever -# timeout โ†’ set GitHub commit status "failure", then sleep forever -# -# On failure the pod stays alive so engineers can inspect logs: -# kubectl exec -it -c ton-node -n ton-synctest -- tail -100 /logs/output.log -# -# The next workflow run replaces the pod via `helm upgrade`. -# -# Required env: METRICS_PORT, SYNC_TIMEOUT, NETWORK, GITHUB_TOKEN, GITHUB_SHA, GITHUB_REPO - -set -eu - -apk add --no-cache curl jq >/dev/null 2>&1 - -# --------------------------------------------------------------------------- -# Graceful shutdown: if pod is killed before sync completes, report failure. -# --------------------------------------------------------------------------- - -FINISHED=false - -cleanup() { - if [ "$FINISHED" = "false" ]; then - elapsed=$(( $(date +%s) - ${START:-0} )) - github_status failure "Cancelled after $(fmt $elapsed)" - echo "[watcher] CANCELLED โ€” pod terminated before sync completed" - fi - exit 0 -} - -trap cleanup TERM INT - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -# Set commit status on the GitHub commit that triggered this test. -github_status() { - local state="$1" description="$2" - curl -sf -X POST \ - "https://api.github.com/repos/${GITHUB_REPO}/statuses/${GITHUB_SHA}" \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$(jq -cn --arg s "$state" --arg d "$description" \ - --arg c "sync-test/${NETWORK}" \ - '{state:$s, description:$d, context:$c}')" >/dev/null 2>&1 || true -} - -# Format seconds as "Xh Ym". -fmt() { - local h=$(($1 / 3600)) m=$((($1 % 3600) / 60)) - if [ "$h" -gt 0 ]; then echo "${h}h${m}m"; else echo "${m}m"; fi -} - -# Human-readable name for sync_status value. -stage_name() { - case "$1" in - 0) echo "not_set" ;; 1) echo "boot" ;; 2) echo "load_states" ;; - 3) echo "finish_boot" ;; 4) echo "sync_archives" ;; 5) echo "sync_blocks" ;; - 6) echo "synced" ;; 7) echo "checking_db" ;; 8) echo "db_broken" ;; - *) echo "unknown($1)" ;; - esac -} - -# Extract a gauge value from Prometheus text output. -# Handles both "name value" and "name{labels} value" formats. -# Usage: metric "$prometheus_text" "metric_name" -metric() { - echo "$1" | awk -v m="$2" '$1 ~ "^" m "($|\\{)" && $1 !~ /^#/ { print int($2); exit }' -} - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -METRICS="http://localhost:${METRICS_PORT}/metrics" -HEALTHZ="http://localhost:${METRICS_PORT}/healthz" -POLL_INTERVAL=60 - -# Wait for the node's HTTP server to come up. -while ! curl -sf "$HEALTHZ" >/dev/null 2>&1; do sleep 5; done - -START=$(date +%s) - -while true; do - # Fetch all metrics in one request. - prom=$(curl -sf "$METRICS" 2>/dev/null) || { sleep "$POLL_INTERVAL"; continue; } - - status=$(metric "$prom" "ton_node_engine_sync_status") - seqno=$(metric "$prom" "ton_node_engine_last_mc_block_seqno") - timediff=$(metric "$prom" "ton_node_engine_timediff_seconds") - shards_td=$(metric "$prom" "ton_node_engine_shards_timediff_seconds") - status=${status:-0} - seqno=${seqno:-0} - timediff=${timediff:--} - shards_td=${shards_td:--} - elapsed=$(( $(date +%s) - START )) - - echo "[watcher] stage=$(stage_name "$status") seqno=$seqno mc_timediff=${timediff}s shards_timediff=${shards_td}s elapsed=$(fmt $elapsed)" - - # --- Terminal states --- - - # sync_status=6: node considers itself synced (MC timediff < 600s, - # shard client within 16 MC blocks of masterchain). - if [ "$status" = "6" ]; then - FINISHED=true - github_status success "Synced in $(fmt $elapsed) (seqno $seqno, mc_timediff ${timediff}s)" - echo "[watcher] SUCCESS โ€” node synced" - exec tail -f /dev/null - fi - - # sync_status=8: database corruption detected. - if [ "$status" = "8" ]; then - FINISHED=true - github_status failure "DB broken (seqno $seqno, $(fmt $elapsed))" - echo "[watcher] FAILURE โ€” DB broken, pod stays alive for debugging" - exec tail -f /dev/null - fi - - # Timeout: node did not sync within the allowed window. - if [ "$elapsed" -gt "$SYNC_TIMEOUT" ]; then - FINISHED=true - github_status failure "Timeout after $(fmt $elapsed): $(stage_name "$status"), mc_timediff=${timediff}s, shards=${shards_td}s" - echo "[watcher] FAILURE โ€” timeout after $(fmt $elapsed), pod stays alive for debugging" - exec tail -f /dev/null - fi - - sleep "$POLL_INTERVAL" & - wait $! -done diff --git a/src/common/config/log_cfg_debug.yml b/src/common/config/log_cfg_debug.yml index 7dc3bbd..f772902 100644 --- a/src/common/config/log_cfg_debug.yml +++ b/src/common/config/log_cfg_debug.yml @@ -45,8 +45,6 @@ loggers: level: debug rldp: level: info - quic: - level: warn ton_block: level: off diff --git a/src/emulator/src/lib.rs b/src/emulator/src/lib.rs index 26e1901..40f32e2 100644 --- a/src/emulator/src/lib.rs +++ b/src/emulator/src/lib.rs @@ -63,8 +63,9 @@ pub extern "C" fn transaction_emulator_create( vm_log_verbosity: u32, ) -> *mut c_void { init_log_without_config(None, log_level_from_verbosity(vm_log_verbosity), None); - match deserialize_boc(config_params_boc).and_then(ConfigParams::with_root) { - Ok(config_params) => { + match deserialize_boc(config_params_boc) { + Ok(config_params_root) => { + let config_params = ConfigParams::with_root(config_params_root); let emulator = Box::new(Emulator::new(config_params)); Box::into_raw(emulator) as *mut c_void } @@ -175,8 +176,9 @@ pub extern "C" fn transaction_emulator_set_config( log::error!("Received null pointer for transaction_emulator"); return; } - match deserialize_boc(config_params_boc).and_then(ConfigParams::with_root) { - Ok(config_params) => { + match deserialize_boc(config_params_boc) { + Ok(config_params_root) => { + let config_params = ConfigParams::with_root(config_params_root); let transaction_emulator = unsafe { &mut *(transaction_emulator as *mut Emulator) }; transaction_emulator.config_params = config_params; } @@ -233,11 +235,12 @@ pub extern "C" fn transaction_emulator_set_prev_blocks_info( match SliceData::load_cell(info_cell).and_then(|mut slice| read_stack_item(&mut slice)) { Ok(info) => { - transaction_emulator.prev_blocks_info = if info.is_tuple() { - PrevBlocksInfo::Tuple(info) + if info.is_tuple() { + transaction_emulator.prev_blocks_info = PrevBlocksInfo::Tuple(info); } else { - PrevBlocksInfo::Tuple(StackItem::tuple(Vec::new())) - }; + transaction_emulator.prev_blocks_info = + PrevBlocksInfo::Tuple(StackItem::tuple(Vec::new())); + } } Err(err) => { log::error!("Failed to parse info_cell: {err}"); diff --git a/src/executor/benches/benchmarks.rs b/src/executor/benches/benchmarks.rs index a57b375..a503194 100644 --- a/src/executor/benches/benchmarks.rs +++ b/src/executor/benches/benchmarks.rs @@ -32,7 +32,7 @@ fn replay_transaction_by_files( ) { let config = read_config(config).unwrap(); let mc_state_proof = mc_state_proof_cell_with_config(config); - replay_transaction(c, acc, acc_after, tr, "", mc_state_proof); + replay_transaction(c, acc, acc_after, tr, mc_state_proof); } fn bench_simple_transaction(c: &mut Criterion) { @@ -326,7 +326,7 @@ fn load_blockchain_config(config_account: &Account) -> Result .ok_or_else(|| error!("config account data loading error"))? .reference(0) .unwrap(); - let config_params = ConfigParams::with_root(config_cell)?; + let config_params = ConfigParams::with_root(config_cell); BlockchainConfig::with_config(config_params) } @@ -351,7 +351,7 @@ fn load_block(block_filename: &str) -> Result { fn replay_block(data: BlockData) -> Status { for acc in data.accounts { - let mut account = Account::construct_from_cell(acc.account_cell.clone())?; + let mut account = acc.account_cell; for tr in acc.transactions { let executor: Box = match tr.read_description()? { TransactionDescr::TickTock(desc) => { @@ -372,7 +372,7 @@ fn replay_block(data: BlockData) -> Status { ..Default::default() }, )?; - if account.serialize()?.repr_hash() != tr.read_state_update()?.new_hash { + if account.repr_hash() != tr.read_state_update()?.new_hash { fail!("new hash mismatch"); } } @@ -414,6 +414,7 @@ criterion_group!( bench_tick_tock_message, bench_count_steps_vm, bench_not_aborted_accepted_transaction, + bench_ihr_fee_output_msg, bench_block_0, ); criterion_main!(benches); diff --git a/src/executor/real_boc/bad_action_with_ignore_flag_account_new.boc b/src/executor/real_boc/bad_action_with_ignore_flag_account_new.boc deleted file mode 100644 index 6a85f8bf93a6c7d731adb153a8dc4a4eac624740..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 756 zcmXw0ZAep57(VB2y1C^xw>2B6J9lRJL-CHX2qaheqs(BN2GV{Eim*hk!!P>h{LH?l zcZz`;lnDK*wDqMnxedzMmqUNtK(XtjFl14erE^y6%!&i=$2sSHpXYtv$6tr)AfbbR zSOr8aD_*SnQ5>}m8hkc=;mVV8Q|e9AyDI(3XU4m)z{;1sNAuc0iB7E!I04xB_4~U% zMKL(jsxdqKP~g0&)?sx&TNij>o&E3UsB_=%qvcw7|;z>`eC^K9j#uub^po ze{A2ukRK5ciIWI;JXrz>K}9SE5qIJRV(i`L5GOaFR@M1S4YZGJRn=dl8+@Z4hlu}s zH{pKdLL3Q$4ICLWQ?Zh0Zmfi=^f7imToP=7M>sN))Fz22YVVjBYT0GxNN4jx5pTi( zEre^Fp?GIVordwHDcH#5KtJVBR_|$-$`s?u+j#_}ccfxs3MJ6rd*3vC!Hm{=);i)W z&_Bpr$EhljcC#Seo3mxzpV1g-^iT63?crIR75ZHWW+f&_5U-*q1paDs8vN}379I|7 zk!@1WNMMtV5+lqT5(kr>1M>!i<2WY+vQ21yyQvz;?Q_T$Ov?g$`6J=ANJfQVyEO;5 z>#!WawlkVNDFnNWo+wy?Bh7)=;Ee&!>Sn?f$P-mAITwmPs731uOd=vCX-^I5E+a_p z5asM6r-9d<54l{_ly$5Ii^56;z87(&EV1n~lc&{wV52bN6|M}Iz>(D&b-QE|zv52- zyYT7I9DppqlCN=jQY~Vf{SXHk>X1z}wxYz%G}@h@AI8k^|9&uO=HGsHMUup~5}eIq zyr*)3zvkA8aAX(w>qaaANkcD2?+FDgqFJ3f0`OR%j=})D-T9=zd*;BlXjaXH5bUn7 jP>g8^wm`gSTrBYJ5N=($Ek>`v-UtCzuN^DNl?DF*!8A^4 diff --git a/src/executor/real_boc/bad_action_with_ignore_flag_account_old.boc b/src/executor/real_boc/bad_action_with_ignore_flag_account_old.boc deleted file mode 100644 index d8395689e29d2d734a2789b5103029a11bef1fad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 756 zcmXw0ZAep57(VB2y1C^xw>2B6J9lOZDc(^Qfz&F0lo@Q(K-!Oi5thhx_(lJmI?cYO zcZxw7lt}%mwDqMnyA8_q>ChiHQ0zJ>3|Yiw<($Pjv*N(}an5<)=Xsy^(YK*SNa!FS zRshk!iWh2s9EjPj8~iqX$@1e$Q~FiY`&#|6r^Y+4!1CApLxt|oqT?$)QUKO|2Wobv zDQ@Qr8Z#uIHUXj|n&>kjC>H^%3jpc!M51S^SXW+$UOMDYimlAA?o9vwlSOMy3Yv!Z z#&+!u2M_^~IE8>mQYDZORK!9EaYtVu#@>4hadHFdP@O*4O#8_WRnu9z*+1%ai1@#E z6P|}I#E~%6%#m?36)%ew#LK8^KV#=3WubO>kRzihosy`c;g*S^mRx3zbhgcx@+J(> zPPoPyig$+985m!hhK)=P^ivLH-43@@rWjY=EF>VkB^?t}D1rX&ht}aUX0+P3+Ld5| z{(kmPoT`#(FAFk#c^lTm@^<8Cpa09twZy>b=9?k0f%hfv?Q?SKM_8QWK;;bt$Daz zmt_yO-Lc$BA=G2^#=s&RX$!sqZw+up_c~IAyfNjXbH4PWTJ$rCNkqjY?X4$06$Huc zqnrceH1K(fAXk7|a}L*IQCO+K_am;HB{qF_>a;olY!pVkz*XQ9II>c&c1tGlEB*wq zi<}700>}a^`6^c^)gs0@2yu|54%<{?%Szl#qumMyVcZP=?+24+{_SU5G(~(P$=NK% zd#V)pOCGHVM|Od~Y{U|fH1uKgzHrbYn$fAF0FMP4DGadPQ$z~9cNT1lX4Omxq24MB i#h8X*JH(5|#RBgMOs#&--12N+H;`Z1<41u>;CWiS;m-D0j`zQeq=JRCi!tzW@FAkra9TD)Dn{9u4uOpRc`K%j{Hr&?e*X zg$+DLdL8=lk^8?d0IB2bSR*$3qR9gWkyL#P4n`(sM#cp!3{6EW2Asjw9FjTR!B;mGPo5w|dv1>&EH_Zuy){zf|_zVR@*^A8~cXyn|tKI-7UB zd$g8`pC^S);-hAZ11A^b(+-B&J^z;f=L$~#zRFo`PuZz7#ZAvF*F4rNX9@XnK&DpZ z1@mlg>*D|ae{}o4nB&2~z|eDC`EQ;K?(LZ) zH=Qr#ed{di18w~&zq0!d_6s&2H)jdI{`USkk?q%RGzhXXaq%5Kk-LbEoq+-9Ee?h> zCx#@2AIG+z|L=P7Q{|NFhZ_FQ=)bzYy>`EM&ZOTbuJ|))%sH|JXf-mJ>y()++yN6& zJ^*8pNh>f!LyTYo8oS7c8d3I_8yKl;wlvoH{@72_o)8I;|Fi0tp3GP IoYLtB0ICPZ5C8xG diff --git a/src/executor/src/blockchain_config.rs b/src/executor/src/blockchain_config.rs index e593b0e..9b85324 100644 --- a/src/executor/src/blockchain_config.rs +++ b/src/executor/src/blockchain_config.rs @@ -10,10 +10,9 @@ */ use num::BigInt; use ton_block::{ - fail, AccountId, BurningConfig, Coins, ConfigParam18, ConfigParamEnum, ConfigParams, - FundamentalSmcAddresses, GasLimitsPrices, GlobalCapabilities, Mask, MsgAddressInt, - MsgForwardPrices, Result, SizeLimitsConfig, StorageInfo, StoragePrices, UInt256, - SUPPORTED_VERSION, + fail, AccountId, Coins, ConfigParam18, ConfigParams, FundamentalSmcAddresses, GasLimitsPrices, + GlobalCapabilities, Mask, MsgAddressInt, MsgForwardPrices, Result, SizeLimitsConfig, + StorageInfo, StoragePrices, UInt256, SUPPORTED_VERSION, }; pub(crate) trait DefaultConfig { @@ -148,7 +147,6 @@ pub struct BlockchainConfig { fwd_prices_mc: MsgForwardPrices, fwd_prices_wc: MsgForwardPrices, storage_prices: AccStoragePrices, - burning_cfg: Option, special_contracts: FundamentalSmcAddresses, limits: SizeLimitsConfig, capabilities: u64, @@ -166,7 +164,6 @@ impl Default for BlockchainConfig { fwd_prices_mc: MsgForwardPrices::default_mc(), fwd_prices_wc: MsgForwardPrices::default_wc(), storage_prices: AccStoragePrices::default(), - burning_cfg: None, special_contracts: Self::get_default_special_contracts(), limits: Default::default(), raw_config: Self::get_defult_raw_config(), @@ -209,17 +206,12 @@ impl BlockchainConfig { log::debug!( "Creating BlockchainConfig: capabilities={capabilities:#x}, block_version={global_version}" ); - let burning_cfg = match config.config(5)? { - Some(ConfigParamEnum::ConfigParam5(burning_cfg)) => Some(burning_cfg), - _ => None, - }; Ok(BlockchainConfig { gas_prices_mc: config.gas_prices(true)?, gas_prices_wc: config.gas_prices(false)?, fwd_prices_mc: config.fwd_prices(true)?, fwd_prices_wc: config.fwd_prices(false)?, storage_prices: AccStoragePrices::with_config(&config.storage_prices()?)?, - burning_cfg, limits: config.size_limits_config()?, special_contracts: config.fundamental_smc_addr()?, capabilities, @@ -242,10 +234,6 @@ impl BlockchainConfig { &self.limits } - pub fn burning_config(&self) -> Option<&BurningConfig> { - self.burning_cfg.as_ref() - } - /// Calculate gas fee for account pub fn calc_gas_fee(&self, gas_used: u64, address: &MsgAddressInt) -> u128 { self.get_gas_config(address.is_masterchain()).calc_gas_fee(gas_used) diff --git a/src/executor/src/ordinary_transaction.rs b/src/executor/src/ordinary_transaction.rs index c80c2bb..3f050f6 100644 --- a/src/executor/src/ordinary_transaction.rs +++ b/src/executor/src/ordinary_transaction.rs @@ -178,20 +178,6 @@ impl TransactionExecutor for OrdinaryTransactionExecutor { tr.add_fee_coins(&in_fwd_fee)?; } - if let Some(burning_cfg) = self.config.burning_config() { - if is_masterchain - && !msg_balance.coins.is_zero() - && burning_cfg.blackhole_addr.as_ref() == Some(&account_id) - { - let burned = std::mem::take(&mut msg_balance.coins); - log::debug!( - target: "executor", - "Burning {burned} nanocoins for blackhole account {account_id:x}", - ); - tr.set_blackhole_burned(burned); - } - } - if description.credit_first && !is_ext_msg { description.credit_ph = match self.credit_phase(&msg_balance, &mut acc_balance) { Ok(credit_ph) => Some(credit_ph), diff --git a/src/executor/src/tests/common/mod.rs b/src/executor/src/tests/common/mod.rs index 320b637..ff71b5d 100644 --- a/src/executor/src/tests/common/mod.rs +++ b/src/executor/src/tests/common/mod.rs @@ -8,6 +8,7 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ +#![cfg(test)] #![allow(dead_code)] #![allow(clippy::duplicate_mod)] #![allow(clippy::field_reassign_with_default)] @@ -18,8 +19,6 @@ use crate::{ BlockchainConfig, ExecuteParams, ExecutorError, OrdinaryTransactionExecutor, TickTockTransactionExecutor, TransactionExecutor, }; -#[cfg(feature = "cross_check")] -use std::sync::Arc; use std::sync::LazyLock; use ton_assembler::compile_code_to_cell; use ton_block::{ @@ -124,42 +123,16 @@ pub fn default_config() -> BlockchainConfig { BlockchainConfig::with_config(create_config("real_boc/default_config.boc").unwrap()).unwrap() } -#[cfg(not(feature = "cross_check"))] pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { + let debug = false; + // let _ = cross_check::DisableCrossCheck::new(); + // init_log_without_config(None, log::LevelFilter::Debug, None); + // cross_check::set_cross_check_verbosity(if debug { 2048 + 4 } else { 4 }); ExecuteParams { block_unixtime: BLOCK_UT, block_lt: last_tr_lt - last_tr_lt % 1_000_000, last_tr_lt, - ..ExecuteParams::default() - } -} - -#[cfg(feature = "cross_check")] -pub fn execute_params(last_tr_lt: u64) -> ExecuteParams { - enum DebugType { - None, - Simple, - Emulator, - } - let debug = DebugType::Emulator; - let _ = cross_check::DisableCrossCheck::new(); - let (verbosity, pattern, trace_callback) = match debug { - DebugType::None => (4, None, None), - DebugType::Simple => (2048 + 4, Some("{m}"), None), - DebugType::Emulator => { - let emulator_trace_callback: Option> = - Some(Arc::new(ton_vm::executor::Engine::emulator_trace_callback)); - (2048 + 4, Some("{m}"), emulator_trace_callback) - } - }; - init_log_without_config(pattern, log::LevelFilter::Debug, None); - cross_check::set_cross_check_verbosity(verbosity); - ExecuteParams { - block_unixtime: BLOCK_UT, - block_lt: last_tr_lt - last_tr_lt % 1_000_000, - last_tr_lt, - trace_callback, - debug: !matches!(debug, DebugType::None), + debug, ..ExecuteParams::default() } } @@ -320,7 +293,6 @@ pub fn check_account_and_transaction_balances( let mut right = acc_after.balance().cloned().unwrap_or_default(); right.add(trans.total_fees()).unwrap(); - right.coins.add(trans.blackhole_burned()).unwrap(); trans .iterate_out_msgs(|out_msg| { if let Some(header) = out_msg.int_header() { @@ -882,7 +854,6 @@ pub fn replay_transaction( let mut right = account.balance().cloned().unwrap_or_default().coins; right.add(&our_transaction.total_fees().coins).unwrap(); - right.add(&our_transaction.blackhole_burned()).unwrap(); our_transaction .iterate_out_msgs(|out_msg| { if let Some(header) = out_msg.int_header() { @@ -916,7 +887,6 @@ pub fn replay_transaction( // transaction.write_to_file(tr).unwrap(); // } // } - // pretty_assertions::assert_eq!(our_transaction, transaction); pretty_assertions::assert_eq!( our_transaction.read_description().unwrap(), transaction.read_description().unwrap() @@ -955,30 +925,29 @@ pub fn replay_transaction( pretty_assertions::assert_eq!(account, account_after); } -pub fn read_config(cfg: &str) -> Result { +fn read_config(cfg: &str) -> Result { println!("prepare to read config"); let config = if let Ok(data) = base64_decode(cfg) { + println!("config read as base64"); let data = read_single_root_boc(data).unwrap(); if let Ok(config) = ConfigParams::construct_from_cell(data.clone()) { - println!("config params read as base64"); config } else { - println!("config hashmap read as base64"); - ConfigParams::with_root(data)? + ConfigParams::with_root(data) } } else if let Ok(config) = create_config(cfg) { - println!("config read from file boc {cfg}"); + println!("config read from file {cfg}"); config // let config = ton_block_json::debug_possible_config_params(&config).unwrap(); // std::fs::write("d:\\config.json", config).unwrap(); } else if let Ok(data) = read_single_root_boc(std::fs::read(cfg).unwrap()) { - println!("config hashmap read from boc"); - ConfigParams::with_root(data)? + println!("config read from file as hashmap"); + ConfigParams::with_root(data) } else { println!("config read from file as json"); let json: serde_json::Map = serde_json::from_str(&std::fs::read_to_string(cfg).unwrap()).unwrap(); - ton_block_json::parse_config(json.get("config").unwrap().as_object().unwrap())? + ton_block_json::parse_config(json.get("config").unwrap().as_object().unwrap()).unwrap() // let cfg = cfg.replace("json", "boc"); // config.write_to_file(&cfg).unwrap(); }; diff --git a/src/executor/src/tests/test_ordinary_libs_and_code.rs b/src/executor/src/tests/test_ordinary_libs_and_code.rs index 653ba63..3d0bb8b 100644 --- a/src/executor/src/tests/test_ordinary_libs_and_code.rs +++ b/src/executor/src/tests/test_ordinary_libs_and_code.rs @@ -192,7 +192,7 @@ fn set_library_test() { fn set_ext_library_test() { // library code and data let mut state_lib = HashmapE::with_bit_len(256); - state_lib.setref(LIBRARY_CELL.repr_hash().into(), LIBRARY_CELL.clone()).unwrap(); + state_lib.setref(LIBRARY_CELL.repr_hash().into(), &LIBRARY_CELL).unwrap(); let code = format!( " @@ -357,7 +357,7 @@ fn test_library_cell_code() { .unwrap(); let mut library = ton_block::HashmapE::with_bit_len(256); let key = my_code.repr_hash().write_to_bitstring().unwrap(); - library.setref(key, my_code.clone()).unwrap(); + library.setref(key.clone(), &my_code).unwrap(); let code = my_code.as_library_cell(); let data = Cell::default(); diff --git a/src/executor/src/tests/test_ordinary_transaction.rs b/src/executor/src/tests/test_ordinary_transaction.rs index 0f36020..9682482 100644 --- a/src/executor/src/tests/test_ordinary_transaction.rs +++ b/src/executor/src/tests/test_ordinary_transaction.rs @@ -27,7 +27,7 @@ use ton_block::{ AccStatusChange, TrActionPhase, TrComputePhase, TrComputePhaseVm, TrCreditPhase, TrStoragePhase, Transaction, TransactionDescr, }, - AccountId, AccountStatus, AnycastInfo, BouncedByPhase, BuilderData, BurningConfig, Cell, Coins, + AccountId, AccountStatus, AnycastInfo, BouncedByPhase, BuilderData, Cell, Coins, ComputeSkipReason, ConfigParam8, ConfigParamEnum, ConfigParams, CurrencyCollection, Deserializable, ExceptionCode, GetRepresentationHash, GlobalVersion, IBitstring, InternalMessageHeader, MerkleProof, NewBounceBody, NewBounceComputePhaseInfo, @@ -4067,180 +4067,3 @@ fn test_new_bounce() { .unwrap() ); } - -#[test] -fn test_masterchain_blackhole_burns_inbound_value() { - let acc_id = AccountId::from([0x44; 32]); - let code = compile_code_to_cell("ACCEPT").unwrap(); - let start_balance = 200_000_000; - let msg_value = 14_200_000; - let msg = create_int_msg_workchain( - -1, - SENDER_ACCOUNT.clone(), - acc_id.clone(), - msg_value, - false, - PREV_BLOCK_LT, - ); - let params = execute_params_none(); - - let mut raw_config = BLOCKCHAIN_CONFIG.raw_config().clone(); - raw_config - .set_config(ConfigParamEnum::ConfigParam5(BurningConfig { - blackhole_addr: None, - fee_burn_num: 0, - fee_burn_denom: 1, - })) - .unwrap(); - let regular_config = BlockchainConfig::with_config(raw_config.clone()).unwrap(); - - raw_config - .set_config(ConfigParamEnum::ConfigParam5(BurningConfig { - blackhole_addr: Some(acc_id.clone()), - fee_burn_num: 0, - fee_burn_denom: 1, - })) - .unwrap(); - let blackhole_config = BlockchainConfig::with_config(raw_config).unwrap(); - - let mut regular_acc = create_test_account_workchain( - start_balance, - -1, - acc_id.clone(), - code.clone(), - Cell::default(), - ); - let regular_before = regular_acc.clone(); - let regular_tr = - try_replay_transaction(&mut regular_acc, Some(&msg), regular_config, ¶ms).unwrap(); - check_account_and_transaction_balances(®ular_before, ®ular_acc, &msg, Some(®ular_tr)); - - let mut blackhole_acc = - create_test_account_workchain(start_balance, -1, acc_id.clone(), code, Cell::default()); - let blackhole_before = blackhole_acc.clone(); - let blackhole_tr = - try_replay_transaction(&mut blackhole_acc, Some(&msg), blackhole_config, ¶ms).unwrap(); - check_account_and_transaction_balances( - &blackhole_before, - &blackhole_acc, - &msg, - Some(&blackhole_tr), - ); - - assert_eq!(*regular_tr.blackhole_burned(), Coins::default()); - assert_eq!(*blackhole_tr.blackhole_burned(), msg_value); - assert_eq!( - regular_acc.balance().unwrap().coins.as_u128() - - blackhole_acc.balance().unwrap().coins.as_u128(), - msg_value as u128 - - (regular_tr.total_fees().coins.as_u128() - blackhole_tr.total_fees().coins.as_u128()) - ); - - let TransactionDescr::Ordinary(regular_descr) = regular_tr.read_description().unwrap() else { - panic!("ordinary description expected"); - }; - let TransactionDescr::Ordinary(blackhole_descr) = blackhole_tr.read_description().unwrap() - else { - panic!("ordinary description expected"); - }; - assert_eq!(regular_descr.credit_ph.unwrap().credit, CurrencyCollection::with_coins(msg_value)); - assert_eq!(blackhole_descr.credit_ph.unwrap().credit, CurrencyCollection::default()); -} - -#[test] -fn special_account_due_payment_fully_covered() { - // Special active account with due_payment > 0 and sufficient balance. - // C++ collects due_payment from balance and clears it. - let code = compile_code_to_cell("ACCEPT").unwrap(); - let acc_id = THIRD_ACCOUNT.clone(); - - let start_balance = 1_000_000_000u64; - let due = 100_000_000u64; - let msg_income = 14_200_000u64; - - let mut acc = create_test_account(start_balance, acc_id.clone(), code, Cell::default()); - acc.set_due_payment(Some(due.into())); - let addr = acc.get_addr().unwrap(); - assert!(BLOCKCHAIN_CONFIG.is_special_account(addr.is_masterchain(), addr.address()).unwrap()); - - let msg = create_int_msg(THIRD_ACCOUNT.clone(), acc_id, msg_income, true, BLOCK_LT - 2); - let params = execute_params(BLOCK_LT + 1); - let trans = execute_with_params( - SIMPLE_MC_STATE.to_owned(), - Some(msg.serialize().unwrap()), - &mut acc, - ¶ms, - ) - .unwrap(); - - let storage = get_tr_descr(&trans).storage_ph.unwrap(); - assert_eq!(storage.storage_fees_collected, Coins::from(due)); - assert_eq!(storage.storage_fees_due, None); - assert_eq!(storage.status_change, AccStatusChange::Unchanged); - assert_eq!(acc.due_payment(), None); - assert_eq!(acc.last_paid(), 0); -} - -#[test] -fn special_account_due_payment_partial() { - // Special active account where balance < due_payment. - // C++ collects the full balance but keeps the ORIGINAL due_payment on the account - // (Transaction::due_payment is never updated for special in the partial path). - let code = compile_code_to_cell("ACCEPT").unwrap(); - let acc_id = THIRD_ACCOUNT.clone(); - - let start_balance = 50_000_000u64; - let due = 100_000_000u64; - let msg_income = 14_200_000u64; - - let mut acc = create_test_account(start_balance, acc_id.clone(), code, Cell::default()); - acc.set_due_payment(Some(due.into())); - - let msg = create_int_msg(THIRD_ACCOUNT.clone(), acc_id, msg_income, true, BLOCK_LT - 2); - let params = execute_params(BLOCK_LT + 1); - let trans = execute_with_params( - SIMPLE_MC_STATE.to_owned(), - Some(msg.serialize().unwrap()), - &mut acc, - ¶ms, - ) - .unwrap(); - - let storage = get_tr_descr(&trans).storage_ph.unwrap(); - assert_eq!(storage.storage_fees_collected, Coins::from(start_balance)); - assert_eq!(storage.storage_fees_due, Some(Coins::from(due - start_balance))); - assert_eq!(storage.status_change, AccStatusChange::Unchanged); - assert_eq!(acc.due_payment(), Some(&Coins::from(due))); - assert_eq!(acc.last_paid(), 0); -} - -#[test] -fn special_account_due_payment_zero_balance() { - // Special active account with due_payment > 0 and balance = 0. - // Nothing to collect, due_payment stays at original. - let code = compile_code_to_cell("ACCEPT").unwrap(); - let acc_id = THIRD_ACCOUNT.clone(); - - let due = 100_000_000u64; - let msg_income = 14_200_000u64; - - let mut acc = create_test_account(0u64, acc_id.clone(), code, Cell::default()); - acc.set_due_payment(Some(due.into())); - - let msg = create_int_msg(THIRD_ACCOUNT.clone(), acc_id, msg_income, true, BLOCK_LT - 2); - let params = execute_params(BLOCK_LT + 1); - let trans = execute_with_params( - SIMPLE_MC_STATE.to_owned(), - Some(msg.serialize().unwrap()), - &mut acc, - ¶ms, - ) - .unwrap(); - - let storage = get_tr_descr(&trans).storage_ph.unwrap(); - assert_eq!(storage.storage_fees_collected, Coins::zero()); - assert_eq!(storage.storage_fees_due, Some(Coins::from(due))); - assert_eq!(storage.status_change, AccStatusChange::Unchanged); - assert_eq!(acc.due_payment(), Some(&Coins::from(due))); - assert_eq!(acc.last_paid(), 0); -} diff --git a/src/executor/src/tests/test_transaction_executor_with_real_data.rs b/src/executor/src/tests/test_transaction_executor_with_real_data.rs index c1ecd1f..1ac4ca9 100644 --- a/src/executor/src/tests/test_transaction_executor_with_real_data.rs +++ b/src/executor/src/tests/test_transaction_executor_with_real_data.rs @@ -519,16 +519,6 @@ fn test_bad_action() { ) } -#[test] -fn test_bad_action_with_ignore_flag() { - replay_transaction_by_files( - "real_boc/bad_action_with_ignore_flag_account_old.boc", - "real_boc/bad_action_with_ignore_flag_account_new.boc", - "real_boc/bad_action_with_ignore_flag_transaction.boc", - "real_boc/config12.boc", - ) -} - #[test] fn test_size_limits_v12() { replay_transaction_by_files( diff --git a/src/executor/src/transaction_executor.rs b/src/executor/src/transaction_executor.rs index 1c7071d..f8d5883 100644 --- a/src/executor/src/transaction_executor.rs +++ b/src/executor/src/transaction_executor.rs @@ -17,8 +17,8 @@ use std::{ sync::{Arc, LazyLock}, }; use ton_block::{ - error, fail, unpack_out_action_slices, AccStatusChange, Account, AccountId, AccountStatus, - AddSub, BlockError, BouncedByPhase, Cell, ChildCell, Coins, ComputeSkipReason, + deserialize_out_action_slices, error, fail, unpack_out_action_slices, AccStatusChange, Account, + AccountId, AccountStatus, AddSub, BouncedByPhase, Cell, ChildCell, Coins, ComputeSkipReason, CurrencyCollection, Deserializable, ExceptionCode, GasLimitsPrices, GetRepresentationHash, GlobalCapabilities, HashmapE, HashmapFilterResult, IBitstring, Mask, Message, MsgAddressInt, NewBounceBody, NewBounceComputePhaseInfo, NewBounceOriginalInfo, OutAction, Result, @@ -147,7 +147,6 @@ pub trait TransactionExecutor { /// If account does not exist - phase skipped. /// Calculates storage fees and substracts them from account balance. /// If account balance becomes negative after that, then account is frozen. - /// is_special - flag indicating that account is in list of special smart contracts, for which storage fees are not applied fn storage_phase( &self, acc: &mut Account, @@ -160,12 +159,23 @@ pub trait TransactionExecutor { if tr.now() < acc.last_paid() { fail!("transaction timestamp must be greater then account timestamp") } - let original_due_payment = acc.due_payment().cloned(); + + if is_special { + log::debug!(target: "executor", "Special account: AccStatusChange::Unchanged"); + return Ok(TrStoragePhase::with_params( + Coins::zero(), + acc.due_payment().cloned(), + AccStatusChange::Unchanged, + )); + } let mut fee = match acc.storage_info() { - Some(storage_info) if !is_special => { + Some(storage_info) => { self.config().calc_storage_fees(storage_info, is_masterchain, tr.now())? } - _ => Default::default(), + None => { + log::debug!(target: "executor", "Account::None"); + return Ok(Default::default()); + } }; if let Some(due_payment) = acc.due_payment() { fee.add(due_payment)?; @@ -182,15 +192,6 @@ pub trait TransactionExecutor { let storage_fees_collected = std::mem::take(&mut acc_balance.coins); tr.add_fee_coins(&storage_fees_collected)?; fee.sub(&storage_fees_collected)?; - if is_special { - log::debug!(target: "executor", "special account, due payment {fee} still active"); - acc.set_due_payment(original_due_payment); - return Ok(TrStoragePhase::with_params( - storage_fees_collected, - Some(fee), - AccStatusChange::Unchanged, - )); - } let need_freeze = acc.is_active() && fee > self.config().get_gas_config(is_masterchain).freeze_due_limit; let need_delete = (acc.is_uninit() || acc.is_frozen()) @@ -564,43 +565,30 @@ pub trait TransactionExecutor { } let limits = self.config().size_limits_config(); - let mut parsed_actions = Vec::with_capacity(action_slices.len()); - for (i, mut slice) in action_slices.into_iter().enumerate() { - match OutAction::construct_from(&mut slice) { - Ok(action) => parsed_actions.push(Some(action)), - Err(err) => { - if let Some(BlockError::OutActionError(_, mode)) = err.downcast_ref() { - if mode.bit(SENDMSG_IGNORE_ERROR) { - phase.skipped_actions += 1; - parsed_actions.push(None); - continue; - } else if mode.bit(SENDMSG_BOUNCE_IF_FAIL) { - bounce = true; - } - }; - log::debug!( - target: "executor", - "invalid action {i} found while preprocessing action list: {err}" - ); - phase.result_code = RESULT_CODE_UNKNOWN_OR_INVALID_ACTION; - if i != 0 { - phase.result_arg = Some(i as i32); - } - return finish_action_phase_with_fine( - tr, - phase, - Some(msg_remaining_balance), - acc_balance, - bounce, - ); + let parsed_actions = match deserialize_out_action_slices(action_slices) { + Ok(actions) => actions, + Err((i, err)) => { + log::debug!( + target: "executor", + "invalid action {} found while preprocessing action list: {}", + i, + err + ); + phase.result_code = RESULT_CODE_UNKNOWN_OR_INVALID_ACTION; + if i != 0 { + phase.result_arg = Some(i as i32); } + return finish_action_phase_with_fine( + tr, + phase, + Some(msg_remaining_balance), + acc_balance, + bounce, + ); } - } + }; for (i, action) in parsed_actions.into_iter().enumerate() { - let Some(action) = action else { - continue; - }; log::debug!(target: "executor", "\nAction #{}\nType: {}\nInitial balance: {}", i, action_type(&action), @@ -1280,8 +1268,8 @@ fn outmsg_action_handler( let (force_body_to_ref, body) = match msg.body() { None => (false, None), Some(body) => { - let b = body.clone().into_cell().unwrap_or_default(); - (b.references_count() >= 2, Some(b)) + let b = body.clone().into_builder().unwrap_or_default(); + (b.references_used() >= 2, Some(b)) } }; let (force_init_to_ref, init) = match msg.state_init() { @@ -1317,7 +1305,7 @@ fn outmsg_action_handler( }; let mut sstat = StorageUsageCalc::with_limits(max_cells, limits.max_msg_bits as u64); if let Some(body) = &body { - sstat.append_cell(body, body_to_ref, &mut 0).map_err(|err| { + sstat.append_builder(body, body_to_ref, &mut 0).map_err(|err| { log::error!(target: "executor", "cannot calc msg storage used for body: {err}"); RESULT_CODE_UNKNOWN_OR_INVALID_ACTION })?; diff --git a/src/node/Cargo.toml b/src/node/Cargo.toml index 8052da5..ac23208 100644 --- a/src/node/Cargo.toml +++ b/src/node/Cargo.toml @@ -3,7 +3,7 @@ build = '../common/build/build.rs' edition = '2021' license = 'GPL-3.0' name = 'node' -version = '0.4.0' +version = '0.3.0' [[bin]] name = 'adnl_resolve' @@ -37,10 +37,6 @@ path = 'bin/print.rs' name = 'zerostate' path = 'bin/zerostate.rs' -[[bin]] -name = 'archive_import' -path = 'bin/archive_import.rs' - [[bin]] name = 'hardfork' path = 'bin/hardfork.rs' @@ -77,7 +73,6 @@ num_cpus = '1.13' openssl = '0.10' parking_lot = '0.12' rand = '0.8' -rayon = '1' regex = '1.10' serde = '1.0' serde_derive = '1.0' @@ -115,7 +110,6 @@ harness = false [dev-dependencies] criterion = { version = "0.5", features = ["html_reports", "async_tokio"] } -tempfile = '3' difference = '2.0' external-ip = '6.0' http-body-util = "0.1" @@ -128,9 +122,10 @@ default = ['telemetry', 'ton_block/export_key', 'validator_session/export_key'] cell_counter = ['ton_block/cell_counter'] ci_run = [] export_key = ['catchain/export_key', 'ton_block/export_key'] -mirrornet = ['ton_block/mirrornet'] only_sorted_clean = [] +simplex = [] telemetry = ['adnl/telemetry', 'storage/telemetry'] trace_alloc = [] trace_alloc_detail = ['trace_alloc'] xp25 = ['ton_block/xp25', 'adnl/xp25'] +mirrornet = ['ton_block/mirrornet'] \ No newline at end of file diff --git a/src/node/bin/adnl_ping.rs b/src/node/bin/adnl_ping.rs index 1012852..fe8bf30 100644 --- a/src/node/bin/adnl_ping.rs +++ b/src/node/bin/adnl_ping.rs @@ -58,7 +58,7 @@ fn ping( let local_key = adnl.key_by_tag(KEY_TAG)?; let other_key = Arc::new(Ed25519KeyOption::from_public_key((&base64_decode(pub_key)?[..]).try_into()?)); - let other_id = adnl.add_peer(local_key.id(), &ip, None, &other_key)?; + let other_id = adnl.add_peer(local_key.id(), &ip, &other_key)?; let other_id = if let Some(other_id) = other_id { other_id } else { fail!("Cannot add peer to ADNL") }; diff --git a/src/node/bin/adnl_resolve.rs b/src/node/bin/adnl_resolve.rs index b087f62..e213062 100644 --- a/src/node/bin/adnl_resolve.rs +++ b/src/node/bin/adnl_resolve.rs @@ -45,8 +45,8 @@ async fn scan(adnlid: &str, cfgfile: &str) -> Result<()> { let mut index = 0; println!("Searching DHT for {}...", keyid); loop { - if let Ok(Some((adnl_addr, quic_addr, key))) = dht.find_address(&mut context).await { - println!("Found ADNL={} QUIC={:?} / {}", adnl_addr, quic_addr, key.id()); + if let Ok(Some((ip, key))) = dht.find_address(&mut context).await { + println!("Found {} / {}", ip, key.id()); return Ok(()); } if index >= nodes.len() { diff --git a/src/node/bin/archive_import.rs b/src/node/bin/archive_import.rs deleted file mode 100644 index 0def3cd..0000000 --- a/src/node/bin/archive_import.rs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use clap::{Arg, ArgAction, Command}; -use node::archive_import::{run_import, ImportConfig}; -use std::path::PathBuf; - -fn main() { - env_logger::Builder::from_default_env().format_timestamp_millis().init(); - - let matches = Command::new("archive_import") - .about("Import raw .pack archive files into epoch-based storage") - .arg( - Arg::new("archives-path") - .long("archives-path") - .required(true) - .help("Path to directory with source .pack files"), - ) - .arg( - Arg::new("epochs-path") - .long("epochs-path") - .required(true) - .help("Path where epoch directories will be created"), - ) - .arg( - Arg::new("epoch-size") - .long("epoch-size") - .default_value("10000000") - .help("Number of MC blocks per epoch (must be multiple of 20000)"), - ) - .arg( - Arg::new("node-db-path") - .long("node-db-path") - .required(true) - .help("Path to node database directory"), - ) - .arg( - Arg::new("mc-zerostate") - .long("mc-zerostate") - .required(true) - .help("Path to masterchain zerostate .boc file"), - ) - .arg( - Arg::new("wc-zerostate") - .long("wc-zerostate") - .action(ArgAction::Append) - .required(true) - .help("Path to workchain zerostate .boc file (one per workchain)"), - ) - .arg( - Arg::new("global-config") - .long("global-config") - .required(true) - .help("Path to global config JSON file (describes zerostate and hard forks)"), - ) - .arg( - Arg::new("skip-validation") - .long("skip-validation") - .action(ArgAction::SetTrue) - .help("Skip block proof validation (for re-importing already validated archives)"), - ) - .arg(Arg::new("copy").long("copy").action(ArgAction::SetTrue).help( - "Copy source .pack files instead of moving them. Use for keeping original \ - files or when source and destination are on different filesystems.", - )) - .get_matches(); - - let config = ImportConfig { - archives_path: PathBuf::from(matches.get_one::("archives-path").unwrap()), - epochs_path: PathBuf::from(matches.get_one::("epochs-path").unwrap()), - epoch_size: matches - .get_one::("epoch-size") - .unwrap() - .parse() - .expect("epoch-size must be a number"), - node_db_path: PathBuf::from(matches.get_one::("node-db-path").unwrap()), - mc_zerostate_path: PathBuf::from(matches.get_one::("mc-zerostate").unwrap()), - wc_zerostate_paths: matches - .get_many::("wc-zerostate") - .unwrap() - .map(|s| PathBuf::from(s)) - .collect(), - global_config_path: PathBuf::from(matches.get_one::("global-config").unwrap()), - skip_validation: matches.get_flag("skip-validation"), - move_files: !matches.get_flag("copy"), - }; - - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("Failed to create tokio runtime"); - - if let Err(e) = rt.block_on(run_import(config)) { - log::error!("Import failed: {}", e); - std::process::exit(1); - } -} diff --git a/src/node/bin/dhtscan.rs b/src/node/bin/dhtscan.rs index f1e54ea..b080808 100644 --- a/src/node/bin/dhtscan.rs +++ b/src/node/bin/dhtscan.rs @@ -8,32 +8,18 @@ */ use adnl::{ node::{AdnlNode, AdnlNodeConfig}, - AddressSearchContext, DhtNode, DhtSearchPolicy, OverlayNode, OverlayNodesSearchContext, + DhtNode, DhtSearchPolicy, OverlayNode, OverlayNodesSearchContext, }; use node::config::TonNodeGlobalConfigJson; -use std::{ - collections::HashMap, - env, - fs::File, - io::BufReader, - net::{IpAddr, Ipv4Addr, SocketAddr}, - ops::Deref, - sync::Arc, -}; +use std::{collections::HashMap, env, fs::File, io::BufReader, ops::Deref, sync::Arc}; use ton_block::{base64_encode, error, fail, KeyOption, Result}; include!("../../common/src/log.rs"); -const DEFAULT_IP: &str = "0.0.0.0:4191"; +const IP: &str = "0.0.0.0:4191"; const KEY_TAG: usize = 1; -fn scan( - cfgfile: &str, - bind_addr: &str, - jsonl: bool, - search_overlay: bool, - use_workchain0: bool, -) -> Result<()> { +fn scan(cfgfile: &str, jsonl: bool, search_overlay: bool, use_workchain0: bool) -> Result<()> { let file = File::open(cfgfile)?; let reader = BufReader::new(file); let config: TonNodeGlobalConfigJson = serde_json::from_reader(reader) @@ -43,8 +29,7 @@ fn scan( let dht_nodes = config.get_dht_nodes_configs()?; let mut rt = tokio::runtime::Runtime::new()?; - let (_, config) = - AdnlNodeConfig::with_ip_address_and_private_key_tags(bind_addr, vec![KEY_TAG])?; + let (_, config) = AdnlNodeConfig::with_ip_address_and_private_key_tags(IP, vec![KEY_TAG])?; let adnl = rt.block_on(AdnlNode::with_config(config))?; let dht = DhtNode::with_adnl_node(adnl.clone(), KEY_TAG)?; let overlay = OverlayNode::with_params(adnl.clone(), zero_state.as_slice(), KEY_TAG)?; @@ -60,16 +45,6 @@ fn scan( } } - // Fetch signed address lists from preset nodes (picks up QUIC addresses) - println!("Querying preset DHT nodes for signed address lists..."); - for node in preset_nodes.iter() { - match rt.block_on(dht.get_signed_address_list(node)) { - Ok(true) => println!(" {} - OK", node), - Ok(false) => println!(" {} - no response", node), - Err(e) => println!(" {} - error: {}", node, e), - } - } - println!("Scanning DHT..."); for node in preset_nodes.iter() { rt.block_on(dht.find_dht_nodes(node))?; @@ -84,47 +59,28 @@ fn scan( } let mut count = 0; - let mut quic_count = 0; let nodes = dht.get_known_nodes(5000)?; - if !nodes.is_empty() { + if nodes.len() > dht_nodes.len() { println!("---- Found DHT nodes:"); for node in nodes { + let mut skip = false; + for dht_node in dht_nodes.iter() { + if dht_node.id == node.id { + skip = true; + break; + } + } + if skip { + continue; + } let key: Arc = (&node.id).try_into()?; match rt.block_on(dht.ping(key.id())) { Ok(true) => (), _ => continue, } - let (adnl_addr, quic_addr) = AdnlNode::parse_address_list(&node.addr_list)? - .ok_or_else(|| error!("Cannot parse address list {:?}", node.addr_list))?; - let adr = adnl_addr.to_udp(); - - // If not in the node record, try DHT value lookup for stored address - // If no QUIC address from the node record, try DHT value lookup - let quic_socket_addr = if let Some(q) = &quic_addr { - Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(q.ip())), q.port())) - } else { - let mut ctx = - AddressSearchContext::with_params(key.id(), DhtSearchPolicy::FastSearch(3))?; - if let Ok(Some((_, quic_addr, _))) = rt.block_on(dht.find_address(&mut ctx)) { - quic_addr.map(|q| SocketAddr::new(IpAddr::V4(Ipv4Addr::from(q.ip())), q.port())) - } else { - None - } - }; - - let mut addrs = vec![serde_json::json!({ - "@type": "adnl.address.udp", - "ip": adr.ip, - "port": adr.port - })]; - if let Some(q) = quic_socket_addr { - quic_count += 1; - addrs.push(serde_json::json!({ - "@type": "adnl.address.quic", - "ip": q.ip().to_string(), - "port": q.port() - })); - } + let adr = AdnlNode::parse_address_list(&node.addr_list)? + .ok_or_else(|| error!("Cannot parse address list {:?}", node.addr_list))? + .to_udp(); let json = serde_json::json!( { "@type": "dht.node", @@ -134,7 +90,13 @@ fn scan( }, "addr_list": { "@type": "adnl.addressList", - "addrs": addrs, + "addrs": [ + { + "@type": "adnl.address.udp", + "ip": adr.ip, + "port": adr.port + } + ], "version": node.addr_list.version, "reinit_date": node.addr_list.reinit_date, "priority": node.addr_list.priority, @@ -154,23 +116,10 @@ fn scan( } ); } - println!("Total: {} DHT nodes ({} with QUIC)", count, quic_count); + println!("Total: {} DHT nodes", count); } else { - println!("---- No DHT nodes found via routing table"); - } - - // Also show QUIC addresses discovered from preset nodes via getSignedAddressList - println!("\n---- Preset node QUIC addresses (from getSignedAddressList):"); - let local_key = adnl.key_by_tag(KEY_TAG)?.id().clone(); - for node in preset_nodes.iter() { - let addrs = adnl.peer_ip_address(&local_key, node).ok().flatten(); - let (adnl_addr, quic_addr) = match addrs { - Some((a, q)) => (Some(a), q), - None => (None, None), - }; - println!(" {} ADNL={:?} QUIC={:?}", node, adnl_addr, quic_addr); + println!("---- No DHT nodes found"); } - Ok(()) } @@ -226,29 +175,24 @@ fn main() { let mut jsonl = false; let mut overlay = false; let mut workchain0 = false; - let mut bind_addr = DEFAULT_IP.to_string(); - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--jsonl" => jsonl = true, - "--overlay" => overlay = true, - "--workchain0" => workchain0 = true, - "--bind" => { - bind_addr = args.next().unwrap_or_else(|| { - eprintln!("--bind requires an argument (e.g. 127.0.0.1:4191)"); - std::process::exit(1); - }); - } - _ => config = Some(arg), + for arg in env::args().skip(1) { + if arg == "--jsonl" { + jsonl = true + } else if arg == "--overlay" { + overlay = true + } else if arg == "--workchain0" { + workchain0 = true + } else { + config = Some(arg) } } let config = if let Some(config) = config { config } else { - println!("Usage: dhtscan [--jsonl] [--overlay] [--workchain0] [--bind ip:port] "); + println!("Usage: dhtscan [--jsonl] [--overlay] [--workchain0] "); return; }; init_log("./common/config/log_cfg.yml"); - scan(&config, &bind_addr, jsonl, overlay, workchain0) + scan(&config, jsonl, overlay, workchain0) .unwrap_or_else(|e| println!("DHT scanning error: {}", e)) } diff --git a/src/node/bin/print.rs b/src/node/bin/print.rs index 3f87272..5ef39ae 100644 --- a/src/node/bin/print.rs +++ b/src/node/bin/print.rs @@ -165,7 +165,7 @@ async fn main() -> Result<()> { print_state(&state, brief)?; } else if let Ok(account) = Account::construct_from_cell(res.roots[0].clone()) { if let Some(data) = account.data().and_then(|data| data.reference(0).ok()) { - let config_params = ConfigParams::with_root(data)?; + let config_params = ConfigParams::with_root(data); let mut json = Default::default(); let mode = ton_block_json::SerializationMode::Debug; if ton_block_json::serialize_config(&mut json, &config_params, mode).is_ok() { diff --git a/src/node/catchain/src/receiver.rs b/src/node/catchain/src/receiver.rs index bc8e1b0..eb154a1 100644 --- a/src/node/catchain/src/receiver.rs +++ b/src/node/catchain/src/receiver.rs @@ -304,7 +304,7 @@ impl Receiver for ReceiverWrapper { self.out_bytes.increment(payload.data().len() as u64); // Send broadcast through overlay directly - self.overlay.send_broadcast_fec_ex(&self.local_adnl_id, &self.local_id, payload, None); + self.overlay.send_broadcast_fec_ex(&self.local_adnl_id, &self.local_id, payload); } /// Send query via RLDP @@ -3457,7 +3457,7 @@ impl ReceiverImpl { //overlay creation - log::info!( + log::debug!( "Receiver: starting up overlay for session {:x} with ID {:x}, short_id {}", session_id, overlay_id, @@ -3486,7 +3486,7 @@ impl ReceiverImpl { let overlay_replay_listener: Arc = overlay_listener.clone(); - log::info!( + log::debug!( "Receiver: starting up overlay for session {:x} with ID/incarnation {:x}, short_id {}", session_id, overlay_id, diff --git a/src/node/consensus-common/src/adnl_overlay.rs b/src/node/consensus-common/src/adnl_overlay.rs index aad8d94..4029141 100644 --- a/src/node/consensus-common/src/adnl_overlay.rs +++ b/src/node/consensus-common/src/adnl_overlay.rs @@ -36,16 +36,15 @@ use ton_api::{ deserialize_boxed, serialize_bare, serialize_boxed, serialize_boxed_append, ton::{ catchain::BroadcastWrapper, - consensus::simplex::{Certificate as SimplexCertificate, Vote as SimplexVote}, overlay::{ - broadcast::BroadcastTwostepSimple, broadcast_twostep::id::Id as BroadcastTwostepId, + broadcast::BroadcastTwostepSimple, broadcast_twostep_simple::tosign::ToSign as BroadcastTwostepSimpleToSign, Certificate as OverlayCertificate, }, }, - BoxedSerialize, IntoBoxed, Serializer, TLObject, + BoxedSerialize, IntoBoxed, TLObject, }; -use ton_block::{error, fail, sha256_digest, KeyId, KeyOption, UInt256}; +use ton_block::{error, fail}; const LOG_TARGET: &str = "consensus_adnl_overlay"; @@ -121,8 +120,7 @@ impl TaskProcessor { const TASK_AGE_WARNING_THRESHOLD: Duration = Duration::from_secs(5); const WARNING_THROTTLE_INTERVAL: Duration = Duration::from_secs(10); - // Allow first warning immediately - let mut last_warning_time = Instant::now() - WARNING_THROTTLE_INTERVAL; + let mut last_warning_time = Instant::now() - WARNING_THROTTLE_INTERVAL; // Allow first warning immediately log::debug!(target: LOG_TARGET, "TaskProcessor loop started: {}", name_clone); @@ -143,14 +141,8 @@ impl TaskProcessor { if let Ok(elapsed) = task_desc.creation_time.elapsed() { if elapsed > TASK_AGE_WARNING_THRESHOLD { let now = Instant::now(); - if now.duration_since(last_warning_time) - >= WARNING_THROTTLE_INTERVAL - { - log::warn!( - target: LOG_TARGET, - "TaskProcessor {name_clone}: \ - Processing delayed task (age: {elapsed:?})" - ); + if now.duration_since(last_warning_time) >= WARNING_THROTTLE_INTERVAL { + log::warn!(target: LOG_TARGET, "TaskProcessor {}: Processing delayed task (age: {:?})", name_clone, elapsed); last_warning_time = now; } } @@ -161,10 +153,7 @@ impl TaskProcessor { } Ok(None) => { // Channel closed - log::debug!( - target: LOG_TARGET, - "TaskProcessor channel closed: {name_clone}" - ); + log::debug!(target: LOG_TARGET, "TaskProcessor channel closed: {}", name_clone); break; } Err(_) => { @@ -189,11 +178,7 @@ impl TaskProcessor { F: FnOnce() -> Pin + Send + 'static>> + Send + 'static, { if self.stop_requested.load(Ordering::Relaxed) { - log::trace!( - target: LOG_TARGET, - "TaskProcessor {} stop requested, ignoring posted closure", - self.name - ); + log::trace!(target: LOG_TARGET, "TaskProcessor {} stop requested, ignoring posted closure", self.name); return; } @@ -226,24 +211,14 @@ impl TaskProcessor { while !self.is_stopped.load(Ordering::Relaxed) { if wait_count % 10 == 0 { // Log every second - log::info!( - target: LOG_TARGET, - "TaskProcessor {}: Waiting for stop completion... ({}ms)", - self.name, - wait_count * 100 - ); + log::info!(target: LOG_TARGET, "TaskProcessor {}: Waiting for stop completion... ({}ms)", self.name, wait_count * 100); } std::thread::sleep(STOP_WAIT_DELAY); wait_count += 1; } - log::debug!( - target: LOG_TARGET, - "TaskProcessor {}: Stopped after {}ms", - self.name, - wait_count * 100 - ); + log::debug!(target: LOG_TARGET, "TaskProcessor {}: Stopped after {}ms", self.name, wait_count * 100); } } @@ -276,10 +251,7 @@ impl TaskProcessorManager { num_tags: u32, runtime_handle: tokio::runtime::Handle, ) -> Self { - log::info!( - target: LOG_TARGET, - "Creating TaskProcessorManager {name} with {num_tags} tags" - ); + log::info!(target: LOG_TARGET, "Creating TaskProcessorManager {} with {} tags", name, num_tags); let metrics_handle = MetricsHandle::new(Some(Duration::from_secs(30))); let mut processors = HashMap::new(); @@ -300,11 +272,7 @@ impl TaskProcessorManager { processors.insert(tag, processor); } - log::info!( - target: LOG_TARGET, - "TaskProcessorManager {name}: Created and started {} TaskProcessors", - processors.len() - ); + log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Created and started {} TaskProcessors", name, processors.len()); let manager = Self { name: name.clone(), @@ -328,10 +296,7 @@ impl TaskProcessorManager { let is_stopped = self.is_stopped.clone(); let manager_name = self.name.clone(); - log::debug!( - target: LOG_TARGET, - "TaskProcessorManager {manager_name}: Starting metrics reporting" - ); + log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Starting metrics reporting", manager_name); let _handle = self.runtime_handle.spawn(async move { const METRICS_DUMP_PERIOD: Duration = Duration::from_secs(30); @@ -339,10 +304,7 @@ impl TaskProcessorManager { let mut next_metrics_dump_time = SystemTime::now() + METRICS_DUMP_PERIOD; - log::debug!( - target: LOG_TARGET, - "TaskProcessorManager {manager_name}: Metrics loop started" - ); + log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Metrics loop started", manager_name); while !stop_requested.load(Ordering::Relaxed) { tokio::time::sleep(SLEEP_PERIOD).await; @@ -354,23 +316,15 @@ impl TaskProcessorManager { let mut metrics_dumper = Self::create_metrics_dumper(&processor_names); metrics_dumper.update(&metrics_handle); - log::debug!( - target: LOG_TARGET, - "TaskProcessorManager {manager_name} metrics:" - ); - metrics_dumper.dump( - |string| log::debug!(target: LOG_TARGET, "{manager_name}: {string}"), - ); + log::debug!(target: LOG_TARGET, "TaskProcessorManager {} metrics:", manager_name); + metrics_dumper.dump(|string| log::debug!(target: LOG_TARGET, "{}: {}", manager_name, string)); } next_metrics_dump_time = SystemTime::now() + METRICS_DUMP_PERIOD; } } - log::debug!( - target: LOG_TARGET, - "TaskProcessorManager {manager_name}: Metrics loop finished" - ); + log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Metrics loop finished", manager_name); // Mark as actually stopped is_stopped.store(true, Ordering::Relaxed); @@ -406,32 +360,20 @@ impl TaskProcessorManager { F: FnOnce() -> Pin + Send + 'static>> + Send + 'static, { if self.stop_requested.load(Ordering::Relaxed) { - log::trace!( - target: LOG_TARGET, - "TaskProcessorManager {} is stopped, ignoring posted closure", - self.name - ); + log::trace!(target: LOG_TARGET, "TaskProcessorManager {} is stopped, ignoring posted closure", self.name); return; } if let Some(processor) = self.processors.get(&tag) { processor.post_closure(closure); } else { - log::warn!( - target: LOG_TARGET, - "TaskProcessorManager {}: No TaskProcessor found for tag {tag}", - self.name - ); + log::warn!(target: LOG_TARGET, "TaskProcessorManager {}: No TaskProcessor found for tag {}", self.name, tag); } } /// Stop all task processors asynchronously pub fn stop_async(&self) { - log::info!( - target: LOG_TARGET, - "TaskProcessorManager {}: Stopping asynchronously", - self.name - ); + log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping asynchronously", self.name); self.stop_requested.store(true, Ordering::Relaxed); @@ -443,11 +385,7 @@ impl TaskProcessorManager { /// Stop all task processors and wait for completion pub fn stop(&self) { - log::info!( - target: LOG_TARGET, - "TaskProcessorManager {}: Stopping synchronously", - self.name - ); + log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping synchronously", self.name); self.stop_async(); @@ -458,12 +396,7 @@ impl TaskProcessorManager { while !self.is_stopped.load(Ordering::Relaxed) { if wait_count % 10 == 0 { // Log every second - log::info!( - target: LOG_TARGET, - "TaskProcessorManager {}: Waiting for stop completion... ({}ms)", - self.name, - wait_count * 100 - ); + log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Waiting for stop completion... ({}ms)", self.name, wait_count * 100); } std::thread::sleep(STOP_WAIT_DELAY); @@ -471,39 +404,20 @@ impl TaskProcessorManager { } // Manually stop all task processors - log::debug!( - target: LOG_TARGET, - "TaskProcessorManager {}: Stopping {} TaskProcessors", - self.name, - self.processors.len() - ); + log::debug!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping {} TaskProcessors", self.name, self.processors.len()); for (tag, processor) in &self.processors { - log::trace!( - target: LOG_TARGET, - "TaskProcessorManager {}: Stopping TaskProcessor for tag={tag}", - self.name - ); + log::trace!(target: LOG_TARGET, "TaskProcessorManager {}: Stopping TaskProcessor for tag={}", self.name, tag); processor.stop(); } - log::info!( - target: LOG_TARGET, - "TaskProcessorManager {}: Stopped after {}ms", - self.name, - wait_count * 100 - ); + log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Stopped after {}ms", self.name, wait_count * 100); } } impl Drop for TaskProcessorManager { fn drop(&mut self) { - log::info!( - target: LOG_TARGET, - "TaskProcessorManager {}: Dropping with {} processors", - self.name, - self.processors.len() - ); + log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Dropping with {} processors", self.name, self.processors.len()); self.stop(); log::info!(target: LOG_TARGET, "TaskProcessorManager {}: Dropped", self.name); } @@ -514,8 +428,8 @@ impl Drop for TaskProcessorManager { */ struct Peer { - src_adnl_addr: Arc, - dst_adnl_addr: Arc, + src_adnl_addr: Arc, + dst_adnl_addr: Arc, overlay_node: Arc, dht_node: Arc, is_stop_requested: Arc, @@ -525,8 +439,8 @@ struct Peer { impl Peer { /// Create new peer and start address resolution loop fn new( - src_adnl_addr: Arc, - dst_adnl_addr: Arc, + src_adnl_addr: Arc, + dst_adnl_addr: Arc, overlay_node: Arc, dht_node: Arc, runtime_handle: tokio::runtime::Handle, @@ -572,7 +486,7 @@ impl Peer { // Try to fetch address from DHT match self.dht_node.fetch_address(&self.dst_adnl_addr).await { - Ok(Some((adnl_addr, quic_addr, key))) => { + Ok(Some((addr, key))) => { // Check if address changed (first time or address different) let addr_changed = current_addr.is_none(); @@ -583,44 +497,28 @@ impl Peer { &self.src_adnl_addr, &[self.dst_adnl_addr.clone()], ) { - log::warn!( - target: LOG_TARGET, - "Error deleting old peer address: {e:?}" - ); + log::warn!(target: LOG_TARGET, "Error deleting old peer address: {:?}", e); } } // Add new address - let add_result = self.overlay_node.add_private_peers( - &self.src_adnl_addr, - vec![(adnl_addr, quic_addr, key)], - ); + let add_result = self + .overlay_node + .add_private_peers(&self.src_adnl_addr, vec![(addr, key)]); if let Err(e) = add_result { log::warn!(target: LOG_TARGET, "Error adding peer address: {:?}", e); } else { - log::debug!( - target: LOG_TARGET, - "Peer address updated: {:?}", - self.dst_adnl_addr - ); + log::debug!(target: LOG_TARGET, "Peer address updated: {:?}", self.dst_adnl_addr); current_addr = Some(()); // Mark that we have an address } } } Ok(None) => { - log::trace!( - target: LOG_TARGET, - "Peer address not found in DHT: {:?}", - self.dst_adnl_addr - ); + log::trace!(target: LOG_TARGET, "Peer address not found in DHT: {:?}", self.dst_adnl_addr); } Err(e) => { - log::warn!( - target: LOG_TARGET, - "DHT fetch error for peer {:?}: {e:?}", - self.dst_adnl_addr - ); + log::warn!(target: LOG_TARGET, "DHT fetch error for peer {:?}: {:?}", self.dst_adnl_addr, e); } } } @@ -644,12 +542,7 @@ impl Peer { /// Stop peer resolution loop synchronously and wait for completion fn stop(&self) { - log::trace!( - target: LOG_TARGET, - "Stopping Peer: {:?} -> {:?}", - self.src_adnl_addr, - self.dst_adnl_addr - ); + log::trace!(target: LOG_TARGET, "Stopping Peer: {:?} -> {:?}", self.src_adnl_addr, self.dst_adnl_addr); // Stop the resolution loop self.stop_async(); @@ -670,12 +563,7 @@ impl Peer { } } - log::trace!( - target: LOG_TARGET, - "Peer resolution loop finished: {:?} -> {:?}", - self.src_adnl_addr, - self.dst_adnl_addr - ); + log::trace!(target: LOG_TARGET, "Peer resolution loop finished: {:?} -> {:?}", self.src_adnl_addr, self.dst_adnl_addr); } } @@ -691,7 +579,8 @@ impl Drop for Peer { */ struct PeerStorage { - peers: Arc, Arc), Weak>>>, + peers: + Arc, Arc), Weak>>>, } impl PeerStorage { @@ -702,8 +591,8 @@ impl PeerStorage { /// Get or create peer for given src/dst ADNL addresses fn get_peer( self: &Arc, - src_adnl_addr: Arc, - dst_adnl_addr: Arc, + src_adnl_addr: Arc, + dst_adnl_addr: Arc, overlay_node: Arc, dht_node: Arc, runtime_handle: tokio::runtime::Handle, @@ -777,10 +666,7 @@ impl AdnlOverlayConsumer { overlay: Weak, stop_requested: Arc, ) -> Self { - log::debug!( - target: LOG_TARGET, - "Creating AdnlOverlayConsumer for overlay_id={overlay_id}" - ); + log::debug!(target: LOG_TARGET, "Creating AdnlOverlayConsumer for overlay_id={}", overlay_id); Self { overlay_id, overlay, stop_requested } } } @@ -805,9 +691,9 @@ impl Subscriber for AdnlOverlayConsumer { }; // Handle simplex direct messages (may come as custom messages in some paths) - let simplex_kind = if object.is::() { + let simplex_kind = if object.is::() { Some("vote") - } else if object.is::() { + } else if object.is::() { Some("certificate") } else { None @@ -834,11 +720,7 @@ impl Subscriber for AdnlOverlayConsumer { async fn try_consume_query(&self, query: TLObject, _peers: &AdnlPeers) -> Result { // Check if overlay is stopped if self.stop_requested.load(Ordering::Relaxed) { - log::warn!( - target: LOG_TARGET, - "AdnlOverlayConsumer: Overlay {} was stopped!", - &self.overlay_id - ); + log::warn!(target: LOG_TARGET, "AdnlOverlayConsumer: Overlay {} was stopped!", &self.overlay_id); fail!("Overlay {} was stopped!", &self.overlay_id); } @@ -846,11 +728,7 @@ impl Subscriber for AdnlOverlayConsumer { if let Some(overlay) = self.overlay.upgrade() { overlay.process_query(query, _peers).await } else { - log::warn!( - target: LOG_TARGET, - "AdnlOverlayConsumer: Overlay {} was dropped!", - &self.overlay_id - ); + log::warn!(target: LOG_TARGET, "AdnlOverlayConsumer: Overlay {} was dropped!", &self.overlay_id); fail!("Overlay {} was dropped!", &self.overlay_id); } } @@ -861,11 +739,11 @@ impl Subscriber for AdnlOverlayConsumer { */ struct AdnlOverlay { - stack: Arc, //ADNL network stack - overlay_id: Arc, //private overlay short identifier - local_id: PublicKeyHash, //local validator key hash - local_validator_key: Arc, //local validator key for signing broadcasts - local_adnl_key: Arc, //local ADNL key for two-step broadcast signing + stack: Arc, //ADNL network stack + overlay_id: Arc, //private overlay short identifier + local_id: PublicKeyHash, //local validator key hash + local_validator_key: Arc, //local validator key for signing broadcasts + local_adnl_key: Arc, //local ADNL key for two-step broadcast signing adnl_to_validator: HashMap, //ADNL key hash โ†’ validator key hash all_node_ids: Vec, //all node ADNL IDs in the overlay for multicast emulation of broadcast messages listener: ConsensusOverlayListenerPtr, //consensus overlay listener for incoming events @@ -907,7 +785,7 @@ impl AdnlOverlay { ); // Find local ADNL key from nodes by matching local_id - let mut local_adnl_key: Option> = None; + let mut local_adnl_key: Option> = None; let mut peers = Vec::new(); for node in nodes { @@ -935,12 +813,12 @@ impl AdnlOverlay { peers.len() ); - // Register QUIC keys if QUIC is enabled - let quic_enabled = if use_quic { + // Register QUIC keys and create transport if QUIC is enabled + let transport = if use_quic { if let Some(quic) = &stack.quic { // Register local validator's ADNL key as a TLS identity on a per-port endpoint let key_bytes: [u8; 32] = *local_adnl_key.pvt_key()?; - let ip_addr = stack.adnl.ip_address_adnl(); + let ip_addr = stack.adnl.ip_address(); let quic_port = ip_addr.port().checked_add(adnl::QuicNode::OFFSET_PORT).ok_or_else(|| { error!( @@ -983,7 +861,7 @@ impl AdnlOverlay { overlay_id: &overlay_id, runtime: Some(runtime_handle.clone()), }; - stack.overlay.add_private_overlay(params, &local_adnl_key, &peers, use_quic)?; + stack.overlay.add_private_overlay(params, &local_adnl_key, &peers)?; let stop_requested = Arc::new(AtomicBool::new(false)); @@ -1020,8 +898,11 @@ impl AdnlOverlay { stop_requested.clone(), )); - // Point-to-point multicast is used for broadcasts when TCP transport is available - let is_tcp_available = stack.is_tcp_available() && allow_tcp_communication; + // Point-to-point multicast is used for broadcasts when TCP or QUIC + // transport is available (FEC/UDP broadcast has no peers in private overlays). + let is_tcp_available = + (stack.is_tcp_available() && allow_tcp_communication) || transport; + if is_tcp_available { log::debug!( target: LOG_TARGET, @@ -1067,7 +948,7 @@ impl AdnlOverlay { peers: peer_objects, peers_storage: peer_storage.clone(), is_tcp_available: is_tcp_available, - is_quic_available: quic_enabled, + is_quic_available: transport, all_node_ids: all_node_ids, task_processor_manager, } @@ -1094,11 +975,7 @@ impl AdnlOverlay { .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) .is_err() { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay already stopped: overlay_id={}", - self.overlay_id - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay already stopped: overlay_id={}", self.overlay_id); return; // Already stopped } @@ -1116,11 +993,7 @@ impl AdnlOverlay { } }); - log::trace!( - target: LOG_TARGET, - "AdnlOverlay: Will cleanup {} peers on drop", - self.peers.len() - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay: Will cleanup {} peers on drop", self.peers.len()); log::debug!( target: LOG_TARGET, @@ -1132,11 +1005,7 @@ impl AdnlOverlay { /// Process incoming query from consumer pub async fn process_query(&self, query: TLObject, peers: &AdnlPeers) -> Result { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay::process_query: overlay_id={}", - self.overlay_id - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay::process_query: overlay_id={}", self.overlay_id); let now = Instant::now(); let data = serialize_boxed(&query).map_err(|e| { @@ -1180,18 +1049,16 @@ impl AdnlOverlay { Box::new(move |result| { // Check if stopped before responding if stop_requested_clone.load(Ordering::Relaxed) { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay: Query response cancelled - overlay stopped" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay: Query response cancelled - overlay stopped"); wait_for_response.respond(None); return; } // Transform BlockPayloadPtr result to Answer let answer_result = result.and_then(|payload| { - deserialize_boxed(payload.data()) - .map(|answer| Some(Answer::Object(answer.into()))) + deserialize_boxed(payload.data()).map(|answer| { + Some(Answer::Object(answer.into())) + }) }); wait_for_response.respond(Some(answer_result)); }), @@ -1199,25 +1066,16 @@ impl AdnlOverlay { } // Wait for response - let res = wait - .wait(&mut queue_reader, true) - .await + let res = wait.wait(&mut queue_reader, true).await .ok_or_else(|| { - log::warn!( - target: LOG_TARGET, - "AdnlOverlay: Waiting returned an internal error (query: {query:?})" - ); + log::warn!(target: LOG_TARGET, "AdnlOverlay: Waiting returned an internal error (query: {:?})", query); error!("Waiting returned an internal error!") })? .ok_or_else(|| error!("Answer was not set!"))?; // Log timing and metrics let elapsed = now.elapsed(); - log::trace!( - target: LOG_TARGET, - "AdnlOverlay: query elapsed: {}ms", - elapsed.as_millis() - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay: query elapsed: {}ms", elapsed.as_millis()); metrics::histogram!("ton_node_network_consensus_overlay_query_seconds").record(elapsed); Ok(TimedAnswer { @@ -1262,11 +1120,7 @@ impl AdnlOverlay { /// Start broadcast listeners (similar to CatchainClient::run_wait_broadcast) pub fn run_wait_broadcast(self: Arc) { - log::trace!( - target: LOG_TARGET, - "Starting broadcast listeners for overlay_id={}", - self.overlay_id - ); + log::trace!(target: LOG_TARGET, "Starting broadcast listeners for overlay_id={}", self.overlay_id); let overlay_id = self.overlay_id.clone(); let overlay = Arc::downgrade(&self); @@ -1331,86 +1185,59 @@ impl AdnlOverlay { // Spawn task for consensus broadcasts self.runtime_handle.spawn(async move { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay::wait_consensus_broadcast started for overlay_id={overlay_id}" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay::wait_consensus_broadcast started for overlay_id={}", overlay_id); let receiver = overlay_node.clone(); let consensus_listener = listener.clone(); loop { if stop_requested2.load(Ordering::Relaxed) { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay::wait_consensus_broadcast stopping for overlay_id={overlay_id}" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay::wait_consensus_broadcast stopping for overlay_id={}", overlay_id); break; } let message = receiver.wait_for_catchain(&overlay_id).await; match message { Ok(Some((catchain_block_update, inner_update, source_id))) => { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay: catchain broadcast ValidatorSession_BlockUpdate received" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay: catchain broadcast ValidatorSession_BlockUpdate received"); if let Some(listener) = consensus_listener.upgrade() { // Serialize catchain block update and inner update similar to reference let mut data: crate::RawBuffer = crate::RawBuffer::default(); - let mut serializer = Serializer::new(&mut data); + let mut serializer = ton_api::Serializer::new(&mut data); match serializer.write_boxed(&catchain_block_update.into_boxed()) { Ok(_) => { match inner_update { CatchainData::Catchain(upd) => { if let Err(e) = serializer.write_boxed(&upd.into_boxed()) { - log::error!( - target: LOG_TARGET, - "AdnlOverlay: Failed to serialize catchain update: {e}" - ); + log::error!(target: LOG_TARGET, "AdnlOverlay: Failed to serialize catchain update: {}", e); continue; } } CatchainData::ValidatorSession(upd) => { if let Err(e) = serializer.write_boxed(&upd.into_boxed()) { - log::error!( - target: LOG_TARGET, - "AdnlOverlay: Failed to serialize validator session update: {e}" - ); + log::error!(target: LOG_TARGET, "AdnlOverlay: Failed to serialize validator session update: {}", e); continue; } } } let data = crate::ConsensusCommonFactory::create_block_payload(data); - log::trace!( - target: LOG_TARGET, - "AdnlOverlay: routing consensus broadcast to listener via on_message" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay: routing consensus broadcast to listener via on_message"); listener.on_message(source_id, &data); } Err(e) => { - log::error!( - target: LOG_TARGET, - "AdnlOverlay: Failed to serialize catchain block update: {e}" - ); + log::error!(target: LOG_TARGET, "AdnlOverlay: Failed to serialize catchain block update: {}", e); } } } } Ok(None) => { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay::wait_consensus_broadcast finished for overlay_id={overlay_id}" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay::wait_consensus_broadcast finished for overlay_id={}", overlay_id); break; } Err(e) => { - log::error!( - target: LOG_TARGET, - "AdnlOverlay: consensus broadcast error: {e}" - ); + log::error!(target: LOG_TARGET, "AdnlOverlay: consensus broadcast error: {}", e); } } } @@ -1456,11 +1283,7 @@ impl ConsensusOverlay for AdnlOverlay { _is_retransmission: bool, ) { if self.stop_requested.load(Ordering::Relaxed) { - log::warn!( - target: LOG_TARGET, - "AdnlOverlay: Overlay {} was stopped!", - &self.overlay_id - ); + log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {} was stopped!", &self.overlay_id); return; } @@ -1628,24 +1451,21 @@ impl ConsensusOverlay for AdnlOverlay { let mut query_data = overlay_node.get_query_prefix(&overlay_id)?; serialize_boxed_append(&mut query_data, &query_body)?; - let (data, _) = overlay_node - .query_via_rldp( - &dst_adnl_id, - &TaggedByteSlice { - object: &query_data[..], - #[cfg(feature = "telemetry")] - tag: query_body.bare_object().constructor(), - }, - &overlay_id, - Some(max_answer_size), - v2, - None, - ) - .await?; + let (data, _) = overlay_node.query_via_rldp( + &dst_adnl_id, + &TaggedByteSlice { + object: &query_data[..], + #[cfg(feature = "telemetry")] + tag: query_body.bare_object().constructor(), + }, + &overlay_id, + Some(max_answer_size), + v2, + None, + ).await?; let data = data.ok_or_else(|| error!("answer is None!"))?; Ok(crate::ConsensusCommonFactory::create_block_payload(data)) - } - .await; + }.await; log::info!(target: LOG_TARGET, "AdnlOverlay::send_query_via_rldp: {:?}", result); @@ -1653,10 +1473,7 @@ impl ConsensusOverlay for AdnlOverlay { if !stop_requested.load(Ordering::Relaxed) { response_callback(result); } else { - log::trace!( - target: LOG_TARGET, - "AdnlOverlay: Skipping RLDP query callback - overlay stopped" - ); + log::trace!(target: LOG_TARGET, "AdnlOverlay: Skipping RLDP query callback - overlay stopped"); } }); } @@ -1667,71 +1484,16 @@ impl ConsensusOverlay for AdnlOverlay { sender_id: &PublicKeyHash, _send_as: &PublicKeyHash, payload: BlockPayloadPtr, - extra: Option>, ) { let overlay_id = self.overlay_id.clone(); let stop_requested = self.stop_requested.clone(); if stop_requested.load(Ordering::Relaxed) { - log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {overlay_id} was stopped!"); + log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {} was stopped!", &overlay_id); return; } - if self.is_quic_available || !self.is_tcp_available { - // QUIC or ADNL/UDP path - // If extra given, use two-step broadcast via QUIC/RLDP - // Otherwise use canonic broadcast via ADNL - let msg = payload.clone(); - let overlay_node = self.stack.overlay.clone(); - let local_validator_key = self.local_validator_key.clone(); - let transport = if self.is_quic_available { "QUIC" } else { "ADNL/UDP" }; - - self.runtime_handle.spawn(async move { - if stop_requested.load(Ordering::Relaxed) { - log::warn!( - target: LOG_TARGET, - "AdnlOverlay: Overlay {overlay_id} was stopped!" - ); - return; - } - - let msg_tagged = TaggedByteSlice { - object: msg.data(), - #[cfg(feature = "telemetry")] - tag: 0x80000002, // Consensus broadcast - }; - - let result = if let Some(extra) = extra { - // Twostep broadcast with extra - overlay_node - .broadcast_twostep( - &overlay_id, - &msg_tagged, - Some(&local_validator_key), - 0, - extra, - ) - .await - } else { - // Canonic broadcast - overlay_node - .broadcast( - &overlay_id, - &msg_tagged, - Some(&local_validator_key), - 0, - AdnlSendMethod::Fast, - ) - .await - }; - - log::debug!( - target: LOG_TARGET, - "AdnlOverlay::send_broadcast_fec_ex ({transport}) status: {result:?}" - ); - }); - } else { - // TCP path: manually build BroadcastTwostepSimple and multicast + if self.is_tcp_available { const IS_RETRANSMISSION: bool = false; log::trace!( @@ -1741,34 +1503,39 @@ impl ConsensusOverlay for AdnlOverlay { payload.data().len(), self.all_node_ids.len(), ); + // Build C++-compatible overlay.broadcastTwostepSimple instead of + // the Rust-only catchain.BroadcastWrapper. This uses the local ADNL + // key for signing (matching C++ Simplex behaviour) so that both Rust + // and C++ nodes can verify and deliver the broadcast. let result = (|| -> Result<()> { let data = payload.data().to_vec(); - let extra = extra.unwrap_or_default(); let date = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i32; let flags: i32 = 0; - let data_hash = sha256_digest(&data); + // Compute broadcast_id for to_sign + let data_hash = ton_block::sha256_digest(&data); let bcast_id = { + use ton_api::ton::overlay::broadcast_twostep::id::Id as BroadcastTwostepId; let id = BroadcastTwostepId { date, flags, - src: UInt256::from_slice(self.local_adnl_key.id().data()), - src_adnl_id: UInt256::from_slice(self.local_adnl_key.id().data()), - data_hash: UInt256::with_array(data_hash), - // Broadcast simulation over TCP, no partitioning - data_size: data.len() as i32, + src: ton_block::UInt256::from_slice(self.local_adnl_key.id().data()), + src_adnl_id: ton_block::UInt256::from_slice( + self.local_adnl_key.id().data(), + ), + data_hash: ton_block::UInt256::with_array(data_hash), part_size: data.len() as i32, - extra: extra.clone(), }; let id_bytes = serialize_bare(&id)?; - sha256_digest(&id_bytes) + ton_block::sha256_digest(&id_bytes) }; + // Sign: BroadcastTwostepSimpleToSign { id, data } let to_sign = BroadcastTwostepSimpleToSign { - id: UInt256::with_array(bcast_id), + id: ton_block::UInt256::with_array(bcast_id), data: data.clone(), }; let to_sign_bytes = serialize_bare(&to_sign)?; @@ -1779,10 +1546,9 @@ impl ConsensusOverlay for AdnlOverlay { date, flags, src: (&self.local_adnl_key).try_into()?, - src_adnl_id: UInt256::from_slice(self.local_adnl_key.id().data()), + src_adnl_id: ton_block::UInt256::from_slice(self.local_adnl_key.id().data()), certificate: OverlayCertificate::Overlay_EmptyCertificate, data, - extra, signature, } .into_boxed(); @@ -1796,7 +1562,7 @@ impl ConsensusOverlay for AdnlOverlay { log::trace!( target: LOG_TARGET, "AdnlOverlay::send_broadcast_fec_ex: sending BroadcastTwostepSimple \ - ({} bytes payload) via TCP multicast to {} peers", + ({} bytes payload) via multicast to {} peers", broadcast_payload.data().len(), self.all_node_ids.len(), ); @@ -1813,9 +1579,37 @@ impl ConsensusOverlay for AdnlOverlay { if let Err(err) = result { log::error!( target: LOG_TARGET, - "AdnlOverlay::send_broadcast_fec_ex: failed to build/send TCP broadcast: {err}" + "AdnlOverlay::send_broadcast_fec_ex: failed to build/send two-step broadcast: {err}" ); } + } else { + let msg = payload.clone(); + let overlay_node = self.stack.overlay.clone(); + let local_validator_key = self.local_validator_key.clone(); + let runtime_handle = self.runtime_handle.clone(); + + runtime_handle.spawn(async move { + if stop_requested.load(Ordering::Relaxed) { + log::warn!(target: LOG_TARGET, "AdnlOverlay: Overlay {} was stopped!", &overlay_id); + return; + } + + let msg_tagged = TaggedByteSlice { + object: msg.data(), + #[cfg(feature = "telemetry")] + tag: 0x80000002, // Catchain broadcast + }; + + let result = overlay_node.broadcast( + &overlay_id, + &msg_tagged, + Some(&local_validator_key), + 0, + AdnlSendMethod::Fast, + ).await; + + log::debug!(target: LOG_TARGET, "AdnlOverlay::send_broadcast_fec_ex status: {:?}", result); + }); } } } @@ -1923,10 +1717,7 @@ impl ConsensusOverlayManager for AdnlOverlayManager { // Add to managed overlays atomically overlays.insert(overlay_short_id.clone(), overlay.clone()); - log::trace!( - target: LOG_TARGET, - "Successfully started overlay: overlay_id={overlay_short_id}" - ); + log::trace!(target: LOG_TARGET, "Successfully started overlay: overlay_id={}", overlay_short_id); overlay }; @@ -1950,10 +1741,7 @@ impl ConsensusOverlayManager for AdnlOverlayManager { overlay_impl.stop(); self.overlays.lock().remove(overlay_short_id); } else { - log::warn!( - target: LOG_TARGET, - "Cannot downcast overlay to AdnlOverlay: overlay_id={overlay_short_id}" - ); + log::warn!(target: LOG_TARGET, "Cannot downcast overlay to AdnlOverlay: overlay_id={}", overlay_short_id); } } } diff --git a/src/node/consensus-common/src/dummy_catchain_overlay.rs b/src/node/consensus-common/src/dummy_catchain_overlay.rs index 85d184b..bdfabfe 100644 --- a/src/node/consensus-common/src/dummy_catchain_overlay.rs +++ b/src/node/consensus-common/src/dummy_catchain_overlay.rs @@ -93,7 +93,6 @@ impl ConsensusOverlay for DummyConsensusOverlay { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, - _extra: Option>, ) { log::trace!( "DummyConsensusOverlay: send broadcast_fec_ex {:?}/{:?}: {:?}", diff --git a/src/node/consensus-common/src/in_process_overlay.rs b/src/node/consensus-common/src/in_process_overlay.rs index ea2ca14..99d57c0 100644 --- a/src/node/consensus-common/src/in_process_overlay.rs +++ b/src/node/consensus-common/src/in_process_overlay.rs @@ -251,7 +251,6 @@ impl ConsensusOverlay for OverlayClientImpl { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, - _extra: Option>, ) { log::trace!( target: LOG_TARGET, diff --git a/src/node/consensus-common/src/lib.rs b/src/node/consensus-common/src/lib.rs index 769c176..d6422ea 100644 --- a/src/node/consensus-common/src/lib.rs +++ b/src/node/consensus-common/src/lib.rs @@ -616,13 +616,12 @@ pub trait ConsensusOverlay: Send + Sync { v2: bool, ); - /// Send broadcast with optional extra metadata (e.g. consensus.broadcastExtra for slot info) + /// Send broadcast fn send_broadcast_fec_ex( &self, sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, - extra: Option>, ); /// Implementation specific @@ -644,7 +643,7 @@ pub enum OverlayTransportType { impl OverlayTransportType { pub fn allow_tcp(&self) -> bool { - matches!(self, Self::CatchainTcp) + matches!(self, Self::CatchainTcp | Self::Simplex) } pub fn use_quic(&self) -> bool { @@ -1033,18 +1032,6 @@ pub trait SessionListener: Send + Sync { /// This is the common interface for all consensus session implementations /// (both catchain-based validator-session and simplex). pub trait Session: fmt::Display + Send + Sync { - /// Signal the session to begin active consensus processing. - /// - /// For Simplex sessions, `initial_block_seqno` is the expected seqno of - /// the first block to be produced (derived from prev_block_ids). The - /// session overlay is created at `create()` time so it can warm up - /// connections to peers. The FSM timeout clock only starts after - /// `start()` is called, preventing premature skip-votes on an - /// unconnected overlay. - /// - /// For Catchain sessions, the parameter is ignored (no-op). - fn start(&self, initial_block_seqno: u32); - /// Stop the session (blocks until all threads have stopped) /// Database is preserved for potential restart/recovery. fn stop(&self); diff --git a/src/node/consensus-common/src/log_player.rs b/src/node/consensus-common/src/log_player.rs index 8ce88e2..4454456 100644 --- a/src/node/consensus-common/src/log_player.rs +++ b/src/node/consensus-common/src/log_player.rs @@ -932,7 +932,6 @@ impl ConsensusOverlay for OverlayImpl { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, - _extra: Option>, ) { log::debug!("LogReplay: send broadcast_fec_ex {}/{}: {:?}", sender_id, send_as, payload); } diff --git a/src/node/consensus-common/src/node_test_network.rs b/src/node/consensus-common/src/node_test_network.rs index 6d16f58..fb20b78 100644 --- a/src/node/consensus-common/src/node_test_network.rs +++ b/src/node/consensus-common/src/node_test_network.rs @@ -243,12 +243,7 @@ impl<'a> NodeTestNetwork<'a> { overlay.set_rldp(rldp.clone()).unwrap(); let quic = if is_quic_enabled { - let quic = QuicNode::new( - vec![overlay.clone()], - cancellation_token.clone(), - None, - tokio::runtime::Handle::current(), - ); + let quic = QuicNode::new(vec![overlay.clone()], cancellation_token.clone()); overlay.set_quic(quic.clone()).unwrap(); Some(quic) } else { @@ -501,7 +496,6 @@ impl ConsensusOverlay for ToggleableOverlay { sender_id: &PublicKeyHash, send_as: &PublicKeyHash, payload: BlockPayloadPtr, - extra: Option>, ) { if !self.enabled.load(Ordering::Relaxed) { log::trace!( @@ -513,7 +507,7 @@ impl ConsensusOverlay for ToggleableOverlay { let _ = payload; return; } - self.inner.send_broadcast_fec_ex(sender_id, send_as, payload, extra); + self.inner.send_broadcast_fec_ex(sender_id, send_as, payload); } fn get_impl(&self) -> &dyn std::any::Any { diff --git a/src/node/consensus-common/tests/test_adnl_overlay.rs b/src/node/consensus-common/tests/test_adnl_overlay.rs index 733c59a..14ae25c 100644 --- a/src/node/consensus-common/tests/test_adnl_overlay.rs +++ b/src/node/consensus-common/tests/test_adnl_overlay.rs @@ -370,7 +370,6 @@ fn run_overlay_test( &node1.adnl_id, &node1.public_key.id(), broadcast_payload.clone(), - None, ); } } @@ -474,8 +473,6 @@ fn test_adnl_overlay_quic_delivery() -> Result<()> { true, // is_tcp_enabled true, // is_quic_enabled ); - // Quinn QUIC requires a Tokio runtime context on the calling thread - let _runtime_guard = test_network.get_runtime().enter(); let result = run_overlay_test(test_network.get_nodes().clone(), TRANSPORT_TYPE); test_network.shutdown(); @@ -779,7 +776,6 @@ fn run_adnl_overlay_performance_test( &node1.adnl_id, &node1.public_key.id(), make_broadcast_payload(), - None, ); broadcasts_sent += 1; thread::sleep(SLEEP_TIME); diff --git a/src/node/consensus-common/tests/test_in_process_overlay.rs b/src/node/consensus-common/tests/test_in_process_overlay.rs index ea3f795..70b1437 100644 --- a/src/node/consensus-common/tests/test_in_process_overlay.rs +++ b/src/node/consensus-common/tests/test_in_process_overlay.rs @@ -253,7 +253,7 @@ fn run_overlay_test(manager: ConsensusOverlayManagerPtr) -> Result<()> { } } // Send a broadcast. This will be received by all, including the sender. - overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload(), None); + overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload()); } // Wait for all broadcasts to be delivered, with a timeout, instead of a fixed sleep @@ -470,7 +470,7 @@ fn run_overlay_performance_test(manager: ConsensusOverlayManagerPtr) -> Result<( ); queries_sent += 1; } - overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload(), None); + overlays[i].send_broadcast_fec_ex(&node_ids[i], &node_ids[i], make_payload()); broadcasts_sent += 1; thread::sleep(SLEEP_TIME); } diff --git a/src/node/simplex/CHANGELOG.md b/src/node/simplex/CHANGELOG.md index 59b917d..3e93cfe 100644 --- a/src/node/simplex/CHANGELOG.md +++ b/src/node/simplex/CHANGELOG.md @@ -2,20 +2,20 @@ All notable changes to the Simplex Consensus Protocol implementation will be documented in this file. -## [0.5.0] - 2026-03-20 +## [Unreleased] ### Added -- Download committed block via full-node proof for MC gap recovery. +- **GET-COMMITTED-1**: Download committed block via full-node proof for MC gap recovery. Replaces Rust-only `requestCandidate2` with C++-compatible mechanism. `SessionListener::get_committed_candidate` trait method, `CommittedBlockProof` type, `ValidatorGroup::on_get_committed_candidate` implementation using `download_block_proof()`. - `test_simplex_consensus_finalcert_recovery`: FinalCert-recovery gremlin test with per-node lossy overlay targeting (7 MC nodes, node 0 gets 40% broadcast + 30% message/query loss). - `lossy_overlay_node_indices` field in `LossyOverlayOpts` for per-node loss targeting. -- C++-parity standstill slot-grid dump (`standstill_slot_grid_dump()` +- **NODE-20 (OBS-1)**: C++-parity standstill slot-grid dump (`standstill_slot_grid_dump()` on `SimplexState`). Mirrors C++ `pool.cpp::alarm()` per-validator markers (F/I/N/S/.) and cert flags (notar/skip/final). Wired into `debug_dump()` on stall detection. -- Receiver-side anomaly checks with configurable thresholds. +- **NODE-19 (HEALTH-1)**: Receiver-side anomaly checks with configurable thresholds. Shared `ReceiverHealthCounters` (`Arc`) for cross-thread standstill trigger and candidate giveup counting. Delta-based anomaly detection in `run_health_checks()` for cert verify failures, standstill triggers, and candidate giveups with cooldown. @@ -44,7 +44,7 @@ All notable changes to the Simplex Consensus Protocol implementation will be doc - Max-base merge for `available_base`: align ordering/merge semantics with C++ `pool.cpp::add_available_base()` while preventing forward-progress regression from out-of-order notarizations and skip propagation. -- Diagnostic dump no longer lists self (local validator) in the inactive nodes summary. `get_last_activity()` in receiver now reports self as always-active (consistent with `calculate_active_weight()`), and `debug_dump()` skips self index in the compact inactive list. +- **TN-754**: Diagnostic dump no longer lists self (local validator) in the inactive nodes summary. `get_last_activity()` in receiver now reports self as always-active (consistent with `calculate_active_weight()`), and `debug_dump()` skips self index in the compact inactive list. - **Restart gremlin test enabled**: `test_simplex_consensus_restart_gremlin` now passes โ€” `first_non_finalized_slot` correctly advances on skip in all modes. - DB is now preserved on session stop (previously destroyed prematurely). - Overlay is registered before bootstrap load completes (prevents missed messages during startup). @@ -52,7 +52,7 @@ All notable changes to the Simplex Consensus Protocol implementation will be doc ### Removed - `requestCandidate2` / `candidateAndCert2` TL types and all v2 request paths. `ENABLE_REQUEST_CANDIDATE_V2` constant removed. `want_final` param removed from - `request_candidate()`. All FinalCert recovery now uses committed-block proof download. + `request_candidate()`. All FinalCert recovery now uses GET-COMMITTED-1. ### Planned - FinalCert proactive rebroadcast (C++ `cfd8850c` parity) @@ -64,6 +64,12 @@ All notable changes to the Simplex Consensus Protocol implementation will be doc --- +## [0.5.0] - 2026-02-01 + +**Baseline**: 0.4.0 release. + +--- + ## [0.4.0] - 2026-02-01 Major release focused on **C++ interoperability** (signatures/certificates/networking), **restart resilience**, and production-grade diagnostics/tests. @@ -427,7 +433,7 @@ Major release focusing on candidate resolution, certificate system, and operatio | Version | Date | Tag | Description | |---------|------|-----|-------------| -| 0.5.0 | 2026-03-20 | `simplex-0.5.0` | Committed-block proof recovery, restart gremlin fix, requestCandidate2 removal, parity docs update | +| 0.5.0 | 2026-02-28 | `simplex-0.5.0` | GET-COMMITTED-1, restart gremlin fix, requestCandidate2 removal | | 0.4.0 | 2026-02-01 | `simplex-0.4.0` | Block signature types, C++ compatibility, restart resilience | | 0.3.0 | 2026-01-14 | `simplex-0.3.0` | Candidate resolver, certificates, operational stability | | 0.2.0 | 2026-01-07 | `simplex-0.2.0` | consensus-common integration, dependency restructuring | @@ -435,5 +441,10 @@ Major release focusing on candidate resolution, certificate system, and operatio --- - +[Unreleased]: https://github.com/RSquad/ton-node/compare/simplex-0.5.0...HEAD +[0.5.0]: https://github.com/RSquad/ton-node/compare/simplex-0.4.0...simplex-0.5.0 +[0.4.0]: https://github.com/RSquad/ton-node/compare/simplex-0.3.0...simplex-0.4.0 +[0.3.0]: https://github.com/RSquad/ton-node/compare/simplex-0.2.0...simplex-0.3.0 +[0.2.0]: https://github.com/RSquad/ton-node/compare/simplex-0.1.0...simplex-0.2.0 +[0.1.0]: https://github.com/RSquad/ton-node/releases/tag/simplex-0.1.0 diff --git a/src/node/simplex/Cargo.toml b/src/node/simplex/Cargo.toml index 3ca3440..49a8bb1 100644 --- a/src/node/simplex/Cargo.toml +++ b/src/node/simplex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'simplex' -version = '0.5.0' +version = '0.4.0' edition = '2021' authors = ['RSquad'] description = 'Simplex consensus protocol implementation for TON blockchain' diff --git a/src/node/simplex/README.md b/src/node/simplex/README.md index 8fced04..bdc818a 100644 --- a/src/node/simplex/README.md +++ b/src/node/simplex/README.md @@ -1,6 +1,6 @@ # Simplex Consensus Protocol -**Version**: 0.5.0 (March 20, 2026) | [Changelog](CHANGELOG.md) +**Version**: 0.5.0 (February 28, 2026) | [Changelog](CHANGELOG.md) Rust implementation of the Simplex consensus protocol for TON blockchain. @@ -45,32 +45,38 @@ overlay / ADNL (lower level, network) This crate targets wire-compatibility with the upstream **C++ Simplex** implementation (`origin/testnet@e40d0e36`, Feb 28, 2026). +### Critical interop blockers + +- **SIG-1**: Block candidate signature input differs โ€” Rust signs `consensus.candidateParent(candidateId)`, C++ signs `consensus.candidateId` directly. **CRITICAL** + ### Protocol parity gaps (from C++ upstream) -- C++ proactively rebroadcasts FinalCerts (`cfd8850c`) โ€” Rust standstill replay is less aggressive. **HIGH** +- **FINALCERT-REBROADCAST**: C++ proactively rebroadcasts FinalCerts (`cfd8850c`) โ€” Rust standstill replay is less aggressive. **HIGH** +- **MC-FORK-PREVENTION**: C++ validator rejects MC candidates that would fork with real blocks (`9aac62b8`) โ€” Rust lacks symmetric check. **CRITICAL** +- **ADAPTIVE-SKIP-TIMEOUT**: C++ adaptively increases first block timeout after skip vote (`3c0cae03`) โ€” not implemented in Rust. + +### Network / transport differences + +- **TWOSTEP-1**: Candidate broadcasts: C++ uses twostep FEC (`send_twostep_broadcast_=true`), Rust uses single-step overlay broadcast (not implemented). +- **QUIC-1**: QUIC transport layer used by C++ for twostep sender; Rust simplex doesn't use QUIC (deferred). ### Implementation parity gaps -- Committed-parent validation gate โ€” needs state-root caching / apply-block-to-state. -- Base selection should use "max available base" like C++ `SlotState::add_available_base` (audit needed). -- C++ has `ImprovedStructureLZ4WithState` (BOC compression algo 2) โ€” Rust only supports algos 0 and 1. -- C++ has `StoreCellHint` for DB commit optimization during MerkleUpdate apply โ€” Rust lacks equivalent. -- C++ overlay manager can buffer messages for unknown overlays (disabled by default) โ€” Rust lacks equivalent. +- **FLOW-1**: Committed-parent validation gate โ€” needs state-root caching / apply-block-to-state. +- **POOL-BASE-1**: Base selection should use "max available base" like C++ `SlotState::add_available_base` (audit needed). +- **ALGO-2**: C++ has `ImprovedStructureLZ4WithState` (BOC compression algo 2) โ€” Rust only supports algos 0 and 1. +- **STORE-HINT-1**: C++ has `StoreCellHint` for DB commit optimization during MerkleUpdate apply โ€” Rust lacks equivalent. +- **OVERLAY-BUFFER-1**: C++ overlay manager can buffer messages for unknown overlays (disabled by default) โ€” Rust lacks equivalent. ### Resolved (for reference) -- Candidate signature now signs bare `consensus.candidateId` directly, matching C++ testnet. Regression test: `test_candidate_id_to_sign_is_bare_candidate_id`. -- MC stale-head rejection implemented in `validator_group.rs` (`should_reject_stale_mc_candidate`), matching C++ `block-validator.cpp` commit `9aac62b8`. -- Adaptive first-block timeout backoff after skip implemented in `simplex_state.rs` (`apply_adaptive_timeout_backoff`), matching C++ `consensus.cpp`. -- Twostep FEC broadcast implemented in `consensus-common/adnl_overlay.rs` (`BroadcastTwostepSimple`), with C++-compatible signing. -- QUIC transport supported via `SessionOptions::use_quic` and `OverlayTransportType::SimplexQuic`. Tested in `test_adnl_overlay_quic_delivery`. -- Overlay ID computation (node ordering, short ID) -- `candidateAndCert.notar` encoding (voteSignatureSet) -- Handle incoming `consensus.simplex.certificate` on vote channel -- `requestCandidate2` removed โ€” replaced by `get_committed_candidate` -- Shard `before_split` empty block rule -- Restart support (DB persistence + startup recovery) -- Certificate rebroadcast on restart +- **OVERLAY-1**: Overlay ID computation (node ordering, short ID) +- **INTEROP-1**: `candidateAndCert.notar` encoding (voteSignatureSet) +- **INTEROP-2**: Handle incoming `consensus.simplex.certificate` on vote channel +- **INTEROP-3**: `requestCandidate2` removed โ€” replaced by `get_committed_candidate` (GET-COMMITTED-1) +- **SPLIT-1**: Shard `before_split` empty block rule +- **U5.6**: Restart support (DB persistence + startup recovery) +- **CERT-1**: Certificate rebroadcast on restart ## Architecture @@ -350,7 +356,7 @@ Single-threaded consensus algorithm (crate-private): - โœ… Standstill coordination - calls `receiver.reschedule_standstill()` on finalization, `set_standstill_slots()` on finalization/skip - โœ… DB persistence - finalized blocks, candidate infos, notar certs, votes, pool state persisted to RocksDB - โœ… Startup recovery - bootstrap load, vote replay, receiver cache restore, recommit to ValidatorGroup -- โœ… Download committed block via full-node proof for MC gap recovery (replaces requestCandidate2) +- โœ… GET-COMMITTED-1 - download committed block via full-node proof for MC gap recovery (replaces requestCandidate2) - โš ๏ธ Precollation parent tracking - needs fix for cross-window scenarios **Key methods:** @@ -544,6 +550,7 @@ Cryptographic and utility functions: | `max_collated_data_size` | `usize` | 4 MB | Max collated data | | `collation_retry_timeout` | `Duration` | 1s | Collation retry timeout | | `collation_retry_max_attempts` | `u32` | 3 | Max collation retries | +| `max_precollated_blocks` | `u32` | 10 | Max precollated blocks | | `use_callback_thread` | `bool` | true | Use separate callback thread | ## Integration @@ -655,7 +662,7 @@ Multi-instance consensus tests with in-process overlay. |------|-------------|--------| | `test_simplex_consensus_basic` | Basic consensus with 7 nodes, 100 rounds | โœ… | | `test_simplex_consensus_with_failures` | Consensus with simulated failures | โœ… | -| `test_simplex_consensus_finalcert_recovery` | FinalCert recovery via `get_committed_candidate` | โœ… | +| `test_simplex_consensus_finalcert_recovery` | FinalCert recovery via `get_committed_candidate` (GET-COMMITTED-1) | โœ… | | `test_simplex_consensus_shard_with_mc_notifications` | MC finalization forwarding to shards | โœ… | | `test_simplex_consensus_adnl_overlay` | ADNL overlay-based consensus | โœ… | | `test_simplex_consensus_adnl_net_gremlin` | ADNL net gremlin (packet loss/delay simulation) | โœ… | diff --git a/src/node/simplex/src/block.rs b/src/node/simplex/src/block.rs index 08fa520..2861d4f 100644 --- a/src/node/simplex/src/block.rs +++ b/src/node/simplex/src/block.rs @@ -869,22 +869,13 @@ impl RawCandidate { leader_idx: ValidatorIndex, shard: &ShardIdent, max_size: usize, - proto_version: u32, ) -> Result { // Parse TL object let data_vec = data.to_vec(); let candidate_tl = consensus_common::utils::deserialize_tl_boxed_object::(&data_vec)?; - Self::from_tl( - &candidate_tl, - session_id, - leader_key, - leader_idx, - shard, - max_size, - proto_version, - ) + Self::from_tl(&candidate_tl, session_id, leader_key, leader_idx, shard, max_size) } /// Create from already-parsed TL object @@ -907,14 +898,12 @@ impl RawCandidate { leader_idx: ValidatorIndex, shard: &ShardIdent, max_size: usize, - proto_version: u32, ) -> Result { // Extract parent let parent_id = Self::extract_parent(candidate_tl)?; // Extract block data - returns CandidateBlockData - let block_data = - Self::extract_block_data(candidate_tl, leader_key, shard, max_size, proto_version)?; + let block_data = Self::extract_block_data(candidate_tl, leader_key, shard, max_size)?; // Validate invariant: empty blocks must have parent if block_data.is_empty() && parent_id.is_none() { @@ -996,7 +985,6 @@ impl RawCandidate { leader_key: &PublicKey, shard: &ShardIdent, max_size: usize, - proto_version: u32, ) -> Result { match candidate_tl { CandidateData::Consensus_Block(block) => { @@ -1011,7 +999,6 @@ impl RawCandidate { candidate_bytes, shard, max_size, - proto_version, )?; match block_info { diff --git a/src/node/simplex/src/database.rs b/src/node/simplex/src/database.rs index 7e51dff..b38802f 100644 --- a/src/node/simplex/src/database.rs +++ b/src/node/simplex/src/database.rs @@ -41,14 +41,7 @@ use consensus_common::{ AsyncKeyValueStorageOptions, AsyncKeyValueStoragePtr, ConsensusCommonFactory, RawBuffer, StorageAsyncResultPtr, }; -use std::{ - path::Path, - sync::{ - atomic::{AtomicI64, Ordering}, - Arc, - }, - time::Duration, -}; +use std::{path::Path, sync::Arc, time::Duration}; use ton_api::{ deserialize_typed, serialize_boxed, ton::{ @@ -64,7 +57,6 @@ use ton_api::{ }, finalizedblock::FinalizedBlock as FinalizedBlockValue, key::{ - candidate::Candidate as CandidatePayloadKey, candidate_resolver::{ candidateinfo::CandidateInfo as CandidateInfoKey, notarcert::NotarCert as NotarCertKey, @@ -72,7 +64,6 @@ use ton_api::{ }, finalizedblock::FinalizedBlock as FinalizedBlockKey, vote::Vote as VoteKey, - Candidate as CandidatePayloadKeyBoxed, FinalizedBlock as FinalizedBlockKeyBoxed, PoolState as PoolStateKey, Vote as VoteKeyBoxed, }, @@ -97,7 +88,7 @@ use ton_block::{error, BlockIdExt, Result, UInt256}; // ============================================================================ /// Log target for database operations (matches simplex crate log target) -const TARGET: &str = "simplex"; +const LOG_TARGET: &str = "simplex"; /// Default sync timeout for blocking reads const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(5); @@ -132,13 +123,6 @@ fn prefix_pool_state() -> u32 { PoolStateKey::default().bare_object().constructor() } -/// Get key prefix for candidate payloads (full serialized CandidateData bytes). -/// -/// C++ parity: `consensus.simplex.db.key.candidate` TL type in candidate-resolver.cpp. -fn prefix_candidate_payload() -> u32 { - CandidatePayloadKey::constructor_const() -} - // ============================================================================ // Record Types // ============================================================================ @@ -182,10 +166,7 @@ pub struct NotarCertRecord { /// /// Stores votes by their hash for standstill recovery. /// Key: vote_hash (sha256 of serialized vote) -/// Value: raw vote data + node index + seqno -/// -/// C++ parity: db.cpp assigns monotonic seqno to each vote for replay ordering. -/// Votes must be replayed in the order they were originally cast. +/// Value: raw vote data + node index #[derive(Debug, Clone)] pub struct VoteRecord { /// Hash of the vote (key) @@ -194,8 +175,6 @@ pub struct VoteRecord { pub data: RawBuffer, /// Validator index that submitted this vote pub node_idx: ValidatorIndex, - /// Monotonic sequence number for replay ordering (C++ parity) - pub seqno: i64, } /// Pool state record for restart support @@ -228,8 +207,6 @@ pub struct Bootstrap { pub votes: Vec, /// Pool state (for skip vote generation) pub pool_state: Option, - /// Candidate payload bytes (serialized CandidateData, for requestCandidate serving) - pub candidate_payloads: Vec<(RawCandidateId, Vec)>, } /// Bootstrap data for recovery processor (session state only, no candidate_infos). @@ -257,8 +234,7 @@ impl Bootstrap { /// Split bootstrap into component-specific parts. /// /// Consumes self for zero-copy transfer of vectors. - /// Returns (session_boot, receiver_boot, candidate_payloads). - pub fn split(self) -> (SessionBootstrap, ReceiverBootstrap, Vec<(RawCandidateId, Vec)>) { + pub fn split(self) -> (SessionBootstrap, ReceiverBootstrap) { ( SessionBootstrap { finalized_blocks: self.finalized_blocks, @@ -266,7 +242,6 @@ impl Bootstrap { pool_state: self.pool_state, }, ReceiverBootstrap { notar_certs: self.notar_certs }, - self.candidate_payloads, ) } @@ -277,7 +252,6 @@ impl Bootstrap { && self.notar_certs.is_empty() && self.votes.is_empty() && self.pool_state.is_none() - && self.candidate_payloads.is_empty() } } @@ -364,16 +338,6 @@ fn deserialize_candidate_info(key_bytes: &[u8], value_bytes: &[u8]) -> Result Result> { - let key = CandidatePayloadKey { candidateId: raw_candidate_id_to_tl(candidate_id) }; - serialize_boxed(&key.into_boxed()).map_err(|e| error!("serialization failed: {}", e)) -} - -fn deserialize_candidate_payload_key(key_bytes: &[u8]) -> Result { - let key: CandidatePayloadKey = deserialize_typed::(key_bytes)?.only(); - Ok(raw_candidate_id_from_tl(key.candidateId)) -} - fn serialize_notar_cert_key(candidate_id: &RawCandidateId) -> Result> { let key = NotarCertKey { candidateId: raw_candidate_id_to_tl(candidate_id) }; serialize_boxed(&key.into_boxed()).map_err(|e| error!("serialization failed: {}", e)) @@ -403,11 +367,8 @@ fn serialize_vote_key(vote_hash: &UInt256) -> Result> { } fn serialize_vote_value(record: &VoteRecord) -> Result> { - let value = VoteValue { - data: record.data.clone(), - node_idx: record.node_idx.value() as i32, - seqno: record.seqno, - }; + let value = + VoteValue { data: record.data.clone(), node_idx: record.node_idx.value() as i32, seqno: 0 }; serialize_boxed(&value.into_boxed()).map_err(|e| error!("serialization failed: {}", e)) } @@ -418,7 +379,6 @@ fn deserialize_vote(key_bytes: &[u8], value_bytes: &[u8]) -> Result vote_hash: key.vote_hash.clone(), data: value.data, node_idx: ValidatorIndex::new(value.node_idx as u32), - seqno: value.seqno, }) } @@ -456,7 +416,7 @@ fn filter_finalized_chain(mut records: Vec) -> Vec) -> Vec { if record.parent.as_ref() != Some(expected) { log::warn!( - target: TARGET, + target: LOG_TARGET, "SimplexDb: skipping finalized block slot={} (parent mismatch)", record.candidate_id.slot.value() ); @@ -499,8 +459,6 @@ pub struct SimplexDb { storage: AsyncKeyValueStoragePtr, /// Storage ID (for logging) storage_id: String, - /// Monotonic vote seqno counter (C++ parity: db.cpp next_seqno_) - next_vote_seqno: AtomicI64, } impl SimplexDb { @@ -519,23 +477,12 @@ impl SimplexDb { let storage_id = storage_id.to_string(); log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: opening at {}", storage_id, db_path.display() ); - if let Some(parent) = db_path.parent() { - std::fs::create_dir_all(parent).map_err(|e| { - error!( - "SimplexDb {}: failed to create parent dir {}: {}", - storage_id, - parent.display(), - e - ) - })?; - } - // SimplexDb does not use callbacks let options = AsyncKeyValueStorageOptions { use_callback_thread: false }; @@ -543,13 +490,13 @@ impl SimplexDb { ConsensusCommonFactory::create_async_key_value_storage(db_path, &storage_id, options)?; log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: opened at {}", storage_id, db_path.display() ); - Ok(Arc::new(Self { storage, storage_id, next_vote_seqno: AtomicI64::new(0) })) + Ok(Arc::new(Self { storage, storage_id })) } // ========================================================================= @@ -573,7 +520,7 @@ impl SimplexDb { /// Called when a block is finalized or notarized with a certificate. pub fn save_finalized_block(&self, record: &FinalizedBlockRecord) -> Result<()> { log::trace!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: save_finalized_block slot={} is_final={}", self.storage_id, record.candidate_id.slot.value(), @@ -600,7 +547,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn save_candidate_info(&self, record: &CandidateInfoRecord) -> Result<()> { log::trace!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: save_candidate_info slot={} leader={}", self.storage_id, record.candidate_id.slot.value(), @@ -628,7 +575,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn save_notar_cert(&self, candidate_id: &RawCandidateId, cert: &NotarCert) -> Result<()> { log::trace!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: save_notar_cert slot={} signatures={}", self.storage_id, candidate_id.slot.value(), @@ -640,14 +587,9 @@ impl SimplexDb { } /// Save vote record (async result). - /// - /// Assigns a monotonic seqno for replay ordering (C++ parity: db.cpp next_seqno_++). pub fn save_vote_async(&self, record: &VoteRecord) -> Result> { - let seqno = self.next_vote_seqno.fetch_add(1, Ordering::Relaxed); - let mut record_with_seqno = record.clone(); - record_with_seqno.seqno = seqno; - let key = serialize_vote_key(&record_with_seqno.vote_hash)?; - let value = serialize_vote_value(&record_with_seqno)?; + let key = serialize_vote_key(&record.vote_hash)?; + let value = serialize_vote_value(record)?; Ok(self.storage.set(key, value, None)) } @@ -657,7 +599,7 @@ impl SimplexDb { #[allow(dead_code)] // Convenience wrapper; prefer `_async()` in production code. pub fn save_vote(&self, record: &VoteRecord) -> Result<()> { log::trace!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: save_vote hash={} node_idx={}", self.storage_id, hex::encode(&record.vote_hash.as_slice()[..8]), @@ -684,7 +626,7 @@ impl SimplexDb { #[allow(dead_code)] // Convenience wrapper; prefer `_async()` in production code. pub fn save_pool_state(&self, record: &PoolStateRecord) -> Result<()> { log::trace!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: save_pool_state first_nonannounced_window={}", self.storage_id, record.first_nonannounced_window @@ -694,100 +636,6 @@ impl SimplexDb { Ok(()) } - // ========================================================================= - // Candidate Payload Storage (C++ CandidateResolver::store_candidate parity) - // ========================================================================= - - /// Save serialized CandidateData bytes (async, fire-and-forget). - /// - /// C++ parity: `candidate-resolver.cpp store_candidate()` persists the full - /// serialized candidate so `requestCandidate(want_candidate=true)` queries - /// can be served from DB after restart. - pub fn save_candidate_payload_async( - &self, - candidate_id: &RawCandidateId, - candidate_data_bytes: &[u8], - ) -> Result> { - let key = serialize_candidate_payload_key(candidate_id)?; - Ok(self.storage.set(key, candidate_data_bytes.to_vec(), None)) - } - - /// Load a single candidate payload by id (blocking, for query fallback). - pub fn load_candidate_payload_by_id( - &self, - candidate_id: &RawCandidateId, - timeout: Duration, - ) -> Result>> { - let key = serialize_candidate_payload_key(candidate_id)?; - let result = self.storage.get(key, None); - match result.wait_timeout(timeout) { - Some(Ok(Some(value))) => Ok(Some(value)), - Some(Ok(None)) => Ok(None), - Some(Err(e)) => Err(e), - None => Err(error!("SimplexDb: timeout loading candidate payload by id")), - } - } - - /// Load all candidate payloads asynchronously (for startup restore). - pub fn load_candidate_payloads_async(&self) -> StorageAsyncResultPtr, Vec)>> { - log::debug!( - target: TARGET, - "SimplexDb {}: load_candidate_payloads_async", - self.storage_id - ); - self.storage.get_by_prefix_u32(prefix_candidate_payload(), None) - } - - // ========================================================================= - // Single-Record Lookups (async, for live query fallback) - // ========================================================================= - - /// Look up a single candidate info record by candidate ID (blocking). - /// - /// Unlike `load_candidate_infos()` which scans all records, this looks up - /// a single record by its exact key. Used by the RequestCandidate fallback - /// when resolver_cache misses. - /// - /// Reference: C++ candidate-resolver.cpp `try_load_candidate_data_from_db()` - pub fn load_candidate_info_by_id( - &self, - candidate_id: &RawCandidateId, - timeout: Duration, - ) -> Result> { - let key = serialize_candidate_info_key(candidate_id)?; - let result = self.storage.get(key.clone(), None); - match result.wait_timeout(timeout) { - Some(Ok(Some(value))) => { - let record = deserialize_candidate_info(&key, &value)?; - Ok(Some(record)) - } - Some(Ok(None)) => Ok(None), - Some(Err(e)) => Err(e), - None => Err(error!("SimplexDb: timeout loading candidate info by id")), - } - } - - /// Look up a single notar cert record by candidate ID (blocking). - /// - /// Used by the RequestCandidate fallback for notar_cert recovery. - pub fn load_notar_cert_by_id( - &self, - candidate_id: &RawCandidateId, - timeout: Duration, - ) -> Result> { - let key = serialize_notar_cert_key(candidate_id)?; - let result = self.storage.get(key.clone(), None); - match result.wait_timeout(timeout) { - Some(Ok(Some(value))) => { - let record = deserialize_notar_cert(&key, &value)?; - Ok(Some(record)) - } - Some(Ok(None)) => Ok(None), - Some(Err(e)) => Err(e), - None => Err(error!("SimplexDb: timeout loading notar cert by id")), - } - } - // ========================================================================= // Async Read Operations (for cancellable bootstrap) // ========================================================================= @@ -798,7 +646,7 @@ impl SimplexDb { /// with cancellation support via `wait_cancellable()`. pub fn load_finalized_blocks_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_finalized_blocks_async", self.storage_id ); @@ -808,7 +656,7 @@ impl SimplexDb { /// Load all candidate infos asynchronously. pub fn load_candidate_infos_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_candidate_infos_async", self.storage_id ); @@ -818,7 +666,7 @@ impl SimplexDb { /// Load all notar certs asynchronously. pub fn load_notar_certs_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_notar_certs_async", self.storage_id ); @@ -828,7 +676,7 @@ impl SimplexDb { /// Load all votes asynchronously. pub fn load_votes_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_votes_async", self.storage_id ); @@ -840,7 +688,7 @@ impl SimplexDb { /// Returns raw key-value pairs; caller deserializes. pub fn load_pool_state_async(&self) -> StorageAsyncResultPtr, Vec)>> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_pool_state_async", self.storage_id ); @@ -857,7 +705,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn load_finalized_blocks(&self) -> Result> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_finalized_blocks", self.storage_id ); @@ -874,7 +722,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: failed to deserialize finalized block: {}", self.storage_id, e @@ -892,7 +740,7 @@ impl SimplexDb { let kept = records.len(); log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: loaded {} finalized blocks (kept {} after chain filter)", self.storage_id, total, @@ -908,7 +756,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn load_candidate_infos(&self) -> Result> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_candidate_infos", self.storage_id ); @@ -925,7 +773,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: failed to deserialize candidate info: {}", self.storage_id, e @@ -935,7 +783,7 @@ impl SimplexDb { } log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: loaded {} candidate infos", self.storage_id, records.len() @@ -950,7 +798,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn load_notar_certs(&self) -> Result> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_notar_certs", self.storage_id ); @@ -967,7 +815,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: failed to deserialize notar cert: {}", self.storage_id, e @@ -977,7 +825,7 @@ impl SimplexDb { } log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: loaded {} notar certs", self.storage_id, records.len() @@ -992,7 +840,7 @@ impl SimplexDb { #[allow(dead_code)] // Not used yet; kept for restart/debug parity. pub fn load_votes(&self) -> Result> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_votes", self.storage_id ); @@ -1009,7 +857,7 @@ impl SimplexDb { Ok(record) => records.push(record), Err(e) => { log::error!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: failed to deserialize vote: {}", self.storage_id, e @@ -1018,20 +866,11 @@ impl SimplexDb { } } - // C++ parity: sort by seqno for deterministic replay order (db.cpp init_votes) - records.sort_by_key(|r| r.seqno); - - // Initialize next_vote_seqno from max seqno + 1 (C++ parity: db.cpp next_seqno_) - if let Some(max_seqno) = records.last().map(|r| r.seqno) { - self.next_vote_seqno.store(max_seqno + 1, Ordering::Relaxed); - } - log::info!( - target: TARGET, - "SimplexDb {}: loaded {} votes (next_seqno={})", + target: LOG_TARGET, + "SimplexDb {}: loaded {} votes", self.storage_id, - records.len(), - self.next_vote_seqno.load(Ordering::Relaxed) + records.len() ); Ok(records) @@ -1043,7 +882,7 @@ impl SimplexDb { #[allow(dead_code)] // Not used yet; kept for restart/debug parity. pub fn load_pool_state(&self) -> Result> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_pool_state", self.storage_id ); @@ -1056,7 +895,7 @@ impl SimplexDb { if result.is_empty() { log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: no pool state found (first run)", self.storage_id ); @@ -1066,7 +905,7 @@ impl SimplexDb { // Should be exactly one record (singleton) if result.len() > 1 { log::warn!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: multiple pool state records found ({}), using first", self.storage_id, result.len() @@ -1077,7 +916,7 @@ impl SimplexDb { let record = deserialize_pool_state(value_bytes)?; log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: loaded pool state first_nonannounced_window={}", self.storage_id, record.first_nonannounced_window @@ -1099,7 +938,7 @@ impl SimplexDb { #[allow(dead_code)] // Prefer `load_bootstrap_cancellable()` in session startup. pub fn load_bootstrap(&self) -> Result { log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_bootstrap", self.storage_id ); @@ -1110,40 +949,17 @@ impl SimplexDb { let votes = self.load_votes()?; let pool_state = self.load_pool_state()?; - // Load candidate payloads (optional, graceful if absent) - let payloads_raw = self - .load_candidate_payloads_async() - .wait_timeout(DEFAULT_SYNC_TIMEOUT) - .ok_or_else(|| error!("SimplexDb: timeout loading candidate payloads"))??; - let mut candidate_payloads = Vec::with_capacity(payloads_raw.len()); - for (k, v) in payloads_raw { - match deserialize_candidate_payload_key(&k) { - Ok(id) => candidate_payloads.push((id, v)), - Err(e) => { - log::error!(target: TARGET, "SimplexDb: skip bad candidate payload key: {e}") - } - } - } - - let bootstrap = Bootstrap { - finalized_blocks, - candidate_infos, - notar_certs, - votes, - pool_state, - candidate_payloads, - }; + let bootstrap = + Bootstrap { finalized_blocks, candidate_infos, notar_certs, votes, pool_state }; log::info!( - target: TARGET, - "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, \ - {} payloads, pool_state={}", + target: LOG_TARGET, + "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, pool_state={}", self.storage_id, bootstrap.finalized_blocks.len(), bootstrap.candidate_infos.len(), bootstrap.notar_certs.len(), bootstrap.votes.len(), - bootstrap.candidate_payloads.len(), bootstrap.pool_state.is_some() ); @@ -1163,7 +979,7 @@ impl SimplexDb { step: Duration, ) -> Result { log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: load_bootstrap_cancellable", self.storage_id ); @@ -1174,7 +990,6 @@ impl SimplexDb { let certs_async = self.load_notar_certs_async(); let votes_async = self.load_votes_async(); let pool_state_async = self.load_pool_state_async(); - let payloads_async = self.load_candidate_payloads_async(); // Wait with cancellation support let finalized_raw = finalized_async.wait_cancellable(cancel, step)?; @@ -1182,7 +997,6 @@ impl SimplexDb { let certs_raw = certs_async.wait_cancellable(cancel, step)?; let votes_raw = votes_async.wait_cancellable(cancel, step)?; let pool_state_raw = pool_state_async.wait_cancellable(cancel, step)?; - let payloads_raw = payloads_async.wait_cancellable(cancel, step)?; // Deserialize results let mut finalized_blocks = Vec::with_capacity(finalized_raw.len()); @@ -1190,7 +1004,7 @@ impl SimplexDb { match deserialize_finalized_block(&k, &v) { Ok(r) => finalized_blocks.push(r), Err(e) => { - log::error!(target: TARGET, "SimplexDb: skip bad finalized block: {e}") + log::error!(target: LOG_TARGET, "SimplexDb: skip bad finalized block: {}", e) } } } @@ -1199,7 +1013,7 @@ impl SimplexDb { let finalized_blocks = filter_finalized_chain(finalized_blocks); if finalized_blocks.len() != total_finalized_blocks { log::warn!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: finalized blocks chain filter dropped {} records", self.storage_id, total_finalized_blocks - finalized_blocks.len() @@ -1211,7 +1025,7 @@ impl SimplexDb { match deserialize_candidate_info(&k, &v) { Ok(r) => candidate_infos.push(r), Err(e) => { - log::error!(target: TARGET, "SimplexDb: skip bad candidate info: {e}") + log::error!(target: LOG_TARGET, "SimplexDb: skip bad candidate info: {}", e) } } } @@ -1220,7 +1034,7 @@ impl SimplexDb { for (k, v) in certs_raw { match deserialize_notar_cert(&k, &v) { Ok(r) => notar_certs.push(r), - Err(e) => log::error!(target: TARGET, "SimplexDb: skip bad notar cert: {e}"), + Err(e) => log::error!(target: LOG_TARGET, "SimplexDb: skip bad notar cert: {}", e), } } @@ -1228,18 +1042,10 @@ impl SimplexDb { for (k, v) in votes_raw { match deserialize_vote(&k, &v) { Ok(r) => votes.push(r), - Err(e) => log::error!(target: TARGET, "SimplexDb: skip bad vote: {e}"), + Err(e) => log::error!(target: LOG_TARGET, "SimplexDb: skip bad vote: {}", e), } } - // C++ parity: sort by seqno for deterministic replay order (db.cpp init_votes) - votes.sort_by_key(|r| r.seqno); - - // Initialize next_vote_seqno from max seqno + 1 (C++ parity: db.cpp next_seqno_) - if let Some(max_seqno) = votes.last().map(|r| r.seqno) { - self.next_vote_seqno.store(max_seqno + 1, Ordering::Relaxed); - } - let pool_state = if pool_state_raw.is_empty() { None } else { @@ -1247,41 +1053,23 @@ impl SimplexDb { match deserialize_pool_state(v) { Ok(r) => Some(r), Err(e) => { - log::error!(target: TARGET, "SimplexDb: skip bad pool state: {e}"); + log::error!(target: LOG_TARGET, "SimplexDb: skip bad pool state: {}", e); None } } }; - let mut candidate_payloads = Vec::with_capacity(payloads_raw.len()); - for (k, v) in payloads_raw { - match deserialize_candidate_payload_key(&k) { - Ok(id) => candidate_payloads.push((id, v)), - Err(e) => { - log::error!(target: TARGET, "SimplexDb: skip bad candidate payload key: {e}") - } - } - } - - let bootstrap = Bootstrap { - finalized_blocks, - candidate_infos, - notar_certs, - votes, - pool_state, - candidate_payloads, - }; + let bootstrap = + Bootstrap { finalized_blocks, candidate_infos, notar_certs, votes, pool_state }; log::info!( - target: TARGET, - "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, \ - {} payloads, pool_state={}", + target: LOG_TARGET, + "SimplexDb {}: bootstrap loaded: {} finalized, {} candidates, {} certs, {} votes, pool_state={}", self.storage_id, bootstrap.finalized_blocks.len(), bootstrap.candidate_infos.len(), bootstrap.notar_certs.len(), bootstrap.votes.len(), - bootstrap.candidate_payloads.len(), bootstrap.pool_state.is_some() ); @@ -1295,7 +1083,7 @@ impl SimplexDb { /// Wait for all pending writes to complete. pub fn sync(&self, timeout: Option) -> Result<()> { log::debug!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: sync", self.storage_id ); @@ -1306,7 +1094,7 @@ impl SimplexDb { #[allow(dead_code)] // Used by unit tests in `node/simplex/src/tests/test_database.rs`. pub fn mark_for_destroy(&self) { log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: marked for destroy", self.storage_id ); @@ -1317,7 +1105,7 @@ impl SimplexDb { impl Drop for SimplexDb { fn drop(&mut self) { log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: dropping, syncing pending writes...", self.storage_id ); @@ -1325,14 +1113,14 @@ impl Drop for SimplexDb { // Force sync to flush all pending writes before closing if let Err(e) = self.sync(Some(DEFAULT_SYNC_TIMEOUT)) { log::error!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: sync on drop failed: {}", self.storage_id, e ); } else { log::info!( - target: TARGET, + target: LOG_TARGET, "SimplexDb {}: sync complete", self.storage_id ); diff --git a/src/node/simplex/src/lib.rs b/src/node/simplex/src/lib.rs index 6bb2aae..afc42f8 100644 --- a/src/node/simplex/src/lib.rs +++ b/src/node/simplex/src/lib.rs @@ -27,13 +27,15 @@ //! // 1. Create overlay manager (for production use ADNL, for tests use in-process) //! let overlay_manager = SessionFactory::create_in_process_overlay_manager(4); //! -//! // 2. Create session with options (overlay starts warming up immediately) +//! // 2. Create session with options //! let options = SessionOptions::default(); //! let shard = ton_block::ShardIdent::masterchain(); +//! let initial_block_seqno = 1; // Expected seqno for first block //! let session = SessionFactory::create_session( //! &options, //! &session_id, -//! &shard, +//! &shard, // Shard identifier +//! initial_block_seqno, // First block will have this seqno //! validator_nodes, //! &local_private_key, //! db_path, @@ -42,12 +44,8 @@ //! session_listener, // Weak //! )?; //! -//! // 3. Start consensus processing with initial block seqno -//! let initial_block_seqno = 1; -//! session.start(initial_block_seqno); -//! -//! // 4. Session runs in background threads, callbacks via SessionListener -//! // 5. Stop when done +//! // 3. Session runs in background threads, callbacks via SessionListener +//! // 4. Stop when done //! session.stop(); //! ``` //! @@ -435,6 +433,9 @@ pub struct SessionOptions { /// Collation retry max attempts pub collation_retry_max_attempts: u32, + /// Maximum number of precollated blocks to keep in pipeline + pub max_precollated_blocks: u32, + /// Standstill timeout - if no finalization occurs within this period, /// re-broadcast all our votes for tracked slots /// Default: 10 seconds (matches C++ standstill_timeout_s) @@ -470,11 +471,6 @@ pub struct SessionOptions { /// Default: `FullReplay` pub restart_recommit_strategy: RestartRecommitStrategy, - /// Use QUIC overlay transport instead of ADNL UDP for this session. - /// When true, overlay messages/queries are sent via QUIC streams. - /// Default: false - pub use_quic: bool, - /// Cooldown between repeated health alerts of the same anomaly type. /// Default: 30 seconds pub health_alert_cooldown: Duration, @@ -488,40 +484,6 @@ pub struct SessionOptions { /// Default: 30s (warn), 120s (error) pub health_parent_aging_warning_secs: u64, pub health_parent_aging_error_secs: u64, - - // -- Noncritical params (from simplex_config_v2 HashmapE) -- - // - // These fields are deserialized from on-chain config and passed through, but not yet - // consumed by the Rust session logic. - - // TODO: replace `timeout_increase_factor` / `max_backoff_delay` with these two fields. - // C++ consensus.cpp applies multiplier+cap on window skip (exponential backoff of - // first_block_timeout_). Rust simplex_state.rs uses hardcoded values instead. - pub first_block_timeout_multiplier: f64, - pub first_block_timeout_cap: Duration, - - // TODO: wire into candidate resolver. C++ candidate-resolver.cpp uses these four params - // for exponential-backoff fetch retries with cooldown. - pub candidate_resolve_timeout: Duration, - pub candidate_resolve_timeout_multiplier: f64, - pub candidate_resolve_timeout_cap: Duration, - pub candidate_resolve_cooldown: Duration, - - // TODO: wire into standstill recovery egress shaping. C++ pool.cpp uses this to - // rate-limit bytes/s during standstill_resolution_task. - pub standstill_max_egress_bytes_per_s: u32, - - // TODO: wire into slot/vote acceptance bounds. C++ consensus.cpp and pool.cpp use this - // to reject candidates/votes from too-far-future windows. - pub max_leader_window_desync: u32, - - // TODO: wire into peer ban logic. C++ pool.cpp bans peers with bad vote/cert signatures - // for this duration. - pub bad_signature_ban_duration: Duration, - - // TODO: wire into candidate resolver rate limiting. C++ candidate-resolver.cpp uses a - // 1-second sliding window with this limit per peer for requestCandidate. - pub candidate_resolve_rate_limit: u32, } impl Default for SessionOptions { @@ -540,26 +502,16 @@ impl Default for SessionOptions { validation_retry_timeout: Duration::from_secs(1), collation_retry_timeout: Duration::from_millis(500), collation_retry_max_attempts: 3, + max_precollated_blocks: 0, // Precollation disabled until pipeline reset is implemented standstill_timeout: Duration::from_secs(10), empty_block_mc_lag_threshold: None, wait_for_db_init: false, restart_recommit_strategy: RestartRecommitStrategy::default(), - use_quic: false, health_alert_cooldown: Duration::from_secs(30), health_stall_warning_secs: 15, health_stall_error_secs: 60, health_parent_aging_warning_secs: 30, health_parent_aging_error_secs: 120, - first_block_timeout_multiplier: 1.2, - first_block_timeout_cap: Duration::from_secs(100), - candidate_resolve_timeout: Duration::from_secs(1), - candidate_resolve_timeout_multiplier: 1.2, - candidate_resolve_timeout_cap: Duration::from_secs(10), - candidate_resolve_cooldown: Duration::from_millis(10), - standstill_max_egress_bytes_per_s: 50 << 17, - max_leader_window_desync: 250, - bad_signature_ban_duration: Duration::from_secs(5), - candidate_resolve_rate_limit: 10, } } } @@ -600,6 +552,14 @@ impl SessionOptions { fail!("collation_retry_timeout must be > 0") } + // Precollation is temporarily disabled until pipeline reset triggering is implemented + // TODO: Remove this check when precollation pipeline reset is implemented + if self.max_precollated_blocks != 0 { + fail!( + "max_precollated_blocks must be 0 (precollation disabled until pipeline reset is implemented)" + ) + } + // collation_retry_max_attempts = 0 is valid (no retries) if self.health_alert_cooldown.is_zero() { @@ -622,39 +582,6 @@ impl SessionOptions { fail!("health_parent_aging_error_secs must be >= health_parent_aging_warning_secs") } - // Noncritical params from on-chain config - if !self.first_block_timeout_multiplier.is_finite() - || self.first_block_timeout_multiplier < 1.0 - { - fail!("first_block_timeout_multiplier must be finite and >= 1.0") - } - - if self.first_block_timeout_cap.is_zero() { - fail!("first_block_timeout_cap must be > 0") - } - - if self.candidate_resolve_timeout.is_zero() { - fail!("candidate_resolve_timeout must be > 0") - } - - if !self.candidate_resolve_timeout_multiplier.is_finite() - || self.candidate_resolve_timeout_multiplier < 1.0 - { - fail!("candidate_resolve_timeout_multiplier must be finite and >= 1.0") - } - - if self.candidate_resolve_timeout_cap.is_zero() { - fail!("candidate_resolve_timeout_cap must be > 0") - } - - if self.candidate_resolve_cooldown.is_zero() { - fail!("candidate_resolve_cooldown must be > 0") - } - - if self.bad_signature_ban_duration.is_zero() { - fail!("bad_signature_ban_duration must be > 0") - } - Ok(()) } @@ -789,19 +716,19 @@ impl SessionFactory { /// * `options` - Session configuration options /// * `session_id` - Unique session identifier /// * `shard` - Shard identifier for this session + /// * `initial_block_seqno` - Expected seqno for the first block produced by this session. + /// For merge scenarios, caller should pass max(prev1.seqno, prev2.seqno) + 1. /// * `ids` - List of validator nodes /// * `local_key` - Private key for signing /// * `db_path` - Full database path /// * `overlay_manager` - Network overlay manager /// * `listener` - Session event listener - /// - /// After creation, call `Session::start(initial_block_seqno)` to provide - /// the expected first block seqno and begin consensus processing. #[allow(clippy::too_many_arguments)] pub fn create_session( options: &SessionOptions, session_id: &SessionId, shard: &ShardIdent, + initial_block_seqno: u32, ids: Vec, local_key: &PrivateKey, db_path: String, @@ -812,6 +739,7 @@ impl SessionFactory { options, session_id, shard, + initial_block_seqno, ids, local_key, db_path, diff --git a/src/node/simplex/src/receiver.rs b/src/node/simplex/src/receiver.rs index 8844318..fff02dc 100644 --- a/src/node/simplex/src/receiver.rs +++ b/src/node/simplex/src/receiver.rs @@ -67,7 +67,7 @@ use consensus_common::{ ConsensusCommonFactory, ConsensusNode, ConsensusOverlayPtr, QueryResponseCallback, }; use crossbeam::channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender}; -use rand::{seq::SliceRandom, Rng}; +use rand::seq::SliceRandom; use std::{ collections::HashMap, mem::discriminant, @@ -83,7 +83,6 @@ use ton_api::{ deserialize_boxed, serialize_boxed, tag_from_data, ton::{ consensus::{ - broadcastextra::BroadcastExtra, candidateid::CandidateId, overlayid::OverlayId, simplex::{ @@ -119,17 +118,11 @@ const RECEIVER_WARN_PROCESSING_LATENCY: Duration = Duration::from_millis(1000); const RECEIVER_LATENCY_WARN_DUMP_PERIOD: Duration = Duration::from_millis(2000); // Latency warning dump period const RECEIVER_PROCESSING_PERIOD_MS: u64 = 100; // Processing period (timeout for queue pull) const SHUFFLE_SEND_ORDER_PERIOD: Duration = Duration::from_secs(10); // Period to shuffle send order -const ACTIVE_WEIGHT_RECOMPUTE_PERIOD: Duration = Duration::from_secs(1); // Period to recompute active weight +const ACTIVE_WEIGHT_RECOMPUTE_PERIOD: Duration = Duration::from_secs(10); // Period to recompute active weight // Candidate request constants (block repair / candidate resolver) -// Per-request network query timeout (overlay send_query deadline) -const CANDIDATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); -// C++ parity: candidate-resolver.cpp uses indefinite retry with exponential backoff. -// bus.h defaults: initial=0.5s, multiplier=1.5, max=30.0s -const CANDIDATE_REQUEST_INITIAL_TIMEOUT: Duration = Duration::from_millis(500); -const CANDIDATE_REQUEST_TIMEOUT_MULTIPLIER: f64 = 1.5; -const CANDIDATE_REQUEST_MAX_TIMEOUT: Duration = Duration::from_secs(30); -const CANDIDATE_REQUEST_MAX_RETRIES: u32 = 50; +const CANDIDATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); // Per-request timeout +const CANDIDATE_REQUEST_MAX_RETRIES: u32 = 5; // Maximum retry attempts before giving up // Standstill initial range - used before first finalization calls set_standstill_slots() // After first finalization, SessionProcessor sets the actual range via set_standstill_slots() @@ -380,24 +373,6 @@ pub(crate) trait ReceiverListener: Send + Sync { /// - active_weight: sum of weights for validators with recent activity /// - last_activity: last receive time per validator (None if never received) fn on_activity(&self, active_weight: ValidatorWeight, last_activity: Vec>); - - /// Fallback for RequestCandidate queries when resolver_cache misses. - /// - /// Called by `handle_query()` when `want_candidate=true` but the resolver_cache - /// does not have the candidate data. Delegates to SessionProcessor which can - /// reconstruct the response from its in-memory `candidate_data_cache`, rebuild - /// an empty candidate from `CandidateInfo`, or load persisted payloads from SimplexDB. - /// - /// This achieves parity with C++ `CandidateResolver::try_load_candidate_data_from_db()`. - /// - /// Reference: Alpenglow-Implementation-Plan.md Section 7.14a - fn on_candidate_query_fallback( - &self, - slot: SlotIndex, - block_hash: UInt256, - want_notar: bool, - response_callback: QueryResponseCallback, - ); } /* @@ -437,18 +412,8 @@ struct CandidateRequestState { start_time: SystemTime, /// Number of retry attempts so far retry_count: u32, - /// Current timeout for this request (grows with exponential backoff) - current_timeout: Duration, /// Validator index of the peer being queried source_idx: ValidatorIndex, - /// Accumulated notar bytes from partial responses (C++ CandidateAndCert::merge parity). - /// Peers may return notar-only when the candidate body is unavailable; we cache it - /// here so that when a body-only response arrives later, the merged result is complete. - cached_notar: Option>, - /// Accumulated candidate bytes from partial responses. - /// Peers may return candidate-only while notar is still missing; cache the body so - /// a later notar-only response can complete the merged result. - cached_candidate: Option>, } /* @@ -497,11 +462,6 @@ impl CandidateResolverCache { self.notar_certs.get(&key) } - /// Remove a cached candidate entry (e.g. after deserialization failure) - fn remove_candidate(&mut self, slot: SlotIndex, block_hash: &UInt256) { - self.candidates.remove(&(slot, block_hash.clone())); - } - /// Cleanup old entries for slots less than the given slot fn cleanup_before(&mut self, up_to_slot: SlotIndex) { self.candidates.retain(|(s, _), _| *s >= up_to_slot); @@ -805,11 +765,6 @@ pub(crate) struct ReceiverImpl { shard: ShardIdent, /// Maximum block + collated data size for candidate verification max_candidate_size: usize, - /// Maximum answer size for candidate request queries (network budget). - /// Matches C++ PR #2195: max_block_size + max_collated_data_size + (1 << 20). - max_candidate_query_answer_size: u64, - /// Protocol version from consensus config (determines BOC serialization flags) - proto_version: u32, /// Metrics in_messages_bytes: metrics::Counter, out_messages_bytes: metrics::Counter, @@ -1119,7 +1074,6 @@ impl ReceiverImpl { &block.candidate, &self.shard, self.max_candidate_size, - self.proto_version, ) { Ok(Some(info)) => (Some(info.block_id), Some(info.collated_file_hash)), Ok(None) => (None, None), @@ -1352,9 +1306,15 @@ impl ReceiverImpl { /// Handle incoming query (requestCandidate) /// - /// Reference: C++ CandidateResolver processes requestCandidate queries. - /// On cache miss, delegates to SessionProcessor via `on_candidate_query_fallback` - /// which can reconstruct the response from in-memory or DB-backed storage. + /// Reference: C++ CandidateResolver processes requestCandidate queries + /// + /// TODO (INT-1): Add DB fallback when candidate not in resolver_cache. + /// If resolver_cache.get_candidate() returns None, call SessionProcessor's + /// notify_get_approved_candidate() to load from validator's persistent storage. + /// This handles the case where this node approved a candidate but restarted before + /// caching it, and now a peer is requesting it. + /// See: validator-session/src/session_processor.rs line 1620 (process_query) + /// Reference: Alpenglow-Implementation-Plan.md Section 7.14a fn handle_query( &mut self, _adnl_id: PublicKeyHash, @@ -1362,7 +1322,6 @@ impl ReceiverImpl { response_callback: QueryResponseCallback, ) { check_execution_time!(50_000); - let request_data = data.data(); let object = match deserialize_boxed(request_data) { Ok(object) => object, @@ -1393,39 +1352,15 @@ impl ReceiverImpl { want_notar ); + // Look up cached data from local cache let candidate_bytes = if want_candidate { - self.resolver_cache.get_candidate(slot, &block_hash).cloned() + self.resolver_cache + .get_candidate(slot, &block_hash) + .cloned() + .unwrap_or_default() } else { - None + Vec::new() }; - - let cache_miss = want_candidate && candidate_bytes.is_none(); - - if cache_miss { - if let Some(listener) = self.listener.upgrade() { - log::debug!( - "SimplexReceiver {}: requestCandidate cache MISS \ - for slot={slot} hash={}, delegating to SessionProcessor", - self.session_id.to_hex_string(), - &block_hash.to_hex_string()[..8], - ); - listener.on_candidate_query_fallback( - slot, - block_hash, - want_notar, - response_callback, - ); - } else { - log::warn!( - "SimplexReceiver {}: requestCandidate cache MISS but listener dropped", - self.session_id.to_hex_string(), - ); - response_callback(Err(error!("Session listener dropped"))); - } - return; - } - - let candidate_bytes = candidate_bytes.unwrap_or_default(); let notar_bytes = if want_notar { self.resolver_cache .get_notar_cert(slot, &block_hash) @@ -1612,14 +1547,8 @@ impl ReceiverImpl { self.candidate_requests_counter.increment(1); // Create request state - let request_state = CandidateRequestState { - start_time: SystemTime::now(), - retry_count: 0, - current_timeout: CANDIDATE_REQUEST_INITIAL_TIMEOUT, - source_idx, - cached_notar: None, - cached_candidate: None, - }; + let request_state = + CandidateRequestState { start_time: SystemTime::now(), retry_count: 0, source_idx }; self.pending_requests.insert(key.clone(), request_state); // Send the query @@ -1629,7 +1558,7 @@ impl ReceiverImpl { let slot_clone = slot; let hash_clone = block_hash.clone(); self.post_delayed_action( - SystemTime::now() + CANDIDATE_REQUEST_INITIAL_TIMEOUT, + SystemTime::now() + CANDIDATE_REQUEST_TIMEOUT, move |receiver: &mut ReceiverImpl| { receiver.handle_candidate_request_timeout(slot_clone, hash_clone); }, @@ -1641,6 +1570,8 @@ impl ReceiverImpl { /// This reduces repeated queries to the same peer across retries, improving /// convergence when only a subset of peers have the requested candidate. fn select_peer_for_candidate_request(&self, exclude: Option) -> Option { + use rand::Rng; + let len = self.send_order.len(); if len <= 1 { return None; // Only self or empty @@ -1732,12 +1663,15 @@ impl ReceiverImpl { let session_id = self.session_id.clone(); let task_queues = self.get_task_queues(); - // Send query via RLDP overlay with explicit response size budget (C++ PR #2195 parity) - let timeout_deadline = SystemTime::now() + CANDIDATE_REQUEST_TIMEOUT; - self.overlay.send_query_via_rldp( - peer_adnl_id, - query_name.to_string(), + // Send query via overlay + self.overlay.send_query( + &peer_adnl_id, + &self.local_adnl_id, + query_name, + CANDIDATE_REQUEST_TIMEOUT, + &payload, Box::new(move |result: Result| { + // Post response handling to receiver thread task_queues.post_closure(Box::new(move |receiver: &mut ReceiverImpl| { receiver.handle_candidate_response( slot_for_cb, @@ -1747,10 +1681,6 @@ impl ReceiverImpl { ); })); }), - timeout_deadline, - payload, - self.max_candidate_query_answer_size, - true, // RLDPv2 ); } @@ -1762,57 +1692,6 @@ impl ReceiverImpl { self.task_queues.clone() } - /// Merge partial `requestCandidate` response pieces with pending-request state. - /// - /// C++ parity: - /// - cache partial candidate/notar parts as they arrive; - /// - merge with previously cached parts; - /// - completion is checked by caller via non-empty merged candidate+notar. - fn merge_candidate_response_parts( - resolver_cache: &mut CandidateResolverCache, - pending_state: Option<&mut CandidateRequestState>, - slot: SlotIndex, - block_hash: &UInt256, - candidate_bytes: &[u8], - notar_bytes: &[u8], - ) -> (Vec, Vec) { - let candidate_vec = candidate_bytes.to_vec(); - let notar_vec = notar_bytes.to_vec(); - - if !candidate_vec.is_empty() { - resolver_cache.cache_candidate(slot, block_hash.clone(), candidate_vec.clone()); - } - if !notar_vec.is_empty() { - resolver_cache.cache_notar_cert(slot, block_hash.clone(), notar_vec.clone()); - } - - if let Some(state) = pending_state { - if !candidate_vec.is_empty() { - state.cached_candidate = Some(candidate_vec); - } - if !notar_vec.is_empty() { - state.cached_notar = Some(notar_vec.clone()); - } - - let merged_candidate = state.cached_candidate.clone().unwrap_or_default(); - let merged_notar = if !notar_vec.is_empty() { - notar_vec - } else if let Some(cached_notar) = state.cached_notar.clone() { - cached_notar - } else { - resolver_cache.get_notar_cert(slot, block_hash).cloned().unwrap_or_default() - }; - return (merged_candidate, merged_notar); - } - - let merged_notar = if !notar_vec.is_empty() { - notar_vec - } else { - resolver_cache.get_notar_cert(slot, block_hash).cloned().unwrap_or_default() - }; - (candidate_bytes.to_vec(), merged_notar) - } - /// Handle response from requestCandidate query fn handle_candidate_response( &mut self, @@ -1865,68 +1744,33 @@ impl ReceiverImpl { source_idx ); - // C++ CandidateAndCert::merge parity: cache both partial fields and - // complete only when the merged result has both candidate+notar. - let (merged_candidate_bytes, merged_notar) = - Self::merge_candidate_response_parts( - &mut self.resolver_cache, - self.pending_requests.get_mut(&key), - slot, - &block_hash, - candidate_bytes, - notar_bytes, - ); - - // If body is still missing after merge, keep pending for retry. - if merged_candidate_bytes.is_empty() { - log::debug!( - "SimplexReceiver {}: body-empty response for slot={} hash={} \ - (notar_len={}), will retry on timeout", + // Check candidate before removing from pending + // If empty, leave request pending so timeout handler can retry + if candidate_bytes.is_empty() { + log::warn!( + "SimplexReceiver {}: empty candidate in response for slot={} hash={}, will retry on timeout", self.session_id.to_hex_string(), slot, - &block_hash.to_hex_string()[..8], - notar_bytes.len(), + &block_hash.to_hex_string()[..8] ); return; } - if merged_notar.is_empty() { - log::debug!( - "SimplexReceiver {}: candidate-only partial response for \ - slot={} hash={}, keep pending until notar arrives", - self.session_id.to_hex_string(), - slot, - &block_hash.to_hex_string()[..8], - ); - return; - } + // Remove from pending - we have all required data + self.pending_requests.remove(&key); - let candidate = match deserialize_boxed( - merged_candidate_bytes.as_slice(), - ) { + let candidate = match deserialize_boxed(candidate_bytes) { Ok(msg) => match msg.downcast::() { Ok(c) => c, Err(_) => { - // Drop cached candidate so retry can fetch a fresh body; - // also purge resolver_cache to avoid serving bad data to peers. - self.resolver_cache.remove_candidate(slot, &block_hash); - if let Some(state) = self.pending_requests.get_mut(&key) { - state.cached_candidate = None; - } log::warn!( - "SimplexReceiver {}: unexpected candidate type in response", - self.session_id.to_hex_string() - ); + "SimplexReceiver {}: unexpected candidate type in response", + self.session_id.to_hex_string() + ); return; } }, Err(e) => { - // Drop cached candidate so retry can fetch a fresh body; - // also purge resolver_cache to avoid serving bad data to peers. - self.resolver_cache.remove_candidate(slot, &block_hash); - if let Some(state) = self.pending_requests.get_mut(&key) { - state.cached_candidate = None; - } log::warn!( "SimplexReceiver {}: failed to deserialize candidate: {}", self.session_id.to_hex_string(), @@ -1936,17 +1780,30 @@ impl ReceiverImpl { } }; - // Remove from pending only when merged candidate+notar is complete. - self.pending_requests.remove(&key); + // Cache candidate for query responses (in case others ask us) + self.resolver_cache.cache_candidate( + slot, + block_hash.clone(), + candidate_bytes.to_vec(), + ); - // Call listener with source_idx, using merged notar. - if let Some(listener) = self.listener.upgrade() { - listener.on_candidate_received( - source_idx, - candidate, - Some(merged_notar), + // Cache notar bytes for query responses (C++ CandidateResolver parity). + // This ensures future `requestCandidate(want_notar=true)` queries can be served immediately. + let notar_vec = notar_bytes.to_vec(); + if !notar_vec.is_empty() { + self.resolver_cache.cache_notar_cert( + slot, + block_hash.clone(), + notar_vec.clone(), ); } + + // Call listener with source_idx + let notar_cert = + if notar_vec.is_empty() { None } else { Some(notar_vec) }; + if let Some(listener) = self.listener.upgrade() { + listener.on_candidate_received(source_idx, candidate, notar_cert); + } } else { log::warn!( "SimplexReceiver {}: unexpected response type for requestCandidate", @@ -1976,16 +1833,15 @@ impl ReceiverImpl { } } - /// Handle request timeout - retry with next peer using exponential backoff. - /// C++ parity: candidate-resolver.cpp retries indefinitely until resolved. + /// Handle request timeout - retry with next peer or give up fn handle_candidate_request_timeout(&mut self, slot: SlotIndex, block_hash: UInt256) { let key = (slot, block_hash.clone()); // Check if request is still pending and get current state - let (retry_count, prev_source_idx, current_timeout) = match self.pending_requests.get(&key) - { - Some(state) => (state.retry_count, state.source_idx.value(), state.current_timeout), + let (retry_count, prev_source_idx) = match self.pending_requests.get(&key) { + Some(state) => (state.retry_count, state.source_idx.value()), None => { + // Request was fulfilled or cancelled log::trace!( "SimplexReceiver {}: handle_candidate_request_timeout slot={} hash={} - request already fulfilled or cancelled", self.session_id.to_hex_string(), @@ -1997,48 +1853,34 @@ impl ReceiverImpl { }; self.candidate_request_timeouts_counter.increment(1); + // Check max retries let new_retry_count = retry_count + 1; - if new_retry_count % CANDIDATE_REQUEST_MAX_RETRIES == 0 { + if new_retry_count >= CANDIDATE_REQUEST_MAX_RETRIES { + self.candidate_request_giveups_counter.increment(1); + self.health_counters.candidate_giveups.fetch_add(1, Ordering::Relaxed); log::warn!( - "SimplexReceiver {}: candidate request slot={slot} hash={} \ - still pending after {new_retry_count} retries, continuing", + "SimplexReceiver {}: giving up on candidate request slot={} hash={} after {} retries", self.session_id.to_hex_string(), - &block_hash.to_hex_string()[..8] + slot, + &block_hash.to_hex_string()[..8], + new_retry_count ); + self.pending_requests.remove(&key); + return; } - // Exponential backoff: timeout * multiplier, capped at max - let next_timeout_ms = - (current_timeout.as_millis() as f64 * CANDIDATE_REQUEST_TIMEOUT_MULTIPLIER) as u128; - let next_timeout = Duration::from_millis( - next_timeout_ms.min(CANDIDATE_REQUEST_MAX_TIMEOUT.as_millis()) as u64, - ); - - // Select next peer (random, excluding previous) + // Select next peer (random) let next_source_idx = match self.select_peer_for_candidate_request(Some(prev_source_idx)) { Some(idx) => idx, None => { - // No peers available right now -- schedule a retry after backoff anyway, - // peers may come back online. - self.candidate_request_retries_counter.increment(1); + self.candidate_request_giveups_counter.increment(1); log::warn!( - "SimplexReceiver {}: no peers for candidate request slot={slot} hash={}, \ - will retry in {next_timeout:?}", + "SimplexReceiver {}: no more peers for candidate request slot={} hash={}", self.session_id.to_hex_string(), + slot, &block_hash.to_hex_string()[..8] ); - if let Some(state) = self.pending_requests.get_mut(&key) { - state.retry_count = new_retry_count; - state.current_timeout = next_timeout; - } - let slot_clone = slot; - let hash_clone = block_hash; - self.post_delayed_action( - SystemTime::now() + next_timeout, - move |receiver: &mut ReceiverImpl| { - receiver.handle_candidate_request_timeout(slot_clone, hash_clone); - }, - ); + self.pending_requests.remove(&key); return; } }; @@ -2048,24 +1890,25 @@ impl ReceiverImpl { if let Some(state) = self.pending_requests.get_mut(&key) { state.retry_count = new_retry_count; state.source_idx = next_source_idx; - state.current_timeout = next_timeout; } log::trace!( - "SimplexReceiver {}: retrying candidate request slot={slot} hash={} \ - to validator {next_source_idx} (retry {new_retry_count}, timeout {next_timeout:?})", + "SimplexReceiver {}: retrying candidate request slot={} hash={} to validator {} (retry {})", self.session_id.to_hex_string(), - &block_hash.to_hex_string()[..8] + slot, + &block_hash.to_hex_string()[..8], + next_source_idx, + new_retry_count ); // Send to next peer self.send_candidate_request(slot, block_hash.clone(), next_source_idx); - // Schedule next timeout with backoff + // Schedule next timeout let slot_clone = slot; let hash_clone = block_hash; self.post_delayed_action( - SystemTime::now() + next_timeout, + SystemTime::now() + CANDIDATE_REQUEST_TIMEOUT, move |receiver: &mut ReceiverImpl| { receiver.handle_candidate_request_timeout(slot_clone, hash_clone); }, @@ -2202,7 +2045,6 @@ impl ReceiverImpl { &block.candidate, &self.shard, self.max_candidate_size, - self.proto_version, ) { Ok(Some(info)) => (Some(info.block_id), Some(info.collated_file_hash)), Ok(None) => (None, None), @@ -2280,17 +2122,8 @@ impl ReceiverImpl { stats.last_send_time = Some(SystemTime::now()); } - // Build consensus.broadcastExtra with slot info (required for C++ interop) - let broadcast_extra = BroadcastExtra { slot: slot as i32 }; - let extra = consensus_common::serialize_tl_boxed_object!(&broadcast_extra.into_boxed()); - // Send via overlay FEC broadcast - self.overlay.send_broadcast_fec_ex( - &self.local_adnl_id, - self.local_key.id(), - payload, - Some(extra), - ); + self.overlay.send_broadcast_fec_ex(&self.local_adnl_id, self.local_key.id(), payload); } /// Shuffle send order for fairness @@ -2400,6 +2233,7 @@ impl ReceiverImpl { /// Get slot from vote (boxed enum version) fn get_vote_slot(vote: &TlVoteBoxed) -> u32 { + use UnsignedVote; match vote.vote() { UnsignedVote::Consensus_Simplex_NotarizeVote(v) => *v.id.slot() as u32, UnsignedVote::Consensus_Simplex_FinalizeVote(v) => *v.id.slot() as u32, @@ -2409,6 +2243,7 @@ impl ReceiverImpl { /// Get slot from inner vote struct (avoids clone+box overhead) fn get_vote_slot_from_inner(vote: &TlVote) -> u32 { + use UnsignedVote; match &vote.vote { UnsignedVote::Consensus_Simplex_NotarizeVote(v) => *v.id.slot() as u32, UnsignedVote::Consensus_Simplex_FinalizeVote(v) => *v.id.slot() as u32, @@ -2531,6 +2366,7 @@ impl ReceiverImpl { // if (notarize_.has_value() && !bundle.notarize_.has_value()) { ... } // if (skip_.has_value() && !bundle.skip_.has_value()) { ... } // if (finalize_.has_value() && !bundle.finalize_.has_value()) { ... } + use UnsignedVote; let votes_to_rebroadcast: Vec<_> = self .our_votes .iter() @@ -2565,8 +2401,7 @@ impl ReceiverImpl { self.standstill_votes_rebroadcast_counter.increment(votes_to_rebroadcast.len() as u64); log::warn!( - "SimplexReceiver {}: Standstill detected, re-broadcasting {} certs + {} votes \ - (range [{}, {}))", + "SimplexReceiver {}: Standstill detected, re-broadcasting {} certs + {} votes (range [{}, {}))", self.session_id.to_hex_string(), cert_count, votes_to_rebroadcast.len(), @@ -3146,15 +2981,12 @@ impl ReceiverWrapper { session_id: SessionId, shard: &ShardIdent, max_candidate_size: usize, - max_candidate_query_answer_size: u64, - proto_version: u32, ids: &[SessionNode], local_key: &PrivateKey, overlay_manager: ConsensusOverlayManagerPtr, listener: ReceiverListenerPtr, standstill_timeout: Duration, panicked_flag: Arc, - use_quic: bool, health_counters: Arc, ) -> Result { log::info!( @@ -3237,11 +3069,7 @@ impl ReceiverWrapper { .collect(); // Start overlay - let transport_type = if use_quic { - consensus_common::OverlayTransportType::SimplexQuic - } else { - consensus_common::OverlayTransportType::Simplex - }; + let transport_type = consensus_common::OverlayTransportType::Simplex; let overlay = overlay_manager.start_overlay( local_key, &overlay_short_id, @@ -3330,8 +3158,6 @@ impl ReceiverWrapper { dedup_votes: HashMap::new(), shard: shard_clone, max_candidate_size, - max_candidate_query_answer_size, - proto_version, in_messages_bytes: in_messages_bytes_clone, out_messages_bytes: out_messages_bytes_clone, in_broadcasts_bytes: in_broadcasts_bytes_clone, diff --git a/src/node/simplex/src/session.rs b/src/node/simplex/src/session.rs index 004789e..1b37b09 100644 --- a/src/node/simplex/src/session.rs +++ b/src/node/simplex/src/session.rs @@ -69,10 +69,7 @@ use crate::{ }; use consensus_common::{ check_execution_time, - utils::{ - add_compute_percentage_metric, add_compute_relative_metric, add_compute_result_metric, - get_elapsed_time, MetricsDumper, - }, + utils::{get_elapsed_time, MetricsDumper}, }; use crossbeam::channel::{bounded, Sender}; use std::{ @@ -82,7 +79,7 @@ use std::{ collections::BTreeMap, fmt, panic, sync::{ - atomic::{AtomicBool, AtomicU32, Ordering}, + atomic::{AtomicBool, Ordering}, Arc, }, thread, @@ -92,7 +89,7 @@ use ton_api::ton::consensus::{ simplex::{Certificate, Vote}, CandidateData, }; -use ton_block::{error, Error, Result, ShardIdent, UInt256}; +use ton_block::{error, Error, Result, ShardIdent}; /* Constants @@ -152,24 +149,6 @@ impl ReceiverListener for ReceiverListenerImpl { processor.on_certificate(source_idx, certificate); })); } - - /// Handle RequestCandidate cache miss by delegating to SessionProcessor - fn on_candidate_query_fallback( - &self, - slot: crate::block::SlotIndex, - block_hash: UInt256, - want_notar: bool, - response_callback: consensus_common::QueryResponseCallback, - ) { - self.task_queue.post_closure(Box::new(move |processor: &mut SessionProcessor| { - processor.handle_candidate_query_fallback( - slot, - block_hash, - want_notar, - response_callback, - ); - })); - } } impl ReceiverListenerImpl { @@ -344,15 +323,6 @@ pub(crate) struct SessionImpl { stop_flag: Arc, /// Indicates database should be destroyed on stop destroy_db_flag: Arc, - /// Atomic flag: main_loop should begin active FSM processing. - /// Set by `start(seqno)`. The overlay is created at `create()` time and - /// warms up while main_loop polls this flag, so peers are connected - /// before the first_block_timeout clock starts ticking. - start_flag: Arc, - /// Initial block seqno, provided by `start(seqno)`. - /// Read by main_loop after start_flag is set, before SessionDescription - /// creation. - deferred_initial_seqno: Arc, /// Atomic flag to indicate main processing thread has stopped main_processing_thread_stopped: Arc, /// Atomic flag to indicate callbacks processing thread has stopped @@ -374,16 +344,6 @@ pub(crate) struct SessionImpl { } impl ConsensusSession for SessionImpl { - fn start(&self, initial_block_seqno: u32) { - log::info!( - "SimplexSession {}: start(seqno={}) called โ€” storing seqno and unblocking main loop", - self.session_id.to_hex_string(), - initial_block_seqno - ); - self.deferred_initial_seqno.store(initial_block_seqno, Ordering::Release); - self.start_flag.store(true, Ordering::Release); - } - fn stop(&self) { self.stop_async(); self.stop_impl(false); @@ -483,21 +443,19 @@ impl SessionImpl { should_stop_flag: Arc, is_stopped_flag: Arc, destroy_db_flag: Arc, - start_flag: Arc, - deferred_initial_seqno: Arc, panicked_flag: Arc, task_queue: TaskQueuePtr, callbacks_task_queue: CallbackTaskQueuePtr, options: SessionOptions, session_id: SessionId, shard: ShardIdent, + initial_block_seqno: u32, ids: Vec, local_key: PrivateKey, listener: SessionListenerPtr, overlay_manager: ConsensusOverlayManagerPtr, receiver_listener: ReceiverListenerPtr, max_candidate_size: usize, - max_candidate_query_answer_size: u64, db_path: String, session_activity_node: ActivityNodePtr, session_creation_time: SystemTime, @@ -514,7 +472,7 @@ impl SessionImpl { // Signal thread start based on wait_for_db_init option: // - If false: send Ok(()) now (non-blocking for caller) // - If true: wait until full initialization completes - let init_signaled = Cell::new(false); + let mut init_signaled = false; if !options.wait_for_db_init { if init_result_sender.send(Ok(())).is_err() { log::warn!( @@ -524,7 +482,7 @@ impl SessionImpl { is_stopped_flag.store(true, Ordering::Release); return; } - init_signaled.set(true); + init_signaled = true; } // Configure metrics @@ -543,7 +501,7 @@ impl SessionImpl { let fail_startup = |err: Error, ctx: &str| { log::error!("Session {} {}: {:?}", session_id.to_hex_string(), ctx, err); startup_errors.set(startup_errors.get().saturating_add(1)); - if !init_signaled.get() { + if !init_signaled { let _ = init_result_sender.send(Err(err)); } is_stopped_flag.store(true, Ordering::Release); @@ -562,7 +520,7 @@ impl SessionImpl { // Check if we should stop before loading bootstrap if should_stop_flag.load(Ordering::Relaxed) { log::info!("Session {} stopping before bootstrap load", session_id.to_hex_string()); - if !init_signaled.get() { + if !init_signaled { let _ = init_result_sender.send(Err(error!("Session stopped before bootstrap load"))); } @@ -580,15 +538,12 @@ impl SessionImpl { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - options.proto_version, &ids, &local_key, overlay_manager.clone(), receiver_listener, options.standstill_timeout, panicked_flag.clone(), - options.use_quic, health_counters.clone(), ) { Ok(r) => r, @@ -607,7 +562,7 @@ impl SessionImpl { // Check if this was a cancellation if should_stop_flag.load(Ordering::Relaxed) { log::info!("Session {} bootstrap load cancelled", session_id.to_hex_string()); - if !init_signaled.get() { + if !init_signaled { let _ = init_result_sender .send(Err(error!("Session bootstrap load cancelled"))); } @@ -630,49 +585,6 @@ impl SessionImpl { bootstrap.notar_certs.len(), ); - // Signal init complete before the start gate โ€” the overlay and DB are - // fully ready. The caller (create()) can return and later call - // start(seqno) to unblock the FSM. - if !init_signaled.get() { - if init_result_sender.send(Ok(())).is_err() { - log::warn!( - "SimplexSession {} main loop: failed to send init result (receiver dropped)", - session_id.to_hex_string() - ); - } - init_signaled.set(true); - } - - // Wait for start(seqno) before creating SessionDescription. - // The overlay is already registered and warming up peer connections - // while we poll here, so by the time start() is called the overlay - // should have established connectivity -- preventing premature - // first_block_timeout skips that occur when the FSM starts before - // any peers are reachable. - if !start_flag.load(Ordering::Acquire) { - log::info!( - "SimplexSession {} waiting for start(seqno) signal (overlay warming up)...", - session_id.to_hex_string() - ); - while !start_flag.load(Ordering::Acquire) { - if should_stop_flag.load(Ordering::Relaxed) { - log::info!( - "SimplexSession {} stopped while waiting for start()", - session_id.to_hex_string() - ); - is_stopped_flag.store(true, Ordering::Release); - return; - } - thread::sleep(Duration::from_millis(10)); - } - } - let initial_block_seqno = deferred_initial_seqno.load(Ordering::Acquire); - log::info!( - "SimplexSession {} start(seqno={}) received, creating SessionDescription", - session_id.to_hex_string(), - initial_block_seqno - ); - // Phase 4a: Create session description (immutable session configuration) let description = match SessionDescription::new( &options, @@ -750,6 +662,16 @@ impl SessionImpl { } } + // Signal full initialization complete (if wait_for_db_init is true) + if !init_signaled { + if init_result_sender.send(Ok(())).is_err() { + log::warn!( + "SimplexSession {} main loop: failed to send init result after full init (receiver dropped)", + session_id.to_hex_string() + ); + } + } + // Create metrics dumper for computed/derivative metrics let mut metrics_dumper = Self::create_metrics_dumper(); @@ -762,10 +684,6 @@ impl SessionImpl { let mut next_profiling_dump_time = next_metrics_dump_time; let mut next_health_check_time = next_metrics_dump_time; - // Arm FSM skip timeouts now that overlay warmup and bootstrap - // recovery are complete. Matches C++ Start event timing. - processor.start(); - loop { { session_activity_node.tick(); @@ -942,6 +860,10 @@ impl SessionImpl { /// Configures derivative metrics (rate of change), percentage metrics, /// and result status metrics similar to validator-session. fn create_metrics_dumper() -> MetricsDumper { + use consensus_common::utils::{ + add_compute_percentage_metric, add_compute_relative_metric, add_compute_result_metric, + }; + let mut metrics_dumper = MetricsDumper::new(); // Derivative metrics for loop counters (rate per second) @@ -1117,6 +1039,7 @@ impl SessionImpl { options: &SessionOptions, session_id: &SessionId, shard: &ShardIdent, + initial_block_seqno: u32, ids: Vec, local_key: &PrivateKey, db_path: String, @@ -1124,9 +1047,10 @@ impl SessionImpl { listener: SessionListenerPtr, ) -> Result { log::info!( - "Creating SimplexSession (session_id is {}, shard={}, nodes_count={}, db_path={})", + "Creating SimplexSession (session_id is {}, shard={}, initial_seqno={}, nodes_count={}, db_path={})", session_id.to_hex_string(), shard, + initial_block_seqno, ids.len(), db_path ); @@ -1151,8 +1075,6 @@ impl SessionImpl { // Create thread synchronization flags let stop_flag = Arc::new(AtomicBool::new(false)); let destroy_db_flag = Arc::new(AtomicBool::new(false)); - let start_flag = Arc::new(AtomicBool::new(false)); - let deferred_initial_seqno = Arc::new(AtomicU32::new(0)); let main_processing_thread_stopped = Arc::new(AtomicBool::new(false)); let callbacks_processing_thread_stopped = Arc::new(AtomicBool::new(false)); let panicked_flag = Arc::new(AtomicBool::new(false)); @@ -1163,18 +1085,13 @@ impl SessionImpl { ReceiverListenerImpl::create(main_task_queue.clone(), session_id.clone()); let receiver_listener_weak: ReceiverListenerPtr = Arc::downgrade(&receiver_listener); - // Compute max candidate size for receiver (local validation guard, +1KB slack) + // Compute max candidate size for receiver let max_candidate_size = options.max_block_size + options.max_collated_data_size + 1024; - // Network response budget for requestCandidate queries (C++ PR #2195 parity: +1MB) - let max_candidate_query_answer_size: u64 = - (options.max_block_size + options.max_collated_data_size) as u64 + (1 << 20); // Create session (receiver is created in main_loop after bootstrap loading) let session = SessionImpl { stop_flag: stop_flag.clone(), destroy_db_flag: destroy_db_flag.clone(), - start_flag: start_flag.clone(), - deferred_initial_seqno: deferred_initial_seqno.clone(), main_processing_thread_stopped: main_processing_thread_stopped.clone(), callbacks_processing_thread_stopped: callbacks_processing_thread_stopped.clone(), panicked_flag: panicked_flag.clone(), @@ -1217,21 +1134,19 @@ impl SessionImpl { stop_flag_for_main_loop, main_processing_thread_stopped, destroy_db_flag, - start_flag, - deferred_initial_seqno, panicked_flag_for_main_loop, main_task_queue, callbacks_task_queue, options_clone, session_id_clone, shard_clone, + initial_block_seqno, ids, local_key_clone, listener, overlay_manager, receiver_listener_weak, max_candidate_size, - max_candidate_query_answer_size, db_path, session_activity_node, session_creation_time, diff --git a/src/node/simplex/src/session_processor.rs b/src/node/simplex/src/session_processor.rs index 45d7df7..77029f6 100644 --- a/src/node/simplex/src/session_processor.rs +++ b/src/node/simplex/src/session_processor.rs @@ -110,20 +110,19 @@ use ton_api::{ candidateid::CandidateId, candidateparent::CandidateParent, simplex::{ - candidateandcert::CandidateAndCert, vote::Vote as TlVote, - votesignature::VoteSignature as TlVoteSignature, + vote::Vote as TlVote, votesignature::VoteSignature as TlVoteSignature, votesignatureset::VoteSignatureSet, Certificate, UnsignedVote, Vote as TlVoteBoxed, VoteSignatureSet as VoteSignatureSetBoxed, }, CandidateData, CandidateHashData, CandidateParent as CandidateParentBoxed, }, - validator_session::candidate::CompressedCandidate, + validator_session::candidate::Candidate as TlCandidate, }, IntoBoxed, }; use ton_block::{ error, fail, sha256_digest, BlockIdExt, BlockSignaturesPure, BlockSignaturesSimplex, - BlockSignaturesVariant, BocFlags, CryptoSignature, CryptoSignaturePair, Deserializable, Error, + BlockSignaturesVariant, CryptoSignature, CryptoSignaturePair, Deserializable, Error, HashmapType, KeyId, Result, UInt256, ValidatorBaseInfo, }; @@ -196,7 +195,6 @@ pub(crate) struct HealthAlertState { last_parent_aging_warn: SystemTime, last_progress_warn: SystemTime, last_standstill_warn: SystemTime, - last_isolation_warn: SystemTime, prev_candidate_giveups: u64, prev_cert_verify_fails: u64, prev_last_finalized_slot: f64, @@ -217,7 +215,6 @@ impl HealthAlertState { last_parent_aging_warn: warn_base, last_progress_warn: warn_base, last_standstill_warn: warn_base, - last_isolation_warn: warn_base, prev_candidate_giveups: 0, prev_cert_verify_fails: 0, prev_last_finalized_slot: 0.0, @@ -315,29 +312,6 @@ struct PrecollatedBlock { /// Map of slot -> precollated block type PrecollatedBlockMap = HashMap; -/// Window-local chain head for candidate chaining (C++ block-producer.cpp parity). -/// -/// In C++, `generate_candidates()` carries mutable `parent` and `state` across slots -/// within a single leader window, so slot N+1 chains off slot N's locally generated -/// candidate without waiting for notarization. This struct tracks the same chain head -/// on the Rust side: after `generated_block()` completes for a slot, we record the -/// produced candidate's identity here so that `precollate_block()` for the next slot -/// in the same window can use it immediately as an explicit parent. -/// -/// Reset when the leader window changes or the progress cursor jumps. -#[derive(Clone, Debug)] -#[allow(dead_code)] -struct LocalChainHead { - /// Window this chain head belongs to - window: WindowIndex, - /// Slot of the last locally generated candidate - slot: SlotIndex, - /// Candidate parent info (slot + candidate-id hash) for the next slot - parent_info: crate::block::CandidateParentInfo, - /// Resolved BlockIdExt of the generated candidate (for seqno derivation and explicit parent hint) - block_id: BlockIdExt, -} - /* Collation result @@ -676,9 +650,6 @@ pub(crate) struct SessionProcessor { delayed_actions: Vec, /// SimplexState FSM - core consensus state machine simplex_state: SimplexState, - /// Slots for which "missing body" has already been logged (throttle). - /// Prevents multi-million-line log floods when a slot body never arrives. - missing_body_logged: HashSet, /* Collation state (session-level only) @@ -697,16 +668,6 @@ pub(crate) struct SessionProcessor { /// precollated block is consumed). Checked at the top of `check_collation`. /// Reference: C++ block-producer.cpp coro_sleep(target_time) earliest_collation_time: Option, - /// Window-local chain head for candidate chaining across slots in the same - /// leader window (C++ block-producer.cpp parity). Updated synchronously in - /// `generated_block()` so that `precollate_block()` can chain the next slot - /// without waiting for the async `on_candidate_received` self-loop. - local_chain_head: Option, - /// Synchronous cache of locally generated parent metadata, keyed by - /// `RawCandidateId`. Populated in `generated_block()` *before* the async - /// `on_candidate_received` self-loop, so `resolve_parent_block_id()` can - /// find the parent immediately for chained precollation. - generated_parent_cache: HashMap, /* Validation state @@ -732,12 +693,6 @@ pub(crate) struct SessionProcessor { validated_candidates: VecDeque, /// All received block candidates: RawCandidateId(slot, candidate_id_hash) โ†’ candidate data received_candidates: HashMap, - /// Serialized CandidateData bytes cache for RequestCandidate query fallback. - /// - /// Populated in `on_candidate_received()` by re-serializing the TL `CandidateData` object. - /// Used by `handle_candidate_query_fallback()` when the receiver's `resolver_cache` misses. - /// This provides C++ parity with `CandidateResolver::try_load_candidate_data_from_db()`. - candidate_data_cache: HashMap>, /* Metrics @@ -782,8 +737,6 @@ pub(crate) struct SessionProcessor { batch_commit_counter: metrics::Counter, /// Histogram for batch commit sizes (number of blocks committed at once) batch_commit_size_histogram: metrics::Histogram, - /// Gauge for finalized-but-uncommitted journal size (commit lag indicator) - finalized_uncommitted_gauge: metrics::Gauge, /* Error tracking for SessionStats @@ -828,11 +781,11 @@ pub(crate) struct SessionProcessor { Block SeqNo Tracking Tracks expected blockchain sequence number for next block */ - /// Last committed block seqno - updated in commit_single_block(). - /// Used for strict commit sequencing and validation checks. + /// Last committed block seqno - updated from BlockFinalizedEvent. + /// Used by `should_generate_empty_block()`, commit gating, and validation checks. last_committed_seqno: Option, - /// Last committed block slot - updated in commit_single_block() + /// Last committed block slot - updated from BlockFinalizedEvent /// Used to retrieve parent BlockIdExt for empty block generation last_committed_slot: Option, /// Last committed non-empty block id (parent for empty blocks) @@ -849,17 +802,6 @@ pub(crate) struct SessionProcessor { /// Reference: C++ block-producer.cpp `is_before_split()` + `should_generate_empty_block()` last_committed_before_split: bool, - /// Last consensus-finalized seqno - tracks the highest seqno of a block committed - /// with FinalCert (is_final=true) in this session. - /// - /// C++ parity: mirrors `last_consensus_finalized_seqno_` in block-producer.cpp, which - /// advances on FinalizeBlock(is_final=true) and on BlockFinalizedInMasterchain events. - /// Used for `should_generate_empty_block()` on masterchain. - /// - /// Updated in `commit_single_block()` when use_final_cert is true, and in - /// `set_mc_finalized_seqno()` (coupled max with last_mc_finalized_seqno). - last_consensus_finalized_seqno: Option, - /// Blocks that have been committed (finalized): RawCandidateId(slot, hash) /// /// Used during batch finalization to track which blocks in a parent chain @@ -1319,9 +1261,6 @@ impl SessionProcessor { health_warnings_counter, ) = Self::init_metrics(&metrics_receiver, &description); - let finalized_uncommitted_gauge = - metrics_receiver.sink().register_gauge(&"simplex_finalized_uncommitted_count".into()); - let now = description.get_time(); let num_validators = description.get_total_nodes() as usize; @@ -1348,14 +1287,11 @@ impl SessionProcessor { last_activity: vec![None; num_validators], delayed_actions: Vec::new(), simplex_state, - missing_body_logged: HashSet::new(), // Collation state precollated_blocks: PrecollatedBlockMap::new(), precollated_blocks_next_request_id: 0, precollated_blocks_max_slot: None, earliest_collation_time: None, - local_chain_head: None, - generated_parent_cache: HashMap::new(), // Validation state pending_validations: HashMap::new(), pending_approve: HashSet::new(), @@ -1365,7 +1301,6 @@ impl SessionProcessor { validation_attempt_map: HashMap::new(), validated_candidates: VecDeque::new(), received_candidates: HashMap::new(), - candidate_data_cache: HashMap::new(), // Metrics metrics_receiver, check_all_counter, @@ -1385,7 +1320,6 @@ impl SessionProcessor { errors_counter, batch_commit_counter, batch_commit_size_histogram, - finalized_uncommitted_gauge, // Error tracking (includes startup errors from before processor was created) session_errors_count: AtomicU32::new(initial_errors), // Slot stage tracking @@ -1407,7 +1341,6 @@ impl SessionProcessor { last_committed_slot: None, last_committed_block_id: None, last_committed_before_split: false, - last_consensus_finalized_seqno: initial_block_seqno.checked_sub(1), // Batch finalization tracking finalized_blocks: HashSet::new(), finalized_journal_pending_commit: HashMap::new(), @@ -1456,9 +1389,16 @@ impl SessionProcessor { Ok(processor) - // Note: C++ simplex resolves candidates from its own consensus DB, not via - // validator manager. The Rust implementation uses in-memory candidate_data_cache - // and peer overlay for candidate resolution. No get_approved_candidate delegation. + // TODO (INT-1): Add session startup recovery - when simplex session starts after restart, + // previously approved candidates may need to be restored from the validator's persistent + // storage via `notify_get_approved_candidate()`. This is needed because: + // 1. Consensus peers might not have the candidate anymore (too old) + // 2. The candidate was approved by this node but session restarted before commit + // See: validator-session's catchain_started() at line 1246 which iterates approved blocks + // and calls get_approved_candidate() to restore them. + // Implementation: After SimplexState is restored from DB (if persisted), call + // notify_get_approved_candidate for each approved block to populate resolver_cache. + // Reference: Alpenglow-Implementation-Plan.md Section 7.14a } /* @@ -1994,11 +1934,10 @@ impl SessionProcessor { /// /// # Reference /// - /// C++ `block-producer.cpp`: + /// C++ `block-producer.cpp` lines 89-92: /// ```cpp - /// void handle(BusHandle, std::shared_ptr event) { - /// last_mc_finalized_seqno_ = std::max(event->block.seqno(), last_mc_finalized_seqno_); - /// last_consensus_finalized_seqno_ = std::max(last_mc_finalized_seqno_, last_consensus_finalized_seqno_); + /// void handle(ConsensusBus::BlockFinalizedInMasterchain event) { + /// last_mc_finalized_seqno_ = event->block.seqno(); /// } /// ``` pub fn set_mc_finalized_seqno(&mut self, seqno: u32) { @@ -2008,17 +1947,7 @@ impl SessionProcessor { seqno, self.last_mc_finalized_seqno ); - // Keep last_mc_finalized_seqno monotonic, mirroring C++ behavior: - // last_mc_finalized_seqno_ = std::max(event->block.seqno(), last_mc_finalized_seqno_); - let prev_mc = self.last_mc_finalized_seqno.unwrap_or(0); - self.last_mc_finalized_seqno = Some(seqno.max(prev_mc)); - // C++ parity: BlockFinalizedInMasterchain also couples to last_consensus_finalized_seqno_ - let consensus = self.last_consensus_finalized_seqno.unwrap_or(0); - let mc = self.last_mc_finalized_seqno.unwrap_or(0); - let new_val = mc.max(consensus); - if new_val > consensus { - self.last_consensus_finalized_seqno = Some(new_val); - } + self.last_mc_finalized_seqno = Some(seqno); } /// Get the last masterchain finalized seqno @@ -2029,6 +1958,15 @@ impl SessionProcessor { self.last_mc_finalized_seqno } + /// Get the last committed (consensus-finalized) block seqno + /// + /// This is updated from `BlockFinalizedEvent` and serves as + /// `last_consensus_finalized_seqno` for masterchain empty block decisions. + #[allow(dead_code)] + pub fn last_committed_seqno(&self) -> Option { + self.last_committed_seqno + } + /// Determines if an empty block should be generated for finalization recovery /// /// Empty blocks are a TON-specific extension (not in Alpenglow White Paper) that @@ -2042,8 +1980,8 @@ impl SessionProcessor { /// /// # Logic /// - /// - **Masterchain**: Generate empty if `last_consensus_finalized_seqno + 1 < new_seqno` - /// (i.e., consensus-finalized is more than 1 behind) + /// - **Masterchain**: Generate empty if `last_committed_seqno + 1 < new_seqno` + /// (i.e., finalized is more than 1 behind) /// - **Shardchain**: Generate empty if `last_mc_finalized_seqno + 8 < new_seqno` /// (i.e., MC is more than 8 behind) /// @@ -2070,7 +2008,7 @@ impl SessionProcessor { } // C++ parity: ALWAYS generate empty if previous block has before_split flag - // This is required for shard split/merge operations. + // This is required for shard split/merge operations (SPLIT-1 fix). // Reference: C++ block-producer.cpp is_before_split() check if self.last_committed_before_split { log::debug!( @@ -2086,10 +2024,9 @@ impl SessionProcessor { if self.description.get_shard().is_masterchain() || DISABLE_NON_FINALIZED_PARENTS_FOR_COLLATION { - // Masterchain: consensus-finalized seqno must be at most 1 behind new seqno. - // C++ parity: block-producer.cpp uses `last_consensus_finalized_seqno_` which - // advances on FinalizeBlock(is_final) and on BlockFinalizedInMasterchain. - match self.last_consensus_finalized_seqno { + // Masterchain: finalized seqno must be at most 1 behind new seqno + // Uses last_committed_seqno as `last_consensus_finalized_seqno_` + match self.last_committed_seqno { Some(finalized) => finalized + 1 < new_seqno, None => false, // No finalization yet, can't be behind } @@ -2431,33 +2368,6 @@ impl SessionProcessor { current_giveups ); } - - // 8. Validator isolation: only self is active for extended period - let isolation_threshold = Duration::from_secs(60); - let session_age = now.duration_since(self.session_creation_time()).unwrap_or_default(); - if session_age > isolation_threshold - && active_weight <= 1 - && total_weight > 1 - && now.duration_since(self.health_alert_state.last_isolation_warn).unwrap_or_default() - >= Duration::from_secs(300) - { - self.health_alert_state.last_isolation_warn = now; - self.health_warnings_counter.increment(1); - let peers_never_seen = self - .last_activity - .iter() - .enumerate() - .filter(|(i, ts)| *i != self.description.get_self_idx().0 as usize && ts.is_none()) - .count(); - log::error!( - "SIMPLEX_HEALTH anomaly=validator_isolated session={session_prefix} \ - active_weight={active_weight} total={total_weight} \ - session_age={:.0}s peers_never_seen={peers_never_seen}/{} โ€” \ - possible validator key mismatch or overlay connectivity failure", - session_age.as_secs_f64(), - total_weight - 1, - ); - } } /// Produce detailed debug dump of session state @@ -2707,24 +2617,6 @@ impl SessionProcessor { Core consensus operations */ - /// Arm FSM timeouts and prepare the processor for the main loop. - /// - /// Must be called exactly once, after overlay warmup and bootstrap - /// recovery, right before the main loop begins. The FSM is created - /// with unarmed timeouts (`skip_timestamp = None`) so that no skip - /// cascade fires during the startup delay. - /// - /// Reference: C++ `Start` event -> `advance_present()` -> - /// `LeaderWindowObserved` -> `alarm_timestamp()`. - pub(crate) fn start(&mut self) { - self.simplex_state.set_timeouts(&self.description); - - log::info!( - "Session {} started: skip timeouts armed", - &self.session_id().to_hex_string()[..8], - ); - } - /// Check all pending operations /// /// Called periodically from main loop when awake time is reached. @@ -2748,18 +2640,15 @@ impl SessionProcessor { self.round_debug_at = now + ROUND_DEBUG_PERIOD; } + // Call SimplexState FSM check_all (processes timeouts, pending blocks) + self.simplex_state.check_all(&self.description); + // Check validation (process pending validations) self.check_validation(); - // Feed validated candidates to FSM BEFORE timeout processing so that - // the FSM has all available candidates before it evaluates timeouts - // (mirrors C++ where process_blocks() feeds candidates before the - // round timer is checked). + // Process validated candidates and feed to FSM self.process_validated_candidates(); - // Call SimplexState FSM check_all (processes timeouts, pending blocks) - self.simplex_state.check_all(&self.description); - // Process all events produced by FSM self.process_simplex_events(); @@ -2847,13 +2736,7 @@ impl SessionProcessor { parent: &crate::block::CandidateParentInfo, ) -> Option { let parent_id = RawCandidateId { slot: parent.slot, hash: parent.hash.clone() }; - // Check synchronous generated-parent cache first (populated in generated_block() - // before the async on_candidate_received self-loop), then fall back to the - // received_candidates map which is populated asynchronously. - self.generated_parent_cache - .get(&parent_id) - .cloned() - .or_else(|| self.received_candidates.get(&parent_id).map(|c| c.block_id.clone())) + self.received_candidates.get(&parent_id).map(|c| c.block_id.clone()) } /// Advance `earliest_collation_time` by `target_rate` from now. @@ -2890,38 +2773,6 @@ impl SessionProcessor { // Reference: C++ block-producer.cpp collates on notarized chain. let current_slot = self.simplex_state.get_first_non_progressed_slot(); - // Stale window guard (C++ parity: consensus.cpp LeaderWindowObserved handler sets - // current_window_ BEFORE the leader check). Skip collation when the progress - // cursor still points at a slot in a window that has already been superseded. - let slot_window = self.description.get_window_idx(current_slot); - let current_window = self.simplex_state.get_current_leader_window_idx(); - if slot_window < current_window { - log::trace!( - "Session {} check_collation: skipping stale slot {} (window {} < current {})", - &self.session_id().to_hex_string()[..8], - current_slot, - slot_window, - current_window - ); - return; - } - - // Invalidate the local chain head and cancel stale precollations when the - // leader window has changed since the last generation (C++ parity: the - // OurLeaderWindowStarted handler cancels the previous generation coroutine). - if let Some(ref head) = self.local_chain_head { - if head.window != current_window { - log::debug!( - "Session {} check_collation: leader window changed ({} -> {}), \ - resetting precollation pipeline", - &self.session_id().to_hex_string()[..8], - head.window, - current_window, - ); - self.reset_precollations(); - } - } - // Don't generate if already generated or pending for this slot if self.slot_is_generated(current_slot) || self.slot_is_pending_generate(current_slot) { return; @@ -3301,10 +3152,6 @@ impl SessionProcessor { self.collates_expire_counter.failure(); self.generated_block(slot, result); - - // C++ parity: after generating a candidate, start precollation for the next - // slot in the same leader window (block-producer.cpp `++slot; parent = id;`). - self.precollate_block(slot + 1); } else if slot > fsm_first_non_progressed_slot { // Store as precollated for future slot // Note: Empty blocks are not precollated - they are generated on-demand @@ -3733,24 +3580,6 @@ impl SessionProcessor { // Remove from precollated blocks self.remove_precollated_block(slot); - // Stale window guard (C++ parity: block-producer.cpp generation loop, - // consensus.cpp start_generation). Discard candidates whose leader window - // has already been superseded โ€” the collation callback arrived too late. - let slot_window = self.description.get_window_idx(slot); - let current_window = self.simplex_state.get_current_leader_window_idx(); - if slot_window != current_window { - log::warn!( - "Session {} generated_block: discarding stale candidate for slot {} \ - (window {} != current {})", - &self.session_id().to_hex_string()[..8], - slot, - slot_window, - current_window - ); - self.invalidate_local_chain_head(); - return; - } - // Use FSM's progress cursor to validate this is for the current slot. // Collation follows notarized/skipped progress, not finalization. let fsm_first_non_progressed_slot = self.simplex_state.get_first_non_progressed_slot(); @@ -3812,31 +3641,6 @@ impl SessionProcessor { self.persist_generated_candidate_info_to_db(slot, &prepared, &parent, is_empty); - // --- C++ candidate-chaining parity (block-producer.cpp `parent = id`) --- - // Synchronously seed the generated-parent cache and local chain head BEFORE - // the async on_candidate_received self-loop, so that precollate_block() for - // the next slot in the same window can resolve the parent immediately. - let candidate_parent_info = - crate::block::CandidateParentInfo { slot, hash: prepared.candidate_hash.clone() }; - let raw_id = RawCandidateId { slot, hash: prepared.candidate_hash.clone() }; - self.generated_parent_cache.insert(raw_id, prepared.block_id_ext.clone()); - - let slot_window = self.description.get_window_idx(slot); - self.local_chain_head = Some(LocalChainHead { - window: slot_window, - slot, - parent_info: candidate_parent_info, - block_id: prepared.block_id_ext.clone(), - }); - - log::trace!( - "Session {} generated_block: updated local_chain_head: window={}, slot={}, hash={}", - &self.session_id().to_hex_string()[..8], - slot_window, - slot, - &prepared.candidate_hash.to_hex_string()[..8], - ); - // Clone TL candidate data before broadcasting (needed for on_candidate_received) let tl_candidate_data_for_self = prepared.tl_candidate_data.clone(); @@ -3897,30 +3701,11 @@ impl SessionProcessor { parent: &Option, ) -> Result { let root_hash = &candidate.id.root_hash; + let file_hash = &candidate.id.file_hash; + let collated_file_hash = &candidate.collated_file_hash; let data = &candidate.data; let collated_data = &candidate.collated_data; - // Compute hashes from canonical BOC representation to match C++ simplex behavior. - // C++ leader hashes the original serialized bytes; C++ receiver hashes decompressed - // bytes โ€” they match because BOC serialization is deterministic given the same mode - // flags (mode 31 for block data, mode 2 for collated data). - // We explicitly canonicalize (deserialize โ†’ re-serialize with target flags) to - // guarantee matching hashes even if the input BOC was serialized with different flags. - // - // Falls back to raw bytes if canonicalization fails (e.g., in unit tests with - // mock data that's not valid BOC). In production, all data is valid BOC. - let file_hash = - match consensus_common::compression::canonicalize_boc(data.data(), BocFlags::all()) { - Ok(canonical) => UInt256::from_slice(&sha256_digest(&canonical)), - Err(_) => UInt256::from_slice(&sha256_digest(data.data())), - }; - let collated_file_hash = match consensus_common::compression::canonicalize_boc( - collated_data.data(), - BocFlags::Crc32, - ) { - Ok(canonical) => UInt256::from_slice(&sha256_digest(&canonical)), - Err(_) => UInt256::from_slice(&sha256_digest(collated_data.data())), - }; log::trace!( "Session {} create_normal_block_desc: slot={}, root_hash={:x}", self.session_id().to_hex_string(), @@ -3996,7 +3781,7 @@ impl SessionProcessor { let candidate_hash = crate::utils::compute_candidate_id_hash( slot, Some(&block_id), - Some(&collated_file_hash), + Some(collated_file_hash), parent_info, ); @@ -4010,19 +3795,14 @@ impl SessionProcessor { .map_err(|e| error!("failed to sign candidate: {e}"))?; // Build TL candidate for broadcast - // C++ simplex always uses compressed candidates (compression_enabled=true hardcoded). - // Serialize as validatorSession.compressedCandidate (LZ4+BOC merged roots). - let (compressed, decompressed_size) = - consensus_common::compression::compress_candidate_data( - data.data(), - collated_data.data(), - )?; - let tl_block_candidate = CompressedCandidate { + // Serialize block candidate as validatorSession.candidate (data + collated_data) + // Note: src must be zeros per C++ protocol (consensus-types.cpp) + let tl_block_candidate = TlCandidate { src: UInt256::default(), round: candidate_seqno as i32, root_hash: root_hash.clone(), - data: compressed, - decompressed_size: decompressed_size as i32, + data: data.data().clone(), + collated_data: collated_data.data().clone(), }; let candidate_bytes = consensus_common::serialize_tl_boxed_object!(&tl_block_candidate.into_boxed()); @@ -4133,41 +3913,44 @@ impl SessionProcessor { } /* - Precollation Pipeline โ€” C++ Candidate Chaining Parity - Reference: C++ block-producer.cpp generate_candidates() loop + Precollation Pipeline + Reference: validator-session/src/session_processor.rs precollation - The precollation pipeline chains candidates across slots within a single leader - window. After `generated_block()` completes slot N, it updates `local_chain_head` - and calls `precollate_block(N+1)`, which uses the local chain head as the explicit - parent instead of waiting for FSM `available_base` propagation via notarization. + NOTE: Precollation is currently DISABLED (max_precollated_blocks=0) - Pipeline reset (`reset_precollations()`) is triggered by: - - Session stop - - Leader window changes (via `invalidate_local_chain_head()`) - - Progress cursor jumping past queued slots + TODO(precollation): Before enabling precollation, fix these issues: + 1. Implement precollation pipeline reset triggering. + See reset_precollations() for details on what needs to be implemented. + 2. NOTE: Round mapping now uses slot directly (round = slot.value()). + - This eliminates the "precollation round mismatch" issue since round is derived + from the slot being collated, not from a separate counter. + - With optimistic validation on notarized parents, precollation slot + advancement is driven by the FSM progress cursor. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Precollation Pipeline โ”‚ โ”‚ โ”‚ - โ”‚ 1. check_collation() โ€” first slot in window: โ”‚ - โ”‚ โ”œโ”€โ”€ Use FSM available_base as parent โ”‚ - โ”‚ โ””โ”€โ”€ invoke_collation(slot, parent) โ”‚ + โ”‚ 1. precollate_block(slot): โ”‚ + โ”‚ โ”œโ”€โ”€ Check max_precollated_blocks limit โ”‚ + โ”‚ โ”œโ”€โ”€ If slot already in pipeline, advance to max_slot + 1 โ”‚ + โ”‚ โ””โ”€โ”€ Call invoke_collation(slot) โ”‚ โ”‚ โ”‚ - โ”‚ 2. generated_block() โ€” after broadcast: โ”‚ - โ”‚ โ”œโ”€โ”€ Update local_chain_head + generated_parent_cache โ”‚ - โ”‚ โ””โ”€โ”€ precollate_block(slot + 1) โ€” chain next slot โ”‚ + โ”‚ 2. invoke_collation(slot): โ”‚ + โ”‚ โ”œโ”€โ”€ Skip if already pending for slot โ”‚ + โ”‚ โ”œโ”€โ”€ Check priority (is leader for slot?) โ”‚ + โ”‚ โ”œโ”€โ”€ Update precollated_blocks_max_slot โ”‚ + โ”‚ โ”œโ”€โ”€ Create AsyncRequest and PrecollatedBlock entry โ”‚ + โ”‚ โ””โ”€โ”€ notify_generate_slot(source_info, request, callback) โ”‚ โ”‚ โ”‚ - โ”‚ 3. precollate_block(slot): โ”‚ - โ”‚ โ”œโ”€โ”€ Prefer local_chain_head for parent (same window) โ”‚ - โ”‚ โ”œโ”€โ”€ Fall back to FSM available_base โ”‚ - โ”‚ โ””โ”€โ”€ invoke_collation(slot, parent) โ”‚ + โ”‚ 3. Collation callback (on_collation_complete): โ”‚ + โ”‚ โ”œโ”€โ”€ Store candidate in PrecollatedBlock โ”‚ + โ”‚ โ””โ”€โ”€ precollate_block(slot + 1) to keep pipeline full โ”‚ โ”‚ โ”‚ โ”‚ 4. check_collation() finds precollated block: โ”‚ โ”‚ โ”œโ”€โ”€ Use precollated candidate directly โ”‚ โ”‚ โ””โ”€โ”€ Remove from pipeline, start next precollation โ”‚ โ”‚ โ”‚ - โ”‚ 5. Window change / progress jump: โ”‚ - โ”‚ โ””โ”€โ”€ invalidate_local_chain_head() + reset_precollations() โ”‚ + โ”‚ 5. Slot skip: reset_precollations() cancels all pending โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ */ @@ -4177,8 +3960,7 @@ impl SessionProcessor { /// Reference: validator-session/src/session_processor.rs precollate_block fn precollate_block(&mut self, slot: SlotIndex) { // Check max precollated blocks limit - let max_precollated = - self.description.opts().slots_per_leader_window.saturating_sub(1) as usize; + let max_precollated = self.description.opts().max_precollated_blocks as usize; if self.precollated_blocks.len() >= max_precollated { log::trace!( "Session {} precollate_block: max precollated blocks limit {} reached", @@ -4200,69 +3982,19 @@ impl SessionProcessor { } } - // Must still be in the same leader window as the current window - let target_window = self.description.get_window_idx(target_slot); - let current_window = self.simplex_state.get_current_leader_window_idx(); - if target_window != current_window { + // Precollation should only start when the parent is available (genesis or resolved). + // Note: `get_available_parent()` returns None for both genesis and "base unknown", + // so use `has_available_parent()` to disambiguate. + if !self.simplex_state.has_available_parent(&self.description, target_slot) { log::trace!( - "Session {} precollate_block: slot {} is in window {} (current={}), skipping", + "Session {} precollate_block: parent is not available for slot {}", self.session_id().to_hex_string(), - target_slot, - target_window, - current_window + target_slot ); return; } - // Must be leader for the target slot - let self_idx = self.description.get_self_idx(); - let leader = self.description.get_leader(target_slot); - if leader != self_idx { - log::trace!( - "Session {} precollate_block: not leader for slot {} (leader={})", - self.session_id().to_hex_string(), - target_slot, - leader - ); - return; - } - - // C++ parity: prefer local chain head for parent when the previous slot in - // the same window was just generated locally (block-producer.cpp `parent = id`). - // Fall back to FSM available_base for the first slot in a window or if the - // local chain head is stale. - let parent = if let Some(ref head) = self.local_chain_head { - if head.window == target_window && head.slot + 1 == target_slot { - log::trace!( - "Session {} precollate_block: using local_chain_head for slot {} \ - (parent=s{}:{})", - &self.session_id().to_hex_string()[..8], - target_slot, - head.parent_info.slot, - &head.parent_info.hash.to_hex_string()[..8], - ); - Some(head.parent_info.clone()) - } else { - None - } - } else { - None - }; - - let parent = if parent.is_some() { - parent - } else { - // Fall back to FSM available_base - if !self.simplex_state.has_available_parent(&self.description, target_slot) { - log::trace!( - "Session {} precollate_block: parent is not available for slot {}", - self.session_id().to_hex_string(), - target_slot - ); - return; - } - self.simplex_state.get_available_parent(&self.description, target_slot) - }; + let parent = self.simplex_state.get_available_parent(&self.description, target_slot); if let Some(ref parent_info) = parent { if self.resolve_parent_block_id(parent_info).is_none() { @@ -4290,16 +4022,10 @@ impl SessionProcessor { } } - /// Reset all precollations and invalidate the local chain head. - /// - /// Called when the precollation pipeline must be flushed: - /// - Session stop - /// - Leader window change (progress cursor moved to a new window) - /// - Progress cursor jumped past queued precollation slots + /// Reset all precollations (on slot skip or session stop) fn reset_precollations(&mut self) { log::debug!( - "Session {} reset_precollations: cancelling {} pending precollations, \ - clearing local_chain_head", + "Session {} reset_precollations: cancelling {} pending precollations", self.session_id().to_hex_string(), self.precollated_blocks.len() ); @@ -4311,26 +4037,6 @@ impl SessionProcessor { self.precollated_blocks.clear(); self.precollated_blocks_max_slot = None; - self.invalidate_local_chain_head(); - } - - /// Invalidate the window-local chain head. - /// - /// Called when the leader window changes, the progress cursor jumps, or a - /// consensus event (skip/notarize) invalidates the locally generated parent - /// chain. After invalidation, the next collation will start fresh from the - /// FSM `available_base`. - fn invalidate_local_chain_head(&mut self) { - if let Some(ref head) = self.local_chain_head { - log::trace!( - "Session {} invalidate_local_chain_head: clearing (was window={}, slot={})", - &self.session_id().to_hex_string()[..8], - head.window, - head.slot, - ); - } - self.local_chain_head = None; - self.generated_parent_cache.clear(); } /* @@ -4511,7 +4217,6 @@ impl SessionProcessor { vote_hash, data: raw_vote_for_db.to_raw_buffer(), node_idx: source_idx, - seqno: 0, // assigned by save_vote_async }; if let Err(e) = self.db.save_vote_async(&record) { log::error!( @@ -4521,24 +4226,6 @@ impl SessionProcessor { ); self.increment_error(); } - - // Proactively request missing candidate when receiving a NotarizeVote - // for a block we don't have. This handles the case where the candidate - // broadcast was lost (e.g., due to QUIC congestion stall with C++ ngtcp2). - // Without this, the node can't vote and NotarizationReached is never triggered, - // which is the normal trigger for candidate requests. - if let Some(ref hash) = tl_hash_opt { - let candidate_id = RawCandidateId { slot: tl_slot, hash: hash.clone() }; - if !self.has_real_candidate_body(&candidate_id) { - log::debug!( - "Session {} on_vote: NotarizeVote for missing candidate \ - slot={tl_slot} hash={} from source_idx={source_idx}, requesting", - &self.session_id().to_hex_string()[..8], - &hash.to_hex_string()[..8] - ); - self.request_candidate(tl_slot, hash.clone(), None); - } - } } VoteResult::Duplicate => { // Duplicate vote, silently ignore @@ -4700,9 +4387,9 @@ impl SessionProcessor { let fsm_first_non_finalized_slot = self.simplex_state.get_first_non_finalized_slot(); if tl_slot < fsm_first_non_finalized_slot { log::trace!( - "Session {} on_certificate: dropping old certificate slot={tl_slot} \ - (< first_non_finalized={fsm_first_non_finalized_slot}) kind={tl_kind} \ - from source_idx={source_idx}", + "Session {} on_certificate: dropping old certificate slot={tl_slot} (< \ + first_non_finalized={fsm_first_non_finalized_slot}) kind={tl_kind} from \ + source_idx={source_idx}", &self.session_id().to_hex_string()[..8], ); return; @@ -4711,8 +4398,8 @@ impl SessionProcessor { // Reject far-future slots before signature verification (DoS protection) if self.simplex_state.is_slot_too_far_ahead(tl_slot) { log::warn!( - "Session {} on_certificate: REJECTED - slot {tl_slot} too far ahead \ - (max={}) kind={tl_kind} from source_idx={source_idx}", + "Session {} on_certificate: REJECTED - slot {tl_slot} too far ahead (max={}) \ + kind={tl_kind} from source_idx={source_idx}", &self.session_id().to_hex_string()[..8], self.simplex_state.max_acceptable_slot(), ); @@ -4752,22 +4439,6 @@ impl SessionProcessor { cert.signatures.len() ); - // Proactively request missing candidate when receiving a certificate - // for a block we don't have. This handles the case where the candidate - // broadcast was lost (e.g., due to QUIC congestion stall with C++ ngtcp2). - if let Some(ref hash) = tl_hash_opt { - let candidate_id = RawCandidateId { slot: tl_slot, hash: hash.clone() }; - if !self.has_real_candidate_body(&candidate_id) { - log::debug!( - "Session {} on_certificate: {tl_kind} cert for missing candidate \ - slot={tl_slot} hash={} from source_idx={source_idx}, requesting", - &self.session_id().to_hex_string()[..8], - &hash.to_hex_string()[..8] - ); - self.request_candidate(tl_slot, hash.clone(), None); - } - } - // Dispatch based on vote type in certificate // If stored (new certificate), relay to other validators and cache for standstill match &cert.vote { @@ -5072,7 +4743,6 @@ impl SessionProcessor { leader_idx, self.description.get_shard(), max_size, - self.description.opts().proto_version, ) { Ok(c) => c, Err(e) => { @@ -5136,20 +4806,12 @@ impl SessionProcessor { candidate_id.slot ); - // Check if candidate already known. - // A finalized-boundary stub (seeded by handle_block_finalized with empty data) is NOT - // "already known" for this purpose -- we want the real body to overwrite it. - let is_finalized_stub = self - .received_candidates - .get(&candidate_id) - .map(|r| r.candidate_hash_data_bytes.is_empty()) - .unwrap_or(false); - if !is_finalized_stub - && (self.pending_validations.contains_key(&candidate_id) - || self.pending_approve.contains(&candidate_id) - || self.approved.contains_key(&candidate_id) - || self.rejected.contains(&candidate_id) - || self.received_candidates.contains_key(&candidate_id)) + // Check if candidate already known + if self.pending_validations.contains_key(&candidate_id) + || self.pending_approve.contains(&candidate_id) + || self.approved.contains_key(&candidate_id) + || self.rejected.contains(&candidate_id) + || self.received_candidates.contains_key(&candidate_id) { log::trace!( "Session {} on_candidate_received: candidate already known: {:?}", @@ -5183,32 +4845,6 @@ impl SessionProcessor { // Determine if this is an empty block from the TL variant let is_empty = matches!(candidate, CandidateData::Consensus_Empty(_)); - // Cache serialized CandidateData for RequestCandidate query fallback (C++ parity). - // This provides a secondary in-memory store that persists independently of - // the receiver's resolver_cache, enabling peers to retrieve candidates even - // after the resolver_cache is cleaned up. - match serialize_boxed(&candidate) { - Ok(bytes) => { - self.candidate_data_cache.insert(candidate_id.clone(), bytes.clone()); - // Persist to DB for restart serving (C++ CandidateResolver::store_candidate parity) - if let Err(e) = self.db.save_candidate_payload_async(&candidate_id, &bytes) { - log::error!( - "Session {} on_candidate_received: failed to persist candidate payload: {}", - &self.session_id().to_hex_string()[..8], - e - ); - self.increment_error(); - } - } - Err(e) => { - log::warn!( - "Session {} on_candidate_received: failed to serialize CandidateData for cache: {}", - &self.session_id().to_hex_string()[..8], - e - ); - } - } - // Seqno validation for on_candidate_received // Validate seqno is consistent with parent (if parent is already received) let received_seqno = block_id.seq_no; @@ -5677,7 +5313,7 @@ impl SessionProcessor { /// Verify notarization certificate from VoteSignatureSet (C++ wire format) /// - /// Parse VoteSignatureSet and verify signatures. + /// INTEROP-1: Parse VoteSignatureSet and verify signatures. /// Reference: C++ NotarCert::from_tl(voteSignatureSet&&, vote, bus) fn verify_notar_cert_from_vote_signature_set( &self, @@ -5850,10 +5486,8 @@ impl SessionProcessor { self.pending_parent_resolutions.entry(key).or_default().push(pending); - // Request the missing parent immediately (no delay). Parent-cascade requests are - // catch-up traffic: the candidate was already produced long ago and won't arrive - // via broadcast, so the 1-second CANDIDATE_REQUEST_DELAY only adds latency. - self.request_candidate(missing_parent.slot, missing_parent.hash, Some(Duration::ZERO)); + // Schedule a request for the missing parent (with delay to allow broadcast to arrive) + self.request_candidate(missing_parent.slot, missing_parent.hash, None); } /// Update the `is_fully_resolved` cache for a specific candidate and its descendants. @@ -6132,7 +5766,7 @@ impl SessionProcessor { /// /// Validates pending candidates whose parent slot has been notarized (or finalized) /// in the FSM. Genesis candidates (no parent) are always eligible. This enables - /// optimistic validation on notarized-only parents (C++ parity). + /// optimistic validation on notarized-only parents (OPTIMISTIC-VALID-1 / C++ parity). fn check_validation(&mut self) { check_execution_time!(10_000); instrument!(); @@ -6175,9 +5809,7 @@ impl SessionProcessor { } } - // Empty blocks skip ValidatorGroup validation but still need FSM-tip reference - // check (performed in try_approve_block). C++ block-validator.cpp rejects unless - // block == event->state->as_normal(). + // Empty blocks don't need validation (C++ skips validation for empty blocks) if pending.raw_candidate.block.is_empty() { to_validate.push(( candidate_id.clone(), @@ -6216,36 +5848,6 @@ impl SessionProcessor { } } - /// Resolve the expected referenced BlockIdExt for an empty candidate. - /// - /// Walks the parent chain through `received_candidates` until a non-empty - /// ancestor is found. Returns its `block_id`, which is the C++ equivalent - /// of `event->state->as_normal()` in `block-validator.cpp`. - /// - /// Returns `None` if the parent chain is broken, missing, or contains - /// only empty ancestors (no normal tip exists). - fn resolve_expected_empty_block(&self, raw_candidate: &RawCandidate) -> Option { - let parent_id = raw_candidate.parent_id.as_ref()?; - let parent = self.received_candidates.get(parent_id)?; - if !parent.is_empty { - return Some(parent.block_id.clone()); - } - let mut current_parent = parent.parent_id.clone(); - let mut depth = 0u32; - while let Some(pid) = current_parent { - depth += 1; - if depth > MAX_CHAIN_DEPTH { - return None; - } - let ancestor = self.received_candidates.get(&pid)?; - if !ancestor.is_empty { - return Some(ancestor.block_id.clone()); - } - current_parent = ancestor.parent_id.clone(); - } - None - } - /// Try to approve a block candidate by sending to higher layer /// /// Reference: validator-session/src/session_processor.rs try_approve_block() @@ -6281,53 +5883,15 @@ impl SessionProcessor { return; }; - // Handle empty blocks: C++ block-validator.cpp rejects unless the referenced - // block equals event->state->as_normal(). We resolve the expected block from - // the parent chain and compare before approving. + // Handle empty blocks (no validation needed) if pending.raw_candidate.block.is_empty() { - let referenced_block = pending.raw_candidate.block.block_id().clone(); - let expected = self.resolve_expected_empty_block(&pending.raw_candidate); - let cid = candidate_id.clone(); - - match expected { - Some(expected_block) if referenced_block == expected_block => { - log::trace!( - "Session {} try_approve_block: empty block matches parent normal tip, \ - approving {:?}", - self.session_id().to_hex_string(), - cid, - ); - self.candidate_decision_ok_internal(cid, slot, receive_time); - } - Some(expected_block) => { - log::warn!( - "Session {} try_approve_block: empty block REJECTED โ€” wrong referenced \ - block (got seqno={}, expected seqno={}) for {:?}", - self.session_id().to_hex_string(), - referenced_block.seq_no, - expected_block.seq_no, - cid, - ); - self.candidate_decision_fail( - slot, - cid, - error!("Wrong referenced block in empty candidate"), - ); - } - None => { - log::warn!( - "Session {} try_approve_block: empty block REJECTED โ€” cannot resolve \ - parent normal tip for {:?}", - self.session_id().to_hex_string(), - cid, - ); - self.candidate_decision_fail( - slot, - cid, - error!("Cannot resolve parent normal tip for empty candidate"), - ); - } - } + log::trace!( + "Session {} try_approve_block: empty block, auto-approving {:?}", + self.session_id().to_hex_string(), + candidate_id, + ); + // Empty blocks are auto-approved - directly push to validated_candidates + self.candidate_decision_ok_internal(candidate_id.clone(), slot, receive_time); return; } @@ -6474,14 +6038,14 @@ impl SessionProcessor { .get(&candidate_id) .and_then(|p| p.raw_candidate.block.as_block().map(|b| b.id.seq_no)), ) { + log::warn!( + "Session {} candidate_decision_ok: slot={slot}, hash={:?}, \ + committed_seqno={committed_seqno}, cand_seqno={cand_seqno} (drop because new \ + block is already committed)", + self.session_id().to_hex_string(), + candidate_id, + ); if cand_seqno <= committed_seqno { - log::warn!( - "Session {} candidate_decision_ok: slot={slot}, hash={:?}, \ - committed_seqno={committed_seqno}, cand_seqno={cand_seqno} (drop because \ - new block is already committed)", - self.session_id().to_hex_string(), - candidate_id, - ); self.pending_approve.remove(&candidate_id); self.pending_validations.remove(&candidate_id); self.validation_attempt_map.remove(&candidate_id); @@ -6491,8 +6055,7 @@ impl SessionProcessor { self.candidate_decision_ok_internal(candidate_id, slot, receive_time); - // Wake immediately so check_all() runs in the very next main-loop iteration - self.set_next_awake_time(self.now()); + self.set_next_awake_time(validity_start_time); } /// Internal helper for successful validation (used by both normal and empty block paths) @@ -6861,7 +6424,6 @@ impl SessionProcessor { vote_hash, data: serialized.into(), node_idx: self.description.get_self_idx(), - seqno: 0, // assigned by save_vote_async }; let result = match self.db.save_vote_async(&record) { @@ -6925,18 +6487,6 @@ impl SessionProcessor { โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ */ - /// Check whether we have a **real** candidate body (not a finalized-boundary stub). - /// - /// Finalized-boundary stubs are inserted by `handle_block_finalized` with empty - /// `candidate_hash_data_bytes` to serve as parent-resolution boundaries. They must - /// NOT suppress `requestCandidate` retries -- a stub is not a real body. - fn has_real_candidate_body(&self, id: &RawCandidateId) -> bool { - self.received_candidates - .get(id) - .map(|r| !r.candidate_hash_data_bytes.is_empty()) - .unwrap_or(false) - } - /// Schedule a candidate request with delay if not already requested /// /// Called by `try_commit_finalized_chains()` when a candidate body or NotarCert is missing. @@ -6979,8 +6529,8 @@ impl SessionProcessor { } } - // Check if we already have what we need (stubs don't count as real bodies) - let have_body = self.has_real_candidate_body(&key); + // Check if we already have what we need + let have_body = self.received_candidates.contains_key(&key); let have_notar = self.simplex_state.get_notarize_certificate(slot, &block_hash).is_some(); if have_body && have_notar { @@ -7017,7 +6567,7 @@ impl SessionProcessor { self.post_delayed_action(expiration_time, move |processor: &mut SessionProcessor| { let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; - let have_body = processor.has_real_candidate_body(&candidate_id); + let have_body = processor.received_candidates.contains_key(&candidate_id); let have_notar = processor.simplex_state.get_notarize_certificate(slot, &block_hash).is_some(); @@ -7106,7 +6656,7 @@ impl SessionProcessor { let received = match self.received_candidates.get(¤t_id) { Some(r) => r, None => { - log::trace!( + log::debug!( "Session {} collect_gapless_commit_chain: missing body for slot={} hash={}", &self.session_id().to_hex_string()[..8], current_id.slot, @@ -7116,36 +6666,6 @@ impl SessionProcessor { } }; - // 1b. Finalized-boundary stub detection. - // - // Stubs are inserted by handle_block_finalized() for parent-resolution boundaries. - // They are not committable bodies. - // - // - Triggered block is a stub: treat as missing body and request it. - // - Non-triggered ancestor is a stub: stop walking (boundary reached). - if received.candidate_hash_data_bytes.is_empty() { - if is_first { - log::debug!( - "Session {} collect_gapless_commit_chain: triggered finalized block is \ - still a boundary stub, waiting for body: slot={} hash={}", - &self.session_id().to_hex_string()[..8], - current_id.slot, - ¤t_id.hash.to_hex_string()[..8], - ); - return ChainCollectionResult::MissingCandidate { - missing_id: current_id.clone(), - }; - } - - log::trace!( - "Session {} collect_gapless_commit_chain: reached finalized boundary stub \ - at slot={}, stopping walk", - &self.session_id().to_hex_string()[..8], - current_id.slot, - ); - break; - } - let current_seqno = received.block_id.seq_no; if is_first && triggered_is_empty.is_none() { @@ -7627,7 +7147,7 @@ impl SessionProcessor { // For empty blocks this is the re-signed parent block id (same as previous non-empty id). self.last_committed_block_id = Some(received.block_id.clone()); - // Extract and track before_split flag for split/merge handling + // Extract and track before_split flag for split/merge handling (SPLIT-1 fix) // C++ parity: C++ checks `is_before_split(prev_block_data)` in should_generate_empty_block() // We extract it here during commit and cache it for the next collation decision. if !is_empty_block { @@ -7799,23 +7319,6 @@ impl SessionProcessor { // Increment commits counter self.commits_counter.success(); - - // C++ parity: block-producer.cpp advances last_consensus_finalized_seqno_ - // only on FinalizeBlock when is_final() is true. This is the Rust equivalent. - if use_final_cert { - let prev = self.last_consensus_finalized_seqno.unwrap_or(0); - if seqno > prev { - self.last_consensus_finalized_seqno = Some(seqno); - log::debug!( - "Session {} commit_single_block: advanced last_consensus_finalized_seqno \ - {} -> {} (slot={}, is_final=true)", - &self.session_id().to_hex_string()[..8], - prev, - seqno, - slot - ); - } - } } // ===== Common finalization (for both empty and non-empty) ===== @@ -8098,21 +7601,14 @@ impl SessionProcessor { /// /// This function is idempotent and safe to call multiple times. fn try_commit_finalized_chains(&mut self) { - // Collect keys to process, sorted by (seqno, slot) for deterministic - // oldest-first commit ordering (avoid arbitrary HashMap iteration order). - let mut finalized_keys: Vec = + // Collect keys to process (avoid borrow conflicts) + let finalized_keys: Vec = self.finalized_journal_pending_commit.keys().cloned().collect(); if finalized_keys.is_empty() { return; } - finalized_keys.sort_unstable_by_key(|id| { - let seqno = - self.received_candidates.get(id).map(|r| r.block_id.seq_no).unwrap_or(u32::MAX); - (seqno, id.slot.0) - }); - log::trace!( "Session {} try_commit_finalized_chains: checking {} finalized blocks", &self.session_id().to_hex_string()[..8], @@ -8163,17 +7659,15 @@ impl SessionProcessor { } ChainCollectionResult::MissingCandidate { missing_id } => { - if self.missing_body_logged.insert(missing_id.slot.0) { - log::debug!( - "Session {} try_commit_finalized_chains: s{}:{} waiting for s{}:{} \ - (body or NotarCert)", - &self.session_id().to_hex_string()[..8], - finalized_id.slot, - &finalized_id.hash.to_hex_string()[..8], - missing_id.slot, - &missing_id.hash.to_hex_string()[..8], - ); - } + log::debug!( + "Session {} try_commit_finalized_chains: s{}:{} waiting for s{}:{} (body \ + or NotarCert)", + &self.session_id().to_hex_string()[..8], + finalized_id.slot, + &finalized_id.hash.to_hex_string()[..8], + missing_id.slot, + &missing_id.hash.to_hex_string()[..8], + ); // Request the missing candidate (includes body + NotarCert with want_notar=true) self.request_candidate(missing_id.slot, missing_id.hash, None); @@ -8298,6 +7792,10 @@ impl SessionProcessor { finalized_id: inner_finalized_id, finalized_seqno: inner_finalized_seqno, } => { + // Invariant: commit_target was selected as the *next committable* non-empty + // masterchain block (seqno == expected_seqno) and we have its FinalCert. + // Therefore, collect_gapless_commit_chain(commit_target) must NOT return + // WaitingForFinalCert again. log::error!( "Session {} try_commit_finalized_chains: MC gap \ recovery invariant violated - \ @@ -8371,18 +7869,9 @@ impl SessionProcessor { } // Remove committed entries from journal - let did_commit = !committed_keys.is_empty(); for key in committed_keys { self.finalized_journal_pending_commit.remove(&key); } - - // If something was committed, newly-unblocked chains may now be ready. - // Reschedule check_all so the session loop re-enters this function. - if did_commit { - self.set_next_awake_time(self.now()); - } - - self.finalized_uncommitted_gauge.set(self.finalized_journal_pending_commit.len() as f64); } /// Commit a finalized chain that has been verified as commit-ready @@ -8539,54 +8028,6 @@ impl SessionProcessor { let entry = FinalizedEntry { event: event.clone(), finalized_at: self.now() }; self.finalized_journal_pending_commit.insert(finalized_id.clone(), entry); - // NOTE: last_consensus_finalized_seqno is NOT advanced here. - // C++ parity: block-producer.cpp advances last_consensus_finalized_seqno_ only on - // FinalizeBlock(is_final=true), which happens AFTER the state-resolver commits. - // In Rust, the equivalent is commit_single_block() with use_final_cert=true. - - // Seed a finalized-boundary entry into received_candidates for parent resolution. - // C++ parity: StateResolver::resolve_state_inner() treats finalized blocks as boundaries - // and stops recursing into their ancestors. Rust needs the same behavior for live sessions, - // not only for restart recovery. - if let Some(ref block_id) = event.block_id { - if !self.received_candidates.contains_key(&finalized_id) { - self.received_candidates.insert( - finalized_id.clone(), - ReceivedCandidate { - slot, - source_idx: self.description.get_self_idx(), - candidate_id_hash: block_hash.clone(), - candidate_hash_data_bytes: Vec::new(), - block_id: block_id.clone(), - root_hash: block_id.root_hash.clone(), - file_hash: block_id.file_hash.clone(), - data: consensus_common::ConsensusCommonFactory::create_block_payload( - Vec::new(), - ), - collated_data: - consensus_common::ConsensusCommonFactory::create_block_payload( - Vec::new(), - ), - receive_time: self.now(), - is_empty: false, - parent_id: None, - is_fully_resolved: true, - }, - ); - log::debug!( - "Session {} handle_block_finalized: seeded finalized boundary for slot={} \ - seqno={} (for parent resolution)", - &self.session_id().to_hex_string()[..8], - slot, - block_id.seq_no() - ); - - // Resolve any pending parent resolutions that were waiting for this candidate - self.update_resolution_cache_chain(&finalized_id); - self.try_resolve_waiting_candidates(&finalized_id); - } - } - log::debug!( "Session {} FINALIZED: slot={}, hash={} - recorded in journal, weight={}/{} ({:.0}%)", &self.session_id().to_hex_string()[..8], @@ -8674,45 +8115,6 @@ impl SessionProcessor { //TODO: implement cleanup of blocks for old candidates //self.received_candidates.retain(|_hash, c| c.slot >= up_to_slot); - // Clean up candidate_data_cache in sync with received_candidates - self.candidate_data_cache.retain(|id, _| id.slot >= up_to_slot); - - // Remove stale finalized-journal entries for old slots. - { - let now = self.now(); - let session_id_hex = self.session_id().to_hex_string(); - let mut stale_count = 0u32; - self.finalized_journal_pending_commit.retain(|id, entry| { - if id.slot < up_to_slot { - let age_secs = now - .duration_since(entry.finalized_at) - .map(|d| d.as_secs_f64()) - .unwrap_or(0.0); - log::warn!( - "Session {} cleanup: removing stale finalized-journal entry slot={} \ - (finalized {:.1}s ago, never committed)", - &session_id_hex[..8], - id.slot, - age_secs, - ); - stale_count += 1; - false - } else { - true - } - }); - if stale_count > 0 { - self.session_errors_count - .fetch_add(stale_count, std::sync::atomic::Ordering::Relaxed); - self.errors_counter.increment(stale_count as u64); - self.finalized_uncommitted_gauge - .set(self.finalized_journal_pending_commit.len() as f64); - } - } - - // Prune log-throttle set to prevent unbounded growth over long sessions - self.missing_body_logged.retain(|&slot| slot >= up_to_slot.value()); - // Remove pending candidate requests for slots < up_to_slot self.requested_candidates.retain(|id, _| id.slot >= up_to_slot); @@ -8818,8 +8220,8 @@ impl SessionProcessor { // unresolved parent chain, causing timeouts and skip cascades in single-host tests. // // C++ parity intent: CandidateResolver/Pool logic requests missing candidate data - // based on observed certificates. Finalized-boundary stubs don't count as real bodies. - if !self.has_real_candidate_body(&candidate_id) { + // based on observed certificates. + if !self.received_candidates.contains_key(&candidate_id) { self.request_candidate(event.slot, event.block_hash.clone(), None); } @@ -8897,10 +8299,12 @@ impl SessionProcessor { cert_bytes.len(), ); - // C++ parity (pool.cpp handle_saved_certificate): relay every newly - // accepted certificate to all validators. Dedup is in SimplexState. - self.certs_relayed_counter.increment(1); - self.receiver.send_certificate(tl_cert); + // Send certificate to all validators unless this cert is learned via + // direct query/repair path (avoid redundant broadcast storms). + if event.should_broadcast { + self.certs_relayed_counter.increment(1); + self.receiver.send_certificate(tl_cert); + } // Cache for standstill re-broadcast self.receiver.cache_standstill_certificate( @@ -8953,17 +8357,19 @@ impl SessionProcessor { // Serialize for caching match serialize_boxed(&tl_cert) { Ok(cert_bytes) => { - log::trace!( - "Session {} handle_skip_certificate_reached: broadcasting skip cert for \ - slot={} ({}B)", - &self.session_id().to_hex_string()[..8], - event.slot, - cert_bytes.len(), - ); + if event.should_broadcast { + log::trace!( + "Session {} handle_skip_certificate_reached: broadcasting skip cert for \ + slot={} ({}B)", + &self.session_id().to_hex_string()[..8], + event.slot, + cert_bytes.len(), + ); - // Send certificate to all validators - self.certs_relayed_counter.increment(1); - self.receiver.send_certificate(tl_cert); + // Send certificate to all validators + self.certs_relayed_counter.increment(1); + self.receiver.send_certificate(tl_cert); + } // Cache for standstill re-broadcast self.receiver.cache_standstill_certificate( @@ -8987,7 +8393,9 @@ impl SessionProcessor { /// /// Called when FSM determines finalization threshold reached for a block. /// Always caches the finalization certificate for standstill replay. - /// Relays certificate to all validators (C++ parity: handle_saved_certificate). + /// Broadcasts only when `event.should_broadcast` is true for locally-created + /// certificates. Peer-ingested certificates are cached but not re-broadcast + /// by this path to avoid amplification. fn handle_finalization_reached(&mut self, event: FinalizationReachedEvent) { check_execution_time!(1_000); @@ -9016,17 +8424,18 @@ impl SessionProcessor { // Serialize for broadcast + caching match serialize_boxed(&tl_cert) { Ok(cert_bytes) => { - // C++ parity (pool.cpp handle_saved_certificate): relay every newly - // accepted certificate to all validators. Dedup is in SimplexState. - log::trace!( - "Session {} handle_finalization_reached: \ - broadcasting final cert for slot={} ({}B)", - &self.session_id().to_hex_string()[..8], - event.slot, - cert_bytes.len(), - ); - self.certs_relayed_counter.increment(1); - self.receiver.send_certificate(tl_cert); + // Broadcast to all validators (C++ parity: handle_our_certificate) + if event.should_broadcast { + log::trace!( + "Session {} handle_finalization_reached: \ + broadcasting final cert for slot={} ({}B)", + &self.session_id().to_hex_string()[..8], + event.slot, + cert_bytes.len(), + ); + self.certs_relayed_counter.increment(1); + self.receiver.send_certificate(tl_cert); + } // Cache per-slot final certificate (for bundle replay) self.receiver.cache_standstill_certificate( @@ -9611,245 +9020,55 @@ impl SessionProcessor { }); } - /// Handle RequestCandidate query fallback when receiver's resolver_cache misses. + /// Request approved candidate from validator /// - /// Called from SXRCV thread via ReceiverListener when a peer's RequestCandidate query - /// cannot be answered from the in-memory resolver_cache. Attempts to reconstruct the - /// response from: - /// 1. `candidate_data_cache` (in-memory, fast path) - /// 2. SimplexDB `CandidateInfoRecord` (empty blocks only -- reconstructed from metadata) + /// Called to retrieve a previously approved block candidate from persistent storage. + /// Used for session restart recovery and as a fallback for candidate resolver queries. /// - /// Non-empty blocks not in the in-memory cache return an empty response; the - /// querying peer will retry with other validators. This matches C++ behavior - /// where `CandidateResolver` only loads from its own consensus DB, never from - /// the validator manager. - /// - /// Reference: C++ `CandidateResolver::try_load_candidate_data_from_db()` - pub fn handle_candidate_query_fallback( - &mut self, - slot: SlotIndex, - block_hash: UInt256, - want_notar: bool, - response_callback: crate::QueryResponseCallback, + /// TODO (INT-1): Remove #[allow(dead_code)] when integrated. Use cases: + /// 1. Session startup: Restore approved candidates from DB to resolver_cache + /// 2. Query fallback: When resolver_cache misses, try loading from DB + /// Reference: validator-session/src/session_processor.rs lines 1246, 1620 + /// See: Alpenglow-Implementation-Plan.md Section 7.14a + #[allow(dead_code)] + fn notify_get_approved_candidate( + &self, + source: crate::PublicKey, + root_hash: crate::BlockHash, + file_hash: crate::BlockHash, + collated_data_hash: crate::BlockHash, + callback: crate::ValidatorBlockCandidateCallback, ) { - check_execution_time!(50_000); - - let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; - let session_hex = &self.session_id().to_hex_string()[..8]; - - // 1. Fast path: check in-memory candidate_data_cache - if let Some(candidate_bytes) = self.candidate_data_cache.get(&candidate_id) { - log::debug!( - "Session {} candidate_query_fallback: cache HIT for slot={} hash={} ({}B)", - session_hex, - slot, - &block_hash.to_hex_string()[..8], - candidate_bytes.len() - ); - let notar_bytes = if want_notar { - self.load_notar_cert_bytes_from_db(&candidate_id) - } else { - Vec::new() - }; - Self::send_candidate_and_cert_response( - candidate_bytes.clone(), - notar_bytes, - response_callback, - ); - return; - } - - // 2. DB path: load CandidateInfoRecord for metadata - let candidate_info = match self.load_candidate_info_from_db(&candidate_id) { - Some(info) => info, - None => { - log::debug!( - "Session {} candidate_query_fallback: NOT FOUND for slot={} hash={}", - session_hex, - slot, - &block_hash.to_hex_string()[..8] - ); - Self::send_empty_candidate_response(response_callback); - return; - } - }; - - let notar_bytes = - if want_notar { self.load_notar_cert_bytes_from_db(&candidate_id) } else { Vec::new() }; - - // 3. Try persisted payload from DB first (works for both empty and non-empty blocks, - // since save_candidate_payload_async persists payloads for all candidates). - { - const DB_TIMEOUT: Duration = Duration::from_secs(2); - match self.db.load_candidate_payload_by_id(&candidate_id, DB_TIMEOUT) { - Ok(Some(payload_bytes)) => { - log::debug!( - "Session {} candidate_query_fallback: loaded payload from DB for slot={} ({}B)", - session_hex, - slot, - payload_bytes.len() - ); - Self::send_candidate_and_cert_response( - payload_bytes, - notar_bytes, - response_callback, - ); - return; - } - Ok(None) => {} - Err(e) => { - log::warn!( - "Session {} candidate_query_fallback: DB payload load error for slot={}: {}", - session_hex, - slot, - e - ); - } - } - } + check_execution_time!(20_000); - // 4. DB payload not available: try metadata reconstruction for empty blocks - let is_empty = matches!( - candidate_info.candidate_hash_data, - CandidateHashData::Consensus_CandidateHashDataEmpty(_) + log::trace!( + "Session {} notify_get_approved_candidate: posting get_approved_candidate event", + self.session_id().to_hex_string() ); - if is_empty { - match self.reconstruct_empty_candidate_data_from_info(&candidate_id, &candidate_info) { - Ok(bytes) => { - log::debug!( - "Session {} candidate_query_fallback: reconstructed empty block for slot={} ({}B)", - session_hex, - slot, - bytes.len() - ); - Self::send_candidate_and_cert_response(bytes, notar_bytes, response_callback); - return; - } - Err(e) => { - log::warn!( - "Session {} candidate_query_fallback: failed to reconstruct empty block \ - for slot={}: {}", - session_hex, - slot, - e - ); - } - } - } - - // 5. Not in memory, DB, or reconstructable: return notar-only if available (partial merge). - log::debug!( - "Session {} candidate_query_fallback: block NOT FOUND for slot={} hash={}, \ - returning notar_only={}", - session_hex, - slot, - &block_hash.to_hex_string()[..8], - !notar_bytes.is_empty() - ); - Self::send_candidate_and_cert_response(Vec::new(), notar_bytes, response_callback); - } + let listener = self.listener.clone(); - /// Load CandidateInfoRecord from DB (blocking, used for rare query fallback). - fn load_candidate_info_from_db( - &self, - candidate_id: &RawCandidateId, - ) -> Option { - const DB_TIMEOUT: Duration = Duration::from_secs(2); + self.invoke_session_callback(move || { + check_execution_time!(20_000); - match self.db.load_candidate_info_by_id(candidate_id, DB_TIMEOUT) { - Ok(record) => record, - Err(e) => { - log::warn!( - "Session {} load_candidate_info_from_db: failed for slot={}: {}", - &self.session_id().to_hex_string()[..8], - candidate_id.slot, - e + if let Some(listener) = listener.upgrade() { + log::trace!( + "SessionProcessor::notify_get_approved_candidate: get_approved_candidate start" ); - None - } - } - } - /// Load notar cert bytes from DB (blocking, used for rare query fallback). - fn load_notar_cert_bytes_from_db(&self, candidate_id: &RawCandidateId) -> Vec { - const DB_TIMEOUT: Duration = Duration::from_secs(2); + listener.get_approved_candidate( + source, + root_hash, + file_hash, + collated_data_hash, + callback, + ); - match self.db.load_notar_cert_by_id(candidate_id, DB_TIMEOUT) { - Ok(Some(record)) => record.notar_cert_bytes, - Ok(None) => Vec::new(), - Err(e) => { - log::debug!( - "Session {} load_notar_cert_bytes_from_db: failed for slot={}: {}", - &self.session_id().to_hex_string()[..8], - candidate_id.slot, - e + log::trace!( + "SessionProcessor::notify_get_approved_candidate: get_approved_candidate finish" ); - Vec::new() } - } - } - - /// Build and send CandidateAndCert response. - fn send_candidate_and_cert_response( - candidate_bytes: Vec, - notar_bytes: Vec, - response_callback: crate::QueryResponseCallback, - ) { - use consensus_common::ConsensusCommonFactory; - - let response = - CandidateAndCert { candidate: candidate_bytes.into(), notar: notar_bytes.into() }; - - let result = match serialize_boxed(&response.into_boxed()) { - Ok(bytes) => Ok(ConsensusCommonFactory::create_block_payload(bytes)), - Err(e) => Err(error!("Failed to serialize fallback response: {}", e)), - }; - response_callback(result); - } - - /// Send empty CandidateAndCert response (when fallback has nothing to return). - fn send_empty_candidate_response(response_callback: crate::QueryResponseCallback) { - Self::send_candidate_and_cert_response(Vec::new(), Vec::new(), response_callback); - } - - /// Reconstruct CandidateData::Consensus_Empty bytes from CandidateInfoRecord. - fn reconstruct_empty_candidate_data_from_info( - &self, - candidate_id: &RawCandidateId, - candidate_info: &crate::database::CandidateInfoRecord, - ) -> Result> { - let parent_id = match &candidate_info.candidate_hash_data { - CandidateHashData::Consensus_CandidateHashDataEmpty(empty) => { - let slot = SlotIndex(empty.parent.slot as u32); - let hash = empty.parent.hash.clone(); - (slot, hash) - } - _ => return Err(error!("Expected empty hash data")), - }; - - let block_id = if let Some(rc) = self.received_candidates.get(candidate_id) { - rc.block_id.clone() - } else { - return Err(error!( - "Cannot reconstruct empty block: no block_id available for slot={}", - candidate_id.slot - )); - }; - - let parent = - CandidateId { slot: parent_id.0.value() as i32, hash: parent_id.1 }.into_boxed(); - - let tl_empty = CandidateDataEmpty { - slot: candidate_id.slot.value() as i32, - parent, - block: block_id, - signature: candidate_info.signature.clone(), - }; - - let candidate_data = CandidateData::Consensus_Empty(tl_empty); - serialize_boxed(&candidate_data) - .map_err(|e| error!("Failed to serialize empty CandidateData: {}", e)) + }); } } @@ -9938,7 +9157,6 @@ impl SessionStartupRecoveryListener for SessionProcessor { let mut dropped_finalized = 0u32; let mut dropped_skipped = 0u32; let mut dropped_notarization = 0u32; - let mut dropped_skip_cert_reached = 0u32; let mut dropped_finalization_reached = 0u32; while let Some(event) = self.simplex_state.pull_event() { @@ -9956,9 +9174,11 @@ impl SessionStartupRecoveryListener for SessionProcessor { dropped_notarization += 1; } SimplexEvent::SkipCertificateReached(_) => { - dropped_skip_cert_reached += 1; + // Skip certificate events during recovery - they'll be regenerated + dropped_skipped += 1; } SimplexEvent::FinalizationReached(_) => { + // Finalization reached events during recovery - they'll be regenerated dropped_finalization_reached += 1; } } @@ -9967,7 +9187,6 @@ impl SessionStartupRecoveryListener for SessionProcessor { log::info!( "Session {}: drained startup events: kept {} votes, dropped {dropped_finalized} \ finalized, {dropped_skipped} skipped, {dropped_notarization} notarization, \ - {dropped_skip_cert_reached} skip_cert_reached, \ {dropped_finalization_reached} finalization_reached", self.session_id().to_hex_string(), kept_votes.len(), @@ -10123,7 +9342,7 @@ impl SessionStartupRecoveryListener for SessionProcessor { ) { log::info!( target: "startup_recovery", - "Session {}: last finalized notification on restart: slot={}, seqno={}, hash={}", + "Session {}: CERT-1 last finalized notification: slot={}, seqno={}, hash={}", self.session_id().to_hex_string(), slot.value(), seqno, @@ -10157,7 +9376,6 @@ impl SessionStartupRecoveryListener for SessionProcessor { self.last_committed_seqno = Some(seqno); self.last_committed_slot = Some(slot); self.last_committed_block_id = Some(block_id.clone()); - self.last_consensus_finalized_seqno = Some(seqno); // Note: We do NOT set available_base here anymore. This is now done in // recovery_finalize_parent_chain() after all kept votes are restored, @@ -10454,6 +9672,43 @@ impl SessionStartupRecoveryListener for SessionProcessor { ); } + fn recovery_notify_get_approved_candidate( + &self, + source: crate::PublicKey, + root_hash: crate::BlockHash, + file_hash: crate::BlockHash, + collated_data_hash: crate::BlockHash, + callback: Box) + Send>, + ) { + log::trace!( + "Session {}: recovery_notify_get_approved_candidate(root_hash={})", + self.session_id().to_hex_string(), + root_hash.to_hex_string() + ); + // IMPORTANT (restart / wait_for_db_init): + // + // Startup recovery may need to synchronously fetch approved candidates (Step 10 / Step 11) + // while `SessionImpl::create()` is still blocked waiting for initialization to complete. + // In that mode, the callbacks thread is NOT started yet, so posting into the callbacks + // queue would deadlock (startup recovery waits for the callback result). + // + // Therefore, call the session listener directly here (bypassing callback queue). + // This is only used by startup recovery via `SessionStartupRecoveryListener`. + if let Some(listener) = self.listener.upgrade() { + listener.get_approved_candidate( + source, + root_hash, + file_hash, + collated_data_hash, + callback, + ); + } else { + callback(Err(error!( + "recovery_notify_get_approved_candidate: session listener dropped" + ))); + } + } + fn recovery_apply_restart_recommit_actions( &mut self, actions: &[RestartRoundAction], diff --git a/src/node/simplex/src/simplex_state.rs b/src/node/simplex/src/simplex_state.rs index 9cc05a3..1985918 100644 --- a/src/node/simplex/src/simplex_state.rs +++ b/src/node/simplex/src/simplex_state.rs @@ -6,7 +6,6 @@ * * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ - //! SimplexState - Core Consensus State Machine //! //! This module implements the core consensus state machine based on: @@ -257,16 +256,6 @@ use crate::{ session_description::SessionDescription, RawVoteData, ValidatorWeight, }; - -/// Maximum number of slots ahead of `first_non_finalized_slot` that the FSM -/// will accept. Any vote, candidate, or certificate referencing a slot beyond -/// this horizon is rejected to prevent a Byzantine validator from triggering -/// unbounded window/slot allocation (DoS). -/// -/// Note: C++ has no equivalent cap. This is a Rust-only defense-in-depth measure. -/// 10,000 is generous enough to never affect liveness under normal conditions. -pub const MAX_FUTURE_SLOTS: u32 = 10_000; - use std::{ cmp, collections::{BinaryHeap, HashMap, HashSet, VecDeque}, @@ -277,6 +266,12 @@ use std::{ }; use ton_block::{error, fail, BlockIdExt, Result, UInt256}; +/// Maximum number of slots ahead of `first_non_finalized_slot` that the FSM +/// will accept. Any vote, candidate, or certificate referencing a slot beyond +/// this horizon is rejected to prevent a Byzantine validator from triggering +/// unbounded window/slot allocation (DoS). C++ parity: commit 4c5eda7c. +pub const MAX_FUTURE_SLOTS: u32 = 10_000; + /* ============================================================================ SimplexState Options @@ -403,6 +398,12 @@ impl SimplexStateOptions { /// Reference: C++ pool.cpp TooManyFallbackVotesMisbehaviorProof (>3 = misbehavior) const MAX_NOTAR_FALLBACK_VOTES_PER_VALIDATOR: usize = 3; +/// Default timeout for first block in window +const DEFAULT_FIRST_BLOCK_TIMEOUT: Duration = Duration::from_secs(3); + +/// Default target rate between blocks +const DEFAULT_TARGET_RATE_TIMEOUT: Duration = Duration::from_secs(1); + /* ============================================================================ Vote Types for FSM @@ -579,6 +580,12 @@ pub struct NotarizationReachedEvent { pub block_hash: UInt256, /// Notarization certificate with signatures pub certificate: NotarCertPtr, + /// Whether SessionProcessor should broadcast the full certificate to peers. + /// + /// Some notarization certificates are learned via direct query/repair paths + /// (e.g. requestCandidate response). In such cases we still need to update + /// local caches/DB, but re-broadcasting may be undesirable. + pub should_broadcast: bool, } /// Event: Skip certificate threshold reached for a slot @@ -595,6 +602,8 @@ pub struct SkipCertificateReachedEvent { pub slot: SlotIndex, /// Skip certificate with signatures pub certificate: SkipCertPtr, + /// Whether SessionProcessor should broadcast the full certificate to peers. + pub should_broadcast: bool, } /// Event: Finalization threshold reached for a block @@ -616,6 +625,12 @@ pub struct FinalizationReachedEvent { pub block_hash: UInt256, /// Finalization certificate with signatures pub certificate: FinalCertPtr, + /// Whether SessionProcessor should broadcast the full certificate to peers. + /// + /// C++ parity (commit `cfd8850c`): `handle_our_certificate()` broadcasts + /// locally-created certificates. Set to `true` for local creation, `false` + /// for external ingest (avoids relay amplification). + pub should_broadcast: bool, } /// Events produced by SimplexState @@ -627,7 +642,7 @@ pub struct FinalizationReachedEvent { /// - `SlotSkipped` โ†’ Notify SessionListener::on_block_skipped /// - `NotarizationReached` โ†’ Cache serialized notarization certificate in receiver /// - `SkipCertificateReached` โ†’ Broadcast skip certificate to validators (C++ mode only) -/// - `FinalizationReached` โ†’ Cache finalization certificate and relay to peers +/// - `FinalizationReached` โ†’ Cache finalization certificate; broadcast when `should_broadcast` is true #[derive(Clone, Debug)] pub enum SimplexEvent { /// Broadcast a vote to all validators @@ -660,8 +675,10 @@ pub enum SimplexEvent { /// A finalization threshold was reached (certificate created) /// - /// Reference: C++ ConsensusBus::FinalizationObserved / handle_saved_certificate - /// Caches finalization certificate for standstill replay and relays to peers. + /// Reference: C++ ConsensusBus::FinalizationObserved / handle_our_certificate + /// Caches finalization certificate for standstill replay; broadcasts for locally-created + /// certificates only (`should_broadcast = true`). Externally ingested certificates are + /// cached without broadcast (`should_broadcast = false`). FinalizationReached(FinalizationReachedEvent), } @@ -711,16 +728,6 @@ struct Slot { /// - restore local skip state on restart (bootstrap) voted_skip: bool, - /// Have we voted to finalize this slot? - /// - /// Reference: C++ `struct SlotState` in `consensus.cpp` (`voted_final`). - /// - /// This is a **local** flag (our node only). In C++, `alarm()` checks - /// `!voted_final` before voting skip โ€” once a node votes final, it cannot - /// vote skip for that slot. This prevents split-brain deadlocks where some - /// nodes vote skip and others vote final, neither reaching the 67% threshold. - voted_final: bool, - /// Observed notarization certificate for a block /// Alpenglow: BlockNotarized(hash(b)) โˆˆ state[s] observed_notar_certificate: Option, @@ -1402,10 +1409,6 @@ pub(crate) struct SimplexState { /// SimplexState options (fallback protocol, etc.) opts: SimplexStateOptions, - - /// Throttle counter for `ensure_window_exists` rejection warnings. - /// Prevents log flooding when standstill re-broadcasts reference far-future windows. - window_reject_count: u64, } impl SimplexState { @@ -1446,11 +1449,11 @@ impl SimplexState { // Validate parameters at construction time if slots_per_window == 0 { - fail!("SimplexState::new: slots_per_leader_window must be > 0"); + fail!("SimplexState::new: slots_per_leader_window must be > 0") } if num_validators == 0 { - fail!("SimplexState::new: num_validators must be > 0"); + fail!("SimplexState::new: num_validators must be > 0") } log::trace!( @@ -1460,8 +1463,8 @@ impl SimplexState { opts ); - let first_block_timeout = desc.opts().first_block_timeout; - let target_rate_timeout = desc.opts().target_rate; + let first_block_timeout = desc.opts().first_block_timeout.max(DEFAULT_FIRST_BLOCK_TIMEOUT); + let target_rate_timeout = desc.opts().target_rate.max(DEFAULT_TARGET_RATE_TIMEOUT); let mut state = Self { events: VecDeque::new(), @@ -1480,7 +1483,6 @@ impl SimplexState { target_rate_timeout, slots_per_leader_window: slots_per_window, opts, - window_reject_count: 0, }; // Initialize first window with genesis (None) as available base @@ -1493,11 +1495,9 @@ impl SimplexState { // (available_base is optional-of-optional; RawParentId{} = nullopt = genesis) window.slots[0].available_base = Some(None); - // Timeouts are NOT armed here. The FSM starts with skip_timestamp=None - // so that no skip cascade fires before the session is actually started. - // SessionProcessor::start() calls set_timeouts() at the correct moment - // (after overlay warmup and bootstrap recovery), matching C++ where - // timeouts are only armed after the Start event. + // Set initial timeouts + // Reference: C++ start_up() โ†’ set_timeouts(window) + state.set_timeouts(desc); Ok(state) } @@ -1558,16 +1558,10 @@ impl SimplexState { let max_slot = self.first_non_finalized_slot.value() + MAX_FUTURE_SLOTS; let max_window = WindowIndex(max_slot / self.slots_per_leader_window + 1); if idx > max_window { - self.window_reject_count += 1; - if self.window_reject_count <= 3 || self.window_reject_count % 10000 == 0 { - log::warn!( - "SimplexState::ensure_window_exists: REJECTED window {} > max {} \ - (defense-in-depth, occurrence #{})", - idx, - max_window, - self.window_reject_count, - ); - } + log::warn!( + "SimplexState::ensure_window_exists: REJECTED window {} > max {} (defense-in-depth)", + idx, max_window + ); return; } @@ -1761,7 +1755,6 @@ impl SimplexState { // C++: slot->state->voted_final = true window.slots[offset].is_voted = true; window.slots[offset].its_over = true; - window.slots[offset].voted_final = true; log::trace!( "SimplexState::mark_slot_voted_on_restart: slot {} marked voted_final=true", slot.value() @@ -1913,11 +1906,6 @@ impl SimplexState { /// /// Used in unit tests to satisfy SessionProcessor assertions when /// injecting events without running full FSM vote accumulation. - #[cfg(test)] - pub fn try_skip_window_for_test(&mut self, window_idx: WindowIndex) { - self.try_skip_window(window_idx); - } - #[cfg(test)] pub fn set_first_non_finalized_slot_for_test(&mut self, slot: SlotIndex) { self.first_non_finalized_slot = slot; @@ -2194,7 +2182,7 @@ impl SimplexState { /// for i โˆˆ windowSlots(s) do // set timeouts for all slots /// schedule event Timeout(i) at time clock()+ฮ”timeout+(iโˆ’s+1)ยทฮ”block /// ``` - pub(crate) fn set_timeouts(&mut self, desc: &SessionDescription) { + fn set_timeouts(&mut self, desc: &SessionDescription) { let window_start = self.current_leader_window_idx * self.slots_per_leader_window; self.skip_slot = window_start; @@ -2202,25 +2190,21 @@ impl SimplexState { self.skip_timestamp = Some(desc.get_time() + self.first_block_timeout + self.target_rate_timeout); - log::warn!( - "SimplexState::set_timeouts: ({}/{}) scheduling timeout in {:.3}s \ - (first_block={:.3}s, target_rate={:.3}s)", + log::trace!( + "SimplexState::set_timeouts: ({}/{}) scheduling timeout in {:.3}s", self.current_leader_window_idx, self.skip_slot, - (self.first_block_timeout + self.target_rate_timeout).as_secs_f64(), - self.first_block_timeout.as_secs_f64(), - self.target_rate_timeout.as_secs_f64(), + (self.first_block_timeout + self.target_rate_timeout).as_secs_f64() ); } /// Restore default timeouts (reset adaptive backoff) fn restore_default_timeouts(&mut self, desc: &SessionDescription) { - self.target_rate_timeout = desc.opts().target_rate; - self.first_block_timeout = desc.opts().first_block_timeout; + self.target_rate_timeout = desc.opts().target_rate.max(DEFAULT_TARGET_RATE_TIMEOUT); + self.first_block_timeout = desc.opts().first_block_timeout.max(DEFAULT_FIRST_BLOCK_TIMEOUT); log::trace!( - "SimplexState::restore_default_timeouts: reset to first_block={:.3}s, \ - target_rate={:.3}s", + "SimplexState::restore_default_timeouts: reset to first_block={:.3}s, target_rate={:.3}s", self.first_block_timeout.as_secs_f64(), self.target_rate_timeout.as_secs_f64() ); @@ -2297,11 +2281,9 @@ impl SimplexState { // Check if we should skip the timeout: // - Alpenglow (enable_fallback_protocol=true): Check is_voted (any vote blocks skip) - // - C++ compatible (enable_fallback_protocol=false): Check voted_final OR voted_skip + // - C++ compatible (enable_fallback_protocol=false): Check its_over (only finalize blocks skip) // - // C++ alarm() checks voted_final and fires once per window (one-shot alarm). - // Rust process_timeouts fires per-slot, so we must also check voted_skip to - // prevent repeated skip vote broadcasts for the same window. + // C++ alarm() only checks voted_final, allowing skip after notarize. // Reference: C++ consensus.cpp alarm(): if (!affected_slot->voted_final) let should_skip_timeout = { let window = self.get_window(window_idx); @@ -2310,10 +2292,8 @@ impl SimplexState { // Alpenglow: Any vote blocks timeout (Voted โˆˆ state[s]) window.slots[offset].is_voted } else { - // C++: voted_final or voted_skip blocks timeout. - // C++ alarm is one-shot so only checks voted_final, but Rust fires - // per-slot so we also check voted_skip to avoid re-broadcasting. - window.slots[offset].voted_final || window.slots[offset].voted_skip + // C++: Only finalize blocks timeout + window.slots[offset].its_over } } else { continue; @@ -2343,31 +2323,6 @@ impl SimplexState { // Alpenglow: trySkipWindow(s) self.try_skip_window(window_idx); - - // C++ compatibility: skip entire remaining window at once, then BREAK. - // Reference: C++ consensus.cpp alarm() lines 120-133: - // C++ fires alarm once and skips ALL remaining slots in the window, - // then sets timeout_slot_ = window_end and reschedules. - // Between alarm firings, incoming events (NotarizationObserved, - // skip certs from peers) can advance timeout_slot_ past active slots. - // We break after one window to give incoming events a chance to - // advance skip_slot before we vote skip for more slots. - if !self.opts.enable_fallback_protocol { - let window_end_slot = (window_idx + 1) * self.slots_per_leader_window; - if self.skip_slot < window_end_slot { - log::debug!( - "SimplexState::process_timeouts: C++ window skip: \ - advancing skip_slot {} -> {} (window_end)", - self.skip_slot, - window_end_slot - ); - self.skip_slot = window_end_slot; - } - // Schedule next timeout at target_rate from now (not accumulated) - skip_timestamp = desc.get_time() + self.target_rate_timeout; - self.skip_timestamp = Some(skip_timestamp); - break; - } } } } @@ -2398,12 +2353,8 @@ impl SimplexState { let factor = desc.opts().timeout_increase_factor; let max_delay = desc.opts().max_backoff_delay; - // Only back off first_block_timeout, not target_rate_timeout. - // C++ reference (consensus.cpp:98-99) only backs off first_block_timeout_s_, - // keeping target_rate_s_ constant. Backing off target_rate causes the full - // rotation of 16 slots to take 16s instead of 8s, making blocks from remote - // leaders arrive after the skip timeout and preventing finalization. self.first_block_timeout = (self.first_block_timeout.mul_f64(factor)).min(max_delay); + self.target_rate_timeout = (self.target_rate_timeout.mul_f64(factor)).min(max_delay); log::trace!( "{}: ({}/{}) adaptive backoff applied -> first={:.3}s target={:.3}s", @@ -2471,19 +2422,15 @@ impl SimplexState { ); fail!( "SimplexState::on_candidate: invalid leader {} (max={}), dropping candidate for slot {}", - leader, - self.num_validators, - slot - ); + leader, self.num_validators, slot + ) } // Ignore finalized slots (not an error, just skip) if slot < self.first_non_finalized_slot { log::trace!( "SimplexState::on_candidate: ({}/{}) slot already finalized (first_non_finalized={}), ignoring", - window_idx, - slot, - self.first_non_finalized_slot + window_idx, slot, self.first_non_finalized_slot ); return Ok(()); } @@ -2499,33 +2446,42 @@ impl SimplexState { return Ok(()); } - // C++ consensus.cpp: if parent exists, parent_slot must be < candidate_slot - if let Some(ref parent) = candidate.parent_id { - if parent.slot >= slot { - fail!( - "SimplexState::on_candidate: MISBEHAVIOR: parent slot {} >= candidate slot {}", - parent.slot, - slot - ); - } + // Validate: non-first slot must have parent + // Reference: C++ handle CandidateReceived + let is_first = desc.is_first_in_window(slot); + if !is_first && candidate.parent_id.is_none() { + log::trace!( + "SimplexState::on_candidate: ({}/{}) non-first slot without parent, MISBEHAVIOR", + window_idx, + slot + ); + fail!( + "SimplexState::on_candidate: MISBEHAVIOR: Dropping candidate {:?} for slot {} which builds upon genesis but is not first in window", + candidate.id, slot + ) } // Convert parent to CandidateParent for matching + // Note: For first slot in window, parent can be None (genesis) let parent: CandidateParent = candidate .parent_id .as_ref() .map(|p| CandidateParentInfo { slot: p.slot, hash: p.hash.clone() }); // Save candidate hash -> CandidateId mapping for BlockFinalizedEvent + // This allows us to provide full CandidateId (including seqno) when finalizing self.candidate_ids.insert(candidate.id.hash.clone(), candidate.id.clone()); log::trace!( - "SimplexState::on_candidate: slot={}, parent={:?}, calling try_notar", + "SimplexState::on_candidate: slot={}, is_first={}, parent={:?}, calling try_notar", slot, + is_first, parent ); // Alpenglow: if tryNotar(Block(s, hash, hashparent)) then + // Note: We use candidate.id.hash (candidate hash) not block.root_hash + // The candidate hash is what's used in votes and for consensus identification if self.try_notar(desc, slot, &candidate.id.hash, parent.as_ref()) { log::trace!( "SimplexState::on_candidate: ({}/{}) try_notar succeeded, checking pending blocks", @@ -2541,46 +2497,26 @@ impl SimplexState { self.ensure_window_exists(window_idx); - // C++ consensus.cpp CandidateReceived only gates on voted_notar (line 170), - // NOT voted_skip. A local skip vote must NOT prevent storing a candidate as - // pending โ€” the pending retry (`check_pending_blocks`) will notarize it once - // the parent base propagates through skip certs. - // - // Alpenglow uses the stricter `is_voted` (any local vote blocks storage). - let dominated = if let Some(window) = self.get_window(window_idx) { - if self.opts.enable_fallback_protocol { - window.slots[offset].is_voted - } else { - window.slots[offset].voted_notar.is_some() - } + let is_voted = if let Some(window) = self.get_window(window_idx) { + window.slots[offset].is_voted } else { false }; - if !dominated { + if !is_voted { log::trace!( "SimplexState::on_candidate: ({}/{}) try_notar=false, storing as pending block", window_idx, slot ); - // C++ parity: first pending candidate wins. If a pending block already - // exists for this slot, reject any different candidate (equivocation). + // Alpenglow: pendingBlocks[s] โ† Block(s, hash, hashparent) if let Some(window) = self.get_window_mut(window_idx) { - if let Some(ref existing) = window.slots[offset].pending_block { - if existing.id.hash != candidate.id.hash { - log::warn!( - "SimplexState::on_candidate: ({window_idx}/{slot}) \ - pending_block already set with different hash, ignoring" - ); - } - return Ok(()); - } window.slots[offset].pending_block = Some(candidate); self.pending_slots.push(PendingSlot(slot)); } } else { log::trace!( - "SimplexState::on_candidate: ({}/{}) already notarized, ignoring candidate", + "SimplexState::on_candidate: ({}/{}) already voted, ignoring candidate", window_idx, slot ); @@ -2918,13 +2854,11 @@ impl SimplexState { slot_votes.notarize_weight_by_block.get(&vote.block_hash).copied().unwrap_or(0); let total_weight = desc.get_total_weight(); log::trace!( - "SimplexState::handle_notarize_vote: ({window_idx}/{slot}) {validator_idx}+{weight} \ - -> notar={total_notar}({:.0}%) n|s={}({:.0}%) for {}:{}", - 100.0 * total_notar as f64 / total_weight as f64, - slot_votes.notarize_or_skip_weight, - 100.0 * slot_votes.notarize_or_skip_weight as f64 / total_weight as f64, - slot, - &vote.block_hash.to_hex_string()[..8] + "SimplexState::handle_notarize_vote: ({}/{}) {} +{} -> notar={}({:.0}%) n|s={}({:.0}%) for {}:{}", + window_idx, slot, validator_idx, weight, + total_notar, 100.0 * total_notar as f64 / total_weight as f64, + slot_votes.notarize_or_skip_weight, 100.0 * slot_votes.notarize_or_skip_weight as f64 / total_weight as f64, + slot, &vote.block_hash.to_hex_string()[..8] ); } @@ -2992,8 +2926,8 @@ impl SimplexState { // When allow_skip_after_notarize=false (Alpenglow strict mode): // Skip + Notarize is MISBEHAVIOR (in Alpenglow, once you vote skip // you shouldn't also vote notarize for the same slot) - if !allow_skip_after_notarize && votes.notarize.is_some() { - let existing_notar = votes.notarize.as_ref().unwrap(); + if let (true, Some(existing_notar)) = (!allow_skip_after_notarize, votes.notarize.as_ref()) + { log::trace!( "SimplexState::handle_skip_vote: ({}/{}) {} has notarize, rejecting skip", window_idx, @@ -3290,9 +3224,7 @@ impl SimplexState { if let Some(ref finalize) = votes.finalize { log::trace!( "SimplexState::handle_notar_fallback_vote: ({}/{}) {} has finalize, rejecting notar-fb", - window_idx, - slot, - validator_idx + window_idx, slot, validator_idx ); // Use stored raw bytes from existing finalize vote and new raw bytes for proof let existing_raw = votes.finalize_raw.clone().unwrap_or_default(); @@ -3393,9 +3325,7 @@ impl SimplexState { if let Some(ref finalize) = votes.finalize { log::trace!( "SimplexState::handle_skip_fallback_vote: ({}/{}) {} has finalize, rejecting skip-fb", - window_idx, - slot, - validator_idx + window_idx, slot, validator_idx ); // Use stored raw bytes from existing finalize vote and new raw bytes for proof let existing_raw = votes.finalize_raw.clone().unwrap_or_default(); @@ -3512,10 +3442,7 @@ impl SimplexState { Ok(true) => { log::trace!( "SimplexState::check_thresholds: ({}/{}) cached notarization cert for {}:{}", - window_idx, - slot_id, - slot_id, - &block.to_hex_string()[..8] + window_idx, slot_id, slot_id, &block.to_hex_string()[..8] ); // Emit event for session processor to cache serialized cert in receiver self.push_event_back(SimplexEvent::NotarizationReached( @@ -3523,6 +3450,7 @@ impl SimplexState { slot: slot_id, block_hash: block.clone(), certificate: cert, + should_broadcast: true, }, )); } @@ -3558,14 +3486,9 @@ impl SimplexState { { log::trace!( "SimplexState::check_thresholds: ({}/{}) SAFE_TO_NOTAR {}:{} notar={}({:.0}%) skip+notar={}({:.0}%)", - window_idx, - slot_id, - slot_id, - &block.to_hex_string()[..8], - weight, - 100.0 * *weight as f64 / total_weight as f64, - skip_plus_notar_b, - 100.0 * skip_plus_notar_b as f64 / total_weight as f64 + window_idx, slot_id, slot_id, &block.to_hex_string()[..8], + weight, 100.0 * *weight as f64 / total_weight as f64, + skip_plus_notar_b, 100.0 * skip_plus_notar_b as f64 / total_weight as f64 ); if let Some(sv) = self.slot_votes.get_mut(&slot_id) { @@ -3596,10 +3519,8 @@ impl SimplexState { { log::trace!( "SimplexState::check_thresholds: ({}/{}) SAFE_TO_SKIP n|s={}({:.0}%) max_notar={}", - window_idx, - slot_id, - notarize_or_skip_weight, - 100.0 * notarize_or_skip_weight as f64 / total_weight as f64, + window_idx, slot_id, + notarize_or_skip_weight, 100.0 * notarize_or_skip_weight as f64 / total_weight as f64, max_notarize ); @@ -3653,10 +3574,7 @@ impl SimplexState { } else if is_new_cert { log::trace!( "SimplexState::check_thresholds: ({}/{}) cached finalization cert for {}:{}", - window_idx, - slot_id, - slot_id, - &block.to_hex_string()[..8] + window_idx, slot_id, slot_id, &block.to_hex_string()[..8] ); } @@ -3681,6 +3599,7 @@ impl SimplexState { slot: slot_id, block_hash: block.clone(), certificate, + should_broadcast: true, }, )); } @@ -3691,9 +3610,7 @@ impl SimplexState { if slot_id >= self.first_non_finalized_slot { log::trace!( "SimplexState::check_thresholds: ({}/{}) advancing first_non_finalized to {}", - window_idx, - slot_id, - slot_id + 1 + window_idx, slot_id, slot_id + 1 ); self.first_non_finalized_slot = slot_id + 1; @@ -3703,9 +3620,7 @@ impl SimplexState { log::trace!( "SimplexState::check_thresholds: ({}/{}) advanced first_non_progressed_slot to {} (finalized boundary)", - window_idx, - slot_id, - self.first_non_progressed_slot + window_idx, slot_id, self.first_non_progressed_slot ); } } @@ -3721,8 +3636,7 @@ impl SimplexState { if missing_notar { log::trace!( "SimplexState::check_thresholds: ({}/{}) finalized without prior notarization, recording cert", - window_idx, - slot_id + window_idx, slot_id ); if let Some(s) = self.get_slot_mut(desc, slot_id) { s.observed_notar_certificate = Some(parent_info.clone()); @@ -3759,11 +3673,7 @@ impl SimplexState { log::trace!( "SimplexState::check_thresholds: ({}/{}) triggering ParentReady for {} parent={}:{}", - window_idx, - slot_id, - next_window_idx, - slot_id, - &block.to_hex_string()[..8] + window_idx, slot_id, next_window_idx, slot_id, &block.to_hex_string()[..8] ); // Call on_window_base_ready to handle all the logic: @@ -3851,7 +3761,11 @@ impl SimplexState { if !self.opts.enable_fallback_protocol { if let Some(cert) = skip_cert { self.push_event_back(SimplexEvent::SkipCertificateReached( - SkipCertificateReachedEvent { slot: slot_id, certificate: cert }, + SkipCertificateReachedEvent { + slot: slot_id, + certificate: cert, + should_broadcast: true, + }, )); } } @@ -3875,23 +3789,34 @@ impl SimplexState { self.current_leader_window_idx, self.first_non_progressed_slot ); - // C++ parity: skip certificates do NOT advance first_non_finalized_slot. - // Only finalization advances it (see C++ state.h notify_finalized()). - // However, the progress cursor (first_non_progressed_slot, C++ `now_`) - // DOES advance on skip -- it tracks notarized-or-skipped progress. - // Only advance sequentially to avoid jumping past unresolved earlier slots. - if slot_id == self.first_non_progressed_slot { - self.first_non_progressed_slot = slot_id + 1; - log::trace!( - "SimplexState::check_thresholds: ({window_idx}/{slot_id}) \ - advanced first_non_progressed_slot to {} (skip)", - self.first_non_progressed_slot - ); - } - if self.opts.use_notarized_parent_chain { + // Advance first_non_finalized_slot only sequentially to avoid + // jumping past unresolved earlier slots whose votes would be rejected. + if slot_id == self.first_non_finalized_slot { + log::trace!( + "SimplexState::check_thresholds: ({}/{}) advancing first_non_finalized to {} (skip)", + window_idx, + slot_id, + slot_id + 1 + ); + self.first_non_finalized_slot = slot_id + 1; + } self.advance_leader_window_on_progress_cursor(desc); } else { + // Legacy mode: advance first_non_finalized_slot and progress cursor. + if slot_id >= self.first_non_finalized_slot { + log::trace!( + "SimplexState::check_thresholds: ({}/{}) advancing first_non_finalized to {} (skip)", + window_idx, + slot_id, + slot_id + 1 + ); + self.first_non_finalized_slot = slot_id + 1; + } + if self.first_non_finalized_slot > self.first_non_progressed_slot { + self.first_non_progressed_slot = self.first_non_finalized_slot; + } + // Check if this is the last slot in the window BEFORE cleanup // If so, and if no block was finalized in this window, we need to // propagate the available bases to the next window (including genesis/None) @@ -3943,11 +3868,11 @@ impl SimplexState { self.on_window_base_ready(desc, next_window_idx, parent.clone()) { log::error!( - "SimplexState: SlotSkipped failed to propagate parent {:?} to window {}: {}", - parent, - next_window_idx, - e - ); + "SimplexState: SlotSkipped failed to propagate parent {:?} to window {}: {}", + parent, + next_window_idx, + e + ); } } } @@ -4027,42 +3952,6 @@ impl SimplexState { self.advance_leader_window_on_progress_cursor(desc); } - // C++ compatibility: advance skip timer when NotarCert arrives - // Reference: C++ consensus.cpp lines 228-243 (NotarizationObserved handler) - // When a NotarCert is observed, C++ advances timeout_slot_ to slot+1 and - // reschedules the alarm to now + target_rate. This prevents the skip cascade - // from racing ahead of active block production. - // - // Important: do NOT shrink skip_timestamp below the current scheduled value. - // During the first_block_timeout window, the skip timer is intentionally set - // far in the future to give all nodes time to join the overlay. Setting it to - // now + target_rate here would bypass that protection entirely. - if !self.opts.enable_fallback_protocol { - let next_slot = slot + 1; - if self.skip_slot <= next_slot { - let new_timestamp = desc.get_time() + self.target_rate_timeout; - // Only update skip_timestamp if it would be later than current, - // preserving the first_block_timeout window. - let effective_timestamp = match self.skip_timestamp { - Some(current) if current > new_timestamp => current, - _ => new_timestamp, - }; - log::debug!( - "SimplexState::on_block_notarized: advancing skip timer: \ - skip_slot {} -> {next_slot}, new timeout in {:?}{}", - self.skip_slot, - self.target_rate_timeout, - if effective_timestamp != new_timestamp { - " (preserved first_block_timeout)" - } else { - "" - } - ); - self.skip_slot = next_slot; - self.skip_timestamp = Some(effective_timestamp); - } - } - // Alpenglow: tryFinal(s, hash(b)) self.try_final(desc, slot, &block_hash); } @@ -4105,10 +3994,7 @@ impl SimplexState { // Alpenglow: broadcast NotarFallbackVote(s, hash(b)) log::trace!( "SimplexState::on_safe_to_notar: ({}/{}) broadcasting notar-fb for {}:{}, marking BadWindow", - window_idx, - slot, - slot, - &block_hash.to_hex_string()[..8] + window_idx, slot, slot, &block_hash.to_hex_string()[..8] ); self.broadcast_vote(Vote::NotarizeFallback(NotarizeFallbackVote { slot, block_hash })); @@ -4192,10 +4078,10 @@ impl SimplexState { // Check for potential overflow in window_idx * slots_per_leader_window if window_idx.value().checked_mul(self.slots_per_leader_window).is_none() { fail!( - "SimplexState::on_window_base_ready: \ - w{window_idx} would overflow with {} slots/window", + "SimplexState::on_window_base_ready: w{} would overflow with {} slots/window", + window_idx, self.slots_per_leader_window - ); + ) } // Validate parent slot if present @@ -4204,10 +4090,11 @@ impl SimplexState { let window_start = window_idx.window_start(self.slots_per_leader_window); if parent_info.slot >= window_start { fail!( - "SimplexState::on_window_base_ready: \ - parent s{} >= window start s{start_slot} for w{window_idx}", - parent_info.slot - ); + "SimplexState::on_window_base_ready: parent s{} >= window start s{} for w{}", + parent_info.slot, + start_slot, + window_idx + ) } } @@ -4217,9 +4104,8 @@ impl SimplexState { <= self.first_non_finalized_slot { log::trace!( - "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) ignored: \ - window fully finalized (first_non_finalized={})", - self.first_non_finalized_slot + "SimplexState::on_window_base_ready: ({}/{}) ignored: window fully finalized (first_non_finalized={})", + window_idx, start_slot, self.first_non_finalized_slot ); return Ok(()); } @@ -4227,9 +4113,8 @@ impl SimplexState { // Reject far-future windows (DoS protection) if self.is_slot_too_far_ahead(start_slot) { log::warn!( - "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) REJECTED - \ - window too far ahead (max={})", - self.max_acceptable_slot() + "SimplexState::on_window_base_ready: ({}/{}) REJECTED - window too far ahead (max={})", + window_idx, start_slot, self.max_acceptable_slot() ); return Ok(()); } @@ -4240,8 +4125,9 @@ impl SimplexState { if let Some(window) = self.get_window_mut(window_idx) { let is_new = window.available_bases.insert(parent.clone()); log::trace!( - "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) \ - {} parent={} to available_bases (count={})", + "SimplexState::on_window_base_ready: ({}/{}) {} parent={} to available_bases (count={})", + window_idx, + start_slot, if is_new { "added" } else { "duplicate" }, Self::format_parent(parent.as_ref()), window.available_bases.len() @@ -4257,15 +4143,14 @@ impl SimplexState { .map(|p| CandidateParentInfo { slot: p.slot, hash: p.hash.clone() }); if pending_parent == parent { log::trace!( - "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) \ - pending block {} matched parent, queuing for notarization", - Self::format_block(pending.id.slot, &pending.id.hash) + "SimplexState::on_window_base_ready: ({}/{}) pending block {} matched parent, queuing for notarization", + window_idx, start_slot, Self::format_block(pending.id.slot, &pending.id.hash) ); self.pending_slots.push(PendingSlot(start_slot)); } else { log::trace!( - "SimplexState::on_window_base_ready: ({window_idx}/{start_slot}) \ - pending block {} has different parent (expected={}, got={})", + "SimplexState::on_window_base_ready: ({}/{}) pending block {} has different parent (expected={}, got={})", + window_idx, start_slot, Self::format_block(pending.id.slot, &pending.id.hash), Self::format_parent(parent.as_ref()), Self::format_parent(pending_parent.as_ref()) @@ -4388,11 +4273,8 @@ impl SimplexState { slot_state.is_voted } else { - // C++ parity: only voted_notar gates notarization. C++ try_notarize() - // does NOT check voted_final/its_over โ€” a slot that was finalized on a - // previous run can still be re-notarized after restart (the later - // auto-finalize simply skips re-broadcasting). - slot_state.voted_notar.is_some() + // C++: only notarize/final blocks further notarize (skip is allowed) + slot_state.voted_notar.is_some() || slot_state.its_over }; if already_voted { @@ -4418,8 +4300,10 @@ impl SimplexState { let matches_parent = base_known && expected_parent == candidate_parent; log::trace!( - "SimplexState::try_notar: ({window_idx}/{slot}) notarized-parent chain: \ - base_known={base_known} expected_base={} candidate_parent={} matches={}", + "SimplexState::try_notar: ({}/{}) notarized-parent chain: base_known={} expected_base={} candidate_parent={} matches={}", + window_idx, + slot, + base_known, Self::format_parent(expected_parent.as_ref()), Self::format_parent(parent), matches_parent @@ -4536,37 +4420,15 @@ impl SimplexState { slot_state.voted_notar.as_ref().map(|c| c.hash == *block_hash).unwrap_or(false); // Alpenglow: BadWindow โˆ‰ state[s] - // C++ try_vote_final does NOT check bad_window โ€” it only checks - // voted_skip, voted_final, and voted_notar==notar_cert. - let not_bad_window = if self.opts.enable_fallback_protocol { - !slot_state.is_bad_window - } else { - true // C++ doesn't check bad_window in try_vote_final - }; + let not_bad_window = !slot_state.is_bad_window; let not_its_over = !slot_state.its_over; - // C++: do not auto-finalize if we already voted skip for this slot. + // C++: do not auto-finalize if we already voted skip for this slot // Reference: C++ consensus.cpp: `!voted_skip && !voted_final && voted_notar==id` - // Both modes now match C++ strictly: once voted_skip, never finalize. let not_voted_skip = !slot_state.voted_skip; let result = has_notar_cert && voted_notar && not_bad_window && not_its_over && not_voted_skip; - // Log when finalize is blocked specifically by voted_skip (Alpenglow mode only) - if has_notar_cert && voted_notar && !not_voted_skip { - log::warn!( - "SimplexState::try_final: ({}/{}) FINALIZE BLOCKED by voted_skip! \ - cert={} notar={} bad_window={} its_over={} voted_skip={}", - window_idx, - slot, - has_notar_cert, - voted_notar, - slot_state.is_bad_window, - slot_state.its_over, - slot_state.voted_skip, - ); - } - // Only format debug info if trace logging is enabled if log::log_enabled!(log::Level::Trace) { // Build compact flags string @@ -4670,10 +4532,8 @@ impl SimplexState { })); // Alpenglow: state[s] โ† state[s] โˆช {ItsOver} - // C++: slot->state->voted_final = true if let Some(window) = self.get_window_mut(window_idx) { window.slots[offset].its_over = true; - window.slots[offset].voted_final = true; } } } @@ -4710,11 +4570,8 @@ impl SimplexState { // Alpenglow: if Voted โˆ‰ state[k] then !window.slots[i].is_voted } else { - // C++: if !voted_final โ€” once this node votes final, it cannot - // vote skip. This prevents split-brain deadlocks where some - // nodes vote skip and others vote final. - // Reference: C++ consensus.cpp alarm(): if (!affected_slot->voted_final) - !window.slots[i].voted_final + // C++: if !voted_final (its_over in Rust) + !window.slots[i].its_over }; if should_skip { slots_to_skip.push(start_slot + i as u32); @@ -4726,10 +4583,10 @@ impl SimplexState { return; } - { + if log::log_enabled!(log::Level::Trace) { let slots_str: Vec = slots_to_skip.iter().map(|s| format!("{}", s)).collect(); - log::warn!( - "SimplexState::try_skip_window: ({}) SKIP VOTING for {} slots: [{}]", + log::trace!( + "SimplexState::try_skip_window: ({}) skipping {} unvoted slots: [{}]", window_idx, slots_to_skip.len(), slots_str.join(",") @@ -4748,13 +4605,7 @@ impl SimplexState { window.slots[offset].is_voted = true; window.slots[offset].voted_skip = true; window.slots[offset].is_bad_window = true; - // C++ alarm() only sets voted_skip โ€” it does NOT clear pending_block. - // The async try_notarize() coroutine can still complete after a skip - // vote, producing both Skip and Notar votes for the same slot. - // Only clear pending_block in Alpenglow mode (strict Voted gate). - if enable_fallback { - window.slots[offset].pending_block = None; - } + window.slots[offset].pending_block = None; } } } @@ -5299,16 +5150,16 @@ impl SimplexState { // window/base/progress tracking updates for finalized slots. if slot < first_non_finalized_slot { log::trace!( - "SimplexState::set_notarize_certificate: \ - slot={slot} < first_non_finalized={first_non_finalized_slot} - \ - stored cert without slot tracking" + "SimplexState::set_notarize_certificate: slot={} < first_non_finalized={} - stored cert without slot tracking", + slot, + first_non_finalized_slot ); return Ok(true); } log::trace!( - "SimplexState::set_notarize_certificate: \ - slot={slot} block={} - stored certificate with {} signatures", + "SimplexState::set_notarize_certificate: slot={} block={} - stored certificate with {} signatures", + slot, &block_hash.to_hex_string()[..8], certificate.signatures.len() ); @@ -5320,13 +5171,11 @@ impl SimplexState { // // This mirrors the threshold-driven path (`check_thresholds_and_trigger`) where // notarization threshold stores the cert and emits NotarizationReached. - // C++ parity (pool.cpp handle_saved_certificate): re-gossip every newly - // accepted certificate regardless of origin. SimplexState deduplication - // (returns Ok(false) for already-stored certs) prevents amplification loops. self.push_event_back(SimplexEvent::NotarizationReached(NotarizationReachedEvent { slot, block_hash: block_hash.clone(), certificate: certificate.clone(), + should_broadcast: false, })); // Trigger the same internal FSM transition as the threshold-driven path. @@ -5384,15 +5233,17 @@ impl SimplexState { } Ok(false) => { log::trace!( - "SimplexState::set_finalize_certificate: \ - slot={slot} - certificate already exists, skipping" + "SimplexState::set_finalize_certificate: slot={} - certificate already exists, skipping", + slot ); return Ok(false); } Err(e) => { log::warn!( - "SimplexState::set_finalize_certificate: slot={slot} block={} - {e}", - &block_hash.to_hex_string()[..8] + "SimplexState::set_finalize_certificate: slot={} block={} - {}", + slot, + &block_hash.to_hex_string()[..8], + e ); return Err(e); } @@ -5403,7 +5254,8 @@ impl SimplexState { let idx = vote_sig.validator_idx; if idx.value() as usize >= sv.votes.len() { log::warn!( - "SimplexState::set_finalize_certificate: invalid validator index {idx} >= {}", + "SimplexState::set_finalize_certificate: invalid validator index {} >= {}", + idx, sv.votes.len() ); continue; @@ -5428,8 +5280,8 @@ impl SimplexState { sv.block_finalized_published = true; log::trace!( - "SimplexState::set_finalize_certificate: \ - slot={slot} block={} - stored certificate with {} signatures", + "SimplexState::set_finalize_certificate: slot={} block={} - stored certificate with {} signatures", + slot, &block_hash.to_hex_string()[..8], certificate.signatures.len() ); @@ -5437,9 +5289,9 @@ impl SimplexState { // For old slots, store cert only (no tracking / no events). if is_old_slot { log::trace!( - "SimplexState::set_finalize_certificate: \ - slot={slot} < first_non_finalized={first_non_finalized_slot} - \ - stored cert without slot tracking" + "SimplexState::set_finalize_certificate: slot={} < first_non_finalized={} - stored cert without slot tracking", + slot, + first_non_finalized_slot ); return Ok(true); } @@ -5456,12 +5308,11 @@ impl SimplexState { block_id, certificate: certificate.clone(), })); - // C++ parity (pool.cpp handle_saved_certificate): re-gossip every newly - // accepted certificate regardless of origin. self.push_event_back(SimplexEvent::FinalizationReached(FinalizationReachedEvent { slot, block_hash: block_hash.clone(), certificate: certificate.clone(), + should_broadcast: false, })); // C++ parity (pool.cpp handle_certificate(FinalCertRef)): @@ -5474,8 +5325,8 @@ impl SimplexState { .unwrap_or(true); if missing_notar_marker { log::trace!( - "SimplexState::set_finalize_certificate: slot={slot} block={} -> \ - treat FinalCert as notarization for parent-chain tracking (missing marker)", + "SimplexState::set_finalize_certificate: slot={} block={} -> treat FinalCert as notarization for parent-chain tracking (missing marker)", + slot, &block_hash.to_hex_string()[..8], ); @@ -5488,8 +5339,8 @@ impl SimplexState { } else { // Should not happen for non-old slots; keep trace only (avoid panic in foreign cert ingestion). log::trace!( - "SimplexState::set_finalize_certificate: \ - slot={slot} block={} missing notar marker but slot state is missing", + "SimplexState::set_finalize_certificate: slot={} block={} missing notar marker but slot state is missing", + slot, &block_hash.to_hex_string()[..8], ); } @@ -5497,10 +5348,10 @@ impl SimplexState { self.propagate_base_after_notarization(desc, parent_info.clone()); log::trace!( - "SimplexState::set_finalize_certificate: slot={slot} block={} \ - FinalCert-as-notar applied (observed_marker_set={observed_marker_set}, \ - first_non_progressed_slot={}, first_non_finalized_slot={})", + "SimplexState::set_finalize_certificate: slot={} block={} FinalCert-as-notar applied (observed_marker_set={}, first_non_progressed_slot={}, first_non_finalized_slot={})", + slot, &block_hash.to_hex_string()[..8], + observed_marker_set, self.first_non_progressed_slot, self.first_non_finalized_slot, ); @@ -5577,8 +5428,8 @@ impl SimplexState { } Ok(false) => { log::trace!( - "SimplexState::set_skip_certificate: \ - slot={slot} - certificate already exists, skipping" + "SimplexState::set_skip_certificate: slot={} - certificate already exists, skipping", + slot ); return Ok(false); } @@ -5625,8 +5476,8 @@ impl SimplexState { sv.slot_skipped_published = true; log::trace!( - "SimplexState::set_skip_certificate: \ - slot={slot} - stored certificate with {} signatures", + "SimplexState::set_skip_certificate: slot={} - stored certificate with {} signatures", + slot, certificate.signatures.len() ); @@ -5643,11 +5494,7 @@ impl SimplexState { // Propagate base after skip (C++ pool.cpp parity) self.propagate_base_after_skip_cert(desc, slot); - // C++ parity: skip certificates do NOT advance first_non_finalized_slot. - // Only finalization advances it (C++ state.h notify_finalized()). - // The progress cursor (first_non_progressed_slot) DOES advance on skip. - - // Advance progress cursor + // If notarized-parent chain mode is enabled, advance progress cursor if self.opts.use_notarized_parent_chain { // Advance first_non_progressed_slot if this slot was blocking progress if slot == self.first_non_progressed_slot { @@ -5661,14 +5508,18 @@ impl SimplexState { // skip certificate is created from votes. self.push_event_back(SimplexEvent::SlotSkipped(SlotSkippedEvent { slot })); - // C++ parity (pool.cpp handle_saved_certificate): re-gossip every newly - // accepted certificate regardless of origin. + // Emit SkipCertificateReached for standstill caching; suppress broadcast for externally + // provided certificates (see `should_broadcast` flag). // // SkipCertificateReached is only relevant in C++-compatible mode // (Alpenglow paper does not require explicit skip certificate broadcast). if !self.opts.enable_fallback_protocol { self.push_event_back(SimplexEvent::SkipCertificateReached( - SkipCertificateReachedEvent { slot, certificate: certificate.clone() }, + SkipCertificateReachedEvent { + slot, + certificate: certificate.clone(), + should_broadcast: false, + }, )); } @@ -5853,14 +5704,6 @@ impl SimplexState { /// `if (auto base = slot.state->available_base) next_slot.state->add_available_base(*base);` /// Note: C++ uses `add_available_base` (max-merge), not a conditional assignment. /// - /// C++ also calls `maybe_resolve_requests()` (pool.cpp) after every certificate, - /// which does a backward walk to resolve pending parent-wait requests even if - /// `available_base` was not set on intermediate slots. Rust has no backward walk, - /// so instead we chain the base forward through all consecutive already-skipped - /// slots, ensuring every intermediate slot gets its `available_base` set. This - /// allows `check_pending_blocks` / `try_notar` to find the base for any pending - /// block regardless of skip-cert arrival order. - /// /// This is always called when a slot is skipped, regardless of mode. /// The tracked state is used for progress when `use_notarized_parent_chain` is enabled. fn propagate_base_after_skip_cert(&mut self, desc: &SessionDescription, slot: SlotIndex) { @@ -5874,70 +5717,20 @@ impl SimplexState { ); } - // Chain base forward: propagate slot-by-slot through consecutive already-skipped - // slots. Unlike the previous `find_next_nonskipped_slot` approach which jumped - // directly to the first non-skipped slot (potentially hundreds of slots away), - // this ensures every intermediate skipped slot gets its `available_base` set. - // - // Without this chaining, skip certs arriving out-of-order leave gaps: - // cert(5) arrives first โ†’ slot 5 has no base โ†’ nothing propagates - // cert(0) arrives โ†’ base jumps from 0 to 388 (next non-skipped) โ†’ slots 1-387 have no base - // With chaining: - // cert(0) โ†’ base set on slot 1 โ†’ slot 1 already skipped โ†’ chain to slot 2 โ†’ ... โ†’ slot 388 - let mut current = slot; - loop { - let current_base = self.get_slot_available_base(desc, current); - let Some(base) = current_base else { - break; - }; - let next = current + 1; - self.ensure_window_exists(desc.get_window_idx(next)); - if let Some(next_state) = self.get_slot_mut(desc, next) { + // Propagate base forward using max-merge: if slot has a base, merge it into + // the next non-skipped slot (C++ pool.cpp on_skip: add_available_base). + let next_slot = self.find_next_nonskipped_slot(desc, slot); + let current_base = self.get_slot_available_base(desc, slot); + + if let Some(base) = current_base { + if let Some(next_state) = self.get_slot_mut(desc, next_slot) { log::trace!( "SimplexState: propagating base from skipped slot {} -> slot {} (max-merge)", - current, - next + slot, + next_slot ); next_state.add_available_base_max(base); } - if self.is_slot_skipped_cert(desc, next) { - current = next; - } else { - break; - } - } - - // C++ compatibility: advance skip timer when SkipCert arrives - // Reference: C++ consensus.cpp lines 228-248 (NotarizationObserved handler) - // C++ advances timeout_slot_ on both NotarCert and SkipCert (via LeaderWindowObserved). - // Without this, the Rust skip cascade takes ~27s for 27 slots (1s/slot) while - // C++ processes entire windows at once and advances the timer on each event. - // - // Important: do NOT shrink skip_timestamp below the current scheduled value - // to preserve the first_block_timeout window. - if !self.opts.enable_fallback_protocol { - let next_slot = slot + 1; - if self.skip_slot <= next_slot { - let new_timestamp = desc.get_time() + self.target_rate_timeout; - let effective_timestamp = match self.skip_timestamp { - Some(current) if current > new_timestamp => current, - _ => new_timestamp, - }; - log::debug!( - "SimplexState::propagate_base_after_skip_cert: advancing skip timer: \ - skip_slot {} -> {}, new timeout in {:?}{}", - self.skip_slot, - next_slot, - self.target_rate_timeout, - if effective_timestamp != new_timestamp { - " (preserved first_block_timeout)" - } else { - "" - } - ); - self.skip_slot = next_slot; - self.skip_timestamp = Some(effective_timestamp); - } } // Advance progress cursor through any progressed slots @@ -6004,9 +5797,10 @@ impl SimplexState { // Should never happen under normal operation log::error!( - "SimplexState::find_next_nonskipped_slot: \ - exceeded scan limit (MAX_SCAN={MAX_SCAN}) from slot {slot} \ - (first_non_finalized={}, first_non_progressed_slot={}, slots_per_window={})", + "SimplexState::find_next_nonskipped_slot: exceeded scan limit (MAX_SCAN={}) \ + from slot {} (first_non_finalized={}, first_non_progressed_slot={}, slots_per_window={})", + MAX_SCAN, + slot, self.first_non_finalized_slot, self.first_non_progressed_slot, self.slots_per_leader_window @@ -6014,62 +5808,49 @@ impl SimplexState { panic!("SimplexState::find_next_nonskipped_slot: exceeded scan limit from slot {}", slot); } - /// Advance leader window when progress cursor crosses window boundary. + /// Advance leader window when progress cursor crosses window boundary /// /// Reference: C++ pool.cpp maybe_publish_new_leader_windows() /// /// This triggers timeout scheduling for the new window and applies adaptive backoff. /// Only called when `SimplexStateOptions::use_notarized_parent_chain` is enabled. - /// - /// # Ordering guarantee (C++ parity: PR #2195) - /// - /// `current_leader_window_idx` is updated here, inside `check_all()` -> - /// notarization/skip handlers -> `advance_progress_cursor()` -> this method. - /// `SessionProcessor::check_collation()` runs strictly after `check_all()` - /// returns, so the leader-status check always sees the up-to-date window. - /// This mirrors C++ consensus.cpp where `current_window_` is set BEFORE - /// the leader check in the `LeaderWindowObserved` handler. fn advance_leader_window_on_progress_cursor(&mut self, desc: &SessionDescription) { let now_window = desc.get_window_idx(self.first_non_progressed_slot); if now_window <= self.current_leader_window_idx { log::trace!( - "SimplexState::advance_leader_window_on_progress_cursor: not advancing window \ - (current={}, now_window={now_window}, first_non_progressed_slot={})", + "SimplexState::advance_leader_window_on_progress_cursor: not advancing window (current={}, now_window={}, first_non_progressed_slot={})", self.current_leader_window_idx, + now_window, self.first_non_progressed_slot ); return; } log::trace!( - "SimplexState: first_non_progressed_slot {} crossed into window {now_window}, \ - advancing leader window", + "SimplexState: first_non_progressed_slot {} crossed into window {}, advancing leader window", self.first_non_progressed_slot, + now_window ); - // C++ parity: read available_base from the progress cursor slot. - // Reference: pool.cpp advance_present(): - // ParentId base = {}; - // if (now_ != 0) { base = slot_at(now_)->state->available_base.value(); } - // publish(now_, base); - // - // For genesis (slot 0), base is None (matches C++ ParentId{} = std::nullopt). - // For later slots, base comes from the per-slot available_base propagated - // by notarization/skip handlers. - let base: CandidateParent = if self.first_non_progressed_slot.value() == 0 { - None - } else { - let slot_base = self.get_slot_available_base(desc, self.first_non_progressed_slot); - assert!( - slot_base.is_some(), - "SimplexState: notarized-parent chain invariant violated โ€” \ - base unknown for progress cursor slot {} (now_window={}). \ - C++ CHECK(maybe_base.has_value()) in pool.cpp advance_present()", - self.first_non_progressed_slot, - now_window + // C++ CHECK(base.has_value()) before publishing LeaderWindowObserved(now_, base) + // Enforce the same invariant for window-start slots + if desc.is_first_in_window(self.first_non_progressed_slot) { + let base_known = + self.get_slot_available_base(desc, self.first_non_progressed_slot).is_some(); + if !base_known { + log::error!( + "SimplexState: notarized-parent chain invariant violated - \ + base unknown for window start slot {} (now_window={})", + self.first_non_progressed_slot, + now_window + ); + } + debug_assert!( + base_known, + "notarized-parent chain: base must be known for window start slot {}", + self.first_non_progressed_slot ); - slot_base.unwrap() - }; + } // Apply adaptive timeout backoff (reuse existing logic) self.apply_adaptive_timeout_backoff( @@ -6082,26 +5863,9 @@ impl SimplexState { self.current_leader_window_idx = now_window; self.set_timeouts(desc); - // C++ parity: populate new window's available_bases and first slot base. - // In C++ this happens via LeaderWindowObserved -> consensus.cpp handler which - // calls start_generation(event->base, ...). In Rust the FSM handles this - // directly: the base is inserted into the window's available_bases set so - // that check_collation() -> has_available_parent() sees it. - self.ensure_window_exists(now_window); - if let Some(window) = self.get_window_mut(now_window) { - window.available_bases.insert(base.clone()); - } - let first_slot = now_window.window_start(self.slots_per_leader_window); - if let Some(slot) = self.get_slot_mut(desc, first_slot) { - if slot.available_base.is_none() { - slot.available_base = Some(base.clone()); - } - } - log::trace!( - "SimplexState: advanced to window {}, base={}, scheduling timeouts from slot {}", + "SimplexState: advanced to window {}, scheduling timeouts from slot {}", now_window, - Self::format_parent(base.as_ref()), self.skip_slot ); } @@ -6219,27 +5983,23 @@ impl SimplexState { if !full_dump { // Compact one-line format for trace logs format!( - "SimplexState: {current_window_idx}/{current_slot} \ - first_non_finalized={} first_non_progressed={} flags=[{slot_flags}] \ - notar={}({:.0}%) skip={}({:.0}%) final={}({:.0}%) n|s={}({:.0}%) \ - s|fb={}({:.0}%) th66/33={}({:.0}%)/{}({:.0}%) bases=[{bases_list}] \ - voted={voted_notar_short} cert={notar_cert_short} evts=[{events_list}]", + "SimplexState: {}/{} first_non_finalized={} first_non_progressed={} flags=[{}] notar={}({:.0}%) skip={}({:.0}%) final={}({:.0}%) n|s={}({:.0}%) s|fb={}({:.0}%) th66/33={}({:.0}%)/{}({:.0}%) bases=[{}] voted={} cert={} evts=[{}]", + current_window_idx, + current_slot, self.first_non_finalized_slot, self.first_non_progressed_slot, - notar_weight, - pct(notar_weight), - skip_weight, - pct(skip_weight), - final_weight, - pct(final_weight), - notar_or_skip, - pct(notar_or_skip), - skip_or_fb, - pct(skip_or_fb), - threshold_66, - pct(threshold_66), - threshold_33, - pct(threshold_33) + slot_flags, + notar_weight, pct(notar_weight), + skip_weight, pct(skip_weight), + final_weight, pct(final_weight), + notar_or_skip, pct(notar_or_skip), + skip_or_fb, pct(skip_or_fb), + threshold_66, pct(threshold_66), + threshold_33, pct(threshold_33), + bases_list, + voted_notar_short, + notar_cert_short, + events_list ) } else { // Full multi-line format for debug dumps @@ -6268,20 +6028,18 @@ impl SimplexState { // Current slot weights result.push_str(&format!( - " - {current_slot} weights: notar={notar_weight}({:.1}%), \ - skip={skip_weight}({:.1}%), final={final_weight}({:.1}%), \ - n|s={notar_or_skip}({:.1}%), s|fb={skip_or_fb}({:.1}%)\n", - pct(notar_weight), - pct(skip_weight), - pct(final_weight), - pct(notar_or_skip), - pct(skip_or_fb) + " - {} weights: notar={}({:.1}%), skip={}({:.1}%), final={}({:.1}%), n|s={}({:.1}%), s|fb={}({:.1}%)\n", + current_slot, + notar_weight, pct(notar_weight), + skip_weight, pct(skip_weight), + final_weight, pct(final_weight), + notar_or_skip, pct(notar_or_skip), + skip_or_fb, pct(skip_or_fb) )); // State info result.push_str(&format!( - " - first_non_finalized: {}, first_non_progressed: {}, \ - skip_slot: {}, pending_events: {}\n", + " - first_non_finalized: {}, first_non_progressed: {}, skip_slot: {}, pending_events: {}\n", self.first_non_finalized_slot, self.first_non_progressed_slot, self.skip_slot, diff --git a/src/node/simplex/src/startup_recovery.rs b/src/node/simplex/src/startup_recovery.rs index 63b4c50..261612b 100644 --- a/src/node/simplex/src/startup_recovery.rs +++ b/src/node/simplex/src/startup_recovery.rs @@ -49,18 +49,28 @@ use crate::{ session_description::SessionDescription, simplex_state::Vote, utils::extract_vote_and_signature, - BlockHash, RawVoteData, RestartRecommitStrategy, SessionId, + BlockHash, PublicKey, RawVoteData, RestartRecommitStrategy, SessionId, }; use consensus_common::ValidatorBlockCandidatePtr; use std::{ - collections::{HashMap, HashSet}, - sync::Arc, + collections::HashMap, + sync::{ + mpsc::{channel, RecvTimeoutError}, + Arc, + }, + time::Duration, }; use ton_api::{ deserialize_boxed, serialize_boxed, - ton::consensus::{ - candidatedata::Empty as CandidateDataEmpty, candidateid::CandidateId, - simplex::Vote as TlVoteBoxed, CandidateData, CandidateHashData, + ton::{ + consensus::{ + candidatedata::{Block as CandidateDataBlock, Empty as CandidateDataEmpty}, + candidateid::CandidateId, + candidateparent::CandidateParent, + simplex::Vote as TlVoteBoxed, + CandidateData, CandidateHashData, CandidateParent as CandidateParentBoxed, + }, + validator_session::candidate::Candidate, }, IntoBoxed, }; @@ -110,7 +120,7 @@ pub(crate) enum RestartRoundAction { /// File hash for candidate lookup file_hash: BlockHash, /// Collated data hash for candidate lookup - _collated_data_hash: BlockHash, + collated_data_hash: BlockHash, /// Candidate hash for certificate lookup candidate_hash: CandidateHash, /// Pre-serialized CandidateHashData bytes (for BlockSignaturesVariant::Simplex) @@ -199,7 +209,7 @@ pub(crate) trait SessionStartupRecoveryListener { /// Seed the current round counter from finalized block count. /// - /// After restart, `current_round` should reflect the number + /// CERT-1 equivalent: After restart, `current_round` should reflect the number /// of finalized blocks so the first new block uses the correct round number. /// /// Reference: C++ publishes `BlockFinalized(last, true)` after loading finalized @@ -256,7 +266,7 @@ pub(crate) trait SessionStartupRecoveryListener { candidate_hash_data_bytes: Vec, ); - /// Notify about last finalized block after restart (Phase 6.5a). + /// Notify about last finalized block after restart (Phase 6.5a / CERT-1). /// /// C++ equivalent: `consensus.cpp::load_from_db()` publishes /// `BlockFinalized(last_known_finalized_block, true)` after loading. @@ -334,6 +344,25 @@ pub(crate) trait SessionStartupRecoveryListener { /// historical state from DB, whereas startup votes are freshly generated on restart. fn recovery_restore_receiver_standstill_cache(&mut self, votes: &[VoteRecord]); + // ======================================================================== + // Approved candidate fetch (delegated to SessionListener by SessionProcessor) + // ======================================================================== + + /// Request approved candidate from validator storage. + /// + /// This delegates to `SessionListener::get_approved_candidate` internally. + /// The coordinator uses this to fetch candidate payloads for recommit. + /// + /// Note: This is a non-blocking request; the callback is invoked when ready. + fn recovery_notify_get_approved_candidate( + &self, + source: PublicKey, + root_hash: BlockHash, + file_hash: BlockHash, + collated_data_hash: BlockHash, + callback: Box) + Send>, + ); + // ======================================================================== // Recommit replay (applies existing notify paths internally) // ======================================================================== @@ -397,7 +426,7 @@ pub(crate) struct SessionStartupRecoveryProcessor { session_id: SessionId, /// Session description (for leader key lookup during candidate reconstruction) - _description: Arc, + description: Arc, /// Recovery options (strategy, initial seqno) options: SessionStartupRecoveryOptions, @@ -446,7 +475,7 @@ impl SessionStartupRecoveryProcessor { Self { session_id, - _description: description, + description, options, self_idx, bootstrap: Some(bootstrap), @@ -531,8 +560,8 @@ impl SessionStartupRecoveryProcessor { self.session_id.to_hex_string() ); - // Split bootstrap into session, receiver, and candidate payload parts - let (session_boot, receiver_boot, candidate_payloads) = bootstrap.split(); + // Split bootstrap into session and receiver parts + let (session_boot, receiver_boot) = bootstrap.split(); // Step 1: Replay ALL votes (global pass - restores weights, certificates) log::debug!( @@ -641,11 +670,7 @@ impl SessionStartupRecoveryProcessor { "Session {}: step 10/12 - restoring candidate bytes cache", self.session_id.to_hex_string() ); - self.restore_candidate_cache( - listener, - &session_boot.finalized_blocks, - &candidate_payloads, - )?; + self.restore_candidate_cache(listener, &session_boot.finalized_blocks)?; // Step 10b: Rebuild receiver standstill caches (votes + cert bundles + last_final_cert) // @@ -1013,7 +1038,7 @@ impl SessionStartupRecoveryProcessor { ); } - /// Notify about the last finalized block (Phase 6.5a). + /// Notify about the last finalized block (Phase 6.5a / CERT-1). /// /// C++ equivalent: `consensus.cpp::load_from_db()` publishes /// `BlockFinalized(last_known_finalized_block, true)` after loading. @@ -1040,7 +1065,7 @@ impl SessionStartupRecoveryProcessor { log::info!( target: LOG_TARGET, - "Session {}: notifying last finalized block on restart: slot={}, seqno={}, hash={}", + "Session {}: CERT-1 notifying last finalized block: slot={}, seqno={}, hash={}", self.session_id.to_hex_string(), slot.value(), seqno, @@ -1052,7 +1077,7 @@ impl SessionStartupRecoveryProcessor { None => { log::debug!( target: LOG_TARGET, - "Session {}: no is_final=true block found, skipping last-finalized-cert notification", + "Session {}: no is_final=true block found, skipping CERT-1 notification", self.session_id.to_hex_string() ); } @@ -1064,46 +1089,38 @@ impl SessionStartupRecoveryProcessor { /// For each finalized block, reconstructs the CandidateData bytes and caches /// them so `requestCandidate(want_candidate=true)` queries can be answered. /// - /// Reference: C++ candidate-resolver.cpp loads full candidate bytes from its - /// own consensus DB. The Rust implementation only reconstructs empty blocks - /// from metadata; non-empty blocks are skipped and will be resolved via peer - /// overlay when requested. + /// Reference: C++ candidate-resolver.cpp loads candidate bytes from DB or + /// fetches via `get_approved_candidate` when needed. /// /// # Empty vs Non-empty blocks /// /// - **Empty blocks**: Reconstruct `CandidateData::Consensus_Empty` from FinalizedBlockRecord /// (block_id, parent info, signature from leader) - /// - **Non-empty blocks**: Skipped (will be served from in-memory cache during - /// normal operation, or peers will query other validators) + /// - **Non-empty blocks**: Fetch approved candidate via `recovery_notify_get_approved_candidate`, + /// then reconstruct `CandidateData::Consensus_Block` fn restore_candidate_cache( &self, listener: &mut dyn SessionStartupRecoveryListener, finalized_blocks: &[FinalizedBlockRecord], - candidate_payloads: &[(RawCandidateId, Vec)], ) -> Result<()> { - let mut restored_empty = 0u32; - let mut restored_payload = 0u32; + if finalized_blocks.is_empty() { + log::debug!( + target: LOG_TARGET, + "Session {}: no finalized blocks, skipping candidate cache restore", + self.session_id.to_hex_string() + ); + return Ok(()); + } + + let mut restored = 0u32; let mut skipped = 0u32; let mut errors = 0u32; - // 1. Restore from persisted candidate payloads (both empty and non-empty). - // C++ parity: CandidateResolver loads full candidate bytes from DB. - let payload_ids: HashSet<_> = candidate_payloads.iter().map(|(id, _)| id.clone()).collect(); - for (id, bytes) in candidate_payloads { - listener.recovery_cache_candidate_bytes(id.slot, id.hash.clone(), bytes.clone()); - restored_payload += 1; - } - - // 2. For finalized empty blocks not already covered by payloads, - // reconstruct from metadata (backward-compat for DBs without payloads). for block in finalized_blocks { let slot = block.candidate_id.slot; let candidate_hash = &block.candidate_id.hash; - if payload_ids.contains(&block.candidate_id) { - continue; - } - + // Look up candidate info for this block let candidate_info = match self.candidate_info_map.get(candidate_hash) { Some(info) => info, None => { @@ -1118,15 +1135,13 @@ impl SessionStartupRecoveryProcessor { } }; + // Determine if this is an empty block by checking candidate_hash_data variant + // Empty blocks use candidateHashDataEmpty, non-empty use candidateHashDataOrdinary let is_empty = Self::is_empty_block_candidate_hash_data(&candidate_info.candidate_hash_data); - if !is_empty { - skipped += 1; - continue; - } - - let candidate_data_bytes = + let candidate_data_bytes = if is_empty { + // Reconstruct empty block CandidateData match self.reconstruct_empty_candidate_data(block, candidate_info) { Ok(bytes) => bytes, Err(e) => { @@ -1140,23 +1155,147 @@ impl SessionStartupRecoveryProcessor { errors += 1; continue; } + } + } else { + // Non-empty blocks: fetch via blocking channel and reconstruct + let leader_idx = ValidatorIndex(candidate_info.leader_idx); + let source = self.description.get_source_public_key(leader_idx).clone(); + + // Extract hashes from candidate_hash_data for fetch + let (root_hash, file_hash, collated_data_hash) = + match Self::extract_hashes_from_candidate_hash_data( + &candidate_info.candidate_hash_data, + ) { + Some(hashes) => hashes, + None => { + log::warn!( + target: LOG_TARGET, + "Session {}: failed to extract hashes from candidate hash data for slot={}", + self.session_id.to_hex_string(), + slot.value() + ); + errors += 1; + continue; + } + }; + + // Create one-shot channel for blocking fetch + let (tx, rx) = channel(); + + log::debug!( + target: LOG_TARGET, + "Session {}: fetching non-empty candidate for slot={}, root_hash={}", + self.session_id.to_hex_string(), + slot.value(), + root_hash.to_hex_string() + ); + + // Request candidate via listener callback + listener.recovery_notify_get_approved_candidate( + source, + root_hash.clone(), + file_hash.clone(), + collated_data_hash.clone(), + Box::new(move |result| { + let _ = tx.send(result); + }), + ); + + // Block waiting for result with timeout + const FETCH_TIMEOUT: Duration = Duration::from_secs(30); + + let fetch_result = match rx.recv_timeout(FETCH_TIMEOUT) { + Ok(Ok(candidate)) => { + log::debug!( + target: LOG_TARGET, + "Session {}: fetched non-empty candidate for slot={}", + self.session_id.to_hex_string(), + slot.value() + ); + // Build validator_session.Candidate TL bytes from stored block data + collated data. + // + // IMPORTANT: CandidateData::Consensus_Block.candidate must contain serialized + // `validator_session.candidate::Candidate` bytes (C++ RawCandidate::serialize()). + // The approved candidate storage returns raw block data + collated_data, NOT TL candidate bytes. + let candidate_seqno = block.block_id.seq_no() as i32; + let tl_vs_candidate = Candidate { + src: UInt256::default(), + round: candidate_seqno, + root_hash: root_hash.clone(), + data: candidate.data.data().to_vec(), + collated_data: candidate.collated_data.data().to_vec(), + }; + let vs_candidate_bytes = consensus_common::serialize_tl_boxed_object!( + &tl_vs_candidate.into_boxed() + ); + + self.reconstruct_block_candidate_data( + block, + candidate_info, + vs_candidate_bytes, + ) + } + Ok(Err(e)) => { + log::warn!( + target: LOG_TARGET, + "Session {}: candidate fetch failed for slot={}: {}", + self.session_id.to_hex_string(), + slot.value(), + e + ); + Err(e) + } + Err(RecvTimeoutError::Timeout) => { + log::warn!( + target: LOG_TARGET, + "Session {}: candidate fetch TIMEOUT for slot={} ({}s)", + self.session_id.to_hex_string(), + slot.value(), + FETCH_TIMEOUT.as_secs() + ); + Err(error!("candidate fetch timeout for slot {}", slot.value())) + } + Err(RecvTimeoutError::Disconnected) => { + log::error!( + target: LOG_TARGET, + "Session {}: candidate fetch channel disconnected for slot={}", + self.session_id.to_hex_string(), + slot.value() + ); + Err(error!("candidate fetch channel disconnected")) + } }; + match fetch_result { + Ok(bytes) => bytes, + Err(e) => { + log::warn!( + target: LOG_TARGET, + "Session {}: failed to reconstruct block candidate for slot={}: {}", + self.session_id.to_hex_string(), + slot.value(), + e + ); + errors += 1; + continue; + } + } + }; + + // Cache the reconstructed candidate bytes listener.recovery_cache_candidate_bytes( slot, candidate_hash.clone(), candidate_data_bytes, ); - restored_empty += 1; + restored += 1; } log::info!( target: LOG_TARGET, - "Session {}: restored candidate cache: {} from payload DB, {} empty reconstructed, \ - {} skipped, {} errors", + "Session {}: restored {} candidate bytes to cache ({} skipped, {} errors)", self.session_id.to_hex_string(), - restored_payload, - restored_empty, + restored, skipped, errors ); @@ -1258,6 +1397,58 @@ impl SessionStartupRecoveryProcessor { } } + /// Reconstruct CandidateData::Consensus_Block bytes for a non-empty block. + /// + /// Reference: C++ RawCandidate::serialize() for block variant + fn reconstruct_block_candidate_data( + &self, + block: &FinalizedBlockRecord, + candidate_info: &CandidateInfoRecord, + candidate_bytes: Vec, + ) -> Result> { + let slot = block.candidate_id.slot; + + // Get parent info from candidate_hash_data + let parent_opt = + Self::extract_parent_id_from_ordinary_hash_data(&candidate_info.candidate_hash_data)?; + + // Build TL parent structure + let tl_parent = match parent_opt { + Some((parent_slot, parent_hash)) => CandidateParent { + id: CandidateId { slot: parent_slot.value() as i32, hash: parent_hash } + .into_boxed(), + } + .into_boxed(), + None => CandidateParentBoxed::Consensus_CandidateWithoutParents, + }; + + // Use signature from candidate_info (leader's original signature) + let signature = candidate_info.signature.clone(); + + // Build the TL Block structure + let tl_block = CandidateDataBlock { + slot: slot.value() as i32, + candidate: candidate_bytes.into(), + parent: tl_parent, + signature, + }; + + // Wrap in CandidateData enum and serialize + let tl_candidate_data = CandidateData::Consensus_Block(tl_block); + let bytes = serialize_boxed(&tl_candidate_data) + .map_err(|e| error!("Failed to serialize block CandidateData: {}", e))?; + + log::trace!( + target: LOG_TARGET, + "Session {}: reconstructed block candidate data for slot={}, size={}", + self.session_id.to_hex_string(), + slot.value(), + bytes.len() + ); + + Ok(bytes) + } + /// Build and apply restart recommit actions. fn apply_restart_recommit( &self, @@ -1284,21 +1475,111 @@ impl SessionStartupRecoveryProcessor { self.options.restart_recommit_strategy ); - // Apply actions through listener. - // Non-empty blocks cannot be fetched (no get_approved_candidate delegation in - // simplex -- C++ resolves candidates from its own DB, not validator manager). - // The get_candidate closure returns an error for non-empty blocks, causing - // the replay to skip them gracefully. + // Pre-fetch candidates for non-empty replay actions. + // We do this before calling recovery_apply_restart_recommit_actions to avoid borrow conflicts + let mut prefetched: HashMap> = HashMap::new(); + + for action in &actions { + let RestartRoundAction::Commit { + slot, + leader_idx, + root_hash, + file_hash, + collated_data_hash, + is_empty, + .. + } = action; + + if *is_empty { + continue; + } + + let source = self.description.get_source_public_key(*leader_idx).clone(); + + // Create one-shot channel for blocking fetch + let (tx, rx) = channel(); + + log::debug!( + target: LOG_TARGET, + "Session {}: fetching candidate for slot={}, root_hash={}", + self.session_id.to_hex_string(), + slot.value(), + root_hash.to_hex_string() + ); + + // Request candidate via listener callback + listener.recovery_notify_get_approved_candidate( + source, + root_hash.clone(), + file_hash.clone(), + collated_data_hash.clone(), + Box::new(move |result| { + // Send result through channel (ignore send error if receiver dropped) + let _ = tx.send(result); + }), + ); + + // Block waiting for result with timeout + // Use a generous timeout (30s) for validator storage fetch + const FETCH_TIMEOUT: Duration = Duration::from_secs(30); + + let result = match rx.recv_timeout(FETCH_TIMEOUT) { + Ok(Ok(candidate)) => { + log::debug!( + target: LOG_TARGET, + "Session {}: fetched candidate for slot={}", + self.session_id.to_hex_string(), + slot.value() + ); + Ok(candidate) + } + Ok(Err(e)) => { + log::warn!( + target: LOG_TARGET, + "Session {}: candidate fetch failed for slot={}: {}", + self.session_id.to_hex_string(), + slot.value(), + e + ); + Err(e) + } + Err(RecvTimeoutError::Timeout) => { + log::warn!( + target: LOG_TARGET, + "Session {}: candidate fetch TIMEOUT for slot={} ({}s)", + self.session_id.to_hex_string(), + slot.value(), + FETCH_TIMEOUT.as_secs() + ); + Err(error!("candidate fetch timeout for slot {}", slot.value())) + } + Err(RecvTimeoutError::Disconnected) => { + log::error!( + target: LOG_TARGET, + "Session {}: candidate fetch channel disconnected for slot={}", + self.session_id.to_hex_string(), + slot.value() + ); + Err(error!("candidate fetch channel disconnected")) + } + }; + + prefetched.insert(*slot, result); + } + + // Apply actions through listener using prefetched candidates listener.recovery_apply_restart_recommit_actions(&actions, &mut |action| { let RestartRoundAction::Commit { slot, is_empty, .. } = action; if *is_empty { fail!("fetch called for empty replay action at slot {}", slot.value()); } - Err(error!( - "non-empty block candidate fetch not supported in simplex recovery (slot {})", - slot.value() - )) + // Look up prefetched candidate + match prefetched.remove(slot) { + Some(Ok(candidate)) => Ok(candidate), + Some(Err(e)) => Err(error!("prefetch failed: {}", e)), + None => Err(error!("no prefetched candidate for slot {}", slot.value())), + } })?; Ok(()) @@ -1403,7 +1684,7 @@ impl SessionStartupRecoveryProcessor { leader_idx: ValidatorIndex(candidate_info.leader_idx), root_hash, file_hash, - _collated_data_hash: collated_data_hash, + collated_data_hash, candidate_hash, candidate_hash_data_bytes, is_empty, diff --git a/src/node/simplex/src/tests/test_block.rs b/src/node/simplex/src/tests/test_block.rs index ec30eca..6a6518f 100644 --- a/src/node/simplex/src/tests/test_block.rs +++ b/src/node/simplex/src/tests/test_block.rs @@ -21,7 +21,6 @@ use crate::{ }, PublicKey, }; -use std::collections::HashMap; use ton_block::{BlockIdExt, Ed25519KeyOption, ShardIdent, UInt256}; /* @@ -167,6 +166,8 @@ fn test_slot_index_window_calculations() { /// Test SlotIndex hashing (for use in HashMap) #[test] fn test_slot_index_hash() { + use std::collections::HashMap; + let mut map: HashMap = HashMap::new(); map.insert(SlotIndex::new(0), "slot_0"); map.insert(SlotIndex::new(42), "slot_42"); @@ -261,6 +262,8 @@ fn test_window_index_slot_calculations() { /// Test WindowIndex hashing (for use in HashMap) #[test] fn test_window_index_hash() { + use std::collections::HashMap; + let mut map: HashMap = HashMap::new(); map.insert(WindowIndex::new(0), "window_0"); map.insert(WindowIndex::new(42), "window_42"); @@ -336,6 +339,8 @@ fn test_validator_index_conversions() { /// Test ValidatorIndex hashing (for use in HashMap) #[test] fn test_validator_index_hash() { + use std::collections::HashMap; + let mut map: HashMap = HashMap::new(); map.insert(ValidatorIndex::new(0), "validator_0"); map.insert(ValidatorIndex::new(42), "validator_42"); diff --git a/src/node/simplex/src/tests/test_candidate_resolver.rs b/src/node/simplex/src/tests/test_candidate_resolver.rs index 13e47b7..9fa3c6a 100644 --- a/src/node/simplex/src/tests/test_candidate_resolver.rs +++ b/src/node/simplex/src/tests/test_candidate_resolver.rs @@ -11,11 +11,7 @@ //! These tests verify the `CandidateResolverCache` correctly stores and retrieves //! candidate data and notarization certificates for responding to queries. -use crate::{ - block::{SlotIndex, ValidatorIndex}, - SessionId, SessionNode, -}; -use std::time::{Duration, SystemTime}; +use crate::{block::SlotIndex, SessionId, SessionNode}; use ton_api::{ ton::{consensus::overlayid::OverlayId, pub_::publickey::Overlay}, IntoBoxed, @@ -224,87 +220,3 @@ fn test_candidate_resolver_cache_cleanup_all() { assert!(cache.get_candidate(SlotIndex::new(i), &hash).is_none()); } } - -#[test] -fn test_merge_candidate_response_parts_body_then_notar_completes_merge() { - let slot = SlotIndex::new(42); - let block_hash = UInt256::rand(); - let candidate_bytes = vec![1, 2, 3, 4]; - let notar_bytes = vec![9, 8, 7]; - - let mut cache = super::CandidateResolverCache::new(); - let mut state = super::CandidateRequestState { - start_time: SystemTime::now(), - retry_count: 0, - current_timeout: Duration::from_millis(500), - source_idx: ValidatorIndex::new(0), - cached_notar: None, - cached_candidate: None, - }; - - // First partial response: candidate body only -> notar remains missing. - let (merged_candidate_1, merged_notar_1) = super::ReceiverImpl::merge_candidate_response_parts( - &mut cache, - Some(&mut state), - slot, - &block_hash, - &candidate_bytes, - &[], - ); - assert_eq!(merged_candidate_1, candidate_bytes); - assert!( - merged_notar_1.is_empty(), - "body-only partial response must not be considered complete" - ); - assert_eq!(state.cached_candidate.as_ref(), Some(&candidate_bytes)); - assert!(state.cached_notar.is_none()); - - // Second partial response: notar only -> merged output must include cached body + new notar. - let (merged_candidate_2, merged_notar_2) = super::ReceiverImpl::merge_candidate_response_parts( - &mut cache, - Some(&mut state), - slot, - &block_hash, - &[], - ¬ar_bytes, - ); - assert_eq!(merged_candidate_2, candidate_bytes); - assert_eq!(merged_notar_2, notar_bytes); - assert_eq!(state.cached_candidate.as_ref(), Some(&candidate_bytes)); - assert_eq!(state.cached_notar.as_ref(), Some(¬ar_bytes)); -} - -#[test] -fn test_merge_candidate_response_parts_uses_locally_cached_notar() { - let slot = SlotIndex::new(7); - let block_hash = UInt256::rand(); - let candidate_bytes = vec![11, 22, 33]; - let cached_notar = vec![44, 55]; - - let mut cache = super::CandidateResolverCache::new(); - cache.cache_notar_cert(slot, block_hash.clone(), cached_notar.clone()); - - let mut state = super::CandidateRequestState { - start_time: SystemTime::now(), - retry_count: 0, - current_timeout: Duration::from_millis(500), - source_idx: ValidatorIndex::new(1), - cached_notar: None, - cached_candidate: None, - }; - - // No notar in this response, but resolver cache already has one. - let (merged_candidate, merged_notar) = super::ReceiverImpl::merge_candidate_response_parts( - &mut cache, - Some(&mut state), - slot, - &block_hash, - &candidate_bytes, - &[], - ); - assert_eq!(merged_candidate, candidate_bytes); - assert_eq!( - merged_notar, cached_notar, - "candidate-only response should complete when notar already exists in local cache" - ); -} diff --git a/src/node/simplex/src/tests/test_receiver.rs b/src/node/simplex/src/tests/test_receiver.rs index 9b58db2..3519f06 100644 --- a/src/node/simplex/src/tests/test_receiver.rs +++ b/src/node/simplex/src/tests/test_receiver.rs @@ -53,7 +53,7 @@ use ton_api::{ }, IntoBoxed, }; -use ton_block::{error, sha256_digest, BlockIdExt, Ed25519KeyOption, Error, ShardIdent, UInt256}; +use ton_block::{sha256_digest, BlockIdExt, Ed25519KeyOption, Error, ShardIdent, UInt256}; include!("../../../../common/src/info.rs"); @@ -193,20 +193,6 @@ impl ReceiverListener for TestReceiverListener { certificate ); } - - fn on_candidate_query_fallback( - &self, - _slot: crate::block::SlotIndex, - _block_hash: UInt256, - _want_notar: bool, - response_callback: consensus_common::QueryResponseCallback, - ) { - log::trace!( - "Receiver {} candidate_query_fallback: no-op (test mock)", - self.stats.receiver_idx - ); - response_callback(Err(error!("Not implemented in test mock"))); - } } impl Drop for TestReceiverListener { @@ -244,7 +230,6 @@ impl ReceiverInstance { // Use masterchain shard and default size limits for tests let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; // 8 MB - let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); let panicked_flag = Arc::new(AtomicBool::new(false)); let health_counters = Arc::new(crate::receiver::ReceiverHealthCounters::new()); @@ -252,15 +237,12 @@ impl ReceiverInstance { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, nodes, &private_key, overlay_manager, listener_weak, Duration::from_secs(10), // standstill_timeout panicked_flag, - false, health_counters, )?; @@ -673,7 +655,6 @@ fn test_receiver_candidate_resolver() { // Use masterchain shard and default size limits let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; // 8 MB - let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); // === Step 1: Create receiver 0 and broadcast a candidate === log::info!("Step 1: Creating receiver 0 and broadcasting candidate..."); @@ -686,15 +667,12 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_secs(10), panicked_flag0, - false, health_counters0, ) .expect("Failed to create receiver 0"); @@ -749,9 +727,6 @@ fn test_receiver_candidate_resolver() { // Send the broadcast (will be cached in receiver 0's resolver cache) receiver0.send_block_broadcast(slot, candidate_hash.clone(), broadcast); - // requestCandidate currently asks for both candidate+notar. Seed notar in - // resolver cache so late joiners can complete merged CandidateAndCert. - receiver0.cache_notarization_cert(slot, candidate_hash.clone(), vec![0xAA, 0xBB, 0xCC]); log::info!( "Receiver 0 broadcast candidate for slot {} with hash {}", slot, @@ -776,15 +751,12 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_secs(10), panicked_flag1, - false, health_counters1, ) .expect("Failed to create receiver 1"); @@ -797,15 +769,12 @@ fn test_receiver_candidate_resolver() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[2], overlay_manager.clone(), Arc::downgrade(&listener2_arc), Duration::from_secs(10), panicked_flag2, - false, health_counters2, ) .expect("Failed to create receiver 2"); @@ -859,153 +828,6 @@ fn test_receiver_candidate_resolver() { println!("โœ“ Candidate resolver test passed: late-joining receivers successfully retrieved missed candidate"); } -/// Test that candidate resolver works with a large candidate payload (~1 MB) -/// that exercises the +1MB RLDP response budget (C++ PR #2195 parity). -/// -/// Scenario: -/// 1. Receiver 0 broadcasts a ~1 MB candidate -/// 2. Receiver 1 joins late and requests the candidate via the resolver -/// 3. Assert receiver 1 receives the full large candidate -#[test] -fn test_receiver_candidate_resolver_large_payload() { - let _ = env_logger::Builder::new().filter_level(log::LevelFilter::Trace).try_init(); - - let overlay_manager = SessionFactory::create_in_process_overlay_manager(2); - let session_id = UInt256::rand(); - - let keys: Vec<_> = - (0..2).map(|_| Ed25519KeyOption::generate().expect("Failed to generate key")).collect(); - let nodes: Vec = keys - .iter() - .map(|k| SessionNode { public_key: k.clone(), adnl_id: k.id().clone(), weight: 1 }) - .collect(); - - let shard = ShardIdent::masterchain(); - let max_candidate_size = 8 << 20; // 8 MB (validation guard) - let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); - - // === Step 1: Create receiver 0 and broadcast a ~1 MB candidate === - log::info!("Step 1: Creating receiver 0 and broadcasting large candidate..."); - - let (listener0, stats0) = TestReceiverListener::create(0); - let listener0_arc: Arc = listener0.clone(); - let receiver0 = crate::receiver::ReceiverWrapper::create( - session_id.clone(), - &shard, - max_candidate_size, - max_candidate_query_answer_size, - 0, - &nodes, - &keys[0], - overlay_manager.clone(), - Arc::downgrade(&listener0_arc), - Duration::from_secs(10), - Arc::new(AtomicBool::new(false)), - false, - Arc::new(crate::receiver::ReceiverHealthCounters::new()), - ) - .expect("Failed to create receiver 0"); - - thread::sleep(Duration::from_millis(500)); - - // Build a ~1 MB block payload - let slot = 7u32; - let block_data = vec![0xABu8; 1 << 20]; // 1 MiB of data - let collated_data: Vec = vec![]; - let root_hash = UInt256::from_slice(&sha256_digest(&block_data)); - let file_hash = UInt256::from_slice(&sha256_digest(&block_data)); - let collated_file_hash = UInt256::from_slice(&sha256_digest(&collated_data)); - - let tl_inner = TlCandidate { - src: UInt256::default(), - round: slot as i32, - root_hash: root_hash.clone(), - data: block_data.clone().into(), - collated_data: collated_data.clone().into(), - }; - let candidate_bytes = consensus_common::serialize_tl_boxed_object!(&tl_inner.into_boxed()); - - let block_id = BlockIdExt { - shard_id: shard.clone(), - seq_no: slot, - root_hash: root_hash.clone(), - file_hash: file_hash.clone(), - }; - - let candidate_hash = crate::utils::compute_candidate_id_hash_u32( - slot, - Some(&block_id), - Some(&collated_file_hash), - None, - ); - - let signature = crate::utils::sign_candidate_u32(&session_id, slot, &candidate_hash, &keys[0]) - .expect("Failed to sign candidate"); - - let broadcast = CandidateData::Consensus_Block(CandidateDataBlock { - slot: slot as i32, - candidate: candidate_bytes.into(), - parent: CandidateParent::Consensus_CandidateWithoutParents, - signature: signature.into(), - }); - - receiver0.send_block_broadcast(slot, candidate_hash.clone(), broadcast); - // requestCandidate currently asks for both candidate+notar. Seed notar in - // resolver cache so late joiners can complete merged CandidateAndCert. - receiver0.cache_notarization_cert(slot, candidate_hash.clone(), vec![0xAA, 0xBB, 0xCC]); - thread::sleep(Duration::from_millis(500)); - - // === Step 2: Create receiver 1 (late joiner) === - log::info!("Step 2: Creating late-joining receiver 1..."); - thread::sleep(Duration::from_secs(4)); - - let (listener1, stats1) = TestReceiverListener::create(1); - let listener1_arc: Arc = listener1.clone(); - let receiver1 = crate::receiver::ReceiverWrapper::create( - session_id.clone(), - &shard, - max_candidate_size, - max_candidate_query_answer_size, - 0, - &nodes, - &keys[1], - overlay_manager.clone(), - Arc::downgrade(&listener1_arc), - Duration::from_secs(10), - Arc::new(AtomicBool::new(false)), - false, - Arc::new(crate::receiver::ReceiverHealthCounters::new()), - ) - .expect("Failed to create receiver 1"); - - thread::sleep(Duration::from_millis(1000)); - - // === Step 3: Receiver 1 requests the large candidate === - log::info!("Step 3: Receiver 1 requesting large candidate via resolver..."); - receiver1.request_candidate(slot, candidate_hash.clone()); - - log::info!("Waiting for large candidate resolver request to complete..."); - thread::sleep(Duration::from_secs(10)); - - // === Step 4: Assert receiver 1 received the candidate === - let r0_broadcasts = stats0.broadcasts_received.load(Ordering::Relaxed); - let r1_broadcasts = stats1.broadcasts_received.load(Ordering::Relaxed); - log::info!("Receiver 0: broadcasts_received = {}", r0_broadcasts); - log::info!("Receiver 1: broadcasts_received = {}", r1_broadcasts); - - receiver0.stop(); - receiver1.stop(); - thread::sleep(Duration::from_millis(500)); - - assert!( - r1_broadcasts >= 1, - "Receiver 1 should have received the large candidate (~1 MB) via resolver, got {}", - r1_broadcasts - ); - - println!("โœ“ Large candidate resolver test passed: ~1 MB candidate successfully retrieved via RLDP path"); -} - // ============================================================================ // Certificate send + standstill re-broadcast tests // ============================================================================ @@ -1080,7 +902,6 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; - let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); // Create receivers with short standstill timeout to simulate retransmission let (listener0, _stats0) = TestReceiverListener::create(0); @@ -1089,15 +910,12 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), - false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 0"); @@ -1108,15 +926,12 @@ fn test_receiver_send_certificate_and_standstill_rebroadcasts_cached_certificate session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), - false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 1"); @@ -1187,7 +1002,6 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; - let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); // Create receivers with short standstill timeout to simulate retransmission let (listener0, _stats0) = TestReceiverListener::create(0); @@ -1196,15 +1010,12 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), - false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 0"); @@ -1215,15 +1026,12 @@ fn test_receiver_standstill_rebroadcasts_cached_local_votes() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), - false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 1"); @@ -1272,7 +1080,6 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { let shard = ShardIdent::masterchain(); let max_candidate_size = 8 << 20; - let max_candidate_query_answer_size: u64 = max_candidate_size as u64 + (1 << 20); let (listener0, _stats0) = TestReceiverListener::create(0); let listener0_arc: Arc = listener0.clone(); @@ -1280,15 +1087,12 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[0], overlay_manager.clone(), Arc::downgrade(&listener0_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), - false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 0"); @@ -1299,15 +1103,12 @@ fn test_receiver_standstill_cache_does_not_overwrite_existing_certificate() { session_id.clone(), &shard, max_candidate_size, - max_candidate_query_answer_size, - 0, &nodes, &keys[1], overlay_manager.clone(), Arc::downgrade(&listener1_arc), Duration::from_millis(200), Arc::new(AtomicBool::new(false)), - false, Arc::new(crate::receiver::ReceiverHealthCounters::new()), ) .expect("Failed to create receiver 1"); diff --git a/src/node/simplex/src/tests/test_restart.rs b/src/node/simplex/src/tests/test_restart.rs index e4439ba..7009bcb 100644 --- a/src/node/simplex/src/tests/test_restart.rs +++ b/src/node/simplex/src/tests/test_restart.rs @@ -32,7 +32,14 @@ use crate::{ utils::sign_vote, RawBuffer, RestartRecommitStrategy, SessionId, SessionNode, SessionOptions, }; -use std::{sync::Arc, time::SystemTime}; +use consensus_common::ConsensusCommonFactory; +use std::{ + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + time::SystemTime, +}; use ton_api::{ deserialize_boxed, serialize_boxed, ton::{ @@ -42,14 +49,13 @@ use ton_api::{ candidateparent::CandidateParent as TlCandidateParent, CandidateData, CandidateHashData, CandidateId as CandidateIdBoxed, CandidateParent, }, - validator_session::candidate::Candidate as TlCandidate, + validator_session::{ + candidate::Candidate as TlCandidate, Candidate as ValidatorSessionCandidate, + }, }, IntoBoxed, }; -use ton_block::{ - sha256_digest, BlockIdExt, BocFlags, BocWriter, BuilderData, Ed25519KeyOption, Result, - ShardIdent, UInt256, -}; +use ton_block::{error, sha256_digest, BlockIdExt, Ed25519KeyOption, Result, ShardIdent, UInt256}; #[test] fn test_restart_recommit_strategy_default() { @@ -180,16 +186,6 @@ fn make_candidate_hash_data_empty( }) } -/// Create valid BOC bytes from raw data (for tests that need valid BOC input). -fn make_test_boc(data: &[u8], flags: BocFlags) -> Vec { - let mut b = BuilderData::new(); - b.append_raw(data, data.len() * 8).unwrap(); - let cell = b.into_cell().unwrap(); - let mut buf = Vec::new(); - BocWriter::with_flags([cell], flags).unwrap().write(&mut buf).unwrap(); - buf -} - fn make_validator_session_candidate_bytes( round: i32, root_hash: UInt256, @@ -679,6 +675,17 @@ impl SessionStartupRecoveryListener for MockRecoveryListener { self.cached_candidates.push((slot, candidate_hash, candidate_data_bytes)); } + fn recovery_notify_get_approved_candidate( + &self, + _source: crate::PublicKey, + _root_hash: crate::BlockHash, + _file_hash: crate::BlockHash, + _collated_data_hash: crate::BlockHash, + _callback: Box) + Send>, + ) { + panic!("unexpected recovery_notify_get_approved_candidate call in this test"); + } + fn recovery_apply_restart_recommit_actions( &mut self, _actions: &[RestartRoundAction], @@ -702,7 +709,7 @@ fn make_vote_record( let tl_vote = sign_vote(&vote, session_id, key).expect("sign_vote failed"); let serialized = serialize_boxed(&tl_vote).expect("serialize vote failed"); let vote_hash = UInt256::from_slice(&sha256_digest(&serialized)); - VoteRecord { vote_hash, data: serialized.into(), node_idx, seqno: 0 } + VoteRecord { vote_hash, data: serialized.into(), node_idx } } #[test] @@ -759,7 +766,6 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() notar_certs, votes: votes.clone(), pool_state, - candidate_payloads: vec![], }; let proc = SessionStartupRecoveryProcessor::new(session_id, desc, options, bootstrap); @@ -771,7 +777,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() proc.apply_bootstrap(&mut listener).expect("apply_bootstrap failed"); // ------------------------------------------------------------------------ - // Ordering invariants (Phase 6.6 + last-finalized-cert sequencing) + // Ordering invariants (Phase 6.6 + CERT-1 sequencing) // ------------------------------------------------------------------------ // // 1) All votes must be replayed BEFORE setting finalized boundary (Phase 6.6) @@ -801,7 +807,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() "Local flags must be applied after finalized boundary is set" ); - // 3) Last-finalized-cert notification must happen AFTER seeding finalized tracking set + // 3) CERT-1 notification must happen AFTER seeding finalized tracking set let last_seed_final_pos = listener .call_log .iter() @@ -814,10 +820,10 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() .expect("expected NotifyLastFinalized call"); assert!( last_seed_final_pos < cert1_pos, - "Last-finalized-cert notification must happen after seeding finalized blocks set" + "CERT-1 notification must happen after seeding finalized blocks set" ); - // 4) Standstill cache rebuild must happen after last-finalized-cert notification. + // 4) Standstill cache rebuild must happen after CERT-1. let standstill_pos = listener .call_log .iter() @@ -825,7 +831,7 @@ fn test_apply_bootstrap_calls_expected_listener_methods_first_commit_strategy() .expect("expected RestoreStandstillCache call"); assert!( cert1_pos < standstill_pos, - "Standstill cache rebuild must happen after last-finalized-cert notification" + "Standstill cache rebuild must happen after CERT-1 notification" ); // Step 1: global replay @@ -898,7 +904,6 @@ fn test_apply_bootstrap_does_not_generate_skip_votes_when_first_nonannounced_win notar_certs: vec![], votes: vec![], pool_state: Some(PoolStateRecord { first_nonannounced_window: WindowIndex::new(0) }), - candidate_payloads: vec![], }; let desc = create_test_desc(); @@ -934,10 +939,19 @@ fn test_apply_bootstrap_does_not_generate_skip_votes_when_first_nonannounced_win // Candidate bytes cache restoration: TL roundtrip + invariants // ============================================================================ -/// Listener that captures cached CandidateData bytes during candidate cache restoration. +/// Listener that supports non-empty candidate fetch and captures cached CandidateData bytes. #[derive(Default)] struct CandidateCacheListener { + // Observations cached_candidates: Vec<(SlotIndex, UInt256, Vec)>, + fetched_non_empty: AtomicU32, + + // Expectations for non-empty fetch request + expected_root_hash: UInt256, + expected_file_hash: UInt256, + expected_collated_file_hash: UInt256, + candidate_block_data: Vec, + candidate_collated_data: Vec, } impl SessionStartupRecoveryListener for CandidateCacheListener { @@ -1024,6 +1038,37 @@ impl SessionStartupRecoveryListener for CandidateCacheListener { self.cached_candidates.push((slot, candidate_hash, candidate_data_bytes)); } + fn recovery_notify_get_approved_candidate( + &self, + source: crate::PublicKey, + root_hash: crate::BlockHash, + file_hash: crate::BlockHash, + collated_data_hash: crate::BlockHash, + callback: Box) + Send>, + ) { + // This must be called exactly for non-empty candidates only + self.fetched_non_empty.fetch_add(1, Ordering::SeqCst); + + assert_eq!(root_hash, self.expected_root_hash, "unexpected root_hash requested"); + assert_eq!(file_hash, self.expected_file_hash, "unexpected file_hash requested"); + assert_eq!( + collated_data_hash, self.expected_collated_file_hash, + "unexpected collated_data_hash requested" + ); + + let candidate = consensus_common::ValidatorBlockCandidate { + public_key: source, + id: BlockIdExt::default(), + collated_file_hash: collated_data_hash, + data: ConsensusCommonFactory::create_block_payload(self.candidate_block_data.clone()), + collated_data: ConsensusCommonFactory::create_block_payload( + self.candidate_collated_data.clone(), + ), + }; + + callback(Ok(Arc::new(candidate))); + } + fn recovery_apply_restart_recommit_actions( &mut self, actions: &[RestartRoundAction], @@ -1072,9 +1117,8 @@ fn test_restart_restore_candidate_bytes_roundtrip_empty_and_non_empty() { // ------------------------------------------------------------------------ let non_empty_round_seqno: i32 = 51; // used as block seqno by extract_block_info_from_candidate let non_empty_root_hash = UInt256::from([0x22; 32]); - // Use valid BOC bytes โ€” compress_candidate_data requires valid BOC input - let non_empty_data = make_test_boc(b"block_data_bytes", BocFlags::all()); - let non_empty_collated = make_test_boc(b"collated_data_bytes", BocFlags::Crc32); + let non_empty_data = b"block_data_bytes".to_vec(); + let non_empty_collated = b"collated_data_bytes".to_vec(); let candidate_payload_bytes = make_validator_session_candidate_bytes( non_empty_round_seqno, non_empty_root_hash.clone(), @@ -1088,7 +1132,6 @@ fn test_restart_restore_candidate_bytes_roundtrip_empty_and_non_empty() { Some((parent_id.slot, &parent_id.hash)), &shard, max_size, - 0, ) .expect("compute_candidate_id_hash_from_bytes failed"); let non_empty_candidate_id = RawCandidateId { slot: SlotIndex::new(11), hash: non_empty_hash }; @@ -1147,41 +1190,80 @@ fn test_restart_restore_candidate_bytes_roundtrip_empty_and_non_empty() { notar_certs: vec![], votes: vec![], pool_state: None, - candidate_payloads: vec![], }; let proc = SessionStartupRecoveryProcessor::new(session_id, desc, options, bootstrap); - let mut listener = CandidateCacheListener::default(); + let mut listener = CandidateCacheListener { + expected_root_hash: non_empty_root_hash.clone(), + expected_file_hash: non_empty_file_hash.clone(), + expected_collated_file_hash: non_empty_collated_file_hash.clone(), + candidate_block_data: non_empty_data.clone(), + candidate_collated_data: non_empty_collated.clone(), + ..Default::default() + }; proc.apply_bootstrap(&mut listener).expect("apply_bootstrap failed"); - // Post-condition: only empty candidate cached; non-empty candidates are skipped - // (simplex resolves non-empty candidates via peer overlay, not validator manager) - assert_eq!(listener.cached_candidates.len(), 1); + // Invariant: exactly one non-empty fetch (empty must not trigger fetch) + assert_eq!(listener.fetched_non_empty.load(Ordering::SeqCst), 1); - let (slot, _hash, bytes) = &listener.cached_candidates[0]; - let msg = deserialize_boxed(bytes).expect("deserialize CandidateData"); - let candidate_data = msg.downcast::().expect("downcast CandidateData"); + // Post-condition: both candidates cached + assert_eq!(listener.cached_candidates.len(), 2); - match candidate_data { - CandidateData::Consensus_Empty(empty) => { - assert_eq!(SlotIndex::new(empty.slot as u32), *slot); - assert_eq!(empty.signature, empty_info.signature); - assert_eq!(empty.block, empty_referenced_block); + // Decode and validate cached CandidateData bytes + for (slot, _hash, bytes) in &listener.cached_candidates { + let msg = deserialize_boxed(bytes).expect("deserialize CandidateData"); + let candidate_data = msg.downcast::().expect("downcast CandidateData"); - // Parent is a CandidateId (boxed), verify it matches the empty hash data parent - assert_eq!(SlotIndex::new(*empty.parent.slot() as u32), parent_id.slot); - assert_eq!(empty.parent.hash(), &parent_id.hash); - } - CandidateData::Consensus_Block(_) => { - panic!("non-empty block should not be cached during startup recovery"); + match candidate_data { + CandidateData::Consensus_Empty(empty) => { + assert_eq!(SlotIndex::new(empty.slot as u32), *slot); + assert_eq!(empty.signature, empty_info.signature); + assert_eq!(empty.block, empty_referenced_block); + + // Parent is a CandidateId (boxed), verify it matches the empty hash data parent + assert_eq!(SlotIndex::new(*empty.parent.slot() as u32), parent_id.slot); + assert_eq!(empty.parent.hash(), &parent_id.hash); + } + CandidateData::Consensus_Block(block) => { + assert_eq!(SlotIndex::new(block.slot as u32), *slot); + assert_eq!(block.signature, non_empty_info.signature); + assert_eq!(block.candidate.as_slice(), candidate_payload_bytes.as_slice()); + + // Nested candidate bytes MUST be validator_session.Candidate + let nested = consensus_common::utils::deserialize_tl_boxed_object::< + ValidatorSessionCandidate, + >(&block.candidate) + .expect("deserialize nested validator_session.Candidate"); + match nested { + ValidatorSessionCandidate::ValidatorSession_Candidate(c) => { + assert_eq!(c.src, UInt256::default()); + assert_eq!(c.round, non_empty_round_seqno); + assert_eq!(c.root_hash, non_empty_root_hash); + assert_eq!(c.data.to_vec(), non_empty_data); + assert_eq!(c.collated_data.to_vec(), non_empty_collated); + } + _ => panic!("unexpected nested Candidate variant"), + } + + // Parent is CandidateParent::CandidateParent with an id + match &block.parent { + CandidateParent::Consensus_CandidateParent(p) => { + assert_eq!(SlotIndex::new(*p.id.slot() as u32), parent_id.slot); + assert_eq!(p.id.hash(), &parent_id.hash); + } + CandidateParent::Consensus_CandidateWithoutParents => { + panic!("expected CandidateParent for non-empty candidate"); + } + } + } } } } #[test] -fn test_restart_restore_candidate_bytes_skips_non_empty_and_keeps_empty() { +fn test_restart_restore_candidate_bytes_skips_non_empty_on_fetch_error_but_keeps_empty() { let session_id = SessionId::default(); let options = SessionStartupRecoveryOptions::new(RestartRecommitStrategy::FirstCommitAfterFinalized, 0); @@ -1252,7 +1334,6 @@ fn test_restart_restore_candidate_bytes_skips_non_empty_and_keeps_empty() { notar_certs: vec![], votes: vec![], pool_state: None, - candidate_payloads: vec![], }; let proc = SessionStartupRecoveryProcessor::new(session_id, desc, options, bootstrap); @@ -1333,6 +1414,16 @@ fn test_restart_restore_candidate_bytes_skips_non_empty_and_keeps_empty() { ) { self.cached.push((slot, candidate_hash, candidate_data_bytes)); } + fn recovery_notify_get_approved_candidate( + &self, + _source: crate::PublicKey, + _root_hash: crate::BlockHash, + _file_hash: crate::BlockHash, + _collated_data_hash: crate::BlockHash, + callback: Box) + Send>, + ) { + callback(Err(error!("simulated fetch error"))); + } fn recovery_apply_restart_recommit_actions( &mut self, _actions: &[RestartRoundAction], @@ -1348,7 +1439,7 @@ fn test_restart_restore_candidate_bytes_skips_non_empty_and_keeps_empty() { let mut listener = FailFetchListener::default(); proc.apply_bootstrap(&mut listener).expect("apply_bootstrap failed"); - // Post-condition: empty candidate cached, non-empty skipped (not fetched) + // Post-condition: empty candidate cached, non-empty skipped due to fetch error assert_eq!(listener.cached.len(), 1); assert_eq!(listener.cached[0].0, SlotIndex::new(10)); diff --git a/src/node/simplex/src/tests/test_session_processor.rs b/src/node/simplex/src/tests/test_session_processor.rs index 5f799b5..0e1124f 100644 --- a/src/node/simplex/src/tests/test_session_processor.rs +++ b/src/node/simplex/src/tests/test_session_processor.rs @@ -15,9 +15,8 @@ use super::*; use crate::{ block::ValidatorIndex, receiver::Receiver, - simplex_state::SimplexStateOptions, task_queue::{CallbackTaskQueuePtr, TaskQueuePtr}, - SessionId, SessionNode, SessionOptions, SIMPLEX_ROUNDLESS, + SessionId, SessionNode, SessionOptions, }; use consensus_common::{ AsyncRequestPtr, BlockPayloadPtr, BlockSourceInfo, CollationParentHint, PublicKey, @@ -29,49 +28,29 @@ use std::{ env, fs, mem, sync::{ atomic::{AtomicBool, Ordering}, - mpsc::channel, Arc, Mutex, }, time::{Duration, SystemTime}, }; use ton_api::{ - deserialize_boxed, - ton::{ - consensus::{ - candidatedata::Block as CandidateDataBlock, - simplex::{ - certificate::Certificate, unsignedvote::SkipVote, vote::Vote as TlVote, - votesignature::VoteSignature, votesignatureset::VoteSignatureSet, CandidateAndCert, - Certificate as CertificateBoxed, VoteSignature as VoteSignatureBoxed, - }, - CandidateData, CandidateParent, + ton::consensus::{ + simplex::{ + certificate::Certificate, unsignedvote::SkipVote, vote::Vote as TlVote, + votesignature::VoteSignature, votesignatureset::VoteSignatureSet, + Certificate as CertificateBoxed, VoteSignature as VoteSignatureBoxed, }, - validator_session::candidate::Candidate as TlCandidate, + CandidateData, }, IntoBoxed, }; use ton_block::{ - error, sha256_digest, signature::BlockSignaturesVariant, BlockIdExt, BocFlags, BocWriter, - BuilderData, Ed25519KeyOption, ShardIdent, UInt256, + error, signature::BlockSignaturesVariant, BlockIdExt, Ed25519KeyOption, ShardIdent, UInt256, }; // ============================================================================ // Test Helpers // ============================================================================ -/// Create valid BOC bytes from raw data (for tests that need valid BOC input). -/// -/// The compress/decompress pipeline requires valid BOC, so mock data must be -/// wrapped in a cell + serialized as BOC with appropriate flags. -fn make_test_boc(data: &[u8], flags: BocFlags) -> Vec { - let mut b = BuilderData::new(); - b.append_raw(data, data.len() * 8).unwrap(); - let cell = b.into_cell().unwrap(); - let mut buf = Vec::new(); - BocWriter::with_flags([cell], flags).unwrap().write(&mut buf).unwrap(); - buf -} - /// Create test validators with equal weights fn create_test_validators(count: u32) -> Vec { (0..count) @@ -84,12 +63,26 @@ fn create_test_validators(count: u32) -> Vec { } /// Create test SessionDescription with default options -#[allow(dead_code)] fn create_test_desc( nodes: &[SessionNode], local_idx: usize, ) -> Arc { - create_test_desc_with_opts(nodes, local_idx, &SessionOptions::default()) + let local_key = nodes[local_idx].public_key.clone(); + let shard = ShardIdent::masterchain(); + let opts = SessionOptions::default(); + Arc::new( + crate::session_description::SessionDescription::new( + &opts, + SessionId::default(), + 1, // initial_block_seqno + nodes, + local_key, + &shard, + SystemTime::now(), + None, + ) + .unwrap(), + ) } // ============================================================================ @@ -120,8 +113,6 @@ enum ReceiverAction { CacheLastFinalCertificate { slot: u32, bytes_len: usize }, /// cleanup() was called Cleanup { up_to_slot: u32 }, - /// request_candidate() was called - RequestCandidate { slot: u32, block_hash: UInt256 }, } /// Mock receiver that records all outbound calls @@ -178,11 +169,8 @@ impl Receiver for MockReceiver { self.actions.lock().unwrap().push_back(ReceiverAction::Cleanup { up_to_slot }); } - fn request_candidate(&self, slot: u32, block_hash: UInt256) { - self.actions - .lock() - .unwrap() - .push_back(ReceiverAction::RequestCandidate { slot, block_hash }); + fn request_candidate(&self, _slot: u32, _block_hash: UInt256) { + // No-op for tests } fn reschedule_standstill(&self) { @@ -505,39 +493,11 @@ struct TestFixture { task_queue: Arc, } -/// Create test SessionDescription with custom options -fn create_test_desc_with_opts( - nodes: &[SessionNode], - local_idx: usize, - opts: &SessionOptions, -) -> Arc { - let local_key = nodes[local_idx].public_key.clone(); - let shard = ShardIdent::masterchain(); - Arc::new( - crate::session_description::SessionDescription::new( - opts, - SessionId::default(), - 1, // initial_block_seqno - nodes, - local_key, - &shard, - SystemTime::now(), - None, // metrics - ) - .unwrap(), - ) -} - impl TestFixture { /// Create a test fixture with N validators (local is validator 0) fn new(validator_count: u32) -> Self { - Self::new_with_opts(validator_count, SessionOptions::default()) - } - - /// Create a test fixture with N validators and custom session options - fn new_with_opts(validator_count: u32, opts: SessionOptions) -> Self { let nodes = create_test_validators(validator_count); - let description = create_test_desc_with_opts(&nodes, 0, &opts); + let description = create_test_desc(&nodes, 0); let listener: Arc = Arc::new(MockListener); @@ -675,15 +635,14 @@ fn test_genesis_collation_expected_seqno_uses_initial_block_seqno() { let genesis_block_id = BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); - // Use valid BOC bytes โ€” compress_candidate_data requires valid BOC input - let block_boc = make_test_boc(&[0xAA], BocFlags::all()); - let collated_boc = make_test_boc(&[0xBB], BocFlags::Crc32); let candidate = crate::ValidatorBlockCandidate { public_key: fixture.nodes[0].public_key.clone(), id: genesis_block_id, - collated_file_hash: UInt256::from_slice(&sha256_digest(&collated_boc)), - data: consensus_common::ConsensusCommonFactory::create_block_payload(block_boc), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload(collated_boc), + collated_file_hash: UInt256::rand(), + data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xAA].into()), + collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( + vec![0xBB].into(), + ), }; fixture @@ -747,9 +706,9 @@ fn test_should_generate_empty_block_uses_committed_head_at_session_start() { assert_eq!(processor.last_committed_seqno, Some(46)); // Slot 0 is the initial `first_non_progressed_slot` in fresh state. - // MC: new_seqno=48, committed=46 -> 46+1=47 < 48 -> empty + // MC: new_seqno=48, committed=46 โ†’ 46+1=47 < 48 โ†’ empty assert!(processor.should_generate_empty_block(SlotIndex::new(0), 48)); - // MC: new_seqno=47, committed=46 -> 46+1=47 == 47 -> NOT empty + // MC: new_seqno=47, committed=46 โ†’ 46+1=47 == 47 โ†’ NOT empty assert!(!processor.should_generate_empty_block(SlotIndex::new(0), 47)); } @@ -1070,9 +1029,12 @@ fn test_on_certificate_relays_and_caches_skip_certificate_once() { }) .count(); - // C++ parity (pool.cpp handle_saved_certificate): every newly accepted - // certificate is relayed once, regardless of origin. - assert_eq!(send_cert_count, 1, "C++ parity: foreign skip cert must be relayed once"); + // External skip certificates are stored for state consistency + standstill caching, + // but are NOT re-broadcast when ingested externally (should_broadcast=false). + assert_eq!( + send_cert_count, 0, + "expected no send_certificate for externally provided skip cert" + ); assert_eq!( cache_standstill_count, 1, "expected exactly one cache_standstill_certificate on first apply" @@ -1111,6 +1073,7 @@ fn test_handle_finalization_reached_caches_final_certificate_for_standstill() { slot, block_hash: block_hash.clone(), certificate: cert, + should_broadcast: true, }; fixture.processor.handle_finalization_reached(event); @@ -1167,6 +1130,8 @@ fn test_handle_notarization_reached_requests_missing_candidate_body() { slot, block_hash: block_hash.clone(), certificate: cert, + // Foreign cert ingestion path: do not re-broadcast. + should_broadcast: false, }; // Act: should schedule requestCandidate for missing body. @@ -1281,6 +1246,8 @@ fn test_batch_finalization_notarized_parents_finalized_descendant() { /// Test that SIMPLEX_ROUNDLESS constant is u32::MAX #[test] fn test_simplex_roundless_constant_value() { + use crate::SIMPLEX_ROUNDLESS; + assert_eq!(SIMPLEX_ROUNDLESS, u32::MAX, "SIMPLEX_ROUNDLESS should be u32::MAX"); assert_eq!(SIMPLEX_ROUNDLESS, 0xFFFFFFFF, "SIMPLEX_ROUNDLESS should be 0xFFFFFFFF"); } @@ -1292,6 +1259,8 @@ fn test_simplex_roundless_constant_value() { /// by forcing EMPTY collation on non-committed parents. #[test] fn test_simplex_state_options_require_finalized_parent() { + use crate::simplex_state::SimplexStateOptions; + // Default (cpp_compatible) should have require_finalized_parent=false let cpp_compat = SimplexStateOptions::cpp_compatible(); assert!( @@ -1299,7 +1268,7 @@ fn test_simplex_state_options_require_finalized_parent() { "cpp_compatible() should have require_finalized_parent=false" ); - // With optimistic validation, the collation gate is disabled: + // With optimistic validation (TN-822), the collation gate is disabled: // ValidatorGroup now uses candidate-native validation, so non-finalized parents are allowed. assert!( !DISABLE_NON_FINALIZED_PARENTS_FOR_COLLATION, @@ -1441,7 +1410,7 @@ fn test_candidate_decision_fail_drops_late_failure_for_committed_block() { } // ============================================================================ -// Optimistic Validation Tests +// Optimistic Validation Tests (TN-822 / OPTIMISTIC-VALID-1) // ============================================================================ /// Helper: create a non-empty RawCandidate for check_validation tests. @@ -1469,51 +1438,26 @@ fn make_test_non_empty_candidate( crate::block::RawCandidate::new(candidate_id, parent_id, ValidatorIndex::new(0), block, vec![]) } -/// Helper: create an empty RawCandidate with a specific referenced BlockIdExt. -fn make_test_empty_candidate_with_block( +/// Helper: create an empty RawCandidate for check_validation tests. +fn make_test_empty_candidate( candidate_id: RawCandidateId, parent_id: RawCandidateId, - referenced_block: BlockIdExt, ) -> crate::block::RawCandidate { + let block_id = BlockIdExt::with_params( + ShardIdent::masterchain(), + parent_id.slot.value() + 1, + UInt256::rand(), + UInt256::rand(), + ); crate::block::RawCandidate::new_empty( candidate_id, parent_id, ValidatorIndex::new(0), - referenced_block, + block_id, vec![], ) } -/// Helper: insert a minimal ReceivedCandidate into the processor's received_candidates map. -fn insert_received_candidate( - processor: &mut SessionProcessor, - candidate_id: &RawCandidateId, - block_id: BlockIdExt, - is_empty: bool, - parent_id: Option, -) { - processor.received_candidates.insert( - candidate_id.clone(), - ReceivedCandidate { - slot: candidate_id.slot, - source_idx: ValidatorIndex::new(0), - candidate_id_hash: candidate_id.hash.clone(), - candidate_hash_data_bytes: Vec::new(), - block_id: block_id.clone(), - root_hash: block_id.root_hash.clone(), - file_hash: block_id.file_hash.clone(), - data: consensus_common::ConsensusCommonFactory::create_block_payload(Vec::new()), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - Vec::new(), - ), - receive_time: SystemTime::now(), - is_empty, - parent_id, - is_fully_resolved: true, - }, - ); -} - /// Helper: insert a PendingValidation into the processor. fn insert_pending_validation( processor: &mut SessionProcessor, @@ -1640,134 +1584,20 @@ fn test_check_validation_auto_approves_empty_blocks() { let parent_slot = SlotIndex::new(0); let parent_id = RawCandidateId { slot: parent_slot, hash: UInt256::rand() }; - let parent_block_id = - BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); - - insert_received_candidate( - &mut fixture.processor, - &parent_id, - parent_block_id.clone(), - false, - None, - ); - - let child_slot = SlotIndex::new(1); - let child_id = RawCandidateId { slot: child_slot, hash: UInt256::rand() }; - - let raw_candidate = make_test_empty_candidate_with_block( - child_id.clone(), - parent_id.clone(), - parent_block_id.clone(), - ); - insert_received_candidate( - &mut fixture.processor, - &child_id, - parent_block_id, - true, - Some(parent_id), - ); - let time = fixture.description.get_time(); - insert_pending_validation(&mut fixture.processor, &child_id, raw_candidate, time); - - // Empty blocks with a matching referenced block should be auto-approved. - // C++ block-validator.cpp accepts when block == event->state->as_normal(). - fixture.processor.check_validation(); - assert!( - !fixture.processor.pending_validations.contains_key(&child_id), - "empty block must be approved when referenced block matches parent normal tip" - ); -} - -#[test] -fn test_empty_block_accepted_when_referenced_block_matches_parent() { - let mut fixture = TestFixture::new(4); - - let parent_slot = SlotIndex::new(0); - let parent_id = RawCandidateId { slot: parent_slot, hash: UInt256::rand() }; - - let parent_block_id = - BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); - - insert_received_candidate( - &mut fixture.processor, - &parent_id, - parent_block_id.clone(), - false, - None, - ); - let child_slot = SlotIndex::new(1); let child_id = RawCandidateId { slot: child_slot, hash: UInt256::rand() }; - let raw_candidate = make_test_empty_candidate_with_block( - child_id.clone(), - parent_id.clone(), - parent_block_id.clone(), - ); - insert_received_candidate( - &mut fixture.processor, - &child_id, - parent_block_id, - true, - Some(parent_id), - ); + let raw_candidate = make_test_empty_candidate(child_id.clone(), parent_id.clone()); let time = fixture.description.get_time(); insert_pending_validation(&mut fixture.processor, &child_id, raw_candidate, time); + // Empty blocks should be auto-approved even without parent notarization. + // candidate_decision_ok_internal removes from both pending_validations and pending_approve, + // so we verify the candidate was processed by checking it left pending_validations. fixture.processor.check_validation(); assert!( !fixture.processor.pending_validations.contains_key(&child_id), - "empty block must be approved when referenced block matches parent normal tip" - ); - assert!( - fixture.processor.approved.contains_key(&child_id), - "empty block must appear in approved set after matching reference check" - ); -} - -#[test] -fn test_empty_block_rejected_when_referenced_block_differs() { - let mut fixture = TestFixture::new(4); - - let parent_slot = SlotIndex::new(0); - let parent_id = RawCandidateId { slot: parent_slot, hash: UInt256::rand() }; - - let parent_block_id = - BlockIdExt::with_params(ShardIdent::masterchain(), 1, UInt256::rand(), UInt256::rand()); - - insert_received_candidate(&mut fixture.processor, &parent_id, parent_block_id, false, None); - - let child_slot = SlotIndex::new(1); - let child_id = RawCandidateId { slot: child_slot, hash: UInt256::rand() }; - - let wrong_block_id = - BlockIdExt::with_params(ShardIdent::masterchain(), 99, UInt256::rand(), UInt256::rand()); - - let raw_candidate = make_test_empty_candidate_with_block( - child_id.clone(), - parent_id.clone(), - wrong_block_id.clone(), - ); - insert_received_candidate( - &mut fixture.processor, - &child_id, - wrong_block_id, - true, - Some(parent_id), - ); - let time = fixture.description.get_time(); - insert_pending_validation(&mut fixture.processor, &child_id, raw_candidate, time); - - // C++ block-validator.cpp rejects empty candidates whose referenced block - // does not match event->state->as_normal(). Rust must do the same. - fixture.processor.check_validation(); - assert!( - fixture.processor.rejected.contains(&child_id), - "empty block must be rejected when referenced block differs from parent normal tip" - ); - assert!( - !fixture.processor.approved.contains_key(&child_id), - "rejected empty block must not appear in approved set" + "empty block must be processed (removed from pending_validations) regardless of parent notarization" ); } @@ -1882,7 +1712,7 @@ fn test_check_validation_chains_notarized_parent_to_descendant() { } // ============================================================================ -// Health check anomaly tests +// Health check anomaly tests (NODE-19) // ============================================================================ /// Reset health alert timestamps to a deterministic base time so that @@ -2138,1058 +1968,3 @@ fn test_check_collation_pacing_gate_is_idempotent() { fixture.processor.check_collation(); assert_eq!(fixture.processor.get_next_awake_time(), gate_time); } - -// ============================================================================ -// Candidate Query Fallback Tests (C++ parity: CandidateResolver DB fallback) -// ============================================================================ - -#[test] -fn test_candidate_query_fallback_cache_hit() { - let mut fixture = TestFixture::new(4); - let slot = SlotIndex::new(5); - let block_hash = UInt256::rand(); - let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; - - let fake_candidate_bytes = vec![0xCA, 0xFE, 0xBA, 0xBE]; - fixture.processor.candidate_data_cache.insert(candidate_id, fake_candidate_bytes.clone()); - - let (tx, rx) = channel(); - let callback: crate::QueryResponseCallback = Box::new(move |result| { - tx.send(result).unwrap(); - }); - - fixture.processor.handle_candidate_query_fallback(slot, block_hash, false, callback); - - let result = rx.recv_timeout(Duration::from_secs(2)).expect("callback not called"); - let payload = result.expect("response should be Ok"); - let response_bytes = payload.data(); - - assert!(!response_bytes.is_empty(), "response should contain serialized CandidateAndCert"); - - let deserialized = deserialize_boxed(response_bytes) - .expect("should deserialize response") - .downcast::() - .expect("should be CandidateAndCert"); - - let inner = match deserialized { - CandidateAndCert::Consensus_Simplex_CandidateAndCert(inner) => inner, - }; - - assert_eq!( - &inner.candidate[..], - &fake_candidate_bytes[..], - "candidate bytes should match the cached data" - ); -} - -#[test] -fn test_candidate_query_fallback_miss_returns_empty() { - let mut fixture = TestFixture::new(4); - let slot = SlotIndex::new(99); - let block_hash = UInt256::rand(); - - let (tx, rx) = channel(); - let callback: crate::QueryResponseCallback = Box::new(move |result| { - tx.send(result).unwrap(); - }); - - fixture.processor.handle_candidate_query_fallback(slot, block_hash, false, callback); - - let result = rx.recv_timeout(Duration::from_secs(5)).expect("callback not called"); - let payload = result.expect("response should be Ok even for empty"); - let response_bytes = payload.data(); - - let deserialized = deserialize_boxed(response_bytes) - .expect("should deserialize response") - .downcast::() - .expect("should be CandidateAndCert"); - - let inner = match deserialized { - CandidateAndCert::Consensus_Simplex_CandidateAndCert(inner) => inner, - }; - - assert!(inner.candidate.is_empty(), "candidate bytes should be empty when not found"); -} - -#[test] -fn test_candidate_data_cache_populated_on_candidate_received() { - let _ = env_logger::Builder::new().filter_level(log::LevelFilter::Debug).try_init(); - let mut fixture = TestFixture::new(4); - - // Use slot 0 so that validator 0 (local) is the slot leader - let slot = 0u32; - let block_data = vec![1u8, 2, 3, 4, 5]; - let collated_data: Vec = vec![]; - let root_hash = UInt256::from_slice(&sha256_digest(&block_data)); - let shard = ShardIdent::masterchain(); - - // Build uncompressed TL candidate (same approach as test_receiver_candidate_resolver) - let tl_inner = TlCandidate { - src: UInt256::default(), - round: slot as i32, - root_hash: root_hash.clone(), - data: block_data.clone().into(), - collated_data: collated_data.clone().into(), - }; - let candidate_bytes = consensus_common::serialize_tl_boxed_object!(&tl_inner.into_boxed()); - - let block_id = BlockIdExt { - shard_id: shard, - seq_no: slot, - root_hash: root_hash.clone(), - file_hash: root_hash.clone(), - }; - let collated_file_hash = UInt256::from_slice(&sha256_digest(&collated_data)); - - let candidate_hash = crate::utils::compute_candidate_id_hash_u32( - slot, - Some(&block_id), - Some(&collated_file_hash), - None, - ); - - let session_id = fixture.processor.session_id().clone(); - let leader_key = fixture.processor.description.get_source_public_key(ValidatorIndex::new(0)); - let signature = - crate::utils::sign_candidate_u32(&session_id, slot, &candidate_hash, leader_key) - .expect("signing failed"); - - let broadcast = CandidateData::Consensus_Block(CandidateDataBlock { - slot: slot as i32, - candidate: candidate_bytes.into(), - parent: CandidateParent::Consensus_CandidateWithoutParents, - signature: signature.into(), - }); - - let candidate_id = RawCandidateId { slot: SlotIndex::new(slot), hash: candidate_hash.clone() }; - - assert!( - !fixture.processor.candidate_data_cache.contains_key(&candidate_id), - "cache should be empty before on_candidate_received" - ); - - fixture.processor.on_candidate_received(0, broadcast, None); - - assert!( - fixture.processor.candidate_data_cache.contains_key(&candidate_id), - "cache should be populated after on_candidate_received" - ); - - assert!( - fixture.processor.received_candidates.contains_key(&candidate_id), - "received_candidates should also have the candidate" - ); -} - -// ============================================================================ -// Protocol Parity Tests (stub body, partial merge, finalized seqno) -// ============================================================================ - -#[test] -fn test_has_real_candidate_body_returns_false_for_stub() { - let mut fixture = TestFixture::new(4); - let slot = SlotIndex::new(10); - let hash = UInt256::rand(); - let candidate_id = RawCandidateId { slot, hash: hash.clone() }; - - // No entry => false - assert!(!fixture.processor.has_real_candidate_body(&candidate_id)); - - // Insert a finalized-boundary stub (empty candidate_hash_data_bytes) - fixture.processor.received_candidates.insert( - candidate_id.clone(), - ReceivedCandidate { - slot, - source_idx: ValidatorIndex::new(0), - candidate_id_hash: hash.clone(), - candidate_hash_data_bytes: Vec::new(), // stub marker - block_id: BlockIdExt::default(), - root_hash: UInt256::default(), - file_hash: UInt256::default(), - data: consensus_common::ConsensusCommonFactory::create_block_payload(Vec::new()), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - Vec::new(), - ), - receive_time: fixture.processor.now(), - is_empty: false, - parent_id: None, - is_fully_resolved: true, - }, - ); - - // Stub => false - assert!( - !fixture.processor.has_real_candidate_body(&candidate_id), - "finalized-boundary stub must NOT count as real body" - ); - - // Overwrite with real data - fixture - .processor - .received_candidates - .get_mut(&candidate_id) - .unwrap() - .candidate_hash_data_bytes = vec![1, 2, 3]; - - // Now should be true - assert!( - fixture.processor.has_real_candidate_body(&candidate_id), - "entry with non-empty candidate_hash_data_bytes must count as real body" - ); -} - -#[test] -fn test_handle_block_finalized_requests_triggered_stub_body_when_committed_head_exists() { - let mut fixture = TestFixture::new(4); - - // Simulate an already-committed head, so triggered finalized block enters - // collect_gapless_commit_chain() as a non-genesis continuation. - fixture.processor.last_committed_seqno = Some(100); - fixture.processor.last_committed_block_id = Some(BlockIdExt::with_params( - ShardIdent::masterchain(), - 100, - UInt256::rand(), - UInt256::rand(), - )); - - let slot = SlotIndex::new(555); - let block_hash = UInt256::rand(); - let candidate_id = RawCandidateId { slot, hash: block_hash.clone() }; - let finalized_block_id = - BlockIdExt::with_params(ShardIdent::masterchain(), 101, UInt256::rand(), UInt256::rand()); - - let final_cert = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { slot, block_hash: block_hash.clone() }, - signatures: vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), - ], - }); - - fixture.processor.handle_block_finalized(BlockFinalizedEvent { - slot, - block_hash: block_hash.clone(), - block_id: Some(finalized_block_id), - certificate: final_cert, - }); - - assert!( - fixture.processor.requested_candidates.contains_key(&candidate_id), - "triggered finalized-boundary stub must be treated as missing body and requested" - ); - - // The core regression guard is scheduling requestCandidate at processor level. - // (Receiver send timing is exercised by dedicated delayed-action tests.) -} - -#[test] -fn test_candidate_query_fallback_returns_notar_only_when_body_missing() { - let mut fixture = TestFixture::new(4); - let slot = SlotIndex::new(99); - let block_hash = UInt256::rand(); - - let (tx, rx) = channel(); - let callback: crate::QueryResponseCallback = Box::new(move |result| { - tx.send(result).unwrap(); - }); - - // No candidate in cache or DB => should return empty/empty - fixture.processor.handle_candidate_query_fallback(slot, block_hash, false, callback); - - let result = rx.recv().unwrap(); - assert!(result.is_ok(), "should return Ok even when nothing found"); -} - -#[test] -fn test_set_mc_finalized_seqno_couples_consensus_finalized_seqno() { - let mut fixture = TestFixture::new(4); - - // Initially 0 - assert_eq!(fixture.processor.last_consensus_finalized_seqno, Some(0)); - - // Set MC finalized to 42 - fixture.processor.set_mc_finalized_seqno(42); - - // C++ parity: consensus finalized should advance to max(mc, consensus) - assert_eq!( - fixture.processor.last_consensus_finalized_seqno, - Some(42), - "set_mc_finalized_seqno should couple to last_consensus_finalized_seqno via max()" - ); - - // Set consensus finalized higher via direct field (simulating a final commit) - fixture.processor.last_consensus_finalized_seqno = Some(100); - - // Set MC finalized lower => should NOT decrease consensus - fixture.processor.set_mc_finalized_seqno(50); - assert_eq!( - fixture.processor.last_consensus_finalized_seqno, - Some(100), - "set_mc_finalized_seqno must not decrease last_consensus_finalized_seqno" - ); - - // Monotonic MC seqno: out-of-order MC event with lower seqno must not regress - fixture.processor.last_mc_finalized_seqno = Some(200); - fixture.processor.set_mc_finalized_seqno(150); - assert_eq!( - fixture.processor.last_mc_finalized_seqno, - Some(200), - "set_mc_finalized_seqno must keep last_mc_finalized_seqno monotonic" - ); -} - -// ============================================================================ -// Foreign Certificate Relay Regression Tests (C++ parity) -// ============================================================================ - -/// Verify that a notarization certificate ingested via set_notarize_certificate -/// (foreign path) triggers relay to peers. -#[test] -fn test_foreign_notarization_cert_is_relayed() { - let mut fixture = TestFixture::new(4); - - let slot = crate::block::SlotIndex::new(3); - let block_hash = UInt256::rand(); - - let signatures = vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![10]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![11]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![12]), - ]; - let vote = crate::simplex_state::NotarizeVote { slot, block_hash: block_hash.clone() }; - let cert = Arc::new(crate::certificate::Certificate { vote, signatures }); - - let event = crate::simplex_state::NotarizationReachedEvent { - slot, - block_hash: block_hash.clone(), - certificate: cert, - }; - - fixture.processor.handle_notarization_reached(event); - - let actions = fixture.drain_receiver_actions(); - assert!( - actions.iter().any(|a| matches!(a, ReceiverAction::SendCertificate { .. })), - "foreign notarization cert must be relayed (C++ parity: handle_saved_certificate)" - ); -} - -/// Verify that a finalization certificate ingested via set_finalize_certificate -/// (foreign path) triggers relay to peers. -#[test] -fn test_foreign_finalization_cert_is_relayed() { - let mut fixture = TestFixture::new(4); - - let slot = crate::block::SlotIndex::new(5); - let block_hash = UInt256::rand(); - - let signatures = vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![20]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![21]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![22]), - ]; - let vote = crate::simplex_state::FinalizeVote { slot, block_hash: block_hash.clone() }; - let cert = Arc::new(crate::certificate::Certificate { vote, signatures }); - - let event = crate::simplex_state::FinalizationReachedEvent { - slot, - block_hash: block_hash.clone(), - certificate: cert, - }; - - fixture.processor.handle_finalization_reached(event); - - let actions = fixture.drain_receiver_actions(); - assert!( - actions.iter().any(|a| matches!(a, ReceiverAction::SendCertificate { .. })), - "foreign finalization cert must be relayed (C++ parity: handle_saved_certificate)" - ); -} - -#[test] -fn test_foreign_vote_is_not_rebroadcast() { - let mut fixture = TestFixture::new(4); - - let slot = crate::block::SlotIndex::new(2); - let block_hash = UInt256::from([0xAB; 32]); - let vote = crate::simplex_state::Vote::Notarize(crate::simplex_state::NotarizeVote { - slot, - block_hash, - }); - let tl_vote = crate::utils::sign_vote( - &vote, - fixture.description.get_session_id(), - &fixture.nodes[1].public_key, - ) - .expect("failed to sign foreign vote"); - let raw_vote: crate::RawVoteData = - consensus_common::serialize_tl_boxed_object!(&tl_vote).into(); - - fixture.processor.on_vote(1, tl_vote, raw_vote); - - let actions = fixture.drain_receiver_actions(); - assert!( - !actions.iter().any(|a| matches!(a, ReceiverAction::SendVote { .. })), - "foreign votes must not be re-broadcast" - ); -} - -#[test] -fn test_recovery_drain_startup_events_drops_certificate_relay_events() { - let mut fixture = TestFixture::new(4); - - let slot = crate::block::SlotIndex::new(3); - let block_hash = UInt256::from([0xCD; 32]); - let signatures = vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![1]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![2]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![3]), - ]; - let vote = crate::simplex_state::NotarizeVote { slot, block_hash: block_hash.clone() }; - let cert = Arc::new(crate::certificate::Certificate { vote, signatures }); - let stored = fixture - .processor - .simplex_state - .set_notarize_certificate(&fixture.description, slot, &block_hash, cert) - .expect("set_notarize_certificate should succeed"); - assert!(stored, "notar cert should be stored before startup drain"); - - let kept_votes = - crate::startup_recovery::SessionStartupRecoveryListener::recovery_drain_startup_events( - &mut fixture.processor, - ); - assert!( - kept_votes.is_empty(), - "this setup should produce only certificate events, no startup votes" - ); - - fixture.processor.check_all(); - let actions = fixture.drain_receiver_actions(); - assert!( - !actions.iter().any(|a| matches!(a, ReceiverAction::SendCertificate { .. })), - "drained startup certificate events must not be re-broadcast on first normal tick" - ); -} - -// ============================================================================ -// Gapless commit scheduler hardening tests -// ============================================================================ - -/// Verify that `cleanup_old_candidates` removes stale journal entries for old slots -/// and increments the session error counter accordingly. -#[test] -fn test_journal_cleanup_removes_stale_entries() { - let mut fixture = TestFixture::new(4); - - let old_slot = SlotIndex::new(5); - let current_slot = SlotIndex::new(20); - let old_hash = UInt256::rand(); - let current_hash = UInt256::rand(); - - let old_id = RawCandidateId { slot: old_slot, hash: old_hash.clone() }; - let current_id = RawCandidateId { slot: current_slot, hash: current_hash.clone() }; - - let dummy_cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { slot: old_slot, block_hash: old_hash.clone() }, - signatures: Vec::new(), - }); - - let dummy_cert2: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { - slot: current_slot, - block_hash: current_hash.clone(), - }, - signatures: Vec::new(), - }); - - let now = fixture.description.get_time(); - - fixture.processor.finalized_journal_pending_commit.insert( - old_id.clone(), - FinalizedEntry { - event: BlockFinalizedEvent { - slot: old_slot, - block_hash: old_hash, - block_id: None, - certificate: dummy_cert, - }, - finalized_at: now - Duration::from_secs(60), - }, - ); - - fixture.processor.finalized_journal_pending_commit.insert( - current_id.clone(), - FinalizedEntry { - event: BlockFinalizedEvent { - slot: current_slot, - block_hash: current_hash, - block_id: None, - certificate: dummy_cert2, - }, - finalized_at: now, - }, - ); - - assert_eq!(fixture.processor.finalized_journal_pending_commit.len(), 2); - - let errors_before = - fixture.processor.session_errors_count.load(std::sync::atomic::Ordering::Relaxed); - - // Cleanup slots < 10 โ€” old_slot(5) should be removed, current_slot(20) kept. - fixture.processor.cleanup_old_candidates(SlotIndex::new(10)); - - assert_eq!(fixture.processor.finalized_journal_pending_commit.len(), 1); - assert!(!fixture.processor.finalized_journal_pending_commit.contains_key(&old_id)); - assert!(fixture.processor.finalized_journal_pending_commit.contains_key(¤t_id)); - - let errors_after = - fixture.processor.session_errors_count.load(std::sync::atomic::Ordering::Relaxed); - assert_eq!(errors_after - errors_before, 1, "stale journal entry should increment error count"); -} - -/// Verify that the scheduler processes entries in seqno-ascending order, -/// not arbitrary HashMap order. -/// Both entries are WaitingForFinalCert on MC (seqno ahead of committed head) -/// so neither can commit. Both stay pending in the journal. -#[test] -fn test_try_commit_processes_in_seqno_order() { - let mut fixture = TestFixture::new(4); - - // TestFixture defaults to masterchain. Committed head seqno = 10. - let committed_block_id = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 10, - UInt256::rand(), - UInt256::rand(), - ); - fixture.processor.last_committed_seqno = Some(10); - fixture.processor.last_committed_block_id = Some(committed_block_id); - - // Both seqnos are ahead of expected (11), so MC fast-path returns - // WaitingForFinalCert and both entries remain in the journal. - let slot_a = SlotIndex::new(30); - let hash_a = UInt256::rand(); - let id_a = RawCandidateId { slot: slot_a, hash: hash_a.clone() }; - let block_id_a = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 13, // ahead: expected=11 - UInt256::rand(), - UInt256::rand(), - ); - - let slot_b = SlotIndex::new(25); - let hash_b = UInt256::rand(); - let id_b = RawCandidateId { slot: slot_b, hash: hash_b.clone() }; - let block_id_b = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 12, // ahead: expected=11 - UInt256::rand(), - UInt256::rand(), - ); - - for (id, slot, hash, block_id) in - [(&id_a, slot_a, &hash_a, &block_id_a), (&id_b, slot_b, &hash_b, &block_id_b)] - { - fixture.processor.received_candidates.insert( - id.clone(), - ReceivedCandidate { - slot, - source_idx: ValidatorIndex::new(0), - candidate_id_hash: hash.clone(), - candidate_hash_data_bytes: vec![1, 2, 3], - block_id: block_id.clone(), - root_hash: block_id.root_hash.clone(), - file_hash: block_id.file_hash.clone(), - data: consensus_common::ConsensusCommonFactory::create_block_payload( - vec![0xAA].into(), - ), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - vec![0xBB].into(), - ), - receive_time: fixture.description.get_time(), - is_empty: false, - parent_id: None, - is_fully_resolved: true, - }, - ); - - let cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { slot, block_hash: hash.clone() }, - signatures: vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), - ], - }); - fixture.processor.finalized_journal_pending_commit.insert( - id.clone(), - FinalizedEntry { - event: BlockFinalizedEvent { - slot, - block_hash: hash.clone(), - block_id: Some(block_id.clone()), - certificate: cert, - }, - finalized_at: fixture.description.get_time(), - }, - ); - } - - fixture.processor.try_commit_finalized_chains(); - - // Both entries remain pending โ€” MC blocks ahead of committed head wait for FinalCert. - assert!(fixture.processor.finalized_journal_pending_commit.contains_key(&id_a)); - assert!(fixture.processor.finalized_journal_pending_commit.contains_key(&id_b)); - assert_eq!(fixture.processor.last_committed_seqno, Some(10)); -} - -/// Verify the finalized_uncommitted_gauge is updated correctly. -#[test] -fn test_finalized_uncommitted_gauge_tracks_journal_size() { - let mut fixture = TestFixture::new(4); - - // Empty journal โ€” gauge should be 0 (function runs without panic). - fixture.processor.try_commit_finalized_chains(); - - // Add a journal entry that will become AlreadyCommitted - let slot = SlotIndex::new(5); - let hash = UInt256::rand(); - let id = RawCandidateId { slot, hash: hash.clone() }; - - fixture.processor.last_committed_seqno = Some(100); - let committed_block_id = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 100, - UInt256::rand(), - UInt256::rand(), - ); - fixture.processor.last_committed_block_id = Some(committed_block_id.clone()); - - // Insert a received candidate with seqno < committed so collect_gapless returns AlreadyCommitted - fixture.processor.received_candidates.insert( - id.clone(), - ReceivedCandidate { - slot, - source_idx: ValidatorIndex::new(0), - candidate_id_hash: hash.clone(), - candidate_hash_data_bytes: vec![1, 2, 3], - block_id: committed_block_id.clone(), - root_hash: committed_block_id.root_hash.clone(), - file_hash: committed_block_id.file_hash.clone(), - data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xAA].into()), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - vec![0xBB].into(), - ), - receive_time: fixture.description.get_time(), - is_empty: false, - parent_id: None, - is_fully_resolved: true, - }, - ); - - let cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { slot, block_hash: hash.clone() }, - signatures: Vec::new(), - }); - fixture.processor.finalized_journal_pending_commit.insert( - id.clone(), - FinalizedEntry { - event: BlockFinalizedEvent { - slot, - block_hash: hash, - block_id: Some(committed_block_id), - certificate: cert, - }, - finalized_at: fixture.description.get_time(), - }, - ); - - assert_eq!(fixture.processor.finalized_journal_pending_commit.len(), 1); - - fixture.processor.try_commit_finalized_chains(); - - // The AlreadyCommitted entry should be removed - assert!( - fixture.processor.finalized_journal_pending_commit.is_empty(), - "AlreadyCommitted entry should be removed from journal" - ); -} - -/// Verify that seqno-sorted iteration commits sequential chains in a single pass -/// and schedules an immediate re-check via set_next_awake_time(now). -#[test] -fn test_sorted_pass_commits_sequential_chains_and_reschedules() { - let mut fixture = TestFixture::new(4); - - // Committed head at seqno 10 - let committed_block_id = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 10, - UInt256::rand(), - UInt256::rand(), - ); - fixture.processor.last_committed_seqno = Some(10); - fixture.processor.last_committed_block_id = Some(committed_block_id.clone()); - - // Build chain: slot_a (seqno 11, parent=boundary) โ†’ slot_b (seqno 12, parent=slot_a) - // On MC with matching expected_seqno, the MC fast-path commits the single block. - // After committing slot_a (seqno=11), re-loop should pick up slot_b (seqno=12). - let slot_a = SlotIndex::new(20); - let hash_a = UInt256::rand(); - let id_a = RawCandidateId { slot: slot_a, hash: hash_a.clone() }; - let block_id_a = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 11, - UInt256::rand(), - UInt256::rand(), - ); - - let slot_b = SlotIndex::new(25); - let hash_b = UInt256::rand(); - let id_b = RawCandidateId { slot: slot_b, hash: hash_b.clone() }; - let block_id_b = ton_block::BlockIdExt::with_params( - ton_block::ShardIdent::masterchain(), - 12, - UInt256::rand(), - UInt256::rand(), - ); - - // slot_a: parent = None (session boundary โ†’ MC fast-path single-commit if seqno matches) - fixture.processor.received_candidates.insert( - id_a.clone(), - ReceivedCandidate { - slot: slot_a, - source_idx: ValidatorIndex::new(0), - candidate_id_hash: hash_a.clone(), - candidate_hash_data_bytes: vec![1, 2, 3], - block_id: block_id_a.clone(), - root_hash: block_id_a.root_hash.clone(), - file_hash: block_id_a.file_hash.clone(), - data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xAA].into()), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - vec![0xBB].into(), - ), - receive_time: fixture.description.get_time(), - is_empty: false, - parent_id: None, - is_fully_resolved: true, - }, - ); - - // slot_b: parent = slot_a (will be WaitingForFinalCert initially since expected=11, seqno=12) - fixture.processor.received_candidates.insert( - id_b.clone(), - ReceivedCandidate { - slot: slot_b, - source_idx: ValidatorIndex::new(0), - candidate_id_hash: hash_b.clone(), - candidate_hash_data_bytes: vec![4, 5, 6], - block_id: block_id_b.clone(), - root_hash: block_id_b.root_hash.clone(), - file_hash: block_id_b.file_hash.clone(), - data: consensus_common::ConsensusCommonFactory::create_block_payload(vec![0xCC].into()), - collated_data: consensus_common::ConsensusCommonFactory::create_block_payload( - vec![0xDD].into(), - ), - receive_time: fixture.description.get_time(), - is_empty: false, - parent_id: Some(id_a.clone()), - is_fully_resolved: true, - }, - ); - - // Provide FinalCert for both (MC requires FinalCert for non-empty blocks) - for (slot, hash) in [(&slot_a, &hash_a), (&slot_b, &hash_b)] { - let final_cert = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { slot: *slot, block_hash: hash.clone() }, - signatures: vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), - ], - }); - fixture - .processor - .simplex_state - .set_finalize_certificate(&fixture.description, *slot, hash, final_cert) - .expect("store final cert"); - } - - // Journal entries for both - for (id, slot, hash, block_id) in - [(&id_a, slot_a, &hash_a, &block_id_a), (&id_b, slot_b, &hash_b, &block_id_b)] - { - let cert: crate::certificate::FinalCertPtr = Arc::new(crate::certificate::Certificate { - vote: crate::simplex_state::FinalizeVote { slot, block_hash: hash.clone() }, - signatures: vec![ - crate::certificate::VoteSignature::new(ValidatorIndex::new(0), vec![0u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(1), vec![1u8; 64]), - crate::certificate::VoteSignature::new(ValidatorIndex::new(2), vec![2u8; 64]), - ], - }); - fixture.processor.finalized_journal_pending_commit.insert( - id.clone(), - FinalizedEntry { - event: BlockFinalizedEvent { - slot, - block_hash: hash.clone(), - block_id: Some(block_id.clone()), - certificate: cert, - }, - finalized_at: fixture.description.get_time(), - }, - ); - } - - // Push next_awake_time into the future so we can verify it gets pulled back after commit. - fixture.processor.reset_next_awake_time(); - assert!( - fixture.processor.get_next_awake_time() > fixture.description.get_time(), - "next_awake_time should be in the future before commit" - ); - - // Because finalized_keys are sorted by seqno, slot_a (seqno=11) is processed - // first. After it commits, last_committed_seqno advances to 11, so when the - // iteration reaches slot_b (seqno=12), expected_seqno matches and it commits too. - fixture.processor.try_commit_finalized_chains(); - - assert_eq!( - fixture.processor.last_committed_seqno, - Some(12), - "sorted iteration should commit both seqno 11 and 12 in one pass" - ); - assert!( - fixture.processor.finalized_journal_pending_commit.is_empty(), - "all journal entries should be committed and removed" - ); - // Commits happened, so an immediate re-check should be scheduled. - assert!( - fixture.processor.get_next_awake_time() <= fixture.description.get_time(), - "next_awake_time should be <= now after a successful commit" - ); -} - -/// Verify that the correct processing order (validated candidates BEFORE -/// FSM timeouts) allows a candidate to be notarized even when the clock -/// has advanced past the skip timeout. -/// -/// Without Fix A (processing-order), calling `simplex_state.check_all()` -/// first would fire the `first_block_timeout` and skip-vote the slot -/// before the already-validated candidate is fed to the FSM. -#[test] -fn test_process_validated_candidates_before_fsm_timeout() { - let mut fixture = TestFixture::new(4); - - let slot = SlotIndex::new(0); - let candidate_hash = UInt256::rand(); - let candidate_id = RawCandidateId { slot, hash: candidate_hash.clone() }; - - // Create a non-empty candidate for slot 0 with no parent (genesis). - let raw_candidate = make_test_non_empty_candidate(candidate_id.clone(), None, &fixture.nodes); - let time = fixture.description.get_time(); - - // Insert pending validation so candidate_decision_ok_internal can find it. - insert_pending_validation(&mut fixture.processor, &candidate_id, raw_candidate, time); - - // Simulate validation success: push the resolved candidate into the queue. - fixture.processor.candidate_decision_ok_internal(candidate_id.clone(), slot, time); - assert!( - !fixture.processor.validated_candidates.is_empty(), - "candidate must be in the validated_candidates queue" - ); - - // Advance time past first_block_timeout + target_rate (defaults: 3s + 1s = 4s). - fixture.advance_time(Duration::from_secs(5)); - - // --- Correct order (Fix A): feed candidates, THEN run FSM timeouts --- - fixture.processor.process_validated_candidates(); - fixture.processor.simplex_state.check_all(&fixture.description); - - // Collect FSM events produced by the two calls above. - let mut has_notarize = false; - while let Some(event) = fixture.processor.simplex_state.pull_event() { - if let crate::simplex_state::SimplexEvent::BroadcastVote( - crate::simplex_state::Vote::Notarize(ref v), - ) = event - { - if v.slot == slot { - has_notarize = true; - } - } - } - - // The critical invariant: the candidate was notarized because - // process_validated_candidates() ran before simplex_state.check_all(). - assert!( - has_notarize, - "slot 0 must be notarized (candidate was fed to FSM before timeout evaluation)" - ); - // In C++ mode (allow_skip_after_notarize=true) a skip vote may follow - // the notarize vote after the timeout fires -- that is harmless and - // expected. The key property is that the notarize vote was emitted. -} - -/// Verify that the `log::warn!` for "drop because new block is already -/// committed" only fires when `cand_seqno <= committed_seqno`, i.e. the -/// candidate is actually dropped. When `cand_seqno > committed_seqno` -/// the candidate must proceed to `validated_candidates`. -#[test] -fn test_candidate_decision_ok_does_not_drop_when_cand_seqno_greater_than_committed() { - let mut fixture = TestFixture::new(4); - - let slot = SlotIndex::new(0); - let candidate_hash = UInt256::rand(); - let candidate_id = RawCandidateId { slot, hash: candidate_hash.clone() }; - - let raw_candidate = make_test_non_empty_candidate(candidate_id.clone(), None, &fixture.nodes); - let time = fixture.description.get_time(); - insert_pending_validation(&mut fixture.processor, &candidate_id, raw_candidate, time); - - // Set last_committed_seqno to a value BELOW the candidate's seqno. - // make_test_non_empty_candidate uses slot.value()+1 as seq_no, so for - // slot 0 the candidate seqno = 1. Setting committed to 0 means - // cand_seqno (1) > committed_seqno (0) โ†’ candidate must NOT be dropped. - fixture.processor.last_committed_seqno = Some(0); - - // Call the public wrapper which contains the guard. - let validity_start = time; - fixture.processor.candidate_decision_ok(slot, candidate_id.clone(), validity_start, time); - - // The candidate must have been pushed to validated_candidates (not dropped). - assert!( - !fixture.processor.validated_candidates.is_empty(), - "candidate with cand_seqno > committed_seqno must NOT be dropped" - ); - // And it must have been removed from pending_validations (consumed, not leaked). - assert!( - !fixture.processor.pending_validations.contains_key(&candidate_id), - "pending_validations entry must be consumed" - ); -} - -// ============================================================================ -// Candidate Chaining Tests (C++ parity) -// ============================================================================ - -/// Test that local_chain_head and generated_parent_cache start empty. -#[test] -fn test_local_chain_head_initial_state() { - let fixture = TestFixture::new(4); - assert!(fixture.processor.local_chain_head.is_none()); - assert!(fixture.processor.generated_parent_cache.is_empty()); -} - -/// Test that invalidate_local_chain_head clears both the chain head and cache. -#[test] -fn test_invalidate_local_chain_head_clears_state() { - let mut fixture = TestFixture::new(4); - - let hash = UInt256::from([0xAA; 32]); - let block_id = BlockIdExt::with_params( - ShardIdent::masterchain(), - 1, - UInt256::from([0xBB; 32]), - UInt256::from([0xCC; 32]), - ); - let parent_info = - crate::block::CandidateParentInfo { slot: SlotIndex::new(0), hash: hash.clone() }; - let raw_id = RawCandidateId { slot: SlotIndex::new(0), hash: hash.clone() }; - - fixture.processor.local_chain_head = Some(LocalChainHead { - window: WindowIndex::new(0), - slot: SlotIndex::new(0), - parent_info, - block_id: block_id.clone(), - }); - fixture.processor.generated_parent_cache.insert(raw_id.clone(), block_id); - - assert!(fixture.processor.local_chain_head.is_some()); - assert!(!fixture.processor.generated_parent_cache.is_empty()); - - fixture.processor.invalidate_local_chain_head(); - - assert!(fixture.processor.local_chain_head.is_none()); - assert!(fixture.processor.generated_parent_cache.is_empty()); -} - -/// Test that resolve_parent_block_id finds parents in generated_parent_cache -/// even before the async on_candidate_received self-loop populates received_candidates. -#[test] -fn test_resolve_parent_from_generated_cache() { - let mut fixture = TestFixture::new(4); - - let hash = UInt256::from([0xDD; 32]); - let block_id = BlockIdExt::with_params( - ShardIdent::masterchain(), - 1, - UInt256::from([0xEE; 32]), - UInt256::from([0xFF; 32]), - ); - let parent_info = - crate::block::CandidateParentInfo { slot: SlotIndex::new(5), hash: hash.clone() }; - let raw_id = RawCandidateId { slot: SlotIndex::new(5), hash: hash.clone() }; - - // Not in received_candidates yet - assert!(fixture.processor.resolve_parent_block_id(&parent_info).is_none()); - - // Seed the generated_parent_cache (as generated_block would) - fixture.processor.generated_parent_cache.insert(raw_id, block_id.clone()); - - // Now resolvable - let resolved = fixture.processor.resolve_parent_block_id(&parent_info); - assert_eq!(resolved, Some(block_id)); -} - -/// Test that reset_precollations clears the local chain head. -#[test] -fn test_reset_precollations_clears_chain_head() { - let mut fixture = TestFixture::new(4); - - let hash = UInt256::from([0x11; 32]); - let block_id = BlockIdExt::with_params( - ShardIdent::masterchain(), - 1, - UInt256::from([0x22; 32]), - UInt256::from([0x33; 32]), - ); - let parent_info = - crate::block::CandidateParentInfo { slot: SlotIndex::new(0), hash: hash.clone() }; - - fixture.processor.local_chain_head = Some(LocalChainHead { - window: WindowIndex::new(0), - slot: SlotIndex::new(0), - parent_info, - block_id, - }); - - fixture.processor.reset_precollations(); - - assert!(fixture.processor.local_chain_head.is_none()); - assert!(fixture.processor.generated_parent_cache.is_empty()); -} - -/// Test that multi-slot window options produce correct precollation depth. -#[test] -fn test_slots_per_leader_window_precollation_depth() { - // Single-slot window: no precollation - let opts1 = SessionOptions { slots_per_leader_window: 1, ..Default::default() }; - assert_eq!(opts1.slots_per_leader_window.saturating_sub(1), 0); - - // 4-slot window: up to 3 precollated - let opts4 = SessionOptions { slots_per_leader_window: 4, ..Default::default() }; - assert_eq!(opts4.slots_per_leader_window.saturating_sub(1), 3); - - // 8-slot window: up to 7 precollated - let opts8 = SessionOptions { slots_per_leader_window: 8, ..Default::default() }; - assert_eq!(opts8.slots_per_leader_window.saturating_sub(1), 7); -} - -/// Test that creating a SessionProcessor with multi-slot window succeeds. -#[test] -fn test_multi_slot_window_session_creation() { - let opts = SessionOptions { slots_per_leader_window: 4, ..Default::default() }; - let fixture = TestFixture::new_with_opts(4, opts); - assert_eq!(fixture.description.opts().slots_per_leader_window, 4); - assert!(fixture.processor.local_chain_head.is_none()); -} diff --git a/src/node/simplex/src/tests/test_simplex_state.rs b/src/node/simplex/src/tests/test_simplex_state.rs index 3ac3276..3fd6884 100644 --- a/src/node/simplex/src/tests/test_simplex_state.rs +++ b/src/node/simplex/src/tests/test_simplex_state.rs @@ -194,8 +194,7 @@ fn test_new_creates_initial_state() { assert_eq!(state.first_non_finalized_slot, SlotIndex::new(0)); assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); assert!(!state.has_pending_events()); - // Timeouts start unarmed; SessionProcessor::start() calls set_timeouts(). - assert!(state.get_next_timeout().is_none()); + assert!(state.get_next_timeout().is_some()); // Window 0 should have None (genesis) as available base assert!(state.get_window(WindowIndex::new(0)).unwrap().available_bases.contains(&None)); @@ -262,12 +261,11 @@ fn test_on_candidate_stores_pending_when_no_parent() { let desc = create_test_desc(4, 2); let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - // Candidate for slot 1 with parent at slot 0, but parent isn't notarized yet - // so it can't be resolved โ†’ candidate stored as pending + // Create candidate for slot 0 but window doesn't have this parent available let parent_hash = UInt256::from_slice(&[1u8; 32]); let candidate = create_test_candidate( - 1, + 0, UInt256::default(), BlockIdExt::default(), Some((0, parent_hash)), @@ -276,7 +274,7 @@ fn test_on_candidate_stores_pending_when_no_parent() { state.on_candidate(&desc, candidate).expect("on_candidate should succeed"); - // Should NOT broadcast (parent not available in window state) + // Should NOT broadcast (parent not available) let events: Vec<_> = from_fn(|| state.pull_event()).collect(); assert!( !events.iter().any(|e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(_)))), @@ -284,8 +282,8 @@ fn test_on_candidate_stores_pending_when_no_parent() { events ); - // Should have pending block (slot 1 = offset 1 in window 0) - assert!(state.get_window(WindowIndex::new(0)).unwrap().slots[1].pending_block.is_some()); + // Should have pending block + assert!(state.get_window(WindowIndex::new(0)).unwrap().slots[0].pending_block.is_some()); } #[test] @@ -974,10 +972,8 @@ fn test_genesis_propagates_to_next_window_on_full_skip() { // Clear events while state.pull_event().is_some() {} - // Slot 0 should be skipped; C++ parity: first_non_finalized_slot stays at 0 - assert_eq!(state.first_non_finalized_slot, SlotIndex::new(0)); - // But first_non_progressed_slot advances - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); + // Slot 0 should be skipped, first_non_finalized_slot = 1 + assert_eq!(state.first_non_finalized_slot, SlotIndex::new(1)); // Window 1 should NOT have genesis yet (slot 0 was not the last slot in window 0) // Note: window 1 may or may not exist at this point @@ -999,9 +995,8 @@ fn test_genesis_propagates_to_next_window_on_full_skip() { // Clear events while state.pull_event().is_some() {} - // C++ parity: first_non_finalized_slot still at 0, progress cursor at 2 - assert_eq!(state.first_non_finalized_slot, SlotIndex::new(0)); - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); + // Now entire window 0 is skipped, first_non_finalized_slot = 2 + assert_eq!(state.first_non_finalized_slot, SlotIndex::new(2)); // Window 1 should now have genesis (None) as available base // This was propagated from window 0 since no finalization occurred @@ -1147,12 +1142,11 @@ fn test_skip_certificate_threshold_66_triggers_slot_skipped() { "Expected SlotSkipped event at skip certificate threshold" ); - // C++ parity: skip does NOT advance first_non_finalized_slot (only finalization does). - // But first_non_progressed_slot (C++ `now_`) does advance on skip. + // Skip advances first_non_finalized_slot (a skipped slot will never be finalized). assert_eq!( state.first_non_finalized_slot, - SlotIndex::new(0), - "first_non_finalized_slot should NOT advance on skip (C++ parity)" + SlotIndex::new(1), + "first_non_finalized_slot should advance past skipped slot" ); assert_eq!( state.first_non_progressed_slot, @@ -1201,6 +1195,10 @@ fn test_skip_certificate_reached_event_emitted_in_cpp_mode() { let ev = skip_cert_events[0]; assert_eq!(ev.slot, slot, "event slot must match"); assert_eq!(ev.certificate.vote.slot, slot, "certificate vote slot must match"); + assert!( + ev.should_broadcast, + "SkipCertificateReached should_broadcast must be true for vote-based emission" + ); assert_eq!( ev.certificate.signatures.len(), 3, @@ -1321,13 +1319,12 @@ fn test_slot_skipped_not_emitted_twice() { let skip_count = events.iter().filter(|e| matches!(e, SimplexEvent::SlotSkipped(_))).count(); assert_eq!(skip_count, 1, "Should emit exactly one SlotSkipped at skip certificate"); - // 5th vote: C++ parity -- first_non_finalized_slot does NOT advance on skip, - // so the slot is still "open" for vote reception (additional votes are accepted - // but won't re-trigger SlotSkipped since the cert is already formed). + // 5th vote: slot is now past first_non_finalized_slot, so it's rejected + // as SlotAlreadyFinalized (the slot was settled by the skip certificate). let result = state.on_vote_test(&desc, ValidatorIndex::new(4), vote, Vec::new()); assert!( - matches!(result, VoteResult::Applied), - "Vote for skipped slot should still be accepted (first_non_finalized_slot unchanged), got: {:?}", + matches!(result, VoteResult::SlotAlreadyFinalized), + "Vote for settled (skipped) slot should be rejected, got: {:?}", result ); @@ -1410,78 +1407,28 @@ fn test_ignore_finalized_slot_vote() { } #[test] -fn test_candidate_without_parent_accepted() { - // C++ consensus.cpp:173 โ€” C++ never rejects a candidate for missing parent. - // It only validates parent_slot < candidate_slot when parent exists. +fn test_reject_non_first_slot_without_parent() { let desc = create_test_desc(4, 2); let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); + // Try to send candidate for slot 1 (non-first in window) without parent + // Note: We construct directly here because create_test_candidate enforces parent for block.id + // but we want to test the FSM's validation let candidate = Candidate::new( crate::block::CandidateId { - slot: SlotIndex::new(1), // Non-first slot + slot: SlotIndex::new(1), // Second slot in window hash: UInt256::default(), block: BlockIdExt::default(), }, - None, // No parent โ€” valid per C++ - ValidatorIndex::new(0), - Some(create_stub_block(BlockIdExt::default())), - vec![], - ); - - let result = state.on_candidate(&desc, candidate); - assert!(result.is_ok(), "Candidate without parent must be accepted (C++ parity)"); -} - -#[test] -fn test_candidate_with_parent_slot_ge_rejected() { - // C++ consensus.cpp:173: parent_slot >= candidate_slot โ†’ misbehavior - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - - let candidate = Candidate::new( - crate::block::CandidateId { - slot: SlotIndex::new(1), - hash: UInt256::from([0xAA; 32]), - block: BlockIdExt::default(), - }, - Some(crate::block::CandidateId { - slot: SlotIndex::new(1), // parent_slot == candidate_slot - hash: UInt256::from([0xBB; 32]), - block: BlockIdExt::default(), - }), - ValidatorIndex::new(0), - Some(create_stub_block(BlockIdExt::default())), - vec![], - ); - - let result = state.on_candidate(&desc, candidate); - assert!(result.is_err(), "Candidate with parent_slot >= candidate_slot must be rejected"); -} - -#[test] -fn test_candidate_with_valid_parent_accepted() { - // C++ consensus.cpp:173: parent_slot < candidate_slot โ†’ accepted - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - - let candidate = Candidate::new( - crate::block::CandidateId { - slot: SlotIndex::new(1), - hash: UInt256::from([0xAA; 32]), - block: BlockIdExt::default(), - }, - Some(crate::block::CandidateId { - slot: SlotIndex::new(0), // parent_slot < candidate_slot - hash: UInt256::from([0xBB; 32]), - block: BlockIdExt::default(), - }), + None, // No parent! ValidatorIndex::new(0), Some(create_stub_block(BlockIdExt::default())), vec![], ); + // Should return error let result = state.on_candidate(&desc, candidate); - assert!(result.is_ok(), "Candidate with parent_slot < candidate_slot must be accepted"); + assert!(result.is_err(), "Non-first slot without parent should be rejected"); } #[test] @@ -1640,133 +1587,11 @@ fn test_timeout_management() { let desc = create_test_desc(4, 2); let state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - // FSM is created with unarmed timeouts (skip_timestamp = None). - // SessionProcessor::start() is responsible for calling set_timeouts(). - assert!(state.get_next_timeout().is_none(), "FSM must start with unarmed timeouts"); + // Should have initial timeout + assert!(state.get_next_timeout().is_some()); assert_eq!(state.skip_slot, SlotIndex::new(0)); } -#[test] -fn test_unarmed_fsm_no_skip_cascade_after_delay() { - // Regression: the FSM must NOT fire skip votes when check_all() runs - // with unarmed timeouts, even after an arbitrary delay. - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - - // Simulate 60 s overlay warmup delay - let future = desc.get_time() + std::time::Duration::from_secs(60); - desc.set_time(future); - - state.check_all(&desc); - - let mut skip_count = 0; - while let Some(event) = state.pull_event() { - if matches!(event, SimplexEvent::BroadcastVote(Vote::Skip(_))) { - skip_count += 1; - } - } - assert_eq!(skip_count, 0, "unarmed FSM must produce NO skip votes regardless of clock delay"); -} - -#[test] -fn test_set_timeouts_enables_skip_after_expiry() { - // After set_timeouts() the skip timer fires once the deadline elapses. - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - - let t0 = desc.get_time(); - - // Arm at t0 - state.set_timeouts(&desc); - assert!(state.get_next_timeout().is_some(), "set_timeouts must set skip_timestamp"); - - // Immediately after arming, check_all should produce no skips - state.check_all(&desc); - let mut skip_count = 0; - while let Some(event) = state.pull_event() { - if matches!(event, SimplexEvent::BroadcastVote(Vote::Skip(_))) { - skip_count += 1; - } - } - assert_eq!(skip_count, 0, "no skip votes before timeout expires"); - - // Advance past first_block_timeout + target_rate (defaults: 3s + 1s = 4s) - desc.set_time(t0 + std::time::Duration::from_secs(5)); - state.check_all(&desc); - - let mut skip_count = 0; - while let Some(event) = state.pull_event() { - if matches!(event, SimplexEvent::BroadcastVote(Vote::Skip(_))) { - skip_count += 1; - } - } - assert!(skip_count > 0, "skip votes must fire after timeout expires"); -} - -#[test] -fn test_try_skip_window_preserves_pending_block_cpp_mode() { - // In C++ mode, alarm() only sets voted_skip=true and does NOT clear - // pending_block. The async try_notarize() coroutine can still complete - // after a skip vote, producing both Skip and Notar for the same slot. - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create SimplexState"); - - // Store a candidate as pending at slot 0 - let hash = UInt256::from([1u8; 32]); - let block_id = BlockIdExt::default(); - let candidate = create_test_candidate(0, hash.clone(), block_id, None, 0); - let _ = state.on_candidate(&desc, candidate); - // Drain events from on_candidate - while state.pull_event().is_some() {} - - // Verify pending_block is set - let pending_before = state - .get_window(WindowIndex::new(0)) - .and_then(|w| w.slots[0].pending_block.as_ref()) - .is_some(); - - // Fire skip for the entire window (simulates timeout -> try_skip_window) - state.try_skip_window_for_test(WindowIndex::new(0)); - - // voted_skip must be set - let voted_skip = - state.get_window(WindowIndex::new(0)).map(|w| w.slots[0].voted_skip).unwrap_or(false); - assert!(voted_skip, "slot 0 must have voted_skip after try_skip_window"); - - // In C++ mode, pending_block must be PRESERVED (not cleared) - let pending_after = state - .get_window(WindowIndex::new(0)) - .and_then(|w| w.slots[0].pending_block.as_ref()) - .is_some(); - assert_eq!( - pending_before, pending_after, - "C++ mode: pending_block must be preserved after skip (was={}, now={})", - pending_before, pending_after - ); -} - -#[test] -fn test_try_skip_window_clears_pending_block_alpenglow_mode() { - // Alpenglow mode: pendingBlocks[k] <- bottom after skip - let desc = create_test_desc(4, 2); - let mut state = - SimplexState::new(&desc, opts_alpenglow()).expect("Failed to create SimplexState"); - - let hash = UInt256::from([1u8; 32]); - let block_id = BlockIdExt::default(); - let candidate = create_test_candidate(0, hash.clone(), block_id, None, 0); - let _ = state.on_candidate(&desc, candidate); - while state.pull_event().is_some() {} - - state.try_skip_window_for_test(WindowIndex::new(0)); - - let pending_after = state - .get_window(WindowIndex::new(0)) - .and_then(|w| w.slots[0].pending_block.as_ref()) - .is_some(); - assert!(!pending_after, "Alpenglow mode: pending_block must be cleared after skip"); -} - /* ======================================================================== Available Parent Tests @@ -2299,6 +2124,10 @@ fn test_notarization_reached_event_emitted() { let event = notar_reached.unwrap(); assert_eq!(event.slot, SlotIndex::new(0)); assert_eq!(event.block_hash, block_hash); + assert!( + event.should_broadcast, + "NotarizationReached should_broadcast must be true for vote-based emission" + ); assert_eq!(event.certificate.signatures.len(), 3, "Certificate should have 3 signatures"); } @@ -2386,6 +2215,10 @@ fn test_finalization_reached_event_emitted() { let event = final_reached.unwrap(); assert_eq!(event.slot, SlotIndex::new(0)); assert_eq!(event.block_hash, block_hash); + assert!( + event.should_broadcast, + "FinalizationReached should_broadcast must be true for vote-based emission" + ); assert_eq!(event.certificate.signatures.len(), 3, "Certificate should have 3 signatures"); // Should also have BlockFinalized event (emitted after FinalizationReached) @@ -2751,12 +2584,11 @@ fn test_skip_certificate_created_at_threshold() { "SlotSkipped event should be emitted when skip threshold reached" ); - // C++ parity: skip does NOT advance first_non_finalized_slot (only finalization does). - // But first_non_progressed_slot (C++ `now_`) does advance on skip. + // Skip advances first_non_finalized_slot (a skipped slot will never be finalized). assert_eq!( state.first_non_finalized_slot, - SlotIndex::new(0), - "first_non_finalized_slot should NOT advance on skip (C++ parity)" + SlotIndex::new(1), + "first_non_finalized_slot should advance past skipped slot" ); assert_eq!( state.first_non_progressed_slot, @@ -2830,8 +2662,6 @@ fn test_set_notarize_certificate_idempotent() { .set_notarize_certificate(&desc, slot, &block_hash, cert.clone()) .expect("should not conflict"); let weight_after_first = state.get_notarize_weight(slot, &block_hash); - // Drain first-store events so we can assert duplicate store emits none. - while state.pull_event().is_some() {} let stored2 = state .set_notarize_certificate(&desc, slot, &block_hash, cert.clone()) @@ -2847,10 +2677,6 @@ fn test_set_notarize_certificate_idempotent() { "Weight should not change on second call (idempotent)" ); assert_eq!(weight_after_first, 3, "Weight should be 3"); - assert!( - !state.has_pending_events(), - "duplicate notar cert must not emit relay-triggering events" - ); } #[test] @@ -2973,6 +2799,10 @@ fn test_set_notarize_certificate_emits_notarization_reached_for_tracked_slot() { let ev = notar_reached.unwrap(); assert_eq!(ev.slot, slot); assert_eq!(ev.block_hash, block_hash); + assert!( + !ev.should_broadcast, + "External set_notarize_certificate must set should_broadcast=false" + ); assert!(Arc::ptr_eq(&ev.certificate, &cert), "Event should carry the stored cert"); } @@ -3287,12 +3117,11 @@ fn test_skip_events_emitted_when_threshold_reached() { "SlotSkipped(1) should be emitted immediately when threshold reached" ); - // C++ parity: first_non_finalized_slot does NOT advance on skip. - // It stays at 0 since nothing was finalized. + // first_non_finalized_slot should have advanced past slot 1 assert_eq!( state.first_non_finalized_slot, - SlotIndex::new(0), - "first_non_finalized_slot should NOT advance on skip (C++ parity)" + SlotIndex::new(2), + "first_non_finalized_slot should advance to 2" ); } @@ -4622,8 +4451,6 @@ fn test_set_finalize_certificate_deduplicates() { .set_finalize_certificate(&desc, slot, &block_hash, final_cert.clone()) .expect("should not conflict"); assert!(stored1, "first application should store"); - // Drain first-store events so we can assert duplicate store emits none. - while state.pull_event().is_some() {} // Apply second time let stored2 = state @@ -4633,35 +4460,6 @@ fn test_set_finalize_certificate_deduplicates() { // Weight should still be 3 assert_eq!(state.get_finalize_weight(slot, &block_hash), 3); - assert!( - !state.has_pending_events(), - "duplicate finalize cert must not emit relay-triggering events" - ); -} - -#[test] -fn test_set_skip_certificate_deduplicates_without_events() { - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); - - let slot = SlotIndex::new(2); - let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; - let skip_cert = create_test_skip_cert(&desc, slot, &signers); - - let stored1 = state - .set_skip_certificate(&desc, slot, skip_cert.clone()) - .expect("first set_skip_certificate should succeed"); - assert!(stored1, "first skip certificate application should store"); - while state.pull_event().is_some() {} - - let stored2 = state - .set_skip_certificate(&desc, slot, skip_cert) - .expect("second set_skip_certificate should succeed"); - assert!(!stored2, "second skip certificate application should be deduplicated"); - assert!( - !state.has_pending_events(), - "duplicate skip cert must not emit relay-triggering events" - ); } #[test] @@ -4772,33 +4570,6 @@ fn test_set_skip_certificate_emits_slot_skipped_event_for_tracked_slot() { ); } -/// C++ parity (pool.cpp handle_saved_certificate): set_skip_certificate must emit -/// SkipCertificateReached so SessionProcessor relays foreign skip certificates. -#[test] -fn test_set_skip_certificate_emits_skip_cert_reached() { - let desc = create_test_desc(4, 2); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); - - while state.pull_event().is_some() {} - - let slot = SlotIndex::new(1); - let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; - let skip_cert = create_test_skip_cert(&desc, slot, &signers); - - let stored = state.set_skip_certificate(&desc, slot, skip_cert).expect("should not error"); - assert!(stored, "skip certificate should be stored"); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - let skip_reached = events - .iter() - .find_map(|e| match e { - SimplexEvent::SkipCertificateReached(ev) if ev.slot == slot => Some(ev), - _ => None, - }) - .expect("Expected SkipCertificateReached event for foreign skip cert"); - assert_eq!(skip_reached.slot, slot); -} - #[test] fn test_set_skip_certificate_does_not_emit_slot_skipped_event_for_old_slot() { let desc = create_test_desc(4, 2); @@ -4824,11 +4595,6 @@ fn test_set_skip_certificate_does_not_emit_slot_skipped_event_for_old_slot() { "SlotSkipped must not be emitted for old slots, got {:?}", events ); - assert!( - !events.iter().any(|e| matches!(e, SimplexEvent::SkipCertificateReached(_))), - "SkipCertificateReached must not be emitted for old slots, got {:?}", - events - ); } #[test] @@ -4919,6 +4685,11 @@ fn test_set_finalize_certificate_emits_block_finalized_and_finalization_reached_ assert_eq!(final_reached.slot, slot); assert_eq!(final_reached.block_hash, block_hash); assert!(Arc::ptr_eq(&final_reached.certificate, &cert)); + assert!( + !final_reached.should_broadcast, + "External set_finalize_certificate must set should_broadcast=false \ + (only local creation broadcasts)" + ); } /* @@ -5291,659 +5062,3 @@ fn test_available_base_skip_propagates_max_merge() { "skip-propagation max-merge must upgrade to the higher-slot parent" ); } - -// ========================================================================== -// Stale window guard tests -// ========================================================================== - -#[test] -fn test_stale_window_guard_current_leader_window_idx_updated_before_collation_check() { - // Verifies the ordering guarantee for leader window state: - // `current_leader_window_idx` must be up-to-date after notarization advances - // the progress cursor across a window boundary, BEFORE any code can check - // leader status (the stale-window guard in SessionProcessor::check_collation - // compares slot_window vs current_leader_window_idx). - // - // Setup: 4 validators, 2 slots per window. - // Progress both slots in window 0 via notarization -> cursor crosses to window 1. - let desc = create_test_desc(4, 2); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(0)); - - // Notarize slot 0 (3 out of 4 validators -> quorum) - let h0 = UInt256::from([0xD0u8; 32]); - let vote0 = Vote::Notarize(NotarizeVote { slot: SlotIndex::new(0), block_hash: h0.clone() }); - state.on_vote_test(&desc, ValidatorIndex::new(0), vote0.clone(), vec![1]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), vote0.clone(), vec![2]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), vote0, vec![3]).unwrap(); - - // Slot 0 notarized -> cursor at slot 1, still in window 0 - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); - assert_eq!( - state.current_leader_window_idx, - WindowIndex::new(0), - "window must not advance until full window is progressed" - ); - - // Notarize slot 1 (3 out of 4) - let h1 = UInt256::from([0xD1u8; 32]); - let vote1 = Vote::Notarize(NotarizeVote { slot: SlotIndex::new(1), block_hash: h1.clone() }); - state.on_vote_test(&desc, ValidatorIndex::new(0), vote1.clone(), vec![4]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), vote1.clone(), vec![5]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), vote1, vec![6]).unwrap(); - - // Both slots in window 0 are notarized -> cursor crosses to slot 2 (window 1) - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); - assert_eq!( - state.current_leader_window_idx, - WindowIndex::new(1), - "current_leader_window_idx must advance when progress cursor crosses window boundary" - ); - - // The stale-window guard: slot 0 is in window 0, but current window is 1. - // SessionProcessor::check_collation would see slot_window(0) < current_window(1) -> skip. - let slot0_window = desc.get_window_idx(SlotIndex::new(0)); - assert!( - slot0_window < state.current_leader_window_idx, - "slot 0 (window {slot0_window}) must be stale relative to current window {}", - state.current_leader_window_idx - ); - - // Slot 2 is in the current window -> not stale - let slot2_window = desc.get_window_idx(SlotIndex::new(2)); - assert_eq!( - slot2_window, state.current_leader_window_idx, - "slot 2 must be in the current window" - ); -} - -#[test] -fn test_stale_window_guard_skip_also_advances_window() { - // Same as above but using skip votes instead of notarization. - // Window advancement via skips must also update current_leader_window_idx. - let desc = create_test_desc(4, 2); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - // Skip slot 0 (3 out of 4 validators) - let skip0 = Vote::Skip(SkipVote { slot: SlotIndex::new(0) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip0.clone(), vec![1]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip0.clone(), vec![2]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip0, vec![3]).unwrap(); - - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); - assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); - - // Skip slot 1 (3 out of 4) - let skip1 = Vote::Skip(SkipVote { slot: SlotIndex::new(1) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip1.clone(), vec![4]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip1.clone(), vec![5]).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip1, vec![6]).unwrap(); - - // Both slots in window 0 skipped -> cursor at slot 2, window must advance - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); - assert_eq!( - state.current_leader_window_idx, - WindowIndex::new(1), - "current_leader_window_idx must advance after full-window skip" - ); -} - -/* - ======================================================================== - C++ parity: candidate pending storage despite local skip vote - - Regression tests for the fix to on_candidate() where candidates were - permanently dropped after a local skip vote. C++ consensus.cpp only - gates on voted_notar โ€” a skip vote must NOT prevent storing a candidate - as pending_block for later retry via check_pending_blocks. - ======================================================================== -*/ - -#[test] -fn test_candidate_stored_as_pending_despite_skip_vote_cpp_mode() { - // A local skip vote must NOT prevent storing a candidate as pending_block - // when try_notar fails (base not propagated yet). - // Reference: C++ consensus.cpp CandidateReceived only checks voted_notar. - let desc = create_test_desc(4, 4); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - // Cast local skip for all of window 1 (slots 4-7). - state.try_skip_window(WindowIndex::new(1)); - drain_events(&mut state); - - let w1 = state.get_window(WindowIndex::new(1)).unwrap(); - assert!(w1.slots[0].voted_skip, "voted_skip must be set for slot 4"); - assert!(w1.slots[0].voted_notar.is_none(), "voted_notar must NOT be set"); - assert!( - w1.slots[0].available_base.is_none(), - "available_base for slot 4 must be None (not propagated)" - ); - - // Submit candidate for slot 4 with genesis parent - let hash4 = UInt256::from([0xAA; 32]); - let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate).unwrap(); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - !events.iter().any(|e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(_)))), - "must NOT broadcast NotarVote โ€” base not propagated yet, got: {:?}", - events - ); - - let w1 = state.get_window(WindowIndex::new(1)).unwrap(); - assert!( - w1.slots[0].pending_block.is_some(), - "candidate must be stored as pending_block despite local skip vote (C++ parity)" - ); -} - -#[test] -fn test_pending_block_notarized_after_base_propagates_via_skip_certs() { - // Full lifecycle: candidate stored as pending after skip vote, then notarized - // when skip certs propagate the genesis base through to the candidate's slot. - let desc = create_test_desc(4, 4); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - // Cast local skip for window 1 (slots 4-7) - state.try_skip_window(WindowIndex::new(1)); - drain_events(&mut state); - - // Store candidate at slot 4 (pending โ€” base not propagated) - let hash4 = UInt256::from([0xBB; 32]); - let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate).unwrap(); - drain_events(&mut state); - - assert!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), - "precondition: candidate stored as pending" - ); - - let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; - - // Issue skip certs for s0, s1, s2, s3 โ€” each propagates genesis base one hop forward - for s in 0..4u32 { - let skip_cert = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(s), skip_cert).unwrap(); - } - - // After all 4 skip certs, genesis base should have reached slot 4. - // check_pending_blocks (called by propagate_base_after_skip_cert) must - // retry the pending candidate โ†’ try_notar succeeds โ†’ NotarVote emitted. - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - events.iter().any( - |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) - ), - "must emit NotarVote for pending candidate at slot 4 after base propagates, got: {:?}", - events - ); - - // Pending block should be cleared after successful notarization - assert!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_none(), - "pending_block must be cleared after notarization" - ); -} - -#[test] -fn test_candidate_dropped_when_voted_notar_cpp_mode() { - // When voted_notar is already set for a slot, a second candidate with a different - // hash must be correctly dropped (not stored as pending). - let desc = create_test_desc(4, 1); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); - - // Slot 0 has genesis base โ†’ first candidate succeeds immediately - let h1 = UInt256::from([0x11; 32]); - let candidate1 = create_test_candidate(0, h1.clone(), BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate1).unwrap(); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - events.iter().any( - |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { block_hash, .. })) if *block_hash == h1) - ), - "first candidate must trigger NotarVote" - ); - - // Now send a second candidate with a different hash for the same slot - let h2 = UInt256::from([0x22; 32]); - let candidate2 = create_test_candidate(0, h2, BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate2).unwrap(); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - !events.iter().any(|e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(_)))), - "second candidate must NOT trigger NotarVote (voted_notar already set)" - ); - - // Candidate must NOT be stored as pending โ€” voted_notar gates it - let w0 = state.get_window(WindowIndex::new(0)).unwrap(); - assert!( - w0.slots[0].pending_block.is_none(), - "candidate must NOT be stored as pending when voted_notar is set" - ); -} - -#[test] -fn test_out_of_order_skip_certs_still_propagate_base_to_pending() { - // Out-of-order skip cert arrival: s3 arrives first but has no base, so - // nothing propagates. Later s0, s1, s2 arrive in order โ€” when s2 is - // processed, find_next_nonskipped_slot skips over s3 (already marked - // skipped) and propagates genesis base directly to s4. - let desc = create_test_desc(4, 4); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - // Cast local skip for window 1 (slots 4-7) - state.try_skip_window(WindowIndex::new(1)); - drain_events(&mut state); - - // Store candidate at slot 4 (pending โ€” no base) - let hash4 = UInt256::from([0xCC; 32]); - let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate).unwrap(); - drain_events(&mut state); - - assert!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), - "precondition: candidate stored as pending" - ); - - let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; - - // Issue skip cert for s3 FIRST (out of order) - let skip3 = create_test_skip_cert(&desc, SlotIndex::new(3), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(3), skip3).unwrap(); - - // s3 has no base โ†’ nothing propagates โ†’ no vote yet - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - !events.iter().any( - |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) - ), - "no NotarVote yet โ€” s3 had no base to propagate" - ); - - // Issue skip certs for s0, s1 - for s in 0..2u32 { - let skip = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(s), skip).unwrap(); - drain_events(&mut state); - } - - // Verify slot 4 still has no base (propagated to s2 only so far) - assert!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), - "candidate still pending after s0+s1 skip certs" - ); - - // Issue skip cert for s2 โ€” propagation chain: s2 skipped, find_next_nonskipped(s2) - // skips over s3 (already skipped) โ†’ lands on s4 โ†’ base arrives โ†’ pending block retried - let skip2 = create_test_skip_cert(&desc, SlotIndex::new(2), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(2), skip2).unwrap(); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - events.iter().any( - |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) - ), - "must emit NotarVote for slot 4 after out-of-order skip certs propagate base, got: {:?}", - events - ); - - assert!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_none(), - "pending_block must be cleared after successful notarization" - ); -} - -/* - ======================================================================== - Base propagation chaining through already-skipped slots - - When skip certs arrive out of order, `propagate_base_after_skip_cert` - must chain the base forward through all consecutive already-skipped - intermediate slots. Without this, the base jumps from the cert's slot - to the first non-skipped slot, leaving intermediate slots baseless - and pending blocks stuck forever (no backward-walk like C++ has). - ======================================================================== -*/ - -#[test] -fn test_base_chains_through_already_skipped_slots() { - // Scenario: skip certs for slots 1-6 arrive BEFORE slot 0's cert. - // When slot 0's cert is finally processed, the chaining loop must - // propagate the genesis base through slots 1โ†’2โ†’3โ†’4โ†’5โ†’6โ†’7. - let desc = create_test_desc(4, 8); // 4 validators, 8 slots/window - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; - - // Issue skip certs for slots 1-6 first (out of order โ€” slot 0 last) - for s in 1..=6u32 { - let cert = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(s), cert).unwrap(); - } - drain_events(&mut state); - - // Verify: slots 1-6 are skipped but have no available_base (no source yet) - for s in 1..=6u32 { - let base = state.get_slot_ref(&desc, SlotIndex::new(s)).unwrap().available_base.clone(); - assert!( - base.is_none(), - "slot {} should have no base before slot 0's cert chains through", - s - ); - } - - // Now issue skip cert for slot 0 โ€” triggers chaining through 1โ†’2โ†’3โ†’4โ†’5โ†’6โ†’7 - let cert0 = create_test_skip_cert(&desc, SlotIndex::new(0), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(0), cert0).unwrap(); - drain_events(&mut state); - - // Every intermediate skipped slot must now have the genesis base - for s in 1..=6u32 { - let base = state.get_slot_ref(&desc, SlotIndex::new(s)).unwrap().available_base.clone(); - assert_eq!( - base, - Some(None), // genesis - "slot {} must have genesis base after chaining from slot 0", - s - ); - } - - // Slot 7 (first non-skipped after the chain) must also have the base - let base7 = state.get_slot_ref(&desc, SlotIndex::new(7)).unwrap().available_base.clone(); - assert_eq!(base7, Some(None), "slot 7 (first non-skipped) must have genesis base"); -} - -#[test] -fn test_base_chaining_enables_pending_block_at_intermediate_skipped_slot() { - // Regression test for the real-network failure mode: - // A pending block sits at a slot whose skip cert arrived before the base - // propagated. The old code would never set `available_base` on that slot - // because `find_next_nonskipped_slot` jumped past it. The chaining fix - // ensures the base reaches it. - let desc = create_test_desc(4, 8); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - let signers = vec![ValidatorIndex::new(0), ValidatorIndex::new(1), ValidatorIndex::new(2)]; - - // Skip-vote slot 4 locally so candidate can be stored as pending - state.try_skip_window(WindowIndex::new(0)); - drain_events(&mut state); - - // Store a pending candidate at slot 4 (parent = genesis) - let hash4 = UInt256::from([0xDD; 32]); - let candidate = create_test_candidate(4, hash4, BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate).unwrap(); - drain_events(&mut state); - - assert!( - state.get_window(WindowIndex::new(0)).unwrap().slots[4].pending_block.is_some(), - "precondition: candidate stored as pending at slot 4" - ); - - // Skip certs for slots 1-3 arrive BEFORE slot 0. - // Slot 4 doesn't get a skip cert (it only has a local skip vote). - for s in 1..=3u32 { - let cert = create_test_skip_cert(&desc, SlotIndex::new(s), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(s), cert).unwrap(); - } - drain_events(&mut state); - - // Verify no notarize vote yet โ€” slot 4 still has no base - assert!( - state.get_window(WindowIndex::new(0)).unwrap().slots[4].pending_block.is_some(), - "candidate still pending โ€” base hasn't reached slot 4 yet" - ); - - // Now process slot 0's skip cert โ†’ chain: 0โ†’1โ†’2โ†’3โ†’4 (slot 4 not skipped-cert) - let cert0 = create_test_skip_cert(&desc, SlotIndex::new(0), &signers); - state.set_skip_certificate(&desc, SlotIndex::new(0), cert0).unwrap(); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - events.iter().any( - |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(4)) - ), - "must emit NotarVote for pending slot 4 after base chains through, got: {:?}", - events - ); - - assert!( - state.get_window(WindowIndex::new(0)).unwrap().slots[4].pending_block.is_none(), - "pending_block must be cleared after notarization" - ); -} - -#[test] -fn test_pending_block_not_overwritten_by_second_candidate_cpp_mode() { - // C++ parity: first pending candidate wins. A second candidate with a different - // hash for the same slot must be rejected (equivocation), keeping the original. - let desc = create_test_desc(4, 4); - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - // Cast local skip for window 1 (slots 4-7) so candidates go to pending - state.try_skip_window(WindowIndex::new(1)); - drain_events(&mut state); - - // Store candidate A at slot 4 as pending (no base โ†’ try_notar fails) - let hash_a = UInt256::from([0xAA; 32]); - let candidate_a = create_test_candidate(4, hash_a.clone(), BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate_a).unwrap(); - drain_events(&mut state); - - assert!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0].pending_block.is_some(), - "precondition: candidate A stored as pending" - ); - assert_eq!( - state.get_window(WindowIndex::new(1)).unwrap().slots[0] - .pending_block - .as_ref() - .unwrap() - .id - .hash, - hash_a, - "precondition: pending_block is candidate A" - ); - - let pending_count_before = state.pending_slots.len(); - - // Submit candidate B with a different hash for the same slot 4 - let hash_b = UInt256::from([0xBB; 32]); - let candidate_b = create_test_candidate(4, hash_b, BlockIdExt::default(), None, 1); - state.on_candidate(&desc, candidate_b).unwrap(); - drain_events(&mut state); - - // pending_block must still hold candidate A (not B) - let w1 = state.get_window(WindowIndex::new(1)).unwrap(); - assert_eq!( - w1.slots[0].pending_block.as_ref().unwrap().id.hash, - hash_a, - "pending_block must still be candidate A โ€” first candidate wins" - ); - - // No additional PendingSlot should have been pushed - assert_eq!( - state.pending_slots.len(), - pending_count_before, - "no additional PendingSlot should be pushed for duplicate/equivocating candidate" - ); -} - -#[test] -fn test_try_notar_not_blocked_by_its_over_after_finalize_restart_cpp_mode() { - // C++ parity: after restart with a persisted Finalize vote, its_over=true and - // voted_final=true are set, but voted_notar remains None. C++ try_notarize() - // does NOT check voted_final, so notarization must still proceed. - let desc = create_test_desc(4, 1); - let mut state = SimplexState::new(&desc, opts_cpp()).expect("Failed to create state"); - - // Simulate restart recovery: mark slot 0 as having a persisted Finalize vote - let finalize_vote = Vote::Finalize(FinalizeVote { - slot: SlotIndex::new(0), - block_hash: UInt256::from([0xFF; 32]), - }); - state.mark_slot_voted_on_restart(&desc, &finalize_vote); - - // Verify preconditions - let w0 = state.get_window(WindowIndex::new(0)).unwrap(); - assert!(w0.slots[0].its_over, "precondition: its_over must be true after Finalize restart"); - assert!( - w0.slots[0].voted_final, - "precondition: voted_final must be true after Finalize restart" - ); - assert!( - w0.slots[0].voted_notar.is_none(), - "precondition: voted_notar must be None (Finalize does not set it)" - ); - - // Submit candidate for slot 0 (has genesis base โ†’ should succeed) - let hash = UInt256::from([0xCC; 32]); - let candidate = create_test_candidate(0, hash, BlockIdExt::default(), None, 0); - state.on_candidate(&desc, candidate).unwrap(); - - let events: Vec<_> = from_fn(|| state.pull_event()).collect(); - assert!( - events.iter().any( - |e| matches!(e, SimplexEvent::BroadcastVote(Vote::Notarize(NotarizeVote { slot, .. })) if *slot == SlotIndex::new(0)) - ), - "must emit NotarVote for slot 0 โ€” its_over must NOT block try_notar in C++ mode, got: {:?}", - events - ); -} - -#[test] -fn test_notarized_parent_chain_genesis_base_propagates_across_skipped_windows() { - // Regression test for bootstrap deadlock: when use_notarized_parent_chain=true (default - // C++ compat mode), skipping an entire window must propagate the available base to the - // next window via advance_leader_window_on_progress_cursor(). - // - // Without the fix, advance_leader_window_on_progress_cursor() only advanced the window - // index and set timeouts but never populated the new window's available_bases, causing - // has_available_parent() to return false and blocking all collation permanently. - // - // Reference: C++ pool.cpp advance_present() reads slot_at(now_)->state->available_base - // and publishes it via LeaderWindowObserved(now_, base). - let desc = create_test_desc(4, 2); // 2 slots per window - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - // Window 0 starts with genesis base - assert!(state.has_available_parent(&desc, SlotIndex::new(0))); - assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); - - // Skip slot 0 (need 3 out of 4 for threshold_66) - let skip_vote_0 = Vote::Skip(SkipVote { slot: SlotIndex::new(0) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip_vote_0.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip_vote_0.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip_vote_0, Vec::new()).unwrap(); - while state.pull_event().is_some() {} - - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(1)); - - // Skip slot 1 (last slot in window 0) -> should trigger window advancement - let skip_vote_1 = Vote::Skip(SkipVote { slot: SlotIndex::new(1) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip_vote_1.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip_vote_1.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip_vote_1, Vec::new()).unwrap(); - while state.pull_event().is_some() {} - - // Progress cursor should be at slot 2 (start of window 1) - assert_eq!(state.first_non_progressed_slot, SlotIndex::new(2)); - - // Window must have advanced to window 1 - assert_eq!( - state.current_leader_window_idx, - WindowIndex::new(1), - "leader window must advance to window 1 after all window 0 slots skipped" - ); - - // Window 1's available_bases must contain the genesis base (None) - let w1 = state.get_window(WindowIndex::new(1)); - assert!(w1.is_some(), "window 1 must exist"); - assert!( - w1.unwrap().available_bases.contains(&None), - "window 1 must have genesis (None) base propagated from window 0 via \ - advance_leader_window_on_progress_cursor(). Got: {:?}", - w1.unwrap().available_bases - ); - - // Slot 2 (first slot of window 1) must have available_base set - let slot2_base = state.get_slot_available_base(&desc, SlotIndex::new(2)); - assert_eq!(slot2_base, Some(None), "slot 2 available_base must be genesis (Some(None))"); - - // has_available_parent must return true for collation to proceed - assert!( - state.has_available_parent(&desc, SlotIndex::new(2)), - "has_available_parent must be true for slot 2 after genesis base propagated" - ); - - // get_available_parent must return None (genesis = no parent info) - let parent = state.get_available_parent(&desc, SlotIndex::new(2)); - assert_eq!(parent, None, "genesis parent should return None (no parent id)"); -} - -#[test] -fn test_notarized_parent_chain_base_propagates_across_multiple_skipped_windows() { - // Verify that base propagation works across multiple consecutive skipped windows. - // This is the sustained stall scenario: window 0 -> 1 -> 2 all skip without finalization. - let desc = create_test_desc(4, 1); // 1 slot per window for simplicity - let mut state = - SimplexState::new(&desc, opts_notarized_parent_chain()).expect("Failed to create state"); - - assert!(state.has_available_parent(&desc, SlotIndex::new(0))); - assert_eq!(state.current_leader_window_idx, WindowIndex::new(0)); - - // Skip window 0 (slot 0) - let skip = Vote::Skip(SkipVote { slot: SlotIndex::new(0) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip, Vec::new()).unwrap(); - while state.pull_event().is_some() {} - - assert_eq!(state.current_leader_window_idx, WindowIndex::new(1)); - assert!( - state.has_available_parent(&desc, SlotIndex::new(1)), - "window 1 must have available parent after window 0 skipped" - ); - - // Skip window 1 (slot 1) - let skip = Vote::Skip(SkipVote { slot: SlotIndex::new(1) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip, Vec::new()).unwrap(); - while state.pull_event().is_some() {} - - assert_eq!(state.current_leader_window_idx, WindowIndex::new(2)); - assert!( - state.has_available_parent(&desc, SlotIndex::new(2)), - "window 2 must have available parent after windows 0+1 skipped" - ); - - // Skip window 2 (slot 2) - let skip = Vote::Skip(SkipVote { slot: SlotIndex::new(2) }); - state.on_vote_test(&desc, ValidatorIndex::new(0), skip.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(1), skip.clone(), Vec::new()).unwrap(); - state.on_vote_test(&desc, ValidatorIndex::new(2), skip, Vec::new()).unwrap(); - while state.pull_event().is_some() {} - - assert_eq!(state.current_leader_window_idx, WindowIndex::new(3)); - assert!( - state.has_available_parent(&desc, SlotIndex::new(3)), - "window 3 must have available parent after windows 0+1+2 all skipped" - ); -} diff --git a/src/node/simplex/src/utils.rs b/src/node/simplex/src/utils.rs index 1e0eee8..28e793b 100644 --- a/src/node/simplex/src/utils.rs +++ b/src/node/simplex/src/utils.rs @@ -37,7 +37,7 @@ //! use crate::{PrivateKey, PublicKey, SessionId, ValidatorWeight}; -use std::{any::Any, backtrace::Backtrace, cmp::max, panic, sync::Once, thread, time::Duration}; +use std::{any::Any, backtrace::Backtrace, panic, sync::Once, thread, time::Duration}; use ton_api::{ ton::{ consensus::{ @@ -476,7 +476,6 @@ pub fn extract_block_info_from_candidate( candidate_bytes: &[u8], shard: &ShardIdent, max_size: usize, - proto_version: u32, ) -> Result> { // Empty candidate means empty block if candidate_bytes.is_empty() { @@ -510,17 +509,13 @@ pub fn extract_block_info_from_candidate( ) } - // C++ simplex always uses mode 2 (CRC32 only) for collated data - // re-serialization, regardless of proto_version. The proto_version >= 5 - // gate in decompress_candidate_data selects mode 2; lower versions select - // mode 31. - let effective_proto = max(proto_version, 5); + // Decompress using validator-session's decompression utility let (block_data, collated_data) = consensus_common::compression::decompress_candidate_data( &c.data, false, c.decompressed_size as usize, - effective_proto, + 0, )?; (c.round, c.root_hash.clone(), block_data, collated_data) @@ -579,10 +574,8 @@ pub fn compute_candidate_id_hash_from_bytes( parent: Option<(SlotIndex, &UInt256)>, shard: &ShardIdent, max_size: usize, - proto_version: u32, ) -> Result { - let block_info = - extract_block_info_from_candidate(candidate_bytes, shard, max_size, proto_version)?; + let block_info = extract_block_info_from_candidate(candidate_bytes, shard, max_size)?; let hash = match block_info { Some(info) => compute_candidate_id_hash( @@ -884,7 +877,7 @@ pub fn get_vote_slot(vote: &tl_simplex::UnsignedVote) -> i32 { } /* - Block Info Extraction (before_split support) + Block Info Extraction (SPLIT-1 Support) */ /// Extract before_split flag from block payload diff --git a/src/node/simplex/tests/test_collation.rs b/src/node/simplex/tests/test_collation.rs index 0be6bdd..222e873 100644 --- a/src/node/simplex/tests/test_collation.rs +++ b/src/node/simplex/tests/test_collation.rs @@ -26,8 +26,7 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocWriter, BuilderData, - Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, }; include!("../../../common/src/info.rs"); @@ -116,16 +115,8 @@ impl SessionListener for CollationTestListener { // Generate dummy candidate with proper hashes // The collator must provide file_hash = sha256(data) and collated_file_hash = sha256(collated_data) - // to match what the receiver will compute from the data. - // Block data MUST be valid BOC โ€” compress_candidate_data requires it. - let block_data = { - let mut b = BuilderData::new(); - b.append_raw(&[1u8, 2, 3, 4], 32).unwrap(); - let cell = b.into_cell().unwrap(); - let mut buf = Vec::new(); - BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); - buf - }; + // to match what the receiver will compute from the data + let block_data = vec![1u8, 2, 3, 4]; let collated_data_bytes: Vec = vec![]; // Compute hashes that match what receiver will compute @@ -350,6 +341,7 @@ fn run_collation_test() { &session_opts, &session_id, &shard, + initial_block_seqno, nodes, &private_key, db_path, @@ -357,7 +349,6 @@ fn run_collation_test() { Arc::downgrade(&session_listener), ) .expect("Failed to create session"); - session.start(initial_block_seqno); log::info!("Session created, waiting for collation callback..."); diff --git a/src/node/simplex/tests/test_consensus.rs b/src/node/simplex/tests/test_consensus.rs index aff2eb5..1922b1e 100644 --- a/src/node/simplex/tests/test_consensus.rs +++ b/src/node/simplex/tests/test_consensus.rs @@ -23,7 +23,7 @@ use spin::mutex::SpinMutex; use std::{ collections::HashMap, fs::{self, File}, - io::{self, Cursor, LineWriter, Write}, + io::{self, LineWriter, Write}, path::Path, sync::{ atomic::{AtomicBool, AtomicU32, Ordering}, @@ -37,8 +37,7 @@ use ton_api::{ IntoBoxed, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocReader, BocWriter, - BuilderData, Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, }; /* @@ -94,22 +93,11 @@ impl DummyCollatedData { } fn to_bytes(&self) -> Vec { - // Wrap in single-cell BOC โ€” compress_candidate_data requires valid BOC input - let raw = bincode::serialize(self).unwrap(); - let mut b = BuilderData::new(); - b.append_raw(&raw, raw.len() * 8).unwrap(); - let cell = b.into_cell().unwrap(); - let mut buf = Vec::new(); - BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); - buf + bincode::serialize(self).unwrap() } fn from_bytes(bytes: &[u8]) -> Self { - // Extract from BOC wrapper - let boc = BocReader::new().read(&mut Cursor::new(bytes)).unwrap(); - let cell = &boc.roots[0]; - let raw = cell.data(); - bincode::deserialize(raw).unwrap() + bincode::deserialize(bytes).unwrap() } } @@ -176,10 +164,6 @@ struct TestConfig { /// Restart tests benefit from a shorter interval so recovered nodes /// receive cached certificates faster than the skip timeout. standstill_timeout: Option, - /// Override slots_per_leader_window (default: 1). - /// Set > 1 to test candidate chaining within a leader window. - /// Precollation depth is derived automatically as (window - 1). - slots_per_leader_window: Option, } /// Network gremlin configuration (net-gremlin). @@ -224,10 +208,10 @@ impl Default for TestConfig { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 200, - target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(1000), + target_rate: Duration::from_millis(100), + first_block_timeout: Duration::from_millis(500), test_name: "simplex_consensus".to_string(), - test_timeout: Duration::from_secs(120), + test_timeout: Duration::from_secs(60), expect_timeout: false, shard: ShardIdent::masterchain(), mc_notification_interval: None, // No MC notifications for masterchain @@ -237,7 +221,6 @@ impl Default for TestConfig { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, - slots_per_leader_window: None, } } } @@ -1237,12 +1220,11 @@ where let session_id: UInt256 = UInt256::from(rng.gen::<[u8; 32]>()); // Session options - let slots_per_window = config.slots_per_leader_window.unwrap_or(1); let mut session_opts = SessionOptions { proto_version: 0, target_rate: config.target_rate, first_block_timeout: config.first_block_timeout, - slots_per_leader_window: slots_per_window, + slots_per_leader_window: 1, empty_block_mc_lag_threshold: if config.shard.is_masterchain() { None // MC uses internal finalization tracking } else { @@ -1298,6 +1280,7 @@ where &session_opts, &session_id, &shard, + initial_block_seqno, nodes.clone(), &local_key, db_path, @@ -1305,7 +1288,6 @@ where Arc::downgrade(&session_listener), ) .unwrap(); - session.start(initial_block_seqno); let session_instance = Arc::new(SpinMutex::new(SessionInstance { public_key: nodes[i].public_key.clone(), @@ -1631,10 +1613,12 @@ where let session_listener: Arc = new_listener.clone(); + // Recreate the session with the same DB path (recovery from persistent storage). let new_session = SessionFactory::create_session( &ctx.session_opts, &ctx.session_id, &ctx.shard, + ctx.initial_block_seqno, ctx.nodes.as_ref().clone(), &ctx.local_key, ctx.db_path.clone(), @@ -1644,7 +1628,6 @@ where match new_session { Ok(session) => { - session.start(ctx.initial_block_seqno); // Create a completely new SessionInstance with fresh state. // The seqno trackers are shared with the listener - they were already // updated by on_block_committed during recovery (before this point). @@ -1986,8 +1969,8 @@ fn test_simplex_consensus_basic() { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 10000, - target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(1000), + target_rate: Duration::from_millis(50), + first_block_timeout: Duration::from_millis(300), test_name: "simplex_basic".to_string(), test_timeout: Duration::from_secs(180), expect_timeout: false, // Expect completion, not timeout @@ -1999,7 +1982,6 @@ fn test_simplex_consensus_basic() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, - slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2034,14 +2016,14 @@ fn test_simplex_consensus_basic() { fn test_simplex_consensus_with_failures() { run_simplex_consensus_test( TestConfig { - total_rounds: 30, + total_rounds: 50, min_commit_percent: 0.3, // Lower threshold due to failures node_count: 11, generation_failure_probability: 0.1, candidate_rejection_probability: 0.1, max_collations: 150, - target_rate: Duration::from_millis(300), - first_block_timeout: Duration::from_millis(2000), + target_rate: Duration::from_millis(100), + first_block_timeout: Duration::from_millis(500), test_name: "simplex_with_failures".to_string(), // This scenario includes randomized generation/rejection failures and can // occasionally complete just above 120s on loaded CI/containers. @@ -2055,7 +2037,6 @@ fn test_simplex_consensus_with_failures() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, - slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2107,10 +2088,10 @@ fn test_simplex_consensus_finalcert_recovery() { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 200, - target_rate: Duration::from_millis(300), - first_block_timeout: Duration::from_millis(2000), + target_rate: Duration::from_millis(100), + first_block_timeout: Duration::from_millis(500), test_name: "simplex_finalcert_recovery".to_string(), - test_timeout: Duration::from_secs(240), + test_timeout: Duration::from_secs(180), expect_timeout: false, shard: ShardIdent::masterchain(), mc_notification_interval: None, @@ -2125,7 +2106,6 @@ fn test_simplex_consensus_finalcert_recovery() { }), lossy_overlay_node_indices: Some(vec![0]), standstill_timeout: None, - slots_per_leader_window: None, }, |instances| { let config = &instances[0].lock().config.clone(); @@ -2184,10 +2164,10 @@ fn test_simplex_consensus_shard_with_mc_notifications() { generation_failure_probability: 0.0, candidate_rejection_probability: 0.0, max_collations: 500, - target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(1000), + target_rate: Duration::from_millis(100), + first_block_timeout: Duration::from_millis(500), test_name: "simplex_shard_mc".to_string(), - test_timeout: Duration::from_secs(180), + test_timeout: Duration::from_secs(120), expect_timeout: false, // Use a shard chain (workchain 0, full shard) shard: ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(), @@ -2199,7 +2179,6 @@ fn test_simplex_consensus_shard_with_mc_notifications() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, - slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2256,7 +2235,6 @@ fn test_simplex_consensus_adnl_overlay() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, - slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement @@ -2317,7 +2295,6 @@ fn test_simplex_consensus_adnl_net_gremlin() { lossy_overlay: None, lossy_overlay_node_indices: None, standstill_timeout: None, - slots_per_leader_window: None, }, |instances| { // Verify commit rate meets minimum requirement (best-effort under partitions). @@ -2363,7 +2340,7 @@ fn test_simplex_consensus_restart_gremlin() { candidate_rejection_probability: 0.0, max_collations: 2000, target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(1200), + first_block_timeout: Duration::from_millis(500), test_name: "simplex_restart_gremlin".to_string(), test_timeout: Duration::from_secs(180), // Longer timeout for restart cycles expect_timeout: false, @@ -2382,7 +2359,6 @@ fn test_simplex_consensus_restart_gremlin() { // 1s rebroadcast cadence can flood restart-gremlin runs (large [begin,end) ranges), // causing receiver queues to explode and the test to stall intermittently. standstill_timeout: Some(Duration::from_secs(5)), - slots_per_leader_window: None, }, |instances| { let config = &instances[0].lock().config.clone(); @@ -2487,257 +2463,6 @@ fn test_collated_file_hash_consistency() { ); } -/// Verify the start gate: sessions create the overlay immediately but do NOT -/// begin FSM processing until `start(seqno)` is called. -/// -/// This tests the overlay-warmup fix for the mixed C++/Rust timing gap where -/// the FSM's `first_block_timeout` would fire before the overlay had established -/// peer connections, permanently stalling finalization. -#[test] -fn test_simplex_start_gate() { - let _test_lock = SIMPLEX_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); - - const DB_PATH: &str = "../../target/test"; - - if !is_test_logging_enabled() { - return; - } - - let _ = env_logger::builder().is_test(true).try_init(); - - let node_count = 7usize; - let shard = ShardIdent::masterchain(); - let initial_block_seqno = 1u32; - - let mut nodes = Vec::with_capacity(node_count); - for _ in 0..node_count { - let key = Ed25519KeyOption::generate().unwrap(); - let adnl_id = key.id().clone(); - nodes.push(SessionNode { public_key: key, adnl_id, weight: 1 }); - } - - let overlay_manager = SessionFactory::create_in_process_overlay_manager(node_count); - - let rand_name: String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(7) - .map(char::from) - .collect(); - let db_path_base = format!("{}/simplex_start_gate_{}", DB_PATH, rand_name); - let mut rng = rand::thread_rng(); - let session_id: UInt256 = UInt256::from(rng.gen::<[u8; 32]>()); - - let session_opts = SessionOptions { - proto_version: 0, - target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(1000), - slots_per_leader_window: 1, - wait_for_db_init: true, - ..Default::default() - }; - - let committed_blocks: CommittedBlocksMap = Arc::new(Mutex::new(HashMap::new())); - let commit_counters: Vec> = - (0..node_count).map(|_| Arc::new(AtomicU32::new(0))).collect(); - let mut sessions: Vec = Vec::new(); - // Keep instances alive so the listener Weak pointers remain valid. - let mut instances: Vec>> = Vec::new(); - - let config = TestConfig { - total_rounds: 10, - min_commit_percent: 0.5, - node_count, - generation_failure_probability: 0.0, - candidate_rejection_probability: 0.0, - max_collations: 10000, - target_rate: Duration::from_millis(200), - first_block_timeout: Duration::from_millis(1000), - test_name: "simplex_start_gate".to_string(), - test_timeout: Duration::from_secs(60), - expect_timeout: false, - shard: shard.clone(), - mc_notification_interval: None, - overlay_type: OverlayType::InProcess, - net_gremlin: None, - restart_gremlin: None, - lossy_overlay: None, - lossy_overlay_node_indices: None, - standstill_timeout: None, - slots_per_leader_window: None, - }; - - for i in 0..node_count { - let local_key = nodes[i].public_key.clone(); - let db_path = format!("{}_node{}", db_path_base, i); - let approved_candidates: Arc< - Mutex>>, - > = Arc::new(Mutex::new(HashMap::new())); - let next_expected_commit_seqno = Arc::new(AtomicU32::new(initial_block_seqno)); - - let listener = Arc::new(SessionInstanceListener { - instance: SpinMutex::new(Weak::new()), - approved_candidates: approved_candidates.clone(), - next_expected_commit_seqno: next_expected_commit_seqno.clone(), - committed_blocks: committed_blocks.clone(), - }); - let session_listener: Arc = listener.clone(); - - let session = SessionFactory::create_session( - &session_opts, - &session_id, - &shard, - nodes.clone(), - &local_key, - db_path, - overlay_manager.clone(), - Arc::downgrade(&session_listener), - ) - .expect("Failed to create session"); - - let session_instance = Arc::new(SpinMutex::new(SessionInstance { - source_index: i as u32, - public_key: nodes[i].public_key.clone(), - batch_processed: Arc::new(AtomicBool::new(false)), - collation_requested: Arc::new(AtomicBool::new(false)), - collation_count: Arc::new(AtomicU32::new(0)), - on_candidate_count: Arc::new(AtomicU32::new(0)), - on_block_committed_count: commit_counters[i].clone(), - is_collator: Arc::new(AtomicBool::new(false)), - config: config.clone(), - current_round: Arc::new(AtomicU32::new(0)), - commit_latencies: Arc::new(Mutex::new(Vec::new())), - next_expected_commit_seqno, - session_errors_count: Arc::new(AtomicU32::new(0)), - approved_candidates, - committed_blocks: committed_blocks.clone(), - _session: session.clone(), - _listener: listener.clone(), - })); - - *listener.instance.lock() = Arc::downgrade(&session_instance); - - sessions.push(session); - instances.push(session_instance); - } - - // Phase 1: verify no commits while sessions are gated (overlay is warming up) - log::info!("[start_gate] Phase 1: verifying no commits for 2s without start()"); - thread::sleep(Duration::from_secs(2)); - for (i, counter) in commit_counters.iter().enumerate() { - let commits = counter.load(Ordering::Relaxed); - assert_eq!( - commits, 0, - "Node {} committed {} blocks before start() was called โ€” start gate failed", - i, commits - ); - } - log::info!("[start_gate] Phase 1 passed: zero commits before start()"); - - // Phase 2: call start(seqno) on all sessions, then wait for commits - log::info!( - "[start_gate] Phase 2: calling start(seqno={}) on all sessions", - initial_block_seqno - ); - for session in &sessions { - session.start(initial_block_seqno); - } - - let deadline = Instant::now() + Duration::from_secs(30); - let min_commits = 3u32; - loop { - thread::sleep(Duration::from_millis(200)); - let all_committed = - commit_counters.iter().all(|c| c.load(Ordering::Relaxed) >= min_commits); - if all_committed { - break; - } - if Instant::now() > deadline { - for (i, counter) in commit_counters.iter().enumerate() { - log::error!("[start_gate] Node {} commits: {}", i, counter.load(Ordering::Relaxed)); - } - panic!( - "Timed out waiting for {} commits after start() โ€” \ - sessions did not begin processing after start gate was released", - min_commits - ); - } - } - - log::info!( - "[start_gate] Phase 2 passed: all nodes committed >= {} blocks after start()", - min_commits - ); - - for session in &sessions { - session.stop(); - } - drop(instances); - log::info!("[start_gate] Test passed"); -} - -/// Test candidate chaining within a multi-slot leader window (C++ parity). -/// -/// Configures `slots_per_leader_window = 4`. Precollation depth is derived -/// automatically from `slots_per_leader_window`, so the leader can chain -/// candidates across slots within a single window. With -/// chaining, the leader generates slot N+1 with slot N's candidate as parent, -/// which causes seqnos to increment (1, 2, 3, 4) instead of repeating seqno=1. -/// -/// Acceptance: the test reaches the commit threshold (at least 30% of rounds -/// committed), proving that chained candidates are notarized and finalized. -#[test] -fn test_simplex_consensus_candidate_chaining() { - run_simplex_consensus_test( - TestConfig { - total_rounds: 20, - min_commit_percent: 0.3, - node_count: 4, - generation_failure_probability: 0.0, - candidate_rejection_probability: 0.0, - max_collations: 2000, - target_rate: Duration::from_millis(300), - first_block_timeout: Duration::from_millis(3000), - test_name: "simplex_candidate_chaining".to_string(), - test_timeout: Duration::from_secs(120), - expect_timeout: false, - shard: ShardIdent::masterchain(), - mc_notification_interval: None, - overlay_type: OverlayType::InProcess, - net_gremlin: None, - restart_gremlin: None, - lossy_overlay: None, - lossy_overlay_node_indices: None, - standstill_timeout: None, - slots_per_leader_window: Some(4), - }, - |instances| { - let config = &instances[0].lock().config.clone(); - let min_commits = (config.total_rounds as f64 * config.min_commit_percent) as u32; - - for (idx, instance) in instances.iter().enumerate() { - let commits = instance.lock().on_block_committed_count(); - log::info!( - "[chaining] Instance {}: {} commits out of {} total_rounds (min required: {})", - idx, - commits, - config.total_rounds, - min_commits - ); - assert!( - commits >= min_commits, - "Instance {} has {} commits but requires at least {} ({}% of {} rounds). \ - Candidate chaining may not be working correctly.", - idx, - commits, - min_commits, - config.min_commit_percent * 100.0, - config.total_rounds - ); - } - }, - ); -} - /// Test that empty collated_data produces a valid (non-default) hash #[test] fn test_empty_collated_data_hash() { diff --git a/src/node/simplex/tests/test_restart.rs b/src/node/simplex/tests/test_restart.rs index 1db5cc0..acd4169 100644 --- a/src/node/simplex/tests/test_restart.rs +++ b/src/node/simplex/tests/test_restart.rs @@ -35,8 +35,7 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocWriter, BuilderData, - Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, }; include!("../../../common/src/info.rs"); @@ -218,16 +217,7 @@ impl SessionListener for RestartSingleSessionListener { ); // Block + collated data (keep small; hashes must match) - // Block data must be valid BOC (compress_candidate_data deserializes it) - let block_data = { - let raw = [1u8, 2, 3, 4, (seqno % 255) as u8]; - let mut b = BuilderData::new(); - b.append_raw(&raw, raw.len() * 8).unwrap(); - let cell = b.into_cell().unwrap(); - let mut buf = Vec::new(); - BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); - buf - }; + let block_data = vec![1u8, 2, 3, 4, (seqno % 255) as u8]; let collated_data: Vec = vec![]; let file_hash = UInt256::from_slice(&sha256_digest(&block_data)); @@ -490,6 +480,7 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate &session_opts, &session_id, &shard, + initial_block_seqno, nodes.clone(), &private_key, db_path.clone(), @@ -497,7 +488,6 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate Arc::downgrade(&session_listener), ) .expect("Failed to create session (phase 1)"); - session_1.start(initial_block_seqno); let rounds_before_restart: u32 = 5; let start = Instant::now(); @@ -530,6 +520,7 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate let last_committed_slot_before = listener.last_committed_slot(); let collation_before = listener.collation_count(); + let approved_fetch_before = listener.approved_candidate_requests(); // Stop session 1 and give some time for DB handles to close session_1.stop(); @@ -550,6 +541,7 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate &session_opts, &session_id, &shard, + restart_initial_seqno, nodes, &private_key, db_path, @@ -557,7 +549,6 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate Arc::downgrade(&session_listener), ) .expect("Failed to create session (phase 2)"); - session_2.start(restart_initial_seqno); // Wait for first post-restart slot generation (proof that current slot was seeded) let start = Instant::now(); @@ -618,6 +609,15 @@ fn run_single_node_restart_test(test_name: &str, strategy: RestartRecommitStrate ); } + // Post-condition: restart recovery should have requested approved candidates + // (for candidate cache restoration). We allow >=1 because small tests may have few blocks. + assert!( + listener.approved_candidate_requests() > approved_fetch_before, + "expected get_approved_candidate to be used during restart recovery (before={}, after={})", + approved_fetch_before, + listener.approved_candidate_requests() + ); + // Post-condition: no session errors recorded assert_eq!( listener.max_errors_count(), diff --git a/src/node/simplex/tests/test_validation.rs b/src/node/simplex/tests/test_validation.rs index b5985cf..37934bd 100644 --- a/src/node/simplex/tests/test_validation.rs +++ b/src/node/simplex/tests/test_validation.rs @@ -26,8 +26,7 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use ton_block::{ - error, sha256_digest, BlockIdExt, BlockSignaturesVariant, BocFlags, BocWriter, BuilderData, - Ed25519KeyOption, ShardIdent, UInt256, + error, sha256_digest, BlockIdExt, BlockSignaturesVariant, Ed25519KeyOption, ShardIdent, UInt256, }; include!("../../../common/src/info.rs"); @@ -130,15 +129,7 @@ impl SessionListener for ValidationTestListener { // Generate dummy candidate with proper hashes // The collator must provide file_hash = sha256(data) and collated_file_hash = sha256(collated_data) // to match what the receiver will compute from the data - // Block data must be valid BOC (compress_candidate_data deserializes it) - let block_data = { - let mut b = BuilderData::new(); - b.append_raw(&[1u8, 2, 3, 4], 32).unwrap(); - let cell = b.into_cell().unwrap(); - let mut buf = Vec::new(); - BocWriter::with_flags([cell], BocFlags::all()).unwrap().write(&mut buf).unwrap(); - buf - }; + let block_data = vec![1u8, 2, 3, 4]; let collated_data_bytes: Vec = vec![]; // Compute hashes that match what receiver will compute @@ -402,6 +393,7 @@ fn run_validation_test() { &session_opts, &session_id, &shard, + initial_block_seqno, nodes.clone(), &private_key_0, db_path_0, @@ -409,12 +401,13 @@ fn run_validation_test() { Arc::downgrade(&session_listener_0), ) .expect("Failed to create session 0"); - session_0.start(initial_block_seqno); + // Create session for node 1 let session_1 = SessionFactory::create_session( &session_opts, &session_id, &shard, + initial_block_seqno, nodes.clone(), &private_key_1, db_path_1, @@ -422,7 +415,6 @@ fn run_validation_test() { Arc::downgrade(&session_listener_1), ) .expect("Failed to create session 1"); - session_1.start(initial_block_seqno); log::info!("Sessions created, waiting for validation callback on node 1..."); diff --git a/src/node/src/archive_import/ingester.rs b/src/node/src/archive_import/ingester.rs deleted file mode 100644 index 8eaccc2..0000000 --- a/src/node/src/archive_import/ingester.rs +++ /dev/null @@ -1,830 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use crate::{ - archive_import::{scanner::PackageGroup, validator::ValidatorState}, - block::BlockIdExtExtention, - block_proof::BlockProofStuff, - internal_db::{ - ARCHIVES_GC_BLOCK, LAST_APPLIED_MC_BLOCK, PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, - }, - shard_state::ShardHashesStuff, -}; -use futures::future::try_join_all; -use rayon::prelude::*; -use std::{ - collections::{HashMap, HashSet}, - path::Path, - sync::Arc, -}; -use storage::{ - archives::{ - archive_manager::{ArchiveManager, ImportBlockMeta, ImportEntry}, - package::read_package_from_file, - package_entry_id::PackageEntryId, - }, - block_handle_db::BlockHandleStorage, - block_info_db::BlockInfoDb, - traits::Serializable, - types::BlockMeta, -}; -use ton_block::{ - error, fail, Block, BlockIdExt, Cell, Deserializable, Result, ShardIdent, UInt256, -}; - -const TARGET: &str = "archive_import"; - -struct RawEntry { - block_data: Vec, - block_offset: u64, - proof_data: Vec, - proof_offset: u64, -} - -async fn read_raw_package(path: &Path) -> Result> { - let mut reader = read_package_from_file(path).await?; - let mut entries = HashMap::::new(); - let mut offset: u64 = 0; - while let Some(entry) = reader.next().await? { - let entry_size = entry.serialized_size(); - let entry_id = PackageEntryId::::from_filename(entry.filename())?; - let (block_id, is_proof) = match entry_id { - PackageEntryId::Block(id) => (id, false), - PackageEntryId::Proof(id) if id.is_masterchain() => (id, true), - PackageEntryId::ProofLink(id) if !id.is_masterchain() => (id, true), - entry_id => { - log::warn!("Unexpected entry type {} in {}", entry_id, path.display()); - offset += entry_size; - continue; - } - }; - let mut data = entry.take_data(); - entries - .entry(block_id) - .and_modify(|e| { - if is_proof { - e.proof_data = std::mem::take(&mut data); - e.proof_offset = offset; - } else { - e.block_data = std::mem::take(&mut data); - e.block_offset = offset; - } - }) - .or_insert_with(|| { - if is_proof { - RawEntry { - block_data: vec![], - block_offset: 0, - proof_data: data, - proof_offset: offset, - } - } else { - RawEntry { - block_data: data, - block_offset: offset, - proof_data: vec![], - proof_offset: 0, - } - } - }); - offset += entry_size; - } - Ok(entries) -} - -struct McEntry { - block_id: BlockIdExt, - prev_block_id: BlockIdExt, - proof: BlockProofStuff, - is_key: bool, - gen_utime: u32, - end_lt: u64, - shard_tops: Vec, - state_update_new: Cell, - proof_data: Vec, - proof_offset: u64, - block_data: Vec, - block_offset: u64, -} - -struct ProcessedEntry { - block_id: BlockIdExt, - gen_utime: u32, - end_lt: u64, - mc_ref_seq_no: u32, - is_key_block: bool, - proof_offset: u64, - block_offset: u64, - prevs: Vec, - state_update_new: Cell, -} - -impl ProcessedEntry { - fn to_import_entries(&self) -> [ImportEntry; 2] { - let proof_entry_id = if self.block_id.is_masterchain() { - PackageEntryId::Proof(self.block_id.clone()) - } else { - PackageEntryId::ProofLink(self.block_id.clone()) - }; - [ - ImportEntry { entry_id: proof_entry_id, offset: self.proof_offset, block_meta: None }, - ImportEntry { - entry_id: PackageEntryId::Block(self.block_id.clone()), - offset: self.block_offset, - block_meta: Some(ImportBlockMeta { - seq_no: self.block_id.seq_no(), - shard: self.block_id.shard_id.clone(), - gen_utime: self.gen_utime, - end_lt: self.end_lt, - mc_ref_seq_no: self.mc_ref_seq_no, - }), - }, - ] - } -} - -struct KeyBlockData { - block_id: BlockIdExt, - proof_data: Vec, - block_data: Vec, -} - -pub struct LastGroupState { - pub mc_block_id: BlockIdExt, - pub shard_tops: Vec, -} - -fn parse_and_verify_block(data: &[u8], declared_id: &BlockIdExt) -> Result { - let file_hash = UInt256::calc_file_hash(data); - let root_cell = ton_block::read_single_root_boc(data)?; - let root_hash = root_cell.repr_hash(); - let block = Block::construct_from_cell(root_cell)?; - let info = block.read_info()?; - let actual_id = - BlockIdExt::with_params(info.shard().clone(), info.seq_no(), root_hash, file_hash); - if actual_id != *declared_id { - return Err(error!("Block declared as {} but data contains {}", declared_id, actual_id)); - } - Ok(block) -} - -fn deserialize_mc_entry(block_id: BlockIdExt, raw: RawEntry) -> Result { - if raw.proof_data.is_empty() { - return Err(error!("MC block {} has no proof in the package", block_id)); - } - if raw.block_data.is_empty() { - return Err(error!("MC block {} has no block data in the package", block_id)); - } - - let proof = BlockProofStuff::deserialize(&block_id, raw.proof_data.clone(), false)?; - let (virt_block, _) = proof.virtualize_block()?; - let is_key = virt_block.read_info()?.key_block(); - - let block = parse_and_verify_block(&raw.block_data, &block_id)?; - let block_info = block.read_info()?; - let gen_utime = block_info.gen_utime(); - let end_lt = block_info.end_lt(); - let mut prev_ids = block_info.read_prev_ids()?; - if prev_ids.len() != 1 { - return Err(error!("MC block {} has {} prev refs, expected 1", block_id, prev_ids.len())); - } - let prev_block_id = prev_ids.pop().unwrap(); - let extra = block - .read_extra()? - .read_custom()? - .ok_or_else(|| error!("No McExtra in master block {}", block_id))?; - let shard_tops = ShardHashesStuff::from(extra.shards().clone()).top_blocks_all()?; - let state_update_new = block.read_state_update()?.new; - - Ok(McEntry { - block_id, - prev_block_id, - proof, - is_key, - gen_utime, - end_lt, - shard_tops, - state_update_new, - proof_data: raw.proof_data, - proof_offset: raw.proof_offset, - block_data: raw.block_data, - block_offset: raw.block_offset, - }) -} - -fn validate_mc_range( - entries: &[McEntry], - key_proof: &Option, - zerostate: &Arc, -) -> Result<()> { - entries.par_iter().try_for_each(|e| match key_proof { - None => e.proof.check_with_master_state(zerostate), - Some(kb) => e.proof.check_with_prev_key_block_proof(kb), - }) -} - -fn check_mc_chain(entries: &[McEntry], expected_first_prev: &BlockIdExt) -> Result<()> { - if let Some(first) = entries.first() { - if first.prev_block_id != *expected_first_prev { - fail!( - "MC chain gap between packages: block {} prev_ref = {} but expected {}", - first.block_id, - first.prev_block_id, - expected_first_prev, - ); - } - } - for w in entries.windows(2) { - if w[1].prev_block_id != w[0].block_id { - fail!( - "MC chain gap: block {} prev_ref = {} but expected {}", - w[1].block_id, - w[1].prev_block_id, - w[0].block_id, - ); - } - } - Ok(()) -} - -fn parse_mc_entries( - raw: HashMap, - validator: &mut ValidatorState, - skip: bool, - expected_first_prev: BlockIdExt, -) -> Result<(Vec, Option, Vec<(u32, Vec)>, LastGroupState)> -{ - let mut entries: Vec = - raw.into_par_iter().map(|(id, r)| deserialize_mc_entry(id, r)).collect::>()?; - entries.sort_by_key(|e| e.block_id.seq_no()); - check_mc_chain(&entries, &expected_first_prev)?; - - let rest_start = if entries.first().map(|e| e.is_key).unwrap_or(false) { - let block_id = &entries[0].block_id; - if !skip { - // Skip re-validation if this key block is already the current validation root (resume). - let already_done = validator - .current_key_block_proof() - .map(|kp| kp.id().seq_no() >= block_id.seq_no()) - .unwrap_or(false); - if !already_done { - let is_hardfork = validator.is_hardfork(block_id); - if is_hardfork { - log::info!( - target: TARGET, - "Hard fork block {} accepted as new validation root", - block_id, - ); - } else { - let key_proof = validator.current_key_block_proof().cloned(); - let zerostate = Arc::clone(validator.zerostate()); - validate_mc_range(&entries[..1], &key_proof, &zerostate)?; - } - } - } - validator.set_key_block_proof(entries[0].proof.clone()); - 1 - } else { - 0 - }; - - if !skip { - let key_proof = validator.current_key_block_proof().cloned(); - let zerostate = Arc::clone(validator.zerostate()); - validate_mc_range(&entries[rest_start..], &key_proof, &zerostate)?; - } - - let mut processed = Vec::with_capacity(entries.len()); - let mut key_block: Option = None; - let mut mc_shard_tops: Vec<(u32, Vec)> = Vec::new(); - - for entry in entries { - if entry.is_key { - if Some(&entry.block_id) != validator.current_key_block_proof().map(|kp| kp.id()) { - fail!("Second key block {} in package", entry.block_id); - } - key_block = Some(KeyBlockData { - block_id: entry.block_id.clone(), - proof_data: entry.proof_data, - block_data: entry.block_data.clone(), - }); - } - mc_shard_tops.push((entry.block_id.seq_no(), entry.shard_tops)); - processed.push(ProcessedEntry { - mc_ref_seq_no: entry.block_id.seq_no(), - block_id: entry.block_id, - gen_utime: entry.gen_utime, - end_lt: entry.end_lt, - is_key_block: entry.is_key, - proof_offset: entry.proof_offset, - block_offset: entry.block_offset, - prevs: vec![entry.prev_block_id], - state_update_new: entry.state_update_new, - }); - } - - let last_group_state = - processed.last().ok_or_else(|| error!("MC package is empty")).map(|e| LastGroupState { - mc_block_id: e.block_id.clone(), - shard_tops: mc_shard_tops.last().map(|(_, tops)| tops.clone()).unwrap_or_default(), - })?; - Ok((processed, key_block, mc_shard_tops, last_group_state)) -} - -fn deserialize_shard_entry( - block_id: BlockIdExt, - raw: RawEntry, - skip: bool, -) -> Result { - if raw.proof_data.is_empty() { - return Err(error!("Shard block {} has no proof link in the package", block_id)); - } - if raw.block_data.is_empty() { - return Err(error!("Shard block {} has no block data in the package", block_id)); - } - - if !skip { - let proof = BlockProofStuff::deserialize(&block_id, raw.proof_data.clone(), true)?; - proof.check_proof_link()?; - } - - let block = parse_and_verify_block(&raw.block_data, &block_id)?; - let info = block.read_info()?; - let prevs = info.read_prev_ids()?; - let state_update_new = block.read_state_update()?.new; - - Ok(ProcessedEntry { - gen_utime: info.gen_utime(), - end_lt: info.end_lt(), - mc_ref_seq_no: 0, - is_key_block: false, - proof_offset: raw.proof_offset, - block_offset: raw.block_offset, - block_id, - prevs, - state_update_new, - }) -} - -fn parse_shard_entries( - raw: HashMap, - archive_id: u32, - shard: ShardIdent, - mc_shard_tops: Vec<(u32, Vec)>, - prev_shard_tops: Vec, - skip: bool, -) -> Result> { - let now = std::time::Instant::now(); - let results: HashMap = raw - .into_par_iter() - .map(|(id, r)| deserialize_shard_entry(id.clone(), r, skip).map(|res| (id, res))) - .collect::>()?; - log::debug!(target: TARGET, "Deserialized shard entries after {:#?}", now.elapsed()); - - let entries = if !skip { - let prev_committed: HashSet = - prev_shard_tops.into_iter().filter(|id| id.shard_id.intersect_with(&shard)).collect(); - validate_shard_and_assign_mc_refs(&shard, mc_shard_tops, results, prev_committed)? - } else { - // mc_ref_seq_no must be >= archive_id for choose_package() to find the right file. - let mut entries: Vec = results - .into_iter() - .map(|(_, mut entry)| { - entry.mc_ref_seq_no = archive_id; - entry - }) - .collect(); - entries.sort_by_key(|e| e.block_id.seq_no()); - entries - }; - - Ok(entries) -} - -fn validate_shard_and_assign_mc_refs( - shard: &ShardIdent, - mut mc_shard_tops: Vec<(u32, Vec)>, - mut blocks: HashMap, - prev_committed: HashSet, -) -> Result> { - if blocks.len() == 0 { - return Ok(vec![]); - } - - let mut known: HashSet = prev_committed; - mc_shard_tops.sort_by_key(|(seqno, _)| *seqno); - - let mut entries = Vec::with_capacity(blocks.len()); - for (mc_seqno, tops) in mc_shard_tops { - for top in tops { - if !top.shard_id.intersect_with(shard) { - continue; - } - let mut current = top; - loop { - if known.contains(¤t) { - break; - } - if let Some(mut entry) = blocks.remove(¤t) { - entry.mc_ref_seq_no = mc_seqno; - let mut prevs = entry.prevs.clone(); - entries.push(entry); - // blocks before merge are always committed by MC block - if prevs.len() > 1 - && (blocks.contains_key(&prevs[0]) || blocks.contains_key(&prevs[1])) - { - fail!("Block {} parents are not committed by MC blocks", current); - } - let prev = - prevs.pop().ok_or_else(|| error!("Block {} has no parents", current))?; - known.insert(current); - current = prev; - } else { - fail!( - "Shard chain break: block {} is not in current package \ - and was not committed by previous archive group", - current, - ); - } - } - } - } - - if !blocks.is_empty() { - fail!("Some blocks in shard {} are not reachable from MC shard_hashes", shard); - } - - // Sort by seqno ascending: prev block handles must exist when setting next links. - // This also handles cross-shard deps (parent shard blocks have lower seqno than children after split). - entries.sort_by_key(|e| e.block_id.seq_no()); - Ok(entries) -} - -pub struct Ingester { - archive_manager: Arc, - block_handle_storage: Arc, - archive_state_db: Arc, - prev1_block_db: BlockInfoDb, - prev2_block_db: BlockInfoDb, - next1_block_db: BlockInfoDb, - next2_block_db: BlockInfoDb, - move_files: bool, - skip_validation: bool, -} - -impl Ingester { - pub fn new( - archive_manager: Arc, - block_handle_storage: Arc, - archive_state_db: Arc, - prev1_block_db: BlockInfoDb, - prev2_block_db: BlockInfoDb, - next1_block_db: BlockInfoDb, - next2_block_db: BlockInfoDb, - move_files: bool, - skip_validation: bool, - ) -> Self { - Self { - archive_manager, - block_handle_storage, - archive_state_db, - prev1_block_db, - prev2_block_db, - next1_block_db, - next2_block_db, - move_files, - skip_validation, - } - } - - pub async fn run_groups( - &self, - groups: &[PackageGroup], - start_idx: usize, - total: usize, - mut validator: ValidatorState, - mut last_group_state: LastGroupState, - ) -> Result { - let mut prefetch: Option< - tokio::task::JoinHandle< - Result<(HashMap, Vec>)>, - >, - > = None; - let start = std::time::Instant::now(); - - for (local_idx, group) in groups.iter().enumerate() { - let global_idx = start_idx + local_idx; - let elapsed = start.elapsed(); - let eta = (elapsed * total as u32 / (global_idx + 1) as u32).saturating_sub(elapsed); - log::info!( - target: TARGET, - "Processing group {}/{}: archive_id={}, {} shard packages. ETA {:#?}", - global_idx + 1, - total, - group.archive_id, - group.shard_packages.len(), - eta, - ); - - let next_prefetch = groups.get(local_idx + 1).map(|next| { - let mc_path = next.mc_package.path.clone(); - let shard_paths: Vec<_> = - next.shard_packages.iter().map(|p| p.path.clone()).collect(); - tokio::spawn(async move { - let (mc_raw, shard_raws) = tokio::try_join!( - read_raw_package(&mc_path), - try_join_all(shard_paths.iter().map(|p| read_raw_package(p))), - )?; - Ok::<_, ton_block::Error>((mc_raw, shard_raws)) - }) - }); - - let (mc_raw, shard_raws) = match prefetch.take() { - Some(handle) => { - handle.await.map_err(|e| error!("Prefetch task panicked: {}", e))?? - } - None => tokio::try_join!( - read_raw_package(&group.mc_package.path), - try_join_all(group.shard_packages.iter().map(|p| read_raw_package(&p.path))), - )?, - }; - - let (new_validator, new_state) = self - .ingest_group_from_raw(group, mc_raw, shard_raws, validator, last_group_state) - .await?; - validator = new_validator; - last_group_state = new_state; - prefetch = next_prefetch; - } - - self.block_handle_storage.save_full_node_state( - LAST_APPLIED_MC_BLOCK.to_string(), - &last_group_state.mc_block_id, - )?; - self.block_handle_storage.save_full_node_state( - SHARD_CLIENT_MC_BLOCK.to_string(), - &last_group_state.mc_block_id, - )?; - self.block_handle_storage - .save_full_node_state(ARCHIVES_GC_BLOCK.to_string(), &last_group_state.mc_block_id)?; - self.block_handle_storage - .save_full_node_state(PSS_KEEPER_MC_BLOCK.to_string(), &last_group_state.mc_block_id)?; - - Ok(validator) - } - - async fn ingest_group_from_raw( - &self, - group: &PackageGroup, - mc_raw: HashMap, - shard_raws: Vec>, - validator: ValidatorState, - prev_group_state: LastGroupState, - ) -> Result<(ValidatorState, LastGroupState)> { - let skip = self.skip_validation; - let expected_first_mc_prev = prev_group_state.mc_block_id; - let prev_shard_tops = prev_group_state.shard_tops; - let mc_block_count = mc_raw.len(); - let group_start = std::time::Instant::now(); - - let t = std::time::Instant::now(); - let (mc_entries, key_block, mc_shard_tops, last_group_state, validator) = - tokio::task::spawn_blocking(move || -> Result<_> { - let mut v = validator; - let (entries, key_block, shard_tops, last_state) = - parse_mc_entries(mc_raw, &mut v, skip, expected_first_mc_prev)?; - Ok((entries, key_block, shard_tops, last_state, v)) - }) - .await - .map_err(|e| error!("MC parse task panicked: {}", e))??; - let parse_mc_ms = t.elapsed().as_millis(); - - let t = std::time::Instant::now(); - for entry in &mc_entries { - self.update_block_handles(entry)?; - } - let mc_handles_ms = t.elapsed().as_millis(); - - let mc_import_entries: Vec = - mc_entries.iter().flat_map(|e| e.to_import_entries()).collect(); - - let archive_id = group.archive_id; - let shard_parse_handles: Vec<_> = shard_raws - .into_iter() - .zip(group.shard_packages.iter()) - .map(|(raw, pkg)| { - let shard = pkg.shard.clone(); - let tops = mc_shard_tops.clone(); - let prev = prev_shard_tops.clone(); - tokio::task::spawn_blocking(move || { - parse_shard_entries(raw, archive_id, shard, tops, prev, skip) - }) - }) - .collect(); - let archive_state_db = Arc::clone(&self.archive_state_db); - let fill_mc_states_db = tokio::task::spawn_blocking(move || -> Result<()> { - for entry in &mc_entries { - archive_state_db.put_update(&entry.block_id, entry.state_update_new.clone())?; - } - Ok(()) - }); - - let mc_shard = ShardIdent::masterchain(); - - // Run mc_import, mc_states, and the full shard pipeline concurrently. - let t = std::time::Instant::now(); - let mut shard_block_count = 0usize; - let (_, _, shard_pipeline_ms) = tokio::try_join!( - // Task 1: import MC package into archive - self.archive_manager.import_package( - &group.mc_package.path, - group.mc_package.archive_id, - &mc_shard, - &mc_import_entries, - false, - key_block.is_some(), - ), - // Task 2: save MC state cells - async { - fill_mc_states_db.await.map_err(|e| error!("MC states db task panicked: {}", e))? - }, - // Task 3: shard pipeline โ€” parse โ†’ handles+states โ†’ import - async { - let t_pipeline = std::time::Instant::now(); - - // 3a: await shard parse (already spawned above) - let shard_parse_results: Vec> = - try_join_all(shard_parse_handles.into_iter().map(|h| async move { - h.await.map_err(|e| error!("Shard parse task panicked: {}", e))? - })) - .await?; - - // 3b: update block handles + save shard state cells - for shard_entries in &shard_parse_results { - shard_block_count += shard_entries.len(); - for entry in shard_entries { - self.update_block_handles(entry)?; - self.archive_state_db - .put_update(&entry.block_id, entry.state_update_new.clone())?; - } - } - - // 3c: import shard packages into archive - let shard_import_entries: Vec> = shard_parse_results - .iter() - .map(|entries| entries.iter().flat_map(|e| e.to_import_entries()).collect()) - .collect(); - - try_join_all( - group - .shard_packages - .iter() - .zip(shard_import_entries.iter()) - .filter(|(_, entries)| !entries.is_empty()) - .map(|(pkg, import_entries)| { - self.archive_manager.import_package( - &pkg.path, - pkg.archive_id, - &pkg.shard, - import_entries, - self.move_files, - false, - ) - }), - ) - .await?; - - Ok(t_pipeline.elapsed().as_millis()) - }, - )?; - let parallel_ms = t.elapsed().as_millis(); - - let t = std::time::Instant::now(); - if let Some(kb) = key_block { - self.archive_key_block(&kb.block_id, kb.proof_data, kb.block_data).await?; - } - let key_block_ms = t.elapsed().as_millis(); - - if self.move_files { - if let Err(e) = tokio::fs::remove_file(&group.mc_package.path).await { - log::warn!( - target: TARGET, - "Failed to remove MC pack {} after import: {}", - group.mc_package.path.display(), - e, - ); - } - } - - log::info!( - target: TARGET, - "Imported archive {} ({} MC, {} shard blocks, {} shard pkgs) total {:#?}: \ - parse_mc {}ms, mc_handles {}ms, \ - parallel {}ms (shard_pipeline {}ms), key_block {}ms", - group.archive_id, - mc_block_count, - shard_block_count, - group.shard_packages.len(), - group_start.elapsed(), - parse_mc_ms, - mc_handles_ms, - parallel_ms, - shard_pipeline_ms, - key_block_ms, - ); - - Ok((validator, last_group_state)) - } - - async fn archive_key_block( - &self, - block_id: &BlockIdExt, - proof_data: Vec, - block_data: Vec, - ) -> Result<()> { - let handle = self.block_handle_storage.load_handle_by_id(block_id)?.ok_or_else(|| { - error!("Block handle not found for key block {} during key archive creation", block_id) - })?; - self.archive_manager - .add_block_data_to_package( - proof_data, - &handle, - &PackageEntryId::Proof(block_id.clone()), - true, - ) - .await?; - self.archive_manager - .add_block_data_to_package( - block_data, - &handle, - &PackageEntryId::Block(block_id.clone()), - true, - ) - .await?; - Ok(()) - } - - fn update_block_handles(&self, entry: &ProcessedEntry) -> Result<()> { - let meta = BlockMeta::for_import( - entry.gen_utime, - entry.end_lt, - entry.mc_ref_seq_no, - entry.is_key_block, - entry.block_id.is_masterchain(), - entry.prevs.len() > 1, - ); - - if let Some(handle) = - self.block_handle_storage.create_handle(entry.block_id.clone(), meta, None)? - { - log::trace!( - target: TARGET, - "Created block handle for {} (key={})", - entry.block_id, - entry.is_key_block, - ); - let _ = handle; - } - - let prev1 = entry - .prevs - .first() - .ok_or_else(|| error!("Block {} has no prev refs", entry.block_id))?; - - self.prev1_block_db.put(&entry.block_id, &prev1.serialize())?; - self.store_next_link(&entry.block_id, prev1)?; - - if let Some(prev2) = entry.prevs.get(1) { - self.prev2_block_db.put(&entry.block_id, &prev2.serialize())?; - self.store_next_link(&entry.block_id, prev2)?; - } - - Ok(()) - } - - fn store_next_link(&self, block_id: &BlockIdExt, prev_id: &BlockIdExt) -> Result<()> { - let prev_handle = - self.block_handle_storage.load_handle_by_id(prev_id)?.ok_or_else(|| { - error!("Block handle not found for prev block {} of {}", prev_id, block_id) - })?; - - let prev_shard = prev_id.shard(); - let shard = block_id.shard(); - if prev_shard != shard && prev_shard.split()?.1 == *shard { - // After split: right child โ†’ next2 - self.next2_block_db.put(prev_id, &block_id.serialize())?; - prev_handle.set_next2(); - } else { - // Simple chain or after merge or left child โ†’ next1 - self.next1_block_db.put(prev_id, &block_id.serialize())?; - prev_handle.set_next1(); - } - self.block_handle_storage.save_handle(&prev_handle, None)?; - Ok(()) - } -} diff --git a/src/node/src/archive_import/mod.rs b/src/node/src/archive_import/mod.rs deleted file mode 100644 index c1bfffa..0000000 --- a/src/node/src/archive_import/mod.rs +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -pub mod ingester; -pub mod scanner; -pub mod validator; - -use crate::{ - block_proof::BlockProofStuff, - collator_test_bundle::create_engine_allocated, - config::TonNodeGlobalConfig, - engine_traits::EngineAlloc, - internal_db::{ - ARCHIVE_CELLS_CF_NAME, ARCHIVE_SHARDSTATE_CF_NAME, CURRENT_DB_VERSION, DB_VERSION, - }, - shard_state::ShardStateStuff, -}; -#[cfg(feature = "telemetry")] -use crate::{collator_test_bundle::create_engine_telemetry, engine_traits::EngineTelemetry}; -use ingester::{Ingester, LastGroupState}; -use std::{ - collections::HashMap, - path::PathBuf, - sync::{atomic::AtomicU8, Arc}, -}; -use storage::{ - archive_shardstate_db::ArchiveShardStateDb, - archives::{ - archive_manager::ArchiveManager, - db_provider::EpochDbProvider, - epoch::{ArchivalModeConfig, EpochRouter}, - ARCHIVE_PACKAGE_SIZE, - }, - block_handle_db::{ - BlockHandleDb, BlockHandleStorage, NodeStateDb, BLOCK_HANDLE_DB_NAME, - VALIDATOR_STATE_DB_NAME, - }, - block_info_db::{ - BlockInfoDb, NEXT1_BLOCK_DB_NAME, NEXT2_BLOCK_DB_NAME, PREV1_BLOCK_DB_NAME, - PREV2_BLOCK_DB_NAME, - }, - db::rocksdb::{AccessType, RocksDb, NODE_DB_NAME}, - shardstate_db_async::CellsDbConfig, - traits::Serializable, - types::BlockMeta, -}; -use ton_block::{ - error, AccountIdPrefixFull, Block, BlockIdExt, Deserializable, Result, ShardIdent, UInt256, - WorkchainDescr, MASTERCHAIN_ID, SHARD_FULL, -}; -use validator::ValidatorState; - -const TARGET: &str = "archive_import"; - -pub struct ImportConfig { - pub archives_path: PathBuf, - pub epochs_path: PathBuf, - pub epoch_size: u32, - pub node_db_path: PathBuf, - pub mc_zerostate_path: PathBuf, - pub wc_zerostate_paths: Vec, - pub global_config_path: PathBuf, - pub skip_validation: bool, - pub move_files: bool, -} - -fn read_wc_zerostates_from_config(mc_zerostate: &ShardStateStuff) -> Result> { - // shard_hashes is empty at genesis; workchain zerostates are in ConfigParams::workchains() - let mut shards = Vec::new(); - mc_zerostate.config_params()?.workchains()?.iterate_with_keys( - |wc_id: i32, descr: WorkchainDescr| { - let shard = ShardIdent::with_tagged_prefix(wc_id, SHARD_FULL)?; - shards.push(BlockIdExt::with_params( - shard, - 0, - descr.zerostate_root_hash, - descr.zerostate_file_hash, - )); - Ok(true) - }, - )?; - Ok(shards) -} - -async fn build_initial_group_state( - zerostate: &ShardStateStuff, - archive_manager: &ArchiveManager, - last_imported: u32, -) -> Result { - if last_imported == 0 { - let shard_tops = read_wc_zerostates_from_config(zerostate)?; - log::info!( - target: TARGET, - "Initial state from zerostate {}, {} workchain shard tops", - zerostate.block_id(), - shard_tops.len(), - ); - return Ok(LastGroupState { mc_block_id: zerostate.block_id().clone(), shard_tops }); - } - - let mc_prefix = AccountIdPrefixFull { workchain_id: MASTERCHAIN_ID, prefix: 0 }; - let (block_id, block_data) = archive_manager - .lookup_block_by_seqno(&mc_prefix, last_imported) - .await? - .ok_or_else(|| error!("Cannot find MC block at seqno {}", last_imported))?; - let block = Block::construct_from_bytes(&block_data)?; - let extra = block - .read_extra()? - .read_custom()? - .ok_or_else(|| error!("No McExtra in MC block {}", block_id))?; - let shard_tops = - crate::shard_state::ShardHashesStuff::from(extra.shards().clone()).top_blocks_all()?; - log::info!( - target: TARGET, - "Resuming from MC block {} (seqno {}), {} shard tops", - block_id, - last_imported, - shard_tops.len(), - ); - Ok(LastGroupState { mc_block_id: block_id, shard_tops }) -} - -fn process_zerostates( - config: &ImportConfig, - global_config: &TonNodeGlobalConfig, - archive_state_db: &ArchiveShardStateDb, - block_handle_storage: &BlockHandleStorage, - #[cfg(feature = "telemetry")] engine_telemetry: Arc, - engine_allocated: Arc, -) -> Result> { - log::info!(target: TARGET, "Loading MC zerostate from {}", config.mc_zerostate_path.display()); - let zerostate_bytes = std::fs::read(&config.mc_zerostate_path).map_err(|e| { - error!("Cannot read MC zerostate file {}: {}", config.mc_zerostate_path.display(), e) - })?; - let expected_mc_zerostate_id = global_config.zero_state()?; - let mc_zerostate = ShardStateStuff::deserialize_zerostate( - expected_mc_zerostate_id.clone(), - &zerostate_bytes, - #[cfg(feature = "telemetry")] - &engine_telemetry, - &engine_allocated, - )?; - log::info!(target: TARGET, "MC zerostate loaded successfully"); - - // Load and validate workchain zerostates - let mut expected_wc_zerostates: HashMap = HashMap::from_iter( - read_wc_zerostates_from_config(&mc_zerostate)? - .into_iter() - .map(|id| (id.file_hash.clone(), id)), - ); - - let mut wc_zerostates = Vec::new(); - for path in &config.wc_zerostate_paths { - log::info!(target: TARGET, "Loading workchain zerostate from {}", path.display()); - let zerostate_bytes = std::fs::read(path) - .map_err(|e| error!("Cannot read WC zerostate file {}: {}", path.display(), e))?; - let id = expected_wc_zerostates.remove(&UInt256::calc_file_hash(&zerostate_bytes)).ok_or_else(|| { - error!( - "Workchain zerostate file {} does not match any expected file hash from MC zerostate", - path.display(), - ) - })?; - let state = ShardStateStuff::deserialize_zerostate( - id.clone(), - &zerostate_bytes, - #[cfg(feature = "telemetry")] - &engine_telemetry, - &engine_allocated, - )?; - wc_zerostates.push((id, state.root_cell().clone())); - } - - if !expected_wc_zerostates.is_empty() { - let missing: Vec<_> = expected_wc_zerostates.into_iter().collect(); - return Err(error!("Missing workchain zerostates: {:?}", missing,)); - } - - let save_handle = |id: &BlockIdExt| -> Result<()> { - let handle = if let Some(handle) = - block_handle_storage.create_handle(id.clone(), BlockMeta::default(), None)? - { - handle - } else { - block_handle_storage - .load_handle_by_id(&id)? - .ok_or_else(|| error!("Failed to create or load block handle for MC zerostate"))? - }; - if handle.set_state() | handle.set_state_saved() | handle.set_block_applied() { - block_handle_storage.save_handle(&handle, None)?; - } - Ok(()) - }; - - archive_state_db.put(&expected_mc_zerostate_id, mc_zerostate.root_cell().clone())?; - save_handle(&expected_mc_zerostate_id)?; - log::info!(target: TARGET, "MC zerostate saved to archive state DB"); - - for (wc_id, wc_root) in wc_zerostates { - archive_state_db.put(&wc_id, wc_root)?; - save_handle(&wc_id)?; - log::info!(target: TARGET, "Workchain zerostate {} saved to archive state DB", wc_id); - } - - Ok(mc_zerostate) -} - -/// Returns the node_db Arc so the caller can wait for all background tasks to release it. -pub async fn run_import(config: ImportConfig) -> Result> { - log::info!( - target: TARGET, - "Loading global config from {}", - config.global_config_path.display() - ); - let global_config = TonNodeGlobalConfig::from_json_file(&config.global_config_path) - .map_err(|e| error!("Cannot load global config: {}", e))?; - let expected_zerostate_id = global_config.zero_state()?; - let mut hardforks = global_config.hardforks()?; - hardforks.sort_by_key(|hf| hf.seq_no()); - log::info!( - target: TARGET, - "Global config: zerostate={}, {} hard fork(s)", - expected_zerostate_id, - hardforks.len(), - ); - - #[cfg(feature = "telemetry")] - let engine_telemetry = create_engine_telemetry(); - let engine_allocated = create_engine_allocated(); - - let epoch_config = ArchivalModeConfig { - epoch_size: config.epoch_size, - new_epochs_path: config.epochs_path.clone(), - existing_epochs: vec![], - }; - let router = Arc::new(EpochRouter::new(&epoch_config).await?); - let db_provider = Arc::new(EpochDbProvider::new(router)); - - std::fs::create_dir_all(&config.node_db_path).map_err(|e| { - error!("Cannot create node_db_path {}: {}", config.node_db_path.display(), e) - })?; - let node_db = RocksDb::new(&config.node_db_path, NODE_DB_NAME, None, AccessType::ReadWrite)?; - - let handle_db = Arc::new(BlockHandleDb::with_db(node_db.clone(), BLOCK_HANDLE_DB_NAME, true)?); - let full_node_state_db = Arc::new(NodeStateDb::with_db( - node_db.clone(), - storage::db::rocksdb::NODE_STATE_DB_NAME, - true, - )?); - full_node_state_db.put(&DB_VERSION, &CURRENT_DB_VERSION.serialize())?; - let validator_state_db = - Arc::new(NodeStateDb::with_db(node_db.clone(), VALIDATOR_STATE_DB_NAME, true)?); - - let prev1_block_db = BlockInfoDb::with_db(node_db.clone(), PREV1_BLOCK_DB_NAME, true)?; - let prev2_block_db = BlockInfoDb::with_db(node_db.clone(), PREV2_BLOCK_DB_NAME, true)?; - let next1_block_db = BlockInfoDb::with_db(node_db.clone(), NEXT1_BLOCK_DB_NAME, true)?; - let next2_block_db = BlockInfoDb::with_db(node_db.clone(), NEXT2_BLOCK_DB_NAME, true)?; - - #[cfg(feature = "telemetry")] - let storage_telemetry = engine_telemetry.storage.clone(); - let storage_alloc = engine_allocated.storage.clone(); - - let mut block_handle_storage = BlockHandleStorage::with_dbs( - handle_db, - full_node_state_db, - validator_state_db, - #[cfg(feature = "telemetry")] - storage_telemetry.clone(), - storage_alloc.clone(), - ); - block_handle_storage.set_no_cache(); - let block_handle_storage = Arc::new(block_handle_storage); - - let db_root_path = Arc::new(config.node_db_path.clone()); - let shard_split_depth = Arc::new(AtomicU8::new(0)); - - let archive_manager = Arc::new( - ArchiveManager::with_data( - node_db.clone(), - db_root_path, - db_provider, - 0, // last_unneeded_key_block - shard_split_depth, - #[cfg(feature = "telemetry")] - storage_telemetry, - storage_alloc, - ) - .await?, - ); - - let cells_db_config = CellsDbConfig::default(); - let archive_states_db = RocksDb::new( - &config.node_db_path, - crate::internal_db::ARCHIVE_STATES_DB_NAME, - std::collections::HashMap::from([( - ARCHIVE_CELLS_CF_NAME.to_string(), - storage::cell_db::CellDb::build_cf_options(cells_db_config.cells_cache_size_bytes), - )]), - AccessType::ReadWrite, - )?; - let archive_state_db = Arc::new(ArchiveShardStateDb::new( - archive_states_db, - ARCHIVE_SHARDSTATE_CF_NAME, - ARCHIVE_CELLS_CF_NAME, - &config.node_db_path, - &cells_db_config, - #[cfg(feature = "telemetry")] - engine_telemetry.storage.clone(), - engine_allocated.storage.clone(), - )?); - - let mc_zerostate = process_zerostates( - &config, - &global_config, - &archive_state_db, - &block_handle_storage, - #[cfg(feature = "telemetry")] - engine_telemetry.clone(), - engine_allocated.clone(), - )?; - - log::info!(target: TARGET, "Scanning packages in {}", config.archives_path.display()); - let packages = scanner::scan_packages(&config.archives_path)?; - log::info!(target: TARGET, "Found {} package files", packages.len()); - - if packages.is_empty() { - log::warn!(target: TARGET, "No packages found, nothing to import"); - return Ok(node_db); - } - - let groups = scanner::group_by_archive_id(packages)?; - log::info!(target: TARGET, "Grouped into {} archive groups", groups.len()); - - let mut validator_state = ValidatorState::new(mc_zerostate.clone(), hardforks); - let mut skip_count = 0; - - let last_imported = if let Some(max_seqno) = archive_manager.get_max_mc_seqno().await { - if max_seqno > groups.last().unwrap().archive_id + ARCHIVE_PACKAGE_SIZE as u32 { - log::warn!(target: TARGET, - "Existing import detected with max MC seqno {}, which is beyond the last archive group ({}), skipping all groups", - max_seqno, groups.last().unwrap().archive_id); - return Ok(node_db); - } - skip_count = - groups.iter().take_while(|g| g.archive_id < max_seqno).count().saturating_sub(1); - - log::info!( - target: TARGET, - "Detected existing import (max MC seqno = {}), skipping {} groups", - max_seqno, - skip_count, - ); - - // Restore key block proof regardless of skip_count: files may have been moved - // and the scanned list may start mid-chain. - if !config.skip_validation { - if let Some(key_seqno) = archive_manager.get_max_key_block_seqno().await { - let mc_prefix = AccountIdPrefixFull { workchain_id: MASTERCHAIN_ID, prefix: 0 }; - let (block_id, proof_data) = archive_manager - .lookup_proof_by_seqno(&mc_prefix, key_seqno) - .await? - .ok_or_else(|| { - error!( - "Key block seqno {} found in index but proof not readable", - key_seqno, - ) - })?; - let proof = BlockProofStuff::deserialize(&block_id, proof_data, false)?; - log::info!( - target: TARGET, - "Restored key block proof: {}", - block_id, - ); - validator_state.set_key_block_proof(proof); - } - } - groups[skip_count].archive_id.saturating_sub(1) - } else { - 0 - }; - - let initial_group_state = - build_initial_group_state(&mc_zerostate, &archive_manager, last_imported).await?; - - let ingester = Ingester::new( - archive_manager, - block_handle_storage, - archive_state_db, - prev1_block_db, - prev2_block_db, - next1_block_db, - next2_block_db, - config.move_files, - config.skip_validation, - ); - - let total = groups.len(); - ingester - .run_groups(&groups[skip_count..], skip_count, total, validator_state, initial_group_state) - .await?; - - log::info!(target: TARGET, "Import complete! Processed {} archive groups", total); - Ok(node_db) -} - -#[cfg(not(target_os = "windows"))] -#[cfg(test)] -#[path = "../tests/test_archive_import.rs"] -mod tests; diff --git a/src/node/src/archive_import/scanner.rs b/src/node/src/archive_import/scanner.rs deleted file mode 100644 index 077e793..0000000 --- a/src/node/src/archive_import/scanner.rs +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, - str::FromStr, -}; -use ton_block::{error, fail, Result, ShardIdent}; - -pub struct PackageFile { - pub path: PathBuf, - pub archive_id: u32, - pub shard: ShardIdent, -} - -pub struct PackageGroup { - pub archive_id: u32, - pub mc_package: PackageFile, - pub shard_packages: Vec, -} - -/// Parse a .pack filename into (archive_id, shard). -fn parse_pack_filename(filename: &str) -> Result> { - if !filename.ends_with(".pack") { - return Ok(None); - } - let stem = &filename[..filename.len() - 5]; - - if stem.starts_with("key.") { - return Ok(None); - } - - if !stem.starts_with("archive.") { - return Ok(None); - } - let rest = &stem[8..]; - - if let Some(dot_pos) = rest.find('.') { - // archive.NNNNN.WC:HHHHHHHHHHHHHHHH - shards - let id_str = &rest[..dot_pos]; - let shard_str = &rest[dot_pos + 1..]; - - let archive_id: u32 = - id_str.parse().map_err(|_| error!("Invalid archive id in filename: {}", filename))?; - - let shard = ShardIdent::from_str(shard_str)?; - Ok(Some((archive_id, shard))) - } else { - // archive.NNNNN โ€” masterchain - let archive_id: u32 = - rest.parse().map_err(|_| error!("Invalid archive id in filename: {}", filename))?; - Ok(Some((archive_id, ShardIdent::masterchain()))) - } -} - -/// Scan the source directory for .pack files, parse filenames, sort by archive_id. -pub fn scan_packages(archives_path: &Path) -> Result> { - let entries = std::fs::read_dir(archives_path) - .map_err(|e| error!("Cannot read archives directory {}: {}", archives_path.display(), e))?; - - let mut packages = Vec::new(); - - for entry in entries { - let entry = entry.map_err(|e| error!("Error reading directory entry: {}", e))?; - let path = entry.path(); - - if !path.is_file() { - continue; - } - - let filename = match path.file_name().and_then(|n| n.to_str()) { - Some(name) => name.to_string(), - None => continue, - }; - - if let Some((archive_id, shard)) = parse_pack_filename(&filename)? { - packages.push(PackageFile { path, archive_id, shard }); - } - } - - // Sort by archive_id, then MC before shards - packages.sort_by(|a, b| { - a.archive_id.cmp(&b.archive_id).then_with(|| { - let a_mc = a.shard.is_masterchain() as u8; - let b_mc = b.shard.is_masterchain() as u8; - b_mc.cmp(&a_mc) // MC first - }) - }); - - Ok(packages) -} - -/// Group packages by archive_id: each group has one MC package and zero or more shard packages. -pub fn group_by_archive_id(packages: Vec) -> Result> { - let mut map: BTreeMap, Vec)> = BTreeMap::new(); - - for pkg in packages { - let entry = map.entry(pkg.archive_id).or_insert_with(|| (None, Vec::new())); - if pkg.shard.is_masterchain() { - if entry.0.is_some() { - fail!("Duplicate MC package for archive_id {}", pkg.archive_id); - } - entry.0 = Some(pkg); - } else { - entry.1.push(pkg); - } - } - - let mut groups = Vec::with_capacity(map.len()); - for (archive_id, (mc_package, shard_packages)) in map { - let mc_package = mc_package - .ok_or_else(|| error!("No MC package found for archive_id {}", archive_id))?; - groups.push(PackageGroup { archive_id, mc_package, shard_packages }); - } - - Ok(groups) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_mc_filename() { - let (id, shard) = parse_pack_filename("archive.00100.pack").unwrap().unwrap(); - assert_eq!(id, 100); - assert!(shard.is_masterchain()); - } - - #[test] - fn test_parse_shard_filename_with_wc() { - let (id, shard) = - parse_pack_filename("archive.00100.0:8000000000000000.pack").unwrap().unwrap(); - assert_eq!(id, 100); - assert!(!shard.is_masterchain()); - assert_eq!(shard.workchain_id(), 0); - assert_eq!(shard.shard_prefix_with_tag(), 0x8000000000000000); - } - - #[test] - fn test_parse_key_filename_skipped() { - assert!(parse_pack_filename("key.archive.000000.pack").unwrap().is_none()); - } - - #[test] - fn test_parse_non_pack_file() { - assert!(parse_pack_filename("readme.txt").unwrap().is_none()); - } -} diff --git a/src/node/src/archive_import/validator.rs b/src/node/src/archive_import/validator.rs deleted file mode 100644 index 9ff74fe..0000000 --- a/src/node/src/archive_import/validator.rs +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use crate::{block_proof::BlockProofStuff, shard_state::ShardStateStuff}; -use std::sync::Arc; -use ton_block::{BlockIdExt, BlockInfo, Result}; - -pub struct ValidatorState { - zerostate: Arc, - current_key_block_proof: Option, - hardforks: Vec, -} - -impl ValidatorState { - pub fn new(zerostate: Arc, hardforks: Vec) -> Self { - Self { zerostate, current_key_block_proof: None, hardforks } - } - - pub(crate) fn is_hardfork(&self, block_id: &BlockIdExt) -> bool { - self.hardforks.iter().any(|hf| hf == block_id) - } - - pub(crate) fn zerostate(&self) -> &Arc { - &self.zerostate - } - - pub(crate) fn current_key_block_proof(&self) -> Option<&BlockProofStuff> { - self.current_key_block_proof.as_ref() - } - - pub fn set_key_block_proof(&mut self, proof: BlockProofStuff) { - self.current_key_block_proof = Some(proof); - } - - pub fn validate_mc_proof(&mut self, proof: &BlockProofStuff) -> Result { - let (virt_block, _virt_root) = proof.virtualize_block()?; - let info = virt_block.read_info()?; - - let prev_key_block_seqno = info.prev_key_block_seqno(); - - if prev_key_block_seqno == 0 { - proof.check_with_master_state(&self.zerostate)?; - } else { - let prev_key_proof = self.current_key_block_proof.as_ref().ok_or_else(|| { - ton_block::error!( - "No key block proof available for validation of block {} \ - (prev_key_block_seqno = {})", - proof.id(), - prev_key_block_seqno - ) - })?; - proof.check_with_prev_key_block_proof(prev_key_proof)?; - } - - if info.key_block() { - self.current_key_block_proof = Some(proof.clone()); - } - - Ok(info) - } - - pub fn extract_mc_info(&mut self, proof: &BlockProofStuff) -> Result { - let (virt_block, _virt_root) = proof.virtualize_block()?; - let info = virt_block.read_info()?; - - if info.key_block() { - self.current_key_block_proof = Some(proof.clone()); - } - - Ok(info) - } - - pub fn validate_shard_proof_link(&self, proof: &BlockProofStuff) -> Result<()> { - proof.check_proof_link() - } -} diff --git a/src/node/src/collator_test_bundle.rs b/src/node/src/collator_test_bundle.rs index 51b6874..e952b4a 100644 --- a/src/node/src/collator_test_bundle.rs +++ b/src/node/src/collator_test_bundle.rs @@ -1497,10 +1497,6 @@ impl EngineOperations for CollatorTestBundle { self.index.now } - fn now_ms(&self) -> u64 { - self.index.now as u64 * 1000 - } - fn load_block_handle(&self, id: &BlockIdExt) -> Result>> { let handle = self.block_handle_storage.create_handle(id.clone(), BlockMeta::default(), None)?; @@ -1749,7 +1745,6 @@ pub async fn try_collate( engine, true, true, - false, ); validator_query.try_validate().await?; } @@ -1796,7 +1791,6 @@ pub async fn try_validate( engine, true, false, - false, ); validator_query.try_validate().await } diff --git a/src/node/src/config.rs b/src/node/src/config.rs index d48bd12..5ae331c 100644 --- a/src/node/src/config.rs +++ b/src/node/src/config.rs @@ -44,7 +44,7 @@ use std::{ }, time::Duration, }; -use storage::{archives::epoch::ArchivalModeConfig, shardstate_db_async::CellsDbConfig}; +use storage::shardstate_db_async::CellsDbConfig; use ton_api::{ ton::{ self, @@ -366,9 +366,6 @@ pub struct TonNodeConfig { unsafe_catchain_patches_path: Option, #[serde(skip_serializing)] ip_address: Option, - /// Explicit QUIC address (ip:port). If absent, derived as same_ip:adnl_port+1000. - #[serde(skip_serializing_if = "Option::is_none")] - ip_address_quic: Option, adnl_node: Option, json_rpc_server: Option, metrics: Option, @@ -411,8 +408,6 @@ pub struct TonNodeConfig { sync_by_archives: bool, #[serde(default)] accelerated_consensus_disabled: bool, - #[serde(skip_serializing_if = "Option::is_none")] - archival_mode: Option, #[serde(skip)] custom_overlays: CustomOverlaysConfigBoxed, #[serde(default)] @@ -570,9 +565,6 @@ impl TonNodeConfig { if let Some(port) = self.port { ret.set_port(port) } - if let Some(quic_addr) = self.quic_address() { - ret.set_ip_address_quic(quic_addr); - } Ok(ret) } @@ -627,10 +619,6 @@ impl TonNodeConfig { } pub fn gc_archives_life_time_hours(&self) -> Option { - // GC disabled in archival mode - if self.archival_mode.is_some() { - return None; - } if let Some(gc) = &self.gc { if gc.enable_for_archives { return gc.archives_life_time_hours.or(Some(0)); @@ -639,10 +627,6 @@ impl TonNodeConfig { None } - pub fn archival_mode(&self) -> Option<&ArchivalModeConfig> { - self.archival_mode.as_ref() - } - pub fn internal_db_path(&self) -> &str { self.internal_db_path.as_deref().unwrap_or(Self::DEFAULT_DB_ROOT) } @@ -693,31 +677,6 @@ impl TonNodeConfig { self.accelerated_consensus_disabled } - pub fn quic_address(&self) -> Option { - self.ip_address_quic.as_ref().and_then(|s| match s.parse::() { - Ok(addr) => { - if !addr.ip().is_ipv4() { - log::warn!( - "ip_address_quic \"{s}\" is not an IPv4 address. \ - ADNL/TL address lists only support IPv4, so this QUIC address \ - cannot be advertised. QUIC address will not be used, \ - node will fall back to derived port." - ); - None - } else { - Some(addr) - } - } - Err(e) => { - log::warn!( - "Failed to parse ip_address_quic \"{s}\": {e}. \ - QUIC address will not be used, node will fall back to derived port." - ); - None - } - }) - } - #[cfg(test)] pub fn set_port(&mut self, port: u16) { self.port.replace(port); @@ -834,7 +793,6 @@ impl TonNodeConfig { Ok(()) } - #[allow(dead_code)] fn get_validator_key_info(&self, validator_key_id: &str) -> Result> { if let Some(validator_keys) = &self.validator_keys { for key_json in validator_keys { @@ -1063,11 +1021,10 @@ impl TonNodeConfig { election_id: i32, expire_at: i32, ) -> Result { - let new_key_id_b64 = base64_encode(key_id); let key_info = ValidatorKeysJson { expire_at, election_id, - validator_key_id: new_key_id_b64.clone(), + validator_key_id: base64_encode(key_id), validator_adnl_key_id: None, }; @@ -1077,16 +1034,7 @@ impl TonNodeConfig { let added_key_info = self.get_validator_key_info_by_election_id(&election_id)?; match &mut self.validator_keys { Some(validator_keys) => match added_key_info { - Some(existing) => { - if existing.validator_key_id != new_key_id_b64 { - log::warn!( - "add_validator_key: OVERWRITING validator key for election_id={}: \ - old_key={} -> new_key={} (adnl_key will be cleared)", - election_id, - existing.validator_key_id, - new_key_id_b64, - ); - } + Some(_) => { self.update_validator_key_info(key_info.clone())?; } None => { @@ -1106,46 +1054,12 @@ impl TonNodeConfig { validator_key_id: &[u8; 32], adnl_key_id: &[u8; 32], ) -> Result { - let key_id_b64 = base64_encode(validator_key_id); - let new_adnl_b64 = base64_encode(adnl_key_id); - - let matching: Vec = self - .validator_keys - .as_ref() - .map(|keys| keys.iter().filter(|k| k.validator_key_id == key_id_b64).cloned().collect()) - .unwrap_or_default(); - - if matching.is_empty() { + if let Some(mut key_info) = self.get_validator_key_info(&base64_encode(validator_key_id))? { + key_info.validator_adnl_key_id = Some(base64_encode(adnl_key_id)); + self.update_validator_key_info(key_info) + } else { fail!("Validator key have not been added!") } - - let mut last_updated = None; - for entry in &matching { - if let Some(existing_adnl) = &entry.validator_adnl_key_id { - if *existing_adnl != new_adnl_b64 { - log::warn!( - "add_validator_adnl_key: OVERWRITING adnl key for election_id={}: \ - old_adnl={} -> new_adnl={} (validator_key={})", - entry.election_id, - existing_adnl, - new_adnl_b64, - key_id_b64, - ); - } - } - let mut updated = entry.clone(); - updated.validator_adnl_key_id = Some(new_adnl_b64.clone()); - last_updated = Some(self.update_validator_key_info(updated)?); - } - if matching.len() > 1 { - log::info!( - "add_validator_adnl_key: updated adnl binding for {} elections sharing \ - validator_key={}", - matching.len(), - key_id_b64, - ); - } - Ok(last_updated.unwrap()) } async fn remove_validator_key( @@ -2240,27 +2154,7 @@ impl ValidatorKeys { // inserted in sorted order let mut first = false; - add_unbound_object_to_map_with_update(&self.values, key.election_id, |old| { - if let Some(existing) = old { - if existing.validator_key_id != key.validator_key_id { - log::warn!( - "ValidatorKeys: replacing validator key for election_id={}: \ - old_key={} -> new_key={}", - key.election_id, - existing.validator_key_id, - key.validator_key_id, - ); - } - if existing.validator_adnl_key_id != key.validator_adnl_key_id { - log::warn!( - "ValidatorKeys: adnl key changed for election_id={}: \ - old_adnl={:?} -> new_adnl={:?}", - key.election_id, - existing.validator_adnl_key_id, - key.validator_adnl_key_id, - ); - } - } + add_unbound_object_to_map_with_update(&self.values, key.election_id, |_| { if self .first .compare_exchange( diff --git a/src/node/src/engine.rs b/src/node/src/engine.rs index ec97f02..1b1d070 100644 --- a/src/node/src/engine.rs +++ b/src/node/src/engine.rs @@ -430,15 +430,14 @@ impl Engine { pub const MASK_SERVICE_ARCHIVES_GC: u32 = 0x0800; pub const MASK_SERVICE_SS_CACHE_KEEPER: u32 = 0x1000; - // Sync status (ordered by normal flow: boot โ†’ states โ†’ finish boot โ†’ archives โ†’ blocks โ†’ synced) - pub const SYNC_STATUS_START_BOOT: u32 = 1; - pub const SYNC_STATUS_LOAD_STATES: u32 = 2; - pub const SYNC_STATUS_FINISH_BOOT: u32 = 3; - pub const SYNC_STATUS_SYNC_ARCHIVES: u32 = 4; - pub const SYNC_STATUS_SYNC_BLOCKS: u32 = 5; - pub const SYNC_STATUS_FINISH_SYNC: u32 = 6; - pub const SYNC_STATUS_CHECKING_DB: u32 = 7; - pub const SYNC_STATUS_DB_BROKEN: u32 = 8; + // Sync status + pub const SYNC_STATUS_START_BOOT: u32 = 0x0001; + pub const SYNC_STATUS_LOAD_STATES: u32 = 0x0003; + pub const SYNC_STATUS_FINISH_BOOT: u32 = 0x0004; + pub const SYNC_STATUS_SYNC_BLOCKS: u32 = 0x0005; + pub const SYNC_STATUS_FINISH_SYNC: u32 = 0x0006; + pub const SYNC_STATUS_CHECKING_DB: u32 = 0x0007; + pub const SYNC_STATUS_DB_BROKEN: u32 = 0x0008; const MASK_STOP: u32 = 0x80000000; const TIMEOUT_STOP_MS: u64 = 1000; @@ -518,11 +517,7 @@ impl Engine { }); let archives_life_time_hours = general_config.gc_archives_life_time_hours(); - let cells_lifetime_sec = if general_config.archival_mode().is_none() { - general_config.cells_gc_config().cells_lifetime_sec - } else { - u64::MAX - }; + let cells_lifetime_sec = general_config.cells_gc_config().cells_lifetime_sec; let enable_shard_state_persistent_gc = general_config.enable_shard_state_persistent_gc(); let skip_saving_persistent_states = general_config.skip_saving_persistent_states(); let states_cache_mode = general_config.states_cache_mode(); @@ -533,7 +528,6 @@ impl Engine { db_directory: general_config.internal_db_path().to_string(), cells_gc_interval_sec: general_config.cells_gc_config().gc_interval_sec, cells_db_config: cells_db_config.clone(), - archival_mode: general_config.archival_mode().cloned(), }; let control_config = general_config.control_server()?; let collator_config = general_config.collator_config().clone(); @@ -1459,16 +1453,10 @@ impl Engine { shardstates_queue: create_metric("Alloc NODE shardstates queue"), cached_cells_counters: create_metric("Alloc NODE cells counters"), - loaded_cells_from_db: create_metric_per_sec("NODE loaded from db cells/sec"), - load_cell_from_db_time_nanos: create_metric_with_total_average( + loaded_cells: create_metric_per_sec("NODE loaded from db cells/sec"), + load_cell_time_nanos: create_metric_with_total_average( "NODE cell load time from db, nanos", ), - load_cell_from_cache_time_nanos: create_metric_with_total_average( - "NODE cell load time from cache, nanos", - ), - store_cell_to_cache_time_nanos: create_metric_with_total_average( - "NODE cell store time to cache, nanos", - ), stored_new_cells: create_metric_per_sec("NODE stored new cells & counters/sec"), deleted_cells: create_metric_per_sec("NODE deleted cells & counters/sec"), @@ -1508,9 +1496,6 @@ impl Engine { delete_boc_commit_micros: create_metric_with_total_average( "NODE delete boc: commit time, micros", ), - cell_cache_hits: create_metric_per_sec("NODE cell cache hits/sec"), - cell_cache_misses: create_metric_per_sec("NODE cell cache misses/sec"), - cell_cache_len: create_metric("NODE cell cache len"), }); let engine_telemetry = Arc::new(EngineTelemetry { storage: storage_telemetry, @@ -1531,10 +1516,8 @@ impl Engine { TelemetryItem::Metric(engine_telemetry.storage.storing_cells.clone()), TelemetryItem::Metric(engine_telemetry.storage.shardstates_queue.clone()), TelemetryItem::Metric(engine_telemetry.storage.cached_cells_counters.clone()), - TelemetryItem::MetricBuilder(engine_telemetry.storage.loaded_cells_from_db.clone()), - TelemetryItem::Metric(engine_telemetry.storage.load_cell_from_db_time_nanos.clone()), - TelemetryItem::Metric(engine_telemetry.storage.load_cell_from_cache_time_nanos.clone()), - TelemetryItem::Metric(engine_telemetry.storage.store_cell_to_cache_time_nanos.clone()), + TelemetryItem::MetricBuilder(engine_telemetry.storage.loaded_cells.clone()), + TelemetryItem::Metric(engine_telemetry.storage.load_cell_time_nanos.clone()), TelemetryItem::MetricBuilder(engine_telemetry.storage.stored_new_cells.clone()), TelemetryItem::MetricBuilder(engine_telemetry.storage.deleted_cells.clone()), TelemetryItem::MetricBuilder(engine_telemetry.storage.loaded_counters.clone()), @@ -2115,7 +2098,7 @@ async fn boot( let (last_applied_mc_block, cold) = match result { Ok(block_id) => (block_id.clone(), false), Err(err) => { - log::warn!("Before cold boot: {err}"); + log::debug!("before cold boot: {}", err); engine.acquire_stop(Engine::MASK_SERVICE_BOOT); let result = boot::cold_boot(engine.clone(), pss_downloading_threads).await; engine.release_stop(Engine::MASK_SERVICE_BOOT); @@ -2299,7 +2282,6 @@ pub async fn run( // Sync by archives if sync_by_archives && !engine.check_sync().await? { - engine.set_sync_status(Engine::SYNC_STATUS_SYNC_ARCHIVES); struct Checker; #[async_trait::async_trait] impl crate::sync::StopSyncChecker for Checker { @@ -2425,30 +2407,6 @@ fn telemetry_logger(engine: Arc) { // print telemetry - { - let hits = engine - .engine_telemetry - .storage - .cell_cache_hits - .metric() - .total_amount() - .unwrap_or(0); - let misses = engine - .engine_telemetry - .storage - .cell_cache_misses - .metric() - .total_amount() - .unwrap_or(0); - let total = hits + misses; - let hit_rate = if total > 0 { hits * 100 / total } else { 0 }; - log::info!( - target: "telemetry", - "Cell cache hit_rate: {}%", - hit_rate - ); - } - engine.telemetry_printer.try_print(); let period = crate::full_node::telemetry::TPS_PERIOD_1; @@ -2538,8 +2496,8 @@ pub fn init_prometheus_recorder( // -- engine metrics::describe_gauge!( "ton_node_engine_sync_status", - "Sync state (0=not_set, 1=boot, 2=load_states, 3=finish_boot, \ - 4=sync_archives, 5=sync_blocks, 6=synced, 7=checking_db, 8=db_broken)" + "Sync state (0=not_set, 1=boot, 3=load_states, 4=finish_boot, \ + 5=syncing, 6=synced, 7=checking_db, 8=db_broken)" ); metrics::describe_gauge!( "ton_node_engine_timediff_seconds", @@ -2568,7 +2526,7 @@ pub fn init_prometheus_recorder( // -- validator metrics::describe_gauge!( "ton_node_validator_status", - "Validation state (0=Disabled, 1=Waiting, 2=Active)" + "Validation state (0=Disabled, 1=Waiting, 2=Countdown, 3=Active)" ); metrics::describe_gauge!( "ton_node_validator_in_current_set", diff --git a/src/node/src/engine_operations.rs b/src/node/src/engine_operations.rs index 668eaed..2d3fdaa 100644 --- a/src/node/src/engine_operations.rs +++ b/src/node/src/engine_operations.rs @@ -13,16 +13,13 @@ use crate::{ block_proof::BlockProofStuff, config::{CollatorConfig, CollatorTestBundlesGeneralConfig}, engine::{Engine, EngineFlags, SplitQueues}, - engine_traits::{ - EngineAlloc, EngineOperations, PrivateOverlayOperations, ValidatorKeyBinding, - ValidatorListOutcome, - }, + engine_traits::{EngineAlloc, EngineOperations, PrivateOverlayOperations}, error::NodeError, ext_messages::{create_ext_message, EXT_MESSAGES_TRACE_TARGET}, full_node::shard_client::{process_block_broadcast, process_block_broadcast_v2}, internal_db::{ - BlockResult, DESTROYED_VALIDATOR_SESSIONS, INITIAL_MC_BLOCK, LAST_APPLIED_MC_BLOCK, - LAST_ROTATION_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, + BlockResult, INITIAL_MC_BLOCK, LAST_APPLIED_MC_BLOCK, LAST_ROTATION_MC_BLOCK, + SHARD_CLIENT_MC_BLOCK, }, shard_state::ShardStateStuff, shard_states_keeper::PinnedShardStateGuard, @@ -47,44 +44,11 @@ use ton_api::ton::{ }; use ton_block::{ error, fail, AccountIdPrefixFull, BlockIdExt, BlockSignaturesVariant, Cell, CellsFactory, - ConfigParams, CryptoSignaturePair, KeyId, Message, OutMsgQueue, Result, ShardIdent, UInt256, + ConfigParams, CryptoSignaturePair, KeyId, KeyOption, Message, OutMsgQueue, Result, ShardIdent, + UInt256, }; use validator_session::{BlockHash, SessionId, ValidatorBlockCandidate}; -fn serialize_destroyed_session_ids(ids: &HashSet) -> Vec { - let mut sorted_ids = ids.iter().cloned().collect::>(); - sorted_ids.sort_by(|left, right| left.as_slice().cmp(right.as_slice())); - - let mut data = Vec::with_capacity(4 + sorted_ids.len() * 32); - data.extend_from_slice(&(sorted_ids.len() as u32).to_le_bytes()); - for id in sorted_ids { - data.extend_from_slice(id.as_slice()); - } - data -} - -fn deserialize_destroyed_session_ids(data: &[u8]) -> Result> { - if data.len() < 4 { - fail!("Destroyed-session payload is too short: {}", data.len()); - } - - let count = u32::from_le_bytes(data[..4].try_into()?) as usize; - let expected_len = 4 + count * 32; - if data.len() != expected_len { - fail!( - "Destroyed-session payload has invalid length: expected {}, got {}", - expected_len, - data.len() - ); - } - - let mut ids = Vec::with_capacity(count); - for chunk in data[4..].chunks_exact(32) { - ids.push(UInt256::from_slice(chunk)); - } - Ok(ids) -} - #[async_trait::async_trait] impl EngineOperations for Engine { // Global node's state @@ -127,28 +91,23 @@ impl EngineOperations for Engine { Engine::validator_network(self) } - /// Register the local node's participation in a validator list and update network overlays. - /// - /// Delegates to [`PrivateOverlayOperations::set_validator_list`] for key matching and - /// ADNL setup, then refreshes private and custom overlays **only** when the network - /// layer is fully ready (`network_ready == true`). Overlay updates require the ADNL key - /// to be loaded into the ADNL stack first, which is why they happen here rather than - /// at the call site. async fn set_validator_list( &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result { - let outcome = + ) -> Result>> { + let key = self.validator_network().set_validator_list(validator_list_id, validators).await?; - if matches!(&outcome, ValidatorListOutcome::Selected { network_ready: true, .. }) { - let state = self.load_last_applied_mc_state().await?; - let config = state.config_params()?; - self.overlays_router()?.update_private_overlays(config).await?; - self.overlays_router()?.update_custom_overlays(None).await?; - } - Ok(outcome) + // Private overlays updated here, because we don't have the needed keys in adnl + // before set_validator_list call. + let state = self.load_last_applied_mc_state().await?; + let config = state.config_params()?; + self.overlays_router()?.update_private_overlays(config).await?; + + // Update custom overlays as well, because some of them can become active with new keys + self.overlays_router()?.update_custom_overlays(None).await?; + Ok(key) } fn activate_validator_list(&self, validator_list_id: UInt256) -> Result<()> { @@ -163,19 +122,6 @@ impl EngineOperations for Engine { self.validation_status() } - fn get_validator_key_bindings(&self) -> Result> { - let keys = self.network().config_handler().get_actual_validator_keys()?; - Ok(keys - .into_iter() - .map(|k| ValidatorKeyBinding { - election_id: k.election_id, - validator_key_id: k.validator_key_id, - validator_adnl_key_id: k.validator_adnl_key_id, - expire_at: k.expire_at, - }) - .collect()) - } - fn set_validation_status(&self, status: ValidationStatus) { self.set_validation_status(status) } @@ -393,26 +339,6 @@ impl EngineOperations for Engine { self.db().drop_validator_state(LAST_ROTATION_MC_BLOCK) } - fn load_destroyed_session_ids(&self) -> Result> { - match self.db().load_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS)? { - Some(data) => deserialize_destroyed_session_ids(&data), - None => Ok(Vec::new()), - } - } - - fn save_destroyed_session_ids(&self, ids: &HashSet) -> Result<()> { - if ids.is_empty() { - self.db().drop_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS) - } else { - let data = serialize_destroyed_session_ids(ids); - self.db().save_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS, &data) - } - } - - fn clear_destroyed_session_ids(&self) -> Result<()> { - self.db().drop_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS) - } - fn save_block_candidate( &self, session_id: &SessionId, @@ -747,16 +673,6 @@ impl EngineOperations for Engine { Ok(state) } - async fn store_state_update( - &self, - handle: &Arc, - state_update: Cell, - ) -> Result<()> { - self.db().store_state_update(handle, state_update).await?; - self.shard_states_awaiters().shunt_async(handle.id(), self.load_state(handle.id())).await?; - Ok(()) - } - async fn store_zerostate( &self, state: Arc, @@ -1258,10 +1174,6 @@ impl EngineOperations for Engine { ) -> Result<()> { self.update_public_overlays(keyblock_id, config).await } - - fn is_archival_mode(&self) -> bool { - self.db().is_archival_mode() - } } async fn redirect_external_message( @@ -1286,7 +1198,3 @@ async fn redirect_external_message( fail!("External message is not properly formatted: {}", message) } } - -#[cfg(test)] -#[path = "tests/test_engine_operations.rs"] -mod tests; diff --git a/src/node/src/engine_traits.rs b/src/node/src/engine_traits.rs index 6f6ca39..40d2e85 100644 --- a/src/node/src/engine_traits.rs +++ b/src/node/src/engine_traits.rs @@ -76,59 +76,13 @@ pub struct EngineAlloc { pub validator_sets: Arc, } -/// Config-level binding of a validator key to an election. -/// -/// Each entry represents the `(election_id, validator_key, adnl_key)` tuple -/// stored in the node configuration. `election_id` is the primary key โ€” -/// at most one binding must exist per election. -#[derive(Debug, Clone)] -pub struct ValidatorKeyBinding { - pub election_id: i32, - pub validator_key_id: String, - pub validator_adnl_key_id: Option, - pub expire_at: i32, -} - -/// Outcome of [`PrivateOverlayOperations::set_validator_list`]. -/// -/// Models the result of checking whether the local node belongs to a given validator list. -/// -/// # C++ counterpart -/// -/// C++ uses `get_validator()` (`manager.cpp`) which returns a `PublicKeyHash` -/// (zero = not a validator). There is no explicit `network_ready` concept in C++ because -/// ADNL identity is always resolvable (falling back to the validator public key hash when -/// `addr` is zero, see `create_validator_group()` in `manager.cpp`). The `network_ready` flag is a -/// Rust-specific extension that decouples validator membership from ADNL/overlay readiness. -/// Rust still records validator membership immediately, while overlay activation is retried -/// until the network layer finishes loading the ADNL key. -/// -/// # Variants -/// -/// - `Selected { key, matching_keys, network_ready }` -- local node's public key is in the -/// validator set. `key` is the first selected local key used for network setup, while -/// `matching_keys` preserves all local matches in C++ `temp_keys_` order so shard subsets -/// can still choose the right local validator key. `network_ready` is `true` when the -/// corresponding ADNL key and overlay infrastructure are operational; `false` when the -/// pubkey matched but ADNL setup is still pending. -/// - `NotValidator` -- no local key matches the validator set. -#[derive(Debug)] -pub enum ValidatorListOutcome { - Selected { - key: Arc, - matching_keys: Vec>, - network_ready: bool, - }, - NotValidator, -} - #[async_trait::async_trait] pub trait PrivateOverlayOperations: Sync + Send { async fn set_validator_list( &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result; + ) -> Result>>; fn activate_validator_list(&self, validator_list_id: UInt256) -> Result<()>; @@ -197,13 +151,6 @@ pub trait EngineOperations: Sync + Send { unimplemented!() } - /// Return all `(election_id, validator_key, adnl_key)` bindings known to this node. - /// - /// Used by the validator manager to display and verify key uniqueness per election_id. - fn get_validator_key_bindings(&self) -> Result> { - unimplemented!() - } - fn set_validation_status(&self, status: ValidationStatus) { unimplemented!() } @@ -240,7 +187,7 @@ pub trait EngineOperations: Sync + Send { &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result { + ) -> Result>> { unimplemented!() } @@ -347,15 +294,6 @@ pub trait EngineOperations: Sync + Send { fn clear_last_rotation_block_id(&self) -> Result<()> { unimplemented!() } - fn load_destroyed_session_ids(&self) -> Result> { - unimplemented!() - } - fn save_destroyed_session_ids(&self, _ids: &HashSet) -> Result<()> { - unimplemented!() - } - fn clear_destroyed_session_ids(&self) -> Result<()> { - unimplemented!() - } fn save_block_candidate( &self, session_id: &SessionId, @@ -636,13 +574,6 @@ pub trait EngineOperations: Sync + Send { ) -> Result> { unimplemented!() } - async fn store_state_update( - &self, - handle: &Arc, - state_update: Cell, - ) -> Result<()> { - unimplemented!() - } async fn store_zerostate( &self, state: Arc, @@ -998,10 +929,6 @@ pub trait EngineOperations: Sync + Send { ) -> Result<()> { Ok(()) } - - fn is_archival_mode(&self) -> bool { - false - } } #[async_trait::async_trait] diff --git a/src/node/src/full_node/apply_block.rs b/src/node/src/full_node/apply_block.rs index 8a58f03..f55e74e 100644 --- a/src/node/src/full_node/apply_block.rs +++ b/src/node/src/full_node/apply_block.rs @@ -37,7 +37,7 @@ pub async fn apply_block( check_prev_blocks(&prev_ids, engine, mc_seq_no, pre_apply, recursion_depth).await?; if !handle.has_state() { - store_state_update(handle, block, &prev_ids, engine).await?; + calc_shard_state(handle, block, &prev_ids, engine).await?; } set_prev_ids(handle, &prev_ids, engine.deref())?; if !pre_apply { @@ -92,87 +92,75 @@ async fn check_prev_blocks( Ok(()) } -// Normal mode - gets prev block(s) state and applies merkle update from block to calculate new state -// Archival mode - just saves state update from block, without applying it -pub async fn store_state_update( +// Gets prev block(s) state and applies merkle update from block to calculate new state +pub async fn calc_shard_state( handle: &Arc, block: &BlockStuff, prev_ids: &(BlockIdExt, Option), engine: &Arc, -) -> Result<()> { +) -> Result<(Arc, (Arc, Option>))> { let block_descr = fmt_block_id_short(block.id()); - log::debug!("({}): store_state_update: block: {}", block_descr, block.id()); + log::debug!("({}): calc_shard_state: block: {}", block_descr, block.id()); - if engine.is_archival_mode() { - log::debug!("({}): store_state_update: store_state_update: {}", block_descr, handle.id()); - engine.store_state_update(handle, block.block()?.read_state_update()?.new).await?; - log::debug!( - "({}): store_state_update: store_state_update: {} done", - block_descr, - handle.id() - ); - } else { - let prev_ss_root = match prev_ids { - (prev1, Some(prev2)) => { - let ss1 = engine.clone().wait_state(prev1, None, true).await?; - let ss2 = engine.clone().wait_state(prev2, None, true).await?; - let root = ShardStateStuff::construct_split_root( - ss1.root_cell().clone(), - ss2.root_cell().clone(), - )?; - root - } - (prev, None) => { - let ss = engine.clone().wait_state(prev, None, true).await?; - ss.root_cell().clone() - } - }; + let (prev_ss_root, prev_ss) = match prev_ids { + (prev1, Some(prev2)) => { + let ss1 = engine.clone().wait_state(prev1, None, true).await?; + let ss2 = engine.clone().wait_state(prev2, None, true).await?; + let root = ShardStateStuff::construct_split_root( + ss1.root_cell().clone(), + ss2.root_cell().clone(), + )?; + (root, (ss1, Some(ss2))) + } + (prev, None) => { + let ss = engine.clone().wait_state(prev, None, true).await?; + (ss.root_cell().clone(), (ss, None)) + } + }; - let merkle_update = block.block()?.read_state_update()?; - let block_id = block.id().clone(); - let engine_cloned = engine.clone(); + let merkle_update = block.block()?.read_state_update()?; + let block_id = block.id().clone(); + let engine_cloned = engine.clone(); - let block_descr_clone = block_descr.clone(); - let ss = tokio::task::spawn_blocking(move || -> Result> { - let now = std::time::Instant::now(); - let cf = engine_cloned.db_cells_factory()?; - let cl = engine_cloned.db_cells_loader()?; - let (ss_root, _metrics) = - merkle_update.apply_for_ex(&prev_ss_root, &cf, cl.deref()).map_err(|e| { - error!( - "Error applying Merkle update for block {}: {}\ - prev_ss_root: {:#.2}\ - merkle_update: {}", - block_id, e, prev_ss_root, merkle_update - ) - })?; - let elapsed = now.elapsed(); - log::debug!( - "({}): TIME: store_state_update: applied Merkle update {}ms {}", - block_descr_clone, - elapsed.as_millis(), - block_id - ); + let block_descr_clone = block_descr.clone(); + let ss = tokio::task::spawn_blocking(move || -> Result> { + let now = std::time::Instant::now(); + let cf = engine_cloned.db_cells_factory()?; + let cl = engine_cloned.db_cells_loader()?; + let (ss_root, _metrics) = + merkle_update.apply_for_ex(&prev_ss_root, &cf, cl.deref()).map_err(|e| { + error!( + "Error applying Merkle update for block {}: {}\ + prev_ss_root: {:#.2}\ + merkle_update: {}", + block_id, e, prev_ss_root, merkle_update + ) + })?; + let elapsed = now.elapsed(); + log::debug!( + "({}): TIME: calc_shard_state: applied Merkle update {}ms {}", + block_descr_clone, + elapsed.as_millis(), + block_id + ); + #[cfg(feature = "telemetry")] + log::debug!(target: "telemetry", "({}): applying Merkle update: \n{}", block_descr_clone, _metrics); + metrics::histogram!("ton_node_db_calc_merkle_update_seconds").record(elapsed); + ShardStateStuff::from_root_cell( + block_id.clone(), + ss_root, #[cfg(feature = "telemetry")] - log::debug!(target: "telemetry", "({}): applying Merkle update: \n{}", block_descr_clone, _metrics); - metrics::histogram!("ton_node_db_calc_merkle_update_seconds").record(elapsed); - ShardStateStuff::from_root_cell( - block_id.clone(), - ss_root, - #[cfg(feature = "telemetry")] - engine_cloned.engine_telemetry(), - engine_cloned.engine_allocated(), - ) - }) - .await??; + engine_cloned.engine_telemetry(), + engine_cloned.engine_allocated(), + ) + }) + .await??; - log::debug!("({}): store_state_update: store_state: {}", block_descr, handle.id()); - engine.store_state(handle, ss).await?; - log::debug!("({}): store_state_update: store_state: {} done", block_descr, handle.id()); - } - - Ok(()) + log::debug!("({}): calc_shard_state: store_state: {}", block_descr, handle.id()); + let ss = engine.store_state(handle, ss).await?; + log::debug!("({}): calc_shard_state: store_state: {} done", block_descr, handle.id()); + Ok((ss, prev_ss)) } // set next block ids for prev blocks diff --git a/src/node/src/internal_db/mod.rs b/src/node/src/internal_db/mod.rs index 6c0d0c9..356ac72 100644 --- a/src/node/src/internal_db/mod.rs +++ b/src/node/src/internal_db/mod.rs @@ -34,28 +34,16 @@ use std::{ #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - archive_shardstate_db::ArchiveShardStateDb, - archives::{ - archive_manager::ArchiveManager, - db_provider::{ArchiveDbProvider, EpochDbProvider, SingleDbProvider}, - epoch::{ArchivalModeConfig, EpochRouter}, - package_entry_id::PackageEntryId, - }, - block_handle_db::{ - self, BlockHandle, BlockHandleDb, BlockHandleStorage, NodeStateDb, BLOCK_HANDLE_DB_NAME, - VALIDATOR_STATE_DB_NAME, - }, - block_info_db::{ - BlockInfoDb, NEXT1_BLOCK_DB_NAME, NEXT2_BLOCK_DB_NAME, PREV1_BLOCK_DB_NAME, - PREV2_BLOCK_DB_NAME, - }, + archives::{archive_manager::ArchiveManager, package_entry_id::PackageEntryId}, + block_handle_db::{self, BlockHandle, BlockHandleDb, BlockHandleStorage, NodeStateDb}, + block_info_db::BlockInfoDb, db::{ filedb::FileDb, - rocksdb::{AccessType, RocksDb, CATCHAINS_DB_NAME, NODE_DB_NAME}, + rocksdb::{AccessType, RocksDb}, }, dynamic_boc_rc_db::{AsyncCellsStorageAdapter, DynamicBocDb}, - shard_top_blocks_db::{ShardTopBlocksDb, SHARD_TOP_BLOCKS_DB_NAME}, - shardstate_db_async::{AllowStateGcResolver, CellsDbConfig, Job, ShardStateDb}, + shard_top_blocks_db::ShardTopBlocksDb, + shardstate_db_async::{AllowStateGcResolver, CellsDbConfig, ShardStateDb}, traits::Serializable, types::{BlockMeta, PersistentStatePartId, PersistentStatePartKey}, StorageAlloc, TimeChecker, @@ -79,17 +67,11 @@ pub const DB_VERSION: &str = "DbVersion"; pub const DB_VERSION_7: u32 = 7; // with block indexes pub const CURRENT_DB_VERSION: u32 = DB_VERSION_7; -pub const SHARDSTATE_DB_NAME: &str = "shardstate_db"; const CELLS_CF_NAME: &str = "cells_db_v6"; const CELLSCOUNTERS_CF_NAME: &str = "cells_db_v6_counters"; -const SHARD_STATE_PERSISTENT_DB_NAME: &str = "shard_state_persistent_db"; -pub const ARCHIVE_STATES_DB_NAME: &str = "archive_states"; -pub const ARCHIVE_CELLS_CF_NAME: &str = "archive_cells_db"; -pub const ARCHIVE_SHARDSTATE_CF_NAME: &str = "archive_shardstate_db"; /// Validator state keys pub(crate) const LAST_ROTATION_MC_BLOCK: &str = "LastRotationBlockId"; -pub(crate) const DESTROYED_VALIDATOR_SESSIONS: &str = "DestroyedValidatorSessions"; #[derive(Clone, Debug)] pub enum DataStatus { @@ -193,59 +175,6 @@ pub struct InternalDbConfig { pub db_directory: String, pub cells_gc_interval_sec: u32, pub cells_db_config: CellsDbConfig, - pub archival_mode: Option, -} - -pub enum StateDb { - Dynamic(Arc), - Archive(Arc), -} - -impl StateDb { - pub fn get(&self, id: &BlockIdExt) -> Result { - match self { - StateDb::Dynamic(db) => db.get(id), - StateDb::Archive(db) => db.get(id), - } - } - - pub fn get_cell(&self, id: &UInt256) -> Result { - match self { - StateDb::Dynamic(db) => db.get_cell(id), - StateDb::Archive(db) => db.get_cell(id), - } - } - - pub fn cells_factory(&self) -> Result> { - match self { - StateDb::Dynamic(db) => db.cells_factory(), - StateDb::Archive(db) => Ok(db.cells_factory()), - } - } - - pub fn create_hashed_cell_storage( - &self, - root: Option<&Cell>, - max_inmemory_cells: usize, - ) -> Result> { - match self { - StateDb::Dynamic(db) => { - Ok(Arc::new(db.create_hashed_cell_storage(root, max_inmemory_cells)?)) - } - StateDb::Archive(db) => { - Ok(Arc::new(db.create_hashed_cell_storage(root, max_inmemory_cells)?)) - } - } - } -} - -impl Clone for StateDb { - fn clone(&self) -> Self { - match self { - StateDb::Dynamic(db) => StateDb::Dynamic(db.clone()), - StateDb::Archive(db) => StateDb::Archive(db.clone()), - } - } } pub struct InternalDb { @@ -256,7 +185,7 @@ pub struct InternalDb { next1_block_db: BlockInfoDb, next2_block_db: BlockInfoDb, shard_state_persistent_db: Arc, - state_db: StateDb, + shard_state_dynamic_db: Arc, archive_manager: Arc, shard_top_blocks_db: ShardTopBlocksDb, full_node_state_db: Arc, @@ -328,39 +257,28 @@ impl InternalDb { allocated: Arc, ) -> Result { let mut cfs_opts = HashMap::new(); - if config.archival_mode.is_none() { - cfs_opts.insert( - CELLS_CF_NAME.to_string(), - DynamicBocDb::build_cells_cf_options(&config.cells_db_config), - ); - cfs_opts.insert( - CELLSCOUNTERS_CF_NAME.to_string(), - DynamicBocDb::build_counters_cf_options(&config.cells_db_config), - ); - } + cfs_opts.insert( + CELLS_CF_NAME.to_string(), + DynamicBocDb::build_cells_cf_options(&config.cells_db_config), + ); + cfs_opts.insert( + CELLSCOUNTERS_CF_NAME.to_string(), + DynamicBocDb::build_counters_cf_options(&config.cells_db_config), + ); let access_type = access_type.unwrap_or(AccessType::ReadWrite); let can_create_db = access_type == AccessType::ReadWrite; - let db = RocksDb::new( - config.db_directory.as_str(), - NODE_DB_NAME, - cfs_opts, - access_type.clone(), - )?; - let db_catchain = RocksDb::new( - config.db_directory.as_str(), - CATCHAINS_DB_NAME, - None, - access_type.clone(), - )?; + let db = RocksDb::new(config.db_directory.as_str(), "db", cfs_opts, access_type.clone())?; + let db_catchain = + RocksDb::new(config.db_directory.as_str(), "catchains", None, access_type)?; let block_handle_db = - Arc::new(BlockHandleDb::with_db(db.clone(), BLOCK_HANDLE_DB_NAME, can_create_db)?); + Arc::new(BlockHandleDb::with_db(db.clone(), "block_handle_db", can_create_db)?); let full_node_state_db = Arc::new(NodeStateDb::with_db( db.clone(), storage::db::rocksdb::NODE_STATE_DB_NAME, can_create_db, )?); let validator_state_db = - Arc::new(NodeStateDb::with_db(db_catchain, VALIDATOR_STATE_DB_NAME, can_create_db)?); + Arc::new(NodeStateDb::with_db(db_catchain, "validator_state_db", can_create_db)?); let block_handle_storage = Arc::new(BlockHandleStorage::with_dbs( block_handle_db.clone(), full_node_state_db.clone(), @@ -370,52 +288,19 @@ impl InternalDb { allocated.storage.clone(), )); - let state_db = if config.archival_mode.is_some() { - let states_db = RocksDb::new( - &config.db_directory, - ARCHIVE_STATES_DB_NAME, - std::collections::HashMap::from([( - ARCHIVE_CELLS_CF_NAME.to_string(), - storage::cell_db::CellDb::build_cf_options( - config.cells_db_config.cells_cache_size_bytes, - ), - )]), - access_type.clone(), - )?; - StateDb::Archive(Arc::new(ArchiveShardStateDb::new( - states_db, - ARCHIVE_SHARDSTATE_CF_NAME, - ARCHIVE_CELLS_CF_NAME, - &config.db_directory, - &config.cells_db_config, - #[cfg(feature = "telemetry")] - telemetry.storage.clone(), - allocated.storage.clone(), - )?)) - } else { - StateDb::Dynamic(Self::create_shard_state_dynamic_db( - db.clone(), - &config, - #[cfg(feature = "telemetry")] - telemetry.storage.clone(), - allocated.storage.clone(), - )?) - }; + let shard_state_dynamic_db = Self::create_shard_state_dynamic_db( + db.clone(), + &config, + #[cfg(feature = "telemetry")] + telemetry.storage.clone(), + allocated.storage.clone(), + )?; let last_unneeded_key_block_id = block_handle_storage.load_full_node_state(LAST_UNNEEDED_KEY_BLOCK)?.unwrap_or_default(); - let db_root_path = Arc::new(PathBuf::from(&config.db_directory)); - let db_provider: Arc = - if let Some(ref archival_config) = config.archival_mode { - let router = Arc::new(EpochRouter::new(archival_config).await?); - Arc::new(EpochDbProvider::new(router)) - } else { - Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())) - }; let archive_manager = Arc::new( ArchiveManager::with_data( db.clone(), - db_root_path, - db_provider, + Arc::new(PathBuf::from(&config.db_directory)), last_unneeded_key_block_id.seq_no(), monitor_min_split, #[cfg(feature = "telemetry")] @@ -428,18 +313,18 @@ impl InternalDb { let db = Self { db: db.clone(), block_handle_storage, - prev1_block_db: BlockInfoDb::with_db(db.clone(), PREV1_BLOCK_DB_NAME, can_create_db)?, - prev2_block_db: BlockInfoDb::with_db(db.clone(), PREV2_BLOCK_DB_NAME, can_create_db)?, - next1_block_db: BlockInfoDb::with_db(db.clone(), NEXT1_BLOCK_DB_NAME, can_create_db)?, - next2_block_db: BlockInfoDb::with_db(db.clone(), NEXT2_BLOCK_DB_NAME, can_create_db)?, + prev1_block_db: BlockInfoDb::with_db(db.clone(), "prev1_block_db", can_create_db)?, + prev2_block_db: BlockInfoDb::with_db(db.clone(), "prev2_block_db", can_create_db)?, + next1_block_db: BlockInfoDb::with_db(db.clone(), "next1_block_db", can_create_db)?, + next2_block_db: BlockInfoDb::with_db(db.clone(), "next2_block_db", can_create_db)?, shard_state_persistent_db: Arc::new(FileDb::with_path( - Path::new(config.db_directory.as_str()).join(SHARD_STATE_PERSISTENT_DB_NAME), + Path::new(config.db_directory.as_str()).join("shard_state_persistent_db"), )), - state_db, + shard_state_dynamic_db, archive_manager, shard_top_blocks_db: ShardTopBlocksDb::with_db( db.clone(), - SHARD_TOP_BLOCKS_DB_NAME, + "shard_top_blocks_db", can_create_db, )?, full_node_state_db, @@ -479,7 +364,7 @@ impl InternalDb { ) -> Result> { ShardStateDb::new( db, - SHARDSTATE_DB_NAME, + "shardstate_db", CELLS_CF_NAME, CELLSCOUNTERS_CF_NAME, &config.db_directory, @@ -491,49 +376,36 @@ impl InternalDb { } pub fn clean_shard_state_dynamic_db(&mut self) -> Result<()> { - match &self.state_db { - StateDb::Dynamic(db) => { - if db.is_gc_run() { - fail!( - "It is forbidden to clear shard_state_dynamic_db while cells GC is running" - ) - } + if self.shard_state_dynamic_db.is_gc_run() { + fail!("It is forbidden to clear shard_state_dynamic_db while cells GC is running") + } - if let Err(e) = self.db.drop_table_force(SHARDSTATE_DB_NAME) { - log::warn!("Can't drop table \"shardstate_db\": {}", e); - } - if let Err(e) = self.db.drop_table_force(CELLS_CF_NAME) { - log::warn!("Can't drop table \"cells_db\": {}", e); - } - let _ = self.db.drop_table_force("cells_db1"); - self.full_node_state_db.put(&ASSUME_OLD_FORMAT_CELLS, &[0])?; + if let Err(e) = self.db.drop_table_force("shardstate_db") { + log::warn!("Can't drop table \"shardstate_db\": {}", e); + } + if let Err(e) = self.db.drop_table_force(CELLS_CF_NAME) { + log::warn!("Can't drop table \"cells_db\": {}", e); + } + let _ = self.db.drop_table_force("cells_db1"); // depricated table, used in db versions 1 & 2 + self.full_node_state_db.put(&ASSUME_OLD_FORMAT_CELLS, &[0])?; - self.state_db = StateDb::Dynamic(Self::create_shard_state_dynamic_db( - self.db.clone(), - &self.config, - #[cfg(feature = "telemetry")] - self.telemetry.storage.clone(), - self.allocated.storage.clone(), - )?); + self.shard_state_dynamic_db = Self::create_shard_state_dynamic_db( + self.db.clone(), + &self.config, + #[cfg(feature = "telemetry")] + self.telemetry.storage.clone(), + self.allocated.storage.clone(), + )?; - Ok(()) - } - StateDb::Archive(_) => { - fail!("clean_shard_state_dynamic_db is not supported in archival mode") - } - } + Ok(()) } pub fn start_states_gc(&self, resolver: Arc) { - if let StateDb::Dynamic(db) = &self.state_db { - db.clone().start_gc(resolver, self.cells_gc_interval.clone()) - } + self.shard_state_dynamic_db.clone().start_gc(resolver, self.cells_gc_interval.clone()) } pub async fn stop_states_db(&self) { - if let StateDb::Dynamic(db) = &self.state_db { - db.stop().await - } + self.shard_state_dynamic_db.stop().await } fn store_block_handle( @@ -806,35 +678,15 @@ impl InternalDb { } let _lock = handle.saving_state_lock().lock().await; if force || !handle.has_saved_state() { - match &self.state_db { - StateDb::Archive(db) => { - let state = state.clone(); - let db = db.clone(); - let state_root = state.root_cell().clone(); - tokio::task::spawn_blocking(move || { - db.put(state.block_id(), state.root_cell().clone()) - }) - .await??; - if let Some(callback) = callback_ss { - callback.invoke(Job::PutState(state_root, handle.id().clone()), true).await; - } - if handle.set_state() | handle.set_state_saved() { - self.store_block_handle(handle, callback_handle)?; - } - } - StateDb::Dynamic(db) => { - let callback = SsCallback::new( - handle.clone(), - self.block_handle_storage.clone(), - callback_ss, - ); - let callback = - Some(Arc::new(callback) as Arc); - db.put(state.block_id(), state.root_cell().clone(), callback).await?; - if handle.set_state() { - self.store_block_handle(handle, callback_handle)?; - } - } + let callback = + SsCallback::new(handle.clone(), self.block_handle_storage.clone(), callback_ss); + let callback = + Some(Arc::new(callback) as Arc); + self.shard_state_dynamic_db + .put(state.block_id(), state.root_cell().clone(), callback) + .await?; + if handle.set_state() { + self.store_block_handle(handle, callback_handle)?; } Ok((state.clone(), true)) } else { @@ -849,62 +701,17 @@ impl InternalDb { callback_ss: Option>, ) -> Result { let timeout = 30; + let callback = + SsCallback::new(handle.clone(), self.block_handle_storage.clone(), callback_ss); + let callback = Some(Arc::new(callback) as Arc); let _tc = TimeChecker::new( format!("store_shard_state_dynamic_raw_force {}", handle.id()), timeout, ); let _lock = handle.saving_state_lock().lock().await; - match &self.state_db { - StateDb::Archive(db) => { - let db = db.clone(); - let id = handle.id().clone(); - let saved = tokio::task::spawn_blocking(move || db.put(&id, state_root)).await??; - if let Some(callback) = callback_ss { - callback.invoke(Job::PutState(saved.clone(), handle.id().clone()), true).await; - } - if handle.set_state() | handle.set_state_saved() { - self.store_block_handle(handle, None)?; - } - Ok(saved) - } - StateDb::Dynamic(db) => { - let callback = - SsCallback::new(handle.clone(), self.block_handle_storage.clone(), callback_ss); - let callback = - Some(Arc::new(callback) as Arc); - db.put(handle.id(), state_root.clone(), callback).await?; - Ok(state_root) - } - } - } - - pub async fn store_state_update( - &self, - handle: &Arc, - state_update: Cell, - ) -> Result<()> { - let timeout = 30; - let _tc = TimeChecker::new(format!("store_state_update {}", handle.id()), timeout); - - let _lock = handle.saving_state_lock().lock().await; - if !handle.has_saved_state() { - match &self.state_db { - StateDb::Archive(db) => { - let db = db.clone(); - let id = handle.id().clone(); - tokio::task::spawn_blocking(move || db.put_update(&id, state_update)).await??; - if handle.set_state() | handle.set_state_saved() { - self.store_block_handle(handle, None)?; - } - } - _ => { - fail!("store_state_update is only supported in archival mode") - } - } - } - - Ok(()) + self.shard_state_dynamic_db.put(handle.id(), state_root.clone(), callback).await?; + Ok(state_root) } pub fn load_shard_state_dynamic(&self, id: &BlockIdExt) -> Result> { @@ -918,7 +725,7 @@ impl InternalDb { fail!("ShardState is not saved for {}", id); } - let root_cell = self.state_db.get(handle.id())?; + let root_cell = self.shard_state_dynamic_db.get(handle.id())?; ShardStateStuff::from_root_cell( handle.id().clone(), @@ -931,7 +738,7 @@ impl InternalDb { pub fn load_cell(&self, id: &UInt256) -> Result { let _tc = TimeChecker::new(format!("load_cell {}", id), 30); - self.state_db.get_cell(id) + self.shard_state_dynamic_db.get_cell(id) } pub fn shard_state_persistent_write_obj( @@ -980,7 +787,7 @@ impl InternalDb { log::info!("store_shard_state_persistent {:x}: already saved", root_hash); } else { let id = handle.id().clone(); - let state_db = self.state_db.clone(); + let shard_state_dynamic_db = self.shard_state_dynamic_db.clone(); let shard_state_persistent_db = self.shard_state_persistent_db.clone(); tokio::task::spawn_blocking(move || -> Result<()> { let root_cell = state.root_cell().clone(); @@ -995,13 +802,13 @@ impl InternalDb { // in memory cells, as we do it while storing part (see store_shard_state_persistent_part). // It means we don't need to pass root cell into the adapter // and can set a zero limit for in-memory cells. - let cells_storage = state_db.create_hashed_cell_storage(None, 0)?; + let cells_storage = shard_state_dynamic_db.create_hashed_cell_storage(None, 0)?; let writer = BigBocWriter::with_params( [root_cell], MAX_SAFE_DEPTH, BocFlags::all(), abort.deref(), - cells_storage, + Arc::new(cells_storage), )?; let arrange_time = now.elapsed(); let cells_count = writer.cells_count(); @@ -1047,7 +854,7 @@ impl InternalDb { log::info!("store_shard_state_persistent_part {}: already saved", id); } else { tokio::task::spawn_blocking({ - let state_db = self.state_db.clone(); + let shard_state_dynamic_db = self.shard_state_dynamic_db.clone(); let shard_state_persistent_db = self.shard_state_persistent_db.clone(); let db_key: PersistentStatePartKey = id.into(); let id = id.clone(); @@ -1081,14 +888,14 @@ impl InternalDb { // and remembers their data in memory. // The adapter does not store the cell (don't keep references), only data. // The maximum number of cells to store in memory is limited - let cells_storage = - state_db.create_hashed_cell_storage(Some(&part), MAX_INMEMORY_CELLS)?; + let cells_storage = shard_state_dynamic_db + .create_hashed_cell_storage(Some(&part), MAX_INMEMORY_CELLS)?; let writer = BigBocWriter::with_params( [part], MAX_SAFE_DEPTH, BocFlags::all(), abort.deref(), - cells_storage, + Arc::new(cells_storage), )?; let arrange_time = now.elapsed(); let cells_count = writer.cells_count(); @@ -1420,21 +1227,6 @@ impl InternalDb { self.block_handle_storage.save_validator_state(key.to_string(), block_id) } - pub fn drop_validator_state_raw(&self, key: &'static str) -> Result<()> { - let _tc = TimeChecker::new(format!("drop_validator_state_raw {}", key), 30); - self.block_handle_storage.drop_validator_state_raw(key) - } - - pub fn load_validator_state_raw(&self, key: &'static str) -> Result>> { - let _tc = TimeChecker::new(format!("load_validator_state_raw {}", key), 30); - self.block_handle_storage.load_validator_state_raw(key) - } - - pub fn save_validator_state_raw(&self, key: &'static str, data: &[u8]) -> Result<()> { - let _tc = TimeChecker::new(format!("save_validator_state_raw {}", key), 30); - self.block_handle_storage.save_validator_state_raw(key, data) - } - pub async fn get_archive_id(&self, mc_seq_no: u32, shard: &ShardIdent) -> Option { let _tc = TimeChecker::new(format!("get_archive_id {mc_seq_no} {shard}"), 30); self.archive_manager.get_archive_id(mc_seq_no, shard).await @@ -1645,12 +1437,7 @@ impl InternalDb { &self, index: Vec<(UInt256, u16)>, ) -> Result { - match &self.state_db { - StateDb::Dynamic(db) => db.create_fast_cell_storage(index), - StateDb::Archive(_) => { - fail!("create_fast_cell_storage is not supported in archival mode") - } - } + self.shard_state_dynamic_db.create_fast_cell_storage(index) } pub fn find_full_block_id(&self, root_hash: &UInt256) -> Result> { @@ -1658,17 +1445,13 @@ impl InternalDb { } pub fn cells_factory(&self) -> Result> { - self.state_db.cells_factory() + self.shard_state_dynamic_db.cells_factory() } pub fn cells_loader(&self) -> Result Result + Send + Sync>> { - let cs = self.state_db.create_hashed_cell_storage(None, 0)?; + let cs = self.shard_state_dynamic_db.create_hashed_cell_storage(None, 0)?; Ok(Arc::new(move |hash| cs.load_cell(hash))) } - - pub fn is_archival_mode(&self) -> bool { - matches!(self.state_db, StateDb::Archive(_)) - } } #[cfg(test)] diff --git a/src/node/src/internal_db/restore.rs b/src/node/src/internal_db/restore.rs index 27c2d82..2f871cc 100644 --- a/src/node/src/internal_db/restore.rs +++ b/src/node/src/internal_db/restore.rs @@ -11,8 +11,8 @@ use crate::{ block::{BlockIdExtExtention, BlockStuff}, internal_db::{ - BlockHandle, InternalDb, ARCHIVES_GC_BLOCK, DESTROYED_VALIDATOR_SESSIONS, - LAST_APPLIED_MC_BLOCK, LAST_ROTATION_MC_BLOCK, PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, + BlockHandle, InternalDb, ARCHIVES_GC_BLOCK, LAST_APPLIED_MC_BLOCK, LAST_ROTATION_MC_BLOCK, + PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, }, shard_state::ShardStateStuff, }; @@ -29,7 +29,7 @@ use std::{ time::Duration, }; use storage::{ - cell_db::BROKEN_CELL_BEACON_FILE, shardstate_db_async::SsNotificationCallback, + dynamic_boc_rc_db::BROKEN_CELL_BEACON_FILE, shardstate_db_async::SsNotificationCallback, traits::Serializable, }; use ton_block::{ @@ -350,9 +350,6 @@ async fn restore( log::info!("Fast restore successfully finished"); return Ok(db); } - if db.config.archival_mode.is_some() { - fail!("Refilling cells db is not supported in archival mode"); - } // If there was broken cell or special flag set - check blocks and restore cells db log::info!("Checking blocks..."); @@ -676,7 +673,6 @@ async fn calc_min_mc_state_id( min_id = id; } else { db.drop_validator_state(LAST_ROTATION_MC_BLOCK)?; - db.drop_validator_state_raw(DESTROYED_VALIDATOR_SESSIONS)?; } } } diff --git a/src/node/src/lib.rs b/src/node/src/lib.rs index a82818e..8cb3936 100644 --- a/src/node/src/lib.rs +++ b/src/node/src/lib.rs @@ -8,7 +8,6 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -pub mod archive_import; pub mod block; pub mod block_proof; pub mod boot; diff --git a/src/node/src/network/catchain_client.rs b/src/node/src/network/catchain_client.rs index 51d7146..e447011 100644 --- a/src/node/src/network/catchain_client.rs +++ b/src/node/src/network/catchain_client.rs @@ -89,7 +89,7 @@ impl CatchainClient { overlay_id, runtime: Some(runtime_handle.clone()), }; - network_context.stack.overlay.add_private_overlay(params, local_adnl_key, &peers, false)?; + network_context.stack.overlay.add_private_overlay(params, local_adnl_key, &peers)?; let consumer = Arc::new(CatchainClientConsumer::new(overlay_id.clone(), catchain_listener)); network_context.stack.overlay.add_consumer(overlay_id, consumer.clone())?; @@ -467,7 +467,6 @@ impl CatchainOverlay for CatchainClient { _sender_id: &PublicKeyHash, _send_as: &PublicKeyHash, payload: BlockPayloadPtr, - _extra: Option>, ) { let msg = payload.clone(); let overlay_id = self.overlay_id.clone(); diff --git a/src/node/src/network/control.rs b/src/node/src/network/control.rs index ab0511b..78478a5 100644 --- a/src/node/src/network/control.rs +++ b/src/node/src/network/control.rs @@ -309,7 +309,6 @@ impl ControlQuerySubscriber { Engine::SYNC_STATUS_START_BOOT => "start_boot".to_string(), Engine::SYNC_STATUS_LOAD_STATES => "load_states".to_string(), Engine::SYNC_STATUS_FINISH_BOOT => "finish_boot".to_string(), - Engine::SYNC_STATUS_SYNC_ARCHIVES => "synchronization_by_archives".to_string(), Engine::SYNC_STATUS_SYNC_BLOCKS => "synchronization_by_blocks".to_string(), Engine::SYNC_STATUS_FINISH_SYNC => "synchronization_finished".to_string(), Engine::SYNC_STATUS_CHECKING_DB => "checking_db".to_string(), diff --git a/src/node/src/network/custom_overlay_client.rs b/src/node/src/network/custom_overlay_client.rs index 1e8023f..77cd4b8 100644 --- a/src/node/src/network/custom_overlay_client.rs +++ b/src/node/src/network/custom_overlay_client.rs @@ -128,9 +128,11 @@ impl CustomOverlayClient { let Some(key) = key else { return Ok(false); }; - let params = - OverlayParams { flags: 0, hops: None, overlay_id: &self.id, runtime: None }; - if let Err(e) = self.overlay_node.add_private_overlay(params, &key, &peers, false) { + if let Err(e) = self.overlay_node.add_private_overlay( + OverlayParams::with_id_only(&self.id), + &key, + &peers, + ) { attempt += 1; if attempt >= 10 { fail!("Error while adding custom overlay \"{}\": {}", self.config.name, e); @@ -439,14 +441,9 @@ impl CustomOverlayClient { )?) .await { - Ok(Some((adnl_addr, quic_addr, key))) => { - log::debug!( - "{}: peer {}: found ip: {adnl_addr:?}, key: {key:x?}", - self.id, - peer - ); - self.overlay_node - .add_private_peers(&local_key, vec![(adnl_addr, quic_addr, key)])?; + Ok(Some((ip, key))) => { + log::debug!("{}: peer {}: found ip: {ip:?}, key: {key:x?}", self.id, peer); + self.overlay_node.add_private_peers(&local_key, vec![(ip, key)])?; } Ok(None) => { log::warn!("{}: find address for {} failed", self.id, &peer); diff --git a/src/node/src/network/fast_sync_overlay_client.rs b/src/node/src/network/fast_sync_overlay_client.rs index 1975f5c..83476d3 100644 --- a/src/node/src/network/fast_sync_overlay_client.rs +++ b/src/node/src/network/fast_sync_overlay_client.rs @@ -97,7 +97,7 @@ impl FastSyncOverlayClient { } pub fn stop(&self) { - log::info!("Stopping fast sync overlay {} {}", self.shard, self.id); + log::debug!("Stopping fast sync overlay {} {}", self.shard, self.id); self.client.delete().ok(); } diff --git a/src/node/src/network/full_node_overlays.rs b/src/node/src/network/full_node_overlays.rs index 3e9ee83..3d99765 100644 --- a/src/node/src/network/full_node_overlays.rs +++ b/src/node/src/network/full_node_overlays.rs @@ -491,9 +491,6 @@ impl FullNodeOverlaysRouter { shard_prefix, )?; if create { - if let Some(old) = self.fast_sync_overlays.remove(&shard) { - old.val().stop(); - } let overlay = create_overlay(&shard).await?; self.fast_sync_overlays.insert(shard, overlay) } else { @@ -504,47 +501,30 @@ impl FullNodeOverlaysRouter { Ok(()) }; - // Delete old overlays if monitor min split changed or we are not a validator anymore if (old_monitor_min_split != new_monitor_min_split) || key.is_none() { - if key.is_none() { - if let Some(old) = self.fast_sync_overlays.remove(&ShardIdent::MASTERCHAIN) { - old.val().stop(); - } - } update_monitor_min_split(old_monitor_min_split, false).await?; } if key.is_none() { self.monitor_min_split_for_fast_sync.store(new_monitor_min_split, Ordering::Relaxed); log::info!("We are not a validator"); - *cur_validators = new_validators.clone(); return Ok(()); } - // Update masterchain overlay if validators_changed { let shard = ShardIdent::MASTERCHAIN; - if let Some(old) = self.fast_sync_overlays.remove(&shard) { - old.val().stop(); - } let overlay = create_overlay(&shard).await?; - self.fast_sync_overlays.insert(shard, overlay); + if let Some(removed) = self.fast_sync_overlays.insert(shard, overlay) { + removed.val().stop(); + } } - - // Create new shard overlays update_monitor_min_split(new_monitor_min_split, true).await?; - self.monitor_min_split_for_fast_sync.store(new_monitor_min_split, Ordering::Relaxed); *cur_validators = new_validators.clone(); Ok(()) } - /// Look up the local ADNL key for the given validator set. - /// - /// Returns `None` both when the node is not a validator and when it is a validator - /// but the ADNL/overlay context is not yet ready (the `network_ready == false` case - /// in [`ValidatorListOutcome`]). Callers must tolerate `None` gracefully. fn try_get_our_key( self: &Arc, validators: &ValidatorSet, @@ -554,11 +534,7 @@ impl FullNodeOverlaysRouter { match self.network.try_get_validator_adnl_key(&val_list_id) { None => { - log::info!( - "No local validator ADNL key for list {:x} (node is either not a validator \ - for this list yet, or validator network context is still not ready)", - val_list_id - ); + log::info!("We are not a validator"); return Ok(None); } Some(k) => Ok(Some(k)), diff --git a/src/node/src/network/liteserver.rs b/src/node/src/network/liteserver.rs index 212af10..8ddecdb 100644 --- a/src/node/src/network/liteserver.rs +++ b/src/node/src/network/liteserver.rs @@ -1656,7 +1656,7 @@ impl LiteServerQuerySubscriber { let result = BlockState { id: block_id, root_hash: state.root_cell().repr_hash(), - file_hash: UInt256::calc_file_hash(&data), + file_hash: state.block_id().file_hash.clone(), data, }; Ok(result) diff --git a/src/node/src/network/node_network.rs b/src/node/src/network/node_network.rs index 682ae38..02e6d9d 100644 --- a/src/node/src/network/node_network.rs +++ b/src/node/src/network/node_network.rs @@ -10,7 +10,7 @@ */ use crate::{ config::{ConfigEvent, NodeConfigHandler, NodeConfigSubscriber, TonNodeConfig}, - engine_traits::{EngineAlloc, PrivateOverlayOperations, ValidatorListOutcome}, + engine_traits::{EngineAlloc, PrivateOverlayOperations}, network::catchain_client::CatchainClient, }; #[cfg(feature = "telemetry")] @@ -28,7 +28,7 @@ use catchain::{ }; use std::{ hash::Hash, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{Ipv4Addr, SocketAddr}, sync::{ atomic::{AtomicI32, Ordering}, Arc, @@ -43,8 +43,6 @@ pub struct NetworkContext { pub stack: Arc, pub catchain_overlay_manager: CatchainOverlayManagerPtr, pub broadcast_hops: Option, - /// Explicit QUIC address from config (None = derive as same_ip:adnl_port+1000) - pub quic_address: Option, #[cfg(feature = "telemetry")] pub telemetry: FullNodeNetworkTelemetry, #[cfg(feature = "telemetry")] @@ -84,38 +82,6 @@ struct ValidatorContext { current_set: Arc>, // zero or one element [0] } -/// Select the local node's entry from the validator list by local-key order. -/// -/// Returns `(Some(node), adnl_missing)` where `adnl_missing` is true when the matched -/// validator's ADNL ID is not among the locally known ADNL keys. This mirrors the C++ -/// `get_validator()` function (`manager.cpp`) which iterates `temp_keys_` and returns the -/// first local key that belongs to the validator set. C++ does not consider ADNL readiness -/// at this layer; the `adnl_missing` flag is a Rust-specific diagnostic for the -/// network-readiness model. -fn select_local_validator_candidate<'a>( - validators: &'a [CatchainNode], - validator_key_ids: &[Arc], - validator_adnl_key_ids: &[Arc], -) -> (Option<&'a CatchainNode>, bool) { - for key_id in validator_key_ids { - if let Some(local_validator) = validators.iter().find(|val| val.public_key.id() == key_id) { - let adnl_missing = !validator_adnl_key_ids.contains(&local_validator.adnl_id); - return (Some(local_validator), adnl_missing); - } - } - (None, false) -} - -fn collect_local_validator_candidates<'a>( - validators: &'a [CatchainNode], - validator_key_ids: &[Arc], -) -> Vec<&'a CatchainNode> { - validator_key_ids - .iter() - .filter_map(|key_id| validators.iter().find(|val| val.public_key.id() == key_id)) - .collect() -} - declare_counted!( struct ValidatorSetContext { validator_peers: Vec>, @@ -163,12 +129,7 @@ impl NodeNetwork { // Initialize QUIC transport (lazy: no endpoint bound until add_key() is called). // Validator ADNL keys are registered when a validator set is activated. let quic = { - let quic = adnl::QuicNode::new( - vec![overlay.clone()], - cancellation_token.clone(), - None, - tokio::runtime::Handle::current(), - ); + let quic = adnl::QuicNode::new(vec![overlay.clone()], cancellation_token.clone()); overlay.set_quic(quic.clone())?; Some(quic) }; @@ -178,13 +139,6 @@ impl NodeNetwork { dht.add_peer(peer)?; } - let default_rldp_roundtrip = config.default_rldp_roundtrip(); - let quic_address = config.quic_address(); - - if quic_address.is_some() { - log::info!("QUIC address set for advertising: {:?}", adnl.ip_address_quic()); - } - let dht_key = adnl.key_by_tag(Self::TAG_DHT_KEY)?; NodeNetwork::periodic_store_ip_addr(dht.clone(), dht_key, None, cancellation_token.clone()); @@ -196,6 +150,8 @@ impl NodeNetwork { cancellation_token.clone(), ); + let default_rldp_roundtrip = config.default_rldp_roundtrip(); + NodeNetwork::find_dht_nodes(dht.clone(), cancellation_token.clone()); let (config_handler, config_handler_context) = @@ -225,7 +181,6 @@ impl NodeNetwork { stack, catchain_overlay_manager, broadcast_hops, - quic_address, #[cfg(feature = "telemetry")] telemetry: FullNodeNetworkTelemetry::new_client(), #[cfg(feature = "telemetry")] @@ -255,7 +210,7 @@ impl NodeNetwork { pub async fn start(&self) -> Result<()> { log::info!( "start network: ip: {}, adnl_id: {}", - self.network_context.stack.adnl.ip_address_adnl(), + self.network_context.stack.adnl.ip_address(), self.network_context.stack.adnl.key_by_tag(Self::TAG_OVERLAY_KEY)?.id() ); self.network_context.stack.start_over_udp_tcp().await?; @@ -297,10 +252,6 @@ impl NodeNetwork { pub async fn stop_adnl(&self) { log::info!("Stopping node network loops..."); self.cancellation_token.cancel(); - if let Some(quic) = &self.network_context.stack.quic { - log::info!("Stopping QUIC..."); - quic.shutdown(); - } log::info!("Node network loops stopped. Stopping adnl..."); self.network_context.stack.adnl.stop().await; log::info!("Stopped adnl"); @@ -427,23 +378,15 @@ impl NodeNetwork { )?) .await { - Ok(Some((adnl_addr, quic_addr, key))) => { - log::info!("peer {}: found ip: {adnl_addr:?}, key: {key:x?}", val.adnl_id); + Ok(Some((ip, key))) => { + log::info!("peer {}: found ip: {ip:?}, key: {key:x?}", val.adnl_id); match full_node_callback { Some(ref callback) => { - adnl.add_peer( - &local_adnl_id, - &adnl_addr, - quic_addr.as_ref(), - &Arc::new(key), - )?; + adnl.add_peer(&local_adnl_id, &ip, &Arc::new(key))?; callback(val.adnl_id.clone()); } None => { - overlay.add_private_peers( - &local_adnl_id, - vec![(adnl_addr, quic_addr, key)], - )?; + overlay.add_private_peers(&local_adnl_id, vec![(ip, key)])?; } } } @@ -505,33 +448,15 @@ impl NodeNetwork { |e| error!("Cannot add validator ADNL key {key_id} for election {election_id}: {e}"), )?; if let Some(quic) = &self.network_context.stack.quic { - let adnl_ip = self.network_context.stack.adnl.ip_address_adnl(); - let quic_addr = if let Some(addr) = self.network_context.quic_address { - addr - } else { - let Some(quic_port) = adnl_ip.port().checked_add(adnl::QuicNode::OFFSET_PORT) - else { - log::warn!( - "QUIC port overflow for ADNL port {}, skipping QUIC key {key_id}", - adnl_ip.port() - ); - return Ok(true); - }; - SocketAddr::new(Ipv4Addr::from(adnl_ip.ip()).into(), quic_port) - }; - let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), quic_addr.port()); - if quic_addr.ip() != IpAddr::from(Ipv4Addr::UNSPECIFIED) - && quic_addr.ip() != IpAddr::from(Ipv4Addr::from(adnl_ip.ip())) - { + let ip_addr = self.network_context.stack.adnl.ip_address(); + let Some(quic_port) = ip_addr.port().checked_add(adnl::QuicNode::OFFSET_PORT) else { log::warn!( - "QUIC configured address {} differs from ADNL IP {}; \ - binding to {} but advertising {}", - quic_addr, - adnl_ip, - bind_addr, - quic_addr + "QUIC port overflow for ADNL port {}, skipping QUIC key {key_id}", + ip_addr.port() ); - } + return Ok(true); + }; + let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), quic_port); match adnl_key.pvt_key() { Ok(pvt_key) => { if let Err(e) = quic.add_key(pvt_key, &key_id, bind_addr) { @@ -557,151 +482,52 @@ impl NodeNetwork { #[async_trait::async_trait] impl PrivateOverlayOperations for NodeNetwork { - /// Check local validator membership, set up ADNL keys, and prepare overlay peers. - /// - /// Flow: - /// 1. Match local public keys against the validator list (`select_local_validator_candidate`) - /// 2. If matched, load or store the ADNL key for the validator's overlay address - /// 3. Fetch peer addresses via DHT; queue missing peers for background resolution - /// 4. Create the `ValidatorSetContext` and register overlay peers - /// - /// Returns `Selected { network_ready: false }` when the pubkey matches but ADNL setup - /// fails -- the caller should retry next round without treating this as non-membership. async fn set_validator_list( &self, validator_list_id: UInt256, validators: &[CatchainNode], - ) -> Result { + ) -> Result>> { log::trace!("start set_validator_list validator_list_id: {validator_list_id:x}"); let validator_adnl_key_ids = self.config_handler.get_actual_validator_adnl_key_ids()?; let validator_key_ids = self.config_handler.get_actual_validator_key_ids()?; - - let local_validators = collect_local_validator_candidates(validators, &validator_key_ids); - let (local_validator, pubkey_matched_but_adnl_missing) = select_local_validator_candidate( - validators, - &validator_key_ids, - &validator_adnl_key_ids, - ); - let Some(local_validator) = local_validator.cloned() else { - log::trace!( - target: "validator_manager", - "set_validator_list {:x}: no local key found among {} validators \ - (local key_ids: {}, adnl_ids: {})", - validator_list_id, - validators.len(), - validator_key_ids.len(), - validator_adnl_key_ids.len() - ); - return Ok(ValidatorListOutcome::NotValidator); + let local_validator = validators.iter().find_map(|val| { + if !validator_adnl_key_ids.contains(&val.adnl_id) { + return None; + } + if !validator_key_ids.contains(&val.public_key.id()) { + return None; + } + Some(val.clone()) + }); + let Some(local_validator) = local_validator else { + return Ok(None); }; - let mut matching_local_keys = Vec::with_capacity(local_validators.len()); - let mut election_id = None; - for validator in &local_validators { - let (validator_key, current_election_id) = self - .config_handler - .get_validator_key(validator.public_key.id()) - .await - .ok_or_else(|| error!("validator key not found!"))?; - if let Some(first_eid) = election_id { - if first_eid != current_election_id { - fail!( - "set_validator_list {:x}: election_id mismatch among matching local \ - keys: first key election_id={}, this key election_id={} (key_id={}). \ - Each election_id must map to exactly one (validator_key, adnl_id) tuple.", - validator_list_id, - first_eid, - current_election_id, - hex::encode(validator.public_key.id().data()), - ); - } - } else { - election_id = Some(current_election_id); - } - matching_local_keys.push(validator_key); - } - let local_validator_key = matching_local_keys - .first() - .cloned() - .ok_or_else(|| error!("validator key not found!"))?; - let election_id = election_id.ok_or_else(|| error!("validator election id not found!"))?; - - if pubkey_matched_but_adnl_missing { - log::warn!( - target: "validator_manager", - "set_validator_list {:x}: public key {} matches local key but ADNL id {} \ - is not in actual ADNL key set ({} keys). Possible config/key-binding issue.", - validator_list_id, - hex::encode(local_validator.public_key.id().data()), - hex::encode(local_validator.adnl_id.data()), - validator_adnl_key_ids.len() - ); - } - let local_validator_adnl_key = match self - .network_context - .stack - .adnl - .key_by_id(&local_validator.adnl_id) - { - Ok(adnl_key) => adnl_key, - Err(e) => { - log::warn!("error load adnl validator key (first attempt): {e}"); - match self - .load_and_store_validator_adnl_key(local_validator.adnl_id.clone(), election_id) - .await - { - Ok(true) => {} - Ok(false) if pubkey_matched_but_adnl_missing => { - log::warn!( - target: "validator_manager", - "set_validator_list {:x}: ADNL key {} not available yet \ - (pubkey matched, network not ready; will retry next round)", - validator_list_id, local_validator.adnl_id, - ); - return Ok(ValidatorListOutcome::Selected { - key: local_validator_key.clone(), - matching_keys: matching_local_keys.clone(), - network_ready: false, - }); - } - Ok(false) => { + let local_validator_key_raw = + self.config_handler.get_validator_key(local_validator.public_key.id()).await; + let (local_validator_key, election_id) = + local_validator_key_raw.ok_or_else(|| error!("validator key not found!"))?; + let local_validator_adnl_key = + match self.network_context.stack.adnl.key_by_id(&local_validator.adnl_id) { + Ok(adnl_key) => adnl_key, + Err(e) => { + // adnl key isn`t stored in two cases: + // 1. First elections. Then make storing adnl key and repeat its load. + // 2. Internal error. In this case the error will be returned + log::warn!("error load adnl validator key (first attempt): {e}"); + if !self + .load_and_store_validator_adnl_key( + local_validator.adnl_id.clone(), + election_id, + ) + .await? + { fail!("can't load and store adnl key (id: {})", &local_validator.adnl_id); } - Err(e) if pubkey_matched_but_adnl_missing => { - log::warn!( - target: "validator_manager", - "set_validator_list {:x}: ADNL key {} load failed for matched \ - validator pubkey (network pending, will retry): {e}", - validator_list_id, local_validator.adnl_id, - ); - return Ok(ValidatorListOutcome::Selected { - key: local_validator_key.clone(), - matching_keys: matching_local_keys.clone(), - network_ready: false, - }); - } - Err(e) => return Err(e), + self.network_context.stack.adnl.key_by_id(&local_validator.adnl_id)? } - match self.network_context.stack.adnl.key_by_id(&local_validator.adnl_id) { - Ok(key) => key, - Err(e) if pubkey_matched_but_adnl_missing => { - log::warn!( - target: "validator_manager", - "set_validator_list {:x}: ADNL key {} still not loadable \ - after store (pubkey matched, network pending): {e}", - validator_list_id, local_validator.adnl_id, - ); - return Ok(ValidatorListOutcome::Selected { - key: local_validator_key.clone(), - matching_keys: matching_local_keys.clone(), - network_ready: false, - }); - } - Err(e) => return Err(e.into()), - } - } - }; + }; let mut peers = Vec::new(); let mut lost_validators = Vec::new(); @@ -712,10 +538,11 @@ impl PrivateOverlayOperations for NodeNetwork { continue; } peers_ids.push(val.adnl_id.clone()); + lost_validators.push(val.clone()); match self.network_context.stack.dht.fetch_address(&val.adnl_id).await { - Ok(Some((adnl_addr, quic_addr, key))) => { - log::info!("addr: {:?}, key: {:x?}", &adnl_addr, &key); - peers.push((adnl_addr, quic_addr, key)); + Ok(Some((addr, key))) => { + log::info!("addr: {:?}, key: {:x?}", &addr, &key); + peers.push((addr, key)); } Ok(None) => { log::info!("addr: {:?} skipped.", &val.adnl_id); @@ -753,17 +580,6 @@ impl PrivateOverlayOperations for NodeNetwork { format!("vaidator set for validator list id {validator_list_id:x}"), )?; - log::info!( - target: "validator_manager", - "set_validator_list {:x}: binding confirmed โ€” election_id={} \ - validator_key={} adnl_key={} peers={}", - validator_list_id, - election_id, - hex::encode(context.validator_key.id().data()), - hex::encode(context.validator_adnl_key.id().data()), - context.validator_peers.len(), - ); - if !lost_validators.is_empty() { self.search_validator_keys_for_validator( local_validator_adnl_key.id().clone(), @@ -809,11 +625,7 @@ impl PrivateOverlayOperations for NodeNetwork { } } log::trace!("finish set_validator_list validator_list_id: {:x}", validator_list_id); - Ok(ValidatorListOutcome::Selected { - key: context.validator_key.clone(), - matching_keys: matching_local_keys, - network_ready: true, - }) + Ok(Some(context.validator_key.clone())) } fn activate_validator_list(&self, validator_list_id: UInt256) -> Result<()> { @@ -968,7 +780,3 @@ impl NodeConfigSubscriber for NodeNetwork { } } } - -#[cfg(test)] -#[path = "tests/test_node_network_validator_list.rs"] -mod tests; diff --git a/src/node/src/network/overlay_client.rs b/src/node/src/network/overlay_client.rs index 9aa3b55..5388cbb 100644 --- a/src/node/src/network/overlay_client.rs +++ b/src/node/src/network/overlay_client.rs @@ -767,14 +767,7 @@ async fn resolve_peer_ips( } log::trace!("{}: resolve_peer_ips: searching IP for peer {}...", ctx.id, peer.key_id()); if let Some(ip) = peer.resolve(ctx.dht_node()).await? { - if let Err(e) = ctx.overlay_node().add_public_peer(&ip, peer.node(), &ctx.id) { - log::warn!( - "{}: resolve_peer_ips: failed to add peer {}, IP {ip} to overlay: {e}", - ctx.id, - peer.key_id() - ); - continue; - } + ctx.overlay_node().add_public_peer(&ip, peer.node(), &ctx.id)?; if ctx.neighbours_manager.add_overlay_peer(peer.key_id().clone()) { log::trace!("{}: resolve_peer_ips: added peer {}, IP {ip}", ctx.id, peer.key_id()); } else { diff --git a/src/node/src/network/tests/test_full_node_overlays.rs b/src/node/src/network/tests/test_full_node_overlays.rs index 6c958b9..a954832 100644 --- a/src/node/src/network/tests/test_full_node_overlays.rs +++ b/src/node/src/network/tests/test_full_node_overlays.rs @@ -232,7 +232,6 @@ async fn test_overlay_client() { .add_peer( this_key.id(), &IpAddress::from_versioned_string("127.0.0.1:5000", None).unwrap(), - None, &peer_key, ) .unwrap() diff --git a/src/node/src/network/tests/test_node_network_validator_list.rs b/src/node/src/network/tests/test_node_network_validator_list.rs deleted file mode 100644 index 466d443..0000000 --- a/src/node/src/network/tests/test_node_network_validator_list.rs +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use super::*; -use ton_block::Ed25519KeyOption; - -fn make_test_key() -> Arc { - Ed25519KeyOption::generate().unwrap() -} - -fn make_validator_node( - public_key: Arc, - adnl_key: Arc, -) -> CatchainNode { - CatchainNode { public_key, adnl_id: adnl_key.id().clone() } -} - -#[test] -fn test_select_local_validator_candidate_matches_pubkey_and_adnl() { - let validator_key = make_test_key(); - let adnl_key = make_test_key(); - let validator = make_validator_node(validator_key.clone(), adnl_key.clone()); - let validator_key_ids = vec![validator_key.id().clone()]; - let validator_adnl_key_ids = vec![adnl_key.id().clone()]; - - let (local_validator, adnl_missing) = select_local_validator_candidate( - std::slice::from_ref(&validator), - &validator_key_ids, - &validator_adnl_key_ids, - ); - - let local_validator = local_validator.expect("local validator should be selected"); - assert_eq!(local_validator.public_key.id(), validator_key.id()); - assert_eq!(local_validator.adnl_id, adnl_key.id().clone()); - assert!(!adnl_missing); -} - -#[test] -fn test_select_local_validator_candidate_matches_pubkey_when_adnl_missing() { - let validator_key = make_test_key(); - let chain_adnl_key = make_test_key(); - let local_adnl_key = make_test_key(); - let validator = make_validator_node(validator_key.clone(), chain_adnl_key.clone()); - let validator_key_ids = vec![validator_key.id().clone()]; - let validator_adnl_key_ids = vec![local_adnl_key.id().clone()]; - - let (local_validator, adnl_missing) = select_local_validator_candidate( - std::slice::from_ref(&validator), - &validator_key_ids, - &validator_adnl_key_ids, - ); - - let local_validator = local_validator.expect("pubkey membership should select the validator"); - assert_eq!(local_validator.public_key.id(), validator_key.id()); - assert_eq!(local_validator.adnl_id, chain_adnl_key.id().clone()); - assert!(adnl_missing); -} - -#[test] -fn test_select_local_validator_candidate_returns_none_without_pubkey_match() { - let validator_key = make_test_key(); - let adnl_key = make_test_key(); - let other_validator_key = make_test_key(); - let validator = make_validator_node(validator_key, adnl_key.clone()); - let validator_key_ids = vec![other_validator_key.id().clone()]; - let validator_adnl_key_ids = vec![adnl_key.id().clone()]; - - let (local_validator, adnl_missing) = select_local_validator_candidate( - std::slice::from_ref(&validator), - &validator_key_ids, - &validator_adnl_key_ids, - ); - - assert!(local_validator.is_none()); - assert!(!adnl_missing); -} - -/// Verifies that selection follows local-key order, not validator-list order. -/// -/// This mirrors C++ `get_validator()` which iterates `temp_keys_` and returns the first -/// local key that is present in the validator set, without ADNL consideration. -#[test] -fn test_select_local_validator_candidate_uses_first_local_key_match() { - let key_a = make_test_key(); - let key_b = make_test_key(); - let adnl_a = make_test_key(); - let adnl_b = make_test_key(); - let local_adnl = make_test_key(); - - let val_a = make_validator_node(key_a.clone(), adnl_a.clone()); - let val_b = make_validator_node(key_b.clone(), adnl_b.clone()); - let validators = vec![val_a, val_b]; - - // Case 1: validator list order is [A, B], but local key order is [B, A]. - // Rust must follow local key order to match C++ temp_keys_ iteration. - let local_key_ids = vec![key_b.id().clone(), key_a.id().clone()]; - let local_adnl_ids = vec![adnl_b.id().clone(), local_adnl.id().clone()]; - - let (selected, adnl_missing) = - select_local_validator_candidate(&validators, &local_key_ids, &local_adnl_ids); - - let selected = selected.expect("should select first local-key match"); - assert_eq!(selected.public_key.id(), key_b.id(), "must follow local key order"); - assert!(!adnl_missing, "selected validator has a ready ADNL key"); - - // Case 2: ADNL readiness still must not change key selection. - let unrelated_adnl = make_test_key(); - let local_adnl_ids_none = vec![unrelated_adnl.id().clone(), adnl_a.id().clone()]; - - let (selected2, adnl_missing2) = - select_local_validator_candidate(&validators, &local_key_ids, &local_adnl_ids_none); - - let selected2 = selected2.expect("should still select first local-key match"); - assert_eq!(selected2.public_key.id(), key_b.id(), "ADNL readiness must not affect key order"); - assert!(adnl_missing2); -} diff --git a/src/node/src/shard_blocks.rs b/src/node/src/shard_blocks.rs index ff2ca81..ef10ed0 100644 --- a/src/node/src/shard_blocks.rs +++ b/src/node/src/shard_blocks.rs @@ -259,11 +259,7 @@ impl ShardBlocksPool { } if last_mc_seq_no != mc_seqno { - log::debug!( - "get_shard_blocks: Given last_mc_seq_no {} is not actual {}", - last_mc_seq_no, - mc_seqno - ); + log::debug!("get_shard_blocks: Given last_mc_seq_no {} is not actual", last_mc_seq_no); fail!("Given last_mc_seq_no {} is not actual {}", last_mc_seq_no, mc_seqno); } else { let mut returned_list = string_builder::Builder::default(); @@ -344,7 +340,6 @@ async fn resend_top_shard_blocks(engine: &dyn EngineOperations) -> Result<()> { Err(e) => { if actual_last_mc_seqno != mc_state.block_id().seq_no { log::trace!("resend_top_shard_blocks: goto next attempt"); - futures_timer::Delay::new(Duration::from_millis(100)).await; continue; } fail!("resend_top_shard_blocks: {:?}", e); diff --git a/src/node/src/sync.rs b/src/node/src/sync.rs index 9a57a22..b1fa501 100644 --- a/src/node/src/sync.rs +++ b/src/node/src/sync.rs @@ -57,9 +57,6 @@ impl ArchiveContext { master + self.shards_count } } - fn has_retryable_shards(&self) -> bool { - self.absent_shards.values().any(|&retries| retries > 0) - } } #[derive(Default, Debug)] @@ -364,7 +361,8 @@ pub(crate) async fn start_sync( Some(Some((seq_no, Err(e)))) => { log::error!( target: TARGET, - "Error while downloading package seq_no {seq_no}: {e}" + "Error while downloading package seq_no {}: {}", + seq_no, e ); download(&mut sync_context, seq_no, None) } @@ -376,8 +374,8 @@ pub(crate) async fn start_sync( fail!("INTERNAL ERROR: sync queue broken") }; sync_context.downloads -= update; - let incomplete = - archive_context.master.is_none() || archive_context.has_retryable_shards(); + let incomplete = archive_context.master.is_none() + || !archive_context.absent_shards.is_empty(); let archive_context = if incomplete { Some(archive_context) } else if seq_no_recv <= last_mc_block_id.seq_no() + 1 { @@ -403,7 +401,8 @@ pub(crate) async fn start_sync( Err(e) => { log::error!( target: TARGET, - "Cannot apply downloaded package for MC seq_no = {seq_no_recv}: {e}" + "Cannot apply downloaded package for MC seq_no = {}: {}", + seq_no_recv, e ); download(&mut sync_context, seq_no_recv, None); None @@ -416,21 +415,6 @@ pub(crate) async fn start_sync( None }; if let Some(mut archive_context) = archive_context { - if !archive_context.has_retryable_shards() - && archive_context.master.is_some() - { - // All absent shard retries exhausted โ€” skip this archive - // and move on rather than looping forever - log::warn!( - target: TARGET, - "Giving up on MC seq_no {seq_no_recv}: \ - shard retries exhausted for {:?}", - archive_context.absent_shards.keys().collect::>() - ); - queue.remove(index); - sync_context.concurrency = max_concurrency; - break; - } let mut msg = format!( "{}, need shards {}", if archive_context.master.is_none() { @@ -440,10 +424,8 @@ pub(crate) async fn start_sync( }, archive_context.need_shards, ); - for (shard, retries) in &archive_context.absent_shards { - msg.push_str( - format!(", shard {shard} absent (retries={retries})").as_str(), - ); + for shard in archive_context.absent_shards.keys() { + msg.push_str(format!(", shard {shard} absent").as_str()); } for shard in archive_context.loaded_shards.keys() { msg.push_str(format!(", shard {shard} loaded").as_str()); @@ -453,7 +435,7 @@ pub(crate) async fn start_sync( "Incomplete archive detected for MC seq_no {seq_no_recv}: {msg}" ); let (_, status) = &mut queue[index]; - if archive_context.has_retryable_shards() { + if !archive_context.absent_shards.is_empty() { archive_context.need_shards = true; } *status = ArchiveStatus::Incomplete(archive_context); @@ -512,21 +494,20 @@ async fn download_archives( tasks.push(task); } if archive_context.need_shards { - let mut scheduled_shards = HashSet::new(); for i in 0..archive_context.shards_count { let shard = ShardIdent::with_tagged_prefix( BASE_WORKCHAIN_ID, ((i as u64) * 2 + 1) << (63 - sync_context.engine.get_monitor_min_split()), )?; - scheduled_shards.insert(shard.clone()); if archive_context.loaded_shards.get(&shard).is_some() { continue; } let retry = archive_context.absent_shards.entry(shard.clone()).or_insert(SHARD_RETRIES); - if *retry == 0 { - continue; + if *retry == 1 { + archive_context.absent_shards.remove(&shard); + } else { + *retry -= 1; } - *retry -= 1; let context = sync_context.clone(); let task = tokio::spawn(async move { let shard = Some(shard); @@ -534,20 +515,6 @@ async fn download_archives( }); tasks.push(task); } - // Decrement retries for absent shards not covered by min_split - // (e.g. shards from a different split depth in the MC block) - for (shard, retries) in archive_context.absent_shards.iter_mut() { - if scheduled_shards.contains(shard) || *retries == 0 { - continue; - } - log::warn!( - target: TARGET, - "Shard {shard} absent but not in min_split layout, \ - decrementing retries ({retries} -> {})", - *retries - 1 - ); - *retries -= 1; - } } if tasks.is_empty() { return Ok((archive_context, 0)); @@ -760,7 +727,7 @@ async fn import_shard_blocks( } }; if let Some(block) = block { - engine.apply_block(&handle, &block, mc_handle.id().seq_no(), false).await?; + engine.apply_block(&handle, &block, mc_seq_no, false).await?; return Ok(id); } } @@ -770,7 +737,7 @@ async fn import_shard_blocks( unapplied blocks. Will try to download it directly" ); absent_blocks.fetch_add(1, Ordering::Relaxed); - engine.download_and_apply_block(&id, mc_handle.id().seq_no(), false).await?; + engine.download_and_apply_block(&id, mc_seq_no, false).await?; Ok(id) }); tasks.push(task) @@ -787,9 +754,7 @@ async fn import_shard_blocks( if !bad_shards.is_empty() { for shard in bad_shards { archive_context.loaded_shards.remove(&shard); - // Use or_insert to avoid resetting retry counter for - // shards that already exhausted their download attempts - archive_context.absent_shards.entry(shard).or_insert(SHARD_RETRIES); + archive_context.absent_shards.insert(shard, SHARD_RETRIES); } let Ok(maps) = Arc::try_unwrap(maps) else { fail!("INTERNAL ERROR: archive master maps are locked") diff --git a/src/node/src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc b/src/node/src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc deleted file mode 100644 index 2e614928ed7e9795793b0e582e3a63a8285a01d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8529 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

E>Ln_xcQT?w{G~vp46o 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∋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

=T-N@$j}nl`hLG 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 diff --git a/src/node/src/tests/static/archives/archive.00000.pack b/src/node/src/tests/static/archives/archive.00000.pack deleted file mode 100644 index c1c06f65270105cae003bfc505babff24e1859cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1185911 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%0Du2-bXP)>pWzV-G7%zZ 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

f(lsi?!f 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

Z4g}#Am@#f1pCm( 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

sgv5~k!2Z8-+5Ks(T8pLDfX%(t5`ww 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

eKa!H^=&4ij_cF6hQHn5fNXfV@c^=6yq=#( ziC-Z+fu)#WVJ9I&j0c3_%^yewAZ7w{ zq@U%AZM$PAe2`A*OH%)it+i)5QWfKmPD<}KxviO6Ii|yQYcc&0uERNKK7aWNGy5D& zKg7;tiJgff7d?_kz5pK9Z}YzfJcvG+34;kIF4G3YOaOm04>q!#C43A2F5Jgn`ux}b z;aItB9h97*+y#?Y^f5EN5>Y}Nt|C`o+%URx+-EL=YrK|&JtLNa=YID!MA-V70stSs z-MhCrOUfzAs-u59$ttQTOKCy|0J;en7%JloRrS>rRrE19WweHdqM;r}Lq$XgMq7{vxrE}WTdKMs9~gMsESrL(udRm zT18DoOyJG=MCBlA`m}c^ zM>S_aNh$>aJRnxePlE>q_&xJG@SqI&lHSd`a$&w4JitryXvo-0hfaL8V#yghO&|}5 zDJp}<9~N$Bf6$YhlZ}!5x@kk+=t^9}P;JAhW785-L86bAlLs^=eud23vuRmzwa1PW zWZJ%bb8dYU=jn!#N*=k}0h}J6%dT&k5g81P>=jHL1&l-FT(d9$~& zlLxEZ5$)jL#y<$;0ipxMlLvWY`jx*S4}?s0&X$se9aeL%BpE#*7P708QEn8sUVfX_ zLeEg_$Z9TmMO}rR{HVfhexP|K`{~0)^nh4sZ?>}9S@j+)ND=GoKOW1*(z@p5jot@+ z0ta6yi#w2wNi3oV8k49`))RK`M@%qV9u>Z!^y4ivKacIDj*z%V9?o`NkboZC^W^bq zq0Tc;NU+#DGNIor?9JwuVI(rX_k67RmGzs~5bOj#YoSA{xwnkSA6W&`5E+} zM>g&0z*8KJ2w5D5C~I$((hHi5=|>y*nY+;K9r?^I4$;B>)xMKs(_?2}TC6YfnLj*s z->TN+xNEN=&0Vaz2nluqpxmsQ;)#2c+Zkzd)UmKz7iJ#_^FIInXwo_7P`tNS$t(|- z*SQl;9Pzy4d*0FtjMZMg&;5ODfLDb-mmQyIQHm50J|1-HwcL*nHhUUhzU$ogQ~SU9M>)zX1nFZ`Q)(~&)%S7asZIcky_ zo_D_CJlj=obLe`efVX(xWM`>-Srz$~5ZY)q;&uYhLaR1ks}IV{+smQ1#wf6bLE9DNI)7I~k{*LL`E$K*|(m12IqnT2Y>4RAgCXL*z8%+~jC-QwkhK2}KKKAY~<0 z2~{gKj`}W*Ce3M@_p~_LLfS8Md~|MfjdbJm91uOQp|7DIVDMsOV=QG-Wr}5HVcx_- z!4kpJ%*w|)%tpm#!{);l%a+g9!ZyXO$L`J^%0AA)z;T*Wg)^M3!)6*Gbyg06z0f@wmgLMcLnLgPa7 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

jqcHb^97;pREV zH$h;J+ergx$GqRG8n6Z5eFB!2P|^d-ACQ+fKBIq@%YlXIRZMaC#=^dF9O+I0rXIJM zclW7-Qjg5PrADX;zR!dzWZS^@aRvBVWPS$e2i2J@sxxuuM-NZLDvXYNb!|uJfu6`f zWNE>re;W_T8~}MVk9CrzK`|hwf1DmT8d4>M7Cj)tE2}0hqbVUOtg0d=DWxVQDJcPj z{t}YH($Z3z8ZzLG1V9BO#U&*)MZh~zQ7Lf|6?IWnDKQmE6)6cR5j9yAbyYPf4QT)v z$Oy~G2&+M60-C~VGMd7`G(c1ofCwTY;GeXLh^m;Hq^5?bsIZ2ln24yfjJT$xu#5%@ zdf?{03=_iaqisj5$<6R0z9u!9`Pe%wCN*=b+~D`j%iqs*qdwrAc-$qTYat5{78J60 zUXzc|UAx)(m5t;uMU0H>M*m^y(%cfJh$+uYkFl%SF^$V;Y_`(_0<|)%7`h&N znU*ovEdowaFVvqH@qEK^?D-QTUvjv?>aJsQ=0XAnANZ;@)ZEXqKKXd5KA5OjRcuj9 z_dF3*{mbp<0Dyb|G5lqOV}l-m6n#i~Am+AL<8SE!)eaS344unq=$iwes@#vE5#5m&l~?4P#W&0W zY=#3!=lpi%C+nTd^GY+!vQlt}$KU5Dy;dVHi`Z|k03(EMH{Nh5cx@ILYNQRu5JYmYnk)J2S-<{V>S~Owao``v5@mb$` zxxz2d1N)DE8c}F%7`m!41LH^v4!SHzGFJpen4oARE=K3z5g5-x-EEdFc^rY3UHj+e&vkn|Fp9ouXRAs{^ZVaj) zYz>d-M8h25x!&qn6YD_Nl|q=EnO^#S`aRFR){TAkcbFQkE5zQFxzogP=irIRms9Uw z@-gL}>#!cE$s5SCV2gX2>`*6jy^;XR&n|o@%mMya^79DBH#m&P?&s)%RJ^zNM);TU zZ{x2M*b?BsU7J{GHx;ta#?aS@+|Ut@?Huy z3S|mgiXw`3ir18Sl=hTwsKTh8P%~3E(%93yrsby%ru|B%OP5SHMjuK4oWX!0one|$ zf^nTmhsmDFg{h9|8S^e?S>^~p5Y)0bveK~LV|~Q>f^`&;wu@rdL$+XcS$0$Q3ieL+ zNe*$2SdMXykDPjd9jN5O;4{000~{P%WK>~7rMy?a>Tn!t5IE5SoT z>_WSRZVL4Yy%u^S94Y)(_@f9+gg_)tR9DnQ^o3ZIxSIGL@ow>HiEv3CDIzHvDTGv> zw4L;%^s-E{tdZ<~*;le}<<83`%Vo-Q$qUQN%72hwR|r;!Q%Fg`?yw;f1nA7;EnXe_KHKiS; zJ+J*)hen4*hetE<%q&A4^|Z-&+5YewKczfs=uo!AC<9!v-TeqozM)2!3rG z+++xl;~!CxK+w=o3f2s1phq_{y9T{s=m+}1^48oBh9P+UB?rk6_;Rrv9uUfT$(h!+vi>mxhbn`vp_ye+9xry% zl=bprbzqG(A6CxeV^WxIP4iu1PQG}=6g)&Xi>#v8!kF;-qrWo*nuD@-!s3R%&k)4l zB1zp~2n=_N?>m;OZbtI;w*B0Lj&65cKkH4%YQfDtuS1i`MZbb760*74)dKhT>*!0{1pnQ=%f zReKMG`2CRe17)bG=r+x#nHaSG)R$BDQ=8*ud_FqQj$IHXc-V1(q~HGZ;U+gs;wzz) z4`A@kBb?oZS(r}Z`w^~nf31v8N*XB$Mq8ynTKYaG6l*2* ziIZj&r)yrZshu#8xw-#hNHfFCTQckenI1+^-?Ev1$z&*p%mesvBOREM@=xc1m06@p zKUbn0JYOHm1OK;G`biY6))?pJV_9_k#vKp37gyEZN~p`nk!bEM>25I|vpdlpR|7Pt zK&8JjOQ3>IhKgM})ecV!CuR8Xn#~lC*;L_e%QMof1k&;3cx|6ymy>Mw_e7U%Zjnv> zr!&;bEU|S#vgj~Px2hV{2hmdL4`06Y7~18|tMpf9MeI)|^eWRX0ILP4JZP%)$&khQ zlNV@PJE+oEx7eTPz@DFCcGZ3TRyjg`CjZ)T_lc9rX3e&4+4vKQRov$lakKwG5`H!JU-N&#-6!2~hs>7X3Rw0#M`&3GGLZ1dvfi zZW@MV2i6}@P+M&T>stkh-{G%$w)Gh~_zr1cF7F@$sO^P6%1j%9ZL?zxu9pyY;qPT7 z{M@2GBGX5H<P540|9y8LmTVRgwJu>hZQu2dkXhJlC zn|pUA0y3fHJOiPH%zuaka5zFiPSdC_MJHD(&~9upZJqxJrp|KOr)_oRYsiS~EeCsF z{%uxi$DJ|A0R!L%Jp`NVs3Qis8uSqC)2rH8!23b7q2S^JHi=Bj7z&qzS@y3yLOj)0 zsD^TgJ|A=(K0X(p6bKWJPX{`GjNY;1ug&XV)q!jzZlz5xmeV_385ZgHDi)t?s61&~ z28;lLx{{|(MLq%2dWjjcx#l4Mhq}IwWfdo$dzvP_3e*fWSUY}Ww1Rx*fmB9&S^26cQ~iX+P+C_|zKPp_HRJX(dL|6$E@6M1WsD2KMPjGGPB^ z+~6Pk>hraCFsY2r^Uvp;UTgNrQ*eeHpKm)ooP%rAjM91` z4$2$}QKvfz76ANlHW+C5{qm8wYp+vavPE%(W2X|`@LPF-rQYs%aEAL4c^)<~)o)8%Vi=KI%70WSnM1#DcK0a-G3sch+-Fmc!8wA;IEOTljtBt*K%K9seFM8M zI=(gCgg|XN+U(VO6m!%;*IZ^iILDfYLxwOxP)i`Ps6a|~FsKN**a>J0fe8j8SYh0~ zN8xAV4&z%LiBtKO%ycTKD80BeXl!Q56r-yJY+a7I?!DRzn5)ty`l7+7^)V&B9J)At zL$NDO+9Y*34| zds3r-59>uF&2a%#>fL+EI^;Jno;fCX5>s-`Br>M4-p>1iINCv|g?Yg1e90uz#wuWSMdDVy_jG|yEg{FicKm0g1=UJ#_x8DBGB!0y1MY;V6};gDd4R<~?~cK;{S zHZZ})V?zmte(X}UYYsK}oeRo}!us+2hdz_MUG^M(J8R*4ZJNl;sC0Vb4w{htLBR%T(!6*ns>lVpd~^_>X?>+jK%=?OL&@XwOCF@3^^ zPpLILdLQ)#JQg4CZWxMZ!}DgH8+*NaUXiaa|9;umk+@<_1*=L|cg`Tm&)Okut#>z` z9uKZj!0$quawFv`2u%0{sv4EtY1ZWO`1DEd*Cz6+qWi2B6y-Bv(sLxR?gQ0Ya=6a@ z$!JQppcLD;H0&+XOBrucPgRSRQ(3~mf~W9_?s*=ORlC4Pt8itgAA&;x0u!J^p|<)K z8s+O$%WKE}YB%lkwD^wio<=O8R`{jF^B%SD$}qV~F6&z$rR2B2{##%Ih&!tA>1kD= z2zsdsp9g#Ri5y-$i*Kb+J8P2>ax5$+*Z8*EX50{%fQ-AY9tMZN1XSRGF#M*B2kEmB zQf=7UDLB`h3dPwc$|t)X959nnJv;tP04bEc147?pv;QQF9BUH2cC(fgGP^`z1h zy~QS5#9~ag`-q1rb)|2QtZpN+H%+|(U_$!?@B{ZDus_g#1a!v`oY1w?8Ta%@$O0kp z7r=x&Yih_lsDCe*aQY);fslv<6P`dxb{|>TLj|MdGprBg!}FxS|2&n0~vC1}I2YZg~;O`g}X{eV4IfxHI` zg%5!VP)_LWgTjYEAXMR9`-V>_z&?#|?}n9wNBwZ54}_6$luhd#YublN&yExG159}N z^>3*P$N~ZM*?$L2096($!z-sWqu!BKRZA68P~S|*(wj_Nm9P0w(>C}CZzu9%Jyf+2 zn2-s;guz#@L2?)ZRg0?XP^B_tzC|X7;SrD=MuGi-_9RFQ5S%cv^T`3?M1L(g?D0bsDz0B?q87bJ3VQ>2L7Z~?vU~nQf&m}fbM=puPnykIKS-;Ny z8f?L;za1$pd=?V*5iva=oa-+enc2Jv-@-rtI<{c?BxwgJ*n$Q|T1&|YgH~hrXhqE4Dp(V5L!?%&`?r> z{DZN&in9$1sZGvuBdT$hAcA@sTW#^V}XIOR)v+SB)KWYU{I3-wkuy z-0Rdj>saMeJ1l(uKxLIxOi<{N6-4}}mtqSyzaYZHzs-)!V+)9gkbo_;S44oni!Bgq zu~p(TA&#$VGAAc&vkSVqc3!QU)|bvp^Nieh_5MZnIMqdLfmq8*i&G+ zR^O@eHR|*|p1g+bo~axUT?bsN7O@4@WQA}ejK{dWN8R+}gV8cqlCTGPtm~}f6lJy^ z3uz0S#}+Q^scmWh0N>`kZ&G1!*8P2Xkt!xvW^yfm&qT@gC*+y)*h2paPSX~Ylk7U~_|pFkg;r0ELW#sX@2SKsd)haSwxkSgw+f0B zG0~rD@ihDzOmiUNCjX&B8c^`hlp!&-E^n>U#O(2+M$J!J6b2H3<5#}Ohj52o(=wx8 zm%igjPg1GHu5e{t&b?)CPXttNwoAXTy(5N|PKp~9-MCdZl4~t4qg0cR?Gba*ZR3*X z-R%Y&`np<5Kb<_hzq3)0IJUsale+3|K(49hJ~zr>l^Ym?ozJ$QMGT}9Vg{#=jY?;l zGmBX}eeRK*?8GL$T%F%)zWu&VR^pmC#j>ox8C}6{PEdm#)E3-`*2|Y4yPZ>)&3b}b z??zS5Q!YH?=e}?-)_ULxN5iZH_w`GmUxoTu)*hM32+&#~dc8BIw)f+*eIj{Rik_Ul z>u^^$Nan;>@keE>#p+>eLO!i~W$2SG$iw(Penu3D&$nd%8e90UiO+o~GWa+d0O*%#63ud-m6Np(jY8X?gV4$7Y_{cW zk!&Z~hS-_drPwpsOF0ZVUU15CZsp?Vs^!LU@8j<0@#QJzndP`^a@RX>Kt08MD>kLtaDLDo?5xGrr33BOjXXQT2Tgbb~2gz3|geb%*99HyD48ZVV#4tmc zDWx4sXO;4l%CVl-1and-UJxPw3C$3JmlO-Wz;2 zq%yo?*le^M&xn73Z#Q-~jxbR)X)@h!nqekrhB4DKvoc$5=4}>W&S!qfVx`4b%Qco8 zEmJKsEwe34E$ggGt+Q=1ZTH&d*$&!?+hOet?8)ru>{IRY96TIm9lgH83$RC^06Cx_ zOE`8XXE!p`Q6PVQZbXE^inXkN-xmVG0H^>pT`~o+5Kk<0BZDH8nln%&Stx(}`nQKg zm>4JE0^ERbc><}#e|~5LH1NMaU>N`{p!;)AAeEKGT8@;#@y}?6KPD3(aLx-avHpT+ zAoL4($-Ou^+6((ip*LUzOn@1%pjczzXUc(-b)^DJegezp!;W@bpcda&rgmIkn8#?948j&trY#H@7>OQ1U<>RP z`G$(tjtsyC3m;gBgAKBv0-p3+oqQ-4k;}+K5VD}Ql z;cK!=g32l7CqvBDwu3G%=xEja>`wu=^P`+NcF3Ks&97eAYjNEJ(pXuEPlCQN4qz=S zE%Dh90dfE#tklG(Lo4zh6(pB62}9ANtofp>`LZ8JSb{oE^gvlRA~_r5$?&f^is zTErlIt?E;Zl4(hejy#|cWs7BX?H?h4GVpZ+f6}48qcTW`B9(R^fG$8Db_NAtw^R=e z1cCbEHya6(Xn0*Oy!eehLGKJifG8}^qN4$j0Tzq!!w-p`VS(rI2ZHPwj`TkcK>SAd z4AavEdO)8P2;zq%#(*RW;)gWG;KwnD?;>Mh1P~D7-w+T36Ce$5DIgXHEbtWk%Y|eX zf@Iw<_;DEGyQCLb0fG*fG#nyj<*}b%3aXiEZlD1) zfgKY0NQNsWMxVo!w1D=~U}OG0*yJ)uZKA<;M0<&JjK1-8HhP$v5Fp(;9x2t(64)UX zek;84_?c9s;-3ER_+fC+AutwnjEV3T8V(rzC}p9j@{vGUWMfW4FNDQl^S@169L-Cm zLFEmnJ=K}Ku-(t*Mt);@(h9etEmfj7smWtHI6h6SeI|x9A*?(dFeC}JNIGbT>f!Pe zPZUZ&K=rIf|3Ji=OhZ#^r!USm-FRH)MenY9_k|=yf$@*B7O)_U?hkxoaGcFB4C0Xv zMG|e%dy7E}$@Q1h7O+4{K4DgwFR z*C=Va9!FJsHu4;v4spD=ttLn8V@=Kx!UYvFDaHXyB#o%M{@>T-*SvCoz}qf6)zpx_O?r$dgdRv$xj{WbFKfx z0mv3G4MI*Uf%^hc0f(d*q_Lq;q#E9|7VEKh-)a<)wfX+-eFJH+TpuiQi}B(E&cz!O zwp?DRS*dY|iKo9dFz@*7=%v@wkhI@wQyk3fLTcuj|fWu{mw(J#Kx5Q{5B{&iE|YEt*S@g{Z-d~Yo`v5i z_#qcjI^r=E2k(GzpQ3#u@c%DY5jx+TfIkMlg0BDIlhd$$`Teg8a|thTSOC6Y9h!Cd z?tNP?R7tJbUm0ZC?8DKJdy3?=7sv_(rT{@;{Q_Ho$m4+@@JG=) zLb`$i1b{%uG+gwu+binSoN*h}*IjF3u-jjzbR?l7tdKXv@A$SF6M?fOpcX`fJjgwWX#>Bv*(V1$%uJ^< zUUK*xs*x-C*sYdd@12lgb$=MYh+=>}pdH2P4L_zLuigh6z(y2hFr3kB0>LQz$|hmz zmdv}_iJnr}Lgu~2>mH^D4L+O1ZMeE;hhWFiY5$_`_hiT9n|{v+w1P4##po=aI}iCd_4 zsioZJ8igB9f$0U}0)&FF1=Ip;23r;2{dCs6Z6hhNfR#HRh9Y)LB~a-4(G?Oh7 zy>IMeK>OJBgh!Ep+K&Y!f+VC|urth;>j4z#ft?^0!Wj(y%@cynxB0yp{rg81EZgqJ z=APXx8W#7Ubgn}-dP)SrIHQCzqaL`d3$lG6UBkr{546not!-^U;uQ_gBnP8|CV(pje9+uH<%mH@We}5>_n;H08(0bD$W=45~MOd zn55Pp9Ekk7YKrzN-P=Cf`m1e%!>(5!2E-P;O4o4iB!AI@V*qKe07jLg^le+O=cy*& zJI}XV`_S5&qR;6gWTp=8)FNq)o2V_rDL@mbn@{@nJdOd#y zppEoucs$-LpSJHymY`w(8cdqls>~2>(_^|_;s%3BX{#Sa#Cs$4gVjaojOuy31e{o` z#zT+=1t7!_3}V0kA))~~ku&=&IQQ!)4R~Lb43vWk(m4%yO$@+!Z~R}b zEGUytwtahRXgNNi^LVIlfjD)X>XXO9bXB_R6|`Rob%;^0w~b|2v$O@ zoRgF_U+jhJJ%vs8rt!P-`%4$9Z+(NFT=#Y>c|yvzQC5;VYc*1YzgU(a8&MzUHG-tq zUu(tq0J-n7p!c_(*qT-+=d`T7QskJt>>zvN2eOwKG1Dw0<8S>G*8eidpRfM|Sbt*m zBX*p=V;i6l%>B258}Pyr&Txw15WkAsT&;+L#S^{@4v#p?~n z+R(f_T_yC(*bSH~8K?r)poVZ-l#)Sy10e6SB_{luDLr-by!@NscU5uWj-UmrLfUtiSz_PNc-aqcJdD{dGun(o_q&Cu{w zD$+UN$sxXYa@+vdKp6=|3{&bEz8>+V>%m%$^7i%u-C?J>{r`dkPqZjwdi8)4Nd;7&K zs+)UH)aTKMu`2BrrwWoL2#Y}_QvY9%a==OdEd=HO-LRy@B+faPZrb!<4A3lELru(w zRWi^UXgU38&&j8sEhCGPJK2QL9H0&vQ<}je66Nr)XK@B_>jXXtPqGuS0k=UtxC3R|pn-%!tK4o~8Q8Z>Ln`p7GKR*V zoL#Fvda^f)+?07&8;(}+@_hFi1aBbL0q%l(D0tBB1Bi5(frn)9-w%*KO}{!gXGS5m z6B$22cRbMDMR=P0&(a;4QHbqC27&H?Gj3!oZYTYSdfLnsb!#0?@)SO)^Yq<=hqqto zJRAI^?R4~MYc*zv*B~KQEFDN%!DH|g8A#jU5hZrSmJPe!bmTu7lkYYq}<&$*F9GqI<`OHC1m1CWS z==-?ECO{YuLd19iI_8_86E*>{D)N~`jF#7s-#v4xHav`y-5@)$e0U_LrqPr8pqoJ? zANhCA19ZW_amDRVCb!?dexd(>-AnM(<5eG)-_(=e{fQxdwbI*XXQPtj$gbVP^Khz; zH}fCY095xwE8xipxH6*<3kukO|1+cq>^mt1hdzk3R!SZe(EKtqN*u1Qck!XLV8p~(gQpL&%p~MYV^PcCf0z&Lfw2&UWgjK04l|V!Ak&H4>Rx@ z2^g=CKLfu$Y9JKu&qa;oerO5Sl_Hl%@Y_3u~boiD_yXCVwwmAxY7JJ{b*|}Mp4_VeeEMGq% z{9%T+;ieVsj+W%BMI(;aysRtr@w>ufU^+_ykUz?J6U>-!OLs|){}lk_k7`USm3+we z(T;-pv;-~cF711C+FuF+J}1*3*`U(Ln)pIHMCu7=aQd-jcf+{yhx7MbzLGwXmVRe(UYT-SrCMFy z);!)L@u-F4v+V~p$Gf$>9vt8AUVwo$_{~+;XYd8gAcNyq7`TYdmWfUHzmp(f7LE*; z{AZEu{$e51ZzQs`d}l#2qYw)VLIC*BvLME+<6|VTAWG6&SQRBrXf&arZm4dIH8Q|x z7;6}qs2O1mpl^kSs+zKzrlyLj65I-_jx|s*QiUEA25>_ajE141x`u(O7FJb7RUO)3 zsH@%(k#8qDm7V{qt<(_yMM^1?d;m~O=gJf zm5Eu5AbOo+p_%cb+;vuAmSXR+mv`20II5zXKm10`w!tBVHHg`Gu=<+5Z0xw9)1BJc zO^-BeHrK6ly=L8$e0!(!tAk&|^Omw84|Mm!;{I(QXPyN?WS0mF@=U0DJMQnYAjDd1 z$3*e^Zb{iw#s`mhUlzx>k;TyZPPH^?(-H>ZqUFzVJsZ&O@f$m!8s|FG&~alXcjte)*sm?VT$=7H0qhvejIa zc^t)Ivr&6%vGt!tgXPWTN4ZOO_y_chzrW=5M&E9p1$nr2H7C#G{m~*X(ZLr3CPKb4 zGh)QO^IbNdy+4wjHRk*iEQqs1Ye!?w;SI_*K1u;~>Q}4x8n3=mMsG-Q;@*drvl3i$ zlEwI%{)(MnBc;$RGs!gtKGu{cTMJAns>{35&&u$liL)Sw%3E7M@e1-5X;I&M+-+EU z!y=#|QKa4khafmfiHVKP&?G}x4)G9NUk8~S-wKt?ag^%zEdff>@=V) zUCGL}+sY*0)(v}kN^RxjX|%Y>g5~d~TK|!h-y_#Akwqg=FoISy(vH zT%cv36{N+`CeS{nlcNi#OQ-9g*P?f&-$I{GpUc3_fMF((#0h$R?d|ofusu-5R}M zy-2+{eSLis{YKm_16>2W!99bQhDVJwjK+;V<74nC_ze6Rd@jBSUv3;|{Ke#wskLdb z=}ps4(>~L8rXQiabj-ZpqTRB=io#0R%EQXnD%4uuTE)8Dy2WOft(a~6cM60IMgDsN z-Cl;-dagcri{`lMD;1Uaqe`6f{84C+y?M2Ec_-D|^AHy4hLc$_{ zZec;_21{l@jd1pG#ha0dc@g#)(eEc%Sm>EiB$SW8xJu2!wd{N-zg$OJN!2U#Zk5oB= zvs293^Pq>5l8STI-SGn|XXP{B9bZ{TDI81_^+{-KaqS(mgzuP0-RVLXQZ!*?CeJQMtze72 zdLEM`bzsbw{e-~h`qoQuU&7AS3+tZ!tgOW6HQ(?Ke==GEyTQXsO?(EkU{FDP60?-t zfCd!{aur^_1+fah0GhU`IJ5h@#G{*hjcyEAQq3rMYHgQ-Hz3%z3`w^0?SKy02w`@hz$J3EF2GPy> zcb?1Ni4)B~a#NtDvUnOh+ZS>Bki-wyR&qf^%+`@B5^{AS; zZ_pU;i!+V70je$MlCFQlNC;e#=$~}f?^KeL5~&CYfhWQ`C_63HeJ|5q__&cE`W6#k z_-Ieia!W`$eWOMcZ5El4AChr9NqrY_OUwK{3AdAhp%E#p$PbC6Wq-LwTK;cHq)p`@ zC-QGdpDh%u2#^RGIg$TrAsG@OS*wisQNxTSBINn^qmxF6m`98Fg{n`g!apEHez~+*MRn0I1Cb27s39RBpmosa1c&~MQt{3Ln&gO+ zhi>0~oo^LRWHy=__ni)bN?|v3(fQY>Bf8v`8cUSDUfv5QvgxRh9km=BP@iqRlo5kG-w zL|nY`YY`1+0xm&1pfPT{M%cv_K5bmJ8adV^!x>Oz=i^2ICP+pzU|myJx+A%e0%qWl zUWw%5;khKRdwGMLX5<$*)DdDSd~nQ-Uw2PlY|3kDu`|669PJ%Djj$dLqGo@3!=2ny%|6M!LUG9S~kzx@?Kai~oM1n*; z*iK~iXNaE65q2vtc9>!nFqwWH@Rr`CM`Qm-2#AmguYqXA2IyTtCcM`77tj@>NR^?Z z1N@&xsQx7r{7f?yE0ra+Kv$e3{0ztfU{mOxxUMyWG)c6hzZiUYvhW*)D5NWHBvbq6 zL%KpRty*yXFQ6-W=JRFshOQNdru;_WXI8r`;}0w_Z4z87LaXQZr-f^p-SLB=I9SK4 z9_!hmYsKPkB5)IdA*U0_{7d*%?idu}7FS6OJlfyAXWwVGut?iHpq|RGvajY+oO?-m ze+RyM4)+Lgy@1;z3$h>cV`~jHxMm%eU3TaROdI(DF8ncnL01F>F0cq(PvXde)}vXw zucattpVFq7y_MjrMbpF+SbW>2GX2Ztuy-ri@6W370P1h96&p4}>_OAMobN0>r^gxn z$85gZr$qRredM2oq-n}yF$XiJo}?~f4^eH{b^Q|rGX6kv&-^E;8RZHEq=~Fz4b({A2JWvbGB4CLguAxuZInX^; zYaUtaJm(eKezB9iLRN2^kLXpg9Nh8662(12kpmWJ6JcEyWPeWS-1NyR*J#uZ!}79& zA1-e;y|G+Av@`3{^jap4Ei}wK%mH);fQ4A*^L2Hle@hZ$XW0{vhoi>nGIdmZ;s;8_ z2R9!&#_GxsYLkz_%^36fth%Xurq@{PLA^EzZqIQ|rckb_=<&v@iN`T)_XD*DEp%>PRUf`wMLN=bh!T75dS89;U=JXxox{Nv{ zC}@*lS;c!*YfEjAveVTX=VI9{J3}yDO=}F-C++eL?tk#IE@G0vG{kffp7ZuYw4*Y# zqqa9I;+jrjnrGpm9Z$L^b3Vo^(mg!ILvz_&#!eCycVyRs>OVeVzJzg0JRasfJ$5I; z@)0{&w0rkJxP#bnqD#J&gr*1j!IxXc9o}kQsyVcN*3{>eU1C%Ir_xW#(xIh6dogPO zbuht`1$u0dtx8_>ZTO8z1j}FF8A0wJ0B?dx1aEK2gKA&f!z$T@3SnFV0f!Q^jCA;# zs3?q9yW;M&$7C&T}>=bZ9tAJpd~n&y#38uF>Obrw={K~mVGn)emCB7LkG_OxEfRP(+(0<_ej55TR2l`bfveY zF?aVV5r)T(7Y8Qn51e~aX!jxE!*)$$VdSttAsa#+j6&+*yGaDGGTPfnbcJ0iwtvSY z(4?^}@Dle2gZ&BSIl!$fP^(Z?OX#1Hr;%QF9f_NbfX=c(Q?Hi(YC(3pb;KEgH{5=n zBb;I`c4}Ok33dP0C6}5+5UU%p_rX?UiX-LOai4pAW-GQ?GvZckaOmxzU44mayqBN=|Ie93z<~Fc8<@;-(0at3&x;c^2t49Fm*F7f z@Q(AAqD;e;C$j?ofL?y_K#>R#z-mREs3@uR8c7O&Xo!2-&CKle)L3!-_{U?42P)T^ zYEADbT&TYDLQME>5<#qb>%<;DUd9&xR6dlwUis?FvU{3h#PnBYgx) z^pO>^rjb)|@7r)ArN}CEq7#2hTv?^cb^Mcj>E~_(IvY88>;9?cWBJ(bN7l>H`@*D|c2JN4=Q`|D-1Rdz3h&d9daqP13K zBQ;)X5`ols)$eKyx0!Eh0wxZ3fNQ!9LktpU*@{DH8bZXDGtrBaznfTP%=$tglfS%Z zgt0H_W7k!C0dm8!*UqIb<9=*yx7fWgfb!Vnv`ieckqk~4$#^Lv@VkuFt8j1wWck-k zB4BV@_GJ@a;FcnMpOU!%hJ0gIjA~`d%<$Q*A@b~9{O68d7e)%UWZ3&|5sC40Uv~i8%P2Ju+mzZ21?3CMyiH}MjBAPuZ%S?F;P`gQZiIh z(lk^z(bP0F#9~cUHC5F#4At<;Dk=u5&mGK%_4NVhG19d|K6+^5#WDKx)RV7s-U_fam zTjR=#_%GOkNmf4AV>^!C5S={aSK5j{U(AUQ3DkaMdFjg?(5`fD_4@L?akcb*`N_%W zbz_sJFP2+M-X3e?mCo8K#bvw9 z$1TXVmiOy{=Le{${dA^wQC*6#RhYhBvgVWM%jmdgf(M5HV?R0PRAE3bm%PGtHCA=~ zW|gI20E8M~75+AzF%JeHGCcwqkZ@oi=C@#gN!$oGYMuXFHR~bUI>=3s2{SPM`CG6_ z5%kL&Y@gGnI<4+^{!y!F^uy2$pAUC8`Ll}dD{@oKKiw*~2nG;qS(g`-3C3fw#@Jnx zn^Y~^TUUGZ)gJVft65ubTJ?+_-?|6}_-KFab$Y3#aFpn?x@|IS6F5`nyH4`oJ zYx~-vn>Yas;GxQv?vg!2CVnz=i)p_6*rIWu$f zDXW9gMhKl%~a$y{LDS*fcS%# zr-kKRC{0<+@B9Q9;3U7pM7u97qoqADboR`>*;Dq`mSWEm@GjGVLwCAS@@G0%O{}5r z|J>QXyBr;<8dxlKV*0?&);FsMsm7TkUZ#>@5^;ocSclst;Qbl*>ov;pC-3B4j(l0U zN1F_5LEf@Oy)g1IL%NPtY(yiiTBnQ7?=f7lL)8&*J?WaK__nM7qlOyJm`9`uK}g`Vq0*(zTy|bvzsK3#0Q)w zV|!g0z1P;^RjvVQYW3Ugg*NTv2isEWvBxqLkoeq6%p~H!B|by+myBQ%aT5ju(oG`l zXfDuvruC(bqaC0#gIGWb-3+}ieKLJMeGC0N22X}aMk>ZUCI_ZwW+~=e=8r5VS@~K0 zSxe9{=q&U*Haa#bHWRi9c5ZeZ_9XT^_IeHt4kwNt&h4Ccxj4AWxxKlc^T_hV@>1~H z^B(7Y!V?^aF~ZM9_KIYRWQ&xF zZV(+MQ9j}w0`fssg&xFAs}$s!pbSs~dX*#j{E6R8ZTF=;Dlcj-bIAmb#n zS|(T~T4tY2mdsU|d$PQ;8)WNcJ7wR?ew9PZ`N~zwGsz3c%gAfWo5(xM`^lFpY*2_% zNKosT58p5&(tF| zL^T;Sxi!T!bF~7rMzvAe=X88@0(Cy%;q~nFJoSb2rS$XltM!}oyY&b3 zNA*ABE*WSW3>kbhL>b;PY&3Gf)8g;qTZ~^$rR>?Q2;>>C^+ z9C;n1z5@b(ViWOmV{cQECXAFAS z+1oo>YP6)ju>-fv#&H<?#$pt3^v4{Rby06sr4#|?g;;~X$1wTscm36AH+6BNo3^h@VZL4#!g+8;^Mg|mk|yf&Xe@4F z?F~|vU<3ol8rxRoh>Pa)t?OyufO~^`*?5e*KS0_@ZyU!lp7`f*C&J#<3+o<`M8^7! z5&%4`w8UpS-%KEgPk9ziAc#+UAWG1D;ub(o8j5)nh^22Rg zU%tMk$yQGM+2%K!f0QgE$H0yJj>l>si8yHe6PWQYm zrEhJr z!d=w?^x4&Y-*5o}+X96Dtbcx|Ti|Pp6-h+~2y6<#z`msf9G9w%fe3uf`@%OH34&uF z3YIN?Gp{CwgSl^kt0LzSVS&#=7bU##?dCF&CipkxxxgIA0ZRfPk|aT#3Sa-N{tqcq7sAM@a}inG z&|%_x7>S>873PNC5jwyTsSU~Kg7APQObIcPSrTlZmB?bSxs;Su$iQv>cG$m*4ODTc z>z%rt^lsDKzWvYFaCWjegv3Ye2`xh^4g)nm3=aB)V8d`iF%BSx1wR~Y3TOxnL%u{T z;ySSqZ2q@7hI19ug=C*+ZptREcnI>(-wYOaS~qU^q-Hv~u7}bklA(!x|1B1z2^9-y z=zuXPVQ}BII&aho9i2E=-j6x=g%#nG%2mG7M#-&@-`fsh3XOFsRQC}c-N7V9f!Pm} z3)%wB=-2P37F5p7J!X3K}>%X_2^zL&&a2ZM)>-FEuu`WihZQa2iR0o40WI z2Y7}8nnj*rF+Be&q5;?=jr9{qL)8B1UyC#VC*X{fg>|NlUP2deyAv==} zHoTlgV!j))6Uk@;tTQq)tVDJq9d1B!at9vBP6jS!ujTfj>0jzmJ}dK3KkY0t;=|!$ zQ75+pzsGGvG8~{#3xf`_lP9!r$U(X}861*gkbgiqUp2h@ZBQ*%^VDqPg9#4nEf-k& z?S}As825(T((EP^_EFk9dvF3kT<1~0zI^ybj?@UbIVK=_p?Uij>2;WrAH2Kk-5>$jXB z7aPlNEjNH~zsfy((`Mz1{rv8R8RR zU$tpwGVh4Sl&a^p453c%T8 ze~{bN>UQh8NC66{tvu1A3@88z-(Sg+HlYSdBTBGm@(>`}u_uX>h|GYV@l z4L&b`_XM8^c-ME{BSNj6lMHMH;V697UUm8bmqd^A%{|N(l$43SS;DSaA;$;Cqpn-8 zj!ba02Z);hnv6l@WK3@G$JbwpVV*V{z39NaO(L`Lj@|j2T6%zn0QG}iAP!AiIp{l}#6aiVpP?Bi zlW;nmmq9%imH6CAa0Aow#^!_qxG@5yfMl>|(P#qF1;n}rCF~Q&!A|HvK&)4Y!49z< znlBLR5bUr&)PWs{8v^V>i*5+eZ~}Id`A-iP3>RQ;Tw=H|4^Du1kO0w%(4H7K{(bLl zP$-#VpT|ObPI5Z0m)%;@Ba3^RMqcQZMTXcR!0K_Yx;|%1>(Z30d~PJ=3ZLHw@q#Wz zwM!eV*D>`cRM0>>*+ghUu^VC_(iIPu^QJk-^p-|5`t~!WGwIyDl45cuUHNs5gHnb^ zFGBB7fuwJ45U`hiD_q-rxnD25k72=;gIG_cgdMdG+Co@XF^*X!9D3fY5#3}(E8F^* znVPyiyywfY-6H<_w@z`G)Xdvzq##BRRkSo5n=Z;K;#FcF8CHk%kISxbq)AH>l2~Q_ z=tbUotwJXuphE@bH$&IGI*z-@AL_nvIUtkgeZwSJ{gro&>8E^pl=S?EY=H_KhKVlLAtFc1H z0JksAg_#ZWifOl1Y&?Ew0 z!v3{M1TndPsY2qI;7M&hMTLx}@F;6d7Wb)v;=?OW3y(6~(T^OEA`w}D?a$*8{knDo z*ZH$Ct0>kdHQC_rV@EPW?717Y>*?2D78CYHjuV`SsS=J8`|nI5h?SGmZ{x+C<(xY+ zzp7+y(~C%%)=pqV^Muulc{88fB=e&3S0fvX8bQ+QuWWwTHF3x+!{7nMg%|Z(4(WP` zXU8Zf+CANWBkQ`T# z4TCF|ef81(NnDA8$#j_)w`s7K;6tvDcNJy)I(DM~&KrJj67d&(SpPA60vs@i!OpR05<#qpU-~^kXki@NP%f^dU9qsDV2&zXw_WPv zY2|E+kZK$QL3IzV!!Ge3GKqkd-_z8&PV&Y?R~3zQ1#je`m&?q%DbPadB2Tr%M9sFJ zVM|5M3|M(caV%E;n@I#QP$L-iT2c>XErH3)T>fOvdQjql>eQfr?Qxs^WD1J1>-iT< zB9Qw3!zK~LB+mAX94FJf;%27z8|4>#(!zaUD~zgAvGo7CH)IsoUE=vOCK0f4S>|lB zR`>auFV7zJ?eA^V!k?iURB#^O4H9JT#QSt?!2G6h!+vKHL9Ebid{l1CGj#%jxffq? zu9@=amYD#$txoPedkk_;I33>Y zOd{YBU9qioR}B4=zO?-2@Ox#qgVol#aNe)>vwoJ)R_aBKLp38u6mB;^;eoN@HzpCp zj@Y3qg>{Vr;NmG~IjOxIN@ZjXUi*(=7z}i^7zbBxz&!c~7>~unMIz$?tF}o*a{GjV zLfxSLT*|h`r921H984`gu6*usUha7%uDjB1p$VK9BgSt`B8XM-YmtYmhUeizwebhZ zoT4u7Obte{1LcWfPhRxs;*#kUzH=U+3kHr~ZxZn&4l#*<{r5jZdcX#b6aM^jN2-efu31A;>H-=`>Bx33>Od_)2bAm$T+(h{Cg*6=r%oea( zvxZ1Vm_7L^x$Q|`$iZ9&d`Ozi!SFv8V;g22QlQ?%oF6UFqa^JGsriEQ|o?8?1 z%jY&~a=h%s#K44>0w8A~4iP~e>V~7y&b$Y{yHor=V;ZJsOzm^`>V=eN3hNa;A8?%t z$!`g_x%$?4RxK!hn?`<9DSot;QSs4er`9P=FB`RY9GU067FyBx#?*$lJQcL z2qfd3BxKCg#hr^9x`Y3^Q|4f7qS4-$R?hshj_P3?6JDvRad)qr>8slqyxTI(`NoJT z$I!*cK}{TO^o3Ql!8TkO(NoPY%#e(cdzT9fC8-(He{p|q-I1zs@4O#l${V?sGkkjt z`JHy0aJ%e(&%~c+&qkEcuA$pEHi{qFSxq5+Wy05fjKS( zKnNLsBTt}7#7LkN*o`zjViEyA7d3l=_`|WI$mdT>Y$3k2l0kR79g1n7C`Ne6uisL{ z!Sqnbw!+9g76v7L5c?|8g5IkpH}-6riRB!v zXJKY~naAS2N6%%MQp`~e}regpRyGFzYDl1~YB zRJT7!{<_moY(1Y#4)^oSe7wW!%>nhQh8SJX%R<>RPXLnH5MnJx zeu{Txs=-B;9n_lXUXClSiO_RX+?5nN?cT!o$G!j0v%pwWQ)04LoW_C$XKc zcl!~#rV<>@EqkwPUq<`BCgAlil)-ce)JL;G8&4QZ+mw)mtW^&L!*l=5&#)n`hY!1~8xFtH7~XTa=XHEelc-&9^`+}>AIvEO)@kizd$_{k znngubh+$)Nx$J{h^SNg)3Z4%!XI!m7yG{gkbCO^hkmxQ&A$fukQ1F2vZ7$017V zLdLnbn{9Ib>|k7PC~{QZ^M;H^+n}fSWZL#UR`I!km(DL6%`l;Eh-8`z?5B|u9cy6h zxKLmyArTg~!&?oB&yR?)A^$D$`5PN@0|o=q*^qRax3qe+xwIeYIO#m;8tC59v(T&1 z+t63jcQg1gGBK7gVVRPd8JJyJ082E>eO6A^J~Rq#gAPI`qqETyY}{-*Y)Nc+Z1wCK z>`v@G9NRhWa&mB%b9r+;=a%J;<)PrQ=Q+;vhBuY>8Q%)Ni+rE>ji6b?N`Xj$M1eMe zF+qJncflONdLcF;jF64cjBu`qtB9{im`JRsmT1wk05MUqPO&L*2k`*$Itf9EO%e$b z{Su!c9kN=oL<%h>C8Y=1kO^s&^m6H3=}PHV=|LGCnY}XoGUKv9mRWYSY^iLWY^&^m z>=!u;Id8cQa@*xnRq)YwKR2Zbyf8ijYv%&%}tuyv`|{L+Gg4-v}bfG zbZ+UI>#oq9(WBGj)H|-1tyikQRX+wth2zFa;8bvWI5V7s0lC3JLnA{Q!xct+Mq);} z_*KRt#xll*#t%$(nTndeG<|0lVisqXW|n1^YgS@bX}-~X&Z5B5-twB|Ez5q(5z84X zIxB8#I_nvm5!)U+GCN&63p+P^9(z&yZ2LNg&5oRoTZytEKQ|^WvLS>)ae)mX7UYQq zZ)8~f&W8N$fe|LQlnwbe2FH0e<1z=QTxwkiGa?*21+Jwm%0=01%|${AwzLzph#X7*!?jZQr;^Sv%rSf z0XY?CPK$e1)Vl@NsyC!xym_j8=av!nxmI<%cK^s2-k02=19I!mZIW{UcW)02*LcKZ zGb^1)WCz)OB^P2s5&sZEp)0P zE9*8!2Iu4!?0b=dU$?(}<27FlcJKSg)i6!eYz-Q>u=WP&OW2Uwrvn{34~CQ3sJTQQ ztiL`-VONopD)Ay|`^3K9NOP*_#l5N*_U-{YSy_osgTB)v>sV=t&xhvekuX+j;*%l* zJ#vVRi}=I{(j!|{(*bf?QNZ>e1|!JijQT|9Y0S!aP7kVfW6Zg7ZM5Ey;dz1jmy8)o z{Fkq`JpBf9%)>?kf7YRvz(%k`kqUFngF^DKGbjMN->9$!q1b!jn~n1-Y#|`L_>IH7 zzFGtV9(Lbh4jEwabNXsKuxyFG8X_WpQ(tXIP=4z}$#L;tV-gtIA5$?){CX9$)ZbAt z8$t5Lk?vp7Cz}9icvHcQ^}P#Gj;b>Z`WZPy5_3tnTqMUUG*aet_AlY?+fXEVEt=l{wV)Me{ z%=<9A)F8KwSFY3#;=AvqtYZi)XE2|<#A{vpMgfp5M1{?@_JA9xEr|-5yoxd0kbcyh zXJtrcGgK*9VMR1vxp!|I?cI274H#5o7(LGJ~>_1I6Eb!%OhVS;9%kcLDEOI{X zy&9``_7$B54FABZLzExb0Ubi@V7#Nc1wi3|!4JodcdP_9K? z>lc;J?P?d&IIg!j}5OQ(@1cM7w z*bJmMK~XKtX(ez+%^MLBMi>tWYvkaNpJ9^T$Qo+hF>tou>1V67VT|kDy{gCfAd~g8 zzVG(5JD=&YMjvi;W4)XA-dLgFJwLf?{eh&?w^%kB`7`A*GU*@bBnM8ukvqk8-)Z^( z9s#lntVRfswZIoS_-h~oQe2hk@_c_r{C8fl%|goo^e-~N;U$tC;@V77n^SKzM43)exLyZ0im zj9mDQ0uG$yP37+%)bd5e&!~IvrsN9sj{`j9HKsGPrD+2QqOz+-(1+GQTy+YfO z=!~(rKRN~g7OkUP$h|~#kikhim(c9R-vr=qym!{HKElB#s(xkb*|U~X{RpH_%NXV+ zeO57Y!mJHd!mF2?ETy!rdo%ynVXSjM`M|}YaX`Z?-!#gC0Q!)=AYZ@An*|{Bh)q@I zMrG%ObLq)t>rd@+yH0+dd_Oj5&4KLm4AY}ddwsdWDX6Q?#o)j`kO?>5we6tD`K={P zLd6GB788@?H8qnbHS`908&=qkzP@c=NC5#mfCC3XF3P0pQ2%zv%m^y6@mG{#S7k(G zVq>)KZ_1KxjQNmpBE)o8D7^cy3*@6{=S#K$AT}L=HLnjRTiPAi!2BvE05@XyNTBg{ zogseiDhJv16$be>-&mGypaVqND5VXtK2|@s3z#$gQm-qg8TX^r8qSGq{ z#ym2UJr#3T?~r{rR2(Vk$gcrv3Al%G<;v`>-MtQ%$aKyb-xeLawTC`Jo#At^+v&(a z!6!_^D#BDOU^h64vad9&ZPDFw=&@hki-^It;4nLf_|uXHKPIf8bo;Wyq*EQzH5F8_ zXw8ryc)W;_%=07AR)N5eplMNVBZ1@Mfq9h1``#o6NjybIW>?ehP(5BS${K`T$CnOh zzY!d0`b#nYkGwa5hpPSm|Idtl-`8xBeH)Bjc9VT6Nm)YnJtSobsgR__l7uKJDkLOZ z5<;Ryb}CzokR?(2UuTALf4=wUp6R~3@B8z6{LkajoXMQI&h@^o>%6Zs=e(ZhC9gPr zA(KnxqtfA}Lmm3eU*g$A-BUFLx#-l-UUW`|$aQi35)MVck0?3-9RxfCzSbjw!k|nj zl#xg)`exAXbhl%OeKC9zVvC80g3M_E8=J8pUC(rU@u2DkoB22=sNm~WbI_@c&{41@ zo^Ew82mP52#lm|4;{dmZ&eb*nJ^j5+UI01>?{NeUp@cPrBoR6Z(;MKR!WEis7_0-;Y&m~0R8{SqLc7XCE+u1wTfw}`w*padX-&Vt6$sun+L z9))HdHpmLHR^oqIu&bYp!3yboIp=*?J(R+ehJ-qxMzERe9}*~ajNphp4Oa*pxVQ>k z$F0U!Ig%78l@Z~&ve8PX;)4s_a-Q&YqYK()6W46J_&p((cfS7l8fls^N0JL&BFe3W zZ~Xqj@(Em$+IZ*>HUJ+jk7t8Sh&bdBi{(htpmR_*g2K72TqFrJ+UlE3qT6_iS(yf< zK;YsbJ-jAhApz5v`6@?p9?FI;A`!y?k-9*TH2;r7`nKEwo2+0LAdA51q+pKZ1>s@r zyd=3_+u0Hs-h5s^fVf^+hVk3$E5hyf+6_4?eH<%Pv*FS~xUj5pEsU(SFgl?eAQwrx z(VVht=`QQL`>p3S6D!9zIGzXzIo?b|5hH)5<>84-_&$eti78-Q3Q;E~*&+ZxD+Z3C zAI?ITp*&=2ZSwX%UXXaM0blEYgMhEVBA_a$hRAv-+Z_JOA1oBuZU#TMA?ON}4_!qP zxdYcLs1!kDt_g-T{CWd~j*ma6Zw*e(nDfL~%s%>7Re9iu+fMf6%T_5Cv+VQ|Up@>* z5Xe!0_Gd?(dEWc9Ob8Jn<{PeYoT>Zew+*~ijgqB8mqsaPS;zh1V8C%KkL@QQe=UFt zp<-CkbDe;CnyV#V{BR?aBa`G{WqFD0{PW_;bZ`6T;oS#aNiqE;Dg+JIx<6=7_#=2W z!fgWH;6#Un$FEHLB}Rb4L;uZyUB%I}$NZg>zwD5hTlvhkVW8eDWlAj)F8TX71@Yg6 zig58i2Jz#I4_`m6<48(?qA6XtIYSfPA-oXbR)7ALC>8HTThViWyksoGy9d`Hz`4?G zs0=D+N-n+Pdf<`j*L_Lt6>`@GhB;pcB$b|SuUKA*pRKUj_;BFYaU|~<5YRnv3UnWP zSX3a0G&LcZBf%#VYvGP~_-?8TW@swAL_+UFlu zFw%U0+KZOU-Z<4P&`;c9lQA<5^RoOH@%j1;BTndl7YvDii6a5QA9FdPEi!uiyxhBH z(GwX@)vsF6B6Htzkv_h(locY{y@P%=_#;1puL0Z-f)B)xj}tN1j_G#NFe)(jYV<6_31E9vRntp|2J?X4bTGwQ8ynn2x^K?JS`v>6zxQ`k}Ow< z5q~osUNScPengHw=Pa*treKL^Yd@AFX@uvLC(u&@CHJ6lIfJG@GB|kkUXh5bX1zRm zB<(V-xY@R9jl%kS%yaZz3U$9}T%ZVVf|{X+2!sRFLO`WwCxi22E@qO4|qJ8ZD^Qvpjq#7y6V8(wdrcSuAZ)C#pBL8m=}+7SqSs1pI+KZajAesywQActK- zuKfv)WSLk8d*Au*2@jY^o+iRvc?cJ484T2#w*ak_!3Da)vWOv8iBLMC^QCV zjW=+@7>8dbetp(}?L+=t)-e1>vIflAgZllia3oVmmge7*0|sj-#fvk($gR*2h;!iqt8;8nls)_ag~maXCe zN#*|gpRYT7j-}aUi)@B>yq+T=bjNgQ4o~kNlp}h@dvZX7oK%_?_58qL;DM!t8)UnE07YLeuUh#RIDyll$Gb%E%* zC6(F^4sNjKvzVl&xr}XgeD3N1J&@UuIp2Qx?hU*ML zYzySDYY6rbf89o(j{7Tk)&KtP&%XIdGK8vrbT>g{2wGGHSc0JBmE;s+{GZ5gzZ zqO3ATPF_z-M_CRHI1f3Dp1ih}yu3C_4y7xjg;CbVC@U(;$Y9V~y2=3ZP(kbIVwAu( zifBb0MLCqVww#=dG6p~*Xk{H;T@-L5K?8gY7+ozi z9x|jt+T2=E8ZFLzOmx=1@QmqEjnT^OMX1i3=Rfafn7f`-W;&E;6BHImbxS78Xf9)b zs@d2qh2++g8m4DU2ZTbuUc!+f_)7WHWC%7P{-&1^ABpk}V> zUGEyQObp96|1zF~BST;bCQOD5^3mvP{+0~UZq*7w8f2ss?izxx#d5eOw)uO7$QFo8 zUn#gN)u`gnZuwQEE%Hl(dHrh3}g7lZ|!!)I&q7iHhl^7yv+EzMzmOg!r;a$+*SoV zJqB~ueAhl6eY()>LT%aQ!S%+3SCW53iZW~GhcI`D8orZ!n@SatLv%=vp$HnO-M_K% zRdkYFuYqxX+K_JSX@yD~92vqR95Cc0d0BMjxo6xCUt(5oYBXrOG z88Re-P4Q`z^|s+04|nU(C-f8#i1V(pMw{!_7{;YIjNcA(KE({(Ia+ZyPN`5p2&%60 zczw^2a=YUW);7JDcVZ=Y@RK2_Q?UY%DbHx?m5~abUJ`e=ztGjjbjP~PHqP6`E5iNj zRvxaK#5n^8PQ(OVJl58Bn%-==)NwR^n>!zwEw6|fJ0Y$i&EmaNgQfjb{2|pLnnof^ z{pAIU#brK^#5q!24@UWODNoeTbGGq55YYx`qn8|QVp-+g7Lt<=qy&&5 z2i+=H58TWa5Ro zMrwm@z0bQIXe9A_tHb$OUjl-hy#4lg?G}-gkx)YYc9D=#`d`V23P}bjHE9@WGnqeG1vx*tFNF%lE{Z&g z28vgd5G4;~8|7Q7(^S3GZPf2+Y-ze_Eom>(HqcH0LPUrzjjohllYWdLfsvBYiE)Gp z#njDg#e9%ClldJBEsF?CI!hU=HtP_ZINN@9F7|p3O^y>BBb=_Bw>Vce7;X5-rNLFj zO~JjB`!bI|Pc=_J&s$zaUQ6DKyfu7Od?I}N_)hZ;@qO8Njo+Hzl|P6-T0ljhWRs^L zzhJN6qL87`C1IqngK&s&tne!lw8&AB43TM3Qc(v{U(tHe4zW#QL1NKjr^JTE(c;?T z7UIt02gDP_$0SrFPD|uT+?9AJ@j_BaGFY-jibqO9N>xfDa<*~}a?j+3<=&$M8#@~N7$+L18Rr?78rPeYn&z3Mna7(qnvYosSfDI4EWT|e-Z1V>S8FF;jXVQM|XeS15py= zn+ouyc#y(bAV!FZIL=UR;p<ZD^Cn-^r5NsY7lw|h(-4`^+$gYgSPHJ z7V|id>DyA%Sjv!vs-nvAMbzPp&ppQ=QXTld9rn4KUXV8XIW ziI4y2x*fp9Lr9jX{(82J zRP&y;=1Nu#Fe-ryOZ*5k+ zaneEXs`O}g9VnUzc`8S9b?pEptb?UKcmq|_ve0$A(E)y5-<(UPQ!l8l^B0EdmeaFe z@wZv1Tm9DBdKfwZ4?g@L#BVhJ;y|fyiTjHmPV+ATNv{26D^ATX1xbU>`)=!^2$}vl zHNPbQAL!kwkvKKK3?%y()%=zaFm-`+2qA%fPBSk@5DD~CntA!ZqM6?Up#cf>kEr1d z0R>`=C4tz%`1-?zU=oOM1-sHulR)dV>E@8d>d=lQ+DeauP}YWap$#gD9T<&l#?2lC z<1AbB7(q!eGP3RfF!^x+Tqk3nro5=2rjwda(S~R^R!q}GF|etz@7#$P1wSq-B6@Zw z6YRX~uocY(@|!010HA&93#1gfklxQ804PyTEY4EOwFkBuSv>&$-^N*PxYcJsDea~^ zLauOKVcITfpeGkg{>eR#mqTK>)W}|hhJ3g}1RjVWcuELE$f21qI`2*`pRBv4w#b zP$3y-H|c^%fU^Lj5y%*x2nfVkIM)jKZ)>=$Ux>5Nt+-HaIA!GD5s}Et5Jk7+z|tY^ zzQT=4Q_tI`!V@A{;A+5UA)1gGyyst!vw+6>jFTxvlC5B0u z0dqsdD)fT=5&%6`=@f8o9trmlkXF@!AKD%l)gIh!-hE5#n*~ELNBQUTCDJ>sj{Beu zZDgFc2;Hh?uB@%{r`f}a5$eltHs&jfpUA(qty?53J@isZ*ltM+>(yexUa-Z2+f{I} z{=L_ajOhym3s$k)y)%xS5xZieBF%j0TC(uTOVMFw6lJ-ZBd zNb{;c3HU#}ApF5UZABRpQn2>jn<`@u?ozFOMyw9bcAuIQD8>W2M8(JK!gj*w6jfZ& z5OE~<{@mek3Qh3+P#t*}qdU*mK4TaLnoHu%7d#z+KS>Aj4E-b>7hT~&Of3bSN7gX}2L z#7kCOQnp9!L{9draOW@OB2=CXl+0`bNJ!+Pl<|YWRiX!2l*Ewm5|zIbX&E%wAjBnl z6FZ#%fY`2Yn=x)m+BvLm*T&+h~ zF2^YBDs6F_FQg|T2YUOPV8fDOt&eU6%4eSnc-H45+L%+(*IJ3L^?gN8==f52Ovqx#pi$B!2iY3zBy5msR%^Y_J*}hgjh!g-mk-*u5 zf*$K&5&|7yq(E7ZiV1DWJ$R(SLo!qC8Y@-kwB@}%9l^k`ogPa?RgSh$BP`B<0B}h-BhjtI z(wi|=w`u!0>P-n9ht5Ospj2^MS7BMU(_<-j?;Lr-<~u8ur{UBm_q1|i z0lDk+iropEM@RxRv|2$hM88+C8&~%U-0CerZE_FA`D@lvwdiwCg%Nx2x^ciR!uV18 z4YfN$;Wc|R)#%BgH=rb)nk+5*H&I8`yQE8>R`-f*W9zmk;EI?(`^LpqMVMBA4qPSe zgqi>+gKs2@fPvfuod#Rt8_FVJBsW24VWI|%eO!$@;QxUEj{Dz}RpcZUN@GNHB8SV* zk-2WlyNxKX65Y08@4hB>2Awb+p4pS#J?@rqFmiGUy8KI#lPLWTEOK%Vpg$MaNR#tx zZ?XtvHNYhpdR*?s`wAl`k*ml_26Pe1fsqr~{G&j0#XV>7Q%x>i{Ovb0-WnCo;tVIP zCO7mOIMCE=z0UC7A*oTT!gE58gmfv;mBplYa=aO4F1 zB!*z%1OSgPa#9N2#tK{uu&*~k==e~D?zxy&v@2_a?lv3oJs0Bb@ZTTQ3F6CMBwn+{z;_xjk%-C|kIhs7S zpMX-f5U?gCuu>LAWAN3}t=AVF7S?Cjb)2qqsyO_l1mcEh;q1OPwGjn7?K>pN1P#_Y zJfW;ZP8P^<`b5IxSK^f}-Sw$lXr@!$SIO1=;E{R?iW}&_w%uvlr^tOv%#+}qe;=nH zegK@{;_m?Q|t>SL8Pe(9$*|DfKPumGQz+u&bPm=gL{IVzPak+wV>-P3?n_= zC({l`76`jp5{R6DPG=Qc(YVWDZcxE_+enoL^Dt5Q12?)jJSpgt{-!b4+`F3a-~a~) zjGRQm)+IFnIJrwe6|>y-U(>$8tE3i~R;yPRWD``usi^++4P|D_(0;RRd|5aqpb89$ ze+fAO!N(SiwWL?o3r_l&SlG;PEsrbLT_`9=j|C+iyg$C}*u~-1;9=xsHF(%)1_2`{ z_>dBgJ95Kq^lg*;LcwP@HEFkQkvc0G6*GQytfclP4?V+VtQMA5e!?ZhG{P4JuotSAUn=Yz)O z-fQ)OBRk|{EEM|X!!1_zBm7mFs>l1boN#ofO|{F$3NrA00%WETE-?K-yO(-zydUWhp~5g|5oG#)DN~U zDZ(Kq&#_6ThwvV?{&uOaZox*#{q0e?sLeXVRCkAB_r;SR{WvT>c(1VQVKyAe`iZ0q zuqOTRMA{8{6km^V=)S&ko&K@Y(CZWqt{sLayQcCAb59n%dRu(4{>asbZ+<;t@x8kU zZ-UU7s{Z6C%?LkUTh+Q~7Ur)n(vO;F59hw~$26;W>rV`?HUW&BU^4~)PH;^y0Ga?_ zh^QnfG4AqE;^6+2dRLp>OcnW?HUx@hTu>h>FaJ8l=(Y|y83c*rmm?>jL160=Fn<3X z>k=?_4)d2|JXriV$@ae6d6sDRIi-a37*U(cuQMwz+r>My4o9vwFpQi;LLh4Z-~@UF zXN}jOf$=pUzEp=x%Bxvp1OiTye89UDfF}CTTR34%z%Orpeb#^};y;%);{TDX0VDaK ze*Y`T$ux|d{4l5Z1TDhI$r22i%s?OE)hsjzujYYq#RqFZedPZR`SFh+Cuv~Ebr4vv z3_fm6D`)C7+V@n!CsEx<=Wbb2(r!pmNoz4 z%XGb`b$xwb_y>OVcO}(xai~a8&VM2&=f1-gxQKsm#}qanPjnp*E{cpCyf+uLyNj6E z|Hvha(T6v#+>m>IbFNnOk6~L8IUjE@O9rsw{Bh1&j;S?xnCTNqP8Wa z9Sl#;wm2cqJS@J^<}4G%E~SS2MR>>miJV;hv17&BIFVabRaaAi&J{(}nh8t`uvG~*0laRbV{*X#`CkB1Hth?RAP7j27gYfi2?~wUlEYvyDEP7IC@py{ zjIOq}mV%xV08L~R(Q?`fTFN>)C|Nlr9UWO&9bGwHjI4q-3Z*Qot0bqQtE{V}rKkmX z6GbH@Wkr;d5?Tv`QP5LHE64)t5?v)3Wf>iewme2x7Zj-r&=)-&Jv2r^RsnoRSx*}e zka8!?Uo-dH^qyOkyT0Ddn^=4qr0SVbCy-Dt@pMS2h(hvkPxqB zeMWk*_IkSnyN_4udy(7e_T0x=QHXqj1($a0hDr)0Zl9#F(14y{p zKW{i$@sdNQHBI%1k4(p=&icZyQ+mgC;dxBkIv@p{aKlo{zf1+;fD~Bfh6PeQ7>pzT zDv*M&#U9;MX248K%r{Z)l`8F@+}Z2ZFR(@aNk+r#TwbOxN-?0tus{mFmaQ>QH{p01 zd8ng2@1;O{$)gP4+j+ZMmM%R{i5DBbpcVy}1K)~eAkJlqUpaZ^)UloJFE?_U2a$ea ztnlbLV^p}oW5cvy5N@k0AG&%w+S~>i&gx!I-MIYv+sEvI#>Kt6biI1pojXY}II9xv zIWjR3&;1P-!#}o8kKR9M%fc9|ev##4Vq;O5hUrmd9FP*ZA#Ca7HWzEF>#aT_XKIrJ z^*1*Qoa#+@neCFE%r30@Cx8^ILZz`UkD2bI@t3Bm3eRT`Upl}bPO`_-FR62T3YVc| z(byYL{-LoLPsN6c>SL$Bbf@cmnO^>CCv$ZR)hqXaOX~Q6loSP|MN?iga(PDnFqfgL z+~~)xp(Be`5972-ua>cmE-z5;b=4?AwGbuP80W{o9jBPwFu)MXHreNMe(&v3!v;W$ z;Ik@W{TAMwuB-p3K$ZV_tcPhKyVZLpy>7+BE@!{OtS)Fyu{-6uT`7L=f zpGEdOWxYKZ*!6LoN=oshYlbX-Af=1HtM$bU?fwGEk^s7$$4}m%f6)GMIOq5&pPUC7 z$zdZ0&Oo)n+0TfbN(?;1UnMYkZgwAq@u<@F_MtX0W#Q9MA(|4M#-i3HAG z!c7pw1ujyoDdqY`t9 z42lYhFTkiIj53|Fg0h`Tjmna$n>vh!k0zbwBdsrOC7lCZFTF5*0D}sH6+;d~6+;)p zG$Spe1mgk56vjd(P9|BVGUjc}SpZ2%WR+ztWc|!$&eq1x!S2jn%wffm%gM#*!CAh6 zXhRDZ8<#AX8dn@w4%ZYnIrnyMU+!2Q1)jq^k9oRzUh%x=y~M}G_iSSVzb?N$zdwJw zfQUe_K$1X)K*c87P0pKY1gQmg33>}g2qp7;mK|HF zw=``T+46qN0(kN{rT!O#4~D}=PmDeoiy6Ba`x}RwD4M97G??_8o-mU#JNW}bK_H2U z5l7EAT|EX*aK!M-pPL6skPHMln_e|kbP9$a-+Go-5g@61?L)7)tqN@AbPY7zV;$y7W{9HBo%PK z{vW7zl3*ph*`c% z>(Q}!nkcl{?~@jf_(hX&!g@2{L2~i7VrIU?MF(O31k9Ps_P6^YbL@2>SM-63nGN)b z<|Uq9WAFJ}rdX+MMFSMxUa-!PzVR`b!78EY!wIt9n79cbt0yDOgJhiC(3bR#iK*F| zr2bx7LK+kEe+cR$LF158H|$}ta&hIl^t4m)!FkyzJ|Dj$+Hvtsd!{R&zkgTI`D)yE zy`WyuipJ%LMS^jqq1-%)(ZR|4iY;H>AmT&jYOI~C>%aMdA|WDDCt1zawZoBhdiv(6 zGP0&B^-@b?h4bDs*|%od4Fc1=EI`h4<-SQ7n8tbhmNEzo6w4 zxxScmMQ&U%M9j|=&0sx(BQv=F%kcTZWtb7{ID#cABu%8u)(f$D6juN1hgD(o@~!@N zD^^^+k>Bh)S|Dby1{VC3uG$P|FoIQ8Z~AF_5paiXou=AMZL>O%7~{1T`6<;hLCzvS zC0hPzV-XPLnr2xWwiWp&w8(m*KphP{Mes_D2_^{$77zclr3h%BHC3IQ#M|dPnVNWcMXbS_C8sYyJ*~1|*-qR_iPM-A)A7@NUJL ziC8Aigcx@ASH71}vFW!A;4)(`_|kL#V*0@gbtkT1yP^_Sp-nJLvGXVsAG@;3kHcO6$Kn3qIX&&8CEL^I zm2}5n<%me2-EQv6e_7&^2~`H)JfwInFC+webo(s`@j3#XlFASnb> z8-#KdpwuFWo(QK|Br?AdE5Ro7G(pIvQi|=l->~}*)AJlBB2Vc!Kj%J}-_O2UBKSE0 zScO0KVUm+AIV%ruk6l1DXzbFcoitfHv~}P^h#+)gg5Agv`}JRrPPhulIPaP8t7Dti z!)$@;Lm)T7wHnWVUBPAjLT+MXFTb&E`K>ER2I|PY``qftq{OmyZ|>W;p;=m*Lq-hi zlLF4CoHY&M9sdp3gsZ@B6XdUgCb0HRe}b8a((nAWdDI-sf)Gqh0L5D_-_~v5{-3QK z+~fq)5QmHZ)`StU#R6On^+uUbKRq!`bN2u zEh#Nag39f3nDEuPkH(*t4d80wXEETdF2^klm`k?ZsUOd^HvduGfJzcfO$0TfTKqQgCry^_OsSXJiP`VlF>+$LefW z#yh2)QI9x<>;wD7<7(Oai~C~`1LmF(DNH5bDz+lHvOfyu6V_5WSy2k}1>##w_56GWhrcb|MguGM$|6kO34Gnu;{3LH#k* zeGgkGU)&X7eIamr>$O)Z4^JUIpL|X$c`D_U>$`b)Aw%7f%H^KAJwiwjwjE9&2-Qot}~g)escT# zg;S;9Dxte1T%2!^-gO8u${jIO5eqqMEjct0c6<>YHm_wwr3 z^RN@E+)(SHf@&gUm9Dp#OiPpQv-sxg3Jw#jJw{kKaAv5s6VLiOl!+|h(ox@~V;eZ4 zTj1%OP+%XIjj{H(_9KTL-Cr#_b>XyX^{_@2({W=-OLqPHi9D}8P~QjRms zfAV&64kAM&oM5DKcI|bh>%2Gi_1mmjfY*uIt5P>DDDS8oEyy}_KYU>O7l_Of>xFVS z1|SRgdWItOAixkpQ?Ykr&#eX=aR_`9Exz|5YroCeiC4{*_B+o$d;5~iLHZTZc+VXU z@}kVMY(M~`8P83B|5zQM*9 z7Yk~JXPnh#A5V=3jTCnUTkbVn^^SM0Q4r4nZh>#~k(|7r2sXnv`bbXQPXb87NgNZ> z4&I)^;U{f4G~vuyM#PX(;@jv>9i8p5W=t^$`B}@Q9(lXy4!9hQKGDlEB76XV3E3Ba zDVA_ph8@Ndl2b0Mq6n$0>+=Lk`N0(|dR*Sa`-+=$)Lm`P>Ze`E1USOEbb`b{@LkkD zSl0PpfO#7ywgE80Gr|L~HM`?WG{v+qa3*q}lkcL&fxY45a~aV-X2{lb{Rixfns8_! zP?$x?#vv6M*Z|tIfcgNRvjx7-f7kh<2Pj5!a(^9z051I@?%e$e0-+59UAmkH;|E2> zu+;Gi{89kI#V1+kHbIP>uv6OkxW>)Xz9>0`w{%+ii{1Xqo-G^gzpmWWCs2A1H@CKT z$T4YirZl2BI;RbZOqB*F%H=kYdk-qHlJcP%&KJSm0#t7>dXNp&)%jO%07b|(0_u3I zeF|-L!I&f`X>U)N-%0!NaweBcF47qg$#DD$hjW|+4a+)s_kXbbzz)E$#A(zBk6ksk zE%C-s?cO^Xf~Yq&J@Fs7{LRJ4<=7Y*Qpr2kKZ6@r=j2kN!H^ zz*PWIQ(Ffg1pvm;)LaBjjZas^Zuo`2L!342)I4QdA^I*cO2=*G>hUf${S+tmn|aM` z1nQ$eH#6EOE8D%w2*@z8ye-Jw&L=RCvnkB^R&=LiNH$s^dN>;%4&YD_Ts;&j%gPC; zU(dOxlU>vN>O5NCIz!5&T)aryFr$|48>>l%YwH_KnXoeOij{whF#vH#eQUe8CqUGf zldkjrE7ZofQyONk@9v8c=3csUVX8O@xfVApgNIW<<=rX>0Rsm30D_xQX%p6MQVY_O z#bYE&yQe>|IO~v)GhcUU&f2=tmEwxkvQ~r8e?4AsuO5aA`amV|32JF8n#hVDxK%Y~ zi0_;k8_1pds{Hc$7Uw8;&86t_Ijd@zEw~RRipL!UYOq0za!Z{bPYn*6yNqc(?1h3_ z$s^0I6H%ttZ=~21(L;DoqM@9NEM*wHjT0g!^`hu|k{ODhlt0~ZUvHt_G!ug7QT; zIAZT(Z&d!hEP)&YY+q4|WeGarr1G5bF0{J8s4{zCQJu!`E$zNT`-!XT_8EkY?js^1 zGt5&fqhd3MyKvpy@Z@PXm>ZsfF2vVGj~0}a8l#FmE$*B^=g*L29KLm?YVX}IJtnTb zNhgow{td}wrGc=-Ec!tRat)^QY#vCCUJrb;rA6%u{!dc0%n;^w?F^RsXGR>OxW z0yrmhy@12-0pa5d&wk5$*2`!08~sHBiS9Y2ETnrToY`T-@Pd^opw7>0pWzRJpm*hO z;0NRwV9N?HX8#?20F0P8qK(cDk$c}6cE!H=d}GJK2m!0${fYkfMx#XYv==9W*G3l1 z4`c&=pnqTxBnKFi!q;^8QW-YiVw1!0OOPC1y#~nvm{stNfPbUEJ~_Y?=$}gtE&oVz zfRSoYzyA?r;Qeoif!R603oHOYVDbZanVJSKGaqrXd2mKf(hMJd>-73`)EmSuT(d-f z8W`uDepX2u;e4t{S))GAeDsQKiN)zVLo1USTqjBu%|jn~)3Gi_zg(VTcFMLqQe?G9 z>v7(4dqpHDj{`0gw~&+oZ;Ij{>UAukkiE8JEIAD}1Z#59=lf5)vdS9djuoBk9$LPj z>WOmnc964vp~S0xRL6)yO8ex7dSMY`{+H4>@5gCA3Fyvvixfy^s%rx8>@*+dv;X6J+`BXPjnY!?IRQ9SVD-cP+$49-;`uC z{RK4(K}7d7QGnpmqXo?p*Mfap+*;^jK3@=-Wsd|cw;mLLBfj&!sn??b^YhRjVgqjoMIDTSqJl08SW?I-C}8xIoC-=_ zO97>Tk(UQwmXp!LXk&myg`PHAR#{I8rJ#dG=_&zKKpCT~tcXErp=6YFQ95XOZDkoi z56CF#DC406DkCB)+c(|IMt4M=N^`q$Fl-^XV{+DY%ckT%d9eUo+W5GCRSwcZ_OC-#RpIsjiOf0_=cgoo;X+Eom|f1?&@g;(&JdL125 z2LFwGohp)oQY@YrpX%`;4a50yW^pE#<=b9eOdc}y6F*{pUyb0%@jMko(_1L zZSP@o-)E41($cQeh$nvT2Kss`wUtng@9u2@7loxK>dH05qi3}(o9b8mJC#iLHMm)q zn+(T4h}u4S=4(jddOCn37S{LuWkdr<2f$iAEFDn7VqW)G=>U8!cJU=Gh1m*^km^3& ziKh1G7ith`24%k)F;U0(L zBqcf2LqoF5^HF2piyEa#I=j7Xt;$E9Wewu!fa!^lfB>m^zVex8WvFW7o>NvHTkYwe z?#T40`m%hf=n{?&c>5X|ui3ZNKUHCn47stjhOBs`iViK6Rq}i%GmhjXJsKc8@pR`5l6<>4UC>P!64V z(6@)?q3?y3tKy2^h%2|Ua9SVk-ky2kY*Z|e?&G5a*e>%ZrZ_)VlS&G6^t522h^M`Y zVUN8^JH$OW-pO%1?*(_?!Q%Jo*Wa=WMQI0R7j`&VAN9)p$iq1HV(Dq@ESey@ibI1vo2t%w+ ze1o`+B#5Mn^ag1=nL1e|xdQoFzz3*PT%%Z^WCeUcGv#|KdMY%PDODX+KeamzElnw{ zENwinsj#Mp=p*P`8CV%!F(MgF8NC_f8S|N}m<}>!GQDG_WfoyhXD(yWW*K4?XWh@n z#a7R*$$o-;gu|8N7RL&w5$DGZ8XKy(D7bcVUFP=ZuIBFNe#@iCW65)or-qk`SA=&T z?`b|wz7N2tf^sAK#twdc0SFKQ{+kjvO$*Wrx(hZ64hqfz8X#L}MVMaLNw`$FMR-_v zLBw3dUZeyVRxpbSh@wQ}MSI1>#FWIgh^31aiPeZb66X>R5|0)?C7vbzQ9@h7O2SK` zMv_%hOj1MAMAAXBM9NhvL@Hj|LfRfhhhj$!q29{`%B0F<%iNINDqAVrBHJbVO3ocE zik3xJ$a^S=0${E};kjaf5~nh;GQBdFa*m3V%7n_2YKEGv+8(txYD=3lHecIZrmm*0 zuWqHmropF?r%|ENsPRl=P-8;lqh_v_oYrft4_ZswceNXI40RE@_jFtI%=LUQ0+{+O zhqol?GwN^Bm(|zQH`KS(w>O|O$TBoDoH5#Aw8tpMDA6dx=$cW5@imhS(?qjKvnsQG za}IMMb9wW5^A(F2i|nlyTR(2w_Ja`sBLWHBlK+zv_;WKNNLD{U0SFjX08{{9ipQ6_ z;Yktpj=;eJe|cI2g#n8KXbl+nC#J?fXG4Lu!PjzxOpgBz8u)zx0Q{~4Mghoi{d@#>sG%#48<`p;6DlajuJpF3>5g! zhHwTc0SsjLPl>PwDQB42@Shf~rwKs&gU4LJx-GNB^m9$Y_T=0ZWR6=dZ&7}ezpv(K zyW^#UEwkru?Kj#EtpNpC9tGh4|7Ae^AW|SOpa}K;!15%(l)w~{g3QATh}BJ7e%OqGM2D9A$u&9fO(v=Q)Gl2#-N|o8jlm z6T%w&l*AVz3^w=~jW6``w80ONFQfz9G`3Ox69Qfc1Bn6$Oc*)9tIj1DIUrbf3;jHE z@Iz7yJQuJ!Si-0S8(+EL=bY^CG31^Ss!aD5H0e!PiU zO!*pn0P9u3^Tz>w0J0`dz6u^Gj{ci8tE*|G@Q5Xf9XMY6NFyBsvPJBW60oZ=hch3+ zaD!J9Gbl*~xT&m5HU>YE&5f8-C>n!@GaDD{L|X!qR47REs+mROr()bI5?H$$SVzxO zDNewVJ7TQ4KypySG7j|bhQPB_G$E~@rW|r5u#`h3qi&sC-CDBwf17kDKfiqrVhd~N zyt%Wduc3b4bmP;ghsKHKX~gPRO?Q9QzWprK4_v7R0_R)_N=Ta^)PY$j5(27m{bM4f2FKz^nAh04}P$WO3_tUIHt^|!;RqxGgU=*PP>B0kw zAll)Ldi8BM5a^fF4!IIC&JGvR5HO0sK&##r1Og8{*c$!83mo1r1Rf4Rr~1dtD8`UM zJ2zfF!SwC6I!kFkC$Gh;rV|&IwJPY~nt=1Z7){6s-t*r;JmgBO5f5wW`ByOykSSc{ zKLI@)mf?t9-vxi&D8d55npO~wJ&-!d@nH?K5PWM+41}W(Z5TM`Tfxy23_pOF;hDi2 z{u06X1NKX+mK5F3h&eQ}PT%{WlO-40h&Z@pwRxOIUNEs)=rH;y?G}uHcu+_gw_c9Kdz?npNLS8t) z0m~gg?vMx4v^LpVK7|D%Bl||=@iP|5cjeSW;=?I)>T4A-R$mWALiqr= zICsY7^xShLEl1=jF?!Ul>WK4l6US@A=*gZ>9IJT#O=AtXfKEYAkW>iVZoMI&RmKDI zh5P`Qu%BVKh?AD2oYTB|c-h@gG3k6SiKfG68UtN(i}l*RW`IE%17!fzVoP|D`sSDO zIXRA{eQqa6veN2}G8y90iGA$bv?s0eeQ~G-6bq#Rn+m;6!5>H|bmNa@9K5*1&3D8qkF#34$~se`qhTg)r7jrQK1&KmQ8j9M!C& z%9K--tn6Qu87i%r#p4_m%125MEE)>Ih6hN)mY2z>@v0oL6i5DHo)44{2b@GAKLg+gJB6zG@VG!I4v*lK_A&}AmA(9uox zz4ci1UfwAhW)rLDXC;6^1dL)pQBXAeoZS6T2qV#reX@Cv@!95%p_O3JhL|X(D4n;6hl)Mg+_Cb=EnjRLSFp|7JbkWGe|5d$a&W@e; zmr0u?&q8Ea_YJHWg-)$!k(Y#6f|JV=i>!Pxn-{(uH9N-jskb#<{&=6Uk!?AI0t_4& zu<&iRo3}oEOqI1~LY-LTx`$(k|8;&VzwmaQfSjTCA|DXo!I=H9M+6vWxEd9JNbn65 z5-?60pu=nJd;~fQ2L;9su87BA$F$21VWIcm>FM70D0|FFr6CiOswTkf!Ci*RJJMTh z-p>d|&oST(LCzu)TkYyqakS#Si70bsxPPszjc;-2(xu{r=^p*+8F(m@LUFiCf&quy zLpp9X4#0|Mr}~G~mvKm0zF>3u*Nu($j2t%PGlOxL0VM!XV|kn? zsEtoZi$qQXl|#0(i+3=-s<=>IonkAOsz`tA1TlEDa03p!fXC_@dI9x<76}w;6?}8e zKf0cT-;&Sa_Q9_TZk=oqi+pqiopgH(Rw78ik(UM~fq>kOQ0{Sz6#u#>FE()?yd(Me z=pu=O$g^p-Mg_qOX(UCFz@CT*It>CM!yXmji2c5&=eV#&S3M%mLdlE>^3xV6G%JY0 zr%#&wKIgpDz7%+~K&n5|ncw$n=}~D-7{!3y2!1JyafHJG>k*Lxr9zo&)(~lHZ!!o3 z8ocs6_$;e-bGH4H(Qu}&4RM+9s-0A!>foxrb!Aar~} z|7W{)hqE`D*2o`36Sp2cQs8rSg-2L<%GK}l*ZGl0AD9S)6+ruQpP#!UsNf&>_<^pu zwfkfH+ZOkoTh$OjllCiq+l{AZu>Azi>0nT!11K_KV+&x@g2;wopafq%?RHZXcs|=j z`ALhN#B}TRt498VY$rX9G8bQac){EtBV_~OIYPd|VS;63r} ze-_!ul>nz8w_q<5rewsCGw&C9Y2$Yq^j)}k+od`CC^}O&T&Sq)47qGP!@1+MeK2h%;@u?`&$&8-02p*N-;DFH$?7?vt z_R%1qisSpw2Ns-DYBxg*Ea4vFe+LYSf5{^P1RtZS zYj8YeA!t~8V-l@Y;UYBpz>+-q!zFSRsZ(>or1pIF{0FA6e4pFRFDUn^4oYA>A{yX1r330DPzVpFUF}M~BHJcY z)VY+2irw35qb}O1c)Kbf^=Qsm?t7c-@9>+(4I+ntuLaZu1lzzY0)M5wKV|uSylT~Y zuYxF6E1JSybZcCM(c^JWW}WQaih2wj^qc*W$UZc?`?kag)FXNTb zVJ*l7ygMbw7~d)W;gjA)p&f9KuAfMNrwH(r0~6_U(4+Wz?653f@pdoAobt2rViH;v zmHl6KCS)7N8OM&?dwZhW>ciiFG^}+O;Y|>NludXYCxTkuve1BJEqg95om&@MwHP?O z=cw6dCF&_3o_QOW#O93-=?|P- zw#Y93)^b0(`>Vrh1H+6*2ka3s01ZMzaMl0;mV{0h)qW)6fjO z`UuP-umna003opHX&r$g=W;6Q~SKRCM)P$X;S|F&4=+Z{(Vjm%ST>0;D`< zXJ;vU!^{|t2rmbUbHBA$GVY;$Hmc(vlbmwnj#%Vt=?$bCXUZ8#+$z2Gb9F5G?p~1O z`9y-HY}TV6@Qyow>{zi@6T2(?9uW(`H3Y9m1h{aByXNwh@Du4xN0tUmTp|OcrO+RU zRgAa3GTkF+@0|9DgnWR6+1O_s1y^wWQbHUa#*6bLU>xEDbbh@z+pMn+yoR#`_uMi(ukBd08xx>|ZlXjxe$EiEN&Ekz|+Wi3TT83i2_Mo~#c8Ko_!t*xakhtbwS>u70dYsu?r z0j~%}85vz=d0hpRHf$9kuOlz3fRRBdD=EroDeCHJV{{d?WYJ(tJQT>?8C%gtZms)g z4tvdt5^FrFI(>WKMht&^er1U0mA&3{AxXFH7HCu%U#qmg>c5vexr<7C#dfP7ecIkD zgSA~brdM$k2)CRR#pvJrMvIt@NH z_u|FQj>Za)o=O&)kaK1VeEIoK>7GyguOngxc+R|oXhw)w7mGY!vPnwaQD9Ki=s>Th zKmfr4V))A_4vqqW1(z@dGQ>Vs3H_D=!Nh!EN4j}@t7AwuYXFc8f{&W!zdsw>Jpn<4 zuf-PiPB(4do66>(IDN83e_Q4u3Bh#z!R`!$#utGZ)+hvC+9;+wr(gfA{;yFJHIAU>y?c5W^j(4AB$<1#$elw(tH z>Fe;j>V~9SNVc+_E>D248rjam3fhv59lUUGLidO>bnZtyW9?CH8smD`a|2nWFA)0B zkWAOi%r@Q;#kIj|l=Fm*39FsY5x#j#yDqv9%KGrDqDmJKL?u3RNCG?}4oz(1oxCM__lQW`X{M>pIR3<@&He>R z&xNvfJaoBx+svTxB&+MO1CX*vmHCcPN(b`60l!DYGct+?`9ZsbxTrEKqYv-dE@o&Oc)TfpZ!--W`Harhs4Fy2pJ1|S z9Y?0CZwO5$OvirBQdT8pe396>ds(0+%bXR?&twvKD3Je^{0z?UiLeyNBuEDT5Cy_G z$HdO0#uUd?$kfQJ#QgtpcOLLm{g3}Y_FmWCGkfpt+C*7pMzUH)LxqGSSy>??86`?2 ziAZ)*MrJZXDkUnbkmCP7_g;PU`F=j1d+XEZ^Zow*=keg&bMHOxz3w@$=Xq^o7-Kxa z_>hT_sfyW|d4xrbC6twr)q?c`>m*w|+cS21_I&n54qZ-aP6y5a&Iryv&N(hEt{q%g zxth7@xuvQtdc~K)#N6`{7NQ_#HQ%qD$Ma)PnTWmpGPuy1A zOZ>Qag!oelafv{QD2X(QD-yRQS|r&dJ0!;>=cFp7nx&6PhsYSpY?B$1S(7D|Ws)0EpR*D0SMpRT~8(50B5n6AX5q@gsZ9IE1};-?a(O0N1y&0Nhzolw0=y;Z|P z!$p%&6QRkad0DeWvqmdeD*{Q4AQ8Blrrg@FXNlEY<+{4*BhPnZs@EC{MyfM4V5 zOC(b;qE!B#1>qpRjA23S7ejqGc&!bgp2PH@rMq*JQ&w$SeY7@NYx`e4X4Llm#)8Bgc7*fWji?Ail(ZWSK6qncOi~BwF>YL>7gk z`i01W4EH#JoARCal3JEGvLJ*WZS-`7)Y`Q?QumMN9;9SmlNx+@V&d-3${VZ>qW5|0 zK$ry71eQq5-WfW+fdzTi#(cXwcF*gNw~StIy-I@}FX3+*pv|XOoYrt+z?~_1n*8I(WC-TTl~;qC2r< z!Oc1;rt8Y^2Q0|17ZGn@K``=Q6bo|vfcnDI);-kaXVir5+{oWPV@`e0?3m-qXGR^F zYf=&1zp)@_UGO>!f>Hdgvmh8PuRJ{bRK9Ap>d>!kC$%qq*~9OcMl-$rUgNO_I>eJL z6!MXx@(>;d?Pyjh4*4GKc$;t*5UM|pcAP%$4MO^JwBv^E7IV_xMaAT4^ZCo1XXkes zXAuZfkg->&Z90}h!{(EFZCmORm^`3eb^CrpI}kowG=C257*jx@9rvgdfPuyTA81Dn z-NzY%UAsIfd*>HkwO%DsvC=BD+DGcy#g!9A?;=mMr`+W&>dL3|m;#PyM|7bUc>4DN zhR>1wDsx)N;}fw`+4`53_$%WF>iPN}5k?6}%%f@r1o<;)$Cv_@<4oL*kRL=l-VVE? zpDO>$(T*_%88=6}CFD<_9XxRB{HqP_dA|_tIIRDy0&%5K#NN|FW2pxkweh^U19`Z` zLYFn)?Bk7HnQ&6TGVby}hjxr9{Li5sr&)LXx6uxA*~V{Z2OtXITE`jf07|_dM>{~C ze;n;Vo%91}2NG^hsN?~(10RhSqNWr_$O*c-{;4zkEr5A}VFrJ4`SX{2ZnSj+A}^gIHgcT4=cSkWlCl5Lv}u>`68v-nV-#0tH^#O$o-2jvAhh^ON$6s z!T);z2|z|*fW#g009Qg`j^ytHBwVy`kpj5F8GsgEI*jgP;I+-jCfQUBK+*tz3ZRjr z;LqSuZHzG3`?_U$8GyS)n9m4p!H=&upLuWc0f1ziA#^bE?*k<2Jg8_Pgfjq04#|FA z$%Y_=*{=Jo@qZs6@k64+<_iJOm=mmM%m&ANu3LGToI%)v6J2q`(Pux?`v#a@hm_(e z9meYm!*-Nay?m-SyoT%oaLW4mh&qrq7pr{}AUH&!Tl{{T-#+64VY2V!5?lxYkc!au zwyLoW)*CPYf}l$i)|&qEZWBxJtCX2aA4k1cLhl7X`5cgy3%aq(A*JR5L_#Ccfs((| zqVNq0Pqh9YhDr2m30&|^O;|?o^1Ri{tBnQsvTq4&*XC#3=62j!NKSa|3y_5+LKpF= z4q`A8m<7?Vk&67#v{$I`tfMXe^FC$MTU=-2gZ6!*HM_Q}(}9@RCWMj)zEcSsX#DlF z7x2&q8c-5+4WG`+y>bM3{Id}u1alphwc%59{PZylclE58j`TWcGFADj(BD#QLbw4oA18g)dI3>sOt|mkY|DXs3oqJ!t_n5WCKs_(K5* zvZ>zL$hm?$(iNWXRoM8bqMQpEs7(?)p7+-Y=b48cKD^GGgri6ZS4HR4v$lt*mhpnb zp3z4**DeP=G5Me{__f{d(EzaiL4yOwp%Db0RBdSfibb5ca)JIUd8roJ?7VB7#UHjl z|4Knep#WVif=Kb8K^JOU^N8UZO>+?l{~!8vb`FZEV&?!e6Lo`)@&Y2~(aCiGh< z22A5p8K7t&%Z~ynd^}KPRC%6WhhKhoZ!)!cgSfRtV4pX>>idqzJmfK9pfz_w&6sik z4fR_&45JeW#D;iK8~{FaYiYlLkkc?{lh#Y zBXYkW_x8@?CR=^(pAA?jkTy=f*Sxf%ObFDDBXH?~E{;x47hHOvH=>VHxPJX zucrJc5R&G1H@E7GMrgEdCqcX@t71w{?evYFwU?)Y`4K=6{8E183|;|>A4!GMpzL*Q zBz^tGMI6BkZ~+lBJ%JJSuQ$MtfT;}S=mDUSY$yljM=n5_IO(^3fFH3)LF=Pnexx7w zY-|%zq3xm^*u=E0I_d6>i8VV8_agS#j^!k=E9S#P^WGtF?m%_{Kk_XbS?k&8f^vau z1REcM?ejn8M*#YT2eTW$jvoP6Vez19sOAUwk-B{lln;#CufirDureQ|R=tM$UktL2 zjXN0dtsNsn^02<5UdBHZJlSa(K7Oh36)mS=X=lh+MjKxo`3X?|Z0F96>&}K)CB5SD zMNn-C5Vm3*CG9%MB2g3}(W=Pg(+SrTQ1$?R1k@AYCkg@JauZhe6ycCgr(`Xa3Q70J zo+nMK=xht;w{IOSGw9YbNpH^W8y36p6{p5p2Pfe#whjD2ae+N zy4Bk{2RQ9vT-g+P*k4V-?G)ty1|U69`F{-Zk1c=L>S+T%Qi_NF zKg*AdDL_c*Hh2J41%2{qwo*Mh0!7lqU|;_%_J%I?n_t`(Btp2_ZwOx{6Dtk-b^OSf z0v=S47W)IN>JEw@fgdT1O)l2MsO*QwyHkn4Br)aOi4<95(j8ATGJEKU8eJh*BLOQM zWpD6U>^M(vaBuf+A9{*DQKbKLRoz$AeJ2 z@bI~M-PULU`J1`HpYj{ZGfwHgRkxkJRl7$C%r@xE1Ab&Z^EH6?!Grk`YV~sR-)6u6~pIaK8V=#9qAW{#m{-Q9un7PZbR`S&G4Mk4fWtCwFiYuH{I0JcdA^> zrmt)^OfVsCjxDfu7jkg7vdl53%At7T^r(9<1yrvt91yVeRcQ3`5yd?Q``vUQ>#SqMH|ZkQ&U70B{5!R9ZU(3_*;bE?5MPf=D`l z_27Jf4fX|T_z8YQnMxOR@A;qQM-H&Tz90=~exw&Q7eB?lMy;HxE#^YTLMytu;(ZRX zF)-2IDp`EYM)F{Qs8mm-PjCzZ+s28c4|)O(!4qjes8MV+wl7g~Qyud&*C)v!+&#uj zq@nTGE>hBN@!ppB070mI_#eO})~gHmA_(Zx`MURE`N`ULx!0Mhj(0CM-3&HlNiN!J za`)ZG>36r`5`c>U@FVCEV*naN7XeIiVasAD17{TJB7#0+%l0szQ-SBoxiel!8xQv1 zI+(e3i1pN$4gAP6FmT+o!?MU-8`WxO*vYlnyKC0^o~Fc^Mat7IGH;(~q=s_7XY5;Hq9yMM;z$wHC znuJFU6h!jo*N+-75)4W``t@>d^oxPvIg%aqZ|mrLLQU^Vj^rcz;Wop*qsB_x1$-SCvj|;fzvakV_M#nfj1s zVCK3?h~HBda?3A7g z{h6&g-~t$lm7C+s2S=Qq27rq>;6fz$!)!!w#Ira=OuA!rFsX#|-UZpUk)(x+P(nuBl@PbHIAoHI^}Mq5sU1)+By~jV&q(p z78EJ2IVNMf&tBJ}D(G4VwdIXOCb}HXWJw!2A8&Tw;TY`^iE#H$KYiN@GbDTQn@#CF zQP;mJpS!N&ZnGz3wCRwr>ot94IO2``2r9?l_z_?o@_xTCbOtVZ@YI+ka9rBy1f5O^ zoq>;+HxMJ=N=bY8i8(&?L}>`mhEKnRhy>x`!{6nBugJil2eAP$5Srq?uog^vGFoV~hS8_idF2;?MPlJil#lH~PSrP^Ot?2Kua6!X}ZbS2*z`$|9-?`f|YW zLt9Tp-#|q{SzbwBTSpOiekkh70_;OkSsnl)I*R(>P+wL-8Bii}$~rRo`m%Czin6i_ zdb%pAvhpfAO2CmsTU$jQc!?uZW<@FS0 z6lLUe<@6MEl%xT3f`uokc2Zd`y0~m>__o|3r6K=G*tNUfo|2o+kSO1L&#%bm_)s7t zknLJhnb1Tme~3zS(UH|CkH?`Mtzl1?=o|Yg@Ho*t3ARvvnkPZ65Pt6}@&c|dN<*^{ z%syrVPf`WHhv7*!^R9-Vc@k`**ej1^@ed+z9rcfU#LuuiW#E6|65i3G{YMB5`j`{n zwXnjX*>#>I&(m_gIriCw8WLU2xt{pIf=3D>ujd-sLL|!d9&cHu`w^Z*nF1et4{>jlZJHD>g^%ZhSt^r(*m0=3T;P13ZKrYz}bS6MY;*z~eJ3HlE~vWqcmT z2eT!ZfknX|h9|j(Z-oDnK#;(Qz=q%eK@`Co;46|yxJu+rG(s#&+)ctp5=PQVdV#c> zjFe1}OpnZkEQDN=+?0HQLY-2Qaw}yGl`2&^H379C^=4{s>Imx3G)y$Pv_`b=={)J0 z5C(`udR6*6^ur7g!xn}E3{ebU7+DxEGifuOWg1`(Vi91;W%(<<{+cBkwq*%;Y$z>-{%E0=4K>yY23z^K5ba9MG?5~C8A(q*N3 zWhWJ4l^K;Us-&v1YFpKM)h5&kONH2O3qG$S-qG;_6twdA#QkOW9dWFqo1vJ_c| ze1z;pj%df}aO*tQ8Pu83&DSl_Q`CE>U!-4dpl)DiNMl%R z5IG^Xi2&OG4^N#C@U=jDkNoY)6Gm51RvD@AL=S$8{__(knm75+DfF*-6FNHVQ`T>~ z|2=eM*yk>^?*9lK3HAvLrTb5zXU0B>0p0&Gg(L{h9wDsyhdu~6{2Ar)C$tK%DS@3j z{`@>tdj;1F))#Iupz5lefuRE|+UJEQZZns%zJso7FUiS=p$v%+<5RO?V{s}eMAPlH zp^9&Odm%zu`dNE?@lz# z+rpb+ZY`_5D*D5e|}RZ#0T-q(vy6>lIRFG<5IIcT=;ijs0{w0wHxjIPcvZX|lkK%^sxFvaf~jJ=;h_@#GoU*Gg3> z4Dm7^dpdjjy={kqtwKT4SZ7zV0=k}YBxdgno!dY|)k9tET5eCK!Vko@ z%xRsqY7{->?9ycIQg}*7Fw@l`JOp!A9cDiUg}{vuJL&M9s9zX}_$|=y_@G7oBEZ)L z^NuB2t}h0OgUX|V?zuB@g^&Ut36pi(mmibsTR;*hCkC`!UlNk~VJC*~<@y$|=?jF! z1@-wcQM@!Reg{7$ikJOizk>~;cs)o4pg#YK+}#k8gN#wOFFc@K{pE%;>VvyPT>+HR z$L)W;S@oGM>uEH9%FdVgFnj9I`#QRxs=sxlPiahVTvK@5}J!@ zAO~atX`ng)eVUat1ZhJ$KiUCgI5kncN*(E@I=MO!9;yTU-?k!Y%_CXq~IR&Q2g{5uXlH#*cZZh2}dguK2(bS;vXE=FXUB@`HaSX;SicI9PuNnjXhaQ zx)Q}dQKuuhyPCF1{YmR&2uKRN3PD1qaLm8niUbta#vuca8?XCi=%JY7;2Ry`Yti|k>BpGr79%Ots&1_`Ju1w5d!B@D zz|pz1_SJpCS+X;1C#Jsc6L^~n*PaXH3gaih{Q^F6102fV_AdeV3MEuL5D|^+@K|_Qg;QWPCC&-ZC{NuU`?k1JJ>O2r?`$zDT zI&|3lV8?y&19c}7;3qH0>}$qw#7{8i(}VMAFRIzWd&HEM$^+ny;)nT+3+&mS8md}^ z0Q@AMHOM4;BYv_M+K09yA@JG-^#1!HPkgJ|2Uvc9WJq+(u$B&gKtjpT0D|sO80ogQ zal1Wv!Ax=;WBz3s1s__5+`lIEQ5%o89o#_+kx-?)_DB1jctPI4@1#Q7seiM|rK~6I z{F%xjZ^W7;{p$61OMVfzA`+$JZ<{A1sl6P61SS$08@*C|paXz*(bdWn%OssyUAZA7 zNG(FUpZt`*M6j6uE#tnk&qj7U3j@X_q2OWr`n77uuP$|tAokZVQJq+xh2&zMckdD9 zVZCDP`AKKwscbb6P=bf?ZH(X`bO@gUiQMT#O9;W%Ar>|L*@^M{i(btPQJhx0r}OH% z&rH zZw&Xx6eYXFkHOdmU`wK)LO`r+U-4MbO(D}nI6v>vT+yqCtuStr%-V0Hnk(dDJ z_&R*z2l*o?43Uz;bK!PUfs0Q@4wl`qdQ3~>fMjs+_H{q2x^##Tn4RdM9cEyPL9e!$ zzQO(d9_9Y)Z-=bT3D@NFYy;P-b%D13C+64Qve{9@Kp#2oYP|n}e!<07b(o z0*wWoqGC9;phcjMIx##7Xn5avl&9;Jo&Y5R7=<-~=!xO2t?YMRdb{WJ8TdR+i)9K@ z=sK!vaq+-yUu$K2Xc8L5H>v3@XtjB@XmHn!uXu$^@m?!yODpkh#054snU6cB9R>sN zKhK0vLg%1Sknm5AqDOUaM=yQYV|$YCi_K@Jp;OTLhh| zE<<_0lt+n}oI&v@X#jG{S;tT^)?Z}d2y%c+IGFhg47q>3!90pIhDW&o0ecphM}Zwd z3Po1;?SyRgEQ2%gq~ET3Dod8~scUu%Y!gZyi8=TX2j|v!U>-#p=25N#5Tz6-gMh6E_Hag z5^y~hdPQODm6em?4R2_r=ASyv`+sS{sj=3f^qoDKO3G$CEf7t421CMeGG|P0M zmJ3KYLUMuD18-J%i?o;g=Qp<|Pqbyf~NdO>bc zvE!+%U5Y>i@*Z`f{l(@z$sMEm3s0NIIB}HCflB8*eY!->>T%C`$;%9{OjoKrwIM!Aba5<{APz{>Ie~Cu{nUCFbw|snCpS#$WD4|+5XZE-6Qp6&t{mF%0a9S`>y}lU=yc_plT2}Au@kzx38QU<5yJ5mkH@|% zT%armGz#CkBiIuiLEa`LMmOVfuHw>m3G+bbWQPOIC1e&wHlf7ydsNA$(9LTK znud9lHrOE$wl4t`iV<*%1Ft*be_g+NaE{G_{Y;vFLRoHTb_#Wu_@CubVzXdBlV&uJ z(uW#!25_%Yi(xPNrL=nwmk&QeJh@*=mO%fpePVDbu*CVQ7#=kn{M-NgiR1|YQHJ4( z^eL!OY&FI6UqOUQNJfER;?{BN`dY%&664{yZ58xN;)rETz9D8|s z)z!M8=2eXO_@?10NL%aBcMC}dLAV<3-&Vs?f1XqX97lz zvwn=9+5_!FF0pxt<*(e-$;N3^Ss-XN#IsrT}27wJcnhxH$(twWm&L2cfF8Q*pfARfueOk^k zLM|0rcP5V3+zynsAQ_~WestP!anX6$?F7|}Ws++##hBOX>2}vOaFp z!{KhsJmvDA)-T zaQzZ}rDYBGrLtJFGO@iTt<^!o8(x=3tJ`RNO~d!lGWrLnT+h0YbKAffr187oQD1;n z$tpbkt%0El+jtn8q9ZS@Ev+xFEv=}m ztgj;@uP7(2Evuj;ucsp`D{CO9C?l(dtXEw8Jfq^m0}qpWP8tShao zAg3U!0{9XI6*)O+6&(ddSw$IX898kOK%&Si8OX@%%IWIpX)7!1$tx;qV7^36p!M`h+i(tye6VOeWyhl-3D#n0?!y5GKZQ=WNl02{sul zX(iEE$!+Pcb#5na7e-gNiHuNd;`^G-SCj}979BXm56fiN36mu&A&Fz8+gu#4Jj;3^ zxTGNYsZnxU;l&H@6Eg*#Pt}+3?o+dslGyrDv|L~J;7RVTPifVml-G6SW(`eZC>g2; zH#(LWn!#erzm55z2@_ZdhayZ$UD)fsCror&wf*poFQnt{Sc0v@a-N%VYrL|yo_{KE zx3;3reEw25hmD5c8X;|_`$x|q{GU*BVuK*BhD1kD@pc; z;6>s)Xqy>++zY}%5hm7J=?T$C#*ba7c-F7(q3?a%SBSY*z2^j3SIWVJL$7(!-?|jY zoBlw!VpT>zdaa(=iGI6w$MC};UwzBh1!kvXYWN@u_#STjmvgKA3&G170m-3s403kQ zrWc+K?Wz6nLF|-+Lf7frXu{-UXn26~DJkLxIXiVetF{U#& zG#M~1ey+F`Fvgu)p%>>$sfnF1Nw&G^$ijDvX7kkx)_I1k2*#a`_Zz$2Ni))ygr^>! zpRrVTJ);uGO~iI3Ql0LBz&^rk6?_xI8_(E`e0{GQ*tr3S1sh?);I6{X^1e&Rl7s9} zeNR$D|8-^EQ_QQOLqt3xZQ)Yt?-QTt=AJHbzl#@{WH7?eqLxH<^WHU|C$DCi7}nk- zR|DS_Y=lXF_vE?855*#ZXTooL$VYH$rgwi4a8^^JB=VbAGTNWo&3z&!fw3(>i^k1^ z(neUvpK5_qR47zlG1PmjvP-|4B0N54iD7Xp`Cl2I$1#M7$QdvgpdCwoPncXLC?^DJ zf5Iff86r!fJw!o7Nks37k;I9_(i}zVh^&S@iu@Y+Ckh4%SqgIsZ;BX7 zdCEX4Z>m_TRq6ohPc*tT9yC!j#WXFnsP)swFPTp=x3jRa)Udj-jb(D77-T-6zLTi7nv1V6WuDBD_SC2E7~E(Cw54zS*%y=h1iVPnm9sS zNIXEiTYN%%MS@I%L&95J~Do(0gs+U#E)oj&S)VbC3)XOz&H9Ry9 zX)K|fOL{biwOq7(wL-PxwbHdNYZW7RB0p$nXh}|A32u>Qt6=I|OgBGGgP$a}tTtt{5`o>>Q z?9U83a+Iz|NLEBrylSQFvmjv)Jd5GMci3qmppQV%iDi$^PoBH-x+Y(fSBxohH`l3W z+#|g_)t09?`jz_AcVeDZdcqG$3vGCECcsP96$8Lle$Du$3Ud+my?fW>q%yT@>NwY#hPLkKvGUC|1 zIs{EMBNC{07(gU=;FkU=1YQ{>`Ds_CT<2b~cPaZUPVYC0)<4JU#(0>{Y;JsVaR6u@ z@F4K8O$3=>s_)H8=ZSg4`9g~pAL~)6#v(%7Q_t~xgLy9$$hQ$fBZTjkt8HL-2Vjj5Bn$zOJiHRoDs1#GEp_#!myOJ44tjT)y|AwO+^u@c zhwc+)&AVML!eGrW<|w2g`#%P2HgFX7Kw7^YH%AyW1Hlc1cs!6joTXaT%@*Yq1}Re+ zrgN?nqG#1YbJIQ>C(HYEymBYxNT~P&um*_OezAHHn2W4~H7H%8Bg7AGL4V+MRY$Y3xG-2P;XWPWmXM2>Q;`l&^&|lm^ji;*Z5Jhu+iP z^Y)G`|EV^2V#Z^s>3UVyyzoeQO&hLKvuJ%IUdd;=GS3i>xU;1eDlypF?Q>%0k2^f|!@LhOQ{m>mPZBr86G-IlLE73>s zMq|+kuPlGFzuB$!j*n%iRztve`4AF(7v4Jqb;{xVbzMbMZTOvum#H2%q+S(U#`ZZ9Q1x2k*1|;8gS+Tm^G6AojdFG%!Nw3U)2ZPzED-XyrhR~R#{B>b zqyy=qhk2X{n8HB=5E8%~Hg*834IQBFemvRC%S5l_lRdYgz}^Q(YGaOO?#QC2Iu-D^ zJnQ5H$=1K1m)2$)6!CL|zWFdg9kHUaaX zx@MT*_GII&HpiJ(av2{Z=-qp^04_oqdqZ-_6ql|UH2^C6 z)8*Y-UE}F{WC~xqc&fx@5Qv*qhLK2?pCP=$=kh|i1QR45WcI^d)83HEN#oV0F+jio z>WBv66jJ=_Zn{{hQ9Ux!;%n4b-Ny3qU5kRHrFaE({{XzmJ@FX97 zkf6I<|JBURox6f)J}!zJ+RQulC~h@QPQ4)OGMo}H(Y{1N+u@l10RqO}@EZYxp*DUQ z0b>g%`6mdNh{@SsyoLk@C2(4BCt&D$UhqJ`#{?gb2LjT2_+*?37@$Rj7n~@Y7n}(g zkmq=yh125 zeR$qMW3D8w&$B|}v{@0^_M^j3cbHI9A9Niy@YK7TXso!Tuw7)2Sv!XzbeP$G>MKKg z7E$JTvg_hPX9~UX)!?LI=WO6_6^u#*aIqP^q>|y+4Y^L2l6#J0fOjVoU1NDfsCbs1 zCZKWfcu?R209s(g02~2I;THfc&LF>FBRc?qmSbxNnZaO4w)=*+c>%^x1^%rEHoO4v zXS@p4fQKyL`8ireID>mwn9m43U?3&jSWg%LDK0X)dJk~}TGZ=MK|_Su;5@3#oX#<@ z&mi>o0kF&ZXNV90jhuPkSr7IIA7YgUzrat$AA|*O{77)4vxl*mHUzxp%P!UWXMvWj zwP6H$o?p*76tgy{@wcohm{DGRcru#KE~=P{H2ug~Z#E235&~R32t4snbccRTv-hcJ zzO^N;o^F9NO(V7%G0t8EwJ$zKUVF(sV^Pb%f)@r@fntDaG`wRAOS`^_n7iR*F8gt+ zD_N^9Hq__$)of>a#C9)l`VIKv?@$t86awr|BJy@^_s@wOU&QlnH;OzPq-(W+pfY@$ zP(oLFEIZh}?3?|`IbeUXPM!d}5MYI}SKgZcq^9`pj7>+Deec%}HIcPH;1MKX^E;fB zl-F~{*0Vyb&ZG74JzzZ13&;~J8h$?>sT-vDguwCQ8pzI2`$OW}TOjun$@vQk`BsOt zlu|R^L~qmb;Si?I6*e#e**OagV=#cn3E%~yTk9KnZ0Y(WYca)m(&)vooObt<_uH;% z3Xoo;NptZveOHHv@}bbh#%knFlW;#y7-WDgpDeGP!=-b_sAVG(xl;k~btl-zT;7)n#8L$rlEn`@}?`j+@a2 zB%rd<;XlTQC9J@51e}kpB8Wf@qyw`TU95E=0E#~fFcjE|p8=G9GZX`79Ml&&MMW_D z0V)N3)P(^nK;3@>D}X6Wt;ObsYxBDF_uu0`OHZI@`x2i2Mu07@kKdW!?E`YPbq^7s zI4%M<#H7|HD9JALUYL_ms0-DFbN;u3IK5}d61e0Zow(?ItmrZlZ7`Drik}44%0L7C z78Azyy`HYud7ArzNGz4Nh{a5AL2+dfbwVH-V$`bSfE}h=)K4=lBh=HeE-Lto=*8MBAS&) z;>2U);d%lhLYU-m{tssnZ0Up!oq2!mOF;PeE>jP!(2v!7Rnp8p3f&i1&K#yBRFv z_-Tr9ttp6Y;ie2vTeCE!&#RoH^;0uwsr_BhB>qbz2grQvyOr6|Q(<3PRvtqYvz@3oTCOLl0Sp?MR&IgO4$ysHK`SWHs6$#i!JPdmCX%vgFEv$LSvT)4q3i53WA9A6B@t8q$-9%Ri5-8J$S0ol#mW&> z0fSNFJ7*DW1&D2^`|mi56!F7w?$3=HkN%ZW118-;`u)$Mh~L120t#(M@5v-e*giMi^}XclU-Ow`QYH&nCI1d)w%IwU)b~#%V<1rfB&`E)T?!`-cY7NhVa&U&p48v z^AQMV>gsJ?T{HLZ>3M8F@{EMG6ONb@{9!gC7^tAbA!3^TP3P~QFN@y%u0HyDpLc$4 zIL#S}`=snkj$?tT`aBa2yUvGMw(Kc#lt}jHEN{{_wi3|zdbXBnLKDxWde&)LLD#I57a2`Ro2c~i1JXEAbRAu$`wE^TIFD)mp0PHwEvKz0rw2So6lGPVwdLgWWOQUzw3Ptwp{u8&B&{p0 zD6b+TYoM>Fq6atB<2g5+!{-6)c=bjpWz(;WVN>w&3RxGqaIzm zqa+_WwDW?27w?yOJrae`2RCI4I6T=vMe{TGtzcH*c!6@Q2Bz9f@Jea3~PGIL&mWG9eH=GKdtu?-!cv z9KQ1zZT8ZM(9*WkJ1jaP-yb9KbJsB$c3|jw)G&tTJnjjKB>S9Udt5oObHQ6y@}N=c zqh9!!+AqIccLiTefbOv(L*uO)pM)*Zs=* zRVRNn>c~|0#aAPU8}+T84syoLar?94Qxg{U^JUes;lO#sK2dDW%25CEE+*O7>>9<< z$-7G1WzGz|*iLf!&0+To(Jm`Tt-`uJJx%-XY**{{W6d(4RZnwUwYE8CnF|Nb zBkjCi+k9LZm%vGp6Vv9#ob7bt8n^n%>USj@u1YRX-Z_{i>%(cuEhaO;D@BJ-sKQRf z`8i|pQ;mHv5lOM$8+>?tb`bw-&f|Y(d@SM=mIe% zaUgLE$pMl(qfIQq@AFPL69R{5HIPa>7O!K1HR)T!xSSmqcCGSV>y!!(si*9Y#wa4*jCw1*ylL3II20xIPEy|xDIgL;~M6A%dNz{jXQ_ChKGVjnCA#jBF`w# z7v5`pwtOCZzI>tls{Ew_`!?}y8r(D|XfBv5_(cdIBq|grG$<@C94>r8_?mE?2tvdK zxQRRx6%Y*)jS(FalNSpUOBBlyD;B#a)+zQ1)4mU|*Mj$%9%6f_l@6ptt= zC}}D+DLq#XQV~?8Rpn3>RxMWZR$EXfRxi-lqv50RQIlA+K(k!4LCZ*Mo0bcbA1Mx8 zM4FJ@$T8$ws+o@-*Po>|k|HNR2fxn@YVTVziQ7Ui|kv7&e zHZ`_3b~fI(nSFDiiG#^kQ&&?T(|FT#(*o0S)21!uTMNw6&11~3o3~kzS?F6>TI{mq zwG_21v23$Cv5jk6Fjm&%=Vr!r)&n&)qFE2wsoLBcUmo9JD{7%svzy_k-YdN;*K)g0 zmXjM;dg|OGzJ$W5bV6WahE@C z?mZnutm|Gmf;|4&h!9rIAn3|h%pb}Py(3m@G39CPl<;)fcjS_EU60_Wp*@9*4^MbQ z#J@fM6X73sB+&MD?#r*TcTr(mtL-C81_@Y3*ss<8*+Sb+PVb&^Y81%M*m9XCZ9T|kzUEfXW+wGNE+!G#+}41;E9Z!kl!9|+g0wm`nVXHHgYYvW}-E;dq_3-`P>2b0>*%a z%QFrnrZME#xwL)n=SeQbMBZ(gqPUwV>TpHapXM|pgVXdKoY9Pnc;}P>n(+=aDj>FA zM>8H+xTT`_qx~gFO!Ec1o$yOy)o6Z}P+fKD9bO6BB#3X%Db>|=?vATuyQ|!KHu4O& zfsp5|jcA65d&q;Ny#Xh-8`^#%K1vleo<`|-a*6s*%HxsDw{f`=NDw9gr+(u4-WiJ9 zfMz_`tPf44l4)+LBF9>+M|gl702QhmnzrzRNubtOhX7UtcM_b@jK-Uf zKuE9!$c7F8JDYuGwox<>d8zGY>#BfENhdpm0Jn-Z#%h z!m<8(G^5u}#?2~B9|C9w0DjQ69yp>If~XYz#XmTEzYxuEec~fySrVO<)iYihr_gii z*@fEz){A;~m<(<#ag|Av!zlq1tpE~2Ieq*CXhyHyH#7qb8F2P~8JdA&VtxY6i0W4P z#c0NM$QsTW?q~+%<-U<8m&Wh7*k| z>aSfj(Fxi!u1t_x)xY!270)H{={s9??NZlpI6(Il6D&v?Dp+tM3J!KK)h4q0b)>Me zPX;pkW%i!fn+DRkHd{*h=66Y4N}%`E=rpxy!;4s|JFOs+di$JohP5Cq{f&yIV1~xl z!H8au0=V`7gaVtBz}6Pv2xd@lGAH56hEN;6IRxPod&f;UgZ-AE&ZDpn6Z07v*ppsr z3W1-tO?E!=@rW>n<>*D73gHC;hQq{8bWs!LI&2Pc^@96Un9m3gfz|G6Ojw30U@^S8 zt8&}_KFgtjM8}H&>7vcLKDjyuSdQ50ARTECHb4IGmtu-`GAnM1Ecobx0|m(qX*5FlJVw_y7&az$bGX@IH1De)t8PHq>)1U~>@tnva(SDxpd5ksCMrHSFUe9z}7J zJUBsp&_q>?b?KP!aaw>;fP1RIMdTS-Sias;@0~$XdyE`j) z(HgxB&t8_Iayyh6w|I;LepUuB?EpR>DaMl{eNI#vWrebf| z?0}%d)m&0*I@PsOl~YOroGG`ACa5 zm(bNo!*lr6K%@-O2V>4~y6ew()#^ynBGOD0!u|<&k=WdpU*IkRuEEg4LnjU!%%o)h z3+^IC{AkV9kGP9~Y-|%4CpaN8f4A<`Foy}t*ONO{&y+tVXC~y=K5cl!KLX&5U<2QS zC7SEm_&@3{0xo3zpt}h8P5jsHBG_04j$Ib6e#*wONrj__wfd%znTcnJA8+;2kKLPM zdVi7NK8{QVD1Z9r4+VzA2=ezq%U5Hkf?3|FR%!~e6MxL|(jsW1?%5jx*ArNzq1Qq{ zO8cF=2)1`8;j~3iBr&3sGO}l^4lfab2yRIsCGr#K6uVzx#1&p}oUq(wg z+sLPYsRRV_RJ&faa=TRtDBZ9Zjg-{C#4>Z2SwtBNC&0xF%WKcBjNnx*?q z9#^s^RmJq(>cM)tt>;?RDj~~oRO`kjX&@%T(;n@Besz*8Q&?Tbu6paxx z-|3%m7lDV}!GsHIhEaR7n{Q-BmP}}RQr^^@pyj9<L)X2(d+$tL zcljSLb7ni|*}u=3b3X6$iHVCJto>Ex0(JSX+(n3Gx^cIb_u(wrB#*$Uho9(pY^~RA z_ItgXFE*|_ZPl&Ny|aYw^-sHtjLE}HO6N~7DSNnZ_{+S1FOxDR4>Kv91SaLj+(kf( zy7VHq7fXEfO!REOao;c6^LU$o&bxzVG)>l_+pPERe%=nZDEJ=XfCt2iU%87AYq6)X zdQP2Go44?~TAHzOu-miW{rY00q9x{$=H@S5_2EN*1A?*CTqH6nplJJ#>5YwX&0o)= z_?D!4$MaV7!-O;4bH3W)EyaEHW8RQ$wx{l1 z;sydDRRacfRb^!Y)?u9()CZsPQXz5n>fcEY$rw?jmFIFq86gQN#Qn zi5l?lBapxU3X=l6i?D9Vs)FJaDEn!kkI)-$ig+*CqI2T` zBkmTH_qxeICei)1VPHSJ;D2$U90Y`h26cclmPYGz3q)`iY5JXnY1S4xR(ZG9toCOZ zJk%$_a>Q9!;>~DD=atk~7>+jOmiSW7=;q!E-4mCerdlO(wo$UqG(0`drf~0L>;62? z+_h<8AVvPAWI+N?LBcY>K|_8oVY-K3-c^eUI^W$^9AAd+jNhC3Ws<4sDP3RbeW7@_ zQ<%EJu^0DLj9SQNo4Mkw@B)mxq1ABr`s4gzQscJx-z|yZMApN2NcdPmItJj~vJ8IQo zge`wDf58HG8Zx-CM3#N}z47z`+Cqj$Ti`P?;8Z9691H?M`hm4*n}U78YznyPr~>U- z2r7=;Va)w3mu5D$kr^@9KgN!QGqilNT!6m8z9CuQlwQOxVn@%!egMF~x|F`G!2(8w z49pZO;zjbU1Wl+!o-uitQNag79@I`??G9`Te7am<}mu-wtFjB}hdqp4r@wwDt zVUA5=MFST$=VGPhJY8Vw`6w(l5q>47wFx1dAR#R6sz$t5B&>BcX zBS5yOA<_E4X+&QQXJlxgW{grptLh^)z;18`U=JF`I2;OtQB~JBMyp~l`l>`2l^VJ8 zs|_z|W6!)|d9){!_oKS~&T~iSqZ@Cit(j)r;PK?i$HP^;_DOwQcPTFCU%3D5ppqf&!!ncpxf-c4iN)`OxBBSz)7YQ*D zZ9vtA2^ZhD?cR)Ji5n|8ePV+??6Vkmbf`;0QXJiwuo@QGE-@-jADdV!LjoHrUX}NV z7m4-A_HLrsg*a8Tud3+ecIAWjM{CRURXt1*uYXLZP4FqvLFC3UAuB&)Lge7afG zL8rdiZ9nYYVVJKGZDgP&jxUV#krz04KXt~M3 ze(2z9vrk5Lxr$VsVM3E- zp5x8h@g(<}T1XwHYF)XH%=K>Vo$ZPwFe-kVC1=JTDzx7lFHR~D6#2~3aAfaooeini ziyfO2L#2-+g{2*eKT;aGQ!zZaxX-_M4lCytCqGiO7iTy_vnNTDIHS_7FF7eF`1+M= z#4Ei=rxQLMkBq%39y6t%Aa-_Q9mCZvnnTh8(GruK-xhQ_DC#)gB#3xC&eKR`HIE{H zobX!2jSh~_F7iYemH$e79wb9h;u#ff5Db2wQ5mJsrAVe&p!B8eqmre1M9oJXN!?D9 zPE!RONF-?uY29hVXxnJV=#uH4(znr%Gpu9iVYFw=W^7;_Wny5GWJ+VY!Hi`dW{GE| zV|8a8Vned^usgB`vuCis;b7vB<~Yq!%4xv)f=iBTFSh`99S@c#foF(!EAMsQ1wISD zX?{KaDgjynXMyvA{(`pzdj-dY)P(GXvW2RJ>4l|*_XsBozYv}m;S*^R4HT0QLy2L< zti`Fs4@js=yjzvGszFjnQdKfZa!d-6;*ye=Iv~|9ttcHP-6uUGLngx`<0BI+b6-|W zR#nzQHdl5=&Qva4u2`;7?vdPpJW@VVK3P6nzEu7`pi}}CA{CA+WGhT6nkqUfZcz+S zJgnHG6t9$_RE*q!K)8Y^YPHTc|szPL&{)ew9g;c{C0BC}uUr3Dc*VsJ2?o zNv%)qi+Y*{R#R3}RntJTNh?~LNn1p_PA5VqMweMvM7K`29q&Wps~3XR!5U-RuzlEZ z>}P!neI|V#{bmDa94(FmCxRQmjT!nINg0h8O&SLpA2%^G88t01y=JCvW^U$Ww$;qv zEZ8j49An;SvB#3zGSo80@}lJp%R0+;%RZ}i>$=r9Z1QaG*pk`m*;?AV+VR^-*j=={ zvu3Y7kNrNPl*-S|jZ2iuGDngNm)l+C_gKW%J_(K!{~)!C>{df7?s3KLO=`~04+2vZ zA$kg8(*dzC4^N;FGsHrmRQ~c5`Ws5+pO{8}&XI(bmH5Q80DAW?Xd zAMU$w?1+lUH=6vwEF)A2hq~Ce+`WpJv5>FpjJ}p`orkfK6{ZNJAlu)?N|sXuwvZ%= zSji#*zzMAegbiK^$irV+6)720|E}eAoX4=zddIczGb|6J^SvJ5Xljvg;qC8WB|yRz z4XWn*a!b(6GR5J8wt@~tWZgs5aN4J1;}UPBkF}neO8Odc$$X~eK(H5E{~Aw>c5;hr z+$pjzS+@7R$_lDKzBbCW+3#Ns0autMtOT#hvxj)$>ON!7h3ROG-8hwceYMyQyUVWh zFKJ7ywj+`w_nsZ_6=V1XD}kkJju77>ID(hIIRR9K70x39|I!5#SpISyBnYQaR7qbI zd2$t9&-OQImV%C#P8aSiy!3%-E>PCft~EX4Hn>+XYZ%xEm4UorJB$~h*xwd}emiHq zuN=u@vZgUJXV=Rj#OK`+QeAIo-&2OCNW3_7nzl1yuva^Jt@oK!t8IJ_MFe&;4EiC? zZ~PLR*)M$u_t?qBt51CNc1m4n0Fy%@URS#yjvw(@ufLxD_E%_(<9 zbH4U9k)`Tf>0h*#QzDX|zg4V9g=DcGJSNPV_(9jJx=)Ke`>tYBbY9p1`NpU4KFLM7 z#ITENHz;`-B{J5#mw{&gOQws-3C;J)Qyt{j#VR;{=q@r?^+v_jZMqz6gx|V$aorE4 z!YxfIC9)``mxk1r#P$9JDLruKS$fBgAd&}uE}-$gQzB@{;^##2wvfUykvw2e{-Q|U z7BAn&l0u36m=qnk@=|ovUy-64Ldt*=`A0nickZPKQ$$(@^q-(kj>`4po zCCdf?5aKTlfNQ!s?ab9YjeB1;433x7VpT>98jegE+ATP`2O_e2`j{3DqHN%tdWba$ zK(>$$egJTDZ=r%<#OX%|0E*QePl+_meS{4>76-upZ%X8m!}KJ15@*V1noxDhyT$hn zckMungujZ{R^l#<5-Q-7KBs>_39dp=csdAX!lJnE-ul23;!w0YZqwI#QK$IevTc2X z7SN9AV^l^7A(Qg4)Y;MqrI>OyHL3$jiI~IGQ%V^Cr@cn#(sB!DUAZeFrTV&rBvmAqfV-*^>sQKP{Ew@V*b$ySe&&R6xEA zIs=P0xSSnx@QFSAl1%U1Ta|qjyiSpxgGW<&cx3k7Smqr~sDOj^zNp*W?1NKbXfOtm)lvlt`+NnsF)S*PqN@`WPqK=Gbbks*h>bW8fLQ$9JSkzs;+7h z3LJ7C7VfQ6J`g{HDLuX3XuH&wstg+DgJuBLI0l^sU$&5^(mrR=F?Lb&Zm~+S&svTq z_i^UhJq@J?W+v=IS-lpyj+4-3GTJrZW*iiFIE5D6W+Q0PbCvKe9#g3y=XvU2+#JVS zme|8xlpnQ8d$N&v3v^Qd>@|kO7@D+Rz{DJZvH{PMC+=(W{^L5UEgViPvkNzb6t#8_ z$u=?z@+4i1X~JnhYc$Nc~E-FlX#8zvri7BW<7_wf>E|5C%{?Jz4HnNV|diD(;O`!#)S0XG{DFjIw~M!2n}f zdIzu`Y~;}Gc8}Q4F+I@fL$7=czo00$^XJl9n9OQC9n0;UxpEPrw!y+avGBGqrb3QF z4=@y6*SpeogYnEw?oj<<+I``fPXjNvzuL1sm;|R<}#z&q`X}e}1?tvY9=~f*_x!jpIq4=*mqCkfQ}NWbQ5tFHqiGxN4W&v_B2h0W|}|lalqeeg`Wl zryiwSS+mfIBsegdY3rr)E6=)dWl-^)o|Fdd-wegV`v)CiiR+*x*zO*M63OTdscUk> zuz>nF0q7GVXpA-+7)D8eXi+NqlF}j7f>8U!I>qyC&G4nSFRy9}>>7`b-NwX%`C33A zT9Tmw7{~!5kC5;pS2705H(ty$ouIV(V*VSU+WWV!u3IA-SFDG4%=Y9<$1U3#qkIYI zB$PsCQT^=bKsa;ArY<|%3XL+hvGZ}qj4t+7c8wa|+Uo5iw3dwg#Um^OluAelj9NlM z#e~)PBC>J{I?aZ#x*=%6ZfLWoCi!%@Me)YWL#&guMop4hO!pPi!k?nYVPqu_xqgYkG3-DBv2WPPqN8Ni0o!z z66^&6CxlqwH3nyR)t(IlnYnOLLy8ArX4hQ&|h|9uyZ=a4r=iXQ>#S(0QOt zNxIT>(&h6_t~d3qXRt>rNBB1-1O>%4G15k2j<+-?r?WMLbVzMNu9O!PwEr? z2##=suYk@<&^MA^evJaC+WcGc3tY&mGY(Hodcq~2m1Q>~DjE*lGc3iCMc{|y_kIe> ze+{}$DE}i+eq!Y#)=tZil@bKSe-&8)zhWUQR1Q@@mFy`2oLcME$wVF}ZgSbJvwrPS zCr_)%fgW6XS&ZH5fqJVCD~7CqR1pwxynycDhs8|<>?XDVDxH`j?PlU=d*@|0#Uh3{ zOqEU+S@S*GFD$Ow_auyMwm&C-`i4Yh9XvSbztBY-zPe?KMt?lAgMLkLs-RmWWbx9C6B~PU11GBN_RQ{2A1aTx@#HHscW7-2 zLr6o_rRfBwpc-_Ee+gLug^v%uH!SPO8u0#e$Xey$O8@=WPv*w3LwDP@8lK4wMRzotCeOg%Uha`u|8hv!!M zsTYr|G{bWWu*D!z-4BP8!4G$D$R5gwc@ZK1)n6z~^ONmtC3&a4Z0Lb;&V8KXzph*$ zK5v0qp*94<4Z24{rseBTa0c(VT+ExORrl2Yh|tW0V613{pt)ZG%L_-RbsmK7^%i^5cK!)uCDQ2-{u1)vimZVA(ctrk z4>ynTkIzVNQHO&MjvDqRe^xq&7L(baa;uN(zG#5E231OCQBtd-T3;;$mgN&k5A+1; zgD29bphbzb*nZi`c^<_FM)4+Vt85C%27Fk8RqjX>ztQNe?+wHZ6|Fp>@x8f7uYy1? zvG%u;muz=vi=Hz#k5Ym@NHiA?ZL?*L3zzV9qT97Jm%_*jK4SDj&k0rV0#pI9A|j+O z1%D(zvYt93QeJwNY_RjS(TS$%XvMqj&r|YqqvV$%EBzpFtT?g)DujX1fBzk11rEs* zF`HNUo$VN2JsT_u)rOxddiLR&h~y>hFtgqdk5jDi2bbV`FtSn$fv7PEy@ZC~sPPI^ zFtG|G7U~E=c`<4XLnCn17=>QLQDYnq7-R6mo0X3mY(E_}V*in-!9~d5e+5~Y2FS`X zNC7|zFtRcaBP;Kr8F=*pnuS*YW+9LxqkyBB1JMau#!)aKIh=|(-+T59I-n25V@Lfj zuTFc+6gTG#(pWfw&ND1 z_ZN5l<^x3=C=@Ek+fEPfunEjcA)B(be8=^6V8)#*ZzK1&_iQtBB~JD8_a0>mn9gmM z4g)DJM^?rFEAx8^<45VTs{Z?t6=0eI9jYkIqJh4EKeZa{V%_|Xt;#Y)<$GiO0O#0d57UYG|ki02VcXo*3w()qwGdAyNZ{F;qqaP6dZE z($vSH(8_8mz&}L=Wo)3KY^Z@$RW?9lR1DEZMmP)_hcr~ds9=yt6*YC7fwGD!@NCgH z23U-dnz5Pz&WH%2Qro;)MtgH_(<^Zf>X5oB^Q;|@4KMiY&)xgBJ$XHcNbmhZ32yBt zs4vH!Q1Kh-SdZJ+cC4`>4{Bw1A1bn6IJo~l0Z}2gm!C#dYVjioK8=Qd0v^CrgCE!o z$4}!w1nahCh)Om5y+uUD@~z$+!A6Ccf|l&Df39dUjh3(0Ja|$vAv{;!dNG zece1aa}O%L-Uczi*NZ|-ymrLS9GruvxWA^GR2^8OB5{es5!O+J3t2cm2Uhzh?| zQj`$+7oC)K*94|FTo##*UN^n-)2_SDl3dlMw z38T64-NDJP$RB8@RN57kgoY-R$|zC`2fXlM|CZt0`M7fgx3iX_HT=^SM`J~+O&WZr zZ%>U3BW$u=Ho6Y2YbPTCQ8`1t5&PKgy4XXe9Y^M>(4DaqF>a9@ci*^ESvO2PiqAM5 zqMuZjGT)saVNNfH%lkHpxqg0_BC0oVq1UJ)xm=7mqSEPctu<=nCyRp@s}GdO#$4Xy zJT)30LBlq2-`2(EBZI5!e3c=YKc$uEst5G0!V5B3=}2ZDruB?c>1&;vG!{Df;P`w7 zTyS#t@!RaZQCdM+ULE=CMP6CszY?DZ$q>|dL}ecc2EUJ}R06;rPNS#Hr4Ck@F6h9oGc67WWYzd7ekSHoRwf zXZQm7>iL=YT?NPlRtvNV@(OMjED;J5x+gR)^jX+M*hBcL@O=?}5p|I$ksOf;Q3g?G zJgD+WtY7S%*u3}+33-X3Rq2xUk{*(!QZ!O7QbAJPQnS)F(pO|?WQ1hUG6pi~GVf&O zWo-bUGAldeK&MydrS5j!U_AjnSv^&~D!nGXZfq(xOJ5RT zp~m|5`Wy7O>F+WSH@J@5jPu0>8)_LE8nzgP8|xdJ8@C(3H92Q$U`B4nV3ur_Yj(}7 z)~v;>)2z=t(L&hbf#pui$CfXxD6E*Q1gvDO)U9P#3)nE(Lbf8dSlbZW3fnujU3MvU z8EYigVAo99C)>X#imB9s9s@x?IzKL9D)i@8d4Za0mxTmQ>t@fHLp^y9-{( z0WQ+W`<=St<1E0l%11f=ZH?il@;UzZS}u}D+q-T`s|5!A4(}%A$^M$wg&R zp||9sf`91-i7e7D_?PaG=-(vRTf}9|?s>FBl8_XNUG>qr=bO9oFB=aT-3d}cGM4QvVnFAkAA1YRxhwr|&O4pd#; z>E6w~O^|=8&vfYHk)o0N89UT=U+|Tq?!NrWg2Tx>^*HIc3`0_)G!+O3z)}UsMA--7 z*#v6VC`^N-A(>y}GH&okRA%=!`#e^9df|RJFS^%r0o=5eF}&5#(x98Kv^ISJ{bb$L z`&t0J^uu2U3-eJ4^y$8gV?$yOQ?u94RcJ1RDsZojzU3FrL0a9Fg$BsY?C;{5Z;TK#+9a{Z!w15Moyo?q=K`MWd7I1(qRUkuB zaG4*|x#$`xX$ZPy4F0)+wZVTBh z;xfoBTWtuC%Tm|2zh&6Y11wc^36lxQxXi=_Nw6n92)ld$ST7p@^PeVZ8qZfhcvG*~ zCs&{9BzyZr6JObSgBNN8M|{@>-`c}bkOtSHM|!saR(>45q`usQo$uXzr$qxu{HEQ0!1m(bmKLSWQWG_ z(i@buHyva`%Fv9-s3;~oeH*Ph(Gi9C7;$Is)hO|nN=hFNYAulqm9pNG6FdS9aI1h_ zib3W-JT$Gn88_VVNOc3a%p#jY5-u|p$M7Cbb>(pxYj0%_%MbU$04@V8RUj*PA|Mf$ zk;UiePafcXtspLA_j<>3N4cwO)-)tEQYc!M9k=y4l9=!R$kFAw>^VlA4{%PvMC*Zt zY~Ymt23*G4`#UZJLI#}3is3SNMCB)NnWQ+@6+Tj3gjYz%Wkk8eMPPb`41tFq;A$Zm zmjO~k4`>l$AsLqeWln+?Ar_Kx8T>{7Vu7so$Q}m257b+= zz%pg<-DO2AdYz8Q8O=^Vu7>XX5!RT|iS0MdurJQ#>0H_>a!!X|=7QzAC;qh_$CQT8 z793^Py?)Di_Nr8yRsH(AZ_^fc3sS-F7F^4McRO8aKVyEbfwtO~QObELGa$-@Pko{( z-W9h;Po!(u>TG6^_w5uj#sMFH^QW;PZ_B24d(oN*ls9I!t#XW=+2XPdZgM>K;tTme zJK$Gdz%TSK(=WmhVWBMs7bPxR@)>0T$C3*+xYeVJPAvU|&kvayJ=zv))ACUXy?C9zvvQ@CAgium+4_}1N6%~OEA;_b^3)MWd$cP zv4DQL#8ndx))Jz+eZ3?)u`F%R4z^>dw-GFYfGdlvnN>S*X<7PAxE9ba7kiU5z-Pke zqvIv|$;crV$RFAX2oCRgclr|_Ki7Y&zBVImBelybN;W{04EwtwC{j|jLEM)QnfZ)_YvCuU#R@ge0aMEKpw1>>D=G2vqC9BVpp|+Pl zm>1i#z)yZMIxVUYvm;!iZu_;0mm>f>=~n@QX~1^$lCcMZmqU~v1|Tfn`NXQm@nyLi z?gh^t`xCy_I~pnr+=pzv%C3|Ivnn~Ceny>AyNKbOgfal;V)*LBPKzF%LHQ5)8y{&M zU$qLm`An@***@+4SJcHC((_sas;B^x(hlrPddRG*Ll5>|Ii+huTho*nefM)FW}}~A zO#91J55qNB|L~%>*s$s<5gHaK8M?AWs_cV;m!9s2LSQ9cD0G00_Ikv{WR>!Pll{Ct znzAen6f9yd26o0|7jCB6j6Lzhg%^06;1Q8SP$V0I(@jD1B0clf=Cr)H^wT!8{BO5Y zV`68UD0fCEJfX1CfbCU|K+*8d4?XEQ^pW>ua6 zZOYuDSlB)WMmGpR3}~6}Oi4e$G(xg8EDW`g)-{krJz45ATmFbb-DVAL>FP4h+u=#C z$ihC(B#{GtWf1=EPSM=%nVCdUuJ~JY6OGM}-@o2gS!MKQ|6qyX_V$}Ca2=CFhoM0N zTk)2#dIPwZeOu{#kIqTc3apK3DYzySXJzn2_r>gprAe<1kC|sh7$xX$El>>jCb7;= z3Ho{q6bB0mKo=#X&;h>!LxAwA3otK4IuDTBCqWqiZNU-kH%Xw&99yhA(9^!x-7^?l z0lkTk=2Za3DBSh0JyMMvE0FF@e-^XHuCXbn%Wu8cjM#?jm{MlBFg84|0(wF!lm>4M zx;bIvERc;J>f%k8BQ8NE5$;A4B3Zpfj|XI0;&_H{Zv= zaMp$eghNsmz_fswNq4L32~&WwnywaQ>cJNq{$yFkv;fHlL9+$(AkRoIhV8`5h+Ah84OnADyOr}a`;Svg z81K8~>a}l=JFUy$mJBRhMo?V9w0tiH0CNb%=mN;m-^H}_!-1&)y0~&o3pn+KfNG(- zA7EP2yCIlp0XQYhw3I>Rcm?eh{NpuHbYd)oultvZ_H8wt=?7w~ik{jG(5ndfr@ZU1 z;EH5&E54AcN+Qz%>R&iSId-Bortyh)SeWO-H8ISC(dXw28^+0TSE~f)7d*1yb^=Fe zfN25k1Z4X~0A?uxY9k0{Z;0iSdzbmCL@R-_*}I)Xs<2yQ8kn9LhT6>p1aA`7?9$_{ zC#kd6!ddu}ehSJDh!#TmJ3#q~m5*3EEn`|r5mf(Krp4MD9M1rv1-i+e(h<3db40}Rj98{` zz1$Fs&TZV(pE9i5BQCepmoc5-b)0fl4PPVT*ODFhkl3 zFtVJ%P+dhQF*AuoU9NogDk*^0SV7`O3>Aq|JkaPuSjV-aqBH&6LiHM~%{SU3s81p% zt2SKY2+p<7Nubum!kq&Q4#2b!2FGncv{aLjMTaltnQx1OB$=B;bJL|Emga`5(RP=E z?G=K1Xg;<(OAw%g8qg*FC8h-wK8D9Z&uz7!0jk43JQccTt&{ow^Z4QY=*RZj$_hK&nagti|-1Q__38Jks(! zpW&I77I;npwi+bL|3T$)R7>`o_VUhE4{{903{gQ=1;xU+;>y=RO_DW-zJVIgiYVn;ib$cbgy^N z1kAKNfPg=s2=ovpSxkXNA9&sYKXk5iaF)WrHZ1)=!L%GdU5~#`{P!{~QW)5VrJrEK z(t{5=y`;COv({=MZ?|OR)C*i)*nYOGFF04;%KW~N!or#hn$*@xlW>bJpGcknnB@gL zkv;=0O02~eoKyQbUUYlB+59MB%Ed#}qf$sk?y0k={;A${6|3=>zX9P`YA({NU@t~k zk5yQv*z2B8h^@Lil72VaH>5%E^qLo!$|mO8%OnEK55r7rey#Gj+JLxq%g1z3+TW94$}hq zPSol%{c1N|Z)95pp1o1rK$*nmt!id4IUMkz)We{9jmT0T15693U=TF`(E`1KqsB0( zU}6c@x~XJX<2|NmKo>+yqbkR!mByx6M<;~SP2yvnfROGEv;!;t}nzg zG}la&G}kJvVa$}?+qP%TCbRS^>1;`#H1j=8RavkNC6H`?zd_6JE}=zzJ{q4u<@3i5X_wn0U8J#GmlxKRSP1Ow6q0 z7QaNv2xcLbqMkp;%wCGzDD_SM=?@KqzReTeYzcl0GlP;oRQFQ4qT_BmA9@}JCk)2> z(n9&`ss^?zz*Ge~0z2RUAm!_?Y*>JY%AZp#%Rc>HhrhOAf$d1);q8CHhGmRS$d@F>MMXwa zUByTZqmNcq)(7S*DjG-?Bno3-42)NhC{+{+Wr#tckOl^7ss;uc8mb1$MyiG?XcZ$A z4ymuKrh&#NYZ~LwI1QAFx-trhGFCxjaQa3DD*DDKoSHr`X3@u};0%m_A&a2_+5oMl zg2L&eRM2Y1%KAn~Bcw4Bqeg^rxqWEF#hC9=KoW9Q&ABj1F$Fu3n}Ih^c?<18>&nw{ zpRk>`Ojc(jdvDb8MsxaZZqj+}>if@c+z61*K_BGn@1I;tU|fjp<);~!+xS+7PpK_6 zH<)_xSIHT2p5xyFYlc5yTz;`(Att|#7dSdS?;NjJC^UJ6#@mBW(o9qf-E@3MA{&#* zxqVs2usnB(ak;bpaXK@GS;$4LRU_@5{`i77VrY(=EKe(3L&J7rSKW^=E`ZMgh4{;$ z5rJ`mm2xoSGC+6gY}l_E7lT&)ATqP`)1)&l#A+82dwQ|GIid6e(gU^N3^ly3g4f?pD;{pja+#4jY+HcZRQlp!+oSAtM*~gM zZ*Wkew-OkaRGYWH@jN_-HmTlzsLgPb*KGI6cMBH1;_Z3-kzKV%`hSLT+4e%YjC~7b z=%x-&Az|6R&zqFXtCd>fSA`tmaGdwZv~2VaabNcilI}um;(Y0D()ckPvH69*Rzz~q zW2apyg(wmjms4ZWkH;lSpbMX*526ul22ZCQGkB!MW88Lcb?6?aKoor|X1RcM$WTv6 zpxbAcvjt@l;9|bZ_355$t#?7A&7G>MeVOYLtsOfIVa8W9@tnrxZ^nv$9Wnxk62S|M78wUV^b zw6e7FwVkzJ>zvZb(J9pR)b-V!(Yu7*g7v{p>(lGsHgLo73etVkwA4a$*r0p8Mz-2>vO5 zdG6!n;^u+U;L^ctDp>u!a~};2B>2B`A1xik30PS2s_UMNw;gOC&KhuhQFV*&L=MD) zb|qF{q|AIt;zfgCpRB*4kKodR|K*e}Tm#i^ynOrue~f|PyTP)f9tQk2j7-cdi<*2} z-^g}WHWa(r8mhXN7XpktnO*=QTvvs1y_Q_*U|02a1?^$+Z0+ z@0v4|iK5wx8=I!0uCih5x<~v^jvsvD94tUJI41Yi+PQ0u6E5It<63l-sC9AS1Fg$} z(`zVC`ke%7d`i;HD_BJy_)xvHu(VoI;F}_mD@Z+WBuZ#_f&{ zVlwfrNLf_WDn>t4?e#15Y?WRfQdP@!_|j!w(suXH)8}z_slV=M_0Srbs(M$jl|9k% ze8WDGZ5Oa$pXAbI21{$V-OFV7eeb^ro+yf%6Uwby<<6OS`~<4%VZwZvjHQgU<xef$g!=&e-UF#tomzG zY%5JIZBjUd9}+uDuDI4&`Y%YGu~L9R_$MUHI2j{6fIw84OfrB#vgTOshxvnF#KdOi z@E{_N-@xCZc*-k#L+l=t(~h$O#}dxDTE@M(&E?5f7iX&b@TMe2_IF@5Ao^TciLerK z$+-!Z{@N(R`Rb%Z9N_lOO~ge;rCvW7k#^@ynyVD|uIxU$Al`#g1+CvfHdyg%zX(?Z z8RU7o@u++su3Hz3`gt&_co90Opv4-*sFWLzn))w;+W7mRzJ9Zmh2d!G5l_*6?NHM~ zFYoxm%EK}qMF?S^TTc5KNA(>B%Y#7lg|={w3Rr322Q}{n`Wujr?hg;@xZQZZ;{LlH zU`GNE>i^$t#q+J}(#RS_&U13t)P|8C&FhZiKE6F~R8-EFUv`z#KL0E;^@K>c>QHgJ z>GVhnH<(B2715kYqo4k(!11gRCGTTHHCUY5LgF2$97-iW&THxQW}% zxZw?IsFtf8s}FZblDLZLs|Pp0Nv=Fs5w}~}!|ckjTJA}lA>I~+M5sa@pPfH>fXlXm zP=)@pJ!n?nH9GvBl!xrQyRYSL8jU)ID?EBBJM-3l)c#v=PQX!9B-Rv8_-|k;;&v}F z6@)OjQbdIXoZp`SDv|@7S2kK&wNFofd1ARS%WneAg z_Go_{sq8>4gkRXfb}V0M7&X9ab}!}!=4$~v0G`8(2>2K17Ya89YvETi>AYFXZiKMW zjcm~hW0N}@?b{uf*_x+3UVX&et=l=Ey+hpMGA2~}?xW|=;)HlqCrlR|G&Xwx8w_H? zaDrGr_!#_EBD!#bV@60e8Rfn(mMvS!Xz@NBp4))y#cG$Tpx2cj-P8p72Wa;yQ`GxD zywh;)IYxb_aLvfDUOyGfq4VHt0aA|+RK5|(@{7FbctAf{PgIRMijk`7w3qKar=x0n zv#402Qh=1J?xD;RMX@+Y78?hU(lawIse6`&L1TXF-K zgv39sa~ECT<+l5<0pI=H^O=Q6Dx1j5lvnNSp}MK*MXbPQ2Y3Q*h>!5~15d!s#u5HI z*$~_5>}@Vjd`L{8P@;4>^iBC%Rx0yt!QSFi1}DNwbT;n-a{WDlZ1meBx)qoua$^$K zT0se0OYImQPv+d?RemPy@1 z+di=(7vJ3)62wMN{$$}G^V!=FCF1ZAHu59u=%c;QmsNZ5+Ni8E3y448eqVzpG)3^* z6(i2UBjQ>Rbs$ApI1v0B-Ly40xkq|Evbx}|b%v|x3%!JqJr>CH;1WggW^yAwW_Nt>c5y?L0y!M)qLpPS~ zJx3VDi!Si3>9AJ{ER(l2=Mife=r-;!^rb8QA~Ccs=X38`Dk|Iw8i`>G#Cg=_vpuP*KN;7WmGNrhC3)n~W9o&2^ zWwM66GtIEj;Og|Q!)s3~*CbKOM6IIDP(7)04R0TDkWj{cfEEzzu{MWpMS!mn>#;Ub zw;}&KKaDbe6-hLw4_N3W%4&0V5;KT#2hEV05g_w=ta zRSfSvoZZnGL>YH1mVhqAJXiu3ItXvXwJyDJpFj&F0FuPU`}+0zJ*#O=6!;wZdACLG z9h*y@|0K9(!cn?Iu=L{?U-RO4g+V#xX*QqhX_qvhGgGo5??_H9E zChr1V_B){L#L7mjeSV<;3g-T^3_#p&2wQmh3VTYXc1^<$=SEA2BjJ8~_lv8&O_S36FvzSo}W?s+yRz$2>foZ0a3_))v0wido2>)XA$` z_JoSj3K9UQze3dMwSZ2yQ=N8 zk5E9O1Z)4{p>U%Z24aZi@5U;XqUQ%Q`Ci>~6wRcmLx-(7e)OPjT)Nf-4*M#zS0As; zzBc!7$^JpPW7jL(yx15h>lJjYJS(dCu5^?HxG?D!sZ`v<-gy{pjA@bc0!Q1q2o{NDud zNRLPt$Rsh1>vqoER__NNnZgQuu-m+F-f=Eih3uw~#1{ z1~tm(P0{|W#+rk=kDj;h4o9NwC}**az<5N)XhBhZm0Q44Pa@RlZkp{87H1{#lbe+>wXp`u+wT`%*)ZUi^LEJjtjHTLZ2fdu13NxZ}ZRk2!Mb zG$YQ(76Th&Oq=||7K;xH`{7vF{tzxc49F2H9^ztadwm(ZD%ae@o9!2(0-S{&daL4}W_$Ps8e*{1mwXes(!<+Gc zS-U?9PQ}x};~v}+3|_%&8qaMToH80ysl`^+XQk>>tlq|3_dt&-?{wRxVP4K^CZ3n( z&0W{R&yH2nQMLp|2yB&|osriym=fMsDY{y#o(2j7`&q8t{|qGb;lCrH|82qy6Y2}S z;r$SqALrxP`)R1+_qSaVY~*b-8b6*bXuULYfIY^2r<|cYrCcsB6`FJzQcI6;Pdq2@Z+ce$*&S5-@dL^fs|WnZ1YS8BvcL&1*Y3j!@B1ZTwVGP+ z1$g-#{|TVv|L#^XF|kak|9$&kxlv3_<4?BCl70*zR#{ zrvx6+xLIK{WqRjY+To0W;6(V9&O!5j<^zcPvmiJTJI*C`oDTmalDKc>#>4t?Zbbz8 zkA5+sFajL&;#mK8O(;Ct2h~Xu=%Zyc(S}ACb)=e#vXQznS_P@Dg49q&!+*;98Ym3| zBpRiLMjGSPa0dE7Td!`YZlsRJ7^tc1tAUSH)QvP%a2O+?$v4Is8X6lTk;XvUZ-g^8 zGEz1I!hM`N_|zC3JZ-f*Sn-it|Qb*2aJge=qJH+-n1aVql{9_RVO zx`9sl5J8|%Y%f18(658L>aS#OU{=B3rH$d5Fc$s{tSy%b^lRYv7ELJ3MHj*dCKSX( zi0eHZ6&DU^$Sep-Wv;E!+v&K&WSlaw*= zYa4hyJ{q<2q26hw;;#Oq$99T(X*nq=SbvZ$HNx!-7k)Txc>=x0u%Uf$RhUjjnX1^Rw>tng~yo(^vr|H%gyJlxA zf%9+9&Nj{8`a(%0(zjP)`k8hAPK~}!H-ZVpRy7Bsv16>PSCO3SCp}6B4_{TW9q(3B zOxIf&XU;jBP7vsmnZ>Fl+mBwz<+Erh+|SDHK`|C`i~3D`{4C@69lqxqe?p*dAIRhu zL?ay;IMn>gQ`mj&`VaYyu{W~}46A8Q)lF;7`X^sAc7ElQL+PRuFcrhU5an}`kzt3= zCkOIdm%Y|~nWG{u&_5}`9TPP_UPxoTy?WP|^Vj{=J}L5&Qy8Px3ZE>I%AHi?*iYl* zdmk!H=X~_=>ftM4a?>s5dN;G)ZSg>f8y(N*Bf*5intGSb@lUQBt5KVbzM^M0U{F)C zd982KYNVTQ1C>W5l_{@Jh3bX2?K%6Z zBO4UX?N<;rPiKSUb1pG~{(mJt69oD>KPJ$(r^%ybpcSI^p{=81qLZcbrE8~qN6$bn zOOK_$LVt(BoZ%f~G7|%^py+3oXMV(D$#RNih_#TliH(Czk*$~QGdmCa7WN4CG!7~b zA&wkQJ#XTX=pPs8uPe~y1bpj41tP)JZ(aF5_o!DoUq zLOH@V!Y;ss!dFC6Btz6uj7jW{SiiWgxRbb-c&!8Oc@wO%pZ<#>{Cstpi z%zS?&(7z2Io&1m3UO-St_>Tqp_-?T5sK*lQqQ-5RK;M};7cbCXFZ-VMRk4naMUPH_ z)S*=Gb5iVIV`PqK7|Tn>eRR0=OyS$b~tu-tZMZ0d~p4!$STu?%18CQG|k~_ag3ST z4&Kw5+k#ssClj3oH`?Xo=kckD?5m|_%rSO2-}ro(_m%NB`)!9v7wB_I8yQj!tp=*5 z#ardKiq8CxKz|clzf{i5=k&erHeZfp8SVA+c)W_Xn&ruKe95S{!D&^hZ+Dx|JV0TW z-6&tmcPQ-HT>9pN*6s5W*D3@V*gjPUxAxTRjS9KrC>W~~2KGrVf8-H%aqV_!nLz*J z84jq`tAr? zy!@V*m74fb3oO4+VCN=&-~#0L@Gnq!WKayf8V0fq_&$+*{A>sY@n%=XX1%6W%ErcSl!VlHL~|uUbO=9xHd@S;LCH6s$Kn!C9g|VMmtSeLS$ZydTt4Pv zkalorcikq44!_6H1F(S0lJbSK!uqp~Uu|+bhm&sa&Cuat)$dp<|J?6Dr<3XWpzq3l zJQyPK2mS6BFvM1gRAoON>Oihmwp~>AWeS^@ls(i@x_+}5wOH6+waPL1&bwR z{|||PZ6$vZ0ZU2$H4(6_t{#>YW&aOJZDm$mY6~mZ->dxjg1UAB0|o(M0gB%xsjZdKnzH`@+tB6?h^zU+`VBFS ze$!u{Z1)hM!|_$O4JAqSZ`Wx49cBNDYgJJyOUiy&g=&w6bAR~_0(-tq=;&wPM+VhR z2^+Q4>GMazMTe_HSHDt{2KPZCU(}q!0#rxL-=sNRi~^1gy0t*;zZ3=b3kYI)G&F7D ze3A?da8WMQ@@Q%Q5-ZHV$BG-lyO1>V9WiGiY!t^N!Lle{8Q z)3=!G zCezMcaK!z!g0a9Z2977^$Bu8m-q&$o`oe+#v`Vi_n*(l9L&tQmvhd2 z?m6eWzTfBW&`~SVdWJ|HlTqwK$J=`BUD4F17Y{i_4A*+pp1-P4cxyDp@%9e$tA)>& z?OuFZg(g(c8acIodK7CtL%NT;<~0xo)&YHtgnb)ry_+b;~Xo)LG{I&1->pVQXS zh4yC*g@N_zpz8F;PtdxyQ5dkd5hgR#E{)D(m0kXDtAI(I&Z+fWflRV(Cu#a|`4cFe z0?guaRSlqm{{~^8^~|O)us*tOmoR|B^nXDZNU+Jdu(jv!vFI8knlCDfDB*@QK9Lwt z`5ZgL{fV^j7(ZDiD}VYIZ`NG1Tr`vB;lfJ~_e*K$dcB;p?^bwrtptDf!Dc(L0oF%FfxduHz&kV;F#WFv0Ki0UmGPb}Y^+*mEB z7HYNNCI_@y24#<2hY~(H9Bi8v1onQ;XPm@U&bO0pj6YQ$sn7A&WNUuN7050oWqZA% zRM!>~etS+Fartu_?04YVn4HG+E}8?WIe0JDGz%NPsb^tjQpUb`_g z43x9i*Ns~bKF3Qe4Y5JEHFbw1!f*F%zDCUsgHaMB3Ri$7HR&uYzHA$^6M*ud2Y0~s z8wO`o!HU6yT@~CAWSFpz4*^J7>SkO!s(a5&}b zA1Nn=*Ar?!x#_QZsvMrhSk%9A&4)_4t-T&ABqD#K9?r+GNj-6Q)a3@K9zOv-I`)t^ zx9@92*JlJ4c7Xa5IBbo& zxUUU(4?CCh0;gmUwx`$X%+Ap8H6IU=(2>``UzUr=g0K7e1}r}KQsF4lgk@k4 z^#Uy4GY)mXH35+E==zk#r`8+14NadSX#C=Q0X6TFgj(^#b*$a5V-sr!h6hTyI2(X% zP?vQqfN+$5pY{Dxrb`drG=4h3oqpP#X4lcZ?7gChcTKC%o{1IOyYZ30AkatX)Caqk2FmM&zT4Zdq3WqN1s=9*8TCu4v^QbF1{le|vK6HVbb8 zjf1a3L#g%`XMb&7Y@h<9XKbqqkfc5dsQ@vtDH{qv+{Q8qLy{g`xrGqE@kIHD!{d32vxicJwZ7U(4pi_#FE;LO0Bm}&()fzzZ`6!?>Yx<7BDT6 zMr{@vf-AX2fc=1U1$1@1Smr+NltUML{^durz=x8j^@J2^tn5|obPpSZeLExPgwbwU z4;B9xk1Z;IX3aW#hI!j1PiOZr*QOab=PVMtHX^(<@&hv= zdCy&+Wb4i9woGm8~)`d}Iz6Xbe~r}tP>%96xm^SFlz$*+tPMtdTR4N&lQ z#s5qN0PVh@a3ViQr0wvPV?$*}(_ZwNMJQ2J@O#8QbC+B$J!E`38#)X?yCHBL)$UtW zFf2#~K!^GdNV3`NRI!)WGX8LNR~bQFQ+huAqg0_jL#6mYvO!tUbU2*5`fpJI?%sn` zfL>5ZbbPEio-xNG5h^!HgGY8&ftt#r^yCwVZDK?Mhw4-=2d{mBQ~+p>+18FBNDX?F z)k6f=j@a3}b2+mk#Yn!8SyiQ{&>f#W^3VaUHZ6IA0MMa7deohuuD$`#zKs~vc66UX zpK>dg#XTX0&uiv>LC-mklV7z?Z6nTnf3Wg(^#W>d8W{yDKvOgL>Uke106Gr=>DvN+ zTDN<|byS8T{lEPM6`-6=9d(ED?^OXDm7z%gZ&4}$P&!OhyD;B`yKb9h89l$ze=_E< za0eh9i2-fh%C6`Q{sA!jo>O`8}Zd=$cnpg4gz9vHTcQ!?)_4SSAFQPq@A_cRRQ1 zpo*Q*n^Q8nKU9Ece}@X-s0>B-2m9>*Mg;(SOptY#gtT|1)>NQeqGEZ^lh< zDdsa;>(6OA5~F5Z;C&vbu5~Bq>wgXghZmr0(RCfVp$s+Nq6UYTufX6i02Bb7VK6X& z^c3tv+dnu!X6COA4toE{-~b8Dpnm@n6<`8V0lr6YpWRdeCZ~V}@OBzJ&AdaY0Qlg@ z!$(a;wh;MbXEK%BG-b#0zcp&OUcx7~TaDuKc+V!zrM0_XGLa{OgQsXe&YSZg3)jL& zYlvn(n+8S6st1fE5p$nQN={(DQwAN+Tf_K6e0uD6zI@$506XS-!H~iXJ;0Le;c0AX zql-gNYQway-Dh#nppK$(3U3G!xJwrn-n!@E8D(7unuv>)gO6&LoqaELDc?x=G+#RM zKxkN$g$?^r4T{KTLahz;g!e}UgVKIiFnJ%J1Vg&>u zj)ctbeW0=!EExRE^02esH4|iQc4<(3{V2bDZcbrkWNL&L%d%wvDDOX2fQuNkm~b$% zWO(*Dp{$R{5o#vNyAP$GilknAa^>V>66J65ED8Jr@ZK|n4^Guzn`Vi{Pf;njmu$5! zd3SM!t896O%ohd(RPcYQ06)6=?}h!rup%TUU**`{q$nA6{V=V!w^}8?6`_bOzd+ns zGSvextbOdDdRtFyKVW}!Gg)*qap+4A|M?Zz_QC!a&Spck1-q`JG8By;bu^E5P^7K@ z7d-2H|Ls?Qv8mtkNh}dfkzCz3#1YE_e4%VK4xFXKH?{7ByRHV&%{`!@z7&NkOd|=y z2=LAv?rlzc{ z22x8}PD@TsPD(;uTwMZ$;YVu9p(6Xsp@IFI?zc2c$t0H8gnbJBFgAVJvhmWngRaR( zT-r&e+g{c6*O2&xUYEUv`~G0J9o^y;TV?-=Gj&#U zuWtt@sV4oRSvj`C{uMh~3$pm!w%N#?G194`FyH*gu!%o9b_2 z|KCR>CL#Uvh{R42SUBi zkFICQTOt zy#A%s0#ER?sC3SgYoAY>IOvhOp+To`d#@g;J)nLG}C99R`f0R@n9ao{`Ikpb_?+_2KAar;?LYSPM zi7m6wzsa?qz4*;kSVb!7&^+D!j9-BLEd)O=IDTS$lPl50l`vn`)+WP4-$Oh!Sq#5V z&)<>w^$>9`y+WF2w|$b(uJVVk!rxZ88%B2HoVcoe?~6s#g8*d=V1JvZ7gQNxJ(UaO z6sxq-Rys|ji(?j++U7iC&L^$Jk|*BXaqK@?;v)QxEo)Xw!<)iUNTHKXzj0Kq6kn{( z>ViB5uz$`F{{yF?PfjmhT_F1to|3=2#Qz{RIbsvYG z+|6CfCwlGtF1ICJPao@9PF^Wa^wIdAA($E^jV6XnZ^SDt%3N|i-A0&Q7{o{a&OAvc zWT<9W&XtIqr#D2u8jiE&y4$Dn#XhjTS)93V@Dji9kvq?MNKml+*_OG8xtY0>g^5LqC6FbJrIK|&8wFb}TPs^P+bi}24ib)LPCqUw zE+ej7u4-;FZcc75?g!kjxMz9fdE$76c;$JucrAIa@-Fdd@TKwP^4;KD<=5di<=@AD zfEEkp;zu*3+&sKm|A zMjb94RUI82Qyp8KL%NoFdish6q6S9|LJgV>It-p0#v7&?u^SK^50E#Hv3PJf2p8LlRuDuw_Q>3U9-a; z&3la4MLw7rJw^)G-6ZV3ZgM{Ib@Ur!6Xdm$*KrpLKIa^jR37$-x%ZSKo#)+}kFU(` zaZ4MK++u+I<6{<+e`oc%3;5$(kpB)khTkLq0ibj6?aP{^GPReFSRN13P@<&?;k;4Z zS=2g^UFj*c(3?jc+NkZlH6}l6AeTsDoL=cx*;gcsqIIyO#qcoV1iZ{<8v)g$n97KD@ac zilC%KKco4M{5$?Y{?X4}Aml%qnjZc1WoyL!aS)H-JMvGsLx$|lt_hj!IIp|LG$BY! zm;LkHUIIB!E+Q~P<}i%A?G;?y!JL?vGZ)_3e5 zrJMX2_PF#~zXh0uu>XGqh26mZ(L$~mWB(ZAt3QwZ|0kG=8TK#7RH26#xsxrW zg6?Cpqr0BOZc@ixPsWT}I2QG>NE$Bx0rtP`#OV+0AH|ZQuzyMu)6(*ZIJx?$j`=<1 zy?g2(@*8{vvGY@?xD*bQnPz{-{!tjIG&E}5D&y-*x8<=&KD5S?Q1`$chbilE8+R_= zz}q@v-dRCh7}f^Ge*{mZYYW96xtG=Crzw7Zt#4g_Lh;{}6Ez`@@aNPG*P2VB8*vsq z5W@bUgPMw@!*?u4{!&;8{vHR^mHk7+B%2gJ(}14jFHroFc%dkYzh~?=2>btkpW>gO zaAmIdv{TV!9pbq%Z9CGX&&ix;E;v$` z3JZ4lQ&YGzUx8`@CT7il7sU@c>fc52C#g?e*xLR7IK?k~{^>f!Z@c%WD1K1!pP=}= zpf-Xl^r;}|J|*-!7BqZ=xcfz_&|g4nvWJ$yr~!K<4xwxkR)-HB345azBO`upegk2b z3Y%zon2bE-@sSLQD8#%^%{SSz^@>9ZD%IW+C&hhxCpq>H)SoRoFr(?k|A6fOoGD

v7hwXa{AtL@#$Kma&=Vl15F4)zql3^1z8 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 diff --git a/src/node/src/tests/test_archive_import.rs b/src/node/src/tests/test_archive_import.rs deleted file mode 100644 index 82b7939..0000000 --- a/src/node/src/tests/test_archive_import.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -#[cfg(feature = "telemetry")] -use crate::collator_test_bundle::create_engine_telemetry; -use crate::{ - archive_import::{run_import, ImportConfig}, - block::{BlockIdExtExtention, BlockStuff}, - collator_test_bundle::create_engine_allocated, - internal_db::{ - InternalDb, InternalDbConfig, ARCHIVES_GC_BLOCK, LAST_APPLIED_MC_BLOCK, - PSS_KEEPER_MC_BLOCK, SHARD_CLIENT_MC_BLOCK, - }, - test_helper::init_test_log, -}; -use std::{ - path::{Path, PathBuf}, - sync::{atomic::AtomicU8, Arc}, -}; -use storage::{archives::epoch::ArchivalModeConfig, db::rocksdb::RocksDb}; -use ton_block::{ - read_single_root_boc, write_boc, AccountIdPrefixFull, BlockIdExt, Result, SHARD_FULL, -}; - -async fn wait_for_db_release(db: Arc) { - while Arc::strong_count(&db) > 1 { - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - } - drop(db); -} - -const ARCHIVES_PATH: &str = "src/tests/static/archives"; -const MC_ZEROSTATE_PATH: &str = - "src/tests/static/5E994FCF4D425C0A6CE6A792594B7173205F740A39CD56F537DEFD28B48A0F6E.boc"; -const WC_ZEROSTATE_PATH: &str = - "src/tests/static/EE0BEDFE4B32761FB35E9E1D8818EA720CAD1A0E7B4D2ED673C488E72E910342.boc"; -const GLOBAL_CONFIG_PATH: &str = "src/tests/config/mainnet.json"; - -/// Copy archive files to a temporary directory, restoring colons in filenames -/// (files are stored with underscores to avoid issues on Windows). -fn prepare_archives(dest: &Path) -> std::io::Result<()> { - std::fs::create_dir_all(dest)?; - for entry in std::fs::read_dir(ARCHIVES_PATH)? { - let entry = entry?; - let name = entry.file_name().to_string_lossy().to_string(); - let restored_name = name.replace("_", ":"); - std::fs::copy(entry.path(), dest.join(restored_name))?; - } - Ok(()) -} - -fn import_config(dir: &Path, archives_path: PathBuf) -> ImportConfig { - ImportConfig { - archives_path, - epochs_path: dir.join("epochs"), - epoch_size: 20_000, - node_db_path: dir.join("node_db"), - mc_zerostate_path: PathBuf::from(MC_ZEROSTATE_PATH), - wc_zerostate_paths: vec![PathBuf::from(WC_ZEROSTATE_PATH)], - global_config_path: PathBuf::from(GLOBAL_CONFIG_PATH), - skip_validation: false, - move_files: false, - } -} - -async fn open_db(dir: &Path) -> Result { - let db_dir = dir.join("node_db"); - let epochs_path = dir.join("epochs"); - InternalDb::with_update( - InternalDbConfig { - db_directory: db_dir.to_string_lossy().to_string(), - archival_mode: Some(ArchivalModeConfig { - epoch_size: 20_000, - new_epochs_path: epochs_path, - existing_epochs: vec![], - }), - ..Default::default() - }, - false, - false, - false, - &|| Ok(()), - None, - Arc::new(AtomicU8::new(0)), - None, - #[cfg(feature = "telemetry")] - create_engine_telemetry(), - create_engine_allocated(), - ) - .await -} - -async fn check_imported_block( - db: &InternalDb, - block_id: &BlockIdExt, -) -> Result> { - let handle = - db.load_block_handle(block_id)?.expect("Block handle must exist for imported block"); - assert!(handle.has_state(), "Imported block must have state"); - assert!(handle.has_saved_state(), "Imported block must have saved state"); - assert!(handle.is_applied(), "Imported block must be applied"); - - let mut block_stuff = None; - if block_id.seq_no() > 0 { - assert!(handle.has_data(), "Imported block must have data"); - assert!(handle.has_prev1(), "Imported block must have prev1"); - if block_id.is_masterchain() { - assert!(handle.has_proof(), "Imported MC block must have proof"); - } else { - assert!(handle.has_proof_link(), "Imported shard block must have proof link"); - } - - let prev1 = db.load_block_prev1(&block_id)?; - assert_eq!(prev1.seq_no(), block_id.seq_no() - 1); - let prev_handle = db.load_block_handle(&prev1)?.expect("Prev block handle must exist"); - assert!(prev_handle.has_next1(), "Imported block must have next1"); - let next1 = db.load_block_next1(prev_handle.id())?; - assert_eq!(&next1, block_id); - - block_stuff = Some(db.load_block_data(&handle).await?); - let _ = db.load_block_proof(&handle, !block_id.is_masterchain()).await?; - } - - let loaded_state = db.load_shard_state_dynamic(block_id)?; - let boc = write_boc(loaded_state.root_cell())?; - let deserialized_state = read_single_root_boc(&boc)?; - assert_eq!(loaded_state.root_cell().repr_hash(), deserialized_state.repr_hash()); - if block_id.seq_no() > 0 { - assert_eq!( - deserialized_state.repr_hash(), - block_stuff.as_ref().unwrap().block()?.read_state_update()?.new_hash - ); - } else { - assert_eq!(&deserialized_state.repr_hash(), block_id.root_hash()); - } - - Ok(block_stuff) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_import_and_verify() -> Result<()> { - init_test_log(); - let dir = tempfile::tempdir().unwrap(); - let archives = dir.path().join("archives"); - prepare_archives(&archives).unwrap(); - let config = import_config(dir.path(), archives); - - run_import(config).await?; - - let db = open_db(dir.path()).await?; - - let last_mc = - db.load_full_node_state(LAST_APPLIED_MC_BLOCK)?.expect("LAST_APPLIED_MC_BLOCK must be set"); - assert_eq!(last_mc.seq_no(), 199); - assert!(last_mc.shard().is_masterchain()); - - let gc_block = db.load_full_node_state(ARCHIVES_GC_BLOCK)?; - assert_eq!(last_mc, gc_block.unwrap()); - - let pss_block = db.load_full_node_state(PSS_KEEPER_MC_BLOCK)?; - assert_eq!(last_mc, pss_block.unwrap()); - - let shard_client = db.load_full_node_state(SHARD_CLIENT_MC_BLOCK)?; - assert_eq!(last_mc, shard_client.unwrap()); - - let last_mc_block = check_imported_block(&db, &last_mc).await?.unwrap(); - - for shard_block in last_mc_block.top_blocks_all()? { - check_imported_block(&db, &shard_block).await?; - } - - let first_mc = - db.lookup_block_by_seqno(&AccountIdPrefixFull::any_masterchain(), 1).await?.unwrap(); - let first_mc_block = check_imported_block(&db, &first_mc.0).await?.unwrap(); - // MC zerostate - check_imported_block(&db, &first_mc_block.construct_prev_id()?.0).await?; - - let first_wc = - db.lookup_block_by_seqno(&AccountIdPrefixFull::workchain(0, SHARD_FULL), 1).await?.unwrap(); - let first_wc_block = check_imported_block(&db, &first_wc.0).await?.unwrap(); - // WC zerostate - check_imported_block(&db, &first_wc_block.construct_prev_id()?.0).await?; - - db.stop_states_db().await; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_import_resume() -> Result<()> { - init_test_log(); - let dir = tempfile::tempdir().unwrap(); - let all_archives = dir.path().join("archives"); - prepare_archives(&all_archives).unwrap(); - - let partial_archives = dir.path().join("partial"); - std::fs::create_dir_all(&partial_archives)?; - - // Copy only the first group (archive.00000.*) - for entry in std::fs::read_dir(&all_archives)? { - let entry = entry?; - let name = entry.file_name().to_string_lossy().to_string(); - if name.starts_with("archive.00000.") { - std::fs::copy(entry.path(), partial_archives.join(&name))?; - } - } - - // First import โ€” only first group - let config1 = ImportConfig { - archives_path: partial_archives.clone(), - epochs_path: dir.path().join("epochs"), - epoch_size: 20_000, - node_db_path: dir.path().join("node_db"), - mc_zerostate_path: PathBuf::from(MC_ZEROSTATE_PATH), - wc_zerostate_paths: vec![PathBuf::from(WC_ZEROSTATE_PATH)], - global_config_path: PathBuf::from(GLOBAL_CONFIG_PATH), - skip_validation: false, - move_files: true, - }; - let node_db = run_import(config1).await?; - wait_for_db_release(node_db).await; - - let db1 = open_db(dir.path()).await?; - let last_mc_1 = db1 - .load_full_node_state(LAST_APPLIED_MC_BLOCK)? - .expect("After first import, LAST_APPLIED_MC_BLOCK must be set"); - assert_eq!(last_mc_1.seq_no(), 99); - drop(db1); - - // Copy remaining files for second import - for entry in std::fs::read_dir(&all_archives)? { - let entry = entry?; - let name = entry.file_name().to_string_lossy().to_string(); - if !name.starts_with("archive.00000.") { - std::fs::copy(entry.path(), partial_archives.join(&name))?; - } - } - - // Second import โ€” should resume and process remaining groups - let config2 = ImportConfig { - archives_path: partial_archives, - epochs_path: dir.path().join("epochs"), - epoch_size: 20_000, - node_db_path: dir.path().join("node_db"), - mc_zerostate_path: PathBuf::from(MC_ZEROSTATE_PATH), - wc_zerostate_paths: vec![PathBuf::from(WC_ZEROSTATE_PATH)], - global_config_path: PathBuf::from(GLOBAL_CONFIG_PATH), - skip_validation: false, - move_files: false, - }; - run_import(config2).await?; - - let db2 = open_db(dir.path()).await?; - let last_mc_2 = db2 - .load_full_node_state(LAST_APPLIED_MC_BLOCK)? - .expect("After second import, LAST_APPLIED_MC_BLOCK must be set"); - assert_eq!(last_mc_2.seq_no(), 199); - db2.stop_states_db().await; - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_import_skip_validation() -> Result<()> { - init_test_log(); - let dir = tempfile::tempdir().unwrap(); - let archives = dir.path().join("archives"); - prepare_archives(&archives).unwrap(); - let mut config = import_config(dir.path(), archives); - config.skip_validation = true; - - run_import(config).await?; - - let db = open_db(dir.path()).await?; - let last_mc = db.load_full_node_state(LAST_APPLIED_MC_BLOCK)?; - assert!(last_mc.is_some(), "Even with skip_validation, last MC must be set"); - - let last_mc = last_mc.unwrap(); - let handle = db - .load_block_handle(&last_mc)? - .expect("Block handle must exist after skip_validation import"); - assert!(handle.has_data()); - - db.stop_states_db().await; - Ok(()) -} diff --git a/src/node/src/tests/test_control.rs b/src/node/src/tests/test_control.rs index 83e2bce..32dd515 100644 --- a/src/node/src/tests/test_control.rs +++ b/src/node/src/tests/test_control.rs @@ -638,7 +638,8 @@ fn test_convert_for_stats() { assert_eq!("Disabled", &format!("{:?}", ValidationStatus::from_u8(5))); assert_eq!("Disabled", &format!("{:?}", ValidationStatus::from_u8(0))); assert_eq!("Waiting", &format!("{:?}", ValidationStatus::from_u8(1))); - assert_eq!("Active", &format!("{:?}", ValidationStatus::from_u8(2))); + assert_eq!("Countdown", &format!("{:?}", ValidationStatus::from_u8(2))); + assert_eq!("Active", &format!("{:?}", ValidationStatus::from_u8(3))); let shard_id = ShardIdent::with_tagged_prefix(15, 0xABCD_0000_0000_0000u64).unwrap(); let root_hash = diff --git a/src/node/src/tests/test_engine_operations.rs b/src/node/src/tests/test_engine_operations.rs deleted file mode 100644 index e444efe..0000000 --- a/src/node/src/tests/test_engine_operations.rs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use super::*; - -#[test] -fn test_destroyed_session_ids_roundtrip() { - let id_a = UInt256::from_slice(&[0x11; 32]); - let id_b = UInt256::from_slice(&[0x22; 32]); - let ids = HashSet::from([id_b.clone(), id_a.clone()]); - - let serialized = serialize_destroyed_session_ids(&ids); - let restored = deserialize_destroyed_session_ids(&serialized).unwrap(); - - assert_eq!(restored, vec![id_a, id_b], "session IDs must round-trip in sorted order"); -} - -#[test] -fn test_destroyed_session_ids_reject_invalid_length() { - let data = vec![1, 0, 0, 0, 0xaa]; - let err = deserialize_destroyed_session_ids(&data).unwrap_err(); - assert!(err.to_string().contains("invalid length"), "unexpected error: {err}"); -} diff --git a/src/node/src/tests/test_helper.rs b/src/node/src/tests/test_helper.rs index 76942a8..0ed3c0f 100644 --- a/src/node/src/tests/test_helper.rs +++ b/src/node/src/tests/test_helper.rs @@ -808,7 +808,6 @@ impl TestEngine { self.clone(), true, false, - false, ); validator_query.try_validate().await?; Ok(()) @@ -849,7 +848,6 @@ impl TestEngine { self.clone(), true, false, - false, ); validator_query.try_validate().await?; @@ -959,7 +957,6 @@ impl TestEngine { self.clone(), true, false, - false, ); validator_query.try_validate().await?; @@ -986,10 +983,6 @@ impl EngineOperations for TestEngine { self.now.load(Ordering::Relaxed) } - fn now_ms(&self) -> u64 { - self.now() as u64 * 1000 - } - async fn lookup_block_by_seqno( &self, prefix: &AccountIdPrefixFull, diff --git a/src/node/src/tests/test_internal_db.rs b/src/node/src/tests/test_internal_db.rs index 7c83c41..faebddb 100644 --- a/src/node/src/tests/test_internal_db.rs +++ b/src/node/src/tests/test_internal_db.rs @@ -551,39 +551,6 @@ async fn test_full_node_state_impl() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread")] -async fn test_validator_state_raw() { - clean_up(true, "test_validator_state_raw").await; - let r = test_validator_state_raw_impl().await; - clean_up(false, "test_validator_state_raw").await; - r.unwrap(); -} - -async fn test_validator_state_raw_impl() -> Result<()> { - let bytes = vec![1u8, 2, 3, 4, 5, 6]; - - { - let db = create_db("test_validator_state_raw", 0).await?; - - db.save_validator_state_raw("test_raw", &bytes)?; - assert_eq!(db.load_validator_state_raw("test_raw")?.unwrap(), bytes); - - db.drop_validator_state_raw("test_raw")?; - - tokio::time::sleep(Duration::from_millis(100)).await; - assert!(db.load_validator_state_raw("test_raw")?.is_none()); - stop_db(&db).await; - } - tokio::time::sleep(Duration::from_millis(100)).await; - { - let db = create_db("test_validator_state_raw", 0).await?; - assert!(db.load_validator_state_raw("test_raw")?.is_none()); - stop_db(&db).await; - } - - Ok(()) -} - const SHARD_PREFIX_LEN: u8 = 5; const THREADS: u64 = 10; const MC_BLOCKS: u32 = 500; diff --git a/src/node/src/tests/test_sync.rs b/src/node/src/tests/test_sync.rs index 2a37923..ba0b3fc 100644 --- a/src/node/src/tests/test_sync.rs +++ b/src/node/src/tests/test_sync.rs @@ -40,10 +40,7 @@ use std::{ }, }; use storage::{ - archives::{ - archive_manager::ArchiveManager, - db_provider::{ArchiveDbProvider, SingleDbProvider}, - }, + archives::archive_manager::ArchiveManager, block_handle_db::BlockHandleStorage, db::rocksdb::{AccessType, RocksDb}, types::{BlockMeta, PersistentStatePartId}, @@ -122,13 +119,9 @@ async fn test_sync() -> Result<()> { let allocated = create_engine_allocated(); #[cfg(feature = "telemetry")] let telemetry = create_engine_telemetry(); - let db_root_path = Arc::new(PathBuf::from(DB_PATH)); - let db_provider: Arc = - Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())); let archive_manager = ArchiveManager::with_data( db.clone(), - db_root_path, - db_provider, + Arc::new(PathBuf::from(DB_PATH)), init_mc_block_id.seq_no(), monitor_min_split.clone(), #[cfg(feature = "telemetry")] diff --git a/src/node/src/types/accounts.rs b/src/node/src/types/accounts.rs index 5b3b790..bea58e1 100644 --- a/src/node/src/types/accounts.rs +++ b/src/node/src/types/accounts.rs @@ -12,8 +12,8 @@ use crate::engine_traits::EngineOperations; use std::sync::Arc; use ton_block::{ fail, Account, AccountBlock, AccountId, AccountStorageStat, Augmentation, Cell, HashUpdate, - HashmapAugType, HashmapRemover, HashmapType, LibDescr, Libraries, Result, Serializable, - ShardAccount, ShardAccounts, StateInitLib, Transaction, Transactions, UInt256, UsageTree, + HashmapAugType, HashmapRemover, LibDescr, Libraries, Result, Serializable, ShardAccount, + ShardAccounts, StateInitLib, Transaction, Transactions, UInt256, UsageTree, }; pub struct ShardAccountStuff { @@ -126,10 +126,6 @@ impl ShardAccountStuff { &self.original_root } - pub fn is_touched(&self) -> bool { - !self.transactions.is_empty() - } - pub fn add_transaction( &mut self, transaction: &mut Transaction, diff --git a/src/node/src/types/awaiters_pool.rs b/src/node/src/types/awaiters_pool.rs index 894cdc4..d27cbe3 100644 --- a/src/node/src/types/awaiters_pool.rs +++ b/src/node/src/types/awaiters_pool.rs @@ -161,18 +161,6 @@ where Ok(()) } - pub async fn shunt_async( - &self, - id: &I, - operation: impl futures::Future>, - ) -> Result<()> { - if let Some(op_awaiters) = self.ops_awaiters.get(id) { - let r = operation.await?; - let _ = op_awaiters.1.tx.send(Some(Ok(r))); - } - Ok(()) - } - async fn wait_operation( &self, id: &I, diff --git a/src/node/src/validator/accept_block.rs b/src/node/src/validator/accept_block.rs index 4951a46..e80dde3 100644 --- a/src/node/src/validator/accept_block.rs +++ b/src/node/src/validator/accept_block.rs @@ -12,7 +12,7 @@ use crate::{ block::{construct_and_check_prev_stuff, BlockStuff}, block_proof::BlockProofStuff, engine_traits::EngineOperations, - full_node::apply_block::store_state_update, + full_node::apply_block::calc_shard_state, shard_state::ShardStateStuff, types::top_block_descr::TopBlockDescrStuff, validating_utils::{fmt_block_id_short, simplex_to_sign_checked, UNREGISTERED_CHAIN_MAX_LEN}, @@ -295,7 +295,8 @@ pub async fn accept_block_routine( } log::debug!(target: "validator", "({}): accept_block: calculating shard state", block_descr); - store_state_update(&handle, &block, &(prev[0].clone(), prev.get(1).cloned()), engine).await?; + let _ss = + calc_shard_state(&handle, &block, &(prev[0].clone(), prev.get(1).cloned()), engine).await?; // Create proof using variant-aware function if Simplex variant provided let (proof, signatures_out) = match signatures_variant { diff --git a/src/node/src/validator/collator.rs b/src/node/src/validator/collator.rs index 2a1fc56..acfe9d9 100644 --- a/src/node/src/validator/collator.rs +++ b/src/node/src/validator/collator.rs @@ -59,7 +59,7 @@ use ton_block::{ Serializable, ShardAccount, ShardAccountBlocks, ShardAccounts, ShardDescr, ShardFees, ShardHashes, ShardIdent, ShardStateSplit, ShardStateUnsplit, SliceData, StorageStatDict, TopBlockDescrSet, Transaction, TransactionTickTock, UInt256, UsageTree, ValidatorSet, - ValueFlow, WorkchainDescr, Workchains, MASTERCHAIN_ID, + ValidatorsStat, ValueFlow, WorkchainDescr, Workchains, MASTERCHAIN_ID, }; #[cfg(feature = "xp25")] use ton_block::{RefShardBlocks, ShardBlockRef, WcExtra}; @@ -262,7 +262,6 @@ struct CollatorData { // determined fields gen_utime: u32, - gen_utime_ms: u64, config: BlockchainConfig, collated_block_descr: Arc, block_limit_class: ParamLimitIndex, @@ -313,7 +312,6 @@ struct CollatorData { impl CollatorData { pub fn new( gen_utime: u32, - gen_utime_ms: u64, config: BlockchainConfig, usage_tree: UsageTree, prev_data: &PrevData, @@ -340,7 +338,6 @@ impl CollatorData { dispatch_queue_total_limit_reached: false, have_unprocessed_account_dispatch_queue: false, gen_utime, - gen_utime_ms, config, collated_block_descr, block_limit_class: ParamLimitIndex::Underload, @@ -386,10 +383,6 @@ impl CollatorData { self.gen_utime } - fn gen_utime_ms(&self) -> u64 { - self.gen_utime_ms - } - // // Lists // @@ -1142,9 +1135,6 @@ impl ExecutionManager { msg_metadata, is_special, )?; - if !tr.blackhole_burned().is_zero() { - collator_data.value_flow.burned.coins.add(tr.blackhole_burned())?; - } collator_data.update_lt(self.max_lt.load(Ordering::Relaxed)); @@ -1602,7 +1592,7 @@ impl Collator { let is_masterchain = self.shard.is_masterchain(); self.check_stop_flag()?; - let (now, now_ms) = self.init_utime(&mc_data, &prev_data)?; + let now = self.init_utime(&mc_data, &prev_data)?; let config = BlockchainConfig::with_params( mc_data.config().capabilities(), supported_version(), @@ -1610,7 +1600,6 @@ impl Collator { )?; let mut collator_data = CollatorData::new( now, - now_ms, config, usage_tree, &prev_data, @@ -1626,7 +1615,7 @@ impl Collator { self.after_split, false, &self.prev_blocks_ids, - collator_data.config.raw_config(), + mc_data.config(), mc_data.mc_state_extra(), false, now, @@ -1646,7 +1635,7 @@ impl Collator { self.collator_settings.is_fake, )?; - self.check_utime(&prev_data, &mut collator_data)?; + self.check_utime(&mc_data, &prev_data, &mut collator_data)?; if is_masterchain { self.adjust_shard_config(&mc_data, &mut collator_data)?; @@ -1770,9 +1759,16 @@ impl Collator { // tick & special transactions if self.shard.is_masterchain() { - self.create_ticktock_transactions(false, prev_data, collator_data, &mut exec_manager) + self.create_ticktock_transactions( + false, + mc_data, + prev_data, + collator_data, + &mut exec_manager, + ) + .await?; + self.create_special_transactions(mc_data, prev_data, collator_data, &mut exec_manager) .await?; - self.create_special_transactions(prev_data, collator_data, &mut exec_manager).await?; } // merge prepare / merge install @@ -1887,8 +1883,14 @@ impl Collator { // tock transactions if self.shard.is_masterchain() { - self.create_ticktock_transactions(true, prev_data, collator_data, &mut exec_manager) - .await?; + self.create_ticktock_transactions( + true, + mc_data, + prev_data, + collator_data, + &mut exec_manager, + ) + .await?; } // process newly-generated messages (only by including them into output queue) @@ -2174,68 +2176,68 @@ impl Collator { Ok(usage_tree) } - fn init_utime(&self, mc_data: &McData, prev_data: &PrevData) -> Result<(u32, u64)> { + fn init_utime(&self, mc_data: &McData, prev_data: &PrevData) -> Result { // consider unixtime and lt from previous block(s) of the same shardchain let prev_now = prev_data.prev_state_utime(); let prev = max(mc_data.state().state()?.gen_time(), prev_now); log::trace!("{}: init_utime prev_time: {}", self.collated_block_descr, prev); let allow_same_timestamp = self.allow_same_timestamp(mc_data); - // Compute gen_utime_ms first, then derive gen_utime from it (like C++). - // This guarantees gen_utime_ms / 1000 == gen_utime, avoiding second-boundary - // mismatches in ConsensusExtraData validation. - let (gen_utime, gen_utime_ms) = - Self::calc_utime(prev, self.engine.now_ms(), allow_same_timestamp); - Ok((gen_utime, gen_utime_ms)) + Ok(Self::calc_utime(prev, self.engine.now(), allow_same_timestamp)) } /// Whether this shard is allowed to have `gen_utime` equal to the previous one. /// - /// C++ parity: `allow_same_timestamp_ = global_version_ >= 13`. - /// Depends only on the global protocol version, not on consensus type. - /// When false (global_version < 13), gen_utime must strictly increase (prev + 1). - /// When true, gen_utime may equal the previous block's (non-decreasing). + /// C++ compatibility: + /// - non-simplex (catchain): always strict (`prev + 1`) + /// - simplex: allow equal timestamps starting from `global_version >= 13` fn allow_same_timestamp(&self, mc_data: &McData) -> bool { - #[cfg(feature = "xp25")] + #[cfg(feature = "simplex")] { - let _ = mc_data; - true + let simplex_enabled_for_shard = if self.shard.is_masterchain() { + mc_data.config().get_mc_simplex_config().ok().flatten().is_some() + } else { + mc_data.config().get_shard_simplex_config().ok().flatten().is_some() + }; + + // C++-compatible gating: allow equal timestamps only starting from global_version >= 13. + // This must match validator-side checks (`validate_query.rs`). + simplex_enabled_for_shard + //TODO: LK: enable after change block version to 13 + //&& mc_data.config().global_version() + // >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION } - #[cfg(not(feature = "xp25"))] + #[cfg(not(feature = "simplex"))] { - mc_data.config().global_version() - >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION + let _ = mc_data; + #[cfg(feature = "xp25")] + { + true + } + #[cfg(not(feature = "xp25"))] + { + false + } } } - /// Compute gen_utime_ms and gen_utime from previous block time and current wall clock. - /// - /// Mirrors C++ collator.cpp: - /// ```cpp - /// now_ms_ = std::max((td::uint64)(prev + (allow_same ? 0 : 1)) * 1000, - /// (td::uint64)(td::Clocks::system() * 1000)); - /// now_ = (UnixTime)(now_ms_ / 1000); - /// ``` - /// - /// By computing milliseconds first and deriving seconds, we guarantee - /// `gen_utime_ms / 1000 == gen_utime` always holds. #[inline] - fn calc_utime(prev: u32, now_ms: u64, allow_same_timestamp: bool) -> (u32, u64) { - let prev_sec = if allow_same_timestamp { - // Non-decreasing: do NOT force +1 when blocks are produced faster than 1/sec. - prev + fn calc_utime(prev: u32, now: u32, allow_same_timestamp: bool) -> u32 { + if allow_same_timestamp { + // NOTE: keep gen_utime monotonic (non-decreasing), but do NOT force +1 when blocks + // are produced faster than 1/sec (otherwise chain time drifts into the future). + max(prev, now) } else { - // Strictly increasing gen_utime (legacy behavior), saturating at u32::MAX. - prev.saturating_add(1) - }; - let prev_ms = prev_sec as u64 * 1000; - // Clamp to u32::MAX seconds range to prevent wraparound on cast. - let max_ms = u32::MAX as u64 * 1000; - let gen_utime_ms = max(prev_ms, now_ms).min(max_ms); - let gen_utime = (gen_utime_ms / 1000) as u32; - (gen_utime, gen_utime_ms) + // C++ non-simplex behavior: strictly increasing gen_utime + max(prev.saturating_add(1), now) + } } - fn check_utime(&self, prev_data: &PrevData, collator_data: &mut CollatorData) -> Result<()> { + fn check_utime( + &self, + mc_data: &McData, + prev_data: &PrevData, + collator_data: &mut CollatorData, + ) -> Result<()> { let now = collator_data.gen_utime; if now > collator_data.now_upper_limit() { fail!( @@ -2246,7 +2248,7 @@ impl Collator { // check whether masterchain catchain rotation is overdue let prev_now = prev_data.prev_state_utime(); - let ccvc = collator_data.config.raw_config().catchain_config()?; + let ccvc = mc_data.config().catchain_config()?; let lifetime = ccvc.mc_catchain_lifetime; if self.shard.is_masterchain() && now / lifetime > prev_now / lifetime @@ -2375,7 +2377,7 @@ impl Collator { log::trace!("{}: adjust_shard_config", self.collated_block_descr); CHECK!(self.shard.is_masterchain()); collator_data.set_shards(mc_data.state().shards()?.clone())?; - let wc_set = collator_data.config.raw_config().workchains()?; + let wc_set = mc_data.config().workchains()?; wc_set.iterate_with_keys(|wc_id: i32, wc_info| { log::trace!( " @@ -2430,8 +2432,7 @@ impl Collator { let mut cancelled = false; let mut new_shard_descrs = collator_data.shards()?.clone(); - let lt_limit = - prev_data.prev_state_lt() + collator_data.config.raw_config().get_max_lt_growth(); + let lt_limit = prev_data.prev_state_lt() + mc_data.config().get_max_lt_growth(); shard_top_blocks.sort_by(|a, b| cmp_shard_block_descr(a, b)); let mut shards_updated = HashSet::new(); let mut tb_act = 0; @@ -2705,22 +2706,6 @@ impl Collator { let shard_fees = collator_data.shard_fees().root_extra().clone(); collator_data.value_flow.fees_collected.add(&shard_fees.fees)?; - if let Some(burning_cfg) = collator_data.config.burning_config() { - let Some(imported_base) = - shard_fees.fees.coins.as_u128().checked_sub(shard_fees.create.coins.as_u128()) - else { - fail!( - "fees_imported is smaller than imported created fees: {} < {}", - shard_fees.fees.coins, - shard_fees.create.coins - ) - }; - let burned = burning_cfg.calculate_burned_fees(imported_base)?; - if !burned.is_zero() { - collator_data.value_flow.burned.coins.add(&burned)?; - collator_data.value_flow.fees_collected.coins.sub(&burned)?; - } - } collator_data.value_flow.fees_imported = shard_fees.fees; Ok(()) @@ -2952,8 +2937,7 @@ impl Collator { log::trace!("{}: update_value_flow", self.collated_block_descr); if self.shard.is_masterchain() { - collator_data.value_flow.created.coins = - collator_data.config.raw_config().block_create_fees(true)?; + collator_data.value_flow.created.coins = mc_data.config().block_create_fees(true)?; collator_data.value_flow.recovered = collator_data.value_flow.created.clone(); collator_data.value_flow.recovered.add(&collator_data.value_flow.fees_collected)?; @@ -2962,7 +2946,7 @@ impl Collator { .recovered .add(mc_data.state().state()?.total_validator_fees())?; - match collator_data.config.raw_config().fee_collector_address() { + match mc_data.config().fee_collector_address() { Err(_) => { log::debug!( "{}: fee recovery disabled \ @@ -2983,10 +2967,10 @@ impl Collator { } }; - collator_data.value_flow.minted = self.compute_minted_amount(mc_data, collator_data)?; + collator_data.value_flow.minted = self.compute_minted_amount(mc_data)?; if !collator_data.value_flow.minted.is_zero()? - && collator_data.config.raw_config().minter_address().is_err() + && mc_data.config().minter_address().is_err() { log::warn!( "{}: minting of {} disabled: no minting smart contract defined", @@ -2996,25 +2980,20 @@ impl Collator { collator_data.value_flow.minted = CurrencyCollection::default(); } } else { - collator_data.value_flow.created.coins = - collator_data.config.raw_config().block_create_fees(false)?; + collator_data.value_flow.created.coins = mc_data.config().block_create_fees(false)?; collator_data.value_flow.created.coins >>= self.shard.prefix_len(); } collator_data.value_flow.from_prev_blk = prev_data.total_balance().clone(); Ok(()) } - fn compute_minted_amount( - &self, - mc_data: &McData, - collator_data: &CollatorData, - ) -> Result { + fn compute_minted_amount(&self, mc_data: &McData) -> Result { log::trace!("{}: compute_minted_amount", self.collated_block_descr); CHECK!(self.shard.is_masterchain()); let mut to_mint = CurrencyCollection::default(); - let to_mint_cp = match collator_data.config.raw_config().to_mint() { + let to_mint_cp = match mc_data.config().to_mint() { Err(e) => { log::warn!( "{}: Can't get config param 7 (to_mint): {}", @@ -3274,12 +3253,13 @@ impl Collator { async fn create_ticktock_transactions( &self, tock: bool, + mc_data: &McData, prev_data: &PrevData, collator_data: &mut CollatorData, exec_manager: &mut ExecutionManager, ) -> Result<()> { log::trace!("{}: create_ticktock_transactions", self.collated_block_descr); - let fundamental_dict = collator_data.config.raw_config().fundamental_smc_addr()?; + let fundamental_dict = mc_data.config().fundamental_smc_addr()?; for res in &fundamental_dict { let account_id = SliceData::load_bitstring(res?.0)?; self.create_ticktock_transaction( @@ -3292,7 +3272,7 @@ impl Collator { .await?; self.check_stop_flag()?; } - let account_id = collator_data.config.raw_config().config_addr.clone(); + let account_id = mc_data.config().config_addr.clone(); self.create_ticktock_transaction(account_id, tock, prev_data, collator_data, exec_manager) .await?; exec_manager.wait_transactions(collator_data).await?; @@ -3341,6 +3321,7 @@ impl Collator { async fn create_special_transactions( &self, + mc_data: &McData, prev_data: &PrevData, collator_data: &mut CollatorData, exec_manager: &mut ExecutionManager, @@ -3350,7 +3331,7 @@ impl Collator { } log::debug!("{}: create_special_transactions", self.collated_block_descr); - let account_id = collator_data.config.raw_config().fee_collector_address()?; + let account_id = mc_data.config().fee_collector_address()?; self.create_special_transaction( account_id, collator_data.value_flow.recovered.clone(), @@ -3362,7 +3343,7 @@ impl Collator { .await?; self.check_stop_flag()?; - let account_id = collator_data.config.raw_config().minter_address()?; + let account_id = mc_data.config().minter_address()?; self.create_special_transaction( account_id, collator_data.value_flow.minted.clone(), @@ -3897,25 +3878,25 @@ impl Collator { new_config_opt = Some(Self::extract_new_config(shard_acc.account(), addr)?); } } - if shard_acc.is_touched() { - let acc_block = shard_acc.update_shard_state(&mut new_accounts)?; + let acc_block = shard_acc.update_shard_state(&mut new_accounts)?; + if !acc_block.transactions().is_empty() { accounts.insert(&acc_block)?; - let account = shard_acc.account(); - if let Some(storage_dict) = shard_acc.storage_dict() { - if account.dict_hash().is_some() { - let size = account.storage_info().map_or(0, |info| info.used().cells()); - log::trace!( - "{}: updated storage dict with hash {:x} for account {:x} of size {}", - self.collated_block_descr, - storage_dict.repr_hash(), - account_id, - size - ); - self.engine.add_account_storage_dict(storage_dict, size) - } + } + let account = shard_acc.account(); + if let Some(storage_dict) = shard_acc.storage_dict() { + if account.dict_hash().is_some() { + let size = account.storage_info().map(|info| info.used().cells()).unwrap_or(0); + log::trace!( + "{}: updated storage dict with hash {:x} for account {:x} of size {}", + self.collated_block_descr, + storage_dict.repr_hash(), + account_id, + size + ); + self.engine.add_account_storage_dict(storage_dict, size) } - changed_accounts.insert(account_id, shard_acc); } + changed_accounts.insert(account_id, shard_acc); } if let Some(hardfork_config) = self.engine.get_config_for_hardfork() { @@ -3930,17 +3911,12 @@ impl Collator { let mut value_flow = collator_data.value_flow.clone(); value_flow.imported = collator_data.in_msgs.root_extra().value_imported.clone(); value_flow.exported = collator_data.out_msgs.root_extra().clone(); - let mut total_fees = accounts.root_extra().clone(); - total_fees.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; + value_flow.fees_collected = accounts.root_extra().clone(); + value_flow.fees_collected.coins.add(&collator_data.in_msgs.root_extra().fees_collected)?; - value_flow.fees_collected.add(&total_fees)?; - if self.shard.is_masterchain() { - if let Some(burning_cfg) = collator_data.config.burning_config() { - let burned = burning_cfg.calculate_burned_fees(total_fees.coins.as_u128())?; - value_flow.fees_collected.coins.sub(&burned)?; - value_flow.burned.coins.add(&burned)?; - } - } + // value_flow.fees_collected.coins.add(&out_msg_dscr.root_extra().coins)?; // TODO: Why only coins? + + value_flow.fees_collected.add(&value_flow.fees_imported)?; value_flow.fees_collected.add(&value_flow.created)?; value_flow.to_next_blk = new_accounts.full_balance().clone(); //value_flow.to_next_blk.add(&value_flow.recovered)?; @@ -3988,7 +3964,7 @@ impl Collator { info.set_prev_key_block_seqno(mc_data.prev_key_block_seqno()); info.write_master_ref(master_ref.as_ref())?; - if collator_data.config.raw_config().has_capability(GlobalCapabilities::CapReportVersion) { + if mc_data.config().has_capability(GlobalCapabilities::CapReportVersion) { info.set_gen_software(Some(GlobalVersion { version: supported_version(), capabilities: supported_capabilities(), @@ -4419,6 +4395,8 @@ impl Collator { None }; + let validators_stat = ValidatorsStat::default(); + Ok(( McStateExtra { shards: collator_data.shards()?.clone(), @@ -4429,6 +4407,7 @@ impl Collator { last_key_block, block_create_stats, global_balance, + validators_stat, }, min_ref_mc_seqno, )) @@ -4884,15 +4863,7 @@ impl Collator { roots.push(tbds.serialize()?); } - // 1.2 store info for simplex consensus (C++ parity) - if self.collator_settings.is_simplex { - let extra = ton_block::ConsensusExtraData { - flags: 0, - gen_utime_ms: collator_data.gen_utime_ms(), - }; - roots.push(extra.serialize()?); - } - + // match collator's BoC flags to consensus version config let collated_data_flags = if collator_data .config .raw_config() diff --git a/src/node/src/validator/consensus.rs b/src/node/src/validator/consensus.rs index d0bb54b..9a3d508 100644 --- a/src/node/src/validator/consensus.rs +++ b/src/node/src/validator/consensus.rs @@ -59,6 +59,23 @@ pub(super) const ACCELERATED_CONSENSUS_VALIDATION_RETRY_TIMEOUT_MS: u64 = 500; pub(super) const ACCELERATED_CONSENSUS_BLOCK_CANDIDATE_SENDING_RETRY_TIMEOUT_MS: u64 = 2000; pub(super) const ACCELERATED_CONSENSUS_BLOCK_CANDIDATE_SENDING_RETRY_ATTEMPTS: u32 = 3; +// ============================================================================= +// Simplex testing constants - Override network config params during testing +// ============================================================================= +// These values are used instead of ConfigParam 30 when testing simplex consensus. +// Reference: p30.mc from testnet config +pub(super) const SIMPLEX_TARGET_RATE_MS: u64 = 500; +pub(super) const SIMPLEX_SLOTS_PER_LEADER_WINDOW: u32 = 4; +pub(super) const SIMPLEX_FIRST_BLOCK_TIMEOUT_MS: u64 = 1000; +pub(super) const SIMPLEX_MAX_LEADER_WINDOW_DESYNC: u32 = 2; + +// Additional simplex timing constants (matching accelerated consensus patterns) +pub(super) const SIMPLEX_VALIDATION_RETRY_ATTEMPTS: u32 = 8; +pub(super) const SIMPLEX_VALIDATION_RETRY_TIMEOUT_MS: u64 = 500; +pub(super) const SIMPLEX_COLLATION_RETRY_TIMEOUT_MS: u64 = 500; +pub(super) const SIMPLEX_COLLATION_RETRY_MAX_ATTEMPTS: u32 = 3; +pub(super) const SIMPLEX_STANDSTILL_TIMEOUT_MS: u64 = 10000; + // ============================================================================= // Common Types from consensus-common (preferred source) // ============================================================================= @@ -72,9 +89,9 @@ pub use consensus_common::{ ConsensusOverlayListener, ConsensusOverlayListenerPtr, ConsensusOverlayLogReplayListener, ConsensusOverlayLogReplayListenerPtr, ConsensusOverlayManager, ConsensusOverlayManagerPtr, ConsensusOverlayPtr, ConsensusReplayListener, ConsensusReplayListenerPtr, LogPlayer, - LogPlayerPtr, LogReplayOptions, OverlayTransportType, PrivateKey, PublicKey, PublicKeyHash, - RawBuffer, Result, Session, SessionId, SessionListener, SessionListenerPtr, SessionNode, - SessionPtr, SessionStats, ValidatorBlockCandidate, ValidatorBlockCandidateCallback, + LogPlayerPtr, LogReplayOptions, PrivateKey, PublicKey, PublicKeyHash, RawBuffer, Result, + Session, SessionId, SessionListener, SessionListenerPtr, SessionNode, SessionPtr, SessionStats, + ValidatorBlockCandidate, ValidatorBlockCandidateCallback, ValidatorBlockCandidateDecisionCallback, ValidatorBlockCandidatePtr, ValidatorWeight, }; @@ -270,10 +287,6 @@ impl SessionHolder { // Implement consensus_common::Session for SessionHolder // Delegates to the common Session interface of the inner session impl consensus_common::Session for SessionHolder { - fn start(&self, initial_block_seqno: u32) { - self.inner.as_common_session().start(initial_block_seqno); - } - fn stop(&self) { self.inner.as_common_session().stop(); } @@ -418,6 +431,7 @@ impl ConsensusFactory { options: &SimplexSessionOptions, session_id: &SessionId, shard: &ShardIdent, + initial_block_seqno: u32, nodes: Vec, local_key: &PrivateKey, db_root: String, @@ -425,15 +439,18 @@ impl ConsensusFactory { overlay_manager: ConsensusOverlayManagerPtr, listener: SessionListenerPtr, ) -> consensus_common::Result { + // Disable callback thread - ValidatorSessionListener has its own let mut options = options.clone(); options.use_callback_thread = false; + // Construct full DB path let db_path = Self::make_simplex_db_path(&db_root, shard, catchain_seqno, session_id); let simplex_session = Self::create_simplex_session( &options, session_id, shard, + initial_block_seqno, nodes, local_key, db_path, @@ -441,9 +458,46 @@ impl ConsensusFactory { listener, )?; + // Wrap in SessionHolder and return as SessionHolderPtr Ok(Arc::new(SessionHolder::simplex(simplex_session))) } + /// Create simplex options with testing constants. + /// + /// Uses hardcoded testing values instead of network config params. + /// Reference values from p30.mc testnet config: + /// - target_rate_ms: 500 + /// - slots_per_leader_window: 4 + /// - first_block_timeout_ms: 1000 + /// - max_leader_window_desync: 2 + pub fn create_simplex_options( + max_block_size: usize, + max_collated_data_size: usize, + ) -> SimplexSessionOptions { + use super::consensus::*; + + SimplexSessionOptions { + // Core timing from testing constants (p30 reference) + target_rate: Duration::from_millis(SIMPLEX_TARGET_RATE_MS), + slots_per_leader_window: SIMPLEX_SLOTS_PER_LEADER_WINDOW, + first_block_timeout: Duration::from_millis(SIMPLEX_FIRST_BLOCK_TIMEOUT_MS), + + // Retry and timeout settings + validation_retry_attempts: SIMPLEX_VALIDATION_RETRY_ATTEMPTS, + validation_retry_timeout: Duration::from_millis(SIMPLEX_VALIDATION_RETRY_TIMEOUT_MS), + collation_retry_timeout: Duration::from_millis(SIMPLEX_COLLATION_RETRY_TIMEOUT_MS), + collation_retry_max_attempts: SIMPLEX_COLLATION_RETRY_MAX_ATTEMPTS, + standstill_timeout: Duration::from_millis(SIMPLEX_STANDSTILL_TIMEOUT_MS), + + // Block size limits from catchain config (ConfigParam 29) + max_block_size, + max_collated_data_size, + + // Other settings use defaults + ..Default::default() + } + } + /// Configure catchain-specific options for accelerated consensus pub fn configure_catchain_options( mut options: CatchainSessionOptions, @@ -560,6 +614,7 @@ impl ConsensusFactory { options: &SimplexSessionOptions, session_id: &SessionId, shard: &ShardIdent, + initial_block_seqno: u32, nodes: Vec, local_key: &PrivateKey, db_path: String, @@ -570,6 +625,7 @@ impl ConsensusFactory { options, session_id, shard, + initial_block_seqno, nodes, local_key, db_path, diff --git a/src/node/src/validator/consensus_overlay.rs b/src/node/src/validator/consensus_overlay.rs index 815c20c..bdd66f5 100644 --- a/src/node/src/validator/consensus_overlay.rs +++ b/src/node/src/validator/consensus_overlay.rs @@ -8,10 +8,11 @@ */ use super::consensus::{ ConsensusNode, ConsensusOverlayListenerPtr, ConsensusOverlayLogReplayListenerPtr, - ConsensusOverlayManager, ConsensusOverlayPtr, OverlayTransportType, PrivateKey, + ConsensusOverlayManager, ConsensusOverlayPtr, PrivateKey, }; use crate::engine_traits::PrivateOverlayOperations; use adnl::PrivateOverlayShortId; +use consensus_common::OverlayTransportType; use std::sync::Arc; use ton_block::{Result, UInt256}; diff --git a/src/node/src/validator/fabric.rs b/src/node/src/validator/fabric.rs index 5853adb..15dcc51 100644 --- a/src/node/src/validator/fabric.rs +++ b/src/node/src/validator/fabric.rs @@ -27,7 +27,7 @@ use crate::{ validate_query::ValidateQuery, validator_group::PipelineContext, validator_utils::PrevBlockHistory, - BlockCandidate, CollatorSettings, + BlockCandidate, }, }; use std::{sync::Arc, time::SystemTime}; @@ -39,7 +39,6 @@ use ton_block::{ pub async fn run_validate_query_any_candidate( block_candidate: BlockCandidate, engine: Arc, - is_simplex: bool, ) -> Result { let block_id = block_candidate.block_id.clone(); let block_data = block_candidate.data.clone(); @@ -82,7 +81,6 @@ pub async fn run_validate_query_any_candidate( engine.clone(), false, true, - is_simplex, ); let validator_result = query.try_validate().await; @@ -151,7 +149,6 @@ pub async fn run_validate_query( block: BlockCandidate, set: ValidatorSet, engine: Arc, - is_simplex: bool, ) -> Result { let next_block_descr = fmt_next_block_descr(&block.block_id); @@ -177,7 +174,6 @@ pub async fn run_validate_query( engine.clone(), false, true, - is_simplex, ) .try_validate() .await @@ -191,7 +187,6 @@ pub async fn run_validate_query( engine.clone(), false, true, - is_simplex, ); let validator_result = query.try_validate().await; if let Err(err) = &validator_result { @@ -288,7 +283,6 @@ pub async fn run_collate_query( collator_id: PublicKey, set: ValidatorSet, engine: Arc, - is_simplex: bool, ) -> Result<(Arc, Arc, Block, Cell)> { let labels = [("shard", shard.to_string())]; metrics::gauge!("ton_node_collator_active", &labels).increment(1.0); @@ -304,7 +298,7 @@ pub async fn run_collate_query( UInt256::from(collator_id.pub_key()?), engine.clone(), None, - CollatorSettings { is_simplex, ..Default::default() }, + Default::default(), )?; let collate_result = collator.collate().await; diff --git a/src/node/src/validator/mod.rs b/src/node/src/validator/mod.rs index 5d4e4f4..fe9c093 100644 --- a/src/node/src/validator/mod.rs +++ b/src/node/src/validator/mod.rs @@ -34,12 +34,10 @@ use ton_block::{ Libraries, McStateExtra, Result, UInt256, }; -/// Minimum global version that allows equal `gen_utime` between consecutive blocks. +/// C++ simplex collator/validator allows equal `gen_utime` starting from this global version. /// -/// C++ parity: `allow_same_timestamp_ = global_version_ >= 13`. -/// Applies to all consensus types (not simplex-specific despite the name). -/// Under `xp25` feature this constant is unused โ€” `allow_same_timestamp` is always true. -#[cfg(not(feature = "xp25"))] +/// Reference (C++): `allow_same_timestamp_ = global_version_ >= 13`. +#[allow(dead_code)] pub(super) const SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION: u32 = 13; #[derive(Clone, Default, Debug)] @@ -60,8 +58,6 @@ pub struct CollatorSettings { pub is_bundle: bool, // produce blocks identical to cpp-node - mostly for tests pub lt_compatible: bool, - // true when running under simplex consensus (passed from ValidatorGroup) - pub is_simplex: bool, } impl CollatorSettings { diff --git a/src/node/src/validator/tests/test_collator.rs b/src/node/src/validator/tests/test_collator.rs index 0a01dae..a8115e3 100644 --- a/src/node/src/validator/tests/test_collator.rs +++ b/src/node/src/validator/tests/test_collator.rs @@ -180,60 +180,13 @@ impl EngineOperations for TestPipelineCollatorEngine { #[test] fn test_calc_utime_allow_same_timestamp_does_not_drift_when_prev_ahead() { // Simplex / allow_same_timestamp=true: monotonic, but no forced +1 drift. - // prev=1015s, now_ms=1000_000ms (1000s) โ†’ gen_utime=1015, gen_utime_ms=1015_000 - let (gen_utime, gen_utime_ms) = Collator::calc_utime(1015, 1_000_000, true); - assert_eq!(gen_utime, 1015); - assert_eq!(gen_utime_ms, 1_015_000); - assert_eq!(gen_utime_ms / 1000, gen_utime as u64); + assert_eq!(Collator::calc_utime(1015, 1000, true), 1015); } #[test] fn test_calc_utime_strict_timestamp_forces_increment_when_prev_ahead() { // Catchain / allow_same_timestamp=false: C++-compatible strict +1. - // prev=1015s, now_ms=1000_000ms (1000s) โ†’ gen_utime=1016, gen_utime_ms=1016_000 - let (gen_utime, gen_utime_ms) = Collator::calc_utime(1015, 1_000_000, false); - assert_eq!(gen_utime, 1016); - assert_eq!(gen_utime_ms, 1_016_000); - assert_eq!(gen_utime_ms / 1000, gen_utime as u64); -} - -#[test] -fn test_calc_utime_ms_preserves_milliseconds_when_now_dominates() { - // now_ms=2000_500ms (2000.5s), prev=1000s โ†’ gen_utime=2000, gen_utime_ms=2000_500 - let (gen_utime, gen_utime_ms) = Collator::calc_utime(1000, 2_000_500, true); - assert_eq!(gen_utime, 2000); - assert_eq!(gen_utime_ms, 2_000_500); - assert_eq!(gen_utime_ms / 1000, gen_utime as u64); -} - -#[test] -fn test_calc_utime_invariant_ms_div_1000_equals_seconds() { - // Verify the core invariant across various inputs - for &(prev, now_ms, allow_same) in &[ - (100u32, 100_500u64, true), - (100, 100_500, false), - (100, 99_999, true), - (100, 99_999, false), - (100, 101_000, true), - (0, 1_000, true), - (0, 1_000, false), - ] { - let (gen_utime, gen_utime_ms) = Collator::calc_utime(prev, now_ms, allow_same); - assert_eq!( - gen_utime_ms / 1000, - gen_utime as u64, - "invariant violated: prev={prev}, now_ms={now_ms}, allow_same={allow_same}" - ); - } -} - -#[test] -fn test_calc_utime_strict_mode_saturates_at_u32_max() { - // Strict mode would normally add +1 second, but must not wrap at u32::MAX. - let (gen_utime, gen_utime_ms) = Collator::calc_utime(u32::MAX, 0, false); - assert_eq!(gen_utime, u32::MAX); - assert_eq!(gen_utime_ms, u32::MAX as u64 * 1000); - assert_eq!(gen_utime_ms / 1000, gen_utime as u64); + assert_eq!(Collator::calc_utime(1015, 1000, false), 1016); } #[tokio::test(flavor = "multi_thread")] diff --git a/src/node/src/validator/tests/test_session_id.rs b/src/node/src/validator/tests/test_session_id.rs index 4e2883a..851c741 100644 --- a/src/node/src/validator/tests/test_session_id.rs +++ b/src/node/src/validator/tests/test_session_id.rs @@ -16,7 +16,7 @@ use std::{ sync::Arc, time::Duration, }; -use ton_block::{signature::SigPubKey, validators::ValidatorDescr, Ed25519KeyOption}; +use ton_block::{signature::SigPubKey, validators::ValidatorDescr}; fn parse_shard_ident(parser: &LogParser, name: &str) -> ShardIdent { ShardIdent::with_tagged_prefix( @@ -131,7 +131,6 @@ fn do_test_catchain_unsafe_rotate(s: &str) { p.general_session_info.clone(), p.val_set.list(), true, - true, Some(prev_block), &config, false, @@ -194,391 +193,3 @@ fn test_session_id_unsafe_v2() { assert_eq!(base64_encode(hash), "zGHYA323cMOmXestPYStZs5hVCDfOY2mdQm2l9zF4Bo="); } } - -#[test] -fn test_cxx_interop_session_options_hash_ignores_accelerated_fields() { - let base_opts = CatchainSessionOptions { - proto_version: 4, - round_candidates: 3, - max_round_attempts: 4, - max_block_size: 1024, - max_collated_data_size: 2048, - new_catchain_ids: true, - ..Default::default() - }; - let (base_hash, base_serialized) = get_validator_session_options_hash(base_opts.clone(), 100); - - let mut accelerated_opts = base_opts.clone(); - accelerated_opts.accelerated_consensus_enabled = true; - accelerated_opts.accelerated_consensus_collation_retry_timeout = Duration::from_millis(777); - accelerated_opts.accelerated_consensus_skip_rounds_count_for_collator_rotation = 9; - accelerated_opts.accelerated_consensus_max_precollated_blocks = 17; - - let (accelerated_hash, _) = get_validator_session_options_hash(accelerated_opts.clone(), 100); - let (interop_hash, interop_serialized) = - get_cxx_interop_session_options_hash(&accelerated_opts, 100); - - assert_eq!( - accelerated_hash, base_hash, - "validator-session config hashing must stay C++-compatible even if runtime accelerated fields differ" - ); - assert_eq!(interop_hash, base_hash, "interop hash must stay aligned with C++"); - assert_eq!(interop_serialized, base_serialized, "interop serialization must match C++ options"); -} - -fn make_test_consensus_config() -> ConsensusConfig { - ConsensusConfig { - new_catchain_ids: true, - round_candidates: 3, - next_candidate_delay_ms: 2000, - consensus_timeout_ms: 16000, - fast_attempts: 4, - attempt_duration: 8, - catchain_max_deps: 4, - max_block_bytes: 1024, - max_collated_bytes: 2048, - proto_version: 4, - catchain_max_blocks_coeff: 2500000, - } -} - -#[cfg(not(feature = "xp25"))] -#[test] -fn test_session_id_hashes_stay_shared_without_xp25() { - let consensus_config = make_test_consensus_config(); - let catchain_config = CatchainConfig::default(); - let mc_options = CatchainSessionOptions { - max_round_attempts: 5, - max_block_size: 1024, - max_collated_data_size: 2048, - new_catchain_ids: true, - proto_version: 4, - ..Default::default() - }; - let shard_options = CatchainSessionOptions { - max_round_attempts: 6, - max_block_size: 1024, - max_collated_data_size: 2048, - new_catchain_ids: true, - proto_version: 4, - ..Default::default() - }; - - let ((mc_hash, _), (shard_hash, _)) = get_session_id_hashes( - &consensus_config, - &catchain_config, - &mc_options, - &shard_options, - 100, - ); - - assert_eq!(mc_hash, shard_hash, "non-xp25 must keep one shared C++-compatible opts_hash"); -} - -#[cfg(feature = "xp25")] -#[test] -fn test_session_id_hashes_can_differ_with_xp25() { - let consensus_config = make_test_consensus_config(); - let catchain_config = CatchainConfig::default(); - let mc_options = CatchainSessionOptions { - max_round_attempts: 5, - max_block_size: 1024, - max_collated_data_size: 2048, - new_catchain_ids: true, - proto_version: 4, - ..Default::default() - }; - let shard_options = CatchainSessionOptions { - max_round_attempts: 6, - max_block_size: 1024, - max_collated_data_size: 2048, - new_catchain_ids: true, - proto_version: 4, - ..Default::default() - }; - - let ((mc_hash, _), (shard_hash, _)) = get_session_id_hashes( - &consensus_config, - &catchain_config, - &mc_options, - &shard_options, - 100, - ); - - assert_ne!(mc_hash, shard_hash, "xp25 must allow MC/shard opts_hash to diverge"); -} - -// --------------------------------------------------------------------------- -// ValidatorListStatus and validator-manager helper tests -// --------------------------------------------------------------------------- - -fn make_test_key() -> PublicKey { - Ed25519KeyOption::generate().unwrap() -} - -fn make_validator_descr_from_key(key: &PublicKey) -> ValidatorDescr { - ValidatorDescr::with_params(SigPubKey::from_bytes(key.pub_key().unwrap()).unwrap(), 1, None) -} - -#[test] -fn test_validator_list_status_get_local_key_for_list() { - let mut status = ValidatorListStatus::default(); - let key_a = make_test_key(); - let key_b = make_test_key(); - let list_curr = UInt256::from_slice(&[1u8; 32]); - let list_next = UInt256::from_slice(&[2u8; 32]); - - status.add_list(list_curr.clone(), vec![key_a.clone()], true); - status.add_list(list_next.clone(), vec![key_b.clone()], true); - status.curr = Some(list_curr.clone()); - status.next = Some(list_next.clone()); - - // get_local_keys returns only the curr list's keys - let local = status.get_local_keys().unwrap(); - assert_eq!(local[0].id(), key_a.id()); - - // get_local_keys_for_list returns the keys for the specified list - let local_curr = status.get_local_keys_for_list(&list_curr).unwrap(); - assert_eq!(local_curr[0].id(), key_a.id()); - - let local_next = status.get_local_keys_for_list(&list_next).unwrap(); - assert_eq!(local_next[0].id(), key_b.id()); - - // unknown list returns None - let unknown = UInt256::from_slice(&[3u8; 32]); - assert!(status.get_local_keys_for_list(&unknown).is_none()); -} - -#[test] -fn test_validator_list_status_get_local_key_curr_none() { - let mut status = ValidatorListStatus::default(); - let key = make_test_key(); - let list_next = UInt256::from_slice(&[2u8; 32]); - - status.add_list(list_next.clone(), vec![key.clone()], true); - status.next = Some(list_next.clone()); - // curr is None - - // get_local_keys returns None when curr is None - assert!(status.get_local_keys().is_none()); - - // but get_local_keys_for_list still finds the next list key - let found = status.get_local_keys_for_list(&list_next).unwrap(); - assert_eq!(found[0].id(), key.id()); -} - -#[test] -fn test_validator_list_status_actual_or_coming() { - let mut status = ValidatorListStatus::default(); - let key = make_test_key(); - let list_curr = UInt256::from_slice(&[1u8; 32]); - let list_next = UInt256::from_slice(&[2u8; 32]); - let list_old = UInt256::from_slice(&[3u8; 32]); - - status.add_list(list_curr.clone(), vec![key.clone()], true); - status.add_list(list_next.clone(), vec![key.clone()], true); - status.curr = Some(list_curr.clone()); - status.next = Some(list_next.clone()); - - assert!(status.actual_or_coming(&list_curr)); - assert!(status.actual_or_coming(&list_next)); - assert!(!status.actual_or_coming(&list_old)); -} - -#[test] -fn test_validator_list_status_network_readiness() { - let mut status = ValidatorListStatus::default(); - let key = make_test_key(); - let list_id = UInt256::from_slice(&[9u8; 32]); - - status.add_list(list_id.clone(), vec![key.clone()], false); - assert!(!status.is_list_network_ready(&list_id)); - assert_eq!(status.get_local_keys_for_list(&list_id).unwrap()[0].id(), key.id()); - - status.add_list(list_id.clone(), vec![key], true); - assert!(status.is_list_network_ready(&list_id)); -} - -#[test] -fn test_validator_list_status_ready_current_list_requires_network_readiness() { - let mut status = ValidatorListStatus::default(); - let key = make_test_key(); - let list_id = UInt256::from_slice(&[7u8; 32]); - - status.add_list(list_id.clone(), vec![key.clone()], false); - status.curr = Some(list_id.clone()); - assert!(status.get_ready_current_list().is_none()); - - status.add_list(list_id.clone(), vec![key], true); - assert_eq!(status.get_ready_current_list(), Some(&list_id)); -} - -#[test] -fn test_validator_list_status_ready_current_list_ignores_next_only_membership() { - let mut status = ValidatorListStatus::default(); - let key = make_test_key(); - let next_list = UInt256::from_slice(&[8u8; 32]); - - status.add_list(next_list.clone(), vec![key], true); - status.next = Some(next_list); - - assert!(status.get_ready_current_list().is_none()); -} - -#[test] -fn test_validator_list_status_next_only_ready_list_remains_usable_for_future_sessions() { - let mut status = ValidatorListStatus::default(); - let key = make_test_key(); - let next_list = UInt256::from_slice(&[10u8; 32]); - - status.add_list(next_list.clone(), vec![key], true); - status.next = Some(next_list.clone()); - - assert!(status.get_ready_current_list().is_none()); - assert!(status.is_list_network_ready(&next_list)); -} - -#[test] -fn test_find_local_validator_key_uses_local_key_order_per_subset() { - let key_a = make_test_key(); - let key_b = make_test_key(); - let validators = vec![make_validator_descr_from_key(&key_b)]; - let local_keys = vec![key_a, key_b.clone()]; - - let selected = find_local_validator_key(&validators, Some(local_keys.as_slice())) - .expect("second local key should match the subset"); - assert_eq!(selected.id(), key_b.id()); -} - -#[test] -fn test_session_id_new_catchain_ids_true_succeeds() { - let session_info = Arc::new(GeneralSessionInfo { - shard: ShardIdent::masterchain(), - opts_hash: UInt256::default(), - catchain_seqno: 1, - key_seqno: 0, - max_vertical_seqno: 0, - }); - let key = make_test_key(); - let val = make_validator_descr_from_key(&key); - - let result = get_session_id_serialize(session_info, &[val], true); - assert!(!result.is_empty()); -} - -#[test] -#[should_panic(expected = "Old catchain IDs format")] -fn test_session_id_new_catchain_ids_false_panics() { - let session_info = Arc::new(GeneralSessionInfo { - shard: ShardIdent::masterchain(), - opts_hash: UInt256::default(), - catchain_seqno: 1, - key_seqno: 0, - max_vertical_seqno: 0, - }); - let key = make_test_key(); - let val = make_validator_descr_from_key(&key); - - // This should panic with the assert message - let _ = get_session_id_serialize(session_info, &[val], false); -} - -#[test] -fn test_session_id_with_accelerated_consensus() { - let session_info = Arc::new(GeneralSessionInfo { - shard: ShardIdent::masterchain(), - opts_hash: UInt256::default(), - catchain_seqno: 1, - key_seqno: 0, - max_vertical_seqno: 0, - }); - let key = make_test_key(); - let val = make_validator_descr_from_key(&key); - - let id_without = get_session_id(session_info.clone(), &[val.clone()], true, false); - let id_with = get_session_id(session_info, &[val], true, true); - - // Accelerated consensus tag changes the session ID - assert_ne!(id_without, id_with); -} - -#[test] -fn test_find_local_validator_key_matches_subset() { - let key = make_test_key(); - let validator = make_validator_descr_from_key(&key); - let local_keys = vec![key.clone()]; - - let found = find_local_validator_key(&[validator], Some(local_keys.as_slice())) - .expect("local validator key should match the subset"); - - assert_eq!(found.id(), key.id()); -} - -#[test] -fn test_find_local_validator_key_returns_none_when_key_not_in_subset() { - let key = make_test_key(); - let other_key = make_test_key(); - let validator = make_validator_descr_from_key(&other_key); - let local_keys = vec![key]; - - assert!(find_local_validator_key(&[validator], Some(local_keys.as_slice())).is_none()); -} - -#[test] -fn test_should_skip_session_for_unsafe_rotation_matches_cpp_policy() { - let masterchain = ShardIdent::masterchain(); - let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - - assert!(!should_skip_session_for_unsafe_rotation(false, &masterchain)); - assert!(!should_skip_session_for_unsafe_rotation(false, &shard)); - assert!(!should_skip_session_for_unsafe_rotation(true, &masterchain)); - assert!(should_skip_session_for_unsafe_rotation(true, &shard)); -} - -#[test] -fn test_unsafe_rotation_block_seqno_uses_last_masterchain_block_only() { - let masterchain = ShardIdent::masterchain(); - let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - let last_masterchain_block = BlockIdExt::with_params( - ShardIdent::masterchain(), - 777, - UInt256::default(), - UInt256::default(), - ); - - assert_eq!(unsafe_rotation_block_seqno(&masterchain, &last_masterchain_block), Some(777)); - assert_eq!(unsafe_rotation_block_seqno(&shard, &last_masterchain_block), None); -} - -#[test] -fn test_get_session_unsafe_id_skips_patch_when_flag_false() { - let session_info = Arc::new(GeneralSessionInfo { - shard: ShardIdent::masterchain(), - opts_hash: UInt256::default(), - catchain_seqno: 42, - key_seqno: 0, - max_vertical_seqno: 0, - }); - let key = make_test_key(); - let val = make_validator_descr_from_key(&key); - - let mut config = ValidatorManagerConfig::default(); - config.unsafe_catchain_rotates.insert(42, (1, 99)); - - let plain_id = get_session_id(session_info.clone(), &[val.clone()], true, false); - - let id_with_flag_false = get_session_unsafe_id( - session_info.clone(), - &[val.clone()], - true, - false, - Some(100), - &config, - false, - ); - assert_eq!(id_with_flag_false, plain_id, "flag=false must return the plain session ID"); - - let id_with_flag_true = - get_session_unsafe_id(session_info, &[val], true, true, Some(100), &config, false); - assert_ne!(id_with_flag_true, plain_id, "flag=true must apply the unsafe rotation patch"); -} diff --git a/src/node/src/validator/validate_query.rs b/src/node/src/validator/validate_query.rs index 4c76662..8f91d99 100644 --- a/src/node/src/validator/validate_query.rs +++ b/src/node/src/validator/validate_query.rs @@ -41,28 +41,28 @@ use std::{ mem, sync::{ atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}, - Arc, Mutex, + Arc, }, time::Instant, }; #[cfg(test)] use ton_block::{base64_encode, write_boc, UsageTree}; +#[cfg(feature = "xp25")] +use ton_block::{fail, ShardDescr, SHARD_FULL}; use ton_block::{ - fail, read_boc, Account, AccountBlock, AccountDispatchQueue, AccountId, AccountIdPrefixFull, + read_boc, Account, AccountBlock, AccountDispatchQueue, AccountId, AccountIdPrefixFull, AccountStatus, AccountStorageDictProof, AddSub, Block, BlockCreateStats, BlockError, BlockExtra, BlockIdExt, BlockInfo, BlockLimits, Cell, CellType, Coins, ConfigParamEnum, - ConfigParams, ConsensusExtraData, Counters, CreatorStats, CurrencyCollection, DepthBalanceInfo, - Deserializable, EnqueuedMsg, FundamentalSmcAddresses, GlobalCapabilities, HashmapAugType, - HashmapType, InMsg, InMsgDescr, KeyExtBlkRef, KeyMaxLt, LibDescr, Libraries, McBlockExtra, - McShardRecord, McStateExtra, MerkleProof, MerkleUpdate, Message, MsgAddressInt, MsgEnvelope, - MsgMetadata, OutMsg, OutMsgDescr, OutMsgQueueKey, Result, Serializable, ShardAccount, - ShardAccountBlocks, ShardAccounts, ShardFeeCreated, ShardHashes, ShardIdent, ShardStateUnsplit, - SizeLimitsConfig, SliceData, StateInitLib, TopBlockDescrSet, TrComputePhase, Transaction, - TransactionDescr, UInt15, UInt256, ValidatorSet, ValueFlow, WorkchainDescr, - INVALID_WORKCHAIN_ID, MASTERCHAIN_ID, MAX_SPLIT_DEPTH, + ConfigParams, Counters, CreatorStats, CurrencyCollection, DepthBalanceInfo, Deserializable, + EnqueuedMsg, FundamentalSmcAddresses, GlobalCapabilities, HashmapAugType, HashmapType, InMsg, + InMsgDescr, KeyExtBlkRef, KeyMaxLt, LibDescr, Libraries, McBlockExtra, McShardRecord, + McStateExtra, MerkleProof, MerkleUpdate, Message, MsgAddressInt, MsgEnvelope, MsgMetadata, + OutMsg, OutMsgDescr, OutMsgQueueKey, Result, Serializable, ShardAccount, ShardAccountBlocks, + ShardAccounts, ShardFeeCreated, ShardHashes, ShardIdent, ShardStateUnsplit, SizeLimitsConfig, + SliceData, StateInitLib, TopBlockDescrSet, TrComputePhase, Transaction, TransactionDescr, + UInt15, UInt256, ValidatorSet, ValueFlow, WorkchainDescr, INVALID_WORKCHAIN_ID, MASTERCHAIN_ID, + MAX_SPLIT_DEPTH, }; -#[cfg(feature = "xp25")] -use ton_block::{ShardDescr, SHARD_FULL}; use ton_executor::{ BlockchainConfig, ExecuteParams, OrdinaryTransactionExecutor, TickTockTransactionExecutor, TransactionExecutor, @@ -115,7 +115,6 @@ struct ValidateResult { removed_dispatch_queue_messages: lockfree::map::Map<(AccountId, u64), Cell>, new_dispatch_queue_messages: lockfree::map::Map<(AccountId, u64), Cell>, account_expected_defer_all_messages: lockfree::set::Set, - blackhole_burned: Mutex, } impl Default for ValidateResult { @@ -136,7 +135,6 @@ impl Default for ValidateResult { removed_dispatch_queue_messages: lockfree::map::Map::new(), new_dispatch_queue_messages: lockfree::map::Map::new(), account_expected_defer_all_messages: lockfree::set::Set::new(), - blackhole_burned: Mutex::new(Coins::default()), } } } @@ -144,7 +142,6 @@ impl Default for ValidateResult { struct ValidateBase { global_id: i32, is_fake: bool, - is_simplex: bool, created_by: UInt256, after_merge: bool, after_split: bool, @@ -170,7 +167,6 @@ struct ValidateBase { virt_states: HashMap, // prev state and neighbour out msg queues proofs by block root hash storage_dict_proofs: HashMap, full_collated_data: bool, - now_ms: Option, // gen_utime_ms from ConsensusExtraData (simplex consensus) gas_used: Arc, transactions_executed: Arc, @@ -261,7 +257,6 @@ pub struct ValidateQuery { validator_set: ValidatorSet, is_fake: bool, multithread: bool, - is_simplex: bool, // previous state can be as two states for merge prev_blocks_ids: Vec, old_mc_shards: ShardHashes, // old_shard_conf_ @@ -300,7 +295,6 @@ impl ValidateQuery { engine: Arc, is_fake: bool, multithread: bool, - is_simplex: bool, ) -> Self { let next_block_descr = Arc::new(fmt_next_block_descr(&block_candidate.block_id)); Self { @@ -311,7 +305,6 @@ impl ValidateQuery { validator_set, is_fake, multithread, - is_simplex, prev_blocks_ids, old_mc_shards: Default::default(), // new state after applying block_candidate @@ -334,7 +327,6 @@ impl ValidateQuery { let mut base = ValidateBase { next_block_descr: self.next_block_descr.clone(), is_fake: self.is_fake, - is_simplex: self.is_simplex, created_by: self.block_candidate.created_by.clone(), prev_blocks_ids: mem::take(&mut self.prev_blocks_ids), ..Default::default() @@ -634,55 +626,17 @@ impl ValidateQuery { if let Some(BlockError::InvalidConstructorTag { t: _, s: _ }) = err.downcast_ref() { - // Try AccountStorageDictProof first - match AccountStorageDictProof::construct_from_cell(croot.clone()) { - Ok(dict_proof) => { - log::debug!( - target: "validate_query", - "({}): collated datum # {idx} is an AccountStorageDictProof", - base.next_block_descr - ); - let dict_proof = - MerkleProof::construct_from_cell(dict_proof.proof)?; - base.storage_dict_proofs - .insert(dict_proof.hash, dict_proof.proof.virtualize(1)); - base.full_collated_data = true; - } - Err(_) => { - // Try ConsensusExtraData - match ConsensusExtraData::construct_from_cell(croot.clone()) { - Ok(extra) => { - log::debug!( - target: "validate_query", - "({}): collated datum # {idx} is a ConsensusExtraData, gen_utime_ms={}", - base.next_block_descr, - extra.gen_utime_ms - ); - if base.now_ms.is_some() { - reject_query!( - "duplicate ConsensusExtraData in collated data" - ) - } - // Check: ConsensusExtraData is only valid when simplex is enabled - if !base.is_simplex { - reject_query!("unexpected ConsensusExtraData") - } - base.now_ms = Some(extra.gen_utime_ms); - } - Err(_) => { - let tag = SliceData::load_cell_ref(croot)? - .get_next_u32() - .unwrap_or(0); - log::warn!( - target: "validate_query", - "({}): collated datum # {idx} has unknown type (tag {:#010x}), ignoring", - base.next_block_descr, - tag - ); - } - } - } - } + let dict_proof = + AccountStorageDictProof::construct_from_cell(croot.clone())?; + log::debug!( + target: "validate_query", + "({}): collated datum # {idx} is an AccountStorageDictProof", + base.next_block_descr + ); + let dict_proof = MerkleProof::construct_from_cell(dict_proof.proof)?; + base.storage_dict_proofs + .insert(dict_proof.hash, dict_proof.proof.virtualize(1)); + base.full_collated_data = true; } else { return Err(err); } @@ -2068,19 +2022,30 @@ impl ValidateQuery { fn check_utime_lt(&self, base: &ValidateBase, mc_data: &McData) -> Result<()> { CHECK!(&base.config_params, inited); - // C++ parity: allow_same_timestamp_ = global_version_ >= 13. - // Depends only on the global protocol version, not on consensus type. - // When true, also skips the future-time check (base.now > engine.now + 15) - // which C++ simplex testnet does not have. let allow_same_timestamp = { - #[cfg(feature = "xp25")] + #[cfg(feature = "simplex")] { - true + let simplex_enabled_for_shard = if base.shard().is_masterchain() { + base.config_params.get_mc_simplex_config().ok().flatten().is_some() + } else { + base.config_params.get_shard_simplex_config().ok().flatten().is_some() + }; + + simplex_enabled_for_shard + //TODO: LK: enable after change block version to 13 + //&& base.config_params.global_version() + // >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION } - #[cfg(not(feature = "xp25"))] + #[cfg(not(feature = "simplex"))] { - base.config_params.global_version() - >= super::SIMPLEX_ALLOW_SAME_TIMESTAMP_FROM_GLOBAL_VERSION + #[cfg(feature = "xp25")] + { + true + } + #[cfg(not(feature = "xp25"))] + { + false + } } }; let mut gen_lt = u64::MIN; @@ -2124,19 +2089,14 @@ impl ValidateQuery { ) } - // C++ parity: C++ variants (mainnet, simplex-testnet, alpenglow-work) do not - // reject blocks for being too far in the future. To stay compatible with blocks - // produced by C++ collators, we only emit a warning instead of rejecting. - if !allow_same_timestamp { - let now = self.engine.now(); - if base.now() > now + 15 { - log::warn!( - "block has creation time {} too much in the future (local time is {now})", - base.now(), - ); - } + let now = self.engine.now(); + if base.now() > now + 15 { + reject_query!( + "block has creation time {} too much in the future (it is only {} now)", + base.now(), + now + ) } - if base.info.start_lt() <= mc_data.state.state()?.gen_lt() { reject_query!( "block has start_lt {} less than or equal to lt {} \ @@ -2174,19 +2134,6 @@ impl ValidateQuery { delta_hard ) } - if base.is_simplex { - match base.now_ms { - None => reject_query!("now_ms is not set"), - Some(now_ms) if now_ms / 1000 != base.info.gen_utime() as u64 => { - reject_query!( - "gen_utime is {}, but gen_utime_ms in ConsensusExtraData is {}", - base.info.gen_utime(), - now_ms - ) - } - _ => {} - } - } Ok(()) } @@ -2345,95 +2292,28 @@ impl ValidateQuery { ) } let transaction_fees = base.account_blocks.full_transaction_fees(); - let expected_fee_burned = Self::expected_fee_burned(&base, transaction_fees, &fees_import)?; - if !base.shard().is_masterchain() && !base.value_flow.burned.is_zero()? { - reject_query!( - "ValueFlow of block {} is invalid (non-zero burned value in a non-masterchain block)", - base.block_id() - ) - } - let mut expected_fees = transaction_fees.clone(); expected_fees.add(&base.value_flow.fees_imported)?; expected_fees.add(&base.value_flow.created)?; expected_fees.add(&fees_import)?; - expected_fees.sub(&expected_fee_burned)?; if base.value_flow.fees_collected != expected_fees { reject_query!( "ValueFlow for {} declares fees_collected={} but \ the total message import fees are {}, the total transaction fees are {}, \ creation fee for this block is {} and the total imported fees from shards \ - are {}, the burned fees are {} with a total of {}", + are {} with a total of {}", base.block_id(), base.value_flow.fees_collected.coins, fees_import, transaction_fees.coins, base.value_flow.created.coins, base.value_flow.fees_imported.coins, - expected_fee_burned.coins, expected_fees.coins ) } Ok(()) } - fn expected_fee_burned( - base: &ValidateBase, - transaction_fees: &CurrencyCollection, - fees_import: &CurrencyCollection, - ) -> Result { - if !base.shard().is_masterchain() { - return Ok(Default::default()); - } - let Some(ConfigParamEnum::ConfigParam5(burning_cfg)) = base.config_params.config(5)? else { - return Ok(Default::default()); - }; - - let total_fees = transaction_fees.coins.as_u128() + fees_import.coins.as_u128(); - let mut burned = burning_cfg.calculate_burned_fees(total_fees)?; - - let mut imported_base = base.value_flow.fees_imported.clone(); - if !imported_base.sub(&base.mc_extra.fees().root_extra().create)? { - fail!( - "fees_imported ({}) is smaller than imported created fees ({})", - base.value_flow.fees_imported, - base.mc_extra.fees().root_extra().create - ); - } - let burned_imported = burning_cfg.calculate_burned_fees(imported_base.coins.as_u128())?; - burned.add(&burned_imported)?; - Ok(CurrencyCollection::from_coins(burned)) - } - - fn check_burned_value_flow(base: &ValidateBase) -> Result<()> { - if !base.shard().is_masterchain() { - return Ok(()); - } - let fees_import = - CurrencyCollection::from_coins(base.in_msg_descr.full_import_fees().fees_collected); - let mut expected_burned = Self::expected_fee_burned( - base, - base.account_blocks.full_transaction_fees(), - &fees_import, - )?; - expected_burned.coins.add( - &*base - .result - .blackhole_burned - .lock() - .map_err(|_| error!("blackhole burned accumulator is poisoned"))?, - )?; - if base.value_flow.burned != expected_burned { - reject_query!( - "ValueFlow of block {} declares burned fees {}, but the expected value is {}", - base.block_id(), - base.value_flow.burned.coins, - expected_burned - ) - } - Ok(()) - } - // similar to Collator::compute_minted_amount() fn compute_minted_amount(base: &ValidateBase) -> Result { let mut to_mint = CurrencyCollection::default(); @@ -5464,7 +5344,6 @@ impl ValidateQuery { let old_account_root = account_root.clone(); #[cfg(test)] let mut our_trans = None; - let mut blackhole_burned = Coins::default(); let mut error = None; match executor.execute_with_params(in_msg_cell, account, params) { Ok(mut trans_execute) => { @@ -5502,14 +5381,6 @@ impl ValidateQuery { base.gas_used.fetch_add(compute_ph.gas_used.as_u64(), Ordering::Relaxed); } base.transactions_executed.fetch_add(1, Ordering::Relaxed); - blackhole_burned = trans_execute.blackhole_burned().clone(); - if !blackhole_burned.is_zero() { - base.result - .blackhole_burned - .lock() - .map_err(|_| error!("blackhole burned accumulator is poisoned"))? - .add(&blackhole_burned)?; - } // we cannot know prev transaction in executor trans_execute.set_prev_trans_hash(trans.prev_trans_hash().clone()); @@ -5535,20 +5406,18 @@ impl ValidateQuery { let mut right_balance = new_balance.clone(); right_balance.add(&money_exported)?; right_balance.add(trans.total_fees())?; - right_balance.coins.add(&blackhole_burned)?; if left_balance != right_balance { error = Some(error!( "transaction {} of {:x} violates the currency flow condition: \ old balance={} + imported={} does not equal new balance={} + exported=\ - {} + total_fees={} + burned={}", + {} + total_fees={}", lt, account_addr, old_balance.coins, money_imported.coins, new_balance.coins, money_exported.coins, - trans.total_fees().coins, - blackhole_burned + trans.total_fees().coins )); } } @@ -6881,7 +6750,6 @@ impl ValidateQuery { // Self::check_delivered_dequeued(&base, &manager)?; Self::check_all_ticktock_processed(&base)?; Self::check_message_processing_order(&mut base)?; - Self::check_burned_value_flow(&base)?; Self::check_new_state(&mut base, &mc_data, &manager)?; Self::check_mc_block_extra(&base, &mc_data)?; self.check_mc_state_extra(&base, &mc_data)?; diff --git a/src/node/src/validator/validator_group.rs b/src/node/src/validator/validator_group.rs index ed8f10c..33d22a6 100644 --- a/src/node/src/validator/validator_group.rs +++ b/src/node/src/validator/validator_group.rs @@ -68,23 +68,6 @@ fn is_simplex_roundless(round: u32) -> bool { round == SIMPLEX_ROUNDLESS } -/// Snapshot of session state for monitoring dumps. -/// Captured atomically from the inner mutex to avoid multiple async calls. -pub struct SessionSnapshot { - pub session_id: UInt256, - pub shard: ShardIdent, - pub cc_seqno: u32, - pub status: ValidatorGroupStatus, - pub consensus_type: ConsensusType, - pub round: u32, - pub mc_initial_seqno: u32, - pub has_engine: bool, - pub is_collator: bool, - pub created_at: SystemTime, - pub key_seqno: u32, - pub last_accepted_mc_seqno: Option, -} - /// When true, non-accelerated consensus (Catchain / Simplex) will block the /// validator-group message loop while a validation task runs. /// Set to false (default) to let those tasks run in the background, keeping @@ -133,7 +116,7 @@ fn should_reject_stale_mc_candidate( #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum ValidatorGroupStatus { Created, - EngineCreated, + Countdown { start_at: tokio::time::Instant }, Sync, Active, Stopping, @@ -144,7 +127,10 @@ impl Display for ValidatorGroupStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ValidatorGroupStatus::Created => write!(f, "created"), - ValidatorGroupStatus::EngineCreated => write!(f, "engine_created"), + ValidatorGroupStatus::Countdown { start_at: at } => { + let now = tokio::time::Instant::now(); + write!(f, "cntdwn {}", at.saturating_duration_since(now).as_secs()) + } ValidatorGroupStatus::Sync => write!(f, "sync"), ValidatorGroupStatus::Active => write!(f, "active"), ValidatorGroupStatus::Stopping => write!(f, "stopping"), @@ -153,22 +139,14 @@ impl Display for ValidatorGroupStatus { } } -impl ValidatorGroupStatus { - pub fn metric_label(&self) -> &'static str { - match self { - Self::Created => "created", - Self::EngineCreated => "engine_created", - Self::Sync => "sync", - Self::Active => "active", - Self::Stopping => "stopping", - Self::Stopped => "stopped", - } - } -} - impl ValidatorGroupStatus { pub fn before(&self, of: &ValidatorGroupStatus) -> bool { - self <= of + match (&self, of) { + (ValidatorGroupStatus::Countdown { .. }, ValidatorGroupStatus::Countdown { .. }) => { + false + } + _ => self <= of, + } } } @@ -266,7 +244,6 @@ pub struct ValidatorGroupImpl { replay_finished: bool, status: ValidatorGroupStatus, - start_pending: bool, /// Highest MC block seqno accepted (committed) in this session. /// Used for MC fork prevention: reject candidates building on stale heads. @@ -275,48 +252,14 @@ pub struct ValidatorGroupImpl { impl Drop for ValidatorGroupImpl { fn drop(&mut self) { - // Does not stop the session to avoid database deletion on validator-manager crash. - log::info!(target: "validator", - "SESSION_LIFECYCLE: dropped shard={} cc_seqno={} session_id={:x} final_status={} \ - has_engine={}", - self.shard, self.cc_seqno, self.session_id, self.status, self.session.is_some()); + // Important: does not stop the session -- to avoid database deletion, + // which otherwise would happen each time the validator-manager crashes. + log::info!(target: "validator", "ValidatorGroupImpl: dropping session {}", self.info()); } } impl ValidatorGroupImpl { - /// Create the consensus engine (session) without starting the validation queue. - /// - /// Two-phase activation (C++ parity): `create_engine()` materializes the consensus - /// session so future groups have a pre-initialized engine. `start()` then spawns - /// the validation queue processor and calls `Session::start(initial_block_seqno)` - /// to begin consensus. If `create_engine()` was not called (e.g. the group was - /// promoted directly), `start()` creates the session inline as a fallback. - fn create_engine( - &mut self, - g: Arc, - session_listener: SessionListenerPtr, - ) -> Result<()> { - if self.session.is_some() { - log::debug!(target: "validator", - "create_engine: session already exists for shard={} cc_seqno={}, skipping", - self.shard, self.cc_seqno); - return Ok(()); - } - if self.status >= ValidatorGroupStatus::Stopping { - fail!("Inactive session cannot have engine created! {}", self.info()) - } - - log::info!(target: "validator", - "SESSION_LIFECYCLE: create_engine shard={} cc_seqno={} session_id={:x} \ - consensus={} status={} -> engine_created", - self.shard, self.cc_seqno, self.session_id, - self.consensus_type, self.status); - let session = self.create_consensus_session(g, session_listener)?; - self.session = Some(session); - self.status = ValidatorGroupStatus::EngineCreated; - Ok(()) - } - + // Creates and starts session #[allow(clippy::too_many_arguments)] fn start( &mut self, @@ -331,35 +274,21 @@ impl ValidatorGroupImpl { fail!("Inactive session cannot be started! {}", self.info()) } + self.status = ValidatorGroupStatus::Sync; + + log::info!(target: "validator", "Starting session {}", self.info()); + self.prev_block_ids.update_prev(prev); self.min_masterchain_block_id = Some(min_masterchain_block_id.clone()); self.min_ts = min_ts; if self.shard.is_masterchain() { + // Seed stale-head guard baseline at session start so fork prevention + // is active before the first local on_block_committed callback. self.last_accepted_mc_seqno = Some(min_masterchain_block_id.seq_no); } - if self.session.is_none() { - log::info!(target: "validator", - "SESSION_LIFECYCLE: create_session_at_start shard={} cc_seqno={} consensus={}", - self.shard, self.cc_seqno, self.consensus_type); - let session = self.create_consensus_session(g.clone(), session_listener)?; - self.session = Some(session); - } - - let initial_block_seqno = self.prev_block_ids.get_next_seqno().unwrap_or(1); - if let Some(session) = &self.session { - log::info!(target: "validator", - "SESSION_LIFECYCLE: session.start(seqno={}) shard={} cc_seqno={}", - initial_block_seqno, self.shard, self.cc_seqno); - session.start(initial_block_seqno); - } - - log::info!(target: "validator", - "SESSION_LIFECYCLE: start shard={} cc_seqno={} session_id={:x} consensus={} \ - mc_init_seqno={} status={} -> sync", - self.shard, self.cc_seqno, self.session_id, self.consensus_type, - self.min_masterchain_block_id.as_ref().map_or(0, |id| id.seq_no), - self.status); + // Create session using unified factory + let session = self.create_consensus_session(g.clone(), session_listener)?; let g_clone = g.clone(); let receiver = g.receiver.lock().unwrap().take().ok_or_else(|| { @@ -369,36 +298,14 @@ impl ValidatorGroupImpl { process_validation_queue(receiver, g_clone.clone(), rt).await; }); - log::debug!(target: "validator", - "Validation queue spawned for shard={} cc_seqno={}, options={:?}", - self.shard, self.cc_seqno, g.consensus_options); + log::trace!(target: "validator", "Started session {}, options {:?}, ref.cnt = {}", + self.info(), g.consensus_options, Arc::strong_count(&session) + ); - self.status = ValidatorGroupStatus::Sync; + self.session = Some(session); Ok(()) } - fn prepare_start(&mut self) -> bool { - if self.start_pending { - return false; - } - match self.status { - ValidatorGroupStatus::Created | ValidatorGroupStatus::EngineCreated => {} - _ => return false, - } - self.start_pending = true; - true - } - - fn reset_after_start_failure(&mut self) { - log::warn!(target: "validator", - "SESSION_LIFECYCLE: reset_after_failure shard={} cc_seqno={} session_id={:x} \ - status={} -> created (session dropped, will retry)", - self.shard, self.cc_seqno, self.session_id, self.status); - self.session = None; - self.start_pending = false; - self.status = ValidatorGroupStatus::Created; - } - /// Get the consensus type for this validator group #[allow(dead_code)] pub fn get_consensus_type(&self) -> ConsensusType { @@ -412,7 +319,8 @@ impl ValidatorGroupImpl { } /// Get simplex session pointer for simplex-specific operations (e.g., MC finalization) - /// Returns None if this is not a simplex session + /// Returns None if this is not a simplex session or simplex feature is not enabled + #[cfg(feature = "simplex")] #[allow(dead_code)] pub fn get_simplex_session(&self) -> Option { self.session.as_ref().and_then(|s| s.get_simplex_session()) @@ -462,10 +370,14 @@ impl ValidatorGroupImpl { ) } ConsensusOptions::Simplex(simplex_options) => { + //TODO: check initial seqno for simplex + let initial_block_seqno = self.prev_block_ids.get_next_seqno().unwrap_or(1); + ConsensusFactory::create_simplex_based_session( simplex_options, &g.session_id, &g.shard, + initial_block_seqno, nodes, &g.local_key, db_root, @@ -508,9 +420,8 @@ impl ValidatorGroupImpl { is_accelerated_consensus_enabled: bool, consensus_type: ConsensusType, ) -> ValidatorGroupImpl { - log::info!(target: "validator", - "SESSION_LIFECYCLE: created shard={} cc_seqno={} session_id={:x} consensus={} local_id={}", - shard, cc_seqno, session_id, consensus_type, local_id); + log::info!(target: "validator", "Initializing session {:x}, shard {}, consensus_type {}", + session_id, shard, consensus_type); let prev_block_ids = PrevBlockHistory::with_shard(&shard); ValidatorGroupImpl { @@ -520,7 +431,6 @@ impl ValidatorGroupImpl { cc_seqno, min_ts: SystemTime::now(), status: ValidatorGroupStatus::Created, - start_pending: false, expected_current_round: 0, expected_collation_round: 0, is_collator: false, @@ -638,8 +548,10 @@ pub struct ValidatorGroup { is_accelerated_consensus_enabled: bool, session_id: SessionId, shard: ShardIdent, + //catchain_seqno: u32, validator_list_id: ValidatorListHash, + //shard: ShardIdent, engine: Arc, validator_set: ValidatorSet, #[allow(dead_code)] @@ -652,8 +564,6 @@ pub struct ValidatorGroup { last_validation_time: Arc, last_collation_time: Arc, is_collating: Arc, - /// Set by the validation queue on prolonged inactivity, cleared on any action. - pub stalled: Arc, } impl ValidatorGroup { @@ -703,7 +613,6 @@ impl ValidatorGroup { last_validation_time: Arc::new(AtomicU64::new(0)), last_collation_time: Arc::new(AtomicU64::new(0)), is_collating: Arc::new(AtomicBool::new(false)), - stalled: Arc::new(AtomicBool::new(false)), } } @@ -718,34 +627,10 @@ impl ValidatorGroup { &self.general_session_info.shard } - pub fn cc_seqno(&self) -> u32 { - self.general_session_info.catchain_seqno - } - pub fn is_simplex(&self) -> bool { matches!(self.consensus_options, ConsensusOptions::Simplex(_)) } - pub async fn snapshot(&self) -> SessionSnapshot { - let key_seqno = self.general_session_info.key_seqno; - self.group_impl - .execute_sync(|g| SessionSnapshot { - session_id: g.session_id.clone(), - shard: g.shard.clone(), - cc_seqno: g.cc_seqno, - status: g.status, - consensus_type: g.consensus_type, - round: g.expected_current_round, - mc_initial_seqno: g.min_masterchain_block_id.as_ref().map_or(0, |id| id.seq_no), - has_engine: g.session.is_some(), - is_collator: g.is_collator, - created_at: g.min_ts, - key_seqno, - last_accepted_mc_seqno: g.last_accepted_mc_seqno, - }) - .await - } - /// Notify this session about masterchain finalization. /// /// For simplex shard sessions, this updates the MC finalization tracking which is @@ -772,10 +657,6 @@ impl ValidatorGroup { .await; } - pub fn is_collating(&self) -> bool { - self.is_collating.load(Ordering::Relaxed) - } - pub fn last_validation_time(&self) -> u64 { self.last_validation_time.load(Ordering::Relaxed) } @@ -788,59 +669,42 @@ impl ValidatorGroup { Arc::downgrade(&self.callback) } - /// Pre-create the consensus engine without starting the validation queue. - /// Called for future-validator groups to warm up the session ahead of time. - pub async fn pre_create_engine(self: Arc) -> Result<()> { - let callback = self.make_validator_session_callback(); - self.group_impl - .execute_sync(|group_impl| { - if let Err(e) = group_impl.create_engine(self.clone(), callback) { - log::error!(target: "validator", - "SESSION_LIFECYCLE: pre_create_engine failed shard={} cc_seqno={} \ - session_id={:x}: {}", - group_impl.shard, group_impl.cc_seqno, group_impl.session_id, e); - } - }) - .await; - Ok(()) - } - #[allow(clippy::too_many_arguments)] - pub async fn start_session( + pub async fn start_with_status( self: Arc, + validation_start_status: ValidatorGroupStatus, prev: Vec, min_masterchain_block_id: BlockIdExt, min_ts: SystemTime, rt: tokio::runtime::Handle, ) -> Result<()> { - rt.clone().spawn(async move { + self.set_status(validation_start_status).await?; + rt.clone().spawn (async move { + if let ValidatorGroupStatus::Countdown { start_at } = validation_start_status { + log::trace!(target: "validator", "Session delay started: {}", self.info().await); + tokio::time::sleep_until(start_at).await; + } + let callback = self.make_validator_session_callback(); - self.group_impl - .execute_sync(|group_impl| { - if group_impl.status <= ValidatorGroupStatus::Active { - if let Err(e) = group_impl.start( - callback, - prev, - min_masterchain_block_id, - min_ts, - self.clone(), - rt, - ) { - group_impl.reset_after_start_failure(); - log::error!( - target: "validator", - "Cannot start group: {}; resetting session to Created for retry", - e - ); - } else { - group_impl.start_pending = false; - } - } else { - group_impl.start_pending = false; - log::trace!(target: "validator", "Session deleted before start: {}", group_impl.info()); + self.group_impl.execute_sync(|group_impl| + { + if group_impl.status <= ValidatorGroupStatus::Active { + if let Err(e) = group_impl.start( + callback, + prev, + min_masterchain_block_id, + min_ts, + self.clone(), + rt + ) + { + log::error!(target: "validator", "Cannot start group: {}", e); } - }) - .await; + } + else { + log::trace!(target: "validator", "Session deleted before countdown: {}", group_impl.info()); + } + }).await; }); Ok(()) } @@ -851,40 +715,39 @@ impl ValidatorGroup { destroy_database: bool, ) -> Result<()> { self.set_status(ValidatorGroupStatus::Stopping).await?; - let consensus_label = if self.is_simplex() { "simplex" } else { "catchain" }; - metrics::counter!("ton_node_validator_session_stopped_total", "consensus" => consensus_label) - .increment(1); - let shard = self.shard.clone(); - let cc_seqno = self.cc_seqno(); - log::info!(target: "validator", - "SESSION_LIFECYCLE: stop_initiated shard={} cc_seqno={} destroy_db={}", - shard, cc_seqno, destroy_database); + log::info!(target: "validator", "Stopping group: {} (destroy database {})", self.info().await, destroy_database); let group_impl = self.group_impl.clone(); let self_clone = self.clone(); rt.spawn({ async move { - let session_ptr = - group_impl.execute_sync(|group_impl| group_impl.session.clone()).await; + log::debug!(target: "validator", "Stopping group (spawn): {}", self_clone.info().await); + let session_ptr = group_impl.execute_sync( + |group_impl| group_impl.session.clone()).await; if let Some(s_ptr) = session_ptr { - log::debug!(target: "validator", - "Stopping consensus engine for shard={} cc_seqno={}", shard, cc_seqno); + log::debug!(target: "validator", "Stopping catchain: {}", self_clone.info().await); if destroy_database { - s_ptr.destroy(); + s_ptr.destroy(); // Blocking, destroys catchain DB } else { - s_ptr.stop(); + s_ptr.stop(); // Blocking, preserves catchain DB } } + log::debug!(target: "validator", "Group stopped: {}", self_clone.info().await); let _ = self_clone.set_status(ValidatorGroupStatus::Stopped).await; + log::info!(target: "validator", "Status set: {}", self_clone.info().await); if destroy_database { let _ = self_clone.destroy_db().await; - log::debug!(target: "validator", - "DB destroyed for shard={} cc_seqno={}", shard, cc_seqno); + log::debug!(target: "validator", "Db destroyed: {}", self_clone.info().await); + } + else { + log::debug!( + target: "validator", + "Db destroy skipped (destroy_databse option set to false): {}", + self_clone.info().await + ); } - log::info!(target: "validator", - "SESSION_LIFECYCLE: stopped shard={} cc_seqno={} destroy_db={}", - shard, cc_seqno, destroy_database); } }); + log::debug!(target: "validator", "Stopping group {}, stop spawned", self.info().await); Ok(()) } @@ -906,30 +769,13 @@ impl ValidatorGroup { self.group_impl.execute_sync(|group_impl| group_impl.status).await } - pub async fn is_start_pending(&self) -> bool { - self.group_impl.execute_sync(|group_impl| group_impl.start_pending).await - } - - pub async fn try_prepare_start(&self) -> Result { - self.group_impl.execute_sync(|group_impl| Ok(group_impl.prepare_start())).await - } - pub async fn set_status(&self, status: ValidatorGroupStatus) -> Result<()> { self.group_impl .execute_sync(|group_impl| { if group_impl.status.before(&status) { - let from = group_impl.status; group_impl.status = status; - log::info!(target: "validator", - "SESSION_LIFECYCLE: transition shard={} cc_seqno={} session_id={:x} {} -> {}", - group_impl.shard, group_impl.cc_seqno, group_impl.session_id, from, status); Ok(()) } else { - log::error!(target: "validator", - "SESSION_LIFECYCLE: invalid transition shard={} cc_seqno={} session_id={:x} \ - {} -> {} (monotonic violation)", - group_impl.shard, group_impl.cc_seqno, group_impl.session_id, - group_impl.status, status); fail!("Status cannot retreat, from {} to {}", group_impl.status, status) } }) @@ -1122,14 +968,11 @@ impl ValidatorGroup { ConsensusOptions::Catchain(opts) => { opts.accelerated_consensus_max_precollated_blocks as usize } - ConsensusOptions::Simplex(opts) => { - opts.slots_per_leader_window.saturating_sub(1) as usize - } + ConsensusOptions::Simplex(opts) => opts.max_precollated_blocks as usize, }; let request_clone = request.clone(); let cc_seqno = self.general_session_info.catchain_seqno; let is_masterchain = self.shard.is_masterchain(); - let is_simplex = matches!(self.consensus_options, ConsensusOptions::Simplex(_)); let collation_task = tokio::spawn(async move { log::info!( @@ -1164,7 +1007,6 @@ impl ValidatorGroup { local_key, validator_set.clone(), engine.clone(), - is_simplex, ) .await { @@ -1331,7 +1173,6 @@ impl ValidatorGroup { let last_validation_time = self.last_validation_time.clone(); let cc_seqno = self.general_session_info.catchain_seqno; let is_masterchain = self.shard.is_masterchain(); - let is_simplex = matches!(self.consensus_options, ConsensusOptions::Simplex(_)); let ( expected_current_round, prev_block_ids, @@ -1432,12 +1273,8 @@ impl ValidatorGroup { candidate_block_id ); - let validation_completion_time = run_validate_query_any_candidate( - candidate.clone(), - engine.clone(), - is_simplex, - ) - .await?; + let validation_completion_time = + run_validate_query_any_candidate(candidate.clone(), engine.clone()).await?; // Post-validation: broadcast + save (shared with legacy path) Self::post_validation_actions( @@ -1516,7 +1353,6 @@ impl ValidatorGroup { candidate.clone(), validator_set.clone(), engine.clone(), - is_simplex, ) .await?; @@ -1717,30 +1553,6 @@ impl ValidatorGroup { if !group_impl.prev_block_ids.same_prevs(&prev_block_history) { Err(error!("Sync error: two requests at a time, prevs have changed!!!")) } else { - // Block verified and accepted: transition Sync -> Active. - // Deferred to this point (after ensure_next_block_new + - // run_accept_block_query) so stale/duplicate commits don't - // produce false-positive activation. - if group_impl.status == ValidatorGroupStatus::Sync { - group_impl.status = ValidatorGroupStatus::Active; - let cl = if group_impl.consensus_type == ConsensusType::Simplex { - "simplex" - } else { - "catchain" - }; - metrics::counter!( - "ton_node_validator_session_activated_total", - "consensus" => cl - ) - .increment(1); - log::info!(target: "validator", - "SESSION_LIFECYCLE: transition shard={} cc_seqno={} \ - session_id={:x} sync -> active \ - (first committed block accepted)", - group_impl.shard, - group_impl.cc_seqno, - group_impl.session_id); - } Ok(()) } } @@ -1895,18 +1707,6 @@ impl Drop for ValidatorGroup { #[cfg(test)] mod tests { use super::*; - use ton_block::KeyId; - - fn make_group_impl_for_start_tests() -> ValidatorGroupImpl { - ValidatorGroupImpl::new( - &KeyId::from_data([0u8; 32]), - ShardIdent::masterchain(), - 1, - UInt256::default(), - false, - ConsensusType::Catchain, - ) - } #[test] fn test_mc_fork_prevention_none_allows() { @@ -1931,218 +1731,4 @@ mod tests { assert!(should_reject_stale_mc_candidate(Some(10), 0)); assert!(should_reject_stale_mc_candidate(Some(100), 50)); } - - #[test] - fn test_prepare_start_immediate_keeps_created_and_marks_pending() { - let mut group = make_group_impl_for_start_tests(); - - assert!(group.prepare_start()); - assert!(group.status == ValidatorGroupStatus::Created); - assert!(group.start_pending); - } - - #[test] - fn test_prepare_start_keeps_created_status() { - let mut group = make_group_impl_for_start_tests(); - - assert!(group.prepare_start()); - assert!(group.status == ValidatorGroupStatus::Created); - assert!(group.start_pending); - } - - #[test] - fn test_prepare_start_rejects_duplicate_pending_start() { - let mut group = make_group_impl_for_start_tests(); - - assert!(group.prepare_start()); - assert!(!group.prepare_start()); - assert!(group.status == ValidatorGroupStatus::Created); - assert!(group.start_pending); - } - - #[test] - fn test_reset_after_start_failure_restores_retryable_state() { - let mut group = make_group_impl_for_start_tests(); - - assert!(group.prepare_start()); - group.reset_after_start_failure(); - - assert!(group.status == ValidatorGroupStatus::Created); - assert!(!group.start_pending); - assert!(group.session.is_none()); - } - - // --- Status ordering / transition table tests (WS6) --- - - #[test] - fn test_status_ordering_is_monotonic() { - let states = [ - ValidatorGroupStatus::Created, - ValidatorGroupStatus::EngineCreated, - ValidatorGroupStatus::Sync, - ValidatorGroupStatus::Active, - ValidatorGroupStatus::Stopping, - ValidatorGroupStatus::Stopped, - ]; - for i in 0..states.len() { - for j in i + 1..states.len() { - assert!(states[i] < states[j], "{} must be < {}", states[i], states[j]); - } - } - } - - #[test] - fn test_before_allows_forward_transitions() { - let created = ValidatorGroupStatus::Created; - let engine_created = ValidatorGroupStatus::EngineCreated; - let sync = ValidatorGroupStatus::Sync; - let active = ValidatorGroupStatus::Active; - let stopping = ValidatorGroupStatus::Stopping; - - assert!(created.before(&engine_created)); - assert!(engine_created.before(&sync)); - assert!(sync.before(&active)); - assert!(active.before(&stopping)); - } - - #[test] - fn test_before_rejects_backward_transitions() { - let sync = ValidatorGroupStatus::Sync; - let active = ValidatorGroupStatus::Active; - let created = ValidatorGroupStatus::Created; - - assert!(!active.before(&sync)); - assert!(!sync.before(&created)); - } - - #[test] - fn test_engine_created_state_between_created_and_sync() { - let created = ValidatorGroupStatus::Created; - let engine_created = ValidatorGroupStatus::EngineCreated; - let sync = ValidatorGroupStatus::Sync; - - assert!(created < engine_created); - assert!(engine_created < sync); - assert!(created.before(&engine_created)); - assert!(engine_created.before(&sync)); - } - - #[test] - fn test_prepare_start_accepts_engine_created_state() { - let mut group = make_group_impl_for_start_tests(); - group.status = ValidatorGroupStatus::EngineCreated; - - assert!(group.prepare_start()); - assert!(group.status == ValidatorGroupStatus::EngineCreated); - assert!(group.start_pending); - } - - #[test] - fn test_prepare_start_rejects_sync_and_later_states() { - for status in [ - ValidatorGroupStatus::Sync, - ValidatorGroupStatus::Active, - ValidatorGroupStatus::Stopping, - ValidatorGroupStatus::Stopped, - ] { - let mut group = make_group_impl_for_start_tests(); - group.status = status; - assert!(!group.prepare_start(), "prepare_start should reject status {}", status); - } - } - - // --- Stale-future culling predicate tests (mirrors manager.cpp equal+related) --- - - /// Reproduces the stale-future culling predicate from validator_manager.rs - /// to verify correctness in isolation with various shard topologies. - fn should_cull_future( - active_shard: &ShardIdent, - active_cc: u32, - future_shard: &ShardIdent, - future_cc: u32, - ) -> bool { - let shards_equal = active_shard == future_shard; - let shards_related = active_shard.is_ancestor_for(future_shard) - || future_shard.is_ancestor_for(active_shard); - let equal_condition = shards_equal && active_cc >= future_cc; - let related_condition = shards_related && active_cc > future_cc; - equal_condition || related_condition - } - - #[test] - fn test_cull_same_shard_equal_seqno() { - let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - assert!(should_cull_future(&shard, 5, &shard, 5)); - } - - #[test] - fn test_cull_same_shard_higher_active_seqno() { - let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - assert!(should_cull_future(&shard, 6, &shard, 5)); - } - - #[test] - fn test_no_cull_same_shard_lower_active_seqno() { - let shard = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - assert!(!should_cull_future(&shard, 4, &shard, 5)); - } - - #[test] - fn test_cull_ancestor_shard_higher_seqno() { - let parent = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - let child = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); - assert!(parent.is_ancestor_for(&child)); - assert!(should_cull_future(&parent, 6, &child, 5)); - } - - #[test] - fn test_cull_descendant_shard_higher_seqno() { - let parent = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - let child = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); - assert!(should_cull_future(&child, 6, &parent, 5)); - } - - #[test] - fn test_no_cull_related_shard_equal_seqno() { - let parent = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - let child = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); - // For related (non-equal) shards, the condition is strict > - assert!(!should_cull_future(&parent, 5, &child, 5)); - } - - #[test] - fn test_no_cull_unrelated_shards() { - let shard_a = ShardIdent::with_tagged_prefix(0, 0x4000_0000_0000_0000).unwrap(); - let shard_b = ShardIdent::with_tagged_prefix(0, 0xC000_0000_0000_0000).unwrap(); - assert!(!shard_a.is_ancestor_for(&shard_b)); - assert!(!shard_b.is_ancestor_for(&shard_a)); - assert!(!should_cull_future(&shard_a, 100, &shard_b, 1)); - } - - #[test] - fn test_no_cull_different_workchain() { - let shard_wc0 = ShardIdent::with_tagged_prefix(0, 0x8000_0000_0000_0000).unwrap(); - let shard_wc1 = ShardIdent::with_tagged_prefix(1, 0x8000_0000_0000_0000).unwrap(); - assert!(!should_cull_future(&shard_wc0, 10, &shard_wc1, 5)); - } - - #[test] - fn test_metric_label_covers_all_states() { - let states = vec![ - (ValidatorGroupStatus::Created, "created"), - (ValidatorGroupStatus::EngineCreated, "engine_created"), - (ValidatorGroupStatus::Sync, "sync"), - (ValidatorGroupStatus::Active, "active"), - (ValidatorGroupStatus::Stopping, "stopping"), - (ValidatorGroupStatus::Stopped, "stopped"), - ]; - for (status, expected_label) in states { - assert_eq!( - status.metric_label(), - expected_label, - "metric_label mismatch for {}", - status - ); - } - } } diff --git a/src/node/src/validator/validator_manager.rs b/src/node/src/validator/validator_manager.rs index ca61cfe..de2451d 100644 --- a/src/node/src/validator/validator_manager.rs +++ b/src/node/src/validator/validator_manager.rs @@ -15,11 +15,11 @@ use super::consensus::{ use crate::{ config::ValidatorManagerConfig, engine::Engine, - engine_traits::{EngineOperations, ValidatorListOutcome}, + engine_traits::EngineOperations, shard_state::ShardStateStuff, validator::{ out_msg_queue::OutMsgQueueInfoStuff, - validator_group::{SessionSnapshot, ValidatorGroup, ValidatorGroupStatus}, + validator_group::{ValidatorGroup, ValidatorGroupStatus}, validator_utils::{ compute_validator_list_id, get_group_members_by_validator_descrs, get_masterchain_seqno, try_calc_subset_for_workchain, @@ -29,20 +29,25 @@ use crate::{ }, }, }; +#[cfg(feature = "simplex")] +use std::sync::atomic::{AtomicU64, Ordering}; use std::{ - cmp::max, + cmp::{max, min}, collections::{HashMap, HashSet}, convert::TryFrom, + fs, ops::RangeInclusive, - sync::{atomic::Ordering, Arc}, + sync::Arc, time::{Duration, SystemTime}, }; use tokio::time::timeout; use ton_api::IntoBoxed; +#[cfg(feature = "simplex")] +use ton_block::SimplexConfig; use ton_block::{ - base64_encode, error, fail, AcceleratedConsensusConfig, BlockIdExt, CatchainConfig, - ConfigParamEnum, ConsensusConfig, FutureSplitMerge, McStateExtra, Result, ShardDescr, - ShardIdent, SimplexConfig, UInt256, UnixTime, ValidatorDescr, ValidatorSet, + error, fail, AcceleratedConsensusConfig, BlockIdExt, CatchainConfig, ConfigParamEnum, + ConsensusConfig, FutureSplitMerge, McStateExtra, Result, ShardDescr, ShardIdent, UInt256, + UnixTime, ValidatorDescr, ValidatorSet, }; #[cfg(feature = "xp25")] @@ -50,65 +55,20 @@ const MC_ACCELERATED_CONSENSUS_ENABLED: bool = true; #[cfg(not(feature = "xp25"))] const MC_ACCELERATED_CONSENSUS_ENABLED: bool = false; -fn format_shard_short(shard: &ShardIdent) -> String { - if shard.is_masterchain() { - "MC".to_string() - } else { - format!("{}:{:04X}..", shard.workchain_id(), shard.shard_prefix_with_tag() >> 48) - } -} - -fn format_time_ago(now_unix: u64, ts: u64) -> String { - if ts == 0 { - "-".to_string() - } else if now_unix >= ts { - format_duration_short(Duration::from_secs(now_unix - ts)) - } else { - "0s".to_string() - } -} - -fn format_duration_short(d: Duration) -> String { - let secs = d.as_secs(); - if secs < 60 { - format!("{}s", secs) - } else if secs < 3600 { - format!("{}m{}s", secs / 60, secs % 60) - } else { - format!("{}h{}m", secs / 3600, (secs % 3600) / 60) - } -} - -fn validation_state_phase_label(status: ValidatorGroupStatus) -> &'static str { - match status { - ValidatorGroupStatus::Created | ValidatorGroupStatus::EngineCreated => "pre-start", - ValidatorGroupStatus::Sync => "pre-commit", - ValidatorGroupStatus::Active => "post-commit", - ValidatorGroupStatus::Stopping => "stopping", - ValidatorGroupStatus::Stopped => "stopped", - } -} +// When true, use hardcoded testing constants for simplex instead of ConfigParam 30. +// Set to true during testing period for consistent behavior across all nodes. +#[cfg(feature = "simplex")] +const SIMPLEX_USE_TESTING_CONSTANTS: bool = true; -/// Magic suffix appended to session-ID serialization when accelerated consensus is enabled. -/// -/// **Rust-specific extension**: the C++ reference (`get_validator_set_id()` in `manager.cpp`) -/// does not include this tag because C++ does not yet support accelerated consensus in the -/// validator manager (`bridge.cpp` has only a TODO). When accelerated consensus is -/// disabled (the default for C++ interop), session IDs are byte-identical to C++. +// Magic tag for accelerated consensus session ID differentiation const ACCELERATED_CONSENSUS_MAGIC_TAG: u32 = 0xACCE1E8A; #[derive(Clone)] pub struct SessionsOptions { pub mc_options: CatchainSessionOptions, + pub mc_hash: UInt256, pub shard_options: CatchainSessionOptions, - /// Session-options hash used inside `validator.groupNew` for masterchain sessions. - /// - /// Without `xp25`, both hashes intentionally collapse to one C++-compatible - /// `ValidatorSessionOptions` hash. With `xp25`, masterchain and shard hashes may - /// diverge if their runtime session options differ in hash-relevant fields. - pub mc_session_id_hash: UInt256, - /// Session-options hash used inside `validator.groupNew` for shard sessions. - pub shard_session_id_hash: UInt256, + pub shard_hash: UInt256, } impl SessionsOptions { @@ -119,21 +79,8 @@ impl SessionsOptions { &self.shard_options } } - - pub fn get_session_id_hash(&self, shard_id: &ShardIdent) -> &UInt256 { - if shard_id.is_masterchain() { - &self.mc_session_id_hash - } else { - &self.shard_session_id_hash - } - } } -/// Serialize the `validator.groupNew` TL object for session-ID hashing. -/// -/// Mirrors `get_validator_set_id()` in C++ (`manager.cpp`) for the -/// `new_catchain_ids == true` branch. Old catchain ID formats are intentionally -/// unsupported (see assertion). fn get_session_id_serialize( session_info: Arc, vals: &[ValidatorDescr], @@ -142,11 +89,9 @@ fn get_session_id_serialize( let mut members = Vec::new(); get_group_members_by_validator_descrs(vals, &mut members); - assert!( - new_catchain_ids, - "Old catchain IDs format (new_catchain_ids=false) is not supported by the Rust implementation" - ); - { + if !new_catchain_ids { + unimplemented!("Old catchain ids format is not supported") + } else { serialize_tl_boxed_object!(&ton_api::ton::validator::group::GroupNew { workchain: session_info.shard.workchain_id(), shard: session_info.shard.shard_prefix_with_tag() as i64, @@ -160,11 +105,7 @@ fn get_session_id_serialize( } } -/// Compute session ID by hashing the serialized `validator.groupNew` TL object. -/// -/// When `accelerated_consensus_enabled` is true, appends [`ACCELERATED_CONSENSUS_MAGIC_TAG`] -/// before hashing to differentiate accelerated sessions from standard ones. Without the -/// tag, the resulting hash is byte-identical to C++ `get_validator_set_id()`. +/// serialize data and calc sha256 fn get_session_id( session_info: Arc, val_set: &[ValidatorDescr], @@ -185,57 +126,13 @@ fn compute_session_unsafe_serialized(session_id: &UInt256, rotate_id: u32) -> Ve unsafe_id_serialized } -/// C++ parity: during unsafe rotation (`force_recover`), skip all non-masterchain shards. -/// -/// Mirrors the `force_recover` early-continue in C++ `update_shards()`: -/// ```cpp -/// if (force_recover && !desc.first.is_masterchain()) { continue; } -/// ``` -fn should_skip_session_for_unsafe_rotation( - do_unsafe_catchain_rotate: bool, - shard: &ShardIdent, -) -> bool { - do_unsafe_catchain_rotate && !shard.is_masterchain() -} - -fn unsafe_rotation_block_seqno( - shard: &ShardIdent, - last_masterchain_block: &BlockIdExt, -) -> Option { - shard.is_masterchain().then_some(last_masterchain_block.seq_no) -} - -/// Check whether any local key belongs to the given validator subset. -/// -/// Mirrors the inner loop of C++ `get_validator()` (`manager.cpp`) which -/// iterates `temp_keys_` and calls `val_set->is_validator(key)`. Returns the first local -/// key, in local-key order, that matches any validator descriptor's short ID. -fn find_local_validator_key( - validators: &[ValidatorDescr], - local_keys: Option<&[PublicKey]>, -) -> Option { - for local_key in local_keys? { - let local_keyhash = local_key.id().data(); - for val in validators { - let pkhash = val.compute_node_id_short(); - if pkhash.as_slice() == local_keyhash { - return Some(local_key.clone()); - } - } - } - None -} - /// Computes session_id and if unsafe rotation is taking place, /// replaces session_id with unsafe rotation session id. -/// The `do_unsafe_catchain_rotate` flag mirrors C++ `force_recover`: -/// the per-shard rotation check only runs when the global gate is set. fn get_session_unsafe_id( session_info: Arc, val_set: &[ValidatorDescr], new_catchain_ids: bool, - do_unsafe_catchain_rotate: bool, - rotation_block_seqno_opt: Option, + prev_block_opt: Option, vm_config: &ValidatorManagerConfig, accelerated_consensus_enabled: bool, ) -> UInt256 { @@ -246,18 +143,18 @@ fn get_session_unsafe_id( accelerated_consensus_enabled, ); - if do_unsafe_catchain_rotate && session_info.shard.is_masterchain() { - if let Some(rotate_id) = vm_config - .check_unsafe_catchain_rotation(rotation_block_seqno_opt, session_info.catchain_seqno) + if session_info.shard.is_masterchain() { + if let Some(rotate_id) = + vm_config.check_unsafe_catchain_rotation(prev_block_opt, session_info.catchain_seqno) { let unsafe_serialized = compute_session_unsafe_serialized(&session_id, rotate_id); let unsafe_id = UInt256::calc_file_hash(unsafe_serialized.as_slice()); log::warn!( - target: "validator_manager", + target: "validator", "Unsafe master session rotation: session {} at block={:?}, cc={} -> rotate_id={}, new session {}", session_id.to_hex_string(), - rotation_block_seqno_opt, + prev_block_opt, session_info.catchain_seqno, rotate_id, unsafe_id.to_hex_string() @@ -361,70 +258,6 @@ fn get_validator_session_options_hash( (UInt256::calc_file_hash(&serialized), serialized) } -#[cfg_attr(feature = "xp25", allow(dead_code))] -fn get_cxx_interop_session_options_hash( - opts: &CatchainSessionOptions, - last_masterchain_block_seqno: u32, -) -> (UInt256, RawBuffer) { - let mut interop_opts = opts.clone(); - let defaults = CatchainSessionOptions::default(); - - interop_opts.accelerated_consensus_enabled = false; - interop_opts.accelerated_consensus_collation_retry_timeout = - defaults.accelerated_consensus_collation_retry_timeout; - interop_opts.accelerated_consensus_skip_rounds_count_for_collator_rotation = - defaults.accelerated_consensus_skip_rounds_count_for_collator_rotation; - interop_opts.accelerated_consensus_max_precollated_blocks = - defaults.accelerated_consensus_max_precollated_blocks; - - get_validator_session_options_hash(interop_opts, last_masterchain_block_seqno) -} - -/// Build the session-options view used by C++ `ValidatorSessionOptions opts{config}`. -/// -/// This is intentionally derived only from config 29 / catchain config and never from -/// Rust's runtime accelerated-consensus toggles. It is used only in non-`xp25` builds -/// where Rust must preserve the C++ single-`opts_hash` behavior. -#[cfg_attr(feature = "xp25", allow(dead_code))] -fn get_cxx_interop_session_options( - opts: &ConsensusConfig, - catchain_config: &CatchainConfig, -) -> CatchainSessionOptions { - let no_accelerated_consensus_config = None; - get_session_options(opts, catchain_config, false, &no_accelerated_consensus_config) -} - -#[cfg(feature = "xp25")] -fn get_session_id_hashes( - _consensus_config: &ConsensusConfig, - _catchain_config: &CatchainConfig, - mc_options: &CatchainSessionOptions, - shard_options: &CatchainSessionOptions, - last_masterchain_block_seqno: u32, -) -> ((UInt256, RawBuffer), (UInt256, RawBuffer)) { - ( - get_validator_session_options_hash(mc_options.clone(), last_masterchain_block_seqno), - get_validator_session_options_hash(shard_options.clone(), last_masterchain_block_seqno), - ) -} - -#[cfg(not(feature = "xp25"))] -fn get_session_id_hashes( - consensus_config: &ConsensusConfig, - catchain_config: &CatchainConfig, - _mc_options: &CatchainSessionOptions, - _shard_options: &CatchainSessionOptions, - last_masterchain_block_seqno: u32, -) -> ((UInt256, RawBuffer), (UInt256, RawBuffer)) { - let session_id_options = get_cxx_interop_session_options(consensus_config, catchain_config); - let (session_id_hash, session_id_options_serialized) = - get_cxx_interop_session_options_hash(&session_id_options, last_masterchain_block_seqno); - ( - (session_id_hash.clone(), session_id_options_serialized.clone()), - (session_id_hash, session_id_options_serialized), - ) -} - fn get_session_options( opts: &ConsensusConfig, catchain_config: &CatchainConfig, @@ -501,51 +334,57 @@ fn get_session_options( result } +async fn clear_catchains_cache(path_str: String) -> Result<()> { + log::info!(target: "validator_manager", "Clearing catchains cache..."); + let removed = tokio::task::spawn_blocking(move || -> Result { + let entries = fs::read_dir(path_str)?; + let mut removed = 0; + for entry in entries { + let path = entry?.path(); + if path.is_dir() { + if let Err(err) = fs::remove_dir_all(path.clone()) { + log::warn!("Error clearing catchains cache {}: {}", path.display(), err); + } else { + removed += 1; + } + } + } + Ok(removed) + }) + .await??; + log::info!(target: "validator_manager", "Cleared catchains cache, removed {} entries", removed); + Ok(()) +} + #[repr(u8)] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] pub enum ValidationStatus { Disabled = 0, Waiting = 1, - Active = 2, + Countdown = 2, + Active = 3, } impl ValidationStatus { fn allows_validate(&self) -> bool { match self { Self::Disabled | Self::Waiting => false, - Self::Active => true, + Self::Countdown | Self::Active => true, } } pub fn from_u8(value: u8) -> Self { match value { 1 => ValidationStatus::Waiting, - 2 => ValidationStatus::Active, + 2 => ValidationStatus::Countdown, + 3 => ValidationStatus::Active, _ => ValidationStatus::Disabled, } } } -/// Local node's participation record for a single validator list. -/// -/// Pairs the node's local validator keys with a `network_ready` flag indicating whether the -/// ADNL/overlay infrastructure was successfully set up for this list. Keys are stored in the -/// same local-key order that C++ uses for `temp_keys_`, so per-shard selection can still pick -/// the first matching local key within a subset. -struct LocalValidatorListEntry { - keys: Vec, - network_ready: bool, -} - -/// Tracks which validator lists the local node belongs to and their readiness state. -/// -/// Maintains the current and next validator list IDs (mirroring the masterchain state's -/// current and next validator sets) along with the local node's keys for each. -/// -/// C++ does not have a direct equivalent structure; it relies on `temp_keys_` for -/// membership checks and `allow_validate_` for the enable/disable gate. #[derive(Default)] struct ValidatorListStatus { - known_lists: HashMap, + known_lists: HashMap, curr: Option, next: Option, curr_utime_since: Option, @@ -553,8 +392,8 @@ struct ValidatorListStatus { } impl ValidatorListStatus { - fn add_list(&mut self, list_id: ValidatorListHash, keys: Vec, network_ready: bool) { - self.known_lists.insert(list_id, LocalValidatorListEntry { keys, network_ready }); + fn add_list(&mut self, list_id: ValidatorListHash, key: PublicKey) { + self.known_lists.insert(list_id, key); } fn contains_list(&self, list_id: &ValidatorListHash) -> bool { @@ -565,24 +404,18 @@ impl ValidatorListStatus { self.known_lists.remove(list_id); } - fn get_list(&self, list_id: &ValidatorListHash) -> Option<&LocalValidatorListEntry> { - self.known_lists.get(list_id) - } - - fn get_local_keys_for_list(&self, list_id: &ValidatorListHash) -> Option<&[PublicKey]> { - self.get_list(list_id).map(|entry| entry.keys.as_slice()) - } - - fn get_local_keys(&self) -> Option<&[PublicKey]> { - self.curr.as_ref().and_then(|current_list| self.get_local_keys_for_list(current_list)) - } - - fn get_ready_current_list(&self) -> Option<&ValidatorListHash> { - self.curr.as_ref().filter(|current_list| self.is_list_network_ready(current_list)) + fn get_list(&self, list_id: &ValidatorListHash) -> Option { + return match self.known_lists.get(list_id) { + None => None, + Some(ch) => Some(ch.clone()), + }; } - fn is_list_network_ready(&self, list_id: &ValidatorListHash) -> bool { - self.known_lists.get(list_id).map(|entry| entry.network_ready).unwrap_or(false) + fn get_local_key(&self) -> Option { + match &self.curr { + None => None, + Some(ch) => self.get_list(ch), + } } fn actual_or_coming(&self, list_id: &ValidatorListHash) -> bool { @@ -606,30 +439,12 @@ fn rotate_all_shards(mc_state_extra: &McStateExtra) -> bool { mc_state_extra.validator_info.nx_cc_updated } -/// Core validator manager state. -/// -/// Mirrors `ValidatorManagerImpl` in C++ (`manager.cpp`). Tracks active and future -/// validator sessions, the local node's membership in validator lists, and a blacklist -/// of destroyed session IDs to prevent recreation during the same masterchain cycle. struct ValidatorManagerImpl { engine: Arc, rt: tokio::runtime::Handle, - /// Sessions for the current validator set (started or starting). - current_sessions: HashMap>, - /// Sessions for the next (future) validator set with pre-created engines. - future_sessions: HashMap>, + validator_sessions: HashMap>, // Sessions: both actual (started) and future validator_list_status: ValidatorListStatus, config: ValidatorManagerConfig, - /// Session IDs that have been destroyed and must not be recreated until the next - /// full shard rotation. Mirrors C++ `destroyed_validator_sessions_` in `manager.cpp`. - /// - /// Persisted in the validator-state DB and cleared when `rotate_all_shards()` returns - /// true, matching the C++ lifecycle around init-block updates. - destroyed_sessions: HashSet, - /// Set to `true` once `check_sync()` succeeds, enabling `Waiting -> Active`. - sync_complete: bool, - /// Wall-clock timestamp of the last full metrics dump. - last_metrics_dump: tokio::time::Instant, } impl ValidatorManagerImpl { @@ -642,74 +457,29 @@ impl ValidatorManagerImpl { ValidatorManagerImpl { engine: engine.clone(), rt: rt.clone(), - current_sessions: HashMap::default(), - future_sessions: HashMap::default(), + validator_sessions: HashMap::default(), validator_list_status: ValidatorListStatus::default(), config, - destroyed_sessions: HashSet::new(), - sync_complete: false, - last_metrics_dump: tokio::time::Instant::now(), } } - fn load_destroyed_sessions(&mut self) -> Result<()> { - let persisted = self.engine.load_destroyed_session_ids()?; - self.destroyed_sessions = persisted.into_iter().collect(); - if !self.destroyed_sessions.is_empty() { - log::info!( - target: "validator_manager", - "Loaded {} destroyed session IDs from persistent storage", - self.destroyed_sessions.len() - ); - } - Ok(()) - } - - fn persist_destroyed_sessions(&self) -> Result<()> { - self.engine.save_destroyed_session_ids(&self.destroyed_sessions) - } - - fn clear_destroyed_sessions(&mut self) -> Result<()> { - if !self.destroyed_sessions.is_empty() { - log::debug!( - target: "validator_manager", - "Clearing {} destroyed session IDs", - self.destroyed_sessions.len() - ); - self.destroyed_sessions.clear(); - } - self.engine.clear_destroyed_session_ids() - } - - /// Find the first matching local key for a subset using a specific validator list. - /// - /// Used for future-session creation where the validator list may differ from `curr`. - fn find_us_for_list( - &self, - validators: &[ValidatorDescr], - list_id: &ValidatorListHash, - ) -> Option { - find_local_validator_key( - validators, - self.validator_list_status.get_local_keys_for_list(list_id), - ) - } - - /// Find the first matching local key for a subset using the current validator list. - /// - /// Used for active-session creation in `start_sessions`. + /// find own key in validator subset fn find_us(&self, validators: &[ValidatorDescr]) -> Option { - find_local_validator_key(validators, self.validator_list_status.get_local_keys()) + if let Some(lk) = self.validator_list_status.get_local_key() { + let local_keyhash = lk.id().data(); + for val in validators { + let pkhash = val.compute_node_id_short(); + if pkhash.as_slice() == local_keyhash { + //log::info!(target: "validator_manager", "Comparing {} with {}", pkhash, local_keyhash); + //log::info!(target: "validator_manager", "({:?})", pk.pub_key().unwrap()); + //compute public key hash + return Some(lk); + } + } + } + None } - /// Register the local node in a validator list and return its hash if matched. - /// - /// Calls [`EngineOperations::set_validator_list`] which checks local keys against the - /// validator set and sets up ADNL/overlay infrastructure. The result is cached in - /// [`ValidatorListStatus`]: even when `network_ready` is `false`, the list hash is - /// returned so that the caller can track membership without ADNL being fully operational. - /// - /// Returns `Ok(None)` only when the local node is genuinely not in the validator set. async fn update_single_validator_list( &mut self, validator_list: &[ValidatorDescr], @@ -717,13 +487,9 @@ impl ValidatorManagerImpl { ) -> Result> { let list_id = match compute_validator_list_id(validator_list, None)? { None => return Ok(None), + Some(l) if self.validator_list_status.contains_list(&l) => return Ok(Some(l)), Some(l) => l, }; - if self.validator_list_status.contains_list(&list_id) - && self.validator_list_status.is_list_network_ready(&list_id) - { - return Ok(Some(list_id)); - } let nodes_res: Vec = validator_list .iter() @@ -732,7 +498,7 @@ impl ValidatorManagerImpl { log::info!(target: "validator_manager", "Updating {} validator list (id {:x}):", name, list_id); for x in &nodes_res { - log::debug!(target: "validator_manager", "pk: {}, pk_id: {}, adnl_id: {}", + log::debug!(target: "validator_manager", "pk: {}, pk_id: {}, andl_id: {}", hex::encode(x.public_key.pub_key().unwrap()), hex::encode(x.public_key.id().data()), hex::encode(x.adnl_id.data()) @@ -740,44 +506,21 @@ impl ValidatorManagerImpl { } match self.engine.set_validator_list(list_id.clone(), &nodes_res).await? { - ValidatorListOutcome::Selected { key, matching_keys, network_ready } => { - self.validator_list_status.add_list( - list_id.clone(), - matching_keys.clone(), - network_ready, + Some(key) => { + self.validator_list_status.add_list(list_id.clone(), key.clone()); + log::info!(target: "validator_manager", "Local node: pk_id: {} id: {}", + hex::encode(key.pub_key().unwrap()), + hex::encode(key.id().data()) ); - if network_ready { - log::info!(target: "validator_manager", "Local node: pk_id: {} id: {}", - hex::encode(key.pub_key().unwrap()), - hex::encode(key.id().data()) - ); - } else { - log::warn!( - target: "validator_manager", - "Local node is a {} validator by pubkey (id {:x}, key {}), but ADNL/network \ - context is not ready yet; will retry and keep validator membership", - name, - list_id, - hex::encode(key.id().data()) - ); - } Ok(Some(list_id)) } - ValidatorListOutcome::NotValidator => { + None => { log::info!(target: "validator_manager", "Local node is not a {} validator", name); Ok(None) } } } - /// Refresh the current and next validator lists from the masterchain state. - /// - /// Returns `true` if the local node belongs to at least one validator set (current or - /// next), `false` if it is not a validator at all. The caller uses `false` to disable - /// validation entirely. - /// - /// Mirrors the implicit list-management in C++ `update_shards()` where `get_validator()` - /// is called per-shard. In Rust, we pre-resolve membership once per update round. async fn update_validator_lists(&mut self, mc_state: &ShardStateStuff) -> Result { let (validator_set, next_validator_set) = match mc_state.state()?.read_custom()? { None => return Ok(false), @@ -788,33 +531,40 @@ impl ValidatorManagerImpl { self.update_single_validator_list(validator_set.list(), "current").await?; self.validator_list_status.curr_utime_since = Some(validator_set.utime_since()); if let Some(id) = self.validator_list_status.curr.as_ref() { - if self.validator_list_status.is_list_network_ready(id) { - self.engine.activate_validator_list(id.clone())?; - } else { - log::warn!( - target: "validator_manager", - "Current validator list {:x} is matched by pubkey but network context \ - is not ready; keeping previous active validator list until ready", - id - ); - } + self.engine.activate_validator_list(id.clone())?; } self.validator_list_status.next = self.update_single_validator_list(next_validator_set.list(), "next").await?; self.validator_list_status.next_utime_since = Some(next_validator_set.utime_since()); - metrics::gauge!("ton_node_validator_in_current_set") - .set(self.validator_list_status.curr.is_some() as u8 as f64); - metrics::gauge!("ton_node_validator_in_next_set") - .set(self.validator_list_status.next.is_some() as u8 as f64); + metrics::gauge!("ton_node_validator_in_current_set").set(if self + .validator_list_status + .curr + .is_some() + { + 1 + } else { + 0 + } as f64); + metrics::gauge!("ton_node_validator_in_next_set").set(if self + .validator_list_status + .next + .is_some() + { + 1 + } else { + 0 + } as f64); Ok(self.validator_list_status.curr.is_some() || self.validator_list_status.next.is_some()) } async fn is_active_shard(&self, shard: &ShardIdent) -> bool { - for group in self.current_sessions.values().chain(self.future_sessions.values()) { + for group in self.validator_sessions.values() { if group.shard() == shard { match group.get_status().await { - ValidatorGroupStatus::Sync | ValidatorGroupStatus::Active => return true, + ValidatorGroupStatus::Sync + | ValidatorGroupStatus::Active + | ValidatorGroupStatus::Countdown { .. } => return true, _ => (), } } @@ -826,7 +576,7 @@ impl ValidatorManagerImpl { log::trace!(target: "validator_manager", "Garbage collect lists"); let mut lists_gc = self.validator_list_status.known_hashes(); - for id in self.current_sessions.values().chain(self.future_sessions.values()) { + for id in self.validator_sessions.values() { lists_gc.remove(&id.get_validator_list_id()); } @@ -866,7 +616,7 @@ impl ValidatorManagerImpl { fn notify_shard_sessions_mc_finalized(&self, mc_block_seqno: u32) { consensus_common::check_execution_time!(5000); // 5ms max - for (session_id, group) in self.current_sessions.iter() { + for (session_id, group) in self.validator_sessions.iter() { if group.shard().is_masterchain() || !group.is_simplex() { continue; } @@ -890,98 +640,39 @@ impl ValidatorManagerImpl { } } - /// Stop sessions that are no longer active or pending, and record their IDs in the - /// destroyed-session blacklist to prevent recreation within the same masterchain cycle. - /// Mirrors C++ group destruction logic in `update_shards()` (`manager.cpp`). async fn stop_and_remove_sessions( &mut self, sessions_to_remove: &HashSet, destroy_database: bool, ) { - if !sessions_to_remove.is_empty() { - log::debug!(target: "validator_manager", - "stop_and_remove_sessions: removing {} sessions, destroy_db={}", - sessions_to_remove.len(), destroy_database); - } for id in sessions_to_remove.iter() { - self.destroyed_sessions.insert(id.clone()); - - let future_group = self.future_sessions.remove(id); - - match self.current_sessions.get(id) { + log::trace!(target: "validator_manager", "stop&remove: removing {:x}", id); + match self.validator_sessions.get(id) { None => { - if let Some(fg) = future_group { - // C++ parity: tentative groups are destroyed via - // IValidatorGroup::destroy in manager.cpp. - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: gc_future shard={} cc_seqno={} session_id={:x} \ - destroy_db={} (no longer needed)", - fg.shard(), fg.cc_seqno(), id, destroy_database); - let cl = if fg.is_simplex() { "simplex" } else { "catchain" }; - metrics::counter!( - "ton_node_validator_session_destroyed_total", - "consensus" => cl - ) - .increment(1); - if let Err(e) = fg.stop(self.rt.clone(), destroy_database).await { - log::error!(target: "validator_manager", - "SESSION_LIFECYCLE: gc_future_stop_failed session_id={:x}: {}", - id, e); - } - } else { - log::trace!(target: "validator_manager", - "Session {:x} not in current or future maps", id); - } + log::error!(target: "validator_manager", + "Session stopping error: {:x} already removed from hash", id + ) } - Some(session) => { - let status = session.get_status().await; - let shard = session.shard().clone(); - match status { - ValidatorGroupStatus::Stopping => { - log::debug!(target: "validator_manager", - "SESSION_LIFECYCLE: already stopping shard={} session_id={:x}", - shard, id); - } - ValidatorGroupStatus::Stopped => { - if let Some(group) = self.current_sessions.remove(id) { - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: gc_stopped shard={} session_id={:x}", - shard, id); - let cl = if group.is_simplex() { "simplex" } else { "catchain" }; - metrics::counter!( - "ton_node_validator_session_destroyed_total", - "consensus" => cl - ) - .increment(1); - if !self.is_active_shard(group.shard()).await { - self.engine.remove_last_validation_time(group.shard()); - self.engine.remove_last_collation_time(group.shard()); - } + Some(session) => match session.get_status().await { + ValidatorGroupStatus::Stopping => {} + ValidatorGroupStatus::Stopped => { + if let Some(group) = self.validator_sessions.remove(id) { + if !self.is_active_shard(group.shard()).await { + self.engine.remove_last_validation_time(group.shard()); + self.engine.remove_last_collation_time(group.shard()); } } - _ => { - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: gc_stop shard={shard} session_id={id:x} \ - status={status} destroy_db={}", - destroy_database - ); - let cl = if session.is_simplex() { "simplex" } else { "catchain" }; - metrics::counter!( - "ton_node_validator_session_destroyed_total", - "consensus" => cl - ) - .increment(1); - if let Err(e) = - session.clone().stop(self.rt.clone(), destroy_database).await - { - log::error!(target: "validator_manager", - "SESSION_LIFECYCLE: gc_stop_failed shard={} session_id={:x}: {}", - shard, id, e); - self.current_sessions.remove(id); - } + } + _ => { + if let Err(e) = + session.clone().stop(self.rt.clone(), destroy_database).await + { + log::error!(target: "validator_manager", + "Could not stop session {:x}: `{}`", id, e); + self.validator_sessions.remove(id); } } - } + }, } } } @@ -996,8 +687,6 @@ impl ValidatorManagerImpl { _ => fail!("no CatchainConfig in config_params"), }; let accelerated_consensus_config = self.get_accelerated_consensus_config(mc_state_extra); - let last_key_block_seqno = - mc_state_extra.last_key_block.as_ref().map(|x| x.seq_no).unwrap_or(0); // Compute session options for masterchain (accelerated consensus controlled by constant) let mc_accelerated_consensus_enabled = MC_ACCELERATED_CONSENSUS_ENABLED @@ -1008,6 +697,10 @@ impl ValidatorManagerImpl { mc_accelerated_consensus_enabled, &accelerated_consensus_config, ); + let (mc_hash, mc_session_options_serialized) = get_validator_session_options_hash( + mc_options.clone(), + mc_state_extra.last_key_block.as_ref().map(|x| x.seq_no).unwrap_or(0), + ); // Compute session options for shards (accelerated consensus may be enabled) let shard_accelerated_consensus_enabled = @@ -1018,39 +711,27 @@ impl ValidatorManagerImpl { shard_accelerated_consensus_enabled, &accelerated_consensus_config, ); - let ( - (mc_session_id_hash, mc_session_id_options_serialized), - (shard_session_id_hash, shard_session_id_options_serialized), - ) = get_session_id_hashes( - &consensus_config, - catchain_config, - &mc_options, - &shard_options, - last_key_block_seqno, + let (shard_hash, shard_session_options_serialized) = get_validator_session_options_hash( + shard_options.clone(), + mc_state_extra.last_key_block.as_ref().map(|x| x.seq_no).unwrap_or(0), ); + log::trace!(target: "validator_manager", "MC SessionOptions from config.29: {:?}", mc_options); log::trace!( target: "validator_manager", - "MC SessionOptions from config.29: {mc_options:?}" - ); - log::trace!( - target: "validator_manager", - "MC Session-ID SessionOptions serialized: {} hash: {:x}", - hex::encode(mc_session_id_options_serialized), - mc_session_id_hash - ); - log::trace!( - target: "validator_manager", - "Shard Session-ID SessionOptions serialized: {} hash: {:x}", - hex::encode(shard_session_id_options_serialized), - shard_session_id_hash + "MC SessionOptions from config.29 serialized: {} hash: {:x}", + hex::encode(mc_session_options_serialized), + mc_hash ); + log::trace!(target: "validator_manager", "Shard SessionOptions from config.29: {:?}", shard_options); log::trace!( target: "validator_manager", - "Shard SessionOptions from config.29: {shard_options:?}" + "Shard SessionOptions from config.29 serialized: {} hash: {:x}", + hex::encode(shard_session_options_serialized), + shard_hash ); - Ok(SessionsOptions { mc_options, shard_options, mc_session_id_hash, shard_session_id_hash }) + Ok(SessionsOptions { mc_options, mc_hash, shard_options, shard_hash }) } fn get_accelerated_consensus_config( @@ -1094,31 +775,53 @@ impl ValidatorManagerImpl { /// Select consensus options based on ConfigParam 30 (NewConsensusConfigAll). /// - /// Returns `ConsensusOptions::Simplex` when ConfigParam 30 contains a valid - /// SimplexConfig (v1 or v2) for the given shard. Otherwise, returns - /// `ConsensusOptions::Catchain` โ€” the node must remain fully catchain-compatible - /// as long as the on-chain config can switch between the two at any time. - /// - /// C++ reference: `validator/manager.cpp` โ€” `ValidatorManagerImpl::create_validator_group` - /// - calls `last_masterchain_state_->get_new_consensus_config(shard.workchain)` - /// - if present โ†’ `IValidatorGroup::create_bridge` (simplex) - /// - if absent โ†’ `IValidatorGroup::create_catchain` + /// If simplex feature is enabled and ConfigParam 30 contains a SimplexConfig for + /// the given shard (mc or shard), returns `ConsensusOptions::Simplex`. + /// Otherwise, returns `ConsensusOptions::Catchain` with the provided catchain options. /// - /// Noncritical params override (not yet implemented): - /// C++ ref: `validator/validator-options.hpp` โ€” - /// `ValidatorManagerOptionsImpl::get_noncritical_params` + /// This follows the C++ pattern in `ValidatorManagerImpl::create_validator_group`: + /// - Get `new_consensus_config` from masterchain state + /// - If present, create bridge (simplex/null consensus) + /// - If absent, create catchain + #[cfg(feature = "simplex")] fn select_consensus_options( &self, shard: &ShardIdent, mc_state: &ShardStateStuff, catchain_options: &CatchainSessionOptions, - _cc_seqno: u32, ) -> ConsensusOptions { - use super::consensus::SimplexSessionOptions; + use super::consensus::{ConsensusFactory, SimplexSessionOptions}; - // C++ ref: mc-config.cpp โ€” Config::get_new_consensus_config reads ConfigParam 30 - // directly without checking global_version. Absence of the param - // (or a parse error) falls through to the catchain path below. + // During testing period, use hardcoded constants instead of ConfigParam 30 + if SIMPLEX_USE_TESTING_CONSTANTS { + let options = ConsensusFactory::create_simplex_options( + catchain_options.max_block_size as usize, + catchain_options.max_collated_data_size as usize, + ); + static LAST_WARN: AtomicU64 = AtomicU64::new(0); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let last = LAST_WARN.load(Ordering::Relaxed); + if now >= last + 30 + && LAST_WARN + .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + log::warn!( + target: "validator_manager", + "Simplex TESTING MODE for {}: target_rate={}ms, slots_per_window={}, first_block_timeout={}ms", + shard, + options.target_rate.as_millis(), + options.slots_per_leader_window, + options.first_block_timeout.as_millis() + ); + } + return ConsensusOptions::Simplex(options); + } + + // Try to get ConfigParam 30 from masterchain state let config_params = match mc_state.config_params() { Ok(cfg) => cfg, Err(e) => { @@ -1131,9 +834,7 @@ impl ValidatorManagerImpl { } }; - // C++ ref: get_new_consensus_config(wc) selects mc or shard inner config, - // then tries simplex_config#21 / simplex_config_v2#22. - // Absence โ†’ catchain fallback (the node must stay catchain-compatible). + // Get simplex config for mc or shard based on workchain let simplex_cfg: Option = if shard.is_masterchain() { config_params.get_mc_simplex_config().ok().flatten() } else { @@ -1141,63 +842,25 @@ impl ValidatorManagerImpl { }; if let Some(cfg) = simplex_cfg { - log::trace!( + log::info!( target: "validator_manager", "Simplex config found for {}: target_rate={}ms, slots_per_window={}, first_block_timeout={}ms", shard, - cfg.noncritical_params.target_rate_ms, + cfg.target_rate_ms, cfg.slots_per_leader_window, - cfg.noncritical_params.first_block_timeout_ms + cfg.first_block_timeout_ms ); - - // C++ ref: mc-config.cpp maps noncritical params to - // NewConsensusConfig::NoncriticalParams fields via ENUMERATE_NONCRITICAL_PARAMS. - // Doubles are stored as f32 bits in the u32 values. - // - // TODO: C++ also applies per-shard/cc_seqno overrides here via - // get_noncritical_params() in validator-options.hpp. - let np = &cfg.noncritical_params; - let opts = SimplexSessionOptions { - proto_version: catchain_options.proto_version as u32, + return ConsensusOptions::Simplex(SimplexSessionOptions { slots_per_leader_window: cfg.slots_per_leader_window, - target_rate: Duration::from_millis(np.target_rate_ms as u64), - first_block_timeout: Duration::from_millis(np.first_block_timeout_ms as u64), - first_block_timeout_multiplier: f32::from_bits( - np.first_block_timeout_multiplier_bits, - ) as f64, - first_block_timeout_cap: Duration::from_millis( - np.first_block_timeout_cap_ms as u64, - ), - candidate_resolve_timeout: Duration::from_millis( - np.candidate_resolve_timeout_ms as u64, - ), - candidate_resolve_timeout_multiplier: f32::from_bits( - np.candidate_resolve_timeout_multiplier_bits, - ) as f64, - candidate_resolve_timeout_cap: Duration::from_millis( - np.candidate_resolve_timeout_cap_ms as u64, - ), - candidate_resolve_cooldown: Duration::from_millis( - np.candidate_resolve_cooldown_ms as u64, - ), - standstill_timeout: Duration::from_millis(np.standstill_timeout_ms as u64), - standstill_max_egress_bytes_per_s: np.standstill_max_egress_bytes_per_s, - max_leader_window_desync: np.max_leader_window_desync, - bad_signature_ban_duration: Duration::from_millis( - np.bad_signature_ban_duration_ms as u64, - ), - candidate_resolve_rate_limit: np.candidate_resolve_rate_limit, + first_block_timeout: Duration::from_millis(cfg.first_block_timeout_ms as u64), + target_rate: Duration::from_millis(cfg.target_rate_ms as u64), + // max_block_size and max_collated_data_size come from ConfigParam 29 (via catchain_options) max_block_size: catchain_options.max_block_size as usize, max_collated_data_size: catchain_options.max_collated_data_size as usize, - use_quic: cfg.use_quic, ..Default::default() - }; - return ConsensusOptions::Simplex(opts); + }); } - // No simplex config โ†’ catchain fallback. - // This is the expected path when testnet has empty ConfigParam 30 or when - // ConfigParam 30 contains null_consensus_config#20. log::trace!( target: "validator_manager", "No simplex config for {}, using catchain", @@ -1206,75 +869,54 @@ impl ValidatorManagerImpl { ConsensusOptions::Catchain(catchain_options.clone()) } + #[cfg(not(feature = "simplex"))] + fn select_consensus_options( + &self, + _shard: &ShardIdent, + _mc_state: &ShardStateStuff, + catchain_options: &CatchainSessionOptions, + ) -> ConsensusOptions { + ConsensusOptions::Catchain(catchain_options.clone()) + } + async fn update_validation_status( &mut self, mc_state: &ShardStateStuff, mc_state_extra: &McStateExtra, ) -> Result<()> { - let prev_status = self.engine.validation_status(); - match prev_status { + match self.engine.validation_status() { ValidationStatus::Waiting => { let last_masterchain_block = mc_state.block_id(); - let later_than_hardfork = - self.engine.get_last_fork_masterchain_seqno() <= last_masterchain_block.seq_no; - - let synced = self.engine.check_sync().await?; - log::trace!(target: "validator_manager", - "update_validation_status: Waiting check: synced={} later_than_hardfork={} \ - mc_seqno={}", - synced, later_than_hardfork, last_masterchain_block.seq_no); - - // Phase 1: mark sync complete and backfill future engines. - // C++ parity: sync_complete() sets started_=true and sweeps both - // validator_groups_ and next_validator_groups_ calling - // create_session() on all groups (manager.cpp sync_complete). - if synced && later_than_hardfork && !self.sync_complete { - self.sync_complete = true; - if !self.future_sessions.is_empty() { - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: backfill_pre_create_engine for {} future sessions \ - after sync_complete", - self.future_sessions.len()); - for (id, group) in &self.future_sessions { - let g = group.clone(); - let session_id = id.clone(); - tokio::spawn(async move { - if let Err(e) = g.pre_create_engine().await { - log::error!(target: "validator_manager", - "SESSION_LIFECYCLE: backfill_pre_create_failed \ - session_id={:x}: {}", - session_id, e); - } - }); + if last_masterchain_block.seq_no == 0 || rotate_all_shards(mc_state_extra) { + let later_than_hardfork = self.engine.get_last_fork_masterchain_seqno() + <= last_masterchain_block.seq_no; + + if self.engine.check_sync().await? && later_than_hardfork { + if last_masterchain_block.seq_no == 0 + && self.config.no_countdown_for_zerostate + { + self.engine.set_validation_status(ValidationStatus::Active); + } else { + self.engine.set_validation_status(ValidationStatus::Countdown); } } } - - // Phase 2: transition Waiting -> Active. - // C++ parity: allow_validate_ is set purely from - // rotated_all_shards() || seqno==0 plus the fork check - // (manager.cpp update_shards). sync_complete (started_) - // only gates create_session() on existing groups (Phase 1 - // above), NOT the allow_validate_ / status transition. - let rotated = - rotate_all_shards(mc_state_extra) || last_masterchain_block.seq_no == 0; - if rotated && later_than_hardfork { - let bootstrap = last_masterchain_block.seq_no == 0; - log::info!(target: "validator_manager", - "VALIDATION_STATUS: Waiting -> Active \ - (rotated_all_shards={}, mc_seqno={}, sync_complete={}, bootstrap={}, \ - no_countdown_for_zerostate={})", - rotate_all_shards(mc_state_extra), - last_masterchain_block.seq_no, - self.sync_complete, - bootstrap, - self.config.no_countdown_for_zerostate); - self.engine.set_validation_status(ValidationStatus::Active); - } else if !rotated { - log::trace!(target: "validator_manager", - "update_validation_status: rotated_all_shards=false, \ - deferring Waiting -> Active (mc_seqno={})", - last_masterchain_block.seq_no); + } + ValidationStatus::Countdown => { + for (_, group) in self.validator_sessions.iter() { + let status = group.get_status().await; + if status == ValidatorGroupStatus::Sync + || status == ValidatorGroupStatus::Active + { + let path_str: String = self.engine.db_root_dir()?.to_owned() + "/catchains"; + tokio::spawn(async move { + if let Err(err) = clear_catchains_cache(path_str).await { + log::warn!("Error clearing catchains cache: {}", err); + } + }); + self.engine.set_validation_status(ValidationStatus::Active); + break; + } } } ValidationStatus::Disabled | ValidationStatus::Active => {} @@ -1283,31 +925,17 @@ impl ValidatorManagerImpl { } async fn disable_validation(&mut self, clear_rotation: bool) -> Result<()> { - let prev_status = self.engine.validation_status(); - let n_current = self.current_sessions.len(); - let n_future = self.future_sessions.len(); - log::info!(target: "validator_manager", - "VALIDATION_STATUS: {prev_status:?} -> Disabled (clear_rotation={clear_rotation}, \ - current_sessions={n_current}, future_sessions={n_future})" - ); - self.engine.set_validation_status(ValidationStatus::Disabled); - self.sync_complete = false; let existing_validator_sessions: HashSet = - self.current_sessions.keys().chain(self.future_sessions.keys()).cloned().collect(); + self.validator_sessions.keys().cloned().collect(); self.stop_and_remove_sessions(&existing_validator_sessions, clear_rotation).await; self.garbage_collect().await; self.engine.set_will_validate(false); if clear_rotation { - self.clear_destroyed_sessions()?; self.engine.clear_last_rotation_block_id()?; - } else { - self.persist_destroyed_sessions()?; } - log::info!(target: "validator_manager", - "VALIDATION_STATUS: Disabled complete (stopped {} current + {} future sessions)", - n_current, n_future); + log::info!(target: "validator_manager", "All sessions were removed, validation disabled"); Ok(()) } @@ -1319,28 +947,11 @@ impl ValidatorManagerImpl { fn enable_validation(&mut self) { self.engine.set_will_validate(true); - let current = self.engine.validation_status(); - // C++ parity: enable_validation() only ensures we are at least - // Waiting. The Waiting -> Active promotion is handled by - // update_validation_status() based on rotated_all_shards(), - // matching C++'s allow_validate_ which is independent of sync. - let target = max(current, ValidationStatus::Waiting); - if target != current { - log::info!(target: "validator_manager", - "VALIDATION_STATUS: {:?} -> {:?}", - current, target); - } - self.engine.set_validation_status(target); + let validation_status = max(self.engine.validation_status(), ValidationStatus::Waiting); + self.engine.set_validation_status(validation_status); + log::debug!(target: "validator_manager", "Validation enabled: status {:?}", validation_status); } - /// Create and start validator sessions for all currently active shards. - /// - /// Mirrors the `new_shards` loop in C++ `update_shards()` (`manager.cpp`): - /// - Skips non-masterchain shards during unsafe rotation (`force_recover`) - /// - Computes validator subset and session ID (with optional unsafe-rotation patch) - /// - Skips sessions in the [`destroyed_sessions`] blacklist - /// - Finds the local validator key in the subset - /// - Creates or reuses the `ValidatorGroup` and starts it if newly created #[allow(clippy::too_many_arguments)] async fn start_sessions( &mut self, @@ -1355,23 +966,23 @@ impl ValidatorManagerImpl { master_cc_range: &RangeInclusive, last_masterchain_block: &BlockIdExt, ) -> Result<()> { - let validator_list_id = match self.validator_list_status.get_ready_current_list() { - Some(list_id) => list_id.clone(), - None => { - if let Some(list_id) = self.validator_list_status.curr.as_ref() { - log::warn!( - target: "validator_manager", - "Skipping current-session start for validator list {:x}: \ - network context is not ready yet", - list_id - ); - } - return Ok(()); - } + let validator_list_id = match &self.validator_list_status.curr { + Some(list_id) => list_id, + None => return Ok(()), }; let full_validator_set = mc_state_extra.config.validator_set()?; + let validation_status = self.engine.validation_status(); let catchain_config = self.read_catchain_config(mc_state)?; + let group_start_status = if validation_status == ValidationStatus::Countdown { + let session_lifetime = + min(catchain_config.mc_catchain_lifetime, catchain_config.shard_catchain_lifetime); + let start_at = + tokio::time::Instant::now() + Duration::from_secs((session_lifetime / 2).into()); + ValidatorGroupStatus::Countdown { start_at } + } else { + ValidatorGroupStatus::Sync + }; let do_unsafe_catchain_rotate = self .config @@ -1394,20 +1005,8 @@ impl ValidatorManagerImpl { let cc_seqno = cc_seqno_from_state; - // C++ parity: during unsafe rotation, skip all non-masterchain shards - // before any expensive subset/session-id computation. - if should_skip_session_for_unsafe_rotation(do_unsafe_catchain_rotate, &ident) { - log::trace!( - target: "validator_manager", - "Shard {}, cc_seqno {}: unsafe rotation skipping", - ident, cc_seqno - ); - continue; - } - - log::trace!( - target: "validator_manager", - "Trying to start/update session for shard {ident}, cc_seqno {cc_seqno_from_state}" + log::trace!(target: "validator_manager", "Trying to start/update session for shard {}, cc_seqno {}", + ident, cc_seqno_from_state ); let prev = PrevBlockHistory::with_prevs(&ident, prev_blocks); @@ -1435,37 +1034,33 @@ impl ValidatorManagerImpl { ValidatorSet::with_cc_seqno(0, 0, 0, cc_seqno, subset.validators.clone())?; let max_vertical_seqno = self.engine.hardforks().len() as u32; + // Select appropriate options hash based on shard type + let current_opts_hash = if ident.is_masterchain() { + &sessions_options.mc_hash + } else { + &sessions_options.shard_hash + }; + let general_session_info = Arc::new(GeneralSessionInfo { shard: ident.clone(), - opts_hash: sessions_options.get_session_id_hash(&ident).clone(), + opts_hash: current_opts_hash.clone(), catchain_seqno: cc_seqno, key_seqno: keyblock_seqno, max_vertical_seqno: max_vertical_seqno, }); - let rotation_block_seqno_opt = - unsafe_rotation_block_seqno(&ident, last_masterchain_block); + let prev_block_seqno_opt = prev.get_prevs().first().map(|x| x.seq_no); let accelerated_consensus_enabled = self.is_accelerated_consensus_enabled_for_shard(mc_state_extra, &ident); let session_id = get_session_unsafe_id( general_session_info.clone(), vsubset.list(), true, - do_unsafe_catchain_rotate, - rotation_block_seqno_opt, + prev_block_seqno_opt, &self.config, accelerated_consensus_enabled, ); - // C++ parity: skip sessions in the destroyed set - if self.destroyed_sessions.contains(&session_id) { - log::trace!( - target: "validator_manager", - "Skipping destroyed session {:x} for shard {}", session_id, ident - ); - continue; - } - let local_id_option = self.find_us(&subset.validators); if let Some(local_id) = &local_id_option { @@ -1481,90 +1076,62 @@ impl ValidatorManagerImpl { gc_validator_sessions.remove(&session_id); + // If blockchain works under unsafe_catchain_rotation, then do not change its status: + // 1. Do not start new sessions + // 2. Do not remove functioning old sessions + if do_unsafe_catchain_rotate && !ident.is_masterchain() && local_id_option.is_none() + { + log::trace!( + target: "validator", + "Current shard {}, session {:x}: unsafe rotation skipping", + ident, session_id + ); + continue; + } + let engine = self.engine.clone(); let allow_unsafe_self_blocks_resync = self.config.unsafe_resync_catchains.contains(&cc_seqno); let current_session_options = sessions_options.get_session_options(&ident); // Select consensus type based on ConfigParam 30 - let consensus_options = self.select_consensus_options( - &ident, - mc_state, - current_session_options, - cc_seqno, - ); + let consensus_options = + self.select_consensus_options(&ident, mc_state, current_session_options); - let session = if let Some(promoted) = self.future_sessions.remove(&session_id) { - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: promote shard={} cc_seqno={} session_id={:x} \ - future -> current", - ident, cc_seqno, session_id); - self.current_sessions.entry(session_id.clone()).or_insert(promoted).clone() - } else { - self.current_sessions - .entry(session_id.clone()) - .or_insert_with(|| { - let consensus_name = match &consensus_options { - ConsensusOptions::Simplex(_) => "simplex", - ConsensusOptions::Catchain(_) => "catchain", - }; - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: create_current shard={} cc_seqno={} \ - session_id={:x} consensus={} local_key={}", - ident, cc_seqno, session_id, consensus_name, - hex::encode(local_id.id().data())); - metrics::counter!( - "ton_node_validator_session_created_total", - "consensus" => consensus_name - ) - .increment(1); - Arc::new(ValidatorGroup::new( - general_session_info.clone(), - local_id.clone(), - session_id.clone(), - validator_list_id.clone(), - vsubset.clone(), - consensus_options.clone(), - engine, - allow_unsafe_self_blocks_resync, - )) - }) - .clone() - }; + let session = self + .validator_sessions + .entry(session_id.clone()) + .or_insert_with(|| { + Arc::new(ValidatorGroup::new( + general_session_info.clone(), + local_id.clone(), + session_id.clone(), + validator_list_id.clone(), + vsubset.clone(), + consensus_options.clone(), + engine, + allow_unsafe_self_blocks_resync, + )) + }) + .clone(); let session_status = session.get_status().await; - if session.try_prepare_start().await? { - log::trace!( - target: "validator_manager", - "Current shard {ident}, session {session_id:x}: starting" - ); + if session_status == ValidatorGroupStatus::Created { + log::trace!(target: "validator_manager", "Current shard {}, session {:x}: starting", ident, session_id); session - .start_session( + .start_with_status( + group_start_status, prev.get_prevs().to_vec(), last_masterchain_block.clone(), SystemTime::UNIX_EPOCH + Duration::from_secs(mc_now as u64), self.rt.clone(), ) .await?; - } else if session.is_start_pending().await { - log::trace!( - target: "validator_manager", - "Current shard {}, session {:x}: start pending", - ident, - session_id - ); } else if session_status >= ValidatorGroupStatus::Stopping { - log::error!( - target: "validator_manager", - "Cannot start stopped session {}", - session.info().await - ); + log::error!(target: "validator_manager", "Cannot start stopped session {}", session.info().await); } else { - log::trace!( - target: "validator_manager", - "Current shard {ident}, session {session_id:x}: working" - ); + log::trace!(target: "validator_manager", "Current shard {}, session {:x}: working", ident, session_id); } } else { log::trace!(target: "validator_manager", "We are not in subset for {}", ident); @@ -1575,16 +1142,6 @@ impl ValidatorManagerImpl { Ok(()) } - /// Main per-masterchain-block update loop. - /// - /// Mirrors `ValidatorManagerImpl::update_shards()` in C++ (`manager.cpp`). - /// Responsibilities: - /// 1. Refresh validator list membership (`update_validator_lists`) - /// 2. Collect current and future shards from the masterchain state - /// 3. Create/start sessions for current shards (`start_sessions`) - /// 4. Pre-create sessions for upcoming shards (future-sessions loop) - /// 5. GC sessions that are no longer needed (`stop_and_remove_sessions`) - /// 6. Clear the destroyed-sessions blacklist on full shard rotation async fn update_shards(&mut self, mc_state: Arc) -> Result<()> { let mc_state_extra = mc_state.shard_state_extra()?; let master_cc_seqno = get_masterchain_seqno(self.engine.clone(), &mc_state).await?; @@ -1592,14 +1149,8 @@ impl ValidatorManagerImpl { let sessions_options = self.compute_session_options(mc_state_extra, &catchain_config).await?; - log::trace!(target: "validator_manager", - "update_shards: mc_seqno={} mc_cc_seqno={} current_sessions={} future_sessions={}", - mc_state.block_id().seq_no, master_cc_seqno, - self.current_sessions.len(), self.future_sessions.len()); - if !self.update_validator_lists(&mc_state).await? { - log::info!(target: "validator_manager", - "VALIDATION_STATUS: not a validator (not in current or next set), disabling"); + log::info!(target: "validator_manager", "Current validator list is empty, validation is disabled."); self.disable_validation(true).await?; return Ok(()); } @@ -1624,7 +1175,7 @@ impl ValidatorManagerImpl { // Collect info about shards let mut gc_validator_sessions: HashSet = - self.current_sessions.keys().chain(self.future_sessions.keys()).cloned().collect(); + self.validator_sessions.keys().cloned().collect(); // Shards that are working or about to start (continue) in this masterstate: shard_ident -> prevs let mut new_shards = HashMap::new(); @@ -1649,14 +1200,14 @@ impl ValidatorManagerImpl { ident.clone(), descr.seq_no, descr.root_hash, - descr.file_hash, + descr.file_hash ); if descr.before_split { let lr_shards = ident.split(); match lr_shards { - Err(e) => log::error!(target: "validator_manager", "Cannot split shard: `{e}`"), - Ok((l, r)) => { + Err(e) => log::error!(target: "validator_manager", "Cannot split shard: `{}`", e), + Ok((l,r)) => { new_shards.insert(l, vec![top_block.clone()]); new_shards.insert(r, vec![top_block.clone()]); blocks_before_split.insert(top_block); @@ -1665,15 +1216,15 @@ impl ValidatorManagerImpl { } else if descr.before_merge { let parent_shard = ident.merge(); match parent_shard { - Err(e) => log::error!(target: "validator_manager", "Cannot merge shard: `{e}`"), + Err(e) => log::error!(target: "validator_manager", "Cannot merge shard: `{}`", e), Ok(p) => { let mut prev_blocks = match new_shards.get(&p) { Some(pb) => pb.clone(), - None => vec![BlockIdExt::default(), BlockIdExt::default()], + None => vec![BlockIdExt::default(), BlockIdExt::default()] }; // Add previous block for the shard: there are two parents for merge, so two prevs - let (_l, r) = p.split()?; + let (_l,r) = p.split()?; prev_blocks[(r == ident) as usize] = top_block; new_shards.insert(p, prev_blocks); } @@ -1689,32 +1240,26 @@ impl ValidatorManagerImpl { FutureSplitMerge::None => { future_shards.insert(ident); } - FutureSplitMerge::Split { split_utime: time, interval: _interval } => { + FutureSplitMerge::Split{split_utime: time, interval: _interval} => { if (time as u64) < cur_time + 60 { match ident.split() { - Ok((l, r)) => { + Ok((l,r)) => { future_shards.insert(l); future_shards.insert(r); } - Err(e) => log::error!( - target: "validator_manager", - "Cannot split shard {ident}: `{e}`" - ), + Err(e) => log::error!(target: "validator_manager", "Cannot split shard {}: `{}`", ident, e) } } else { future_shards.insert(ident); } } - FutureSplitMerge::Merge { merge_utime: time, interval: _interval } => { + FutureSplitMerge::Merge{merge_utime: time, interval: _interval} => { if (time as u64) < cur_time + 60 { match ident.merge() { Ok(p) => { future_shards.insert(p); } - Err(e) => log::error!( - target: "validator_manager", - "Cannot merge shard {ident}: `{e}`" - ), + Err(e) => log::error!(target: "validator_manager", "Cannot merge shard {}: `{}`", ident, e) } } else { future_shards.insert(ident); @@ -1820,22 +1365,18 @@ impl ValidatorManagerImpl { mc_validators.append(&mut wc.validators.clone()); } - if !self.validator_list_status.is_list_network_ready(next_val_list_id) { - log::trace!( - target: "validator_manager", - "Skipping future-session precreation for shard {}: validator list {:x} \ - network context is not ready yet", - ident, - next_val_list_id - ); - continue; - } - - if let Some(local_id) = self.find_us_for_list(&wc.validators, next_val_list_id) { + if let Some(local_id) = self.find_us(&wc.validators) { let max_vertical_seqno = self.engine.hardforks().len() as u32; + // Select appropriate options and hash based on shard type + let current_opts_hash = if ident.is_masterchain() { + &sessions_options.mc_hash + } else { + &sessions_options.shard_hash + }; + let new_session_info = Arc::new(GeneralSessionInfo { shard: ident.clone(), - opts_hash: sessions_options.get_session_id_hash(&ident).clone(), + opts_hash: current_opts_hash.clone(), catchain_seqno: *next_cc_seqno, key_seqno: keyblock_seqno, max_vertical_seqno: max_vertical_seqno, @@ -1849,16 +1390,6 @@ impl ValidatorManagerImpl { true, accelerated_consensus_enabled, ); - - // C++ parity: skip sessions in the destroyed set - if self.destroyed_sessions.contains(&session_id) { - log::trace!( - target: "validator_manager", - "Skipping destroyed future session {:x} for shard {}", session_id, ident - ); - continue; - } - let vsubset = wc.compute_validator_set(*next_cc_seqno)?; let current_session_options = sessions_options.get_session_options(&ident); gc_validator_sessions.remove(&session_id); @@ -1868,111 +1399,24 @@ impl ValidatorManagerImpl { &ident, mc_state.as_ref(), current_session_options, - *next_cc_seqno, ); - if self.current_sessions.contains_key(&session_id) { - log::trace!( - target: "validator_manager", - "Future session {:x} for shard {} already in current_sessions, skipping", - session_id, ident - ); - continue; - } - - let is_new = !self.future_sessions.contains_key(&session_id); - let group = self - .future_sessions - .entry(session_id.clone()) - .or_insert_with(|| { - let consensus_name = match &consensus_options { - ConsensusOptions::Simplex(_) => "simplex", - ConsensusOptions::Catchain(_) => "catchain", - }; - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: create_future shard={} cc_seqno={} \ - session_id={:x} consensus={}", - ident, next_cc_seqno, session_id, consensus_name); - metrics::counter!( - "ton_node_validator_session_created_total", - "consensus" => consensus_name - ) - .increment(1); - Arc::new(ValidatorGroup::new( - new_session_info, - local_id, - session_id.clone(), - next_val_list_id.clone(), - vsubset.clone(), - consensus_options.clone(), - self.engine.clone(), - self.config.unsafe_resync_catchains.contains(next_cc_seqno), - )) - }) - .clone(); - - if is_new && self.sync_complete { - log::debug!(target: "validator_manager", - "Pre-creating engine for future session shard={} cc_seqno={} session_id={:x}", - ident, next_cc_seqno, session_id); - let g = group.clone(); - let sid = session_id.clone(); - tokio::spawn(async move { - if let Err(e) = g.pre_create_engine().await { - log::error!(target: "validator_manager", - "SESSION_LIFECYCLE: pre_create_engine_failed session_id={:x} error={}", sid, e); - } - }); - } - } - } - - // Stale-future culling: remove future entries whose shard already has a current - // group with equal or higher cc_seqno, or whose shard is an ancestor/descendant - // of a current shard with strictly higher cc_seqno. - // C++ parity: equal + related conditions from manager.cpp update_shards(). - { - let stale_ids: Vec = self - .future_sessions - .iter() - .filter(|(_, fg)| { - self.current_sessions.values().any(|cg| { - let shards_equal = cg.shard() == fg.shard(); - let shards_related = cg.shard().is_ancestor_for(fg.shard()) - || fg.shard().is_ancestor_for(cg.shard()); - let equal_condition = shards_equal && cg.cc_seqno() >= fg.cc_seqno(); - let related_condition = shards_related && cg.cc_seqno() > fg.cc_seqno(); - equal_condition || related_condition - }) - }) - .map(|(id, _)| id.clone()) - .collect(); - for id in stale_ids { - if let Some(fg) = self.future_sessions.remove(&id) { - // C++ parity: destroyed_validator_sessions_.insert(id) - self.destroyed_sessions.insert(id.clone()); - log::info!(target: "validator_manager", - "SESSION_LIFECYCLE: cull_stale_future shard={} cc_seqno={} session_id={:x} \ - (superseded by active current session)", - fg.shard(), fg.cc_seqno(), id); - let cl = if fg.is_simplex() { "simplex" } else { "catchain" }; - metrics::counter!( - "ton_node_validator_session_destroyed_total", - "consensus" => cl - ) - .increment(1); - // C++ parity: IValidatorGroup::destroy - if let Err(e) = fg.stop(self.rt.clone(), true).await { - log::error!(target: "validator_manager", - "SESSION_LIFECYCLE: cull_stale_future_stop_failed session_id={:x}: {}", - id, e); - } - } + self.validator_sessions.entry(session_id.clone()).or_insert_with(|| { + Arc::new(ValidatorGroup::new( + new_session_info, + local_id, + session_id.clone(), + next_val_list_id.clone(), + vsubset.clone(), + consensus_options.clone(), + self.engine.clone(), + self.config.unsafe_resync_catchains.contains(next_cc_seqno), + )) + }); } } - let mut precalc_split_queues_for: HashSet = HashSet::new(); - for session in self.current_sessions.values().chain(self.future_sessions.values()) { + for session in self.validator_sessions.values() { for id in &blocks_before_split { if id.shard().is_parent_for(session.shard()) { log::trace!( @@ -2002,6 +1446,11 @@ impl ValidatorManagerImpl { }); } + if rotate_all_shards(mc_state_extra) { + log::info!(target: "validator_manager", "New last rotation block: {}", last_masterchain_block); + self.engine.save_last_rotation_block_id(last_masterchain_block)?; + } + // Notify shard simplex sessions about MC finalization // This is needed for empty block generation (finalization recovery) self.notify_shard_sessions_mc_finalized(last_masterchain_block.seq_no); @@ -2009,43 +1458,20 @@ impl ValidatorManagerImpl { log::trace!(target: "validator_manager", "starting stop&remove"); self.stop_and_remove_sessions(&gc_validator_sessions, true).await; - if rotate_all_shards(mc_state_extra) { - log::info!(target: "validator_manager", "New last rotation block: {}", last_masterchain_block); - self.engine.save_last_rotation_block_id(last_masterchain_block)?; - self.clear_destroyed_sessions()?; - } else { - self.persist_destroyed_sessions()?; - } - log::trace!(target: "validator_manager", "starting garbage collect"); self.garbage_collect().await; log::trace!(target: "validator_manager", "exiting"); Ok(()) } - /// Light per-iteration update: Prometheus gauges, engine timing, health warnings. - /// Called on every wait-loop iteration (every few seconds). async fn stats(&mut self) { - let validation_status = self.engine.validation_status(); - let in_current_set = self.validator_list_status.curr.is_some(); + log::info!(target: "validator_manager", "{:32} {}", "session id", "st round shard"); + log::info!(target: "validator_manager", "{:-64}", ""); - let mut state_counts: HashMap<&'static str, u64> = HashMap::new(); - let mut stalled_count: u64 = 0; - let mut simplex_count: u64 = 0; - let mut catchain_count: u64 = 0; - - for group in self.current_sessions.values() { + // Validation shards statistics + for (_, group) in self.validator_sessions.iter() { + log::info!(target: "validator_manager", "{}", group.info().await); let status = group.get_status().await; - let is_stalled = group.stalled.load(Ordering::Relaxed); - *state_counts.entry(status.metric_label()).or_default() += 1; - if is_stalled { - stalled_count += 1; - } - if group.is_simplex() { - simplex_count += 1; - } else { - catchain_count += 1; - } if status == ValidatorGroupStatus::Sync || status == ValidatorGroupStatus::Active || status == ValidatorGroupStatus::Stopping @@ -2056,266 +1482,7 @@ impl ValidatorManagerImpl { .set_last_collation_time(group.shard().clone(), group.last_collation_time()); } } - for group in self.future_sessions.values() { - let status = group.get_status().await; - *state_counts.entry(status.metric_label()).or_default() += 1; - if group.is_simplex() { - simplex_count += 1; - } else { - catchain_count += 1; - } - } - - // Health warnings for operator attention - if stalled_count > 0 { - log::warn!(target: "validator_manager", - "HEALTH_CHECK: {} session(s) stalled (validation queue inactive)", stalled_count); - } - if in_current_set && self.current_sessions.is_empty() && validation_status.allows_validate() - { - log::warn!(target: "validator_manager", - "HEALTH_CHECK: node is in current validator set but has no current sessions \ - (validation_status={:?})", validation_status); - } - if validation_status.allows_validate() { - let sync_count = state_counts.get("sync").copied().unwrap_or(0); - let active_count = state_counts.get("active").copied().unwrap_or(0); - if sync_count == 0 && active_count == 0 && !self.current_sessions.is_empty() { - log::warn!(target: "validator_manager", - "HEALTH_CHECK: validation enabled but no current session reached sync yet \ - (possible startup/session-start regression, validation_status={:?})", - validation_status); - } - } - - // Prometheus metrics - metrics::gauge!("ton_node_validator_sessions_total", "role" => "current") - .set(self.current_sessions.len() as f64); - metrics::gauge!("ton_node_validator_sessions_total", "role" => "future") - .set(self.future_sessions.len() as f64); - metrics::gauge!("ton_node_validator_sessions_by_consensus", "type" => "simplex") - .set(simplex_count as f64); - metrics::gauge!("ton_node_validator_sessions_by_consensus", "type" => "catchain") - .set(catchain_count as f64); - for (state, count) in &state_counts { - metrics::gauge!("ton_node_validator_sessions_by_state", "state" => *state) - .set(*count as f64); - } - metrics::gauge!("ton_node_validator_group_stalled").set(stalled_count as f64); - metrics::gauge!("ton_node_validator_sync_complete").set(self.sync_complete as u8 as f64); - - // Full metrics dump once per minute - if self.last_metrics_dump.elapsed() >= Duration::from_secs(60) { - self.last_metrics_dump = tokio::time::Instant::now(); - self.dump_metrics().await; - } - } - - /// Comprehensive metrics dump emitted once per minute. - /// Structured for easy grep and readable operator dashboards. - async fn dump_metrics(&self) { - let now = SystemTime::now(); - let now_unix = now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); - let validation_status = self.engine.validation_status(); - let in_current_set = self.validator_list_status.curr.is_some(); - let in_next_set = self.validator_list_status.next.is_some(); - // โ”€โ”€ Header: overall manager state โ”€โ”€ - let mut simplex_count = 0u32; - let mut catchain_count = 0u32; - let mut stalled_count = 0u32; - let mut state_counts: HashMap<&'static str, u32> = HashMap::new(); - - // Collect current session snapshots - let mut current_snapshots: Vec<(SessionSnapshot, bool, bool, u64, u64)> = Vec::new(); - for group in self.current_sessions.values() { - let snap = group.snapshot().await; - let is_stalled = group.stalled.load(Ordering::Relaxed); - let is_collating = group.is_collating(); - let last_val = group.last_validation_time(); - let last_col = group.last_collation_time(); - *state_counts.entry(snap.status.metric_label()).or_default() += 1; - if is_stalled { - stalled_count += 1; - } - if snap.consensus_type == super::consensus::ConsensusType::Simplex { - simplex_count += 1; - } else { - catchain_count += 1; - } - current_snapshots.push((snap, is_stalled, is_collating, last_val, last_col)); - } - - // Collect future session snapshots - let mut future_snapshots: Vec = Vec::new(); - for group in self.future_sessions.values() { - let snap = group.snapshot().await; - *state_counts.entry(snap.status.metric_label()).or_default() += 1; - if snap.consensus_type == super::consensus::ConsensusType::Simplex { - simplex_count += 1; - } else { - catchain_count += 1; - } - future_snapshots.push(snap); - } - - let state_str: String = - state_counts.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(" "); - - let mut lines = Vec::::new(); - lines.push(format!( - "=== VALIDATOR MANAGER METRICS (once/min) ===\n\ - \x20 validation_status={:?} sync_complete={}\n\ - \x20 in_current_set={} in_next_set={}\n\ - \x20 sessions: current={} future={} total={} (simplex={} catchain={})\n\ - \x20 by_state: [{}] stalled={}", - validation_status, - self.sync_complete, - in_current_set, - in_next_set, - self.current_sessions.len(), - self.future_sessions.len(), - self.current_sessions.len() + self.future_sessions.len(), - simplex_count, - catchain_count, - state_str, - stalled_count, - )); - - // โ”€โ”€ Validator keys โ”€โ”€ - lines.push(String::from(" VALIDATOR KEYS:")); - for (role, list_id_opt, utime_opt) in [ - ( - "current", - &self.validator_list_status.curr, - self.validator_list_status.curr_utime_since, - ), - ("next", &self.validator_list_status.next, self.validator_list_status.next_utime_since), - ] { - if let Some(list_id) = list_id_opt { - let entry = self.validator_list_status.get_list(list_id); - let net_ready = entry.map_or(false, |e| e.network_ready); - let key_strs: Vec = entry - .map(|e| e.keys.iter().map(|k| base64_encode(k.id().data())).collect()) - .unwrap_or_default(); - lines.push(format!( - " [{}] list_id={:x} election_utime={} net_ready={} keys=[{}]", - role, - list_id, - utime_opt.map_or("-".to_string(), |u| u.to_string()), - net_ready, - key_strs.join(", "), - )); - } else { - lines.push(format!(" [{}] not in set", role)); - } - } - - // โ”€โ”€ Config-level key bindings (election_id โ†’ validator_key, adnl_key) โ”€โ”€ - match self.engine.get_validator_key_bindings() { - Ok(bindings) => { - lines.push(format!(" KEY BINDINGS ({}):", bindings.len())); - let mut seen_elections: HashMap = HashMap::new(); - for (idx, b) in bindings.iter().enumerate() { - let adnl_str = b.validator_adnl_key_id.as_deref().unwrap_or("(none)"); - lines.push(format!( - " election_id={:<12} key={} adnl={} expire_at={}", - b.election_id, b.validator_key_id, adnl_str, b.expire_at, - )); - if let Some(prev_idx) = seen_elections.insert(b.election_id, idx) { - log::error!( - target: "validator_manager", - "KEY BINDING INVARIANT VIOLATION: duplicate election_id={}: \ - binding[{}] and binding[{}] share the same election_id", - b.election_id, prev_idx, idx, - ); - } - if b.validator_adnl_key_id.is_none() { - log::warn!( - target: "validator_manager", - "KEY BINDING: election_id={} has validator_key={} but no ADNL key bound", - b.election_id, b.validator_key_id, - ); - } - } - } - Err(e) => { - lines.push(format!(" KEY BINDINGS: error retrieving: {e}")); - } - } - - // โ”€โ”€ Current sessions detail โ”€โ”€ - if current_snapshots.is_empty() { - lines.push(String::from(" CURRENT SESSIONS: (none)")); - } else { - lines.push(format!(" CURRENT SESSIONS ({}):", current_snapshots.len())); - for (snap, is_stalled, is_collating, last_val, last_col) in ¤t_snapshots { - let shard_str = format_shard_short(&snap.shard); - let consensus_str = - if snap.consensus_type == super::consensus::ConsensusType::Simplex { - "splx" - } else { - "cch" - }; - let age = now.duration_since(snap.created_at).unwrap_or_default(); - let val_ago = format_time_ago(now_unix, *last_val); - let col_ago = format_time_ago(now_unix, *last_col); - let status_str = format!("{}", snap.status); - let last_mc = - snap.last_accepted_mc_seqno.map_or("-".to_string(), |s| s.to_string()); - let phase_str = validation_state_phase_label(snap.status); - lines.push(format!( - " {:<8} cc={:<4} {:<4} {:<14} phase={:<13} rnd={:<4} collator={:<3} \ - collating={:<3} stall={:<3} val_ago={:<6} col_ago={:<6} \ - mc_init={:<6} mc_last={:<6} age={} id={:x}", - shard_str, - snap.cc_seqno, - consensus_str, - status_str, - phase_str, - snap.round, - if snap.is_collator { "yes" } else { "no" }, - if *is_collating { "yes" } else { "no" }, - if *is_stalled { "yes" } else { "no" }, - val_ago, - col_ago, - snap.mc_initial_seqno, - last_mc, - format_duration_short(age), - snap.session_id, - )); - } - } - - // โ”€โ”€ Future sessions detail โ”€โ”€ - if future_snapshots.is_empty() { - lines.push(String::from(" FUTURE SESSIONS: (none)")); - } else { - lines.push(format!(" FUTURE SESSIONS ({}):", future_snapshots.len())); - for snap in &future_snapshots { - let shard_str = format_shard_short(&snap.shard); - let consensus_str = - if snap.consensus_type == super::consensus::ConsensusType::Simplex { - "splx" - } else { - "cch" - }; - let age = now.duration_since(snap.created_at).unwrap_or_default(); - lines.push(format!( - " {:<8} cc={:<4} {:<4} {:<14} engine={:<3} key_seq={} age={} id={:x}", - shard_str, - snap.cc_seqno, - consensus_str, - format!("{}", snap.status), - if snap.has_engine { "yes" } else { "no" }, - snap.key_seqno, - format_duration_short(age), - snap.session_id, - )); - } - } - - lines.push(String::from("=== END VALIDATOR MANAGER METRICS ===")); - log::info!(target: "validator_manager", "{}", lines.join("\n")); + log::trace!(target: "validator_manager", "======= sessions stats over ======="); } fn read_catchain_config(&self, state: &ShardStateStuff) -> Result { @@ -2325,7 +1492,6 @@ impl ValidatorManagerImpl { /// infinite loop with possible error cancellation async fn invoke(&mut self) -> Result<()> { - self.load_destroyed_sessions()?; let last_applied_block_id = self.engine.load_last_applied_mc_block_id()?.ok_or_else(|| { error!("Cannot run validator_manager if no last applied block is present") @@ -2344,10 +1510,8 @@ impl ValidatorManagerImpl { .load_block_handle(&id)? .ok_or_else(|| error!("Cannot load handle for master block {}", id))? } else { - log::info!( - target: "validator_manager", - "Validator manager initialization: no last rotation block, \ - using last applied block: {last_applied_block_id}" + log::info!(target: "validator_manager", + "Validator manager initialization: no last rotation block, using last applied block: {}", last_applied_block_id ); last_applied_block_handle.clone() }; @@ -2355,29 +1519,15 @@ impl ValidatorManagerImpl { //let block_observer = self.initialize_block_observer(&last_applied_block_handle).await?; while !self.engine.check_stop() { - log::trace!( - target: "validator_manager", - "Trying to load state for masterblock {}", - mc_handle.id().seq_no - ); + log::trace!(target: "validator_manager", "Trying to load state for masterblock {}", mc_handle.id().seq_no); match self.engine.load_state(mc_handle.id()).await { Ok(mc_state) => { - let seqno = mc_handle.id().seq_no; - log::info!(target: "validator_manager", "Processing masterblock {seqno}"); - log::trace!( - target: "validator_manager", - "Processing messages from masterblock {seqno}" - ); - log::trace!( - target: "validator_manager", - "Updating shards according to masterblock {seqno}" - ); + log::info!(target: "validator_manager", "Processing masterblock {}", mc_handle.id().seq_no); + log::trace!(target: "validator_manager", "Processing messages from masterblock {}", mc_handle.id().seq_no); + log::trace!(target: "validator_manager", "Updating shards according to masterblock {}", mc_handle.id().seq_no); self.update_shards(mc_state).await?; - log::trace!( - target: "validator_manager", - "Shards for masterblock {seqno} updated" - ); + log::trace!(target: "validator_manager", "Shards for masterblock {} updated", mc_handle.id().seq_no); } Err(e) => { if self.engine.validation_status().allows_validate() { @@ -2388,30 +1538,19 @@ impl ValidatorManagerImpl { e ) } - log::info!( - target: "validator_manager", - "Processing masterblock {}: state not available, going forward", - mc_handle.id().seq_no - ); + log::info!(target: "validator_manager", "Processing masterblock {}: state not available, going forward", mc_handle.id().seq_no); } } mc_handle = loop { log::trace!(target: "validator_manager", "Checking stop engine"); if self.engine.check_stop() { - log::trace!( - target: "validator_manager", - "Engine is stopped. Exiting from invocation loop (while loading block)" - ); + log::trace!(target: "validator_manager", "Engine is stoped. Exiting from invocation loop (while loading block)"); return Ok(()); } log::trace!(target: "validator_manager", "Checked stop engine: going on"); self.stats().await; - log::trace!( - target: "validator_manager", - "Waiting next applied masterblock after {}", - mc_handle.id().seq_no - ); + log::trace!(target: "validator_manager", "Waiting next applied masterblock after {}", mc_handle.id().seq_no); match timeout( self.config.update_interval, self.engine.wait_next_applied_mc_block(&mc_handle, None), @@ -2419,9 +1558,7 @@ impl ValidatorManagerImpl { .await { Ok(r_res) => { - log::trace!( - target: "validator_manager", - "Got next applied master block (result): {}", + log::trace!(target: "validator_manager", "Got next applied master block (result): {}", match &r_res { Err(e) => format!("Err({})", e), Ok((h, _bs)) => format!("Ok({})", h.id()) @@ -2440,10 +1577,7 @@ impl ValidatorManagerImpl { } } - log::info!( - target: "validator_manager", - "Engine is stopped. Exiting from invocation loop (while applying state)" - ); + log::info!(target: "validator_manager", "Engine is stopped. Exiting from invocation loop (while applying state)"); Ok(()) } } @@ -2456,15 +1590,11 @@ pub fn start_validator_manager( ) { const CHECK_VALIDATOR_TIMEOUT: u64 = 60; //secs runtime.clone().spawn(async move { - log::info!( - target: "validator_manager", - "checking if current node is a validator during {CHECK_VALIDATOR_TIMEOUT} secs" - ); + log::info!(target: "validator_manager", "checking if current node is a validator during {CHECK_VALIDATOR_TIMEOUT} secs"); engine.acquire_stop(Engine::MASK_SERVICE_VALIDATOR_MANAGER); while !engine.get_validator_status() { log::trace!(target: "validator_manager", "Not a validator, waiting..."); let _ = engine.clear_last_rotation_block_id(); - let _ = engine.clear_destroyed_session_ids(); for _ in 0..CHECK_VALIDATOR_TIMEOUT { tokio::time::sleep(Duration::from_secs(1)).await; if engine.check_stop() { @@ -2479,10 +1609,7 @@ pub fn start_validator_manager( let mut manager = ValidatorManagerImpl::create(engine.clone(), runtime.clone(), config); if let Err(e) = manager.invoke().await { - log::error!( - target: "validator_manager", - "FATAL!!! Unexpected error in validator manager: {e}" - ); + log::error!(target: "validator_manager", "FATAL!!! Unexpected error in validator manager: {}", e); } manager.stop_validation().await; diff --git a/src/node/src/validator/validator_session_listener.rs b/src/node/src/validator/validator_session_listener.rs index 1fece8b..257cbc7 100644 --- a/src/node/src/validator/validator_session_listener.rs +++ b/src/node/src/validator/validator_session_listener.rs @@ -17,7 +17,7 @@ use super::consensus::{ use crate::validator::validator_group::{ValidatorGroup, ValidatorGroupStatus}; use std::{ fmt, - sync::{atomic::Ordering, Arc}, + sync::Arc, time::{Duration, SystemTime, SystemTimeError}, }; use ton_block::{BlockIdExt, BlockSignaturesVariant, ShardIdent}; @@ -399,22 +399,23 @@ pub async fn process_validation_queue( ); break 'queue_loop; } - Err(_elapsed) => match (last_action + VALIDATION_QUEUE_EMPTY_TOO_LONG).elapsed() { - Ok(_) => { - g.stalled.store(true, Ordering::Relaxed); - log::info!( - target: "validator", - "({}): Session {}: validation action queue empty (stalled=true)", - next_block_descr, - g_info - ); - last_action = SystemTime::now(); + Err(_elapsed) => { + // Timeout occurred, queue is empty + match (last_action + VALIDATION_QUEUE_EMPTY_TOO_LONG).elapsed() { + Ok(_) => { + log::info!( + target: "validator", + "({}): Session {}: validation action queue empty", + next_block_descr, + g_info + ); + last_action = SystemTime::now(); + } + Err(SystemTimeError { .. }) => (), } - Err(SystemTimeError { .. }) => (), - }, + } Ok(Some(action)) => { last_action = SystemTime::now(); - g.stalled.store(false, Ordering::Relaxed); let action_str = action.to_string(); log::info!( diff --git a/src/node/storage/Cargo.toml b/src/node/storage/Cargo.toml index 8119d15..5b263b6 100644 --- a/src/node/storage/Cargo.toml +++ b/src/node/storage/Cargo.toml @@ -23,7 +23,6 @@ rocksdb = '0.23' serde = '1.0' serde_cbor = '0.11' serde_derive = '1.0' -serde_json = '1.0' smallvec = { features = [ 'const_new', 'union', 'write' ], version = '1.10' } strum = '0.18' strum_macros = '0.18' @@ -39,7 +38,6 @@ ton_api = { path = '../../tl/ton_api' } cc = { features = [ 'parallel' ], version = '1.0.61' } [dev-dependencies] -tempfile = '3' zip = '2.2' [features] diff --git a/src/node/storage/benches/shardstate_db1.rs b/src/node/storage/benches/shardstate_db1.rs index d222286..a6e9234 100644 --- a/src/node/storage/benches/shardstate_db1.rs +++ b/src/node/storage/benches/shardstate_db1.rs @@ -11,7 +11,7 @@ use std::{collections::HashMap, sync::Arc}; #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, + db::rocksdb::{destroy_rocks_db, RocksDb}, dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::{CellsDbConfig, ShardStateDb, SsNotificationCallback}, StorageAlloc, @@ -49,7 +49,7 @@ async fn main() -> Result<()> { "counters".to_string(), DynamicBocDb::build_counters_cf_options(&CellsDbConfig::default()), ); - let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, AccessType::ReadOnly)?; + let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, None)?; let ss_db = ShardStateDb::new( db.clone(), "shardstate_db", diff --git a/src/node/storage/benches/shardstate_db2.rs b/src/node/storage/benches/shardstate_db2.rs index e46736b..6a89130 100644 --- a/src/node/storage/benches/shardstate_db2.rs +++ b/src/node/storage/benches/shardstate_db2.rs @@ -10,7 +10,7 @@ use std::{collections::HashMap, io::Cursor, ops::Deref, sync::Arc}; #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, + db::rocksdb::{destroy_rocks_db, RocksDb}, dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::{CellsDbConfig, ShardStateDb}, StorageAlloc, @@ -40,7 +40,7 @@ async fn main() -> Result<()> { "counters".to_string(), DynamicBocDb::build_counters_cf_options(&CellsDbConfig::default()), ); - let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, AccessType::ReadOnly)?; + let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, None)?; let ss_db = ShardStateDb::new( db.clone(), "shardstate_db", diff --git a/src/node/storage/benches/shardstate_db3.rs b/src/node/storage/benches/shardstate_db3.rs index 67e9cf1..05519e2 100644 --- a/src/node/storage/benches/shardstate_db3.rs +++ b/src/node/storage/benches/shardstate_db3.rs @@ -10,7 +10,7 @@ use std::{collections::HashMap, fs::OpenOptions, path::Path, sync::Arc}; #[cfg(feature = "telemetry")] use storage::StorageTelemetry; use storage::{ - db::rocksdb::{AccessType, RocksDb}, + db::rocksdb::RocksDb, dynamic_boc_rc_db::DynamicBocDb, shardstate_db_async::{CellsDbConfig, ShardStateDb}, StorageAlloc, @@ -37,7 +37,7 @@ async fn main() -> Result<()> { "counters".to_string(), DynamicBocDb::build_counters_cf_options(&CellsDbConfig::default()), ); - let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, AccessType::ReadOnly)?; + let db = RocksDb::new(DB_PATH, DB_NAME, cfs_opts, None)?; let ss_db = ShardStateDb::new( db.clone(), "shardstate_db", diff --git a/src/node/storage/src/archive_shardstate_db.rs b/src/node/storage/src/archive_shardstate_db.rs deleted file mode 100644 index c1362ac..0000000 --- a/src/node/storage/src/archive_shardstate_db.rs +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -#[cfg(feature = "telemetry")] -use crate::StorageTelemetry; -use crate::{ - cell_db::CellByHashStorageAdapter, - db::rocksdb::{RocksDb, RocksDbTable}, - dynamic_boc_archive_db::DynamicBocArchiveDb, - shardstate_db_async::{CellsDbConfig, DbEntry}, - traits::Serializable, - StorageAlloc, TARGET, -}; -use std::{path::Path, sync::Arc}; -use ton_block::{BlockIdExt, Cell, CellsFactory, CellsStorage, Result, UInt256, UnixTime}; - -pub struct ArchiveShardStateDb { - index: Arc>, - boc_db: Arc, -} - -impl ArchiveShardStateDb { - #[allow(clippy::too_many_arguments)] - pub fn new( - db: Arc, - index_cf: &str, - cells_cf: &str, - db_root_path: impl AsRef, - config: &CellsDbConfig, - #[cfg(feature = "telemetry")] telemetry: Arc, - allocated: Arc, - ) -> Result { - let boc_db = Arc::new(DynamicBocArchiveDb::with_db( - db.clone(), - cells_cf, - db_root_path.as_ref(), - config, - #[cfg(feature = "telemetry")] - telemetry, - allocated, - )?); - let index = Arc::new(RocksDbTable::with_db(db, index_cf, true)?); - Ok(Self { index, boc_db }) - } - - pub fn put(&self, id: &BlockIdExt, state_root: Cell) -> Result { - let cell_id = state_root.repr_hash(); - log::debug!( - target: TARGET, - "ArchiveShardStateDb::put id {} root_cell_id {:x}", id, cell_id - ); - - if self.index.contains(id)? { - log::debug!( - target: TARGET, - "ArchiveShardStateDb::put ALREADY EXISTS id {}", id - ); - let data = self.index.get(id)?; - let db_entry = DbEntry::deserialize(&data)?; - return self.boc_db.cell_db().load_cell(&db_entry.cell_id, false); - } - - let saved = self.boc_db.save_boc(state_root, &|| Ok(()))?; - let save_utime = UnixTime::now(); - let db_entry = DbEntry::with_params(id.clone(), cell_id, save_utime); - self.index.put(id, &db_entry.serialize())?; - Ok(saved) - } - - pub fn put_update(&self, id: &BlockIdExt, state_root: Cell) -> Result<()> { - let state_root = state_root.virtualize(1); - let cell_id = state_root.repr_hash(); - log::debug!( - target: TARGET, - "ArchiveShardStateDb::put_update id {} root_cell_id {:x}", id, cell_id - ); - - if self.index.contains(id)? { - log::info!( - target: TARGET, - "ArchiveShardStateDb::put_update ALREADY EXISTS id {}", id - ); - return Ok(()); - } - - self.boc_db.save_update(state_root)?; - let save_utime = UnixTime::now(); - let db_entry = DbEntry::with_params(id.clone(), cell_id, save_utime); - self.index.put(id, &db_entry.serialize())?; - Ok(()) - } - - pub fn get(&self, id: &BlockIdExt) -> Result { - let data = self.index.get(id)?; - let db_entry = DbEntry::deserialize(&data)?; - log::debug!( - target: TARGET, - "ArchiveShardStateDb::get id {} cell_id {:x}", id, db_entry.cell_id - ); - self.boc_db.cell_db().load_cell(&db_entry.cell_id, false) - } - - pub fn get_cell(&self, id: &UInt256) -> Result { - self.boc_db.cell_db().load_cell(id, false) - } - - pub fn contains(&self, id: &BlockIdExt) -> Result { - self.index.contains(id) - } - - pub fn cells_factory(&self) -> Arc { - self.boc_db.cell_db().clone() as Arc - } - - pub fn create_hashed_cell_storage( - &self, - root: Option<&Cell>, - max_inmemory_cells: usize, - ) -> Result { - CellByHashStorageAdapter::new(self.boc_db.cell_db().clone(), root, max_inmemory_cells) - } -} diff --git a/src/node/storage/src/archives/archive_manager.rs b/src/node/storage/src/archives/archive_manager.rs index 1910fa4..c54ed3a 100644 --- a/src/node/storage/src/archives/archive_manager.rs +++ b/src/node/storage/src/archives/archive_manager.rs @@ -13,13 +13,11 @@ use crate::StorageTelemetry; use crate::{ archives::{ archive_slice::ArchiveSlice, - db_provider::ArchiveDbProvider, file_maps::{BlockRanges, FileDescription, FileMaps}, get_mc_seq_no, - package::PKG_HEADER_SIZE, package_entry::PackageEntry, package_entry_id::{parse_short_filename, GetFileName, PackageEntryId}, - package_id::{PackageId, PackageType}, + package_id::PackageId, ARCHIVE_SLICE_SIZE, KEY_ARCHIVE_PACKAGE_SIZE, }, block_handle_db::BlockHandle, @@ -30,7 +28,7 @@ use std::{ borrow::Borrow, hash::Hash, io::ErrorKind, - path::{Path, PathBuf}, + path::PathBuf, sync::{ atomic::{AtomicU8, Ordering}, Arc, @@ -40,32 +38,12 @@ use std::{ use tokio::io::AsyncWriteExt; use ton_block::{error, fail, AccountIdPrefixFull, BlockIdExt, Result, ShardIdent, MASTERCHAIN_ID}; -/// Metadata about a block being imported into the archive. -pub struct ImportBlockMeta { - pub seq_no: u32, - pub shard: ShardIdent, - pub gen_utime: u32, - pub end_lt: u64, - pub mc_ref_seq_no: u32, -} - -/// A single entry from a .pack file being imported. -pub struct ImportEntry { - pub entry_id: PackageEntryId, - pub offset: u64, - /// Metadata for Block entries. Must be Some for PackageEntryId::Block, - /// None for Proof/ProofLink. - pub block_meta: Option, -} - pub struct ArchiveManager { db: Arc, db_root_path: Arc, - db_provider: Arc, file_maps: FileMaps, shard_split_depth: Arc, unapplied_files_path: PathBuf, - create_slice_mutex: tokio::sync::Mutex<()>, #[cfg(feature = "telemetry")] telemetry: Arc, allocated: Arc, @@ -77,7 +55,6 @@ impl ArchiveManager { pub async fn with_data( db: Arc, db_root_path: Arc, - db_provider: Arc, last_unneeded_key_block: u32, shard_split_depth: Arc, #[cfg(feature = "telemetry")] telemetry: Arc, @@ -86,7 +63,6 @@ impl ArchiveManager { let file_maps = FileMaps::new( db.clone(), &db_root_path, - &db_provider, last_unneeded_key_block, #[cfg(feature = "telemetry")] &telemetry, @@ -100,11 +76,9 @@ impl ArchiveManager { let ret = Self { db, db_root_path, - db_provider, file_maps, shard_split_depth, unapplied_files_path, - create_slice_mutex: tokio::sync::Mutex::new(()), #[cfg(feature = "telemetry")] telemetry, allocated, @@ -399,14 +373,14 @@ impl ArchiveManager { } Ok(read) => read, }; - let data = self.add_block_data_to_package(data, handle, entry_id, false).await?; + let data = self.move_file_to_archive(data, handle, entry_id, false).await?; if handle.is_key_block()? { - self.add_block_data_to_package(data, handle, entry_id, true).await?; + self.move_file_to_archive(data, handle, entry_id, true).await?; } Ok(Some(filename)) } - pub async fn add_block_data_to_package + Hash>( + async fn move_file_to_archive + Hash>( &self, data: Vec, handle: &BlockHandle, @@ -430,10 +404,8 @@ impl ArchiveManager { .get_file_desc(&package_id, true) .await? .ok_or_else(|| error!("Expected some value for {package_id:?}"))?; - if fd.update_block_ranges(handle) { - let file_map = - if key_archive { self.file_maps.key_files() } else { self.file_maps.files() }; - file_map.update(fd.id().id(), &fd).await?; + if !key_archive && fd.update_block_ranges(handle) { + self.file_maps.files().update(fd.id().id(), &fd).await?; } fd.archive_slice().add_file(handle, entry_id, data).await } @@ -464,6 +436,7 @@ impl ArchiveManager { id: &PackageId, force_create: bool, ) -> Result>> { + // TODO: Rewrite logics in order to handle multithreaded adding of packages if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { if fd.deleted() { return Ok(None); @@ -471,13 +444,6 @@ impl ArchiveManager { return Ok(Some(fd)); } if force_create { - let _guard = self.create_slice_mutex.lock().await; - if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { - if fd.deleted() { - return Ok(None); - } - return Ok(Some(fd)); - } Ok(Some(self.add_file_desc(id).await?)) } else { Ok(None) @@ -485,17 +451,14 @@ impl ArchiveManager { } async fn add_file_desc(&self, id: &PackageId) -> Result> { + // TODO: Rewrite logics in order to handle multithreaded adding of packages let file_map = self.file_maps.get(id.package_type()); assert!(file_map.get(id.id()).await.is_none()); - let (slice_db, slice_root_path) = match id.package_type() { - PackageType::KeyBlocks => (self.db.clone(), Arc::clone(&self.db_root_path)), - PackageType::Blocks => self.db_provider.db_for_archive(id.id()).await?, - }; - let dir = slice_root_path.join(id.path()); + let dir = self.db_root_path.join(id.path()); tokio::fs::create_dir_all(&dir).await?; let archive_slice = ArchiveSlice::new_empty( - slice_db, - slice_root_path, + self.db.clone(), + Arc::clone(&self.db_root_path), id.id(), id.package_type(), self.shard_split_depth.load(Ordering::Relaxed), @@ -559,137 +522,6 @@ impl ArchiveManager { } } - pub async fn import_package( - &self, - source_path: &Path, - archive_id: u32, - shard: &ShardIdent, - entries: &[ImportEntry], - move_file: bool, - contains_key_block: bool, - ) -> Result<()> { - let slice_id = self.get_package_id_force(archive_id, false, contains_key_block).await; - let fd = self.get_or_create_import_desc(&slice_id, shard.prefix_len()).await?; - - let pkg_id = PackageId::for_block(archive_id); - let target_path = pkg_id.full_path(fd.archive_slice().db_root_path(), shard)?; - - if target_path.exists() { - tokio::fs::remove_file(&target_path).await.map_err(|e| { - error!("Failed to remove existing file {}: {}", target_path.display(), e) - })?; - } else { - if let Some(parent) = target_path.parent() { - tokio::fs::create_dir_all(parent).await?; - } - } - - if move_file { - tokio::fs::rename(source_path, &target_path).await.map_err(|e| { - error!( - "Failed to move {} to {}: {}", - source_path.display(), - target_path.display(), - e - ) - })?; - } else { - tokio::fs::copy(source_path, &target_path).await.map_err(|e| { - error!( - "Failed to copy {} to {}: {}", - source_path.display(), - target_path.display(), - e - ) - })?; - } - - let file_len = tokio::fs::metadata(&target_path).await?.len(); - let file_size = file_len.checked_sub(PKG_HEADER_SIZE as u64).ok_or_else(|| { - error!("Package file {} is too short ({} bytes)", target_path.display(), file_len) - })?; - - fd.archive_slice().import_package_entries(archive_id, &shard, file_size, entries).await?; - - let file_map = self.file_maps.get(PackageType::Blocks); - let mut ranges_updated = false; - for entry in entries { - if let Some(meta) = &entry.block_meta { - ranges_updated |= fd.update_block_ranges_raw( - &meta.shard, - meta.seq_no, - meta.gen_utime, - meta.end_lt, - ); - } - } - if ranges_updated { - file_map.update(fd.id().id(), &fd).await?; - } - - Ok(()) - } - - async fn get_or_create_import_desc( - &self, - id: &PackageId, - shard_split_depth: u8, - ) -> Result> { - if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { - if !fd.deleted() { - return Ok(fd); - } - } - let _guard = self.create_slice_mutex.lock().await; - if let Some(fd) = self.file_maps.get(id.package_type()).get(id.id()).await { - if !fd.deleted() { - return Ok(fd); - } - } - self.add_file_desc_for_import(id, shard_split_depth).await - } - - async fn add_file_desc_for_import( - &self, - id: &PackageId, - shard_split_depth: u8, - ) -> Result> { - let file_map = self.file_maps.get(id.package_type()); - let (slice_db, slice_root_path) = match id.package_type() { - PackageType::KeyBlocks => (self.db.clone(), Arc::clone(&self.db_root_path)), - PackageType::Blocks => self.db_provider.db_for_archive(id.id()).await?, - }; - let dir = slice_root_path.join(id.path()); - tokio::fs::create_dir_all(&dir).await?; - let archive_slice = ArchiveSlice::new_for_import( - slice_db, - slice_root_path, - id.id(), - id.package_type(), - shard_split_depth, - #[cfg(feature = "telemetry")] - self.telemetry.clone(), - self.allocated.clone(), - ) - .await?; - let fd = Arc::new(FileDescription::with_data( - id.clone(), - archive_slice, - false, - lockfree::map::Map::new(), - )); - file_map - .put( - id.id(), - Arc::clone(&fd), - #[cfg(feature = "telemetry")] - &self.telemetry, - &self.allocated, - ) - .await?; - Ok(fd) - } - pub async fn trunc bool>( &self, block_id: &BlockIdExt, @@ -736,31 +568,6 @@ impl ArchiveManager { Ok(()) } - pub async fn get_max_mc_seqno(&self) -> Option { - let fd = self.file_maps.files().get_closest(u32::MAX).await?; - let guard = fd.blocks_ranges().get(&ShardIdent::masterchain())?; - Some(guard.val().max_seqno.load(Ordering::Relaxed)) - } - - pub async fn get_max_key_block_seqno(&self) -> Option { - let fd = self.file_maps.key_files().get_closest(u32::MAX).await?; - let guard = fd.blocks_ranges().get(&ShardIdent::masterchain())?; - Some(guard.val().max_seqno.load(Ordering::Relaxed)) - } - - pub async fn lookup_proof_by_seqno( - &self, - prefix: &AccountIdPrefixFull, - seqno: u32, - ) -> Result)>> { - if let Some(fd) = - self.lookup_file_descr_by(prefix, &mut |br| br.compare_seqno(&seqno)).await - { - return fd.archive_slice().lookup_proof_by_seqno(prefix, seqno).await; - } - Ok(None) - } - async fn lookup_file_descr_by( &self, prefix: &AccountIdPrefixFull, diff --git a/src/node/storage/src/archives/archive_slice.rs b/src/node/storage/src/archives/archive_slice.rs index f49b21e..6bb346e 100644 --- a/src/node/storage/src/archives/archive_slice.rs +++ b/src/node/storage/src/archives/archive_slice.rs @@ -12,7 +12,7 @@ use crate::StorageTelemetry; use crate::{ archives::{ - archive_manager::{ArchiveManager, ImportEntry}, + archive_manager::ArchiveManager, block_index_db::{BlockIndexDb, LookupResult}, get_mc_seq_no, package::{read_package_from, Package}, @@ -161,86 +161,6 @@ impl ArchiveSlice { Ok(ret) } - /// Create a new archive slice for importing existing .pack files. - /// Unlike `new_empty()`, this does not create an initial package file. - /// Packages are registered later via `import_package_entries()`. - pub async fn new_for_import( - db: Arc, - db_root_path: Arc, - archive_id: u32, - package_type: PackageType, - shard_split_depth: u8, - #[cfg(feature = "telemetry")] telemetry: Arc, - allocated: Arc, - ) -> Result { - let mut ret = Self::create( - db, - db_root_path, - archive_id, - package_type, - true, // finalized: prevents truncation when opening packages - true, // create_if_not_exist - shard_split_depth, - #[cfg(feature = "telemetry")] - telemetry, - allocated, - ) - .await?; - let mut transaction = ret.package_status_db.begin_transaction()?; - if ret.sliced_mode { - ret.shard_separated = true; - transaction.put(&PackageStatusKey::SlicedMode, &true.serialize())?; - transaction.put(&PackageStatusKey::TotalSlices, &0u32.serialize())?; - transaction.put(&PackageStatusKey::SliceSize, &ret.slice_size.serialize())?; - transaction - .put(&PackageStatusKey::ShardSplitDepth, &ret.shard_split_depth.serialize())?; - } else { - transaction.put(&PackageStatusKey::SlicedMode, &false.serialize())?; - transaction.put(&PackageStatusKey::NonSlicedSize, &0u64.serialize())?; - } - transaction.commit()?; - Ok(ret) - } - - pub async fn import_package_entries( - &self, - package_archive_id: u32, - shard: &ShardIdent, - file_size: u64, - entries: &[ImportEntry], - ) -> Result<()> { - let entry = PackageEntryInfo { seqno: package_archive_id, shard: shard.clone() }; - - if self.package_store.get(&entry).is_none() { - self.add_package(entry, file_size).await?; - } - - for import_entry in entries { - let offset_key = (&import_entry.entry_id).into(); - self.offsets_db.put_value(&offset_key, &import_entry.offset)?; - if let (PackageEntryId::Block(_), Some(bm)) = - (&import_entry.entry_id, &import_entry.block_meta) - { - self.block_index_db.put_raw( - &bm.shard, - bm.seq_no, - bm.end_lt, - bm.gen_utime, - bm.mc_ref_seq_no, - u32::try_from(import_entry.offset).map_err(|_| { - error!("entry offset {} exceeds u32 range", import_entry.offset) - })?, - )?; - } - } - - Ok(()) - } - - pub fn db_root_path(&self) -> &std::path::Path { - self.db_root_path.as_path() - } - #[allow(clippy::too_many_arguments)] pub async fn with_data( db: Arc, @@ -424,58 +344,6 @@ impl ArchiveSlice { } } - async fn add_package(&self, entry: PackageEntryInfo, size: u64) -> Result<()> { - let try_add_package = async |package_count, entry: &PackageEntryInfo| { - if self - .new_package(entry.clone(), Some(package_count), size, DEFAULT_PKG_VERSION) - .await? - { - let info = if self.shard_separated { Some(entry) } else { None }; - self.entry_db.put_value( - &package_count.into(), - &PackageEntryMeta::with_data(size, DEFAULT_PKG_VERSION, info), - )?; - self.package_status_db - .put_value(&PackageStatusKey::TotalSlices, &(package_count + 1))?; - Ok(true) - } else { - Ok(false) - } - }; - loop { - const BUSY: u32 = 0x80000000; - let package_count = self.package_count.fetch_or(BUSY, Ordering::Relaxed); - if (package_count & BUSY) != 0 { - tokio::task::yield_now().await; - continue; - } - let result = try_add_package(package_count, &entry).await; - let new_count = match &result { - Err(_) | Ok(false) => package_count, - Ok(true) => package_count + 1, - }; - if self - .package_count - .compare_exchange( - package_count | BUSY, - new_count, - Ordering::Relaxed, - Ordering::Relaxed, - ) - .is_err() - && result.is_ok() - { - tokio::task::yield_now().await; - continue; - } - if let Err(e) = result { - break Err(e); - } else { - break Ok(()); - } - } - } - pub async fn add_file + Hash>( &self, block_handle: &BlockHandle, @@ -504,7 +372,55 @@ impl ArchiveSlice { mc_seq_no - (mc_seq_no - self.archive_id) / self.slice_size ) } - self.add_package(entry, 0).await?; + let try_add_package = async |package_count, entry: &PackageEntryInfo| { + if self + .new_package(entry.clone(), Some(package_count), 0, DEFAULT_PKG_VERSION) + .await? + { + let info = if self.shard_separated { Some(entry) } else { None }; + self.entry_db.put_value( + &package_count.into(), + &PackageEntryMeta::with_data(0, DEFAULT_PKG_VERSION, info), + )?; + self.package_status_db + .put_value(&PackageStatusKey::TotalSlices, &(package_count + 1))?; + Ok(true) + } else { + Ok(false) + } + }; + loop { + const BUSY: u32 = 0x80000000; + let package_count = self.package_count.fetch_or(BUSY, Ordering::Relaxed); + if (package_count & BUSY) != 0 { + tokio::task::yield_now().await; + continue; + } + let result = try_add_package(package_count, &entry).await; + let new_count = match &result { + Err(_) | Ok(false) => package_count, + Ok(true) => package_count + 1, + }; + if self + .package_count + .compare_exchange( + package_count | BUSY, + new_count, + Ordering::Relaxed, + Ordering::Relaxed, + ) + .is_err() + && result.is_ok() + { + tokio::task::yield_now().await; + continue; + } + if let Err(e) = result { + return Err(e); + } else { + break; + } + } } } }; @@ -535,23 +451,14 @@ impl ArchiveSlice { &self, block_handle: &BlockHandle, entry_id: &PackageEntryId, - ) -> Result> { - let mc_seq_no = get_mc_seq_no(block_handle); - let shard = block_handle.id().shard(); - self.get_file_raw(mc_seq_no, &shard, entry_id).await - } - - async fn get_file_raw + Hash>( - &self, - mc_seq_no: u32, - shard: &ShardIdent, - entry_id: &PackageEntryId, ) -> Result> { let offset_key = entry_id.into(); let offset = match self.offsets_db.try_get_value(&offset_key)? { Some(offset) => offset, None => return Ok(None), }; + let mc_seq_no = get_mc_seq_no(block_handle); + let shard = block_handle.id().shard(); let package_info = match self.choose_package(mc_seq_no, shard).await? { ChosenPackage::Info(info) => info, ChosenPackage::Slot(_) => { @@ -662,31 +569,6 @@ impl ArchiveSlice { self.get_block_by_lookup_result(lr).await } - pub async fn lookup_proof_by_seqno( - &self, - prefix: &AccountIdPrefixFull, - seqno: u32, - ) -> Result)>> { - let Some(lr) = self.block_index_db.lookup_by_seqno(prefix, seqno)? else { - return Ok(None); - }; - let mc_seq_no = lr.mc_ref; - let Some((block_id, _)) = self.get_block_by_lookup_result(lr).await? else { - return Ok(None); - }; - - // Masterchain blocks store proofs under `Proof`, shard blocks under `ProofLink`. - let entry_id = if block_id.shard().is_masterchain() { - PackageEntryId::Proof(block_id.clone()) - } else { - PackageEntryId::ProofLink(block_id.clone()) - }; - - self.get_file_raw(mc_seq_no, block_id.shard(), &entry_id) - .await - .map(|opt_entry| opt_entry.map(|entry| (block_id, entry.take_data()))) - } - pub async fn lookup_block_by_lt( &self, prefix: &AccountIdPrefixFull, @@ -1046,7 +928,7 @@ impl ArchiveSlice { .map_err(|e| error!("Cannot create directory {} : {e}", parent.display()))?; if add_unbound_object_to_map(&self.package_store, entry.clone(), || Ok(OnceLock::new()))? { let create_package = async || { - let package = match Package::open(path.clone(), self.finalized, true).await { + let package = match Package::open(path.clone(), false, true).await { Ok(p) => p, Err(e) => match tokio::fs::remove_file(path.as_path()).await { Ok(_) => fail!( diff --git a/src/node/storage/src/archives/block_index_db.rs b/src/node/storage/src/archives/block_index_db.rs index 6493868..e87fcee 100644 --- a/src/node/storage/src/archives/block_index_db.rs +++ b/src/node/storage/src/archives/block_index_db.rs @@ -164,40 +164,25 @@ impl BlockIndexDb { offset, block.masterchain_ref_seq_no() ); - self.put_raw( - block.id().shard(), - block.id().seq_no(), - block.end_lt(), - block.gen_utime(), - block.masterchain_ref_seq_no(), - offset, - ) - } - /// Write block index entries from raw values (for archive import). - pub fn put_raw( - &self, - shard: &ShardIdent, - seq_no: u32, - end_lt: u64, - gen_utime: u32, - mc_ref_seq_no: u32, - offset: u32, - ) -> Result<()> { let cf = self.cf()?; - let value = Self::serialize_value(mc_ref_seq_no, offset); + let value = Self::serialize_value(block.masterchain_ref_seq_no(), offset); let mut transaction = rocksdb::WriteBatch::default(); - let key = BlocksIndexKey::key_with_lt(shard, end_lt); + let key = BlocksIndexKey::key_with_lt(block.id().shard(), block.end_lt()); log::trace!("Putting key: {}", key); transaction.put_cf(&cf, &key, value); - let key = BlocksIndexKey::key_with_seqno(shard, seq_no); + let key = BlocksIndexKey::key_with_seqno(block.id().shard(), block.id().seq_no()); log::trace!("Putting key: {}", key); transaction.put_cf(&cf, &key, value); - let key = BlocksIndexKey::key_with_utime(shard, gen_utime, seq_no); + let key = BlocksIndexKey::key_with_utime( + block.id().shard(), + block.gen_utime(), + block.id().seq_no(), + ); log::trace!("Putting key: {}", key); transaction.put_cf(&cf, &key, value); diff --git a/src/node/storage/src/archives/db_provider.rs b/src/node/storage/src/archives/db_provider.rs deleted file mode 100644 index b574192..0000000 --- a/src/node/storage/src/archives/db_provider.rs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use super::epoch::EpochRouter; -use crate::db::rocksdb::RocksDb; -use std::{path::PathBuf, sync::Arc}; -use ton_block::Result; - -/// Abstracts over single-db and epoch-based db selection for archive slices. -/// Provides the correct RocksDb instance and root path for a given archive_id. -#[async_trait::async_trait] -pub trait ArchiveDbProvider: Send + Sync { - /// Get the root path and RocksDb instance for the archive slice - async fn db_for_archive(&self, archive_id: u32) -> Result<(Arc, Arc)>; -} - -/// Single shared RocksDb, single root path. -/// Used when archival_mode is not configured. -pub struct SingleDbProvider { - db: Arc, - db_root_path: Arc, -} - -impl SingleDbProvider { - pub fn new(db: Arc, db_root_path: Arc) -> Self { - Self { db, db_root_path } - } -} - -#[async_trait::async_trait] -impl ArchiveDbProvider for SingleDbProvider { - async fn db_for_archive(&self, _archive_id: u32) -> Result<(Arc, Arc)> { - Ok((self.db.clone(), self.db_root_path.clone())) - } -} - -/// Epoch-based provider: routes archive requests to the correct epoch's RocksDb and path. -pub struct EpochDbProvider { - router: Arc, -} - -impl EpochDbProvider { - pub fn new(router: Arc) -> Self { - Self { router } - } - - pub fn router(&self) -> &Arc { - &self.router - } -} - -#[async_trait::async_trait] -impl ArchiveDbProvider for EpochDbProvider { - async fn db_for_archive(&self, archive_id: u32) -> Result<(Arc, Arc)> { - let epoch_db = self.router.resolve_or_create(archive_id).await?; - Ok((epoch_db.db().clone(), epoch_db.path().clone())) - } -} diff --git a/src/node/storage/src/archives/epoch.rs b/src/node/storage/src/archives/epoch.rs deleted file mode 100644 index 5251d28..0000000 --- a/src/node/storage/src/archives/epoch.rs +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use crate::{ - archives::ARCHIVE_SLICE_SIZE, - db::rocksdb::{AccessType, RocksDb}, - TARGET, -}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use ton_block::{error, fail, Result}; - -const EPOCH_META_FILENAME: &str = "epoch_meta.json"; - -/// Persisted metadata for an epoch directory -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub(crate) struct EpochMeta { - pub mc_seq_no_start: u32, - pub mc_seq_no_end: u32, -} - -async fn read_epoch_meta(epoch_path: &Path) -> Result { - let meta_path = epoch_path.join(EPOCH_META_FILENAME); - let data = tokio::fs::read_to_string(&meta_path) - .await - .map_err(|e| error!("Cannot read {}: {}", meta_path.display(), e))?; - serde_json::from_str(&data).map_err(|e| error!("Cannot parse {}: {}", meta_path.display(), e)) -} - -pub(crate) async fn write_epoch_meta(epoch_path: &Path, meta: &EpochMeta) -> Result<()> { - let meta_path = epoch_path.join(EPOCH_META_FILENAME); - let data = serde_json::to_string_pretty(meta) - .map_err(|e| error!("Cannot serialize epoch meta: {}", e))?; - tokio::fs::write(&meta_path, data.as_bytes()) - .await - .map_err(|e| error!("Cannot write {}: {}", meta_path.display(), e)) -} - -/// Configuration for a single existing epoch directory -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct EpochEntry { - pub path: PathBuf, -} - -/// Archival mode configuration. -/// When present, archives are split into epochs and GC is disabled. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct ArchivalModeConfig { - /// Number of MC blocks per epoch. Must be a positive multiple of ARCHIVE_SLICE_SIZE (20_000). - pub epoch_size: u32, - /// Path where new epoch directories will be created - pub new_epochs_path: PathBuf, - /// List of existing epoch directories, ordered by ascending MC seq_no. - #[serde(default)] - pub existing_epochs: Vec, -} - -/// Runtime state for a single epoch -pub struct Epoch { - mc_seq_no_start: u32, - mc_seq_no_end: u32, - path: Arc, - db: Arc, -} - -impl Epoch { - pub fn mc_seq_no_start(&self) -> u32 { - self.mc_seq_no_start - } - - pub fn mc_seq_no_end(&self) -> u32 { - self.mc_seq_no_end - } - - pub fn path(&self) -> &Arc { - &self.path - } - - pub fn db(&self) -> &Arc { - &self.db - } -} - -/// Routes mc_seq_no to the appropriate epoch's RocksDb and filesystem path. -/// -/// All epochs must have the same size (`epoch_size`), which allows O(1) arithmetic lookup -/// without any map search. -pub struct EpochRouter { - epochs: lockfree::map::Map>, - epoch_size: u32, - new_epochs_path: PathBuf, - creation_mutex: tokio::sync::Mutex<()>, -} - -impl EpochRouter { - pub async fn new(config: &ArchivalModeConfig) -> Result { - if config.epoch_size == 0 || config.epoch_size % ARCHIVE_SLICE_SIZE != 0 { - fail!( - "epoch_size must be a positive multiple of ARCHIVE_SLICE_SIZE ({}), got {}", - ARCHIVE_SLICE_SIZE, - config.epoch_size - ); - } - - let epochs = lockfree::map::Map::new(); - - for (i, entry) in config.existing_epochs.iter().enumerate() { - if !entry.path.exists() { - fail!("Epoch {} path does not exist: {}", i, entry.path.display()); - } - - let meta = read_epoch_meta(&entry.path).await?; - Self::validate_epoch_meta(&meta, config.epoch_size, &entry.path)?; - - let db = RocksDb::new(&entry.path, "archive_db", None, AccessType::ReadWrite)?; - - log::info!( - target: TARGET, - "Opened epoch {}: mc_seq_no [{}, {}], path: {}", - i, meta.mc_seq_no_start, meta.mc_seq_no_end, entry.path.display() - ); - - epochs.insert( - meta.mc_seq_no_start, - Arc::new(Epoch { - mc_seq_no_start: meta.mc_seq_no_start, - mc_seq_no_end: meta.mc_seq_no_end, - path: Arc::new(entry.path.clone()), - db, - }), - ); - } - - tokio::fs::create_dir_all(&config.new_epochs_path).await.map_err(|e| { - error!("Cannot create new_epochs_path {}: {}", config.new_epochs_path.display(), e) - })?; - - // Discover epochs previously created in new_epochs_path (survive restarts) - let mut read_dir = tokio::fs::read_dir(&config.new_epochs_path).await.map_err(|e| { - error!("Cannot read new_epochs_path {}: {}", config.new_epochs_path.display(), e) - })?; - let mut discovered = Vec::new(); - while let Some(entry) = read_dir - .next_entry() - .await - .map_err(|e| error!("Error reading new_epochs_path: {}", e))? - { - let epoch_path = entry.path(); - if epoch_path.is_dir() && epoch_path.join(EPOCH_META_FILENAME).exists() { - discovered.push(epoch_path); - } - } - - for epoch_path in discovered { - let meta = read_epoch_meta(&epoch_path).await?; - Self::validate_epoch_meta(&meta, config.epoch_size, &epoch_path)?; - - // Skip if already loaded from existing_epochs - if epochs.get(&meta.mc_seq_no_start).is_some() { - continue; - } - - let db = RocksDb::new(&epoch_path, "archive_db", None, AccessType::ReadWrite)?; - - log::info!( - target: TARGET, - "Discovered epoch: mc_seq_no [{}, {}], path: {}", - meta.mc_seq_no_start, meta.mc_seq_no_end, epoch_path.display() - ); - - epochs.insert( - meta.mc_seq_no_start, - Arc::new(Epoch { - mc_seq_no_start: meta.mc_seq_no_start, - mc_seq_no_end: meta.mc_seq_no_end, - path: Arc::new(epoch_path), - db, - }), - ); - } - - Ok(Self { - epochs, - epoch_size: config.epoch_size, - new_epochs_path: config.new_epochs_path.clone(), - creation_mutex: tokio::sync::Mutex::new(()), - }) - } - - pub fn resolve(&self, mc_seq_no: u32) -> Option> { - let start = (mc_seq_no / self.epoch_size) * self.epoch_size; - self.epochs.get(&start).map(|g| Arc::clone(g.val())) - } - - /// Resolve the epoch for a given mc_seq_no, creating a new one if needed. - pub async fn resolve_or_create(&self, mc_seq_no: u32) -> Result> { - if let Some(epoch) = self.resolve(mc_seq_no) { - return Ok(epoch); - } - - // Serialize creation to prevent concurrent RocksDb::new() on the same path - let _creation_guard = self.creation_mutex.lock().await; - - // Double-check after acquiring the mutex โ€” another caller may have created the epoch - if let Some(epoch) = self.resolve(mc_seq_no) { - return Ok(epoch); - } - - let epoch_index = mc_seq_no / self.epoch_size; - let start = epoch_index * self.epoch_size; - let end = start + self.epoch_size - 1; - - let epoch_dir = self.new_epochs_path.join(format!("epoch_{}", epoch_index)); - tokio::fs::create_dir_all(&epoch_dir) - .await - .map_err(|e| error!("Cannot create epoch directory {}: {}", epoch_dir.display(), e))?; - - let meta = EpochMeta { mc_seq_no_start: start, mc_seq_no_end: end }; - write_epoch_meta(&epoch_dir, &meta).await?; - - let db = RocksDb::new(&epoch_dir, "archive_db", None, AccessType::ReadWrite)?; - - log::info!( - target: TARGET, - "Created new epoch {}: mc_seq_no [{}, {}], path: {}", - epoch_index, start, end, epoch_dir.display() - ); - - let epoch = Arc::new(Epoch { - mc_seq_no_start: start, - mc_seq_no_end: end, - path: Arc::new(epoch_dir), - db, - }); - self.epochs.insert(start, Arc::clone(&epoch)); - - Ok(epoch) - } - - pub fn epoch_size(&self) -> u32 { - self.epoch_size - } - - fn validate_epoch_meta(meta: &EpochMeta, epoch_size: u32, path: &Path) -> Result<()> { - if meta.mc_seq_no_start % epoch_size != 0 { - fail!( - "Epoch at {} has mc_seq_no_start={} which is not aligned to epoch_size={}", - path.display(), - meta.mc_seq_no_start, - epoch_size - ); - } - let expected_end = meta.mc_seq_no_start + epoch_size - 1; - if meta.mc_seq_no_end != expected_end { - fail!( - "Epoch at {} has mc_seq_no_end={} but expected {} for epoch_size={}", - path.display(), - meta.mc_seq_no_end, - expected_end, - epoch_size - ); - } - Ok(()) - } -} - -#[cfg(test)] -#[path = "../tests/test_epoch.rs"] -mod tests; diff --git a/src/node/storage/src/archives/file_maps.rs b/src/node/storage/src/archives/file_maps.rs index 0a0d4ad..fd66f4f 100644 --- a/src/node/storage/src/archives/file_maps.rs +++ b/src/node/storage/src/archives/file_maps.rs @@ -14,7 +14,6 @@ use crate::StorageTelemetry; use crate::{ archives::{ archive_slice::ArchiveSlice, - db_provider::{ArchiveDbProvider, SingleDbProvider}, package_id::{PackageId, PackageType}, package_index_db::{PackageIndexDb, PackageIndexEntry}, }, @@ -36,9 +35,6 @@ use std::{ }; use ton_block::{error, fail, BlockIdExt, Result, ShardIdent, LT_ALIGN}; -pub const FILES_DB_NAME: &str = "files"; -pub const KEY_FILES_DB_NAME: &str = "key_files"; - #[derive(serde::Serialize, serde::Deserialize)] pub struct BlockRanges { pub min_seqno: AtomicU32, @@ -75,6 +71,17 @@ impl Clone for BlockRanges { } } impl BlockRanges { + pub fn new(handle: &BlockHandle) -> Self { + Self { + min_seqno: AtomicU32::new(handle.id().seq_no()), + max_seqno: AtomicU32::new(handle.id().seq_no()), + min_utime: AtomicU32::new(handle.gen_utime()), + max_utime: AtomicU32::new(handle.gen_utime()), + min_lt: AtomicU64::new(handle.end_lt()), + max_lt: AtomicU64::new(handle.end_lt()), + } + } + pub fn compare_seqno(&self, seqno: &u32) -> std::cmp::Ordering { let min_sn = self.min_seqno.load(Ordering::Relaxed); let max_sn = self.max_seqno.load(Ordering::Relaxed); @@ -143,21 +150,6 @@ impl FileDescription { } pub fn update_block_ranges(&self, handle: &BlockHandle) -> bool { - self.update_block_ranges_raw( - handle.id().shard(), - handle.id().seq_no(), - handle.gen_utime(), - handle.end_lt(), - ) - } - - pub fn update_block_ranges_raw( - &self, - shard: &ShardIdent, - seq_no: u32, - gen_utime: u32, - end_lt: u64, - ) -> bool { macro_rules! update_atomic { ($atomic:expr, $new:expr, $cmp_fn:expr) => {{ let mut prev = $atomic.load(Ordering::Relaxed); @@ -189,27 +181,26 @@ impl FileDescription { } let mut updated = false; - let _ = add_unbound_object_to_map_with_update(&self.blocks_ranges, shard.clone(), |prev| { - if let Some(prev) = prev { - updated |= update_min_32(&prev.min_seqno, seq_no); - updated |= update_max_32(&prev.max_seqno, seq_no); - updated |= update_min_32(&prev.min_utime, gen_utime); - updated |= update_max_32(&prev.max_utime, gen_utime); - updated |= update_min_64(&prev.min_lt, end_lt - end_lt % LT_ALIGN); - updated |= update_max_64(&prev.max_lt, end_lt); - Ok(None) - } else { - updated = true; - Ok(Some(BlockRanges { - min_seqno: AtomicU32::new(seq_no), - max_seqno: AtomicU32::new(seq_no), - min_utime: AtomicU32::new(gen_utime), - max_utime: AtomicU32::new(gen_utime), - min_lt: AtomicU64::new(end_lt - end_lt % LT_ALIGN), - max_lt: AtomicU64::new(end_lt), - })) - } - }); + let _ = add_unbound_object_to_map_with_update( + &self.blocks_ranges, + handle.id().shard().clone(), + |prev| { + if let Some(prev) = prev { + let sn = handle.id().seq_no(); + updated |= update_min_32(&prev.min_seqno, sn); + updated |= update_max_32(&prev.max_seqno, sn); + let ut = handle.gen_utime(); + updated |= update_min_32(&prev.min_utime, ut); + updated |= update_max_32(&prev.max_utime, ut); + let lt = handle.end_lt(); + updated |= update_min_64(&prev.min_lt, lt - lt % LT_ALIGN); + updated |= update_max_64(&prev.max_lt, lt); + Ok(None) + } else { + Ok(Some(BlockRanges::new(handle))) + } + }, + ); updated } @@ -250,15 +241,15 @@ pub struct FileMap { impl FileMap { pub async fn new( - index_db: Arc, - db_provider: &Arc, + db: Arc, + db_root_path: &Arc, path: impl ToString, package_type: PackageType, last_unneeded_key_block: u32, #[cfg(feature = "telemetry")] telemetry: &Arc, allocated: &Arc, ) -> Result { - let storage = PackageIndexDb::with_db(index_db, path, true)?; + let storage = PackageIndexDb::with_db(db.clone(), path, true)?; let mut index_pairs = Vec::new(); storage.for_each_deserialized(|key, value| { @@ -267,21 +258,19 @@ impl FileMap { })?; index_pairs.sort_by_key(|pair| pair.0); - let last = index_pairs.last().map(|pair| pair.0); let mut elements = Vec::new(); for (key, value) in index_pairs { let unneeded = key < last_unneeded_key_block; - let finalized = value.finalized() && Some(key) != last; + let finalized = value.finalized(); log::info!( target: TARGET, "Opening archive slice {}, finalized {}, unneeded {}", key, finalized, unneeded ); - let (slice_db, slice_root_path) = db_provider.db_for_archive(key).await?; let archive_slice = match ArchiveSlice::with_data( - slice_db, - slice_root_path, + db.clone(), + db_root_path.clone(), key, package_type, finalized, @@ -520,18 +509,15 @@ impl FileMaps { pub async fn new( db: Arc, db_root_path: &Arc, - db_provider: &Arc, last_unneeded_key_block: u32, #[cfg(feature = "telemetry")] telemetry: &Arc, allocated: &Arc, ) -> Result { - let key_db_provider: Arc = - Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())); Ok(Self { files: FileMap::new( db.clone(), - db_provider, - FILES_DB_NAME, + db_root_path, + "files", PackageType::Blocks, last_unneeded_key_block, #[cfg(feature = "telemetry")] @@ -541,15 +527,15 @@ impl FileMaps { .await?, key_files: FileMap::new( db.clone(), - &key_db_provider, - KEY_FILES_DB_NAME, + db_root_path, + "key_files", PackageType::KeyBlocks, 0, #[cfg(feature = "telemetry")] telemetry, allocated, ) - .await?, + .await?, // temp_files: FileMap::new(db_root_path, path.join("temp_files"), PackageType::Temp).await?, }) } diff --git a/src/node/storage/src/archives/mod.rs b/src/node/storage/src/archives/mod.rs index 89d96d2..20420b4 100644 --- a/src/node/storage/src/archives/mod.rs +++ b/src/node/storage/src/archives/mod.rs @@ -13,8 +13,6 @@ use crate::block_handle_db::BlockHandle; mod package_index_db; pub mod archive_manager; -pub mod db_provider; -pub mod epoch; pub mod package; pub mod package_entry; pub mod package_entry_id; @@ -23,7 +21,7 @@ mod archive_slice; mod block_index_db; mod file_maps; mod package_entry_meta_db; -pub mod package_id; +mod package_id; mod package_info; mod package_offsets_db; mod package_status_db; diff --git a/src/node/storage/src/archives/package.rs b/src/node/storage/src/archives/package.rs index 97d523b..48eceda 100644 --- a/src/node/storage/src/archives/package.rs +++ b/src/node/storage/src/archives/package.rs @@ -45,36 +45,27 @@ async fn read_header(reader: &mut R) -> Resu impl Package { pub async fn open(path: PathBuf, read_only: bool, create: bool) -> Result { - let (file, size) = if read_only { - let size = tokio::fs::metadata(&path).await?.len(); - if size < PKG_HEADER_SIZE as u64 { + let mut file = Self::open_file_ext(read_only, create, path.as_path()).await?; + let mut size = file.metadata().await?.len(); + + file.seek(SeekFrom::Start(0)).await?; + if size < PKG_HEADER_SIZE as u64 { + if !create { fail!("Package file is too short") } - (None, size) + file.write_all(&PKG_HEADER_MAGIC.to_le_bytes()).await?; + file.flush().await?; + size = PKG_HEADER_SIZE as u64; } else { - let mut file = Self::open_file_ext(read_only, create, path.as_path()).await?; - let mut size = file.metadata().await?.len(); - - file.seek(SeekFrom::Start(0)).await?; - if size < PKG_HEADER_SIZE as u64 { - if !create { - fail!("Package file is too short") - } - file.write_all(&PKG_HEADER_MAGIC.to_le_bytes()).await?; - file.flush().await?; - size = PKG_HEADER_SIZE as u64; - } else { - read_header(&mut file).await?; - file.seek(SeekFrom::End(0)).await?; - } - (Some(file), size) - }; + read_header(&mut file).await?; + file.seek(SeekFrom::End(0)).await?; + } Ok(Self { path, read_only, size: AtomicU64::new(size), - write_mutex: tokio::sync::Mutex::new(file), + write_mutex: tokio::sync::Mutex::new(Some(file)), }) } @@ -111,21 +102,22 @@ impl Package { pub async fn truncate(&self, size: u64) -> Result<()> { let new_size = PKG_HEADER_SIZE as u64 + size; + // let md = tokio::fs::metadata(self.path()).await?; + // if md.len() == new_size { + // return Ok(()) + // } + log::debug!( + target: TARGET, + "Truncating package {}, new size: {new_size} bytes", + self.path.display() + ); + self.size.store(new_size, Ordering::SeqCst); let Some(file) = &*self.write_mutex.lock().await else { fail!( "Cannot truncate package file {}, because it was not opened", self.path().display() ) }; - let old_raw = self.size.load(Ordering::SeqCst); - let old_file_len = file.metadata().await?.len(); - log::warn!( - target: TARGET, - "Truncating package {}: raw_size {old_raw} -> {new_size}, \ - file_len {old_file_len} -> {new_size}", - self.path.display() - ); - self.size.store(new_size, Ordering::SeqCst); file.set_len(new_size).await?; Ok(()) } @@ -172,34 +164,23 @@ impl Package { self.path().display() ) }; - let actual_before = file.metadata().await?.len(); - let raw_size = self.size.load(Ordering::SeqCst); - let entry_offset = raw_size - PKG_HEADER_SIZE as u64; - if raw_size != actual_before { + let actual = file.metadata().await?.len(); + let entry_offset = self.size(); + if entry_offset + PKG_HEADER_SIZE as u64 != actual { log::error!( target: TARGET, - "Package {} entry {} offset mismatch BEFORE write: \ - raw_size={raw_size}, file_len={actual_before}, \ - diff={}, entry_data_len={}, entry_filename={}", - self.path.display(), - entry.filename(), - actual_before as i64 - raw_size as i64, - entry.data().len(), - entry.filename(), + "Package entry {} offset mismatch: expected {entry_offset} vs {actual}", + entry.filename() ) } let entry_size = entry.write_to(file).await?; let total_size = self.size.fetch_add(entry_size, Ordering::SeqCst) + entry_size; - let actual_after = file.metadata().await?.len(); - if total_size != actual_after { + let actual = file.metadata().await?.len(); + if total_size != actual { log::error!( target: TARGET, - "Package {} entry {} size mismatch AFTER write: \ - expected_total={total_size}, file_len={actual_after}, \ - diff={}, entry_size={entry_size}, raw_size_before={raw_size}", - self.path.display(), - entry.filename(), - actual_after as i64 - total_size as i64, + "Package entry {} size mismatch: expected {total_size} vs {actual}", + entry.filename() ) } after_append(entry_offset, entry_offset + entry_size) diff --git a/src/node/storage/src/archives/package_entry.rs b/src/node/storage/src/archives/package_entry.rs index 1cb705e..2fcfdf8 100644 --- a/src/node/storage/src/archives/package_entry.rs +++ b/src/node/storage/src/archives/package_entry.rs @@ -109,11 +109,4 @@ impl PackageEntry { pub fn take_data(self) -> Vec { self.data } - - /// Returns the serialized size of this entry (header + filename + data). - pub fn serialized_size(&self) -> u64 { - PKG_ENTRY_HEADER_SIZE as u64 - + self.filename.as_bytes().len() as u64 - + self.data.len() as u64 - } } diff --git a/src/node/storage/src/archives/package_id.rs b/src/node/storage/src/archives/package_id.rs index 7d59352..21e2988 100644 --- a/src/node/storage/src/archives/package_id.rs +++ b/src/node/storage/src/archives/package_id.rs @@ -13,13 +13,13 @@ use std::path::{Path, PathBuf}; use ton_block::{fail, Result, ShardIdent}; #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum PackageType { +pub(crate) enum PackageType { Blocks, KeyBlocks, //Temp } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct PackageId { +pub(crate) struct PackageId { id: u32, package_type: PackageType, } diff --git a/src/node/storage/src/block_handle_db.rs b/src/node/storage/src/block_handle_db.rs index f0f6037..ffce6cd 100644 --- a/src/node/storage/src/block_handle_db.rs +++ b/src/node/storage/src/block_handle_db.rs @@ -29,27 +29,25 @@ use ton_block::{error, fail, BlockIdExt, Result, ShardIdent, UInt256}; #[path = "tests/test_block_handle_db.rs"] mod tests; -pub(crate) const FLAG_DATA: u32 = 0x00000001; -pub(crate) const FLAG_PROOF: u32 = 0x00000002; -pub(crate) const FLAG_PROOF_LINK: u32 = 0x00000004; +const FLAG_DATA: u32 = 0x00000001; +const FLAG_PROOF: u32 = 0x00000002; +const FLAG_PROOF_LINK: u32 = 0x00000004; //const FLAG_EXT_DB: u32 = 0x00000008; -pub(crate) const FLAG_STATE: u32 = 0x00000010; +const FLAG_STATE: u32 = 0x00000010; const FLAG_PERSISTENT_STATE: u32 = 0x00000020; const FLAG_NEXT_1: u32 = 0x00000040; const FLAG_NEXT_2: u32 = 0x00000080; -pub(crate) const FLAG_PREV_1: u32 = 0x00000100; -pub(crate) const FLAG_PREV_2: u32 = 0x00000200; -pub(crate) const FLAG_APPLIED: u32 = 0x00000400; +const FLAG_PREV_1: u32 = 0x00000100; +const FLAG_PREV_2: u32 = 0x00000200; +const FLAG_APPLIED: u32 = 0x00000400; pub(crate) const FLAG_KEY_BLOCK: u32 = 0x00000800; -pub(crate) const FLAG_MOVED_TO_ARCHIVE: u32 = 0x00002000; -pub(crate) const FLAG_STATE_SAVED: u32 = 0x00010000; +const FLAG_MOVED_TO_ARCHIVE: u32 = 0x00002000; +const FLAG_STATE_SAVED: u32 = 0x00010000; const FLAG_HAS_FULL_ID: u32 = 0x00020000; // not serializing flags (possible flags - 1, 2, 4, 8) const FLAG_ARCHIVING: u32 = 0x80000000; -pub const VALIDATOR_STATE_DB_NAME: &str = "validator_state_db"; - db_impl_base!(NodeStateDb, &'static str); /// Meta information related to block @@ -438,8 +436,6 @@ impl Drop for BlockHandle { // Real value is // - BlockMeta if FLAG_HAS_FULL_ID is not set // - BlockMeta + wc (i32) + shard (u64) + seqno (u32) + file_hash (UInt256) if FLAG_HAS_FULL_ID is set -pub const BLOCK_HANDLE_DB_NAME: &str = "block_handle_db"; - db_impl_base!(BlockHandleDb, BlockIdExt); declare_counted!( @@ -468,7 +464,6 @@ pub trait Callback: Sync + Send { pub struct BlockHandleStorage { handle_db: Arc, handle_cache: Arc, - no_cache: bool, full_node_state_db: Arc, validator_state_db: Arc, state_cache: lockfree::map::Map>, @@ -490,7 +485,6 @@ impl BlockHandleStorage { let ret = Self { handle_db: handle_db.clone(), handle_cache: Arc::new(lockfree::map::Map::new()), - no_cache: false, full_node_state_db: full_node_state_db.clone(), validator_state_db: validator_state_db.clone(), state_cache: lockfree::map::Map::new(), @@ -584,10 +578,6 @@ impl BlockHandleStorage { ret } - pub fn set_no_cache(&mut self) { - self.no_cache = true; - } - pub fn create_handle( &self, id: BlockIdExt, @@ -623,13 +613,10 @@ impl BlockHandleStorage { pub fn load_full_block_id(&self, root_hash: &UInt256) -> Result> { log::trace!(target: TARGET, "load_full_block_id {:x}", root_hash); - if !self.no_cache { - let weak = self.handle_cache.get(root_hash); - if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { - return Ok(Some(handle.id.clone())); - } - } - if let Some(data) = self.handle_db.try_get_raw(root_hash.as_slice())? { + let weak = self.handle_cache.get(root_hash); + if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { + Ok(Some(handle.id.clone())) + } else if let Some(data) = self.handle_db.try_get_raw(root_hash.as_slice())? { Ok(BlockHandle::deserialize_full_id(root_hash, &data)?) } else { Ok(None) @@ -648,18 +635,11 @@ impl BlockHandleStorage { self.load_state(key, &self.validator_state_db) } - pub fn load_validator_state_raw(&self, key: &str) -> Result>> { - Ok(self.validator_state_db.try_get_raw(key.as_bytes())?.map(|value| value.to_vec())) - } - pub fn save_handle( &self, handle: &Arc, - callback: Option>, // not invoked in no-cache mode + callback: Option>, ) -> Result<()> { - if self.no_cache { - return self.handle_db.put_raw(handle.id().root_hash().as_slice(), &handle.serialize()); - } self.storer .send((StoreJob::SaveHandle(handle.clone()), callback)) .map_err(|_| error!("Cannot store handle {}: storer thread dropped", handle.id())) @@ -679,16 +659,6 @@ impl BlockHandleStorage { .map_err(|_| error!("Cannot store validator state {}: storer thread dropped", id)) } - pub fn save_validator_state_raw(&self, key: &str, data: &[u8]) -> Result<()> { - self.delete_state(key)?; - self.validator_state_db.put_raw(key.as_bytes(), data) - } - - pub fn drop_validator_state_raw(&self, key: &str) -> Result<()> { - self.delete_state(key)?; - self.validator_state_db.delete_raw(key.as_bytes()) - } - pub fn drop_handle(&self, id: BlockIdExt, callback: Option>) -> Result<()> { let _ = self.handle_cache.remove(id.root_hash()); self.storer @@ -721,35 +691,23 @@ impl BlockHandleStorage { ) -> Result>> { let rh = id.root_hash().clone(); let ret = Arc::new(BlockHandle::with_values(id, meta, self.handle_cache.clone())); - let ret = if self.no_cache { - if self.handle_db.try_get_raw(rh.as_slice())?.is_some() { - None - } else { - if store { - self.save_handle(&ret, callback)? - } - Some(ret) + let added = add_counted_object_to_map(&self.handle_cache, rh, || { + let ret = HandleObject { + object: Arc::downgrade(&ret), + counter: self.allocated.handles.clone().into(), + }; + #[cfg(feature = "telemetry")] + self.telemetry.handles.update(self.allocated.handles.load(Ordering::Relaxed)); + Ok(ret) + })?; + if added { + if store { + self.save_handle(&ret, callback)? } + Ok(Some(ret)) } else { - let added = add_counted_object_to_map(&self.handle_cache, rh, || { - let ret = HandleObject { - object: Arc::downgrade(&ret), - counter: self.allocated.handles.clone().into(), - }; - #[cfg(feature = "telemetry")] - self.telemetry.handles.update(self.allocated.handles.load(Ordering::Relaxed)); - Ok(ret) - })?; - if added { - if store { - self.save_handle(&ret, callback)? - } - Some(ret) - } else { - None - } - }; - Ok(ret) + Ok(None) + } } fn create_state(&self, key: String, id: &BlockIdExt) -> Result> { @@ -773,7 +731,11 @@ impl BlockHandleStorage { } else { log::trace!(target: TARGET, "load block handle by id {id}") } - let ret = if self.no_cache { + let ret = loop { + let weak = self.handle_cache.get(id.root_hash()); + if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { + break Some(handle); + } if let Some(data) = self.handle_db.try_get_raw(id.root_hash().as_slice())? { let meta = if rh_only { BlockHandle::deserialize_nonchecked(&mut id, &data)? @@ -782,31 +744,12 @@ impl BlockHandleStorage { meta.set_flags(FLAG_HAS_FULL_ID); meta }; - Some(Arc::new(BlockHandle::with_values(id, meta, self.handle_cache.clone()))) - } else { - None - } - } else { - loop { - let weak = self.handle_cache.get(id.root_hash()); - if let Some(Some(handle)) = weak.map(|weak| weak.val().object.upgrade()) { + let handle = self.create_handle_and_store(id.clone(), meta, None, false)?; + if let Some(handle) = handle { break Some(handle); } - if let Some(data) = self.handle_db.try_get_raw(id.root_hash().as_slice())? { - let meta = if rh_only { - BlockHandle::deserialize_nonchecked(&mut id, &data)? - } else { - let meta = BlockHandle::deserialize(&id, &data)?; - meta.set_flags(FLAG_HAS_FULL_ID); - meta - }; - let handle = self.create_handle_and_store(id.clone(), meta, None, false)?; - if let Some(handle) = handle { - break Some(handle); - } - } else { - break None; - } + } else { + break None; } }; Ok(ret) diff --git a/src/node/storage/src/block_info_db.rs b/src/node/storage/src/block_info_db.rs index de5f575..376c68d 100644 --- a/src/node/storage/src/block_info_db.rs +++ b/src/node/storage/src/block_info_db.rs @@ -11,9 +11,4 @@ use crate::db_impl_base; use ton_block::BlockIdExt; -pub const PREV1_BLOCK_DB_NAME: &str = "prev1_block_db"; -pub const PREV2_BLOCK_DB_NAME: &str = "prev2_block_db"; -pub const NEXT1_BLOCK_DB_NAME: &str = "next1_block_db"; -pub const NEXT2_BLOCK_DB_NAME: &str = "next2_block_db"; - db_impl_base!(BlockInfoDb, BlockIdExt); diff --git a/src/node/storage/src/cell_db.rs b/src/node/storage/src/cell_db.rs deleted file mode 100644 index 1ad3c4f..0000000 --- a/src/node/storage/src/cell_db.rs +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -#[cfg(feature = "telemetry")] -use crate::StorageTelemetry; -use crate::{ - db::rocksdb::RocksDb, - shardstate_db_async::CellsDbConfig, - types::{StoredCell, StoringCell}, - StorageAlloc, TARGET, -}; -#[cfg(feature = "telemetry")] -use std::sync::atomic::{AtomicU64, Ordering}; -use std::{ - fs::write, - io::Write, - ops::Deref, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use ton_block::{ - error, fail, merkle_update::CellsFactory, BuilderData, Cell, CellsStorage, Result, UInt256, -}; - -pub const BROKEN_CELL_BEACON_FILE: &str = "ton_node.broken_cell"; - -pub struct CellDb { - db: Arc, - cells_cf_name: String, - db_root_path: PathBuf, - storing_cells: Arc>, - #[cfg(feature = "telemetry")] - storing_cells_count: AtomicU64, - cell_cache: quick_cache::sync::Cache, - #[cfg(feature = "telemetry")] - telemetry: Arc, - allocated: Arc, -} - -impl CellDb { - pub fn with_db( - db: Arc, - cell_db_cf: &str, - db_root_path: impl AsRef, - config: &CellsDbConfig, - #[cfg(feature = "telemetry")] telemetry: Arc, - allocated: Arc, - ) -> Result { - if db.cf_handle(cell_db_cf).is_none() { - db.create_cf(cell_db_cf, &Self::build_cf_options(config.cells_cache_size_bytes))?; - } - Ok(Self { - db, - cells_cf_name: cell_db_cf.to_string(), - db_root_path: db_root_path.as_ref().to_path_buf(), - storing_cells: Arc::new(lockfree::map::Map::new()), - #[cfg(feature = "telemetry")] - storing_cells_count: AtomicU64::new(0), - cell_cache: quick_cache::sync::Cache::new(config.cells_lru_cache_capacity), - #[cfg(feature = "telemetry")] - telemetry, - allocated, - }) - } - - pub fn build_cf_options(cache_size: u64) -> rocksdb::Options { - let mut options = rocksdb::Options::default(); - let mut block_opts = rocksdb::BlockBasedOptions::default(); - - // specified cache for blocks. - let cache = rocksdb::Cache::new_lru_cache(cache_size as usize); - block_opts.set_block_cache(&cache); - - // save in LRU block cache also indexes and bloom filters - block_opts.set_cache_index_and_filter_blocks(true); - - // keep indexes and filters in block cache until tablereader freed - block_opts.set_pin_l0_filter_and_index_blocks_in_cache(true); - - // Setup bloom filter with length of 10 bits per key. - // This length provides less than 1% false positive rate. - block_opts.set_bloom_filter(10.0, false); - - options.set_block_based_table_factory(&block_opts); - - // Enable whole key bloom filter in memtable. - options.set_memtable_whole_key_filtering(true); - - // Amount of data to build up in memory (backed by an unsorted log - // on disk) before converting to a sorted on-disk file. - // - // Larger values increase performance, especially during bulk loads. - // Up to max_write_buffer_number write buffers may be held in memory - // at the same time, - // so you may wish to adjust this parameter to control memory usage. - // Also, a larger write buffer will result in a longer recovery time - // the next time the database is opened. - options.set_write_buffer_size(1024 * 1024 * 1024); - - // The maximum number of write buffers that are built up in memory. - // The default and the minimum number is 2, so that when 1 write buffer - // is being flushed to storage, new writes can continue to the other - // write buffer. - // If max_write_buffer_number > 3, writing will be slowed down to - // options.delayed_write_rate if we are writing to the last write buffer - // allowed. - options.set_max_write_buffer_number(4); - - // if prefix_extractor is set and memtable_prefix_bloom_size_ratio is not 0, - // create prefix bloom for memtable with the size of - // write_buffer_size * memtable_prefix_bloom_size_ratio. - // If it is larger than 0.25, it is sanitized to 0.25. - let transform = rocksdb::SliceTransform::create_fixed_prefix(32); - options.set_prefix_extractor(transform); - options.set_memtable_prefix_bloom_ratio(0.1); - - options - } - - pub fn db(&self) -> &Arc { - &self.db - } - - pub fn allocated(&self) -> &StorageAlloc { - &self.allocated - } - - pub fn cells_cf(&self) -> Result>> { - self.db - .cf_handle(&self.cells_cf_name) - .ok_or_else(|| error!("Can't get `{}` cf handle", self.cells_cf_name)) - } - - pub fn storing_cells(&self) -> &Arc> { - &self.storing_cells - } - - #[cfg(feature = "telemetry")] - pub fn telemetry(&self) -> &Arc { - &self.telemetry - } - - /// If root cell already exists in DB, load and return it. Otherwise return None. - pub fn try_load_existing_root( - self: &Arc, - root_id: &UInt256, - cells_cf: &impl rocksdb::AsColumnFamilyRef, - ) -> Result> { - #[cfg(feature = "telemetry")] - let now = std::time::Instant::now(); - if let Some(val) = self.db.get_pinned_cf(cells_cf, root_id.as_slice())? { - let cell = StoredCell::deserialize(self, root_id, &val)?; - #[cfg(feature = "telemetry")] - { - self.telemetry - .stored_cells - .update(self.allocated.storage_cells.load(Ordering::Relaxed)); - self.telemetry.loaded_cells_from_db.update(1); - self.telemetry.load_cell_from_db_time_nanos.update(now.elapsed().as_nanos() as u64); - } - Ok(Some(Cell::with_cell_impl(cell))) - } else { - Ok(None) - } - } - - /// Remove saved cell hashes from the storing_cells in-memory cache. - pub fn cleanup_storing_cells<'a>(&self, saved_ids: impl Iterator) { - for id in saved_ids { - let mut stack = vec![id.clone()]; - while let Some(id) = stack.pop() { - if let Some(removed) = self.storing_cells.remove(&id) { - log::trace!( - target: TARGET, - "CellDb::cleanup_storing_cells {:x} removed from storing_cells", id - ); - #[cfg(feature = "telemetry")] - { - let _count = self.storing_cells_count.fetch_sub(1, Ordering::Relaxed); - self.telemetry.storing_cells.update(_count - 1); - } - - for i in 0..removed.val().references_count() { - if let Ok(ref_hash) = removed.val().reference_repr_hash(i) { - stack.push(ref_hash); - } - } - } - } - } - } - - #[cfg(test)] - pub fn count(&self) -> usize { - if let Ok(cf) = self.cells_cf() { - self.db.iterator_cf(&cf, rocksdb::IteratorMode::Start).count() - } else { - 0 - } - } - - pub(crate) fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { - #[cfg(feature = "telemetry")] - let now = std::time::Instant::now(); - if let Some(cell) = self.cell_cache.get(cell_id) { - #[cfg(feature = "telemetry")] - { - self.telemetry.cell_cache_hits.update(1); - self.telemetry - .load_cell_from_cache_time_nanos - .update(now.elapsed().as_nanos() as u64); - } - return Ok(cell); - } - #[cfg(feature = "telemetry")] - self.telemetry.cell_cache_misses.update(1); - let cell = self.load_cell_uncached(cell_id, panic)?; - #[cfg(feature = "telemetry")] - let now_insert = std::time::Instant::now(); - self.cell_cache.insert(cell_id.clone(), cell.clone()); - #[cfg(feature = "telemetry")] - { - self.telemetry - .store_cell_to_cache_time_nanos - .update(now_insert.elapsed().as_nanos() as u64); - self.telemetry.cell_cache_len.update(self.cell_cache.len() as u64); - } - Ok(cell) - } - - fn load_cell_uncached(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { - #[cfg(feature = "telemetry")] - let now = std::time::Instant::now(); - let storage_cell_data = match self.db.get_pinned_cf(&self.cells_cf()?, cell_id.as_slice()) { - Ok(Some(data)) => data, - _ => { - if let Some(guard) = self.storing_cells.get(cell_id) { - log::trace!( - target: TARGET, - "CellDb::load_cell from storing_cells by id {cell_id:x}", - ); - return Ok(guard.val().clone()); - } - - if !panic { - fail!("Can't load cell {:x} from db", cell_id); - } - - log::error!("FATAL!"); - log::error!("FATAL! Can't load cell {:x} from db", cell_id); - log::error!("FATAL!"); - - let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); - write(path, "")?; - - std::thread::sleep(Duration::from_millis(100)); - std::process::exit(0xFF); - } - }; - - #[cfg(feature = "telemetry")] - let load_cell_from_db_time_nanos = now.elapsed().as_nanos() as u64; - - let storage_cell = match StoredCell::deserialize(self, cell_id, &storage_cell_data) { - Ok(cell) => Arc::new(cell), - Err(e) => { - if !panic { - fail!("Can't deserialize cell {:x} from db, error: {:?}", cell_id, e); - } - - log::error!("FATAL!"); - log::error!( - "FATAL! Can't deserialize cell {:x} from db, data: {}, error: {:?}", - cell_id, - hex::encode(&storage_cell_data), - e - ); - log::error!("FATAL!"); - - let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); - write(path, "")?; - - std::thread::sleep(Duration::from_millis(100)); - std::process::exit(0xFF); - } - }; - - #[cfg(feature = "telemetry")] - { - self.telemetry - .stored_cells - .update(self.allocated.storage_cells.load(Ordering::Relaxed)); - self.telemetry.load_cell_from_db_time_nanos.update(load_cell_from_db_time_nanos); - self.telemetry.loaded_cells_from_db.update(1); - } - - log::trace!( - target: TARGET, - "CellDb::load_cell from DB id {cell_id:x}" - ); - - Ok(Cell::with_cell_impl_arc(storage_cell)) - } -} - -impl CellsFactory for CellDb { - fn create_cell(self: Arc, builder: BuilderData) -> Result { - let cell = StoringCell::with_cell(&*builder.into_cell()?, &self)?; - let cell = Cell::with_cell_impl(cell); - let repr_hash = cell.repr_hash(); - - let mut result_cell = None; - - let result = self.storing_cells.insert_with(repr_hash, |_, inserted, found| { - if let Some((_, found)) = found { - result_cell = Some(found.clone()); - lockfree::map::Preview::Discard - } else if let Some(inserted) = inserted { - result_cell = Some(inserted.clone()); - lockfree::map::Preview::Keep - } else { - result_cell = Some(cell.clone()); - lockfree::map::Preview::New(cell.clone()) - } - }); - - let result_cell = result_cell - .ok_or_else(|| error!("INTERNAL ERROR: result_cell {:x} is None", cell.repr_hash()))?; - - match result { - lockfree::map::Insertion::Created => { - log::trace!(target: TARGET, "CellDb::create_cell {:x} - created new", cell.repr_hash()); - #[cfg(feature = "telemetry")] - { - let storing_cells_count = - self.storing_cells_count.fetch_add(1, Ordering::Relaxed); - self.telemetry.storing_cells.update(storing_cells_count + 1); - } - } - lockfree::map::Insertion::Failed(_) => { - log::trace!(target: TARGET, "CellDb::create_cell {:x} - already exists", cell.repr_hash()); - } - lockfree::map::Insertion::Updated(old) => { - fail!( - "INTERNAL ERROR: storing_cells.insert_with {:x} returned Updated({:?})", - cell.repr_hash(), - old - ) - } - } - - Ok(result_cell) - } -} - -// This wrapper-struct is added because it is impossible -// to implement foreign trait (CellByHashStorage) for foreign type (Arc) -pub struct CellByHashStorageAdapter { - db: Arc, - root_cells_data: ahash::HashMap>, -} - -impl CellByHashStorageAdapter { - pub fn new( - db: Arc, - root_cell: Option<&Cell>, - max_inmemory_cells: usize, - ) -> Result { - let mut root_cells_data = ahash::HashMap::default(); - if let Some(root_cell) = root_cell { - if db.load_cell(&root_cell.repr_hash(), false).is_err() { - let mut stack = vec![root_cell.clone()]; - while let Some(cell) = stack.pop() { - if root_cells_data.len() >= max_inmemory_cells { - fail!( - "Too many cells in boc to store in memory: {}, max_inmemory_cells: {}", - root_cells_data.len(), - max_inmemory_cells - ); - } - let cell_data = StoredCell::serialize(cell.cell_impl().deref())?; - let cell_hash = cell.repr_hash(); - root_cells_data.insert(cell_hash, cell_data); - - for i in 0..cell.references_count() { - if db.load_cell(&cell.reference_repr_hash(i)?, false).is_err() { - stack.push(cell.reference(i)?); - } - } - } - } - } - Ok(Self { db, root_cells_data }) - } -} - -impl CellsStorage for CellByHashStorageAdapter { - fn load_cell(&self, hash: &UInt256) -> Result { - if let Ok(c) = self.db.clone().load_cell_uncached(hash, false) { - Ok(c) - } else if let Some(data) = self.root_cells_data.get(hash) { - StoredCell::deserialize(&self.db, hash, data).map(Cell::with_cell_impl) - } else { - fail!("Can't load cell {:x} from db", hash); - } - } - - fn load_cell_data( - &self, - hash: &UInt256, - write_hashes: bool, - dest: &mut dyn Write, - ) -> Result<()> { - #[cfg(feature = "telemetry")] - let now = std::time::Instant::now(); - if let Ok(Some(data)) = self.db.db.get_pinned_cf(&self.db.cells_cf()?, hash.as_slice()) { - #[cfg(feature = "telemetry")] - { - self.db - .telemetry - .load_cell_from_db_time_nanos - .update(now.elapsed().as_nanos() as u64); - self.db.telemetry.loaded_cells_from_db.update(1); - } - - StoredCell::write_cell_data(&data, hash, write_hashes, dest) - } else if let Some(data) = self.root_cells_data.get(hash) { - StoredCell::write_cell_data(data, hash, write_hashes, dest) - } else { - fail!("Can't load cell {:x} from db", hash); - } - } -} diff --git a/src/node/storage/src/db/rocksdb.rs b/src/node/storage/src/db/rocksdb.rs index 5a487ca..41596ca 100644 --- a/src/node/storage/src/db/rocksdb.rs +++ b/src/node/storage/src/db/rocksdb.rs @@ -35,8 +35,6 @@ pub enum AccessType { pub const LAST_UNNEEDED_KEY_BLOCK: &str = "LastUnneededKeyBlockId"; // Latest key block we can delete in archives GC pub const NODE_STATE_DB_NAME: &str = "node_state_db"; -pub const NODE_DB_NAME: &str = "db"; -pub const CATCHAINS_DB_NAME: &str = "catchains"; pub type DbPredicateMut<'a> = &'a mut dyn FnMut(&[u8], &[u8]) -> Result; @@ -342,7 +340,7 @@ impl RocksDbTable { /// Returns true, if collection is empty; false otherwise pub fn is_empty(&self) -> Result { - Ok(self.db.iterator_cf(&self.cf()?, IteratorMode::Start).next().is_none()) + Ok(self.len()? == 0) } pub fn destroy(&mut self) -> Result { diff --git a/src/node/storage/src/dynamic_boc_archive_db.rs b/src/node/storage/src/dynamic_boc_archive_db.rs deleted file mode 100644 index 2588bc7..0000000 --- a/src/node/storage/src/dynamic_boc_archive_db.rs +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -#[cfg(feature = "telemetry")] -use crate::StorageTelemetry; -use crate::{ - cell_db::CellDb, db::rocksdb::RocksDb, shardstate_db_async::CellsDbConfig, types::StoredCell, - StorageAlloc, TARGET, -}; -use std::{ops::Deref, path::Path, sync::Arc}; -use ton_block::{Cell, Result, UInt256, MAX_LEVEL}; - -pub struct DynamicBocArchiveDb { - cell_db: Arc, -} - -impl DynamicBocArchiveDb { - pub fn with_db( - db: Arc, - cell_db_cf: &str, - db_root_path: impl AsRef, - config: &CellsDbConfig, - #[cfg(feature = "telemetry")] telemetry: Arc, - allocated: Arc, - ) -> Result { - let cell_db = Arc::new(CellDb::with_db( - db, - cell_db_cf, - db_root_path, - config, - #[cfg(feature = "telemetry")] - telemetry, - allocated, - )?); - Ok(Self { cell_db }) - } - - pub fn cell_db(&self) -> &Arc { - &self.cell_db - } - - /// Thread-safe append-only save. - pub fn save_boc( - &self, - root_cell: Cell, - check_stop: &(dyn Fn() -> Result<()> + Sync), - ) -> Result { - let root_id = root_cell.hash(MAX_LEVEL); - let cells_cf = self.cell_db.cells_cf()?; - - log::debug!(target: TARGET, "DynamicBocArchiveDb::save_boc {:x}", root_id); - - if let Some(existing) = self.cell_db.try_load_existing_root(&root_id, &cells_cf)? { - log::info!(target: TARGET, "DynamicBocArchiveDb::save_boc ALREADY EXISTS {:x}", root_id); - return Ok(existing); - } - - let start = std::time::Instant::now(); - - // Traverse cell tree, collect new cells - let mut new_cells = fnv::FnvHashMap::default(); - let mut visited = fnv::FnvHashSet::default(); - self.collect_new_cells(&root_cell, &mut new_cells, &mut visited, &cells_cf, check_stop)?; - let cells_traverse_time = start.elapsed().as_micros(); - - // Batch write all new cells - let wrote_cells = new_cells.len(); - let write_start = std::time::Instant::now(); - if !new_cells.is_empty() { - let mut batch = rocksdb::WriteBatch::default(); - for (id, data) in &new_cells { - batch.put_cf(&cells_cf, id.as_slice(), data); - } - self.cell_db.db().write(batch)?; - } - #[cfg(feature = "telemetry")] - if wrote_cells > 0 { - self.cell_db - .telemetry() - .boc_db_element_write_nanos - .update(write_start.elapsed().as_nanos() as u64 / wrote_cells as u64); - } - let write_time = write_start.elapsed().as_micros(); - - let now4 = std::time::Instant::now(); - self.cell_db.cleanup_storing_cells(new_cells.keys()); - let storing_cells_cleanup_time = now4.elapsed().as_micros(); - - let total_time = start.elapsed().as_micros() as u64; - #[cfg(feature = "telemetry")] - { - self.cell_db.telemetry().stored_new_cells.update(wrote_cells as u64); - self.cell_db.telemetry().save_boc_total_micros.update(total_time); - self.cell_db.telemetry().save_boc_traverse_micros.update(cells_traverse_time as u64); - self.cell_db.telemetry().save_boc_commit_micros.update(write_time as u64); - self.cell_db - .telemetry() - .save_boc_cleanup_micros - .update(storing_cells_cleanup_time as u64); - } - - log::debug!( - target: TARGET, - "DynamicBocArchiveDb::save_boc {:x} wrote {}, visited {} TIME: {} (tr:{}|cmt:{}|scc:{})", - root_id, wrote_cells, visited.len(), total_time, cells_traverse_time, write_time, - storing_cells_cleanup_time - ); - - self.cell_db.load_cell(&root_id, true) - } - - fn collect_new_cells( - &self, - cell: &Cell, - new_cells: &mut fnv::FnvHashMap>, - visited: &mut fnv::FnvHashSet, - cells_cf: &impl rocksdb::AsColumnFamilyRef, - check_stop: &(dyn Fn() -> Result<()> + Sync), - ) -> Result<()> { - check_stop()?; - let cell_id = cell.repr_hash(); - - // Already visited in this traversal (new or existing) โ€” skip - if !visited.insert(cell_id.clone()) { - return Ok(()); - } - - // Already a StoredCell (loaded from DB) - if cell.is::() { - return Ok(()); - } - - // Recurse into children first - for i in 0..cell.references_count() { - let reference = cell.reference(i)?; - self.collect_new_cells(&reference, new_cells, visited, cells_cf, check_stop)?; - } - - // Check if cell exists in DB - if self.cell_db.db().get_pinned_cf(cells_cf, cell_id.as_slice())?.is_some() { - return Ok(()); - } - - // Serialize and add to batch - let data = StoredCell::serialize(cell.deref())?; - new_cells.insert(cell_id, data); - Ok(()) - } - - /// Fast import-only save: writes all non-pruned cells from state update unconditionally, - /// without checking the DB. - pub fn save_update(&self, root_cell: Cell) -> Result<()> { - let root_id = root_cell.hash(MAX_LEVEL); - let cells_cf = self.cell_db.cells_cf()?; - - log::debug!(target: TARGET, "DynamicBocArchiveDb::save_update {:x}", root_id); - - let start = std::time::Instant::now(); - - let mut new_cells = fnv::FnvHashMap::default(); - Self::collect_cells_from_update(&root_cell, &mut new_cells)?; - let cells_traverse_time = start.elapsed().as_micros(); - - let wrote_cells = new_cells.len(); - let write_start = std::time::Instant::now(); - if !new_cells.is_empty() { - let mut batch = rocksdb::WriteBatch::default(); - for (id, data) in &new_cells { - batch.put_cf(&cells_cf, id.as_slice(), data); - } - self.cell_db.db().write(batch)?; - } - let write_time = write_start.elapsed().as_micros(); - - log::debug!( - target: TARGET, - "DynamicBocArchiveDb::save_update {:x} wrote {} TIME: {} (tr:{}|cmt:{})", - root_id, wrote_cells, start.elapsed().as_micros(), cells_traverse_time, write_time, - ); - - Ok(()) - } - - /// Collect all non-pruned cells from the tree. No DB lookups โ€” pruned branches - /// are the boundary (they represent unchanged subtrees already in the DB). - fn collect_cells_from_update( - cell: &Cell, - new_cells: &mut fnv::FnvHashMap>, - ) -> Result<()> { - let cell_id = cell.repr_hash(); - - if new_cells.contains_key(&cell_id) { - return Ok(()); - } - - // PrunedBranch = unchanged subtree, already in DB - if cell.is_pruned() && cell.level() == 0 { - return Ok(()); - } - - for i in 0..cell.references_count() { - let reference = cell.reference(i)?; - Self::collect_cells_from_update(&reference, new_cells)?; - } - - let data = StoredCell::serialize_virtual(cell.deref())?; - new_cells.insert(cell_id, data); - Ok(()) - } - - pub fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { - self.cell_db.load_cell(cell_id, panic) - } -} diff --git a/src/node/storage/src/dynamic_boc_rc_db.rs b/src/node/storage/src/dynamic_boc_rc_db.rs index 1dc497b..3b16d3a 100644 --- a/src/node/storage/src/dynamic_boc_rc_db.rs +++ b/src/node/storage/src/dynamic_boc_rc_db.rs @@ -11,15 +11,29 @@ #[cfg(feature = "telemetry")] use crate::StorageTelemetry; use crate::{ - cell_db::CellDb, db::rocksdb::RocksDb, shardstate_db_async::CellsDbConfig, types::StoredCell, + db::rocksdb::RocksDb, + shardstate_db_async::CellsDbConfig, + types::{StoredCell, StoringCell}, StorageAlloc, TARGET, }; -use std::{io::Cursor, ops::Deref, path::Path, sync::Arc, time::Instant}; +use std::{ + fs::write, + io::{Cursor, Write}, + ops::Deref, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; use ton_block::{ - error, fail, ByteOrderRead, Cell, CellData, CellsFactory, CellsTempStorage, Result, UInt256, - MAX_LEVEL, MAX_REFERENCES_COUNT, + error, fail, merkle_update::CellsFactory, BuilderData, ByteOrderRead, Cell, CellData, + CellsStorage, CellsTempStorage, Result, UInt256, MAX_LEVEL, MAX_REFERENCES_COUNT, }; +pub const BROKEN_CELL_BEACON_FILE: &str = "ton_node.broken_cell"; + // FnvHashMap is a standard HashMap with FNV hasher. This hasher is bit faster than default one. pub type CellsCounters = fnv::FnvHashMap; @@ -97,9 +111,16 @@ impl VisitedCell { } pub struct DynamicBocDb { - cell_db: Arc, + db: Arc, + cells_cf_name: String, counters_cf_name: String, + db_root_path: PathBuf, + storing_cells: Arc>, + storing_cells_count: AtomicU64, cells_counters: Option>>, + #[cfg(feature = "telemetry")] + telemetry: Arc, + allocated: Arc, } impl DynamicBocDb { @@ -112,17 +133,11 @@ impl DynamicBocDb { #[cfg(feature = "telemetry")] telemetry: Arc, allocated: Arc, ) -> Result { - let cell_db = CellDb::with_db( - db.clone(), - cell_db_cf, - db_root_path.as_ref(), - config, - #[cfg(feature = "telemetry")] - telemetry, - allocated, - )?; + if db.cf_handle(cell_db_cf).is_none() { + db.create_cf(cell_db_cf, &Self::build_cells_cf_options(config))?; + } if db.cf_handle(counters_cf_name).is_none() { - db.create_cf(counters_cf_name, &Self::build_counters_cf_options(config))?; + db.create_cf(counters_cf_name, &Self::build_cells_cf_options(config))?; } let cells_counters = if config.prefill_cells_counters { let counters = CellsCounters::default(); @@ -131,41 +146,85 @@ impl DynamicBocDb { None }; Ok(Self { - cell_db: Arc::new(cell_db), + db, + cells_cf_name: cell_db_cf.to_string(), counters_cf_name: counters_cf_name.to_string(), + db_root_path: db_root_path.as_ref().to_path_buf(), + storing_cells: Arc::new(lockfree::map::Map::new()), + storing_cells_count: AtomicU64::new(0), cells_counters, + #[cfg(feature = "telemetry")] + telemetry, + allocated, }) } - pub fn cell_db(&self) -> &Arc { - &self.cell_db - } - pub fn build_cells_cf_options(config: &CellsDbConfig) -> rocksdb::Options { - CellDb::build_cf_options(config.cells_cache_size_bytes) + Self::build_cf_options(config.cells_cache_size_bytes) } pub fn build_counters_cf_options(config: &CellsDbConfig) -> rocksdb::Options { - CellDb::build_cf_options(config.counters_cache_size_bytes) - } - - pub(crate) fn load_cell(&self, cell_id: &UInt256, panic: bool) -> Result { - self.cell_db.load_cell(cell_id, panic) + Self::build_cf_options(config.counters_cache_size_bytes) } - #[allow(dead_code)] - fn allocated(&self) -> &StorageAlloc { - self.cell_db.allocated() - } - - pub fn cells_factory(&self) -> Arc { - self.cell_db.clone() as Arc + fn build_cf_options(cache_size: u64) -> rocksdb::Options { + let mut options = rocksdb::Options::default(); + let mut block_opts = rocksdb::BlockBasedOptions::default(); + + // specified cache for blocks. + let cache = rocksdb::Cache::new_lru_cache(cache_size as usize); + block_opts.set_block_cache(&cache); + + // save in LRU block cache also indexes and bloom filters + block_opts.set_cache_index_and_filter_blocks(true); + + // keep indexes and filters in block cache until tablereader freed + block_opts.set_pin_l0_filter_and_index_blocks_in_cache(true); + + // Setup bloom filter with length of 10 bits per key. + // This length provides less than 1% false positive rate. + block_opts.set_bloom_filter(10.0, false); + + options.set_block_based_table_factory(&block_opts); + + // Enable whole key bloom filter in memtable. + options.set_memtable_whole_key_filtering(true); + + // Amount of data to build up in memory (backed by an unsorted log + // on disk) before converting to a sorted on-disk file. + // + // Larger values increase performance, especially during bulk loads. + // Up to max_write_buffer_number write buffers may be held in memory + // at the same time, + // so you may wish to adjust this parameter to control memory usage. + // Also, a larger write buffer will result in a longer recovery time + // the next time the database is opened. + options.set_write_buffer_size(1024 * 1024 * 1024); + + // The maximum number of write buffers that are built up in memory. + // The default and the minimum number is 2, so that when 1 write buffer + // is being flushed to storage, new writes can continue to the other + // write buffer. + // If max_write_buffer_number > 3, writing will be slowed down to + // options.delayed_write_rate if we are writing to the last write buffer + // allowed. + options.set_max_write_buffer_number(4); + + // if prefix_extractor is set and memtable_prefix_bloom_size_ratio is not 0, + // create prefix bloom for memtable with the size of + // write_buffer_size * memtable_prefix_bloom_size_ratio. + // If it is larger than 0.25, it is sanitized to 0.25. + let transform = rocksdb::SliceTransform::create_fixed_prefix(32); + options.set_prefix_extractor(transform); + options.set_memtable_prefix_bloom_ratio(0.1); + + options } #[cfg(test)] pub fn count(&self) -> usize { if let Ok(cf) = self.counters_cf() { - self.cell_db.db().iterator_cf(&cf, rocksdb::IteratorMode::Start).count() + self.db.iterator_cf(&cf, rocksdb::IteratorMode::Start).count() } else { 0 } @@ -180,18 +239,28 @@ impl DynamicBocDb { let root_id = root_cell.hash(MAX_LEVEL); log::debug!(target: TARGET, "DynamicBocDb::save_boc {:x}", root_id); - let cells_cf = self.cell_db.cells_cf()?; + let cells_cf = self.cells_cf()?; - if let Some(existing) = self.cell_db.try_load_existing_root(&root_id, &cells_cf)? { + #[cfg(feature = "telemetry")] + let now = Instant::now(); + if let Some(val) = self.db.get_pinned_cf(&cells_cf, root_id.as_slice())? { log::info!(target: TARGET, "DynamicBocDb::save_boc ALREADY EXISTS {:x}", root_id); - return Ok(existing); + let cell = StoredCell::deserialize(self, &root_id, &val)?; + #[cfg(feature = "telemetry")] + { + self.telemetry + .stored_cells + .update(self.allocated.storage_cells.load(Ordering::Relaxed)); + self.telemetry.loaded_cells.update(1); + self.telemetry.load_cell_time_nanos.update(now.elapsed().as_nanos() as u64); + } + return Ok(Cell::with_cell_impl(cell)); } let mut guard = self.cells_counters.as_ref().map(|m| m.lock()); let mut cells_counters: Option<&mut CellsCounters> = guard.as_deref_mut(); #[cfg(feature = "telemetry")] - self.cell_db - .telemetry() + self.telemetry .cached_cells_counters .update(cells_counters.as_ref().map(|c| c.len()).unwrap_or_default() as u64); @@ -226,40 +295,55 @@ impl DynamicBocDb { let tr_build_time = now2.elapsed().as_micros(); let now3 = Instant::now(); - self.cell_db.db().write(transaction)?; + self.db.write(transaction)?; #[cfg(feature = "telemetry")] if !visited.is_empty() { - self.cell_db.telemetry().boc_db_element_write_nanos.update( + self.telemetry.boc_db_element_write_nanos.update( now3.elapsed().as_nanos() as u64 / (wrote_cells as u64 + wrote_counters as u64), ); } let tr_commit_time = now3.elapsed().as_micros(); let now4 = Instant::now(); - self.cell_db.cleanup_storing_cells(visited.keys()); + for (id, _) in visited.iter() { + let mut stack = vec![id.clone()]; + while let Some(id) = stack.pop() { + if let Some(removed) = self.storing_cells.remove(&id) { + log::trace!( + target: TARGET, + "DynamicBocDb::save_boc {:x} cell removed from storing_cells", id + ); + let _storing_cells_count = + self.storing_cells_count.fetch_sub(1, Ordering::Relaxed); + #[cfg(feature = "telemetry")] + self.telemetry.storing_cells.update(_storing_cells_count - 1); + + for i in 0..removed.val().references_count() { + stack.push(removed.val().reference_repr_hash(i)?); + } + } + } + } let storing_cells_cleanup_time = now4.elapsed().as_micros(); let saved_root = if let Some(c) = visited.get(&root_id).and_then(|vc| vc.cell()) { c.clone() } else { // only if the root cell was already saved (just updated counter) - we need to load it here - self.cell_db.load_cell(&root_id, true)? + self.load_cell(&root_id, true)? }; let updated = visited.len() - wrote_cells; let total_time = now.elapsed().as_micros() as u64; #[cfg(feature = "telemetry")] { - self.cell_db.telemetry().stored_new_cells.update(wrote_cells as u64); - self.cell_db.telemetry().updated_counters.update((wrote_counters - wrote_cells) as u64); - self.cell_db.telemetry().save_boc_total_micros.update(total_time); - self.cell_db.telemetry().save_boc_traverse_micros.update(cells_traverse_time as u64); - self.cell_db.telemetry().save_boc_tr_build_micros.update(tr_build_time as u64); - self.cell_db.telemetry().save_boc_commit_micros.update(tr_commit_time as u64); - self.cell_db - .telemetry() - .save_boc_cleanup_micros - .update(storing_cells_cleanup_time as u64); + self.telemetry.stored_new_cells.update(wrote_cells as u64); + self.telemetry.updated_counters.update((wrote_counters - wrote_cells) as u64); + self.telemetry.save_boc_total_micros.update(total_time); + self.telemetry.save_boc_traverse_micros.update(cells_traverse_time as u64); + self.telemetry.save_boc_tr_build_micros.update(tr_build_time as u64); + self.telemetry.save_boc_commit_micros.update(tr_commit_time as u64); + self.telemetry.save_boc_cleanup_micros.update(storing_cells_cleanup_time as u64); } log::debug!( @@ -282,7 +366,7 @@ impl DynamicBocDb { fail!("INTERNAL ERROR: fill_counters called with already filled counters cache"); } let counters_cf = self.counters_cf()?; - for kv in self.cell_db.db().iterator_cf(&counters_cf, rocksdb::IteratorMode::Start) { + for kv in self.db.iterator_cf(&counters_cf, rocksdb::IteratorMode::Start) { let (key, value) = kv?; let cell_id = UInt256::from_slice(key.as_ref()); let counter = Cursor::new(value).read_le_u32()?; @@ -326,8 +410,7 @@ impl DynamicBocDb { let mut guard = self.cells_counters.as_ref().map(|m| m.lock()); let cells_counters: Option<&mut CellsCounters> = guard.as_deref_mut(); #[cfg(feature = "telemetry")] - self.cell_db - .telemetry() + self.telemetry .cached_cells_counters .update(cells_counters.as_ref().map(|c| c.len()).unwrap_or_default() as u64); self.delete_cells_recursive( @@ -342,7 +425,7 @@ impl DynamicBocDb { #[cfg(feature = "telemetry")] let now2 = std::time::Instant::now(); - let cells_cf = self.cell_db.cells_cf()?; + let cells_cf = self.cells_cf()?; let counters_cf = self.counters_cf()?; let mut deleted = 0; let mut transaction = rocksdb::WriteBatch::default(); @@ -367,7 +450,7 @@ impl DynamicBocDb { #[cfg(feature = "telemetry")] let now3 = Instant::now(); - self.cell_db.db().write(transaction)?; + self.db.write(transaction)?; #[cfg(feature = "telemetry")] let tr_commit_time = now3.elapsed().as_micros(); @@ -377,16 +460,15 @@ impl DynamicBocDb { let updated = visited.len() - deleted; #[cfg(feature = "telemetry")] if !visited.is_empty() { - self.cell_db - .telemetry() + self.telemetry .boc_db_element_write_nanos .update(now3.elapsed().as_nanos() as u64 / (visited.len() as u64 + deleted as u64)); - self.cell_db.telemetry().deleted_cells.update(deleted as u64); - self.cell_db.telemetry().updated_counters.update(updated as u64); - self.cell_db.telemetry().delete_boc_total_micros.update(total_time); - self.cell_db.telemetry().delete_boc_traverse_micros.update(traverse_time as u64); - self.cell_db.telemetry().delete_boc_tr_build_micros.update(tr_build_time as u64); - self.cell_db.telemetry().delete_boc_commit_micros.update(tr_commit_time as u64); + self.telemetry.deleted_cells.update(deleted as u64); + self.telemetry.updated_counters.update(updated as u64); + self.telemetry.delete_boc_total_micros.update(total_time); + self.telemetry.delete_boc_traverse_micros.update(traverse_time as u64); + self.telemetry.delete_boc_tr_build_micros.update(tr_build_time as u64); + self.telemetry.delete_boc_commit_micros.update(tr_commit_time as u64); } #[cfg(feature = "telemetry")] @@ -404,9 +486,92 @@ impl DynamicBocDb { Ok(()) } + pub(crate) fn load_cell(self: &Arc, cell_id: &UInt256, panic: bool) -> Result { + #[cfg(feature = "telemetry")] + let now = Instant::now(); + let storage_cell_data = match self.db.get_pinned_cf(&self.cells_cf()?, cell_id.as_slice()) { + Ok(Some(data)) => data, + _ => { + if let Some(guard) = self.storing_cells.get(cell_id) { + log::trace!( + target: TARGET, + "DynamicBocDb::load_cell from storing_cells by id {cell_id:x}", + ); + return Ok(guard.val().clone()); + } + + if !panic { + fail!("Can't load cell {:x} from db", cell_id); + } + + log::error!("FATAL!"); + log::error!("FATAL! Can't load cell {:x} from db", cell_id); + log::error!("FATAL!"); + + let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); + write(path, "")?; + + std::thread::sleep(Duration::from_millis(100)); + std::process::exit(0xFF); + } + }; + + #[cfg(feature = "telemetry")] + let load_cell_time_nanos = now.elapsed().as_nanos() as u64; + + let storage_cell = match StoredCell::deserialize(self, cell_id, &storage_cell_data) { + Ok(cell) => Arc::new(cell), + Err(e) => { + if !panic { + fail!("Can't deserialize cell {:x} from db, error: {:?}", cell_id, e); + } + + log::error!("FATAL!"); + log::error!( + "FATAL! Can't deserialize cell {:x} from db, data: {}, error: {:?}", + cell_id, + hex::encode(&storage_cell_data), + e + ); + log::error!("FATAL!"); + + let path = Path::new(&self.db_root_path).join(BROKEN_CELL_BEACON_FILE); + write(path, "")?; + + std::thread::sleep(Duration::from_millis(100)); + std::process::exit(0xFF); + } + }; + + #[cfg(feature = "telemetry")] + { + self.telemetry + .stored_cells + .update(self.allocated.storage_cells.load(Ordering::Relaxed)); + self.telemetry.load_cell_time_nanos.update(load_cell_time_nanos); + self.telemetry.loaded_cells.update(1); + } + + log::trace!( + target: TARGET, + "DynamicBocDb::load_cell from DB id {cell_id:x}" + ); + + Ok(Cell::with_cell_impl_arc(storage_cell)) + } + + pub(crate) fn allocated(&self) -> &StorageAlloc { + &self.allocated + } + + fn cells_cf(&self) -> Result>> { + self.db + .cf_handle(&self.cells_cf_name) + .ok_or_else(|| error!("Can't get `{}` cf handle", self.cells_cf_name)) + } + fn counters_cf(&self) -> Result>> { - self.cell_db - .db() + self.db .cf_handle(&self.counters_cf_name) .ok_or_else(|| error!("Can't get `{}` cf handle", self.counters_cf_name)) } @@ -466,15 +631,12 @@ impl DynamicBocDb { } #[cfg(feature = "telemetry")] let now = Instant::now(); - if let Some(raw) = self.cell_db.db().get_pinned_cf(counters_cf, cell_id.as_slice())? { + if let Some(raw) = self.db.get_pinned_cf(counters_cf, cell_id.as_slice())? { // Cell is existing #[cfg(feature = "telemetry")] { - self.cell_db - .telemetry() - .load_counter_time_nanos - .update(now.elapsed().as_nanos() as u64); - self.cell_db.telemetry().loaded_counters.update(1); + self.telemetry.load_counter_time_nanos.update(now.elapsed().as_nanos() as u64); + self.telemetry.loaded_counters.update(1); } let mut reader = Cursor::new(raw); return Ok((false, Some(reader.read_le_u32()?))); @@ -624,7 +786,7 @@ impl DynamicBocDb { let cell = if let Some(c) = cell { c } else { - match self.cell_db.load_cell(&cell_id, true) { + match self.load_cell(&cell_id, true) { Ok(cell) => cell, Err(e) => { log::warn!("DynamicBocDb::delete_cells_recursive {:?}", e); @@ -696,18 +858,13 @@ impl DynamicBocDb { if cells_counters.is_none() { #[cfg(feature = "telemetry")] let now = Instant::now(); - if let Some(counter_raw) = - self.cell_db.db().get_pinned_cf(counters_cf, cell_id.as_slice())? - { + if let Some(counter_raw) = self.db.get_pinned_cf(counters_cf, cell_id.as_slice())? { // Cell's counter is in DB - load it and update #[cfg(feature = "telemetry")] { - self.cell_db - .telemetry() - .load_counter_time_nanos - .update(now.elapsed().as_nanos() as u64); - self.cell_db.telemetry().loaded_counters.update(1); + self.telemetry.load_counter_time_nanos.update(now.elapsed().as_nanos() as u64); + self.telemetry.loaded_counters.update(1); } let mut visited_cell = VisitedCell::with_raw_counter(&counter_raw)?; @@ -730,6 +887,132 @@ impl DynamicBocDb { } } +impl CellsFactory for DynamicBocDb { + fn create_cell(self: Arc, builder: BuilderData) -> Result { + let cell = StoringCell::with_cell(&*builder.into_cell()?, &self)?; + let cell = Cell::with_cell_impl(cell); + let repr_hash = cell.repr_hash(); + + let mut result_cell = None; + + let result = self.storing_cells.insert_with(repr_hash, |_, inserted, found| { + if let Some((_, found)) = found { + result_cell = Some(found.clone()); + lockfree::map::Preview::Discard + } else if let Some(inserted) = inserted { + result_cell = Some(inserted.clone()); + lockfree::map::Preview::Keep + } else { + result_cell = Some(cell.clone()); + lockfree::map::Preview::New(cell.clone()) + } + }); + + let result_cell = result_cell + .ok_or_else(|| error!("INTERNAL ERROR: result_cell {:x} is None", cell.repr_hash()))?; + + match result { + lockfree::map::Insertion::Created => { + log::trace!(target: TARGET, "DynamicBocDb::create_cell {:x} - created new", cell.repr_hash()); + #[cfg(feature = "telemetry")] + { + let storing_cells_count = + self.storing_cells_count.fetch_add(1, Ordering::Relaxed); + self.telemetry.storing_cells.update(storing_cells_count + 1); + } + } + lockfree::map::Insertion::Failed(_) => { + log::trace!(target: TARGET, "DynamicBocDb::create_cell {:x} - already exists", cell.repr_hash()); + } + lockfree::map::Insertion::Updated(old) => { + fail!( + "INTERNAL ERROR: storing_cells.insert_with {:x} returned Updated({:?})", + cell.repr_hash(), + old + ) + } + } + + Ok(result_cell) + } +} + +// This wrapper-struct is added because it is impossible +// to implement foreign trait (CellByHashStorage) for foreign type (Arc) +pub struct CellByHashStorageAdapter { + db: Arc, + root_cells_data: ahash::HashMap>, +} + +impl CellByHashStorageAdapter { + pub fn new( + db: Arc, + root_cell: Option<&Cell>, + max_inmemory_cells: usize, + ) -> Result { + let mut root_cells_data = ahash::HashMap::default(); + if let Some(root_cell) = root_cell { + if db.load_cell(&root_cell.repr_hash(), false).is_err() { + let mut stack = vec![root_cell.clone()]; + while let Some(cell) = stack.pop() { + if root_cells_data.len() >= max_inmemory_cells { + fail!( + "Too many cells in boc to store in memory: {}, max_inmemory_cells: {}", + root_cells_data.len(), + max_inmemory_cells + ); + } + let cell_data = StoredCell::serialize(cell.cell_impl().deref())?; + let cell_hash = cell.repr_hash(); + root_cells_data.insert(cell_hash, cell_data); + + for i in 0..cell.references_count() { + if db.load_cell(&cell.reference_repr_hash(i)?, false).is_err() { + stack.push(cell.reference(i)?); + } + } + } + } + } + Ok(Self { db, root_cells_data }) + } +} + +impl CellsStorage for CellByHashStorageAdapter { + fn load_cell(&self, hash: &UInt256) -> Result { + if let Ok(c) = self.db.clone().load_cell(hash, false) { + Ok(c) + } else if let Some(data) = self.root_cells_data.get(hash) { + StoredCell::deserialize(&self.db, hash, data).map(Cell::with_cell_impl) + } else { + fail!("Can't load cell {:x} from db", hash); + } + } + + fn load_cell_data( + &self, + hash: &UInt256, + write_hashes: bool, + dest: &mut dyn Write, + ) -> Result<()> { + #[cfg(feature = "telemetry")] + let now = std::time::Instant::now(); + if let Ok(Some(data)) = self.db.db.get_pinned_cf(&self.db.cells_cf()?, hash.as_slice()) { + #[cfg(feature = "telemetry")] + { + self.db.telemetry.load_cell_time_nanos.update(now.elapsed().as_nanos() as u64); + self.db.telemetry.loaded_cells.update(1); + } + + StoredCell::write_cell_data(&data, hash, write_hashes, dest) + } else if let Some(data) = self.root_cells_data.get(hash) { + StoredCell::write_cell_data(data, hash, write_hashes, dest) + } else { + fail!("Can't load cell {:x} from db", hash); + } + } +} + pub struct AsyncCellsStorageAdapter { boc_db: Arc, index: Vec<(UInt256, u16)>, // hash & depth. @@ -751,7 +1034,7 @@ impl AsyncCellsStorageAdapter { let mut guard = boc_db_clone.cells_counters.as_ref().map(|m| m.lock()); let mut cells_counters: Option<&mut CellsCounters> = guard.as_deref_mut(); - let cells_cf = boc_db_clone.cell_db.cells_cf()?; + let cells_cf = boc_db_clone.cells_cf()?; let counters_cf = boc_db_clone.counters_cf()?; let mut visited = fnv::FnvHashMap::::default(); @@ -767,7 +1050,7 @@ impl AsyncCellsStorageAdapter { // counter transaction.put_cf(&counters_cf, id.as_slice(), vc.serialize_counter()); } - boc_db_clone.cell_db.db().write(transaction)?; + boc_db_clone.db.write(transaction)?; visited.clear(); Ok(()) }; @@ -822,7 +1105,7 @@ impl CellsTempStorage for AsyncCellsStorageAdapter { Ok(guard.val().clone()) } else { let (hash, _) = self.load_hash_and_depth(index)?; - let cell = self.boc_db.cell_db.load_cell(&hash, false)?; + let cell = self.boc_db.clone().load_cell(&hash, false)?; self.cache.insert(index, cell.clone()); Ok(cell) } @@ -841,8 +1124,7 @@ impl CellsTempStorage for AsyncCellsStorageAdapter { fail!("AsyncCellsStorageAdapter::store_simple_cell supports only zero level cells"); } self.index[index as usize] = (data.hash(0), data.depth(0)); - let cell = - Cell::with_cell_impl(StoredCell::with_cell_data(data, refs, &self.boc_db.cell_db)?); + let cell = Cell::with_cell_impl(StoredCell::with_cell_data(data, refs, &self.boc_db)?); self.cache.insert(index, cell.clone()); self.sender.blocking_send((index, cell))?; Ok(()) diff --git a/src/node/storage/src/lib.rs b/src/node/storage/src/lib.rs index dd82204..27c9408 100644 --- a/src/node/storage/src/lib.rs +++ b/src/node/storage/src/lib.rs @@ -8,14 +8,11 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -pub mod archive_shardstate_db; pub mod archives; pub mod block_handle_db; pub mod block_info_db; pub mod catchain_persistent_db; -pub mod cell_db; pub mod db; -pub mod dynamic_boc_archive_db; pub mod dynamic_boc_rc_db; pub mod error; mod macros; @@ -74,10 +71,8 @@ pub struct StorageTelemetry { pub shardstates_queue: Arc, pub cached_cells_counters: Arc, - pub loaded_cells_from_db: Arc, - pub load_cell_from_db_time_nanos: Arc, - pub load_cell_from_cache_time_nanos: Arc, - pub store_cell_to_cache_time_nanos: Arc, + pub loaded_cells: Arc, + pub load_cell_time_nanos: Arc, pub stored_new_cells: Arc, pub deleted_cells: Arc, @@ -97,10 +92,6 @@ pub struct StorageTelemetry { pub delete_boc_traverse_micros: Arc, pub delete_boc_tr_build_micros: Arc, pub delete_boc_commit_micros: Arc, - - pub cell_cache_hits: Arc, - pub cell_cache_misses: Arc, - pub cell_cache_len: Arc, } #[cfg(feature = "telemetry")] impl Default for StorageTelemetry { @@ -113,13 +104,11 @@ impl Default for StorageTelemetry { storing_cells: Metric::without_totals("", 1), shardstates_queue: Metric::without_totals("", 1), cached_cells_counters: Metric::without_totals("", 1), - loaded_cells_from_db: MetricBuilder::with_metric_and_period( + loaded_cells: MetricBuilder::with_metric_and_period( Metric::with_total_amount("", 1), 1000000000, ), - load_cell_from_db_time_nanos: Metric::with_total_average("", 1), - load_cell_from_cache_time_nanos: Metric::with_total_average("", 1), - store_cell_to_cache_time_nanos: Metric::with_total_average("", 1), + load_cell_time_nanos: Metric::with_total_average("", 1), stored_new_cells: MetricBuilder::with_metric_and_period( Metric::with_total_amount("", 1), 1000000000, @@ -147,15 +136,6 @@ impl Default for StorageTelemetry { delete_boc_traverse_micros: Metric::with_total_average("", 1), delete_boc_tr_build_micros: Metric::with_total_average("", 1), delete_boc_commit_micros: Metric::with_total_average("", 1), - cell_cache_hits: MetricBuilder::with_metric_and_period( - Metric::with_total_amount("", 1), - 1000000000, - ), - cell_cache_misses: MetricBuilder::with_metric_and_period( - Metric::with_total_amount("", 1), - 1000000000, - ), - cell_cache_len: Metric::without_totals("", 1), } } } diff --git a/src/node/storage/src/shard_top_blocks_db.rs b/src/node/storage/src/shard_top_blocks_db.rs index 01619cd..23d2dbf 100644 --- a/src/node/storage/src/shard_top_blocks_db.rs +++ b/src/node/storage/src/shard_top_blocks_db.rs @@ -10,6 +10,4 @@ */ use crate::db_impl_base; -pub const SHARD_TOP_BLOCKS_DB_NAME: &str = "shard_top_blocks_db"; - db_impl_base!(ShardTopBlocksDb, Vec); diff --git a/src/node/storage/src/shardstate_db_async.rs b/src/node/storage/src/shardstate_db_async.rs index 78f8599..809cd31 100644 --- a/src/node/storage/src/shardstate_db_async.rs +++ b/src/node/storage/src/shardstate_db_async.rs @@ -11,12 +11,11 @@ #[cfg(feature = "telemetry")] use crate::StorageTelemetry; use crate::{ - cell_db::CellByHashStorageAdapter, db::{ rocksdb::{RocksDb, RocksDbTable}, DbKey, }, - dynamic_boc_rc_db::{AsyncCellsStorageAdapter, DynamicBocDb}, + dynamic_boc_rc_db::{AsyncCellsStorageAdapter, CellByHashStorageAdapter, DynamicBocDb}, error::StorageError, traits::Serializable, StorageAlloc, TARGET, @@ -113,14 +112,6 @@ pub struct CellsDbConfig { pub prefill_cells_counters: bool, pub cells_cache_size_bytes: u64, pub counters_cache_size_bytes: u64, - #[serde(default = "CellsDbConfig::default_cells_lru_cache_capacity")] - pub cells_lru_cache_capacity: usize, -} - -impl CellsDbConfig { - fn default_cells_lru_cache_capacity() -> usize { - 5_000_000 - } } impl Default for CellsDbConfig { @@ -128,9 +119,8 @@ impl Default for CellsDbConfig { Self { states_db_queue_len: 1000, prefill_cells_counters: false, - cells_cache_size_bytes: 500_000_000, - counters_cache_size_bytes: 500_000_000, - cells_lru_cache_capacity: Self::default_cells_lru_cache_capacity(), + cells_cache_size_bytes: 4_000_000_000, + counters_cache_size_bytes: 4_000_000_000, } } } @@ -490,11 +480,7 @@ impl ShardStateDb { root: Option<&Cell>, max_inmemory_cells: usize, ) -> Result { - CellByHashStorageAdapter::new( - self.dynamic_boc_db.cell_db().clone(), - root, - max_inmemory_cells, - ) + CellByHashStorageAdapter::new(self.dynamic_boc_db.clone(), root, max_inmemory_cells) } pub fn create_fast_cell_storage( @@ -505,7 +491,7 @@ impl ShardStateDb { } pub fn cells_factory(&self) -> Result> { - Ok(self.dynamic_boc_db.cells_factory()) + Ok(self.dynamic_boc_db.clone() as Arc) } pub fn enumerate_ids( diff --git a/src/node/storage/src/tests/mod.rs b/src/node/storage/src/tests/mod.rs index 1d864c8..2abde60 100644 --- a/src/node/storage/src/tests/mod.rs +++ b/src/node/storage/src/tests/mod.rs @@ -9,7 +9,6 @@ * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ mod test_catchain_persistent_db; -mod test_dynamic_boc_archive_db; mod test_dynamic_boc_rc_db; mod test_shardstate_db_async; diff --git a/src/node/storage/src/tests/test_archive_manager.rs b/src/node/storage/src/tests/test_archive_manager.rs index 763f7d1..2d07c52 100644 --- a/src/node/storage/src/tests/test_archive_manager.rs +++ b/src/node/storage/src/tests/test_archive_manager.rs @@ -11,10 +11,8 @@ use crate::StorageTelemetry; use crate::{ archives::{ archive_manager::ArchiveManager, - db_provider::{ArchiveDbProvider, EpochDbProvider, SingleDbProvider}, - epoch::{ArchivalModeConfig, EpochRouter}, package_entry_id::{GetFileName, PackageEntryId}, - ARCHIVE_PACKAGE_SIZE, ARCHIVE_SLICE_SIZE, + ARCHIVE_PACKAGE_SIZE, }, block_handle_db::{BlockHandleStorage, FLAG_KEY_BLOCK}, db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, @@ -46,12 +44,9 @@ async fn create_manager( std::fs::remove_dir_all(&path).ok(); } let db = RocksDb::new(root, name, None, AccessType::ReadWrite)?; - let db_root_path = Arc::new(path); - let db_provider = Arc::new(SingleDbProvider::new(db.clone(), db_root_path.clone())); let manager = ArchiveManager::with_data( db.clone(), - db_root_path, - db_provider, + Arc::new(path), 0, Arc::new(AtomicU8::new(0)), #[cfg(feature = "telemetry")] @@ -368,11 +363,6 @@ async fn test_block_index() -> Result<()> { data.extend_from_slice(&id.seq_no().to_le_bytes()); data } - fn make_proof(id: &BlockIdExt) -> Vec { - let mut data = id.shard().shard_prefix_with_tag().to_be_bytes().to_vec(); - data.extend_from_slice(&id.seq_no().to_be_bytes()); - data - } const DB_NAME: &str = "test_block_index"; @@ -392,7 +382,7 @@ async fn test_block_index() -> Result<()> { for mc_seqno in 1..total_mc_blocks { let id = generate_block_id(-1, 0x8000_0000_0000_0000, mc_seqno); manager.add_file(&PackageEntryId::Block(&id), &make_data(&id)).await?; - manager.add_file(&PackageEntryId::Proof(&id), &make_proof(&id)).await?; + manager.add_file(&PackageEntryId::Proof(&id), &[1, 2, 3]).await?; let flags = if rand::random::() % 12345 == 0 { FLAG_KEY_BLOCK } else { 0 }; let block_meta = BlockMeta::with_data(flags, gen_utime, lt, mc_seqno, 0); let handle = block_handle_storage.create_handle(id.clone(), block_meta, None)?.unwrap(); @@ -411,7 +401,7 @@ async fn test_block_index() -> Result<()> { *seqno, ); manager.add_file(&PackageEntryId::Block(&id), &make_data(&id)).await?; - manager.add_file(&PackageEntryId::ProofLink(&id), &make_proof(&id)).await?; + manager.add_file(&PackageEntryId::ProofLink(&id), &[1, 2, 3]).await?; let block_meta = BlockMeta::with_data(0, gen_utime, lt + i as u64 * 1_000_000, mc_seqno, 0); let handle = @@ -453,11 +443,8 @@ async fn test_block_index() -> Result<()> { let prefix = AccountIdPrefixFull { workchain_id: -1, prefix: rand::random::() }; let (id, data) = manager.lookup_block_by_seqno(&prefix, seqno).await?.unwrap(); assert_eq!(data, make_data(&id)); - let (id, data) = manager.lookup_proof_by_seqno(&prefix, seqno).await?.unwrap(); - assert_eq!(data, make_proof(&id)); let mut found = 0; - let mut ids = vec![]; let utime = init_utime + (rand::random::() % (gen_utime - init_utime)) - 100; log::info!("lookup by utime {}", utime); let prefix = AccountIdPrefixFull { workchain_id: 0, prefix: rand::random::() }; @@ -468,21 +455,12 @@ async fn test_block_index() -> Result<()> { Box::new(|id, data| { assert_eq!(data, make_data(&id)); found += 1; - ids.push(id); Ok(true) }), ) .await?; assert!(found > 0); - for id in ids { - let (id, data) = manager - .lookup_proof_by_seqno(&id.shard().account_id_prefix(), id.seq_no()) - .await? - .unwrap(); - assert_eq!(data, make_proof(&id)); - } } - assert_eq!(manager.get_max_mc_seqno().await, Some(total_mc_blocks - 1)); drop(block_handle_storage); drop(manager); @@ -506,8 +484,6 @@ async fn test_block_index() -> Result<()> { assert_eq!(data, make_data(&id)); assert_eq!(id.seq_no(), 20_000); - assert_eq!(manager.get_max_mc_seqno().await, Some(total_mc_blocks - 1)); - for _ in 0..20_000 { let lt = rand::random::() % lt; log::info!("lookup by lt {}", lt); @@ -521,12 +497,8 @@ async fn test_block_index() -> Result<()> { if let Some((id, data)) = manager.lookup_block_by_seqno(&prefix, seqno).await? { assert_eq!(data, make_data(&id)); } - if let Some((id, data)) = manager.lookup_proof_by_seqno(&prefix, seqno).await? { - assert_eq!(data, make_proof(&id)); - } let mut found = 0; - let mut ids = vec![]; let utime = init_utime + rand::random::() % (gen_utime - init_utime) - 100; log::info!("lookup by utime {}", utime); let prefix = AccountIdPrefixFull { workchain_id: -1, prefix: rand::random::() }; @@ -537,19 +509,11 @@ async fn test_block_index() -> Result<()> { Box::new(|id, data| { assert_eq!(data, make_data(&id)); found += 1; - ids.push(id); Ok(true) }), ) .await?; assert!(found > 0); - for id in ids { - let (id, data) = manager - .lookup_proof_by_seqno(&id.shard().account_id_prefix(), id.seq_no()) - .await? - .unwrap(); - assert_eq!(data, make_proof(&id)); - } } drop(manager); @@ -557,180 +521,3 @@ async fn test_block_index() -> Result<()> { destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); Ok(()) } - -// --- Archival mode (epoch-based) tests --- - -fn mc_block_id(mc_seq_no: u32) -> BlockIdExt { - BlockIdExt::with_params( - ShardIdent::masterchain(), - mc_seq_no, - UInt256::from_le_bytes(&mc_seq_no.to_le_bytes()), - UInt256::default(), - ) -} - -async fn write_blocks( - manager: &ArchiveManager, - bhs: &BlockHandleStorage, - range: std::ops::Range, - data: &[u8], -) -> Result<()> { - for mc_seq_no in range { - let block_id = mc_block_id(mc_seq_no); - let meta = BlockMeta::with_data(0, 0, 0, 0, 0); - let handle = bhs - .create_handle(block_id.clone(), meta, None)? - .ok_or_else(|| error!("Cannot create handle for block {}", block_id))?; - manager.add_file(&PackageEntryId::Proof(&block_id), data).await?; - handle.set_proof(); - handle.set_block_applied(); - manager.move_to_archive(&handle, || Ok(())).await?; - handle.set_archived(); - bhs.save_handle(&handle, None)?; - } - Ok(()) -} - -async fn read_block( - manager: &ArchiveManager, - bhs: &BlockHandleStorage, - mc_seq_no: u32, -) -> Result> { - let block_id = mc_block_id(mc_seq_no); - let handle = bhs.load_handle_by_id(&block_id)?.unwrap(); - manager.get_file(&handle, &PackageEntryId::Proof(&block_id)).await -} - -async fn create_epoch_manager( - dir: &Path, -) -> Result<(ArchiveManager, Arc, Arc)> { - let db_root = dir.join("main_db"); - let new_epochs_path = dir.join("new_epochs"); - - let config = ArchivalModeConfig { - epoch_size: ARCHIVE_SLICE_SIZE, - new_epochs_path, - existing_epochs: vec![], - }; - - let db = RocksDb::new(&db_root, "db", None, AccessType::ReadWrite)?; - let db_root_path = Arc::new(db_root); - - let router = Arc::new(EpochRouter::new(&config).await?); - let db_provider: Arc = Arc::new(EpochDbProvider::new(router.clone())); - - let manager = ArchiveManager::with_data( - db.clone(), - db_root_path, - db_provider, - 0, - Arc::new(AtomicU8::new(0)), - #[cfg(feature = "telemetry")] - Arc::new(StorageTelemetry::default()), - Arc::new(StorageAlloc::default()), - ) - .await?; - - Ok((manager, db, router)) -} - -#[tokio::test] -async fn test_archival_mode_minimal() -> Result<()> { - let dir = tempfile::tempdir().unwrap(); - let (manager, db, router) = create_epoch_manager(dir.path()).await?; - let (bhs, _) = create_block_handle_storage(db.clone()); - router.resolve_or_create(0).await?; - - write_blocks(&manager, &bhs, 50..51, &[1, 2, 3]).await?; - - let result = read_block(&manager, &bhs, 50).await?; - assert_eq!(result, vec![1, 2, 3]); - Ok(()) -} - -#[tokio::test] -async fn test_archival_mode_write_and_read() -> Result<()> { - let dir = tempfile::tempdir().unwrap(); - let (manager, db, router) = create_epoch_manager(dir.path()).await?; - let (bhs, _) = create_block_handle_storage(db.clone()); - router.resolve_or_create(0).await?; - - let data = vec![1, 2, 3, 4, 5]; - write_blocks(&manager, &bhs, 0..150, &data).await?; - - for mc_seq_no in 0..150 { - assert_eq!(read_block(&manager, &bhs, mc_seq_no).await?, data); - } - - // Verify .pack files are in epoch directory, not main db - let epoch_dir = dir.path().join("new_epochs").join("epoch_0"); - assert!(epoch_dir.exists(), "Epoch directory should exist"); - assert!( - epoch_dir.join("archive").join("packages").exists(), - "Pack files should be in epoch directory" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_archival_mode_multiple_epochs() -> Result<()> { - let dir = tempfile::tempdir().unwrap(); - let (manager, db, router) = create_epoch_manager(dir.path()).await?; - let (bhs, _) = create_block_handle_storage(db.clone()); - - router.resolve_or_create(0).await?; - router.resolve_or_create(20_000).await?; - - let data_epoch0 = vec![10, 20, 30]; - let data_epoch1 = vec![40, 50, 60]; - - write_blocks(&manager, &bhs, 0..100, &data_epoch0).await?; - assert_eq!(read_block(&manager, &bhs, 50).await?, data_epoch0); - - write_blocks(&manager, &bhs, 20_000..20_100, &data_epoch1).await?; - - assert_eq!(read_block(&manager, &bhs, 50).await?, data_epoch0); - assert_eq!(read_block(&manager, &bhs, 20_050).await?, data_epoch1); - - assert!(dir.path().join("new_epochs").join("epoch_0").exists()); - assert!(dir.path().join("new_epochs").join("epoch_1").exists()); - - Ok(()) -} - -#[tokio::test] -async fn test_archival_mode_restart_preserves_data() -> Result<()> { - let dir = tempfile::tempdir().unwrap(); - let data = vec![7, 8, 9]; - - // First "run": write some blocks - let db = { - let (manager, db, router) = create_epoch_manager(dir.path()).await?; - let (bhs, _bh_db) = create_block_handle_storage(db.clone()); - router.resolve_or_create(0).await?; - write_blocks(&manager, &bhs, 0..50, &data).await?; - db - }; - - // BlockHandleStorage has background task which holds RocksDB instance - while Arc::strong_count(&db) > 1 { - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - } - drop(db); - - // Second "run": recreate manager, verify data is accessible - let (manager, db, _router) = create_epoch_manager(dir.path()).await?; - let (bhs, _) = create_block_handle_storage(db.clone()); - - for mc_seq_no in 0..50 { - assert_eq!( - read_block(&manager, &bhs, mc_seq_no).await?, - data, - "Block {} data mismatch after restart", - mc_seq_no - ); - } - - Ok(()) -} diff --git a/src/node/storage/src/tests/test_archive_slice.rs b/src/node/storage/src/tests/test_archive_slice.rs index 53cbaf1..313042d 100644 --- a/src/node/storage/src/tests/test_archive_slice.rs +++ b/src/node/storage/src/tests/test_archive_slice.rs @@ -23,7 +23,7 @@ use crate::{ StorageAlloc, }; use std::{future::Future, path::Path, pin::Pin, sync::Arc}; -use ton_block::{error, AccountIdPrefixFull, BlockIdExt, Result, ShardIdent, UInt256}; +use ton_block::{error, BlockIdExt, Result, ShardIdent, UInt256}; const DB_PATH: &str = "../../target/test"; @@ -41,7 +41,6 @@ async fn prepare_test( name: &str, package_type: PackageType, shard_split_depth: u8, - archive_id: u32, ) -> Result<(Arc, TestContext)> { let db_root = Path::new(DB_PATH).join(name); let _ = std::fs::remove_dir_all(&db_root); @@ -49,7 +48,7 @@ async fn prepare_test( let archive_slice = ArchiveSlice::new_empty( db.clone(), Arc::new(db_root), - archive_id, + 0, package_type, shard_split_depth, #[cfg(feature = "telemetry")] @@ -73,11 +72,9 @@ async fn run_test( name: &str, package_type: PackageType, shard_split_depth: u8, - archive_id: u32, scenario: impl Fn(TestContext) -> Pinned, ) -> Result<()> { - let (db, test_context) = - prepare_test(name, package_type, shard_split_depth, archive_id).await?; + let (db, test_context) = prepare_test(name, package_type, shard_split_depth).await?; scenario(test_context).await?; destroy_db(db, name).await; Ok(()) @@ -150,7 +147,7 @@ async fn test_scenario_gold() -> Result<()> { Ok(()) } - run_test("test_archive_slice_scenario_gold", PackageType::Blocks, 0, 0, |ctx| { + run_test("test_archive_slice_scenario_gold", PackageType::Blocks, 0, |ctx| { Box::pin(scenario(ctx)) }) .await @@ -187,53 +184,6 @@ async fn test_key_blocks_slice() -> Result<()> { Ok(()) } - run_test("test_key_blocks_slice", PackageType::KeyBlocks, 0, 0, |ctx| Box::pin(scenario(ctx))) + run_test("test_key_blocks_slice", PackageType::KeyBlocks, 0, |ctx| Box::pin(scenario(ctx))) .await } - -#[tokio::test] -async fn test_lookup_proof_by_seqno() -> Result<()> { - async fn scenario(test_context: TestContext) -> Result<()> { - let proof_data = vec![7u8, 8, 9]; - let mc_seqno = 55u32; - - let block_id = BlockIdExt::with_params( - ShardIdent::masterchain(), - mc_seqno, - UInt256::with_array([mc_seqno as u8; 32]), - UInt256::default(), - ); - let meta = BlockMeta::with_data(0, 1000, 100_000, mc_seqno, 0); - let handle = test_context - .block_handle_storage - .create_handle(block_id.clone(), meta, None)? - .ok_or_else(|| error!("Cannot create handle"))?; - - test_context - .archive_slice - .add_file(&handle, &PackageEntryId::Block(&block_id), vec![1, 2, 3]) - .await?; - test_context - .archive_slice - .add_file(&handle, &PackageEntryId::Proof(&block_id), proof_data.clone()) - .await?; - - let prefix = AccountIdPrefixFull { workchain_id: -1, prefix: 0 }; - - let result = test_context.archive_slice.lookup_proof_by_seqno(&prefix, mc_seqno).await?; - let (found_id, found_data) = result.expect("proof should be found"); - assert_eq!(found_id, block_id); - assert_eq!(found_data, proof_data); - - let result = test_context.archive_slice.lookup_proof_by_seqno(&prefix, 999).await?; - assert!(result.is_none(), "lookup of non-existent seqno should return None"); - - drop(test_context); - Ok(()) - } - - run_test("test_lookup_proof_by_seqno", PackageType::Blocks, 0, 50, |ctx| { - Box::pin(scenario(ctx)) - }) - .await -} diff --git a/src/node/storage/src/tests/test_dynamic_boc_archive_db.rs b/src/node/storage/src/tests/test_dynamic_boc_archive_db.rs deleted file mode 100644 index 5f01ee9..0000000 --- a/src/node/storage/src/tests/test_dynamic_boc_archive_db.rs +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -#[cfg(feature = "telemetry")] -use crate::StorageTelemetry; -use crate::{ - archive_shardstate_db::ArchiveShardStateDb, - cell_db::CellByHashStorageAdapter, - db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, - dynamic_boc_archive_db::DynamicBocArchiveDb, - shardstate_db_async::CellsDbConfig, - tests::utils::{count_tree_unique_cells, get_test_tree_of_cells, init_test_log}, - StorageAlloc, -}; -use std::sync::Arc; -use ton_block::{ - read_single_root_boc, BigBocWriter, BlockIdExt, BocFlags, BuilderData, CellsFactory, - IBitstring, Result, ShardIdent, UInt256, MAX_SAFE_DEPTH, SHARD_FULL, -}; - -const DB_PATH: &str = "../../target/test"; - -fn make_block_id(seq_no: u32) -> BlockIdExt { - BlockIdExt::with_params( - ShardIdent::with_tagged_prefix(-1, SHARD_FULL).unwrap(), - seq_no, - UInt256::from([seq_no as u8; 32]), - UInt256::from([(seq_no + 100) as u8; 32]), - ) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_dynamic_boc_archive_db() -> Result<()> { - init_test_log(); - - const DB_NAME: &str = "test_dynamic_boc_archive_db"; - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - - let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; - let boc_db = Arc::new(DynamicBocArchiveDb::with_db( - db.clone(), - "cells", - "", - &CellsDbConfig::default(), - #[cfg(feature = "telemetry")] - Arc::new(StorageTelemetry::default()), - Arc::new(StorageAlloc::default()), - )?); - - let root_cell = get_test_tree_of_cells(); - let initial_count = count_tree_unique_cells(root_cell.clone()); - - // Save and verify - boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; - assert_eq!(boc_db.cell_db().count(), initial_count); - - // Load and verify - let loaded = boc_db.cell_db().load_cell(&root_cell.repr_hash(), false)?; - assert_eq!(count_tree_unique_cells(loaded), initial_count); - - drop(boc_db); - drop(db); - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_archive_save_idempotent() -> Result<()> { - init_test_log(); - - const DB_NAME: &str = "test_archive_save_idempotent"; - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - - let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; - let boc_db = Arc::new(DynamicBocArchiveDb::with_db( - db.clone(), - "cells", - "", - &CellsDbConfig::default(), - #[cfg(feature = "telemetry")] - Arc::new(StorageTelemetry::default()), - Arc::new(StorageAlloc::default()), - )?); - - let root_cell = get_test_tree_of_cells(); - let initial_count = count_tree_unique_cells(root_cell.clone()); - - // Save twice - boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; - boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; - - // Count should not change - assert_eq!(boc_db.cell_db().count(), initial_count); - - drop(boc_db); - drop(db); - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_archive_shared_cells() -> Result<()> { - init_test_log(); - - const DB_NAME: &str = "test_archive_shared_cells"; - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - - let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; - let boc_db = Arc::new(DynamicBocArchiveDb::with_db( - db.clone(), - "cells", - "", - &CellsDbConfig::default(), - #[cfg(feature = "telemetry")] - Arc::new(StorageTelemetry::default()), - Arc::new(StorageAlloc::default()), - )?); - - // Create shared cells via CellsFactory - let cells_factory = boc_db.cell_db().clone() as Arc; - let create_chain = |data_values: Vec<&str>| -> ton_block::Cell { - let mut child = None; - let mut cell = ton_block::Cell::default(); - for data in data_values.iter().rev() { - let mut builder = BuilderData::new(); - let mut data = data.as_bytes().to_vec(); - data.push(0x80); - builder.append_bitstring(&data).unwrap(); - if let Some(child) = child { - builder.checked_append_reference(child).unwrap(); - } - cell = cells_factory.clone().create_cell(builder).unwrap(); - child = Some(cell.clone()); - } - cell - }; - - let r1 = create_chain(vec!["r1", "shared", "leaf"]); - boc_db.save_boc(r1.clone(), &|| Ok(()))?; - let count_after_r1 = boc_db.cell_db().count(); - - let r2 = create_chain(vec!["r2", "shared", "leaf"]); - boc_db.save_boc(r2.clone(), &|| Ok(()))?; - let count_after_r2 = boc_db.cell_db().count(); - - // r2 shares "shared" and "leaf" with r1, so only 1 new cell ("r2") should be added - assert_eq!(count_after_r2, count_after_r1 + 1); - - // Both roots should be loadable - let _ = boc_db.cell_db().load_cell(&r1.repr_hash(), false)?; - let _ = boc_db.cell_db().load_cell(&r2.repr_hash(), false)?; - - drop(cells_factory); - drop(boc_db); - drop(db); - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_archive_shardstate_db() -> Result<()> { - init_test_log(); - - const DB_NAME: &str = "test_archive_shardstate_db"; - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - - let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; - let ss_db = ArchiveShardStateDb::new( - db.clone(), - "shardstate_idx", - "cells", - "", - &CellsDbConfig::default(), - #[cfg(feature = "telemetry")] - Arc::new(StorageTelemetry::default()), - Arc::new(StorageAlloc::default()), - )?; - - let root_cell = get_test_tree_of_cells(); - let block_id = make_block_id(1); - - // Put - assert!(!ss_db.contains(&block_id)?); - ss_db.put(&block_id, root_cell.clone())?; - assert!(ss_db.contains(&block_id)?); - - // Get - let loaded = ss_db.get(&block_id)?; - assert_eq!(count_tree_unique_cells(loaded), count_tree_unique_cells(root_cell)); - - // Put idempotent - ss_db.put(&block_id, ton_block::Cell::default())?; // should return existing, not overwrite - let loaded2 = ss_db.get(&block_id)?; - assert_eq!(loaded2.repr_hash(), ss_db.get(&block_id)?.repr_hash()); - - drop(ss_db); - drop(db); - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_archive_cell_by_hash_storage() -> Result<()> { - init_test_log(); - - const DB_NAME: &str = "test_archive_cell_by_hash_storage"; - destroy_rocks_db(DB_PATH, DB_NAME).await?; - - let db = RocksDb::new(DB_PATH, DB_NAME, None, AccessType::ReadWrite)?; - let boc_db = Arc::new(DynamicBocArchiveDb::with_db( - db.clone(), - "cells", - "", - &CellsDbConfig::default(), - #[cfg(feature = "telemetry")] - Arc::new(StorageTelemetry::default()), - Arc::new(StorageAlloc::default()), - )?); - - let data = std::fs::read( - "../../block/src/tests/data/6A3BD5B96ABEA186BFEE202B70D510C29F85E126A522B08C1DCAD39F92CF5C51.boc", - )?; - let root_cell = read_single_root_boc(&data)?; - - // Repack without hashes (same as test_cell_by_hash_storage in test_dynamic_boc_rc_db.rs) - fn repack(cell: ton_block::Cell) -> Result { - let mut builder = BuilderData::with_raw(cell.data(), cell.bit_length())?; - builder.set_type(cell.cell_type()); - for r in cell.clone_references() { - builder.checked_append_reference(repack(r)?)?; - } - builder.finalize(MAX_SAFE_DEPTH) - } - let root_cell = repack(root_cell)?; - - boc_db.save_boc(root_cell.clone(), &|| Ok(()))?; - - let writer = BigBocWriter::with_params( - [root_cell.clone()], - MAX_SAFE_DEPTH, - BocFlags::all(), - &|| false, - Arc::new(CellByHashStorageAdapter::new(boc_db.cell_db().clone(), None, 0)?), - )?; - - let mut boc = Vec::new(); - writer.write(&mut boc)?; - - assert_eq!(boc.len(), data.len()); - assert_eq!(boc, data); - - drop(boc_db); - drop(db); - destroy_rocks_db(DB_PATH, DB_NAME).await.unwrap(); - Ok(()) -} diff --git a/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs b/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs index bdda250..d543db0 100644 --- a/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs +++ b/src/node/storage/src/tests/test_dynamic_boc_rc_db.rs @@ -11,9 +11,8 @@ #[cfg(feature = "telemetry")] use crate::StorageTelemetry; use crate::{ - cell_db::CellByHashStorageAdapter, db::rocksdb::{destroy_rocks_db, AccessType, RocksDb}, - dynamic_boc_rc_db::DynamicBocDb, + dynamic_boc_rc_db::{CellByHashStorageAdapter, DynamicBocDb}, shardstate_db_async::CellsDbConfig, tests::utils::{ count_tree_unique_cells, get_another_test_tree_of_cells, get_test_tree_of_cells, @@ -23,8 +22,8 @@ use crate::{ }; use std::sync::Arc; use ton_block::{ - read_single_root_boc, BigBocWriter, BocFlags, BuilderData, Cell, IBitstring, Result, - MAX_SAFE_DEPTH, + read_single_root_boc, BigBocWriter, BocFlags, BuilderData, Cell, CellsFactory, IBitstring, + Result, MAX_SAFE_DEPTH, }; const DB_PATH: &str = "../../target/test"; @@ -95,7 +94,7 @@ async fn test_dynamic_boc_rc_db_2() -> Result<()> { Arc::new(StorageAlloc::default()), )?); - let cells_factory = boc_db.cells_factory(); + let cells_factory = boc_db.clone() as Arc; let create_ss = |cells_chain: Vec<&str>| -> Cell { let mut child = None; let mut cell = Cell::default(); @@ -181,7 +180,7 @@ async fn test_cell_by_hash_storage() -> Result<()> { MAX_SAFE_DEPTH, BocFlags::all(), &|| false, - Arc::new(CellByHashStorageAdapter::new(boc_db.cell_db().clone(), None, 0)?), + Arc::new(CellByHashStorageAdapter::new(boc_db.clone(), None, 0)?), )?; let mut boc = Vec::new(); diff --git a/src/node/storage/src/tests/test_epoch.rs b/src/node/storage/src/tests/test_epoch.rs deleted file mode 100644 index e65e734..0000000 --- a/src/node/storage/src/tests/test_epoch.rs +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -use super::*; - -#[tokio::test] -async fn test_epoch_router_validation() { - let dir = tempfile::tempdir().unwrap(); - - let config = ArchivalModeConfig { - epoch_size: 0, - new_epochs_path: dir.path().to_path_buf(), - existing_epochs: vec![], - }; - assert!(EpochRouter::new(&config).await.is_err()); - - let config = ArchivalModeConfig { - epoch_size: 10_000, // not a multiple of ARCHIVE_SLICE_SIZE - new_epochs_path: dir.path().to_path_buf(), - existing_epochs: vec![], - }; - assert!(EpochRouter::new(&config).await.is_err()); -} - -#[tokio::test] -async fn test_epoch_router_resolve_and_create() { - let dir = tempfile::tempdir().unwrap(); - let new_epochs_path = dir.path().join("new_epochs"); - - let config = ArchivalModeConfig { - epoch_size: 40_000, - new_epochs_path: new_epochs_path.clone(), - existing_epochs: vec![], - }; - - let router = EpochRouter::new(&config).await.unwrap(); - - // No epochs exist yet - assert!(router.resolve(0).is_none()); - assert!(router.resolve(39_999).is_none()); - - // Create epoch for mc_seq_no 0 - let epoch = router.resolve_or_create(0).await.unwrap(); - assert_eq!(epoch.mc_seq_no_start(), 0); - assert_eq!(epoch.mc_seq_no_end(), 39_999); - assert!(epoch.path().starts_with(&new_epochs_path)); - - // Resolve same epoch - let epoch2 = router.resolve(20_000).unwrap(); - assert_eq!(epoch2.mc_seq_no_start(), 0); - - // Create second epoch - let epoch3 = router.resolve_or_create(50_000).await.unwrap(); - assert_eq!(epoch3.mc_seq_no_start(), 40_000); - assert_eq!(epoch3.mc_seq_no_end(), 79_999); - - // Verify both exist - assert!(router.resolve(0).is_some()); - assert!(router.resolve(50_000).is_some()); - assert!(router.resolve(80_000).is_none()); -} - -#[tokio::test] -async fn test_epoch_router_with_existing_epochs() { - let dir = tempfile::tempdir().unwrap(); - let epoch0_path = dir.path().join("epoch_0"); - let epoch1_path = dir.path().join("epoch_1"); - let new_epochs_path = dir.path().join("new_epochs"); - - std::fs::create_dir_all(&epoch0_path).unwrap(); - std::fs::create_dir_all(&epoch1_path).unwrap(); - - // Write metadata for existing epochs - let meta0 = EpochMeta { mc_seq_no_start: 0, mc_seq_no_end: 39_999 }; - let meta1 = EpochMeta { mc_seq_no_start: 40_000, mc_seq_no_end: 79_999 }; - write_epoch_meta(&epoch0_path, &meta0).await.unwrap(); - write_epoch_meta(&epoch1_path, &meta1).await.unwrap(); - - let config = ArchivalModeConfig { - epoch_size: 40_000, - new_epochs_path, - existing_epochs: vec![EpochEntry { path: epoch0_path }, EpochEntry { path: epoch1_path }], - }; - - let router = EpochRouter::new(&config).await.unwrap(); - - let e0 = router.resolve(0).unwrap(); - assert_eq!(e0.mc_seq_no_start(), 0); - assert_eq!(e0.mc_seq_no_end(), 39_999); - - let e1 = router.resolve(40_000).unwrap(); - assert_eq!(e1.mc_seq_no_start(), 40_000); - assert_eq!(e1.mc_seq_no_end(), 79_999); - - assert!(router.resolve(80_000).is_none()); -} - -#[tokio::test] -async fn test_epoch_router_rejects_misaligned_existing() { - let dir = tempfile::tempdir().unwrap(); - let epoch_path = dir.path().join("bad_epoch"); - std::fs::create_dir_all(&epoch_path).unwrap(); - - // Epoch with wrong size (60_000 != 40_000) - let meta = EpochMeta { mc_seq_no_start: 0, mc_seq_no_end: 59_999 }; - write_epoch_meta(&epoch_path, &meta).await.unwrap(); - - let config = ArchivalModeConfig { - epoch_size: 40_000, - new_epochs_path: dir.path().join("new_epochs"), - existing_epochs: vec![EpochEntry { path: epoch_path }], - }; - - assert!(EpochRouter::new(&config).await.is_err()); -} - -#[tokio::test] -async fn test_epoch_router_discovers_on_restart() { - let dir = tempfile::tempdir().unwrap(); - let new_epochs_path = dir.path().join("new_epochs"); - - // First "run": create epochs dynamically - let config = ArchivalModeConfig { - epoch_size: 40_000, - new_epochs_path: new_epochs_path.clone(), - existing_epochs: vec![], - }; - let router = EpochRouter::new(&config).await.unwrap(); - router.resolve_or_create(0).await.unwrap(); - router.resolve_or_create(50_000).await.unwrap(); - assert!(router.resolve(0).is_some()); - assert!(router.resolve(50_000).is_some()); - drop(router); - - // Second "run": new router should discover epochs from new_epochs_path - let config2 = ArchivalModeConfig { - epoch_size: 40_000, - new_epochs_path: new_epochs_path.clone(), - existing_epochs: vec![], - }; - let router2 = EpochRouter::new(&config2).await.unwrap(); - let e0 = router2.resolve(0).unwrap(); - assert_eq!(e0.mc_seq_no_start(), 0); - assert_eq!(e0.mc_seq_no_end(), 39_999); - - let e1 = router2.resolve(50_000).unwrap(); - assert_eq!(e1.mc_seq_no_start(), 40_000); - assert_eq!(e1.mc_seq_no_end(), 79_999); - - assert!(router2.resolve(80_000).is_none()); -} diff --git a/src/node/storage/src/types/block_meta.rs b/src/node/storage/src/types/block_meta.rs index e95aece..945eb61 100644 --- a/src/node/storage/src/types/block_meta.rs +++ b/src/node/storage/src/types/block_meta.rs @@ -25,35 +25,6 @@ pub struct BlockMeta { } impl BlockMeta { - /// Create BlockMeta for archive import with all necessary flags pre-set. - pub fn for_import( - gen_utime: u32, - end_lt: u64, - masterchain_ref_seq_no: u32, - is_key_block: bool, - is_masterchain: bool, - has_prev2: bool, - ) -> Self { - let mut flags = block_handle_db::FLAG_DATA - | block_handle_db::FLAG_APPLIED - | block_handle_db::FLAG_STATE - | block_handle_db::FLAG_STATE_SAVED - | block_handle_db::FLAG_MOVED_TO_ARCHIVE - | block_handle_db::FLAG_PREV_1; - if has_prev2 { - flags |= block_handle_db::FLAG_PREV_2; - } - if is_masterchain { - flags |= block_handle_db::FLAG_PROOF; - } else { - flags |= block_handle_db::FLAG_PROOF_LINK; - } - if is_key_block { - flags |= block_handle_db::FLAG_KEY_BLOCK; - } - Self::with_data(flags, gen_utime, end_lt, masterchain_ref_seq_no, 0) - } - pub fn from_block(block: &Block) -> Result { let info = block.read_info()?; let flags = if info.key_block() { block_handle_db::FLAG_KEY_BLOCK } else { 0 }; diff --git a/src/node/storage/src/types/storage_cell.rs b/src/node/storage/src/types/storage_cell.rs index 7dbd352..9264a8b 100644 --- a/src/node/storage/src/types/storage_cell.rs +++ b/src/node/storage/src/types/storage_cell.rs @@ -8,8 +8,7 @@ * This file has been modified from its original version. * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -use crate::{cell_db::CellDb, TARGET}; -use smallvec::SmallVec; +use crate::{dynamic_boc_rc_db::DynamicBocDb, TARGET}; use std::{ io::Write, sync::{ @@ -18,9 +17,9 @@ use std::{ }, }; use ton_block::{ - append_tag, calc_d1, cell_type, error, fail, full_len, hashes_count, level, level_mask, - refs_count, store_hashes, Cell, CellData, CellImpl, CellType, LevelMask, Result, UInt256, - DEPTH_SIZE, MAX_LEVEL, SHA256_SIZE, + calc_d1, cell_type, error, fail, full_len, hashes_count, level, level_mask, refs_count, + store_hashes, Cell, CellData, CellImpl, CellType, LevelMask, Result, UInt256, DEPTH_SIZE, + MAX_LEVEL, SHA256_SIZE, }; #[cfg(test)] @@ -29,9 +28,6 @@ mod tests; const NOT_INITIALIZED_DEPTH: u16 = u16::MAX; -// Max raw data: d1(1) + d2(1) + hashes(32*3) + depths(2*4) + data(128) + ref_hashes(32*4) + ref_depths(2*4) -pub const STORED_CELL_MAX_RAW_LEN: usize = 1 + 1 + 32 * 3 + 2 * 4 + 128 + 32 * 4 + 2 * 4; - struct Reference { hash: UInt256, depth: u16, @@ -41,7 +37,7 @@ struct Reference { pub struct StoredCell { cell_data: CellData, references: parking_lot::RwLock>, - boc_db: Weak, + boc_db: Weak, } static STORED_CELL_COUNT: AtomicU64 = AtomicU64::new(0); @@ -66,7 +62,11 @@ impl<'a> SliceReader<'a> { /// Represents Cell for storing in persistent storage impl StoredCell { - pub fn deserialize(boc_db: &Arc, repr_hash: &UInt256, data: &[u8]) -> Result { + pub fn deserialize( + boc_db: &Arc, + repr_hash: &UInt256, + data: &[u8], + ) -> Result { if data.len() < 2 { fail!("Buffer is too small to read description bytes"); } @@ -221,35 +221,9 @@ impl StoredCell { } pub fn serialize(cell: &dyn CellImpl) -> Result> { - Self::serialize_internal(cell, cell.raw_data()?, cell.store_hashes()) - } - - pub fn serialize_virtual(cell: &dyn CellImpl) -> Result> { - if cell.is_pruned() && cell.level() == 0 { - fail!("Virtual pruned cell can't be serialized"); - } - - let mut data = SmallVec::from_slice(cell.data()); - if cell.bit_length() % 8 == 0 { - append_tag(&mut data, cell.bit_length()); - }; - let data = CellData::with_params( - cell.cell_type(), - data.as_slice(), - cell.level_mask().mask(), - cell.references_count() as u8, - )?; - - Self::serialize_internal(cell, data.raw_data(), false) - } - - fn serialize_internal( - cell: &dyn CellImpl, - raw_data: &[u8], - store_hashes: bool, - ) -> Result> { + let store_hashes = cell.store_hashes(); let data_size = Self::calc_serialized_size( - raw_data.len(), + cell.raw_data()?.len(), store_hashes, cell.level(), cell.references_count(), @@ -257,7 +231,7 @@ impl StoredCell { ); let mut data = Vec::with_capacity(data_size); - data.extend_from_slice(raw_data); + data.extend_from_slice(cell.raw_data()?); if !store_hashes { if cell.cell_type() != CellType::PrunedBranch { @@ -284,7 +258,7 @@ impl StoredCell { pub fn with_cell_data( cell_data: CellData, refs: &[(UInt256, u16)], - boc_db: &Arc, + boc_db: &Arc, ) -> Result { if cell_data.references_count() != refs.len() { fail!("References count mismatch: {} != {}", cell_data.references_count(), refs.len()); @@ -324,7 +298,7 @@ impl PartialEq for StoredCell { pub struct StoringCell { cell_data: CellData, references: parking_lot::RwLock>, - boc_db: Weak, + boc_db: Weak, } impl PartialEq for StoringCell { @@ -334,7 +308,7 @@ impl PartialEq for StoringCell { } impl StoringCell { - pub fn with_cell(cell: &dyn CellImpl, boc_db: &Arc) -> Result { + pub fn with_cell(cell: &dyn CellImpl, boc_db: &Arc) -> Result { let references_count = cell.references_count(); let mut references = Vec::with_capacity(references_count); for i in 0..references_count { @@ -462,7 +436,7 @@ define_CellImpl!(StoringCell); fn reference( index: usize, references: &parking_lot::RwLock>, - boc_db: &Weak, + boc_db: &Weak, repr_hash: &dyn Fn() -> UInt256, ) -> Result> { let hash = { diff --git a/src/node/storage/src/types/tests/test_storage_cell.rs b/src/node/storage/src/types/tests/test_storage_cell.rs index bb2c531..8767dfa 100644 --- a/src/node/storage/src/types/tests/test_storage_cell.rs +++ b/src/node/storage/src/types/tests/test_storage_cell.rs @@ -21,12 +21,13 @@ use ton_block::{create_cell, BuilderData, IBitstring}; const DB_PATH: &str = "../../target/test"; -async fn init_cell_db(db_name: &str) -> Result> { +async fn init_boc_db(db_name: &str) -> Result> { destroy_rocks_db(DB_PATH, db_name).await?; let db = RocksDb::new(DB_PATH, db_name, None, AccessType::ReadWrite)?; - Ok(Arc::new(CellDb::with_db( + Ok(Arc::new(DynamicBocDb::with_db( db.clone(), "cells", + "counters", DB_PATH, &CellsDbConfig::default(), #[cfg(feature = "telemetry")] @@ -37,7 +38,7 @@ async fn init_cell_db(db_name: &str) -> Result> { #[tokio::test] async fn test_storage_cell_serde() -> Result<()> { - let cell_db = init_cell_db("test_storage_cell_serde").await?; + let boc_db = init_boc_db("test_storage_cell_serde").await?; let c1 = create_cell(vec![], &[1, 2, 45, 76, 200])?; let c2 = create_cell(vec![], &[10, 200, 45, 7, 20])?; @@ -51,20 +52,20 @@ async fn test_storage_cell_serde() -> Result<()> { b.append_u16(47)?; let c4 = b.into_cell()?; - let s1 = StoringCell::with_cell(c1.cell_impl().deref(), &cell_db)?; - let s2 = StoringCell::with_cell(c2.cell_impl().deref(), &cell_db)?; - let s3 = StoringCell::with_cell(c3.cell_impl().deref(), &cell_db)?; - let s4 = StoringCell::with_cell(c4.cell_impl().deref(), &cell_db)?; + let s1 = StoringCell::with_cell(c1.cell_impl().deref(), &boc_db)?; + let s2 = StoringCell::with_cell(c2.cell_impl().deref(), &boc_db)?; + let s3 = StoringCell::with_cell(c3.cell_impl().deref(), &boc_db)?; + let s4 = StoringCell::with_cell(c4.cell_impl().deref(), &boc_db)?; let d1 = StoredCell::serialize(&s1)?; let d2 = StoredCell::serialize(&s2)?; let d3 = StoredCell::serialize(&s3)?; let d4 = StoredCell::serialize(&s4)?; - assert!(s1.cell_data == StoredCell::deserialize(&cell_db, &c1.repr_hash(), &d1)?.cell_data); - assert!(s2.cell_data == StoredCell::deserialize(&cell_db, &c2.repr_hash(), &d2)?.cell_data); - assert!(s3.cell_data == StoredCell::deserialize(&cell_db, &c3.repr_hash(), &d3)?.cell_data); - assert!(s4.cell_data == StoredCell::deserialize(&cell_db, &c4.repr_hash(), &d4)?.cell_data); + assert!(s1.cell_data == StoredCell::deserialize(&boc_db, &c1.repr_hash(), &d1)?.cell_data); + assert!(s2.cell_data == StoredCell::deserialize(&boc_db, &c2.repr_hash(), &d2)?.cell_data); + assert!(s3.cell_data == StoredCell::deserialize(&boc_db, &c3.repr_hash(), &d3)?.cell_data); + assert!(s4.cell_data == StoredCell::deserialize(&boc_db, &c4.repr_hash(), &d4)?.cell_data); Ok(()) } diff --git a/src/node/tests/compat_test/.gitignore b/src/node/tests/compat_test/.gitignore deleted file mode 100644 index 5d3aa6f..0000000 --- a/src/node/tests/compat_test/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Build artifacts -/build/ -/target/ -Cargo.lock - -# Temporary test databases -/tmp/ diff --git a/src/node/tests/compat_test/Cargo.toml b/src/node/tests/compat_test/Cargo.toml deleted file mode 100644 index 26adde0..0000000 --- a/src/node/tests/compat_test/Cargo.toml +++ /dev/null @@ -1,79 +0,0 @@ -[package] -name = "compat_test" -version = "0.1.0" -edition = "2021" -description = "Cross-implementation compatibility tests for ADNL/overlay" -publish = false - -[features] -default = [] -telemetry = ["adnl/telemetry"] - -[dependencies] -adnl = { path = "../../../adnl" } -ton_api = { path = "../../../tl/ton_api" } -ton_block = { path = "../../../block" } - -tokio = { version = "1", features = ["full", "process", "io-util"] } -tokio-util = "0.7" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -base64 = "0.22" -hex = "0.4" -rand = "0.8" -log = "0.4" -env_logger = "0.11" -thiserror = "1" -async-trait = "0.1" -anyhow = "1" - -[dev-dependencies] -tokio-test = "0.4" - -[[test]] -name = "test_overlay_id" -path = "tests/test_overlay_id.rs" - -[[test]] -name = "test_broadcast" -path = "tests/test_broadcast.rs" - -[[test]] -name = "test_public_overlay" -path = "tests/test_public_overlay.rs" - -[[test]] -name = "test_broadcast_validation" -path = "tests/test_broadcast_validation.rs" - -[[test]] -name = "test_overlay_message" -path = "tests/test_overlay_message.rs" - -[[test]] -name = "test_boc_compression" -path = "tests/test_boc_compression.rs" - -[[test]] -name = "test_candidate_id_to_sign" -path = "tests/test_candidate_id_to_sign.rs" - -[[test]] -name = "test_fec_relay" -path = "tests/test_fec_relay.rs" - -[[test]] -name = "test_twostep_fec_relay" -path = "tests/test_twostep_fec_relay.rs" - -[[test]] -name = "test_rldp_query" -path = "tests/test_rldp_query.rs" - -[[test]] -name = "test_quic_transport" -path = "tests/test_quic_transport.rs" - -[[test]] -name = "test_quic_overlay" -path = "tests/test_quic_overlay.rs" diff --git a/src/node/tests/compat_test/Makefile b/src/node/tests/compat_test/Makefile deleted file mode 100644 index f0f87a8..0000000 --- a/src/node/tests/compat_test/Makefile +++ /dev/null @@ -1,112 +0,0 @@ -# Cross-Implementation Compatibility Tests Makefile - -# Check if CPP_SRC_PATH is set -ifndef CPP_SRC_PATH -$(error CPP_SRC_PATH is not set. Usage: CPP_SRC_PATH=/path/to/ton-cpp-testnet make test) -endif - -# Directories -ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -WORKSPACE_ROOT := $(ROOT_DIR)/../../.. -BUILD_DIR := $(ROOT_DIR)/build -CPP_BUILD_DIR ?= $(BUILD_DIR)/cpp -RUST_TARGET_DIR := $(BUILD_DIR)/rust - -# C++ source files location -CPP_TEST_SRC := $(ROOT_DIR)/cpp_src - -# Output binary -CPP_TEST_BIN := $(CPP_BUILD_DIR)/compat_test_node - -# CMake settings -CMAKE_BUILD_TYPE ?= Release -CMAKE_JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - -# Rust test settings -RUST_TEST_THREADS ?= 1 -TEST ?= - -.PHONY: all build build-cpp build-rust test test-rust clean help check-cpp-path - -all: build - -help: - @echo "Cross-Implementation Compatibility Tests" - @echo "" - @echo "Usage: CPP_SRC_PATH=/path/to/ton-cpp-testnet make " - @echo "" - @echo "Targets:" - @echo " build - Build both C++ and Rust components" - @echo " build-cpp - Build C++ test harness only" - @echo " build-rust - Build Rust test code only" - @echo " test - Run all compatibility tests" - @echo " test-rust - Run Rust tests (requires C++ binary)" - @echo " clean - Remove build artifacts" - @echo " help - Show this help message" - @echo "" - @echo "Environment Variables:" - @echo " CPP_SRC_PATH - Path to C++ TON source (required)" - @echo " CPP_BUILD_DIR - C++ build directory (default: ./build/cpp)" - @echo " CMAKE_BUILD_TYPE - CMake build type (default: Release)" - @echo " TEST - Specific test to run (optional)" - @echo " RUST_LOG - Rust log level (optional)" - -check-cpp-path: - @if [ ! -d "$(CPP_SRC_PATH)/adnl" ]; then \ - echo "Error: $(CPP_SRC_PATH) does not appear to be a valid TON C++ source directory"; \ - echo "Expected to find $(CPP_SRC_PATH)/adnl"; \ - exit 1; \ - fi - @echo "Using C++ source: $(CPP_SRC_PATH)" - -build: build-cpp build-rust - -build-cpp: check-cpp-path - @echo "Building C++ test harness..." - @echo "Note: TON C++ libraries must be pre-built in $(CPP_SRC_PATH)/build" - @echo "If not built, run: cd $(CPP_SRC_PATH) && mkdir -p build && cd build && cmake .. && make overlay adnl dht" - @mkdir -p $(CPP_BUILD_DIR) - @cd $(CPP_BUILD_DIR) && cmake \ - -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \ - -DTON_SRC_PATH=$(CPP_SRC_PATH) \ - $(CPP_TEST_SRC) - @cd $(CPP_BUILD_DIR) && cmake --build . -j$(CMAKE_JOBS) - @mkdir -p $(CPP_TEST_SRC)/build - @cp $(CPP_TEST_BIN) $(CPP_TEST_SRC)/build/compat_test_node - @if [ "$$(uname)" = "Darwin" ]; then codesign --force --sign - $(CPP_TEST_SRC)/build/compat_test_node; fi - @echo "C++ test harness built: $(CPP_TEST_BIN)" - -build-rust: - @echo "Building Rust test code..." - @cd $(WORKSPACE_ROOT) && cargo build --package compat_test - @echo "Rust test code built" - -test: build test-rust - -test-rust: - @if [ ! -f "$(CPP_TEST_BIN)" ]; then \ - echo "Error: C++ test binary not found at $(CPP_TEST_BIN)"; \ - echo "Run 'make build-cpp' first"; \ - exit 1; \ - fi - @echo "Running compatibility tests..." - @cd $(WORKSPACE_ROOT) && \ - CPP_COMPAT_TEST_BIN=$(CPP_TEST_BIN) \ - RUST_TEST_THREADS=$(RUST_TEST_THREADS) \ - RUST_BACKTRACE=1 \ - cargo test --package compat_test \ - $(if $(TEST),--test $(TEST),) - -clean: - @echo "Cleaning build artifacts..." - @rm -rf $(BUILD_DIR) - @echo "Clean complete" - -# Development helpers -.PHONY: fmt clippy - -fmt: - @cd $(WORKSPACE_ROOT) && cargo fmt --package compat_test - -clippy: - @cd $(WORKSPACE_ROOT) && cargo clippy --package compat_test diff --git a/src/node/tests/compat_test/README.md b/src/node/tests/compat_test/README.md deleted file mode 100644 index 375ae2b..0000000 --- a/src/node/tests/compat_test/README.md +++ /dev/null @@ -1,281 +0,0 @@ -# Cross-Implementation Compatibility Tests - -This directory contains tests that verify compatibility between the Rust ADNL/overlay implementation and the C++ reference implementation. - -## Prerequisites - -1. **C++ TON source code**: You need access to the C++ TON node source code (ton-cpp-testnet) -2. **Pre-built C++ libraries**: The C++ TON libraries must be built before running tests -3. **C++ build dependencies**: CMake, C++ compiler (with C++20 support), OpenSSL, ZLIB, etc. -4. **Rust toolchain**: cargo, rustc - -## Directory Structure - -``` -compat_test/ -โ”œโ”€โ”€ README.md # This file -โ”œโ”€โ”€ Cargo.toml # Rust package manifest -โ”œโ”€โ”€ Makefile # Build and test automation -โ”œโ”€โ”€ incompatibilities.md # Detailed compatibility report and found bugs -โ”œโ”€โ”€ cpp_src/ # C++ test harness source code -โ”‚ โ”œโ”€โ”€ CMakeLists.txt -โ”‚ โ”œโ”€โ”€ compat_test_node.cpp -โ”‚ โ””โ”€โ”€ compat_test_node.hpp -โ”œโ”€โ”€ src/ # Rust library code -โ”‚ โ”œโ”€โ”€ lib.rs # CppTestNode wrapper and JSON protocol -โ”‚ โ”œโ”€โ”€ overlay_id.rs # Overlay ID computation helpers -โ”‚ โ””โ”€โ”€ test_helpers.rs # RustTestNode, RustQuicTestNode, and test utilities -โ”œโ”€โ”€ tests/ # Rust integration tests -โ”‚ โ”œโ”€โ”€ test_overlay_id.rs # Overlay ID computation compatibility -โ”‚ โ”œโ”€โ”€ test_broadcast.rs # Broadcast delivery (small + FEC, both directions) -โ”‚ โ”œโ”€โ”€ test_broadcast_validation.rs # 2-phase broadcast accept/reject callbacks -โ”‚ โ”œโ”€โ”€ test_public_overlay.rs # Overlay query/response compatibility -โ”‚ โ”œโ”€โ”€ test_overlay_message.rs # Point-to-point overlay messages -โ”‚ โ”œโ”€โ”€ test_boc_compression.rs # BOC compression interoperability -โ”‚ โ”œโ”€โ”€ test_candidate_id_to_sign.rs # Consensus candidate ID TL serialization -โ”‚ โ”œโ”€โ”€ test_rldp_query.rs # RLDP v1/v2 query/response (multiple sizes) -โ”‚ โ”œโ”€โ”€ test_fec_relay.rs # FEC broadcast relay (3-node topology) -โ”‚ โ”œโ”€โ”€ test_twostep_fec_relay.rs # TwostepFec broadcast relay (6-node topology) -โ”‚ โ”œโ”€โ”€ test_quic_transport.rs # QUIC transport: raw queries, large messages, TLS -โ”‚ โ”œโ”€โ”€ test_quic_overlay.rs # QUIC overlay: messages and queries via QUIC -โ”‚ โ””โ”€โ”€ test_quic_private_overlay.rs # QUIC private overlay: ADNL vs QUIC transport -โ””โ”€โ”€ build/ # Build artifacts (gitignored) - โ””โ”€โ”€ cpp/ # C++ binary output -``` - -## Usage - -### Building C++ TON Libraries (One-time Setup) - -Before running tests, you must build the C++ TON libraries: - -```bash -cd /path/to/ton-cpp-testnet -mkdir -p build && cd build -cmake .. -cmake --build . --target overlay adnl dht tl_api keys keyring fec rldp rldp2 tdutils tdactor tdnet ton_crypto -``` - -### Running All Tests - -```bash -CPP_SRC_PATH=/path/to/ton-cpp-testnet make test -``` - -### Running Specific Test Suite - -```bash -CPP_SRC_PATH=/path/to/ton-cpp-testnet make test TEST=test_broadcast -``` - -### Building Only - -```bash -CPP_SRC_PATH=/path/to/ton-cpp-testnet make build -``` - -### Cleaning Build Artifacts - -```bash -make clean -``` - -## Compatibility Status - -| Test Suite | Tests | Pass | Ignored | Status | -|------------|-------|------|---------|--------| -| `test_overlay_id` | 4 | 4 | 0 | Compatible | -| `test_broadcast` | 4 | 4 | 0 | Compatible | -| `test_broadcast_validation` | 4 | 4 | 0 | Compatible | -| `test_public_overlay` | 2 | 2 | 0 | Compatible | -| `test_overlay_message` | 5 | 4 | 1 | 1 ignored (Safe/RLDP) | -| `test_boc_compression` | 4 | 4 | 0 | Compatible | -| `test_candidate_id_to_sign` | 2 | 2 | 0 | Compatible | -| `test_rldp_query` | 8 | 8 | 0 | Compatible | -| `test_fec_relay` | 4 | 4 | 0 | Compatible | -| `test_twostep_fec_relay` | 4 | 4 | 0 | Compatible | -| `test_quic_transport` | 3 | 3 | 0 | Compatible | -| `test_quic_overlay` | 4 | 4 | 0 | Compatible | -| `test_quic_private_overlay` | 5 | 5 | 0 | Compatible | -| **Total** | **53** | **52** | **1** | | - -## Test Suites - -### 1. Overlay ID (`test_overlay_id`) -- Rust and C++ compute identical overlay short IDs for various name formats (ASCII, binary, Unicode) -- C++ harness infrastructure checks (ping, ADNL ID) - -### 2. Broadcasts (`test_broadcast`) -Small (inline) and FEC-encoded (2KB) broadcasts in both directions: - -| Test | Direction | Payload | Result | -|------|-----------|---------|--------| -| `test_broadcast_rust_to_cpp` | Rust โ†’ C++ | small (26 B) | PASS | -| `test_broadcast_cpp_to_rust` | C++ โ†’ Rust | small (25 B) | PASS | -| `test_fec_broadcast_rust_to_cpp` | Rust โ†’ C++ | FEC (2 KB) | PASS | -| `test_fec_broadcast_cpp_to_rust` | C++ โ†’ Rust | FEC (2 KB) | PASS | - -### 3. Broadcast Validation (`test_broadcast_validation`) -- 2-phase `check_broadcast` accept/reject callback (Rust sender โ†’ C++ receiver) -- Accept mode: broadcast delivered to application layer -- Reject mode: broadcast dropped, not delivered -- Validator mode toggling - -### 4. Overlay Queries (`test_public_overlay`) -- Query/response echo roundtrip (C++ โ†’ Rust) -- Query rejection behavior (Rust โ†’ C++, expects timeout โ€” C++ drops rejected queries silently) - -### 5. Overlay Messages (`test_overlay_message`) -Point-to-point overlay messages (the same path used by simplex consensus for votes and certificates): - -| Test | Direction | What | Result | -|------|-----------|------|--------| -| `test_overlay_message_cpp_to_rust` | C++ โ†’ Rust | Single message, receipt verified | PASS | -| `test_overlay_message_rust_to_cpp` | Rust โ†’ C++ | Single message via Fast/UDP | PASS | -| `test_overlay_message_rust_to_cpp_safe` | Rust โ†’ C++ | Single message via Safe/RLDP | IGNORED | -| `test_overlay_message_burst_rust_to_cpp` | Rust โ†’ C++ | 20 messages, โ‰ฅ90% delivery | PASS | -| `test_overlay_message_cpp_to_cpp_baseline` | C++ โ†” C++ | Baseline (no Rust) | PASS | - -### 6. BOC Compression (`test_boc_compression`) -- Bidirectional compress/decompress with `BaselineLZ4` and `ImprovedStructureLZ4` algorithms -- Three cell topologies: single cell, tree with shared refs (DAG), simple tree -- Full round-trip (Rust compress โ†’ C++ decompress โ†’ C++ compress โ†’ Rust decompress) -- Multi-root BOC in both directions - -### 7. Candidate ID Signing (`test_candidate_id_to_sign`) -- TL serialization byte match for `consensus.candidateId` across 4 (slot, hash) combos -- Negative check: verifies C++ signs `candidateId` directly, not `candidateParent` wrapper - -### 8. RLDP Query/Response (`test_rldp_query`) -Both RLDP v1 and v2, both directions, three payload sizes: - -| Test | Sender | Responder | RLDP | Payload | Result | -|------|--------|-----------|------|---------|--------| -| `test_rldp_v1_rust_to_cpp` | Rust | C++ | v1 | 256 B | PASS | -| `test_rldp_v1_cpp_to_rust` | C++ | Rust | v1 | 256 B | PASS | -| `test_rldp_v2_rust_to_cpp` | Rust | C++ | v2 | 256 B | PASS | -| `test_rldp_v2_cpp_to_rust` | C++ | Rust | v2 | 256 B | PASS | -| `test_rldp_v2_4kb_rust_to_cpp` | Rust | C++ | v2 | 4 KB | PASS | -| `test_rldp_v2_4kb_cpp_to_rust` | C++ | Rust | v2 | 4 KB | PASS | -| `test_rldp_v2_7kb_rust_to_cpp` | Rust | C++ | v2 | 7 KB | PASS | -| `test_rldp_v2_7kb_cpp_to_rust` | C++ | Rust | v2 | 7 KB | PASS | - -**Note**: Query data must be a valid TL-serialized object (not raw bytes) because Rust's `deserialize_boxed_bundle` requires it. - -**Payload size constraints**: -- RLDP v1 `default_mtu` = 1024 bytes โ€” limits unsolicited incoming transfers to ~928 bytes of user data -- RLDP v2 `DEFAULT_MTU` = 7680 bytes โ€” allows up to ~7.5 KB without configuration -- C++ overlay `huge_packet_max_size()` = 8192 bytes โ€” hard limit on query data before RLDP wrapping - -### 9. FEC Relay (`test_fec_relay`) -3-node linear topology (Sender โ†’ Relay โ†’ Receiver), sender and receiver NOT directly connected. Broadcast size: 2000 bytes (triggers FEC encoding at >768 bytes): - -| Test | Sender | Relay | Receiver | Result | -|------|--------|-------|----------|--------| -| `test_fec_relay_rust_cpp_rust` | Rust | C++ | Rust | PASS | -| `test_fec_relay_cpp_rust_cpp` | C++ | Rust | C++ | PASS | -| `test_fec_relay_rust_rust_cpp` | Rust | Rust | C++ | PASS | -| `test_fec_relay_cpp_cpp_rust` | C++ | C++ | Rust | PASS | - -### 10. TwostepFec Relay (`test_twostep_fec_relay`) -6-node topology (Sender โ†’ 4 Bridges โ†’ Leaf), leaf NOT directly connected to sender. Broadcast size: 2048 bytes (>= 513 bytes triggers TwostepFec with FEC encoding): - -| Test | Sender | Bridges | Leaf | Result | -|------|--------|---------|------|--------| -| `test_twostep_rust_sender_cpp_leaf` | Rust | 4 Rust | C++ | PASS | -| `test_twostep_cpp_sender_rust_leaf` | C++ | 4 C++ | Rust | PASS | -| `test_twostep_mixed_bridges_rust_leaf` | Rust | 2 Rust + 2 C++ | Rust | PASS | -| `test_twostep_mixed_bridges_cpp_leaf` | C++ | 2 Rust + 2 C++ | C++ | PASS | - -### 11. QUIC Transport (`test_quic_transport`) -- Raw QUIC query echo (C++ โ†’ Rust) with TL-serialized payload -- Large overlay message via QUIC (900B, near C++ 1024-byte per-stream limit) -- QUIC connection establishment โ€” TLS handshake with RPK (Raw Public Key) certificates - -### 12. QUIC Overlay (`test_quic_overlay`) -- C++ โ†” C++ QUIC overlay message baseline -- Overlay message via QUIC (Rust โ†’ C++, with UDP baseline comparison) -- Raw QUIC message delivery (C++ โ†’ Rust) -- Overlay query via QUIC (Rust โ†’ C++) with echo handler - -### 13. QUIC Private Overlay (`test_quic_private_overlay`) -- Private overlay message via ADNL (baseline) -- Private overlay message via QUIC transport (Rust โ†’ C++) -- QUIC message burst (20 messages, 100% delivery required โ€” stream-based, no UDP loss) -- QUIC overlay query (Rust โ†’ C++) -- Private overlay message (C++ โ†’ Rust, with receipt verification) - -## Environment Variables - -| Variable | Description | Required | -|----------|-------------|----------| -| `CPP_SRC_PATH` | Path to C++ TON source (ton-cpp-testnet) | Yes | -| `CPP_BUILD_DIR` | Path for C++ test binary build (default: `./build/cpp`) | No | -| `CMAKE_BUILD_TYPE` | CMake build type (default: `Release`) | No | -| `TEST` | Specific test suite name filter | No | -| `RUST_LOG` | Rust logging level (e.g., `debug`, `trace`) | No | -| `RUST_TEST_THREADS` | Number of test threads (default: `1` for serial execution) | No | - -## C++ Test Node Protocol - -The C++ test node (`compat_test_node`) communicates via JSON over stdin/stdout: - -```json -// Commands: -{"cmd": "ping"} -{"cmd": "add_peer", "pubkey": "BASE64_TL_PUBKEY", "ip": "127.0.0.1", "port": 14001} -{"cmd": "create_overlay", "type": "private", "overlay_name": "BASE64", "peers": ["ADNL_ID_HEX"]} -{"cmd": "send_broadcast", "overlay_id": "HEX", "data": "BASE64", "use_fec": false} -{"cmd": "send_message", "overlay_id": "HEX", "peer_adnl_id": "HEX", "data": "BASE64"} -{"cmd": "set_broadcast_validator", "overlay_id": "HEX", "mode": "accept_all|reject_all"} -{"cmd": "set_query_handler", "overlay_id": "HEX", "mode": "echo|reject"} -{"cmd": "get_received_broadcasts", "overlay_id": "HEX"} -{"cmd": "get_received_messages", "overlay_id": "HEX"} -{"cmd": "send_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", "data": "BASE64", "timeout_ms": 5000} -{"cmd": "send_rldp_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", "data": "BASE64", "max_answer_size": 1048576, "v2": true} -{"cmd": "enable_quic"} -{"cmd": "send_quic_message", "peer_adnl_id": "HEX", "data": "BASE64"} -{"cmd": "send_quic_query", "peer_adnl_id": "HEX", "data": "BASE64", "timeout_ms": 5000} -{"cmd": "shutdown"} - -// Responses: -{"result": ...} -{"error": "..."} -``` - -## Port Ranges - -Tests use different port ranges to avoid conflicts: -- `test_overlay_id`: 14010-14019 -- `test_broadcast`: 15100-15149 -- `test_public_overlay`: 15150-15199 -- `test_broadcast_validation`: 15300-15399 -- `test_overlay_message`: 15400-15499 -- `test_boc_compression`: 15500-15599 -- `test_fec_relay`: 15600-15699 -- `test_twostep_fec_relay`: 15700-15799 -- `test_rldp_query`: 15800-15899 -- `test_candidate_id_to_sign`: 15900-15909 -- `test_quic_transport`: 18000-18099 -- `test_quic_overlay`: 18100-18199 -- `test_quic_private_overlay`: 18200-18299 - -## Troubleshooting - -### C++ Build Fails -- Ensure C++ TON libraries are pre-built in `$CPP_SRC_PATH/build` -- Check that CMake can find OpenSSL and ZLIB -- Verify C++20 compiler support - -### Tests Timeout -- Ensure no firewall blocks UDP ports 14000-19000 on localhost -- Check that no other processes use the same ports -- Try increasing sleep durations in tests if running on slow hardware - -### "broadcast source certificate is invalid" -This error in C++ logs indicates the overlay privacy rules are too restrictive. The test node should use `AllowFec` flag without `Trusted` to enable 2-phase validation. - -### Overlay ID Mismatch -If Rust and C++ compute different overlay IDs: -- Verify the overlay name bytes are identical (check base64 encoding) -- Ensure both use the TL `pub.overlay{name}` wrapper before hashing diff --git a/src/node/tests/compat_test/cpp_src/CMakeLists.txt b/src/node/tests/compat_test/cpp_src/CMakeLists.txt deleted file mode 100644 index f0f2749..0000000 --- a/src/node/tests/compat_test/cpp_src/CMakeLists.txt +++ /dev/null @@ -1,258 +0,0 @@ -cmake_minimum_required(VERSION 3.16) -project(compat_test_node) - -# Require paths to be set -if(NOT DEFINED TON_SRC_PATH) - message(FATAL_ERROR "TON_SRC_PATH must be defined. Pass -DTON_SRC_PATH=/path/to/ton-cpp-testnet") -endif() - -if(NOT DEFINED TON_BUILD_PATH) - # Default to build directory inside source - set(TON_BUILD_PATH "${TON_SRC_PATH}/build") -endif() - -if(NOT EXISTS "${TON_SRC_PATH}/adnl") - message(FATAL_ERROR "TON_SRC_PATH (${TON_SRC_PATH}) does not appear to be a valid TON source directory") -endif() - -if(NOT EXISTS "${TON_BUILD_PATH}") - message(FATAL_ERROR "TON_BUILD_PATH (${TON_BUILD_PATH}) does not exist. Please build TON first:\n" - " cd ${TON_SRC_PATH}\n" - " mkdir build && cd build\n" - " cmake ..\n" - " cmake --build . --target overlay adnl adnltest dht tl_api keys keyring fec rldp rldp2 tdutils tdactor tdnet ton_crypto") -endif() - -message(STATUS "Using TON source: ${TON_SRC_PATH}") -message(STATUS "Using TON build: ${TON_BUILD_PATH}") - -# Set C++ standard (TON requires C++20 for 'requires' expressions) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Find required packages (same as TON uses) -find_package(OpenSSL REQUIRED) -find_package(ZLIB REQUIRED) -find_package(Threads REQUIRED) - -# Our test binary -add_executable(compat_test_node - compat_test_node.cpp -) - -# Include directories from TON source -target_include_directories(compat_test_node PRIVATE - ${TON_SRC_PATH} - ${TON_SRC_PATH}/tdutils - ${TON_SRC_PATH}/tdactor - ${TON_SRC_PATH}/tdnet - ${TON_SRC_PATH}/crypto - ${TON_SRC_PATH}/tl - ${TON_SRC_PATH}/tl-utils - ${TON_SRC_PATH}/keys - ${TON_SRC_PATH}/keyring - ${TON_SRC_PATH}/adnl - ${TON_SRC_PATH}/overlay - ${TON_SRC_PATH}/rldp - ${TON_SRC_PATH}/rldp2 - ${TON_SRC_PATH}/fec - ${TON_SRC_PATH}/quic - ${TON_SRC_PATH}/metrics - ${TON_SRC_PATH}/tddb - ${TON_SRC_PATH}/third-party/abseil-cpp - ${TON_SRC_PATH}/third-party/ngtcp2/lib/includes - ${TON_SRC_PATH}/third-party/ngtcp2/crypto/includes - ${TON_BUILD_PATH}/third-party/ngtcp2/lib/includes - ${TON_SRC_PATH}/tl/generate # For auto-generated TL headers - ${TON_BUILD_PATH} - ${TON_BUILD_PATH}/tdutils -) - -# Find libraries in TON build directory -set(TON_LIB_DIRS - ${TON_BUILD_PATH}/overlay - ${TON_BUILD_PATH}/adnl - ${TON_BUILD_PATH}/dht - ${TON_BUILD_PATH}/rldp - ${TON_BUILD_PATH}/rldp2 - ${TON_BUILD_PATH}/fec - ${TON_BUILD_PATH}/tl - ${TON_BUILD_PATH}/tl-utils - ${TON_BUILD_PATH}/keys - ${TON_BUILD_PATH}/keyring - ${TON_BUILD_PATH}/tdutils - ${TON_BUILD_PATH}/tdactor - ${TON_BUILD_PATH}/tdnet - ${TON_BUILD_PATH}/tddb - ${TON_BUILD_PATH}/tddb/td/db - ${TON_BUILD_PATH}/tdfec - ${TON_BUILD_PATH}/tdfec/td/fec - ${TON_BUILD_PATH}/crypto - ${TON_BUILD_PATH}/common - ${TON_BUILD_PATH}/quic - ${TON_BUILD_PATH}/metrics - ${TON_BUILD_PATH}/third-party/crc32c - ${TON_BUILD_PATH}/third-party/libraptorq - ${TON_BUILD_PATH}/third-party/rocksdb - ${TON_BUILD_PATH}/third-party/ngtcp2/lib - ${TON_BUILD_PATH}/third-party/ngtcp2/crypto/ossl -) - -# Function to find a library in TON build dirs -function(find_ton_library VAR_NAME LIB_NAME) - find_library(${VAR_NAME} - NAMES ${LIB_NAME} - PATHS ${TON_LIB_DIRS} - PATH_SUFFIXES Release Debug RelWithDebInfo - NO_DEFAULT_PATH - ) - if(NOT ${VAR_NAME}) - message(FATAL_ERROR "Could not find TON library: ${LIB_NAME}\n" - "Make sure you have built TON with: cmake --build . --target ${LIB_NAME}") - endif() - message(STATUS "Found ${LIB_NAME}: ${${VAR_NAME}}") -endfunction() - -# Find TON libraries -find_ton_library(TON_OVERLAY overlay) -find_ton_library(TON_ADNL adnl) -find_ton_library(TON_DHT dht) -find_ton_library(TON_RLDP rldp) -find_ton_library(TON_RLDP2 rldp2) -find_ton_library(TON_FEC fec) -find_ton_library(TON_TL_API tl_api) -find_ton_library(TON_TL_UTILS tl-utils) -find_ton_library(TON_KEYS keys) -find_ton_library(TON_KEYRING keyring) -find_ton_library(TON_TDUTILS tdutils) -find_ton_library(TON_TDACTOR tdactor) -find_ton_library(TON_TDNET tdnet) -find_ton_library(TON_TDDB tddb) -find_ton_library(TON_TDFEC tdfec) -find_ton_library(TON_CRYPTO ton_crypto) -find_ton_library(TON_CRYPTO_CORE ton_crypto_core) -find_ton_library(TON_COMMON common) -find_ton_library(TON_TON_BLOCK ton_block) -find_ton_library(TON_CRC32C crc32c) -find_ton_library(TON_ROCKSDB rocksdb) -find_ton_library(TON_QUIC quic) -find_ton_library(TON_METRICS metrics) - -# ngtcp2 libraries (required by quic) -find_library(NGTCP2_LIB - NAMES ngtcp2 - PATHS ${TON_BUILD_PATH}/third-party/ngtcp2/lib - NO_DEFAULT_PATH -) -find_library(NGTCP2_CRYPTO_OSSL_LIB - NAMES ngtcp2_crypto_ossl - PATHS ${TON_BUILD_PATH}/third-party/ngtcp2/crypto/ossl - NO_DEFAULT_PATH -) - -# Optional: RaptorQ library (for FEC) -find_library(RAPTORQ_LIB - NAMES RaptorQ - PATHS ${TON_BUILD_PATH}/third-party/libraptorq - NO_DEFAULT_PATH -) - -target_link_libraries(compat_test_node PRIVATE - ${TON_QUIC} - ${TON_METRICS} - ${TON_OVERLAY} - ${TON_ADNL} - ${TON_DHT} - ${TON_RLDP} - ${TON_RLDP2} - ${TON_FEC} - ${TON_TDFEC} - ${TON_TL_API} - ${TON_TL_UTILS} - ${TON_KEYS} - ${TON_KEYRING} - ${TON_COMMON} - ${TON_TON_BLOCK} - ${TON_TDDB} - ${TON_TDUTILS} - ${TON_TDACTOR} - ${TON_TDNET} - ${TON_CRYPTO} - ${TON_CRYPTO_CORE} - ${TON_CRC32C} - ${TON_ROCKSDB} - OpenSSL::Crypto - OpenSSL::SSL - ZLIB::ZLIB - Threads::Threads -) - -# Add ngtcp2 if found (required by quic) -if(NGTCP2_LIB) - target_link_libraries(compat_test_node PRIVATE ${NGTCP2_LIB}) -endif() -if(NGTCP2_CRYPTO_OSSL_LIB) - target_link_libraries(compat_test_node PRIVATE ${NGTCP2_CRYPTO_OSSL_LIB}) -endif() - -# Add RaptorQ if found -if(RAPTORQ_LIB) - target_link_libraries(compat_test_node PRIVATE ${RAPTORQ_LIB}) -endif() - -# Platform-specific settings -if(APPLE) - target_link_libraries(compat_test_node PRIVATE - "-framework Security" - "-framework CoreFoundation" - "-framework CoreServices" - ) - target_link_libraries(compat_test_node PRIVATE c++) -elseif(UNIX) - target_link_libraries(compat_test_node PRIVATE dl) -endif() - -# Add additional libraries that TON depends on -find_library(LZ4_LIBRARY lz4) -if(LZ4_LIBRARY) - target_link_libraries(compat_test_node PRIVATE ${LZ4_LIBRARY}) -endif() - -find_library(SODIUM_LIBRARY sodium) -if(SODIUM_LIBRARY) - target_link_libraries(compat_test_node PRIVATE ${SODIUM_LIBRARY}) -endif() - -# blst (BLS crypto, required by ton_crypto) -find_library(BLST_LIBRARY - NAMES blst - PATHS ${TON_BUILD_PATH}/third-party/blst - NO_DEFAULT_PATH -) -if(BLST_LIBRARY) - target_link_libraries(compat_test_node PRIVATE ${BLST_LIBRARY}) -endif() - -# secp256k1 (required by ton_crypto_core) -find_library(SECP256K1_LIBRARY - NAMES secp256k1 - PATHS ${TON_BUILD_PATH}/third-party/secp256k1/lib - NO_DEFAULT_PATH -) -if(SECP256K1_LIBRARY) - target_link_libraries(compat_test_node PRIVATE ${SECP256K1_LIBRARY}) -endif() - -# Abseil libraries (required by ton_block hash containers) -file(GLOB_RECURSE ABSEIL_LIBS "${TON_BUILD_PATH}/third-party/abseil-cpp/absl/*.a") -if(ABSEIL_LIBS) - target_link_libraries(compat_test_node PRIVATE ${ABSEIL_LIBS}) -endif() - -# macOS: re-sign binary to avoid dyld hang caused by com.apple.provenance xattr -if(APPLE) - add_custom_command(TARGET compat_test_node POST_BUILD - COMMAND codesign --force --sign - $ - COMMENT "Re-signing binary for macOS" - ) -endif() diff --git a/src/node/tests/compat_test/cpp_src/compat_test_node.cpp b/src/node/tests/compat_test/cpp_src/compat_test_node.cpp deleted file mode 100644 index 896ac78..0000000 --- a/src/node/tests/compat_test/cpp_src/compat_test_node.cpp +++ /dev/null @@ -1,1302 +0,0 @@ -/* - * Cross-Implementation Compatibility Test Node - * - * A minimal ADNL/overlay node for testing compatibility between rust and cpp implementations. - * - * Controlled via stdin/stdout by end of line terminated JSON: - * - * {"cmd": "ping"} - * {"cmd": "get_info"} - * {"cmd": "compute_overlay_id", "name": "BASE64_BYTES"} - * {"cmd": "add_peer", "pubkey": "BASE64_TL_PUBKEY", "ip": "127.0.0.1", "port": 14001, "quic_port": 0} - * {"cmd": "create_overlay", "type": "public|private|semiprivate", - * "overlay_name": "BASE64_TL_BYTES", - * "peers": ["ADNL_ID_HEX", ...], - * "root_pub_keys": ["HEX", ...], "certificate": "BASE64_TL", "max_slaves": 5} - * {"cmd": "delete_overlay", "overlay_id": "HEX"} - * {"cmd": "get_overlay_node_info", "overlay_id": "HEX"} - * {"cmd": "send_broadcast", "overlay_id": "HEX", "data": "BASE64", "use_fec": false} - * {"cmd": "send_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", - * "data": "BASE64", "timeout_ms": 5000} - * {"cmd": "send_rldp_query", "overlay_id": "HEX", "peer_adnl_id": "HEX", - * "data": "BASE64", "max_answer_size": 1048576, "v2": false} - * {"cmd": "set_query_handler", "overlay_id": "HEX", "mode": "echo|capabilities|reject"} - * {"cmd": "set_broadcast_validator", "overlay_id": "HEX", "mode": "accept_all|reject_all"} - * {"cmd": "get_received_broadcasts", "overlay_id": "HEX"} - * {"cmd": "clear_received_broadcasts", "overlay_id": "HEX"} - * {"cmd": "compute_candidate_id_to_sign", "slot": 1, "hash": "HEX_64"} - * {"cmd": "compress_boc", "data": "BASE64_STANDARD_BOC", "algorithm": "baseline|improved"} - * {"cmd": "decompress_boc", "data": "BASE64_COMPRESSED", "max_size": 10485760} - * {"cmd": "shutdown"} - */ - -#include "compat_test_node.hpp" - -#include "td/utils/port/Stat.h" -#include "td/utils/port/path.h" -#include "td/utils/Random.h" -#include "crypto/Ed25519.h" -#include "vm/boc.h" -#include "vm/boc-compression.h" - -#include -#include - -namespace compat_test { - -// ---------- Helpers ---------- - -CompatTestNode::CompatTestNode(Config config) : config_(std::move(config)) {} - -std::string CompatTestNode::get_string(td::JsonObject &obj, const std::string &key) { - for (auto &kv : obj.field_values_) { - if (kv.first == key && kv.second.type() == td::JsonValue::Type::String) { - return kv.second.get_string().str(); - } - } - return ""; -} - -bool CompatTestNode::get_bool(td::JsonObject &obj, const std::string &key, bool def) { - for (auto &kv : obj.field_values_) { - if (kv.first == key && kv.second.type() == td::JsonValue::Type::Boolean) { - return kv.second.get_boolean(); - } - } - return def; -} - -td::int64 CompatTestNode::get_int(td::JsonObject &obj, const std::string &key, td::int64 def) { - for (auto &kv : obj.field_values_) { - if (kv.first == key && kv.second.type() == td::JsonValue::Type::Number) { - return td::to_integer(kv.second.get_number()); - } - } - return def; -} - -void CompatTestNode::respond(const std::string &json) { - std::cout << json << std::endl; - std::cout.flush(); -} - -void CompatTestNode::respond_ok() { - respond("{\"result\": \"ok\"}"); -} - -void CompatTestNode::respond_ok(const std::string &extra_fields) { - respond("{\"result\": {" + extra_fields + "}}"); -} - -void CompatTestNode::respond_error(const std::string &msg) { - // Escape quotes in msg - std::string escaped; - for (char c : msg) { - if (c == '"') escaped += "\\\""; - else if (c == '\\') escaped += "\\\\"; - else if (c == '\n') escaped += "\\n"; - else escaped += c; - } - respond("{\"error\": \"" + escaped + "\"}"); -} - -// ---------- Startup ---------- - -void CompatTestNode::start_up() { - LOG(INFO) << "Starting compat test node on UDP port " << config_.udp_port; - - // Create database directory - td::mkdir(config_.db_path).ignore(); - - // Generate local key - local_privkey_ = ton::PrivateKey{ton::privkeys::Ed25519::random()}; - local_pubkey_ = local_privkey_.compute_public_key(); - local_id_short_ = ton::adnl::AdnlNodeIdShort{local_pubkey_.compute_short_id()}; - - LOG(INFO) << "Local ADNL ID: " << local_id_short_.bits256_value().to_hex(); - - // Create keyring - keyring_ = ton::keyring::Keyring::create(config_.db_path + "/keyring"); - - // Add local key to keyring - generate a new key for keyring since we can't clone - auto keyring_privkey = ton::PrivateKey{ton::privkeys::Ed25519::random()}; - // Actually, we need to use the same key. Let's re-generate with the same approach - // and store export/import to share between local and keyring - auto key_slice = local_privkey_.export_as_slice(); - auto key_import = ton::PrivateKey::import(key_slice.as_slice()); - CHECK(key_import.is_ok()); - td::actor::send_closure(keyring_, &ton::keyring::Keyring::add_key, - key_import.move_as_ok(), true, - td::PromiseCreator::lambda([](td::Result) {})); - - // Create network manager with real UDP - network_manager_ = ton::adnl::AdnlNetworkManager::create(config_.udp_port); - - // Register self address on network manager - td::IPAddress self_addr; - self_addr.init_ipv4_port("127.0.0.1", config_.udp_port).ensure(); - - ton::adnl::AdnlCategoryMask cat_mask; - cat_mask[0] = true; - td::actor::send_closure(network_manager_, &ton::adnl::AdnlNetworkManager::add_self_addr, - self_addr, std::move(cat_mask), 0); - - // Create ADNL instance - adnl_ = ton::adnl::Adnl::create(config_.db_path, keyring_.get()); - - // Register network manager with ADNL - td::actor::send_closure(adnl_, &ton::adnl::Adnl::register_network_manager, - network_manager_.get()); - - // Build proper address list for our identity - ton::adnl::AdnlAddressList addr_list; - addr_list.add_udp_adnl_address(self_addr).ensure(); - addr_list.set_version(static_cast(td::Clocks::system())); - addr_list.set_reinit_date(ton::adnl::Adnl::adnl_start_time()); - - // Add local ID to ADNL with proper address list - td::actor::send_closure(adnl_, &ton::adnl::Adnl::add_id, - ton::adnl::AdnlNodeIdFull{local_pubkey_}, - std::move(addr_list), static_cast(0)); - - // Create RLDP v1 and v2 - rldp_ = ton::rldp::Rldp::create(adnl_.get()); - rldp2_ = ton::rldp2::Rldp::create(adnl_.get()); - td::actor::send_closure(rldp_, &ton::rldp::Rldp::add_id, local_id_short_); - td::actor::send_closure(rldp2_, &ton::rldp2::Rldp::add_id, local_id_short_); - - // Create overlay manager (without DHT for direct peering) - overlays_ = ton::overlay::Overlays::create(config_.db_path, keyring_.get(), - adnl_.get(), td::actor::ActorId{}); - - // Setup stdin polling - alarm_timestamp() = td::Timestamp::in(0.1); - - // Output ready message - auto pubkey_tl = local_pubkey_.tl(); - auto pubkey_serialized = ton::serialize_tl_object(pubkey_tl, true); - auto pubkey_b64 = td::base64_encode(pubkey_serialized.as_slice()); - std::ostringstream oss; - oss << "{\"status\": \"ready\"" - << ", \"adnl_id\": \"" << local_id_short_.bits256_value().to_hex() << "\"" - << ", \"pubkey\": \"" << pubkey_b64 << "\"" - << ", \"udp_port\": " << config_.udp_port - << "}"; - respond(oss.str()); -} - -void CompatTestNode::tear_down() { - LOG(INFO) << "Shutting down compat test node"; -} - -void CompatTestNode::alarm() { - process_stdin(); - alarm_timestamp() = td::Timestamp::in(0.1); -} - -// ---------- Control interface ---------- - -void CompatTestNode::process_stdin() { - fd_set readfds; - FD_ZERO(&readfds); - FD_SET(STDIN_FILENO, &readfds); - - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 0; - - if (select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &tv) > 0) { - std::string line; - if (std::getline(std::cin, line)) { - handle_command(line); - } else { - // stdin closed - LOG(INFO) << "stdin closed, shutting down"; - stop(); - } - } -} - -void CompatTestNode::handle_command(std::string cmd_line) { - if (cmd_line.empty()) return; - - LOG(INFO) << "CMD: " << cmd_line; - - auto json_res = td::json_decode(cmd_line); - if (json_res.is_error()) { - respond_error("Invalid JSON: " + json_res.error().message().str()); - return; - } - - auto &json = json_res.ok_ref(); - if (json.type() != td::JsonValue::Type::Object) { - respond_error("Expected JSON object"); - return; - } - - auto &obj = json.get_object(); - auto cmd = get_string(obj, "cmd"); - - if (cmd.empty()) { - respond_error("Missing 'cmd' field"); - return; - } - - if (cmd == "ping") { - respond("{\"result\": \"pong\"}"); - } else if (cmd == "get_info") { - cmd_get_info(obj); - } else if (cmd == "compute_overlay_id") { - cmd_compute_overlay_id(obj); - } else if (cmd == "add_peer") { - cmd_add_peer(obj); - } else if (cmd == "create_overlay") { - cmd_create_overlay(obj); - } else if (cmd == "delete_overlay") { - cmd_delete_overlay(obj); - } else if (cmd == "get_overlay_node_info") { - cmd_get_overlay_node_info(obj); - } else if (cmd == "send_broadcast") { - cmd_send_broadcast(obj); - } else if (cmd == "send_query") { - cmd_send_query(obj); - } else if (cmd == "send_rldp_query") { - cmd_send_rldp_query(obj); - } else if (cmd == "set_query_handler") { - cmd_set_query_handler(obj); - } else if (cmd == "set_broadcast_validator") { - cmd_set_broadcast_validator(obj); - } else if (cmd == "get_received_broadcasts") { - cmd_get_received_broadcasts(obj); - } else if (cmd == "clear_received_broadcasts") { - cmd_clear_received_broadcasts(obj); - } else if (cmd == "send_message") { - cmd_send_message(obj); - } else if (cmd == "get_received_messages") { - cmd_get_received_messages(obj); - } else if (cmd == "clear_received_messages") { - cmd_clear_received_messages(obj); - } else if (cmd == "compute_candidate_id_to_sign") { - cmd_compute_candidate_id_to_sign(obj); - } else if (cmd == "compress_boc") { - cmd_compress_boc(obj); - } else if (cmd == "decompress_boc") { - cmd_decompress_boc(obj); - } else if (cmd == "enable_quic") { - cmd_enable_quic(obj); - } else if (cmd == "send_quic_message") { - cmd_send_quic_message(obj); - } else if (cmd == "send_quic_query") { - cmd_send_quic_query(obj); - } else if (cmd == "shutdown") { - respond("{\"result\": \"shutting_down\"}"); - std::_Exit(0); // Force immediate exit - } else { - respond_error("Unknown command: " + cmd); - } -} - -// ---------- Command implementations ---------- - -void CompatTestNode::cmd_get_info(td::JsonObject &obj) { - auto pubkey_tl = local_pubkey_.tl(); - auto pubkey_serialized = ton::serialize_tl_object(pubkey_tl, true); - auto pubkey_b64 = td::base64_encode(pubkey_serialized.as_slice()); - std::ostringstream oss; - oss << "{\"result\": {" - << "\"adnl_id\": \"" << local_id_short_.bits256_value().to_hex() << "\"" - << ", \"pubkey\": \"" << pubkey_b64 << "\"" - << ", \"udp_port\": " << config_.udp_port - << "}}"; - respond(oss.str()); -} - -void CompatTestNode::cmd_compute_overlay_id(td::JsonObject &obj) { - auto name_b64 = get_string(obj, "name"); - if (name_b64.empty()) { - respond_error("Missing 'name' field (base64)"); - return; - } - auto name_res = td::base64_decode(name_b64); - if (name_res.is_error()) { - respond_error("Invalid base64 name"); - return; - } - auto name = name_res.move_as_ok(); - - ton::overlay::OverlayIdFull full_id{td::BufferSlice(name)}; - auto short_id = full_id.compute_short_id(); - - std::ostringstream oss; - oss << "{\"result\": {" - << "\"overlay_id\": \"" << short_id.bits256_value().to_hex() << "\"" - << "}}"; - respond(oss.str()); -} - -void CompatTestNode::cmd_add_peer(td::JsonObject &obj) { - auto pubkey_b64 = get_string(obj, "pubkey"); - auto ip = get_string(obj, "ip"); - auto port = static_cast(get_int(obj, "port")); - - if (pubkey_b64.empty() || ip.empty() || port == 0) { - respond_error("Missing 'pubkey', 'ip', or 'port'"); - return; - } - - auto pubkey_res = td::base64_decode(pubkey_b64); - if (pubkey_res.is_error()) { - respond_error("Invalid base64 pubkey"); - return; - } - auto pk_res = ton::PublicKey::import(pubkey_res.ok()); - if (pk_res.is_error()) { - respond_error("Invalid pubkey format: " + pk_res.error().message().str()); - return; - } - auto pk = pk_res.move_as_ok(); - - td::IPAddress addr; - auto addr_res = addr.init_ipv4_port(ip, port); - if (addr_res.is_error()) { - respond_error("Invalid address: " + addr_res.message().str()); - return; - } - - // Build address list for the peer - ton::adnl::AdnlAddressList peer_addr_list; - peer_addr_list.add_udp_adnl_address(addr).ensure(); - auto quic_port = static_cast(get_int(obj, "quic_port")); - if (quic_port != 0) { - td::IPAddress quic_addr; - quic_addr.init_ipv4_port(ip, quic_port).ensure(); - peer_addr_list.add_quic_addr(quic_addr).ensure(); - } - peer_addr_list.set_version(static_cast(td::Clocks::system())); - peer_addr_list.set_reinit_date(ton::adnl::Adnl::adnl_start_time()); - - auto peer_id = ton::adnl::AdnlNodeIdFull{pk}; - auto peer_short = peer_id.compute_short_id(); - - td::actor::send_closure(adnl_, &ton::adnl::Adnl::add_peer, - local_id_short_, peer_id, std::move(peer_addr_list)); - - respond_ok("\"peer_id\": \"" + peer_short.bits256_value().to_hex() + "\""); -} - -void CompatTestNode::cmd_create_overlay(td::JsonObject &obj) { - auto type = get_string(obj, "type"); - auto overlay_name_b64 = get_string(obj, "overlay_name"); - - if (type.empty()) { - respond_error("Missing 'type' (public|private|semiprivate)"); - return; - } - if (overlay_name_b64.empty()) { - respond_error("Missing 'overlay_name' (base64 TL bytes)"); - return; - } - - auto name_res = td::base64_decode(overlay_name_b64); - if (name_res.is_error()) { - respond_error("Invalid base64 overlay_name"); - return; - } - auto name = name_res.move_as_ok(); - - ton::overlay::OverlayIdFull id_full{td::BufferSlice(name)}; - auto id_short = id_full.compute_short_id(); - - LOG(INFO) << "Creating " << type << " overlay: " << id_short.bits256_value().to_hex(); - - auto callback = make_overlay_callback(id_short); - - // Use permissive rules: allow broadcasts up to 32MB with AllowFec flag. - // NOTE: We do NOT set CertificateFlags::Trusted here because that would skip - // the check_broadcast callback entirely. Without Trusted, all broadcasts go - // through the 2-phase validation (check_broadcast callback). - td::uint32 max_bcast_size = 32 << 20; // 32 MB - td::uint32 privacy_flags = ton::overlay::CertificateFlags::AllowFec; - - if (type == "public") { - ton::overlay::OverlayOptions opts; - opts.announce_self_ = false; // No DHT - - ton::overlay::OverlayPrivacyRules rules{max_bcast_size, privacy_flags, {}}; - td::actor::send_closure(overlays_, &ton::overlay::Overlays::create_public_overlay_ex, - local_id_short_, - id_full.clone(), - std::move(callback), - std::move(rules), - "compat_test", - opts); - } else if (type == "private") { - // Parse peer list - std::vector peers; - for (auto &kv : obj.field_values_) { - if (kv.first == "peers" && kv.second.type() == td::JsonValue::Type::Array) { - for (auto &p : kv.second.get_array()) { - if (p.type() == td::JsonValue::Type::String) { - td::Bits256 bits; - auto hex = p.get_string().str(); - if (bits.from_hex(hex) == 256) { - peers.push_back(ton::adnl::AdnlNodeIdShort{bits}); - } - } - } - break; - } - } - - ton::overlay::OverlayPrivacyRules rules{max_bcast_size, privacy_flags, {}}; - ton::overlay::OverlayOptions opts; - opts.announce_self_ = false; - - auto enable_twostep = get_bool(obj, "enable_twostep", false); - if (enable_twostep) { - opts.twostep_broadcast_sender_ = rldp2_.get(); - opts.send_twostep_broadcast_ = true; - LOG(INFO) << "TwostepFec enabled for private overlay"; - } - - td::actor::send_closure(overlays_, &ton::overlay::Overlays::create_private_overlay_ex, - local_id_short_, - id_full.clone(), - std::move(peers), - std::move(callback), - std::move(rules), - "compat_test", - std::move(opts)); - } else if (type == "semiprivate") { - // Parse peer list - std::vector peers; - for (auto &kv : obj.field_values_) { - if (kv.first == "peers" && kv.second.type() == td::JsonValue::Type::Array) { - for (auto &p : kv.second.get_array()) { - if (p.type() == td::JsonValue::Type::String) { - td::Bits256 bits; - auto hex = p.get_string().str(); - if (bits.from_hex(hex) == 256) { - peers.push_back(ton::adnl::AdnlNodeIdShort{bits}); - } - } - } - break; - } - } - - // Parse root public key hashes - std::vector root_keys; - for (auto &kv : obj.field_values_) { - if (kv.first == "root_pub_keys" && kv.second.type() == td::JsonValue::Type::Array) { - for (auto &r : kv.second.get_array()) { - if (r.type() == td::JsonValue::Type::String) { - td::Bits256 bits; - auto hex = r.get_string().str(); - if (bits.from_hex(hex) == 256) { - root_keys.push_back(ton::PublicKeyHash{bits}); - } - } - } - break; - } - } - - // Parse certificate - ton::overlay::OverlayMemberCertificate cert; - auto cert_b64 = get_string(obj, "certificate"); - if (!cert_b64.empty()) { - auto cert_res = td::base64_decode(cert_b64); - if (cert_res.is_ok()) { - auto cert_data = cert_res.move_as_ok(); - auto tl_res = ton::fetch_tl_object( - td::BufferSlice(cert_data), true); - if (tl_res.is_ok()) { - cert = ton::overlay::OverlayMemberCertificate(tl_res.ok().get()); - } - } - } - - auto max_slaves = static_cast(get_int(obj, "max_slaves", 5)); - - ton::overlay::OverlayOptions opts; - opts.announce_self_ = false; - opts.max_slaves_in_semiprivate_overlay_ = max_slaves; - - ton::overlay::OverlayPrivacyRules rules{max_bcast_size, privacy_flags, {}}; - td::actor::send_closure(overlays_, &ton::overlay::Overlays::create_semiprivate_overlay, - local_id_short_, - id_full.clone(), - std::move(peers), - std::move(root_keys), - std::move(cert), - std::move(callback), - std::move(rules), - "compat_test", - opts); - } else { - respond_error("Unknown overlay type: " + type); - return; - } - - // Store overlay state - OverlayState state; - state.id_full = std::move(id_full); - state.id_short = id_short; - state.type = type; - overlay_states_[id_short] = std::move(state); - - respond_ok("\"overlay_id\": \"" + id_short.bits256_value().to_hex() + "\""); -} - -void CompatTestNode::cmd_delete_overlay(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto id_short = ton::overlay::OverlayIdShort{bits}; - - td::actor::send_closure(overlays_, &ton::overlay::Overlays::delete_overlay, - local_id_short_, id_short); - overlay_states_.erase(id_short); - respond_ok(); -} - -void CompatTestNode::cmd_get_overlay_node_info(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - respond_error("Overlay not found"); - return; - } - - // Build OverlayNode, sign it via keyring, serialize as TL - auto node = ton::overlay::OverlayNode{local_id_short_, overlay_id, 0}; - auto to_sign = node.to_sign(); - - td::actor::send_closure( - keyring_, &ton::keyring::Keyring::sign_add_get_public_key, - local_id_short_.pubkey_hash(), std::move(to_sign), - [SelfId = actor_id(this), overlay_id]( - td::Result> R) { - if (R.is_error()) { - td::actor::send_closure(SelfId, &CompatTestNode::respond_error, - "Failed to sign: " + R.error().message().str()); - return; - } - auto V = R.move_as_ok(); - auto node = ton::overlay::OverlayNode{ - ton::adnl::AdnlNodeIdFull{V.second}, overlay_id, 0, - static_cast(td::Clocks::system()), V.first.as_slice()}; - auto tl = node.tl(); - auto serialized = ton::serialize_tl_object(tl, true); - auto b64 = td::base64_encode(serialized.as_slice()); - std::ostringstream oss; - oss << "{\"result\": {\"node_tl\": \"" << b64 << "\"}}"; - td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); - }); -} - -void CompatTestNode::cmd_send_broadcast(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - auto data_b64 = get_string(obj, "data"); - auto use_fec = get_bool(obj, "use_fec", false); - - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - if (data_b64.empty()) { - respond_error("Missing 'data'"); - return; - } - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data"); - return; - } - - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - auto data = td::BufferSlice(data_res.move_as_ok()); - - LOG(INFO) << "Sending " << (use_fec ? "FEC " : "") << "broadcast to overlay " - << overlay_id.bits256_value().to_hex() << " size=" << data.size(); - - if (use_fec) { - td::actor::send_closure(overlays_, &ton::overlay::Overlays::send_broadcast_fec_ex, - local_id_short_, overlay_id, local_id_short_.pubkey_hash(), - 0, std::move(data)); - } else { - td::actor::send_closure(overlays_, &ton::overlay::Overlays::send_broadcast_ex, - local_id_short_, overlay_id, local_id_short_.pubkey_hash(), - 0, std::move(data)); - } - respond_ok(); -} - -void CompatTestNode::cmd_send_query(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - auto peer_hex = get_string(obj, "peer_adnl_id"); - auto data_b64 = get_string(obj, "data"); - auto timeout_ms = get_int(obj, "timeout_ms", 5000); - - td::Bits256 overlay_bits, peer_bits; - if (overlay_bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - if (peer_bits.from_hex(peer_hex) != 256) { - respond_error("Invalid peer_adnl_id hex"); - return; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data"); - return; - } - - auto overlay_id = ton::overlay::OverlayIdShort{overlay_bits}; - auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; - auto data = td::BufferSlice(data_res.move_as_ok()); - auto timeout = td::Timestamp::in(timeout_ms / 1000.0); - - td::actor::send_closure( - overlays_, &ton::overlay::Overlays::send_query, - peer_id, local_id_short_, overlay_id, "compat_test_query", - td::PromiseCreator::lambda( - [SelfId = actor_id(this)](td::Result R) { - if (R.is_error()) { - td::actor::send_closure(SelfId, &CompatTestNode::respond_error, - "Query failed: " + R.error().message().str()); - return; - } - auto answer = R.move_as_ok(); - auto b64 = td::base64_encode(answer.as_slice()); - std::ostringstream oss; - oss << "{\"result\": {\"answer\": \"" << b64 << "\"}}"; - td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); - }), - timeout, std::move(data)); -} - -void CompatTestNode::cmd_send_rldp_query(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - auto peer_hex = get_string(obj, "peer_adnl_id"); - auto data_b64 = get_string(obj, "data"); - auto max_answer_size = static_cast(get_int(obj, "max_answer_size", 1 << 20)); - auto v2 = get_bool(obj, "v2", false); - - td::Bits256 overlay_bits, peer_bits; - if (overlay_bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - if (peer_bits.from_hex(peer_hex) != 256) { - respond_error("Invalid peer_adnl_id hex"); - return; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data"); - return; - } - - auto overlay_id = ton::overlay::OverlayIdShort{overlay_bits}; - auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; - auto data = td::BufferSlice(data_res.move_as_ok()); - auto timeout = td::Timestamp::in(10.0); - - auto promise = td::PromiseCreator::lambda( - [SelfId = actor_id(this)](td::Result R) { - if (R.is_error()) { - td::actor::send_closure(SelfId, &CompatTestNode::respond_error, - "RLDP query failed: " + R.error().message().str()); - return; - } - auto answer = R.move_as_ok(); - auto b64 = td::base64_encode(answer.as_slice()); - std::ostringstream oss; - oss << "{\"result\": {\"answer\": \"" << b64 << "\"}}"; - td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); - }); - - if (v2) { - td::actor::send_closure( - overlays_, &ton::overlay::Overlays::send_query_via, - peer_id, local_id_short_, overlay_id, "compat_rldp_query", - std::move(promise), - timeout, std::move(data), max_answer_size, - rldp2_.get()); - } else { - td::actor::send_closure( - overlays_, &ton::overlay::Overlays::send_query_via, - peer_id, local_id_short_, overlay_id, "compat_rldp_query", - std::move(promise), - timeout, std::move(data), max_answer_size, - rldp_.get()); - } -} - -void CompatTestNode::cmd_set_query_handler(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - auto mode = get_string(obj, "mode"); - - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - respond_error("Overlay not found"); - return; - } - - it->second.query_handler_mode = mode; - respond_ok(); -} - -void CompatTestNode::cmd_set_broadcast_validator(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - auto mode = get_string(obj, "mode"); - - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - respond_error("Overlay not found"); - return; - } - - it->second.broadcast_validator_mode = mode; - respond_ok(); -} - -void CompatTestNode::cmd_get_received_broadcasts(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - - LOG(INFO) << "get_received_broadcasts: overlay_hex='" << overlay_hex << "' len=" << overlay_hex.length(); - - td::Bits256 bits; - auto hex_result = bits.from_hex(overlay_hex); - if (hex_result != 256) { - LOG(INFO) << "from_hex returned " << hex_result << " (expected 64)"; - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - respond_error("Overlay not found"); - return; - } - - std::ostringstream oss; - oss << "{\"result\": ["; - bool first = true; - for (auto &b : it->second.received_broadcasts) { - if (!first) oss << ", "; - first = false; - oss << "{\"source\": \"" << b.source.bits256_value().to_hex() << "\"" - << ", \"size\": " << b.data.size() - << ", \"data\": \"" << td::base64_encode(td::Slice(b.data.data(), b.data.size())) << "\"" - << ", \"timestamp\": " << b.timestamp - << ", \"accepted\": " << (b.was_accepted ? "true" : "false") - << "}"; - } - oss << "]}"; - respond(oss.str()); -} - -void CompatTestNode::cmd_clear_received_broadcasts(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it != overlay_states_.end()) { - it->second.received_broadcasts.clear(); - } - respond_ok(); -} - -// ---------- Callback factory ---------- - -std::unique_ptr CompatTestNode::make_overlay_callback( - ton::overlay::OverlayIdShort overlay_id) { - return std::make_unique( - overlay_id, - [this, overlay_id](ton::PublicKeyHash src, td::BufferSlice data) { - on_broadcast_received(overlay_id, src, std::move(data)); - }, - [this, overlay_id](ton::adnl::AdnlNodeIdShort src, td::BufferSlice data, - td::Promise promise) { - on_query_received(overlay_id, src, std::move(data), std::move(promise)); - }, - [this, overlay_id](ton::PublicKeyHash src, td::BufferSlice data, - td::Promise promise) { - on_check_broadcast(overlay_id, src, std::move(data), std::move(promise)); - }, - [this, overlay_id](ton::adnl::AdnlNodeIdShort src, td::BufferSlice data) { - on_message_received(overlay_id, src, std::move(data)); - }); -} - -// ---------- Callback handlers ---------- - -void CompatTestNode::on_broadcast_received(ton::overlay::OverlayIdShort overlay_id, - ton::PublicKeyHash source, - td::BufferSlice data) { - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) return; - - ReceivedBroadcast record; - record.source = source; - record.overlay_id = overlay_id; - auto slice = data.as_slice(); - record.data = std::vector(slice.ubegin(), slice.uend()); - record.timestamp = static_cast(td::Clocks::system()); - record.was_accepted = true; - - it->second.received_broadcasts.push_back(std::move(record)); -} - -void CompatTestNode::on_query_received(ton::overlay::OverlayIdShort overlay_id, - ton::adnl::AdnlNodeIdShort src, - td::BufferSlice data, - td::Promise promise) { - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - promise.set_error(td::Status::Error("Overlay not found")); - return; - } - - it->second.received_queries.emplace_back( - src.bits256_value().to_hex(), data.size()); - - auto &mode = it->second.query_handler_mode; - if (mode == "echo") { - promise.set_value(std::move(data)); - } else if (mode == "capabilities") { - // Return a fixed capabilities response - std::string caps = "compat_test_cpp_node:v1"; - promise.set_value(td::BufferSlice(caps)); - } else if (mode == "reject") { - promise.set_error(td::Status::Error("Rejected by test query handler")); - } else { - // Default echo - promise.set_value(std::move(data)); - } -} - -void CompatTestNode::on_check_broadcast(ton::overlay::OverlayIdShort overlay_id, - ton::PublicKeyHash source, - td::BufferSlice data, - td::Promise promise) { - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - promise.set_value(td::Unit()); - return; - } - - auto &mode = it->second.broadcast_validator_mode; - if (mode == "accept_all") { - promise.set_value(td::Unit()); - } else if (mode == "reject_all") { - // Record the rejection - ReceivedBroadcast record; - record.source = source; - record.overlay_id = overlay_id; - auto slice = data.as_slice(); - record.data = std::vector(slice.ubegin(), slice.uend()); - record.timestamp = static_cast(td::Clocks::system()); - record.was_accepted = false; - it->second.received_broadcasts.push_back(std::move(record)); - - promise.set_error(td::Status::Error("Rejected by test broadcast validator")); - } else { - // Default: accept - promise.set_value(td::Unit()); - } -} - -// ---------- Message commands ---------- - -void CompatTestNode::on_message_received(ton::overlay::OverlayIdShort overlay_id, - ton::adnl::AdnlNodeIdShort source, - td::BufferSlice data) { - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) return; - - ReceivedMessage record; - record.source = source; - record.overlay_id = overlay_id; - auto slice = data.as_slice(); - record.data = std::vector(slice.ubegin(), slice.uend()); - record.timestamp = static_cast(td::Clocks::system()); - - it->second.received_messages.push_back(std::move(record)); - LOG(INFO) << "Message recorded for overlay " << overlay_id.bits256_value().to_hex() - << " from " << source.bits256_value().to_hex() - << " size=" << it->second.received_messages.back().data.size(); -} - -void CompatTestNode::cmd_send_message(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - auto peer_hex = get_string(obj, "peer_adnl_id"); - auto data_b64 = get_string(obj, "data"); - - td::Bits256 overlay_bits, peer_bits; - if (overlay_bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - if (peer_bits.from_hex(peer_hex) != 256) { - respond_error("Invalid peer_adnl_id hex"); - return; - } - if (data_b64.empty()) { - respond_error("Missing 'data'"); - return; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data"); - return; - } - - auto overlay_id = ton::overlay::OverlayIdShort{overlay_bits}; - auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; - auto data = td::BufferSlice(data_res.move_as_ok()); - - LOG(INFO) << "Sending message to overlay " << overlay_id.bits256_value().to_hex() - << " peer=" << peer_id.bits256_value().to_hex() - << " size=" << data.size(); - - td::actor::send_closure(overlays_, &ton::overlay::Overlays::send_message, - peer_id, local_id_short_, overlay_id, std::move(data)); - respond_ok(); -} - -void CompatTestNode::cmd_get_received_messages(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it == overlay_states_.end()) { - respond_error("Overlay not found"); - return; - } - - std::ostringstream oss; - oss << "{\"result\": ["; - bool first = true; - for (auto &m : it->second.received_messages) { - if (!first) oss << ", "; - first = false; - oss << "{\"source\": \"" << m.source.bits256_value().to_hex() << "\"" - << ", \"size\": " << m.data.size() - << ", \"data\": \"" << td::base64_encode(td::Slice(m.data.data(), m.data.size())) << "\"" - << ", \"timestamp\": " << m.timestamp - << "}"; - } - oss << "]}"; - respond(oss.str()); -} - -void CompatTestNode::cmd_clear_received_messages(td::JsonObject &obj) { - auto overlay_hex = get_string(obj, "overlay_id"); - - td::Bits256 bits; - if (bits.from_hex(overlay_hex) != 256) { - respond_error("Invalid overlay_id hex"); - return; - } - auto overlay_id = ton::overlay::OverlayIdShort{bits}; - - auto it = overlay_states_.find(overlay_id); - if (it != overlay_states_.end()) { - it->second.received_messages.clear(); - } - respond_ok(); -} - -void CompatTestNode::cmd_compute_candidate_id_to_sign(td::JsonObject &obj) { - auto slot = static_cast(get_int(obj, "slot", 0)); - auto hash_hex = get_string(obj, "hash"); - if (hash_hex.empty()) { - respond_error("Missing 'hash' (hex)"); - return; - } - - td::Bits256 hash_bits; - if (hash_bits.from_hex(hash_hex) != 256) { - respond_error("Invalid hash hex (expected 32 bytes)"); - return; - } - - // C++ simplex/catchain signs consensus.candidateId{slot,hash} directly. - auto tl = ton::create_tl_object(slot, hash_bits); - auto serialized = ton::serialize_tl_object(tl, true); - auto data_b64 = td::base64_encode(serialized.as_slice()); - - respond_ok("\"data\": \"" + data_b64 + "\""); -} - -// ---------- BOC Compression ---------- - -void CompatTestNode::cmd_compress_boc(td::JsonObject &obj) { - auto data_b64 = get_string(obj, "data"); - auto algorithm = get_string(obj, "algorithm"); - - if (data_b64.empty()) { - respond_error("Missing 'data' (base64 standard BOC)"); - return; - } - if (algorithm.empty()) { - algorithm = "baseline"; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data: " + data_res.error().message().str()); - return; - } - auto data = data_res.move_as_ok(); - - // Deserialize standard BOC to cell roots - auto roots_res = vm::std_boc_deserialize_multi(td::Slice(data)); - if (roots_res.is_error()) { - respond_error("Failed to deserialize BOC: " + roots_res.error().message().str()); - return; - } - auto roots = roots_res.move_as_ok(); - - // Determine algorithm - vm::CompressionAlgorithm algo; - if (algorithm == "baseline") { - algo = vm::CompressionAlgorithm::BaselineLZ4; - } else if (algorithm == "improved") { - algo = vm::CompressionAlgorithm::ImprovedStructureLZ4; - } else { - respond_error("Unknown algorithm: " + algorithm + " (use 'baseline' or 'improved')"); - return; - } - - // Compress - auto compressed_res = vm::boc_compress(roots, algo); - if (compressed_res.is_error()) { - respond_error("Compression failed: " + compressed_res.error().message().str()); - return; - } - auto compressed = compressed_res.move_as_ok(); - auto compressed_b64 = td::base64_encode(compressed.as_slice()); - - respond_ok("\"compressed\": \"" + compressed_b64 + "\""); -} - -void CompatTestNode::cmd_decompress_boc(td::JsonObject &obj) { - auto data_b64 = get_string(obj, "data"); - auto max_size = static_cast(get_int(obj, "max_size", 10 * 1024 * 1024)); - - if (data_b64.empty()) { - respond_error("Missing 'data' (base64 compressed BOC)"); - return; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data: " + data_res.error().message().str()); - return; - } - auto data = data_res.move_as_ok(); - - // Decompress - auto roots_res = vm::boc_decompress(td::Slice(data), max_size); - if (roots_res.is_error()) { - respond_error("Decompression failed: " + roots_res.error().message().str()); - return; - } - auto roots = roots_res.move_as_ok(); - - // Re-serialize as standard BOC - auto boc_res = vm::std_boc_serialize_multi(std::move(roots), 2); - if (boc_res.is_error()) { - respond_error("BOC re-serialization failed: " + boc_res.error().message().str()); - return; - } - auto boc = boc_res.move_as_ok(); - auto boc_b64 = td::base64_encode(boc.as_slice()); - - respond_ok("\"boc\": \"" + boc_b64 + "\""); -} - -// ---------- QUIC commands ---------- - -void CompatTestNode::cmd_enable_quic(td::JsonObject &obj) { - if (!quic_.empty()) { - respond_error("QUIC already enabled"); - return; - } - - auto peer_table = td::actor::actor_dynamic_cast(adnl_.get()); - if (peer_table.empty()) { - respond_error("ADNL peer table not available"); - return; - } - - quic_ = td::actor::create_actor("QuicSender", peer_table, keyring_.get()); - td::actor::send_closure(quic_, &ton::quic::QuicSender::add_id, local_id_short_); - - auto quic_port = config_.udp_port + 1000; - LOG(INFO) << "QUIC enabled, listening on port " << quic_port; - - respond_ok("\"quic_port\": " + std::to_string(quic_port)); -} - -void CompatTestNode::cmd_send_quic_message(td::JsonObject &obj) { - if (quic_.empty()) { - respond_error("QUIC not enabled. Call enable_quic first"); - return; - } - - auto peer_hex = get_string(obj, "peer_adnl_id"); - auto data_b64 = get_string(obj, "data"); - - td::Bits256 peer_bits; - if (peer_bits.from_hex(peer_hex) != 256) { - respond_error("Invalid peer_adnl_id hex"); - return; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data"); - return; - } - - auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; - auto data = td::BufferSlice(data_res.move_as_ok()); - - td::actor::send_closure(quic_, &ton::quic::QuicSender::send_message, - local_id_short_, peer_id, std::move(data)); - - respond_ok(); -} - -void CompatTestNode::cmd_send_quic_query(td::JsonObject &obj) { - if (quic_.empty()) { - respond_error("QUIC not enabled. Call enable_quic first"); - return; - } - - auto peer_hex = get_string(obj, "peer_adnl_id"); - auto data_b64 = get_string(obj, "data"); - auto timeout_ms = get_int(obj, "timeout_ms", 5000); - - td::Bits256 peer_bits; - if (peer_bits.from_hex(peer_hex) != 256) { - respond_error("Invalid peer_adnl_id hex"); - return; - } - - auto data_res = td::base64_decode(data_b64); - if (data_res.is_error()) { - respond_error("Invalid base64 data"); - return; - } - - auto peer_id = ton::adnl::AdnlNodeIdShort{peer_bits}; - auto data = td::BufferSlice(data_res.move_as_ok()); - auto timeout = td::Timestamp::in(timeout_ms / 1000.0); - - td::actor::send_closure( - quic_, &ton::quic::QuicSender::send_query, - local_id_short_, peer_id, std::string("compat_test_quic_query"), - td::PromiseCreator::lambda( - [SelfId = actor_id(this)](td::Result R) { - if (R.is_error()) { - td::actor::send_closure(SelfId, &CompatTestNode::respond_error, - "QUIC query failed: " + R.error().message().str()); - return; - } - auto answer = R.move_as_ok(); - auto b64 = td::base64_encode(answer.as_slice()); - std::ostringstream oss; - oss << "{\"result\": {\"answer\": \"" << b64 << "\"}}"; - td::actor::send_closure(SelfId, &CompatTestNode::respond, oss.str()); - }), - timeout, std::move(data)); -} - -} // namespace compat_test - -// ---------- Main ---------- - -int main(int argc, char** argv) { - SET_VERBOSITY_LEVEL(verbosity_INFO); - - td::uint16 udp_port = 14000; - std::string db_path = "/tmp/compat_test_node"; - - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - if (arg == "--port" && i + 1 < argc) { - udp_port = static_cast(std::stoi(argv[++i])); - } else if (arg == "--db" && i + 1 < argc) { - db_path = argv[++i]; - } else if (arg == "--help" || arg == "-h") { - std::cerr << "Usage: " << argv[0] << " [options]" << std::endl; - std::cerr << "Options:" << std::endl; - std::cerr << " --port PORT ADNL UDP listening port (default: 14000)" << std::endl; - std::cerr << " --db PATH Database path (default: /tmp/compat_test_node)" << std::endl; - return 0; - } - } - - td::actor::Scheduler scheduler({2}); - - compat_test::CompatTestNode::Config config; - config.udp_port = udp_port; - config.db_path = db_path; - - scheduler.run_in_context([&] { - td::actor::create_actor("compat_test_node", config).release(); - }); - - scheduler.run(); - - return 0; -} diff --git a/src/node/tests/compat_test/cpp_src/compat_test_node.hpp b/src/node/tests/compat_test/cpp_src/compat_test_node.hpp deleted file mode 100644 index 111d5fa..0000000 --- a/src/node/tests/compat_test/cpp_src/compat_test_node.hpp +++ /dev/null @@ -1,222 +0,0 @@ -#pragma once - -#include "adnl/adnl.h" -#include "adnl/adnl-network-manager.h" -#include "adnl/adnl-address-list.h" -#include "overlay/overlays.h" -#include "overlay/overlay-id.hpp" -#include "rldp/rldp.h" -#include "rldp2/rldp.h" -#include "quic-sender.h" -#include "keys/keys.hpp" -#include "keyring/keyring.h" -#include "td/actor/actor.h" -#include "td/utils/JsonBuilder.h" -#include "td/utils/base64.h" -#include "auto/tl/ton_api.h" -#include "auto/tl/ton_api_json.h" -#include "tl-utils/tl-utils.hpp" - -#include -#include -#include -#include -#include - -namespace compat_test { - -// Received broadcast record -struct ReceivedBroadcast { - ton::PublicKeyHash source; - ton::overlay::OverlayIdShort overlay_id; - std::vector data; - td::int32 timestamp; - bool was_accepted; -}; - -// Received message record (point-to-point overlay messages) -struct ReceivedMessage { - ton::adnl::AdnlNodeIdShort source; - ton::overlay::OverlayIdShort overlay_id; - std::vector data; - td::int32 timestamp; -}; - -// Per-overlay state -struct OverlayState { - ton::overlay::OverlayIdFull id_full; - ton::overlay::OverlayIdShort id_short; - std::string type; // "public", "private", "semiprivate" - std::string query_handler_mode = "echo"; // "echo", "capabilities" - std::string broadcast_validator_mode = "accept_all"; // "accept_all", "reject_all" - std::vector received_broadcasts; - std::vector received_messages; - std::vector> received_queries; // (from_hex, data_size) -}; - -// Overlay callback implementation for testing -class TestOverlayCallback : public ton::overlay::Overlays::Callback { -public: - using BroadcastHandler = std::function; - using QueryHandler = std::function)>; - using CheckBroadcastHandler = std::function)>; - using MessageHandler = std::function; - - TestOverlayCallback( - ton::overlay::OverlayIdShort overlay_id, - BroadcastHandler on_broadcast, - QueryHandler on_query, - CheckBroadcastHandler check_broadcast, - MessageHandler on_message = nullptr - ) : overlay_id_(overlay_id) - , on_broadcast_(std::move(on_broadcast)) - , on_query_(std::move(on_query)) - , check_broadcast_(std::move(check_broadcast)) - , on_message_(std::move(on_message)) {} - - void receive_message(ton::adnl::AdnlNodeIdShort src, - ton::overlay::OverlayIdShort overlay_id, - td::BufferSlice data) override { - LOG(INFO) << "MSG_RECEIVED overlay=" << overlay_id.bits256_value().to_hex() - << " src=" << src.bits256_value().to_hex() - << " size=" << data.size(); - if (on_message_) { - on_message_(src, std::move(data)); - } - } - - void receive_query(ton::adnl::AdnlNodeIdShort src, - ton::overlay::OverlayIdShort overlay_id, - td::BufferSlice data, - td::Promise promise) override { - LOG(INFO) << "QUERY_RECEIVED overlay=" << overlay_id.bits256_value().to_hex() - << " src=" << src.bits256_value().to_hex() - << " size=" << data.size(); - if (on_query_) { - on_query_(src, std::move(data), std::move(promise)); - } else { - // Default: echo back - promise.set_value(std::move(data)); - } - } - - void receive_broadcast(ton::PublicKeyHash src, - ton::overlay::OverlayIdShort overlay_id, - td::BufferSlice data) override { - LOG(INFO) << "BROADCAST_DELIVERED overlay=" << overlay_id.bits256_value().to_hex() - << " src=" << src.bits256_value().to_hex() - << " size=" << data.size(); - if (on_broadcast_) { - on_broadcast_(src, std::move(data)); - } - } - - void check_broadcast(ton::PublicKeyHash src, - ton::overlay::OverlayIdShort overlay_id, - td::BufferSlice data, - td::Promise promise) override { - LOG(INFO) << "CHECK_BROADCAST overlay=" << overlay_id.bits256_value().to_hex() - << " src=" << src.bits256_value().to_hex() - << " size=" << data.size(); - if (check_broadcast_) { - check_broadcast_(src, std::move(data), std::move(promise)); - } else { - promise.set_value(td::Unit()); - } - } - -private: - ton::overlay::OverlayIdShort overlay_id_; - BroadcastHandler on_broadcast_; - QueryHandler on_query_; - CheckBroadcastHandler check_broadcast_; - MessageHandler on_message_; -}; - -// Main test node actor -class CompatTestNode : public td::actor::Actor { -public: - struct Config { - td::uint16 udp_port = 14000; - std::string db_path = "/tmp/compat_test_node"; - }; - - explicit CompatTestNode(Config config); - - void start_up() override; - void tear_down() override; - void alarm() override; - -private: - Config config_; - - // ADNL components - td::actor::ActorOwn network_manager_; - td::actor::ActorOwn adnl_; - td::actor::ActorOwn keyring_; - td::actor::ActorOwn overlays_; - td::actor::ActorOwn rldp_; - td::actor::ActorOwn rldp2_; - td::actor::ActorOwn quic_; - - // Local identity - ton::PrivateKey local_privkey_; - ton::PublicKey local_pubkey_; - ton::adnl::AdnlNodeIdShort local_id_short_; - - // Active overlays - std::map overlay_states_; - - // Control interface - void process_stdin(); - void handle_command(std::string cmd_line); - - // Command handlers - void cmd_get_info(td::JsonObject &obj); - void cmd_compute_overlay_id(td::JsonObject &obj); - void cmd_add_peer(td::JsonObject &obj); - void cmd_create_overlay(td::JsonObject &obj); - void cmd_delete_overlay(td::JsonObject &obj); - void cmd_get_overlay_node_info(td::JsonObject &obj); - void cmd_send_broadcast(td::JsonObject &obj); - void cmd_send_query(td::JsonObject &obj); - void cmd_send_rldp_query(td::JsonObject &obj); - void cmd_set_query_handler(td::JsonObject &obj); - void cmd_set_broadcast_validator(td::JsonObject &obj); - void cmd_get_received_broadcasts(td::JsonObject &obj); - void cmd_clear_received_broadcasts(td::JsonObject &obj); - void cmd_send_message(td::JsonObject &obj); - void cmd_get_received_messages(td::JsonObject &obj); - void cmd_clear_received_messages(td::JsonObject &obj); - void cmd_compress_boc(td::JsonObject &obj); - void cmd_decompress_boc(td::JsonObject &obj); - void cmd_compute_candidate_id_to_sign(td::JsonObject &obj); - void cmd_enable_quic(td::JsonObject &obj); - void cmd_send_quic_message(td::JsonObject &obj); - void cmd_send_quic_query(td::JsonObject &obj); - - // Helpers - std::string get_string(td::JsonObject &obj, const std::string &key); - bool get_bool(td::JsonObject &obj, const std::string &key, bool def = false); - td::int64 get_int(td::JsonObject &obj, const std::string &key, td::int64 def = 0); - - std::unique_ptr make_overlay_callback(ton::overlay::OverlayIdShort overlay_id); - - void on_broadcast_received(ton::overlay::OverlayIdShort overlay_id, - ton::PublicKeyHash source, td::BufferSlice data); - void on_message_received(ton::overlay::OverlayIdShort overlay_id, - ton::adnl::AdnlNodeIdShort source, td::BufferSlice data); - void on_query_received(ton::overlay::OverlayIdShort overlay_id, - ton::adnl::AdnlNodeIdShort src, td::BufferSlice data, - td::Promise promise); - void on_check_broadcast(ton::overlay::OverlayIdShort overlay_id, - ton::PublicKeyHash source, td::BufferSlice data, - td::Promise promise); - - void respond(const std::string &json); - void respond_ok(); - void respond_ok(const std::string &extra_fields); - void respond_error(const std::string &msg); -}; - -} // namespace compat_test diff --git a/src/node/tests/compat_test/incompatibilities.md b/src/node/tests/compat_test/incompatibilities.md deleted file mode 100644 index aded1ef..0000000 --- a/src/node/tests/compat_test/incompatibilities.md +++ /dev/null @@ -1,62 +0,0 @@ -# Rust โ†” C++ Compatibility Test Results - -Cross-implementation compatibility testing between the Rust (`adnl` crate) and C++ (`ton-cpp-testnet`) overlay/ADNL implementations. - -## Known Issues - -### Safe/RLDP Overlay Message Delivery (BROKEN) - -Overlay messages sent via Safe/RLDP transport (TCP-like) from Rust to C++ are not delivered. RLDP queries work fine in both directions; only fire-and-forget `overlay.message()` via RLDP is affected. - -- **Test**: `test_overlay_message::test_overlay_message_rust_to_cpp_safe` (ignored) -- **Workaround**: Use Fast/UDP for overlay messages (works correctly) - -## Test Summary - -| Test Suite | Tests | Pass | Ignored | Status | -|------------|-------|------|---------|--------| -| `test_overlay_id` | 4 | 4 | 0 | Compatible | -| `test_broadcast` | 4 | 4 | 0 | Compatible | -| `test_broadcast_validation` | 4 | 4 | 0 | Compatible | -| `test_public_overlay` | 2 | 2 | 0 | Compatible | -| `test_overlay_message` | 5 | 4 | 1 | 1 ignored (Safe/RLDP) | -| `test_boc_compression` | 4 | 4 | 0 | Compatible | -| `test_candidate_id_to_sign` | 2 | 2 | 0 | Compatible | -| `test_rldp_query` | 8 | 8 | 0 | Compatible | -| `test_fec_relay` | 4 | 4 | 0 | Compatible | -| `test_twostep_fec_relay` | 4 | 4 | 0 | Compatible | -| `test_quic_transport` | 3 | 3 | 0 | Compatible | -| `test_quic_overlay` | 4 | 4 | 0 | Compatible | -| `test_quic_private_overlay` | 5 | 5 | 0 | Compatible | -| **Total** | **53** | **52** | **1** | | - -## What Is Tested - -- **Overlay ID computation** โ€” identical IDs from same inputs (ASCII, binary, Unicode) -- **Broadcasts** โ€” small (inline) and FEC-encoded (2KB), both directions -- **Broadcast validation** โ€” 2-phase accept/reject callback -- **Overlay queries** โ€” echo roundtrip and rejection behavior -- **Overlay messages** โ€” point-to-point delivery, burst (20 msgs, โ‰ฅ90% required) -- **BOC compression** โ€” bidirectional, 2 algorithms, multiple cell topologies, round-trip -- **Candidate ID signing** โ€” TL serialization byte match -- **RLDP v1/v2** โ€” query/response at 256B, 4KB, 7KB payloads, both directions -- **FEC relay** โ€” 3-node redistribution, all 4 Rust/C++ role combinations -- **TwostepFec relay** โ€” 6-node redistribution with mixed Rust/C++ bridges -- **QUIC transport** โ€” TLS/RPK handshake, raw queries, large messages (900B) -- **QUIC overlay** โ€” overlay messages and queries routed via QUIC -- **QUIC private overlay** โ€” ADNL vs QUIC transport, burst delivery (100% required) - -## Reproduction - -```bash -export CPP_SRC_PATH=/path/to/ton-cpp-testnet - -# Run all tests -make test - -# Run a specific test suite -make test TEST=test_broadcast - -# Run with verbose output -RUST_LOG=debug make test TEST=test_rldp_query -``` diff --git a/src/node/tests/compat_test/src/lib.rs b/src/node/tests/compat_test/src/lib.rs deleted file mode 100644 index 30828ef..0000000 --- a/src/node/tests/compat_test/src/lib.rs +++ /dev/null @@ -1,945 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Cross-Implementation Compatibility Test Library -//! -//! This crate provides utilities for testing compatibility between the rust -//! and cpp ADNL/overlay implementations. - -use base64::Engine; -use std::{ - io::{BufRead, BufReader, Write}, - path::Path, - process::{self, Child, ChildStdin, Command, Stdio}, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc::{channel, Receiver, RecvTimeoutError}, - Arc, - }, - thread::{self, JoinHandle}, - time::{Duration, Instant}, -}; - -pub mod overlay_id; -pub mod test_helpers; - -/// Error type for compatibility tests -#[derive(thiserror::Error, Debug)] -pub enum CompatTestError { - #[error("C++ binary not found: {0}")] - BinaryNotFound(String), - - #[error("C++ node failed to start: {0}")] - NodeStartFailed(String), - - #[error("Command failed: {0}")] - CommandFailed(String), - - #[error("Invalid response: {0}")] - InvalidResponse(String), - - #[error("Timeout waiting for response")] - Timeout, - - #[error("IO error: {0}")] - IoError(#[from] std::io::Error), - - #[error("JSON error: {0}")] - JsonError(#[from] serde_json::Error), - - #[error("Node not ready")] - NotReady, -} - -pub type Result = std::result::Result; - -/// Default paths to look for the C++ test binary -const DEFAULT_CPP_BINARY_PATHS: &[&str] = - &["cpp_src/build/compat_test_node", "../compat_test/cpp_src/build/compat_test_node"]; - -/// Timeout for waiting for C++ node to become ready -const DEFAULT_READY_TIMEOUT: Duration = Duration::from_secs(10); - -/// Timeout for individual command responses -const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(5); - -/// Check if C++ test binary is available -pub fn cpp_binary_available() -> bool { - get_cpp_binary_path().is_ok() -} - -/// Get path to C++ test binary -pub fn get_cpp_binary_path() -> Result { - // First check environment variable - if let Ok(path) = std::env::var("CPP_COMPAT_TEST_BIN") { - if Path::new(&path).exists() { - return Ok(path); - } - } - - // Try default paths relative to CARGO_MANIFEST_DIR - if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { - for rel_path in DEFAULT_CPP_BINARY_PATHS { - let full_path = Path::new(&manifest_dir).join(rel_path); - if full_path.exists() { - return Ok(full_path.to_string_lossy().to_string()); - } - } - } - - // Try default paths relative to current directory - for rel_path in DEFAULT_CPP_BINARY_PATHS { - if Path::new(rel_path).exists() { - return Ok(rel_path.to_string()); - } - } - - Err(CompatTestError::BinaryNotFound( - "C++ binary not found. Set CPP_COMPAT_TEST_BIN or build cpp_src/build/compat_test_node" - .to_string(), - )) -} - -fn b64_encode(data: &[u8]) -> String { - base64::engine::general_purpose::STANDARD.encode(data) -} - -fn b64_decode(s: &str) -> std::result::Result, base64::DecodeError> { - base64::engine::general_purpose::STANDARD.decode(s) -} - -/// Command to send to C++ node (JSON over stdin) -#[derive(Debug, Clone, serde::Serialize)] -#[serde(tag = "cmd")] -pub enum CppCommand { - #[serde(rename = "ping")] - Ping, - - #[serde(rename = "get_info")] - GetInfo, - - #[serde(rename = "compute_overlay_id")] - ComputeOverlayId { - /// base64-encoded overlay name bytes - name: String, - }, - - #[serde(rename = "add_peer")] - AddPeer { - /// base64-encoded TL-serialized public key - pubkey: String, - ip: String, - port: u16, - /// Optional explicit QUIC port (included as adnl.address.quic in address list) - #[serde(skip_serializing_if = "Option::is_none")] - quic_port: Option, - }, - - #[serde(rename = "create_overlay")] - CreateOverlay { - /// "public", "private", or "semiprivate" - #[serde(rename = "type")] - overlay_type: String, - /// base64-encoded overlay name bytes - overlay_name: String, - #[serde(skip_serializing_if = "Vec::is_empty")] - peers: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - root_pub_keys: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - certificate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - max_slaves: Option, - #[serde(skip_serializing_if = "std::ops::Not::not")] - #[serde(default)] - enable_twostep: bool, - }, - - #[serde(rename = "delete_overlay")] - DeleteOverlay { overlay_id: String }, - - #[serde(rename = "get_overlay_node_info")] - GetOverlayNodeInfo { overlay_id: String }, - - #[serde(rename = "send_broadcast")] - SendBroadcast { - overlay_id: String, - /// base64-encoded data - data: String, - #[serde(default)] - use_fec: bool, - }, - - #[serde(rename = "send_query")] - SendQuery { - overlay_id: String, - peer_adnl_id: String, - /// base64-encoded query data - data: String, - timeout_ms: i64, - }, - - #[serde(rename = "send_rldp_query")] - SendRldpQuery { - overlay_id: String, - peer_adnl_id: String, - /// base64-encoded query data - data: String, - max_answer_size: u64, - #[serde(default)] - v2: bool, - }, - - #[serde(rename = "set_query_handler")] - SetQueryHandler { - overlay_id: String, - /// "echo", "capabilities", or "reject" - mode: String, - }, - - #[serde(rename = "set_broadcast_validator")] - SetBroadcastValidator { - overlay_id: String, - /// "accept_all" or "reject_all" - mode: String, - }, - - #[serde(rename = "get_received_broadcasts")] - GetReceivedBroadcasts { overlay_id: String }, - - #[serde(rename = "clear_received_broadcasts")] - ClearReceivedBroadcasts { overlay_id: String }, - - #[serde(rename = "send_message")] - SendMessage { - overlay_id: String, - peer_adnl_id: String, - /// base64-encoded data - data: String, - }, - - #[serde(rename = "get_received_messages")] - GetReceivedMessages { overlay_id: String }, - - #[serde(rename = "clear_received_messages")] - ClearReceivedMessages { overlay_id: String }, - - #[serde(rename = "compress_boc")] - CompressBoc { - /// base64-encoded standard BOC data - data: String, - /// "baseline" or "improved" - algorithm: String, - }, - - #[serde(rename = "decompress_boc")] - DecompressBoc { - /// base64-encoded compressed BOC data - data: String, - /// Maximum decompressed size in bytes - max_size: u32, - }, - - #[serde(rename = "compute_candidate_id_to_sign")] - ComputeCandidateIdToSign { - slot: i32, - /// 32-byte candidate hash as hex - hash: String, - }, - - #[serde(rename = "enable_quic")] - EnableQuic {}, - - #[serde(rename = "send_quic_message")] - SendQuicMessage { - peer_adnl_id: String, - /// base64-encoded data - data: String, - }, - - #[serde(rename = "send_quic_query")] - SendQuicQuery { - peer_adnl_id: String, - /// base64-encoded data - data: String, - timeout_ms: i64, - }, - - #[serde(rename = "shutdown")] - Shutdown, -} - -/// Ready response from C++ node -#[derive(Debug, Clone, serde::Deserialize)] -pub struct ReadyResponse { - pub status: String, - pub adnl_id: String, - pub pubkey: String, - pub udp_port: u16, -} - -/// Response from C++ node -#[derive(Debug, Clone, serde::Deserialize)] -#[serde(untagged)] -pub enum CppResponse { - Ready(ReadyResponse), - Result { result: serde_json::Value }, - Error { error: String }, -} - -/// Received broadcast record from C++ node -#[derive(Debug, Clone, serde::Deserialize)] -pub struct ReceivedBroadcast { - pub source: String, - pub size: usize, - pub data: String, // base64 encoded - pub timestamp: i32, - pub accepted: bool, -} - -/// Received message record from C++ node (point-to-point overlay messages) -#[derive(Debug, Clone, serde::Deserialize)] -pub struct ReceivedMessage { - pub source: String, - pub size: usize, - pub data: String, // base64 encoded - pub timestamp: i32, -} - -/// Info about the C++ node -#[derive(Debug, Clone)] -pub struct NodeInfo { - pub adnl_id: String, - pub pubkey: String, - pub udp_port: u16, -} - -/// Handle to a running C++ test node -pub struct CppTestNode { - process: Child, - stdin: ChildStdin, - response_rx: Receiver, - _reader_thread: Option>, - info: NodeInfo, -} - -impl CppTestNode { - /// Spawn a new C++ test node on the given UDP port - pub fn spawn(udp_port: u16) -> Result { - let binary_path = get_cpp_binary_path()?; - - let db_path = format!("/tmp/compat_test_cpp_{}", udp_port); - - // Clean up old database - let _ = std::fs::remove_dir_all(&db_path); - - let mut process = Command::new(&binary_path) - .arg("--port") - .arg(udp_port.to_string()) - .arg("--db") - .arg(&db_path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .spawn() - .map_err(|e| CompatTestError::NodeStartFailed(e.to_string()))?; - - let stdin = process - .stdin - .take() - .ok_or_else(|| CompatTestError::NodeStartFailed("Failed to get stdin".to_string()))?; - let stdout = process - .stdout - .take() - .ok_or_else(|| CompatTestError::NodeStartFailed("Failed to get stdout".to_string()))?; - - // Spawn a reader thread to avoid blocking on stdout - let (tx, rx) = channel(); - let reader_thread = thread::spawn(move || { - let mut reader = BufReader::new(stdout); - loop { - let mut line = String::new(); - match reader.read_line(&mut line) { - Ok(0) => break, - Ok(_) => { - if tx.send(line).is_err() { - break; - } - } - Err(_) => break, - } - } - }); - - let mut node = Self { - process, - stdin, - response_rx: rx, - _reader_thread: Some(reader_thread), - info: NodeInfo { adnl_id: String::new(), pubkey: String::new(), udp_port }, - }; - - // Wait for ready message - node.wait_ready()?; - - Ok(node) - } - - /// Read one line from the response channel with a timeout - fn recv_line(&self, timeout: Duration) -> Result { - self.response_rx.recv_timeout(timeout).map_err(|e| match e { - RecvTimeoutError::Timeout => CompatTestError::Timeout, - RecvTimeoutError::Disconnected => CompatTestError::InvalidResponse( - "Reader thread disconnected (process may have crashed)".to_string(), - ), - }) - } - - /// Wait for the node to be ready - fn wait_ready(&mut self) -> Result<()> { - let line = self.recv_line(DEFAULT_READY_TIMEOUT).map_err(|e| { - CompatTestError::NodeStartFailed(format!( - "Timed out waiting for C++ node to become ready: {}", - e - )) - })?; - - let response: CppResponse = serde_json::from_str(&line)?; - - match response { - CppResponse::Ready(ready) => { - if ready.status != "ready" { - return Err(CompatTestError::NodeStartFailed(format!( - "Unexpected status: {}", - ready.status - ))); - } - self.info.adnl_id = ready.adnl_id; - self.info.pubkey = ready.pubkey; - self.info.udp_port = ready.udp_port; - Ok(()) - } - _ => Err(CompatTestError::NodeStartFailed(format!( - "Unexpected response: {:?}", - response - ))), - } - } - - /// Send a command and get response - pub fn send_command(&mut self, cmd: &CppCommand) -> Result { - let json = serde_json::to_string(cmd)?; - writeln!(self.stdin, "{}", json)?; - self.stdin.flush()?; - - let line = self.recv_line(DEFAULT_COMMAND_TIMEOUT)?; - - if line.is_empty() { - return Err(CompatTestError::InvalidResponse( - "Empty response (process may have crashed)".to_string(), - )); - } - - let response: CppResponse = serde_json::from_str(&line)?; - Ok(response) - } - - /// Extract result value, returning error if response is an error - fn expect_result(&mut self, cmd: &CppCommand) -> Result { - let response = self.send_command(cmd)?; - match response { - CppResponse::Result { result } => Ok(result), - CppResponse::Error { error } => Err(CompatTestError::CommandFailed(error)), - _ => Err(CompatTestError::InvalidResponse("Unexpected response type".to_string())), - } - } - - // ---- Info ---- - - /// Get node info - pub fn info(&self) -> &NodeInfo { - &self.info - } - - /// Get local ADNL ID (hex) - pub fn adnl_id(&self) -> &str { - &self.info.adnl_id - } - - /// Get local public key (base64 TL) - pub fn pubkey(&self) -> &str { - &self.info.pubkey - } - - /// Get UDP port - pub fn udp_port(&self) -> u16 { - self.info.udp_port - } - - // ---- Basic commands ---- - - /// Ping the node - pub fn ping(&mut self) -> Result<()> { - let result = self.expect_result(&CppCommand::Ping)?; - if result.as_str() == Some("pong") { - Ok(()) - } else { - Err(CompatTestError::InvalidResponse(format!("{:?}", result))) - } - } - - /// Get full info from running node - pub fn get_info(&mut self) -> Result { - let result = self.expect_result(&CppCommand::GetInfo)?; - Ok(NodeInfo { - adnl_id: result["adnl_id"].as_str().unwrap_or_default().to_string(), - pubkey: result["pubkey"].as_str().unwrap_or_default().to_string(), - udp_port: result["udp_port"].as_u64().unwrap_or_default() as u16, - }) - } - - // ---- Overlay ID ---- - - /// Compute overlay ID from name bytes (raw bytes, will be base64-encoded) - pub fn compute_overlay_id(&mut self, name: &[u8]) -> Result { - let result = - self.expect_result(&CppCommand::ComputeOverlayId { name: b64_encode(name) })?; - result["overlay_id"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) - } - - // ---- Peer management ---- - - /// Add a peer to the ADNL peer table - pub fn add_peer(&mut self, pubkey_tl_b64: &str, ip: &str, port: u16) -> Result { - self.add_peer_with_quic(pubkey_tl_b64, ip, port, None) - } - - /// Add a peer with an optional explicit QUIC address (adnl.address.quic) - pub fn add_peer_with_quic( - &mut self, - pubkey_tl_b64: &str, - ip: &str, - port: u16, - quic_port: Option, - ) -> Result { - let result = self.expect_result(&CppCommand::AddPeer { - pubkey: pubkey_tl_b64.to_string(), - ip: ip.to_string(), - port, - quic_port, - })?; - result["peer_id"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected peer_id".to_string())) - } - - // ---- Overlay creation ---- - - /// Create a public overlay - pub fn create_public_overlay(&mut self, overlay_name: &[u8]) -> Result { - let result = self.expect_result(&CppCommand::CreateOverlay { - overlay_type: "public".to_string(), - overlay_name: b64_encode(overlay_name), - peers: vec![], - root_pub_keys: vec![], - certificate: None, - max_slaves: None, - enable_twostep: false, - })?; - result["overlay_id"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) - } - - /// Create a private overlay with given peer ADNL IDs (hex) - pub fn create_private_overlay( - &mut self, - overlay_name: &[u8], - peers: Vec, - ) -> Result { - let result = self.expect_result(&CppCommand::CreateOverlay { - overlay_type: "private".to_string(), - overlay_name: b64_encode(overlay_name), - peers, - root_pub_keys: vec![], - certificate: None, - max_slaves: None, - enable_twostep: false, - })?; - result["overlay_id"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) - } - - /// Create a private overlay with TwostepFec enabled - pub fn create_private_overlay_twostep( - &mut self, - overlay_name: &[u8], - peers: Vec, - ) -> Result { - let result = self.expect_result(&CppCommand::CreateOverlay { - overlay_type: "private".to_string(), - overlay_name: b64_encode(overlay_name), - peers, - root_pub_keys: vec![], - certificate: None, - max_slaves: None, - enable_twostep: true, - })?; - result["overlay_id"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) - } - - /// Create a semiprivate overlay - pub fn create_semiprivate_overlay( - &mut self, - overlay_name: &[u8], - peers: Vec, - root_pub_keys: Vec, - certificate: Option<&[u8]>, - max_slaves: Option, - ) -> Result { - let result = self.expect_result(&CppCommand::CreateOverlay { - overlay_type: "semiprivate".to_string(), - overlay_name: b64_encode(overlay_name), - peers, - root_pub_keys, - certificate: certificate.map(b64_encode), - max_slaves, - enable_twostep: false, - })?; - result["overlay_id"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected overlay_id".to_string())) - } - - /// Delete an overlay - pub fn delete_overlay(&mut self, overlay_id: &str) -> Result<()> { - self.expect_result(&CppCommand::DeleteOverlay { overlay_id: overlay_id.to_string() })?; - Ok(()) - } - - // ---- Overlay node info ---- - - /// Get TL-serialized overlay.node for this node in the given overlay - /// Returns base64-encoded TL bytes - pub fn get_overlay_node_info(&mut self, overlay_id: &str) -> Result { - let result = self.expect_result(&CppCommand::GetOverlayNodeInfo { - overlay_id: overlay_id.to_string(), - })?; - result["node_tl"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected node_tl".to_string())) - } - - // ---- Broadcasts ---- - - /// Send a broadcast (optionally FEC) - pub fn send_broadcast(&mut self, overlay_id: &str, data: &[u8], use_fec: bool) -> Result<()> { - self.expect_result(&CppCommand::SendBroadcast { - overlay_id: overlay_id.to_string(), - data: b64_encode(data), - use_fec, - })?; - Ok(()) - } - - /// Get received broadcasts for an overlay - pub fn get_received_broadcasts(&mut self, overlay_id: &str) -> Result> { - let result = self.expect_result(&CppCommand::GetReceivedBroadcasts { - overlay_id: overlay_id.to_string(), - })?; - let broadcasts: Vec = serde_json::from_value(result)?; - Ok(broadcasts) - } - - /// Clear received broadcasts for an overlay - pub fn clear_received_broadcasts(&mut self, overlay_id: &str) -> Result<()> { - self.expect_result(&CppCommand::ClearReceivedBroadcasts { - overlay_id: overlay_id.to_string(), - })?; - Ok(()) - } - - /// Set broadcast validator mode - pub fn set_broadcast_validator(&mut self, overlay_id: &str, mode: &str) -> Result<()> { - self.expect_result(&CppCommand::SetBroadcastValidator { - overlay_id: overlay_id.to_string(), - mode: mode.to_string(), - })?; - Ok(()) - } - - // ---- Queries ---- - - /// Send an overlay query, returns answer bytes - pub fn send_query( - &mut self, - overlay_id: &str, - peer_adnl_id: &str, - data: &[u8], - timeout_ms: i64, - ) -> Result> { - let result = self.expect_result(&CppCommand::SendQuery { - overlay_id: overlay_id.to_string(), - peer_adnl_id: peer_adnl_id.to_string(), - data: b64_encode(data), - timeout_ms, - })?; - let answer_b64 = result["answer"] - .as_str() - .ok_or_else(|| CompatTestError::InvalidResponse("Expected answer".to_string()))?; - b64_decode(answer_b64) - .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 answer: {}", e))) - } - - /// Send an RLDP query via overlay - pub fn send_rldp_query( - &mut self, - overlay_id: &str, - peer_adnl_id: &str, - data: &[u8], - max_answer_size: u64, - v2: bool, - ) -> Result> { - let result = self.expect_result(&CppCommand::SendRldpQuery { - overlay_id: overlay_id.to_string(), - peer_adnl_id: peer_adnl_id.to_string(), - data: b64_encode(data), - max_answer_size, - v2, - })?; - let answer_b64 = result["answer"] - .as_str() - .ok_or_else(|| CompatTestError::InvalidResponse("Expected answer".to_string()))?; - b64_decode(answer_b64) - .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 answer: {}", e))) - } - - // ---- Point-to-point messages ---- - - /// Send a point-to-point overlay message (not broadcast) - pub fn send_message( - &mut self, - overlay_id: &str, - peer_adnl_id: &str, - data: &[u8], - ) -> Result<()> { - self.expect_result(&CppCommand::SendMessage { - overlay_id: overlay_id.to_string(), - peer_adnl_id: peer_adnl_id.to_string(), - data: b64_encode(data), - })?; - Ok(()) - } - - /// Get received messages for an overlay - pub fn get_received_messages(&mut self, overlay_id: &str) -> Result> { - let result = self.expect_result(&CppCommand::GetReceivedMessages { - overlay_id: overlay_id.to_string(), - })?; - let messages: Vec = serde_json::from_value(result)?; - Ok(messages) - } - - /// Clear received messages for an overlay - pub fn clear_received_messages(&mut self, overlay_id: &str) -> Result<()> { - self.expect_result(&CppCommand::ClearReceivedMessages { - overlay_id: overlay_id.to_string(), - })?; - Ok(()) - } - - // ---- Queries ---- - - /// Set query handler mode - pub fn set_query_handler(&mut self, overlay_id: &str, mode: &str) -> Result<()> { - self.expect_result(&CppCommand::SetQueryHandler { - overlay_id: overlay_id.to_string(), - mode: mode.to_string(), - })?; - Ok(()) - } - - // ---- BOC Compression ---- - - /// Compress BOC data on the C++ side. - /// Takes base64-encoded standard BOC, returns base64-encoded compressed data. - pub fn compress_boc(&mut self, boc_b64: &str, algorithm: &str) -> Result { - let result = self.expect_result(&CppCommand::CompressBoc { - data: boc_b64.to_string(), - algorithm: algorithm.to_string(), - })?; - result["compressed"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected 'compressed'".to_string())) - } - - /// Decompress BOC data on the C++ side. - /// Takes base64-encoded compressed data, returns base64-encoded standard BOC. - pub fn decompress_boc(&mut self, compressed_b64: &str, max_size: u32) -> Result { - let result = self.expect_result(&CppCommand::DecompressBoc { - data: compressed_b64.to_string(), - max_size, - })?; - result["boc"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| CompatTestError::InvalidResponse("Expected 'boc'".to_string())) - } - - /// Build serialized TL bytes for consensus.candidateId(slot, hash) on C++ side. - pub fn compute_candidate_id_to_sign(&mut self, slot: i32, hash_hex: &str) -> Result> { - let result = self.expect_result(&CppCommand::ComputeCandidateIdToSign { - slot, - hash: hash_hex.to_string(), - })?; - let data_b64 = result["data"] - .as_str() - .ok_or_else(|| CompatTestError::InvalidResponse("Expected 'data'".to_string()))?; - b64_decode(data_b64) - .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 data: {}", e))) - } - - // ---- QUIC ---- - - /// Enable QUIC transport (creates QuicSender, listens on udp_port + 1000) - pub fn enable_quic(&mut self) -> Result { - let result = self.expect_result(&CppCommand::EnableQuic {})?; - let quic_port = result["quic_port"] - .as_u64() - .ok_or_else(|| CompatTestError::InvalidResponse("Expected quic_port".to_string()))?; - Ok(quic_port as u16) - } - - /// Send a message via QUIC transport (bypasses overlay, goes through ADNL) - pub fn send_quic_message(&mut self, peer_adnl_id: &str, data: &[u8]) -> Result<()> { - self.expect_result(&CppCommand::SendQuicMessage { - peer_adnl_id: peer_adnl_id.to_string(), - data: b64_encode(data), - })?; - Ok(()) - } - - /// Send a query via QUIC transport, returns answer bytes - pub fn send_quic_query( - &mut self, - peer_adnl_id: &str, - data: &[u8], - timeout_ms: i64, - ) -> Result> { - let result = self.expect_result(&CppCommand::SendQuicQuery { - peer_adnl_id: peer_adnl_id.to_string(), - data: b64_encode(data), - timeout_ms, - })?; - let answer_b64 = result["answer"] - .as_str() - .ok_or_else(|| CompatTestError::InvalidResponse("Expected answer".to_string()))?; - b64_decode(answer_b64) - .map_err(|e| CompatTestError::InvalidResponse(format!("Invalid base64 answer: {}", e))) - } - - // ---- Lifecycle ---- - - /// Shutdown the node - pub fn shutdown(&mut self) -> Result<()> { - let _ = self.send_command(&CppCommand::Shutdown); - let _ = self.process.wait(); - Ok(()) - } -} - -impl Drop for CppTestNode { - fn drop(&mut self) { - let _ = self.shutdown(); - } -} - -/// Default test timeout in seconds. Can be overridden via TEST_TIMEOUT env var. -const DEFAULT_TEST_TIMEOUT_SECS: u64 = 90; - -/// Guard that aborts the test process if it exceeds the timeout. -/// Create at the start of each test; the watchdog thread is cancelled on drop. -pub struct TestTimeout { - cancel: Arc, -} - -impl TestTimeout { - /// Create a new test timeout guard. - /// `timeout_secs` โ€” maximum duration for the test; 0 means use the default (90s). - /// The timeout can also be overridden globally via the `TEST_TIMEOUT` env var. - pub fn new(timeout_secs: u64) -> Self { - let secs = std::env::var("TEST_TIMEOUT") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(if timeout_secs == 0 { DEFAULT_TEST_TIMEOUT_SECS } else { timeout_secs }); - - let cancel = Arc::new(AtomicBool::new(false)); - let cancel_clone = cancel.clone(); - let thread_name = thread::current().name().unwrap_or("unknown").to_string(); - - thread::spawn(move || { - let deadline = Instant::now() + Duration::from_secs(secs); - while Instant::now() < deadline { - if cancel_clone.load(Ordering::Relaxed) { - return; - } - thread::sleep(Duration::from_millis(500)); - } - if !cancel_clone.load(Ordering::Relaxed) { - eprintln!( - "\n\x1b[1;31mTEST TIMEOUT: '{}' exceeded {}s limit โ€” aborting process\x1b[0m", - thread_name, secs - ); - process::exit(1); - } - }); - - Self { cancel } - } -} - -impl Drop for TestTimeout { - fn drop(&mut self) { - self.cancel.store(true, Ordering::Relaxed); - } -} - -/// Skip test if C++ binary is not available -#[macro_export] -macro_rules! skip_if_no_cpp { - () => { - if !$crate::cpp_binary_available() { - eprintln!("Skipping test: CPP_COMPAT_TEST_BIN not set"); - return; - } - }; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cpp_binary_check() { - // This just verifies the check works - let _ = cpp_binary_available(); - } -} diff --git a/src/node/tests/compat_test/src/overlay_id.rs b/src/node/tests/compat_test/src/overlay_id.rs deleted file mode 100644 index fd95663..0000000 --- a/src/node/tests/compat_test/src/overlay_id.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Overlay ID calculation utilities -//! -//! This module provides functions to compute overlay IDs in a way compatible -//! with the cpp implementation. - -use ton_api::{serialize_boxed, ton::pub_::publickey::Overlay as OverlayKey, IntoBoxed}; -use ton_block::sha256_digest; - -/// Compute overlay short ID from overlay name (same as C++ OverlayIdFull::compute_short_id) -/// -/// The overlay ID is computed by: -/// 1. Creating an "overlay pubkey" from the name using the overlay key type -/// 2. Computing the short ID (SHA256 hash) of that boxed pubkey -/// -/// The input `name` is the raw TL bytes that would be passed to OverlayIdFull. -pub fn compute_overlay_id(name: &[u8]) -> [u8; 32] { - // Use the same approach as adnl/src/overlay/mod.rs: - // OverlayKey { name: ... } then hash_boxed - let overlay_key = OverlayKey { name: name.to_vec().into() }; - let boxed = overlay_key.into_boxed(); - let serialized = serialize_boxed(&boxed).expect("serialize overlay key"); - sha256_digest(&serialized) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_overlay_id_basic() { - let name = b"test_overlay"; - let id = compute_overlay_id(name); - - // The ID should be a valid 32-byte hash - assert_eq!(id.len(), 32); - - // Same input should produce same output - let id2 = compute_overlay_id(name); - assert_eq!(id, id2); - - // Different input should produce different output - let id3 = compute_overlay_id(b"other_overlay"); - assert_ne!(id, id3); - } - - #[test] - fn test_overlay_id_empty() { - let id = compute_overlay_id(b""); - assert_eq!(id.len(), 32); - } - - #[test] - fn test_overlay_id_long_name() { - // Test with name > 254 bytes - let name = vec![b'x'; 300]; - let id = compute_overlay_id(&name); - assert_eq!(id.len(), 32); - } -} diff --git a/src/node/tests/compat_test/src/test_helpers.rs b/src/node/tests/compat_test/src/test_helpers.rs deleted file mode 100644 index c774c99..0000000 --- a/src/node/tests/compat_test/src/test_helpers.rs +++ /dev/null @@ -1,851 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Test helpers for cross-implementation compatibility tests. -//! -//! Provides utilities for creating Rust ADNL+overlay nodes and -//! exchanging peers with cpp test nodes. - -use crate::CppTestNode; -use adnl::{ - common::{ - hash, AdnlPeers, Answer, QueryAnswer, QueryResult, Subscriber, TaggedByteSlice, - TaggedTlObject, - }, - node::{AdnlNode, AdnlNodeConfig, AdnlSendMethod, IpAddress}, - OverlayNode, OverlayNodeInfo, OverlayParams, OverlayShortId, QuicNode, RldpNode, -}; -use std::{ - net::{Ipv4Addr, SocketAddr}, - sync::{Arc, Mutex}, - time::Duration, -}; -use ton_api::{ - deserialize_boxed, serialize_boxed, serialize_boxed_append, - ton::{ - adnl::{ - address::address::{Quic as AdnlAddrQuic, Udp as AdnlAddrUdp}, - addresslist::AddressList, - Address as AdnlAddress, - }, - overlay::{ - message::Message as OverlayMessage, node::Node as OverlayNodeV1, - nodev2::NodeV2 as OverlayNodeV2, Node as OverlayNodeBoxed, - }, - pub_::publickey::{Ed25519 as Ed25519PubKey, Overlay as OverlayKey}, - rpc::overlay::Query as OverlayQuery, - ton_node::data::Data as TonNodeData, - }, - IntoBoxed, TLObject, -}; -use ton_block::{ - base64_decode, base64_encode, sha256_digest, Ed25519KeyOption, KeyId, Result, UInt256, -}; - -const KEY_TAG_OVERLAY: usize = 2; - -/// A Rust ADNL + overlay test node -pub struct RustTestNode { - pub rt: tokio::runtime::Runtime, - pub adnl: Arc, - pub overlay: Arc, - pub addr: String, - pub port: u16, -} - -impl RustTestNode { - /// Create a new Rust ADNL+overlay node on the given IP:port. - /// If `with_rldp` is true, an RLDP node is created and registered, - /// enabling TwostepFec broadcasts. - pub fn new(ip: &str, port: u16, with_rldp: bool) -> Self { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .worker_threads(2) - .build() - .expect("Failed to create tokio runtime"); - - let addr = format!("{}:{}", ip, port); - let zero_state = [0u8; 32]; // Test zero state - - // Generate deterministic key from address - let key_data = sha256_digest(addr.as_bytes()); - let keys = vec![(key_data, KEY_TAG_OVERLAY)]; - let (_, config) = - AdnlNodeConfig::from_ip_address_and_private_keys(&addr, keys).expect("Config failed"); - - let adnl = rt.block_on(AdnlNode::with_config(config)).expect("ADNL node creation failed"); - - let overlay = OverlayNode::with_params(adnl.clone(), &zero_state, KEY_TAG_OVERLAY) - .expect("Overlay node creation failed"); - - if with_rldp { - let rldp = RldpNode::with_params(adnl.clone(), vec![overlay.clone()], None) - .expect("RLDP node creation failed"); - overlay.set_rldp(rldp.clone()).expect("set_rldp failed"); - - let subscribers: Vec> = vec![overlay.clone(), rldp]; - - rt.block_on(async { - adnl.start_over_udp(subscribers).await.expect("Failed to start ADNL UDP"); - }); - } else { - let subscribers: Vec> = vec![overlay.clone()]; - - rt.block_on(async { - adnl.start_over_udp(subscribers).await.expect("Failed to start ADNL UDP"); - }); - } - - Self { rt, adnl, overlay, addr, port } - } - - /// Get the ADNL key ID (hex) - pub fn adnl_id_hex(&self) -> String { - self.adnl - .key_by_tag(KEY_TAG_OVERLAY) - .expect("No key") - .id() - .data() - .iter() - .map(|b| format!("{:02x}", b)) - .collect() - } - - /// Get the ADNL public key as base64 TL-serialized - pub fn pubkey_tl_b64(&self) -> String { - let key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - let pub_key = key.pub_key().expect("No pub key"); - // Export as TL-serialized pub.ed25519{key:int256} - let tl_key = - Ed25519PubKey { key: UInt256::with_array(pub_key.try_into().expect("Wrong key size")) }; - let serialized = serialize_boxed(&tl_key.into_boxed()).expect("Serialization failed"); - base64_encode(&serialized) - } - - /// Get the ADNL key Arc - pub fn adnl_key_id(&self) -> Arc { - self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key").id().clone() - } - - /// Compute the overlay name TL bytes for a given workchain/shard - /// This is the bytes that should be passed to C++ as overlay_name - pub fn compute_overlay_name(&self, workchain: i32, shard: i64) -> Vec { - let overlay_id = self.overlay.calc_overlay_id(workchain, shard).expect("calc_overlay_id"); - overlay_id.to_vec() - } - - /// Compute overlay short ID - pub fn compute_overlay_short_id(&self, workchain: i32, shard: i64) -> Arc { - self.overlay.calc_overlay_short_id(workchain, shard).expect("calc_overlay_short_id") - } - - /// Compute overlay short ID from arbitrary name bytes. - /// This wraps the name in a TL pub.overlay{name} structure before hashing, - /// matching the C++ OverlayIdFull::compute_short_id() behavior. - pub fn compute_overlay_short_id_from_name(&self, name: &[u8]) -> Arc { - let overlay_key = OverlayKey { name: name.to_vec().into() }; - let id = hash(overlay_key).expect("hash overlay key"); - OverlayShortId::from_data(id) - } - - /// Add public overlay - pub fn add_public_overlay(&self, overlay_id: &Arc) { - self.rt.block_on(async { - let params = OverlayParams::with_id_only(overlay_id); - self.overlay.add_local_workchain_overlay(params).expect("Failed to add overlay"); - }); - } - - /// Add private overlay with given peer ADNL IDs (hex strings) - /// Note: For simplicity, this creates a public overlay internally since creating - /// a true private overlay requires a signing key. The C++ side creates a private - /// overlay which doesn't require DHT. - pub fn add_private_overlay(&self, overlay_id: &Arc, _peers: Vec) { - self.rt.block_on(async { - // For test purposes, just create as public overlay on Rust side - // The C++ side creates a true private overlay - let params = OverlayParams::with_id_only(overlay_id); - self.overlay.add_local_workchain_overlay(params).expect("Failed to add overlay"); - }); - } - - /// Add a true private overlay with signing key and peer list. - /// Unlike `add_private_overlay` (which creates a public overlay as shortcut), - /// this creates a real private overlay where `try_consume_custom` is dispatched - /// to the registered consumer. - pub fn add_true_private_overlay( - &self, - overlay_id: &Arc, - peers: &[Arc], - use_quic: bool, - ) { - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - let params = OverlayParams { - flags: 0, - hops: None, - overlay_id, - runtime: Some(self.rt.handle().clone()), - }; - self.overlay - .add_private_overlay(params, &local_key, peers, use_quic) - .expect("add_private_overlay failed"); - } - - /// Parse C++ node's base64 TL public key to raw 32-byte key - fn parse_cpp_pubkey(pubkey_tl_b64: &str) -> [u8; 32] { - let tl_bytes = base64_decode(pubkey_tl_b64).expect("decode pubkey b64"); - // TL: pub.ed25519#4813b4c6 key:int256 = PublicKey - // Skip 4-byte constructor, take 32-byte key - assert!(tl_bytes.len() >= 36, "TL pubkey too short: {}", tl_bytes.len()); - let key_bytes: [u8; 32] = tl_bytes[4..36].try_into().expect("wrong key len"); - key_bytes - } - - /// Add the C++ node as an ADNL peer (but not to any overlay) - pub fn add_cpp_peer(&self, cpp: &CppTestNode) { - let raw_key = Self::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) - .expect("parse IP"); - - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); - } - - /// Add the C++ node as an ADNL peer AND to a specific public overlay via signed node. - pub fn add_cpp_peer_to_overlay(&self, cpp: &mut CppTestNode, overlay_id: &Arc) { - let raw_key = Self::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) - .expect("parse IP"); - - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); - - let signed_node = Self::get_cpp_signed_node(cpp, overlay_id); - self.overlay.add_public_peer(&ip, &signed_node, overlay_id).expect("add_public_peer"); - } - - /// Add another Rust node as an ADNL peer AND to a specific public overlay via signed node. - pub fn add_rust_peer_to_overlay(&self, other: &RustTestNode, overlay_id: &Arc) { - let other_key = other.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key on other node"); - let other_pubkey_data = other_key.pub_key().expect("No pub key on other node"); - let other_pubkey = Ed25519KeyOption::from_public_key( - other_pubkey_data.try_into().expect("Wrong key size"), - ); - let other_ip = IpAddress::from_versioned_string(&other.addr, None).expect("parse other IP"); - - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &other_ip, None, &other_pubkey).expect("add_peer"); - - let signed_node = - other.overlay.get_signed_node(overlay_id, false).expect("get_signed_node"); - self.overlay.add_public_peer(&other_ip, &signed_node, overlay_id).expect("add_public_peer"); - } - - /// Get the KeyId for the C++ node (based on its public key) - pub fn cpp_key_id(cpp: &CppTestNode) -> Arc { - let raw_key = Self::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - pubkey.id().clone() - } - - /// Get a signed overlay node description from the C++ node. - fn get_cpp_signed_node( - cpp: &mut CppTestNode, - overlay_id: &Arc, - ) -> OverlayNodeInfo { - let node_b64 = cpp - .get_overlay_node_info(&hex::encode(overlay_id.data())) - .expect("get_overlay_node_info failed"); - let node_bytes = base64_decode(&node_b64).expect("decode node_tl"); - let tl_obj = deserialize_boxed(&node_bytes).expect("deserialize node TL"); - let node = tl_obj.downcast::().expect("downcast to overlay.Node"); - OverlayNodeInfo::V1(node.only()) - } - - /// Send a point-to-point overlay message (not broadcast) to a specific peer. - /// This uses overlay.message() - the same path as consensus votes/certificates. - pub fn send_message(&self, overlay_id: &Arc, dst: &Arc, data: &[u8]) { - self.rt.block_on(async { - let tagged = TaggedByteSlice::with_object(data); - self.overlay.message(dst, &tagged, overlay_id).await.expect("overlay message failed"); - println!("send_message: OK"); - }); - } - - /// Send broadcast via overlay - pub fn send_broadcast(&self, overlay_id: &Arc, data: &[u8]) { - self.rt.block_on(async { - let tagged = TaggedByteSlice::with_object(data); - self.overlay - .broadcast(overlay_id, &tagged, None, 0, AdnlSendMethod::Fast) - .await - .expect("broadcast failed"); - }); - } - - /// Send two-step FEC broadcast via overlay (requires RLDP) - pub fn send_broadcast_twostep(&self, overlay_id: &Arc, data: &[u8]) { - self.rt.block_on(async { - let tagged = TaggedByteSlice::with_object(data); - self.overlay - .broadcast_twostep(overlay_id, &tagged, None, 0, Vec::new()) - .await - .expect("broadcast_twostep failed"); - }); - } - - /// Wait for a broadcast with timeout - pub fn wait_for_broadcast( - &self, - overlay_id: &Arc, - timeout_secs: u64, - ) -> Option> { - self.rt.block_on(async { - tokio::time::timeout( - Duration::from_secs(timeout_secs), - self.overlay.wait_for_broadcast(overlay_id), - ) - .await - .ok() - .and_then(|r| r.ok()) - .flatten() - .map(|info| info.data) - }) - } - - /// Register a query consumer (Subscriber) for an overlay. - /// This is required for receiving RLDP queries on the Rust side. - pub fn register_consumer( - &self, - overlay_id: &Arc, - consumer: Arc, - ) { - self.overlay.add_consumer(overlay_id, consumer).expect("add_consumer failed"); - } - - /// Send an RLDP query via overlay and return the answer. - /// Requires the node to have been created with `with_rldp=true`. - /// - /// The data is wrapped in a `tonNode.data` TL envelope and prepended with - /// the `overlay.query` prefix, matching the C++ `Overlays::send_query_via` behavior. - /// The answer is the raw bytes returned by the responder's echo handler. - pub fn send_rldp_query( - &self, - overlay_id: &Arc, - dst: &Arc, - data: &[u8], - max_answer_size: u64, - v2: bool, - ) -> Option> { - self.rt.block_on(async { - // Wrap data in tonNode.data TL envelope - let tl_data = TonNodeData { data: data.to_vec().into() }; - // Get overlay query prefix (overlay.query{overlay=id}) - let mut query = - self.overlay.get_query_prefix(overlay_id).expect("get_query_prefix failed"); - // Append the TL-serialized data object after the prefix - serialize_boxed_append(&mut query, &tl_data.into_boxed()) - .expect("serialize_boxed_append failed"); - let tagged = TaggedByteSlice::with_object(&query); - let (answer, _roundtrip) = self - .overlay - .query_via_rldp(dst, &tagged, overlay_id, Some(max_answer_size), v2, None) - .await - .expect("query_via_rldp failed"); - answer - }) - } - - /// Stop the node - pub fn stop(&self) { - self.rt.block_on(async { - self.adnl.stop().await; - }); - } -} - -/// A test consumer that echoes back queries -pub struct EchoConsumer; - -impl EchoConsumer { - pub fn new() -> Arc { - Arc::new(Self) - } -} - -#[async_trait::async_trait] -impl Subscriber for EchoConsumer { - async fn try_consume_query(&self, object: TLObject, _peers: &AdnlPeers) -> Result { - // Echo back - use .into() to properly handle telemetry feature - Ok(QueryResult::Consumed(QueryAnswer::Ready(Some(Answer::Object(object.into()))))) - } -} - -/// A subscriber that collects overlay messages (point-to-point, not broadcasts). -/// Used to verify delivery of overlay.message() calls on the receiving side. -pub struct MessageCollector { - messages: Mutex>>, - notify: tokio::sync::Notify, -} - -impl MessageCollector { - pub fn new() -> Arc { - Arc::new(Self { messages: Mutex::new(Vec::new()), notify: tokio::sync::Notify::new() }) - } - - /// Wait until at least `count` messages are collected, or timeout. - pub fn wait_for_messages( - &self, - rt: &tokio::runtime::Runtime, - count: usize, - timeout_secs: u64, - ) -> Vec> { - rt.block_on(async { - let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs); - loop { - { - let msgs = self.messages.lock().unwrap(); - if msgs.len() >= count { - return msgs.clone(); - } - } - if tokio::time::Instant::now() >= deadline { - return self.messages.lock().unwrap().clone(); - } - tokio::select! { - _ = self.notify.notified() => {} - _ = tokio::time::sleep_until(deadline) => { - return self.messages.lock().unwrap().clone(); - } - } - } - }) - } -} - -#[async_trait::async_trait] -impl Subscriber for MessageCollector { - async fn try_consume_custom(&self, data: &[u8], _peers: &AdnlPeers) -> Result { - self.messages.lock().unwrap().push(data.to_vec()); - self.notify.notify_waiters(); - Ok(true) - } -} - -/// A QUIC-capable subscriber that stores received messages and echoes queries. -/// Used for transport-level QUIC tests. -pub struct QuicTestSubscriber { - key_id: Arc, - msg_tx: tokio::sync::mpsc::UnboundedSender>, -} - -impl QuicTestSubscriber { - pub fn new(key_id: Arc) -> (Arc, tokio::sync::mpsc::UnboundedReceiver>) { - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - (Arc::new(Self { key_id, msg_tx: tx }), rx) - } -} - -#[async_trait::async_trait] -impl Subscriber for QuicTestSubscriber { - async fn try_consume_custom(&self, data: &[u8], peers: &AdnlPeers) -> Result { - if peers.local() != &self.key_id { - return Ok(false); - } - let _ = self.msg_tx.send(data.to_vec()); - Ok(true) - } - - async fn try_consume_query(&self, object: TLObject, peers: &AdnlPeers) -> Result { - if peers.local() != &self.key_id { - return Ok(QueryResult::Rejected(object)); - } - // Echo back - Ok(QueryResult::Consumed(QueryAnswer::Ready(Some(Answer::Object(object.into()))))) - } -} - -/// A Rust ADNL + overlay + QUIC test node. -/// Extends RustTestNode with QuicNode for cross-implementation QUIC testing. -pub struct RustQuicTestNode { - pub rt: tokio::runtime::Runtime, - pub adnl: Arc, - pub overlay: Arc, - pub quic: Arc, - pub addr: String, - pub port: u16, - #[allow(dead_code)] - key_data: [u8; 32], - cancellation_token: tokio_util::sync::CancellationToken, - quic_msg_rx: Mutex>>, -} - -impl RustQuicTestNode { - /// Create a new Rust ADNL+overlay+QUIC node on the given IP:port. - /// ADNL listens on `port` (UDP), QUIC listens on `port+1000`. - pub fn new(ip: &str, port: u16) -> Self { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .worker_threads(2) - .build() - .expect("Failed to create tokio runtime"); - - let addr = format!("{}:{}", ip, port); - let zero_state = [0u8; 32]; - - // Generate deterministic key from address - let key_data = sha256_digest(addr.as_bytes()); - let keys = vec![(key_data, KEY_TAG_OVERLAY)]; - let (_, config) = - AdnlNodeConfig::from_ip_address_and_private_keys(&addr, keys).expect("Config failed"); - - let key_id = config.key_by_tag(KEY_TAG_OVERLAY).expect("No key").id().clone(); - - let adnl = rt.block_on(AdnlNode::with_config(config)).expect("ADNL node creation failed"); - - let overlay = OverlayNode::with_params(adnl.clone(), &zero_state, KEY_TAG_OVERLAY) - .expect("Overlay node creation failed"); - - // Start ADNL over UDP with overlay as subscriber - let subscribers: Vec> = vec![overlay.clone()]; - rt.block_on(async { - adnl.start_over_udp(subscribers).await.expect("Failed to start ADNL UDP"); - }); - - // Create QuicNode with both a test subscriber and overlay - let cancellation_token = tokio_util::sync::CancellationToken::new(); - let (test_sub, quic_msg_rx) = QuicTestSubscriber::new(key_id.clone()); - - let quic = { - let _guard = rt.enter(); - let quic_subscribers: Vec> = - vec![test_sub as Arc, overlay.clone()]; - let quic = QuicNode::new( - quic_subscribers, - cancellation_token.clone(), - None, - rt.handle().clone(), - ); - let bind_addr = SocketAddr::new( - Ipv4Addr::from(adnl.ip_address_adnl().ip()).into(), - adnl.ip_address_adnl().port() + QuicNode::OFFSET_PORT, - ); - quic.add_key(&key_data, &key_id, bind_addr).expect("QUIC add_key failed"); - quic - }; - - Self { - rt, - adnl, - overlay, - quic, - addr, - port, - key_data, - cancellation_token, - quic_msg_rx: Mutex::new(quic_msg_rx), - } - } - - /// Get the ADNL key ID (hex) - pub fn adnl_id_hex(&self) -> String { - self.adnl - .key_by_tag(KEY_TAG_OVERLAY) - .expect("No key") - .id() - .data() - .iter() - .map(|b| format!("{:02x}", b)) - .collect() - } - - /// Get the ADNL public key as base64 TL-serialized - pub fn pubkey_tl_b64(&self) -> String { - let key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - let pub_key = key.pub_key().expect("No pub key"); - let tl_key = - Ed25519PubKey { key: UInt256::with_array(pub_key.try_into().expect("Wrong key size")) }; - let serialized = serialize_boxed(&tl_key.into_boxed()).expect("Serialization failed"); - base64_encode(&serialized) - } - - /// Get the ADNL key Arc - pub fn adnl_key_id(&self) -> Arc { - self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key").id().clone() - } - - /// Add the C++ node as an ADNL peer (UDP) - pub fn add_cpp_peer(&self, cpp: &CppTestNode) { - let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) - .expect("parse IP"); - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); - } - - /// Add the C++ node as a QUIC peer (registers its QUIC address = udp_port + 1000) - pub fn add_cpp_quic_peer(&self, cpp: &CppTestNode) { - let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - let quic_addr: SocketAddr = - format!("127.0.0.1:{}", cpp.udp_port() + 1000).parse().expect("parse QUIC addr"); - self.quic.add_peer_key(pubkey.id().clone(), quic_addr).expect("add_quic_peer"); - } - - /// Add the C++ node as both ADNL and QUIC peer by simulating reception of an - /// AddressList that contains adnl.address.quic (the new address type from C++ PR #2184). - /// The QUIC address is discovered via `parse_quic_address` โ€” no hardcoded offset. - pub fn add_cpp_peer_via_address_list(&self, cpp: &CppTestNode, quic_port: u16) { - let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - - // Build an AddressList as a C++ node with PR #2184 would advertise: - // both adnl.address.udp and adnl.address.quic - let ip: u32 = u32::from(std::net::Ipv4Addr::new(127, 0, 0, 1)); - let addr_list = AddressList { - addrs: vec![ - AdnlAddress::Adnl_Address_Udp(AdnlAddrUdp { - ip: ip as i32, - port: cpp.udp_port() as i32, - }), - AdnlAddress::Adnl_Address_Quic(AdnlAddrQuic { - ip: ip as i32, - port: quic_port as i32, - }), - ] - .into(), - version: adnl::common::Version::get(), - reinit_date: adnl::common::Version::get(), - priority: 0, - expire_at: 0, - }; - - // Parse ADNL and QUIC addresses from the address list - let (adnl_addr, quic_addr) = - AdnlNode::parse_address_list(&addr_list).expect("parse").expect("has ADNL addr"); - - // Add ADNL peer using the UDP address, passing QUIC address too - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - let quic_addr = quic_addr.expect("AddressList should contain adnl.address.quic"); - self.adnl - .add_peer(local_key.id(), &adnl_addr, Some(&quic_addr), &pubkey) - .expect("add_peer"); - - // Do NOT call quic.add_peer_key โ€” let ensure_peer_registered discover - // the QUIC address via adnl.peer_ip_address() at connection time. - } - - /// Add C++ node as both ADNL peer (UDP) and QUIC peer, and to overlay - pub fn add_cpp_peer_full(&self, cpp: &mut CppTestNode, overlay_id: &Arc) { - let raw_key = RustTestNode::parse_cpp_pubkey(cpp.pubkey()); - let pubkey = Ed25519KeyOption::from_public_key(&raw_key); - let ip = IpAddress::from_versioned_string(&format!("127.0.0.1:{}", cpp.udp_port()), None) - .expect("parse IP"); - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - self.adnl.add_peer(local_key.id(), &ip, None, &pubkey).expect("add_peer"); - - // Add to overlay via signed node - let signed_node = RustTestNode::get_cpp_signed_node(cpp, overlay_id); - self.overlay.add_public_peer(&ip, &signed_node, overlay_id).expect("add_public_peer"); - - // Add QUIC peer - let quic_addr: SocketAddr = - format!("127.0.0.1:{}", cpp.udp_port() + 1000).parse().expect("parse QUIC addr"); - self.quic.add_peer_key(pubkey.id().clone(), quic_addr).expect("add_quic_peer"); - } - - /// Get the KeyId for the C++ node - pub fn cpp_key_id(cpp: &CppTestNode) -> Arc { - RustTestNode::cpp_key_id(cpp) - } - - /// Compute overlay name - pub fn compute_overlay_name(&self, workchain: i32, shard: i64) -> Vec { - let overlay_id = self.overlay.calc_overlay_id(workchain, shard).expect("calc_overlay_id"); - overlay_id.to_vec() - } - - /// Compute overlay short ID - pub fn compute_overlay_short_id(&self, workchain: i32, shard: i64) -> Arc { - self.overlay.calc_overlay_short_id(workchain, shard).expect("calc_overlay_short_id") - } - - /// Add public overlay - pub fn add_public_overlay(&self, overlay_id: &Arc) { - self.rt.block_on(async { - let params = OverlayParams::with_id_only(overlay_id); - self.overlay.add_local_workchain_overlay(params).expect("Failed to add overlay"); - }); - } - - /// Send a QUIC message (internally wrapped in quic.request.Message TL by QuicNode). - /// Note: `data` is sent as-is inside quic_message.data_. On C++ side this goes through - /// AdnlLocalId::deliver which requires matching TL prefix (e.g. overlay.message). - /// Use `send_quic_overlay_message` to properly format data for overlay delivery. - pub fn send_quic_message(&self, dst: &Arc, data: &[u8]) { - let src = self.adnl_key_id(); - self.rt.block_on(async { - self.quic - .message(data.to_vec(), Some(&*self.adnl), &AdnlPeers::with_keys(src, dst.clone())) - .await - .expect("QUIC message failed"); - }); - } - - /// Send a QUIC message with overlay TL wrapping. - /// Data is formatted as: overlay.message { overlay_id } ++ payload - /// which matches the C++ AdnlLocalId callback prefix for overlay routing. - pub fn send_quic_overlay_message( - &self, - dst: &Arc, - overlay_id: &Arc, - payload: &[u8], - ) { - let src = self.adnl_key_id(); - let mut overlay_data = serialize_boxed( - &OverlayMessage { overlay: UInt256::with_array(*overlay_id.data()) }.into_boxed(), - ) - .expect("serialize overlay message prefix"); - overlay_data.extend_from_slice(payload); - - self.rt.block_on(async { - self.quic - .message(overlay_data, Some(&*self.adnl), &AdnlPeers::with_keys(src, dst.clone())) - .await - .expect("QUIC message failed"); - }); - } - - /// Send a QUIC query (internally wrapped in quic.request.Query TL by QuicNode). - /// Returns the TL-deserialized answer bytes. - /// Use `send_quic_overlay_query` for overlay-routed queries. - pub fn send_quic_query(&self, dst: &Arc, data: &[u8]) -> Vec { - let src = self.adnl_key_id(); - self.rt.block_on(async { - self.quic - .query( - data.to_vec(), - Some(&*self.adnl), - &AdnlPeers::with_keys(src, dst.clone()), - None, - ) - .await - .expect("QUIC query failed") - .expect("empty QUIC query answer") - }) - } - - /// Send a QUIC query with overlay TL wrapping, with a timeout. - /// Data is formatted as: overlay.query { overlay_id } ++ payload - /// Returns Ok(answer) or Err if timeout/connection fails. - pub fn send_quic_overlay_query( - &self, - dst: &Arc, - overlay_id: &Arc, - payload: &[u8], - timeout_secs: u64, - ) -> std::result::Result, String> { - let src = self.adnl_key_id(); - let mut overlay_data = - serialize_boxed(&OverlayQuery { overlay: UInt256::with_array(*overlay_id.data()) }) - .expect("serialize overlay query prefix"); - overlay_data.extend_from_slice(payload); - - self.rt.block_on(async { - match tokio::time::timeout( - Duration::from_secs(timeout_secs), - self.quic.query( - overlay_data, - Some(&*self.adnl), - &AdnlPeers::with_keys(src, dst.clone()), - None, - ), - ) - .await - { - Ok(Ok(Some(answer))) => Ok(answer), - Ok(Ok(None)) => Err("empty QUIC query answer".to_string()), - Ok(Err(e)) => Err(format!("QUIC query failed: {}", e)), - Err(_) => Err("QUIC query timed out".to_string()), - } - }) - } - - /// Receive a QUIC message with timeout (from the test subscriber channel) - pub fn recv_quic_message(&self, timeout_secs: u64) -> Option> { - let mut rx = self.quic_msg_rx.lock().unwrap(); - self.rt.block_on(async { - tokio::time::timeout(Duration::from_secs(timeout_secs), rx.recv()).await.ok().flatten() - }) - } - - /// Send overlay message via ADNL (overlay.message()). - pub fn send_overlay_message( - &self, - overlay_id: &Arc, - dst: &Arc, - data: &[u8], - ) { - self.rt.block_on(async { - let tagged = TaggedByteSlice::with_object(data); - self.overlay.message(dst, &tagged, overlay_id).await.expect("overlay message failed"); - println!("send_overlay_message: OK"); - }); - } - - /// Send overlay query via ADNL (overlay.query()). - /// Returns deserialized TLObject response, or None on timeout. - pub fn send_overlay_query( - &self, - overlay_id: &Arc, - dst: &Arc, - query: &TaggedTlObject, - timeout_ms: Option, - ) -> Option { - self.rt.block_on(async { - self.overlay - .query(dst, query, overlay_id, timeout_ms) - .await - .expect("overlay query failed") - }) - } - - /// Create a private overlay with signing key and peer list. - pub fn add_private_overlay( - &self, - overlay_id: &Arc, - peers: &[Arc], - use_quic: bool, - ) { - let local_key = self.adnl.key_by_tag(KEY_TAG_OVERLAY).expect("No key"); - let params = OverlayParams { - flags: 0, - hops: None, - overlay_id, - runtime: Some(self.rt.handle().clone()), - }; - self.overlay - .add_private_overlay(params, &local_key, peers, use_quic) - .expect("add_private_overlay failed"); - } - - /// Stop the node. Shuts down the QUIC endpoint and ADNL node. - pub fn stop(self) { - self.cancellation_token.cancel(); - self.quic.shutdown(); - let adnl = self.adnl.clone(); - self.rt.block_on(async move { - adnl.stop().await; - // Give time for spawned tasks to observe cancellation and endpoint shutdown - tokio::time::sleep(Duration::from_millis(100)).await; - }); - } -} diff --git a/src/node/tests/compat_test/tests/test_boc_compression.rs b/src/node/tests/compat_test/tests/test_boc_compression.rs deleted file mode 100644 index 8cf4b78..0000000 --- a/src/node/tests/compat_test/tests/test_boc_compression.rs +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! BOC Compression cross-implementation tests. -//! -//! Tests that BOC compressed by one implementation (Rust or C++) can be -//! decompressed by the other, verifying wire-format compatibility. -//! -//! The compress/decompress commands are standalone (no networking required), -//! so each test only spawns a single CppTestNode as a compression oracle. - -use compat_test::{skip_if_no_cpp, CppTestNode}; -use ton_block::{ - boc_compression::{boc_compress, boc_decompress, CompressionAlgorithm}, - read_boc, write_boc, write_boc_multi, BuilderData, Cell, IBitstring, -}; - -const PORT_BASE: u16 = 15500; -const MAX_DECOMPRESS_SIZE: u32 = 10 * 1024 * 1024; - -// ---- Cell construction helpers ---- - -fn build_single_cell(data: u64) -> Cell { - let mut builder = BuilderData::new(); - builder.append_u64(data).unwrap(); - builder.into_cell().unwrap() -} - -fn build_simple_tree() -> Cell { - let mut leaf = BuilderData::new(); - leaf.append_u64(0xDEADBEEF_CAFEBABE).unwrap(); - let leaf_cell = leaf.into_cell().unwrap(); - - let mut child1 = BuilderData::new(); - child1.append_u32(1).unwrap(); - child1.checked_append_reference(leaf_cell).unwrap(); - let child1_cell = child1.into_cell().unwrap(); - - let mut child2 = BuilderData::new(); - child2.append_u32(2).unwrap(); - let child2_cell = child2.into_cell().unwrap(); - - let mut root = BuilderData::new(); - root.append_u32(0).unwrap(); - root.checked_append_reference(child1_cell).unwrap(); - root.checked_append_reference(child2_cell).unwrap(); - root.into_cell().unwrap() -} - -fn build_dag_tree() -> Cell { - let mut shared = BuilderData::new(); - shared.append_u32(0x5AAED).unwrap(); - let shared_cell = shared.into_cell().unwrap(); - - let mut c1 = BuilderData::new(); - c1.append_u32(1).unwrap(); - c1.checked_append_reference(shared_cell.clone()).unwrap(); - let c1_cell = c1.into_cell().unwrap(); - - let mut c2 = BuilderData::new(); - c2.append_u32(2).unwrap(); - c2.checked_append_reference(shared_cell).unwrap(); - let c2_cell = c2.into_cell().unwrap(); - - let mut root = BuilderData::new(); - root.append_u32(0).unwrap(); - root.checked_append_reference(c1_cell).unwrap(); - root.checked_append_reference(c2_cell).unwrap(); - root.into_cell().unwrap() -} - -fn cells_to_boc_b64(cells: &[Cell]) -> String { - let boc_bytes = if cells.len() == 1 { - write_boc(&cells[0]).unwrap() - } else { - write_boc_multi(cells.to_vec()).unwrap() - }; - base64::engine::general_purpose::STANDARD.encode(&boc_bytes) -} - -fn b64_to_cells(b64: &str) -> Vec { - let bytes = base64::engine::general_purpose::STANDARD.decode(b64).unwrap(); - let result = read_boc(&bytes).unwrap(); - result.roots -} - -use base64::Engine; - -// ---- Tests ---- - -/// Rust compresses BOC, C++ decompresses it โ€” verify cell hashes match. -#[test] -fn test_boc_compress_rust_decompress_cpp() { - skip_if_no_cpp!(); - - let mut cpp = CppTestNode::spawn(PORT_BASE).expect("spawn C++"); - - let test_cases: Vec<(&str, Vec)> = vec![ - ("single_cell", vec![build_single_cell(0x12345678)]), - ("simple_tree", vec![build_simple_tree()]), - ("dag_tree", vec![build_dag_tree()]), - ]; - - for algo_name in &["baseline", "improved"] { - let rust_algo = match *algo_name { - "baseline" => CompressionAlgorithm::BaselineLZ4, - "improved" => CompressionAlgorithm::ImprovedStructureLZ4, - _ => unreachable!(), - }; - - for (name, cells) in &test_cases { - println!("Testing Rust compress -> C++ decompress: {} ({})", name, algo_name); - - // Rust compresses - let compressed = boc_compress(cells.clone(), rust_algo).unwrap(); - - // Send compressed to C++ for decompression - let compressed_b64 = base64::engine::general_purpose::STANDARD.encode(&compressed); - let decompressed_boc_b64 = - cpp.decompress_boc(&compressed_b64, MAX_DECOMPRESS_SIZE).expect("C++ decompress"); - - // Parse the decompressed BOC back into cells - let decompressed_cells = b64_to_cells(&decompressed_boc_b64); - - // Verify cell hashes match - assert_eq!( - cells.len(), - decompressed_cells.len(), - "{} ({}): root count mismatch", - name, - algo_name - ); - for (i, (original, decompressed)) in - cells.iter().zip(decompressed_cells.iter()).enumerate() - { - assert_eq!( - original.repr_hash(), - decompressed.repr_hash(), - "{} ({}): root {} hash mismatch", - name, - algo_name, - i - ); - } - println!(" OK: {} roots verified", cells.len()); - } - } - - cpp.shutdown().expect("shutdown"); -} - -/// C++ compresses BOC, Rust decompresses it โ€” verify cell hashes match. -#[test] -fn test_boc_compress_cpp_decompress_rust() { - skip_if_no_cpp!(); - - let mut cpp = CppTestNode::spawn(PORT_BASE + 10).expect("spawn C++"); - - let test_cases: Vec<(&str, Vec)> = vec![ - ("single_cell", vec![build_single_cell(0x12345678)]), - ("simple_tree", vec![build_simple_tree()]), - ("dag_tree", vec![build_dag_tree()]), - ]; - - for algo_name in &["baseline", "improved"] { - for (name, cells) in &test_cases { - println!("Testing C++ compress -> Rust decompress: {} ({})", name, algo_name); - - // Send standard BOC to C++ for compression - let boc_b64 = cells_to_boc_b64(cells); - let compressed_b64 = cpp.compress_boc(&boc_b64, algo_name).expect("C++ compress"); - - // Rust decompresses - let compressed_bytes = - base64::engine::general_purpose::STANDARD.decode(&compressed_b64).unwrap(); - let decompressed_cells = - boc_decompress(&compressed_bytes, MAX_DECOMPRESS_SIZE as usize).unwrap(); - - // Verify cell hashes match - assert_eq!( - cells.len(), - decompressed_cells.len(), - "{} ({}): root count mismatch", - name, - algo_name - ); - for (i, (original, decompressed)) in - cells.iter().zip(decompressed_cells.iter()).enumerate() - { - assert_eq!( - original.repr_hash(), - decompressed.repr_hash(), - "{} ({}): root {} hash mismatch", - name, - algo_name, - i - ); - } - println!(" OK: {} roots verified", cells.len()); - } - } - - cpp.shutdown().expect("shutdown"); -} - -/// Full round-trip: Rust compress -> C++ decompress -> C++ compress -> Rust decompress. -/// Verifies that data survives two cross-implementation transitions. -#[test] -fn test_boc_compression_roundtrip() { - skip_if_no_cpp!(); - - let mut cpp = CppTestNode::spawn(PORT_BASE + 20).expect("spawn C++"); - - let test_cases: Vec<(&str, Vec)> = vec![ - ("single_cell", vec![build_single_cell(0xAAAABBBB)]), - ("simple_tree", vec![build_simple_tree()]), - ("dag_tree", vec![build_dag_tree()]), - ]; - - for algo_name in &["baseline", "improved"] { - let rust_algo = match *algo_name { - "baseline" => CompressionAlgorithm::BaselineLZ4, - "improved" => CompressionAlgorithm::ImprovedStructureLZ4, - _ => unreachable!(), - }; - - for (name, cells) in &test_cases { - println!("Testing full round-trip: {} ({})", name, algo_name); - - // Step 1: Rust compresses - let compressed1 = boc_compress(cells.clone(), rust_algo).unwrap(); - let compressed1_b64 = base64::engine::general_purpose::STANDARD.encode(&compressed1); - - // Step 2: C++ decompresses - let decompressed_boc_b64 = - cpp.decompress_boc(&compressed1_b64, MAX_DECOMPRESS_SIZE).expect("C++ decompress"); - - // Step 3: C++ compresses again - let compressed2_b64 = - cpp.compress_boc(&decompressed_boc_b64, algo_name).expect("C++ compress"); - - // Step 4: Rust decompresses - let compressed2_bytes = - base64::engine::general_purpose::STANDARD.decode(&compressed2_b64).unwrap(); - let final_cells = - boc_decompress(&compressed2_bytes, MAX_DECOMPRESS_SIZE as usize).unwrap(); - - // Verify cell hashes match the original - assert_eq!( - cells.len(), - final_cells.len(), - "{} ({}): root count mismatch after round-trip", - name, - algo_name - ); - for (i, (original, final_cell)) in cells.iter().zip(final_cells.iter()).enumerate() { - assert_eq!( - original.repr_hash(), - final_cell.repr_hash(), - "{} ({}): root {} hash mismatch after round-trip", - name, - algo_name, - i - ); - } - println!(" OK: full round-trip verified"); - } - } - - cpp.shutdown().expect("shutdown"); -} - -/// Test with multiple root cells (multi-root BOC). -#[test] -fn test_boc_compression_multi_root() { - skip_if_no_cpp!(); - - let mut cpp = CppTestNode::spawn(PORT_BASE + 30).expect("spawn C++"); - - let roots = vec![build_single_cell(1), build_single_cell(2), build_simple_tree()]; - - for algo_name in &["baseline", "improved"] { - let rust_algo = match *algo_name { - "baseline" => CompressionAlgorithm::BaselineLZ4, - "improved" => CompressionAlgorithm::ImprovedStructureLZ4, - _ => unreachable!(), - }; - - println!("Testing multi-root BOC ({})", algo_name); - - // Rust compress -> C++ decompress - let compressed = boc_compress(roots.clone(), rust_algo).unwrap(); - let compressed_b64 = base64::engine::general_purpose::STANDARD.encode(&compressed); - let decompressed_boc_b64 = cpp - .decompress_boc(&compressed_b64, MAX_DECOMPRESS_SIZE) - .expect("C++ decompress multi-root"); - let decompressed = b64_to_cells(&decompressed_boc_b64); - - assert_eq!(roots.len(), decompressed.len(), "multi-root ({}): count mismatch", algo_name); - for (i, (orig, dec)) in roots.iter().zip(decompressed.iter()).enumerate() { - assert_eq!( - orig.repr_hash(), - dec.repr_hash(), - "multi-root ({}): root {} hash mismatch", - algo_name, - i - ); - } - - // C++ compress -> Rust decompress - let boc_b64 = cells_to_boc_b64(&roots); - let cpp_compressed_b64 = - cpp.compress_boc(&boc_b64, algo_name).expect("C++ compress multi-root"); - let cpp_compressed = - base64::engine::general_purpose::STANDARD.decode(&cpp_compressed_b64).unwrap(); - let cpp_decompressed = - boc_decompress(&cpp_compressed, MAX_DECOMPRESS_SIZE as usize).unwrap(); - - assert_eq!( - roots.len(), - cpp_decompressed.len(), - "multi-root ({}): count mismatch (C++ direction)", - algo_name - ); - for (i, (orig, dec)) in roots.iter().zip(cpp_decompressed.iter()).enumerate() { - assert_eq!( - orig.repr_hash(), - dec.repr_hash(), - "multi-root ({}): root {} hash mismatch (C++ direction)", - algo_name, - i - ); - } - println!(" OK: {} roots verified both directions", roots.len()); - } - - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_broadcast.rs b/src/node/tests/compat_test/tests/test_broadcast.rs deleted file mode 100644 index 7e0e649..0000000 --- a/src/node/tests/compat_test/tests/test_broadcast.rs +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Broadcast compatibility tests between Rust and C++ implementations. -//! -//! Tests that overlay broadcasts sent from one implementation are correctly -//! received by the other. Covers both small (inline) and large (FEC-encoded) -//! broadcasts in both directions. - -use adnl::OverlayShortId; -use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; -use std::{sync::Arc, thread::sleep, time::Duration}; - -/// Port base for this test file (each test offsets by 10) -const PORT_BASE: u16 = 15100; - -/// Set up a Rust + C++ node pair on an overlay. -/// Uses private overlay on C++ side (no DHT required) and public overlay on Rust side. -/// Returns (cpp_node, rust_node, overlay_short_id, cpp_overlay_id_hex) -fn setup_overlay_pair( - port_offset: u16, -) -> (CppTestNode, RustTestNode, Arc, String) { - let cpp_port = PORT_BASE + port_offset; - let rust_port = PORT_BASE + port_offset + 1; - - // 1. Spawn C++ node - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - - // 2. Create Rust node - let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); - - // 3. Compute overlay ID on Rust side (workchain=0, shard=-9223372036854775808 i.e. 0x8000000000000000) - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - - // 4. Create overlay on both sides - // Use public overlay on Rust, private overlay on C++ (C++ public overlay requires DHT) - rust_node.add_public_overlay(&overlay_short_id); - - let rust_adnl_id = rust_node.adnl_id_hex(); - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_adnl_id]) - .expect("create C++ overlay"); - - // Verify overlay IDs match - let rust_overlay_hex = - overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); - assert_eq!( - cpp_overlay_id.to_lowercase(), - rust_overlay_hex.to_lowercase(), - "Overlay IDs should match between C++ and Rust" - ); - - // 5. Exchange ADNL peers AND add to overlay neighbours - // Rust -> C++: add C++ as ADNL peer and to overlay's known_peers/neighbours - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - - // C++ -> Rust: add Rust as ADNL peer - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add_peer"); - - (cpp, rust_node, overlay_short_id, cpp_overlay_id) -} - -/// Test small broadcast from Rust to C++ -#[test] -fn test_broadcast_rust_to_cpp() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(10); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send a small broadcast from Rust - let test_data = b"Hello from Rust broadcast!"; - rust_node.send_broadcast(&overlay_id, test_data); - - // Wait for C++ to receive it - sleep(Duration::from_secs(2)); - - let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); - - println!("C++ received {} broadcasts after Rust->C++ send", received.len()); - - // Broadcast MUST be delivered for the test to pass - assert!( - !received.is_empty(), - "Rust->C++ broadcast was not delivered. Expected at least 1 broadcast." - ); - assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); - println!("Rust->C++ broadcast delivered successfully!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test small broadcast from C++ to Rust -#[test] -fn test_broadcast_cpp_to_rust() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(20); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send a small broadcast from C++ - let test_data = b"Hello from C++ broadcast!"; - cpp.send_broadcast(&cpp_overlay_id, test_data, false).expect("C++ send broadcast"); - - // Wait for Rust to receive it - let received = rust_node.wait_for_broadcast(&overlay_id, 3); - - // Broadcast MUST be delivered for the test to pass - assert!(received.is_some(), "C++->Rust broadcast was not delivered within timeout"); - let data = received.unwrap(); - assert_eq!(data, test_data, "Broadcast data mismatch"); - println!("C++->Rust broadcast delivered successfully!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test FEC broadcast from Rust to C++ (large data, triggers FEC encoding at >768 bytes) -#[test] -fn test_fec_broadcast_rust_to_cpp() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(30); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send a large broadcast (triggers FEC path, > 768 bytes) - let test_data: Vec = (0..2000).map(|i| (i % 256) as u8).collect(); - rust_node.send_broadcast(&overlay_id, &test_data); - - // Wait for C++ to receive it - sleep(Duration::from_secs(3)); - - let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); - - println!("C++ received {} FEC broadcasts after Rust->C++ send", received.len()); - - // FEC broadcast MUST be delivered for the test to pass - assert!( - !received.is_empty(), - "Rust->C++ FEC broadcast was not delivered. Expected at least 1 broadcast." - ); - assert_eq!(received[0].size, test_data.len(), "FEC broadcast size mismatch"); - println!("Rust->C++ FEC broadcast delivered successfully!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test FEC broadcast from C++ to Rust (large data, triggers FEC encoding at >768 bytes) -#[test] -fn test_fec_broadcast_cpp_to_rust() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(40); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send a large broadcast from C++ (triggers FEC path, > 768 bytes) - let test_data: Vec = (0..2000).map(|i| (i % 256) as u8).collect(); - cpp.send_broadcast(&cpp_overlay_id, &test_data, true).expect("C++ send FEC broadcast"); - - // Wait for Rust to receive it - let received = rust_node.wait_for_broadcast(&overlay_id, 5); - - // FEC broadcast MUST be delivered for the test to pass - assert!(received.is_some(), "C++->Rust FEC broadcast was not delivered within timeout"); - let data = received.unwrap(); - assert_eq!(data, test_data, "FEC broadcast data mismatch"); - println!("C++->Rust FEC broadcast delivered successfully!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_broadcast_validation.rs b/src/node/tests/compat_test/tests/test_broadcast_validation.rs deleted file mode 100644 index 0288e8d..0000000 --- a/src/node/tests/compat_test/tests/test_broadcast_validation.rs +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Broadcast Validation (2-Phase) Compatibility Tests -//! -//! Tests that the 2-phase broadcast validation (check_broadcast) works -//! consistently between Rust and C++ implementations. -//! -//! In 2-phase broadcast: -//! 1. Receiving node gets check_broadcast callback with data -//! 2. Callback accepts or rejects the broadcast -//! 3. If accepted: broadcast delivered to application + redistributed -//! 4. If rejected: broadcast dropped, NOT redistributed - -use adnl::OverlayShortId; -use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; -use std::{sync::Arc, thread::sleep, time::Duration}; - -/// Port base for broadcast validation tests -const PORT_BASE: u16 = 15300; - -/// Helper: set up a Rust+C++ pair with a private overlay and accept mode on C++ -fn setup_validation_pair( - port_offset: u16, - validator_mode: &str, -) -> (CppTestNode, RustTestNode, Arc, String) { - let cpp_port = PORT_BASE + port_offset; - let rust_port = PORT_BASE + port_offset + 1; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); - - // Use a unique overlay name for each test to avoid conflicts - let overlay_name = format!("validation_test_{}", port_offset); - let overlay_name_bytes = overlay_name.as_bytes(); - - // Compute overlay short ID from name - let overlay_short_id = rust_node.compute_overlay_short_id_from_name(overlay_name_bytes); - - // Create private overlay on both sides (private overlays don't require DHT) - let cpp_adnl_id = cpp.adnl_id(); - let rust_adnl_id = rust_node.adnl_id_hex(); - - rust_node.add_private_overlay(&overlay_short_id, vec![cpp_adnl_id.to_string()]); - let cpp_overlay_id = - cpp.create_private_overlay(overlay_name_bytes, vec![rust_adnl_id]).expect("create overlay"); - - // Set broadcast validator mode on C++ - cpp.set_broadcast_validator(&cpp_overlay_id, validator_mode).expect("set validator mode"); - - // Exchange peers - add to both ADNL and overlay neighbours - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("add peer"); - - (cpp, rust_node, overlay_short_id, cpp_overlay_id) -} - -/// Test: C++ in accept_all mode receives broadcast from Rust -#[test] -fn test_cpp_accept_all_receives_broadcast() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_validation_pair(0, "accept_all"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_secs(1)); - - // Send broadcast from Rust using the overlay we created - let test_data = b"Broadcast with accept_all validation"; - rust_node.send_broadcast(&overlay_id, test_data); - - // Wait for delivery - sleep(Duration::from_secs(3)); - - let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); - - // Broadcast MUST be delivered for the test to pass - assert!(!received.is_empty(), "Rust->C++ broadcast was not delivered in accept_all mode"); - assert!(received[0].accepted, "Broadcast should be accepted"); - assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); - println!("accept_all: broadcast correctly accepted and delivered!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: C++ in reject_all mode does NOT deliver broadcast from Rust -#[test] -fn test_cpp_reject_all_drops_broadcast() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_validation_pair(10, "reject_all"); - - // Allow time for ADNL channel - sleep(Duration::from_millis(500)); - - // Send broadcast from Rust using the overlay we created - let test_data = b"Broadcast with reject_all validation"; - rust_node.send_broadcast(&overlay_id, test_data); - - // Wait to ensure it would have arrived - sleep(Duration::from_secs(2)); - - let received = cpp.get_received_broadcasts(&cpp_overlay_id).expect("get broadcasts"); - - // In reject_all mode, no broadcasts should be delivered to application layer - println!("reject_all: C++ received {} broadcasts (should be 0)", received.len()); - - // If the broadcast was received at the ADNL level but rejected by validator, - // it should NOT appear in received_broadcasts - for bc in &received { - assert!(!bc.accepted, "In reject_all mode, no broadcast should be accepted"); - } - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: toggling validator mode between accept and reject -#[test] -fn test_cpp_toggle_validator_mode() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 20; - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - - let overlay_name = b"toggle_test_overlay"; - // Use private overlay (doesn't require DHT) - let overlay_id = cpp.create_private_overlay(overlay_name, vec![]).expect("create overlay"); - - // Toggle modes - for mode in ["accept_all", "reject_all", "accept_all", "reject_all", "accept_all"] { - cpp.set_broadcast_validator(&overlay_id, mode).expect(&format!("set mode={}", mode)); - println!("Set broadcast_validator mode={}", mode); - } - - cpp.shutdown().expect("shutdown"); -} - -/// Test: C++ node correctly reports acceptance state -#[test] -fn test_broadcast_acceptance_tracking() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 30; - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - - let overlay_name = b"acceptance_tracking_test"; - // Use private overlay (doesn't require DHT) - let overlay_id = cpp.create_private_overlay(overlay_name, vec![]).expect("create overlay"); - - // Initially should have no broadcasts - let received = cpp.get_received_broadcasts(&overlay_id).expect("get broadcasts"); - assert!(received.is_empty(), "Should have no broadcasts initially"); - - // Clear should work on empty list - cpp.clear_received_broadcasts(&overlay_id).expect("clear broadcasts"); - - let received = cpp.get_received_broadcasts(&overlay_id).expect("get broadcasts"); - assert!(received.is_empty(), "Should still be empty after clear"); - - println!("Broadcast acceptance tracking works correctly"); - - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs b/src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs deleted file mode 100644 index bb382f4..0000000 --- a/src/node/tests/compat_test/tests/test_candidate_id_to_sign.rs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Cross-implementation tests for candidate_id_to_sign bytes. -//! -//! Verifies Rust and C++ build identical TL bytes for: -//! consensus.candidateId slot:int hash:int256 - -use compat_test::{skip_if_no_cpp, CppTestNode}; -use ton_api::{serialize_boxed, ton::consensus, IntoBoxed}; -use ton_block::UInt256; - -fn parse_uint256(hex_hash: &str) -> UInt256 { - let bytes = hex::decode(hex_hash).expect("hex decode failed"); - let arr: [u8; 32] = bytes.try_into().expect("hash must be exactly 32 bytes"); - UInt256::with_array(arr) -} - -fn rust_candidate_id_to_sign(slot: i32, hex_hash: &str) -> Vec { - let hash = parse_uint256(hex_hash); - let candidate_id = consensus::candidateid::CandidateId { slot, hash }; - serialize_boxed(&candidate_id.into_boxed()).expect("serialize candidateId") -} - -fn rust_candidate_parent_wrapped(slot: i32, hex_hash: &str) -> Vec { - let hash = parse_uint256(hex_hash); - let candidate_id = consensus::candidateid::CandidateId { slot, hash }; - let parent = consensus::candidateparent::CandidateParent { - id: consensus::CandidateId::Consensus_CandidateId(candidate_id), - }; - serialize_boxed(&parent.into_boxed()).expect("serialize candidateParent") -} - -#[test] -fn test_candidate_id_to_sign_matches_cpp() { - skip_if_no_cpp!(); - - let mut cpp = CppTestNode::spawn(15900).expect("spawn C++"); - let cases: &[(i32, &str)] = &[ - (0, "0000000000000000000000000000000000000000000000000000000000000000"), - (1, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - (17, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), - (777, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"), - ]; - - for (slot, hash_hex) in cases { - let cpp_bytes = - cpp.compute_candidate_id_to_sign(*slot, hash_hex).expect("cpp compute candidate id"); - let rust_bytes = rust_candidate_id_to_sign(*slot, hash_hex); - assert_eq!( - cpp_bytes, rust_bytes, - "candidateId bytes mismatch for slot={} hash={}", - slot, hash_hex - ); - } - - cpp.shutdown().expect("shutdown"); -} - -#[test] -fn test_candidate_id_to_sign_not_candidate_parent() { - skip_if_no_cpp!(); - - let mut cpp = CppTestNode::spawn(15901).expect("spawn C++"); - let slot = 42; - let hash_hex = "1111111111111111111111111111111111111111111111111111111111111111"; - - let cpp_bytes = - cpp.compute_candidate_id_to_sign(slot, hash_hex).expect("cpp compute candidate id"); - let rust_candidate_id = rust_candidate_id_to_sign(slot, hash_hex); - let rust_parent_wrapped = rust_candidate_parent_wrapped(slot, hash_hex); - - assert_eq!(cpp_bytes, rust_candidate_id); - assert_ne!( - cpp_bytes, rust_parent_wrapped, - "C++ side must sign candidateId directly, not candidateParent wrapper" - ); - - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_fec_relay.rs b/src/node/tests/compat_test/tests/test_fec_relay.rs deleted file mode 100644 index 556d175..0000000 --- a/src/node/tests/compat_test/tests/test_fec_relay.rs +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! FEC broadcast relay tests between Rust and C++ implementations. -//! -//! Tests a 3-node linear topology: Sender -> Relay -> Receiver -//! where Sender and Receiver are NOT directly connected. -//! A large broadcast (>768 bytes) triggers FEC encoding. -//! The relay node must receive, reassemble, and redistribute the broadcast -//! to the receiver. -//! -//! Test matrix: -//! | Test | Sender | Relay | Receiver | -//! |------|--------|-------|----------| -//! | 1 | Rust | C++ | Rust | -//! | 2 | C++ | Rust | C++ | -//! | 3 | Rust | Rust | C++ | -//! | 4 | C++ | C++ | Rust | - -use adnl::OverlayShortId; -use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; -use std::{sync::Arc, thread::sleep, time::Duration}; - -/// Port base for this test file (each test offsets by 10) -const PORT_BASE: u16 = 15600; - -/// FEC broadcast data size (must be > 768 to trigger FEC) -const FEC_DATA_SIZE: usize = 2000; - -/// Generate test data of given size with a tag byte for identification -fn make_test_data(size: usize, tag: u8) -> Vec { - (0..size).map(|i| ((i % 251) as u8).wrapping_add(tag)).collect() -} - -/// Enum to track node role in the topology -enum Node { - Rust(RustTestNode), - Cpp(CppTestNode), -} - -/// Setup result containing the 3 nodes plus overlay info -struct RelayTopology { - sender: Node, - relay: Node, - receiver: Node, - /// Overlay short ID (for Rust nodes) - overlay_short_id: Arc, - /// Overlay ID hex string (for C++ nodes) - overlay_id_hex: String, - /// Overlay name bytes (for creating overlays) - _overlay_name: Vec, -} - -impl RelayTopology { - fn shutdown(self) { - match self.sender { - Node::Rust(r) => r.stop(), - Node::Cpp(mut c) => { - let _ = c.shutdown(); - } - } - match self.relay { - Node::Rust(r) => r.stop(), - Node::Cpp(mut c) => { - let _ = c.shutdown(); - } - } - match self.receiver { - Node::Rust(r) => r.stop(), - Node::Cpp(mut c) => { - let _ = c.shutdown(); - } - } - } -} - -/// Create a 3-node relay topology. -/// -/// `roles` is (sender_is_rust, relay_is_rust, receiver_is_rust). -/// Wiring: sender <-> relay, relay <-> receiver. NOT sender <-> receiver. -fn setup_relay_topology( - port_offset: u16, - sender_is_rust: bool, - relay_is_rust: bool, - receiver_is_rust: bool, -) -> RelayTopology { - let port0 = PORT_BASE + port_offset; // sender - let port1 = PORT_BASE + port_offset + 1; // relay - let port2 = PORT_BASE + port_offset + 2; // receiver - - // First, create a temporary Rust node just to compute overlay IDs consistently - // (we'll reuse it if sender is Rust, otherwise stop it) - let helper = RustTestNode::new("127.0.0.1", port0 + 5, false); - let overlay_short_id = helper.compute_overlay_short_id(0, i64::MIN); - let overlay_name_bytes = helper.compute_overlay_name(0, i64::MIN); - let overlay_id_hex = - overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); - helper.stop(); - - // Create all nodes - let sender_rust = - if sender_is_rust { Some(RustTestNode::new("127.0.0.1", port0, false)) } else { None }; - let mut sender_cpp = if !sender_is_rust { - Some(CppTestNode::spawn(port0).expect("spawn sender C++")) - } else { - None - }; - - let relay_rust = - if relay_is_rust { Some(RustTestNode::new("127.0.0.1", port1, false)) } else { None }; - let mut relay_cpp = if !relay_is_rust { - Some(CppTestNode::spawn(port1).expect("spawn relay C++")) - } else { - None - }; - - let receiver_rust = - if receiver_is_rust { Some(RustTestNode::new("127.0.0.1", port2, false)) } else { None }; - let mut receiver_cpp = if !receiver_is_rust { - Some(CppTestNode::spawn(port2).expect("spawn receiver C++")) - } else { - None - }; - - // Collect all ADNL IDs for C++ private overlay creation - let sender_adnl_id = match (&sender_rust, &sender_cpp) { - (Some(r), _) => r.adnl_id_hex(), - (_, Some(c)) => c.adnl_id().to_string(), - _ => unreachable!(), - }; - let relay_adnl_id = match (&relay_rust, &relay_cpp) { - (Some(r), _) => r.adnl_id_hex(), - (_, Some(c)) => c.adnl_id().to_string(), - _ => unreachable!(), - }; - let receiver_adnl_id = match (&receiver_rust, &receiver_cpp) { - (Some(r), _) => r.adnl_id_hex(), - (_, Some(c)) => c.adnl_id().to_string(), - _ => unreachable!(), - }; - - // Create overlays on all nodes - // Rust nodes use public overlay; C++ nodes use private overlay with their direct neighbors - if let Some(ref r) = sender_rust { - r.add_public_overlay(&overlay_short_id); - } - if let Some(ref mut c) = sender_cpp { - // Sender's only neighbor is relay - c.create_private_overlay(&overlay_name_bytes, vec![relay_adnl_id.clone()]) - .expect("sender C++ create overlay"); - } - - if let Some(ref r) = relay_rust { - r.add_public_overlay(&overlay_short_id); - } - if let Some(ref mut c) = relay_cpp { - // Relay's neighbors are sender and receiver - c.create_private_overlay( - &overlay_name_bytes, - vec![sender_adnl_id.clone(), receiver_adnl_id.clone()], - ) - .expect("relay C++ create overlay"); - } - - if let Some(ref r) = receiver_rust { - r.add_public_overlay(&overlay_short_id); - } - if let Some(ref mut c) = receiver_cpp { - // Receiver's only neighbor is relay - c.create_private_overlay(&overlay_name_bytes, vec![relay_adnl_id.clone()]) - .expect("receiver C++ create overlay"); - } - - // Wire ADNL peers: sender <-> relay, relay <-> receiver - // NOT sender <-> receiver (that's the whole point of the relay test) - - // === sender <-> relay === - wire_pair(&sender_rust, &mut sender_cpp, &relay_rust, &mut relay_cpp, &overlay_short_id); - - // === relay <-> receiver === - wire_pair(&relay_rust, &mut relay_cpp, &receiver_rust, &mut receiver_cpp, &overlay_short_id); - - // Package nodes - let sender = - if let Some(r) = sender_rust { Node::Rust(r) } else { Node::Cpp(sender_cpp.unwrap()) }; - let relay = - if let Some(r) = relay_rust { Node::Rust(r) } else { Node::Cpp(relay_cpp.unwrap()) }; - let receiver = - if let Some(r) = receiver_rust { Node::Rust(r) } else { Node::Cpp(receiver_cpp.unwrap()) }; - - RelayTopology { - sender, - relay, - receiver, - overlay_short_id, - overlay_id_hex, - _overlay_name: overlay_name_bytes, - } -} - -/// Wire two nodes as ADNL peers and overlay neighbors (bidirectional). -/// Handles all 4 combinations of Rust/C++ for each side. -fn wire_pair( - a_rust: &Option, - a_cpp: &mut Option, - b_rust: &Option, - b_cpp: &mut Option, - overlay_id: &Arc, -) { - match (a_rust.as_ref(), a_cpp.as_mut(), b_rust.as_ref(), b_cpp.as_mut()) { - // Both Rust - (Some(a), _, Some(b), _) => { - a.add_rust_peer_to_overlay(b, overlay_id); - b.add_rust_peer_to_overlay(a, overlay_id); - } - // A=Rust, B=C++ - (Some(a), _, _, Some(b)) => { - a.add_cpp_peer_to_overlay(b, overlay_id); - b.add_peer(&a.pubkey_tl_b64(), "127.0.0.1", a.port).expect("C++ add_peer"); - } - // A=C++, B=Rust - (_, Some(a), Some(b), _) => { - b.add_cpp_peer_to_overlay(a, overlay_id); - a.add_peer(&b.pubkey_tl_b64(), "127.0.0.1", b.port).expect("C++ add_peer"); - } - // Both C++ - (_, Some(a), _, Some(b)) => { - // C++ private overlays handle peering automatically via the peer list - // But we still need to add ADNL peers - let b_pubkey = b.pubkey().to_string(); - let b_port = b.udp_port(); - a.add_peer(&b_pubkey, "127.0.0.1", b_port).expect("C++ add_peer a->b"); - let a_pubkey = a.pubkey().to_string(); - let a_port = a.udp_port(); - b.add_peer(&a_pubkey, "127.0.0.1", a_port).expect("C++ add_peer b->a"); - } - _ => unreachable!("Invalid node combination"), - } -} - -// =================== Tests =================== - -/// Test 1: Rust sender -> C++ relay -> Rust receiver -#[test] -fn test_fec_relay_rust_cpp_rust() { - skip_if_no_cpp!(); - - let topo = setup_relay_topology(0, true, false, true); - sleep(Duration::from_secs(2)); - - let test_data = make_test_data(FEC_DATA_SIZE, 0x11); - - // Send from Rust sender, then immediately wait on Rust receiver. - // NOTE: wait_for_broadcast must be called soon after send because - // BroadcastReceiver drops data pushed before pop() is first called. - if let Node::Rust(ref sender) = topo.sender { - sender.send_broadcast(&topo.overlay_short_id, &test_data); - } - - if let Node::Rust(ref receiver) = topo.receiver { - let received = receiver.wait_for_broadcast(&topo.overlay_short_id, 20); - assert!(received.is_some(), "Rust receiver did not get FEC broadcast via C++ relay"); - assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); - println!("test_fec_relay_rust_cpp_rust: PASSED"); - } - - topo.shutdown(); -} - -/// Test 2: C++ sender -> Rust relay -> C++ receiver -#[test] -fn test_fec_relay_cpp_rust_cpp() { - skip_if_no_cpp!(); - - let mut topo = setup_relay_topology(10, false, true, false); - sleep(Duration::from_secs(2)); - - let test_data = make_test_data(FEC_DATA_SIZE, 0x22); - - // Send from C++ sender - if let Node::Cpp(ref mut sender) = topo.sender { - sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); - } - - // Wait for relay and redistribution - sleep(Duration::from_secs(12)); - - // Check C++ receiver got the broadcast - if let Node::Cpp(ref mut receiver) = topo.receiver { - let received = - receiver.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); - assert!(!received.is_empty(), "C++ receiver did not get FEC broadcast via Rust relay"); - assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); - println!("test_fec_relay_cpp_rust_cpp: PASSED"); - } - - topo.shutdown(); -} - -/// Test 3: Rust sender -> Rust relay -> C++ receiver -#[test] -fn test_fec_relay_rust_rust_cpp() { - skip_if_no_cpp!(); - - let mut topo = setup_relay_topology(20, true, true, false); - sleep(Duration::from_secs(2)); - - let test_data = make_test_data(FEC_DATA_SIZE, 0x33); - - // Send from Rust sender - if let Node::Rust(ref sender) = topo.sender { - sender.send_broadcast(&topo.overlay_short_id, &test_data); - } - - // Wait for relay and redistribution - sleep(Duration::from_secs(12)); - - // Check C++ receiver got the broadcast - if let Node::Cpp(ref mut receiver) = topo.receiver { - let received = - receiver.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); - assert!(!received.is_empty(), "C++ receiver did not get FEC broadcast via Rust relay"); - assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); - println!("test_fec_relay_rust_rust_cpp: PASSED"); - } - - topo.shutdown(); -} - -/// Test 4: C++ sender -> C++ relay -> Rust receiver -#[test] -fn test_fec_relay_cpp_cpp_rust() { - skip_if_no_cpp!(); - - let mut topo = setup_relay_topology(30, false, false, true); - sleep(Duration::from_secs(2)); - - let test_data = make_test_data(FEC_DATA_SIZE, 0x44); - - // Send from C++ sender, then immediately wait on Rust receiver. - if let Node::Cpp(ref mut sender) = topo.sender { - sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); - } - - if let Node::Rust(ref receiver) = topo.receiver { - let received = receiver.wait_for_broadcast(&topo.overlay_short_id, 20); - assert!(received.is_some(), "Rust receiver did not get FEC broadcast via C++ relay"); - assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); - println!("test_fec_relay_cpp_cpp_rust: PASSED"); - } - - topo.shutdown(); -} diff --git a/src/node/tests/compat_test/tests/test_overlay_id.rs b/src/node/tests/compat_test/tests/test_overlay_id.rs deleted file mode 100644 index fdb1af8..0000000 --- a/src/node/tests/compat_test/tests/test_overlay_id.rs +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Overlay ID Calculation Compatibility Tests -//! -//! Tests that verify both Rust and C++ implementations compute identical -//! overlay IDs from the same input. - -use compat_test::{skip_if_no_cpp, CppTestNode}; - -/// Test that overlay ID calculation matches between Rust and C++ -#[test] -fn test_overlay_id_calculation_matches() { - skip_if_no_cpp!(); - - let mut cpp_node = CppTestNode::spawn(14010).expect("Failed to spawn C++ node"); - - // Test various overlay names - let medium_name = "x".repeat(100); - let long_name = "y".repeat(300); - let test_names: Vec<&str> = vec![ - "test_overlay", - "catchain", - "validator_session", - // Note: Empty name is not tested here as C++ rejects empty base64 input - "a", // short name - &medium_name, // medium name - &long_name, // long name (> 254 bytes) - ]; - - for name in test_names { - // Get C++ computed overlay ID - let cpp_id = cpp_node - .compute_overlay_id(name.as_bytes()) - .expect(&format!("C++ failed to compute overlay ID for '{}'", name)); - - // Get Rust computed overlay ID - let rust_id = compat_test::overlay_id::compute_overlay_id(name.as_bytes()); - let rust_id_hex = hex::encode(rust_id); - - // Compare (C++ returns uppercase hex, Rust returns lowercase) - assert_eq!( - cpp_id.to_lowercase(), - rust_id_hex.to_lowercase(), - "Overlay ID mismatch for name '{}': C++={}, Rust={}", - name, - cpp_id, - rust_id_hex - ); - - println!( - "Overlay ID matches for '{}': {}", - if name.len() > 20 { &name[..20] } else { name }, - rust_id_hex - ); - } - - cpp_node.shutdown().expect("Failed to shutdown C++ node"); -} - -/// Test overlay ID with binary data -#[test] -fn test_overlay_id_binary_data() { - skip_if_no_cpp!(); - - let mut cpp_node = CppTestNode::spawn(14011).expect("Failed to spawn C++ node"); - - // Test with various byte patterns - let test_cases: Vec<&[u8]> = vec![ - b"binary\x00data", // embedded null - "unicode_ั‚ะตัั‚".as_bytes(), // unicode - b"\x01\x02\x03\x04", // low bytes - ]; - - for name in test_cases { - let cpp_id = cpp_node.compute_overlay_id(name).expect("C++ failed for binary test"); - - let rust_id = compat_test::overlay_id::compute_overlay_id(name); - let rust_id_hex = hex::encode(rust_id); - - assert_eq!( - cpp_id.to_lowercase(), - rust_id_hex.to_lowercase(), - "Overlay ID mismatch for binary data" - ); - } - - cpp_node.shutdown().expect("Failed to shutdown"); -} - -/// Test that C++ node responds to ping -#[test] -fn test_cpp_node_ping() { - skip_if_no_cpp!(); - - let mut cpp_node = CppTestNode::spawn(14012).expect("Failed to spawn C++ node"); - - cpp_node.ping().expect("Ping failed"); - println!("C++ node ping successful"); - - cpp_node.shutdown().expect("Failed to shutdown"); -} - -/// Test getting ADNL ID from C++ node -#[test] -fn test_cpp_node_adnl_id() { - skip_if_no_cpp!(); - - let mut cpp_node = CppTestNode::spawn(14013).expect("Failed to spawn C++ node"); - - let adnl_id = cpp_node.adnl_id(); - assert!(!adnl_id.is_empty(), "ADNL ID should not be empty"); - assert_eq!(adnl_id.len(), 64, "ADNL ID should be 64 hex chars (32 bytes)"); - - // Verify it's valid hex - hex::decode(adnl_id).expect("ADNL ID should be valid hex"); - - println!("C++ node ADNL ID: {}", adnl_id); - - cpp_node.shutdown().expect("Failed to shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_overlay_message.rs b/src/node/tests/compat_test/tests/test_overlay_message.rs deleted file mode 100644 index 837964d..0000000 --- a/src/node/tests/compat_test/tests/test_overlay_message.rs +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Overlay point-to-point message delivery tests between Rust and C++ implementations. -//! -//! These tests verify that Rust and C++ nodes can exchange overlay messages -//! (not broadcasts) through overlays. This is the same path used by -//! simplex consensus for sending votes and certificates. -//! -//! The key difference from broadcast tests: -//! - Broadcasts use overlay.broadcast() / Overlays::send_broadcast_ex() -//! - Messages use overlay.message() / Overlays::send_message() - -use compat_test::{ - skip_if_no_cpp, - test_helpers::{MessageCollector, RustTestNode}, - CppTestNode, -}; -use std::{thread::sleep, time::Duration}; - -/// Port base for overlay message tests -const PORT_BASE: u16 = 15400; - -/// Test: Send overlay message from C++ to Rust (C++ โ†’ Rust) -/// C++ uses Overlays::send_message(), Rust receives via overlay consumer callback. -#[test] -fn test_overlay_message_cpp_to_rust() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE; - let rust_port = PORT_BASE + 1; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get both ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - println!("Rust ADNL ID: {}", rust_id); - println!("C++ ADNL ID: {}", cpp_id); - - // Create private overlay on C++ - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add true private overlay on Rust side with C++ peer in the member list. - // Must be private overlay โ€” the overlay dispatcher only calls try_consume_custom - // on the consumer for private overlays. - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - rust_node.add_true_private_overlay(&overlay_short_id, &[cpp_key_id], false); - let collector = MessageCollector::new(); - rust_node.overlay.add_consumer(&overlay_short_id, collector.clone()).expect("add consumer"); - - // Exchange ADNL peers (just ADNL level, overlay peers are set at creation time) - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send overlay message from C++ to Rust - let test_data = b"Hello from C++ overlay message"; - cpp.send_message(&cpp_overlay_id, &rust_id, test_data).expect("C++ send message"); - - println!("C++ sent overlay message ({} bytes) to Rust", test_data.len()); - - // Wait for Rust to receive via MessageCollector - let received = collector.wait_for_messages(&rust_node.rt, 1, 5); - - assert!(!received.is_empty(), "C++->Rust overlay message was NOT delivered"); - assert_eq!(received[0], test_data, "Message data mismatch"); - println!("C++->Rust overlay message delivered and verified!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Send overlay message from Rust to C++ (Rust โ†’ C++) -/// This is the critical path: Rust overlay.message() โ†’ C++ receive_message callback. -/// This is what simplex consensus uses to send votes and certificates. -#[test] -fn test_overlay_message_rust_to_cpp() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 10; - let rust_port = PORT_BASE + 11; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get both ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - println!("Rust ADNL ID: {}", rust_id); - println!("C++ ADNL ID: {}", cpp_id); - - // Create private overlay on C++ - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add overlay on Rust side - rust_node.add_public_overlay(&overlay_short_id); - - // Exchange ADNL peers - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Get C++ key id for targeting - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send overlay message from Rust to C++ using Fast (UDP) method - let test_data = b"Hello from Rust overlay message (Fast/UDP)"; - println!("Sending overlay message from Rust to C++ ({} bytes, Fast/UDP)", test_data.len()); - rust_node.send_message(&overlay_short_id, &cpp_key_id, test_data); - - // Wait for C++ to receive - sleep(Duration::from_secs(2)); - - // Check C++ received messages - let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); - - println!("C++ received {} overlay messages", received.len()); - for (i, msg) in received.iter().enumerate() { - println!(" msg[{}]: source={}, size={}", i, msg.source, msg.size); - } - - assert!(!received.is_empty(), "Rust->C++ overlay message was NOT delivered via Fast/UDP"); - assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Send multiple overlay messages from Rust to C++ and check delivery rate -/// This simulates the real consensus scenario where many small messages are sent. -#[test] -fn test_overlay_message_burst_rust_to_cpp() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 30; - let rust_port = PORT_BASE + 31; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get both ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - // Create private overlay on C++ - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add overlay on Rust side - rust_node.add_public_overlay(&overlay_short_id); - - // Exchange ADNL peers - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Get C++ key id for targeting - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - - // Allow time for ADNL channel establishment - sleep(Duration::from_secs(1)); - - // Send a burst of messages (simulating consensus votes) - let num_messages = 20; - println!("Sending {} overlay messages from Rust to C++ (Fast/UDP)", num_messages); - - for i in 0..num_messages { - let msg = format!("vote_message_{:04}", i); - rust_node.send_message(&overlay_short_id, &cpp_key_id, msg.as_bytes()); - } - - // Wait for delivery - sleep(Duration::from_secs(3)); - - // Check delivery rate - let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); - - println!( - "Delivery rate: {}/{} ({:.1}%)", - received.len(), - num_messages, - (received.len() as f64 / num_messages as f64) * 100.0 - ); - - for (i, msg) in received.iter().enumerate() { - println!(" msg[{}]: source={}, size={}", i, msg.source, msg.size); - } - - // Require at least 90% delivery โ€” UDP on localhost should be reliable. - // Anything less indicates a real problem, not normal network loss. - let min_required = (num_messages as f64 * 0.9) as usize; - assert!( - received.len() >= min_required, - "Too many messages lost: {}/{} delivered ({:.1}% loss, need >=90%)", - received.len(), - num_messages, - (1.0 - received.len() as f64 / num_messages as f64) * 100.0 - ); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Send overlay message from C++ to C++ (C++ โ†’ C++) as baseline -/// This confirms C++ send_message works when both sides are C++. -#[test] -fn test_overlay_message_cpp_to_cpp_baseline() { - skip_if_no_cpp!(); - - let cpp1_port = PORT_BASE + 40; - let cpp2_port = PORT_BASE + 41; - - let mut cpp1 = CppTestNode::spawn(cpp1_port).expect("spawn C++ node 1"); - let mut cpp2 = CppTestNode::spawn(cpp2_port).expect("spawn C++ node 2"); - - // Use a simple overlay name for testing - let overlay_name = b"test_message_overlay_cpp2cpp"; - - let cpp1_id = cpp1.adnl_id().to_string(); - let cpp2_id = cpp2.adnl_id().to_string(); - - println!("C++ node 1 ADNL ID: {}", cpp1_id); - println!("C++ node 2 ADNL ID: {}", cpp2_id); - - // Create private overlay on both C++ nodes - let overlay_id_1 = cpp1 - .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) - .expect("C++ 1 create private overlay"); - let overlay_id_2 = cpp2 - .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) - .expect("C++ 2 create private overlay"); - - assert_eq!(overlay_id_1, overlay_id_2, "Overlay IDs should match"); - - // Exchange peers - let cpp1_pubkey = cpp1.pubkey().to_string(); - let cpp2_pubkey = cpp2.pubkey().to_string(); - cpp1.add_peer(&cpp2_pubkey, "127.0.0.1", cpp2_port).expect("C++ 1 add peer"); - cpp2.add_peer(&cpp1_pubkey, "127.0.0.1", cpp1_port).expect("C++ 2 add peer"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send overlay message from C++ node 1 to C++ node 2 - let test_data = b"C++ to C++ overlay message"; - cpp1.send_message(&overlay_id_1, &cpp2_id, test_data).expect("C++ 1 send message"); - - // Wait for delivery - sleep(Duration::from_secs(2)); - - // Check if C++ node 2 received - let received = cpp2.get_received_messages(&overlay_id_2).expect("get messages"); - - println!("C++ node 2 received {} overlay messages", received.len()); - - assert!(!received.is_empty(), "C++->C++ overlay message was NOT delivered (baseline test)"); - assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); - - cpp1.shutdown().expect("shutdown node 1"); - cpp2.shutdown().expect("shutdown node 2"); -} diff --git a/src/node/tests/compat_test/tests/test_public_overlay.rs b/src/node/tests/compat_test/tests/test_public_overlay.rs deleted file mode 100644 index f6803d6..0000000 --- a/src/node/tests/compat_test/tests/test_public_overlay.rs +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Overlay query compatibility tests between Rust and C++ implementations. -//! -//! Tests that overlay queries (request/response) work correctly between -//! Rust and C++ nodes in both directions. - -use adnl::{common::TaggedTlObject, OverlayShortId}; -use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; -use std::{sync::Arc, thread::sleep, time::Duration}; -use ton_api::{serialize_boxed, ton::rpc::adnl::Ping as AdnlPing, AnyBoxedSerialize}; - -/// Port base for this test file (each test offsets by 10) -const PORT_BASE: u16 = 15150; - -/// Set up a Rust + C++ node pair on an overlay. -/// Uses private overlay on C++ side (no DHT required) and public overlay on Rust side. -/// Returns (cpp_node, rust_node, overlay_short_id, cpp_overlay_id_hex) -fn setup_overlay_pair( - port_offset: u16, -) -> (CppTestNode, RustTestNode, Arc, String) { - let cpp_port = PORT_BASE + port_offset; - let rust_port = PORT_BASE + port_offset + 1; - - // 1. Spawn C++ node - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - - // 2. Create Rust node - let rust_node = RustTestNode::new("127.0.0.1", rust_port, false); - - // 3. Compute overlay ID on Rust side (workchain=0, shard=-9223372036854775808 i.e. 0x8000000000000000) - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - - // 4. Create overlay on both sides - rust_node.add_public_overlay(&overlay_short_id); - - let rust_adnl_id = rust_node.adnl_id_hex(); - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_adnl_id]) - .expect("create C++ overlay"); - - // Verify overlay IDs match - let rust_overlay_hex = - overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); - assert_eq!( - cpp_overlay_id.to_lowercase(), - rust_overlay_hex.to_lowercase(), - "Overlay IDs should match between C++ and Rust" - ); - - // 5. Exchange ADNL peers AND add to overlay neighbours - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add_peer"); - - (cpp, rust_node, overlay_short_id, cpp_overlay_id) -} - -/// Deterministic positive query test: C++ sends a valid TL query and Rust echoes it back. -#[test] -fn test_query_cpp_to_rust_echo_roundtrip() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(0); - - // Add echo consumer on Rust side - let echo = compat_test::test_helpers::EchoConsumer::new(); - rust_node.overlay.add_consumer(&overlay_id, echo).expect("add consumer"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // C++ sends a valid TL-serialized query to Rust - let rust_adnl_id = rust_node.adnl_id_hex(); - let query = AdnlPing { value: 0x1122_3344_5566_7788 }; - let query_bytes = serialize_boxed(&query).expect("serialize query"); - - let answer = cpp - .send_query(&cpp_overlay_id, &rust_adnl_id, &query_bytes, 5000) - .expect("C++->Rust query should succeed"); - - assert_eq!(answer, query_bytes, "C++->Rust echo reply mismatch"); - println!("C++->Rust query echo roundtrip succeeded"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Deterministic negative query test: C++ rejects Rust query. -/// Note: C++ ADNL drops errors server-side (logs them but sends no response), -/// so the Rust side sees a timeout (Ok(None)) rather than an explicit error. -#[test] -fn test_query_rust_to_cpp_rejects_with_error() { - skip_if_no_cpp!(); - - let (mut cpp, rust_node, overlay_id, cpp_overlay_id) = setup_overlay_pair(10); - - // Configure C++ side to explicitly reject queries. - cpp.set_query_handler(&cpp_overlay_id, "reject").expect("set reject handler"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Rust sends query to C++ - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - - // Build a valid TL query object - let query_data = AdnlPing { value: 0x0102_0304_0506_0708 }; - let tagged: TaggedTlObject = query_data.into_tl_object().into(); - - let result = rust_node.rt.block_on(async { - rust_node.overlay.query(&cpp_key_id, &tagged, &overlay_id, Some(5000)).await - }); - - // C++ ADNL drops rejected queries without sending a response back to the peer, - // so the Rust side either times out (Ok(None)) or gets a transport-level error. - match result { - Ok(None) => { - println!("Rust->C++ query timed out as expected (C++ dropped the rejected query)"); - } - Err(e) => { - println!("Rust->C++ query failed with error (expected): {}", e); - } - Ok(Some(_)) => panic!("Expected timeout or error from C++ reject mode, got a response"), - } - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_quic_address.rs b/src/node/tests/compat_test/tests/test_quic_address.rs deleted file mode 100644 index 6dfae77..0000000 --- a/src/node/tests/compat_test/tests/test_quic_address.rs +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! QUIC address (adnl.address.quic) compatibility tests. -//! -//! Verifies that Rust and C++ nodes can establish QUIC connections when the -//! peer's QUIC address is discovered via `adnl.address.quic` in the address -//! list, rather than derived from the ADNL UDP port + 1000 offset. -//! -//! This tests the changes from C++ PR ton-blockchain/ton#2184 -//! ("Store ip:port for quic in AdnlAddressList"). - -use adnl::common::AdnlPeers; -use compat_test::{skip_if_no_cpp, test_helpers::RustQuicTestNode, CppTestNode, TestTimeout}; -use std::{thread::sleep, time::Duration}; -use ton_api::{serialize_boxed, ton::ton_node::data::Data as TonNodeData, IntoBoxed}; - -/// Port base for QUIC address tests (unique range to avoid conflicts) -const PORT_BASE: u16 = 18300; - -/// Test: Rust discovers C++ QUIC port via adnl.address.quic and sends a QUIC query. -/// -/// Instead of hardcoding the QUIC port as `udp_port + 1000`, the Rust node receives -/// an AddressList containing `adnl.address.quic` with an explicit port. The QUIC -/// connection is established using that address, and a query echo roundtrip verifies -/// it works end-to-end. -#[test] -fn test_quic_query_via_address_list_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE; - let rust_port = PORT_BASE + 1; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let cpp_quic_port = cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - // Rust adds C++ peer via an AddressList containing adnl.address.quic. - // This exercises the new parse_quic_address โ†’ set_peer_quic_address โ†’ - // ensure_peer_registered path (no hardcoded port offset). - rust_node.add_cpp_peer_via_address_list(&cpp, cpp_quic_port); - - // C++ adds Rust as a peer (standard way) - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // Send a QUIC query from Rust to C++. - // The query must be a valid TL-serialized object for the C++ side to process. - let payload = b"QUIC query via adnl.address.quic"; - let tl_query = TonNodeData { data: payload.to_vec().into() }; - let query_data = serialize_boxed(&tl_query.into_boxed()).expect("serialize query TL"); - - let src = rust_node.adnl_key_id(); - let dst = RustQuicTestNode::cpp_key_id(&cpp); - - println!( - "Sending QUIC query from Rust to C++ ({} bytes), QUIC addr discovered via adnl.address.quic", - query_data.len() - ); - - let result = rust_node.rt.block_on(async { - tokio::time::timeout( - Duration::from_secs(10), - rust_node.quic.query( - query_data.clone(), - Some(&*rust_node.adnl), - &AdnlPeers::with_keys(src, dst.clone()), - None, - ), - ) - .await - }); - - match result { - Ok(Ok(Some(answer))) => { - println!("SUCCESS: Got QUIC echo answer ({} bytes)", answer.len()); - assert_eq!(answer, query_data, "Echo data mismatch"); - } - Ok(Ok(None)) => panic!("QUIC query returned empty answer"), - Ok(Err(e)) => panic!("QUIC query failed: {}", e), - Err(_) => panic!("QUIC query timed out"), - } - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: C++ discovers Rust QUIC port via adnl.address.quic and sends a QUIC query. -/// -/// The C++ node receives the Rust peer with an explicit `quic_port` in the -/// `add_peer` command, which includes `adnl.address.quic` in the address list. -/// C++ then sends a QUIC query to Rust using that address. -#[test] -fn test_quic_query_via_address_list_cpp_to_rust() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 10; - let rust_port = PORT_BASE + 11; - let rust_quic_port = rust_port + adnl::QuicNode::OFFSET_PORT; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - let rust_id = rust_node.adnl_id_hex(); - - // Rust adds C++ as a standard peer - rust_node.add_cpp_peer(&cpp); - - // C++ adds Rust with explicit quic_port in the address list. - // This makes C++ include adnl.address.quic in the peer's AddressList, - // so QuicSender::get_ip_address() uses it instead of the UDP+offset fallback. - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer_with_quic(&rust_pubkey, "127.0.0.1", rust_port, Some(rust_quic_port)) - .expect("C++ add peer with quic"); - - sleep(Duration::from_millis(500)); - - // C++ sends QUIC query to Rust - let payload = b"QUIC query via adnl.address.quic from C++"; - let tl_query = TonNodeData { data: payload.to_vec().into() }; - let query_data = serialize_boxed(&tl_query.into_boxed()).expect("serialize query TL"); - - println!( - "C++ sending QUIC query to Rust ({} bytes), QUIC addr via adnl.address.quic", - query_data.len() - ); - - let result = cpp.send_quic_query(&rust_id, &query_data, 5000); - - match result { - Ok(answer) => { - println!("SUCCESS: C++ got QUIC echo answer ({} bytes)", answer.len()); - assert_eq!(answer, query_data, "Echo data mismatch"); - } - Err(e) => panic!("C++โ†’Rust QUIC query failed: {}", e), - } - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_quic_overlay.rs b/src/node/tests/compat_test/tests/test_quic_overlay.rs deleted file mode 100644 index b4dadbc..0000000 --- a/src/node/tests/compat_test/tests/test_quic_overlay.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Overlay-level QUIC compatibility tests between Rust and C++ implementations. -//! -//! Tests overlay operations where at least one direction uses QUIC transport. -//! The C++ QuicSender routes messages through ADNL to the overlay layer, -//! so overlay operations should work if the QUIC transport layer is compatible. -//! -//! Note: The overlay itself does not directly use QUIC. Instead: -//! - C++ sends via QuicSender.send_message() โ†’ QUIC โ†’ receiver's QuicSender -//! โ†’ receiver's ADNL.receive_message() โ†’ overlay callback -//! - Rust sends via QuicTransport.send_message() โ†’ QUIC stream โ†’ C++ QuicSender -//! โ†’ ADNL.receive_message() โ†’ overlay callback - -use compat_test::{skip_if_no_cpp, test_helpers::RustQuicTestNode, CppTestNode, TestTimeout}; -use std::{thread::sleep, time::Duration}; -use ton_api::{serialize_boxed, ton::overlay::message::Message as OverlayMessage, IntoBoxed}; -use ton_block::UInt256; - -/// Port base for QUIC overlay tests -const PORT_BASE: u16 = 18100; - -/// Test: C++ sends overlay message via QUIC to another C++ node -/// -/// Baseline test: verifies C++ QUIC works between two C++ nodes. -/// Both nodes enable QUIC and exchange messages. -#[test] -fn test_quic_overlay_cpp_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp1_port = PORT_BASE; - let cpp2_port = PORT_BASE + 1; - - let mut cpp1 = CppTestNode::spawn(cpp1_port).expect("spawn C++ node 1"); - let mut cpp2 = CppTestNode::spawn(cpp2_port).expect("spawn C++ node 2"); - - cpp1.enable_quic().expect("enable QUIC on C++ node 1"); - cpp2.enable_quic().expect("enable QUIC on C++ node 2"); - - let cpp1_id = cpp1.adnl_id().to_string(); - let cpp2_id = cpp2.adnl_id().to_string(); - println!("C++ node 1 ADNL ID: {}", cpp1_id); - println!("C++ node 2 ADNL ID: {}", cpp2_id); - - // Create overlay on both nodes - let overlay_name = b"test_quic_overlay_cpp2cpp"; - let overlay_id_1 = cpp1 - .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) - .expect("C++ 1 create private overlay"); - let overlay_id_2 = cpp2 - .create_private_overlay(overlay_name, vec![cpp1_id.clone(), cpp2_id.clone()]) - .expect("C++ 2 create private overlay"); - assert_eq!(overlay_id_1, overlay_id_2, "Overlay IDs should match"); - - // Exchange ADNL peers (needed for QuicSender address resolution) - let cpp1_pubkey = cpp1.pubkey().to_string(); - let cpp2_pubkey = cpp2.pubkey().to_string(); - cpp1.add_peer(&cpp2_pubkey, "127.0.0.1", cpp2_port).expect("C++ 1 add peer"); - cpp2.add_peer(&cpp1_pubkey, "127.0.0.1", cpp1_port).expect("C++ 2 add peer"); - - sleep(Duration::from_millis(500)); - - // C++ node 1 sends QUIC message to C++ node 2. - // Data must be prefixed with overlay.message TL for the receiver's ADNL - // to route it to the overlay callback. - let test_data = b"C++ to C++ overlay via QUIC"; - let overlay_bytes = hex::decode(&overlay_id_1).expect("decode overlay hex"); - let mut overlay_msg = serialize_boxed( - &OverlayMessage { overlay: UInt256::with_array(overlay_bytes.try_into().unwrap()) } - .into_boxed(), - ) - .expect("serialize overlay message prefix"); - overlay_msg.extend_from_slice(test_data); - - println!("C++ 1 sending QUIC message to C++ 2 ({} bytes)", overlay_msg.len()); - cpp1.send_quic_message(&cpp2_id, &overlay_msg).expect("C++ 1 send QUIC message"); - - sleep(Duration::from_secs(2)); - - // Check if C++ node 2 received (via ADNL โ†’ overlay callback) - let received = cpp2.get_received_messages(&overlay_id_2).expect("get messages"); - println!("C++ node 2 received {} messages", received.len()); - - assert!( - !received.is_empty(), - "C++โ†’C++ QUIC overlay message not received: QuicSender may not match overlay expectations" - ); - println!("SUCCESS: C++โ†’C++ QUIC overlay message delivered"); - - cpp1.shutdown().expect("shutdown node 1"); - cpp2.shutdown().expect("shutdown node 2"); -} - -/// Test: Rust sends overlay message via both UDP and QUIC, C++ receives both -/// -/// First sends a baseline UDP overlay message to confirm overlay routing works, -/// then sends via QUIC transport and verifies C++ receives it through the -/// ADNL โ†’ overlay callback path. -#[test] -fn test_quic_overlay_message_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 10; - let rust_port = PORT_BASE + 11; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - // Create overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - - // Exchange peers (ADNL + QUIC) - rust_node.add_cpp_peer_full(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_secs(1)); - - // Send overlay message from Rust via regular ADNL (as baseline) - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - let baseline_data = b"Baseline: Rust overlay message via UDP"; - println!("Sending baseline overlay message via UDP ({} bytes)", baseline_data.len()); - rust_node.send_overlay_message(&overlay_short_id, &cpp_key_id, baseline_data); - - sleep(Duration::from_secs(2)); - - let baseline_received = - cpp.get_received_messages(&cpp_overlay_id).expect("get baseline messages"); - println!("Baseline (UDP): C++ received {} overlay messages", baseline_received.len()); - - // Clear for QUIC test - cpp.clear_received_messages(&cpp_overlay_id).expect("clear messages"); - - // Now send via QUIC transport with overlay TL wrapping - let quic_data = b"QUIC: Rust to C++ overlay test"; - println!("Sending QUIC overlay message from Rust to C++ ({} bytes)", quic_data.len()); - rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, quic_data); - - sleep(Duration::from_secs(2)); - - let quic_received = cpp.get_received_messages(&cpp_overlay_id).expect("get QUIC messages"); - println!("QUIC transport: C++ received {} messages via overlay", quic_received.len()); - - assert!(!baseline_received.is_empty(), "Baseline UDP overlay message not received"); - assert!( - !quic_received.is_empty(), - "UDP overlay messages work but QUIC messages don't reach overlay" - ); - println!("SUCCESS: Both UDP and QUIC messages delivered to C++ overlay"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: C++ sends QUIC message to Rust, Rust receives via QUIC transport -/// -/// Verifies raw QUIC message delivery from C++ to Rust. The message arrives -/// at the QuicTestSubscriber (transport level), not through overlay routing. -/// Expected: PASS โ€” Rust server accepts C++ connections without SNI. -#[test] -fn test_quic_message_cpp_to_rust() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 20; - let rust_port = PORT_BASE + 21; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - // Create overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let _cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - - // Exchange peers - rust_node.add_cpp_peer_full(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_secs(1)); - - // C++ sends QUIC message to Rust - let test_data = b"C++ overlay message via QUIC to Rust"; - println!("C++ sending QUIC message to Rust ({} bytes)", test_data.len()); - let send_result = cpp.send_quic_message(&rust_id, test_data); - println!("C++ send_quic_message: {:?}", send_result.is_ok()); - - // Try to receive on Rust QUIC subscriber - let received = rust_node.recv_quic_message(3); - - let data = received.expect("C++โ†’Rust QUIC overlay message should be received"); - println!("SUCCESS: Rust received QUIC message from C++ ({} bytes)", data.len()); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: QUIC overlay query from Rust to C++ with echo handler -/// -/// Sends a QUIC query from Rust to C++ where C++ has an echo handler set up. -/// The query goes through QuicTransport โ†’ C++ QuicSender โ†’ ADNL โ†’ overlay query handler. -#[test] -fn test_quic_overlay_query_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 30; - let rust_port = PORT_BASE + 31; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - // Create overlay with echo handler - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set echo handler"); - - rust_node.add_public_overlay(&overlay_short_id); - - // Exchange peers - rust_node.add_cpp_peer_full(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_secs(1)); - - // First verify baseline: overlay query via UDP works - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - println!("Baseline: testing overlay query via UDP..."); - let baseline_result = - cpp.send_query(&cpp_overlay_id, &rust_node.adnl_id_hex(), b"baseline query", 5000); - println!("Baseline overlay query result: {:?}", baseline_result.is_ok()); - - // Now try QUIC query with overlay wrapping - let query_data = b"QUIC overlay query from Rust"; - println!("Sending QUIC overlay query from Rust to C++ ({} bytes)", query_data.len()); - - let result = rust_node.send_quic_overlay_query(&cpp_key_id, &overlay_short_id, query_data, 10); - - let answer = result.expect("QUIC overlay query should succeed"); - println!("SUCCESS: Got QUIC overlay query answer ({} bytes)", answer.len()); - assert!(!answer.is_empty(), "QUIC overlay query answer should not be empty"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_quic_private_overlay.rs b/src/node/tests/compat_test/tests/test_quic_private_overlay.rs deleted file mode 100644 index a752121..0000000 --- a/src/node/tests/compat_test/tests/test_quic_private_overlay.rs +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! Private overlay tests with ADNL and QUIC transport. -//! -//! These tests create **true private overlays** on the Rust side using -//! `OverlayNode::add_private_overlay()` with a signing key โ€” matching how -//! validator consensus overlays are created in production. -//! -//! QUIC transport is used by calling `send_quic_overlay_message` / -//! `send_quic_overlay_query` directly, which bypass the overlay layer and -//! send via `QuicNode`. The overlay's own transport is always ADNL. -//! -//! Test matrix: -//! - Private overlay + ADNL send: baseline -//! - Private overlay + QUIC send: message and query delivery through QUIC -//! - Private overlay + C++โ†’Rust: inbound message delivery via ADNL - -use compat_test::{ - skip_if_no_cpp, - test_helpers::{MessageCollector, RustQuicTestNode}, - CppTestNode, -}; -use std::{thread::sleep, time::Duration}; - -/// Port base for QUIC private overlay tests (must not conflict with other test suites) -const PORT_BASE: u16 = 18200; - -/// Test: Private overlay message via ADNL (baseline). -/// -/// Creates a true private overlay on Rust side and sends a message via ADNL. -/// This verifies that `add_private_overlay()` + `overlay.message()` work -/// correctly with C++ โ€” the same code path validators use. -#[test] -fn test_private_overlay_adnl_message_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - - let cpp_port = PORT_BASE; - let rust_port = PORT_BASE + 1; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - println!("Rust ADNL ID: {rust_id}"); - println!("C++ ADNL ID: {cpp_id}"); - - // Compute overlay (same method as existing tests) - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // C++ creates private overlay - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Rust creates TRUE private overlay (not public shortcut) with ADNL transport - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); - - // Exchange ADNL peers - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Add C++ peer to overlay's known peers - // Peer already registered in overlay via add_private_overlay - - sleep(Duration::from_millis(500)); - - // Send message through overlay.message() โ€” routes via ADNL - let test_data = b"Private overlay message via ADNL (baseline)"; - println!("Sending private overlay message via ADNL ({} bytes)", test_data.len()); - rust_node.send_overlay_message(&overlay_short_id, &cpp_key_id, test_data); - - sleep(Duration::from_secs(2)); - - let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); - println!("C++ received {} overlay messages (ADNL)", received.len()); - for (i, msg) in received.iter().enumerate() { - println!(" msg[{i}]: source={}, size={}", msg.source, msg.size); - } - - assert!(!received.is_empty(), "Private overlay message via ADNL was NOT delivered (Rustโ†’C++)"); - assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Private overlay message via QUIC transport. -/// -/// Sends a message via `send_quic_overlay_message` which uses `QuicNode` -/// directly (bypassing `overlay.message()` which always uses ADNL). -#[test] -fn test_private_overlay_quic_message_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - - let cpp_port = PORT_BASE + 10; - let rust_port = PORT_BASE + 11; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - println!("Rust ADNL ID: {rust_id}"); - println!("C++ ADNL ID: {cpp_id}"); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // C++ creates private overlay (with QUIC enabled) - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Rust creates private overlay with QUIC transport - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); - - // Exchange ADNL peers (needed for peer identity resolution) - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Register QUIC peer address - rust_node.add_cpp_quic_peer(&cpp); - - // Add C++ peer to overlay's known peers - // Peer already registered in overlay via add_private_overlay - - sleep(Duration::from_secs(1)); - - // Send message directly via QUIC transport (bypassing overlay.message() which - // always routes via ADNL/UDP โ€” the overlay layer has no QUIC transport config). - let test_data = b"Private overlay message via QUIC transport"; - println!("Sending private overlay message via QUIC ({} bytes)", test_data.len()); - rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, test_data); - - sleep(Duration::from_secs(2)); - - let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); - println!("C++ received {} overlay messages (QUIC)", received.len()); - for (i, msg) in received.iter().enumerate() { - println!(" msg[{i}]: source={}, size={}", msg.source, msg.size); - } - - assert!(!received.is_empty(), "Private overlay message via QUIC was NOT delivered (Rustโ†’C++)"); - assert_eq!(received[0].size, test_data.len(), "Message size mismatch"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Multiple messages via QUIC transport in private overlay. -/// -/// Sends a burst of messages (simulating consensus votes) through a QUIC-backed -/// private overlay and checks delivery rate. -#[test] -fn test_private_overlay_quic_message_burst() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - - let cpp_port = PORT_BASE + 20; - let rust_port = PORT_BASE + 21; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); - - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - rust_node.add_cpp_quic_peer(&cpp); - // Peer already registered in overlay via add_private_overlay - - sleep(Duration::from_secs(1)); - - // Send burst of messages (simulating consensus votes) - let num_messages = 20; - println!("Sending {num_messages} overlay messages via QUIC"); - - for i in 0..num_messages { - let msg = format!("quic_vote_{i:04}"); - rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, msg.as_bytes()); - } - - sleep(Duration::from_secs(3)); - - let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); - println!( - "Delivery rate: {}/{} ({:.1}%)", - received.len(), - num_messages, - (received.len() as f64 / num_messages as f64) * 100.0 - ); - - assert!(!received.is_empty(), "No QUIC overlay messages delivered in burst of {num_messages}"); - // QUIC should deliver reliably โ€” stream-based, no UDP loss - assert_eq!( - received.len(), - num_messages, - "QUIC should deliver all messages (stream-based, no UDP loss)" - ); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Private overlay query via QUIC transport. -/// -/// Sends an overlay query from Rust to C++ through `overlay.query()` which -/// routes through the QUIC transport. The C++ echo handler returns the query -/// data in the response. -#[test] -fn test_private_overlay_quic_query_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - - let cpp_port = PORT_BASE + 30; - let rust_port = PORT_BASE + 31; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - println!("Rust ADNL ID: {rust_id}"); - println!("C++ ADNL ID: {cpp_id}"); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // C++ creates overlay with echo handler - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set echo handler"); - - // Rust creates private overlay with QUIC transport - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); - - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - rust_node.add_cpp_quic_peer(&cpp); - // Peer already registered in overlay via add_private_overlay - - sleep(Duration::from_secs(1)); - - // Send query through overlay.query() โ€” routes via QUIC transport - let query_data = b"QUIC private overlay query"; - println!("Sending overlay query via QUIC ({} bytes)", query_data.len()); - - // Use raw QUIC overlay query (overlay.query() needs a proper TL object, - // so we use the lower-level send_quic_overlay_query for now) - let result = rust_node.send_quic_overlay_query(&cpp_key_id, &overlay_short_id, query_data, 10); - - match result { - Ok(answer) => { - println!("SUCCESS: Got QUIC overlay query answer ({} bytes)", answer.len()); - assert!(!answer.is_empty(), "Answer should not be empty"); - } - Err(e) => { - panic!("QUIC overlay query via private overlay failed: {e}"); - } - } - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: C++ sends overlay message to Rust via private overlay (C++โ†’Rust direction). -/// -/// Verifies that messages from C++ arrive at Rust through the overlay callback -/// when Rust uses a private overlay. The C++ send_message goes through ADNL, -/// Rust receives via its overlay consumer regardless of its outbound transport. -#[test] -fn test_private_overlay_message_cpp_to_rust() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - - let cpp_port = PORT_BASE + 40; - let rust_port = PORT_BASE + 41; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - println!("Rust ADNL ID: {rust_id}"); - println!("C++ ADNL ID: {cpp_id}"); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // C++ creates private overlay - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Rust creates private overlay with ADNL (inbound transport doesn't matter โ€” - // incoming messages arrive via ADNL subscriber regardless) - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - rust_node.add_private_overlay(&overlay_short_id, &[cpp_key_id.clone()], true); - - // Register message collector to verify receipt on Rust side - let collector = MessageCollector::new(); - rust_node.overlay.add_consumer(&overlay_short_id, collector.clone()).expect("add consumer"); - - // Exchange peers - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // C++ sends overlay message to Rust - let test_data = b"C++ to Rust private overlay message"; - cpp.send_message(&cpp_overlay_id, &rust_id, test_data).expect("C++ send overlay message"); - println!("C++ sent overlay message to Rust ({} bytes)", test_data.len()); - - // Wait for Rust to receive via MessageCollector - let received = collector.wait_for_messages(&rust_node.rt, 1, 5); - - assert!(!received.is_empty(), "C++โ†’Rust private overlay message was NOT delivered"); - assert_eq!(received[0], test_data, "Message data mismatch"); - println!("C++โ†’Rust private overlay message delivered and verified!"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_quic_transport.rs b/src/node/tests/compat_test/tests/test_quic_transport.rs deleted file mode 100644 index a5224ea..0000000 --- a/src/node/tests/compat_test/tests/test_quic_transport.rs +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! QUIC transport compatibility tests between Rust and C++ implementations. -//! -//! Tests raw QUIC query exchange (C++โ†’Rust), large overlay message delivery -//! via QUIC, and QUIC connection establishment (TLS handshake with RPK certs). -//! -//! For overlay-routed QUIC messages/queries (Rustโ†’C++ and C++โ†’Rust), see -//! `test_quic_overlay.rs`. For private overlay QUIC tests, see -//! `test_quic_private_overlay.rs`. - -use compat_test::{skip_if_no_cpp, test_helpers::RustQuicTestNode, CppTestNode, TestTimeout}; -use std::{ - panic::{catch_unwind, AssertUnwindSafe}, - thread::sleep, - time::Duration, -}; -use ton_api::{serialize_boxed, ton::ton_node::data::Data as TonNodeData, IntoBoxed}; - -/// Port base for QUIC transport tests -const PORT_BASE: u16 = 18000; - -/// Test: C++ sends QUIC query to Rust, expects echo answer -/// -/// Expected: PASS โ€” the Rust QUIC server processes the query and echoes it back. -/// Note: Query data must be a valid TL-serialized object because the Rust -/// query processing pipeline deserializes inner data via deserialize_boxed_bundle(). -#[test] -fn test_quic_query_cpp_to_rust() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 30; - let rust_port = PORT_BASE + 31; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - - // Exchange peers - rust_node.add_cpp_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // C++ sends QUIC query to Rust. - // The inner data must be a valid TL-serialized object because the Rust - // Query::process() calls deserialize_boxed_bundle() on the raw bytes. - let payload = b"QUIC query from C++ to Rust"; - let tl_query = TonNodeData { data: payload.to_vec().into() }; - let query_data = serialize_boxed(&tl_query.into_boxed()).expect("serialize query TL"); - println!("C++ sending QUIC query to Rust ({} bytes TL-wrapped)", query_data.len()); - - let result = cpp.send_quic_query(&rust_id, &query_data, 5000); - - match result { - Ok(answer) => { - println!("SUCCESS: C++ got QUIC query answer ({} bytes)", answer.len()); - // The echo subscriber returns the same TL object; verify it matches - assert_eq!(answer, query_data, "Query echo data mismatch"); - } - Err(e) => { - panic!("C++โ†’Rust QUIC query failed: {}", e); - } - } - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Rust sends a QUIC message close to the C++ stream size limit (900 bytes payload). -/// -/// The C++ QuicSender enforces a 1024-byte per-stream limit for messages. -/// With overlay prefix (~36 bytes) and quic.message TL wrapper (~8 bytes), -/// 900 bytes of payload stays just under the limit. -/// The message is overlay-routed so C++ can verify receipt. -#[test] -fn test_quic_large_overlay_message_rust_to_cpp() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 40; - let rust_port = PORT_BASE + 41; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - cpp.enable_quic().expect("enable QUIC on C++"); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - // Create overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - - // Exchange peers - rust_node.add_cpp_peer(&cpp); - rust_node.add_cpp_quic_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // Send 900-byte message with overlay wrapping (under C++ 1024-byte stream limit) - let large_data: Vec = (0..900u32).map(|i| (i % 256) as u8).collect(); - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - println!("Sending {} byte QUIC overlay message from Rust to C++", large_data.len()); - rust_node.send_quic_overlay_message(&cpp_key_id, &overlay_short_id, &large_data); - - sleep(Duration::from_secs(3)); - - let received = cpp.get_received_messages(&cpp_overlay_id).expect("get messages"); - println!("C++ received {} messages", received.len()); - - assert!(!received.is_empty(), "QUIC overlay message not received"); - println!("SUCCESS: QUIC message delivered ({} bytes)", received[0].size); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: QUIC connection establishment between Rust and C++ -/// -/// Minimal test that just verifies whether a QUIC connection can be established -/// from Rust to C++ (TLS handshake with RPK certificates). -#[test] -fn test_quic_connection_establishment() { - skip_if_no_cpp!(); - let _ = env_logger::try_init(); - let _timeout = TestTimeout::new(0); - - let cpp_port = PORT_BASE + 50; - let rust_port = PORT_BASE + 51; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let quic_port = cpp.enable_quic().expect("enable QUIC on C++"); - println!("C++ QUIC port: {}", quic_port); - - let rust_node = RustQuicTestNode::new("127.0.0.1", rust_port); - println!("Rust QUIC port: {}", rust_port + 1000); - - // Exchange ADNL peers - rust_node.add_cpp_peer(&cpp); - rust_node.add_cpp_quic_peer(&cpp); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // Try to send a small message โ€” the connection attempt itself is the test - let cpp_key_id = RustQuicTestNode::cpp_key_id(&cpp); - let result = catch_unwind(AssertUnwindSafe(|| { - rust_node.send_quic_message(&cpp_key_id, b"ping"); - })); - - result.expect("QUIC connection should succeed (Rust โ†’ C++)"); - println!("SUCCESS: QUIC connection established (Rust โ†’ C++)"); - println!("TLS handshake with RPK certificates succeeded"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} diff --git a/src/node/tests/compat_test/tests/test_rldp_query.rs b/src/node/tests/compat_test/tests/test_rldp_query.rs deleted file mode 100644 index 0cc3447..0000000 --- a/src/node/tests/compat_test/tests/test_rldp_query.rs +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! RLDP query/response cross-implementation compatibility tests. -//! -//! Tests verify that Rust and C++ nodes can exchange RLDP queries and receive -//! correct echo answers, for both RLDP v1 and v2 protocols. -//! -//! Topology: 2 nodes (sender + responder), echo handler on responder side. -//! The sender sends a query, the responder echoes it back, and the sender -//! verifies the answer matches the original data. -//! -//! Important: All query data is wrapped in a `tonNode.data` TL envelope because -//! the Rust overlay requires valid TL-serialized objects in the query bundle -//! (overlay.query prefix + TL-serialized inner object). The C++ overlay treats -//! query data as opaque bytes, but Rust's `deserialize_boxed_bundle` must -//! successfully parse both TL objects. - -use compat_test::{ - skip_if_no_cpp, - test_helpers::{EchoConsumer, RustTestNode}, - CppTestNode, -}; -use std::{thread::sleep, time::Duration}; -use ton_api::{ - deserialize_boxed, serialize_boxed, - ton::ton_node::{data::Data as TonNodeData, Data as TonNodeDataBoxed}, - IntoBoxed, -}; - -/// Port base for RLDP query tests (each test offsets by 10) -const PORT_BASE: u16 = 15800; - -/// Wrap test data in tonNode.data TL envelope and serialize to bytes. -/// This is needed because overlay RLDP queries must carry valid TL objects. -fn wrap_in_tl(data: &[u8]) -> Vec { - let tl_data = TonNodeData { data: data.to_vec().into() }; - serialize_boxed(&tl_data.into_boxed()).expect("serialize tonNode.data") -} - -/// Extract inner data from a TL-serialized tonNode.data response. -fn unwrap_from_tl(tl_bytes: &[u8]) -> Vec { - let obj = deserialize_boxed(tl_bytes).expect("deserialize TL answer"); - match obj.downcast::() { - Ok(data) => data.only().data.to_vec(), - Err(obj) => panic!("Unexpected TL type in answer: {:?}", obj), - } -} - -// --------------------------------------------------------------------------- -// RLDP v1 tests -// --------------------------------------------------------------------------- - -/// Test: RLDP v1 query from Rust to C++ (Rust โ†’ C++) -/// Rust sends an RLDP query, C++ echoes it back. -#[test] -fn test_rldp_v1_rust_to_cpp() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE; - let rust_port = PORT_BASE + 1; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - println!("Rust ADNL ID: {}", rust_id); - println!("C++ ADNL ID: {}", cpp_id); - - // Create private overlay on C++ (echo handler is default) - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add overlay on Rust side - rust_node.add_public_overlay(&overlay_short_id); - - // Set echo query handler on C++ - cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); - - // Exchange ADNL peers - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Get C++ key id for targeting - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send RLDP v1 query from Rust to C++ - // send_rldp_query wraps in tonNode.data + overlay.query prefix internally - let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); - println!("Sending RLDP v1 query from Rust to C++ ({} bytes)", test_data.len()); - - let answer = rust_node.send_rldp_query( - &overlay_short_id, - &cpp_key_id, - &test_data, - 1 << 20, // 1MB max answer - false, // v1 - ); - - assert!(answer.is_some(), "RLDP v1 Rustโ†’C++ query got no answer"); - let answer_bytes = answer.unwrap(); - println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); - - // C++ echo handler returns the raw bytes it received (= TL-serialized tonNode.data) - // Unwrap the TL envelope to get the original data - let answer_data = unwrap_from_tl(&answer_bytes); - assert_eq!(answer_data, test_data, "RLDP v1 Rustโ†’C++ echo mismatch"); - - println!("PASS: RLDP v1 Rustโ†’C++ query/response works"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: RLDP v1 query from C++ to Rust (C++ โ†’ Rust) -/// C++ sends an RLDP query, Rust echoes it back via EchoConsumer. -#[test] -fn test_rldp_v1_cpp_to_rust() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 10; - let rust_port = PORT_BASE + 11; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - println!("Rust ADNL ID: {}", rust_id); - println!("C++ ADNL ID: {}", cpp_id); - - // Create private overlay on C++ - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add overlay on Rust side and register echo consumer - rust_node.add_public_overlay(&overlay_short_id); - rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); - - // Exchange ADNL peers - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send RLDP v1 query from C++ to Rust - // Pre-wrap data in TL so Rust's deserialize_boxed_bundle can parse it - let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); - let tl_data = wrap_in_tl(&test_data); - println!( - "Sending RLDP v1 query from C++ to Rust ({} bytes, {} TL-wrapped)", - test_data.len(), - tl_data.len() - ); - - let answer = cpp - .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, false) - .expect("C++ send_rldp_query failed"); - - println!("Got answer: {} bytes", answer.len()); - // Rust EchoConsumer echoes back the TLObject, which gets TL-serialized - let answer_data = unwrap_from_tl(&answer); - assert_eq!(answer_data, test_data, "RLDP v1 C++โ†’Rust echo mismatch"); - - println!("PASS: RLDP v1 C++โ†’Rust query/response works"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -// --------------------------------------------------------------------------- -// RLDP v2 tests -// --------------------------------------------------------------------------- - -/// Test: RLDP v2 query from Rust to C++ (Rust โ†’ C++) -/// Same as v1 test but using RLDP v2 (BBR congestion control, selective ACKs). -#[test] -fn test_rldp_v2_rust_to_cpp() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 20; - let rust_port = PORT_BASE + 21; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - println!("Rust ADNL ID: {}", rust_id); - println!("C++ ADNL ID: {}", cpp_id); - - // Create private overlay on C++ - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add overlay on Rust side - rust_node.add_public_overlay(&overlay_short_id); - - // Set echo query handler on C++ - cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); - - // Exchange ADNL peers - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Get C++ key id for targeting - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send RLDP v2 query from Rust to C++ - let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); - println!("Sending RLDP v2 query from Rust to C++ ({} bytes)", test_data.len()); - - let answer = rust_node.send_rldp_query( - &overlay_short_id, - &cpp_key_id, - &test_data, - 1 << 20, // 1MB max answer - true, // v2 - ); - - assert!(answer.is_some(), "RLDP v2 Rustโ†’C++ query got no answer"); - let answer_bytes = answer.unwrap(); - println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); - let answer_data = unwrap_from_tl(&answer_bytes); - assert_eq!(answer_data, test_data, "RLDP v2 Rustโ†’C++ echo mismatch"); - - println!("PASS: RLDP v2 Rustโ†’C++ query/response works"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: RLDP v2 query from C++ to Rust (C++ โ†’ Rust) -/// C++ sends an RLDP v2 query, Rust echoes it back via EchoConsumer. -#[test] -fn test_rldp_v2_cpp_to_rust() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 30; - let rust_port = PORT_BASE + 31; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - // Compute overlay - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - // Get ADNL IDs - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - println!("Rust ADNL ID: {}", rust_id); - println!("C++ ADNL ID: {}", cpp_id); - - // Create private overlay on C++ - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - // Add overlay on Rust side and register echo consumer - rust_node.add_public_overlay(&overlay_short_id); - rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); - - // Exchange ADNL peers - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - // Allow time for ADNL channel establishment - sleep(Duration::from_millis(500)); - - // Send RLDP v2 query from C++ to Rust - let test_data: Vec = (0..256).map(|i| (i % 256) as u8).collect(); - let tl_data = wrap_in_tl(&test_data); - println!( - "Sending RLDP v2 query from C++ to Rust ({} bytes, {} TL-wrapped)", - test_data.len(), - tl_data.len() - ); - - let answer = cpp - .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, true) - .expect("C++ send_rldp_query v2 failed"); - - println!("Got answer: {} bytes", answer.len()); - let answer_data = unwrap_from_tl(&answer); - assert_eq!(answer_data, test_data, "RLDP v2 C++โ†’Rust echo mismatch"); - - println!("PASS: RLDP v2 C++โ†’Rust query/response works"); - - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -// --------------------------------------------------------------------------- -// Larger payload tests (multi-symbol FEC, RLDP v2 only) -// --------------------------------------------------------------------------- -// 256-byte tests above fit in a single 768-byte FEC symbol. -// These tests exercise multi-symbol RaptorQ encoding/decoding (4KB โ‰ˆ 6 symbols). -// -// IMPORTANT: These tests must use RLDP v2 because of MTU limits: -// - C++ RLDP v1 default_mtu_ = 1024 bytes (adnl::Adnl::get_mtu()) โ€” drops incoming -// transfers with total_size > 1024 unless pre-registered via max_size_ or set_default_mtu -// - C++ RLDP v2 DEFAULT_MTU = 7680 bytes (RldpConnection::DEFAULT_MTU) โ€” allows larger -// unsolicited transfers, sufficient for our test payloads -// - Rust RLDP has no incoming MTU check (accepts any size) -// - In production, C++ uses PeersMtuLimitGuard to raise limits to max_block_size + 1024 - -/// Test: 4KB RLDP v2 query from Rust to C++ (multi-symbol FEC) -#[test] -fn test_rldp_v2_4kb_rust_to_cpp() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 40; - let rust_port = PORT_BASE + 41; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); - - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - sleep(Duration::from_millis(500)); - - // 4096 bytes โ‰ˆ 6 FEC symbols (768 bytes each) - let test_data: Vec = (0..4096).map(|i| (i % 256) as u8).collect(); - println!("Sending RLDP v2 4KB query from Rust to C++ ({} bytes)", test_data.len()); - - let answer = rust_node.send_rldp_query( - &overlay_short_id, - &cpp_key_id, - &test_data, - 1 << 20, - true, // v2 โ€” required for 4KB (RLDP v1 default_mtu=1024 would reject) - ); - - assert!(answer.is_some(), "RLDP v2 4KB Rustโ†’C++ query got no answer"); - let answer_bytes = answer.unwrap(); - println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); - let answer_data = unwrap_from_tl(&answer_bytes); - assert_eq!(answer_data, test_data, "RLDP v2 4KB Rustโ†’C++ echo mismatch"); - - println!("PASS: RLDP v2 4KB Rustโ†’C++ query/response works"); - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: 4KB RLDP v2 query from C++ to Rust (multi-symbol FEC) -#[test] -fn test_rldp_v2_4kb_cpp_to_rust() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 50; - let rust_port = PORT_BASE + 51; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); - - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // 4096 bytes + TL wrapper โ‰ˆ 4104 bytes (under C++ 8192-byte overlay limit) - let test_data: Vec = (0..4096).map(|i| (i % 256) as u8).collect(); - let tl_data = wrap_in_tl(&test_data); - println!( - "Sending RLDP v2 4KB query from C++ to Rust ({} bytes, {} TL-wrapped)", - test_data.len(), - tl_data.len() - ); - - let answer = cpp - .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, true) - .expect("C++ send_rldp_query v2 4KB failed"); - - println!("Got answer: {} bytes", answer.len()); - let answer_data = unwrap_from_tl(&answer); - assert_eq!(answer_data, test_data, "RLDP v2 4KB C++โ†’Rust echo mismatch"); - - println!("PASS: RLDP v2 4KB C++โ†’Rust query/response works"); - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Near-limit (7KB) RLDP v2 query from Rust to C++ (multi-symbol FEC) -/// 7168 bytes โ†’ rldp.query total โ‰ˆ 7300 bytes (under RLDP v2 DEFAULT_MTU=7680) -#[test] -fn test_rldp_v2_7kb_rust_to_cpp() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 60; - let rust_port = PORT_BASE + 61; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - cpp.set_query_handler(&cpp_overlay_id, "echo").expect("set query handler"); - - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - let cpp_key_id = RustTestNode::cpp_key_id(&cpp); - sleep(Duration::from_millis(500)); - - // 7168 bytes โ‰ˆ 10 FEC symbols, near RLDP v2 DEFAULT_MTU limit - let test_data: Vec = (0..7168).map(|i| (i % 256) as u8).collect(); - println!("Sending RLDP v2 7KB query from Rust to C++ ({} bytes)", test_data.len()); - - let answer = rust_node.send_rldp_query( - &overlay_short_id, - &cpp_key_id, - &test_data, - 1 << 20, - true, // v2 - ); - - assert!(answer.is_some(), "RLDP v2 7KB Rustโ†’C++ query got no answer"); - let answer_bytes = answer.unwrap(); - println!("Got answer: {} bytes (TL-wrapped)", answer_bytes.len()); - let answer_data = unwrap_from_tl(&answer_bytes); - assert_eq!(answer_data, test_data, "RLDP v2 7KB Rustโ†’C++ echo mismatch"); - - println!("PASS: RLDP v2 7KB Rustโ†’C++ query/response works"); - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -/// Test: Near-limit (7KB) RLDP v2 query from C++ to Rust (multi-symbol FEC) -#[test] -fn test_rldp_v2_7kb_cpp_to_rust() { - skip_if_no_cpp!(); - - let cpp_port = PORT_BASE + 70; - let rust_port = PORT_BASE + 71; - - let mut cpp = CppTestNode::spawn(cpp_port).expect("spawn C++"); - let rust_node = RustTestNode::new("127.0.0.1", rust_port, true); - - let overlay_name_bytes = rust_node.compute_overlay_name(0, i64::MIN); - let overlay_short_id = rust_node.compute_overlay_short_id(0, i64::MIN); - - let rust_id = rust_node.adnl_id_hex(); - let cpp_id = cpp.adnl_id().to_string(); - - let cpp_overlay_id = cpp - .create_private_overlay(&overlay_name_bytes, vec![rust_id.clone(), cpp_id.clone()]) - .expect("C++ create private overlay"); - - rust_node.add_public_overlay(&overlay_short_id); - rust_node.register_consumer(&overlay_short_id, EchoConsumer::new()); - - rust_node.add_cpp_peer_to_overlay(&mut cpp, &overlay_short_id); - let rust_pubkey = rust_node.pubkey_tl_b64(); - cpp.add_peer(&rust_pubkey, "127.0.0.1", rust_port).expect("C++ add peer"); - - sleep(Duration::from_millis(500)); - - // 7168 bytes + TL wrapper โ‰ˆ 7176 bytes (under C++ 8192-byte overlay limit) - let test_data: Vec = (0..7168).map(|i| (i % 256) as u8).collect(); - let tl_data = wrap_in_tl(&test_data); - println!( - "Sending RLDP v2 7KB query from C++ to Rust ({} bytes, {} TL-wrapped)", - test_data.len(), - tl_data.len() - ); - - let answer = cpp - .send_rldp_query(&cpp_overlay_id, &rust_id, &tl_data, 1 << 20, true) - .expect("C++ send_rldp_query v2 7KB failed"); - - println!("Got answer: {} bytes", answer.len()); - let answer_data = unwrap_from_tl(&answer); - assert_eq!(answer_data, test_data, "RLDP v2 7KB C++โ†’Rust echo mismatch"); - - println!("PASS: RLDP v2 7KB C++โ†’Rust query/response works"); - rust_node.stop(); - cpp.shutdown().expect("shutdown"); -} - -// Note on payload size limits: -// -// RLDP v1 default_mtu = 1024 bytes (total transfer size for unsolicited incoming): -// - Only ~928 bytes of user data fits (after rldp.query + overlay.query + tonNode.data overhead) -// - Production nodes call set_default_mtu() to raise this -// -// RLDP v2 DEFAULT_MTU = 7680 bytes: -// - Up to ~7.5KB user data fits without configuration -// - Production nodes use PeersMtuLimitGuard to raise to max_block_size + 1024 -// -// C++ overlay CHECK: query.size() <= huge_packet_max_size() (8192 bytes): -// - Hard limit on data passed to Overlays::send_query_via before RLDP wrapping -// - Applies to C++ sender side only -// -// Rust RaptorQ NEON alignment bug (aarch64 only): -// - Pre-existing bug triggered by certain larger payload sizes -// - Not related to cross-implementation compatibility diff --git a/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs b/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs deleted file mode 100644 index 70b50a0..0000000 --- a/src/node/tests/compat_test/tests/test_twostep_fec_relay.rs +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (C) 2025-2026 RSquad Blockchain Lab. - * - * Licensed under the GNU General Public License v3.0. - * See the LICENSE file in the root of this repository. - * - * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. - */ -//! TwostepFec broadcast relay tests between Rust and C++ implementations. -//! -//! Tests a 6-node topology: Sender -> 4 Bridges -> Leaf -//! - Sender connected to all 4 bridges (TwostepFec requires >=4 neighbors) -//! - Each bridge connected to leaf -//! - Leaf NOT directly connected to sender -//! - Data >= 513 bytes triggers TwostepFec (if RLDP + enough neighbors) -//! - All nodes need RLDP enabled -//! -//! TwostepFec sends unique FEC parts to each neighbor via RLDP. -//! Each receiver redistributes received parts to its neighbors. -//! The leaf can only receive through redistribution from bridges. - -use adnl::OverlayShortId; -use compat_test::{skip_if_no_cpp, test_helpers::RustTestNode, CppTestNode}; -use std::{sync::Arc, thread::sleep, time::Duration}; - -/// Port base for this test file -const PORT_BASE: u16 = 15700; - -/// Number of bridge nodes (must be >= 4 for TwostepFec) -const NUM_BRIDGES: usize = 4; - -/// Data size for TwostepFec (>= 513 bytes) -const TWOSTEP_DATA_SIZE: usize = 2048; - -fn make_test_data(size: usize, tag: u8) -> Vec { - (0..size).map(|i| ((i % 251) as u8).wrapping_add(tag)).collect() -} - -enum Node { - Rust(RustTestNode), - Cpp(CppTestNode), -} - -struct TwostepTopology { - sender: Node, - bridges: Vec, - leaf: Node, - overlay_short_id: Arc, - overlay_id_hex: String, -} - -impl TwostepTopology { - fn shutdown(self) { - match self.sender { - Node::Rust(r) => r.stop(), - Node::Cpp(mut c) => { - let _ = c.shutdown(); - } - } - for node in self.bridges { - match node { - Node::Rust(r) => r.stop(), - Node::Cpp(mut c) => { - let _ = c.shutdown(); - } - } - } - match self.leaf { - Node::Rust(r) => r.stop(), - Node::Cpp(mut c) => { - let _ = c.shutdown(); - } - } - } -} - -/// Create a 6-node topology for TwostepFec relay testing. -/// -/// `sender_is_rust`: whether sender is Rust (true) or C++ (false) -/// `bridge_rust_mask`: bitmask of which bridges are Rust (bit 0 = bridge 0, etc.) -/// `leaf_is_rust`: whether leaf is Rust (true) or C++ (false) -fn setup_twostep_topology( - port_offset: u16, - sender_is_rust: bool, - bridge_rust_mask: u8, - leaf_is_rust: bool, -) -> TwostepTopology { - let base = PORT_BASE + port_offset; - // Ports: sender=base, bridges=base+1..base+4, leaf=base+5, helper=base+8 - let sender_port = base; - let bridge_ports: Vec = (0..NUM_BRIDGES).map(|i| base + 1 + i as u16).collect(); - let leaf_port = base + 1 + NUM_BRIDGES as u16; - let helper_port = base + 8; - - // Compute overlay IDs using a helper node - let helper = RustTestNode::new("127.0.0.1", helper_port, false); - let overlay_short_id = helper.compute_overlay_short_id(0, i64::MIN); - let overlay_name_bytes = helper.compute_overlay_name(0, i64::MIN); - let overlay_id_hex = - overlay_short_id.data().iter().map(|b| format!("{:02x}", b)).collect::(); - helper.stop(); - - // Create all nodes (with RLDP for Rust, twostep for C++) - let sender_rust = - if sender_is_rust { Some(RustTestNode::new("127.0.0.1", sender_port, true)) } else { None }; - let mut sender_cpp = if !sender_is_rust { - Some(CppTestNode::spawn(sender_port).expect("spawn sender C++")) - } else { - None - }; - - let mut bridge_rusts: Vec> = Vec::new(); - let mut bridge_cpps: Vec> = Vec::new(); - for i in 0..NUM_BRIDGES { - let is_rust = (bridge_rust_mask >> i) & 1 == 1; - if is_rust { - bridge_rusts.push(Some(RustTestNode::new("127.0.0.1", bridge_ports[i], true))); - bridge_cpps.push(None); - } else { - bridge_rusts.push(None); - bridge_cpps.push(Some(CppTestNode::spawn(bridge_ports[i]).expect("spawn bridge C++"))); - } - } - - let leaf_rust = - if leaf_is_rust { Some(RustTestNode::new("127.0.0.1", leaf_port, true)) } else { None }; - let mut leaf_cpp = if !leaf_is_rust { - Some(CppTestNode::spawn(leaf_port).expect("spawn leaf C++")) - } else { - None - }; - - // Collect ADNL IDs - let sender_id = match (&sender_rust, &sender_cpp) { - (Some(r), _) => r.adnl_id_hex(), - (_, Some(c)) => c.adnl_id().to_string(), - _ => unreachable!(), - }; - let bridge_ids: Vec = (0..NUM_BRIDGES) - .map(|i| match (&bridge_rusts[i], &bridge_cpps[i]) { - (Some(r), _) => r.adnl_id_hex(), - (_, Some(c)) => c.adnl_id().to_string(), - _ => unreachable!(), - }) - .collect(); - let leaf_id = match (&leaf_rust, &leaf_cpp) { - (Some(r), _) => r.adnl_id_hex(), - (_, Some(c)) => c.adnl_id().to_string(), - _ => unreachable!(), - }; - - // Create overlays - // Sender: neighbors are all 4 bridges - if let Some(ref r) = sender_rust { - r.add_public_overlay(&overlay_short_id); - } - if let Some(ref mut c) = sender_cpp { - c.create_private_overlay_twostep(&overlay_name_bytes, bridge_ids.clone()) - .expect("sender C++ create overlay"); - } - - // Each bridge: neighbors are sender + leaf + other bridges - for i in 0..NUM_BRIDGES { - let mut bridge_peers = vec![sender_id.clone(), leaf_id.clone()]; - for j in 0..NUM_BRIDGES { - if i != j { - bridge_peers.push(bridge_ids[j].clone()); - } - } - if let Some(ref r) = bridge_rusts[i] { - r.add_public_overlay(&overlay_short_id); - } - if let Some(ref mut c) = bridge_cpps[i] { - c.create_private_overlay_twostep(&overlay_name_bytes, bridge_peers) - .expect("bridge C++ create overlay"); - } - } - - // Leaf: neighbors are all 4 bridges (NOT sender) - if let Some(ref r) = leaf_rust { - r.add_public_overlay(&overlay_short_id); - } - if let Some(ref mut c) = leaf_cpp { - c.create_private_overlay_twostep(&overlay_name_bytes, bridge_ids.clone()) - .expect("leaf C++ create overlay"); - } - - // Wire ADNL peers - // Sender <-> each bridge - for i in 0..NUM_BRIDGES { - wire_nodes( - &sender_rust, - sender_cpp.as_mut(), - &bridge_rusts[i], - bridge_cpps[i].as_mut(), - &overlay_short_id, - ); - } - - // Each bridge <-> leaf - for i in 0..NUM_BRIDGES { - wire_nodes( - &bridge_rusts[i], - bridge_cpps[i].as_mut(), - &leaf_rust, - leaf_cpp.as_mut(), - &overlay_short_id, - ); - } - - // Bridges <-> each other (for redistribution of FEC parts) - for i in 0..NUM_BRIDGES { - for j in (i + 1)..NUM_BRIDGES { - wire_nodes_by_idx(&bridge_rusts, &mut bridge_cpps, i, j, &overlay_short_id); - } - } - - // Package nodes - let sender = - if let Some(r) = sender_rust { Node::Rust(r) } else { Node::Cpp(sender_cpp.unwrap()) }; - - let bridges: Vec = (0..NUM_BRIDGES) - .map(|i| { - if let Some(r) = bridge_rusts[i].take() { - Node::Rust(r) - } else { - Node::Cpp(bridge_cpps[i].take().unwrap()) - } - }) - .collect(); - - let leaf = if let Some(r) = leaf_rust { Node::Rust(r) } else { Node::Cpp(leaf_cpp.unwrap()) }; - - TwostepTopology { sender, bridges, leaf, overlay_short_id, overlay_id_hex } -} - -/// Wire two nodes as ADNL peers and overlay neighbors (bidirectional). -fn wire_nodes( - a_rust: &Option, - a_cpp: Option<&mut CppTestNode>, - b_rust: &Option, - b_cpp: Option<&mut CppTestNode>, - overlay_id: &Arc, -) { - match (a_rust.as_ref(), a_cpp, b_rust.as_ref(), b_cpp) { - (Some(a), _, Some(b), _) => { - a.add_rust_peer_to_overlay(b, overlay_id); - b.add_rust_peer_to_overlay(a, overlay_id); - } - (Some(a), _, _, Some(b)) => { - a.add_cpp_peer_to_overlay(b, overlay_id); - b.add_peer(&a.pubkey_tl_b64(), "127.0.0.1", a.port).expect("C++ add_peer"); - } - (_, Some(a), Some(b), _) => { - b.add_cpp_peer_to_overlay(a, overlay_id); - a.add_peer(&b.pubkey_tl_b64(), "127.0.0.1", b.port).expect("C++ add_peer"); - } - (_, Some(a), _, Some(b)) => { - let b_pubkey = b.pubkey().to_string(); - let b_port = b.udp_port(); - a.add_peer(&b_pubkey, "127.0.0.1", b_port).expect("C++ add_peer a->b"); - let a_pubkey = a.pubkey().to_string(); - let a_port = a.udp_port(); - b.add_peer(&a_pubkey, "127.0.0.1", a_port).expect("C++ add_peer b->a"); - } - _ => unreachable!(), - } -} - -/// Wire two bridge nodes by index from the bridge arrays. -fn wire_nodes_by_idx( - rusts: &[Option], - cpps: &mut [Option], - i: usize, - j: usize, - overlay_id: &Arc, -) { - // Can't borrow two elements mutably at once, so handle it carefully - match (rusts[i].as_ref(), rusts[j].as_ref()) { - (Some(a), Some(b)) => { - a.add_rust_peer_to_overlay(b, overlay_id); - b.add_rust_peer_to_overlay(a, overlay_id); - } - (Some(a), None) => { - let b = cpps[j].as_mut().unwrap(); - a.add_cpp_peer_to_overlay(b, overlay_id); - b.add_peer(&a.pubkey_tl_b64(), "127.0.0.1", a.port).expect("C++ add_peer"); - } - (None, Some(b)) => { - let a = cpps[i].as_mut().unwrap(); - b.add_cpp_peer_to_overlay(a, overlay_id); - a.add_peer(&b.pubkey_tl_b64(), "127.0.0.1", b.port).expect("C++ add_peer"); - } - (None, None) => { - // Both C++ - split borrow by using pointers - let (left, right) = cpps.split_at_mut(j); - let a = left[i].as_mut().unwrap(); - let b = right[0].as_mut().unwrap(); - let b_pubkey = b.pubkey().to_string(); - let b_port = b.udp_port(); - a.add_peer(&b_pubkey, "127.0.0.1", b_port).expect("C++ add_peer"); - let a_pubkey = a.pubkey().to_string(); - let a_port = a.udp_port(); - b.add_peer(&a_pubkey, "127.0.0.1", a_port).expect("C++ add_peer"); - } - } -} - -// =================== Tests =================== - -/// Test A: Rust sender, all Rust bridges, C++ leaf -/// Verifies Rust TwostepFec reaches C++ through Rust redistribution -#[test] -fn test_twostep_rust_sender_cpp_leaf() { - skip_if_no_cpp!(); - - let mut topo = setup_twostep_topology(0, true, 0b1111, false); - sleep(Duration::from_millis(500)); - - let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xA1); - - // Send TwostepFec from Rust sender - if let Node::Rust(ref sender) = topo.sender { - sender.send_broadcast_twostep(&topo.overlay_short_id, &test_data); - } - - // Wait for redistribution and delivery - sleep(Duration::from_secs(5)); - - // Check C++ leaf received the broadcast - if let Node::Cpp(ref mut leaf) = topo.leaf { - let received = leaf.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); - assert!(!received.is_empty(), "C++ leaf did not get TwostepFec broadcast via Rust bridges"); - assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); - println!("test_twostep_rust_sender_cpp_leaf: PASSED"); - } - - topo.shutdown(); -} - -/// Test B: C++ sender, all C++ bridges, Rust leaf -/// Verifies C++ TwostepFec reaches Rust through C++ redistribution -#[test] -fn test_twostep_cpp_sender_rust_leaf() { - skip_if_no_cpp!(); - - let mut topo = setup_twostep_topology(10, false, 0b0000, true); - sleep(Duration::from_millis(500)); - - let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xB2); - - // Send FEC broadcast from C++ sender (C++ will use twostep if enabled) - if let Node::Cpp(ref mut sender) = topo.sender { - sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); - } - - // Check Rust leaf - call wait_for_broadcast immediately to avoid BroadcastReceiver drop - if let Node::Rust(ref leaf) = topo.leaf { - let received = leaf.wait_for_broadcast(&topo.overlay_short_id, 10); - assert!(received.is_some(), "Rust leaf did not get TwostepFec broadcast via C++ bridges"); - assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); - println!("test_twostep_cpp_sender_rust_leaf: PASSED"); - } - - topo.shutdown(); -} - -/// Test C: Rust sender, mixed bridges (2 Rust, 2 C++), Rust leaf -/// Verifies mixed Rust/C++ redistribution works -#[test] -fn test_twostep_mixed_bridges_rust_leaf() { - skip_if_no_cpp!(); - - // Bridges 0,1 are Rust, 2,3 are C++ - let topo = setup_twostep_topology(20, true, 0b0011, true); - sleep(Duration::from_millis(500)); - - let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xC3); - - // Send TwostepFec from Rust sender - if let Node::Rust(ref sender) = topo.sender { - sender.send_broadcast_twostep(&topo.overlay_short_id, &test_data); - } - - // Check Rust leaf - if let Node::Rust(ref leaf) = topo.leaf { - let received = leaf.wait_for_broadcast(&topo.overlay_short_id, 10); - assert!(received.is_some(), "Rust leaf did not get TwostepFec broadcast via mixed bridges"); - assert_eq!(received.unwrap(), test_data, "Broadcast data mismatch"); - println!("test_twostep_mixed_bridges_rust_leaf: PASSED"); - } - - topo.shutdown(); -} - -/// Test D: C++ sender, mixed bridges (2 Rust, 2 C++), C++ leaf -/// Verifies C++ TwostepFec works with mixed redistribution to C++ leaf -#[test] -fn test_twostep_mixed_bridges_cpp_leaf() { - skip_if_no_cpp!(); - - // Bridges 0,1 are Rust, 2,3 are C++ - let mut topo = setup_twostep_topology(30, false, 0b0011, false); - sleep(Duration::from_millis(500)); - - let test_data = make_test_data(TWOSTEP_DATA_SIZE, 0xD4); - - // Send FEC broadcast from C++ sender - if let Node::Cpp(ref mut sender) = topo.sender { - sender.send_broadcast(&topo.overlay_id_hex, &test_data, true).expect("C++ send broadcast"); - } - - // Wait for redistribution and delivery - sleep(Duration::from_secs(5)); - - // Check C++ leaf received the broadcast - if let Node::Cpp(ref mut leaf) = topo.leaf { - let received = leaf.get_received_broadcasts(&topo.overlay_id_hex).expect("get broadcasts"); - assert!( - !received.is_empty(), - "C++ leaf did not get TwostepFec broadcast via mixed bridges" - ); - assert_eq!(received[0].size, test_data.len(), "Broadcast size mismatch"); - println!("test_twostep_mixed_bridges_cpp_leaf: PASSED"); - } - - topo.shutdown(); -} diff --git a/src/node/tests/test_run_net_py/log_cfg_blank.yml b/src/node/tests/test_run_net_py/log_cfg_blank.yml index c2d0117..39355f0 100644 --- a/src/node/tests/test_run_net_py/log_cfg_blank.yml +++ b/src/node/tests/test_run_net_py/log_cfg_blank.yml @@ -99,9 +99,4 @@ loggers: simplex: level: debug - quic: - level: debug - - consensus_adnl_overlay: - level: debug diff --git a/src/node/tests/test_run_net_py/simplex_config.json b/src/node/tests/test_run_net_py/simplex_config.json index 196f51a..600f48c 100644 --- a/src/node/tests/test_run_net_py/simplex_config.json +++ b/src/node/tests/test_run_net_py/simplex_config.json @@ -1,6 +1,6 @@ { "target_rate_ms": 500, "slots_per_leader_window": 4, - "first_block_timeout_ms": 3000, + "first_block_timeout_ms": 1000, "max_leader_window_desync": 2 } diff --git a/src/node/tests/test_run_net_py/test_run_net.py b/src/node/tests/test_run_net_py/test_run_net.py index 6d05466..8304114 100644 --- a/src/node/tests/test_run_net_py/test_run_net.py +++ b/src/node/tests/test_run_net_py/test_run_net.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 import argparse -import base64 -import hashlib -import json import os -import shutil import subprocess +import shutil +import json +import yaml import time from pathlib import Path +import base64 +import hashlib -import yaml node_proc_name: str rust_proc_suffix: str @@ -164,12 +164,7 @@ def print_current_branches(): print(f"Current C++ branch: {current_branch_cpp}") -def run_command( - cmd: list[str], - cwd: Path | None = None, - check: bool = True, - capture_output: bool = True, -): +def run_command(cmd: list[str], cwd: Path | None = None, check: bool = True, capture_output: bool = True): try: result = subprocess.run( cmd, @@ -258,10 +253,7 @@ def build_node_work_path(node_index: int) -> Path: return work_dirs_path / f"node_{node_index}" -def prepare_default_config( - node_index: int, config_blank: str, log_config_blank: str, - use_quic: bool = False, quic_port_offset: int = 1000, -): +def prepare_default_config(node_index: int, config_blank: str, log_config_blank: str): node_work_path = build_node_work_path(node_index) node_work_path.mkdir(parents=True, exist_ok=True) @@ -284,11 +276,7 @@ def prepare_default_config( config["log_config_name"] = str(node_work_path / "log_cfg.yml") config["ton_global_config_name"] = str(common_config_path / "global_config.json") config["internal_db_path"] = str(node_work_path) - adnl_port = main_port_base + node_index - config["ip_address"] = f"{ip_address}:{adnl_port}" - if use_quic: - quic_port = adnl_port + quic_port_offset - config["ip_address_quic"] = f"{ip_address}:{quic_port}" + config["ip_address"] = f"{ip_address}:{main_port_base + node_index}" config["control_server_port"] = control_port_base + node_index config["lite_server_port"] = liteserver_port_base + node_index config["json_rpc_server"] = {"address": f"0.0.0.0:{jsonrpc_port_base + node_index}"} @@ -324,11 +312,6 @@ def run_cpp_node( stderr_path = logs_path / f"output_{node_index}.log" working_dir = build_node_work_path(node_index) node_bin_path = bins_path / (node_proc_name + "_" + cpp_proc_suffix) - if not node_bin_path.exists(): - raise FileNotFoundError( - f"C++ binary not found at {node_bin_path}. " - f"Either build it or copy it to {bins_path}/ before running with cpp_nodes_count > 0." - ) if start_new_session: print(f"Starting C++ node {node_index}...") with stdout_path.open("w") as out_log, stderr_path.open("w") as err_log: @@ -418,18 +401,14 @@ def export_validator_pubkey( def prepare_node( - node_index: int, config_blank: str, log_config_blank: str, - use_quic: bool = False, quic_port_offset: int = 1000, + node_index: int, config_blank: str, log_config_blank: str ) -> str | None: # Prepare console key keygen_result = run_command([str(bins_path / "crypto"), "gen", "key"], cwd=bins_path) console_key_json = json.loads(keygen_result.stdout) - prepare_default_config( - node_index, config_blank, log_config_blank, - use_quic=use_quic, quic_port_offset=quic_port_offset, - ) + prepare_default_config(node_index, config_blank, log_config_blank) # Run node console_public = {"type_id": 1209251014, "pub_key": console_key_json["pubkey"]} @@ -457,10 +436,9 @@ def prepare_node( # Build full console config console_config_path = node_work_path / "console.json" - with ( - open(console_part_config_path) as f, - open(console_config_path, "w") as fout, - ): + with open(console_part_config_path) as f, open( + console_config_path, "w" + ) as fout: c = json.load(f) c["client_key"] = {"type_id": 1209251014, "pvt_key": console_key_json["secret"]} console_full_config = {"config": c, "wallet_id": "", "max_factor": 3} @@ -502,7 +480,6 @@ def prepare_node( return validator_pubkey_hex - def extract_keys_from_rust_config(rust_config: dict): dht_pvt_key = None fullnode_pvt_key = None @@ -518,7 +495,7 @@ def extract_keys_from_rust_config(rust_config: dict): return dht_pvt_key, fullnode_pvt_key -def transform_configs_for_cpp(node_index: int, use_quic: bool = False, quic_port_offset: int = 1000): +def transform_configs_for_cpp(node_index: int): print(f"Transforming configs for C++ node {node_index}...", end="") node_work_path = build_node_work_path(node_index) @@ -636,20 +613,6 @@ def transform_configs_for_cpp(node_index: int, use_quic: bool = False, quic_port add_to_cpp_keyring(node_index, console_srv_secret_b64, base64.b64decode(console_srv_id)) add_to_cpp_keyring(node_index, liteserver_pvt_key, base64.b64decode(liteserver_key_id_b64)) - # add QUIC address if enabled - if use_quic: - import ipaddress - adnl_port = main_port_base + node_index - quic_port = adnl_port + quic_port_offset - ip_int = int(ipaddress.IPv4Address(ip_address)) - cpp_config.setdefault("addrs", []).append({ - "@type": "engine.quicAddr", - "ip": ip_int, - "port": quic_port, - "categories": [0, 1, 2, 3], - "priority_categories": [], - }) - # save modified cpp config with open(node_work_path / "config.json", "w") as f: json.dump(cpp_config, f, indent=2) @@ -708,7 +671,6 @@ def build_zerostate( validator_pub_key_hex: list[str], simplex_mc: bool = False, simplex_config: dict = None, - use_quic: bool = False, ) -> str: print("Building zerostate...", end="") zerostate = json.loads(zerostate_blank) @@ -733,38 +695,35 @@ def build_zerostate( zerostate["master"]["config"]["p34"]["list"] = validators # Add ConfigParam 30 (NewConsensusConfigAll) for simplex if enabled - if simplex_config: + if simplex_mc and simplex_config: # Simplex (C++/Rust) allows equal `gen_utime` only starting from global_version >= 13. # Our default zerostate template uses version=11, which forces strict `prev + 1` and # makes fast single-host nets drift into the future, triggering validation rejects. # # Keep behavior C++-compatible by bumping version to at least 13 when simplex is enabled. - zerostate["master"]["config"]["p8"]["version"] = max( - int(zerostate["master"]["config"]["p8"].get("version", 0)), - 13, - ) + #TODO: LK: enable after change block version to 13 + #zerostate["master"]["config"]["p8"]["version"] = max( + # int(zerostate["master"]["config"]["p8"].get("version", 0)), + # 13, + #) p30 = {} - simplex_entry = { + # MC simplex config (enabled when --simplex-mc is specified) + p30["mc"] = { "target_rate_ms": simplex_config.get("target_rate_ms", 500), "slots_per_leader_window": simplex_config.get("slots_per_leader_window", 4), - "first_block_timeout_ms": simplex_config.get( - "first_block_timeout_ms", 1000 - ), - "max_leader_window_desync": simplex_config.get( - "max_leader_window_desync", 2 - ), + "first_block_timeout_ms": simplex_config.get("first_block_timeout_ms", 1000), + "max_leader_window_desync": simplex_config.get("max_leader_window_desync", 2), } - if use_quic: - simplex_entry["use_quic"] = 1 - # MC simplex config (enabled when --simplex-mc is specified) - if simplex_mc: - p30["mc"] = dict(simplex_entry) # Shard simplex config (always enabled when simplex is used) - p30["shard"] = dict(simplex_entry) + p30["shard"] = { + "target_rate_ms": simplex_config.get("target_rate_ms", 500), + "slots_per_leader_window": simplex_config.get("slots_per_leader_window", 4), + "first_block_timeout_ms": simplex_config.get("first_block_timeout_ms", 1000), + "max_leader_window_desync": simplex_config.get("max_leader_window_desync", 2), + } zerostate["master"]["config"]["p30"] = p30 - quic_str = ", quic=true" if use_quic else "" - print(f" [simplex enabled: mc={simplex_mc}{quic_str}]", end="") + print(f" [simplex enabled: mc={simplex_mc}]", end="") zs_json_path = common_config_path / "zerostate.json" with zs_json_path.open("w") as fout: @@ -834,7 +793,6 @@ def build_global_config(zerostate_info: str): print(" done") - def build_nodectl_config(root_path): global run_fullnode, nodes_count, common_config_path @@ -846,10 +804,9 @@ def build_nodectl_config(root_path): c = json.load(f) node_control_servers["node" + str(n)] = c["config"] node_control_servers["node" + str(n)]["timeouts"] = 5 - with ( - open(root_path / "nodectl_blank.json") as f, - open(common_config_path / "nodectl-local.json", "w") as fout, - ): + with open(root_path / "nodectl_blank.json") as f, open( + common_config_path / "nodectl-local.json", "w" + ) as fout: c = json.load(f) # New nodectl config expects `nodes`. c["nodes"] = node_control_servers @@ -857,7 +814,6 @@ def build_nodectl_config(root_path): json.dump(c, fout, indent=2) print(" done") - def main(): parser = argparse.ArgumentParser() @@ -877,50 +833,27 @@ def main(): help="Start nodes (not all the network) with given numbers (whitespace separated) or all if not specified", ) parser.add_argument("--stop", action="store_true", help="Only kill nodes and exit") - parser.add_argument( - "--prepare", - action="store_true", - help="Kill, build, generate configs and zerostate, but do not start nodes", - ) parser.add_argument( "--simplex", action="store_true", - help="Enable simplex consensus config in zerostate (ConfigParam 30)", + help="Build with simplex feature enabled", ) parser.add_argument( "--simplex-mc", action="store_true", help="Enable simplex consensus for masterchain (implies --simplex)", ) - parser.add_argument( - "--quic", - action="store_true", - help="Enable QUIC overlay transport in ConfigParam 30 (use_quic flag). Implies --simplex.", - ) - parser.add_argument( - "--quic_custom_port", - action="store_true", - help="Use QUIC port offset 2000 (instead of 1000) to verify DHT announces. " - "Nodes bind QUIC on adnl_port+2000 but the auto-derive fallback is adnl_port+1000, " - "so QUIC connections only work if advertised addresses are used. Implies --quic.", - ) args = parser.parse_args() - # --quic_custom_port implies --quic - if args.quic_custom_port: - args.quic = True - # --quic implies --simplex - if args.quic: - args.simplex = True # --simplex-mc implies --simplex if args.simplex_mc: args.simplex = True if args.start is None: args.start = False - run_net = not args.stop and not args.start and not args.restart and not args.prepare - stop = run_net or args.stop or args.restart or args.prepare - build = not args.nobuild and (run_net or args.restart or args.prepare) - gen_configs = run_net or args.prepare + run_net = not args.stop and not args.start and not args.restart + stop = run_net or args.stop or args.restart + build = not args.nobuild and run_net or args.restart + gen_configs = run_net start = run_net or args.start or args.restart # Init script config @@ -938,9 +871,11 @@ def main(): cleanup() if build: - build_rust([]) # always build rust because we need tools etc. - #if cpp_nodes_count > 0: - # build_cpp() + # Build with simplex feature if --simplex is specified + build_features = ["simplex"] if args.simplex else [] + build_rust(build_features) # always build rust because we need tools etc. + if cpp_nodes_count > 0: + build_cpp() test_root_path = Path(__file__).parent @@ -954,12 +889,8 @@ def main(): test_root_path / "global_config_blank.json", common_config_path / "global_config.json", ) - quic_port_offset = 2000 if args.quic_custom_port else 1000 for n in range(0 if run_fullnode else 1, nodes_count + 1): - vk = prepare_node( - n, node_config_blank, log_config_blank, - use_quic=args.quic, quic_port_offset=quic_port_offset, - ) + vk = prepare_node(n, node_config_blank, log_config_blank) if n != 0: validator_pub_keys.append(vk) @@ -985,18 +916,13 @@ def main(): print(f"Created default simplex config: {simplex_config_path}") # Build zerostate - zerostate_name = ( - "zerostate_blank_elections.json" - if args.elections - else "zerostate_blank.json" - ) + zerostate_name = "zerostate_blank_elections.json" if args.elections else "zerostate_blank.json" zerostate_blank = Path(test_root_path / zerostate_name).read_text() zerostate_info = build_zerostate( zerostate_blank, validator_pub_keys, simplex_mc=args.simplex_mc, simplex_config=simplex_config, - use_quic=args.quic, ) # Build global config @@ -1004,9 +930,7 @@ def main(): # Transform configs for C++ nodes for node_index in range(rust_nodes_count + 1, nodes_count + 1): - transform_configs_for_cpp( - node_index, use_quic=args.quic, quic_port_offset=quic_port_offset, - ) + transform_configs_for_cpp(node_index) if start: # Start nodes diff --git a/src/node/tests/test_sync/.dockerignore b/src/node/tests/test_sync/.dockerignore new file mode 100644 index 0000000..251ef5d --- /dev/null +++ b/src/node/tests/test_sync/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +*.log +.git +.gitignore +README.md +.env +.DS_Store +db/ +*.pid diff --git a/src/node/tests/test_sync/.gitignore b/src/node/tests/test_sync/.gitignore new file mode 100644 index 0000000..ba802bb --- /dev/null +++ b/src/node/tests/test_sync/.gitignore @@ -0,0 +1,12 @@ +# Node modules +node_modules/ + +# Runtime files +watcher.pid +watcher.log +server.log +db/ + +# Editor directories +.vscode/ +.idea/ diff --git a/src/node/tests/test_sync/Dockerfile b/src/node/tests/test_sync/Dockerfile new file mode 100644 index 0000000..c312003 --- /dev/null +++ b/src/node/tests/test_sync/Dockerfile @@ -0,0 +1,89 @@ +# Multi-stage Dockerfile for test_sync (located at node/tests/test_sync/) +# Based on the main TON Node Dockerfile +# Adds Node.js-based watcher server for node management and testing + +# =================================================================== +# Stage 1: Build TON Node +# =================================================================== +FROM rust:slim AS builder + +RUN apt-get update && apt-get install -y \ + pkg-config \ + make \ + clang \ + libssl-dev \ + libzstd-dev \ + libgoogle-perftools-dev \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy ton-node source (from repository root, 3 levels up) +COPY . /ton-node +WORKDIR /ton-node + +# Accept build arguments for git metadata +ARG GIT_BRANCH +ARG GIT_COMMIT +ARG GIT_COMMIT_DATE + +# Pass them as environment variables to build.rs +ENV GIT_BRANCH=${GIT_BRANCH} +ENV GIT_COMMIT=${GIT_COMMIT} +ENV GIT_COMMIT_DATE=${GIT_COMMIT_DATE} + +# Build TON node binary +RUN cargo build --release --bin node + +# =================================================================== +# Stage 2: Runtime with Node.js and Watcher Server +# =================================================================== +FROM debian:stable-slim + +# Install runtime dependencies for TON node and Node.js +RUN apt-get update && apt-get install -y \ + openssl \ + libzstd1 \ + libgoogle-perftools4 \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js (using NodeSource repository for latest LTS) +RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Copy TON node binary from builder stage (ะฟะตั€ะตะธะผะตะฝะพะฒะฐะฝ ะฒ ton-node) +COPY --from=builder /ton-node/target/release/node /usr/local/bin/ton-node + +# Create working directory structure +WORKDIR /watcher + + +# Copy watcher server files +COPY node/tests/test_sync/package.json ./ +COPY node/tests/test_sync/server.js ./ + +# Install Node.js dependencies (form-data for Slack upload) +RUN npm install --production + +# Create necessary directories +RUN mkdir -p /main /main/static /db /logs + +# Environment variables +ENV NODE_WATCHER_HTTP=0.0.0.0:32080 +ENV SERVER_IP=127.0.0.1 + +# Expose watcher HTTP port +EXPOSE 32080 + +# Expose TON node ports (if needed) +EXPOSE 9100 +EXPOSE 30303 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:32080/status || exit 1 + +# Start watcher server (which will manage node and run tests) +CMD ["/usr/bin/node", "server.js"] diff --git a/src/node/tests/test_sync/README.md b/src/node/tests/test_sync/README.md new file mode 100644 index 0000000..c562f24 --- /dev/null +++ b/src/node/tests/test_sync/README.md @@ -0,0 +1,118 @@ +# TON Node Sync Test Watcher + +This directory contains an automated sync test watcher for `ton-node`. + +The watcher is a Node.js HTTP server (`server.js`) that: +- Starts on `NODE_WATCHER_HTTP` +- Runs sync test cases automatically on startup +- Manages `ton-node` lifecycle (start/stop) +- Optionally wipes `/db` between test cases +- Produces JSON/HTML status and report endpoints +- Optionally sends a summary/report to Slack + +## Files + +- `server.js`: watcher and test orchestration logic +- `package.json`: Node.js dependencies and start script +- `Dockerfile`: containerized build/runtime for the sync test watcher + +## Test Cases + +The watcher executes tests sequentially: + +1. `Stop -> Wipe DB -> Start -> Wait for Sync` +2. `Stop -> Start -> Wait for Sync` + +Sync is considered complete when both of these log-derived ages are `< 10s`: +- `Applied master block ... Ns old` +- `Applied block ... Ns old` + +After all tests complete, the process exits with code `0`. + +## HTTP API + +Only `GET` is supported. + +- `/status` + - Returns watcher/node status, PID, uptime, sync flags, and config values. +- `/getlogs?last=N` + - Returns the last `N` lines from `/logs/node-watcher.log`. + - Default `N=100`, maximum `N=3000`. +- `/report` + - Returns an HTML report for current/last test execution. + +## Environment Variables + +Required: +- `NODE_WATCHER_HTTP` + - Listen address in `:` format (example: `0.0.0.0:32080`). + +Optional: +- `SERVER_IP` (default: `127.0.0.1`) +- `NODE_RUN_ARGS` (default: `-c /main`) +- `SYNC_TEST_NETWORK` (label in report/slack) +- `SYNC_TEST_NODE_ID` (label in report/slack) + +Slack (optional): +- `SLACK_WEBHOOK_URL` +- `SLACK_BOT_TOKEN` +- `SLACK_CHANNEL_ID` + +## Logs and Data Paths + +Inside the runtime/container, watcher expects: +- Node watcher log: `/logs/node-watcher.log` +- Node log: `/logs/output.log` +- Node DB: `/db` +- Node config base path usually under `/main` (via `NODE_RUN_ARGS`) + +## Local Run (without Docker) + +From this directory: + +```bash +npm install +NODE_WATCHER_HTTP=127.0.0.1:32080 npm start +``` + +Notes: +- `ton-node` must be available in `PATH`. +- Ensure `/db`, `/logs`, and config path referenced by `NODE_RUN_ARGS` are valid for your environment. + +## Dockerfile Usage + +This project includes a dedicated `Dockerfile` at: +- `node/tests/test_sync/Dockerfile` + +Build from repository root: + +```bash +docker build -f node/tests/test_sync/Dockerfile -t ton-sync-test:local . +``` + +Run example: + +```bash +docker run --rm \ + -p 32080:32080 \ + -e NODE_WATCHER_HTTP=0.0.0.0:32080 \ + -e SERVER_IP=127.0.0.1 \ + -e NODE_RUN_ARGS='-c /main' \ + -v $(pwd)/node/tests/test_sync/main:/main \ + -v $(pwd)/node/tests/test_sync/db:/db \ + -v $(pwd)/node/tests/test_sync/logs:/logs \ + ton-sync-test:local +``` + +Then query: + +```bash +curl http://127.0.0.1:32080/status +curl 'http://127.0.0.1:32080/getlogs?last=200' +``` + +## Behavior Notes + +- The watcher rotates `/logs/output.log` before each test case. +- `ton-node` is started detached and monitored by PID. +- On shutdown signals (`SIGINT`, `SIGTERM`), watcher tries graceful node stop and server close. diff --git a/src/node/tests/test_sync/package.json b/src/node/tests/test_sync/package.json new file mode 100644 index 0000000..6b56ec6 --- /dev/null +++ b/src/node/tests/test_sync/package.json @@ -0,0 +1,15 @@ +{ + "name": "node-watcher", + "version": "1.0.0", + "description": "Simple HTTP server for node watcher endpoints", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "keywords": ["http", "server", "watcher"], + "author": "", + "license": "ISC", + "dependencies": { + "form-data": "^4.0.0" + } +} diff --git a/src/node/tests/test_sync/server.js b/src/node/tests/test_sync/server.js new file mode 100644 index 0000000..f150881 --- /dev/null +++ b/src/node/tests/test_sync/server.js @@ -0,0 +1,1353 @@ +/** + * Node Watcher HTTP Server + * + * A monitoring server for automated testing of node synchronization. + * + * Features: + * - Automatically runs test sequences on startup + * - Monitor sync status via metrics endpoint + * - View server logs + * - Generate test reports + * - Health status endpoint + * + * Environment Variables: + * - NODE_WATCHER_HTTP: Server address (e.g., 127.0.0.1:3000) + * - SERVER_IP: Metrics server IP (default: 127.0.0.1) + */ + +const http = require('http'); +const https = require('https'); +const fs = require('fs').promises; +const path = require('path'); +const { spawn } = require('child_process'); +const util = require('util'); +const execPromise = util.promisify(require('child_process').exec); + +// Simple HTML escaping helper to safely render text in HTML contexts. +// Escapes only the characters that are significant in HTML, preserving +// whitespace and newlines for use inside

 blocks.
+function escapeHtml(str) {
+  if (str === null || str === undefined) return '';
+  return String(str)
+    .replace(/&/g, '&')
+    .replace(//g, '>')
+    .replace(/"/g, '"')
+    .replace(/'/g, ''');
+}
+
+// ===================================================================
+// CONFIGURATION
+// ===================================================================
+
+const LOG_FILE = '/logs/node-watcher.log';
+const NODE_LOG_FILE = '/logs/output.log';
+const DB_PATH = '/db';
+const NODE_STOP_TIMEOUT = 60000; // 1 minute in milliseconds
+const NODE_STABILITY_CHECK_DELAY = 5000; // 5 seconds
+const SYNC_WAIT_TIMEOUT = 6 * 60 * 60 * 1000; // 6 hours in milliseconds
+const NODE_RUN_ARGS = process.env.NODE_RUN_ARGS && process.env.NODE_RUN_ARGS.trim() !== '' ? process.env.NODE_RUN_ARGS.trim().split(/\s+/) : ['-c', '/main']; // Base arguments to run the node
+
+// Get SERVER_IP from environment variable
+const SERVER_IP = process.env.SERVER_IP || '127.0.0.1';
+
+// Slack webhook URL (set via environment variables)
+const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL || '';
+const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || '';
+const SLACK_CHANNEL_ID = process.env.SLACK_CHANNEL_ID || '';
+
+// ===================================================================
+// GLOBAL STATE
+let FormData;
+let nodePid = null;
+let nodeStartTime = null;
+let syncCheckRunning = false;
+let syncWaiters = [];
+let isSynced = false;
+let testResults = null;
+
+class SyncTimeoutError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = 'SyncTimeoutError';
+  }
+}
+
+// Global config values for network and node_id
+let GLOBAL_NETWORK = process.env.SYNC_TEST_NETWORK && process.env.SYNC_TEST_NETWORK !== '' ? process.env.SYNC_TEST_NETWORK : 'unknown-net';
+let GLOBAL_NODE_ID = process.env.SYNC_TEST_NODE_ID && process.env.SYNC_TEST_NODE_ID !== '' ? process.env.SYNC_TEST_NODE_ID : 'unknown-node';
+
+// ===================================================================
+// UTILITY FUNCTIONS
+// ===================================================================
+
+function normalizePid(pid) {
+  const pidNum = typeof pid === 'number' ? pid : Number(pid);
+  if (!Number.isInteger(pidNum) || pidNum <= 0) {
+    return null;
+  }
+  return pidNum;
+}
+
+function isProcessRunning(pid) {
+  const pidNum = normalizePid(pid);
+  if (pidNum === null) {
+    return false;
+  }
+  try {
+    process.kill(pidNum, 0);
+    return true;
+  } catch (error) {
+    return false;
+  }
+}
+
+async function stopProcessWithTimeout(pid) {
+  const pidNum = normalizePid(pid);
+  if (pidNum === null) {
+    await log(`Invalid PID '${pid}', cannot stop process`);
+    return false;
+  }
+
+  try {
+    process.kill(pidNum, 'SIGTERM');
+    await log(`Sent SIGTERM to process ${pidNum}`);
+  } catch (error) {
+    if (error && error.code === 'ESRCH') {
+      await log(`Process ${pidNum} is already stopped`);
+      return true;
+    }
+    await log(`Failed to send SIGTERM to process ${pidNum}: ${error.message}`);
+    return false;
+  }
+
+  const stopStartTime = Date.now();
+  while (Date.now() - stopStartTime < NODE_STOP_TIMEOUT) {
+    if (!isProcessRunning(pidNum)) {
+      await log(`Process ${pidNum} has stopped successfully`);
+      return true;
+    }
+    await new Promise(resolve => setTimeout(resolve, 1000));
+  }
+
+  await log(`Process ${pidNum} did not stop after ${NODE_STOP_TIMEOUT / 1000} seconds, sending SIGKILL`);
+  try {
+    process.kill(pidNum, 'SIGKILL');
+    await log(`Sent SIGKILL to process ${pidNum}`);
+  } catch (error) {
+    if (error && error.code === 'ESRCH') {
+      await log(`Process ${pidNum} exited before SIGKILL was delivered`);
+      return true;
+    }
+    await log(`Error sending SIGKILL to process ${pidNum}: ${error.message}`);
+    return false;
+  }
+
+  // Give the OS a short window to reap the process after SIGKILL.
+  const killConfirmDeadline = Date.now() + 5000;
+  while (Date.now() < killConfirmDeadline) {
+    if (!isProcessRunning(pidNum)) {
+      await log(`Process ${pidNum} has stopped after SIGKILL`);
+      return true;
+    }
+    await new Promise(resolve => setTimeout(resolve, 200));
+  }
+
+  await log(`WARNING: Process ${pidNum} is still running after SIGKILL attempt`);
+  return false;
+}
+
+// Stop all running node processes (by name)
+async function stopAllNodeProcesses() {
+  try {
+    const { stdout } = await execPromise('pgrep -f "ton-node" || true');
+    const pids = stdout.trim().split(/\s+/).filter(pid => pid);
+    if (pids.length > 0) {
+      await log(`Found ${pids.length} running node process(es). Stopping all...`);
+      for (const pidStr of pids) {
+        const pidNumber = parseInt(pidStr, 10);
+        if (!Number.isInteger(pidNumber) || pidNumber <= 0) {
+          await log(`Skipping invalid PID from pgrep output: "${pidStr}"`);
+          continue;
+        }
+        await stopProcessWithTimeout(pidNumber);
+      }
+    } else {
+      await log('No running node processes found.');
+    }
+  } catch (e) {
+    await log('Error checking/stopping node processes: ' + e.message);
+  }
+}
+
+// Analyzes logs and returns {blocksAccepted, syncSpeed}
+async function analyzeSyncLog(syncDurationSeconds) {
+  let blocksAccepted = null;
+  try {
+    const { stdout: blocksStdout } = await execPromise(`cat "${NODE_LOG_FILE}" | grep "Applied master block" | wc -l`);
+    blocksAccepted = parseInt(blocksStdout.trim(), 10);
+    if (isNaN(blocksAccepted)) blocksAccepted = 0;
+  } catch (err) {
+    await log(`Could not count applied master blocks: ${err.message}`);
+    blocksAccepted = null;
+  }
+  const syncSpeed = (blocksAccepted !== null && syncDurationSeconds > 0) ? Number((blocksAccepted / syncDurationSeconds).toFixed(2)) : null;
+  return { blocksAccepted, syncSpeed };
+}
+
+// Logging function
+async function log(message) {
+  const timestamp = new Date().toISOString();
+  const logMessage = `${timestamp} - ${message}\n`;
+  
+  // Write to file
+  try {
+    await fs.appendFile(LOG_FILE, logMessage);
+  } catch (err) {
+    console.error('Failed to write to log file:', err);
+  }
+  
+  // Also output to console
+  console.log(logMessage.trim());
+}
+
+// Helper function to send JSON response
+function sendJsonResponse(res, statusCode, data) {
+  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
+  res.end(JSON.stringify(data));
+}
+
+// Helper function to get uptime in seconds
+function getUptime() {
+  if (!nodeStartTime) return null;
+  return Math.round((Date.now() - nodeStartTime) / 1000);
+}
+
+// ===================================================================
+// SERVER CONFIGURATION
+// ===================================================================
+
+// Parse the NODE_WATCHER_HTTP environment variable
+function parseAddress() {
+  const address = process.env.NODE_WATCHER_HTTP;
+  
+  if (!address) {
+    throw new Error('NODE_WATCHER_HTTP environment variable is not set. Example: NODE_WATCHER_HTTP=127.0.0.1:3000');
+  }
+  
+  const [host, port] = address.split(':');
+  
+  if (!host || !port) {
+    throw new Error('Invalid NODE_WATCHER_HTTP format. Expected: :. Example: NODE_WATCHER_HTTP=127.0.0.1:3000');
+  }
+  
+  const portNum = parseInt(port, 10);
+  
+  if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
+    throw new Error('Invalid port number in NODE_WATCHER_HTTP');
+  }
+  
+  return { host, port: portNum };
+}
+
+// ===================================================================
+// HTTP ENDPOINT HANDLERS
+// ===================================================================
+
+// Handler functions for each endpoint
+// IMPORTANT: These handlers provide read-only access to monitoring data
+// The server runs tests automatically on startup
+
+async function handleLogs(req, res, queryParams) {
+  await log(`Received /getlogs request${queryParams.size > 0 ? ` with params: ${JSON.stringify(Object.fromEntries(queryParams))}` : ''}`);
+  
+  try {
+    // Get the 'last' parameter, default to 100, maximum 3000
+    const lastParam = queryParams.get('last');
+    let last = lastParam ? parseInt(lastParam, 10) : 100;
+    
+    // Validate the parameter
+    if (isNaN(last) || last < 1) {
+      sendJsonResponse(res, 400, { 
+        status: 'error', 
+        message: 'Invalid "last" parameter. Must be a positive number.',
+        endpoint: '/getlogs'
+      });
+      return;
+    }
+    
+    // Limit to maximum 3000 lines
+    if (last > 3000) {
+      last = 3000;
+    }
+    
+    // Check if log file exists
+    try {
+      await fs.access(LOG_FILE);
+    } catch (err) {
+      sendJsonResponse(res, 200, { 
+        status: 'success', 
+        message: 'No logs available',
+        lines: [],
+        count: 0,
+        endpoint: '/getlogs'
+      });
+      return;
+    }
+    
+    // Use tail command to read log file efficiently
+    let lines;
+    let totalLines = null;
+    
+    try {
+      // Return last N lines using tail (max 3000), with a 5s timeout
+      const { stdout } = await execPromise(`timeout 5 tail -n ${last} "${LOG_FILE}"`);
+      lines = stdout.split('\n').filter(line => line.trim() !== '');
+      // Get total line count efficiently for reporting (timeout 5s)
+      const { stdout: wcOutput } = await execPromise(`timeout 5 wc -l < "${LOG_FILE}"`);
+      totalLines = parseInt(wcOutput.trim(), 10);
+      if (isNaN(totalLines)) totalLines = lines.length;
+    } catch (error) {
+      await log(`Error reading log file with tail: ${error.message}`);
+      sendJsonResponse(res, 500, { 
+        status: 'error', 
+        message: `Failed to read log file: ${error.message}`,
+        endpoint: '/getlogs'
+      });
+      return;
+    }
+    
+    sendJsonResponse(res, 200, { 
+      status: 'success', 
+      message: `Retrieved ${lines.length} log lines`,
+      lines: lines,
+      count: lines.length,
+      total: totalLines,
+      endpoint: '/getlogs'
+    });
+  } catch (error) {
+    await log(`Error in /getlogs handler: ${error.message}`);
+    sendJsonResponse(res, 500, { 
+      status: 'error', 
+      message: error.message,
+      endpoint: '/getlogs'
+    });
+  }
+}
+
+async function handleReport(req, res, queryParams) {
+  await log('Received /report request');
+  
+  try {
+    if (!testResults) {
+      sendJsonResponse(res, 200, {
+        status: 'success',
+        message: 'No test results available. Run tests first.',
+        endpoint: '/report'
+      });
+      return;
+    }
+    
+    // Generate HTML report
+    const html = generateHtmlReport(testResults);
+    
+    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+    res.end(html);
+  } catch (error) {
+    await log(`Error in /report handler: ${error.message}`);
+    sendJsonResponse(res, 500, { 
+      status: 'error', 
+      message: error.message,
+      endpoint: '/report'
+    });
+  }
+}
+
+function generateHtmlReport(results) {
+  const isRunning = results.endTime === null;
+  const totalDuration = isRunning ? Math.round((Date.now() - results.startTime) / 1000) : Math.round((results.endTime - results.startTime) / 1000);
+  const successCount = results.cases.filter(c => c.status === 'SUCCESS').length;
+  const failedCount = results.cases.filter(c => c.status === 'FAILED').length;
+  const completedCount = successCount + failedCount;
+  const passRate = completedCount > 0 ? ((successCount / completedCount) * 100).toFixed(1) : '0.0';
+  
+  let statusColor, statusText;
+  if (isRunning) {
+    statusColor = '#f59e0b'; // Orange/amber color for in progress
+    statusText = 'IN PROGRESS';
+  } else if (results.cases.length === 0) {
+    statusColor = '#64748b'; // Gray for no tests
+    statusText = 'NO TESTS';
+  } else if (failedCount === 0) {
+    statusColor = '#10b981'; // Green for all passed
+    statusText = 'ALL PASSED';
+  } else {
+    statusColor = '#ef4444'; // Red for failures
+    statusText = `${failedCount} FAILED`;
+  }
+  
+  return `
+
+
+  
+  
+  Test Report - Node Watcher
+  
+
+
+  
+
+

๐Ÿงช Test Execution Report

+

Node Watcher Automated Tests

+
+ Network: ${encodeURIComponent(GLOBAL_NETWORK)} + Node ID: ${encodeURIComponent(GLOBAL_NODE_ID)} +
+
+
+
+

Status

+
${statusText}
+
+
+

Pass Rate

+
${passRate}%
+
+
+

Total Tests

+
${isRunning ? `${completedCount}/${results.cases.length}` : results.cases.length}
+
+
+

Duration

+
${totalDuration}s
+
+
+
+ ${results.cases.map((testCase, index) => { + const isTestRunning = testCase.endTime === null || testCase.status === 'RUNNING'; + const displayStatus = isTestRunning ? 'RUNNING' : testCase.status; + const currentDuration = isTestRunning ? Math.round((Date.now() - testCase.startTime) / 1000) : testCase.duration; + return ` +
+
+
Test Case ${index + 1}: ${encodeURIComponent(testCase.name)}
+ ${displayStatus} +
+
+
+ Duration + ${currentDuration}s${isTestRunning ? ' (ongoing)' : ''} +
+
+ Started + ${new Date(testCase.startTime).toLocaleTimeString()} +
+
+ Ended + ${isTestRunning ? 'In progress...' : new Date(testCase.endTime).toLocaleTimeString()} +
+
+ Blocks Accepted + ${typeof testCase.blocksAccepted === 'number' && testCase.blocksAccepted !== null ? testCase.blocksAccepted : 'N/A'} +
+
+ Sync Speed + ${typeof testCase.syncSpeed === 'number' && testCase.syncSpeed !== null ? testCase.syncSpeed + ' blocks/s' : 'N/A'} +
+
+ ${testCase.error ? ` +
+ โŒ Error: +
${escapeHtml(String(testCase.error))}
+
+ ` : ''} +
+ `; + }).join('')} +
+
+ Generated at ${new Date(isRunning ? Date.now() : results.endTime).toLocaleString()}${isRunning ? ' (report refreshes on reload)' : ''} +
+
+ +`; +} + +async function handleStatus(req, res, queryParams) { + await log('Received /status request'); + + try { + const status = { + status: 'success', + node: { + pid: nodePid, + running: nodePid !== null, + startTime: nodeStartTime, + uptime: getUptime(), + synced: isSynced, + syncChecking: syncCheckRunning + }, + server: { + serverIp: SERVER_IP, + dbPath: DB_PATH, + stopTimeout: NODE_STOP_TIMEOUT / 1000, + stabilityCheckDelay: NODE_STABILITY_CHECK_DELAY / 1000 + }, + endpoint: '/status' + }; + + sendJsonResponse(res, 200, status); + } catch (error) { + await log(`Error in /status handler: ${error.message}`); + sendJsonResponse(res, 500, { + status: 'error', + message: error.message, + endpoint: '/status' + }); + } +} + +async function handleNotFound(req, res) { + sendJsonResponse(res, 404, { + status: 'error', + message: 'Endpoint not found', + path: req.url + }); +} + +async function handleMethodNotAllowed(req, res) { + res.writeHead(405, { + 'Content-Type': 'application/json', + 'Allow': 'GET' + }); + res.end(JSON.stringify({ + status: 'error', + message: 'Method not allowed. Only GET requests are supported.', + method: req.method + })); +} + +// =================================================================== +// REQUEST ROUTING +// =================================================================== + +// Request handler with async/await +async function handleRequest(req, res) { + try { + // Check if the method is GET + if (req.method !== 'GET') { + await handleMethodNotAllowed(req, res); + return; + } + + // Parse URL to extract pathname and query parameters + const parsedUrl = new URL(req.url, `http://${req.headers.host}`); + const pathname = parsedUrl.pathname; + const queryParams = parsedUrl.searchParams; + + // Route the request based on the pathname + switch (pathname) { + case '/getlogs': + await handleLogs(req, res, queryParams); + break; + case '/status': + await handleStatus(req, res, queryParams); + break; + case '/report': + await handleReport(req, res, queryParams); + break; + default: + await handleNotFound(req, res); + break; + } + } catch (error) { + await log(`Error handling request: ${error && error.stack ? error.stack : error}`); + console.error('Error handling request:', error); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + status: 'error', + message: 'Internal server error' + })); + } + } +} + +// Create the HTTP server +const server = http.createServer((req, res) => { + handleRequest(req, res); +}); + +// Start the server with async/await +async function startServer() { + const { host, port } = parseAddress(); + // Load global labels before starting server + await log(`Loaded global labels: network='${GLOBAL_NETWORK}', node_id='${GLOBAL_NODE_ID}'`); + return new Promise((resolve, reject) => { + server.listen(port, host, async () => { + await log(`Server is running on http://${host}:${port}`); + console.log('Available endpoints:'); + console.log(` - GET http://${host}:${port}/getlogs?last=N`); + console.log(` - GET http://${host}:${port}/status`); + console.log(` - GET http://${host}:${port}/report`); + // Start automated tests + await log('Starting automated tests...'); + runAllTests().catch(async (error) => { + await log(`Automated test execution failed: ${error.message}`); + }); + resolve(); + }); + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`ERROR: Port ${port} is already in use`); + } else if (err.code === 'EADDRNOTAVAIL') { + console.error(`ERROR: Address ${host} is not available`); + } else { + console.error('ERROR:', err.message); + } + reject(err); + }); + }); +} + +// Graceful shutdown handler for HTTP server and node process +async function shutdown(signal) { + await log(`${signal} received, shutting down HTTP server and node process gracefully`); + + // Stop the node process if running + await stopNode(); + + return new Promise((resolve) => { + server.close(async () => { + await log('HTTP server closed'); + resolve(); + }); + }); +} + + +// =================================================================== +// NODE LOG ROTATION +// =================================================================== + +// Rotate node log file: if exists, move to _N where N is next available +async function rotateNodeLog() { + try { + // Check if log file exists + await fs.access(NODE_LOG_FILE); + } catch (e) { + await log(`Node log file not found, nothing to rotate: ${NODE_LOG_FILE}`); + return; + } + // Find next available suffix N, insert before extension + const parsed = path.parse(NODE_LOG_FILE); + let n = 1; + let nextLog; + while (true) { + nextLog = path.join(parsed.dir, `${parsed.name}_${n}${parsed.ext}`); + try { + await fs.access(nextLog); + n++; + } catch (e) { + break; + } + } + // Move the log file + await fs.rename(NODE_LOG_FILE, nextLog); + await log(`Node log rotated: ${NODE_LOG_FILE} -> ${nextLog}`); +} + +// =================================================================== +// NODE PROCESS MANAGEMENT +// =================================================================== + +// Function to start the node process if not already running +async function startNode() { + try { + await log('Starting node process check...'); + + // Check if node process is already running + const { stdout } = await execPromise('pgrep -f "ton-node" || true'); + const pids = stdout.trim().split(/\s+/).filter(pid => pid); + + if (pids.length > 0) { + // Found existing node process(es) + const pid = pids[0]; // Use the first one + await log(`Found existing node process with PID: ${pid}`); + + // Save PID to global variable + nodePid = parseInt(pid, 10); + nodeStartTime = Date.now(); + await log(`PID ${pid} saved to global variable`); + return; + } + + // No existing node process found + await log('No existing node process found, starting new one...'); + + + + // No existing node process, start a new one + const nodeProcess = spawn('ton-node', NODE_RUN_ARGS, { + detached: true + }); + nodeStartTime = Date.now(); + const pid = nodeProcess.pid; + // Set nodePid BEFORE exit handler to avoid race condition + nodePid = pid; + nodeProcess.on('exit', (code, signal) => { + log(`Node process exited with code ${code}, signal ${signal}`); + nodePid = null; + nodeStartTime = null; + syncCheckRunning = false; + isSynced = false; + }); + // Detach the process so it continues running independently + nodeProcess.unref(); + await log(`Started new node process with PID: ${pid}`); + await log(`Waiting ${NODE_STABILITY_CHECK_DELAY / 1000} seconds to verify process stability...`); + await new Promise(resolve => setTimeout(resolve, NODE_STABILITY_CHECK_DELAY)); + try { + process.kill(pid, 0); + await log(`Process ${pid} is still running after ${NODE_STABILITY_CHECK_DELAY / 1000} seconds`); + await log(`PID ${pid} confirmed in global variable`); + } catch (error) { + await log(`ERROR: Process ${pid} is no longer running after ${NODE_STABILITY_CHECK_DELAY / 1000} seconds`); + await log('Node process failed to start or crashed immediately'); + try { + const { stdout: nodeLogs } = await execPromise(`timeout 5 tail -n 100 "${NODE_LOG_FILE}"`); + if (nodeLogs.trim()) { + await log('=== Last 100 lines of node log ==='); + const logLines = nodeLogs.trim().split('\n'); + for (const line of logLines) { + await log(`[NODE] ${line}`); + } + await log('=== End of node log ==='); + } else { + await log('Node log file is empty'); + } + } catch (logError) { + await log(`Could not read node logs: ${logError.message}`); + } + nodePid = null; + nodeStartTime = null; + throw new Error(`Node process failed to start - exited within ${NODE_STABILITY_CHECK_DELAY / 1000} seconds`); + } + + } catch (error) { + await log(`Error in startNode: ${error.message}`); + throw error; + } +} + + +// Wait for node to sync by polling the log +async function waitForSync() { + if (!nodePid) { + syncCheckRunning = false; + isSynced = false; + await log('WARNING: No node process running, cannot wait for sync'); + return; + } + + syncCheckRunning = true; + isSynced = false; + await log(`Waiting for node to sync (log polling, timeout ${Math.round(SYNC_WAIT_TIMEOUT / 3600000)}h)...`); + + try { + let prevMasterAge = null; + let prevShardAge = null; + let lastProgressAt = Date.now(); + while (true) { + // Read last "Applied master block" and "Applied block" lines + let masterLine = null, shardLine = null; + try { + const { stdout: masterStdout } = await execPromise(`grep 'Applied master block' "${NODE_LOG_FILE}" | tail -n 1`); + masterLine = masterStdout.trim(); + } catch {} + try { + const { stdout: shardStdout } = await execPromise(`grep 'Applied block' "${NODE_LOG_FILE}" | tail -n 1`); + shardLine = shardStdout.trim(); + } catch {} + + // Parse "Ns old" from both lines + function parseAge(line) { + if (!line) return null; + const m = line.match(/(\d+)s old/); + return m ? parseInt(m[1], 10) : null; + } + const masterAge = parseAge(masterLine); + const shardAge = parseAge(shardLine); + + // If both values < 10, consider sync complete + if (masterAge !== null && shardAge !== null && masterAge < 10 && shardAge < 10) { + isSynced = true; + await log(`Sync complete: masterAge=${masterAge}, shardAge=${shardAge}`); + return; + } + + // Check progress: if at least one value decreased, reset lastProgressAt + if ( + (prevMasterAge !== null && masterAge !== null && masterAge < prevMasterAge) || + (prevShardAge !== null && shardAge !== null && shardAge < prevShardAge) + ) { + lastProgressAt = Date.now(); + } + + // Save current values for the next iteration + prevMasterAge = masterAge; + prevShardAge = shardAge; + + // Timeout only if there is no progress + const elapsed = Date.now() - lastProgressAt; + if (elapsed >= SYNC_WAIT_TIMEOUT) { + const timeoutSeconds = Math.round(elapsed / 1000); + const timeoutError = new SyncTimeoutError(`Sync timeout exceeded after ${timeoutSeconds} seconds (no progress)`); + await log(`ERROR: ${timeoutError.message}`); + await stopNode(); + throw timeoutError; + } + + await new Promise(r => setTimeout(r, 2000)); // Wait before next attempt + } + } finally { + syncCheckRunning = false; + } +} + +// =================================================================== +// TEST CASES +// =================================================================== + +// Test Case 1: Stop node, wipe DB, start node, wait for sync +async function testCase1() { + await log('========================================'); + await log('=== TEST CASE 1 START ==='); + await log('=== Stop -> Wipe DB -> Start -> Wait for Sync ==='); + await log('========================================'); + const caseStartTime = Date.now(); + let caseEndTime = null; + let caseDuration = null; + let blocksAccepted = null, syncSpeed = null; + let error = null; + + try { + if(nodePid) { + await stopNode(); + await log('Test Case 1: Node stopped'); + } + // Rotate node log before starting node + await rotateNodeLog(); + await cleanDb(); + await log('Test Case 1: Database wiped'); + await startNode(); + await log('Test Case 1: Node started, waiting for sync...'); + await waitForSync(); + caseEndTime = Date.now(); + caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); + try { + const logStats = await analyzeSyncLog(caseDuration); + blocksAccepted = logStats.blocksAccepted; + syncSpeed = logStats.syncSpeed; + await log(`Test Case 1: Blocks accepted=${blocksAccepted}, speed=${syncSpeed} blocks/s`); + } catch (e) { + await log(`Test Case 1: Failed to analyze log: ${e.message}`); + } + await log('========================================'); + await log(`=== TEST CASE 1 STOP ===`); + await log(`=== Duration: ${caseDuration} seconds ===`); + await log(`=== Status: SUCCESS ===`); + await log('========================================'); + } catch (err) { + error = err; + if (err instanceof SyncTimeoutError) { + await log(`Test Case 1: Timeout exceeded: ${err.message}`); + } + caseEndTime = Date.now(); + caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); + await log('========================================'); + await log(`=== TEST CASE 1 STOP ===`); + await log(`=== Status: FAILED - ${err.message}`); + await log('========================================'); + } + + return { + name: 'Stop -> Wipe DB -> Start -> Wait for Sync', + startTime: caseStartTime, + endTime: caseEndTime, + duration: caseDuration, + status: error ? 'FAILED' : 'SUCCESS', + error: error ? error.message : null, + blocksAccepted, + syncSpeed + }; +} + +// Test Case 2: Stop node, start node, wait for sync +async function testCase2() { + await log('========================================'); + await log('=== TEST CASE 2 START ==='); + await log('=== Stop -> Start -> Wait for Sync ==='); + await log('========================================'); + const caseStartTime = Date.now(); + let caseEndTime = null; + let caseDuration = null; + let blocksAccepted = null, syncSpeed = null; + let error = null; + + try { + if (nodePid) { + await stopNode(); + await log('Test Case 2: Node stopped'); + } + // Rotate node log before starting node + await rotateNodeLog(); + await startNode(); + await log('Test Case 2: Node started, waiting for sync...'); + await waitForSync(); + caseEndTime = Date.now(); + caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); + try { + const logStats = await analyzeSyncLog(caseDuration); + blocksAccepted = logStats.blocksAccepted; + syncSpeed = logStats.syncSpeed; + await log(`Test Case 2: Blocks accepted=${blocksAccepted}, speed=${syncSpeed} blocks/s`); + } catch (e) { + await log(`Test Case 2: Failed to analyze log: ${e.message}`); + } + await log('========================================'); + await log(`=== TEST CASE 2 STOP ===`); + await log(`=== Duration: ${caseDuration} seconds ===`); + await log(`=== Status: SUCCESS ===`); + await log('========================================'); + } catch (err) { + error = err; + if (err instanceof SyncTimeoutError) { + await log(`Test Case 2: Timeout exceeded: ${err.message}`); + } + caseEndTime = Date.now(); + caseDuration = Math.round((caseEndTime - caseStartTime) / 1000); + await log('========================================'); + await log(`=== TEST CASE 2 STOP ===`); + await log(`=== Status: FAILED - ${err.message}`); + await log('========================================'); + } + + // Stop node after test case completes + await stopNode(); + await log('Test Case 2: Node stopped'); + + return { + name: 'Stop -> Start -> Wait for Sync', + startTime: caseStartTime, + endTime: caseEndTime, + duration: caseDuration, + status: error ? 'FAILED' : 'SUCCESS', + error: error ? error.message : null, + blocksAccepted, + syncSpeed + }; +} + +// Run all test cases in sequence (one-by-one) +async function runAllTests() { + await log('====== Starting All Test Cases (Sequential Execution) ======'); + const overallStartTime = Date.now(); + + // Reset test results for new run + testResults = { + startTime: overallStartTime, + endTime: null, + cases: [] + }; + + try { + // Add placeholder for Case 1 + testResults.cases.push({ + name: 'Stop -> Wipe DB -> Start -> Wait for Sync', + startTime: Date.now(), + endTime: null, + duration: 0, + status: 'RUNNING', + error: null + }); + + // Execute Case 1 + const result1 = await testCase1(); + testResults.cases[0] = result1; // Update with actual result + await log('>>> Proceeding to Test Case 2...'); + + // Add placeholder for Case 2 + testResults.cases.push({ + name: 'Stop -> Start -> Wait for Sync', + startTime: Date.now(), + endTime: null, + duration: 0, + status: 'RUNNING', + error: null + }); + + // Execute Case 2 + const result2 = await testCase2(); + testResults.cases[1] = result2; // Update with actual result + + const totalTime = Math.round((Date.now() - overallStartTime) / 1000); + testResults.endTime = Date.now(); + + await log(`====== All Test Cases Completed in ${totalTime} seconds ======`); + await log(`Report available at: /report`); + // Send report to Slack + try { + await sendSlackReport(testResults); + await log('Slack report sent'); + } catch (e) { + await log('Failed to send Slack report: ' + e.message); + } + } catch (error) { + testResults.endTime = Date.now(); + await log(`====== Test execution failed: ${error.message} ======`); + } + // Only exit if all tests succeeded + const allPassed = testResults.cases.every(c => c.status === 'SUCCESS'); + if (allPassed) { + await log('All tests complete, exiting...'); + process.exit(0); + } else { + await log('All tests complete, but some tests failed. Server will remain running for investigation.'); + // Ensure all node processes are stopped + await stopAllNodeProcesses(); + // Do not exit, keep server running + } +} + +// Builds and sends a report to Slack +async function sendSlackReport(results) { + // Short summary message + const case1 = results.cases[0]; + const case2 = results.cases[1]; + const msg = `Node: ${GLOBAL_NODE_ID}\nNetwork: ${GLOBAL_NETWORK}\nCase 1: ${case1.status === 'SUCCESS' ? 'success' : 'fail'} in ${case1.duration} seconds\nCase 2: ${case2.status === 'SUCCESS' ? 'success' : 'fail'} in ${case2.duration} seconds`; + + let fileSent = false; + let fileError = null; + // Try to send HTML report as file if both token and channel are set + if (SLACK_BOT_TOKEN && SLACK_CHANNEL_ID) { + try { + if (!FormData) FormData = require('form-data'); + const html = generateHtmlReport(results); + const tmpPath = `/tmp/node_report_${Date.now()}.html`; + await fs.writeFile(tmpPath, html, 'utf8'); + await uploadFileToSlack(tmpPath, 'Node Test Report', msg); + await fs.unlink(tmpPath).catch(() => {}); + fileSent = true; + } catch (e) { + fileError = e; + await log('Slack file upload failed: ' + e.message); + } + } + + // If file not sent, send message to channel (webhook or chat.postMessage) + if (!fileSent) { + // Prefer webhook if set + if (SLACK_WEBHOOK_URL) { + const payload = { text: msg }; + await postToSlack(payload); + } else if (SLACK_BOT_TOKEN && SLACK_CHANNEL_ID) { + // Fallback: use chat.postMessage + await sendSlackTextMessage(SLACK_CHANNEL_ID, msg); + } else { + await log('No Slack credentials for sending message'); + } + } +} +// Send a plain text message to a Slack channel using chat.postMessage +async function sendSlackTextMessage(channel, text) { + const payload = JSON.stringify({ channel, text }); + const options = { + method: 'POST', + hostname: 'slack.com', + path: '/api/chat.postMessage', + headers: { + 'Authorization': `Bearer ${SLACK_BOT_TOKEN}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload) + } + }; + await new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', async () => { + try { + const json = JSON.parse(data); + await log(`Slack chat.postMessage response: ${data}`); + if (json.ok) resolve(); + else reject(new Error('Slack chat.postMessage error: ' + (json.error || data))); + } catch (e) { reject(e); } + }); + }); + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + +// Upload file to Slack using files.upload API +async function uploadFileToSlack(filePath, title, initialComment) { + // Step 1: Get upload URL and file_id + const stat = require('fs').statSync(filePath); + const fileSize = stat.size; + const fileName = 'report.html'; + const getUrlForm = new (require('form-data'))(); + getUrlForm.append('filename', fileName); + getUrlForm.append('length', fileSize.toString()); + const getUrlOptions = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${SLACK_BOT_TOKEN}`, + ...getUrlForm.getHeaders() + } + }; + const uploadUrlResp = await new Promise((resolve, reject) => { + const req = https.request('https://slack.com/api/files.getUploadURLExternal', getUrlOptions, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + try { + const json = JSON.parse(data); + if (json.ok && json.upload_url && json.file_id) resolve(json); + else reject(new Error('Slack getUploadURLExternal error: ' + (json.error || data))); + } catch (e) { reject(e); } + }); + }); + req.on('error', reject); + getUrlForm.pipe(req); + }); + const { upload_url, file_id } = uploadUrlResp; + + // Step 2: Upload file binary to upload_url + const fileBuffer = require('fs').readFileSync(filePath); + await new Promise((resolve, reject) => { + const url = new URL(upload_url); + const options = { + method: 'POST', + hostname: url.hostname, + path: url.pathname + url.search, + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': fileBuffer.length + } + }; + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', async () => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve(); + else reject(new Error('Slack upload_url HTTP error: ' + res.statusCode + ' ' + data)); + }); + }); + req.on('error', reject); + req.write(fileBuffer); + req.end(); + }); + + // Step 3: Complete upload and share in channel + // Find a default channel from env or fallback + const channel = SLACK_CHANNEL_ID || null; + if (!channel) { + await log('No SLACK_CHANNEL_ID set, file will not be shared in a channel'); + return; + } + const completeForm = new (require('form-data'))(); + completeForm.append('files', JSON.stringify([{ id: file_id, title: title || fileName }])); + completeForm.append('channel_id', channel); + if (initialComment) completeForm.append('initial_comment', initialComment); + const completeOptions = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${SLACK_BOT_TOKEN}`, + ...completeForm.getHeaders() + } + }; + await new Promise((resolve, reject) => { + const req = https.request('https://slack.com/api/files.completeUploadExternal', completeOptions, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + try { + const json = JSON.parse(data); + if (json.ok) resolve(); + else reject(new Error('Slack completeUploadExternal error: ' + (json.error || data))); + } catch (e) { reject(e); } + }); + }); + req.on('error', reject); + completeForm.pipe(req); + }); +} + +function postToSlack(payload) { + return new Promise((resolve, reject) => { + const url = new URL(SLACK_WEBHOOK_URL); + const data = JSON.stringify(payload); + const options = { + hostname: url.hostname, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + } + }; + const req = https.request(options, (res) => { + let body = ''; + res.on('data', (chunk) => { body += chunk; }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve(); + else reject(new Error('Slack error: ' + res.statusCode + ' ' + body)); + }); + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +} + +// =================================================================== +// NODE LIFECYCLE FUNCTIONS +// =================================================================== + +// Stop node process if it's running +async function stopNode() { + if (!nodePid) { + await log('No node process is currently running'); + return; + } + + try { + const savedPid = nodePid; + await log(`Attempting to stop node process with PID: ${savedPid}`); + + // Stop sync checking and reject waiting promises + syncCheckRunning = false; + isSynced = false; + + // Reject all waiting promises before clearing + const waitersToReject = [...syncWaiters]; + syncWaiters = []; + waitersToReject.forEach(resolve => { + // Resolve instead of reject to avoid unhandled rejections + // The waiters will just get unblocked when node stops + resolve(); + }); + + await stopProcessWithTimeout(savedPid); + nodePid = null; + nodeStartTime = null; + } catch (error) { + await log(`Error stopping node process: ${error.message}`); + nodePid = null; + nodeStartTime = null; + } +} + +// =================================================================== +// DATABASE MANAGEMENT +// =================================================================== + +// Clean the database folder contents (keep the folder itself) +async function cleanDb() { + try { + await log(`Cleaning database contents at path: ${DB_PATH}`); + + // Check if the db directory exists + try { + await fs.access(DB_PATH); + } catch (err) { + await log(`Database directory does not exist, nothing to clean`); + return; + } + + // Get all items in the directory + const items = await fs.readdir(DB_PATH); + + if (items.length === 0) { + await log(`Database directory is already empty`); + return; + } + + await log(`Found ${items.length} items to delete`); + + // Delete each item in the directory (but keep the directory itself) + for (const item of items) { + const itemPath = path.join(DB_PATH, item); + await fs.rm(itemPath, { recursive: true, force: true }); + } + + await log(`Database directory contents cleaned successfully`); + } catch (error) { + await log(`Error cleaning database: ${error.message}`); + } +} + +// =================================================================== +// SIGNAL HANDLERS & STARTUP +// =================================================================== + +// Signal handlers for graceful shutdown +process.on('SIGTERM', async () => { + await shutdown('SIGTERM'); + process.exit(0); +}); + +process.on('SIGINT', async () => { + await shutdown('SIGINT'); + process.exit(0); +}); + +// Handle uncaught errors +process.on('uncaughtException', async (error) => { + await log(`Uncaught exception: ${error && error.stack ? error.stack : error}`); + console.error('Uncaught exception:', error); + await shutdown('UNCAUGHT_EXCEPTION'); + process.exit(0); +}); + +process.on('unhandledRejection', async (reason, promise) => { + await log(`Unhandled rejection at: ${promise}, reason: ${reason}`); + console.error('Unhandled rejection:', reason); +}); + +// Start the server +startServer().catch(async (err) => { + await log(`Failed to start server: ${err && err.stack ? err.stack : err}`); + process.exit(0); +}); diff --git a/src/node/validator-session/src/session.rs b/src/node/validator-session/src/session.rs index e1ad2c0..a70caa9 100644 --- a/src/node/validator-session/src/session.rs +++ b/src/node/validator-session/src/session.rs @@ -358,7 +358,6 @@ impl CatchainOverlay for LoopbackOverlay { _sender_id: &PublicKeyHash, _send_as: &PublicKeyHash, _payload: BlockPayloadPtr, - _extra: Option>, ) { // no need to send broadcast to itself /*if let Some(listener) = self.listener.upgrade() { @@ -440,10 +439,6 @@ pub(crate) struct SessionImpl { */ impl consensus_common::Session for SessionImpl { - fn start(&self, _initial_block_seqno: u32) { - log::trace!("CatchainSession::start() called (no-op for catchain)"); - } - fn stop(&self) { self.stop_impl(false); // Stop without destroying DB (preserve for recovery) } diff --git a/src/tl/ton_api/tl/ton_api.tl b/src/tl/ton_api/tl/ton_api.tl index fe5e706..62a71a2 100644 --- a/src/tl/ton_api/tl/ton_api.tl +++ b/src/tl/ton_api/tl/ton_api.tl @@ -69,7 +69,6 @@ adnl.address.udp6 ip:int128 port:int = adnl.Address; adnl.address.tunnel to:int256 pubkey:PublicKey = adnl.Address; adnl.address.reverse = adnl.Address; -adnl.address.quic ip:int port:int = adnl.Address; adnl.addressList addrs:(vector adnl.Address) version:int reinit_date:int priority:int expire_at:int = adnl.AddressList; @@ -270,11 +269,11 @@ overlay.broadcastFecShort src:PublicKey certificate:overlay.Certificate broadcas overlay.broadcastNotFound = overlay.Broadcast; overlay.broadcastStream data:overlay.broadcast trace:(vector int256) = overlay.Broadcast; -overlay.broadcastTwostepSimple flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data:bytes extra:bytes signature:bytes = overlay.Broadcast; -overlay.broadcastTwostepFec flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data_hash:int256 data_size:int seqno:int part:bytes extra:bytes signature:bytes = overlay.Broadcast; -overlay.broadcastTwostep.id flags:int date:int src:int256 src_adnl_id:int256 data_hash:int256 data_size:int part_size:int extra:bytes = overlay.broadcastTwostep.Id; +overlay.broadcastTwostepSimple flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data:bytes signature:bytes = overlay.Broadcast; +overlay.broadcastTwostepFec flags:int date:int src:PublicKey src_adnl_id:int256 certificate:overlay.Certificate data_hash:int256 data_size:int seqno:int part:bytes signature:bytes = overlay.Broadcast; +overlay.broadcastTwostep.id flags:int date:int src:int256 src_adnl_id:int256 data_hash:int256 part_size:int = overlay.broadcastTwostep.Id; overlay.broadcastTwostepSimple.toSign id:int256 data:bytes = overlay.broadcastTwostepSimple.ToSign; -overlay.broadcastTwostepFec.toSign id:int256 seqno:int part:bytes = overlay.broadcastTwostepFec.ToSign; +overlay.broadcastTwostepFec.toSign id:int256 data_size:int seqno:int part:bytes = overlay.broadcastTwostepFec.ToSign; ---functions--- @@ -1092,8 +1091,6 @@ consensus.candidateHashDataEmpty block:tonNode.blockIdExt parent:consensus.candi consensus.block slot:int parent:consensus.CandidateParent candidate:bytes signature:bytes = consensus.CandidateData; consensus.empty slot:int parent:consensus.CandidateId block:tonNode.blockIdExt signature:bytes = consensus.CandidateData; -consensus.broadcastExtra slot:int = consensus.BroadcastExtra; - // Simplex consensus votes consensus.requestError = consensus.RequestError; @@ -1133,10 +1130,6 @@ consensus.simplex.db.candidateResolver.candidateInfo leader_id:int candidate_has consensus.simplex.db.key.candidateResolver.notarCert candidateId:consensus.candidateId = consensus.simplex.db.key.candidateResolver.NotarCert; consensus.simplex.db.candidateResolver.notarCert notar:consensus.simplex.voteSignatureSet = consensus.simplex.db.candidateResolver.NotarCert; -// Candidate resolver: full candidate payload (serialized CandidateData bytes) -// Reference: C++ candidate-resolver.cpp store_candidate() / try_load_candidate_data_from_db() -consensus.simplex.db.key.candidate candidateId:consensus.candidateId = consensus.simplex.db.key.Candidate; - ---functions--- consensus.simplex.requestCandidate id:consensus.CandidateId want_candidate:Bool want_notar:Bool = consensus.simplex.CandidateAndCert; consensus.simplex.vote unsignedVote:consensus.simplex.UnsignedVote signature:bytes = consensus.simplex.Vote; diff --git a/src/vm/benches/benchmarks.rs b/src/vm/benches/benchmarks.rs index b374f9a..43fb0fc 100644 --- a/src/vm/benches/benchmarks.rs +++ b/src/vm/benches/benchmarks.rs @@ -241,7 +241,7 @@ fn bench_mergesort_tuple(c: &mut Criterion) { engine.execute().unwrap(); assert_eq!(engine.gas_used(), 51_216_096); assert_eq!(engine.stack().depth(), 1); - assert_eq!(engine.stack().get(0).unwrap(), &expected); + assert_eq!(engine.stack().get(0), &expected); }) }); } diff --git a/src/vm/src/executor/dictionary.rs b/src/vm/src/executor/dictionary.rs index bd045d2..0e093c9 100644 --- a/src/vm/src/executor/dictionary.rs +++ b/src/vm/src/executor/dictionary.rs @@ -25,7 +25,7 @@ use ton_block::{ fn try_unref_leaf(slice: SliceData) -> Result { match slice.remaining_bits() == 0 && slice.remaining_references() != 0 { - true => slice.reference(0).map(StackItem::Cell), + true => Ok(StackItem::Cell(slice.reference(0)?)), false => fail!(ExceptionCode::DictionaryError), } } @@ -85,27 +85,37 @@ fn dict( let nbits = engine.cmd.var(0).as_integer_value(0..=1023)?; let mut dict = HashmapE::with_hashmap(nbits, engine.cmd.var(1).as_dict()?.cloned()); let key = keyreader(engine.cmd.var(2), nbits)?; - if !key.is_empty_bitstring() { + if key.is_empty_bitstring() { + if how.any(SET | DEL) { + fail!(ExceptionCode::RangeCheckError, "key cannot be empty for set or delete") + } else { + if how.bit(RET) { + engine.cc.stack.push(boolean!(false)); + } + Ok(()) + } + } else { let val = handler(engine, &mut dict, key)?; if how.any(SET | DEL) { engine.cc.stack.push(StackItem::dict(dict.data())); } - if let Some(val) = val { - if how.bit(GET) { - engine.cc.stack.push(val); + match val { + None => { + if how.bit(RET) { + engine.cc.stack.push(boolean!(ret)); + } } - if how.bit(RET) { - engine.cc.stack.push(boolean!(!ret)); + Some(val) => { + if how.bit(GET) { + engine.cc.stack.push(val); + } + if how.bit(RET) { + engine.cc.stack.push(boolean!(!ret)); + } } - } else if how.bit(RET) { - engine.cc.stack.push(boolean!(ret)); - } - } else if how.any(SET | DEL) { - fail!(ExceptionCode::RangeCheckError, "key cannot be empty for set or delete") - } else if how.bit(RET) { - engine.cc.stack.push(boolean!(false)); + }; + Ok(()) } - Ok(()) } // (key slice nbits - ) @@ -119,17 +129,19 @@ fn dictcont(engine: &mut Engine, name: &'static str, keyreader: KeyReader, how: engine.cmd.vars.push(StackItem::continuation(ContinuationData::with_code(data))); let n = engine.cmd.var_count() - 1; if how.bit(SWITCH) { - switch(engine, var!(n))?; + switch(engine, var!(n)) } else if how.bit(CALLX) { - callx(engine, n, false)?; + callx(engine, n, false) } else { fail!("dictcont: {:X}", how) } } else if how.bit(STAY) { let var = engine.cmd.vars.remove(2); engine.cc.stack.push(var); + Ok(()) + } else { + Ok(()) } - Ok(()) } // (key slice nbits - (value' key' -1) | (0)) @@ -381,8 +393,8 @@ fn valwriter_add_ref( dict: &mut HashmapE, key: SliceData, ) -> Result> { - let value = engine.cmd.var(3).as_cell()?.clone(); - match convert_dict_error(dict.addref_with_gas(key, value, engine))? { + let new_val = engine.cmd.var(3).as_cell()?.clone(); + match convert_dict_error(dict.addref_with_gas(key, &new_val, engine))? { Some(val) => Ok(Some(try_unref_leaf(val)?)), None => Ok(None), } @@ -397,7 +409,7 @@ fn valwriter_add_ref_without_unref( match convert_dict_error(dict.get_with_gas(key.clone(), engine))? { Some(val) => Ok(Some(StackItem::Slice(val))), None => { - convert_dict_error(dict.setref_with_gas(key, new_val, engine))?; + convert_dict_error(dict.setref_with_gas(key, &new_val, engine))?; Ok(None) } } @@ -409,7 +421,7 @@ fn valwriter_add_or_remove_refopt( key: SliceData, ) -> Result> { let old_value = match engine.cmd.var(3).as_dict()? { - Some(new_val) => convert_dict_error(dict.setref_with_gas(key, new_val.clone(), engine))?, + Some(new_val) => convert_dict_error(dict.setref_with_gas(key, &new_val.clone(), engine))?, None => convert_dict_error(dict.remove_with_gas(key, engine))?, }; old_value.map(try_unref_leaf).or(Some(Ok(StackItem::None))).transpose() @@ -460,8 +472,8 @@ fn valwriter_replace_ref( dict: &mut HashmapE, key: SliceData, ) -> Result> { - let value = engine.cmd.var(3).as_cell()?.clone(); - match convert_dict_error(dict.replaceref_with_gas(key, value, engine))? { + let val = engine.cmd.var(3).as_cell()?.clone(); + match convert_dict_error(dict.replaceref_with_gas(key, &val, engine))? { Some(val) => Some(try_unref_leaf(val)).transpose(), None => Ok(None), } @@ -491,8 +503,8 @@ fn valwriter_to_ref( dict: &mut HashmapE, key: SliceData, ) -> Result> { - let value = engine.cmd.var(3).as_cell()?.clone(); - convert_dict_error(dict.setref_with_gas(key, value, engine))?.map(try_unref_leaf).transpose() + let val = engine.cmd.var(3).as_cell()?.clone(); + convert_dict_error(dict.setref_with_gas(key, &val, engine))?.map(try_unref_leaf).transpose() } const PREV: u8 = 0x00; diff --git a/src/vm/src/executor/engine/core.rs b/src/vm/src/executor/engine/core.rs index 239bb01..6b8cb8b 100644 --- a/src/vm/src/executor/engine/core.rs +++ b/src/vm/src/executor/engine/core.rs @@ -68,7 +68,7 @@ impl From<&SliceData> for SliceProto { } } -pub type TraceCallback = dyn Fn(&Engine, &EngineTraceInfo) + Send + Sync + 'static; +pub type TraceCallback = dyn Fn(&Engine, &EngineTraceInfo) + Send + Sync; #[derive(Debug)] pub struct RunChildVm { @@ -179,7 +179,7 @@ impl Engine { let trace = if cfg!(feature = "verbose") { Engine::TRACE_ALL } else if cfg!(feature = "fift_check") { - Engine::TRACE_ALL_BUT_CTRLS + Engine::TRACE_CODE | Engine::TRACE_GAS } else { Engine::TRACE_NONE }; @@ -447,27 +447,6 @@ impl Engine { } } - pub fn emulator_trace_callback(&self, info: &EngineTraceInfo) { - if info.has_cmd() { - if self.trace_bit(Engine::TRACE_CODE) { - log::info!(target: "executor", "code cell hash: {:X} offset: {}\n", - info.cmd_code.cell().unwrap().repr_hash(), info.cmd_code.pos()); - log::info!(target: "executor", "{}\n", info.cmd_str); - } - if self.trace_bit(Engine::TRACE_STACK) { - log::info!(target: "executor", " [ {} ] \n", self.get_stack_result_fift()); - } - if self.trace_bit(Engine::TRACE_GAS) { - log::info!(target: "executor", "gas - {}\n", info.gas_used); - } - // log::info!(target: "executor", "code cell hash: {:X} offset: {}\n", - // info.cmd_code.cell().unwrap().repr_hash(), info.cmd_code.pos()); - // log::info!(target: "executor", "{}\n", info.cmd_str); - // log::info!(target: "executor", " [ {} ] \n", self.get_stack_result_fift()); - // log::info!(target: "executor", "gas - {}\n", info.gas_used); - } - } - #[allow(dead_code)] fn dump_stack_result(stack: &Stack) -> String { static PREV_STACK: LazyLock> = LazyLock::new(|| Mutex::new(Stack::new())); diff --git a/src/vm/src/tests/test_executor.rs b/src/vm/src/tests/test_executor.rs index 9c6956e..60979a5 100644 --- a/src/vm/src/tests/test_executor.rs +++ b/src/vm/src/tests/test_executor.rs @@ -9,7 +9,6 @@ * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ use crate::{ - error::tvm_exception_code, executor::{ engine::Engine, math::DivMode, @@ -25,10 +24,7 @@ use crate::{ }, }; use std::collections::HashSet; -use ton_block::{ - BuilderData, Cell, CurrencyCollection, Deserializable, ExceptionCode, IBitstring, SliceData, - Status, -}; +use ton_block::{BuilderData, Cell, IBitstring, SliceData, Status}; #[test] fn test_assert_stack() { @@ -261,19 +257,3 @@ fn test_currency_collection_ser() { let b2 = BuilderData::with_raw(vec![0x3b, 0xc6, 0x14, 0xe0], 29).unwrap(); assert_eq!(b1, b2); } - -#[test] -fn test_tvm_serialize_currency_collection() { - let coins = 1u64 << 63; - let coins1 = int!(coins).as_coins().unwrap(); - let builder = serialize_currency_collection(coins1, None).unwrap(); - let mut slice = SliceData::load_builder(builder).unwrap(); - let coins1 = CurrencyCollection::construct_from(&mut slice).unwrap(); - let coins2 = CurrencyCollection::with_coins(coins); - assert_eq!(coins1, coins2); - - assert_eq!( - tvm_exception_code(&int!(1u128 << 120).as_coins().expect_err("Expect range check error")), - Some(ExceptionCode::RangeCheckError) - ); -} diff --git a/src/vm/tests/test_config.rs b/src/vm/tests/test_config.rs index 711f86f..78771dd 100644 --- a/src/vm/tests/test_config.rs +++ b/src/vm/tests/test_config.rs @@ -218,7 +218,7 @@ mod getparam { let library_cell_code = code.as_library_cell(); let mut library = ton_block::HashmapE::with_bit_len(256); let key = code.repr_hash().write_to_bitstring().unwrap(); - library.setref(key, code.clone()).unwrap(); + library.setref(key, &code).unwrap(); // simple case test_case_with_bytecode(code) .with_account(SHARD_ACCOUNT.clone()) @@ -246,13 +246,13 @@ mod root { params .setref( SliceData::from_raw(2000i32.to_be_bytes().to_vec(), 32), - SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), + &SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), ) .unwrap(); params .setref( SliceData::from_raw((-1i32).to_be_bytes().to_vec(), 32), - SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), + &SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), ) .unwrap(); test_case_with_c7("CONFIGROOT").expect_item(StackItem::dict(params.data())); @@ -300,13 +300,13 @@ mod dict { params .setref( SliceData::from_raw(2000i32.to_be_bytes().to_vec(), 32), - SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), + &SliceData::new(vec![0x67, 0x89, 0x08]).into_cell().unwrap(), ) .unwrap(); params .setref( SliceData::from_raw((-1i32).to_be_bytes().to_vec(), 32), - SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), + &SliceData::new(vec![0x12, 0x34, 0x58]).into_cell().unwrap(), ) .unwrap(); test_case_with_c7("CONFIGDICT").expect_stack( diff --git a/src/vm/tests/test_library.rs b/src/vm/tests/test_library.rs index 3f0c2b7..8b50536 100644 --- a/src/vm/tests/test_library.rs +++ b/src/vm/tests/test_library.rs @@ -37,7 +37,7 @@ fn test_use_library_normal_load_cell_from_ref() { code_use_lib.set_type(CellType::LibraryReference); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case_with_ref( " @@ -65,7 +65,7 @@ fn test_use_library_normal_compose_cell() { ); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case( " @@ -96,7 +96,7 @@ fn test_use_library_normal_jmpref() { code_use_lib.set_type(CellType::LibraryReference); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case_with_ref( " @@ -118,7 +118,7 @@ fn test_use_library_with_wrong_cell_hash() { code_use_lib.set_type(CellType::LibraryReference); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case_with_ref( " @@ -151,8 +151,8 @@ fn test_use_library_with_cell_type_error() { code_use_lib.set_type(CellType::MerkleProof); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash1.into(), lib_code1.clone()).unwrap(); - lib.setref(hash2.into(), lib_code2.clone()).unwrap(); + lib.setref(hash1.into(), &lib_code1).unwrap(); + lib.setref(hash2.into(), &lib_code2).unwrap(); test_case_with_ref( " @@ -237,7 +237,7 @@ fn test_incorrect_library() { assert_ne!(hash, lib_code.repr_hash()); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case_with_ref( " @@ -698,7 +698,7 @@ fn test_compose_exotic_cell_and_load_as_cell() { let hash = lib_code.repr_hash(); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case_with_ref( " @@ -727,7 +727,7 @@ fn test_code_as_exotic_cell() { let lib_code = BuilderData::with_raw(vec![0x72], 8).unwrap().into_cell().unwrap(); let hash = lib_code.repr_hash(); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); let code = lib_code.as_library_cell(); // normal case with code as library cell @@ -739,7 +739,7 @@ fn test_code_as_exotic_cell() { let lib_code = code; let hash = lib_code.repr_hash(); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); let code = lib_code.as_library_cell(); // code as library cell with recursive library cell @@ -766,7 +766,7 @@ fn test_code_as_exotic_cell() { let hash = lib_code.repr_hash(); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); let code = lib_code.as_library_cell(); @@ -780,7 +780,7 @@ fn test_compose_exotic_cell_and_load_quite_as_cell() { let hash = lib_code.repr_hash(); let mut lib = HashmapE::with_bit_len(256); - lib.setref(hash.into(), lib_code.clone()).unwrap(); + lib.setref(hash.into(), &lib_code).unwrap(); test_case_with_ref( "